Correct unread badges for tabs; people first contacts sort option

This commit is contained in:
Serge Tarkovski
2026-02-09 12:56:38 +02:00
parent c320378be1
commit 8668564464
26 changed files with 215 additions and 22 deletions
+15 -4
View File
@@ -336,15 +336,26 @@ class MeshCoreConnector extends ChangeNotifier {
}
int getTotalUnreadCount() {
if (!_unreadStateLoaded) return 0;
return getTotalContactsUnreadCount() + getTotalChannelsUnreadCount();
}
int getTotalContactsUnreadCount() {
if (!_unreadStateLoaded) return 0;
var total = 0;
// Count unread contact messages
for (final contact in _contacts) {
total += getUnreadCountForContact(contact);
}
// Count unread channel messages
for (final channelIndex in _channelMessages.keys) {
total += getUnreadCountForChannelIndex(channelIndex);
return total;
}
int getTotalChannelsUnreadCount() {
if (!_unreadStateLoaded) return 0;
var total = 0;
// Check all channels (from _channels or _cachedChannels)
final allChannels = _channels.isNotEmpty ? _channels : _cachedChannels;
for (final channel in allChannels) {
total += channel.unreadCount;
}
return total;
}
+1
View File
@@ -1304,6 +1304,7 @@
"listFilter_latestMessages": "Latest messages",
"listFilter_heardRecently": "Heard recently",
"listFilter_az": "A-Z",
"listFilter_usersFirst": "Users first",
"listFilter_filters": "Filters",
"listFilter_all": "All",
"listFilter_users": "Users",
+6
View File
@@ -4658,6 +4658,12 @@ abstract class AppLocalizations {
/// **'A-Z'**
String get listFilter_az;
/// No description provided for @listFilter_usersFirst.
///
/// In en, this message translates to:
/// **'Users first'**
String get listFilter_usersFirst;
/// No description provided for @listFilter_filters.
///
/// In en, this message translates to:
+3
View File
@@ -2662,6 +2662,9 @@ class AppLocalizationsBg extends AppLocalizations {
@override
String get listFilter_az => 'A-Z';
@override
String get listFilter_usersFirst => 'Users first';
@override
String get listFilter_filters => 'Филтри';
+3
View File
@@ -2666,6 +2666,9 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get listFilter_az => 'A-Z';
@override
String get listFilter_usersFirst => 'Users first';
@override
String get listFilter_filters => 'Filtere';
+3
View File
@@ -2622,6 +2622,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get listFilter_az => 'A-Z';
@override
String get listFilter_usersFirst => 'Users first';
@override
String get listFilter_filters => 'Filters';
+3
View File
@@ -2661,6 +2661,9 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get listFilter_az => 'A-Z';
@override
String get listFilter_usersFirst => 'Users first';
@override
String get listFilter_filters => 'Filtros';
+3
View File
@@ -2678,6 +2678,9 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get listFilter_az => 'A à Z';
@override
String get listFilter_usersFirst => 'Users first';
@override
String get listFilter_filters => 'Filtres';
+3
View File
@@ -2661,6 +2661,9 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get listFilter_az => 'A-Z';
@override
String get listFilter_usersFirst => 'Users first';
@override
String get listFilter_filters => 'Filtri';
+3
View File
@@ -2652,6 +2652,9 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get listFilter_az => 'A-Z';
@override
String get listFilter_usersFirst => 'Users first';
@override
String get listFilter_filters => 'Filters';
+3
View File
@@ -2660,6 +2660,9 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get listFilter_az => 'A-Z';
@override
String get listFilter_usersFirst => 'Users first';
@override
String get listFilter_filters => 'Filtry';
+3
View File
@@ -2663,6 +2663,9 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get listFilter_az => 'A-Z';
@override
String get listFilter_usersFirst => 'Users first';
@override
String get listFilter_filters => 'Filtros';
+3
View File
@@ -2665,6 +2665,9 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get listFilter_az => 'По алфавиту';
@override
String get listFilter_usersFirst => 'Users first';
@override
String get listFilter_filters => 'Фильтры';
+3
View File
@@ -2648,6 +2648,9 @@ class AppLocalizationsSk extends AppLocalizations {
@override
String get listFilter_az => 'A-Z';
@override
String get listFilter_usersFirst => 'Users first';
@override
String get listFilter_filters => 'Filtre';
+3
View File
@@ -2651,6 +2651,9 @@ class AppLocalizationsSl extends AppLocalizations {
@override
String get listFilter_az => 'A-Z';
@override
String get listFilter_usersFirst => 'Users first';
@override
String get listFilter_filters => 'Filtri';
+3
View File
@@ -2636,6 +2636,9 @@ class AppLocalizationsSv extends AppLocalizations {
@override
String get listFilter_az => 'A-Z';
@override
String get listFilter_usersFirst => 'Users first';
@override
String get listFilter_filters => 'Filteralternativ';
+7 -4
View File
@@ -2672,6 +2672,9 @@ class AppLocalizationsUk extends AppLocalizations {
@override
String get listFilter_az => 'А';
@override
String get listFilter_usersFirst => 'Спочатку користувачі';
@override
String get listFilter_filters => 'Фільтри';
@@ -2703,7 +2706,7 @@ class AppLocalizationsUk extends AppLocalizations {
String get pathTrace_notAvailable => 'Трасування шляху недоступне.';
@override
String get pathTrace_refreshTooltip => 'Оновити Path Trace';
String get pathTrace_refreshTooltip => 'Оновити трасування шляху';
@override
String get contacts_pathTrace => 'Трасування шляхів';
@@ -2744,10 +2747,10 @@ class AppLocalizationsUk extends AppLocalizations {
String get contacts_contactImportFailed => 'Контакт не вдалося імпортувати';
@override
String get contacts_zeroHopAdvert => 'Реклама без перехоплення';
String get contacts_zeroHopAdvert => 'Оголошення без ретрансляції';
@override
String get contacts_floodAdvert => 'Залив реклами';
String get contacts_floodAdvert => 'Оголошення з ретрансляцією';
@override
String get contacts_copyAdvertToClipboard =>
@@ -2774,7 +2777,7 @@ class AppLocalizationsUk extends AppLocalizations {
@override
String get contacts_contactAdvertCopied =>
'Рекламу скопійовано до буфера обміну.';
'Оголошення скопійовано до буфера обміну.';
@override
String get contacts_contactAdvertCopyFailed =>
+3
View File
@@ -2521,6 +2521,9 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get listFilter_az => 'A 到 Z';
@override
String get listFilter_usersFirst => 'Users first';
@override
String get listFilter_filters => '过滤器';
+5 -4
View File
@@ -1343,6 +1343,7 @@
"listFilter_latestMessages": "Останні повідомлення",
"listFilter_heardRecently": "Нещодавно чули",
"listFilter_az": "А-Я",
"listFilter_usersFirst": "Спочатку користувачі",
"listFilter_filters": "Фільтри",
"listFilter_all": "Все",
"listFilter_users": "Користувачі",
@@ -1545,7 +1546,7 @@
"pathTrace_you": "Ви",
"pathTrace_failed": "Відстеження шляху не вдалося.",
"pathTrace_notAvailable": "Трасування шляху недоступне.",
"pathTrace_refreshTooltip": "Оновити Path Trace",
"pathTrace_refreshTooltip": "Оновити трасування шляху",
"contacts_pathTrace": "Трасування шляхів",
"contacts_ping": "Пінгувати",
"contacts_repeaterPathTrace": "Трасування шляху до повторювача",
@@ -1557,14 +1558,14 @@
"contacts_invalidAdvertFormat": "Недійсні контактні дані",
"contacts_contactImported": "Контакт було імпортовано.",
"contacts_contactImportFailed": "Контакт не вдалося імпортувати",
"contacts_zeroHopAdvert": "Реклама без перехоплення",
"contacts_floodAdvert": "Залив реклами",
"contacts_zeroHopAdvert": "Оголошення без ретрансляції",
"contacts_floodAdvert": "Оголошення з ретрансляцією",
"contacts_copyAdvertToClipboard": "Копіювати оголошення в буфер обміну",
"contacts_clipboardEmpty": "Буфер обміну порожній",
"appSettings_languageRu": "Російська",
"contacts_ShareContact": "Копіювати контакт у буфер обміну",
"contacts_zeroHopContactAdvertFailed": "Не вдалося надіслати контакт.",
"contacts_contactAdvertCopied": "Рекламу скопійовано до буфера обміну.",
"contacts_contactAdvertCopied": "Оголошення скопійовано до буфера обміну.",
"contacts_contactAdvertCopyFailed": "Копіювання оголошення в буфер обміну завершилося невдало",
"contacts_zeroHopContactAdvertSent": "Відправлено контакт за оголошенням",
"contacts_addContactFromClipboard": "Додати контакт з буфера обміну",
+2
View File
@@ -343,6 +343,8 @@ class _ChannelsScreenState extends State<ChannelsScreen>
selectedIndex: 1,
onDestinationSelected: (index) =>
_handleQuickSwitch(index, context),
contactsUnreadCount: connector.getTotalContactsUnreadCount(),
channelsUnreadCount: connector.getTotalChannelsUnreadCount(),
),
),
),
+32 -5
View File
@@ -48,6 +48,7 @@ class _ContactsScreenState extends State<ContactsScreen>
String _searchQuery = '';
ContactSortOption _sortOption = ContactSortOption.lastSeen;
bool _showUnreadOnly = false;
bool _prioritizePeople = true;
ContactTypeFilter _typeFilter = ContactTypeFilter.all;
final ContactGroupStore _groupStore = ContactGroupStore();
List<ContactGroup> _groups = [];
@@ -332,6 +333,8 @@ class _ContactsScreenState extends State<ContactsScreen>
selectedIndex: 0,
onDestinationSelected: (index) =>
_handleQuickSwitch(index, context),
contactsUnreadCount: connector.getTotalContactsUnreadCount(),
channelsUnreadCount: connector.getTotalChannelsUnreadCount(),
),
),
),
@@ -350,6 +353,7 @@ class _ContactsScreenState extends State<ContactsScreen>
sortOption: _sortOption,
typeFilter: _typeFilter,
showUnreadOnly: _showUnreadOnly,
prioritizePeople: _prioritizePeople,
onSortChanged: (value) {
setState(() {
_sortOption = value;
@@ -365,6 +369,11 @@ class _ContactsScreenState extends State<ContactsScreen>
_showUnreadOnly = value;
});
},
onPrioritizePeopleChanged: (value) {
setState(() {
_prioritizePeople = value;
});
},
onNewGroup: () => _showGroupEditor(context, connector.contacts),
);
}
@@ -545,14 +554,34 @@ class _ContactsScreenState extends State<ContactsScreen>
}).toList();
}
// Apply sorting within groups if prioritizing people
if (_prioritizePeople) {
// Separate people (advTypeChat) from others
final people = filtered.where((c) => c.type == advTypeChat).toList();
final others = filtered.where((c) => c.type != advTypeChat).toList();
// Sort each group separately
_applySorting(people, connector);
_applySorting(others, connector);
// Combine: people first, then others
filtered = [...people, ...others];
} else {
_applySorting(filtered, connector);
}
return filtered;
}
void _applySorting(List<Contact> contacts, MeshCoreConnector connector) {
switch (_sortOption) {
case ContactSortOption.lastSeen:
filtered.sort(
contacts.sort(
(a, b) => _resolveLastSeen(b).compareTo(_resolveLastSeen(a)),
);
break;
case ContactSortOption.recentMessages:
filtered.sort((a, b) {
contacts.sort((a, b) {
final aMessages = connector.getMessages(a);
final bMessages = connector.getMessages(b);
final aLastMsg = aMessages.isEmpty
@@ -565,13 +594,11 @@ class _ContactsScreenState extends State<ContactsScreen>
});
break;
case ContactSortOption.name:
filtered.sort(
contacts.sort(
(a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase()),
);
break;
}
return filtered;
}
bool _matchesTypeFilter(Contact contact) {
+4 -2
View File
@@ -71,7 +71,7 @@ class _DeviceScreenState extends State<DeviceScreen>
const SizedBox(height: 16),
_buildSectionLabel(theme, context.l10n.device_quickSwitch),
const SizedBox(height: 12),
_buildQuickSwitchBar(context),
_buildQuickSwitchBar(context, connector),
],
),
),
@@ -196,12 +196,14 @@ class _DeviceScreenState extends State<DeviceScreen>
);
}
Widget _buildQuickSwitchBar(BuildContext context) {
Widget _buildQuickSwitchBar(BuildContext context, MeshCoreConnector connector) {
return QuickSwitchBar(
selectedIndex: _quickIndex,
onDestinationSelected: (index) {
_openQuickDestination(index, context);
},
contactsUnreadCount: connector.getTotalContactsUnreadCount(),
channelsUnreadCount: connector.getTotalChannelsUnreadCount(),
);
}
+2
View File
@@ -362,6 +362,8 @@ class _MapScreenState extends State<MapScreen> {
selectedIndex: 2,
onDestinationSelected: (index) =>
_handleQuickSwitch(index, context),
contactsUnreadCount: connector.getTotalContactsUnreadCount(),
channelsUnreadCount: connector.getTotalChannelsUnreadCount(),
),
),
floatingActionButton: FloatingActionButton(
+13
View File
@@ -99,14 +99,17 @@ const int _actionFilterRepeaters = 6;
const int _actionFilterRooms = 7;
const int _actionToggleUnreadOnly = 8;
const int _actionNewGroup = 9;
const int _actionTogglePrioritizePeople = 10;
class ContactsFilterMenu extends StatelessWidget {
final ContactSortOption sortOption;
final ContactTypeFilter typeFilter;
final bool showUnreadOnly;
final bool prioritizePeople;
final ValueChanged<ContactSortOption> onSortChanged;
final ValueChanged<ContactTypeFilter> onTypeFilterChanged;
final ValueChanged<bool> onUnreadOnlyChanged;
final ValueChanged<bool> onPrioritizePeopleChanged;
final VoidCallback onNewGroup;
const ContactsFilterMenu({
@@ -114,9 +117,11 @@ class ContactsFilterMenu extends StatelessWidget {
required this.sortOption,
required this.typeFilter,
required this.showUnreadOnly,
required this.prioritizePeople,
required this.onSortChanged,
required this.onTypeFilterChanged,
required this.onUnreadOnlyChanged,
required this.onPrioritizePeopleChanged,
required this.onNewGroup,
});
@@ -144,6 +149,11 @@ class ContactsFilterMenu extends StatelessWidget {
label: l10n.listFilter_az,
checked: sortOption == ContactSortOption.name,
),
SortFilterMenuOption(
value: _actionTogglePrioritizePeople,
label: l10n.listFilter_usersFirst,
checked: prioritizePeople,
),
],
),
SortFilterMenuSection(
@@ -192,6 +202,9 @@ class ContactsFilterMenu extends StatelessWidget {
case _actionSortLastSeen:
onSortChanged(ContactSortOption.lastSeen);
break;
case _actionTogglePrioritizePeople:
onPrioritizePeopleChanged(!prioritizePeople);
break;
case _actionFilterAll:
onTypeFilterChanged(ContactTypeFilter.all);
break;
+33 -2
View File
@@ -6,11 +6,15 @@ import '../l10n/l10n.dart';
class QuickSwitchBar extends StatelessWidget {
final int selectedIndex;
final ValueChanged<int> onDestinationSelected;
final int contactsUnreadCount;
final int channelsUnreadCount;
const QuickSwitchBar({
super.key,
required this.selectedIndex,
required this.onDestinationSelected,
this.contactsUnreadCount = 0,
this.channelsUnreadCount = 0,
});
@override
@@ -62,15 +66,30 @@ class QuickSwitchBar extends StatelessWidget {
onDestinationSelected: onDestinationSelected,
destinations: [
NavigationDestination(
icon: const Icon(Icons.people_outline),
icon: _buildIconWithBadge(
const Icon(Icons.people_outline),
contactsUnreadCount,
),
selectedIcon: _buildIconWithBadge(
const Icon(Icons.people),
contactsUnreadCount,
),
label: context.l10n.nav_contacts,
),
NavigationDestination(
icon: const Icon(Icons.tag),
icon: _buildIconWithBadge(
const Icon(Icons.tag),
channelsUnreadCount,
),
selectedIcon: _buildIconWithBadge(
const Icon(Icons.tag),
channelsUnreadCount,
),
label: context.l10n.nav_channels,
),
NavigationDestination(
icon: const Icon(Icons.map_outlined),
selectedIcon: const Icon(Icons.map),
label: context.l10n.nav_map,
),
],
@@ -81,4 +100,16 @@ class QuickSwitchBar extends StatelessWidget {
),
);
}
Widget _buildIconWithBadge(Icon icon, int count) {
if (count <= 0) return icon;
return Badge(
label: Text(
count > 99 ? '99+' : count.toString(),
style: const TextStyle(fontSize: 10),
),
child: icon,
);
}
}
+53 -1
View File
@@ -1 +1,53 @@
{}
{
"bg": [
"listFilter_usersFirst"
],
"de": [
"listFilter_usersFirst"
],
"es": [
"listFilter_usersFirst"
],
"fr": [
"listFilter_usersFirst"
],
"it": [
"listFilter_usersFirst"
],
"nl": [
"listFilter_usersFirst"
],
"pl": [
"listFilter_usersFirst"
],
"pt": [
"listFilter_usersFirst"
],
"ru": [
"listFilter_usersFirst"
],
"sk": [
"listFilter_usersFirst"
],
"sl": [
"listFilter_usersFirst"
],
"sv": [
"listFilter_usersFirst"
],
"zh": [
"listFilter_usersFirst"
]
}