Merge pull request #145 from pioneer/unread-peoplefirst

Unread badges for tabs
This commit is contained in:
zjs81
2026-05-12 09:53:33 -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, RepeaterBatterySnapshot> _repeaterBatterySnapshots = {};
bool _unreadStateLoaded = false;
int _cachedContactsUnreadTotal = 0;
int _cachedChannelsUnreadTotal = 0;
final Map<String, _RepeaterAckContext> _pendingRepeaterAcks = {};
String? _activeContactKey;
int? _activeChannelIndex;
@@ -606,16 +608,42 @@ class MeshCoreConnector extends ChangeNotifier {
int getTotalUnreadCount() {
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;
return getTotalContactsUnreadCount() + getTotalChannelsUnreadCount();
}
int getTotalContactsUnreadCount() {
if (!_unreadStateLoaded) return 0;
return _cachedContactsUnreadTotal;
}
int getTotalChannelsUnreadCount() {
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) {
@@ -649,11 +677,13 @@ class MeshCoreConnector extends ChangeNotifier {
..clear()
..addAll(await _unreadStore.loadContactUnreadCount());
_unreadStateLoaded = true;
_recalculateCachedUnreadTotals();
notifyListeners();
}
Future<void> loadCachedChannels() async {
_cachedChannels = await _channelStore.loadChannels();
_recalculateCachedChannelsUnreadTotal();
}
void setActiveContact(String? contactKeyHex) {
@@ -680,6 +710,8 @@ class MeshCoreConnector extends ChangeNotifier {
final previousCount = _contactUnreadCount[contactKeyHex] ?? 0;
if (previousCount > 0) {
_contactUnreadCount[contactKeyHex] = 0;
_cachedContactsUnreadTotal = (_cachedContactsUnreadTotal - previousCount)
.clamp(0, _cachedContactsUnreadTotal);
_appDebugLogService?.info(
'Contact $contactKeyHex marked as read (was $previousCount unread)',
tag: 'Unread',
@@ -721,6 +753,8 @@ class MeshCoreConnector extends ChangeNotifier {
if (channel != null && channel.unreadCount > 0) {
final previousCount = channel.unreadCount;
channel.unreadCount = 0;
_cachedChannelsUnreadTotal = (_cachedChannelsUnreadTotal - previousCount)
.clamp(0, _cachedChannelsUnreadTotal);
_appDebugLogService?.info(
'Channel ${channel.name.isNotEmpty ? channel.name : channelIndex} marked as read (was $previousCount unread)',
tag: 'Unread',
@@ -3123,6 +3157,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<String, int>.from(_contactUnreadCount),
@@ -3516,6 +3553,7 @@ class MeshCoreConnector extends ChangeNotifier {
// Cache channels for offline use
_cachedChannels = List<Channel>.from(_channels);
unawaited(_channelStore.saveChannels(_channels));
_recalculateCachedChannelsUnreadTotal();
// Apply ordering and notify UI
_applyChannelOrder();
@@ -4068,6 +4106,9 @@ class MeshCoreConnector extends ChangeNotifier {
_handleDiscovery(contact, frame, noNotify: true, addActive: true);
if (contact.type == advTypeRepeater) {
final removedCount = _contactUnreadCount[contact.publicKeyHex] ?? 0;
_cachedContactsUnreadTotal = (_cachedContactsUnreadTotal - removedCount)
.clamp(0, _cachedContactsUnreadTotal);
_contactUnreadCount.remove(contact.publicKeyHex);
_unreadStore.saveContactUnreadCount(
Map<String, int>.from(_contactUnreadCount),
@@ -4158,6 +4199,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<String, int>.from(_contactUnreadCount),
@@ -5170,6 +5214,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',
@@ -5214,6 +5259,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',
+2
View File
@@ -360,6 +360,8 @@ class _ChannelsScreenState extends State<ChannelsScreen>
selectedIndex: 1,
onDestinationSelected: (index) =>
_handleQuickSwitch(index, context),
contactsUnreadCount: connector.getTotalContactsUnreadCount(),
channelsUnreadCount: connector.getTotalChannelsUnreadCount(),
),
),
),
+2
View File
@@ -430,6 +430,8 @@ class _ContactsScreenState extends State<ContactsScreen>
selectedIndex: 0,
onDestinationSelected: (index) =>
_handleQuickSwitch(index, context),
contactsUnreadCount: connector.getTotalContactsUnreadCount(),
channelsUnreadCount: connector.getTotalChannelsUnreadCount(),
),
),
),
@@ -539,6 +539,12 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
child: QuickSwitchBar(
selectedIndex: 2,
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,
onDestinationSelected: (index) =>
_handleQuickSwitch(index, context),
contactsUnreadCount: connector.getTotalContactsUnreadCount(),
channelsUnreadCount: connector.getTotalChannelsUnreadCount(),
),
),
floatingActionButton: FloatingActionButton(
+44 -2
View File
@@ -6,11 +6,15 @@ import '../l10n/l10n.dart';
class QuickSwitchBar extends StatelessWidget {
final int selectedIndex;
final ValueChanged<int> onDestinationSelected;
final int contactsUnreadCount;
final int channelsUnreadCount;
const QuickSwitchBar({
super.key,
required this.selectedIndex,
required this.onDestinationSelected,
this.contactsUnreadCount = 0,
this.channelsUnreadCount = 0,
});
@override
@@ -62,15 +66,30 @@ class QuickSwitchBar extends StatelessWidget {
onDestinationSelected: onDestinationSelected,
destinations: [
NavigationDestination(
icon: const Icon(Icons.people_outline),
icon: _buildIconWithBadge(
const Icon(Icons.people_outline),
contactsUnreadCount,
),
selectedIcon: _buildIconWithBadge(
const Icon(Icons.people),
contactsUnreadCount,
),
label: context.l10n.nav_contacts,
),
NavigationDestination(
icon: const Icon(Icons.tag),
icon: _buildIconWithBadge(
const Icon(Icons.tag),
channelsUnreadCount,
),
selectedIcon: _buildIconWithBadge(
const Icon(Icons.tag),
channelsUnreadCount,
),
label: context.l10n.nav_channels,
),
NavigationDestination(
icon: const Icon(Icons.map_outlined),
selectedIcon: const Icon(Icons.map),
label: context.l10n.nav_map,
),
],
@@ -81,4 +100,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,
),
),
),
],
);
}
}