import 'dart:typed_data'; import 'package:latlong2/latlong.dart'; import 'sar_marker.dart'; /// Message text types from MeshCore protocol enum MessageTextType { plain(0), cliData(1), signedPlain(2); const MessageTextType(this.value); final int value; static MessageTextType fromValue(int value) { return MessageTextType.values.firstWhere( (e) => e.value == value, orElse: () => MessageTextType.plain, ); } } /// Message type (contact or channel) enum MessageType { contact, channel, } /// MeshCore message model class Message { final String id; final MessageType messageType; final Uint8List? senderPublicKeyPrefix; // 6 bytes for contact messages final int? channelIdx; // For channel messages final int pathLen; final MessageTextType textType; final int senderTimestamp; // Unix timestamp final String text; // SAR marker data (if this is a SAR message) final bool isSarMarker; final SarMarkerType? sarMarkerType; final LatLng? sarGpsCoordinates; // Display metadata final DateTime receivedAt; final String? senderName; Message({ required this.id, required this.messageType, this.senderPublicKeyPrefix, this.channelIdx, required this.pathLen, required this.textType, required this.senderTimestamp, required this.text, this.isSarMarker = false, this.sarMarkerType, this.sarGpsCoordinates, required this.receivedAt, this.senderName, }); /// Get sender public key as hex string String? get senderKeyShort { if (senderPublicKeyPrefix == null) return null; return senderPublicKeyPrefix! .map((b) => b.toRadixString(16).padLeft(2, '0')) .join(''); } /// Get sender timestamp as DateTime DateTime get sentAt { return DateTime.fromMillisecondsSinceEpoch(senderTimestamp * 1000); } /// Check if message is from a channel bool get isChannelMessage => messageType == MessageType.channel; /// Check if message is from a contact bool get isContactMessage => messageType == MessageType.contact; /// Get friendly time since message was sent String get timeAgo { final diff = DateTime.now().difference(sentAt); if (diff.inMinutes < 1) return 'Just now'; if (diff.inMinutes < 60) return '${diff.inMinutes}m ago'; if (diff.inHours < 24) return '${diff.inHours}h ago'; return '${diff.inDays}d ago'; } /// Get display name for sender String get displaySender { if (senderName != null && senderName!.isNotEmpty) { return senderName!; } if (senderKeyShort != null) { return senderKeyShort!.substring(0, 8); } if (isChannelMessage && channelIdx != null) { return 'Channel $channelIdx'; } return 'Unknown'; } /// Convert to SAR marker if applicable SarMarker? toSarMarker() { if (!isSarMarker || sarMarkerType == null || sarGpsCoordinates == null) { return null; } return SarMarker( id: id, type: sarMarkerType!, location: sarGpsCoordinates!, timestamp: sentAt, senderPublicKey: senderPublicKeyPrefix, senderName: senderName, notes: text, ); } Message copyWith({ String? id, MessageType? messageType, Uint8List? senderPublicKeyPrefix, int? channelIdx, int? pathLen, MessageTextType? textType, int? senderTimestamp, String? text, bool? isSarMarker, SarMarkerType? sarMarkerType, LatLng? sarGpsCoordinates, DateTime? receivedAt, String? senderName, }) { return Message( id: id ?? this.id, messageType: messageType ?? this.messageType, senderPublicKeyPrefix: senderPublicKeyPrefix ?? this.senderPublicKeyPrefix, channelIdx: channelIdx ?? this.channelIdx, pathLen: pathLen ?? this.pathLen, textType: textType ?? this.textType, senderTimestamp: senderTimestamp ?? this.senderTimestamp, text: text ?? this.text, isSarMarker: isSarMarker ?? this.isSarMarker, sarMarkerType: sarMarkerType ?? this.sarMarkerType, sarGpsCoordinates: sarGpsCoordinates ?? this.sarGpsCoordinates, receivedAt: receivedAt ?? this.receivedAt, senderName: senderName ?? this.senderName, ); } @override String toString() { if (isSarMarker) { return 'Message(SAR: ${sarMarkerType?.displayName}, from: $displaySender)'; } return 'Message(from: $displaySender, text: ${text.length > 30 ? '${text.substring(0, 30)}...' : text})'; } @override bool operator ==(Object other) { if (identical(this, other)) return true; return other is Message && id == other.id; } @override int get hashCode => id.hashCode; }