👽 Update follow server side IM changes

This commit is contained in:
LittleSheep 2024-11-18 23:59:08 +08:00
parent 6c5377d9fa
commit fd272ead37
9 changed files with 163 additions and 26 deletions

View File

@ -160,5 +160,8 @@
"realmDelete": "Delete realm {}",
"realmDeleteDescription": "Are you sure you want to delete this realm? This operation is irreversible, all resources (posts, chat channels, publishers, etc) belonging to this realm will be permanently deleted. Be careful and think twice!",
"fieldChatMessage": "Message in {}",
"eventResourceTag": "Event {}"
"eventResourceTag": "Event {}",
"messageDelete": "Delete message {}",
"messageDeleteDescription": "Are you sure you want to delete this message? This operation is irreversible. You will leave a record of the deleted message.",
"messageDeleted": "Message {} has been deleted"
}

View File

@ -160,5 +160,8 @@
"realmDelete": "删除领域 {}",
"realmDeleteDescription": "你确定要删除这个领域吗?该操作不可撤销,其隶属于该领域的所有资源(帖子、聊天频道、发布者、制品等)都将被永久删除。三思而后行!",
"fieldChatMessage": "在 {} 中发消息",
"eventResourceTag": "消息 {}"
"eventResourceTag": "消息 {}",
"messageDelete": "删除消息 {}",
"messageDeleteDescription": "你确定要删除这个消息吗?该操作不可撤销。同时您将留下一条删除消息的记录。",
"messageDeleted": "消息 {} 已被删除"
}

View File

@ -1,6 +1,7 @@
import 'dart:async';
import 'dart:math' as math;
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:hive/hive.dart';
import 'package:provider/provider.dart';
@ -176,9 +177,9 @@ class ChatMessageController extends ChangeNotifier {
switch (message.type) {
case 'messages.edit':
final body = message.body;
if (body['related_event'] != null) {
final idx = messages.indexWhere((x) => x.id == body['related_event']);
if (message.relatedEventId != null) {
final idx =
messages.indexWhere((x) => x.id == message.relatedEventId);
if (idx != -1) {
final newBody = message.body;
newBody.remove('related_event');
@ -186,17 +187,16 @@ class ChatMessageController extends ChangeNotifier {
body: newBody,
updatedAt: message.updatedAt,
);
if (_box!.containsKey(body['related_event'])) {
await _box!.put(body['related_event'], messages[idx]);
if (_box!.containsKey(message.relatedEventId)) {
await _box!.put(message.relatedEventId, messages[idx]);
}
}
}
case 'messages.delete':
final body = message.body;
if (body['related_event'] != null) {
messages.removeWhere((x) => x.id == body['related_event']);
if (_box!.containsKey(body['related_event'])) {
await _box!.delete(body['related_event']);
if (message.relatedEventId != null) {
messages.removeWhere((x) => x.id == message.relatedEventId);
if (_box!.containsKey(message.relatedEventId)) {
await _box!.delete(message.relatedEventId);
}
}
}
@ -208,6 +208,7 @@ class ChatMessageController extends ChangeNotifier {
int? quoteId,
int? relatedId,
List<String>? attachments,
SnChatMessage? editingMessage,
}) async {
if (channel == null) return;
const uuid = Uuid();
@ -216,7 +217,7 @@ class ChatMessageController extends ChangeNotifier {
'text': content,
'algorithm': 'plain',
if (quoteId != null) 'quote_event': quoteId,
if (relatedId != null) 'quote_event': relatedId,
if (relatedId != null) 'related_event': relatedId,
if (attachments != null && attachments.isNotEmpty)
'attachments': attachments,
};
@ -235,21 +236,40 @@ class ChatMessageController extends ChangeNotifier {
sender: profile!,
senderId: profile!.id,
quoteEventId: quoteId,
relatedEventId: relatedId,
);
_addUnconfirmedMessage(message);
// Send to server
try {
await _sn.client.post(
'/cgi/im/channels/${channel!.keyPath}/messages',
await _sn.client.request(
editingMessage != null
? '/cgi/im/channels/${channel!.keyPath}/messages/${editingMessage.id}'
: '/cgi/im/channels/${channel!.keyPath}/messages',
data: {
'type': type,
'uuid': nonce,
'body': body,
},
options: Options(
method: editingMessage != null ? 'PUT' : 'POST',
),
);
} catch (err) {
print(err);
// ignore
}
}
Future<void> deleteMessage(SnChatMessage message) async {
if (message.channelId != channel?.id) return;
try {
await _sn.client.delete(
'/cgi/im/channels/${channel!.keyPath}/messages/${message.id}',
);
messages.removeWhere((x) => x.id == message.id);
} catch (err) {
// ignore
}
}
@ -376,7 +396,11 @@ class ChatMessageController extends ChangeNotifier {
}
// Preload sender accounts
await _ud.listAccount(out.map((ele) => ele.sender.accountId).toSet());
final accountId = out
.where((ele) => ele.sender.accountId >= 0)
.map((ele) => ele.sender.accountId)
.toSet();
await _ud.listAccount(accountId);
return out;
}

View File

@ -121,8 +121,12 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
onReply: (value) {
_inputGlobalKey.currentState?.setReply(value);
},
onEdit: (value) {},
onDelete: (value) {},
onEdit: (value) {
_inputGlobalKey.currentState?.setEdit(value);
},
onDelete: (value) {
_inputGlobalKey.currentState?.deleteMessage(value);
},
);
},
),

View File

@ -74,13 +74,14 @@ class SnChatMessage with _$SnChatMessage {
@HiveField(2) required DateTime updatedAt,
@HiveField(3) required DateTime? deletedAt,
@HiveField(4) required String uuid,
@HiveField(5) required Map<String, dynamic> body,
@HiveField(5) @Default({}) Map<String, dynamic> body,
@HiveField(6) required String type,
@HiveField(7) required SnChannel channel,
@HiveField(8) required SnChannelMember sender,
@HiveField(9) required int channelId,
@HiveField(10) required int senderId,
@HiveField(11) required int? quoteEventId,
@HiveField(12) required int? relatedEventId,
SnChatMessagePreload? preload,
}) = _SnChatMessage;

