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 <noreply@anthropic.com>
This commit is contained in:
Tommy Chow 2026-03-07 17:25:56 -08:00
parent 89f8859ba3
commit 4c696745b0
2 changed files with 118 additions and 24 deletions

View file

@ -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

View file

@ -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',
);
}
}