From bfd15065f0f3f14f530396291d760f0100ae5514 Mon Sep 17 00:00:00 2001 From: Janez T Date: Sun, 8 Mar 2026 20:15:48 +0100 Subject: [PATCH] Add message filters and counts --- lib/providers/messages_provider.dart | 53 ++++++++++++++++++++ lib/screens/contacts_tab.dart | 21 +++++++- lib/widgets/contacts/contact_tile.dart | 67 ++++++++++++++++++++++++++ 3 files changed, 139 insertions(+), 2 deletions(-) diff --git a/lib/providers/messages_provider.dart b/lib/providers/messages_provider.dart index 04d304e..96507ab 100644 --- a/lib/providers/messages_provider.dart +++ b/lib/providers/messages_provider.dart @@ -193,6 +193,59 @@ class MessagesProvider with ChangeNotifier { .where((m) => !m.isRead && !m.isSentMessage && !m.isSystemMessage) .length; + int getMessageCountForDestination(Contact contact) => _messages + .where((message) => _isMessageForDestination(message, contact)) + .length; + + int getUnreadCountForDestination(Contact contact) => _messages + .where( + (message) => + _isMessageForDestination(message, contact) && + !message.isRead && + !message.isSentMessage && + !message.isSystemMessage, + ) + .length; + + bool _isMessageForDestination(Message message, Contact contact) { + if (message.isSystemMessage) return false; + + if (contact.isChannel) { + final channelIdx = contact.publicKey.length > 1 + ? contact.publicKey[1] + : 0; + return message.isChannelMessage && + (message.channelIdx ?? 0) == channelIdx; + } + + if (!message.isContactMessage || contact.publicKey.length < 6) { + return false; + } + + final selectedPrefix = contact.publicKey.sublist(0, 6); + + if (message.recipientPublicKey != null && + message.recipientPublicKey!.length >= 6 && + _matchesPrefix(message.recipientPublicKey!, selectedPrefix)) { + return true; + } + + if (message.senderPublicKeyPrefix != null && + message.senderPublicKeyPrefix!.length >= 6 && + _matchesPrefix(message.senderPublicKeyPrefix!, selectedPrefix)) { + return true; + } + + return false; + } + + bool _matchesPrefix(Uint8List key, Uint8List prefix) { + for (var i = 0; i < 6; i++) { + if (key[i] != prefix[i]) return false; + } + return true; + } + /// Initialize and load persisted messages Future initialize() async { if (_isInitialized) return; diff --git a/lib/screens/contacts_tab.dart b/lib/screens/contacts_tab.dart index 3dd82b7..ab098aa 100644 --- a/lib/screens/contacts_tab.dart +++ b/lib/screens/contacts_tab.dart @@ -7,6 +7,7 @@ import '../models/contact.dart'; import '../providers/contacts_provider.dart'; import '../providers/app_provider.dart'; import '../providers/connection_provider.dart'; +import '../providers/messages_provider.dart'; import '../widgets/contacts/contact_tile.dart'; import '../widgets/contacts/add_channel_dialog.dart'; @@ -201,8 +202,8 @@ class _ContactsTabState extends State { final l10n = AppLocalizations.of(context)!; return Scaffold( - body: Consumer( - builder: (context, contactsProvider, child) { + body: Consumer2( + builder: (context, contactsProvider, messagesProvider, child) { final chatContacts = _sortContactsByDistance( contactsProvider.chatContacts, ); @@ -284,6 +285,10 @@ class _ContactsTabState extends State { calculateDistance: _calculateDistanceInMeters, formatDistance: _formatDistance, onNavigateToMap: widget.onNavigateToMap, + messageCount: messagesProvider + .getMessageCountForDestination(contact), + unreadMessageCount: messagesProvider + .getUnreadCountForDestination(contact), ), ), const Divider(height: 32), @@ -303,6 +308,10 @@ class _ContactsTabState extends State { calculateDistance: _calculateDistanceInMeters, formatDistance: _formatDistance, onNavigateToMap: widget.onNavigateToMap, + messageCount: messagesProvider + .getMessageCountForDestination(contact), + unreadMessageCount: messagesProvider + .getUnreadCountForDestination(contact), ), ), const Divider(height: 32), @@ -322,6 +331,10 @@ class _ContactsTabState extends State { calculateDistance: _calculateDistanceInMeters, formatDistance: _formatDistance, onNavigateToMap: widget.onNavigateToMap, + messageCount: messagesProvider + .getMessageCountForDestination(contact), + unreadMessageCount: messagesProvider + .getUnreadCountForDestination(contact), ), ), const Divider(height: 32), @@ -341,6 +354,10 @@ class _ContactsTabState extends State { calculateDistance: _calculateDistanceInMeters, formatDistance: _formatDistance, onNavigateToMap: widget.onNavigateToMap, + messageCount: messagesProvider + .getMessageCountForDestination(contact), + unreadMessageCount: messagesProvider + .getUnreadCountForDestination(contact), ), ), ], diff --git a/lib/widgets/contacts/contact_tile.dart b/lib/widgets/contacts/contact_tile.dart index db80347..330e422 100644 --- a/lib/widgets/contacts/contact_tile.dart +++ b/lib/widgets/contacts/contact_tile.dart @@ -21,6 +21,8 @@ class ContactTile extends StatelessWidget { final double Function(double, double, double, double)? calculateDistance; final String Function(double)? formatDistance; final VoidCallback? onNavigateToMap; + final int messageCount; + final int unreadMessageCount; const ContactTile({ super.key, @@ -29,6 +31,8 @@ class ContactTile extends StatelessWidget { this.calculateDistance, this.formatDistance, this.onNavigateToMap, + this.messageCount = 0, + this.unreadMessageCount = 0, }); /// Get localized time since last seen @@ -263,6 +267,13 @@ class ContactTile extends StatelessWidget { ], ), ), + if (messageCount > 0) ...[ + const SizedBox(width: 8), + _MessageCountBadge( + totalCount: messageCount, + unreadCount: unreadMessageCount, + ), + ], const SizedBox(width: 8), Text(timeAgoText, style: timeAgoStyle), if (isPingInProgress) ...[ @@ -1301,3 +1312,59 @@ class ContactTile extends StatelessWidget { ); } } + +class _MessageCountBadge extends StatelessWidget { + final int totalCount; + final int unreadCount; + + const _MessageCountBadge({ + required this.totalCount, + required this.unreadCount, + }); + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + final hasUnread = unreadCount > 0; + + return Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: hasUnread + ? colorScheme.primaryContainer + : colorScheme.surfaceContainerHighest, + borderRadius: BorderRadius.circular(999), + border: Border.all( + color: hasUnread + ? colorScheme.primary.withValues(alpha: 0.35) + : colorScheme.outlineVariant.withValues(alpha: 0.45), + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (hasUnread) ...[ + Container( + width: 6, + height: 6, + decoration: BoxDecoration( + color: colorScheme.primary, + shape: BoxShape.circle, + ), + ), + const SizedBox(width: 5), + ], + Text( + hasUnread ? '$unreadCount/$totalCount' : '$totalCount', + style: Theme.of(context).textTheme.labelSmall?.copyWith( + color: hasUnread + ? colorScheme.onPrimaryContainer + : colorScheme.onSurfaceVariant, + fontWeight: FontWeight.w800, + ), + ), + ], + ), + ); + } +}