✨ Reply / forward mesage
This commit is contained in:
parent
7edfd56bf7
commit
6fd61b547e
@ -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();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user