Merge pull request #448 from zjs81/main

Main
This commit is contained in:
zjs81
2026-05-12 09:54:39 -07:00
committed by GitHub
6 changed files with 112 additions and 12 deletions
+56 -10
View File
@@ -302,6 +302,8 @@ class MeshCoreConnector extends ChangeNotifier {
final Map<String, int> _contactUnreadCount = {}; final Map<String, int> _contactUnreadCount = {};
final Map<String, RepeaterBatterySnapshot> _repeaterBatterySnapshots = {}; final Map<String, RepeaterBatterySnapshot> _repeaterBatterySnapshots = {};
bool _unreadStateLoaded = false; bool _unreadStateLoaded = false;
int _cachedContactsUnreadTotal = 0;
int _cachedChannelsUnreadTotal = 0;
final Map<String, _RepeaterAckContext> _pendingRepeaterAcks = {}; final Map<String, _RepeaterAckContext> _pendingRepeaterAcks = {};
String? _activeContactKey; String? _activeContactKey;
int? _activeChannelIndex; int? _activeChannelIndex;
@@ -606,16 +608,42 @@ class MeshCoreConnector extends ChangeNotifier {
int getTotalUnreadCount() { int getTotalUnreadCount() {
if (!_unreadStateLoaded) return 0; if (!_unreadStateLoaded) return 0;
var total = 0; return getTotalContactsUnreadCount() + getTotalChannelsUnreadCount();
// Count unread contact messages }
for (final contact in _contacts) {
total += getUnreadCountForContact(contact); int getTotalContactsUnreadCount() {
} if (!_unreadStateLoaded) return 0;
// Count unread channel messages return _cachedContactsUnreadTotal;
for (final channelIndex in _channelMessages.keys) { }
total += getUnreadCountForChannelIndex(channelIndex);
} int getTotalChannelsUnreadCount() {
return total; if (!_unreadStateLoaded) return 0;
return _cachedChannelsUnreadTotal;
}
/// Recalculates both cached unread totals from scratch.
/// Called when unread state is first loaded.
void _recalculateCachedUnreadTotals() {
_recalculateCachedContactsUnreadTotal();
_recalculateCachedChannelsUnreadTotal();
}
void _recalculateCachedContactsUnreadTotal() {
int total = 0;
_contactUnreadCount.forEach((contactKeyHex, count) {
if (_shouldTrackUnreadForContactKey(contactKeyHex)) {
total += count;
}
});
_cachedContactsUnreadTotal = total;
}
void _recalculateCachedChannelsUnreadTotal() {
final allChannels = _channels.isNotEmpty ? _channels : _cachedChannels;
_cachedChannelsUnreadTotal = allChannels.fold(
0,
(total, ch) => total + ch.unreadCount,
);
} }
bool isChannelSmazEnabled(int channelIndex) { bool isChannelSmazEnabled(int channelIndex) {
@@ -649,11 +677,13 @@ class MeshCoreConnector extends ChangeNotifier {
..clear() ..clear()
..addAll(await _unreadStore.loadContactUnreadCount()); ..addAll(await _unreadStore.loadContactUnreadCount());
_unreadStateLoaded = true; _unreadStateLoaded = true;
_recalculateCachedUnreadTotals();
notifyListeners(); notifyListeners();
} }
Future<void> loadCachedChannels() async { Future<void> loadCachedChannels() async {
_cachedChannels = await _channelStore.loadChannels(); _cachedChannels = await _channelStore.loadChannels();
_recalculateCachedChannelsUnreadTotal();
} }
void setActiveContact(String? contactKeyHex) { void setActiveContact(String? contactKeyHex) {
@@ -680,6 +710,8 @@ class MeshCoreConnector extends ChangeNotifier {
final previousCount = _contactUnreadCount[contactKeyHex] ?? 0; final previousCount = _contactUnreadCount[contactKeyHex] ?? 0;
if (previousCount > 0) { if (previousCount > 0) {
_contactUnreadCount[contactKeyHex] = 0; _contactUnreadCount[contactKeyHex] = 0;
_cachedContactsUnreadTotal = (_cachedContactsUnreadTotal - previousCount)
.clamp(0, _cachedContactsUnreadTotal);
_appDebugLogService?.info( _appDebugLogService?.info(
'Contact $contactKeyHex marked as read (was $previousCount unread)', 'Contact $contactKeyHex marked as read (was $previousCount unread)',
tag: 'Unread', tag: 'Unread',
@@ -721,6 +753,8 @@ class MeshCoreConnector extends ChangeNotifier {
if (channel != null && channel.unreadCount > 0) { if (channel != null && channel.unreadCount > 0) {
final previousCount = channel.unreadCount; final previousCount = channel.unreadCount;
channel.unreadCount = 0; channel.unreadCount = 0;
_cachedChannelsUnreadTotal = (_cachedChannelsUnreadTotal - previousCount)
.clamp(0, _cachedChannelsUnreadTotal);
_appDebugLogService?.info( _appDebugLogService?.info(
'Channel ${channel.name.isNotEmpty ? channel.name : channelIndex} marked as read (was $previousCount unread)', 'Channel ${channel.name.isNotEmpty ? channel.name : channelIndex} marked as read (was $previousCount unread)',
tag: 'Unread', tag: 'Unread',
@@ -3123,6 +3157,9 @@ class MeshCoreConnector extends ChangeNotifier {
unawaited(_persistContacts()); unawaited(_persistContacts());
_conversations.remove(contact.publicKeyHex); _conversations.remove(contact.publicKeyHex);
_loadedConversationKeys.remove(contact.publicKeyHex); _loadedConversationKeys.remove(contact.publicKeyHex);
final removedCount = _contactUnreadCount[contact.publicKeyHex] ?? 0;
_cachedContactsUnreadTotal = (_cachedContactsUnreadTotal - removedCount)
.clamp(0, _cachedContactsUnreadTotal);
_contactUnreadCount.remove(contact.publicKeyHex); _contactUnreadCount.remove(contact.publicKeyHex);
_unreadStore.saveContactUnreadCount( _unreadStore.saveContactUnreadCount(
Map<String, int>.from(_contactUnreadCount), Map<String, int>.from(_contactUnreadCount),
@@ -3516,6 +3553,7 @@ class MeshCoreConnector extends ChangeNotifier {
// Cache channels for offline use // Cache channels for offline use
_cachedChannels = List<Channel>.from(_channels); _cachedChannels = List<Channel>.from(_channels);
unawaited(_channelStore.saveChannels(_channels)); unawaited(_channelStore.saveChannels(_channels));
_recalculateCachedChannelsUnreadTotal();
// Apply ordering and notify UI // Apply ordering and notify UI
_applyChannelOrder(); _applyChannelOrder();
@@ -4068,6 +4106,9 @@ class MeshCoreConnector extends ChangeNotifier {
_handleDiscovery(contact, frame, noNotify: true, addActive: true); _handleDiscovery(contact, frame, noNotify: true, addActive: true);
if (contact.type == advTypeRepeater) { if (contact.type == advTypeRepeater) {
final removedCount = _contactUnreadCount[contact.publicKeyHex] ?? 0;
_cachedContactsUnreadTotal = (_cachedContactsUnreadTotal - removedCount)
.clamp(0, _cachedContactsUnreadTotal);
_contactUnreadCount.remove(contact.publicKeyHex); _contactUnreadCount.remove(contact.publicKeyHex);
_unreadStore.saveContactUnreadCount( _unreadStore.saveContactUnreadCount(
Map<String, int>.from(_contactUnreadCount), Map<String, int>.from(_contactUnreadCount),
@@ -4158,6 +4199,9 @@ class MeshCoreConnector extends ChangeNotifier {
} }
if (contact.type == advTypeRepeater) { if (contact.type == advTypeRepeater) {
final removedCount = _contactUnreadCount[contact.publicKeyHex] ?? 0;
_cachedContactsUnreadTotal = (_cachedContactsUnreadTotal - removedCount)
.clamp(0, _cachedContactsUnreadTotal);
_contactUnreadCount.remove(contact.publicKeyHex); _contactUnreadCount.remove(contact.publicKeyHex);
_unreadStore.saveContactUnreadCount( _unreadStore.saveContactUnreadCount(
Map<String, int>.from(_contactUnreadCount), Map<String, int>.from(_contactUnreadCount),
@@ -5170,6 +5214,7 @@ class MeshCoreConnector extends ChangeNotifier {
final channel = _findChannelByIndex(channelIndex); final channel = _findChannelByIndex(channelIndex);
if (channel != null) { if (channel != null) {
channel.unreadCount++; channel.unreadCount++;
_cachedChannelsUnreadTotal++;
_appDebugLogService?.info( _appDebugLogService?.info(
'Channel ${channel.name.isNotEmpty ? channel.name : channelIndex} unread count incremented to ${channel.unreadCount}', 'Channel ${channel.name.isNotEmpty ? channel.name : channelIndex} unread count incremented to ${channel.unreadCount}',
tag: 'Unread', tag: 'Unread',
@@ -5214,6 +5259,7 @@ class MeshCoreConnector extends ChangeNotifier {
final currentCount = _contactUnreadCount[contactKey] ?? 0; final currentCount = _contactUnreadCount[contactKey] ?? 0;
_contactUnreadCount[contactKey] = currentCount + 1; _contactUnreadCount[contactKey] = currentCount + 1;
_cachedContactsUnreadTotal++;
_appDebugLogService?.info( _appDebugLogService?.info(
'Contact $contactKey unread count incremented to ${currentCount + 1}', 'Contact $contactKey unread count incremented to ${currentCount + 1}',
tag: 'Unread', tag: 'Unread',
+2
View File
@@ -360,6 +360,8 @@ class _ChannelsScreenState extends State<ChannelsScreen>
selectedIndex: 1, selectedIndex: 1,
onDestinationSelected: (index) => onDestinationSelected: (index) =>
_handleQuickSwitch(index, context), _handleQuickSwitch(index, context),
contactsUnreadCount: connector.getTotalContactsUnreadCount(),
channelsUnreadCount: connector.getTotalChannelsUnreadCount(),
), ),
), ),
), ),
+2
View File
@@ -430,6 +430,8 @@ class _ContactsScreenState extends State<ContactsScreen>
selectedIndex: 0, selectedIndex: 0,
onDestinationSelected: (index) => onDestinationSelected: (index) =>
_handleQuickSwitch(index, context), _handleQuickSwitch(index, context),
contactsUnreadCount: connector.getTotalContactsUnreadCount(),
channelsUnreadCount: connector.getTotalChannelsUnreadCount(),
), ),
), ),
), ),
@@ -539,6 +539,12 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
child: QuickSwitchBar( child: QuickSwitchBar(
selectedIndex: 2, selectedIndex: 2,
onDestinationSelected: (index) => _handleQuickSwitch(index, context), onDestinationSelected: (index) => _handleQuickSwitch(index, context),
contactsUnreadCount: context
.watch<MeshCoreConnector>()
.getTotalContactsUnreadCount(),
channelsUnreadCount: context
.watch<MeshCoreConnector>()
.getTotalChannelsUnreadCount(),
), ),
), ),
); );
+2
View File
@@ -676,6 +676,8 @@ class _MapScreenState extends State<MapScreen> {
selectedIndex: 2, selectedIndex: 2,
onDestinationSelected: (index) => onDestinationSelected: (index) =>
_handleQuickSwitch(index, context), _handleQuickSwitch(index, context),
contactsUnreadCount: connector.getTotalContactsUnreadCount(),
channelsUnreadCount: connector.getTotalChannelsUnreadCount(),
), ),
), ),
floatingActionButton: FloatingActionButton( floatingActionButton: FloatingActionButton(
+44 -2
View File
@@ -6,11 +6,15 @@ import '../l10n/l10n.dart';
class QuickSwitchBar extends StatelessWidget { class QuickSwitchBar extends StatelessWidget {
final int selectedIndex; final int selectedIndex;
final ValueChanged<int> onDestinationSelected; final ValueChanged<int> onDestinationSelected;
final int contactsUnreadCount;
final int channelsUnreadCount;
const QuickSwitchBar({ const QuickSwitchBar({
super.key, super.key,
required this.selectedIndex, required this.selectedIndex,
required this.onDestinationSelected, required this.onDestinationSelected,
this.contactsUnreadCount = 0,
this.channelsUnreadCount = 0,
}); });
@override @override
@@ -62,15 +66,30 @@ class QuickSwitchBar extends StatelessWidget {
onDestinationSelected: onDestinationSelected, onDestinationSelected: onDestinationSelected,
destinations: [ destinations: [
NavigationDestination( 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, label: context.l10n.nav_contacts,
), ),
NavigationDestination( 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, label: context.l10n.nav_channels,
), ),
NavigationDestination( NavigationDestination(
icon: const Icon(Icons.map_outlined), icon: const Icon(Icons.map_outlined),
selectedIcon: const Icon(Icons.map),
label: context.l10n.nav_map, label: context.l10n.nav_map,
), ),
], ],
@@ -81,4 +100,27 @@ class QuickSwitchBar extends StatelessWidget {
), ),
); );
} }
Widget _buildIconWithBadge(Icon icon, int count) {
if (count <= 0) return icon;
return Stack(
clipBehavior: Clip.none,
children: [
icon,
Positioned(
right: -2,
top: -2,
child: Container(
width: 8,
height: 8,
decoration: const BoxDecoration(
color: Colors.redAccent,
shape: BoxShape.circle,
),
),
),
],
);
}
} }