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,
- ),
- ),
],
],
),