From f2de6c653d9510cabb1681b3f5e441d801ed7eec Mon Sep 17 00:00:00 2001 From: Tommy Chow Date: Sat, 18 Apr 2026 15:18:08 -0700 Subject: [PATCH] add focus current channel toggle to fade non-active shared chat messages Replaces the merge button in the chat tab bar with a more-actions menu that exposes both "Merge chats" and "Focus current channel" toggles. When focus mode is on, messages whose source channel differs from the active channel render at reduced opacity in both Twitch native shared chat and merged-tabs views. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../channel/chat/widgets/chat_message.dart | 15 ++++- .../channel/chat/widgets/chat_tabs.dart | 61 +++++++++++++------ lib/screens/settings/chat_settings.dart | 10 +++ .../settings/stores/settings_store.dart | 6 ++ .../settings/stores/settings_store.g.dart | 21 +++++++ 5 files changed, 95 insertions(+), 18 deletions(-) diff --git a/lib/screens/channel/chat/widgets/chat_message.dart b/lib/screens/channel/chat/widgets/chat_message.dart index c11b4d9e..70ba68fb 100644 --- a/lib/screens/channel/chat/widgets/chat_message.dart +++ b/lib/screens/channel/chat/widgets/chat_message.dart @@ -609,6 +609,19 @@ class ChatMessage extends StatelessWidget { child: dividedMessage, ); + final sourceChannelId = + ircMessage.tags['source-room-id'] ?? ircMessage.tags['room-id']; + final currentChannelId = + overrideCurrentChannelId ?? chatStore.channelId; + final shouldFade = + chatStore.settings.focusCurrentChannel && + sourceChannelId != null && + sourceChannelId != currentChannelId; + + final fadedMessage = shouldFade + ? Opacity(opacity: 0.4, child: coloredMessage) + : coloredMessage; + final finalMessage = InkWell( onTap: () { FocusScope.of(context).unfocus(); @@ -617,7 +630,7 @@ class ChatMessage extends StatelessWidget { } }, onLongPress: () => onLongPressMessage(context, defaultTextStyle), - child: coloredMessage, + child: fadedMessage, ); return finalMessage; diff --git a/lib/screens/channel/chat/widgets/chat_tabs.dart b/lib/screens/channel/chat/widgets/chat_tabs.dart index fe946202..e64d4cc8 100644 --- a/lib/screens/channel/chat/widgets/chat_tabs.dart +++ b/lib/screens/channel/chat/widgets/chat_tabs.dart @@ -171,7 +171,9 @@ class ChatTabs extends StatelessWidget { right: 12, top: 0, bottom: 0, - child: Center(child: _buildMergeToggle(context)), + child: Center( + child: _buildMoreActionsMenu(context), + ), ), ], ), @@ -184,22 +186,47 @@ class ChatTabs extends StatelessWidget { ); } - Widget _buildMergeToggle(BuildContext context) { - final isMerged = chatTabsStore.mergedMode; - return IconButton.filledTonal( - icon: const Icon(Icons.call_merge, size: 18), - tooltip: isMerged ? 'Split chats' : 'Merge loaded chats', - visualDensity: VisualDensity.compact, - isSelected: isMerged, - style: IconButton.styleFrom( - minimumSize: const Size(42, 42), - backgroundColor: isMerged - ? null - : Theme.of(context).colorScheme.surfaceContainerHighest, - ), - onPressed: () { - HapticFeedback.selectionClick(); - chatTabsStore.toggleMergedMode(); + Widget _buildMoreActionsMenu(BuildContext context) { + final settings = chatTabsStore.activeChatStore.settings; + + return MenuAnchor( + menuChildren: [ + Observer( + builder: (_) => CheckboxMenuButton( + value: chatTabsStore.mergedMode, + onChanged: (_) { + HapticFeedback.selectionClick(); + chatTabsStore.toggleMergedMode(); + }, + child: const Text('Merge chats'), + ), + ), + Observer( + builder: (_) => CheckboxMenuButton( + value: settings.focusCurrentChannel, + onChanged: (newValue) { + HapticFeedback.selectionClick(); + settings.focusCurrentChannel = newValue ?? false; + }, + child: const Text('Focus current channel'), + ), + ), + ], + builder: (context, controller, child) { + return IconButton.filledTonal( + icon: const Icon(Icons.more_vert, size: 18), + tooltip: 'Chat options', + visualDensity: VisualDensity.compact, + style: IconButton.styleFrom( + minimumSize: const Size(42, 42), + backgroundColor: + Theme.of(context).colorScheme.surfaceContainerHighest, + ), + onPressed: () { + HapticFeedback.selectionClick(); + controller.isOpen ? controller.close() : controller.open(); + }, + ); }, ); } diff --git a/lib/screens/settings/chat_settings.dart b/lib/screens/settings/chat_settings.dart index 761b90ab..16b086ef 100644 --- a/lib/screens/settings/chat_settings.dart +++ b/lib/screens/settings/chat_settings.dart @@ -221,6 +221,16 @@ class ChatSettings extends StatelessWidget { onChanged: (newValue) => settingsStore.showHistoricalTimestamps = newValue, ), + SettingsListSwitch( + title: 'Focus current channel', + subtitle: const Text( + 'Fades messages from other channels in shared chat and ' + 'merged views, so the current channel stands out.', + ), + value: settingsStore.focusCurrentChannel, + onChanged: (newValue) => + settingsStore.focusCurrentChannel = newValue, + ), const SectionHeader('Delay'), SettingsListSwitch( title: 'Auto-sync chat delay', diff --git a/lib/screens/settings/stores/settings_store.dart b/lib/screens/settings/stores/settings_store.dart index 3975849e..333f4e38 100644 --- a/lib/screens/settings/stores/settings_store.dart +++ b/lib/screens/settings/stores/settings_store.dart @@ -128,6 +128,7 @@ abstract class _SettingsStoreBase with Store { static const defaultShowChatMessageDividers = false; static const defaultTimestampType = TimestampType.disabled; static const defaultShowHistoricalTimestamps = false; + static const defaultFocusCurrentChannel = false; // Alert defaults static const defaultHighlightFirstTimeChatter = true; @@ -201,6 +202,10 @@ abstract class _SettingsStoreBase with Store { @observable var showHistoricalTimestamps = defaultShowHistoricalTimestamps; + @JsonKey(defaultValue: defaultFocusCurrentChannel) + @observable + var focusCurrentChannel = defaultFocusCurrentChannel; + @JsonKey(defaultValue: defaultHighlightFirstTimeChatter) @observable var highlightFirstTimeChatter = defaultHighlightFirstTimeChatter; @@ -320,6 +325,7 @@ abstract class _SettingsStoreBase with Store { showChatMessageDividers = defaultShowChatMessageDividers; timestampType = defaultTimestampType; showHistoricalTimestamps = defaultShowHistoricalTimestamps; + focusCurrentChannel = defaultFocusCurrentChannel; highlightFirstTimeChatter = defaultHighlightFirstTimeChatter; showUserNotices = defaultShowUserNotices; diff --git a/lib/screens/settings/stores/settings_store.g.dart b/lib/screens/settings/stores/settings_store.g.dart index 9ea423cc..2e713294 100644 --- a/lib/screens/settings/stores/settings_store.g.dart +++ b/lib/screens/settings/stores/settings_store.g.dart @@ -43,6 +43,7 @@ SettingsStore _$SettingsStoreFromJson( TimestampType.disabled ..showHistoricalTimestamps = json['showHistoricalTimestamps'] as bool? ?? false + ..focusCurrentChannel = json['focusCurrentChannel'] as bool? ?? false ..highlightFirstTimeChatter = json['highlightFirstTimeChatter'] as bool? ?? true ..showUserNotices = json['showUserNotices'] as bool? ?? true @@ -117,6 +118,7 @@ Map _$SettingsStoreToJson( 'showChatMessageDividers': instance.showChatMessageDividers, 'timestampType': _$TimestampTypeEnumMap[instance.timestampType]!, 'showHistoricalTimestamps': instance.showHistoricalTimestamps, + 'focusCurrentChannel': instance.focusCurrentChannel, 'highlightFirstTimeChatter': instance.highlightFirstTimeChatter, 'showUserNotices': instance.showUserNotices, 'showTwitchEmotes': instance.showTwitchEmotes, @@ -572,6 +574,24 @@ mixin _$SettingsStore on _SettingsStoreBase, Store { ); } + late final _$focusCurrentChannelAtom = Atom( + name: '_SettingsStoreBase.focusCurrentChannel', + context: context, + ); + + @override + bool get focusCurrentChannel { + _$focusCurrentChannelAtom.reportRead(); + return super.focusCurrentChannel; + } + + @override + set focusCurrentChannel(bool value) { + _$focusCurrentChannelAtom.reportWrite(value, super.focusCurrentChannel, () { + super.focusCurrentChannel = value; + }); + } + late final _$highlightFirstTimeChatterAtom = Atom( name: '_SettingsStoreBase.highlightFirstTimeChatter', context: context, @@ -1223,6 +1243,7 @@ showDeletedMessages: ${showDeletedMessages}, showChatMessageDividers: ${showChatMessageDividers}, timestampType: ${timestampType}, showHistoricalTimestamps: ${showHistoricalTimestamps}, +focusCurrentChannel: ${focusCurrentChannel}, highlightFirstTimeChatter: ${highlightFirstTimeChatter}, showUserNotices: ${showUserNotices}, showTwitchEmotes: ${showTwitchEmotes},