View File

@ -1085,6 +1085,8 @@ mixin _$SnChatMessage {
int get senderId => throw _privateConstructorUsedError;
@HiveField(11)
int? get quoteEventId => throw _privateConstructorUsedError;
@HiveField(12)
int? get relatedEventId => throw _privateConstructorUsedError;
SnChatMessagePreload? get preload => throw _privateConstructorUsedError;
/// Serializes this SnChatMessage to a JSON map.
@ -1116,6 +1118,7 @@ abstract class $SnChatMessageCopyWith<$Res> {
@HiveField(9) int channelId,
@HiveField(10) int senderId,
@HiveField(11) int? quoteEventId,
@HiveField(12) int? relatedEventId,
SnChatMessagePreload? preload});
$SnChannelCopyWith<$Res> get channel;
@ -1150,6 +1153,7 @@ class _$SnChatMessageCopyWithImpl<$Res, $Val extends SnChatMessage>
Object? channelId = null,
Object? senderId = null,
Object? quoteEventId = freezed,
Object? relatedEventId = freezed,
Object? preload = freezed,
}) {
return _then(_value.copyWith(
@ -1201,6 +1205,10 @@ class _$SnChatMessageCopyWithImpl<$Res, $Val extends SnChatMessage>
? _value.quoteEventId
: quoteEventId // ignore: cast_nullable_to_non_nullable
as int?,
relatedEventId: freezed == relatedEventId
? _value.relatedEventId
: relatedEventId // ignore: cast_nullable_to_non_nullable
as int?,
preload: freezed == preload
? _value.preload
: preload // ignore: cast_nullable_to_non_nullable
@ -1264,6 +1272,7 @@ abstract class _$$SnChatMessageImplCopyWith<$Res>
@HiveField(9) int channelId,
@HiveField(10) int senderId,
@HiveField(11) int? quoteEventId,
@HiveField(12) int? relatedEventId,
SnChatMessagePreload? preload});
@override
@ -1299,6 +1308,7 @@ class __$$SnChatMessageImplCopyWithImpl<$Res>
Object? channelId = null,
Object? senderId = null,
Object? quoteEventId = freezed,
Object? relatedEventId = freezed,
Object? preload = freezed,
}) {
return _then(_$SnChatMessageImpl(
@ -1350,6 +1360,10 @@ class __$$SnChatMessageImplCopyWithImpl<$Res>
? _value.quoteEventId
: quoteEventId // ignore: cast_nullable_to_non_nullable
as int?,
relatedEventId: freezed == relatedEventId
? _value.relatedEventId
: relatedEventId // ignore: cast_nullable_to_non_nullable
as int?,
preload: freezed == preload
? _value.preload
: preload // ignore: cast_nullable_to_non_nullable
@ -1368,13 +1382,14 @@ class _$SnChatMessageImpl extends _SnChatMessage {
@HiveField(2) required this.updatedAt,
@HiveField(3) required this.deletedAt,
@HiveField(4) required this.uuid,
@HiveField(5) required final Map<String, dynamic> body,
@HiveField(5) final Map<String, dynamic> body = const {},
@HiveField(6) required this.type,
@HiveField(7) required this.channel,
@HiveField(8) required this.sender,
@HiveField(9) required this.channelId,
@HiveField(10) required this.senderId,
@HiveField(11) required this.quoteEventId,
@HiveField(12) required this.relatedEventId,
this.preload})
: _body = body,
super._();
@ -1399,6 +1414,7 @@ class _$SnChatMessageImpl extends _SnChatMessage {
final String uuid;
final Map<String, dynamic> _body;
@override
@JsonKey()
@HiveField(5)
Map<String, dynamic> get body {
if (_body is EqualUnmodifiableMapView) return _body;
@ -1425,11 +1441,14 @@ class _$SnChatMessageImpl extends _SnChatMessage {
@HiveField(11)
final int? quoteEventId;
@override
@HiveField(12)
final int? relatedEventId;
@override
final SnChatMessagePreload? preload;
@override
String toString() {
return 'SnChatMessage(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, uuid: $uuid, body: $body, type: $type, channel: $channel, sender: $sender, channelId: $channelId, senderId: $senderId, quoteEventId: $quoteEventId, preload: $preload)';
return 'SnChatMessage(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, uuid: $uuid, body: $body, type: $type, channel: $channel, sender: $sender, channelId: $channelId, senderId: $senderId, quoteEventId: $quoteEventId, relatedEventId: $relatedEventId, preload: $preload)';
}
@override
@ -1455,6 +1474,8 @@ class _$SnChatMessageImpl extends _SnChatMessage {
other.senderId == senderId) &&
(identical(other.quoteEventId, quoteEventId) ||
other.quoteEventId == quoteEventId) &&
(identical(other.relatedEventId, relatedEventId) ||
other.relatedEventId == relatedEventId) &&
(identical(other.preload, preload) || other.preload == preload));
}
@ -1474,6 +1495,7 @@ class _$SnChatMessageImpl extends _SnChatMessage {
channelId,
senderId,
quoteEventId,
relatedEventId,
preload);
/// Create a copy of SnChatMessage
@ -1499,13 +1521,14 @@ abstract class _SnChatMessage extends SnChatMessage {
@HiveField(2) required final DateTime updatedAt,
@HiveField(3) required final DateTime? deletedAt,
@HiveField(4) required final String uuid,
@HiveField(5) required final Map<String, dynamic> body,
@HiveField(5) final Map<String, dynamic> body,
@HiveField(6) required final String type,
@HiveField(7) required final SnChannel channel,
@HiveField(8) required final SnChannelMember sender,
@HiveField(9) required final int channelId,
@HiveField(10) required final int senderId,
@HiveField(11) required final int? quoteEventId,
@HiveField(12) required final int? relatedEventId,
final SnChatMessagePreload? preload}) = _$SnChatMessageImpl;
const _SnChatMessage._() : super._();
@ -1549,6 +1572,9 @@ abstract class _SnChatMessage extends SnChatMessage {
@HiveField(11)
int? get quoteEventId;
@override
@HiveField(12)
int? get relatedEventId;
@override
SnChatMessagePreload? get preload;
/// Create a copy of SnChatMessage

View File

@ -163,13 +163,14 @@ class SnChatMessageImplAdapter extends TypeAdapter<_$SnChatMessageImpl> {
channelId: fields[9] as int,
senderId: fields[10] as int,
quoteEventId: fields[11] as int?,
relatedEventId: fields[12] as int?,
);
}
@override
void write(BinaryWriter writer, _$SnChatMessageImpl obj) {
writer
..writeByte(12)
..writeByte(13)
..writeByte(0)
..write(obj.id)
..writeByte(1)
@ -192,6 +193,8 @@ class SnChatMessageImplAdapter extends TypeAdapter<_$SnChatMessageImpl> {
..write(obj.senderId)
..writeByte(11)
..write(obj.quoteEventId)
..writeByte(12)
..write(obj.relatedEventId)
..writeByte(5)
..write(obj.body);
}
@ -306,13 +309,14 @@ _$SnChatMessageImpl _$$SnChatMessageImplFromJson(Map<String, dynamic> json) =>
? null
: DateTime.parse(json['deleted_at'] as String),
uuid: json['uuid'] as String,
body: json['body'] as Map<String, dynamic>,
body: json['body'] as Map<String, dynamic>? ?? const {},
type: json['type'] as String,
channel: SnChannel.fromJson(json['channel'] as Map<String, dynamic>),
sender: SnChannelMember.fromJson(json['sender'] as Map<String, dynamic>),
channelId: (json['channel_id'] as num).toInt(),
senderId: (json['sender_id'] as num).toInt(),
quoteEventId: (json['quote_event_id'] as num?)?.toInt(),
relatedEventId: (json['related_event_id'] as num?)?.toInt(),
preload: json['preload'] == null
? null
: SnChatMessagePreload.fromJson(
@ -333,6 +337,7 @@ Map<String, dynamic> _$$SnChatMessageImplToJson(_$SnChatMessageImpl instance) =>
'channel_id': instance.channelId,
'sender_id': instance.senderId,
'quote_event_id': instance.quoteEventId,
'related_event_id': instance.relatedEventId,
'preload': instance.preload?.toJson(),
};

View File

@ -147,6 +147,18 @@ class ChatMessage extends StatelessWidget {
content: data.body['text'],
isAutoWarp: true,
),
if (data.type == 'messages.delete' &&
data.relatedEventId != null)
Row(
children: [
const Icon(Symbols.delete),
const Gap(8),
Text(
'messageDeleted'
.tr(args: ['#${data.relatedEventId}']),
),
],
),
],
),
)

