Solian/lib/widgets/chat/message_editor.dart

227 lines
7.4 KiB
Dart
Raw Normal View History

2024-04-17 15:24:09 +00:00
import 'dart:convert';
import 'package:easy_debounce/easy_debounce.dart';
2024-04-17 15:24:09 +00:00
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
2024-04-17 15:24:09 +00:00
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:http/http.dart';
import 'package:provider/provider.dart';
import 'package:solian/models/message.dart';
2024-04-19 11:36:03 +00:00
import 'package:solian/models/post.dart';
2024-04-17 15:24:09 +00:00
import 'package:solian/providers/auth.dart';
import 'package:solian/utils/service_url.dart';
2024-04-29 12:22:06 +00:00
import 'package:solian/widgets/exts.dart';
2024-04-19 11:36:03 +00:00
import 'package:solian/widgets/posts/attachment_editor.dart';
import 'package:badges/badges.dart' as badge;
2024-04-17 15:24:09 +00:00
class ChatMessageEditor extends StatefulWidget {
final String channel;
2024-05-05 15:01:08 +00:00
final String realm;
2024-04-17 15:24:09 +00:00
final Message? editing;
final Message? replying;
final Function? onReset;
2024-04-17 15:24:09 +00:00
2024-05-05 15:01:08 +00:00
const ChatMessageEditor({
super.key,
required this.channel,
this.realm = 'global',
this.editing,
this.replying,
this.onReset,
});
2024-04-17 15:24:09 +00:00
@override
State<ChatMessageEditor> createState() => _ChatMessageEditorState();
}
class _ChatMessageEditorState extends State<ChatMessageEditor> {
final _textController = TextEditingController();
2024-05-01 11:39:48 +00:00
final _focusNode = FocusNode();
2024-04-17 15:24:09 +00:00
List<int> _pendingMessages = List.empty(growable: true);
2024-04-27 12:10:15 +00:00
int? _prevEditingId;
2024-04-17 15:24:09 +00:00
2024-04-19 11:36:03 +00:00
List<Attachment> _attachments = List.empty(growable: true);
void viewAttachments(BuildContext context) {
showModalBottomSheet(
context: context,
builder: (context) => AttachmentEditor(
provider: 'messaging',
current: _attachments,
2024-04-24 15:19:26 +00:00
onUpdate: (value) => setState(() => _attachments = value),
2024-04-19 11:36:03 +00:00
),
);
}
2024-04-17 15:24:09 +00:00
Future<void> sendMessage(BuildContext context) async {
2024-05-01 11:39:48 +00:00
_focusNode.requestFocus();
2024-04-17 15:24:09 +00:00
final auth = context.read<AuthProvider>();
if (!await auth.isAuthorized()) return;
final uri = widget.editing == null
2024-05-05 15:01:08 +00:00
? getRequestUri('messaging', '/api/channels/${widget.realm}/${widget.channel}/messages')
: getRequestUri('messaging', '/api/channels/${widget.realm}/${widget.channel}/messages/${widget.editing!.id}');
2024-04-17 15:24:09 +00:00
2024-05-01 16:49:38 +00:00
final req = Request(widget.editing == null ? 'POST' : 'PUT', uri);
2024-04-17 15:24:09 +00:00
req.headers['Content-Type'] = 'application/json';
req.body = jsonEncode(<String, dynamic>{
'content': _textController.value.text,
2024-04-19 11:36:03 +00:00
'attachments': _attachments,
'reply_to': widget.replying?.id,
2024-04-17 15:24:09 +00:00
});
reset();
final messageMarkId = DateTime.now().microsecondsSinceEpoch >> 10;
final messageDebounceId = 'm-pending$messageMarkId';
EasyDebounce.debounce(messageDebounceId, 350.ms, () {
setState(() => _pendingMessages.add(messageMarkId));
});
2024-04-17 15:24:09 +00:00
var res = await Response.fromStream(await auth.client!.send(req));
if (res.statusCode != 200) {
var message = utf8.decode(res.bodyBytes);
2024-04-29 12:22:06 +00:00
context.showErrorDialog(message);
2024-04-17 15:24:09 +00:00
}
EasyDebounce.cancel(messageDebounceId);
if (_pendingMessages.isNotEmpty) {
setState(() => _pendingMessages.remove(messageMarkId));
}
2024-04-17 15:24:09 +00:00
}
void reset() {
_textController.clear();
2024-04-19 11:36:03 +00:00
_attachments.clear();
if (widget.onReset != null) widget.onReset!();
}
void syncWidget() {
2024-04-27 12:10:15 +00:00
if (widget.editing != null && _prevEditingId != widget.editing!.id) {
setState(() {
2024-04-27 12:10:15 +00:00
_prevEditingId = widget.editing!.id;
2024-05-01 11:39:48 +00:00
_attachments = widget.editing!.attachments ?? List.empty(growable: true);
_textController.text = widget.editing!.content;
});
}
}
@override
void initState() {
super.initState();
}
@override
void didUpdateWidget(oldWidget) {
syncWidget();
super.didUpdateWidget(oldWidget);
2024-04-17 15:24:09 +00:00
}
@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),
2024-05-01 14:35:58 +00:00
backgroundColor: Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.9),
dividerColor: const Color.fromARGB(1, 0, 0, 0),
content: Text(AppLocalizations.of(context)!.chatMessageEditNotify),
actions: [
TextButton(
child: Text(AppLocalizations.of(context)!.cancel),
onPressed: () => reset(),
2024-04-17 15:24:09 +00:00
),
],
);
final replyingBanner = MaterialBanner(
padding: const EdgeInsets.only(top: 4, bottom: 4, left: 20),
leading: const Icon(Icons.reply),
2024-05-01 14:35:58 +00:00
backgroundColor: Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.9),
dividerColor: const Color.fromARGB(1, 0, 0, 0),
content: Text(AppLocalizations.of(context)!.chatMessageReplyNotify),
actions: [
TextButton(
child: Text(AppLocalizations.of(context)!.cancel),
onPressed: () => reset(),
),
],
);
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(
height: 56,
2024-04-19 11:36:03 +00:00
padding: const EdgeInsets.only(top: 4, bottom: 4, right: 8),
2024-05-02 04:16:01 +00:00
decoration: BoxDecoration(
border: Border(
2024-05-02 04:16:01 +00:00
top: BorderSide(width: 0.3, color: Theme.of(context).dividerColor),
2024-04-17 15:24:09 +00:00
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
2024-04-19 11:36:03 +00:00
badge.Badge(
showBadge: _attachments.isNotEmpty,
2024-05-01 11:39:48 +00:00
badgeContent: Text(_attachments.length.toString(), style: const TextStyle(color: Colors.white)),
2024-04-19 11:36:03 +00:00
position: badge.BadgePosition.custom(top: -2, end: 8),
child: TextButton(
2024-05-01 11:39:48 +00:00
style: TextButton.styleFrom(shape: const CircleBorder(), padding: const EdgeInsets.all(4)),
onPressed: () => viewAttachments(context),
2024-04-19 11:36:03 +00:00
child: const Icon(Icons.attach_file),
),
),
Expanded(
child: TextField(
2024-05-01 11:39:48 +00:00
focusNode: _focusNode,
controller: _textController,
maxLines: null,
autocorrect: true,
keyboardType: TextInputType.text,
decoration: InputDecoration.collapsed(
2024-05-01 11:39:48 +00:00
hintText: AppLocalizations.of(context)!.chatMessagePlaceholder,
),
onSubmitted: (_) => sendMessage(context),
2024-05-01 11:39:48 +00:00
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
),
),
TextButton(
2024-05-01 11:39:48 +00:00
style: TextButton.styleFrom(shape: const CircleBorder(), padding: const EdgeInsets.all(4)),
onPressed: () => sendMessage(context),
child: const Icon(Icons.send),
)
],
),
),
],
2024-04-17 15:24:09 +00:00
);
}
}