feat(#289): channel list in the nav drawer, switchable from inside a channel

Epic B of the navigation redesign (#102). Fills the drawer Epic A left empty
and puts it where the reported pain actually is: inside an open channel.

Previously, checking another channel meant backing out to the list, scanning
it, and navigating back in, with no way to see per-channel unread counts while
reading a channel. Now the hamburger opens a channel list showing every
channel's unread count, and tapping one switches straight to it.

- New lib/widgets/channel_drawer_list.dart. Per-tile unread via
  context.select on getUnreadCountForChannelIndex, so a new message rebuilds
  only that tile. Reuses UnreadBadge. Highlights the current channel.
- ChannelChatScreen gains the drawer plus an explicit leading hamburger: it is
  a pushed route, so Scaffold would render a back arrow in that slot instead.
  Switching uses pushReplacement, keeping the back stack one deep so system
  back still returns to the channel list (owner decision, #84).
- ChannelsScreen shares one _openChannel path between its tiles and the
  drawer, and closes the drawer before pushing.

flutter analyze clean, dart format applied. Not yet run on hardware.
pull/324/head
Strycher 4 days ago
parent 431b93f0fd
commit c73ea7b82e

@ -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<ChannelChatScreen> {
);
}
/// 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<MeshCoreConnector>();
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),

@ -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<ChannelsScreen>
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<ChannelsScreen>
);
}
/// Shared open path for the channel tiles and the nav drawer list.
Future<void> _openChannel(BuildContext context, Channel channel) async {
final connector = context.read<MeshCoreConnector>();
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) {

@ -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<Channel> onChannelSelected;
const ChannelDrawerList({
super.key,
required this.onChannelSelected,
this.currentChannelIndex,
});
@override
Widget build(BuildContext context) {
final channels = context.watch<MeshCoreConnector>().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<MeshCoreConnector, int>(
(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,
);
}
}
Loading…
Cancel
Save

Powered by TurnKey Linux.