From 7715dc479abae088c0cc5086317c1a22ec34f3e4 Mon Sep 17 00:00:00 2001 From: foxyz Date: Sun, 18 Oct 2026 01:29:58 +0300 Subject: [PATCH] Chat: no pop-ups while the chat panel is open (the sound still plays) Co-Authored-By: Claude Opus 5.5 --- apps/desktop/lib/src/session/notifier.dart | 11 ++++++++++- apps/desktop/lib/src/shell/app_shell.dart | 2 ++ apps/desktop/test/notice_table_test.dart | 6 ++++++ docs/ARCHITECTURE.md | 6 ++++-- 4 files changed, 22 insertions(+), 3 deletions(-) diff --git a/apps/desktop/lib/src/session/notifier.dart b/apps/desktop/lib/src/session/notifier.dart index aeba3c0..5ae5241 100644 --- a/apps/desktop/lib/src/session/notifier.dart +++ b/apps/desktop/lib/src/session/notifier.dart @@ -98,12 +98,15 @@ class NoticeContext { final WindowActivity window; final bool playing; final NotifyPrefs prefs; + /// The chat panel is open: chat is read there, not in pop-ups. + final bool chatOpen; const NoticeContext({ this.self, this.alone = false, this.window = WindowActivity.focused, this.playing = false, this.prefs = const NotifyPrefs(), + this.chatOpen = false, }); bool get away => self == Presence.away; @@ -317,6 +320,8 @@ Notice noticeFor(AppEvent e, [NoticeContext c = const NoticeContext()]) { } if (cat == NoticeCategory.start && !c.away && !c.window.background) toast = false; if (cat == NoticeCategory.chat && !c.prefs.chat.shows(playing: c.playing)) toast = false; + // The open chat shows it already; the sound still plays. + if (cat == NoticeCategory.chat && c.chatOpen && !c.window.background) toast = false; SystemNote? system; if (toast && !own && c.window.background && cat.system && cat.reachesAway(c.prefs)) { final actor = n.actor; @@ -351,6 +356,9 @@ class Notifier { /// The window's activity ([ShellPort.watchActivity], wired in `main`). WindowActivity window = WindowActivity.focused; + /// The chat panel is open (set by the shell). + bool chatOpen = false; + Notifier({ this.sfx = const NoopSfxService(), Future Function()? attention, @@ -379,7 +387,8 @@ class Notifier { NoticeContext get context { final s = session(); - return NoticeContext(self: s.self, alone: s.alone, window: window, playing: s.playing, prefs: prefs()); + return NoticeContext( + self: s.self, alone: s.alone, window: window, playing: s.playing, prefs: prefs(), chatOpen: chatOpen); } void notify(AppEvent e) { diff --git a/apps/desktop/lib/src/shell/app_shell.dart b/apps/desktop/lib/src/shell/app_shell.dart index b5248b1..8f62642 100644 --- a/apps/desktop/lib/src/shell/app_shell.dart +++ b/apps/desktop/lib/src/shell/app_shell.dart @@ -287,6 +287,7 @@ class AppShellState extends State with SingleTickerProviderStateMixin if (_fullscreen) _toggleFullscreen(); setState(() { _chatOpen = false; + _graph.notifier.chatOpen = false; _unread = 0; }); _show(PlayerMode.none); @@ -362,6 +363,7 @@ class AppShellState extends State with SingleTickerProviderStateMixin void _toggleChat() => setState(() { _chatOpen = !_chatOpen; + _graph.notifier.chatOpen = _chatOpen; _unread = 0; if (!_chatOpen) _chatFocus.unfocus(); }); diff --git a/apps/desktop/test/notice_table_test.dart b/apps/desktop/test/notice_table_test.dart index 924232e..0c3c3de 100644 --- a/apps/desktop/test/notice_table_test.dart +++ b/apps/desktop/test/notice_table_test.dart @@ -66,6 +66,8 @@ const _alone = NoticeContext(self: Presence.watching, alone: true); const _background = NoticeContext(self: Presence.watching, window: WindowActivity.unfocused); const _minimisedAway = NoticeContext(self: Presence.away, window: WindowActivity.minimised); const _playing = NoticeContext(self: Presence.watching, playing: true); +const _chatOpen = NoticeContext(self: Presence.watching, chatOpen: true); +const _chatOpenBackground = NoticeContext(self: Presence.watching, chatOpen: true, window: WindowActivity.unfocused); const _chatPaused = NoticeContext(self: Presence.watching, playing: true, prefs: NotifyPrefs(chat: ChatPopups.paused)); const _chatOff = NoticeContext(self: Presence.watching, window: WindowActivity.unfocused, prefs: NotifyPrefs(chat: ChatPopups.off)); @@ -115,6 +117,10 @@ final _rows = <_Row>[ _Row('finished', _of(_feed('finished', actor: 'server')), Finished, logged: true), _Row('chat from someone', _of(_feed('chat', reason: 'hi')), ChatPosted, toast: 'Ann: hi', actor: 'Ann', sound: SfxKind.chat, logged: true), + _Row('chat with the chat open: only its sound', _of(_feed('chat', reason: 'hi')), ChatPosted, + context: _chatOpen, sound: SfxKind.chat, logged: true), + _Row('chat open but the window in the background: pops up and goes out', _of(_feed('chat', reason: 'hi')), ChatPosted, + context: _chatOpenBackground, toast: 'Ann: hi', actor: 'Ann', sound: SfxKind.chat, logged: true, system: ('Ann', 'hi')), _Row('own chat', _of(_feed('chat', actor: 'Me', reason: 'hi')), ChatPosted, toast: 'Me: hi', actor: 'Me', logged: true), _Row('rename', _of(_feed('rename', target: 'Annie')), MemberRenamed, logged: true), diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 48aeb92..75a0f20 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -1675,8 +1675,10 @@ background, default off). The coordinator gives the session's part like, "starting now"); the app's notices, problems and your own show. - "Starting now" pops up only while you sit out or the window is in the background. -- Chat pop-ups follow the setting (it is in the chat either way); the - chat sound has its own switch. +- Chat pop-ups follow the setting (it is in the chat either way), and + none show while the chat panel is open in a focused window + (`Notifier.chatOpen`, set by the shell); the chat sound has its own + switch and still plays. - The window in the background (unfocused, minimised, hidden): what reaches a member who sits out (chat, nudges, "starting now", Up next, joins and leaves when chosen) also goes out as a system notification