✨ Reply / forward mesage
This commit is contained in:
		| @@ -87,5 +87,7 @@ | |||||||
|   }, |   }, | ||||||
|   "permissionOwner": "Owner", |   "permissionOwner": "Owner", | ||||||
|   "permissionModerator": "Moderator", |   "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/database/message.dart'; | ||||||
| import 'package:island/models/chat.dart'; | import 'package:island/models/chat.dart'; | ||||||
| import 'package:island/models/file.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:island/widgets/alert.dart'; | ||||||
| import 'package:uuid/uuid.dart'; | import 'package:uuid/uuid.dart'; | ||||||
|  |  | ||||||
| @@ -169,11 +171,16 @@ class MessageRepository { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   Future<LocalChatMessage> sendMessage( |   Future<LocalChatMessage> sendMessage( | ||||||
|  |     String atk, | ||||||
|  |     String baseUrl, | ||||||
|     int roomId, |     int roomId, | ||||||
|     String content, |     String content, | ||||||
|     String nonce, { |     String nonce, { | ||||||
|     List<SnCloudFile>? attachments, |     required List<UniversalFile> attachments, | ||||||
|     Map<String, dynamic>? meta, |     Map<String, dynamic>? meta, | ||||||
|  |     SnChatMessage? replyingTo, | ||||||
|  |     SnChatMessage? forwardingTo, | ||||||
|  |     SnChatMessage? editingTo, | ||||||
|   }) async { |   }) async { | ||||||
|     // Generate a unique nonce for this message |     // Generate a unique nonce for this message | ||||||
|     final nonce = const Uuid().v4(); |     final nonce = const Uuid().v4(); | ||||||
| @@ -200,15 +207,44 @@ class MessageRepository { | |||||||
|     await _database.saveMessage(_database.messageToCompanion(localMessage)); |     await _database.saveMessage(_database.messageToCompanion(localMessage)); | ||||||
|  |  | ||||||
|     try { |     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 |       // Send to server | ||||||
|       final response = await _apiClient.post( |       final response = await _apiClient.request( | ||||||
|         '/chat/$roomId/messages', |         editingTo == null | ||||||
|  |             ? '/chat/$roomId/messages' | ||||||
|  |             : '/chat/$roomId/messages/${editingTo.id}', | ||||||
|         data: { |         data: { | ||||||
|           'content': content, |           '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, |           'meta': meta, | ||||||
|           'nonce': nonce, |           'nonce': nonce, | ||||||
|         }, |         }, | ||||||
|  |         options: Options(method: editingTo == null ? 'POST' : 'PATCH'), | ||||||
|       ); |       ); | ||||||
|  |  | ||||||
|       // Update with server response |       // Update with server response | ||||||
| @@ -380,4 +416,33 @@ class MessageRepository { | |||||||
|       rethrow; |       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<SnCloudFile> attachments, | ||||||
|     @Default([]) List<SnChatReaction> reactions, |     @Default([]) List<SnChatReaction> reactions, | ||||||
|     String? repliedMessageId, |     String? repliedMessageId, | ||||||
|     SnChatMessage? repliedMessage, |  | ||||||
|     String? forwardedMessageId, |     String? forwardedMessageId, | ||||||
|     SnChatMessage? forwardedMessage, |  | ||||||
|     required String senderId, |     required String senderId, | ||||||
|     required SnChatMember sender, |     required SnChatMember sender, | ||||||
|     required int chatRoomId, |     required int chatRoomId, | ||||||
|   | |||||||
| @@ -260,7 +260,7 @@ $SnRealmCopyWith<$Res>? get realm { | |||||||
| /// @nodoc | /// @nodoc | ||||||
| mixin _$SnChatMessage { | 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 | /// Create a copy of SnChatMessage | ||||||
| /// with the given fields replaced by the non-null parameter values. | /// with the given fields replaced by the non-null parameter values. | ||||||
| @JsonKey(includeFromJson: false, includeToJson: false) | @JsonKey(includeFromJson: false, includeToJson: false) | ||||||
| @@ -273,16 +273,16 @@ $SnChatMessageCopyWith<SnChatMessage> get copyWith => _$SnChatMessageCopyWithImp | |||||||
|  |  | ||||||
| @override | @override | ||||||
| bool operator ==(Object other) { | 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) | @JsonKey(includeFromJson: false, includeToJson: false) | ||||||
| @override | @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 | @override | ||||||
| String toString() { | 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; |   factory $SnChatMessageCopyWith(SnChatMessage value, $Res Function(SnChatMessage) _then) = _$SnChatMessageCopyWithImpl; | ||||||
| @useResult | @useResult | ||||||
| $Res call({ | $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 | /// @nodoc | ||||||
| @@ -310,7 +310,7 @@ class _$SnChatMessageCopyWithImpl<$Res> | |||||||
|  |  | ||||||
| /// Create a copy of SnChatMessage | /// Create a copy of SnChatMessage | ||||||
| /// with the given fields replaced by the non-null parameter values. | /// 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( |   return _then(_self.copyWith( | ||||||
| createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable | 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 | 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 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<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 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 String?,forwardedMessageId: freezed == forwardedMessageId ? _self.forwardedMessageId : forwardedMessageId // ignore: cast_nullable_to_non_nullable | ||||||
| as SnChatMessage?,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?,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,sender: null == sender ? _self.sender : sender // 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 SnChatMember,chatRoomId: null == chatRoomId ? _self.chatRoomId : chatRoomId // ignore: cast_nullable_to_non_nullable | ||||||
| as int, | as int, | ||||||
| @@ -337,30 +335,6 @@ as int, | |||||||
| /// with the given fields replaced by the non-null parameter values. | /// with the given fields replaced by the non-null parameter values. | ||||||
| @override | @override | ||||||
| @pragma('vm:prefer-inline') | @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 { | $SnChatMemberCopyWith<$Res> get sender { | ||||||
|    |    | ||||||
|   return $SnChatMemberCopyWith<$Res>(_self.sender, (value) { |   return $SnChatMemberCopyWith<$Res>(_self.sender, (value) { | ||||||
| @@ -374,7 +348,7 @@ $SnChatMemberCopyWith<$Res> get sender { | |||||||
| @JsonSerializable() | @JsonSerializable() | ||||||
|  |  | ||||||
| class _SnChatMessage implements SnChatMessage { | 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); |   factory _SnChatMessage.fromJson(Map<String, dynamic> json) => _$SnChatMessageFromJson(json); | ||||||
|  |  | ||||||
| @override final  DateTime createdAt; | @override final  DateTime createdAt; | ||||||
| @@ -413,9 +387,7 @@ class _SnChatMessage implements SnChatMessage { | |||||||
| } | } | ||||||
|  |  | ||||||
| @override final  String? repliedMessageId; | @override final  String? repliedMessageId; | ||||||
| @override final  SnChatMessage? repliedMessage; |  | ||||||
| @override final  String? forwardedMessageId; | @override final  String? forwardedMessageId; | ||||||
| @override final  SnChatMessage? forwardedMessage; |  | ||||||
| @override final  String senderId; | @override final  String senderId; | ||||||
| @override final  SnChatMember sender; | @override final  SnChatMember sender; | ||||||
| @override final  int chatRoomId; | @override final  int chatRoomId; | ||||||
| @@ -433,16 +405,16 @@ Map<String, dynamic> toJson() { | |||||||
|  |  | ||||||
| @override | @override | ||||||
| bool operator ==(Object other) { | 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) | @JsonKey(includeFromJson: false, includeToJson: false) | ||||||
| @override | @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 | @override | ||||||
| String toString() { | 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; |   factory _$SnChatMessageCopyWith(_SnChatMessage value, $Res Function(_SnChatMessage) _then) = __$SnChatMessageCopyWithImpl; | ||||||
| @override @useResult | @override @useResult | ||||||
| $Res call({ | $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 | /// @nodoc | ||||||
| @@ -470,7 +442,7 @@ class __$SnChatMessageCopyWithImpl<$Res> | |||||||
|  |  | ||||||
| /// Create a copy of SnChatMessage | /// Create a copy of SnChatMessage | ||||||
| /// with the given fields replaced by the non-null parameter values. | /// 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( |   return _then(_SnChatMessage( | ||||||
| createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable | 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 | 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 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<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 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 String?,forwardedMessageId: freezed == forwardedMessageId ? _self.forwardedMessageId : forwardedMessageId // ignore: cast_nullable_to_non_nullable | ||||||
| as SnChatMessage?,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?,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,sender: null == sender ? _self.sender : sender // 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 SnChatMember,chatRoomId: null == chatRoomId ? _self.chatRoomId : chatRoomId // ignore: cast_nullable_to_non_nullable | ||||||
| as int, | as int, | ||||||
| @@ -498,30 +468,6 @@ as int, | |||||||
| /// with the given fields replaced by the non-null parameter values. | /// with the given fields replaced by the non-null parameter values. | ||||||
| @override | @override | ||||||
| @pragma('vm:prefer-inline') | @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 { | $SnChatMemberCopyWith<$Res> get sender { | ||||||
|    |    | ||||||
|   return $SnChatMemberCopyWith<$Res>(_self.sender, (value) { |   return $SnChatMemberCopyWith<$Res>(_self.sender, (value) { | ||||||
|   | |||||||
| @@ -84,19 +84,7 @@ _SnChatMessage _$SnChatMessageFromJson(Map<String, dynamic> json) => | |||||||
|               .toList() ?? |               .toList() ?? | ||||||
|           const [], |           const [], | ||||||
|       repliedMessageId: json['replied_message_id'] as String?, |       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?, |       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, |       senderId: json['sender_id'] as String, | ||||||
|       sender: SnChatMember.fromJson(json['sender'] as Map<String, dynamic>), |       sender: SnChatMember.fromJson(json['sender'] as Map<String, dynamic>), | ||||||
|       chatRoomId: (json['chat_room_id'] as num).toInt(), |       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(), |       'attachments': instance.attachments.map((e) => e.toJson()).toList(), | ||||||
|       'reactions': instance.reactions.map((e) => e.toJson()).toList(), |       'reactions': instance.reactions.map((e) => e.toJson()).toList(), | ||||||
|       'replied_message_id': instance.repliedMessageId, |       'replied_message_id': instance.repliedMessageId, | ||||||
|       'replied_message': instance.repliedMessage?.toJson(), |  | ||||||
|       'forwarded_message_id': instance.forwardedMessageId, |       'forwarded_message_id': instance.forwardedMessageId, | ||||||
|       'forwarded_message': instance.forwardedMessage?.toJson(), |  | ||||||
|       'sender_id': instance.senderId, |       'sender_id': instance.senderId, | ||||||
|       'sender': instance.sender.toJson(), |       'sender': instance.sender.toJson(), | ||||||
|       'chat_room_id': instance.chatRoomId, |       '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/database/message_repository.dart'; | ||||||
| import 'package:island/models/chat.dart'; | import 'package:island/models/chat.dart'; | ||||||
| import 'package:island/models/file.dart'; | import 'package:island/models/file.dart'; | ||||||
|  | import 'package:island/pods/config.dart'; | ||||||
| import 'package:island/pods/message.dart'; | import 'package:island/pods/message.dart'; | ||||||
| import 'package:island/pods/network.dart'; | import 'package:island/pods/network.dart'; | ||||||
| import 'package:island/pods/websocket.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:island/widgets/content/cloud_files.dart'; | ||||||
| import 'package:material_symbols_icons/material_symbols_icons.dart'; | import 'package:material_symbols_icons/material_symbols_icons.dart'; | ||||||
| import 'package:styled_widget/styled_widget.dart'; | import 'package:styled_widget/styled_widget.dart'; | ||||||
|  | import 'package:super_context_menu/super_context_menu.dart'; | ||||||
| import 'package:uuid/uuid.dart'; | import 'package:uuid/uuid.dart'; | ||||||
| import 'chat.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 { |     try { | ||||||
|       final repository = await _ref.read( |       final repository = await _ref.read( | ||||||
|         messageRepositoryProvider(_roomId).future, |         messageRepositoryProvider(_roomId).future, | ||||||
| @@ -102,7 +110,28 @@ class MessagesNotifier | |||||||
|  |  | ||||||
|       final nonce = const Uuid().v4(); |       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( |       final pendingMessage = repository.pendingMessages.values.firstWhereOrNull( | ||||||
|         (m) => m.roomId == _roomId && m.nonce == nonce, |         (m) => m.roomId == _roomId && m.nonce == nonce, | ||||||
|       ); |       ); | ||||||
| @@ -288,6 +317,18 @@ class MessagesNotifier | |||||||
|       showErrorAlert(err); |       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() | @RoutePage() | ||||||
| @@ -306,6 +347,10 @@ class ChatRoomScreen extends HookConsumerWidget { | |||||||
|     final messageController = useTextEditingController(); |     final messageController = useTextEditingController(); | ||||||
|     final scrollController = useScrollController(); |     final scrollController = useScrollController(); | ||||||
|  |  | ||||||
|  |     final messageReplyingTo = useState<SnChatMessage?>(null); | ||||||
|  |     final messageForwardingTo = useState<SnChatMessage?>(null); | ||||||
|  |     final messageEditingTo = useState<SnChatMessage?>(null); | ||||||
|  |  | ||||||
|     // Add scroll listener for pagination |     // Add scroll listener for pagination | ||||||
|     useEffect(() { |     useEffect(() { | ||||||
|       void onScroll() { |       void onScroll() { | ||||||
| @@ -340,9 +385,17 @@ class ChatRoomScreen extends HookConsumerWidget { | |||||||
|       return () => subscription.cancel(); |       return () => subscription.cancel(); | ||||||
|     }, [ws, chatRoom]); |     }, [ws, chatRoom]); | ||||||
|  |  | ||||||
|  |     final attachments = useState<List<UniversalFile>>([]); | ||||||
|  |  | ||||||
|     void sendMessage() { |     void sendMessage() { | ||||||
|       if (messageController.text.trim().isNotEmpty) { |       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(); |         messageController.clear(); | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
| @@ -410,11 +463,29 @@ class ChatRoomScreen extends HookConsumerWidget { | |||||||
|                                       message: message, |                                       message: message, | ||||||
|                                       isCurrentUser: |                                       isCurrentUser: | ||||||
|                                           identity?.id == message.senderId, |                                           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: |                                 loading: | ||||||
|                                     () => _MessageBubble( |                                     () => _MessageBubble( | ||||||
|                                       message: message, |                                       message: message, | ||||||
|                                       isCurrentUser: false, |                                       isCurrentUser: false, | ||||||
|  |                                       onAction: null, | ||||||
|                                     ), |                                     ), | ||||||
|                                 error: (_, __) => const SizedBox.shrink(), |                                 error: (_, __) => const SizedBox.shrink(), | ||||||
|                               ); |                               ); | ||||||
| @@ -442,6 +513,17 @@ class ChatRoomScreen extends HookConsumerWidget { | |||||||
|                   messageController: messageController, |                   messageController: messageController, | ||||||
|                   chatRoom: room!, |                   chatRoom: room!, | ||||||
|                   onSend: sendMessage, |                   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(), |             error: (_, __) => const SizedBox.shrink(), | ||||||
|             loading: () => const SizedBox.shrink(), |             loading: () => const SizedBox.shrink(), | ||||||
| @@ -456,11 +538,19 @@ class _ChatInput extends StatelessWidget { | |||||||
|   final TextEditingController messageController; |   final TextEditingController messageController; | ||||||
|   final SnChat chatRoom; |   final SnChat chatRoom; | ||||||
|   final VoidCallback onSend; |   final VoidCallback onSend; | ||||||
|  |   final VoidCallback onClear; | ||||||
|  |   final SnChatMessage? messageReplyingTo; | ||||||
|  |   final SnChatMessage? messageForwardingTo; | ||||||
|  |   final SnChatMessage? messageEditingTo; | ||||||
|  |  | ||||||
|   const _ChatInput({ |   const _ChatInput({ | ||||||
|     required this.messageController, |     required this.messageController, | ||||||
|     required this.chatRoom, |     required this.chatRoom, | ||||||
|     required this.onSend, |     required this.onSend, | ||||||
|  |     required this.onClear, | ||||||
|  |     required this.messageReplyingTo, | ||||||
|  |     required this.messageForwardingTo, | ||||||
|  |     required this.messageEditingTo, | ||||||
|   }); |   }); | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
| @@ -468,7 +558,52 @@ class _ChatInput extends StatelessWidget { | |||||||
|     return Material( |     return Material( | ||||||
|       elevation: 8, |       elevation: 8, | ||||||
|       color: Theme.of(context).colorScheme.surface, |       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), |             padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 8), | ||||||
|             child: Row( |             child: Row( | ||||||
|               children: [ |               children: [ | ||||||
| @@ -498,19 +633,80 @@ class _ChatInput extends StatelessWidget { | |||||||
|               ], |               ], | ||||||
|             ).padding(bottom: MediaQuery.of(context).padding.bottom), |             ).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 LocalChatMessage message; | ||||||
|   final bool isCurrentUser; |   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 |   @override | ||||||
|   Widget build(BuildContext context) { |   Widget build(BuildContext context, WidgetRef ref) { | ||||||
|     return Padding( |     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), |           padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), | ||||||
|           child: Row( |           child: Row( | ||||||
|             mainAxisAlignment: |             mainAxisAlignment: | ||||||
| @@ -519,28 +715,48 @@ class _MessageBubble extends StatelessWidget { | |||||||
|               if (!isCurrentUser) |               if (!isCurrentUser) | ||||||
|                 ProfilePictureWidget( |                 ProfilePictureWidget( | ||||||
|                   fileId: |                   fileId: | ||||||
|                   message.toRemoteMessage().sender.account.profile.pictureId, |                       message | ||||||
|  |                           .toRemoteMessage() | ||||||
|  |                           .sender | ||||||
|  |                           .account | ||||||
|  |                           .profile | ||||||
|  |                           .pictureId, | ||||||
|                   radius: 18, |                   radius: 18, | ||||||
|                 ), |                 ), | ||||||
|               const Gap(8), |               const Gap(8), | ||||||
|               Flexible( |               Flexible( | ||||||
|                 child: Container( |                 child: Container( | ||||||
|               padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), |                   padding: const EdgeInsets.symmetric( | ||||||
|  |                     horizontal: 12, | ||||||
|  |                     vertical: 8, | ||||||
|  |                   ), | ||||||
|                   decoration: BoxDecoration( |                   decoration: BoxDecoration( | ||||||
|                     color: |                     color: | ||||||
|                         isCurrentUser |                         isCurrentUser | ||||||
|                         ? Theme.of(context).colorScheme.primary.withOpacity(0.8) |                             ? Theme.of( | ||||||
|  |                               context, | ||||||
|  |                             ).colorScheme.primary.withOpacity(0.8) | ||||||
|                             : Colors.grey.shade200, |                             : Colors.grey.shade200, | ||||||
|                     borderRadius: BorderRadius.circular(16), |                     borderRadius: BorderRadius.circular(16), | ||||||
|                   ), |                   ), | ||||||
|                   child: Column( |                   child: Column( | ||||||
|                     crossAxisAlignment: CrossAxisAlignment.start, |                     crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|                     children: [ |                     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( |                       Text( | ||||||
|                         message.toRemoteMessage().content ?? '', |                         message.toRemoteMessage().content ?? '', | ||||||
|                     style: TextStyle( |                         style: TextStyle(color: textColor), | ||||||
|                       color: isCurrentUser ? Colors.white : Colors.black, |  | ||||||
|                     ), |  | ||||||
|                       ), |                       ), | ||||||
|                       const Gap(4), |                       const Gap(4), | ||||||
|                       Row( |                       Row( | ||||||
| @@ -548,11 +764,7 @@ class _MessageBubble extends StatelessWidget { | |||||||
|                         children: [ |                         children: [ | ||||||
|                           Text( |                           Text( | ||||||
|                             DateFormat.Hm().format(message.createdAt.toLocal()), |                             DateFormat.Hm().format(message.createdAt.toLocal()), | ||||||
|                         style: TextStyle( |                             style: TextStyle(fontSize: 10, color: textColor), | ||||||
|                           fontSize: 10, |  | ||||||
|                           color: |  | ||||||
|                               isCurrentUser ? Colors.white70 : Colors.black54, |  | ||||||
|                         ), |  | ||||||
|                           ), |                           ), | ||||||
|                           const Gap(4), |                           const Gap(4), | ||||||
|                           if (isCurrentUser) |                           if (isCurrentUser) | ||||||
| @@ -567,11 +779,18 @@ class _MessageBubble extends StatelessWidget { | |||||||
|               if (isCurrentUser) |               if (isCurrentUser) | ||||||
|                 ProfilePictureWidget( |                 ProfilePictureWidget( | ||||||
|                   fileId: |                   fileId: | ||||||
|                   message.toRemoteMessage().sender.account.profile.pictureId, |                       message | ||||||
|  |                           .toRemoteMessage() | ||||||
|  |                           .sender | ||||||
|  |                           .account | ||||||
|  |                           .profile | ||||||
|  |                           .pictureId, | ||||||
|                   radius: 18, |                   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