From 4c696745b074346eb94ba2ec155c647b88462fbd Mon Sep 17 00:00:00 2001 From: Tommy Chow Date: Sat, 7 Mar 2026 17:25:56 -0800 Subject: [PATCH] add tab disconnect option via context menu X button on secondary tabs now opens a MenuAnchor popup with Disconnect (deactivates without removing) and Remove options. Menu styled to match app overlay surfaces (12px radius, outline border). Co-Authored-By: Claude Opus 4.6 --- .../channel/chat/stores/chat_tabs_store.dart | 33 ++++++ .../channel/chat/widgets/chat_tabs.dart | 109 ++++++++++++++---- 2 files changed, 118 insertions(+), 24 deletions(-) diff --git a/lib/screens/channel/chat/stores/chat_tabs_store.dart b/lib/screens/channel/chat/stores/chat_tabs_store.dart index e87767a7..0fc54c46 100644 --- a/lib/screens/channel/chat/stores/chat_tabs_store.dart +++ b/lib/screens/channel/chat/stores/chat_tabs_store.dart @@ -531,6 +531,39 @@ abstract class ChatTabsStoreBase with Store { return true; } + /// Deactivates a tab by disposing its ChatStore without removing the tab. + /// The tab stays in the tab bar but appears dimmed. Tapping reactivates it. + @action + void deactivateTab(int index) { + if (index < 0 || index >= _tabs.length) return; + final tab = _tabs[index]; + if (tab.isPrimary) return; + if (tab.chatStore == null) return; + + // If this is the active tab, switch to primary + if (index == activeTabIndex) { + activeTabIndex = 0; + } + + // Clean up merged mode state + if (mergedMode) { + _tabChannelProfiles.remove(tab.channelId); + _mergedSnapshot = null; + } + + // Dispose the ChatStore (closes IRC connection) + tab.chatStore!.dispose(); + tab.chatStore = null; + + // Exit merged mode if fewer than 2 tabs remain activated + if (mergedMode && _tabs.where((t) => t.chatStore != null).length < 2) { + mergedMode = false; + _mergedRenderTimer?.cancel(); + _mergedRenderTimer = null; + _mergedRenderedMessages = []; + } + } + /// Removes the tab at the given index. /// Returns true if removed, false if primary tab or invalid index. @action diff --git a/lib/screens/channel/chat/widgets/chat_tabs.dart b/lib/screens/channel/chat/widgets/chat_tabs.dart index 68072c08..7d565190 100644 --- a/lib/screens/channel/chat/widgets/chat_tabs.dart +++ b/lib/screens/channel/chat/widgets/chat_tabs.dart @@ -245,32 +245,93 @@ class ChatTabs extends StatelessWidget { final avatar = ProfilePicture(userLogin: tabInfo.channelLogin, radius: 12); - return InputChip( - avatar: isActivated ? avatar : Opacity(opacity: 0.5, child: avatar), - label: Text( - displayName, - style: isActivated - ? null - : TextStyle( - color: Theme.of( - context, - ).textTheme.bodyMedium?.color?.withValues(alpha: 0.5), - ), + if (tabInfo.isPrimary) { + return InputChip( + avatar: avatar, + label: Text(displayName), + selected: isActive, + showCheckmark: false, + visualDensity: VisualDensity.compact, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + onPressed: () { + if (!isActive) { + HapticFeedback.selectionClick(); + chatTabsStore.setActiveTab(index); + } + }, + ); + } + + final borderColor = + Theme.of(context).colorScheme.outlineVariant.withValues(alpha: 0.5); + + return MenuAnchor( + style: MenuStyle( + padding: const WidgetStatePropertyAll(EdgeInsets.symmetric(vertical: 4)), + shape: WidgetStatePropertyAll( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + side: BorderSide(color: borderColor), + ), + ), ), - selected: isActive, - showCheckmark: false, - visualDensity: VisualDensity.compact, - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - onPressed: () { - if (!isActive) { - HapticFeedback.selectionClick(); - chatTabsStore.setActiveTab(index); - } + menuChildren: [ + if (isActivated) + MenuItemButton( + leadingIcon: const Icon(Icons.power_off_rounded, size: 18), + style: MenuItemButton.styleFrom( + visualDensity: VisualDensity.compact, + ), + child: const Text('Disconnect'), + onPressed: () { + HapticFeedback.lightImpact(); + chatTabsStore.deactivateTab(index); + }, + ), + MenuItemButton( + leadingIcon: Icon( + Icons.delete_outline_rounded, + size: 18, + color: Theme.of(context).colorScheme.error, + ), + style: MenuItemButton.styleFrom( + visualDensity: VisualDensity.compact, + ), + child: Text( + 'Remove', + style: TextStyle(color: Theme.of(context).colorScheme.error), + ), + onPressed: () => _confirmRemoveTab(context, index, displayName), + ), + ], + builder: (context, controller, child) { + return InputChip( + avatar: + isActivated ? avatar : Opacity(opacity: 0.5, child: avatar), + label: Text( + displayName, + style: isActivated + ? null + : TextStyle( + color: Theme.of( + context, + ).textTheme.bodyMedium?.color?.withValues(alpha: 0.5), + ), + ), + selected: isActive, + showCheckmark: false, + visualDensity: VisualDensity.compact, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + onPressed: () { + if (!isActive) { + HapticFeedback.selectionClick(); + chatTabsStore.setActiveTab(index); + } + }, + onDeleted: () => controller.open(), + deleteButtonTooltipMessage: 'Tab options', + ); }, - onDeleted: tabInfo.isPrimary - ? null - : () => _confirmRemoveTab(context, index, displayName), - deleteButtonTooltipMessage: 'Close chat', ); } }