mirror of
https://github.com/zjs81/meshcore-open.git
synced 2026-07-24 17:49:03 +10:00
@@ -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',
|
||||||
|
|||||||
@@ -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(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user