diff --git a/lib/screens/channel_chat_screen.dart b/lib/screens/channel_chat_screen.dart index 2d3c207..b4bfcd8 100644 --- a/lib/screens/channel_chat_screen.dart +++ b/lib/screens/channel_chat_screen.dart @@ -28,6 +28,7 @@ import '../services/block_service.dart'; import '../services/chat_text_scale_service.dart'; import '../services/translation_service.dart'; import '../utils/emoji_utils.dart'; +import '../widgets/channel_drawer_list.dart'; import '../widgets/mention_autocomplete.dart'; import '../helpers/emoji_shortcodes.dart'; import '../widgets/chat_zoom_wrapper.dart'; @@ -271,10 +272,49 @@ class _ChannelChatScreenState extends State { ); } + /// Switch to another channel from the drawer without backing out to the + /// channel list. pushReplacement keeps the back stack one deep, so system + /// back still returns to the channel list rather than the previous channel. + void _switchChannel(Channel channel) { + if (channel.index == widget.channel.index) { + Navigator.pop(context); + return; + } + final connector = context.read(); + final unread = connector.getUnreadCountForChannelIndex(channel.index); + connector.markChannelRead(channel.index); + Navigator.pushReplacement( + context, + MaterialPageRoute( + builder: (context) => + ChannelChatScreen(channel: channel, initialUnreadCount: unread), + ), + ); + } + @override Widget build(BuildContext context) { return Scaffold( + drawer: Drawer( + width: 300, + child: SafeArea( + child: ChannelDrawerList( + currentChannelIndex: widget.channel.index, + onChannelSelected: _switchChannel, + ), + ), + ), appBar: AppBar( + // Explicit hamburger: this is a pushed route, so Scaffold would show a + // back arrow here instead of the drawer button. System back still pops + // to the channel list. + leading: Builder( + builder: (context) => IconButton( + icon: const Icon(Icons.menu), + tooltip: context.l10n.channels_title, + onPressed: () => Scaffold.of(context).openDrawer(), + ), + ), title: Row( children: [ _channelIcon(widget.channel), diff --git a/lib/screens/channels_screen.dart b/lib/screens/channels_screen.dart index 4dc1afc..d6e3116 100644 --- a/lib/screens/channels_screen.dart +++ b/lib/screens/channels_screen.dart @@ -24,6 +24,7 @@ import '../widgets/list_filter_widget.dart'; import '../widgets/empty_state.dart'; import '../widgets/qr_code_display.dart'; import '../widgets/app_shell.dart'; +import '../widgets/channel_drawer_list.dart'; import '../widgets/sync_progress_overlay.dart'; import '../widgets/unread_badge.dart'; import '../helpers/snack_bar_builder.dart'; @@ -105,6 +106,9 @@ class _ChannelsScreenState extends State onDestinationSelected: (index) => _handleQuickSwitch(index, context), contactsUnreadCount: connector.getTotalContactsUnreadCount(), channelsUnreadCount: connector.getTotalChannelsUnreadCount(), + drawerContent: ChannelDrawerList( + onChannelSelected: (channel) => _openChannel(context, channel), + ), appBar: AppBar( title: AppBarTitle(context.l10n.channels_title), centerTitle: true, @@ -629,6 +633,28 @@ class _ChannelsScreenState extends State ); } + /// Shared open path for the channel tiles and the nav drawer list. + Future _openChannel(BuildContext context, Channel channel) async { + final connector = context.read(); + final unread = connector.getUnreadCountForChannelIndex(channel.index); + connector.markChannelRead(channel.index); + // Close the drawer if the tap came from it, so it is not left open behind + // the chat screen when the user comes back. + final scaffold = Scaffold.maybeOf(context); + if (scaffold?.isDrawerOpen ?? false) { + Navigator.pop(context); + } + await Future.delayed(const Duration(milliseconds: 50)); + if (!context.mounted) return; + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + ChannelChatScreen(channel: channel, initialUnreadCount: unread), + ), + ); + } + void _handleQuickSwitch(int index, BuildContext context) { if (index == 1) return; switch (index) { diff --git a/lib/widgets/channel_drawer_list.dart b/lib/widgets/channel_drawer_list.dart new file mode 100644 index 0000000..3846621 --- /dev/null +++ b/lib/widgets/channel_drawer_list.dart @@ -0,0 +1,97 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import '../connector/meshcore_connector.dart'; +import '../l10n/l10n.dart'; +import '../models/channel.dart'; +import 'unread_badge.dart'; + +/// Channel list rendered inside the nav drawer. +/// +/// Lets you see every channel's unread count and jump straight to another +/// channel without backing out of the one you are reading. +/// +/// [currentChannelIndex] highlights the channel you are in, and is null on the +/// channels list screen where no channel is open. +class ChannelDrawerList extends StatelessWidget { + final int? currentChannelIndex; + final ValueChanged onChannelSelected; + + const ChannelDrawerList({ + super.key, + required this.onChannelSelected, + this.currentChannelIndex, + }); + + @override + Widget build(BuildContext context) { + final channels = context.watch().channels; + + if (channels.isEmpty) { + return Center( + child: Padding( + padding: const EdgeInsets.all(24), + child: Text( + context.l10n.channels_title, + style: Theme.of(context).textTheme.bodyMedium, + textAlign: TextAlign.center, + ), + ), + ); + } + + return ListView.builder( + padding: EdgeInsets.zero, + itemCount: channels.length, + itemBuilder: (context, i) { + final channel = channels[i]; + return _ChannelDrawerTile( + channel: channel, + selected: channel.index == currentChannelIndex, + onTap: () => onChannelSelected(channel), + ); + }, + ); + } +} + +class _ChannelDrawerTile extends StatelessWidget { + final Channel channel; + final bool selected; + final VoidCallback onTap; + + const _ChannelDrawerTile({ + required this.channel, + required this.selected, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + // Selector keeps the rebuild to this tile when its unread count moves. + final unread = context.select( + (c) => c.getUnreadCountForChannelIndex(channel.index), + ); + final theme = Theme.of(context); + + return ListTile( + dense: true, + selected: selected, + selectedTileColor: theme.colorScheme.secondaryContainer, + leading: Icon( + channel.isPublicChannel ? Icons.public : Icons.tag, + size: 20, + ), + title: Text( + channel.name, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontWeight: unread > 0 ? FontWeight.w700 : FontWeight.w500, + ), + ), + trailing: unread > 0 ? UnreadBadge(count: unread) : null, + onTap: onTap, + ); + } +}