💄 Better user send message experience
This commit is contained in:
parent
b3e266d564
commit
dffa0077de
@ -123,6 +123,7 @@
|
|||||||
"chatCallDisconnectConfirm": "Are you sure you want to disconnect? You can reconnect after this if you want.",
|
"chatCallDisconnectConfirm": "Are you sure you want to disconnect? You can reconnect after this if you want.",
|
||||||
"chatCallChangeSpeaker": "Change Speaker",
|
"chatCallChangeSpeaker": "Change Speaker",
|
||||||
"chatMessagePlaceholder": "Write a message...",
|
"chatMessagePlaceholder": "Write a message...",
|
||||||
|
"chatMessageSending": "Now delivering your messages...",
|
||||||
"chatMessageEditNotify": "You are about editing a message.",
|
"chatMessageEditNotify": "You are about editing a message.",
|
||||||
"chatMessageReplyNotify": "You are about replying 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!"
|
"chatMessageDeleteConfirm": "Are you sure you want to delete this message? This operation cannot be revert and no local history is saved!"
|
||||||
|
@ -123,6 +123,7 @@
|
|||||||
"chatCallDisconnect": "断开连接",
|
"chatCallDisconnect": "断开连接",
|
||||||
"chatCallDisconnectConfirm": "你确定你要断开连接吗?你可以之后在任何时候重新连接。",
|
"chatCallDisconnectConfirm": "你确定你要断开连接吗?你可以之后在任何时候重新连接。",
|
||||||
"chatMessagePlaceholder": "发条消息……",
|
"chatMessagePlaceholder": "发条消息……",
|
||||||
|
"chatMessageSending": "正在送出你的信息……",
|
||||||
"chatMessageEditNotify": "你正在编辑信息中……",
|
"chatMessageEditNotify": "你正在编辑信息中……",
|
||||||
"chatMessageReplyNotify": "你正在回复消息中……",
|
"chatMessageReplyNotify": "你正在回复消息中……",
|
||||||
"chatMessageDeleteConfirm": "你确定要删除这条消息吗?这条消息将永远的从所有人的视图中消失,并且不会有本地消息记录保存!"
|
"chatMessageDeleteConfirm": "你确定要删除这条消息吗?这条消息将永远的从所有人的视图中消失,并且不会有本地消息记录保存!"
|
||||||
|
@ -8,8 +8,6 @@ class Channel {
|
|||||||
String alias;
|
String alias;
|
||||||
String name;
|
String name;
|
||||||
String description;
|
String description;
|
||||||
dynamic members;
|
|
||||||
dynamic calls;
|
|
||||||
int type;
|
int type;
|
||||||
Account account;
|
Account account;
|
||||||
int accountId;
|
int accountId;
|
||||||
@ -25,8 +23,6 @@ class Channel {
|
|||||||
required this.alias,
|
required this.alias,
|
||||||
required this.name,
|
required this.name,
|
||||||
required this.description,
|
required this.description,
|
||||||
this.members,
|
|
||||||
this.calls,
|
|
||||||
required this.type,
|
required this.type,
|
||||||
required this.account,
|
required this.account,
|
||||||
required this.accountId,
|
required this.accountId,
|
||||||
@ -41,8 +37,6 @@ class Channel {
|
|||||||
alias: json['alias'],
|
alias: json['alias'],
|
||||||
name: json['name'],
|
name: json['name'],
|
||||||
description: json['description'],
|
description: json['description'],
|
||||||
members: json['members'],
|
|
||||||
calls: json['calls'],
|
|
||||||
type: json['type'],
|
type: json['type'],
|
||||||
account: Account.fromJson(json['account']),
|
account: Account.fromJson(json['account']),
|
||||||
accountId: json['account_id'],
|
accountId: json['account_id'],
|
||||||
@ -57,8 +51,6 @@ class Channel {
|
|||||||
'alias': alias,
|
'alias': alias,
|
||||||
'name': name,
|
'name': name,
|
||||||
'description': description,
|
'description': description,
|
||||||
'members': members,
|
|
||||||
'calls': calls,
|
|
||||||
'type': type,
|
'type': type,
|
||||||
'account': account,
|
'account': account,
|
||||||
'account_id': accountId,
|
'account_id': accountId,
|
||||||
|
@ -8,7 +8,7 @@ class Message {
|
|||||||
DateTime updatedAt;
|
DateTime updatedAt;
|
||||||
DateTime? deletedAt;
|
DateTime? deletedAt;
|
||||||
String content;
|
String content;
|
||||||
dynamic metadata;
|
Map<String, dynamic>? metadata;
|
||||||
int type;
|
int type;
|
||||||
List<Attachment>? attachments;
|
List<Attachment>? attachments;
|
||||||
Channel? channel;
|
Channel? channel;
|
||||||
@ -18,6 +18,8 @@ class Message {
|
|||||||
int channelId;
|
int channelId;
|
||||||
int senderId;
|
int senderId;
|
||||||
|
|
||||||
|
bool isSending = false;
|
||||||
|
|
||||||
Message({
|
Message({
|
||||||
required this.id,
|
required this.id,
|
||||||
required this.createdAt,
|
required this.createdAt,
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:easy_debounce/easy_debounce.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_animate/flutter_animate.dart';
|
||||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
import 'package:http/http.dart';
|
import 'package:http/http.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
@ -36,7 +38,8 @@ class _ChatMessageEditorState extends State<ChatMessageEditor> {
|
|||||||
final _textController = TextEditingController();
|
final _textController = TextEditingController();
|
||||||
final _focusNode = FocusNode();
|
final _focusNode = FocusNode();
|
||||||
|
|
||||||
bool _isSubmitting = false;
|
List<int> _pendingMessages = List.empty(growable: true);
|
||||||
|
|
||||||
int? _prevEditingId;
|
int? _prevEditingId;
|
||||||
|
|
||||||
List<Attachment> _attachments = List.empty(growable: true);
|
List<Attachment> _attachments = List.empty(growable: true);
|
||||||
@ -53,8 +56,6 @@ class _ChatMessageEditorState extends State<ChatMessageEditor> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> sendMessage(BuildContext context) async {
|
Future<void> sendMessage(BuildContext context) async {
|
||||||
if (_isSubmitting) return;
|
|
||||||
|
|
||||||
_focusNode.requestFocus();
|
_focusNode.requestFocus();
|
||||||
|
|
||||||
final auth = context.read<AuthProvider>();
|
final auth = context.read<AuthProvider>();
|
||||||
@ -72,15 +73,25 @@ class _ChatMessageEditorState extends State<ChatMessageEditor> {
|
|||||||
'reply_to': widget.replying?.id,
|
'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));
|
var res = await Response.fromStream(await auth.client!.send(req));
|
||||||
if (res.statusCode != 200) {
|
if (res.statusCode != 200) {
|
||||||
var message = utf8.decode(res.bodyBytes);
|
var message = utf8.decode(res.bodyBytes);
|
||||||
context.showErrorDialog(message);
|
context.showErrorDialog(message);
|
||||||
} else {
|
|
||||||
reset();
|
|
||||||
}
|
}
|
||||||
setState(() => _isSubmitting = false);
|
|
||||||
|
EasyDebounce.cancel(messageDebounceId);
|
||||||
|
if (_pendingMessages.isNotEmpty) {
|
||||||
|
setState(() => _pendingMessages.remove(messageMarkId));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void reset() {
|
void reset() {
|
||||||
@ -94,8 +105,8 @@ class _ChatMessageEditorState extends State<ChatMessageEditor> {
|
|||||||
if (widget.editing != null && _prevEditingId != widget.editing!.id) {
|
if (widget.editing != null && _prevEditingId != widget.editing!.id) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_prevEditingId = widget.editing!.id;
|
_prevEditingId = widget.editing!.id;
|
||||||
_textController.text = widget.editing!.content;
|
|
||||||
_attachments = widget.editing!.attachments ?? List.empty(growable: true);
|
_attachments = widget.editing!.attachments ?? List.empty(growable: true);
|
||||||
|
_textController.text = widget.editing!.content;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -113,6 +124,15 @@ class _ChatMessageEditorState extends State<ChatMessageEditor> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
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(
|
final editingBanner = MaterialBanner(
|
||||||
padding: const EdgeInsets.only(top: 4, bottom: 4, left: 20),
|
padding: const EdgeInsets.only(top: 4, bottom: 4, left: 20),
|
||||||
leading: const Icon(Icons.edit_note),
|
leading: const Icon(Icons.edit_note),
|
||||||
@ -143,6 +163,18 @@ class _ChatMessageEditorState extends State<ChatMessageEditor> {
|
|||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
|
_pendingMessages.isNotEmpty
|
||||||
|
? sendingBanner
|
||||||
|
.animate()
|
||||||
|
.scaleY(
|
||||||
|
begin: 0,
|
||||||
|
curve: Curves.fastEaseInToSlowEaseOut,
|
||||||
|
)
|
||||||
|
.slideY(
|
||||||
|
begin: 1,
|
||||||
|
curve: Curves.fastEaseInToSlowEaseOut,
|
||||||
|
)
|
||||||
|
: Container(),
|
||||||
widget.editing != null ? editingBanner : Container(),
|
widget.editing != null ? editingBanner : Container(),
|
||||||
widget.replying != null ? replyingBanner : Container(),
|
widget.replying != null ? replyingBanner : Container(),
|
||||||
Container(
|
Container(
|
||||||
@ -162,7 +194,7 @@ class _ChatMessageEditorState extends State<ChatMessageEditor> {
|
|||||||
position: badge.BadgePosition.custom(top: -2, end: 8),
|
position: badge.BadgePosition.custom(top: -2, end: 8),
|
||||||
child: TextButton(
|
child: TextButton(
|
||||||
style: TextButton.styleFrom(shape: const CircleBorder(), padding: const EdgeInsets.all(4)),
|
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),
|
child: const Icon(Icons.attach_file),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -182,7 +214,7 @@ class _ChatMessageEditorState extends State<ChatMessageEditor> {
|
|||||||
),
|
),
|
||||||
TextButton(
|
TextButton(
|
||||||
style: TextButton.styleFrom(shape: const CircleBorder(), padding: const EdgeInsets.all(4)),
|
style: TextButton.styleFrom(shape: const CircleBorder(), padding: const EdgeInsets.all(4)),
|
||||||
onPressed: !_isSubmitting ? () => sendMessage(context) : null,
|
onPressed: () => sendMessage(context),
|
||||||
child: const Icon(Icons.send),
|
child: const Icon(Icons.send),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
|
@ -209,6 +209,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.1.0"
|
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:
|
fake_async:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -71,6 +71,7 @@ dependencies:
|
|||||||
package_info_plus: ^7.0.0
|
package_info_plus: ^7.0.0
|
||||||
cached_network_image: ^3.3.1
|
cached_network_image: ^3.3.1
|
||||||
desktop_drop: ^0.4.4
|
desktop_drop: ^0.4.4
|
||||||
|
easy_debounce: ^2.0.3
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
Loading…
Reference in New Issue
Block a user