From 8668564464d577ba14958f45fc1031ddc4793c9b Mon Sep 17 00:00:00 2001 From: Serge Tarkovski Date: Mon, 9 Feb 2026 12:56:38 +0200 Subject: [PATCH 01/26] Correct unread badges for tabs; people first contacts sort option --- lib/connector/meshcore_connector.dart | 19 ++++++++-- lib/l10n/app_en.arb | 1 + lib/l10n/app_localizations.dart | 6 +++ lib/l10n/app_localizations_bg.dart | 3 ++ lib/l10n/app_localizations_de.dart | 3 ++ lib/l10n/app_localizations_en.dart | 3 ++ lib/l10n/app_localizations_es.dart | 3 ++ lib/l10n/app_localizations_fr.dart | 3 ++ lib/l10n/app_localizations_it.dart | 3 ++ lib/l10n/app_localizations_nl.dart | 3 ++ lib/l10n/app_localizations_pl.dart | 3 ++ lib/l10n/app_localizations_pt.dart | 3 ++ lib/l10n/app_localizations_ru.dart | 3 ++ lib/l10n/app_localizations_sk.dart | 3 ++ lib/l10n/app_localizations_sl.dart | 3 ++ lib/l10n/app_localizations_sv.dart | 3 ++ lib/l10n/app_localizations_uk.dart | 11 ++++-- lib/l10n/app_localizations_zh.dart | 3 ++ lib/l10n/app_uk.arb | 9 +++-- lib/screens/channels_screen.dart | 2 + lib/screens/contacts_screen.dart | 37 +++++++++++++++--- lib/screens/device_screen.dart | 6 ++- lib/screens/map_screen.dart | 2 + lib/widgets/list_filter_widget.dart | 13 +++++++ lib/widgets/quick_switch_bar.dart | 35 ++++++++++++++++- untranslated.json | 54 ++++++++++++++++++++++++++- 26 files changed, 215 insertions(+), 22 deletions(-) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index 2c56c373..b96aef98 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -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; } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index ee5cf7d6..c91345d7 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -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", diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 055667fa..33aa4032 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -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: diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index 701429e6..e373a156 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -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 => 'Филтри'; diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 514a7a19..779a5344 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -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'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 040d8090..29f83919 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -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'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index e65cbcd3..7f61ee25 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -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'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 4496fc84..d858934e 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -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'; diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index 02345c4e..9786262d 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -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'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index 292181fc..74f2abae 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -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'; diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index 08323295..13bb1bf4 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -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'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index eadea3bc..c555be3b 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -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'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index ec0f1ba3..6e3a9d7c 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -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 => 'Фильтры'; diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index 346047b0..65e41fcd 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -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'; diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index ed711224..c3c46aed 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -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'; diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index 97b849fa..f8652ae2 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -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'; diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index 899d540d..cfb88d58 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -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 => diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 77467924..d6dc2ff4 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -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 => '过滤器'; diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb index 3362d409..35edeee3 100644 --- a/lib/l10n/app_uk.arb +++ b/lib/l10n/app_uk.arb @@ -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": "Додати контакт з буфера обміну", diff --git a/lib/screens/channels_screen.dart b/lib/screens/channels_screen.dart index 6b8b92d4..6d8ef038 100644 --- a/lib/screens/channels_screen.dart +++ b/lib/screens/channels_screen.dart @@ -343,6 +343,8 @@ class _ChannelsScreenState extends State selectedIndex: 1, onDestinationSelected: (index) => _handleQuickSwitch(index, context), + contactsUnreadCount: connector.getTotalContactsUnreadCount(), + channelsUnreadCount: connector.getTotalChannelsUnreadCount(), ), ), ), diff --git a/lib/screens/contacts_screen.dart b/lib/screens/contacts_screen.dart index f04bc502..ac157356 100644 --- a/lib/screens/contacts_screen.dart +++ b/lib/screens/contacts_screen.dart @@ -48,6 +48,7 @@ class _ContactsScreenState extends State String _searchQuery = ''; ContactSortOption _sortOption = ContactSortOption.lastSeen; bool _showUnreadOnly = false; + bool _prioritizePeople = true; ContactTypeFilter _typeFilter = ContactTypeFilter.all; final ContactGroupStore _groupStore = ContactGroupStore(); List _groups = []; @@ -332,6 +333,8 @@ class _ContactsScreenState extends State selectedIndex: 0, onDestinationSelected: (index) => _handleQuickSwitch(index, context), + contactsUnreadCount: connector.getTotalContactsUnreadCount(), + channelsUnreadCount: connector.getTotalChannelsUnreadCount(), ), ), ), @@ -350,6 +353,7 @@ class _ContactsScreenState extends State sortOption: _sortOption, typeFilter: _typeFilter, showUnreadOnly: _showUnreadOnly, + prioritizePeople: _prioritizePeople, onSortChanged: (value) { setState(() { _sortOption = value; @@ -365,6 +369,11 @@ class _ContactsScreenState extends State _showUnreadOnly = value; }); }, + onPrioritizePeopleChanged: (value) { + setState(() { + _prioritizePeople = value; + }); + }, onNewGroup: () => _showGroupEditor(context, connector.contacts), ); } @@ -545,14 +554,34 @@ class _ContactsScreenState extends State }).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 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 }); break; case ContactSortOption.name: - filtered.sort( + contacts.sort( (a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase()), ); break; } - - return filtered; } bool _matchesTypeFilter(Contact contact) { diff --git a/lib/screens/device_screen.dart b/lib/screens/device_screen.dart index c5967cf4..c4a5310b 100644 --- a/lib/screens/device_screen.dart +++ b/lib/screens/device_screen.dart @@ -71,7 +71,7 @@ class _DeviceScreenState extends State 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 ); } - 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(), ); } diff --git a/lib/screens/map_screen.dart b/lib/screens/map_screen.dart index 0da99601..4253391f 100644 --- a/lib/screens/map_screen.dart +++ b/lib/screens/map_screen.dart @@ -362,6 +362,8 @@ class _MapScreenState extends State { selectedIndex: 2, onDestinationSelected: (index) => _handleQuickSwitch(index, context), + contactsUnreadCount: connector.getTotalContactsUnreadCount(), + channelsUnreadCount: connector.getTotalChannelsUnreadCount(), ), ), floatingActionButton: FloatingActionButton( diff --git a/lib/widgets/list_filter_widget.dart b/lib/widgets/list_filter_widget.dart index e9c0d9e8..6075fecb 100644 --- a/lib/widgets/list_filter_widget.dart +++ b/lib/widgets/list_filter_widget.dart @@ -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 onSortChanged; final ValueChanged onTypeFilterChanged; final ValueChanged onUnreadOnlyChanged; + final ValueChanged 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; diff --git a/lib/widgets/quick_switch_bar.dart b/lib/widgets/quick_switch_bar.dart index 134091ff..78874fdf 100644 --- a/lib/widgets/quick_switch_bar.dart +++ b/lib/widgets/quick_switch_bar.dart @@ -6,11 +6,15 @@ import '../l10n/l10n.dart'; class QuickSwitchBar extends StatelessWidget { final int selectedIndex; final ValueChanged 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, + ); + } } diff --git a/untranslated.json b/untranslated.json index 9e26dfee..29112d61 100644 --- a/untranslated.json +++ b/untranslated.json @@ -1 +1,53 @@ -{} \ No newline at end of file +{ + "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" + ] +} From fd305fd55bcaf0b562c21b2b8b9bc773bf07e319 Mon Sep 17 00:00:00 2001 From: Serge Tarkovski Date: Mon, 9 Feb 2026 13:19:31 +0200 Subject: [PATCH 02/26] Update generated plugin registrants after merge --- macos/Flutter/GeneratedPluginRegistrant.swift | 2 - pubspec.lock | 50 +------------------ .../flutter/generated_plugin_registrant.cc | 3 -- windows/flutter/generated_plugins.cmake | 1 - 4 files changed, 1 insertion(+), 55 deletions(-) diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index d2ea57e9..b4a41dd1 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -9,7 +9,6 @@ import flutter_blue_plus_darwin import flutter_local_notifications import mobile_scanner import package_info_plus -import share_plus import shared_preferences_foundation import sqflite_darwin import url_launcher_macos @@ -20,7 +19,6 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin")) MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin")) FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) - SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) diff --git a/pubspec.lock b/pubspec.lock index fc116566..1e275d4f 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -121,14 +121,6 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.2" - cross_file: - dependency: transitive - description: - name: cross_file - sha256: "28bb3ae56f117b5aec029d702a90f57d285cd975c3c5c281eaca38dbc47c5937" - url: "https://pub.dev" - source: hosted - version: "0.3.5+2" crypto: dependency: "direct main" description: @@ -349,14 +341,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.3" - gpx: - dependency: "direct main" - description: - name: gpx - sha256: f5b12b86402c639079243600ee2b3afd85cd08d26117fc8885cf48efce471d8e - url: "https://pub.dev" - source: hosted - version: "2.3.0" hooks: dependency: transitive description: @@ -517,14 +501,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0" - mime: - dependency: transitive - description: - name: mime - sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" - url: "https://pub.dev" - source: hosted - version: "2.0.0" mobile_scanner: dependency: "direct main" description: @@ -590,7 +566,7 @@ packages: source: hosted version: "1.9.1" path_provider: - dependency: "direct main" + dependency: transitive description: name: path_provider sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" @@ -725,14 +701,6 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.0" - quiver: - dependency: transitive - description: - name: quiver - sha256: ea0b925899e64ecdfbf9c7becb60d5b50e706ade44a85b2363be2a22d88117d2 - url: "https://pub.dev" - source: hosted - version: "3.2.2" rxdart: dependency: transitive description: @@ -741,22 +709,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.28.0" - share_plus: - dependency: "direct main" - description: - name: share_plus - sha256: "14c8860d4de93d3a7e53af51bff479598c4e999605290756bbbe45cf65b37840" - url: "https://pub.dev" - source: hosted - version: "12.0.1" - share_plus_platform_interface: - dependency: transitive - description: - name: share_plus_platform_interface - sha256: "88023e53a13429bd65d8e85e11a9b484f49d4c190abbd96c7932b74d6927cc9a" - url: "https://pub.dev" - source: hosted - version: "6.1.0" shared_preferences: dependency: "direct main" description: diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index cd4fc19b..eeb548fa 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -7,14 +7,11 @@ #include "generated_plugin_registrant.h" #include -#include #include void RegisterPlugins(flutter::PluginRegistry* registry) { FlutterBluePlusPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("FlutterBluePlusPlugin")); - SharePlusWindowsPluginCApiRegisterWithRegistrar( - registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi")); UrlLauncherWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("UrlLauncherWindows")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 571addb8..68825d8b 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -4,7 +4,6 @@ list(APPEND FLUTTER_PLUGIN_LIST flutter_blue_plus_winrt - share_plus url_launcher_windows ) From 2bce14224d9d756df8571054d48d657b543bc913 Mon Sep 17 00:00:00 2001 From: Serge Tarkovski Date: Mon, 9 Feb 2026 16:27:53 +0200 Subject: [PATCH 03/26] Update generated plugin registrants after merge --- macos/Flutter/GeneratedPluginRegistrant.swift | 2 + pubspec.lock | 50 ++++++++++++++++++- .../flutter/generated_plugin_registrant.cc | 3 ++ windows/flutter/generated_plugins.cmake | 1 + 4 files changed, 55 insertions(+), 1 deletion(-) diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index b4a41dd1..d2ea57e9 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -9,6 +9,7 @@ import flutter_blue_plus_darwin import flutter_local_notifications import mobile_scanner import package_info_plus +import share_plus import shared_preferences_foundation import sqflite_darwin import url_launcher_macos @@ -19,6 +20,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin")) MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin")) FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) + SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) diff --git a/pubspec.lock b/pubspec.lock index 1e275d4f..fc116566 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -121,6 +121,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.2" + cross_file: + dependency: transitive + description: + name: cross_file + sha256: "28bb3ae56f117b5aec029d702a90f57d285cd975c3c5c281eaca38dbc47c5937" + url: "https://pub.dev" + source: hosted + version: "0.3.5+2" crypto: dependency: "direct main" description: @@ -341,6 +349,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.3" + gpx: + dependency: "direct main" + description: + name: gpx + sha256: f5b12b86402c639079243600ee2b3afd85cd08d26117fc8885cf48efce471d8e + url: "https://pub.dev" + source: hosted + version: "2.3.0" hooks: dependency: transitive description: @@ -501,6 +517,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0" + mime: + dependency: transitive + description: + name: mime + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + url: "https://pub.dev" + source: hosted + version: "2.0.0" mobile_scanner: dependency: "direct main" description: @@ -566,7 +590,7 @@ packages: source: hosted version: "1.9.1" path_provider: - dependency: transitive + dependency: "direct main" description: name: path_provider sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" @@ -701,6 +725,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.0" + quiver: + dependency: transitive + description: + name: quiver + sha256: ea0b925899e64ecdfbf9c7becb60d5b50e706ade44a85b2363be2a22d88117d2 + url: "https://pub.dev" + source: hosted + version: "3.2.2" rxdart: dependency: transitive description: @@ -709,6 +741,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.28.0" + share_plus: + dependency: "direct main" + description: + name: share_plus + sha256: "14c8860d4de93d3a7e53af51bff479598c4e999605290756bbbe45cf65b37840" + url: "https://pub.dev" + source: hosted + version: "12.0.1" + share_plus_platform_interface: + dependency: transitive + description: + name: share_plus_platform_interface + sha256: "88023e53a13429bd65d8e85e11a9b484f49d4c190abbd96c7932b74d6927cc9a" + url: "https://pub.dev" + source: hosted + version: "6.1.0" shared_preferences: dependency: "direct main" description: diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index eeb548fa..cd4fc19b 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -7,11 +7,14 @@ #include "generated_plugin_registrant.h" #include +#include #include void RegisterPlugins(flutter::PluginRegistry* registry) { FlutterBluePlusPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("FlutterBluePlusPlugin")); + SharePlusWindowsPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi")); UrlLauncherWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("UrlLauncherWindows")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 68825d8b..571addb8 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -4,6 +4,7 @@ list(APPEND FLUTTER_PLUGIN_LIST flutter_blue_plus_winrt + share_plus url_launcher_windows ) From c4f5c7b1717265b430530dbee8f2782a9790cdb5 Mon Sep 17 00:00:00 2001 From: Serge Tarkovski Date: Mon, 9 Feb 2026 17:18:21 +0200 Subject: [PATCH 04/26] Cache for unread total --- lib/connector/meshcore_connector.dart | 37 +++++++++++++++++++-------- 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index b96aef98..9483a202 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -150,6 +150,8 @@ class MeshCoreConnector extends ChangeNotifier { final Set _knownContactKeys = {}; final Map _contactUnreadCount = {}; bool _unreadStateLoaded = false; + int _cachedContactsUnreadTotal = 0; + int _cachedChannelsUnreadTotal = 0; final Map _pendingRepeaterAcks = {}; String? _activeContactKey; int? _activeChannelIndex; @@ -342,22 +344,28 @@ class MeshCoreConnector extends ChangeNotifier { int getTotalContactsUnreadCount() { if (!_unreadStateLoaded) return 0; - var total = 0; - for (final contact in _contacts) { - total += getUnreadCountForContact(contact); - } - return total; + return _cachedContactsUnreadTotal; } int getTotalChannelsUnreadCount() { if (!_unreadStateLoaded) return 0; - var total = 0; - // Check all channels (from _channels or _cachedChannels) + return _cachedChannelsUnreadTotal; + } + + /// Recalculates both cached unread totals from scratch. + /// Called when unread state is first loaded. + void _recalculateCachedUnreadTotals() { + _recalculateCachedContactsUnreadTotal(); + _recalculateCachedChannelsUnreadTotal(); + } + + void _recalculateCachedContactsUnreadTotal() { + _cachedContactsUnreadTotal = _contactUnreadCount.values.fold(0, (a, b) => a + b); + } + + void _recalculateCachedChannelsUnreadTotal() { final allChannels = _channels.isNotEmpty ? _channels : _cachedChannels; - for (final channel in allChannels) { - total += channel.unreadCount; - } - return total; + _cachedChannelsUnreadTotal = allChannels.fold(0, (total, ch) => total + ch.unreadCount); } bool isChannelSmazEnabled(int channelIndex) { @@ -377,11 +385,13 @@ class MeshCoreConnector extends ChangeNotifier { ..clear() ..addAll(await _unreadStore.loadContactUnreadCount()); _unreadStateLoaded = true; + _recalculateCachedUnreadTotals(); notifyListeners(); } Future loadCachedChannels() async { _cachedChannels = await _channelStore.loadChannels(); + _recalculateCachedChannelsUnreadTotal(); } void setActiveContact(String? contactKeyHex) { @@ -408,6 +418,7 @@ class MeshCoreConnector extends ChangeNotifier { final previousCount = _contactUnreadCount[contactKeyHex] ?? 0; if (previousCount > 0) { _contactUnreadCount[contactKeyHex] = 0; + _cachedContactsUnreadTotal -= previousCount; _appDebugLogService?.info( 'Contact $contactKeyHex marked as read (was $previousCount unread)', tag: 'Unread', @@ -424,6 +435,7 @@ class MeshCoreConnector extends ChangeNotifier { if (channel != null && channel.unreadCount > 0) { final previousCount = channel.unreadCount; channel.unreadCount = 0; + _cachedChannelsUnreadTotal -= previousCount; _appDebugLogService?.info( 'Channel ${channel.name.isNotEmpty ? channel.name : channelIndex} marked as read (was $previousCount unread)', tag: 'Unread', @@ -1656,6 +1668,7 @@ class MeshCoreConnector extends ChangeNotifier { if (completed) { _hasLoadedChannels = true; _previousChannelsCache.clear(); + _recalculateCachedChannelsUnreadTotal(); } // Keep cache on failure/disconnection for future attempts } @@ -2703,6 +2716,7 @@ class MeshCoreConnector extends ChangeNotifier { final channel = _findChannelByIndex(channelIndex); if (channel != null) { channel.unreadCount++; + _cachedChannelsUnreadTotal++; _appDebugLogService?.info( 'Channel ${channel.name.isNotEmpty ? channel.name : channelIndex} unread count incremented to ${channel.unreadCount}', tag: 'Unread', @@ -2747,6 +2761,7 @@ class MeshCoreConnector extends ChangeNotifier { final currentCount = _contactUnreadCount[contactKey] ?? 0; _contactUnreadCount[contactKey] = currentCount + 1; + _cachedContactsUnreadTotal++; _appDebugLogService?.info( 'Contact $contactKey unread count incremented to ${currentCount + 1}', tag: 'Unread', From 68bb031bb68c0bd2cf25d7ce85263e243e464bcd Mon Sep 17 00:00:00 2001 From: Serge Tarkovski Date: Mon, 9 Feb 2026 17:34:18 +0200 Subject: [PATCH 05/26] "Users first" instead of "People first" everywhere --- lib/screens/contacts_screen.dart | 23 ++++++++++++----------- lib/widgets/list_filter_widget.dart | 18 +++++++++--------- 2 files changed, 21 insertions(+), 20 deletions(-) diff --git a/lib/screens/contacts_screen.dart b/lib/screens/contacts_screen.dart index 394970f9..97e0d513 100644 --- a/lib/screens/contacts_screen.dart +++ b/lib/screens/contacts_screen.dart @@ -48,7 +48,7 @@ class _ContactsScreenState extends State String _searchQuery = ''; ContactSortOption _sortOption = ContactSortOption.lastSeen; bool _showUnreadOnly = false; - bool _prioritizePeople = true; + bool _prioritizeUsers = true; ContactTypeFilter _typeFilter = ContactTypeFilter.all; final ContactGroupStore _groupStore = ContactGroupStore(); List _groups = []; @@ -353,7 +353,7 @@ class _ContactsScreenState extends State sortOption: _sortOption, typeFilter: _typeFilter, showUnreadOnly: _showUnreadOnly, - prioritizePeople: _prioritizePeople, + prioritizeUsers: _prioritizeUsers, onSortChanged: (value) { setState(() { _sortOption = value; @@ -369,9 +369,9 @@ class _ContactsScreenState extends State _showUnreadOnly = value; }); }, - onPrioritizePeopleChanged: (value) { + onPrioritizeUsersChanged: (value) { setState(() { - _prioritizePeople = value; + _prioritizeUsers = value; }); }, onNewGroup: () => _showGroupEditor(context, connector.contacts), @@ -554,18 +554,19 @@ class _ContactsScreenState extends State }).toList(); } - // Apply sorting within groups if prioritizing people - if (_prioritizePeople) { - // Separate people (advTypeChat) from others - final people = filtered.where((c) => c.type == advTypeChat).toList(); + // Apply "users first" partitioning: separate users from other node types, + // sort each partition, then combine with users on top + if (_prioritizeUsers) { + // Separate users (advTypeChat) from others + final users = filtered.where((c) => c.type == advTypeChat).toList(); final others = filtered.where((c) => c.type != advTypeChat).toList(); // Sort each group separately - _applySorting(people, connector); + _applySorting(users, connector); _applySorting(others, connector); - // Combine: people first, then others - filtered = [...people, ...others]; + // Combine: users first, then others + filtered = [...users, ...others]; } else { _applySorting(filtered, connector); } diff --git a/lib/widgets/list_filter_widget.dart b/lib/widgets/list_filter_widget.dart index 6075fecb..4b233708 100644 --- a/lib/widgets/list_filter_widget.dart +++ b/lib/widgets/list_filter_widget.dart @@ -99,17 +99,17 @@ const int _actionFilterRepeaters = 6; const int _actionFilterRooms = 7; const int _actionToggleUnreadOnly = 8; const int _actionNewGroup = 9; -const int _actionTogglePrioritizePeople = 10; +const int _actionTogglePrioritizeUsers = 10; class ContactsFilterMenu extends StatelessWidget { final ContactSortOption sortOption; final ContactTypeFilter typeFilter; final bool showUnreadOnly; - final bool prioritizePeople; + final bool prioritizeUsers; final ValueChanged onSortChanged; final ValueChanged onTypeFilterChanged; final ValueChanged onUnreadOnlyChanged; - final ValueChanged onPrioritizePeopleChanged; + final ValueChanged onPrioritizeUsersChanged; final VoidCallback onNewGroup; const ContactsFilterMenu({ @@ -117,11 +117,11 @@ class ContactsFilterMenu extends StatelessWidget { required this.sortOption, required this.typeFilter, required this.showUnreadOnly, - required this.prioritizePeople, + required this.prioritizeUsers, required this.onSortChanged, required this.onTypeFilterChanged, required this.onUnreadOnlyChanged, - required this.onPrioritizePeopleChanged, + required this.onPrioritizeUsersChanged, required this.onNewGroup, }); @@ -150,9 +150,9 @@ class ContactsFilterMenu extends StatelessWidget { checked: sortOption == ContactSortOption.name, ), SortFilterMenuOption( - value: _actionTogglePrioritizePeople, + value: _actionTogglePrioritizeUsers, label: l10n.listFilter_usersFirst, - checked: prioritizePeople, + checked: prioritizeUsers, ), ], ), @@ -202,8 +202,8 @@ class ContactsFilterMenu extends StatelessWidget { case _actionSortLastSeen: onSortChanged(ContactSortOption.lastSeen); break; - case _actionTogglePrioritizePeople: - onPrioritizePeopleChanged(!prioritizePeople); + case _actionTogglePrioritizeUsers: + onPrioritizeUsersChanged(!prioritizeUsers); break; case _actionFilterAll: onTypeFilterChanged(ContactTypeFilter.all); From 87bcb6a6a37c1fc6f7b7a653b06432103386c358 Mon Sep 17 00:00:00 2001 From: Serge Tarkovski Date: Mon, 9 Feb 2026 17:40:56 +0200 Subject: [PATCH 06/26] Proper formatting --- lib/widgets/quick_switch_bar.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/widgets/quick_switch_bar.dart b/lib/widgets/quick_switch_bar.dart index 78874fdf..bbac8d3b 100644 --- a/lib/widgets/quick_switch_bar.dart +++ b/lib/widgets/quick_switch_bar.dart @@ -103,7 +103,7 @@ class QuickSwitchBar extends StatelessWidget { Widget _buildIconWithBadge(Icon icon, int count) { if (count <= 0) return icon; - + return Badge( label: Text( count > 99 ? '99+' : count.toString(), From afcc4db4054a5d4886b003dfafb7909ba4757e05 Mon Sep 17 00:00:00 2001 From: Serge Tarkovski Date: Wed, 18 Feb 2026 20:15:29 +0200 Subject: [PATCH 07/26] fix: clamp cached unread totals to prevent negative badge counts Clamp both _cachedContactsUnreadTotal and _cachedChannelsUnreadTotal to >= 0 after decrementing in markContactRead() and markChannelRead(). This prevents the totals from going negative if the cache drifts out-of-sync, which could cause UI badges to display incorrect values. --- lib/connector/meshcore_connector.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index 9483a202..a426df34 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -418,7 +418,7 @@ class MeshCoreConnector extends ChangeNotifier { final previousCount = _contactUnreadCount[contactKeyHex] ?? 0; if (previousCount > 0) { _contactUnreadCount[contactKeyHex] = 0; - _cachedContactsUnreadTotal -= previousCount; + _cachedContactsUnreadTotal = (_cachedContactsUnreadTotal - previousCount).clamp(0, _cachedContactsUnreadTotal); _appDebugLogService?.info( 'Contact $contactKeyHex marked as read (was $previousCount unread)', tag: 'Unread', @@ -435,7 +435,7 @@ class MeshCoreConnector extends ChangeNotifier { if (channel != null && channel.unreadCount > 0) { final previousCount = channel.unreadCount; channel.unreadCount = 0; - _cachedChannelsUnreadTotal -= previousCount; + _cachedChannelsUnreadTotal = (_cachedChannelsUnreadTotal - previousCount).clamp(0, _cachedChannelsUnreadTotal); _appDebugLogService?.info( 'Channel ${channel.name.isNotEmpty ? channel.name : channelIndex} marked as read (was $previousCount unread)', tag: 'Unread', From a30fc439f3b5e7553c3cb41dda0e2bf96de84200 Mon Sep 17 00:00:00 2001 From: Serge Tarkovski Date: Fri, 27 Feb 2026 12:22:26 +0200 Subject: [PATCH 08/26] refactor: use UnreadBadge widget in QuickSwitchBar for consistent badge styling --- lib/widgets/quick_switch_bar.dart | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/lib/widgets/quick_switch_bar.dart b/lib/widgets/quick_switch_bar.dart index bbac8d3b..5a157a02 100644 --- a/lib/widgets/quick_switch_bar.dart +++ b/lib/widgets/quick_switch_bar.dart @@ -2,6 +2,7 @@ import 'dart:ui'; import 'package:flutter/material.dart'; import '../l10n/l10n.dart'; +import 'unread_badge.dart'; class QuickSwitchBar extends StatelessWidget { final int selectedIndex; @@ -104,12 +105,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, + return Stack( + clipBehavior: Clip.none, + children: [ + icon, + Positioned( + right: -6, + top: -4, + child: UnreadBadge(count: count), + ), + ], ); } } From c47a4cb622b715fe1a506848f01291a3e442237f Mon Sep 17 00:00:00 2001 From: Serge Tarkovski Date: Fri, 27 Feb 2026 12:28:57 +0200 Subject: [PATCH 09/26] fix: filter by _shouldTrackUnreadForContactKey when recalculating cached contacts unread total --- lib/connector/meshcore_connector.dart | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index a426df34..34ab744d 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -360,7 +360,13 @@ class MeshCoreConnector extends ChangeNotifier { } void _recalculateCachedContactsUnreadTotal() { - _cachedContactsUnreadTotal = _contactUnreadCount.values.fold(0, (a, b) => a + b); + int total = 0; + _contactUnreadCount.forEach((contactKeyHex, count) { + if (_shouldTrackUnreadForContactKey(contactKeyHex)) { + total += count; + } + }); + _cachedContactsUnreadTotal = total; } void _recalculateCachedChannelsUnreadTotal() { From 87b25655d02b65a520135a5aac52f3ebe41a7f2a Mon Sep 17 00:00:00 2001 From: Serge Tarkovski Date: Fri, 27 Feb 2026 12:43:21 +0200 Subject: [PATCH 10/26] Package updates from main --- pubspec.lock | 210 +++++++++++++++--------- windows/flutter/generated_plugins.cmake | 1 + 2 files changed, 130 insertions(+), 81 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index fc116566..2e3329c4 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,10 +5,10 @@ packages: dependency: transitive description: name: archive - sha256: "2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd" + sha256: a96e8b390886ee8abb49b7bd3ac8df6f451c621619f52a26e815fdcf568959ff url: "https://pub.dev" source: hosted - version: "4.0.7" + version: "4.0.9" args: dependency: transitive description: @@ -153,14 +153,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.0" + dart_polylabel2: + dependency: transitive + description: + name: dart_polylabel2 + sha256: "7eeab15ce72894e4bdba6a8765712231fc81be0bd95247de4ad9966abc57adc6" + url: "https://pub.dev" + source: hosted + version: "1.0.0" dbus: dependency: transitive description: name: dbus - sha256: "79e0c23480ff85dc68de79e2cd6334add97e48f7f4865d17686dd6ea81a47e8c" + sha256: d0c98dcd4f5169878b6cf8f6e0a52403a9dff371a3e2f019697accbf6f44a270 url: "https://pub.dev" source: hosted - version: "0.7.11" + version: "0.7.12" fake_async: dependency: transitive description: @@ -173,10 +181,10 @@ packages: dependency: transitive description: name: ffi - sha256: d07d37192dbf97461359c1518788f203b0c9102cfd2c35a716b823741219542c + sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45" url: "https://pub.dev" source: hosted - version: "2.1.5" + version: "2.2.0" file: dependency: transitive description: @@ -202,58 +210,58 @@ packages: dependency: "direct main" description: name: flutter_blue_plus - sha256: "399b3dbc15562ef59749f04e43a99ccbb91540022380d5f269aff3c2787534e4" + sha256: "4fba86c513feab2c5cdb9497da0910ed5b50c0fa8d6cec4a26ffb1a558a24eb8" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.2.1" flutter_blue_plus_android: dependency: transitive description: name: flutter_blue_plus_android - sha256: "5010b0960cce533a8fa71401573f044362c3e2e111dc6eb4898c92e85f85f50c" + sha256: "2a73e264685574d1d29dcdd565bad9ecfdf237630237c508ae8b47f5cc791f1d" url: "https://pub.dev" source: hosted - version: "8.1.0" + version: "8.2.1" flutter_blue_plus_darwin: dependency: transitive description: name: flutter_blue_plus_darwin - sha256: d160a8128e3a016fa58dd65ab6dac05cbc73e0fa799a1f24211d041641ed63ba + sha256: cfef171db550670cf8110f6eb25baf15d9bc8bad2af29550f9bbc0d8fceaf285 url: "https://pub.dev" source: hosted - version: "8.1.0" + version: "8.2.1" flutter_blue_plus_linux: dependency: transitive description: name: flutter_blue_plus_linux - sha256: f5b02244d89465ba82c8c512686c66362fbb01f52fa03d645ed353ebf3883242 + sha256: "5add6c14d2f90672c5e3ded1455b9ca8e6fe44adf9b53cdc60eb3417d38f34fe" url: "https://pub.dev" source: hosted - version: "8.1.0" + version: "8.2.1" flutter_blue_plus_platform_interface: dependency: transitive description: name: flutter_blue_plus_platform_interface - sha256: "6e0fc04b77491dbfdbcd46c1a021b12f2f5fc5d6e01777f93a38a8431989b7f0" + sha256: "226fb6753a74a407e3b9975c0fc00de02c490ae655b31c6508cb5790ad30965d" url: "https://pub.dev" source: hosted - version: "8.1.0" + version: "8.2.1" flutter_blue_plus_web: dependency: transitive description: name: flutter_blue_plus_web - sha256: "376aad9595ee389c7cd56e0c373e78abcaa790c821ece9cb81f0969ec94c5bca" + sha256: "10a7465ccfc50138280abf32c8ab314f5029aa19039628ad9b4d0ed786e0021f" url: "https://pub.dev" source: hosted - version: "8.1.0" + version: "8.2.1" flutter_blue_plus_winrt: dependency: transitive description: name: flutter_blue_plus_winrt - sha256: "34be2d8e23d5881b46accebb0e71025f7d52869d72ea98b5082c20764e06aa80" + sha256: ed894f0ab341f4cece8fa33edc381d46424a7c5bfd0e841d933d0f8c34c86521 url: "https://pub.dev" source: hosted - version: "0.0.16" + version: "0.0.18" flutter_cache_manager: dependency: "direct main" description: @@ -266,18 +274,18 @@ packages: dependency: "direct main" description: name: flutter_foreground_task - sha256: "6cf10a27f5e344cd2ecad0752d3a5f4ec32846d82fda8753b3fe2480ebb832a3" + sha256: "48ea45056155a99fb30b15f14f4039a044d925bc85f381ed0b2d3b00a60b99de" url: "https://pub.dev" source: hosted - version: "6.5.0" + version: "9.2.0" flutter_launcher_icons: dependency: "direct dev" description: name: flutter_launcher_icons - sha256: "526faf84284b86a4cb36d20a5e45147747b7563d921373d4ee0559c54fcdbcea" + sha256: "10f13781741a2e3972126fae08393d3c4e01fa4cd7473326b94b72cf594195e7" url: "https://pub.dev" source: hosted - version: "0.13.1" + version: "0.14.4" flutter_linkify: dependency: "direct main" description: @@ -290,34 +298,42 @@ packages: dependency: "direct dev" description: name: flutter_lints - sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1" + sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1" url: "https://pub.dev" source: hosted - version: "5.0.0" + version: "6.0.0" flutter_local_notifications: dependency: "direct main" description: name: flutter_local_notifications - sha256: ef41ae901e7529e52934feba19ed82827b11baa67336829564aeab3129460610 + sha256: "2b50e938a275e1ad77352d6a25e25770f4130baa61eaf02de7a9a884680954ad" url: "https://pub.dev" source: hosted - version: "18.0.1" + version: "20.1.0" flutter_local_notifications_linux: dependency: transitive description: name: flutter_local_notifications_linux - sha256: "8f685642876742c941b29c32030f6f4f6dacd0e4eaecb3efbb187d6a3812ca01" + sha256: dce0116868cedd2cdf768af0365fc37ff1cbef7c02c4f51d0587482e625868d0 url: "https://pub.dev" source: hosted - version: "5.0.0" + version: "7.0.0" flutter_local_notifications_platform_interface: dependency: transitive description: name: flutter_local_notifications_platform_interface - sha256: "6c5b83c86bf819cdb177a9247a3722067dd8cc6313827ce7c77a4b238a26fd52" + sha256: "23de31678a48c084169d7ae95866df9de5c9d2a44be3e5915a2ff067aeeba899" url: "https://pub.dev" source: hosted - version: "8.0.0" + version: "10.0.0" + flutter_local_notifications_windows: + dependency: transitive + description: + name: flutter_local_notifications_windows + sha256: e97a1a3016512437d9c0b12fae7d1491c3c7b9aa7f03a69b974308840656b02a + url: "https://pub.dev" + source: hosted + version: "2.0.1" flutter_localizations: dependency: "direct main" description: flutter @@ -327,10 +343,18 @@ packages: dependency: "direct main" description: name: flutter_map - sha256: "2ecb34619a4be19df6f40c2f8dce1591675b4eff7a6857bd8f533706977385da" + sha256: "391e7dc95cc3f5190748210a69d4cfeb5d8f84dcdfa9c3235d0a9d7742ccb3f8" url: "https://pub.dev" source: hosted - version: "7.0.2" + version: "8.2.2" + flutter_svg: + dependency: "direct main" + description: + name: flutter_svg + sha256: "87fbd7c534435b6c5d9d98b01e1fd527812b82e68ddd8bd35fc45ed0fa8f0a95" + url: "https://pub.dev" + source: hosted + version: "2.2.3" flutter_test: dependency: "direct dev" description: flutter @@ -361,10 +385,10 @@ packages: dependency: transitive description: name: hooks - sha256: "5d309c86e7ce34cd8e37aa71cb30cb652d3829b900ab145e4d9da564b31d59f7" + sha256: "7a08a0d684cb3b8fb604b78455d5d352f502b68079f7b80b831c62220ab0a4f6" url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.0.1" http: dependency: "direct main" description: @@ -385,10 +409,10 @@ packages: dependency: transitive description: name: image - sha256: "492bd52f6c4fbb6ee41f781ff27765ce5f627910e1e0cbecfa3d9add5562604c" + sha256: f9881ff4998044947ec38d098bc7c8316ae1186fa786eddffdb867b9bc94dfce url: "https://pub.dev" source: hosted - version: "4.7.2" + version: "4.8.0" intl: dependency: "direct main" description: @@ -397,22 +421,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.20.2" - js: - dependency: transitive - description: - name: js - sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc" - url: "https://pub.dev" - source: hosted - version: "0.7.2" json_annotation: dependency: transitive description: name: json_annotation - sha256: "805fa86df56383000f640384b282ce0cb8431f1a7a2396de92fb66186d8c57df" + sha256: cb09e7dac6210041fad964ed7fbee004f14258b4eca4040f72d1234062ace4c8 url: "https://pub.dev" source: hosted - version: "4.10.0" + version: "4.11.0" latlong2: dependency: "direct main" description: @@ -457,10 +473,10 @@ packages: dependency: transitive description: name: lints - sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7 + sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df" url: "https://pub.dev" source: hosted - version: "5.1.1" + version: "6.1.0" lists: dependency: transitive description: @@ -501,6 +517,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.11.1" + material_symbols_icons: + dependency: "direct main" + description: + name: material_symbols_icons + sha256: c62b15f2b3de98d72cbff0148812f5ef5159f05e61fc9f9a089ec2bb234df082 + url: "https://pub.dev" + source: hosted + version: "4.2906.0" meta: dependency: transitive description: @@ -529,10 +553,10 @@ packages: dependency: "direct main" description: name: mobile_scanner - sha256: "0b466a0a8a211b366c2e87f3345715faef9b6011c7147556ad22f37de6ba3173" + sha256: c92c26bf2231695b6d3477c8dcf435f51e28f87b1745966b1fe4c47a286171ce url: "https://pub.dev" source: hosted - version: "6.0.11" + version: "7.2.0" native_toolchain_c: dependency: transitive description: @@ -553,10 +577,10 @@ packages: dependency: transitive description: name: objective_c - sha256: "983c7fa1501f6dcc0cb7af4e42072e9993cb28d73604d25ebf4dab08165d997e" + sha256: "100a1c87616ab6ed41ec263b083c0ef3261ee6cd1dc3b0f35f8ddfa4f996fe52" url: "https://pub.dev" source: hosted - version: "9.2.5" + version: "9.3.0" octo_image: dependency: transitive description: @@ -569,10 +593,10 @@ packages: dependency: "direct main" description: name: package_info_plus - sha256: "16eee997588c60225bda0488b6dcfac69280a6b7a3cf02c741895dd370a02968" + sha256: f69da0d3189a4b4ceaeb1a3defb0f329b3b352517f52bed4290f83d4f06bc08d url: "https://pub.dev" source: hosted - version: "8.3.1" + version: "9.0.0" package_info_plus_platform_interface: dependency: transitive description: @@ -589,6 +613,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.1" + path_parsing: + dependency: transitive + description: + name: path_parsing + sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca" + url: "https://pub.dev" + source: hosted + version: "1.1.0" path_provider: dependency: "direct main" description: @@ -641,10 +673,10 @@ packages: dependency: transitive description: name: petitparser - sha256: "1a97266a94f7350d30ae522c0af07890c70b8e62c71e8e3920d1db4d23c057d1" + sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675" url: "https://pub.dev" source: hosted - version: "7.0.1" + version: "7.0.2" platform: dependency: transitive description: @@ -665,26 +697,18 @@ packages: dependency: "direct main" description: name: pointycastle - sha256: "4be0097fcf3fd3e8449e53730c631200ebc7b88016acecab2b0da2f0149222fe" + sha256: "92aa3841d083cc4b0f4709b5c74fd6409a3e6ba833ffc7dc6a8fee096366acf5" url: "https://pub.dev" source: hosted - version: "3.9.1" - polylabel: - dependency: transitive - description: - name: polylabel - sha256: "41b9099afb2aa6c1730bdd8a0fab1400d287694ec7615dd8516935fa3144214b" - url: "https://pub.dev" - source: hosted - version: "1.0.1" + version: "4.0.0" posix: dependency: transitive description: name: posix - sha256: "6323a5b0fa688b6a010df4905a56b00181479e6d10534cecfecede2aa55add61" + sha256: "185ef7606574f789b40f289c233efa52e96dead518aed988e040a10737febb07" url: "https://pub.dev" source: hosted - version: "6.0.3" + version: "6.5.0" proj4dart: dependency: transitive description: @@ -769,10 +793,10 @@ packages: dependency: transitive description: name: shared_preferences_android - sha256: cbc40be9be1c5af4dab4d6e0de4d5d3729e6f3d65b89d21e1815d57705644a6f + sha256: "8374d6200ab33ac99031a852eba4c8eb2170c4bf20778b3e2c9eccb45384fb41" url: "https://pub.dev" source: hosted - version: "2.4.20" + version: "2.4.21" shared_preferences_foundation: dependency: transitive description: @@ -822,10 +846,10 @@ packages: dependency: transitive description: name: source_span - sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" + sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" url: "https://pub.dev" source: hosted - version: "1.10.1" + version: "1.10.2" sqflite: dependency: transitive description: @@ -958,10 +982,10 @@ packages: dependency: transitive description: name: url_launcher_ios - sha256: cfde38aa257dae62ffe79c87fab20165dfdf6988c1d31b58ebf59b9106062aad + sha256: "580fe5dfb51671ae38191d316e027f6b76272b026370708c2d898799750a02b0" url: "https://pub.dev" source: hosted - version: "6.3.6" + version: "6.4.1" url_launcher_linux: dependency: transitive description: @@ -1006,10 +1030,34 @@ packages: dependency: "direct main" description: name: uuid - sha256: a11b666489b1954e01d992f3d601b1804a33937b5a8fe677bd26b8a9f96f96e8 + sha256: "1fef9e8e11e2991bb773070d4656b7bd5d850967a2456cfc83cf47925ba79489" url: "https://pub.dev" source: hosted - version: "4.5.2" + version: "4.5.3" + vector_graphics: + dependency: transitive + description: + name: vector_graphics + sha256: a4f059dc26fc8295b5921376600a194c4ec7d55e72f2fe4c7d2831e103d461e6 + url: "https://pub.dev" + source: hosted + version: "1.1.19" + vector_graphics_codec: + dependency: transitive + description: + name: vector_graphics_codec + sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146" + url: "https://pub.dev" + source: hosted + version: "1.1.13" + vector_graphics_compiler: + dependency: transitive + description: + name: vector_graphics_compiler + sha256: "5a88dd14c0954a5398af544651c7fb51b457a2a556949bfb25369b210ef73a74" + url: "https://pub.dev" + source: hosted + version: "1.2.0" vector_math: dependency: transitive description: @@ -1030,10 +1078,10 @@ packages: dependency: "direct main" description: name: wakelock_plus - sha256: "61713aa82b7f85c21c9f4cd0a148abd75f38a74ec645fcb1e446f882c82fd09b" + sha256: "9296d40c9adbedaba95d1e704f4e0b434be446e2792948d0e4aa977048104228" url: "https://pub.dev" source: hosted - version: "1.3.3" + version: "1.4.0" wakelock_plus_platform_interface: dependency: transitive description: @@ -1043,7 +1091,7 @@ packages: source: hosted version: "1.3.0" web: - dependency: transitive + dependency: "direct main" description: name: web sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 571addb8..4c358e7f 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -9,6 +9,7 @@ list(APPEND FLUTTER_PLUGIN_LIST ) list(APPEND FLUTTER_FFI_PLUGIN_LIST + flutter_local_notifications_windows ) set(PLUGIN_BUNDLED_LIBRARIES) From 3ae14781f0c07650a0d6bfa84830cc1cec941ccf Mon Sep 17 00:00:00 2001 From: Serge Tarkovski Date: Fri, 27 Feb 2026 12:58:32 +0200 Subject: [PATCH 11/26] AI translations for "Users first" --- lib/l10n/app_bg.arb | 1 + lib/l10n/app_de.arb | 1 + lib/l10n/app_es.arb | 1 + lib/l10n/app_fr.arb | 1 + lib/l10n/app_it.arb | 1 + lib/l10n/app_localizations_bg.dart | 2 +- lib/l10n/app_localizations_de.dart | 2 +- lib/l10n/app_localizations_es.dart | 2 +- lib/l10n/app_localizations_fr.dart | 2 +- lib/l10n/app_localizations_it.dart | 2 +- lib/l10n/app_localizations_nl.dart | 2 +- lib/l10n/app_localizations_pl.dart | 2 +- lib/l10n/app_localizations_pt.dart | 2 +- lib/l10n/app_localizations_ru.dart | 2 +- lib/l10n/app_localizations_sk.dart | 2 +- lib/l10n/app_localizations_sl.dart | 2 +- lib/l10n/app_localizations_sv.dart | 2 +- lib/l10n/app_nl.arb | 1 + lib/l10n/app_pl.arb | 1 + lib/l10n/app_pt.arb | 1 + lib/l10n/app_ru.arb | 1 + lib/l10n/app_sk.arb | 1 + lib/l10n/app_sl.arb | 1 + lib/l10n/app_sv.arb | 1 + lib/l10n/app_zh.arb | 1 + 25 files changed, 25 insertions(+), 12 deletions(-) diff --git a/lib/l10n/app_bg.arb b/lib/l10n/app_bg.arb index 975e0671..daa2b11b 100644 --- a/lib/l10n/app_bg.arb +++ b/lib/l10n/app_bg.arb @@ -1352,6 +1352,7 @@ "listFilter_users": "Потребители", "listFilter_repeaters": "Повторители", "listFilter_roomServers": "Сървъри на стая", + "listFilter_usersFirst": "Първо потребители", "listFilter_unreadOnly": "Само непрочетените", "listFilter_newGroup": "Нова група", "@neighbors_errorLoading": { diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 74b6c05a..f39c3420 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -1355,6 +1355,7 @@ "listFilter_users": "Benutzer", "listFilter_repeaters": "Repeater", "listFilter_roomServers": "Raumserver", + "listFilter_usersFirst": "Benutzer zuerst", "listFilter_unreadOnly": "Nicht gelesen", "listFilter_newGroup": "Neue Gruppe", "@neighbors_errorLoading": { diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 74339ff7..8824ddb1 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -1352,6 +1352,7 @@ "listFilter_users": "Usuarios", "listFilter_repeaters": "Repetidores", "listFilter_roomServers": "Servidores de la sala", + "listFilter_usersFirst": "Usuarios primero", "listFilter_unreadOnly": "Solo sin leer", "listFilter_newGroup": "Nuevo grupo", "@neighbors_errorLoading": { diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 0697aee7..9b1c79fb 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -1352,6 +1352,7 @@ "listFilter_users": "Utilisateurs", "listFilter_repeaters": "Répéteurs", "listFilter_roomServers": "Room servers", + "listFilter_usersFirst": "Utilisateurs en premier", "listFilter_unreadOnly": "Messages non lus seulement", "listFilter_newGroup": "Nouveau groupe", "@neighbors_errorLoading": { diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index 4798d263..16a1e770 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -1352,6 +1352,7 @@ "listFilter_users": "Utenti", "listFilter_repeaters": "Ripetitori", "listFilter_roomServers": "Server della stanza", + "listFilter_usersFirst": "Utenti per primi", "listFilter_unreadOnly": "Solo non letto", "listFilter_newGroup": "Nuovo gruppo", "@neighbors_errorLoading": { diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index 123d01c2..0221945c 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -2751,7 +2751,7 @@ class AppLocalizationsBg extends AppLocalizations { String get listFilter_az => 'A-Z'; @override - String get listFilter_usersFirst => 'Users first'; + String get listFilter_usersFirst => 'Първо потребители'; @override String get listFilter_filters => 'Филтри'; diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 18a9aec3..afcb7c76 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -2756,7 +2756,7 @@ class AppLocalizationsDe extends AppLocalizations { String get listFilter_az => 'A-Z'; @override - String get listFilter_usersFirst => 'Users first'; + String get listFilter_usersFirst => 'Benutzer zuerst'; @override String get listFilter_filters => 'Filtere'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index 66fe8237..1f00a634 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -2749,7 +2749,7 @@ class AppLocalizationsEs extends AppLocalizations { String get listFilter_az => 'A-Z'; @override - String get listFilter_usersFirst => 'Users first'; + String get listFilter_usersFirst => 'Usuarios primero'; @override String get listFilter_filters => 'Filtros'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 903ef299..1515a99d 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -2765,7 +2765,7 @@ class AppLocalizationsFr extends AppLocalizations { String get listFilter_az => 'A à Z'; @override - String get listFilter_usersFirst => 'Users first'; + String get listFilter_usersFirst => 'Utilisateurs en premier'; @override String get listFilter_filters => 'Filtres'; diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index 67c6c925..ce25bdf7 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -2749,7 +2749,7 @@ class AppLocalizationsIt extends AppLocalizations { String get listFilter_az => 'A-Z'; @override - String get listFilter_usersFirst => 'Users first'; + String get listFilter_usersFirst => 'Utenti per primi'; @override String get listFilter_filters => 'Filtri'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index 313d37f7..634c7717 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -2740,7 +2740,7 @@ class AppLocalizationsNl extends AppLocalizations { String get listFilter_az => 'A-Z'; @override - String get listFilter_usersFirst => 'Users first'; + String get listFilter_usersFirst => 'Gebruikers eerst'; @override String get listFilter_filters => 'Filters'; diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index 77137f2e..91e26df7 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -2747,7 +2747,7 @@ class AppLocalizationsPl extends AppLocalizations { String get listFilter_az => 'A-Z'; @override - String get listFilter_usersFirst => 'Users first'; + String get listFilter_usersFirst => 'Najpierw użytkownicy'; @override String get listFilter_filters => 'Filtry'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index 0606f1c6..e781514c 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -2750,7 +2750,7 @@ class AppLocalizationsPt extends AppLocalizations { String get listFilter_az => 'A-Z'; @override - String get listFilter_usersFirst => 'Users first'; + String get listFilter_usersFirst => 'Utilizadores primeiro'; @override String get listFilter_filters => 'Filtros'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 1c05f651..f2784f7c 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -2753,7 +2753,7 @@ class AppLocalizationsRu extends AppLocalizations { String get listFilter_az => 'По алфавиту'; @override - String get listFilter_usersFirst => 'Users first'; + String get listFilter_usersFirst => 'Сначала пользователи'; @override String get listFilter_filters => 'Фильтры'; diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index f71695b7..640fbc4c 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -2735,7 +2735,7 @@ class AppLocalizationsSk extends AppLocalizations { String get listFilter_az => 'A-Z'; @override - String get listFilter_usersFirst => 'Users first'; + String get listFilter_usersFirst => 'Najskôr používatelia'; @override String get listFilter_filters => 'Filtre'; diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index 62de2713..2cbf9b40 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -2738,7 +2738,7 @@ class AppLocalizationsSl extends AppLocalizations { String get listFilter_az => 'A-Z'; @override - String get listFilter_usersFirst => 'Users first'; + String get listFilter_usersFirst => 'Najprej uporabniki'; @override String get listFilter_filters => 'Filtri'; diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index 49ee78e0..63e27e51 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -2723,7 +2723,7 @@ class AppLocalizationsSv extends AppLocalizations { String get listFilter_az => 'A-Z'; @override - String get listFilter_usersFirst => 'Users first'; + String get listFilter_usersFirst => 'Användare först'; @override String get listFilter_filters => 'Filteralternativ'; diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index 69ebecce..0cf24fa0 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -1352,6 +1352,7 @@ "listFilter_users": "Gebruikers", "listFilter_repeaters": "Repeaters", "listFilter_roomServers": "Roomservers", + "listFilter_usersFirst": "Gebruikers eerst", "listFilter_unreadOnly": "Alleen ongelezen", "listFilter_newGroup": "Nieuwe groep", "@neighbors_errorLoading": { diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index 75e1d348..34543eb1 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -1352,6 +1352,7 @@ "listFilter_users": "Użytkownicy", "listFilter_repeaters": "Powtarzacze", "listFilter_roomServers": "Serwery pokoju", + "listFilter_usersFirst": "Najpierw użytkownicy", "listFilter_unreadOnly": "Tylko nieprzeczytane", "listFilter_newGroup": "Nowa grupa", "@neighbors_errorLoading": { diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index f6ada195..38c7ed69 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -1352,6 +1352,7 @@ "listFilter_users": "Usuários", "listFilter_repeaters": "Repetidores", "listFilter_roomServers": "Servidores de sala", + "listFilter_usersFirst": "Utilizadores primeiro", "listFilter_unreadOnly": "Apenas não lido", "listFilter_newGroup": "Novo grupo", "@neighbors_errorLoading": { diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 9aef298e..a87fe09a 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -758,6 +758,7 @@ "listFilter_users": "Пользователи", "listFilter_repeaters": "Репитеры", "listFilter_roomServers": "Серверы комнат", + "listFilter_usersFirst": "Сначала пользователи", "listFilter_unreadOnly": "Только непрочитанные", "listFilter_newGroup": "Новая группа", "@chat_couldNotOpenLink": { diff --git a/lib/l10n/app_sk.arb b/lib/l10n/app_sk.arb index 672b7d74..34b4c86a 100644 --- a/lib/l10n/app_sk.arb +++ b/lib/l10n/app_sk.arb @@ -1352,6 +1352,7 @@ "listFilter_users": "Používatelia", "listFilter_repeaters": "Opakovadlá", "listFilter_roomServers": "Servéry miestnosti", + "listFilter_usersFirst": "Najskôr používatelia", "listFilter_unreadOnly": "Nezaregistrované len", "listFilter_newGroup": "Nová skupina", "@neighbors_errorLoading": { diff --git a/lib/l10n/app_sl.arb b/lib/l10n/app_sl.arb index 09359a13..ff16b953 100644 --- a/lib/l10n/app_sl.arb +++ b/lib/l10n/app_sl.arb @@ -1352,6 +1352,7 @@ "listFilter_users": "Uporabniki", "listFilter_repeaters": "Ponovitve", "listFilter_roomServers": "Smeti za prostore", + "listFilter_usersFirst": "Najprej uporabniki", "listFilter_unreadOnly": "Nezbrani samo", "listFilter_newGroup": "Nova skupina", "@neighbors_errorLoading": { diff --git a/lib/l10n/app_sv.arb b/lib/l10n/app_sv.arb index a923cc9c..45541a15 100644 --- a/lib/l10n/app_sv.arb +++ b/lib/l10n/app_sv.arb @@ -1352,6 +1352,7 @@ "listFilter_users": "Användare", "listFilter_repeaters": "Upprepare", "listFilter_roomServers": "Rumservrar", + "listFilter_usersFirst": "Användare först", "listFilter_unreadOnly": "Endast oinlästa", "listFilter_newGroup": "Ny grupp", "@neighbors_errorLoading": { diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 63c02a57..68bb5252 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -1546,6 +1546,7 @@ "listFilter_users": "用户", "listFilter_repeaters": "转发节点", "listFilter_roomServers": "房间服务器", + "listFilter_usersFirst": "用户优先", "listFilter_unreadOnly": "仅显示未读", "listFilter_newGroup": "新建群聊", "pathTrace_you": "我自己", From 1b94442ab657ee70b02329c0858d0a28e27cde90 Mon Sep 17 00:00:00 2001 From: Serge Tarkovski Date: Fri, 27 Feb 2026 21:19:13 +0200 Subject: [PATCH 12/26] Fix action constant collision: change _actionTogglePrioritizeUsers from 10 to 11 --- lib/widgets/list_filter_widget.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/widgets/list_filter_widget.dart b/lib/widgets/list_filter_widget.dart index 63ab5807..fa7e67e2 100644 --- a/lib/widgets/list_filter_widget.dart +++ b/lib/widgets/list_filter_widget.dart @@ -100,7 +100,7 @@ const int _actionFilterRepeaters = 7; const int _actionFilterRooms = 8; const int _actionToggleUnreadOnly = 9; const int _actionNewGroup = 10; -const int _actionTogglePrioritizeUsers = 10; +const int _actionTogglePrioritizeUsers = 11; class ContactsFilterMenu extends StatelessWidget { final ContactSortOption sortOption; From 297516fc805516a7e026f744228f6ecb85db7c14 Mon Sep 17 00:00:00 2001 From: Serge Tarkovski Date: Tue, 21 Apr 2026 16:31:09 +0300 Subject: [PATCH 13/26] Update cached unread total when removing contact unread entries When contacts are removed in removeContact, _handleContact, or _handleContactAdvert, subtract their unread count from _cachedContactsUnreadTotal immediately so badge counts reflect the true total without waiting for a full reload. --- lib/connector/meshcore_connector.dart | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index e1740461..63a8639e 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -431,7 +431,10 @@ class MeshCoreConnector extends ChangeNotifier { void _recalculateCachedChannelsUnreadTotal() { final allChannels = _channels.isNotEmpty ? _channels : _cachedChannels; - _cachedChannelsUnreadTotal = allChannels.fold(0, (total, ch) => total + ch.unreadCount); + _cachedChannelsUnreadTotal = allChannels.fold( + 0, + (total, ch) => total + ch.unreadCount, + ); } bool isChannelSmazEnabled(int channelIndex) { @@ -484,7 +487,8 @@ class MeshCoreConnector extends ChangeNotifier { final previousCount = _contactUnreadCount[contactKeyHex] ?? 0; if (previousCount > 0) { _contactUnreadCount[contactKeyHex] = 0; - _cachedContactsUnreadTotal = (_cachedContactsUnreadTotal - previousCount).clamp(0, _cachedContactsUnreadTotal); + _cachedContactsUnreadTotal = (_cachedContactsUnreadTotal - previousCount) + .clamp(0, _cachedContactsUnreadTotal); _appDebugLogService?.info( 'Contact $contactKeyHex marked as read (was $previousCount unread)', tag: 'Unread', @@ -501,7 +505,8 @@ class MeshCoreConnector extends ChangeNotifier { if (channel != null && channel.unreadCount > 0) { final previousCount = channel.unreadCount; channel.unreadCount = 0; - _cachedChannelsUnreadTotal = (_cachedChannelsUnreadTotal - previousCount).clamp(0, _cachedChannelsUnreadTotal); + _cachedChannelsUnreadTotal = (_cachedChannelsUnreadTotal - previousCount) + .clamp(0, _cachedChannelsUnreadTotal); _appDebugLogService?.info( 'Channel ${channel.name.isNotEmpty ? channel.name : channelIndex} marked as read (was $previousCount unread)', tag: 'Unread', @@ -1524,6 +1529,9 @@ class MeshCoreConnector extends ChangeNotifier { unawaited(_persistContacts()); _conversations.remove(contact.publicKeyHex); _loadedConversationKeys.remove(contact.publicKeyHex); + final removedCount = _contactUnreadCount[contact.publicKeyHex] ?? 0; + _cachedContactsUnreadTotal = (_cachedContactsUnreadTotal - removedCount) + .clamp(0, _cachedContactsUnreadTotal); _contactUnreadCount.remove(contact.publicKeyHex); _unreadStore.saveContactUnreadCount( Map.from(_contactUnreadCount), @@ -2158,6 +2166,9 @@ class MeshCoreConnector extends ChangeNotifier { final contact = Contact.fromFrame(frame); if (contact != null) { if (contact.type == advTypeRepeater) { + final removedCount = _contactUnreadCount[contact.publicKeyHex] ?? 0; + _cachedContactsUnreadTotal = (_cachedContactsUnreadTotal - removedCount) + .clamp(0, _cachedContactsUnreadTotal); _contactUnreadCount.remove(contact.publicKeyHex); _unreadStore.saveContactUnreadCount( Map.from(_contactUnreadCount), @@ -2233,6 +2244,9 @@ class MeshCoreConnector extends ChangeNotifier { } if (contact.type == advTypeRepeater) { + final removedCount = _contactUnreadCount[contact.publicKeyHex] ?? 0; + _cachedContactsUnreadTotal = (_cachedContactsUnreadTotal - removedCount) + .clamp(0, _cachedContactsUnreadTotal); _contactUnreadCount.remove(contact.publicKeyHex); _unreadStore.saveContactUnreadCount( Map.from(_contactUnreadCount), From 8611adab1fcd2a438e6f47e4d33dab02f876c826 Mon Sep 17 00:00:00 2001 From: Serge Tarkovski Date: Tue, 21 Apr 2026 16:51:10 +0300 Subject: [PATCH 14/26] Run dart format and verify analyze --- lib/widgets/quick_switch_bar.dart | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/lib/widgets/quick_switch_bar.dart b/lib/widgets/quick_switch_bar.dart index 5a157a02..4f014bd3 100644 --- a/lib/widgets/quick_switch_bar.dart +++ b/lib/widgets/quick_switch_bar.dart @@ -109,11 +109,7 @@ class QuickSwitchBar extends StatelessWidget { clipBehavior: Clip.none, children: [ icon, - Positioned( - right: -6, - top: -4, - child: UnreadBadge(count: count), - ), + Positioned(right: -6, top: -4, child: UnreadBadge(count: count)), ], ); } From 9fe4a3710de2f720b4c2395d89bb2a4ac8e122fd Mon Sep 17 00:00:00 2001 From: Serge Tarkovski Date: Tue, 21 Apr 2026 17:01:51 +0300 Subject: [PATCH 15/26] Add missing users-first translations for hu/ja/ko and regen outputs --- lib/l10n/app_hu.arb | 1 + lib/l10n/app_ja.arb | 1 + lib/l10n/app_ko.arb | 1 + lib/l10n/app_localizations_hu.dart | 3 +++ lib/l10n/app_localizations_ja.dart | 3 +++ lib/l10n/app_localizations_ko.dart | 3 +++ linux/flutter/generated_plugins.cmake | 1 - windows/flutter/generated_plugins.cmake | 1 - 8 files changed, 12 insertions(+), 2 deletions(-) diff --git a/lib/l10n/app_hu.arb b/lib/l10n/app_hu.arb index 6f43463c..1985899c 100644 --- a/lib/l10n/app_hu.arb +++ b/lib/l10n/app_hu.arb @@ -1698,6 +1698,7 @@ "listFilter_latestMessages": "Legfrissebb üzenetek", "listFilter_heardRecently": "Úgy hallottam, hogy...", "listFilter_az": "A-Z", + "listFilter_usersFirst": "Felhasználók elöl", "listFilter_filters": "Szűrők", "listFilter_all": "Mind", "listFilter_favorites": "Kedvencek", diff --git a/lib/l10n/app_ja.arb b/lib/l10n/app_ja.arb index b63f146d..e6ac0151 100644 --- a/lib/l10n/app_ja.arb +++ b/lib/l10n/app_ja.arb @@ -1698,6 +1698,7 @@ "listFilter_latestMessages": "最新のメッセージ", "listFilter_heardRecently": "最近、聞いた", "listFilter_az": "AからZ", + "listFilter_usersFirst": "ユーザー優先", "listFilter_filters": "フィルター", "listFilter_all": "すべて", "listFilter_favorites": "お気に入り", diff --git a/lib/l10n/app_ko.arb b/lib/l10n/app_ko.arb index 40721fe1..d3930a1b 100644 --- a/lib/l10n/app_ko.arb +++ b/lib/l10n/app_ko.arb @@ -1698,6 +1698,7 @@ "listFilter_latestMessages": "최신 메시지", "listFilter_heardRecently": "최근에 들었습니다", "listFilter_az": "A부터 Z까지", + "listFilter_usersFirst": "사용자 우선", "listFilter_filters": "필터", "listFilter_all": "모든", "listFilter_favorites": "관심 목록", diff --git a/lib/l10n/app_localizations_hu.dart b/lib/l10n/app_localizations_hu.dart index 1ad8558c..9557c3cc 100644 --- a/lib/l10n/app_localizations_hu.dart +++ b/lib/l10n/app_localizations_hu.dart @@ -3081,6 +3081,9 @@ class AppLocalizationsHu extends AppLocalizations { @override String get listFilter_az => 'A-Z'; + @override + String get listFilter_usersFirst => 'Felhasználók elöl'; + @override String get listFilter_filters => 'Szűrők'; diff --git a/lib/l10n/app_localizations_ja.dart b/lib/l10n/app_localizations_ja.dart index afb8c29b..b838f9a1 100644 --- a/lib/l10n/app_localizations_ja.dart +++ b/lib/l10n/app_localizations_ja.dart @@ -2929,6 +2929,9 @@ class AppLocalizationsJa extends AppLocalizations { @override String get listFilter_az => 'AからZ'; + @override + String get listFilter_usersFirst => 'ユーザー優先'; + @override String get listFilter_filters => 'フィルター'; diff --git a/lib/l10n/app_localizations_ko.dart b/lib/l10n/app_localizations_ko.dart index ff4bd261..3af9dd04 100644 --- a/lib/l10n/app_localizations_ko.dart +++ b/lib/l10n/app_localizations_ko.dart @@ -2929,6 +2929,9 @@ class AppLocalizationsKo extends AppLocalizations { @override String get listFilter_az => 'A부터 Z까지'; + @override + String get listFilter_usersFirst => '사용자 우선'; + @override String get listFilter_filters => '필터'; diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 93e46829..379e36fa 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -8,7 +8,6 @@ list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_FFI_PLUGIN_LIST flserial - jni ) set(PLUGIN_BUNDLED_LIBRARIES) diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 533a1712..f02857f4 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -11,7 +11,6 @@ list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_FFI_PLUGIN_LIST flserial flutter_local_notifications_windows - jni ) set(PLUGIN_BUNDLED_LIBRARIES) From 16ce1359d7e0cc23d51427c8a0d1078da8d96800 Mon Sep 17 00:00:00 2001 From: Serge Tarkovski Date: Tue, 21 Apr 2026 23:10:39 +0300 Subject: [PATCH 16/26] Remove unused 'Users first' translation key --- lib/l10n/app_bg.arb | 1 - lib/l10n/app_de.arb | 1 - lib/l10n/app_en.arb | 1 - lib/l10n/app_es.arb | 1 - lib/l10n/app_fr.arb | 1 - lib/l10n/app_hu.arb | 1 - lib/l10n/app_it.arb | 1 - lib/l10n/app_ja.arb | 1 - lib/l10n/app_ko.arb | 1 - lib/l10n/app_localizations.dart | 6 ------ lib/l10n/app_localizations_bg.dart | 3 --- lib/l10n/app_localizations_de.dart | 3 --- lib/l10n/app_localizations_en.dart | 3 --- lib/l10n/app_localizations_es.dart | 3 --- lib/l10n/app_localizations_fr.dart | 3 --- lib/l10n/app_localizations_hu.dart | 3 --- lib/l10n/app_localizations_it.dart | 3 --- lib/l10n/app_localizations_ja.dart | 3 --- lib/l10n/app_localizations_ko.dart | 3 --- lib/l10n/app_localizations_nl.dart | 3 --- lib/l10n/app_localizations_pl.dart | 3 --- lib/l10n/app_localizations_pt.dart | 3 --- lib/l10n/app_localizations_ru.dart | 3 --- lib/l10n/app_localizations_sk.dart | 3 --- lib/l10n/app_localizations_sl.dart | 3 --- lib/l10n/app_localizations_sv.dart | 3 --- lib/l10n/app_localizations_uk.dart | 3 --- lib/l10n/app_localizations_zh.dart | 3 --- lib/l10n/app_nl.arb | 1 - lib/l10n/app_pl.arb | 1 - lib/l10n/app_pt.arb | 1 - lib/l10n/app_ru.arb | 1 - lib/l10n/app_sk.arb | 1 - lib/l10n/app_sl.arb | 1 - lib/l10n/app_sv.arb | 1 - lib/l10n/app_uk.arb | 1 - lib/l10n/app_zh.arb | 1 - 37 files changed, 78 deletions(-) diff --git a/lib/l10n/app_bg.arb b/lib/l10n/app_bg.arb index bd87d03a..7ac54177 100644 --- a/lib/l10n/app_bg.arb +++ b/lib/l10n/app_bg.arb @@ -1353,7 +1353,6 @@ "listFilter_users": "Потребители", "listFilter_repeaters": "Повторители", "listFilter_roomServers": "Сървъри на стая", - "listFilter_usersFirst": "Първо потребители", "listFilter_unreadOnly": "Само непрочетените", "listFilter_newGroup": "Нова група", "@neighbors_errorLoading": { diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 0e63be20..46955052 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -1356,7 +1356,6 @@ "listFilter_users": "Benutzer", "listFilter_repeaters": "Repeater", "listFilter_roomServers": "Raumserver", - "listFilter_usersFirst": "Benutzer zuerst", "listFilter_unreadOnly": "Nicht gelesen", "listFilter_newGroup": "Neue Gruppe", "@neighbors_errorLoading": { diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 6b845b23..8ad6bf37 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1754,7 +1754,6 @@ "listFilter_latestMessages": "Latest messages", "listFilter_heardRecently": "Heard recently", "listFilter_az": "A-Z", - "listFilter_usersFirst": "Users first", "listFilter_filters": "Filters", "listFilter_all": "All", "listFilter_favorites": "Favorites", diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 6e7500ce..ac9527bd 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -1353,7 +1353,6 @@ "listFilter_users": "Usuarios", "listFilter_repeaters": "Repetidores", "listFilter_roomServers": "Servidores de la sala", - "listFilter_usersFirst": "Usuarios primero", "listFilter_unreadOnly": "Solo sin leer", "listFilter_newGroup": "Nuevo grupo", "@neighbors_errorLoading": { diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 7890c3de..a942aa2b 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -1353,7 +1353,6 @@ "listFilter_users": "Utilisateurs", "listFilter_repeaters": "Répéteurs", "listFilter_roomServers": "Room servers", - "listFilter_usersFirst": "Utilisateurs en premier", "listFilter_unreadOnly": "Messages non lus seulement", "listFilter_newGroup": "Nouveau groupe", "@neighbors_errorLoading": { diff --git a/lib/l10n/app_hu.arb b/lib/l10n/app_hu.arb index 1985899c..6f43463c 100644 --- a/lib/l10n/app_hu.arb +++ b/lib/l10n/app_hu.arb @@ -1698,7 +1698,6 @@ "listFilter_latestMessages": "Legfrissebb üzenetek", "listFilter_heardRecently": "Úgy hallottam, hogy...", "listFilter_az": "A-Z", - "listFilter_usersFirst": "Felhasználók elöl", "listFilter_filters": "Szűrők", "listFilter_all": "Mind", "listFilter_favorites": "Kedvencek", diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index a14c5ca5..387c8cff 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -1353,7 +1353,6 @@ "listFilter_users": "Utenti", "listFilter_repeaters": "Ripetitori", "listFilter_roomServers": "Server della stanza", - "listFilter_usersFirst": "Utenti per primi", "listFilter_unreadOnly": "Solo non letto", "listFilter_newGroup": "Nuovo gruppo", "@neighbors_errorLoading": { diff --git a/lib/l10n/app_ja.arb b/lib/l10n/app_ja.arb index e6ac0151..b63f146d 100644 --- a/lib/l10n/app_ja.arb +++ b/lib/l10n/app_ja.arb @@ -1698,7 +1698,6 @@ "listFilter_latestMessages": "最新のメッセージ", "listFilter_heardRecently": "最近、聞いた", "listFilter_az": "AからZ", - "listFilter_usersFirst": "ユーザー優先", "listFilter_filters": "フィルター", "listFilter_all": "すべて", "listFilter_favorites": "お気に入り", diff --git a/lib/l10n/app_ko.arb b/lib/l10n/app_ko.arb index d3930a1b..40721fe1 100644 --- a/lib/l10n/app_ko.arb +++ b/lib/l10n/app_ko.arb @@ -1698,7 +1698,6 @@ "listFilter_latestMessages": "최신 메시지", "listFilter_heardRecently": "최근에 들었습니다", "listFilter_az": "A부터 Z까지", - "listFilter_usersFirst": "사용자 우선", "listFilter_filters": "필터", "listFilter_all": "모든", "listFilter_favorites": "관심 목록", diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 4c54dd39..2c1342d0 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -5354,12 +5354,6 @@ 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: diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index c51bb6b9..b3e12799 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -3070,9 +3070,6 @@ class AppLocalizationsBg extends AppLocalizations { @override String get listFilter_az => 'A-Z'; - @override - String get listFilter_usersFirst => 'Първо потребители'; - @override String get listFilter_filters => 'Филтри'; diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 7d69ebd3..d7c16914 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -3074,9 +3074,6 @@ class AppLocalizationsDe extends AppLocalizations { @override String get listFilter_az => 'A-Z'; - @override - String get listFilter_usersFirst => 'Benutzer zuerst'; - @override String get listFilter_filters => 'Filtere'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index c36d11c0..a2a88b0d 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -3015,9 +3015,6 @@ class AppLocalizationsEn extends AppLocalizations { @override String get listFilter_az => 'A-Z'; - @override - String get listFilter_usersFirst => 'Users first'; - @override String get listFilter_filters => 'Filters'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index 326144cf..a1270124 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -3067,9 +3067,6 @@ class AppLocalizationsEs extends AppLocalizations { @override String get listFilter_az => 'A-Z'; - @override - String get listFilter_usersFirst => 'Usuarios primero'; - @override String get listFilter_filters => 'Filtros'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 3bfc05d3..a0063914 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -3086,9 +3086,6 @@ class AppLocalizationsFr extends AppLocalizations { @override String get listFilter_az => 'A à Z'; - @override - String get listFilter_usersFirst => 'Utilisateurs en premier'; - @override String get listFilter_filters => 'Filtres'; diff --git a/lib/l10n/app_localizations_hu.dart b/lib/l10n/app_localizations_hu.dart index 9557c3cc..1ad8558c 100644 --- a/lib/l10n/app_localizations_hu.dart +++ b/lib/l10n/app_localizations_hu.dart @@ -3081,9 +3081,6 @@ class AppLocalizationsHu extends AppLocalizations { @override String get listFilter_az => 'A-Z'; - @override - String get listFilter_usersFirst => 'Felhasználók elöl'; - @override String get listFilter_filters => 'Szűrők'; diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index d077cc87..3a55559a 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -3070,9 +3070,6 @@ class AppLocalizationsIt extends AppLocalizations { @override String get listFilter_az => 'A-Z'; - @override - String get listFilter_usersFirst => 'Utenti per primi'; - @override String get listFilter_filters => 'Filtri'; diff --git a/lib/l10n/app_localizations_ja.dart b/lib/l10n/app_localizations_ja.dart index b838f9a1..afb8c29b 100644 --- a/lib/l10n/app_localizations_ja.dart +++ b/lib/l10n/app_localizations_ja.dart @@ -2929,9 +2929,6 @@ class AppLocalizationsJa extends AppLocalizations { @override String get listFilter_az => 'AからZ'; - @override - String get listFilter_usersFirst => 'ユーザー優先'; - @override String get listFilter_filters => 'フィルター'; diff --git a/lib/l10n/app_localizations_ko.dart b/lib/l10n/app_localizations_ko.dart index 3af9dd04..ff4bd261 100644 --- a/lib/l10n/app_localizations_ko.dart +++ b/lib/l10n/app_localizations_ko.dart @@ -2929,9 +2929,6 @@ class AppLocalizationsKo extends AppLocalizations { @override String get listFilter_az => 'A부터 Z까지'; - @override - String get listFilter_usersFirst => '사용자 우선'; - @override String get listFilter_filters => '필터'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index 28fe6dee..dd770e15 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -3053,9 +3053,6 @@ class AppLocalizationsNl extends AppLocalizations { @override String get listFilter_az => 'A-Z'; - @override - String get listFilter_usersFirst => 'Gebruikers eerst'; - @override String get listFilter_filters => 'Filters'; diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index 4fab5c11..357dd7e3 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -3079,9 +3079,6 @@ class AppLocalizationsPl extends AppLocalizations { @override String get listFilter_az => 'A-Z'; - @override - String get listFilter_usersFirst => 'Najpierw użytkownicy'; - @override String get listFilter_filters => 'Filtry'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index eeb20f3d..2dfcd8bd 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -3068,9 +3068,6 @@ class AppLocalizationsPt extends AppLocalizations { @override String get listFilter_az => 'A-Z'; - @override - String get listFilter_usersFirst => 'Utilizadores primeiro'; - @override String get listFilter_filters => 'Filtros'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 97f0d26e..4fac42ce 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -3073,9 +3073,6 @@ class AppLocalizationsRu extends AppLocalizations { @override String get listFilter_az => 'По алфавиту'; - @override - String get listFilter_usersFirst => 'Сначала пользователи'; - @override String get listFilter_filters => 'Фильтры'; diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index d2787b69..c42e0249 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -3048,9 +3048,6 @@ class AppLocalizationsSk extends AppLocalizations { @override String get listFilter_az => 'A-Z'; - @override - String get listFilter_usersFirst => 'Najskôr používatelia'; - @override String get listFilter_filters => 'Filtre'; diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index 0dd91eb8..2d89aa41 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -3049,9 +3049,6 @@ class AppLocalizationsSl extends AppLocalizations { @override String get listFilter_az => 'A-Z'; - @override - String get listFilter_usersFirst => 'Najprej uporabniki'; - @override String get listFilter_filters => 'Filtri'; diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index a789c3c6..38e08939 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -3033,9 +3033,6 @@ class AppLocalizationsSv extends AppLocalizations { @override String get listFilter_az => 'A-Z'; - @override - String get listFilter_usersFirst => 'Användare först'; - @override String get listFilter_filters => 'Filteralternativ'; diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index 4bba7f7f..4cab52cd 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -3075,9 +3075,6 @@ class AppLocalizationsUk extends AppLocalizations { @override String get listFilter_az => 'А-Я'; - @override - String get listFilter_usersFirst => 'Спочатку користувачі'; - @override String get listFilter_filters => 'Фільтри'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index ec3182d3..4f38c64a 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -2854,9 +2854,6 @@ class AppLocalizationsZh extends AppLocalizations { @override String get listFilter_az => 'A-Z'; - @override - String get listFilter_usersFirst => '用户优先'; - @override String get listFilter_filters => '筛选'; diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index a7509184..96bdb845 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -1353,7 +1353,6 @@ "listFilter_users": "Gebruikers", "listFilter_repeaters": "Repeaters", "listFilter_roomServers": "Roomservers", - "listFilter_usersFirst": "Gebruikers eerst", "listFilter_unreadOnly": "Alleen ongelezen", "listFilter_newGroup": "Nieuwe groep", "@neighbors_errorLoading": { diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index 2215f632..b62a78af 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -1363,7 +1363,6 @@ "listFilter_users": "Użytkownicy", "listFilter_repeaters": "Przekaźniki", "listFilter_roomServers": "Serwery pokoju", - "listFilter_usersFirst": "Najpierw użytkownicy", "listFilter_unreadOnly": "Tylko nieprzeczytane", "listFilter_newGroup": "Nowa grupa", "@neighbors_errorLoading": { diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index e6fe447c..bf3e8936 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -1353,7 +1353,6 @@ "listFilter_users": "Usuários", "listFilter_repeaters": "Repetidores", "listFilter_roomServers": "Servidores de sala", - "listFilter_usersFirst": "Utilizadores primeiro", "listFilter_unreadOnly": "Apenas não lido", "listFilter_newGroup": "Novo grupo", "@neighbors_errorLoading": { diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index a089c496..a83d1394 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -759,7 +759,6 @@ "listFilter_users": "Пользователи", "listFilter_repeaters": "Репитеры", "listFilter_roomServers": "Серверы комнат", - "listFilter_usersFirst": "Сначала пользователи", "listFilter_unreadOnly": "Только непрочитанные", "listFilter_newGroup": "Новая группа", "@chat_couldNotOpenLink": { diff --git a/lib/l10n/app_sk.arb b/lib/l10n/app_sk.arb index bed40af7..e4466c30 100644 --- a/lib/l10n/app_sk.arb +++ b/lib/l10n/app_sk.arb @@ -1353,7 +1353,6 @@ "listFilter_users": "Používatelia", "listFilter_repeaters": "Opakovadlá", "listFilter_roomServers": "Servéry miestnosti", - "listFilter_usersFirst": "Najskôr používatelia", "listFilter_unreadOnly": "Nezaregistrované len", "listFilter_newGroup": "Nová skupina", "@neighbors_errorLoading": { diff --git a/lib/l10n/app_sl.arb b/lib/l10n/app_sl.arb index 98bf60d1..f6a317ef 100644 --- a/lib/l10n/app_sl.arb +++ b/lib/l10n/app_sl.arb @@ -1353,7 +1353,6 @@ "listFilter_users": "Uporabniki", "listFilter_repeaters": "Ponovitve", "listFilter_roomServers": "Smeti za prostore", - "listFilter_usersFirst": "Najprej uporabniki", "listFilter_unreadOnly": "Nezbrani samo", "listFilter_newGroup": "Nova skupina", "@neighbors_errorLoading": { diff --git a/lib/l10n/app_sv.arb b/lib/l10n/app_sv.arb index 3fbf1ece..eab348c7 100644 --- a/lib/l10n/app_sv.arb +++ b/lib/l10n/app_sv.arb @@ -1353,7 +1353,6 @@ "listFilter_users": "Användare", "listFilter_repeaters": "Upprepare", "listFilter_roomServers": "Rumservrar", - "listFilter_usersFirst": "Användare först", "listFilter_unreadOnly": "Endast oinlästa", "listFilter_newGroup": "Ny grupp", "@neighbors_errorLoading": { diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb index f272d0ff..90f2f267 100644 --- a/lib/l10n/app_uk.arb +++ b/lib/l10n/app_uk.arb @@ -1349,7 +1349,6 @@ "listFilter_latestMessages": "Останні повідомлення", "listFilter_heardRecently": "Нещодавно чули", "listFilter_az": "А-Я", - "listFilter_usersFirst": "Спочатку користувачі", "listFilter_filters": "Фільтри", "listFilter_all": "Все", "listFilter_users": "Користувачі", diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index f6937949..9dc2325c 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -1547,7 +1547,6 @@ "listFilter_users": "用户", "listFilter_repeaters": "转发节点", "listFilter_roomServers": "房间服务器", - "listFilter_usersFirst": "用户优先", "listFilter_unreadOnly": "仅显示未读", "listFilter_newGroup": "新建群聊", "pathTrace_you": "我自己", From d0d6a34fb547877f795126ddf8fb1127894185ea Mon Sep 17 00:00:00 2001 From: Serge Tarkovski Date: Tue, 21 Apr 2026 23:44:14 +0300 Subject: [PATCH 17/26] Restore jni to whatever is in main --- linux/flutter/generated_plugins.cmake | 1 + windows/flutter/generated_plugins.cmake | 1 + 2 files changed, 2 insertions(+) diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 379e36fa..93e46829 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -8,6 +8,7 @@ list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_FFI_PLUGIN_LIST flserial + jni ) set(PLUGIN_BUNDLED_LIBRARIES) diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index f02857f4..533a1712 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -11,6 +11,7 @@ list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_FFI_PLUGIN_LIST flserial flutter_local_notifications_windows + jni ) set(PLUGIN_BUNDLED_LIBRARIES) From 0c1e163b889a292bc47be76ef5f359754540f4fa Mon Sep 17 00:00:00 2001 From: Serge Tarkovski Date: Tue, 21 Apr 2026 23:50:08 +0300 Subject: [PATCH 18/26] Reverted Ukrainian translations, will be in a separate PR --- lib/l10n/app_localizations_uk.dart | 8 ++++---- lib/l10n/app_uk.arb | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index 4cab52cd..8ed4b9ff 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -3115,7 +3115,7 @@ class AppLocalizationsUk extends AppLocalizations { String get pathTrace_notAvailable => 'Трасування шляху недоступне.'; @override - String get pathTrace_refreshTooltip => 'Оновити трасування шляху'; + String get pathTrace_refreshTooltip => 'Оновити Path Trace'; @override String get pathTrace_someHopsNoLocation => @@ -3302,10 +3302,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 => @@ -3332,7 +3332,7 @@ class AppLocalizationsUk extends AppLocalizations { @override String get contacts_contactAdvertCopied => - 'Оголошення скопійовано до буфера обміну.'; + 'Рекламу скопійовано до буфера обміну.'; @override String get contacts_contactAdvertCopyFailed => diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb index 90f2f267..a005b369 100644 --- a/lib/l10n/app_uk.arb +++ b/lib/l10n/app_uk.arb @@ -1551,7 +1551,7 @@ "pathTrace_you": "Ви", "pathTrace_failed": "Відстеження шляху не вдалося.", "pathTrace_notAvailable": "Трасування шляху недоступне.", - "pathTrace_refreshTooltip": "Оновити трасування шляху", + "pathTrace_refreshTooltip": "Оновити Path Trace", "contacts_pathTrace": "Трасування шляхів", "contacts_ping": "Пінгувати", "contacts_repeaterPathTrace": "Трасування шляху до повторювача", @@ -1563,8 +1563,8 @@ "contacts_invalidAdvertFormat": "Недійсні контактні дані", "contacts_contactImported": "Контакт було імпортовано.", "contacts_contactImportFailed": "Контакт не вдалося імпортувати", - "contacts_zeroHopAdvert": "Оголошення без ретрансляції", - "contacts_floodAdvert": "Оголошення з ретрансляцією", + "contacts_zeroHopAdvert": "Реклама без перехоплення", + "contacts_floodAdvert": "Залив реклами", "contacts_copyAdvertToClipboard": "Копіювати оголошення в буфер обміну", "contacts_clipboardEmpty": "Буфер обміну порожній", "appSettings_languageRu": "Російська", @@ -1572,7 +1572,7 @@ "appSettings_enableMessageTracingSubtitle": "Показувати детальні метадані про маршрутизацію та час для повідомлень", "contacts_ShareContact": "Копіювати контакт у буфер обміну", "contacts_zeroHopContactAdvertFailed": "Не вдалося надіслати контакт.", - "contacts_contactAdvertCopied": "Оголошення скопійовано до буфера обміну.", + "contacts_contactAdvertCopied": "Рекламу скопійовано до буфера обміну.", "contacts_contactAdvertCopyFailed": "Копіювання оголошення в буфер обміну завершилося невдало", "contacts_zeroHopContactAdvertSent": "Відправлено контакт за оголошенням", "contacts_addContactFromClipboard": "Додати контакт з буфера обміну", From d3c7d8e43aa3660d95c1d862d3e07e76b48bed6d Mon Sep 17 00:00:00 2001 From: Serge Tarkovski Date: Wed, 22 Apr 2026 01:57:12 +0300 Subject: [PATCH 19/26] Red dot unread indicator in bottom tabs, keep numeric unreads only for the lists; fixed unread indicator wasn't on all screens --- lib/screens/contacts_screen.dart | 2 ++ lib/screens/line_of_sight_map_screen.dart | 6 ++++++ lib/widgets/quick_switch_bar.dart | 14 ++++++++++++-- 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/lib/screens/contacts_screen.dart b/lib/screens/contacts_screen.dart index 54d32990..a034d04a 100644 --- a/lib/screens/contacts_screen.dart +++ b/lib/screens/contacts_screen.dart @@ -429,6 +429,8 @@ class _ContactsScreenState extends State selectedIndex: 0, onDestinationSelected: (index) => _handleQuickSwitch(index, context), + contactsUnreadCount: connector.getTotalContactsUnreadCount(), + channelsUnreadCount: connector.getTotalChannelsUnreadCount(), ), ), ), diff --git a/lib/screens/line_of_sight_map_screen.dart b/lib/screens/line_of_sight_map_screen.dart index ec8a391f..1148edf0 100644 --- a/lib/screens/line_of_sight_map_screen.dart +++ b/lib/screens/line_of_sight_map_screen.dart @@ -431,6 +431,12 @@ class _LineOfSightMapScreenState extends State { child: QuickSwitchBar( selectedIndex: 2, onDestinationSelected: (index) => _handleQuickSwitch(index, context), + contactsUnreadCount: context + .watch() + .getTotalContactsUnreadCount(), + channelsUnreadCount: context + .watch() + .getTotalChannelsUnreadCount(), ), ), ); diff --git a/lib/widgets/quick_switch_bar.dart b/lib/widgets/quick_switch_bar.dart index 4f014bd3..40dcb59a 100644 --- a/lib/widgets/quick_switch_bar.dart +++ b/lib/widgets/quick_switch_bar.dart @@ -2,7 +2,6 @@ import 'dart:ui'; import 'package:flutter/material.dart'; import '../l10n/l10n.dart'; -import 'unread_badge.dart'; class QuickSwitchBar extends StatelessWidget { final int selectedIndex; @@ -109,7 +108,18 @@ class QuickSwitchBar extends StatelessWidget { clipBehavior: Clip.none, children: [ icon, - Positioned(right: -6, top: -4, child: UnreadBadge(count: count)), + Positioned( + right: -2, + top: -2, + child: Container( + width: 8, + height: 8, + decoration: const BoxDecoration( + color: Colors.redAccent, + shape: BoxShape.circle, + ), + ), + ), ], ); } From 703d5a1ec460e272b91178e4cf04ad7c33378fb5 Mon Sep 17 00:00:00 2001 From: Max Cooley Date: Sun, 10 May 2026 17:12:48 -0700 Subject: [PATCH 20/26] Add "NRF52" as a device name prefix --- lib/connector/meshcore_uuids.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/connector/meshcore_uuids.dart b/lib/connector/meshcore_uuids.dart index ae6697b6..084cc424 100644 --- a/lib/connector/meshcore_uuids.dart +++ b/lib/connector/meshcore_uuids.dart @@ -11,5 +11,6 @@ class MeshCoreUuids { "Lilygo", "HT-", "LowMesh_MC_", + "NRF52", ]; } From 3af97ff6dd2035cc1b7e89ea5a2df90bd17b916c Mon Sep 17 00:00:00 2001 From: Max Cooley Date: Sun, 10 May 2026 17:16:38 -0700 Subject: [PATCH 21/26] Accidentally wrote quotes instead of backticks...oops --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 94d54bac..041ea6e9 100644 --- a/README.md +++ b/README.md @@ -193,6 +193,7 @@ Devices are discovered by scanning for BLE advertisements with known MeshCore de - `WisCore-` - `HT-` - `LowMesh_MC_` + - `NRF52` New device prefixes can be added in `lib/connector/meshcore_uuids.dart`. From 77018dc35887cdbebb739a6c232efffbb5522c81 Mon Sep 17 00:00:00 2001 From: Serge Tarkovski Date: Tue, 12 May 2026 00:47:26 +0300 Subject: [PATCH 22/26] Recompute channels unread total after cachedChannels is updated --- lib/connector/meshcore_connector.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index 78e02a7b..ead7fb36 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -3442,6 +3442,7 @@ class MeshCoreConnector extends ChangeNotifier { // Cache channels for offline use _cachedChannels = List.from(_channels); unawaited(_channelStore.saveChannels(_channels)); + _recalculateCachedChannelsUnreadTotal(); // Apply ordering and notify UI _applyChannelOrder(); @@ -3460,7 +3461,6 @@ class MeshCoreConnector extends ChangeNotifier { if (completed) { _hasLoadedChannels = true; _previousChannelsCache.clear(); - _recalculateCachedChannelsUnreadTotal(); } // Fallback: if contact sync was deferred waiting for channel 0 but From 2763d83fe41ab42890e6ac43005693c35ca828fa Mon Sep 17 00:00:00 2001 From: Seth Golub Date: Mon, 11 May 2026 21:13:50 -0700 Subject: [PATCH 23/26] Use correct channel icons in channel chat screen At the top of the channel chat screen is an icon, indicating the channel type. Previously, the public icon was used correctly, but the hashtag icon was used for all other types. Now, consistent with the channels screen, we use the lock icon for private channels, and the composite icons for community public & community hashtag types. The fix for private channels was trivial, as we can identify hashtag channels by their name. Finding out whether a channel belongs to a community is much more involved. All the hard-working code was copied from channels_screen.dart. (I tried refactoring to reduce duplication, but my results were complex and not worth it.) Closes #432 --- lib/models/channel.dart | 34 ++++++++++++ lib/models/community.dart | 33 +++++++++++ lib/screens/channel_chat_screen.dart | 74 +++++++++++++++++++++++-- lib/screens/channels_screen.dart | 82 +++++++++------------------- 4 files changed, 164 insertions(+), 59 deletions(-) diff --git a/lib/models/channel.dart b/lib/models/channel.dart index 4fdd6270..9baf6302 100644 --- a/lib/models/channel.dart +++ b/lib/models/channel.dart @@ -4,6 +4,9 @@ import 'dart:typed_data'; import 'package:crypto/crypto.dart' as crypto; import '../connector/meshcore_protocol.dart'; +import 'community.dart'; + +enum ChannelType { public, private, hashtag, communityPublic, communityHashtag } class Channel { final int index; @@ -111,5 +114,36 @@ class Channel { return bytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join(); } + static bool isCommunityChannel(ChannelType channelType) { + switch (channelType) { + case ChannelType.communityPublic: + case ChannelType.communityHashtag: + return true; + case ChannelType.public: + case ChannelType.private: + case ChannelType.hashtag: + return false; + } + } + + static ChannelType getChannelType( + Channel channel, + CommunityPskIndex communityIndex, + ) { + Community? community = communityIndex.getCommunityForChannel(channel); + if (community != null) { + if (Community.isCommunityPublicChannel(channel, community)) { + return ChannelType.communityPublic; + } + return ChannelType.communityHashtag; + } + if (channel.isPublicChannel) { + return ChannelType.public; + } else if (channel.name.startsWith('#')) { + return ChannelType.hashtag; + } + return ChannelType.private; + } + static const String publicChannelPsk = '8b3387e9c5cdea6ac9e5edbaa115cd72'; } diff --git a/lib/models/community.dart b/lib/models/community.dart index c829f3d1..7261ddf9 100644 --- a/lib/models/community.dart +++ b/lib/models/community.dart @@ -4,6 +4,8 @@ import 'dart:typed_data'; import 'package:crypto/crypto.dart' as crypto; +import 'channel.dart'; + /// Represents a community with a shared secret for deriving channel PSKs. /// /// A Community is a namespace with a shared secret K (32 random bytes), @@ -162,6 +164,12 @@ class Community { return hashtag.replaceFirst(RegExp(r'^#'), '').toLowerCase().trim(); } + /// Returns true if this is the community's public channel + static bool isCommunityPublicChannel(Channel channel, Community community) { + final publicPsk = community.deriveCommunityPublicPsk(); + return channel.pskHex == Channel.formatPskHex(publicPsk); + } + /// Add a hashtag channel to this community's list Community addHashtagChannel(String hashtag) { final normalized = _normalizeCommunityHashtag(hashtag); @@ -237,3 +245,28 @@ class Community { @override int get hashCode => id.hashCode; } + +class CommunityPskIndex { + // Cache of PSK hex -> Community for quick lookup + final Map _pskToCommunity = {}; + + void initialize(List communities) { + _pskToCommunity.clear(); + for (final community in communities) { + // Map the community public channel PSK + final publicPsk = community.deriveCommunityPublicPsk(); + _pskToCommunity[Channel.formatPskHex(publicPsk)] = community; + + // Map all known hashtag channel PSKs + for (final hashtag in community.hashtagChannels) { + final hashtagPsk = community.deriveCommunityHashtagPsk(hashtag); + _pskToCommunity[Channel.formatPskHex(hashtagPsk)] = community; + } + } + } + + /// Returns the community this channel belongs to, or null if not a community channel + Community? getCommunityForChannel(Channel channel) { + return _pskToCommunity[channel.pskHex]; + } +} diff --git a/lib/screens/channel_chat_screen.dart b/lib/screens/channel_chat_screen.dart index fcd50861..be72eaa8 100644 --- a/lib/screens/channel_chat_screen.dart +++ b/lib/screens/channel_chat_screen.dart @@ -8,6 +8,8 @@ import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; import '../connector/meshcore_connector.dart'; +import '../models/community.dart'; +import '../storage/community_store.dart'; import '../utils/platform_info.dart'; import '../helpers/chat_scroll_controller.dart'; import '../connector/meshcore_protocol.dart'; @@ -56,8 +58,11 @@ class _ChannelChatScreenState extends State { final ChatScrollController _scrollController = ChatScrollController(); final FocusNode _textFieldFocusNode = FocusNode(); ChannelMessage? _replyingToMessage; + final CommunityStore _communityStore = CommunityStore(); + final CommunityPskIndex _communityIndex = CommunityPskIndex(); final Map _messageKeys = {}; bool _isLoadingOlder = false; + bool _communitiesLoaded = false; MeshCoreConnector? _connector; DateTime? _lastChannelSendAt; @@ -81,6 +86,7 @@ class _ChannelChatScreenState extends State { final idx = widget.channel.index; final unread = widget.initialUnreadCount; final messages = connector.getChannelMessages(widget.channel); + _loadCommunities(); ChannelMessage? anchor; if (unread > 0) { anchor = _findOldestUnreadChannelAnchor(messages, unread); @@ -107,6 +113,19 @@ class _ChannelChatScreenState extends State { }); } + // TODO: Reload communities when returning from another screen + Future _loadCommunities() async { + final connector = context.read(); + _communityStore.setPublicKeyHex = connector.selfPublicKeyHex; + final communities = await _communityStore.loadCommunities(); + if (mounted) { + setState(() { + _communityIndex.initialize(communities); + _communitiesLoaded = true; + }); + } + } + ChannelMessage? _findOldestUnreadChannelAnchor( List messages, int unreadCount, @@ -193,16 +212,63 @@ class _ChannelChatScreenState extends State { ); } + Widget _channelIcon(Channel channel) { + // Determine icon based on channel type + final ChannelType channelType = Channel.getChannelType( + channel, + _communityIndex, + ); + final bool isCommunityChannel = Channel.isCommunityChannel(channelType); + IconData icon; + switch (channelType) { + case ChannelType.communityPublic: + icon = Icons.groups; + case ChannelType.communityHashtag: + icon = Icons.tag; + case ChannelType.public: + icon = Icons.public; + case ChannelType.hashtag: + icon = Icons.tag; + case ChannelType.private: + icon = Icons.lock; + } + return Stack( + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 3), + child: _communitiesLoaded + ? Icon(icon, size: 20) + : SizedBox.square(dimension: 20), + ), + if (isCommunityChannel) + Positioned( + right: 0, + bottom: 0, + child: Container( + width: 12, + height: 12, + decoration: BoxDecoration( + color: Colors.purple, + shape: BoxShape.circle, + border: Border.all( + color: Theme.of(context).cardColor, + width: 2, + ), + ), + child: const Icon(Icons.people, size: 8, color: Colors.white), + ), + ), + ], + ); + } + @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Row( children: [ - Icon( - widget.channel.isPublicChannel ? Icons.public : Icons.tag, - size: 20, - ), + _channelIcon(widget.channel), const SizedBox(width: 8), Expanded( child: Column( diff --git a/lib/screens/channels_screen.dart b/lib/screens/channels_screen.dart index 4613a8ee..e97705fc 100644 --- a/lib/screens/channels_screen.dart +++ b/lib/screens/channels_screen.dart @@ -44,11 +44,9 @@ class _ChannelsScreenState extends State with DisconnectNavigationMixin { final TextEditingController _searchController = TextEditingController(); final CommunityStore _communityStore = CommunityStore(); - Timer? _searchDebounce; + final CommunityPskIndex _communityIndex = CommunityPskIndex(); List _communities = []; - - // Cache of PSK hex -> Community for quick lookup - final Map _pskToCommunity = {}; + Timer? _searchDebounce; ChannelMessageStore get _channelMessageStore => ChannelMessageStore(); @@ -71,37 +69,11 @@ class _ChannelsScreenState extends State if (mounted) { setState(() { _communities = communities; - _buildPskCommunityMap(); + _communityIndex.initialize(communities); }); } } - void _buildPskCommunityMap() { - _pskToCommunity.clear(); - for (final community in _communities) { - // Map the community public channel PSK - final publicPsk = community.deriveCommunityPublicPsk(); - _pskToCommunity[Channel.formatPskHex(publicPsk)] = community; - - // Map all known hashtag channel PSKs - for (final hashtag in community.hashtagChannels) { - final hashtagPsk = community.deriveCommunityHashtagPsk(hashtag); - _pskToCommunity[Channel.formatPskHex(hashtagPsk)] = community; - } - } - } - - /// Returns the community this channel belongs to, or null if not a community channel - Community? _getCommunityForChannel(Channel channel) { - return _pskToCommunity[channel.pskHex]; - } - - /// Returns true if this is the community's public channel - bool _isCommunityPublicChannel(Channel channel, Community community) { - final publicPsk = community.deriveCommunityPublicPsk(); - return channel.pskHex == Channel.formatPskHex(publicPsk); - } - @override void dispose() { _searchDebounce?.cancel(); @@ -375,37 +347,37 @@ class _ChannelsScreenState extends State int? dragIndex, }) { final unreadCount = connector.getUnreadCountForChannel(channel); - final community = _getCommunityForChannel(channel); - final isCommunityChannel = community != null; - final isCommunityPublic = - isCommunityChannel && _isCommunityPublicChannel(channel, community); // Determine icon and colors based on channel type IconData icon; Color iconColor; Color bgColor; - - if (isCommunityChannel) { - // Community channel styling - iconColor = Colors.purple; - bgColor = Colors.purple.withValues(alpha: 0.2); - if (isCommunityPublic) { + final ChannelType channelType = Channel.getChannelType( + channel, + _communityIndex, + ); + final bool isCommunityChannel = Channel.isCommunityChannel(channelType); + switch (channelType) { + case ChannelType.communityPublic: icon = Icons.groups; - } else { + iconColor = Colors.purple; + bgColor = Colors.purple.withValues(alpha: 0.2); + case ChannelType.communityHashtag: icon = Icons.tag; - } - } else if (channel.isPublicChannel) { - icon = Icons.public; - iconColor = Colors.green; - bgColor = Colors.green.withValues(alpha: 0.2); - } else if (channel.name.startsWith('#')) { - icon = Icons.tag; - iconColor = Colors.blue; - bgColor = Colors.blue.withValues(alpha: 0.2); - } else { - icon = Icons.lock; - iconColor = Colors.blue; - bgColor = Colors.blue.withValues(alpha: 0.2); + iconColor = Colors.purple; + bgColor = Colors.purple.withValues(alpha: 0.2); + case ChannelType.public: + icon = Icons.public; + iconColor = Colors.green; + bgColor = Colors.green.withValues(alpha: 0.2); + case ChannelType.hashtag: + icon = Icons.tag; + iconColor = Colors.blue; + bgColor = Colors.blue.withValues(alpha: 0.2); + case ChannelType.private: + icon = Icons.lock; + iconColor = Colors.blue; + bgColor = Colors.blue.withValues(alpha: 0.2); } return Card( From 5f9259e41f457cdc6c0bbb9108b1da23683a8ad8 Mon Sep 17 00:00:00 2001 From: Stempit Date: Wed, 13 May 2026 01:33:05 +0300 Subject: [PATCH 24/26] Add Russian regional presets --- lib/models/radio_settings.dart | 280 +++++++++++++++++++++++++++++++++ 1 file changed, 280 insertions(+) diff --git a/lib/models/radio_settings.dart b/lib/models/radio_settings.dart index 099d9201..81a65cd1 100644 --- a/lib/models/radio_settings.dart +++ b/lib/models/radio_settings.dart @@ -181,6 +181,286 @@ class RadioSettings { txPowerDbm: 14, ), ), + ( + 'Russia Biysk (BSK)', + RadioSettings( + frequencyMHz: 869.000, + bandwidth: LoRaBandwidth.bw62_5, + spreadingFactor: LoRaSpreadingFactor.sf8, + codingRate: LoRaCodingRate.cr4_5, + txPowerDbm: 20, + ), + ), + ( + 'Russia Cherepovets (CEE)', + RadioSettings( + frequencyMHz: 868.570, + bandwidth: LoRaBandwidth.bw62_5, + spreadingFactor: LoRaSpreadingFactor.sf7, + codingRate: LoRaCodingRate.cr4_8, + txPowerDbm: 20, + ), + ), + ( + 'Russia Chelyabinsk (CEK)', + RadioSettings( + frequencyMHz: 868.731, + bandwidth: LoRaBandwidth.bw62_5, + spreadingFactor: LoRaSpreadingFactor.sf8, + codingRate: LoRaCodingRate.cr4_6, + txPowerDbm: 20, + ), + ), + ( + 'Russia Nizhny Novgorod (GOJ)', + RadioSettings( + frequencyMHz: 868.731, + bandwidth: LoRaBandwidth.bw62_5, + spreadingFactor: LoRaSpreadingFactor.sf8, + codingRate: LoRaCodingRate.cr4_6, + txPowerDbm: 20, + ), + ), + ( + 'Russia Saratov (GSV)', + RadioSettings( + frequencyMHz: 864.281, + bandwidth: LoRaBandwidth.bw62_5, + spreadingFactor: LoRaSpreadingFactor.sf8, + codingRate: LoRaCodingRate.cr4_7, + txPowerDbm: 20, + ), + ), + ( + 'Russia St. Petersburg (LED)', + RadioSettings( + frequencyMHz: 868.856, + bandwidth: LoRaBandwidth.bw62_5, + spreadingFactor: LoRaSpreadingFactor.sf7, + codingRate: LoRaCodingRate.cr4_7, + txPowerDbm: 20, + ), + ), + ( + 'Russia Izhevsk (IJK)', + RadioSettings( + frequencyMHz: 868.732, + bandwidth: LoRaBandwidth.bw62_5, + spreadingFactor: LoRaSpreadingFactor.sf8, + codingRate: LoRaCodingRate.cr4_8, + txPowerDbm: 20, + ), + ), + ( + 'Russia Irkutsk (IKT)', + RadioSettings( + frequencyMHz: 868.731, + bandwidth: LoRaBandwidth.bw62_5, + spreadingFactor: LoRaSpreadingFactor.sf7, + codingRate: LoRaCodingRate.cr4_7, + txPowerDbm: 20, + ), + ), + ( + 'Russia Ivanovo (IWA)', + RadioSettings( + frequencyMHz: 868.731, + bandwidth: LoRaBandwidth.bw62_5, + spreadingFactor: LoRaSpreadingFactor.sf8, + codingRate: LoRaCodingRate.cr4_8, + txPowerDbm: 20, + ), + ), + ( + 'Russia Khabarovsk (KHV)', + RadioSettings( + frequencyMHz: 864.281, + bandwidth: LoRaBandwidth.bw62_5, + spreadingFactor: LoRaSpreadingFactor.sf8, + codingRate: LoRaCodingRate.cr4_6, + txPowerDbm: 20, + ), + ), + ( + 'Russia Tver (KLD)', + RadioSettings( + frequencyMHz: 869.169, + bandwidth: LoRaBandwidth.bw62_5, + spreadingFactor: LoRaSpreadingFactor.sf8, + codingRate: LoRaCodingRate.cr4_8, + txPowerDbm: 20, + ), + ), + ( + 'Russia Kaluga (KLF)', + RadioSettings( + frequencyMHz: 868.731, + bandwidth: LoRaBandwidth.bw62_5, + spreadingFactor: LoRaSpreadingFactor.sf7, + codingRate: LoRaCodingRate.cr4_7, + txPowerDbm: 20, + ), + ), + ( + 'Russia Samara (KUF)', + RadioSettings( + frequencyMHz: 864.281, + bandwidth: LoRaBandwidth.bw62_5, + spreadingFactor: LoRaSpreadingFactor.sf8, + codingRate: LoRaCodingRate.cr4_7, + txPowerDbm: 20, + ), + ), + ( + 'Russia Kirov (KVX)', + RadioSettings( + frequencyMHz: 868.731, + bandwidth: LoRaBandwidth.bw62_5, + spreadingFactor: LoRaSpreadingFactor.sf8, + codingRate: LoRaCodingRate.cr4_8, + txPowerDbm: 20, + ), + ), + ( + 'Russia Kazan (KZN)', + RadioSettings( + frequencyMHz: 868.731, + bandwidth: LoRaBandwidth.bw62_5, + spreadingFactor: LoRaSpreadingFactor.sf8, + codingRate: LoRaCodingRate.cr4_6, + txPowerDbm: 20, + ), + ), + ( + 'Russia Lipetsk (LPK)', + RadioSettings( + frequencyMHz: 868.950, + bandwidth: LoRaBandwidth.bw62_5, + spreadingFactor: LoRaSpreadingFactor.sf9, + codingRate: LoRaCodingRate.cr4_7, + txPowerDbm: 20, + ), + ), + ( + 'Russia Moscow (MOW)', + RadioSettings( + frequencyMHz: 868.731, + bandwidth: LoRaBandwidth.bw62_5, + spreadingFactor: LoRaSpreadingFactor.sf7, + codingRate: LoRaCodingRate.cr4_7, + txPowerDbm: 20, + ), + ), + ( + 'Russia Novosibirsk (OVB)', + RadioSettings( + frequencyMHz: 869.000, + bandwidth: LoRaBandwidth.bw62_5, + spreadingFactor: LoRaSpreadingFactor.sf9, + codingRate: LoRaCodingRate.cr4_8, + txPowerDbm: 20, + ), + ), + ( + 'Russia Rostov-on-Don (ROV)', + RadioSettings( + frequencyMHz: 868.731, + bandwidth: LoRaBandwidth.bw62_5, + spreadingFactor: LoRaSpreadingFactor.sf9, + codingRate: LoRaCodingRate.cr4_7, + txPowerDbm: 20, + ), + ), + ( + 'Russia Ryazan (RZN)', + RadioSettings( + frequencyMHz: 868.880, + bandwidth: LoRaBandwidth.bw62_5, + spreadingFactor: LoRaSpreadingFactor.sf9, + codingRate: LoRaCodingRate.cr4_5, + txPowerDbm: 20, + ), + ), + ( + 'Russia Yekaterinburg (SVX)', + RadioSettings( + frequencyMHz: 869.046, + bandwidth: LoRaBandwidth.bw62_5, + spreadingFactor: LoRaSpreadingFactor.sf7, + codingRate: LoRaCodingRate.cr4_7, + txPowerDbm: 20, + ), + ), + ( + 'Russia Tambov (TBW)', + RadioSettings( + frequencyMHz: 868.950, + bandwidth: LoRaBandwidth.bw62_5, + spreadingFactor: LoRaSpreadingFactor.sf10, + codingRate: LoRaCodingRate.cr4_5, + txPowerDbm: 20, + ), + ), + ( + 'Russia Tula (TYA)', + RadioSettings( + frequencyMHz: 868.731, + bandwidth: LoRaBandwidth.bw62_5, + spreadingFactor: LoRaSpreadingFactor.sf8, + codingRate: LoRaCodingRate.cr4_7, + txPowerDbm: 20, + ), + ), + ( + 'Russia Ufa (UFA)', + RadioSettings( + frequencyMHz: 868.732, + bandwidth: LoRaBandwidth.bw62_5, + spreadingFactor: LoRaSpreadingFactor.sf8, + codingRate: LoRaCodingRate.cr4_8, + txPowerDbm: 20, + ), + ), + ( + 'Russia Volgograd (VOG)', + RadioSettings( + frequencyMHz: 869.525, + bandwidth: LoRaBandwidth.bw62_5, + spreadingFactor: LoRaSpreadingFactor.sf7, + codingRate: LoRaCodingRate.cr4_7, + txPowerDbm: 20, + ), + ), + ( + 'Russia Voronezh (VOZ)', + RadioSettings( + frequencyMHz: 868.731, + bandwidth: LoRaBandwidth.bw62_5, + spreadingFactor: LoRaSpreadingFactor.sf8, + codingRate: LoRaCodingRate.cr4_6, + txPowerDbm: 20, + ), + ), + ( + 'Russia Lugansk (VSG)', + RadioSettings( + frequencyMHz: 868.825, + bandwidth: LoRaBandwidth.bw62_5, + spreadingFactor: LoRaSpreadingFactor.sf7, + codingRate: LoRaCodingRate.cr4_7, + txPowerDbm: 20, + ), + ), + ( + 'Russia Artyom (VVO)', + RadioSettings( + frequencyMHz: 864.281, + bandwidth: LoRaBandwidth.bw62_5, + spreadingFactor: LoRaSpreadingFactor.sf8, + codingRate: LoRaCodingRate.cr4_6, + txPowerDbm: 20, + ), + ), ( 'Switzerland', RadioSettings( From 352a6c427eb3ffff3c8dbfafaafe8b12df6628ab Mon Sep 17 00:00:00 2001 From: Stempit <3481898+Stempit@users.noreply.github.com> Date: Wed, 13 May 2026 01:49:51 +0300 Subject: [PATCH 25/26] Reorder alphabetically --- lib/models/radio_settings.dart | 150 +++++++++++++++------------------ 1 file changed, 70 insertions(+), 80 deletions(-) diff --git a/lib/models/radio_settings.dart b/lib/models/radio_settings.dart index 81a65cd1..e74cf209 100644 --- a/lib/models/radio_settings.dart +++ b/lib/models/radio_settings.dart @@ -181,6 +181,16 @@ class RadioSettings { txPowerDbm: 14, ), ), + ( + 'Russia Artyom (VVO)', + RadioSettings( + frequencyMHz: 864.281, + bandwidth: LoRaBandwidth.bw62_5, + spreadingFactor: LoRaSpreadingFactor.sf8, + codingRate: LoRaCodingRate.cr4_6, + txPowerDbm: 20, + ), + ), ( 'Russia Biysk (BSK)', RadioSettings( @@ -191,16 +201,6 @@ class RadioSettings { txPowerDbm: 20, ), ), - ( - 'Russia Cherepovets (CEE)', - RadioSettings( - frequencyMHz: 868.570, - bandwidth: LoRaBandwidth.bw62_5, - spreadingFactor: LoRaSpreadingFactor.sf7, - codingRate: LoRaCodingRate.cr4_8, - txPowerDbm: 20, - ), - ), ( 'Russia Chelyabinsk (CEK)', RadioSettings( @@ -212,41 +212,11 @@ class RadioSettings { ), ), ( - 'Russia Nizhny Novgorod (GOJ)', + 'Russia Cherepovets (CEE)', RadioSettings( - frequencyMHz: 868.731, - bandwidth: LoRaBandwidth.bw62_5, - spreadingFactor: LoRaSpreadingFactor.sf8, - codingRate: LoRaCodingRate.cr4_6, - txPowerDbm: 20, - ), - ), - ( - 'Russia Saratov (GSV)', - RadioSettings( - frequencyMHz: 864.281, - bandwidth: LoRaBandwidth.bw62_5, - spreadingFactor: LoRaSpreadingFactor.sf8, - codingRate: LoRaCodingRate.cr4_7, - txPowerDbm: 20, - ), - ), - ( - 'Russia St. Petersburg (LED)', - RadioSettings( - frequencyMHz: 868.856, + frequencyMHz: 868.570, bandwidth: LoRaBandwidth.bw62_5, spreadingFactor: LoRaSpreadingFactor.sf7, - codingRate: LoRaCodingRate.cr4_7, - txPowerDbm: 20, - ), - ), - ( - 'Russia Izhevsk (IJK)', - RadioSettings( - frequencyMHz: 868.732, - bandwidth: LoRaBandwidth.bw62_5, - spreadingFactor: LoRaSpreadingFactor.sf8, codingRate: LoRaCodingRate.cr4_8, txPowerDbm: 20, ), @@ -272,19 +242,9 @@ class RadioSettings { ), ), ( - 'Russia Khabarovsk (KHV)', + 'Russia Izhevsk (IJK)', RadioSettings( - frequencyMHz: 864.281, - bandwidth: LoRaBandwidth.bw62_5, - spreadingFactor: LoRaSpreadingFactor.sf8, - codingRate: LoRaCodingRate.cr4_6, - txPowerDbm: 20, - ), - ), - ( - 'Russia Tver (KLD)', - RadioSettings( - frequencyMHz: 869.169, + frequencyMHz: 868.732, bandwidth: LoRaBandwidth.bw62_5, spreadingFactor: LoRaSpreadingFactor.sf8, codingRate: LoRaCodingRate.cr4_8, @@ -302,12 +262,22 @@ class RadioSettings { ), ), ( - 'Russia Samara (KUF)', + 'Russia Kazan (KZN)', + RadioSettings( + frequencyMHz: 868.731, + bandwidth: LoRaBandwidth.bw62_5, + spreadingFactor: LoRaSpreadingFactor.sf8, + codingRate: LoRaCodingRate.cr4_6, + txPowerDbm: 20, + ), + ), + ( + 'Russia Khabarovsk (KHV)', RadioSettings( frequencyMHz: 864.281, bandwidth: LoRaBandwidth.bw62_5, spreadingFactor: LoRaSpreadingFactor.sf8, - codingRate: LoRaCodingRate.cr4_7, + codingRate: LoRaCodingRate.cr4_6, txPowerDbm: 20, ), ), @@ -321,16 +291,6 @@ class RadioSettings { txPowerDbm: 20, ), ), - ( - 'Russia Kazan (KZN)', - RadioSettings( - frequencyMHz: 868.731, - bandwidth: LoRaBandwidth.bw62_5, - spreadingFactor: LoRaSpreadingFactor.sf8, - codingRate: LoRaCodingRate.cr4_6, - txPowerDbm: 20, - ), - ), ( 'Russia Lipetsk (LPK)', RadioSettings( @@ -351,6 +311,16 @@ class RadioSettings { txPowerDbm: 20, ), ), + ( + 'Russia Nizhny Novgorod (GOJ)', + RadioSettings( + frequencyMHz: 868.731, + bandwidth: LoRaBandwidth.bw62_5, + spreadingFactor: LoRaSpreadingFactor.sf8, + codingRate: LoRaCodingRate.cr4_6, + txPowerDbm: 20, + ), + ), ( 'Russia Novosibirsk (OVB)', RadioSettings( @@ -382,9 +352,29 @@ class RadioSettings { ), ), ( - 'Russia Yekaterinburg (SVX)', + 'Russia Samara (KUF)', RadioSettings( - frequencyMHz: 869.046, + frequencyMHz: 864.281, + bandwidth: LoRaBandwidth.bw62_5, + spreadingFactor: LoRaSpreadingFactor.sf8, + codingRate: LoRaCodingRate.cr4_7, + txPowerDbm: 20, + ), + ), + ( + 'Russia Saratov (GSV)', + RadioSettings( + frequencyMHz: 864.281, + bandwidth: LoRaBandwidth.bw62_5, + spreadingFactor: LoRaSpreadingFactor.sf8, + codingRate: LoRaCodingRate.cr4_7, + txPowerDbm: 20, + ), + ), + ( + 'Russia St. Petersburg (LED)', + RadioSettings( + frequencyMHz: 868.856, bandwidth: LoRaBandwidth.bw62_5, spreadingFactor: LoRaSpreadingFactor.sf7, codingRate: LoRaCodingRate.cr4_7, @@ -411,6 +401,16 @@ class RadioSettings { txPowerDbm: 20, ), ), + ( + 'Russia Tver (KLD)', + RadioSettings( + frequencyMHz: 869.169, + bandwidth: LoRaBandwidth.bw62_5, + spreadingFactor: LoRaSpreadingFactor.sf8, + codingRate: LoRaCodingRate.cr4_8, + txPowerDbm: 20, + ), + ), ( 'Russia Ufa (UFA)', RadioSettings( @@ -442,25 +442,15 @@ class RadioSettings { ), ), ( - 'Russia Lugansk (VSG)', + 'Russia Yekaterinburg (SVX)', RadioSettings( - frequencyMHz: 868.825, + frequencyMHz: 869.046, bandwidth: LoRaBandwidth.bw62_5, spreadingFactor: LoRaSpreadingFactor.sf7, codingRate: LoRaCodingRate.cr4_7, txPowerDbm: 20, ), ), - ( - 'Russia Artyom (VVO)', - RadioSettings( - frequencyMHz: 864.281, - bandwidth: LoRaBandwidth.bw62_5, - spreadingFactor: LoRaSpreadingFactor.sf8, - codingRate: LoRaCodingRate.cr4_6, - txPowerDbm: 20, - ), - ), ( 'Switzerland', RadioSettings( From 72448f67d0f6210ef04536ef869a39938e0744be Mon Sep 17 00:00:00 2001 From: HDDen <62592944+HDDen@users.noreply.github.com> Date: Sun, 17 May 2026 13:20:49 +0300 Subject: [PATCH 26/26] Room-server: fixed first message letters trim --- lib/connector/meshcore_connector.dart | 28 +++++++++++++++------------ lib/screens/chat_screen.dart | 12 +++++------- 2 files changed, 21 insertions(+), 19 deletions(-) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index 9a316897..7cbc8ff0 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -4462,9 +4462,7 @@ class MeshCoreConnector extends ChangeNotifier { } else if (contact?.type == advTypeRoom) { _notificationService.showMessageNotification( contactName: contact?.name ?? 'Unknown Room', - message: message.text.length > 4 - ? message.text.substring(4) - : message.text, + message: message.text, contactId: message.senderKeyHex, badgeCount: getTotalUnreadCount(), ); @@ -4511,16 +4509,24 @@ class MeshCoreConnector extends ChangeNotifier { timestampRaw * 1000, ); - if (txtType == 2) { - reader.skipBytes(4); // Skip extra 4 bytes for signed/plain variants + final flags = txtType; + final shiftedType = flags >> 2; + final rawType = flags; + final isSigned = shiftedType == txtTypeSigned || rawType == txtTypeSigned; + final Uint8List? roomAuthorPrefix; + if (isSigned) { + // Room-server pushed posts use signed/plain contact messages where this + // 4-byte "signature" field is actually the original author's pubkey + // prefix. Keep it as metadata; the text starts after these bytes. + roomAuthorPrefix = reader.readBytes(4); + } else { + roomAuthorPrefix = null; } final msgText = reader.readCString(); - final flags = txtType; - final shiftedType = flags >> 2; - final rawType = flags; - final isPlain = shiftedType == txtTypePlain || rawType == txtTypePlain; + final isPlain = + shiftedType == txtTypePlain || rawType == txtTypePlain || isSigned; final isCli = shiftedType == txtTypeCliData || rawType == txtTypeCliData; if (!isPlain && !isCli) { appLogger.warn( @@ -4557,9 +4563,7 @@ class MeshCoreConnector extends ChangeNotifier { status: MessageStatus.delivered, pathLength: pathLength == 0xFF ? 0 : pathLength, pathBytes: Uint8List(0), - fourByteRoomContactKey: msgText.length >= 4 - ? Uint8List.fromList(msgText.substring(0, 4).codeUnits) - : null, + fourByteRoomContactKey: roomAuthorPrefix, ); } catch (e) { appLogger.warn('Error parsing contact direct message: $e'); diff --git a/lib/screens/chat_screen.dart b/lib/screens/chat_screen.dart index 8d3bc66c..1548b44e 100644 --- a/lib/screens/chat_screen.dart +++ b/lib/screens/chat_screen.dart @@ -485,6 +485,8 @@ class _ChatScreenState extends State { final message = reversedMessages[messageIndex]; String fourByteHex = ''; if (contact.type == advTypeRoom) { + // Room-server messages carry the original author's 4-byte prefix + // separately from message.text; use it only for resolving the name. contact = _resolveContactFrom4Bytes( connector, message.fourByteRoomContactKey.isEmpty @@ -509,7 +511,6 @@ class _ChatScreenState extends State { ? "${contact.name} [$fourByteHex]" : contact.name, sourceId: widget.contact.publicKeyHex, - isRoomServer: resolvedContact.type == advTypeRoom, textScale: textScale, onTap: () => _openMessagePath(message, contact), onLongPress: () => _showMessageActions(message, contact), @@ -1721,7 +1722,6 @@ class _ChatScreenState extends State { class _MessageBubble extends StatelessWidget { final Message message; final String senderName; - final bool isRoomServer; final VoidCallback? onTap; final VoidCallback? onLongPress; final void Function(Message message, String emoji)? onRetryReaction; @@ -1732,7 +1732,6 @@ class _MessageBubble extends StatelessWidget { required this.message, required this.senderName, required this.sourceId, - required this.isRoomServer, required this.textScale, this.onTap, this.onLongPress, @@ -1758,10 +1757,9 @@ class _MessageBubble extends StatelessWidget { : (isOutgoing ? colorScheme.onPrimary : colorScheme.onSurface); final metaColor = textColor.withValues(alpha: 0.7); const bodyFontSize = 14.0; - String messageText = message.text; - if (isRoomServer && !isOutgoing) { - messageText = message.text.substring(4.clamp(0, message.text.length)); - } + // Do not strip room-server author bytes here: the parser stores them in + // fourByteRoomContactKey, so message.text is safe to render as-is. + final messageText = message.text; final translatedDisplayText = message.translatedText != null && message.translatedText!.trim().isNotEmpty