View File

@ -25,7 +25,7 @@ class ChatMessageInputState extends State<ChatMessageInput> {
bool _isBusy = false;
double? _progress;
SnChatMessage? _replyingMessage;
SnChatMessage? _replyingMessage, _editingMessage;
final TextEditingController _contentController = TextEditingController();
final FocusNode _focusNode = FocusNode();
@ -34,6 +34,26 @@ class ChatMessageInputState extends State<ChatMessageInput> {
setState(() => _replyingMessage = value);
}
void setEdit(SnChatMessage? value) {
setState(() => _editingMessage = value);
}
Future<void> deleteMessage(SnChatMessage message) async {
final confirm = await context.showConfirmDialog(
'messageDelete'.tr(args: ['#${message.id}']),
'messageDeleteDescription'.tr(),
);
if (!confirm) return;
if (!mounted) return;
setState(() => _isBusy = true);
await widget.controller.deleteMessage(message);
if (!mounted) return;
setState(() => _isBusy = false);
}
Future<void> _sendMessage() async {
if (_isBusy) return;
@ -89,10 +109,13 @@ class ChatMessageInputState extends State<ChatMessageInput> {
.where((e) => e.attachment != null)
.map((e) => e.attachment!.rid)
.toList(),
relatedId: _editingMessage?.id,
quoteId: _replyingMessage?.id,
editingMessage: _editingMessage,
);
_contentController.clear();
_attachments.clear();
_editingMessage = null;
_replyingMessage = null;
setState(() => _isBusy = false);
@ -184,6 +207,42 @@ class ChatMessageInputState extends State<ChatMessageInput> {
),
).height(_replyingMessage != null ? 54 + 8 : 0, animate: true).animate(
const Duration(milliseconds: 300), Curves.fastEaseInToSlowEaseOut),
SingleChildScrollView(
physics: const NeverScrollableScrollPhysics(),
child: Padding(
padding: _editingMessage != null
? const EdgeInsets.only(top: 8)
: EdgeInsets.zero,
child: _editingMessage != null
? MaterialBanner(
padding: const EdgeInsets.only(left: 16.0),
leading: const Icon(Symbols.edit),
content: SingleChildScrollView(
physics: const NeverScrollableScrollPhysics(),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (_editingMessage?.body['text'] != null)
MarkdownTextContent(
content: _editingMessage?.body['text'],
),
],
),
),
actions: [
TextButton(
child: Text('cancel'.tr()),
onPressed: () {
setState(() => _editingMessage = null);
},
),
],
)
: const SizedBox.shrink(),
),
).height(_editingMessage != null ? 54 + 8 : 0, animate: true).animate(
const Duration(milliseconds: 300), Curves.fastEaseInToSlowEaseOut),
SizedBox(
height: 56,
child: Row(