Merge branch 'add_notification_groups' into 'main'

Draft: notifications: Implement per app grouping

See merge request GNOME/gnome-shell!3012
This commit is contained in:
Julian Sparber 2024-06-29 20:00:35 +00:00
commit f5f594063d
9 changed files with 1426 additions and 449 deletions

View file

@ -5,6 +5,7 @@
<file preprocess="xml-stripblanks">scalable/actions/carousel-arrow-next-symbolic.svg</file>
<file preprocess="xml-stripblanks">scalable/actions/carousel-arrow-previous-symbolic.svg</file>
<file preprocess="xml-stripblanks">scalable/actions/dark-mode-symbolic.svg</file>
<file preprocess="xml-stripblanks">scalable/actions/group-collapse-symbolic.svg</file>
<file preprocess="xml-stripblanks">scalable/actions/notification-expand-symbolic.svg</file>
<file preprocess="xml-stripblanks">scalable/actions/ornament-check-symbolic.svg</file>
<file preprocess="xml-stripblanks">scalable/actions/ornament-dot-checked-symbolic.svg</file>

View file

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
height="16px"
viewBox="0 0 16 16"
width="16px"
version="1.1"
id="svg1"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<g
id="g456"
transform="matrix(1,0,0,-1,392,1185.9336)"
style="fill:#2e3436;fill-opacity:1">
<g
id="g143"
transform="translate(0,-0.50051875)"
style="fill:#2e3436;fill-opacity:1">
<g
id="g144"
transform="translate(0,0.5)"
style="fill:#2e3436;fill-opacity:1">
<path
d="m -388,1172.4341 c 0,-0.2656 0.1055,-0.5195 0.293,-0.707 0.3906,-0.3906 1.0234,-0.3906 1.414,0 l 2.293,2.293 2.293,-2.293 c 0.3906,-0.3906 1.0234,-0.3906 1.414,0 0.1875,0.1875 0.293,0.4414 0.293,0.707 0,0.2656 -0.1055,0.5195 -0.293,0.707 l -3,3 c -0.3906,0.3907 -1.0234,0.3907 -1.414,0 l -3,-3 c -0.1875,-0.1875 -0.293,-0.4414 -0.293,-0.707 z"
fill="#2e3436"
id="path177459-1"
style="fill:#2e3436;fill-opacity:1" />
<path
d="m -388,1183.4342 c 0,0.2656 0.1055,0.5195 0.293,0.707 0.3906,0.3906 1.0234,0.3906 1.414,0 l 2.293,-2.293 2.293,2.293 c 0.3906,0.3906 1.0234,0.3906 1.414,0 0.1875,-0.1875 0.293,-0.4414 0.293,-0.707 0,-0.2656 -0.1055,-0.5195 -0.293,-0.707 l -3,-3 c -0.3906,-0.3907 -1.0234,-0.3907 -1.414,0 l -3,3 c -0.1875,0.1875 -0.293,0.4414 -0.293,0.707 z"
fill="#2e3436"
id="path142"
style="fill:#2e3436;fill-opacity:1" />
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -25,17 +25,18 @@
}
}
.message-list-sections {
.message-view {
// to account for scrollbar
&:ltr {margin-right: $base_margin * 3; }
&:rtl {margin-left: $base_margin * 3;}
spacing: $base_padding * 2;
}
.message {
margin-bottom: $base_padding * 2 !important;
}
.message-list-section,
.message-list-section-list {
spacing: $base_padding * 2;
> :last-child > .message {
margin-bottom: 0 !important;
}
}
// do-not-disturb + clear button
@ -61,6 +62,30 @@
}
}
// message notification group
.message-notification-group {
spacing: $base_padding * 2;
.message-group-header {
padding: $base_padding;
.message-group-title {
@extend %title_2;
margin: 0 $base_margin;
}
}
// close button
.message-collapse-button {
@extend .icon-button;
color: $fg_color;
background-color: transparentize($fg_color, 0.8);
padding: 4px !important;
border: 4px transparent solid;
&:hover {background-color: transparentize($fg_color, 0.7);}
&:active {background-color: transparentize($fg_color, 0.8);}
}
}
// message bubbles
.message {
@extend %card;

View file

@ -7,18 +7,13 @@ import GObject from 'gi://GObject';
import Shell from 'gi://Shell';
import St from 'gi://St';
import * as Main from './main.js';
import * as MessageList from './messageList.js';
import * as MessageTray from './messageTray.js';
import * as Mpris from './mpris.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';
const SHOW_WEEKDATE_KEY = 'show-weekdate';
const MAX_NOTIFICATION_BUTTONS = 3;
const NC_ = (context, str) => `${context}\u0004${str}`;
@ -765,146 +760,122 @@ export const Calendar = GObject.registerClass({
}
});
export const NotificationMessage = GObject.registerClass(
class NotificationMessage extends MessageList.Message {
constructor(notification) {
super(notification.source);
const FadeEffect = GObject.registerClass({
Properties: {
'top-fade': GObject.ParamSpec.float(
'top-fade', 'top-fade', 'top-fade',
GObject.ParamFlags.READWRITE,
0, GLib.MAXINT32, 0),
'bottom-fade': GObject.ParamSpec.float(
'bottom-fade', 'bottom-fade', 'bottom-fade',
GObject.ParamFlags.READWRITE,
0, GLib.MAXINT32, 0),
'opacity': GObject.ParamSpec.float(
'opacity', 'opacity', 'opacity',
GObject.ParamFlags.READWRITE,
0, 1, 0),
},
}, class FadeEffect extends Shell.GLSLEffect {
_init(params) {
super._init(params);
this.notification = notification;
this._heightLocation = this.get_uniform_location('height');
this._widthLocation = this.get_uniform_location('width');
this._topFadeHeightLocation = this.get_uniform_location('top_fade_height');
this._bottomFadeHeightLocation = this.get_uniform_location('bottom_fade_height');
this._opacityLocation = this.get_uniform_location('opacity');
this.connect('close', () => {
this._closed = true;
if (this.notification)
this.notification.destroy(MessageTray.NotificationDestroyedReason.DISMISSED);
});
notification.connectObject(
'action-added', (_, action) => this._addAction(action),
'action-removed', (_, action) => this._removeAction(action),
'destroy', () => {
this.notification = null;
if (!this._closed)
this.close();
}, this);
notification.bind_property('title',
this, 'title',
GObject.BindingFlags.SYNC_CREATE);
notification.bind_property('body',
this, 'body',
GObject.BindingFlags.SYNC_CREATE);
notification.bind_property('use-body-markup',
this, 'use-body-markup',
GObject.BindingFlags.SYNC_CREATE);
notification.bind_property('datetime',
this, 'datetime',
GObject.BindingFlags.SYNC_CREATE);
notification.bind_property('gicon',
this, 'icon',
GObject.BindingFlags.SYNC_CREATE);
this._actions = new Map();
this.notification.actions.forEach(action => {
this._addAction(action);
});
this._topFade = 0;
this._bottomFade = 0;
this._opacity = 0;
}
vfunc_clicked() {
this.notification.activate();
get opacity() {
return this._opacity;
}
canClose() {
return true;
}
_addAction(action) {
if (!this._buttonBox) {
this._buttonBox = new St.BoxLayout({
x_expand: true,
style_class: 'notification-buttons-bin',
});
this.setActionArea(this._buttonBox);
global.focus_manager.add_group(this._buttonBox);
}
if (this._buttonBox.get_n_children() >= MAX_NOTIFICATION_BUTTONS)
set opacity(opacity) {
if (this._opacity === opacity)
return;
const button = new St.Button({
style_class: 'notification-button',
x_expand: true,
label: action.label,
});
this._opacity = opacity;
this.set_uniform_float(this._opacityLocation, 1, [opacity]);
this.actor.queue_redraw();
button.connect('clicked', () => action.activate());
this._actions.set(action, button);
this._buttonBox.add_child(button);
this.notify('opacity');
}
_removeAction(action) {
this._actions.get(action)?.destroy();
this._actions.delete(action);
}
});
const NotificationSection = GObject.registerClass(
class NotificationSection extends MessageList.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);
});
get topFade() {
return this._topFade;
}
get allowed() {
return Main.sessionMode.hasNotifications &&
!Main.sessionMode.isGreeter;
set topFade(height) {
if (this._topFade === height)
return;
this._topFade = height;
this.set_uniform_float(this._topFadeHeightLocation, 1, [height]);
this.actor.queue_redraw();
this.notify('top-fade');
}
_sourceAdded(tray, source) {
source.connectObject('notification-added',
this._onNotificationAdded.bind(this), this);
get bottomFade() {
return this._bottomFade;
}
_onNotificationAdded(source, notification) {
let message = new NotificationMessage(notification);
set bottomFade(height) {
if (this._bottomFade === height)
return;
let isUrgent = notification.urgency === MessageTray.Urgency.CRITICAL;
this._bottomFade = height;
this.set_uniform_float(this._bottomFadeHeightLocation, 1, [height]);
this.actor.queue_redraw();
notification.connectObject(
'destroy', () => {
if (isUrgent)
this._nUrgent--;
},
'notify::datetime', () => {
// The datetime property changes whenever the notification is updated
this.moveMessage(message, isUrgent ? 0 : this._nUrgent, this.mapped);
this.notify('bottom-fade');
}
vfunc_set_actor(actor) {
if (this.actor)
this.actor.disconnectObject(this);
if (actor) {
actor.connectObject('notify::allocation', () => {
this.set_uniform_float(this._heightLocation, 1, [this.get_actor().height]);
this.set_uniform_float(this._widthLocation, 1, [this.get_actor().width]);
}, 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;
}
let index = isUrgent ? 0 : this._nUrgent;
this.addMessageAtIndex(message, index, this.mapped);
super.vfunc_set_actor(actor);
}
vfunc_map() {
this._messages.forEach(message => {
if (message.notification.urgency !== MessageTray.Urgency.CRITICAL)
message.notification.acknowledged = true;
});
super.vfunc_map();
vfunc_build_pipeline() {
const dec = `uniform sampler2D tex; \n
uniform float height; \n
uniform float width; \n
uniform float opacity; \n
uniform float top_fade_height; \n
uniform float bottom_fade_height; \n`;
const src = `cogl_color_out = cogl_color_in * texture2D (tex, vec2 (cogl_tex_coord_in[0].xy)); \n
float base_fade = 200; \n
float y = height * cogl_tex_coord_in[0].y; \n
float y_from_bottom = height - y; \n
float ratio = 1.0; \n
if (y < top_fade_height) { \n
ratio = y / (top_fade_height + base_fade); \n
ratio = 1 - ((1 - ratio) * opacity); \n
} \n
if (y_from_bottom < bottom_fade_height) { \n
ratio = y_from_bottom / (bottom_fade_height + base_fade); \n
ratio = 1 - ((1 - ratio) * opacity); \n
} \n
cogl_color_out *= ratio; \n`;
this.add_glsl_snippet(Shell.SnippetHook.FRAGMENT, dec, src, true);
}
});
@ -944,8 +915,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,
@ -962,10 +933,14 @@ class CalendarMessageList extends St.Widget {
});
this.add_child(box);
this._messageView = new MessageList.MessageView();
this._scrollView = new St.ScrollView({
style_class: 'vfade',
effect: new FadeEffect({name: 'highlight'}),
overlay_scrollbars: true,
x_expand: true, y_expand: true,
child: this._messageView,
});
box.add_child(this._scrollView);
@ -1001,58 +976,36 @@ 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 Mpris.MediaSection();
this._addSection(this._mediaSection);
this._notificationSection = new NotificationSection();
this._addSection(this._notificationSection);
Main.sessionMode.connect('updated', this._sync.bind(this));
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);
}
_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);
}
maybeCollapseForEvent(event) {
if (!this._messageView.expandedGroup)
return Clutter.EVENT_PROPAGATE;
_sync() {
let sections = this._sectionList.get_children();
let visible = sections.some(s => s.allowed);
this.visible = visible;
if (!visible)
return;
const targetActor = global.stage.get_event_actor(event);
if ((event.type() === Clutter.EventType.BUTTON_PRESS ||
event.type() === Clutter.EventType.TOUCH_BEGIN) &&
!this._messageView.expandedGroup.contains(targetActor)) {
this._messageView.collapse();
} else if (event.type() === Clutter.EventType.KEY_PRESS &&
event.get_key_symbol() === Clutter.KEY_Escape) {
this._messageView.collapse();
return Clutter.EVENT_STOP;
}
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;
return Clutter.EVENT_PROPAGATE;
}
});

View file

@ -800,7 +800,7 @@ class MessagesIndicator extends St.Icon {
}
});
const FreezableBinLayout = GObject.registerClass(
export const FreezableBinLayout = GObject.registerClass(
class FreezableBinLayout extends Clutter.BinLayout {
_init() {
super._init();
@ -922,6 +922,11 @@ class DateMenuButton extends PanelMenu.Button {
this._messageList = new Calendar.CalendarMessageList();
hbox.add_child(this._messageList);
// Collapse notification groups when the user clicks outside of the expanded group
this.menu.actor.connect('captured-event', (_, event) => {
return this._messageList.maybeCollapseForEvent(event);
});
// Fill up the second column
const boxLayout = new CalendarColumnLayout([this._calendar, this._date]);
const vbox = new St.Widget({
@ -1002,5 +1007,7 @@ class DateMenuButton extends PanelMenu.Button {
// but the corresponding app (clocks, weather); however we can consider
// that display-specific settings, so re-use "allowSettings" here ...
this._displaysSection.visible = Main.sessionMode.allowSettings;
this._messageList.visible = Main.sessionMode.hasNotifications && !Main.sessionMode.isGreeter;
}
});

File diff suppressed because it is too large Load diff

View file

@ -8,7 +8,6 @@ import Meta from 'gi://Meta';
import Shell from 'gi://Shell';
import St from 'gi://St';
import * as Calendar from './calendar.js';
import * as GnomeSession from '../misc/gnomeSession.js';
import * as Layout from './layout.js';
import * as Main from './main.js';
@ -17,15 +16,13 @@ import * as SignalTracker from '../misc/signalTracker.js';
const SHELL_KEYBINDINGS_SCHEMA = 'org.gnome.shell.keybindings';
export const ANIMATION_TIME = 200;
const NOTIFICATION_TIMEOUT = 4000;
const HIDE_TIMEOUT = 200;
const LONGER_HIDE_TIMEOUT = 600;
const MAX_NOTIFICATIONS_IN_QUEUE = 3;
const MAX_NOTIFICATIONS_PER_SOURCE = 3;
const MAX_NOTIFICATIONS_PER_SOURCE = 10;
// We delay hiding of the tray if the mouse is within MOUSE_LEFT_ACTOR_THRESHOLD
// range from the point where it left the tray.
@ -597,13 +594,6 @@ export const Source = GObject.registerClass({
// To be overridden by subclasses
open() {
}
destroyNonResidentNotifications() {
for (let i = this.notifications.length - 1; i >= 0; i--) {
if (!this.notifications[i].resident)
this.notifications[i].destroy();
}
}
});
SignalTracker.registerDestroyableType(Source);
@ -1111,7 +1101,7 @@ export const MessageTray = GObject.registerClass({
this.idleMonitor.add_user_active_watch(this._onIdleMonitorBecameActive.bind(this));
}
this._banner = new Calendar.NotificationMessage(this._notification);
this._banner = new MessageList.NotificationMessage(this._notification);
this._banner.can_focus = false;
this._banner._header.expandButton.visible = false;
this._banner.add_style_class_name('notification-banner');
@ -1167,12 +1157,12 @@ export const MessageTray = GObject.registerClass({
this._bannerBin.remove_all_transitions();
this._bannerBin.ease({
opacity: 255,
duration: ANIMATION_TIME,
duration: MessageList.MESSAGE_ANIMATION_TIME,
mode: Clutter.AnimationMode.LINEAR,
});
this._bannerBin.ease({
y: 0,
duration: ANIMATION_TIME,
duration: MessageList.MESSAGE_ANIMATION_TIME,
mode: Clutter.AnimationMode.EASE_OUT_BACK,
onComplete: () => {
this._notificationState = State.SHOWN;
@ -1232,7 +1222,7 @@ export const MessageTray = GObject.registerClass({
this._resetNotificationLeftTimeout();
this._bannerBin.remove_all_transitions();
const duration = animate ? ANIMATION_TIME : 0;
const duration = animate ? MessageList.MESSAGE_ANIMATION_TIME : 0;
this._notificationState = State.HIDING;
this._bannerBin.ease({
opacity: 0,

View file

@ -1,9 +1,7 @@
import Gio from 'gi://Gio';
import GObject from 'gi://GObject';
import Shell from 'gi://Shell';
import * as Signals from '../misc/signals.js';
import * as Main from './main.js';
import * as MessageList from './messageList.js';
import {loadInterfaceXML} from '../misc/fileUtils.js';
@ -19,71 +17,17 @@ const MprisPlayerProxy = Gio.DBusProxy.makeProxyWrapper(MprisPlayerIface);
const MPRIS_PLAYER_PREFIX = 'org.mpris.MediaPlayer2.';
export const MediaMessage = GObject.registerClass(
class MediaMessage extends MessageList.Message {
constructor(player) {
super(player.source);
this._player = player;
this.add_style_class_name('media-message');
this._prevButton = this.addMediaControl('media-skip-backward-symbolic',
() => {
this._player.previous();
});
this._playPauseButton = this.addMediaControl('',
() => {
this._player.playPause();
});
this._nextButton = this.addMediaControl('media-skip-forward-symbolic',
() => {
this._player.next();
});
this._player.connectObject(
'changed', this._update.bind(this),
'closed', this.close.bind(this), this);
this._update();
}
vfunc_clicked() {
this._player.raise();
Main.panel.closeCalendar();
}
_updateNavButton(button, sensitive) {
button.reactive = sensitive;
}
_update() {
let icon;
if (this._player.trackCoverUrl) {
const file = Gio.File.new_for_uri(this._player.trackCoverUrl);
icon = new Gio.FileIcon({file});
} else {
icon = new Gio.ThemedIcon({name: 'audio-x-generic-symbolic'});
}
this.set({
title: this._player.trackTitle,
body: this._player.trackArtists.join(', '),
icon,
});
let isPlaying = this._player.status === 'Playing';
let iconName = isPlaying
? 'media-playback-pause-symbolic'
: 'media-playback-start-symbolic';
this._playPauseButton.child.icon_name = iconName;
this._updateNavButton(this._prevButton, this._player.canGoPrevious);
this._updateNavButton(this._nextButton, this._player.canGoNext);
}
});
export class MprisPlayer extends Signals.EventEmitter {
export const MprisPlayer = GObject.registerClass({
Properties: {
'can-play': GObject.ParamSpec.boolean(
'can-play', 'can-play', 'can-play',
GObject.ParamFlags.READABLE,
false),
},
Signals: {
'changed': {},
},
}, class MprisPlayer extends GObject.Object {
constructor(busName) {
super();
@ -94,7 +38,7 @@ export class MprisPlayer extends Signals.EventEmitter {
'/org/mpris/MediaPlayer2',
this._onPlayerProxyReady.bind(this));
this._visible = false;
this._canPlay = false;
this._trackArtists = [];
this._trackTitle = '';
this._trackCoverUrl = '';
@ -102,6 +46,10 @@ export class MprisPlayer extends Signals.EventEmitter {
this.source = new MessageList.Source();
}
get canPlay() {
return this._canPlay;
}
get status() {
return this._playerProxy.PlaybackStatus;
}
@ -157,8 +105,6 @@ export class MprisPlayer extends Signals.EventEmitter {
this._playerProxy.disconnectObject(this);
this._playerProxy = null;
this.emit('closed');
}
_onMprisProxyReady() {
@ -175,8 +121,7 @@ export class MprisPlayer extends Signals.EventEmitter {
}
_onPlayerProxyReady() {
this._playerProxy.connectObject(
'g-properties-changed', () => this._updateState(), this);
this._playerProxy.connectObject('g-properties-changed', this._updateState.bind(this), this);
this._updateState();
}
@ -230,22 +175,22 @@ export class MprisPlayer extends Signals.EventEmitter {
icon: this._app?.get_icon() ?? null,
});
this.emit('changed');
const canPlay = !!this._playerProxy.CanPlay;
let visible = this._playerProxy.CanPlay;
if (this._visible !== visible) {
this._visible = visible;
if (visible)
this.emit('show');
else
this.emit('hide');
if (this.canPlay !== canPlay) {
this._canPlay = canPlay;
this.notify('can-play');
}
this.emit('changed');
}
}
});
export const MediaSection = GObject.registerClass(
class MediaSection extends MessageList.MessageListSection {
export const MediaSource = GObject.registerClass({
Signals: {
'player-added': {param_types: [MprisPlayer]},
'player-removed': {param_types: [MprisPlayer]},
},
}, class MediaSource extends GObject.Object {
_init() {
super._init();
@ -257,30 +202,24 @@ class MediaSection extends MessageList.MessageListSection {
this._onProxyReady.bind(this));
}
get allowed() {
return !Main.sessionMode.isGreeter;
get players() {
return [...this._players.values()];
}
_addPlayer(busName) {
if (this._players.get(busName))
if (this._players.has(busName))
return;
let player = new MprisPlayer(busName);
let message = null;
player.connect('closed',
() => {
this._players.delete(busName);
});
player.connect('show', () => {
message = new MediaMessage(player);
this.addMessage(message, true);
});
player.connect('hide', () => {
this.removeMessage(message, true);
message = null;
});
const player = new MprisPlayer(busName);
this._players.set(busName, player);
player.connectObject('notify::can-play',
() => {
if (player.canPlay)
this.emit('player-added', player);
else
this.emit('player-removed', player);
}, this);
}
async _onProxyReady() {
@ -299,7 +238,16 @@ class MediaSection extends MessageList.MessageListSection {
if (!name.startsWith(MPRIS_PLAYER_PREFIX))
return;
if (newOwner && !oldOwner)
if (oldOwner) {
const player = this._players.get(name);
if (player) {
this._players.delete(name);
player.disconnectObject(this);
this.emit('player-removed', player);
}
}
if (newOwner)
this._addPlayer(name);
}
});

View file

@ -368,7 +368,6 @@ class FdoNotificationDaemonSource extends MessageTray.Source {
open() {
this.openApp();
this.destroyNonResidentNotifications();
}
openApp() {