From dffa0077de25adf36114b9c3769ba68f6d53c1d9 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Tue, 7 May 2024 21:49:24 +0800 Subject: [PATCH] :lipstick: Better user send message experience --- lib/i18n/app_en.arb | 1 + lib/i18n/app_zh.arb | 1 + lib/models/channel.dart | 8 ----- lib/models/message.dart | 4 ++- lib/widgets/chat/message_editor.dart | 52 ++++++++++++++++++++++------ pubspec.lock | 8 +++++ pubspec.yaml | 1 + 7 files changed, 56 insertions(+), 19 deletions(-) diff --git a/lib/i18n/app_en.arb b/lib/i18n/app_en.arb index e7a0c28..09c4dfa 100644 --- a/lib/i18n/app_en.arb +++ b/lib/i18n/app_en.arb @@ -123,6 +123,7 @@ "chatCallDisconnectConfirm": "Are you sure you want to disconnect? You can reconnect after this if you want.", "chatCallChangeSpeaker": "Change Speaker", "chatMessagePlaceholder": "Write a message...", + "chatMessageSending": "Now delivering your messages...", "chatMessageEditNotify": "You are about editing a message.", "chatMessageReplyNotify": "You are about replying a message.", "chatMessageDeleteConfirm": "Are you sure you want to delete this message? This operation cannot be revert and no local history is saved!" diff --git a/lib/i18n/app_zh.arb b/lib/i18n/app_zh.arb index faa71ff..8ad3b84 100644 --- a/lib/i18n/app_zh.arb +++ b/lib/i18n/app_zh.arb @@ -123,6 +123,7 @@ "chatCallDisconnect": "断开连接", "chatCallDisconnectConfirm": "你确定你要断开连接吗?你可以之后在任何时候重新连接。", "chatMessagePlaceholder": "发条消息……", + "chatMessageSending": "正在送出你的信息……", "chatMessageEditNotify": "你正在编辑信息中……", "chatMessageReplyNotify": "你正在回复消息中……", "chatMessageDeleteConfirm": "你确定要删除这条消息吗?这条消息将永远的从所有人的视图中消失,并且不会有本地消息记录保存!" diff --git a/lib/models/channel.dart b/lib/models/channel.dart index f4b0290..3b9e96d 100644 --- a/lib/models/channel.dart +++ b/lib/models/channel.dart @@ -8,8 +8,6 @@ class Channel { String alias; String name; String description; - dynamic members; - dynamic calls; int type; Account account; int accountId; @@ -25,8 +23,6 @@ class Channel { required this.alias, required this.name, required this.description, - this.members, - this.calls, required this.type, required this.account, required this.accountId, @@ -41,8 +37,6 @@ class Channel { alias: json['alias'], name: json['name'], description: json['description'], - members: json['members'], - calls: json['calls'], type: json['type'], account: Account.fromJson(json['account']), accountId: json['account_id'], @@ -57,8 +51,6 @@ class Channel { 'alias': alias, 'name': name, 'description': description, - 'members': members, - 'calls': calls, 'type': type, 'account': account, 'account_id': accountId, diff --git a/lib/models/message.dart b/lib/models/message.dart index 17a95ea..476e76d 100644 --- a/lib/models/message.dart +++ b/lib/models/message.dart @@ -8,7 +8,7 @@ class Message { DateTime updatedAt; DateTime? deletedAt; String content; - dynamic metadata; + Map? metadata; int type; List? attachments; Channel? channel; @@ -18,6 +18,8 @@ class Message { int channelId; int senderId; + bool isSending = false; + Message({ required this.id, required this.createdAt, diff --git a/lib/widgets/chat/message_editor.dart b/lib/widgets/chat/message_editor.dart index 72c0eb0..049751b 100644 --- a/lib/widgets/chat/message_editor.dart +++ b/lib/widgets/chat/message_editor.dart @@ -1,6 +1,8 @@ import 'dart:convert'; +import 'package:easy_debounce/easy_debounce.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:http/http.dart'; import 'package:provider/provider.dart'; @@ -36,7 +38,8 @@ class _ChatMessageEditorState extends State { final _textController = TextEditingController(); final _focusNode = FocusNode(); - bool _isSubmitting = false; + List _pendingMessages = List.empty(growable: true); + int? _prevEditingId; List _attachments = List.empty(growable: true); @@ -53,8 +56,6 @@ class _ChatMessageEditorState extends State { } Future sendMessage(BuildContext context) async { - if (_isSubmitting) return; - _focusNode.requestFocus(); final auth = context.read(); @@ -72,15 +73,25 @@ class _ChatMessageEditorState extends State { 'reply_to': widget.replying?.id, }); - setState(() => _isSubmitting = true); + reset(); + + final messageMarkId = DateTime.now().microsecondsSinceEpoch >> 10; + final messageDebounceId = 'm-pending$messageMarkId'; + + EasyDebounce.debounce(messageDebounceId, 350.ms, () { + setState(() => _pendingMessages.add(messageMarkId)); + }); + var res = await Response.fromStream(await auth.client!.send(req)); if (res.statusCode != 200) { var message = utf8.decode(res.bodyBytes); context.showErrorDialog(message); - } else { - reset(); } - setState(() => _isSubmitting = false); + + EasyDebounce.cancel(messageDebounceId); + if (_pendingMessages.isNotEmpty) { + setState(() => _pendingMessages.remove(messageMarkId)); + } } void reset() { @@ -94,8 +105,8 @@ class _ChatMessageEditorState extends State { if (widget.editing != null && _prevEditingId != widget.editing!.id) { setState(() { _prevEditingId = widget.editing!.id; - _textController.text = widget.editing!.content; _attachments = widget.editing!.attachments ?? List.empty(growable: true); + _textController.text = widget.editing!.content; }); } } @@ -113,6 +124,15 @@ class _ChatMessageEditorState extends State { @override Widget build(BuildContext context) { + final sendingBanner = MaterialBanner( + padding: const EdgeInsets.only(top: 4, bottom: 4, left: 20), + leading: const Icon(Icons.schedule_send), + backgroundColor: Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.9), + dividerColor: const Color.fromARGB(1, 0, 0, 0), + content: Text('${AppLocalizations.of(context)!.chatMessageSending} (${_pendingMessages.length})'), + actions: const [SizedBox()], + ); + final editingBanner = MaterialBanner( padding: const EdgeInsets.only(top: 4, bottom: 4, left: 20), leading: const Icon(Icons.edit_note), @@ -143,6 +163,18 @@ class _ChatMessageEditorState extends State { return Column( children: [ + _pendingMessages.isNotEmpty + ? sendingBanner + .animate() + .scaleY( + begin: 0, + curve: Curves.fastEaseInToSlowEaseOut, + ) + .slideY( + begin: 1, + curve: Curves.fastEaseInToSlowEaseOut, + ) + : Container(), widget.editing != null ? editingBanner : Container(), widget.replying != null ? replyingBanner : Container(), Container( @@ -162,7 +194,7 @@ class _ChatMessageEditorState extends State { position: badge.BadgePosition.custom(top: -2, end: 8), child: TextButton( style: TextButton.styleFrom(shape: const CircleBorder(), padding: const EdgeInsets.all(4)), - onPressed: !_isSubmitting ? () => viewAttachments(context) : null, + onPressed: () => viewAttachments(context), child: const Icon(Icons.attach_file), ), ), @@ -182,7 +214,7 @@ class _ChatMessageEditorState extends State { ), TextButton( style: TextButton.styleFrom(shape: const CircleBorder(), padding: const EdgeInsets.all(4)), - onPressed: !_isSubmitting ? () => sendMessage(context) : null, + onPressed: () => sendMessage(context), child: const Icon(Icons.send), ) ], diff --git a/pubspec.lock b/pubspec.lock index 1245e47..6821f5c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -209,6 +209,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.1.0" + easy_debounce: + dependency: "direct main" + description: + name: easy_debounce + sha256: f082609cfb8f37defb9e37fc28bc978c6712dedf08d4c5a26f820fa10165a236 + url: "https://pub.dev" + source: hosted + version: "2.0.3" fake_async: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 87bca59..d9f765c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -71,6 +71,7 @@ dependencies: package_info_plus: ^7.0.0 cached_network_image: ^3.3.1 desktop_drop: ^0.4.4 + easy_debounce: ^2.0.3 dev_dependencies: flutter_test: