diff --git a/ios/Podfile.lock b/ios/Podfile.lock index e4b96bd..246f2a8 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1,38 +1,4 @@ PODS: - - DKImagePickerController/Core (4.3.9): - - DKImagePickerController/ImageDataManager - - DKImagePickerController/Resource - - DKImagePickerController/ImageDataManager (4.3.9) - - DKImagePickerController/PhotoGallery (4.3.9): - - DKImagePickerController/Core - - DKPhotoGallery - - DKImagePickerController/Resource (4.3.9) - - DKPhotoGallery (0.0.19): - - DKPhotoGallery/Core (= 0.0.19) - - DKPhotoGallery/Model (= 0.0.19) - - DKPhotoGallery/Preview (= 0.0.19) - - DKPhotoGallery/Resource (= 0.0.19) - - SDWebImage - - SwiftyGif - - DKPhotoGallery/Core (0.0.19): - - DKPhotoGallery/Model - - DKPhotoGallery/Preview - - SDWebImage - - SwiftyGif - - DKPhotoGallery/Model (0.0.19): - - SDWebImage - - SwiftyGif - - DKPhotoGallery/Preview (0.0.19): - - DKPhotoGallery/Model - - DKPhotoGallery/Resource - - SDWebImage - - SwiftyGif - - DKPhotoGallery/Resource (0.0.19): - - SDWebImage - - SwiftyGif - - file_picker (0.0.1): - - DKImagePickerController/PhotoGallery - - Flutter - Flutter (1.0.0) - flutter_background_service_ios (0.0.3): - Flutter @@ -55,18 +21,13 @@ PODS: - FlutterMacOS - permission_handler_apple (9.3.0): - Flutter - - SDWebImage (5.21.3): - - SDWebImage/Core (= 5.21.3) - - SDWebImage/Core (5.21.3) - share_plus (0.0.1): - Flutter - shared_preferences_foundation (0.0.1): - Flutter - FlutterMacOS - - SwiftyGif (5.4.5) DEPENDENCIES: - - file_picker (from `.symlinks/plugins/file_picker/ios`) - Flutter (from `Flutter`) - flutter_background_service_ios (from `.symlinks/plugins/flutter_background_service_ios/ios`) - flutter_blue_plus_darwin (from `.symlinks/plugins/flutter_blue_plus_darwin/darwin`) @@ -81,15 +42,9 @@ DEPENDENCIES: SPEC REPOS: trunk: - - DKImagePickerController - - DKPhotoGallery - ObjectBox - - SDWebImage - - SwiftyGif EXTERNAL SOURCES: - file_picker: - :path: ".symlinks/plugins/file_picker/ios" Flutter: :path: Flutter flutter_background_service_ios: @@ -114,9 +69,6 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/shared_preferences_foundation/darwin" SPEC CHECKSUMS: - DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c - DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60 - file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 flutter_background_service_ios: 00d31bdff7b4bfe06d32375df358abe0329cf87e flutter_blue_plus_darwin: 20a08bfeaa0f7804d524858d3d8744bcc1b6dbc3 @@ -127,10 +79,8 @@ SPEC CHECKSUMS: package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d - SDWebImage: 16309af6d214ba3f77a7c6f6fdda888cb313a50a share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 - SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4 PODFILE CHECKSUM: 3c63482e143d1b91d2d2560aee9fb04ecc74ac7e diff --git a/ios/Runner.app.dSYM.zip b/ios/Runner.app.dSYM.zip new file mode 100644 index 0000000..bfb8add Binary files /dev/null and b/ios/Runner.app.dSYM.zip differ diff --git a/ios/Runner.ipa b/ios/Runner.ipa index d39957c..547b200 100644 Binary files a/ios/Runner.ipa and b/ios/Runner.ipa differ diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index b4e7147..08d6121 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -489,7 +489,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 4; DEVELOPMENT_TEAM = JND55328G8; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -511,7 +511,7 @@ buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 4; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.meshcore.sar.meshcoreSarApp.RunnerTests; @@ -529,7 +529,7 @@ buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 4; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.meshcore.sar.meshcoreSarApp.RunnerTests; @@ -545,7 +545,7 @@ buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 4; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.meshcore.sar.meshcoreSarApp.RunnerTests; @@ -676,7 +676,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 4; DEVELOPMENT_TEAM = JND55328G8; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -699,7 +699,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 4; DEVELOPMENT_TEAM = JND55328G8; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 1207dd5..c5ab99d 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -21,7 +21,7 @@ CFBundleSignature ???? CFBundleVersion - 2 + 4 LSRequiresIPhoneOS UILaunchStoryboardName diff --git a/ios/fastlane/report.xml b/ios/fastlane/report.xml index 84a4a7a..def4b39 100644 --- a/ios/fastlane/report.xml +++ b/ios/fastlane/report.xml @@ -5,7 +5,24 @@ - + + + + + + + + + + + + + + + + + + diff --git a/lib/models/message.dart b/lib/models/message.dart index 488d62b..fa78dbb 100644 --- a/lib/models/message.dart +++ b/lib/models/message.dart @@ -19,10 +19,11 @@ enum MessageTextType { } } -/// Message type (contact or channel) +/// Message type (contact, channel, or system) enum MessageType { contact, channel, + system, // System messages (log entries, status updates) } /// Message delivery status @@ -103,6 +104,9 @@ class Message { /// Check if message is from a contact bool get isContactMessage => messageType == MessageType.contact; + /// Check if message is a system message + bool get isSystemMessage => messageType == MessageType.system; + /// Get friendly time since message was sent String get timeAgo { final diff = DateTime.now().difference(sentAt); diff --git a/lib/providers/messages_provider.dart b/lib/providers/messages_provider.dart index dcda8a8..075ff3f 100644 --- a/lib/providers/messages_provider.dart +++ b/lib/providers/messages_provider.dart @@ -30,6 +30,9 @@ class MessagesProvider with ChangeNotifier { List get sarMarkerMessages => _messages.where((m) => m.isSarMarker).toList(); + List get systemMessages => + _messages.where((m) => m.isSystemMessage).toList(); + List get sarMarkers => _sarMarkers.values.toList(); List get foundPersonMarkers => @@ -348,10 +351,39 @@ class MessagesProvider with ChangeNotifier { 'contact': contactMessages.length, 'channel': channelMessages.length, 'sar': sarMarkerMessages.length, + 'system': systemMessages.length, 'sarMarkers': sarMarkers.length, }; } + /// Log a system message (replaces toast notifications) + void logSystemMessage({ + required String text, + String level = 'info', // 'info', 'success', 'warning', 'error' + }) { + final timestamp = DateTime.now().millisecondsSinceEpoch ~/ 1000; + final messageId = '${DateTime.now().millisecondsSinceEpoch}_system_$level'; + + final systemMessage = Message( + id: messageId, + messageType: MessageType.system, + pathLen: 0, + textType: MessageTextType.plain, + senderTimestamp: timestamp, + text: text, + receivedAt: DateTime.now(), + senderName: level, // Use senderName to store log level + deliveryStatus: MessageDeliveryStatus.received, + ); + + _messages.add(systemMessage); + + // Don't persist system messages to reduce storage + // _persistMessages(); + + notifyListeners(); + } + /// Get SAR marker statistics Map get sarMarkerStats { return { diff --git a/lib/screens/contacts_tab.dart b/lib/screens/contacts_tab.dart index 9fbcafd..5bad31e 100644 --- a/lib/screens/contacts_tab.dart +++ b/lib/screens/contacts_tab.dart @@ -49,13 +49,21 @@ class _ContactsTabState extends State { } /// Calculate distance between two points in meters - double _calculateDistanceInMeters(double lat1, double lon1, double lat2, double lon2) { + double _calculateDistanceInMeters( + double lat1, + double lon1, + double lat2, + double lon2, + ) { const R = 6371000; // Earth's radius in meters final dLat = (lat2 - lat1) * pi / 180; final dLon = (lon2 - lon1) * pi / 180; - final a = sin(dLat / 2) * sin(dLat / 2) + - cos(lat1 * pi / 180) * cos(lat2 * pi / 180) * - sin(dLon / 2) * sin(dLon / 2); + final a = + sin(dLat / 2) * sin(dLat / 2) + + cos(lat1 * pi / 180) * + cos(lat2 * pi / 180) * + sin(dLon / 2) * + sin(dLon / 2); final c = 2 * atan2(sqrt(a), sqrt(1 - a)); return R * c; } @@ -110,52 +118,58 @@ class _ContactsTabState extends State { child: ListView( padding: const EdgeInsets.all(8), children: [ - // Team Members (Chat contacts) - if (chatContacts.isNotEmpty) ...[ - _SectionHeader( - title: 'Team Members', - count: chatContacts.length, - icon: Icons.people, - ), - ...chatContacts.map((contact) => ContactTile( - contact: contact, - currentPosition: _currentPosition, - calculateDistance: _calculateDistanceInMeters, - formatDistance: _formatDistance, - )), - const Divider(height: 32), - ], + // Team Members (Chat contacts) + if (chatContacts.isNotEmpty) ...[ + _SectionHeader( + title: 'Team Members', + count: chatContacts.length, + icon: Icons.people, + ), + ...chatContacts.map( + (contact) => ContactTile( + contact: contact, + currentPosition: _currentPosition, + calculateDistance: _calculateDistanceInMeters, + formatDistance: _formatDistance, + ), + ), + const Divider(height: 32), + ], - // Repeaters - if (repeaters.isNotEmpty) ...[ - _SectionHeader( - title: 'Repeaters', - count: repeaters.length, - icon: Icons.router, - ), - ...repeaters.map((contact) => ContactTile( - contact: contact, - currentPosition: _currentPosition, - calculateDistance: _calculateDistanceInMeters, - formatDistance: _formatDistance, - )), - const Divider(height: 32), - ], + // Repeaters + if (repeaters.isNotEmpty) ...[ + _SectionHeader( + title: 'Repeaters', + count: repeaters.length, + icon: Icons.router, + ), + ...repeaters.map( + (contact) => ContactTile( + contact: contact, + currentPosition: _currentPosition, + calculateDistance: _calculateDistanceInMeters, + formatDistance: _formatDistance, + ), + ), + const Divider(height: 32), + ], - // Rooms/Channels - if (rooms.isNotEmpty) ...[ - _SectionHeader( - title: 'Rooms/Channels', - count: rooms.length, - icon: Icons.tag, - ), - ...rooms.map((contact) => ContactTile( - contact: contact, - currentPosition: _currentPosition, - calculateDistance: _calculateDistanceInMeters, - formatDistance: _formatDistance, - )), - ], + // Rooms/Channels + if (rooms.isNotEmpty) ...[ + _SectionHeader( + title: 'Rooms', + count: rooms.length, + icon: Icons.tag, + ), + ...rooms.map( + (contact) => ContactTile( + contact: contact, + currentPosition: _currentPosition, + calculateDistance: _calculateDistanceInMeters, + formatDistance: _formatDistance, + ), + ), + ], ], ), ); @@ -185,9 +199,9 @@ class _SectionHeader extends StatelessWidget { const SizedBox(width: 8), Text( title, - style: Theme.of(context).textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.bold, - ), + style: Theme.of( + context, + ).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold), ), const SizedBox(width: 8), Container( diff --git a/lib/screens/home_screen.dart b/lib/screens/home_screen.dart index 7551b4d..4422d6d 100644 --- a/lib/screens/home_screen.dart +++ b/lib/screens/home_screen.dart @@ -11,6 +11,7 @@ import 'map_management_screen.dart'; import 'settings_screen.dart'; import 'device_config_screen.dart'; import 'packet_log_screen.dart'; +import '../utils/toast_logger.dart'; class HomeScreen extends StatefulWidget { final Function(AppThemeMode) onThemeChanged; @@ -52,9 +53,7 @@ class _HomeScreenState extends State with SingleTickerProviderStateM if (!connectionProvider.deviceInfo.isConnected) { if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Device not connected')), - ); + ToastLogger.error(context, 'Device not connected'); } return; } @@ -64,12 +63,7 @@ class _HomeScreenState extends State with SingleTickerProviderStateM bool serviceEnabled = await Geolocator.isLocationServiceEnabled(); if (!serviceEnabled) { if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Location services are disabled. Please enable them in Settings.'), - duration: Duration(seconds: 3), - ), - ); + ToastLogger.error(context, 'Location services are disabled. Please enable them in Settings.'); } return; } @@ -80,12 +74,7 @@ class _HomeScreenState extends State with SingleTickerProviderStateM permission = await Geolocator.requestPermission(); if (permission == LocationPermission.denied) { if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Location permission denied'), - duration: Duration(seconds: 2), - ), - ); + ToastLogger.error(context, 'Location permission denied'); } return; } @@ -93,12 +82,7 @@ class _HomeScreenState extends State with SingleTickerProviderStateM if (permission == LocationPermission.deniedForever) { if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Location permission permanently denied. Please enable in Settings.'), - duration: Duration(seconds: 3), - ), - ); + ToastLogger.error(context, 'Location permission permanently denied. Please enable in Settings.'); } return; } @@ -115,9 +99,7 @@ class _HomeScreenState extends State with SingleTickerProviderStateM } catch (e) { print('❌ Failed to get GPS position: $e'); if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Failed to get GPS location')), - ); + ToastLogger.error(context, 'Failed to get GPS location'); } return; } @@ -135,21 +117,12 @@ class _HomeScreenState extends State with SingleTickerProviderStateM await connectionProvider.sendSelfAdvert(floodMode: true); if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - 'Advertised at ${position.latitude.toStringAsFixed(6)}, ${position.longitude.toStringAsFixed(6)}', - ), - duration: const Duration(seconds: 2), - ), - ); + ToastLogger.success(context, 'Advertised at ${position.latitude.toStringAsFixed(6)}, ${position.longitude.toStringAsFixed(6)}'); } } catch (e) { print('❌ Failed to advertise device: $e'); if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Failed to advertise: $e')), - ); + ToastLogger.error(context, 'Failed to advertise: $e'); } } } @@ -720,9 +693,7 @@ class _HomeScreenState extends State with SingleTickerProviderStateM final appProvider = context.read(); await appProvider.refresh(); if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Refreshed contacts')), - ); + ToastLogger.success(context, 'Refreshed contacts'); } }, icon: const Icon(Icons.refresh), diff --git a/lib/screens/messages_tab.dart b/lib/screens/messages_tab.dart index d0cae89..f1dc810 100644 --- a/lib/screens/messages_tab.dart +++ b/lib/screens/messages_tab.dart @@ -10,6 +10,7 @@ import '../providers/connection_provider.dart'; import '../models/message.dart'; import '../models/sar_marker.dart'; import '../widgets/messages/sar_update_sheet.dart'; +import '../utils/toast_logger.dart'; class MessagesTab extends StatefulWidget { final VoidCallback onNavigateToMap; @@ -63,12 +64,7 @@ class _MessagesTabState extends State { if (!connectionProvider.deviceInfo.isConnected) { if (!mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Not connected to device'), - backgroundColor: Colors.red, - ), - ); + ToastLogger.error(context, 'Not connected to device'); return; } @@ -109,21 +105,10 @@ class _MessagesTabState extends State { _focusNode.unfocus(); if (!mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Message sent to public channel'), - backgroundColor: Colors.green, - duration: Duration(seconds: 1), - ), - ); + ToastLogger.success(context, 'Message sent to public channel'); } catch (e) { if (!mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Failed to send: $e'), - backgroundColor: Colors.red, - ), - ); + ToastLogger.error(context, 'Failed to send: $e'); } } @@ -153,23 +138,13 @@ class _MessagesTabState extends State { if (!connectionProvider.deviceInfo.isConnected) { if (!mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Not connected to device'), - backgroundColor: Colors.red, - ), - ); + ToastLogger.error(context, 'Not connected to device'); return; } if (!sendToChannel && roomPublicKey == null) { if (!mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Please select a room to send SAR marker'), - backgroundColor: Colors.red, - ), - ); + ToastLogger.error(context, 'Please select a room to send SAR marker'); return; } @@ -190,13 +165,7 @@ class _MessagesTabState extends State { ); if (!mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('${sarType.displayName} marker broadcast to public channel'), - backgroundColor: Colors.orange, - duration: const Duration(seconds: 2), - ), - ); + ToastLogger.warning(context, '${sarType.displayName} marker broadcast to public channel'); } else { // Create message ID final messageId = '${DateTime.now().millisecondsSinceEpoch}_sent'; @@ -245,22 +214,11 @@ class _MessagesTabState extends State { } if (!mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('${sarType.displayName} marker sent to room'), - backgroundColor: Colors.green, - duration: const Duration(seconds: 2), - ), - ); + ToastLogger.success(context, '${sarType.displayName} marker sent to room'); } } catch (e) { if (!mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Failed to send SAR marker: $e'), - backgroundColor: Colors.red, - ), - ); + ToastLogger.error(context, 'Failed to send SAR marker: $e'); } } @@ -313,6 +271,12 @@ class _MessagesTabState extends State { itemCount: messages.length, itemBuilder: (context, index) { final message = messages[index]; + + // Display system messages with minimal styling + if (message.isSystemMessage) { + return _SystemMessageBubble(message: message); + } + return _MessageBubble( message: message, onTap: message.isSarMarker && @@ -438,12 +402,7 @@ class _MessageBubble extends StatelessWidget { final messagesProvider = context.read(); if (!connectionProvider.deviceInfo.isConnected) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Not connected to device'), - backgroundColor: Colors.red, - ), - ); + ToastLogger.error(context, 'Not connected to device'); return; } @@ -465,12 +424,7 @@ class _MessageBubble extends StatelessWidget { // Direct message retry (for SAR markers sent to rooms) if (failedMessage.recipientPublicKey == null) { messagesProvider.markMessageFailed(retryMessageId); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Cannot retry: recipient information missing'), - backgroundColor: Colors.red, - ), - ); + ToastLogger.error(context, 'Cannot retry: recipient information missing'); return; } @@ -491,20 +445,9 @@ class _MessageBubble extends StatelessWidget { if (!sentSuccessfully) { messagesProvider.markMessageFailed(retryMessageId); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Failed to resend message'), - backgroundColor: Colors.red, - ), - ); + ToastLogger.error(context, 'Failed to resend message'); } else { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Retrying message...'), - backgroundColor: Colors.orange, - duration: Duration(seconds: 2), - ), - ); + ToastLogger.info(context, 'Retrying message...'); } } else if (failedMessage.messageType == MessageType.channel) { // Channel message retry @@ -514,21 +457,10 @@ class _MessageBubble extends StatelessWidget { messageId: retryMessageId, ); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Retrying message...'), - backgroundColor: Colors.orange, - duration: const Duration(seconds: 2), - ), - ); + ToastLogger.info(context, 'Retrying message...'); } } catch (e) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Retry failed: $e'), - backgroundColor: Colors.red, - ), - ); + ToastLogger.error(context, 'Retry failed: $e'); } } @@ -551,12 +483,7 @@ class _MessageBubble extends StatelessWidget { onTap: () { Clipboard.setData(ClipboardData(text: message.text)); Navigator.pop(context); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Text copied to clipboard'), - duration: Duration(seconds: 1), - ), - ); + ToastLogger.success(context, 'Text copied to clipboard'); }, ), // Delete message option @@ -590,12 +517,7 @@ class _MessageBubble extends StatelessWidget { final messagesProvider = context.read(); messagesProvider.deleteMessage(message.id); Navigator.pop(context); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Message deleted'), - duration: Duration(seconds: 1), - ), - ); + ToastLogger.info(context, 'Message deleted'); }, style: TextButton.styleFrom(foregroundColor: Colors.red), child: const Text('Delete'), @@ -953,3 +875,85 @@ class _MessageBubble extends StatelessWidget { } } +/// System message bubble - compact log-style display +class _SystemMessageBubble extends StatelessWidget { + final Message message; + + const _SystemMessageBubble({required this.message}); + + Color _getLevelColor(String? level) { + switch (level?.toLowerCase()) { + case 'success': + return Colors.green; + case 'warning': + return Colors.orange; + case 'error': + return Colors.red; + case 'info': + default: + return Colors.blue.shade300; + } + } + + IconData _getLevelIcon(String? level) { + switch (level?.toLowerCase()) { + case 'success': + return Icons.check_circle_outline; + case 'warning': + return Icons.warning_amber_outlined; + case 'error': + return Icons.error_outline; + case 'info': + default: + return Icons.info_outline; + } + } + + @override + Widget build(BuildContext context) { + final isDarkMode = Theme.of(context).brightness == Brightness.dark; + final level = message.senderName ?? 'info'; + final levelColor = _getLevelColor(level); + + return Container( + margin: const EdgeInsets.only(bottom: 2), + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: isDarkMode + ? levelColor.withValues(alpha: 0.1) + : levelColor.withValues(alpha: 0.05), + borderRadius: BorderRadius.circular(4), + ), + child: Row( + children: [ + Icon( + _getLevelIcon(level), + size: 14, + color: levelColor, + ), + const SizedBox(width: 6), + Text( + message.timeAgo, + style: Theme.of(context).textTheme.labelSmall?.copyWith( + color: Theme.of(context).textTheme.bodySmall?.color?.withValues(alpha: 0.6), + fontSize: 10, + ), + ), + const SizedBox(width: 8), + Expanded( + child: Text( + message.text, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + fontSize: 11, + color: Theme.of(context).textTheme.bodySmall?.color?.withValues(alpha: 0.8), + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ); + } +} + diff --git a/lib/utils/toast_logger.dart b/lib/utils/toast_logger.dart new file mode 100644 index 0000000..f59c0db --- /dev/null +++ b/lib/utils/toast_logger.dart @@ -0,0 +1,37 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import '../providers/messages_provider.dart'; + +/// Toast logger utility - replaces SnackBar with system messages in the channels tab +class ToastLogger { + /// Log an info message + static void info(BuildContext context, String message) { + _log(context, message, 'info'); + } + + /// Log a success message + static void success(BuildContext context, String message) { + _log(context, message, 'success'); + } + + /// Log a warning message + static void warning(BuildContext context, String message) { + _log(context, message, 'warning'); + } + + /// Log an error message + static void error(BuildContext context, String message) { + _log(context, message, 'error'); + } + + /// Internal method to log a system message + static void _log(BuildContext context, String message, String level) { + try { + final messagesProvider = context.read(); + messagesProvider.logSystemMessage(text: message, level: level); + } catch (e) { + // Fallback to print if provider is not available + debugPrint('[$level] $message'); + } + } +} diff --git a/lib/widgets/contacts/contact_tile.dart b/lib/widgets/contacts/contact_tile.dart index 6e10822..aedadeb 100644 --- a/lib/widgets/contacts/contact_tile.dart +++ b/lib/widgets/contacts/contact_tile.dart @@ -95,8 +95,25 @@ class ContactTile extends StatelessWidget { overflow: TextOverflow.ellipsis, ), ), - // Connection type indicator (direct/flood) - if (contact.type == ContactType.chat) ...[ + // Battery indicator + if (battery != null) ...[ + Icon( + _getBatteryIcon(battery), + size: 16, + color: _getBatteryColor(battery), + ), + const SizedBox(width: 4), + Text( + '${battery.round()}%', + style: Theme.of(context).textTheme.labelMedium?.copyWith( + color: _getBatteryColor(battery), + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(width: 8), + ], + // Connection type indicator (direct/flood) - shown for all contact types + if (contact.type != ContactType.channel) ...[ Container( padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2), decoration: BoxDecoration( @@ -129,23 +146,6 @@ class ContactTile extends StatelessWidget { ], ), ), - const SizedBox(width: 8), - ], - // Battery indicator on the right - if (battery != null) ...[ - Icon( - _getBatteryIcon(battery), - size: 16, - color: _getBatteryColor(battery), - ), - const SizedBox(width: 4), - Text( - '${battery.round()}%', - style: Theme.of(context).textTheme.labelMedium?.copyWith( - color: _getBatteryColor(battery), - fontWeight: FontWeight.w600, - ), - ), ], ], ),