From 38cb3dc0315069d09d0ff8d3801a7c1c26ed6e15 Mon Sep 17 00:00:00 2001 From: Janez T Date: Thu, 12 Mar 2026 14:41:12 +0100 Subject: [PATCH] Fix retry flow and contact filters --- lib/screens/contacts_tab.dart | 135 +++++++++++++++++++++++++++++----- 1 file changed, 118 insertions(+), 17 deletions(-) diff --git a/lib/screens/contacts_tab.dart b/lib/screens/contacts_tab.dart index b1a8c7b..6dfb8a4 100644 --- a/lib/screens/contacts_tab.dart +++ b/lib/screens/contacts_tab.dart @@ -33,6 +33,12 @@ class _ContactsTabState extends State { Position? _currentPosition; final Set _resolvingAdvertKeys = {}; bool _isResolvingPendingBatch = false; + final Map _sectionFilters = { + ContactSection.teamMembers: '', + ContactSection.repeaters: '', + ContactSection.rooms: '', + ContactSection.channels: '', + }; final Map _sortModes = { ContactSection.teamMembers: ContactSortMode.lastSeen, ContactSection.repeaters: ContactSortMode.lastSeen, @@ -168,6 +174,22 @@ class _ContactsTabState extends State { return l10n.daysAgo(diff.inDays); } + List _filterContactsForSection( + List contacts, + ContactSection section, + ) { + final query = (_sectionFilters[section] ?? '').trim().toLowerCase(); + if (query.isEmpty) { + return contacts; + } + + return contacts.where((contact) { + final name = contact.displayName.toLowerCase(); + final advertisedName = contact.advName.toLowerCase(); + return name.contains(query) || advertisedName.contains(query); + }).toList(); + } + List _sortContacts(List contacts, ContactSection section) { final sorted = List.from(contacts); if (section == ContactSection.channels) { @@ -264,32 +286,48 @@ class _ContactsTabState extends State { builder: (context, contactsProvider, child) { final messagesProvider = context.watch(); final connectionProvider = context.watch(); - final chatContacts = _sortContacts( + final allChatContacts = _sortContacts( contactsProvider.chatContacts, ContactSection.teamMembers, ); - final repeaters = _sortContacts( + final allRepeaters = _sortContacts( contactsProvider.repeaters, ContactSection.repeaters, ); - final rooms = _sortContacts( + final allRooms = _sortContacts( contactsProvider.rooms, ContactSection.rooms, ); - final channels = _sortContacts( + final allChannels = _sortContacts( contactsProvider.channels, ContactSection.channels, ); + final chatContacts = _filterContactsForSection( + allChatContacts, + ContactSection.teamMembers, + ); + final repeaters = _filterContactsForSection( + allRepeaters, + ContactSection.repeaters, + ); + final rooms = _filterContactsForSection( + allRooms, + ContactSection.rooms, + ); + final filteredChannels = _filterContactsForSection( + allChannels, + ContactSection.channels, + ); final pendingAdverts = contactsProvider.pendingAdverts; _schedulePendingAdvertResolution(pendingAdverts, connectionProvider); // Check if there are any displayable contacts final hasDisplayableContacts = - chatContacts.isNotEmpty || - repeaters.isNotEmpty || - rooms.isNotEmpty || - channels.isNotEmpty || + allChatContacts.isNotEmpty || + allRepeaters.isNotEmpty || + allRooms.isNotEmpty || + allChannels.isNotEmpty || pendingAdverts.isNotEmpty; if (!hasDisplayableContacts) { @@ -324,7 +362,7 @@ class _ContactsTabState extends State { padding: const EdgeInsets.all(8), children: [ // Team Members (Chat contacts) - if (chatContacts.isNotEmpty) ...[ + if (allChatContacts.isNotEmpty) ...[ _SectionHeader( title: l10n.teamMembers, count: chatContacts.length, @@ -334,31 +372,43 @@ class _ContactsTabState extends State { ContactSection.teamMembers, ), ), - ..._buildContactSectionItems(chatContacts), + _buildSectionFilterField(context, ContactSection.teamMembers), + if (chatContacts.isEmpty) + _buildEmptyFilterState(context) + else + ..._buildContactSectionItems(chatContacts), const Divider(height: 32), ], // Repeaters - if (repeaters.isNotEmpty) ...[ + if (allRepeaters.isNotEmpty) ...[ _SectionHeader( title: l10n.repeaters, count: repeaters.length, icon: Icons.router, trailing: _buildSortMenu(context, ContactSection.repeaters), ), - ..._buildContactSectionItems(repeaters), + _buildSectionFilterField(context, ContactSection.repeaters), + if (repeaters.isEmpty) + _buildEmptyFilterState(context) + else + ..._buildContactSectionItems(repeaters), const Divider(height: 32), ], // Rooms - if (rooms.isNotEmpty) ...[ + if (allRooms.isNotEmpty) ...[ _SectionHeader( title: l10n.rooms, count: rooms.length, icon: Icons.tag, trailing: _buildSortMenu(context, ContactSection.rooms), ), - ..._buildContactSectionItems(rooms), + _buildSectionFilterField(context, ContactSection.rooms), + if (rooms.isEmpty) + _buildEmptyFilterState(context) + else + ..._buildContactSectionItems(rooms), const Divider(height: 32), ], @@ -386,11 +436,14 @@ class _ContactsTabState extends State { // Channels (visible in both simple and advanced mode) _SectionHeader( title: l10n.channels, - count: channels.length, + count: filteredChannels.length, icon: Icons.broadcast_on_personal, ), - if (channels.isNotEmpty) ...[ - ...channels.map( + _buildSectionFilterField(context, ContactSection.channels), + if (allChannels.isNotEmpty && filteredChannels.isEmpty) + _buildEmptyFilterState(context), + if (filteredChannels.isNotEmpty) ...[ + ...filteredChannels.map( (channel) => _ChannelActivityCard( channel: channel, messagesProvider: messagesProvider, @@ -454,6 +507,54 @@ class _ContactsTabState extends State { }).toList(); } + Widget _buildSectionFilterField( + BuildContext context, + ContactSection section, + ) { + final hasFilter = (_sectionFilters[section] ?? '').isNotEmpty; + + return Padding( + padding: const EdgeInsets.only(bottom: 8), + child: TextFormField( + key: ValueKey('${section.name}:${_sectionFilters[section] ?? ''}'), + initialValue: _sectionFilters[section] ?? '', + onChanged: (value) { + setState(() { + _sectionFilters[section] = value; + }); + }, + decoration: InputDecoration( + isDense: true, + prefixIcon: const Icon(Icons.search, size: 18), + hintText: 'Filter by name', + border: const OutlineInputBorder(), + suffixIcon: hasFilter + ? IconButton( + icon: const Icon(Icons.close, size: 18), + onPressed: () { + setState(() { + _sectionFilters[section] = ''; + }); + }, + ) + : null, + ), + ), + ); + } + + Widget _buildEmptyFilterState(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(bottom: 8), + child: Text( + 'No matches for this filter.', + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ); + } + Widget _buildSortMenu(BuildContext context, ContactSection section) { final l10n = AppLocalizations.of(context)!; final selectedMode = _sortModes[section] ?? ContactSortMode.lastSeen;