Unread badges for tabs; "People first" contacts sorting option#145
Unread badges for tabs; "People first" contacts sorting option#145pioneer wants to merge 7 commits intozjs81:mainfrom
Conversation
There was a problem hiding this comment.
Pull request overview
Adds unread-count badges to the bottom navigation tabs (Contacts/Channels) and introduces a new Contacts list ordering toggle to keep people (users) at the top, with localization updates to support the new UI label.
Changes:
- Add unread badge rendering to
QuickSwitchBarand wire unread totals fromMeshCoreConnectorinto screens using the bottom navigation. - Add a “users/people first” toggle to the Contacts filter menu and update contacts sorting to partition users before other contact types.
- Add the new localization key + Ukrainian translation, and update
untranslated.jsonplus generated localization outputs.
Reviewed changes
Copilot reviewed 26 out of 26 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| untranslated.json | Updates the untranslated-keys list to reflect the new listFilter_usersFirst string. |
| lib/widgets/quick_switch_bar.dart | Adds badge display to Contacts/Channels icons and adds selected icon for Map. |
| lib/widgets/list_filter_widget.dart | Adds “users first” toggle option to the Contacts filter popup menu. |
| lib/screens/map_screen.dart | Passes unread totals into QuickSwitchBar. |
| lib/screens/device_screen.dart | Passes unread totals into QuickSwitchBar from the device hub screen. |
| lib/screens/contacts_screen.dart | Introduces _prioritizePeople and applies people-first ordering + refactors sorting into a helper. |
| lib/screens/channels_screen.dart | Passes unread totals into QuickSwitchBar. |
| lib/connector/meshcore_connector.dart | Splits unread total counting into contacts vs channels for badge usage. |
| lib/l10n/app_en.arb | Adds the new listFilter_usersFirst string. |
| lib/l10n/app_uk.arb | Adds Ukrainian translation for listFilter_usersFirst and updates a few other Ukrainian strings. |
| lib/l10n/app_localizations.dart | Adds the new listFilter_usersFirst abstract getter (generated). |
| lib/l10n/app_localizations_en.dart | Adds listFilter_usersFirst getter (generated). |
| lib/l10n/app_localizations_bg.dart | Adds listFilter_usersFirst getter (generated). |
| lib/l10n/app_localizations_de.dart | Adds listFilter_usersFirst getter (generated). |
| lib/l10n/app_localizations_es.dart | Adds listFilter_usersFirst getter (generated). |
| lib/l10n/app_localizations_fr.dart | Adds listFilter_usersFirst getter (generated). |
| lib/l10n/app_localizations_it.dart | Adds listFilter_usersFirst getter (generated). |
| lib/l10n/app_localizations_nl.dart | Adds listFilter_usersFirst getter (generated). |
| lib/l10n/app_localizations_pl.dart | Adds listFilter_usersFirst getter (generated). |
| lib/l10n/app_localizations_pt.dart | Adds listFilter_usersFirst getter (generated). |
| lib/l10n/app_localizations_ru.dart | Adds listFilter_usersFirst getter (generated). |
| lib/l10n/app_localizations_sk.dart | Adds listFilter_usersFirst getter (generated). |
| lib/l10n/app_localizations_sl.dart | Adds listFilter_usersFirst getter (generated). |
| lib/l10n/app_localizations_sv.dart | Adds listFilter_usersFirst getter (generated). |
| lib/l10n/app_localizations_uk.dart | Adds listFilter_usersFirst getter + Ukrainian string updates (generated). |
| lib/l10n/app_localizations_zh.dart | Adds listFilter_usersFirst getter (generated). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 26 out of 26 changed files in this pull request and generated 4 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| final previousCount = _contactUnreadCount[contactKeyHex] ?? 0; | ||
| if (previousCount > 0) { | ||
| _contactUnreadCount[contactKeyHex] = 0; | ||
| _cachedContactsUnreadTotal -= previousCount; | ||
| _appDebugLogService?.info( |
There was a problem hiding this comment.
_cachedContactsUnreadTotal is decremented by previousCount when marking a contact read, but the cached total can become out-of-sync (e.g., when entries are removed from _contactUnreadCount elsewhere) and end up negative. It would be safer to clamp to >= 0 (or periodically recalculate) after applying the delta so UI badges don’t show incorrect totals.
| if (channel != null && channel.unreadCount > 0) { | ||
| final previousCount = channel.unreadCount; | ||
| channel.unreadCount = 0; | ||
| _cachedChannelsUnreadTotal -= previousCount; | ||
| _appDebugLogService?.info( |
There was a problem hiding this comment.
_cachedChannelsUnreadTotal is decremented by previousCount when marking a channel read; if the cache ever becomes out-of-sync, this can drive the total negative. Consider clamping to >= 0 (or recalculating) after applying the delta to avoid incorrect badge counts.
lib/widgets/quick_switch_bar.dart
Outdated
|
|
||
| return Badge( | ||
| label: Text( | ||
| count > 99 ? '99+' : count.toString(), | ||
| style: const TextStyle(fontSize: 10), | ||
| ), | ||
| child: icon, |
There was a problem hiding this comment.
QuickSwitchBar introduces a second unread badge implementation using Material Badge, while the rest of the UI uses the custom UnreadBadge widget for unread counts. This is likely to create inconsistent styling/behavior (colors, text scaling, theming) across the app. Consider reusing UnreadBadge here (e.g., via a Stack/Positioned overlay) or centralizing badge rendering so unread indicators look and behave consistently.
| return Badge( | |
| label: Text( | |
| count > 99 ? '99+' : count.toString(), | |
| style: const TextStyle(fontSize: 10), | |
| ), | |
| child: icon, | |
| return UnreadBadge( | |
| count: count, | |
| child: icon, | |
| ); | |
| } | |
| } | |
| class UnreadBadge extends StatelessWidget { | |
| final int count; | |
| final Widget child; | |
| const UnreadBadge({ | |
| super.key, | |
| required this.count, | |
| required this.child, | |
| }); | |
| @override | |
| Widget build(BuildContext context) { | |
| if (count <= 0) return child; | |
| final displayText = count > 99 ? '99+' : count.toString(); | |
| return Badge( | |
| label: Text( | |
| displayText, | |
| style: const TextStyle(fontSize: 10), | |
| ), | |
| child: child, |
| } | ||
|
|
||
| void _recalculateCachedContactsUnreadTotal() { | ||
| _cachedContactsUnreadTotal = _contactUnreadCount.values.fold(0, (a, b) => a + b); |
There was a problem hiding this comment.
_recalculateCachedContactsUnreadTotal() sums all values in _contactUnreadCount, which can include repeaters or other contacts that are intentionally excluded by getUnreadCountForContact/getUnreadCountForContactKey (e.g., advTypeRepeater). This can inflate the Contacts tab badge/notification counts compared to the previous behavior. Consider recalculating the cached total using the same filtering logic as getTotalUnreadCount previously did (sum over non-repeater contacts / _shouldTrackUnreadForContactKey), and ensure the cache stays consistent when contacts change type (e.g., when a contact is refreshed as a repeater).
| _cachedContactsUnreadTotal = _contactUnreadCount.values.fold(0, (a, b) => a + b); | |
| int total = 0; | |
| _contactUnreadCount.forEach((contactKeyHex, count) { | |
| if (_shouldTrackUnreadForContactKey(contactKeyHex)) { | |
| total += count; | |
| } | |
| }); | |
| _cachedContactsUnreadTotal = total; |
|
I'll take a look later today. |
Bottom tabs (Contacts, Channels) didn't show whether there are new messages in channels or in direct messages. Now, when any channel has a new message, the Channels tab receives an unread badge with the total count of unread messages in all channels. Same for Contacts.
Added "Users first" option in Contacts so that people will be on top of the list, leaving repeaters and the rest below them.
Added Ukrainian translation for "Users first", and translation stubs for all other languages.
Closes #91