diff --git a/lib/screens/contacts_tab.dart b/lib/screens/contacts_tab.dart index 9d1773b..7a22043 100644 --- a/lib/screens/contacts_tab.dart +++ b/lib/screens/contacts_tab.dart @@ -7,7 +7,6 @@ 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'; @@ -207,8 +206,8 @@ class _ContactsTabState extends State { final l10n = AppLocalizations.of(context)!; return Scaffold( - body: Consumer2( - builder: (context, contactsProvider, messagesProvider, child) { + body: Consumer( + builder: (context, contactsProvider, child) { final chatContacts = _sortContactsByDistance( contactsProvider.chatContacts, ); @@ -292,10 +291,6 @@ class _ContactsTabState extends State { formatDistance: _formatDistance, onNavigateToMap: widget.onNavigateToMap, onNavigateToMessages: widget.onNavigateToMessages, - messageCount: messagesProvider - .getMessageCountForDestination(contact), - unreadMessageCount: messagesProvider - .getUnreadCountForDestination(contact), ), ), const Divider(height: 32), @@ -316,10 +311,6 @@ class _ContactsTabState extends State { formatDistance: _formatDistance, onNavigateToMap: widget.onNavigateToMap, onNavigateToMessages: widget.onNavigateToMessages, - messageCount: messagesProvider - .getMessageCountForDestination(contact), - unreadMessageCount: messagesProvider - .getUnreadCountForDestination(contact), ), ), const Divider(height: 32), @@ -340,10 +331,6 @@ class _ContactsTabState extends State { formatDistance: _formatDistance, onNavigateToMap: widget.onNavigateToMap, onNavigateToMessages: widget.onNavigateToMessages, - messageCount: messagesProvider - .getMessageCountForDestination(contact), - unreadMessageCount: messagesProvider - .getUnreadCountForDestination(contact), ), ), const Divider(height: 32), @@ -364,10 +351,6 @@ class _ContactsTabState extends State { formatDistance: _formatDistance, onNavigateToMap: widget.onNavigateToMap, onNavigateToMessages: widget.onNavigateToMessages, - messageCount: messagesProvider - .getMessageCountForDestination(contact), - unreadMessageCount: messagesProvider - .getUnreadCountForDestination(contact), ), ), ], diff --git a/lib/screens/sensors_tab.dart b/lib/screens/sensors_tab.dart index f07c252..4350d72 100644 --- a/lib/screens/sensors_tab.dart +++ b/lib/screens/sensors_tab.dart @@ -371,6 +371,7 @@ class _SensorCard extends StatelessWidget { final l10n = AppLocalizations.of(context)!; final telemetry = contact?.telemetry; final theme = Theme.of(context); + final colorScheme = theme.colorScheme; final metrics = contact == null || telemetry == null ? const <_MetricCardData>[] : _buildMetricCards(l10n, telemetry, contact!); @@ -378,16 +379,23 @@ class _SensorCard extends StatelessWidget { return Container( margin: const EdgeInsets.only(bottom: 16), decoration: BoxDecoration( - color: theme.colorScheme.surface, borderRadius: BorderRadius.circular(28), + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + colorScheme.surfaceContainerLow, + colorScheme.surfaceContainerHighest.withValues(alpha: 0.9), + ], + ), border: Border.all( - color: theme.colorScheme.outline.withValues(alpha: 0.14), + color: colorScheme.outlineVariant.withValues(alpha: 0.35), ), boxShadow: [ BoxShadow( - color: Colors.black.withValues(alpha: 0.04), - blurRadius: 18, - offset: const Offset(0, 8), + color: Colors.black.withValues(alpha: 0.045), + blurRadius: 12, + offset: const Offset(0, 4), ), ], ), diff --git a/lib/widgets/contacts/contact_tile.dart b/lib/widgets/contacts/contact_tile.dart index 38e4640..084f7bf 100644 --- a/lib/widgets/contacts/contact_tile.dart +++ b/lib/widgets/contacts/contact_tile.dart @@ -24,8 +24,6 @@ class ContactTile extends StatelessWidget { final String Function(double)? formatDistance; final VoidCallback? onNavigateToMap; final VoidCallback? onNavigateToMessages; - final int messageCount; - final int unreadMessageCount; const ContactTile({ super.key, @@ -35,8 +33,6 @@ class ContactTile extends StatelessWidget { this.formatDistance, this.onNavigateToMap, this.onNavigateToMessages, - this.messageCount = 0, - this.unreadMessageCount = 0, }); /// Get localized time since last seen @@ -240,13 +236,6 @@ 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) ...[ @@ -321,21 +310,22 @@ class ContactTile extends StatelessWidget { _showSetRouteDialog(context, contact); }, ), - ListTile( - leading: const Icon(Icons.delete, color: Colors.red), - title: Text( - contact.isChannel ? l10n.deleteChannel : l10n.deleteContact, - style: const TextStyle(color: Colors.red), + if (!contact.isPublicChannel) + ListTile( + leading: const Icon(Icons.delete, color: Colors.red), + title: Text( + contact.isChannel ? l10n.deleteChannel : l10n.deleteContact, + style: const TextStyle(color: Colors.red), + ), + onTap: () { + Navigator.pop(sheetContext); + if (contact.isChannel) { + _showDeleteChannelDialog(context, contact); + } else { + _showDeleteConfirmation(context, contact); + } + }, ), - onTap: () { - Navigator.pop(sheetContext); - if (contact.isChannel) { - _showDeleteChannelDialog(context, contact); - } else { - _showDeleteConfirmation(context, contact); - } - }, - ), ], ), ), @@ -373,7 +363,11 @@ class ContactTile extends StatelessWidget { ); } - void _showDeleteConfirmation(BuildContext context, Contact contact) { + void _showDeleteConfirmation( + BuildContext context, + Contact contact, { + bool closeDetailsSheetOnDelete = false, + }) { showDialog( context: context, builder: (context) => AlertDialog( @@ -391,7 +385,9 @@ class ContactTile extends StatelessWidget { TextButton( onPressed: () async { Navigator.pop(context); // Close confirmation dialog - Navigator.pop(context); // Close contact details sheet + if (closeDetailsSheetOnDelete) { + Navigator.pop(context); // Close contact details sheet + } await _deleteContact(context, contact); }, style: TextButton.styleFrom(foregroundColor: Colors.red), @@ -895,7 +891,11 @@ class ContactTile extends StatelessWidget { return; } - _showDeleteConfirmation(context, contact); + _showDeleteConfirmation( + context, + contact, + closeDetailsSheetOnDelete: true, + ); }, icon: const Icon(Icons.delete_outline), label: Text( @@ -1256,6 +1256,10 @@ class ContactTile extends StatelessWidget { Contact contact, { bool closeDetailsSheetOnDelete = false, }) { + if (contact.isPublicChannel) { + return; + } + final l10n = AppLocalizations.of(context)!; showDialog( @@ -1390,59 +1394,3 @@ 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, - ), - ), - ], - ), - ); - } -} diff --git a/lib/widgets/messages/recipient_selector_sheet.dart b/lib/widgets/messages/recipient_selector_sheet.dart index 8c921af..bb410d2 100644 --- a/lib/widgets/messages/recipient_selector_sheet.dart +++ b/lib/widgets/messages/recipient_selector_sheet.dart @@ -192,7 +192,8 @@ class _RecipientSelectorSheetState extends State { ? l10n.broadcastToAllNearby : '${l10n.channel} ${channel.publicKey[1]}', // Show slot number unreadCount: - widget.unreadCountsByPublicKey[channel.publicKeyHex] ?? + widget.unreadCountsByPublicKey[channel + .publicKeyHex] ?? 0, isSelected: _isSelected('channel', channel), onTap: () { @@ -240,7 +241,8 @@ class _RecipientSelectorSheetState extends State { title: contact.displayName, subtitle: contact.publicKeyShort, unreadCount: - widget.unreadCountsByPublicKey[contact.publicKeyHex] ?? + widget.unreadCountsByPublicKey[contact + .publicKeyHex] ?? 0, isSelected: _isSelected('contact', contact), onTap: () { @@ -288,7 +290,8 @@ class _RecipientSelectorSheetState extends State { title: room.displayName, subtitle: room.publicKeyShort, unreadCount: - widget.unreadCountsByPublicKey[room.publicKeyHex] ?? 0, + widget.unreadCountsByPublicKey[room.publicKeyHex] ?? + 0, isSelected: _isSelected('room', room), onTap: () { widget.onSelect('room', room); diff --git a/pubspec.yaml b/pubspec.yaml index c22c31b..e2ac06e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 2026.0308.1+21 +version: 2026.0308.3+23 environment: sdk: ^3.9.2 diff --git a/test/widgets/recipient_selector_sheet_test.dart b/test/widgets/recipient_selector_sheet_test.dart new file mode 100644 index 0000000..76b173e --- /dev/null +++ b/test/widgets/recipient_selector_sheet_test.dart @@ -0,0 +1,74 @@ +import 'dart:typed_data'; + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:meshcore_sar_app/l10n/app_localizations.dart'; +import 'package:meshcore_sar_app/models/contact.dart'; +import 'package:meshcore_sar_app/widgets/messages/recipient_selector_sheet.dart'; + +void main() { + Contact buildContact({ + required String name, + required ContactType type, + int secondByte = 0, + }) { + final publicKey = Uint8List(32); + publicKey[1] = secondByte; + + return Contact( + publicKey: publicKey, + type: type, + flags: 0, + outPathLen: 0, + outPath: Uint8List(0), + advName: name, + lastAdvert: 0, + advLat: 0, + advLon: 0, + lastMod: 0, + ); + } + + Future pumpSheet(WidgetTester tester) async { + final channel = buildContact( + name: 'Ops', + type: ContactType.channel, + secondByte: 3, + ); + final contact = buildContact(name: 'John Smith', type: ContactType.chat); + + await tester.pumpWidget( + MaterialApp( + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + home: Scaffold( + body: RecipientSelectorSheet( + contacts: [contact], + rooms: const [], + channels: [channel], + unreadCount: 11, + unreadCountsByPublicKey: { + channel.publicKeyHex: 7, + contact.publicKeyHex: 3, + }, + currentDestinationType: 'all', + onSelect: (_, __) {}, + ), + ), + ), + ); + } + + testWidgets('renders unread badges for all and destination filters', ( + tester, + ) async { + await pumpSheet(tester); + + expect(find.byKey(const Key('unread-badge-11')), findsOneWidget); + expect(find.byKey(const Key('unread-badge-7')), findsOneWidget); + expect(find.byKey(const Key('unread-badge-3')), findsOneWidget); + expect(find.text('11'), findsOneWidget); + expect(find.text('7'), findsOneWidget); + expect(find.text('3'), findsOneWidget); + }); +}