Chat message attachments

This commit is contained in:
LittleSheep 2024-04-19 19:36:03 +08:00
parent 82fc191bad
commit ac280f2d40
12 changed files with 121 additions and 34 deletions

View File

@ -37,6 +37,7 @@
"postEditNotify": "You are about editing a post that already published.",
"reactionAdded": "Your reaction has been added.",
"reactionRemoved": "Your reaction has been removed.",
"chatNew": "New Chat",
"chatMessagePlaceholder": "Write a message...",
"chatMessageEditNotify": "You are about editing a message.",
"chatMessageReplyNotify": "You are about replying a message.",

View File

@ -37,6 +37,7 @@
"postEditNotify": "你正在修改一个已经发布了的帖子。",
"reactionAdded": "你的反应已被添加。",
"reactionRemoved": "你的反应已被移除。",
"chatNew": "新聊天",
"chatMessagePlaceholder": "发条消息……",
"chatMessageEditNotify": "你正在编辑信息中……",
"chatMessageReplyNotify": "你正在回复消息中……",

View File

@ -74,6 +74,7 @@ class _ChatScreenState extends State<ChatScreen> {
}
bool getMessageMergeable(Message? a, Message? b) {
if (a?.replyTo != null || b?.replyTo != null) return false;
if (a == null || b == null) return false;
if (a.senderId != b.senderId) return false;
return a.createdAt.difference(b.createdAt).inMinutes <= 5;

View File

@ -6,6 +6,7 @@ import 'package:solian/models/channel.dart';
import 'package:solian/providers/auth.dart';
import 'package:solian/router.dart';
import 'package:solian/utils/service_url.dart';
import 'package:solian/widgets/chat/chat_new.dart';
import 'package:solian/widgets/indent_wrapper.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:solian/widgets/signin_required.dart';
@ -40,6 +41,13 @@ class _ChatIndexScreenState extends State<ChatIndexScreen> {
}
}
void viewNewChatAction() {
showModalBottomSheet(
context: context,
builder: (context) => const ChatNewAction(),
);
}
@override
void initState() {
Future.delayed(Duration.zero, () {
@ -55,6 +63,20 @@ class _ChatIndexScreenState extends State<ChatIndexScreen> {
return IndentWrapper(
title: AppLocalizations.of(context)!.chat,
floatingActionButton: FutureBuilder(
future: auth.isAuthorized(),
builder: (context, snapshot) {
if (snapshot.hasData && snapshot.data!) {
return FloatingActionButton.extended(
icon: const Icon(Icons.add),
label: Text(AppLocalizations.of(context)!.chatNew),
onPressed: () => viewNewChatAction(),
);
} else {
return Container();
}
},
),
child: FutureBuilder(
future: auth.isAuthorized(),
builder: (context, snapshot) {

View File

@ -40,6 +40,7 @@ class _CommentEditorScreenState extends State<CommentEditorScreen> {
showModalBottomSheet(
context: context,
builder: (context) => AttachmentEditor(
provider: 'interactive',
current: _attachments,
onUpdate: (value) => _attachments = value,
),
@ -151,8 +152,7 @@ class _CommentEditorScreenState extends State<CommentEditorScreen> {
const Divider(thickness: 0.3),
Expanded(
child: Container(
padding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: TextField(
maxLines: null,
autofocus: true,
@ -160,9 +160,9 @@ class _CommentEditorScreenState extends State<CommentEditorScreen> {
keyboardType: TextInputType.multiline,
controller: _textController,
decoration: InputDecoration.collapsed(
hintText:
AppLocalizations.of(context)!.postContentPlaceholder,
hintText: AppLocalizations.of(context)!.postContentPlaceholder,
),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
),
),
),

View File

@ -32,6 +32,7 @@ class _MomentEditorScreenState extends State<MomentEditorScreen> {
showModalBottomSheet(
context: context,
builder: (context) => AttachmentEditor(
provider: 'interactive',
current: _attachments,
onUpdate: (value) => _attachments = value,
),
@ -151,6 +152,7 @@ class _MomentEditorScreenState extends State<MomentEditorScreen> {
decoration: InputDecoration.collapsed(
hintText: AppLocalizations.of(context)!.postContentPlaceholder,
),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
),
),
),

View File

@ -0,0 +1,26 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class ChatNewAction extends StatelessWidget {
const ChatNewAction({super.key});
@override
Widget build(BuildContext context) {
return SizedBox(
height: 320,
width: double.infinity,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.only(left: 20, top: 20, bottom: 8),
child: Text(
AppLocalizations.of(context)!.chatNew,
style: Theme.of(context).textTheme.headlineSmall,
),
),
],
),
);
}
}

View File

@ -1,6 +1,4 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:solian/models/message.dart';
import 'package:solian/widgets/chat/content.dart';
import 'package:solian/widgets/posts/content/attachment.dart';
@ -25,14 +23,11 @@ class ChatMessage extends StatelessWidget {
}
Widget renderReply() {
final padding =
underMerged ? const EdgeInsets.only(left: 14, right: 8, top: 4) : const EdgeInsets.only(left: 8, right: 8);
if (item.replyTo != null) {
return Row(
children: [
Container(
padding: padding,
padding: const EdgeInsets.only(left: 8, right: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,

View File

@ -5,8 +5,11 @@ 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';
import 'package:solian/models/post.dart';
import 'package:solian/providers/auth.dart';
import 'package:solian/utils/service_url.dart';
import 'package:solian/widgets/posts/attachment_editor.dart';
import 'package:badges/badges.dart' as badge;
class ChatMessageEditor extends StatefulWidget {
final String channel;
@ -25,6 +28,19 @@ class _ChatMessageEditorState extends State<ChatMessageEditor> {
bool _isSubmitting = false;
List<Attachment> _attachments = List.empty(growable: true);
void viewAttachments(BuildContext context) {
showModalBottomSheet(
context: context,
builder: (context) => AttachmentEditor(
provider: 'messaging',
current: _attachments,
onUpdate: (value) => _attachments = value,
),
);
}
Future<void> sendMessage(BuildContext context) async {
if (_isSubmitting) return;
@ -39,6 +55,7 @@ class _ChatMessageEditorState extends State<ChatMessageEditor> {
req.headers['Content-Type'] = 'application/json';
req.body = jsonEncode(<String, dynamic>{
'content': _textController.value.text,
'attachments': _attachments,
'reply_to': widget.replying?.id,
});
@ -57,6 +74,7 @@ class _ChatMessageEditorState extends State<ChatMessageEditor> {
void reset() {
_textController.clear();
_attachments.clear();
if (widget.onReset != null) widget.onReset!();
}
@ -65,6 +83,7 @@ class _ChatMessageEditorState extends State<ChatMessageEditor> {
if (widget.editing != null) {
setState(() {
_textController.text = widget.editing!.content;
_attachments = widget.editing!.attachments ?? List.empty(growable: true);
});
}
}
@ -116,7 +135,7 @@ class _ChatMessageEditorState extends State<ChatMessageEditor> {
widget.replying != null ? replyingBanner : Container(),
Container(
height: 56,
padding: const EdgeInsets.only(top: 4, left: 16, right: 8),
padding: const EdgeInsets.only(top: 4, bottom: 4, right: 8),
decoration: const BoxDecoration(
border: Border(
top: BorderSide(width: 0.3, color: Color(0xffdedede)),
@ -125,6 +144,16 @@ class _ChatMessageEditorState extends State<ChatMessageEditor> {
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
badge.Badge(
showBadge: _attachments.isNotEmpty,
badgeContent: Text(_attachments.length.toString(), style: const TextStyle(color: Colors.white)),
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,
child: const Icon(Icons.attach_file),
),
),
Expanded(
child: TextField(
controller: _textController,
@ -136,6 +165,7 @@ class _ChatMessageEditorState extends State<ChatMessageEditor> {
hintText: AppLocalizations.of(context)!.chatMessagePlaceholder,
),
onSubmitted: (_) => sendMessage(context),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
),
),
TextButton(

View File

@ -13,10 +13,16 @@ import 'package:solian/utils/service_url.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class AttachmentEditor extends StatefulWidget {
final String provider;
final List<Attachment> current;
final void Function(List<Attachment> data) onUpdate;
const AttachmentEditor({super.key, required this.current, required this.onUpdate});
const AttachmentEditor({
super.key,
required this.provider,
required this.current,
required this.onUpdate,
});
@override
State<AttachmentEditor> createState() => _AttachmentEditorState();
@ -97,10 +103,8 @@ class _AttachmentEditorState extends State<AttachmentEditor> {
Future<void> uploadAttachment(File file, String hashcode) async {
final auth = context.read<AuthProvider>();
final req = MultipartRequest(
'POST',
getRequestUri('interactive', '/api/attachments'),
);
final req = MultipartRequest('POST', getRequestUri(widget.provider, '/api/attachments'));
req.files.add(await MultipartFile.fromPath('attachment', file.path));
req.fields['hashcode'] = hashcode;
@ -119,10 +123,7 @@ class _AttachmentEditorState extends State<AttachmentEditor> {
Future<void> disposeAttachment(BuildContext context, Attachment item, int index) async {
final auth = context.read<AuthProvider>();
final req = MultipartRequest(
'DELETE',
getRequestUri('interactive', '/api/attachments/${item.id}'),
);
final req = MultipartRequest('DELETE', getRequestUri(widget.provider, '/api/attachments/${item.id}'));
setState(() => _isSubmitting = true);
var res = await auth.client!.send(req);
@ -293,14 +294,16 @@ class AttachmentEditorMethodPopup extends StatelessWidget {
),
),
Expanded(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
child: GridView.count(
primary: false,
crossAxisSpacing: 10,
mainAxisSpacing: 10,
crossAxisCount: 4,
children: [
InkWell(
borderRadius: BorderRadius.circular(8),
onTap: () => pickImage(),
child: Padding(
padding: const EdgeInsets.all(8),
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
@ -314,8 +317,7 @@ class AttachmentEditorMethodPopup extends StatelessWidget {
InkWell(
borderRadius: BorderRadius.circular(8),
onTap: () => takeImage(),
child: Padding(
padding: const EdgeInsets.all(8),
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
@ -329,12 +331,11 @@ class AttachmentEditorMethodPopup extends StatelessWidget {
InkWell(
borderRadius: BorderRadius.circular(8),
onTap: () => pickVideo(),
child: Padding(
padding: const EdgeInsets.all(8),
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.camera, color: Colors.indigo),
const Icon(Icons.camera, color: Colors.teal),
const SizedBox(height: 8),
Text(AppLocalizations.of(context)!.pickVideo),
],
@ -344,12 +345,11 @@ class AttachmentEditorMethodPopup extends StatelessWidget {
InkWell(
borderRadius: BorderRadius.circular(8),
onTap: () => takeVideo(),
child: Padding(
padding: const EdgeInsets.all(8),
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.video_call, color: Colors.indigo),
const Icon(Icons.video_call, color: Colors.teal),
const SizedBox(height: 8),
Text(AppLocalizations.of(context)!.takeVideo),
],

View File

@ -25,6 +25,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.11.0"
badges:
dependency: "direct main"
description:
name: badges
sha256: a7b6bbd60dce418df0db3058b53f9d083c22cdb5132a052145dc267494df0b84
url: "https://pub.dev"
source: hosted
version: "3.1.2"
boolean_selector:
dependency: transitive
description:
@ -697,10 +705,10 @@ packages:
dependency: transitive
description:
name: pointycastle
sha256: "70fe966348fe08c34bf929582f1d8247d9d9408130723206472b4687227e4333"
sha256: "79fbafed02cfdbe85ef3fd06c7f4bc2cbcba0177e61b765264853d4253b21744"
url: "https://pub.dev"
source: hosted
version: "3.8.0"
version: "3.9.0"
provider:
dependency: "direct main"
description:

View File

@ -59,6 +59,7 @@ dependencies:
hive_flutter: ^1.1.0
flutter_launcher_icons: ^0.13.1
web_socket_channel: ^2.4.5
badges: ^3.1.2
dev_dependencies:
flutter_test: