✨ Chat message attachments
This commit is contained in:
parent
82fc191bad
commit
ac280f2d40
@ -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.",
|
||||
|
@ -37,6 +37,7 @@
|
||||
"postEditNotify": "你正在修改一个已经发布了的帖子。",
|
||||
"reactionAdded": "你的反应已被添加。",
|
||||
"reactionRemoved": "你的反应已被移除。",
|
||||
"chatNew": "新聊天",
|
||||
"chatMessagePlaceholder": "发条消息……",
|
||||
"chatMessageEditNotify": "你正在编辑信息中……",
|
||||
"chatMessageReplyNotify": "你正在回复消息中……",
|
||||
|
@ -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;
|
||||
|
@ -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) {
|
||||
|
@ -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(),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -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(),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
26
lib/widgets/chat/chat_new.dart
Normal file
26
lib/widgets/chat/chat_new.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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,
|
||||
|
@ -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(
|
||||
|
@ -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),
|
||||
],
|
||||
|
12
pubspec.lock
12
pubspec.lock
@ -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:
|
||||
|
@ -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:
|
||||
|
Loading…
Reference in New Issue
Block a user