messageList: Drop MessageSection in favor of MessageView

We need more control over the MessageView for grouping messages by app.
This commit is contained in:
Julian Sparber 2024-05-09 18:16:16 +02:00
parent 0022b8fd51
commit bd6e7f6c43
3 changed files with 12 additions and 359 deletions

View file

@ -39,19 +39,6 @@
}
}
.message-list-sections {
// to account for scrollbar
&:ltr {margin-right: $base_margin * 3; }
&:rtl {margin-left: $base_margin * 3;}
spacing: $base_padding * 2;
}
.message-list-section,
.message-list-section-list {
spacing: $base_padding * 2;
}
// do-not-disturb + clear button
.message-list-controls {
// NOTE: remove the padding if notification_bubble could remove margin for drop shadow

View file

@ -9,7 +9,6 @@ import St from 'gi://St';
import * as MessageList from './messageList.js';
import * as PopupMenu from './popupMenu.js';
import {ensureActorVisibleInScrollView} from '../misc/animationUtils.js';
import {formatDateWithCFormatString} from '../misc/dateUtils.js';
import {loadInterfaceXML} from '../misc/fileUtils.js';
@ -797,8 +796,8 @@ class DoNotDisturbSwitch extends PopupMenu.Switch {
export const CalendarMessageList = GObject.registerClass(
class CalendarMessageList extends St.Widget {
_init() {
super._init({
constructor() {
super({
style_class: 'message-list',
layout_manager: new Clutter.BinLayout(),
x_expand: true,
@ -815,10 +814,13 @@ class CalendarMessageList extends St.Widget {
});
this.add_child(box);
this._messageView = new MessageList.MessageView();
this._scrollView = new St.ScrollView({
style_class: 'vfade',
overlay_scrollbars: true,
x_expand: true, y_expand: true,
child: this._messageView,
});
box.add_child(this._scrollView);
@ -854,52 +856,18 @@ class CalendarMessageList extends St.Widget {
accessible_name: C_('action', 'Clear all notifications'),
});
this._clearButton.connect('clicked', () => {
this._sectionList.get_children().forEach(s => s.clear());
this._messageView.clear();
});
hbox.add_child(this._clearButton);
this._placeholder.bind_property('visible',
this._clearButton, 'visible',
GObject.BindingFlags.INVERT_BOOLEAN);
this._sectionList = new St.BoxLayout({
style_class: 'message-list-sections',
vertical: true,
x_expand: true,
y_expand: true,
});
this._sectionList.connectObject(
'child-added', this._sync.bind(this),
'child-removed', this._sync.bind(this),
this);
this._scrollView.child = this._sectionList;
this._mediaSection = new MessageList.MediaSection();
this._addSection(this._mediaSection);
this._notificationSection = new MessageList.NotificationSection();
this._addSection(this._notificationSection);
}
_addSection(section) {
section.connectObject(
'notify::visible', this._sync.bind(this),
'notify::empty', this._sync.bind(this),
'notify::can-clear', this._sync.bind(this),
'destroy', () => this._sectionList.remove_child(section),
'message-focused', (_s, messageActor) => {
ensureActorVisibleInScrollView(this._scrollView, messageActor);
}, this);
this._sectionList.add_child(section);
}
_sync() {
let sections = this._sectionList.get_children();
let empty = sections.every(s => s.empty || !s.visible);
this._placeholder.visible = empty;
let canClear = sections.some(s => s.canClear && s.visible);
this._clearButton.reactive = canClear;
this._messageView.bind_property('empty',
this._placeholder, 'visible',
GObject.BindingFlags.SYNC_CREATE);
this._messageView.bind_property('can-clear',
this._clearButton, 'reactive',
GObject.BindingFlags.SYNC_CREATE);
}
});

View file

@ -803,308 +803,6 @@ class MediaMessage extends Message {
}
});
export const MessageListSection = GObject.registerClass({
Properties: {
'can-clear': GObject.ParamSpec.boolean(
'can-clear', 'can-clear', 'can-clear',
GObject.ParamFlags.READABLE,
false),
'empty': GObject.ParamSpec.boolean(
'empty', 'empty', 'empty',
GObject.ParamFlags.READABLE,
true),
},
Signals: {
'can-clear-changed': {},
'empty-changed': {},
'message-focused': {param_types: [Message.$gtype]},
},
}, class MessageListSection extends St.BoxLayout {
_init() {
super._init({
style_class: 'message-list-section',
clip_to_allocation: true,
vertical: true,
x_expand: true,
});
this._list = new St.BoxLayout({
style_class: 'message-list-section-list',
vertical: true,
});
this.add_child(this._list);
this._list.connect('child-added', this._sync.bind(this));
this._list.connect('child-removed', this._sync.bind(this));
Main.sessionMode.connectObject(
'updated', () => this._sync(), this);
this._empty = true;
this._canClear = false;
this._sync();
}
get empty() {
return this._empty;
}
get canClear() {
return this._canClear;
}
get _messages() {
return this._list.get_children().map(i => i.child);
}
_onKeyFocusIn(messageActor) {
this.emit('message-focused', messageActor);
}
addMessage(message, animate) {
this.addMessageAtIndex(message, -1, animate);
}
addMessageAtIndex(message, index, animate) {
if (this._messages.includes(message))
throw new Error('Message was already added previously');
let listItem = new St.Bin({
child: message,
layout_manager: new ScaleLayout(),
pivot_point: new Graphene.Point({x: .5, y: .5}),
});
listItem._connectionsIds = [];
listItem._connectionsIds.push(message.connect('key-focus-in',
this._onKeyFocusIn.bind(this)));
listItem._connectionsIds.push(message.connect('close', () => {
this.removeMessage(message, true);
}));
listItem._connectionsIds.push(message.connect('destroy', () => {
listItem._connectionsIds.forEach(id => message.disconnect(id));
listItem.destroy();
}));
this._list.insert_child_at_index(listItem, index);
const duration = animate ? MESSAGE_ANIMATION_TIME : 0;
listItem.set({scale_x: 0, scale_y: 0});
listItem.ease({
scale_x: 1,
scale_y: 1,
duration,
mode: Clutter.AnimationMode.EASE_OUT_QUAD,
});
}
moveMessage(message, index, animate) {
if (!this._messages.includes(message))
throw new Error('Impossible to move untracked message');
let listItem = message.get_parent();
if (!animate) {
this._list.set_child_at_index(listItem, index);
return;
}
let onComplete = () => {
this._list.set_child_at_index(listItem, index);
listItem.ease({
scale_x: 1,
scale_y: 1,
duration: MESSAGE_ANIMATION_TIME,
mode: Clutter.AnimationMode.EASE_OUT_QUAD,
});
};
listItem.ease({
scale_x: 0,
scale_y: 0,
duration: MESSAGE_ANIMATION_TIME,
mode: Clutter.AnimationMode.EASE_OUT_QUAD,
onComplete,
});
}
removeMessage(message, animate) {
const messages = this._messages;
if (!messages.includes(message))
throw new Error('Impossible to remove untracked message');
let listItem = message.get_parent();
listItem._connectionsIds.forEach(id => message.disconnect(id));
let nextMessage = null;
if (message.has_key_focus()) {
const index = messages.indexOf(message);
nextMessage =
messages[index + 1] ||
messages[index - 1] ||
this._list;
}
const duration = animate ? MESSAGE_ANIMATION_TIME : 0;
listItem.ease({
scale_x: 0,
scale_y: 0,
duration,
mode: Clutter.AnimationMode.EASE_OUT_QUAD,
onComplete: () => {
listItem.destroy();
nextMessage?.grab_key_focus();
},
});
}
clear() {
let messages = this._messages.filter(msg => msg.canClose());
// If there are few messages, letting them all zoom out looks OK
if (messages.length < 2) {
messages.forEach(message => {
message.close();
});
} else {
// Otherwise we slide them out one by one, and then zoom them
// out "off-screen" in the end to smoothly shrink the parent
let delay = MESSAGE_ANIMATION_TIME / Math.max(messages.length, 5);
for (let i = 0; i < messages.length; i++) {
let message = messages[i];
message.get_parent().ease({
translation_x: this._list.width,
opacity: 0,
duration: MESSAGE_ANIMATION_TIME,
delay: i * delay,
mode: Clutter.AnimationMode.EASE_OUT_QUAD,
onComplete: () => message.close(),
});
}
}
}
_shouldShow() {
return !this.empty;
}
_sync() {
let messages = this._messages;
let empty = messages.length === 0;
if (this._empty !== empty) {
this._empty = empty;
this.notify('empty');
}
let canClear = messages.some(m => m.canClose());
if (this._canClear !== canClear) {
this._canClear = canClear;
this.notify('can-clear');
}
}
});
export const NotificationSection = GObject.registerClass(
class NotificationSection extends MessageListSection {
_init() {
super._init();
this._nUrgent = 0;
Main.messageTray.connect('source-added', this._sourceAdded.bind(this));
Main.messageTray.getSources().forEach(source => {
this._sourceAdded(Main.messageTray, source);
});
}
_sourceAdded(tray, source) {
source.connectObject(
'notification-added', this._onNotificationAdded.bind(this),
'notification-removed', this._onNotificationRemoved.bind(this),
this);
}
_onNotificationAdded(source, notification) {
let message = new NotificationMessage(notification);
const isUrgent = notification.urgency === MessageTray.Urgency.CRITICAL;
notification.connectObject(
'notify::datetime', () => {
// The datetime property changes whenever the notification is updated
this.moveMessage(message, isUrgent ? 0 : this._nUrgent, this.mapped);
}, this);
if (isUrgent) {
// Keep track of urgent notifications to keep them on top
this._nUrgent++;
} else if (this.mapped) {
// Only acknowledge non-urgent notifications in case it
// has important actions that are inaccessible when not
// shown as banner
notification.acknowledged = true;
}
const index = isUrgent ? 0 : this._nUrgent;
this.addMessageAtIndex(message, index, this.mapped);
}
_onNotificationRemoved(source_, notification) {
const message = this._messages.find(m => m.notification === notification);
if (notification.urgency === MessageTray.Urgency.CRITICAL)
this._nUrgent--;
this.removeMessage(message, this.mapped);
}
vfunc_map() {
this._messages.forEach(message => {
if (message.notification.urgency !== MessageTray.Urgency.CRITICAL)
message.notification.acknowledged = true;
});
super.vfunc_map();
}
});
export const MediaSection = GObject.registerClass(
class MediaSection extends MessageListSection {
constructor() {
super();
this._players = new Map();
this._mediaSource = new Mpris.MediaSource();
this._mediaSource.connectObject(
'player-added', (_, player) => this._addPlayer(player),
'player-removed', (_, player) => this._removePlayer(player),
this);
this._mediaSource.players.forEach(player => {
this._addPlayer(player);
});
}
_addPlayer(player) {
if (this._players.has(player))
throw new Error('Player was already added previously');
const message = new MediaMessage(player);
this._players.set(player, message);
this.addMessage(message, true);
}
_removePlayer(player) {
const message = this._players.get(player);
if (message)
this.removeMessage(message, true);
this._players.delete(player);
}
});
export const MessageView = GObject.registerClass({
Implements: [St.Scrollable],
Properties: {