✨ Reply / forward mesage
This commit is contained in:
		| @@ -87,5 +87,7 @@ | ||||
|   }, | ||||
|   "permissionOwner": "Owner", | ||||
|   "permissionModerator": "Moderator", | ||||
|   "permissionMember": "Member" | ||||
|   "permissionMember": "Member", | ||||
|   "reply": "Reply", | ||||
|   "forward": "Forward" | ||||
| } | ||||
|   | ||||
| @@ -3,6 +3,8 @@ import 'package:island/database/drift_db.dart'; | ||||
| import 'package:island/database/message.dart'; | ||||
| import 'package:island/models/chat.dart'; | ||||
| import 'package:island/models/file.dart'; | ||||
| import 'package:island/pods/network.dart'; | ||||
| import 'package:island/services/file.dart'; | ||||
| import 'package:island/widgets/alert.dart'; | ||||
| import 'package:uuid/uuid.dart'; | ||||
|  | ||||
| @@ -169,11 +171,16 @@ class MessageRepository { | ||||
|   } | ||||
|  | ||||
|   Future<LocalChatMessage> sendMessage( | ||||
|     String atk, | ||||
|     String baseUrl, | ||||
|     int roomId, | ||||
|     String content, | ||||
|     String nonce, { | ||||
|     List<SnCloudFile>? attachments, | ||||
|     required List<UniversalFile> attachments, | ||||
|     Map<String, dynamic>? meta, | ||||
|     SnChatMessage? replyingTo, | ||||
|     SnChatMessage? forwardingTo, | ||||
|     SnChatMessage? editingTo, | ||||
|   }) async { | ||||
|     // Generate a unique nonce for this message | ||||
|     final nonce = const Uuid().v4(); | ||||
| @@ -200,15 +207,44 @@ class MessageRepository { | ||||
|     await _database.saveMessage(_database.messageToCompanion(localMessage)); | ||||
|  | ||||
|     try { | ||||
|       var cloudAttachments = List.empty(growable: true); | ||||
|       // Upload files | ||||
|       for (var idx = 0; idx < attachments.length; idx++) { | ||||
|         final cloudFile = | ||||
|             await putMediaToCloud( | ||||
|               fileData: attachments[idx].data, | ||||
|               atk: atk, | ||||
|               baseUrl: baseUrl, | ||||
|               filename: attachments[idx].data.name ?? 'Post media', | ||||
|               mimetype: | ||||
|                   attachments[idx].data.mimeType ?? | ||||
|                   switch (attachments[idx].type) { | ||||
|                     UniversalFileType.image => 'image/unknown', | ||||
|                     UniversalFileType.video => 'video/unknown', | ||||
|                     UniversalFileType.audio => 'audio/unknown', | ||||
|                     UniversalFileType.file => 'application/octet-stream', | ||||
|                   }, | ||||
|             ).future; | ||||
|         if (cloudFile == null) { | ||||
|           throw ArgumentError('Failed to upload the file...'); | ||||
|         } | ||||
|         cloudAttachments.add(cloudFile); | ||||
|       } | ||||
|  | ||||
|       // Send to server | ||||
|       final response = await _apiClient.post( | ||||
|         '/chat/$roomId/messages', | ||||
|       final response = await _apiClient.request( | ||||
|         editingTo == null | ||||
|             ? '/chat/$roomId/messages' | ||||
|             : '/chat/$roomId/messages/${editingTo.id}', | ||||
|         data: { | ||||
|           'content': content, | ||||
|           'attachments_id': attachments, | ||||
|           'attachments_id': cloudAttachments.map((e) => e.id).toList(), | ||||
|           'replied_message_id': replyingTo?.id, | ||||
|           'forwarded_message_id': forwardingTo?.id, | ||||
|           'meta': meta, | ||||
|           'nonce': nonce, | ||||
|         }, | ||||
|         options: Options(method: editingTo == null ? 'POST' : 'PATCH'), | ||||
|       ); | ||||
|  | ||||
|       // Update with server response | ||||
| @@ -380,4 +416,33 @@ class MessageRepository { | ||||
|       rethrow; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   Future<LocalChatMessage?> getMessageById(String messageId) async { | ||||
|     try { | ||||
|       // Attempt to get the message from the local database | ||||
|       final localMessage = | ||||
|           await (_database.select(_database.chatMessages) | ||||
|             ..where((tbl) => tbl.id.equals(messageId))).getSingleOrNull(); | ||||
|       if (localMessage != null) { | ||||
|         return _database.companionToMessage(localMessage); | ||||
|       } | ||||
|  | ||||
|       // If not found locally, fetch from the server | ||||
|       final response = await _apiClient.get( | ||||
|         '/chat/${room.id}/messages/$messageId', | ||||
|       ); | ||||
|       final remoteMessage = SnChatMessage.fromJson(response.data); | ||||
|       final message = LocalChatMessage.fromRemoteMessage( | ||||
|         remoteMessage, | ||||
|         MessageStatus.sent, | ||||
|       ); | ||||
|  | ||||
|       // Save the fetched message to the local database | ||||
|       await _database.saveMessage(_database.messageToCompanion(message)); | ||||
|       return message; | ||||
|     } catch (e) { | ||||
|       // Handle errors | ||||
|       rethrow; | ||||
|     } | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -43,9 +43,7 @@ abstract class SnChatMessage with _$SnChatMessage { | ||||
|     @Default([]) List<SnCloudFile> attachments, | ||||
|     @Default([]) List<SnChatReaction> reactions, | ||||
|     String? repliedMessageId, | ||||
|     SnChatMessage? repliedMessage, | ||||
|     String? forwardedMessageId, | ||||
|     SnChatMessage? forwardedMessage, | ||||
|     required String senderId, | ||||
|     required SnChatMember sender, | ||||
|     required int chatRoomId, | ||||
|   | ||||
| @@ -260,7 +260,7 @@ $SnRealmCopyWith<$Res>? get realm { | ||||
| /// @nodoc | ||||
| mixin _$SnChatMessage { | ||||
|  | ||||
|  DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; String get id; String? get content; String? get nonce; Map<String, dynamic> get meta; List<String> get membersMetioned; DateTime? get editedAt; List<SnCloudFile> get attachments; List<SnChatReaction> get reactions; String? get repliedMessageId; SnChatMessage? get repliedMessage; String? get forwardedMessageId; SnChatMessage? get forwardedMessage; String get senderId; SnChatMember get sender; int get chatRoomId; | ||||
|  DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; String get id; String? get content; String? get nonce; Map<String, dynamic> get meta; List<String> get membersMetioned; DateTime? get editedAt; List<SnCloudFile> get attachments; List<SnChatReaction> get reactions; String? get repliedMessageId; String? get forwardedMessageId; String get senderId; SnChatMember get sender; int get chatRoomId; | ||||
| /// Create a copy of SnChatMessage | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @JsonKey(includeFromJson: false, includeToJson: false) | ||||
| @@ -273,16 +273,16 @@ $SnChatMessageCopyWith<SnChatMessage> get copyWith => _$SnChatMessageCopyWithImp | ||||
|  | ||||
| @override | ||||
| bool operator ==(Object other) { | ||||
|   return identical(this, other) || (other.runtimeType == runtimeType&&other is SnChatMessage&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&(identical(other.id, id) || other.id == id)&&(identical(other.content, content) || other.content == content)&&(identical(other.nonce, nonce) || other.nonce == nonce)&&const DeepCollectionEquality().equals(other.meta, meta)&&const DeepCollectionEquality().equals(other.membersMetioned, membersMetioned)&&(identical(other.editedAt, editedAt) || other.editedAt == editedAt)&&const DeepCollectionEquality().equals(other.attachments, attachments)&&const DeepCollectionEquality().equals(other.reactions, reactions)&&(identical(other.repliedMessageId, repliedMessageId) || other.repliedMessageId == repliedMessageId)&&(identical(other.repliedMessage, repliedMessage) || other.repliedMessage == repliedMessage)&&(identical(other.forwardedMessageId, forwardedMessageId) || other.forwardedMessageId == forwardedMessageId)&&(identical(other.forwardedMessage, forwardedMessage) || other.forwardedMessage == forwardedMessage)&&(identical(other.senderId, senderId) || other.senderId == senderId)&&(identical(other.sender, sender) || other.sender == sender)&&(identical(other.chatRoomId, chatRoomId) || other.chatRoomId == chatRoomId)); | ||||
|   return identical(this, other) || (other.runtimeType == runtimeType&&other is SnChatMessage&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&(identical(other.id, id) || other.id == id)&&(identical(other.content, content) || other.content == content)&&(identical(other.nonce, nonce) || other.nonce == nonce)&&const DeepCollectionEquality().equals(other.meta, meta)&&const DeepCollectionEquality().equals(other.membersMetioned, membersMetioned)&&(identical(other.editedAt, editedAt) || other.editedAt == editedAt)&&const DeepCollectionEquality().equals(other.attachments, attachments)&&const DeepCollectionEquality().equals(other.reactions, reactions)&&(identical(other.repliedMessageId, repliedMessageId) || other.repliedMessageId == repliedMessageId)&&(identical(other.forwardedMessageId, forwardedMessageId) || other.forwardedMessageId == forwardedMessageId)&&(identical(other.senderId, senderId) || other.senderId == senderId)&&(identical(other.sender, sender) || other.sender == sender)&&(identical(other.chatRoomId, chatRoomId) || other.chatRoomId == chatRoomId)); | ||||
| } | ||||
|  | ||||
| @JsonKey(includeFromJson: false, includeToJson: false) | ||||
| @override | ||||
| int get hashCode => Object.hash(runtimeType,createdAt,updatedAt,deletedAt,id,content,nonce,const DeepCollectionEquality().hash(meta),const DeepCollectionEquality().hash(membersMetioned),editedAt,const DeepCollectionEquality().hash(attachments),const DeepCollectionEquality().hash(reactions),repliedMessageId,repliedMessage,forwardedMessageId,forwardedMessage,senderId,sender,chatRoomId); | ||||
| int get hashCode => Object.hash(runtimeType,createdAt,updatedAt,deletedAt,id,content,nonce,const DeepCollectionEquality().hash(meta),const DeepCollectionEquality().hash(membersMetioned),editedAt,const DeepCollectionEquality().hash(attachments),const DeepCollectionEquality().hash(reactions),repliedMessageId,forwardedMessageId,senderId,sender,chatRoomId); | ||||
|  | ||||
| @override | ||||
| String toString() { | ||||
|   return 'SnChatMessage(createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, id: $id, content: $content, nonce: $nonce, meta: $meta, membersMetioned: $membersMetioned, editedAt: $editedAt, attachments: $attachments, reactions: $reactions, repliedMessageId: $repliedMessageId, repliedMessage: $repliedMessage, forwardedMessageId: $forwardedMessageId, forwardedMessage: $forwardedMessage, senderId: $senderId, sender: $sender, chatRoomId: $chatRoomId)'; | ||||
|   return 'SnChatMessage(createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, id: $id, content: $content, nonce: $nonce, meta: $meta, membersMetioned: $membersMetioned, editedAt: $editedAt, attachments: $attachments, reactions: $reactions, repliedMessageId: $repliedMessageId, forwardedMessageId: $forwardedMessageId, senderId: $senderId, sender: $sender, chatRoomId: $chatRoomId)'; | ||||
| } | ||||
|  | ||||
|  | ||||
| @@ -293,11 +293,11 @@ abstract mixin class $SnChatMessageCopyWith<$Res>  { | ||||
|   factory $SnChatMessageCopyWith(SnChatMessage value, $Res Function(SnChatMessage) _then) = _$SnChatMessageCopyWithImpl; | ||||
| @useResult | ||||
| $Res call({ | ||||
|  DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, String id, String? content, String? nonce, Map<String, dynamic> meta, List<String> membersMetioned, DateTime? editedAt, List<SnCloudFile> attachments, List<SnChatReaction> reactions, String? repliedMessageId, SnChatMessage? repliedMessage, String? forwardedMessageId, SnChatMessage? forwardedMessage, String senderId, SnChatMember sender, int chatRoomId | ||||
|  DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, String id, String? content, String? nonce, Map<String, dynamic> meta, List<String> membersMetioned, DateTime? editedAt, List<SnCloudFile> attachments, List<SnChatReaction> reactions, String? repliedMessageId, String? forwardedMessageId, String senderId, SnChatMember sender, int chatRoomId | ||||
| }); | ||||
|  | ||||
|  | ||||
| $SnChatMessageCopyWith<$Res>? get repliedMessage;$SnChatMessageCopyWith<$Res>? get forwardedMessage;$SnChatMemberCopyWith<$Res> get sender; | ||||
| $SnChatMemberCopyWith<$Res> get sender; | ||||
|  | ||||
| } | ||||
| /// @nodoc | ||||
| @@ -310,7 +310,7 @@ class _$SnChatMessageCopyWithImpl<$Res> | ||||
|  | ||||
| /// Create a copy of SnChatMessage | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @pragma('vm:prefer-inline') @override $Res call({Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,Object? id = null,Object? content = freezed,Object? nonce = freezed,Object? meta = null,Object? membersMetioned = null,Object? editedAt = freezed,Object? attachments = null,Object? reactions = null,Object? repliedMessageId = freezed,Object? repliedMessage = freezed,Object? forwardedMessageId = freezed,Object? forwardedMessage = freezed,Object? senderId = null,Object? sender = null,Object? chatRoomId = null,}) { | ||||
| @pragma('vm:prefer-inline') @override $Res call({Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,Object? id = null,Object? content = freezed,Object? nonce = freezed,Object? meta = null,Object? membersMetioned = null,Object? editedAt = freezed,Object? attachments = null,Object? reactions = null,Object? repliedMessageId = freezed,Object? forwardedMessageId = freezed,Object? senderId = null,Object? sender = null,Object? chatRoomId = null,}) { | ||||
|   return _then(_self.copyWith( | ||||
| createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable | ||||
| as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable | ||||
| @@ -324,10 +324,8 @@ as List<String>,editedAt: freezed == editedAt ? _self.editedAt : editedAt // ign | ||||
| as DateTime?,attachments: null == attachments ? _self.attachments : attachments // ignore: cast_nullable_to_non_nullable | ||||
| as List<SnCloudFile>,reactions: null == reactions ? _self.reactions : reactions // ignore: cast_nullable_to_non_nullable | ||||
| as List<SnChatReaction>,repliedMessageId: freezed == repliedMessageId ? _self.repliedMessageId : repliedMessageId // ignore: cast_nullable_to_non_nullable | ||||
| as String?,repliedMessage: freezed == repliedMessage ? _self.repliedMessage : repliedMessage // ignore: cast_nullable_to_non_nullable | ||||
| as SnChatMessage?,forwardedMessageId: freezed == forwardedMessageId ? _self.forwardedMessageId : forwardedMessageId // ignore: cast_nullable_to_non_nullable | ||||
| as String?,forwardedMessage: freezed == forwardedMessage ? _self.forwardedMessage : forwardedMessage // ignore: cast_nullable_to_non_nullable | ||||
| as SnChatMessage?,senderId: null == senderId ? _self.senderId : senderId // ignore: cast_nullable_to_non_nullable | ||||
| as String?,forwardedMessageId: freezed == forwardedMessageId ? _self.forwardedMessageId : forwardedMessageId // ignore: cast_nullable_to_non_nullable | ||||
| as String?,senderId: null == senderId ? _self.senderId : senderId // ignore: cast_nullable_to_non_nullable | ||||
| as String,sender: null == sender ? _self.sender : sender // ignore: cast_nullable_to_non_nullable | ||||
| as SnChatMember,chatRoomId: null == chatRoomId ? _self.chatRoomId : chatRoomId // ignore: cast_nullable_to_non_nullable | ||||
| as int, | ||||
| @@ -337,30 +335,6 @@ as int, | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @override | ||||
| @pragma('vm:prefer-inline') | ||||
| $SnChatMessageCopyWith<$Res>? get repliedMessage { | ||||
|     if (_self.repliedMessage == null) { | ||||
|     return null; | ||||
|   } | ||||
|  | ||||
|   return $SnChatMessageCopyWith<$Res>(_self.repliedMessage!, (value) { | ||||
|     return _then(_self.copyWith(repliedMessage: value)); | ||||
|   }); | ||||
| }/// Create a copy of SnChatMessage | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @override | ||||
| @pragma('vm:prefer-inline') | ||||
| $SnChatMessageCopyWith<$Res>? get forwardedMessage { | ||||
|     if (_self.forwardedMessage == null) { | ||||
|     return null; | ||||
|   } | ||||
|  | ||||
|   return $SnChatMessageCopyWith<$Res>(_self.forwardedMessage!, (value) { | ||||
|     return _then(_self.copyWith(forwardedMessage: value)); | ||||
|   }); | ||||
| }/// Create a copy of SnChatMessage | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @override | ||||
| @pragma('vm:prefer-inline') | ||||
| $SnChatMemberCopyWith<$Res> get sender { | ||||
|    | ||||
|   return $SnChatMemberCopyWith<$Res>(_self.sender, (value) { | ||||
| @@ -374,7 +348,7 @@ $SnChatMemberCopyWith<$Res> get sender { | ||||
| @JsonSerializable() | ||||
|  | ||||
| class _SnChatMessage implements SnChatMessage { | ||||
|   const _SnChatMessage({required this.createdAt, required this.updatedAt, this.deletedAt, required this.id, this.content, this.nonce, final  Map<String, dynamic> meta = const {}, final  List<String> membersMetioned = const [], this.editedAt, final  List<SnCloudFile> attachments = const [], final  List<SnChatReaction> reactions = const [], this.repliedMessageId, this.repliedMessage, this.forwardedMessageId, this.forwardedMessage, required this.senderId, required this.sender, required this.chatRoomId}): _meta = meta,_membersMetioned = membersMetioned,_attachments = attachments,_reactions = reactions; | ||||
|   const _SnChatMessage({required this.createdAt, required this.updatedAt, this.deletedAt, required this.id, this.content, this.nonce, final  Map<String, dynamic> meta = const {}, final  List<String> membersMetioned = const [], this.editedAt, final  List<SnCloudFile> attachments = const [], final  List<SnChatReaction> reactions = const [], this.repliedMessageId, this.forwardedMessageId, required this.senderId, required this.sender, required this.chatRoomId}): _meta = meta,_membersMetioned = membersMetioned,_attachments = attachments,_reactions = reactions; | ||||
|   factory _SnChatMessage.fromJson(Map<String, dynamic> json) => _$SnChatMessageFromJson(json); | ||||
|  | ||||
| @override final  DateTime createdAt; | ||||
| @@ -413,9 +387,7 @@ class _SnChatMessage implements SnChatMessage { | ||||
| } | ||||
|  | ||||
| @override final  String? repliedMessageId; | ||||
| @override final  SnChatMessage? repliedMessage; | ||||
| @override final  String? forwardedMessageId; | ||||
| @override final  SnChatMessage? forwardedMessage; | ||||
| @override final  String senderId; | ||||
| @override final  SnChatMember sender; | ||||
| @override final  int chatRoomId; | ||||
| @@ -433,16 +405,16 @@ Map<String, dynamic> toJson() { | ||||
|  | ||||
| @override | ||||
| bool operator ==(Object other) { | ||||
|   return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnChatMessage&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&(identical(other.id, id) || other.id == id)&&(identical(other.content, content) || other.content == content)&&(identical(other.nonce, nonce) || other.nonce == nonce)&&const DeepCollectionEquality().equals(other._meta, _meta)&&const DeepCollectionEquality().equals(other._membersMetioned, _membersMetioned)&&(identical(other.editedAt, editedAt) || other.editedAt == editedAt)&&const DeepCollectionEquality().equals(other._attachments, _attachments)&&const DeepCollectionEquality().equals(other._reactions, _reactions)&&(identical(other.repliedMessageId, repliedMessageId) || other.repliedMessageId == repliedMessageId)&&(identical(other.repliedMessage, repliedMessage) || other.repliedMessage == repliedMessage)&&(identical(other.forwardedMessageId, forwardedMessageId) || other.forwardedMessageId == forwardedMessageId)&&(identical(other.forwardedMessage, forwardedMessage) || other.forwardedMessage == forwardedMessage)&&(identical(other.senderId, senderId) || other.senderId == senderId)&&(identical(other.sender, sender) || other.sender == sender)&&(identical(other.chatRoomId, chatRoomId) || other.chatRoomId == chatRoomId)); | ||||
|   return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnChatMessage&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&(identical(other.id, id) || other.id == id)&&(identical(other.content, content) || other.content == content)&&(identical(other.nonce, nonce) || other.nonce == nonce)&&const DeepCollectionEquality().equals(other._meta, _meta)&&const DeepCollectionEquality().equals(other._membersMetioned, _membersMetioned)&&(identical(other.editedAt, editedAt) || other.editedAt == editedAt)&&const DeepCollectionEquality().equals(other._attachments, _attachments)&&const DeepCollectionEquality().equals(other._reactions, _reactions)&&(identical(other.repliedMessageId, repliedMessageId) || other.repliedMessageId == repliedMessageId)&&(identical(other.forwardedMessageId, forwardedMessageId) || other.forwardedMessageId == forwardedMessageId)&&(identical(other.senderId, senderId) || other.senderId == senderId)&&(identical(other.sender, sender) || other.sender == sender)&&(identical(other.chatRoomId, chatRoomId) || other.chatRoomId == chatRoomId)); | ||||
| } | ||||
|  | ||||
| @JsonKey(includeFromJson: false, includeToJson: false) | ||||
| @override | ||||
| int get hashCode => Object.hash(runtimeType,createdAt,updatedAt,deletedAt,id,content,nonce,const DeepCollectionEquality().hash(_meta),const DeepCollectionEquality().hash(_membersMetioned),editedAt,const DeepCollectionEquality().hash(_attachments),const DeepCollectionEquality().hash(_reactions),repliedMessageId,repliedMessage,forwardedMessageId,forwardedMessage,senderId,sender,chatRoomId); | ||||
| int get hashCode => Object.hash(runtimeType,createdAt,updatedAt,deletedAt,id,content,nonce,const DeepCollectionEquality().hash(_meta),const DeepCollectionEquality().hash(_membersMetioned),editedAt,const DeepCollectionEquality().hash(_attachments),const DeepCollectionEquality().hash(_reactions),repliedMessageId,forwardedMessageId,senderId,sender,chatRoomId); | ||||
|  | ||||
| @override | ||||
| String toString() { | ||||
|   return 'SnChatMessage(createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, id: $id, content: $content, nonce: $nonce, meta: $meta, membersMetioned: $membersMetioned, editedAt: $editedAt, attachments: $attachments, reactions: $reactions, repliedMessageId: $repliedMessageId, repliedMessage: $repliedMessage, forwardedMessageId: $forwardedMessageId, forwardedMessage: $forwardedMessage, senderId: $senderId, sender: $sender, chatRoomId: $chatRoomId)'; | ||||
|   return 'SnChatMessage(createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, id: $id, content: $content, nonce: $nonce, meta: $meta, membersMetioned: $membersMetioned, editedAt: $editedAt, attachments: $attachments, reactions: $reactions, repliedMessageId: $repliedMessageId, forwardedMessageId: $forwardedMessageId, senderId: $senderId, sender: $sender, chatRoomId: $chatRoomId)'; | ||||
| } | ||||
|  | ||||
|  | ||||
| @@ -453,11 +425,11 @@ abstract mixin class _$SnChatMessageCopyWith<$Res> implements $SnChatMessageCopy | ||||
|   factory _$SnChatMessageCopyWith(_SnChatMessage value, $Res Function(_SnChatMessage) _then) = __$SnChatMessageCopyWithImpl; | ||||
| @override @useResult | ||||
| $Res call({ | ||||
|  DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, String id, String? content, String? nonce, Map<String, dynamic> meta, List<String> membersMetioned, DateTime? editedAt, List<SnCloudFile> attachments, List<SnChatReaction> reactions, String? repliedMessageId, SnChatMessage? repliedMessage, String? forwardedMessageId, SnChatMessage? forwardedMessage, String senderId, SnChatMember sender, int chatRoomId | ||||
|  DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, String id, String? content, String? nonce, Map<String, dynamic> meta, List<String> membersMetioned, DateTime? editedAt, List<SnCloudFile> attachments, List<SnChatReaction> reactions, String? repliedMessageId, String? forwardedMessageId, String senderId, SnChatMember sender, int chatRoomId | ||||
| }); | ||||
|  | ||||
|  | ||||
| @override $SnChatMessageCopyWith<$Res>? get repliedMessage;@override $SnChatMessageCopyWith<$Res>? get forwardedMessage;@override $SnChatMemberCopyWith<$Res> get sender; | ||||
| @override $SnChatMemberCopyWith<$Res> get sender; | ||||
|  | ||||
| } | ||||
| /// @nodoc | ||||
| @@ -470,7 +442,7 @@ class __$SnChatMessageCopyWithImpl<$Res> | ||||
|  | ||||
| /// Create a copy of SnChatMessage | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @override @pragma('vm:prefer-inline') $Res call({Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,Object? id = null,Object? content = freezed,Object? nonce = freezed,Object? meta = null,Object? membersMetioned = null,Object? editedAt = freezed,Object? attachments = null,Object? reactions = null,Object? repliedMessageId = freezed,Object? repliedMessage = freezed,Object? forwardedMessageId = freezed,Object? forwardedMessage = freezed,Object? senderId = null,Object? sender = null,Object? chatRoomId = null,}) { | ||||
| @override @pragma('vm:prefer-inline') $Res call({Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,Object? id = null,Object? content = freezed,Object? nonce = freezed,Object? meta = null,Object? membersMetioned = null,Object? editedAt = freezed,Object? attachments = null,Object? reactions = null,Object? repliedMessageId = freezed,Object? forwardedMessageId = freezed,Object? senderId = null,Object? sender = null,Object? chatRoomId = null,}) { | ||||
|   return _then(_SnChatMessage( | ||||
| createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable | ||||
| as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable | ||||
| @@ -484,10 +456,8 @@ as List<String>,editedAt: freezed == editedAt ? _self.editedAt : editedAt // ign | ||||
| as DateTime?,attachments: null == attachments ? _self._attachments : attachments // ignore: cast_nullable_to_non_nullable | ||||
| as List<SnCloudFile>,reactions: null == reactions ? _self._reactions : reactions // ignore: cast_nullable_to_non_nullable | ||||
| as List<SnChatReaction>,repliedMessageId: freezed == repliedMessageId ? _self.repliedMessageId : repliedMessageId // ignore: cast_nullable_to_non_nullable | ||||
| as String?,repliedMessage: freezed == repliedMessage ? _self.repliedMessage : repliedMessage // ignore: cast_nullable_to_non_nullable | ||||
| as SnChatMessage?,forwardedMessageId: freezed == forwardedMessageId ? _self.forwardedMessageId : forwardedMessageId // ignore: cast_nullable_to_non_nullable | ||||
| as String?,forwardedMessage: freezed == forwardedMessage ? _self.forwardedMessage : forwardedMessage // ignore: cast_nullable_to_non_nullable | ||||
| as SnChatMessage?,senderId: null == senderId ? _self.senderId : senderId // ignore: cast_nullable_to_non_nullable | ||||
| as String?,forwardedMessageId: freezed == forwardedMessageId ? _self.forwardedMessageId : forwardedMessageId // ignore: cast_nullable_to_non_nullable | ||||
| as String?,senderId: null == senderId ? _self.senderId : senderId // ignore: cast_nullable_to_non_nullable | ||||
| as String,sender: null == sender ? _self.sender : sender // ignore: cast_nullable_to_non_nullable | ||||
| as SnChatMember,chatRoomId: null == chatRoomId ? _self.chatRoomId : chatRoomId // ignore: cast_nullable_to_non_nullable | ||||
| as int, | ||||
| @@ -498,30 +468,6 @@ as int, | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @override | ||||
| @pragma('vm:prefer-inline') | ||||
| $SnChatMessageCopyWith<$Res>? get repliedMessage { | ||||
|     if (_self.repliedMessage == null) { | ||||
|     return null; | ||||
|   } | ||||
|  | ||||
|   return $SnChatMessageCopyWith<$Res>(_self.repliedMessage!, (value) { | ||||
|     return _then(_self.copyWith(repliedMessage: value)); | ||||
|   }); | ||||
| }/// Create a copy of SnChatMessage | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @override | ||||
| @pragma('vm:prefer-inline') | ||||
| $SnChatMessageCopyWith<$Res>? get forwardedMessage { | ||||
|     if (_self.forwardedMessage == null) { | ||||
|     return null; | ||||
|   } | ||||
|  | ||||
|   return $SnChatMessageCopyWith<$Res>(_self.forwardedMessage!, (value) { | ||||
|     return _then(_self.copyWith(forwardedMessage: value)); | ||||
|   }); | ||||
| }/// Create a copy of SnChatMessage | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @override | ||||
| @pragma('vm:prefer-inline') | ||||
| $SnChatMemberCopyWith<$Res> get sender { | ||||
|    | ||||
|   return $SnChatMemberCopyWith<$Res>(_self.sender, (value) { | ||||
|   | ||||
| @@ -84,19 +84,7 @@ _SnChatMessage _$SnChatMessageFromJson(Map<String, dynamic> json) => | ||||
|               .toList() ?? | ||||
|           const [], | ||||
|       repliedMessageId: json['replied_message_id'] as String?, | ||||
|       repliedMessage: | ||||
|           json['replied_message'] == null | ||||
|               ? null | ||||
|               : SnChatMessage.fromJson( | ||||
|                 json['replied_message'] as Map<String, dynamic>, | ||||
|               ), | ||||
|       forwardedMessageId: json['forwarded_message_id'] as String?, | ||||
|       forwardedMessage: | ||||
|           json['forwarded_message'] == null | ||||
|               ? null | ||||
|               : SnChatMessage.fromJson( | ||||
|                 json['forwarded_message'] as Map<String, dynamic>, | ||||
|               ), | ||||
|       senderId: json['sender_id'] as String, | ||||
|       sender: SnChatMember.fromJson(json['sender'] as Map<String, dynamic>), | ||||
|       chatRoomId: (json['chat_room_id'] as num).toInt(), | ||||
| @@ -116,9 +104,7 @@ Map<String, dynamic> _$SnChatMessageToJson(_SnChatMessage instance) => | ||||
|       'attachments': instance.attachments.map((e) => e.toJson()).toList(), | ||||
|       'reactions': instance.reactions.map((e) => e.toJson()).toList(), | ||||
|       'replied_message_id': instance.repliedMessageId, | ||||
|       'replied_message': instance.repliedMessage?.toJson(), | ||||
|       'forwarded_message_id': instance.forwardedMessageId, | ||||
|       'forwarded_message': instance.forwardedMessage?.toJson(), | ||||
|       'sender_id': instance.senderId, | ||||
|       'sender': instance.sender.toJson(), | ||||
|       'chat_room_id': instance.chatRoomId, | ||||
|   | ||||
| @@ -9,6 +9,7 @@ import 'package:island/database/message.dart'; | ||||
| import 'package:island/database/message_repository.dart'; | ||||
| import 'package:island/models/chat.dart'; | ||||
| import 'package:island/models/file.dart'; | ||||
| import 'package:island/pods/config.dart'; | ||||
| import 'package:island/pods/message.dart'; | ||||
| import 'package:island/pods/network.dart'; | ||||
| import 'package:island/pods/websocket.dart'; | ||||
| @@ -17,6 +18,7 @@ import 'package:island/widgets/alert.dart'; | ||||
| import 'package:island/widgets/content/cloud_files.dart'; | ||||
| import 'package:material_symbols_icons/material_symbols_icons.dart'; | ||||
| import 'package:styled_widget/styled_widget.dart'; | ||||
| import 'package:super_context_menu/super_context_menu.dart'; | ||||
| import 'package:uuid/uuid.dart'; | ||||
| import 'chat.dart'; | ||||
|  | ||||
| @@ -94,7 +96,13 @@ class MessagesNotifier | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   Future<void> sendMessage(String content) async { | ||||
|   Future<void> sendMessage( | ||||
|     String content, | ||||
|     List<UniversalFile> attachments, { | ||||
|     SnChatMessage? replyingTo, | ||||
|     SnChatMessage? forwardingTo, | ||||
|     SnChatMessage? editingTo, | ||||
|   }) async { | ||||
|     try { | ||||
|       final repository = await _ref.read( | ||||
|         messageRepositoryProvider(_roomId).future, | ||||
| @@ -102,7 +110,28 @@ class MessagesNotifier | ||||
|  | ||||
|       final nonce = const Uuid().v4(); | ||||
|  | ||||
|       final messageTask = repository.sendMessage(_roomId, content, nonce); | ||||
|       final baseUrl = _ref.read(serverUrlProvider); | ||||
|       final atk = await getFreshAtk( | ||||
|         _ref.watch(tokenPairProvider), | ||||
|         baseUrl, | ||||
|         onRefreshed: (atk, rtk) { | ||||
|           setTokenPair(_ref.watch(sharedPreferencesProvider), atk, rtk); | ||||
|           _ref.invalidate(tokenPairProvider); | ||||
|         }, | ||||
|       ); | ||||
|       if (atk == null) throw Exception("Unauthorized"); | ||||
|  | ||||
|       final messageTask = repository.sendMessage( | ||||
|         atk, | ||||
|         baseUrl, | ||||
|         _roomId, | ||||
|         content, | ||||
|         nonce, | ||||
|         attachments: attachments, | ||||
|         replyingTo: replyingTo, | ||||
|         forwardingTo: forwardingTo, | ||||
|         editingTo: editingTo, | ||||
|       ); | ||||
|       final pendingMessage = repository.pendingMessages.values.firstWhereOrNull( | ||||
|         (m) => m.roomId == _roomId && m.nonce == nonce, | ||||
|       ); | ||||
| @@ -288,6 +317,18 @@ class MessagesNotifier | ||||
|       showErrorAlert(err); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   Future<LocalChatMessage?> fetchMessageById(String messageId) async { | ||||
|     try { | ||||
|       final repository = await _ref.read( | ||||
|         messageRepositoryProvider(_roomId).future, | ||||
|       ); | ||||
|       return await repository.getMessageById(messageId); | ||||
|     } catch (err) { | ||||
|       showErrorAlert(err); | ||||
|       return null; | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| @RoutePage() | ||||
| @@ -306,6 +347,10 @@ class ChatRoomScreen extends HookConsumerWidget { | ||||
|     final messageController = useTextEditingController(); | ||||
|     final scrollController = useScrollController(); | ||||
|  | ||||
|     final messageReplyingTo = useState<SnChatMessage?>(null); | ||||
|     final messageForwardingTo = useState<SnChatMessage?>(null); | ||||
|     final messageEditingTo = useState<SnChatMessage?>(null); | ||||
|  | ||||
|     // Add scroll listener for pagination | ||||
|     useEffect(() { | ||||
|       void onScroll() { | ||||
| @@ -340,9 +385,17 @@ class ChatRoomScreen extends HookConsumerWidget { | ||||
|       return () => subscription.cancel(); | ||||
|     }, [ws, chatRoom]); | ||||
|  | ||||
|     final attachments = useState<List<UniversalFile>>([]); | ||||
|  | ||||
|     void sendMessage() { | ||||
|       if (messageController.text.trim().isNotEmpty) { | ||||
|         messagesNotifier.sendMessage(messageController.text.trim()); | ||||
|         messagesNotifier.sendMessage( | ||||
|           messageController.text.trim(), | ||||
|           attachments.value, | ||||
|           editingTo: messageEditingTo.value, | ||||
|           forwardingTo: messageForwardingTo.value, | ||||
|           replyingTo: messageReplyingTo.value, | ||||
|         ); | ||||
|         messageController.clear(); | ||||
|       } | ||||
|     } | ||||
| @@ -410,11 +463,29 @@ class ChatRoomScreen extends HookConsumerWidget { | ||||
|                                       message: message, | ||||
|                                       isCurrentUser: | ||||
|                                           identity?.id == message.senderId, | ||||
|                                       onAction: (action) { | ||||
|                                         switch (action) { | ||||
|                                           case _MessageBubbleAction.delete: | ||||
|                                             messagesNotifier.deleteMessage( | ||||
|                                               message.id, | ||||
|                                             ); | ||||
|                                           case _MessageBubbleAction.edit: | ||||
|                                             messageEditingTo.value = | ||||
|                                                 message.toRemoteMessage(); | ||||
|                                           case _MessageBubbleAction.forward: | ||||
|                                             messageForwardingTo.value = | ||||
|                                                 message.toRemoteMessage(); | ||||
|                                           case _MessageBubbleAction.reply: | ||||
|                                             messageReplyingTo.value = | ||||
|                                                 message.toRemoteMessage(); | ||||
|                                         } | ||||
|                                       }, | ||||
|                                     ), | ||||
|                                 loading: | ||||
|                                     () => _MessageBubble( | ||||
|                                       message: message, | ||||
|                                       isCurrentUser: false, | ||||
|                                       onAction: null, | ||||
|                                     ), | ||||
|                                 error: (_, __) => const SizedBox.shrink(), | ||||
|                               ); | ||||
| @@ -442,6 +513,17 @@ class ChatRoomScreen extends HookConsumerWidget { | ||||
|                   messageController: messageController, | ||||
|                   chatRoom: room!, | ||||
|                   onSend: sendMessage, | ||||
|                   onClear: () { | ||||
|                     if (messageEditingTo.value != null) { | ||||
|                       messageController.clear(); | ||||
|                     } | ||||
|                     messageEditingTo.value = null; | ||||
|                     messageReplyingTo.value = null; | ||||
|                     messageForwardingTo.value = null; | ||||
|                   }, | ||||
|                   messageEditingTo: messageEditingTo.value, | ||||
|                   messageReplyingTo: messageReplyingTo.value, | ||||
|                   messageForwardingTo: messageForwardingTo.value, | ||||
|                 ), | ||||
|             error: (_, __) => const SizedBox.shrink(), | ||||
|             loading: () => const SizedBox.shrink(), | ||||
| @@ -456,11 +538,19 @@ class _ChatInput extends StatelessWidget { | ||||
|   final TextEditingController messageController; | ||||
|   final SnChat chatRoom; | ||||
|   final VoidCallback onSend; | ||||
|   final VoidCallback onClear; | ||||
|   final SnChatMessage? messageReplyingTo; | ||||
|   final SnChatMessage? messageForwardingTo; | ||||
|   final SnChatMessage? messageEditingTo; | ||||
|  | ||||
|   const _ChatInput({ | ||||
|     required this.messageController, | ||||
|     required this.chatRoom, | ||||
|     required this.onSend, | ||||
|     required this.onClear, | ||||
|     required this.messageReplyingTo, | ||||
|     required this.messageForwardingTo, | ||||
|     required this.messageEditingTo, | ||||
|   }); | ||||
|  | ||||
|   @override | ||||
| @@ -468,7 +558,52 @@ class _ChatInput extends StatelessWidget { | ||||
|     return Material( | ||||
|       elevation: 8, | ||||
|       color: Theme.of(context).colorScheme.surface, | ||||
|       child: Padding( | ||||
|       child: Column( | ||||
|         children: [ | ||||
|           if (messageReplyingTo != null || | ||||
|               messageForwardingTo != null || | ||||
|               messageEditingTo != null) | ||||
|             Container( | ||||
|               padding: const EdgeInsets.symmetric(horizontal: 16), | ||||
|               decoration: BoxDecoration( | ||||
|                 color: Theme.of(context).colorScheme.surfaceContainer, | ||||
|                 borderRadius: BorderRadius.circular(8), | ||||
|               ), | ||||
|               margin: const EdgeInsets.only(left: 8, right: 8, top: 8), | ||||
|               child: Row( | ||||
|                 children: [ | ||||
|                   Icon( | ||||
|                     messageReplyingTo != null | ||||
|                         ? Symbols.reply | ||||
|                         : messageForwardingTo != null | ||||
|                         ? Symbols.forward | ||||
|                         : Symbols.edit, | ||||
|                     size: 20, | ||||
|                     color: Theme.of(context).colorScheme.primary, | ||||
|                   ), | ||||
|                   const Gap(8), | ||||
|                   Expanded( | ||||
|                     child: Text( | ||||
|                       messageReplyingTo != null | ||||
|                           ? 'Replying to ${messageReplyingTo?.sender.account.nick}' | ||||
|                           : messageForwardingTo != null | ||||
|                           ? 'Forwarding message' | ||||
|                           : 'Editing message', | ||||
|                       style: Theme.of(context).textTheme.bodySmall, | ||||
|                       maxLines: 1, | ||||
|                       overflow: TextOverflow.ellipsis, | ||||
|                     ), | ||||
|                   ), | ||||
|                   IconButton( | ||||
|                     icon: const Icon(Icons.close, size: 20), | ||||
|                     onPressed: onClear, | ||||
|                     padding: EdgeInsets.zero, | ||||
|                     constraints: const BoxConstraints(), | ||||
|                   ), | ||||
|                 ], | ||||
|               ), | ||||
|             ), | ||||
|           Padding( | ||||
|             padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 8), | ||||
|             child: Row( | ||||
|               children: [ | ||||
| @@ -498,19 +633,80 @@ class _ChatInput extends StatelessWidget { | ||||
|               ], | ||||
|             ).padding(bottom: MediaQuery.of(context).padding.bottom), | ||||
|           ), | ||||
|         ], | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| class _MessageBubble extends StatelessWidget { | ||||
| class _MessageBubbleAction { | ||||
|   static const String edit = "edit"; | ||||
|   static const String delete = "delete"; | ||||
|   static const String reply = "reply"; | ||||
|   static const String forward = "forward"; | ||||
| } | ||||
|  | ||||
| class _MessageBubble extends HookConsumerWidget { | ||||
|   final LocalChatMessage message; | ||||
|   final bool isCurrentUser; | ||||
|   final Function(String action)? onAction; | ||||
|  | ||||
|   const _MessageBubble({required this.message, required this.isCurrentUser}); | ||||
|   const _MessageBubble({ | ||||
|     required this.message, | ||||
|     required this.isCurrentUser, | ||||
|     required this.onAction, | ||||
|   }); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return Padding( | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
|     final messagesNotifier = ref.watch( | ||||
|       messagesProvider(message.roomId).notifier, | ||||
|     ); | ||||
|  | ||||
|     final textColor = isCurrentUser ? Colors.white : Colors.black; | ||||
|  | ||||
|     return ContextMenuWidget( | ||||
|       menuProvider: (_) { | ||||
|         if (onAction == null) return Menu(children: []); | ||||
|         return Menu( | ||||
|           children: [ | ||||
|             if (isCurrentUser) | ||||
|               MenuAction( | ||||
|                 title: 'edit'.tr(), | ||||
|                 image: MenuImage.icon(Symbols.edit), | ||||
|                 callback: () { | ||||
|                   onAction!.call(_MessageBubbleAction.edit); | ||||
|                 }, | ||||
|               ), | ||||
|             if (isCurrentUser) | ||||
|               MenuAction( | ||||
|                 title: 'delete'.tr(), | ||||
|                 image: MenuImage.icon(Symbols.delete), | ||||
|                 callback: () { | ||||
|                   onAction!.call(_MessageBubbleAction.delete); | ||||
|                 }, | ||||
|               ), | ||||
|             if (isCurrentUser) MenuSeparator(), | ||||
|             MenuAction( | ||||
|               title: 'reply'.tr(), | ||||
|               image: MenuImage.icon(Symbols.reply), | ||||
|               callback: () { | ||||
|                 onAction!.call(_MessageBubbleAction.reply); | ||||
|               }, | ||||
|             ), | ||||
|             MenuAction( | ||||
|               title: 'forward'.tr(), | ||||
|               image: MenuImage.icon(Symbols.forward), | ||||
|               callback: () { | ||||
|                 onAction!.call(_MessageBubbleAction.forward); | ||||
|               }, | ||||
|             ), | ||||
|           ], | ||||
|         ); | ||||
|       }, | ||||
|       child: Material( | ||||
|         color: Theme.of(context).colorScheme.surface, | ||||
|         child: Padding( | ||||
|           padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), | ||||
|           child: Row( | ||||
|             mainAxisAlignment: | ||||
| @@ -519,28 +715,48 @@ class _MessageBubble extends StatelessWidget { | ||||
|               if (!isCurrentUser) | ||||
|                 ProfilePictureWidget( | ||||
|                   fileId: | ||||
|                   message.toRemoteMessage().sender.account.profile.pictureId, | ||||
|                       message | ||||
|                           .toRemoteMessage() | ||||
|                           .sender | ||||
|                           .account | ||||
|                           .profile | ||||
|                           .pictureId, | ||||
|                   radius: 18, | ||||
|                 ), | ||||
|               const Gap(8), | ||||
|               Flexible( | ||||
|                 child: Container( | ||||
|               padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), | ||||
|                   padding: const EdgeInsets.symmetric( | ||||
|                     horizontal: 12, | ||||
|                     vertical: 8, | ||||
|                   ), | ||||
|                   decoration: BoxDecoration( | ||||
|                     color: | ||||
|                         isCurrentUser | ||||
|                         ? Theme.of(context).colorScheme.primary.withOpacity(0.8) | ||||
|                             ? Theme.of( | ||||
|                               context, | ||||
|                             ).colorScheme.primary.withOpacity(0.8) | ||||
|                             : Colors.grey.shade200, | ||||
|                     borderRadius: BorderRadius.circular(16), | ||||
|                   ), | ||||
|                   child: Column( | ||||
|                     crossAxisAlignment: CrossAxisAlignment.start, | ||||
|                     children: [ | ||||
|                       if (message.toRemoteMessage().repliedMessageId != null) | ||||
|                         _MessageQuoteWidget( | ||||
|                           message: message, | ||||
|                           textColor: textColor, | ||||
|                           isReply: true, | ||||
|                         ), | ||||
|                       if (message.toRemoteMessage().forwardedMessageId != null) | ||||
|                         _MessageQuoteWidget( | ||||
|                           message: message, | ||||
|                           textColor: textColor, | ||||
|                           isReply: false, | ||||
|                         ), | ||||
|                       Text( | ||||
|                         message.toRemoteMessage().content ?? '', | ||||
|                     style: TextStyle( | ||||
|                       color: isCurrentUser ? Colors.white : Colors.black, | ||||
|                     ), | ||||
|                         style: TextStyle(color: textColor), | ||||
|                       ), | ||||
|                       const Gap(4), | ||||
|                       Row( | ||||
| @@ -548,11 +764,7 @@ class _MessageBubble extends StatelessWidget { | ||||
|                         children: [ | ||||
|                           Text( | ||||
|                             DateFormat.Hm().format(message.createdAt.toLocal()), | ||||
|                         style: TextStyle( | ||||
|                           fontSize: 10, | ||||
|                           color: | ||||
|                               isCurrentUser ? Colors.white70 : Colors.black54, | ||||
|                         ), | ||||
|                             style: TextStyle(fontSize: 10, color: textColor), | ||||
|                           ), | ||||
|                           const Gap(4), | ||||
|                           if (isCurrentUser) | ||||
| @@ -567,11 +779,18 @@ class _MessageBubble extends StatelessWidget { | ||||
|               if (isCurrentUser) | ||||
|                 ProfilePictureWidget( | ||||
|                   fileId: | ||||
|                   message.toRemoteMessage().sender.account.profile.pictureId, | ||||
|                       message | ||||
|                           .toRemoteMessage() | ||||
|                           .sender | ||||
|                           .account | ||||
|                           .profile | ||||
|                           .pictureId, | ||||
|                   radius: 18, | ||||
|                 ), | ||||
|             ], | ||||
|           ), | ||||
|         ), | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
| @@ -600,3 +819,73 @@ class _MessageBubble extends StatelessWidget { | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| class _MessageQuoteWidget extends HookConsumerWidget { | ||||
|   final LocalChatMessage message; | ||||
|   final Color textColor; | ||||
|   final bool isReply; | ||||
|  | ||||
|   const _MessageQuoteWidget({ | ||||
|     Key? key, | ||||
|     required this.message, | ||||
|     required this.textColor, | ||||
|     required this.isReply, | ||||
|   }) : super(key: key); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
|     final messagesNotifier = ref.watch( | ||||
|       messagesProvider(message.roomId).notifier, | ||||
|     ); | ||||
|  | ||||
|     return FutureBuilder<LocalChatMessage?>( | ||||
|       future: messagesNotifier.fetchMessageById( | ||||
|         isReply | ||||
|             ? message.toRemoteMessage().repliedMessageId! | ||||
|             : message.toRemoteMessage().forwardedMessageId!, | ||||
|       ), | ||||
|       builder: (context, snapshot) { | ||||
|         if (snapshot.hasData) { | ||||
|           return ClipRRect( | ||||
|             borderRadius: BorderRadius.all(Radius.circular(8)), | ||||
|             child: Container( | ||||
|               padding: EdgeInsets.symmetric(vertical: 4, horizontal: 6), | ||||
|               color: Theme.of(context).colorScheme.surface.withOpacity(0.2), | ||||
|               child: Column( | ||||
|                 crossAxisAlignment: CrossAxisAlignment.start, | ||||
|                 children: [ | ||||
|                   if (isReply) | ||||
|                     Row( | ||||
|                       spacing: 4, | ||||
|                       children: [ | ||||
|                         Icon(Symbols.reply, size: 16, color: textColor), | ||||
|                         Text( | ||||
|                           'Replying to ${snapshot.data!.toRemoteMessage().sender.account.nick}', | ||||
|                         ).textColor(textColor).bold(), | ||||
|                       ], | ||||
|                     ) | ||||
|                   else | ||||
|                     Row( | ||||
|                       spacing: 4, | ||||
|                       children: [ | ||||
|                         Icon(Symbols.forward, size: 16, color: textColor), | ||||
|                         Text( | ||||
|                           'Forwarded from ${snapshot.data!.toRemoteMessage().sender.account.nick}', | ||||
|                         ).textColor(textColor).bold(), | ||||
|                       ], | ||||
|                     ), | ||||
|                   Text( | ||||
|                     snapshot.data!.toRemoteMessage().content ?? "", | ||||
|                     style: TextStyle(color: textColor), | ||||
|                   ), | ||||
|                 ], | ||||
|               ), | ||||
|             ), | ||||
|           ).padding(bottom: 4); | ||||
|         } else { | ||||
|           return SizedBox.shrink(); | ||||
|         } | ||||
|       }, | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user