From ce414d92a2a892815fa175ed911a8acac81bb4dc Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Mon, 18 Nov 2024 22:52:22 +0800 Subject: [PATCH] :sparkles: Chat context menu (w.i.p) --- assets/translations/en.json | 4 +- assets/translations/zh.json | 6 +- lib/screens/chat/room.dart | 6 +- lib/widgets/chat/chat_message.dart | 203 +++++++++++------- lib/widgets/post/post_item.dart | 2 +- lib/widgets/post/post_media_pending_list.dart | 4 + 6 files changed, 139 insertions(+), 86 deletions(-) diff --git a/assets/translations/en.json b/assets/translations/en.json index 9ba725b..74807be 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -47,6 +47,7 @@ "compress": "Compress", "report": "Report", "repost": "Repost", + "replyPost": "Reply", "reply": "Reply", "unset": "Unset", "untitled": "Untitled", @@ -158,5 +159,6 @@ "realmDeleted": "Realm {} has been deleted.", "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 {}" + "fieldChatMessage": "Message in {}", + "eventResourceTag": "Event {}" } diff --git a/assets/translations/zh.json b/assets/translations/zh.json index 85e69e9..8df7661 100644 --- a/assets/translations/zh.json +++ b/assets/translations/zh.json @@ -47,7 +47,8 @@ "compress": "压缩", "report": "检举", "repost": "转帖", - "reply": "回贴", + "replyPost": "回贴", + "reply": "回复", "unset": "未设置", "untitled": "无题", "postDetail": "帖子详情", @@ -158,5 +159,6 @@ "realmDeleted": "领域 {} 已被删除" , "realmDelete": "删除领域 {}", "realmDeleteDescription": "你确定要删除这个领域吗?该操作不可撤销,其隶属于该领域的所有资源(帖子、聊天频道、发布者、制品等)都将被永久删除。三思而后行!", - "fieldChatMessage": "在 {} 中发消息" + "fieldChatMessage": "在 {} 中发消息", + "eventResourceTag": "消息 {}" } diff --git a/lib/screens/chat/room.dart b/lib/screens/chat/room.dart index f78f748..4e8472d 100644 --- a/lib/screens/chat/room.dart +++ b/lib/screens/chat/room.dart @@ -118,9 +118,11 @@ class _ChatRoomScreenState extends State { hasMerged: canMergePrevious, isPending: _messageController.unconfirmedMessages .contains(message.uuid), - onReply: () { - _inputGlobalKey.currentState?.setReply(message); + onReply: (value) { + _inputGlobalKey.currentState?.setReply(value); }, + onEdit: (value) {}, + onDelete: (value) {}, ); }, ), diff --git a/lib/widgets/chat/chat_message.dart b/lib/widgets/chat/chat_message.dart index 49b7d49..e53d15b 100644 --- a/lib/widgets/chat/chat_message.dart +++ b/lib/widgets/chat/chat_message.dart @@ -1,10 +1,12 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_context_menu/flutter_context_menu.dart'; import 'package:gap/gap.dart'; import 'package:material_symbols_icons/symbols.dart'; import 'package:provider/provider.dart'; import 'package:styled_widget/styled_widget.dart'; import 'package:surface/providers/user_directory.dart'; +import 'package:surface/providers/userinfo.dart'; import 'package:surface/types/chat.dart'; import 'package:surface/widgets/account/account_image.dart'; import 'package:surface/widgets/attachment/attachment_list.dart'; @@ -17,7 +19,9 @@ class ChatMessage extends StatelessWidget { final bool isMerged; final bool hasMerged; final bool isPending; - final Function()? onReply; + final Function(SnChatMessage)? onReply; + final Function(SnChatMessage)? onEdit; + final Function(SnChatMessage)? onDelete; const ChatMessage({ super.key, required this.data, @@ -26,100 +30,139 @@ class ChatMessage extends StatelessWidget { this.hasMerged = false, this.isPending = false, this.onReply, + this.onEdit, + this.onDelete, }); @override Widget build(BuildContext context) { + final ua = context.read(); final ud = context.read(); final user = ud.getAccountFromCache(data.sender.accountId); + final isOwner = ua.isAuthorized && data.sender.accountId == ua.user!.id; + final dateFormatter = DateFormat('MM/dd HH:mm'); return SwipeTo( key: Key('chat-message-${data.id}'), iconOnLeftSwipe: Symbols.reply, swipeSensitivity: 20, - onLeftSwipe: onReply != null ? (_) => onReply!() : null, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (!isMerged && !isCompact) - AccountImage( - content: user?.avatar, - ) - else if (isMerged) - const Gap(40), - const Gap(8), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (!isMerged) - Row( - crossAxisAlignment: CrossAxisAlignment.baseline, - textBaseline: TextBaseline.alphabetic, - children: [ - if (isCompact) - AccountImage( - content: user?.avatar, - radius: 12, - ).padding(right: 6), - Text( - (data.sender.nick?.isNotEmpty ?? false) - ? data.sender.nick! - : user!.nick, - ).bold(), - const Gap(6), - Text( - dateFormatter.format(data.createdAt.toLocal()), - ).fontSize(13), - ], - ), - if (isCompact) const Gap(4), - if (data.preload?.quoteEvent != null) - StyledWidget(Container( - decoration: BoxDecoration( - borderRadius: - const BorderRadius.all(Radius.circular(8)), - border: Border.all( - color: Theme.of(context).dividerColor, - width: 1, + onLeftSwipe: onReply != null ? (_) => onReply!(data) : null, + child: ContextMenuRegion( + contextMenu: ContextMenu( + entries: [ + MenuHeader(text: "eventResourceTag".tr(args: ['#${data.id}'])), + if (onReply != null) + MenuItem( + label: 'reply'.tr(), + icon: Symbols.reply, + onSelected: () { + onReply!(data); + }, + ), + if (isOwner && onEdit != null) + MenuItem( + label: 'edit'.tr(), + icon: Symbols.edit, + onSelected: () { + onEdit!(data); + }, + ), + if (isOwner && onDelete != null) + MenuItem( + label: 'delete'.tr(), + icon: Symbols.delete, + onSelected: () { + onDelete!(data); + }, + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (!isMerged && !isCompact) + AccountImage( + content: user?.avatar, + ) + else if (isMerged) + const Gap(40), + const Gap(8), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (!isMerged) + Row( + crossAxisAlignment: CrossAxisAlignment.baseline, + textBaseline: TextBaseline.alphabetic, + children: [ + if (isCompact) + AccountImage( + content: user?.avatar, + radius: 12, + ).padding(right: 6), + Text( + (data.sender.nick?.isNotEmpty ?? false) + ? data.sender.nick! + : user!.nick, + ).bold(), + const Gap(6), + Text( + dateFormatter.format(data.createdAt.toLocal()), + ).fontSize(13), + ], + ), + if (isCompact) const Gap(4), + if (data.preload?.quoteEvent != null) + StyledWidget(Container( + decoration: BoxDecoration( + borderRadius: + const BorderRadius.all(Radius.circular(8)), + border: Border.all( + color: Theme.of(context).dividerColor, + width: 1, + ), ), + padding: const EdgeInsets.only( + left: 4, + right: 4, + top: 8, + bottom: 6, + ), + child: ChatMessage( + data: data.preload!.quoteEvent!, + isCompact: true, + onReply: onReply, + onEdit: onEdit, + onDelete: onDelete, + ), + )).padding(bottom: 4, top: isMerged ? 4 : 2), + if (data.body['text'] != null) + MarkdownTextContent( + content: data.body['text'], + isAutoWarp: true, ), - padding: const EdgeInsets.only( - left: 4, - right: 4, - top: 8, - bottom: 6, - ), - child: ChatMessage( - data: data.preload!.quoteEvent!, - isCompact: true, - ), - )).padding(bottom: 4, top: isMerged ? 4 : 2), - if (data.body['text'] != null) - MarkdownTextContent( - content: data.body['text'], - isAutoWarp: true, - ), - ], - ), - ) - ], - ).opacity(isPending ? 0.5 : 1), - if (data.preload?.attachments?.isNotEmpty ?? false) - AttachmentList( - data: data.preload!.attachments!, - bordered: true, - noGrow: true, - maxHeight: 520, - listPadding: const EdgeInsets.only(top: 8), - ), - if (!hasMerged && !isCompact) const Gap(12), - ], + ], + ), + ) + ], + ).opacity(isPending ? 0.5 : 1), + if (data.preload?.attachments?.isNotEmpty ?? false) + AttachmentList( + data: data.preload!.attachments!, + bordered: true, + noGrow: true, + maxHeight: 520, + listPadding: const EdgeInsets.only(top: 8), + ), + if (!hasMerged && !isCompact) const Gap(12), + ], + ), ), ); } diff --git a/lib/widgets/post/post_item.dart b/lib/widgets/post/post_item.dart index 1f2800e..038e231 100644 --- a/lib/widgets/post/post_item.dart +++ b/lib/widgets/post/post_item.dart @@ -248,7 +248,7 @@ class _PostContentHeader extends StatelessWidget { children: [ const Icon(Symbols.reply), const Gap(16), - Text('reply').tr(), + Text('replyPost').tr(), ], ), onTap: () { diff --git a/lib/widgets/post/post_media_pending_list.dart b/lib/widgets/post/post_media_pending_list.dart index 34a9c99..b41dceb 100644 --- a/lib/widgets/post/post_media_pending_list.dart +++ b/lib/widgets/post/post_media_pending_list.dart @@ -76,11 +76,15 @@ class PostMediaPendingListRaw extends StatelessWidget { final media = attachments[idx]; final result = (!kIsWeb && (Platform.isIOS || Platform.isMacOS)) ? await showCupertinoImageCropper( + // ignore: use_build_context_synchronously context, + // ignore: use_build_context_synchronously imageProvider: media.getImageProvider(context)!, ) : await showMaterialImageCropper( + // ignore: use_build_context_synchronously context, + // ignore: use_build_context_synchronously imageProvider: media.getImageProvider(context)!, );