// Models for Secure E2EE Chat import 'dart:convert'; import 'dart:typed_data'; /// Encrypted conversation metadata class SecureConversation { final String id; final String participantA; final String participantB; final DateTime createdAt; final DateTime lastMessageAt; // Resolved participant info (loaded separately) final String? otherUserHandle; final String? otherUserDisplayName; final String? otherUserAvatarUrl; final int? unreadCount; SecureConversation({ required this.id, required this.participantA, required this.participantB, required this.createdAt, required this.lastMessageAt, this.otherUserHandle, this.otherUserDisplayName, this.otherUserAvatarUrl, this.unreadCount, }); factory SecureConversation.fromJson( Map json, String currentUserId) { final participantA = json['participant_a'] as String; final participantB = json['participant_b'] as String; final isParticipantA = currentUserId == participantA; // Get the other participant's info if included final otherProfile = isParticipantA ? json['participant_b_profile'] : json['participant_a_profile']; return SecureConversation( id: json['id'] as String, participantA: participantA, participantB: participantB, createdAt: DateTime.parse(json['created_at'] as String), lastMessageAt: DateTime.parse(json['last_message_at'] as String), otherUserHandle: otherProfile?['handle'] as String?, otherUserDisplayName: otherProfile?['display_name'] as String?, otherUserAvatarUrl: otherProfile?['avatar_url'] as String?, unreadCount: json['unread_count'] as int?, ); } String getOtherId(String currentUserId) { return currentUserId == participantA ? participantB : participantA; } } /// Encrypted message (what the server stores and returns) class EncryptedMessage { final String id; final String conversationId; final String senderId; final String ciphertext; final String iv; final Object messageHeader; final int messageType; final DateTime createdAt; final DateTime? deliveredAt; final DateTime? readAt; final DateTime? expiresAt; // Decrypted content (populated client-side) String? decryptedContent; EncryptedMessage({ required this.id, required this.conversationId, required this.senderId, required this.ciphertext, required this.iv, required this.messageHeader, required this.messageType, required this.createdAt, this.deliveredAt, this.readAt, this.expiresAt, this.decryptedContent, }); factory EncryptedMessage.fromJson(Map json) { final cipher = json['ciphertext']; final ciphertext = cipher is String ? cipher : cipher is List ? base64Encode(Uint8List.fromList(cipher.cast())) : ''; final iv = json['iv'] as String? ?? ''; final rawType = json['message_type']; final parsedType = rawType is int ? rawType : rawType is num ? rawType.toInt() : int.tryParse(rawType?.toString() ?? ''); final header = json['message_header']; final messageHeader = header is Map ? header : header is String ? header : ''; return EncryptedMessage( id: json['id'] as String, conversationId: json['conversation_id'] as String, senderId: json['sender_id'] as String, ciphertext: ciphertext, iv: iv, messageHeader: messageHeader, messageType: parsedType ?? MessageType.standardMessage, createdAt: DateTime.parse(json['created_at'] as String), deliveredAt: json['delivered_at'] != null ? DateTime.parse(json['delivered_at'] as String) : null, readAt: json['read_at'] != null ? DateTime.parse(json['read_at'] as String) : null, expiresAt: json['expires_at'] != null ? DateTime.parse(json['expires_at'] as String) : null, ); } bool get isRead => readAt != null; bool get isDelivered => deliveredAt != null; bool get isExpired => expiresAt != null && DateTime.now().isAfter(expiresAt!); } /// Message type constants class MessageType { static const int standardMessage = 1; // Normal user message static const int commandMessage = 2; // System command (delete, etc.) } /// Command types for E2EE system commands class CommandType { static const String deleteMessage = 'command_delete_message'; static const String deleteConversation = 'command_delete_conversation'; static const String resyncRequest = 'command_resync_request'; static const String resyncPayload = 'command_resync_payload'; } /// System command payload for E2EE commands class E2EECommand { final String type; final Map payload; E2EECommand({ required this.type, required this.payload, }); factory E2EECommand.deleteMessage(String targetMessageId) { return E2EECommand( type: CommandType.deleteMessage, payload: {'target_message_id': targetMessageId}, ); } factory E2EECommand.deleteConversation(String targetConversationId) { return E2EECommand( type: CommandType.deleteConversation, payload: {'target_conversation_id': targetConversationId}, ); } factory E2EECommand.resyncRequest(String conversationId, {int? limit}) { return E2EECommand( type: CommandType.resyncRequest, payload: { 'conversation_id': conversationId, if (limit != null) 'limit': limit, }, ); } factory E2EECommand.resyncPayload( String conversationId, List> messages, ) { return E2EECommand( type: CommandType.resyncPayload, payload: { 'conversation_id': conversationId, 'messages': messages, }, ); } factory E2EECommand.fromJson(Map json) { return E2EECommand( type: json['type'] as String, payload: Map.from(json['payload'] as Map), ); } Map toJson() => { 'type': type, 'payload': payload, }; String toJsonString() => jsonEncode(toJson()); static E2EECommand? tryParse(String content) { try { final json = jsonDecode(content) as Map; if (json.containsKey('type') && json.containsKey('payload')) { return E2EECommand.fromJson(json); } } catch (_) {} return null; } } /// Result of a delete operation class DeleteResult { final bool success; final String? error; final bool remoteWipeFailed; DeleteResult({ required this.success, this.error, this.remoteWipeFailed = false, }); }