✨ Chat files
This commit is contained in:
parent
ee1fcd8752
commit
795052a950
@ -90,5 +90,7 @@
|
|||||||
"permissionMember": "Member",
|
"permissionMember": "Member",
|
||||||
"reply": "Reply",
|
"reply": "Reply",
|
||||||
"forward": "Forward",
|
"forward": "Forward",
|
||||||
"edited": "Edited"
|
"edited": "Edited",
|
||||||
|
"addVideo": "Add video",
|
||||||
|
"addPhoto": "Add photo"
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:image_picker/image_picker.dart';
|
||||||
import 'package:island/database/message.dart';
|
import 'package:island/database/message.dart';
|
||||||
import 'package:island/database/message_repository.dart';
|
import 'package:island/database/message_repository.dart';
|
||||||
import 'package:island/models/chat.dart';
|
import 'package:island/models/chat.dart';
|
||||||
@ -14,7 +15,9 @@ import 'package:island/pods/message.dart';
|
|||||||
import 'package:island/pods/network.dart';
|
import 'package:island/pods/network.dart';
|
||||||
import 'package:island/pods/websocket.dart';
|
import 'package:island/pods/websocket.dart';
|
||||||
import 'package:island/route.gr.dart';
|
import 'package:island/route.gr.dart';
|
||||||
|
import 'package:island/screens/posts/compose.dart';
|
||||||
import 'package:island/widgets/alert.dart';
|
import 'package:island/widgets/alert.dart';
|
||||||
|
import 'package:island/widgets/content/cloud_file_collection.dart';
|
||||||
import 'package:island/widgets/content/cloud_files.dart';
|
import 'package:island/widgets/content/cloud_files.dart';
|
||||||
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
@ -387,6 +390,30 @@ class ChatRoomScreen extends HookConsumerWidget {
|
|||||||
|
|
||||||
final attachments = useState<List<UniversalFile>>([]);
|
final attachments = useState<List<UniversalFile>>([]);
|
||||||
|
|
||||||
|
Future<void> pickPhotoMedia() async {
|
||||||
|
final result = await ref
|
||||||
|
.watch(imagePickerProvider)
|
||||||
|
.pickMultiImage(requestFullMetadata: true);
|
||||||
|
if (result.isEmpty) return;
|
||||||
|
attachments.value = [
|
||||||
|
...attachments.value,
|
||||||
|
...result.map(
|
||||||
|
(e) => UniversalFile(data: e, type: UniversalFileType.image),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> pickVideoMedia() async {
|
||||||
|
final result = await ref
|
||||||
|
.watch(imagePickerProvider)
|
||||||
|
.pickVideo(source: ImageSource.gallery);
|
||||||
|
if (result == null) return;
|
||||||
|
attachments.value = [
|
||||||
|
...attachments.value,
|
||||||
|
UniversalFile(data: result, type: UniversalFileType.video),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
void sendMessage() {
|
void sendMessage() {
|
||||||
if (messageController.text.trim().isNotEmpty) {
|
if (messageController.text.trim().isNotEmpty) {
|
||||||
messagesNotifier.sendMessage(
|
messagesNotifier.sendMessage(
|
||||||
@ -397,6 +424,10 @@ class ChatRoomScreen extends HookConsumerWidget {
|
|||||||
replyingTo: messageReplyingTo.value,
|
replyingTo: messageReplyingTo.value,
|
||||||
);
|
);
|
||||||
messageController.clear();
|
messageController.clear();
|
||||||
|
messageEditingTo.value = null;
|
||||||
|
messageReplyingTo.value = null;
|
||||||
|
messageForwardingTo.value = null;
|
||||||
|
attachments.value = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -531,6 +562,7 @@ class ChatRoomScreen extends HookConsumerWidget {
|
|||||||
onSend: sendMessage,
|
onSend: sendMessage,
|
||||||
onClear: () {
|
onClear: () {
|
||||||
if (messageEditingTo.value != null) {
|
if (messageEditingTo.value != null) {
|
||||||
|
attachments.value.clear();
|
||||||
messageController.clear();
|
messageController.clear();
|
||||||
}
|
}
|
||||||
messageEditingTo.value = null;
|
messageEditingTo.value = null;
|
||||||
@ -540,6 +572,36 @@ class ChatRoomScreen extends HookConsumerWidget {
|
|||||||
messageEditingTo: messageEditingTo.value,
|
messageEditingTo: messageEditingTo.value,
|
||||||
messageReplyingTo: messageReplyingTo.value,
|
messageReplyingTo: messageReplyingTo.value,
|
||||||
messageForwardingTo: messageForwardingTo.value,
|
messageForwardingTo: messageForwardingTo.value,
|
||||||
|
onPickFile: (bool isVideo) {
|
||||||
|
if (isVideo) {
|
||||||
|
pickPhotoMedia();
|
||||||
|
} else {
|
||||||
|
pickVideoMedia();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
attachments: attachments.value,
|
||||||
|
onUploadAttachment: (_) {
|
||||||
|
// not going to do anything, only upload when send the message
|
||||||
|
},
|
||||||
|
onDeleteAttachment: (index) async {
|
||||||
|
final attachment = attachments.value[index];
|
||||||
|
if (attachment.isOnCloud) {
|
||||||
|
final client = ref.watch(apiClientProvider);
|
||||||
|
await client.delete('/files/${attachment.data.id}');
|
||||||
|
}
|
||||||
|
final clone = List.of(attachments.value);
|
||||||
|
clone.removeAt(index);
|
||||||
|
attachments.value = clone;
|
||||||
|
},
|
||||||
|
onMoveAttachment: (idx, delta) {
|
||||||
|
if (idx + delta < 0 ||
|
||||||
|
idx + delta >= attachments.value.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final clone = List.of(attachments.value);
|
||||||
|
clone.insert(idx + delta, clone.removeAt(idx));
|
||||||
|
attachments.value = clone;
|
||||||
|
},
|
||||||
),
|
),
|
||||||
error: (_, __) => const SizedBox.shrink(),
|
error: (_, __) => const SizedBox.shrink(),
|
||||||
loading: () => const SizedBox.shrink(),
|
loading: () => const SizedBox.shrink(),
|
||||||
@ -555,18 +617,28 @@ class _ChatInput extends StatelessWidget {
|
|||||||
final SnChat chatRoom;
|
final SnChat chatRoom;
|
||||||
final VoidCallback onSend;
|
final VoidCallback onSend;
|
||||||
final VoidCallback onClear;
|
final VoidCallback onClear;
|
||||||
|
final Function(bool isVideo) onPickFile;
|
||||||
final SnChatMessage? messageReplyingTo;
|
final SnChatMessage? messageReplyingTo;
|
||||||
final SnChatMessage? messageForwardingTo;
|
final SnChatMessage? messageForwardingTo;
|
||||||
final SnChatMessage? messageEditingTo;
|
final SnChatMessage? messageEditingTo;
|
||||||
|
final List<UniversalFile> attachments;
|
||||||
|
final Function(int) onUploadAttachment;
|
||||||
|
final Function(int) onDeleteAttachment;
|
||||||
|
final Function(int, int) onMoveAttachment;
|
||||||
|
|
||||||
const _ChatInput({
|
const _ChatInput({
|
||||||
required this.messageController,
|
required this.messageController,
|
||||||
required this.chatRoom,
|
required this.chatRoom,
|
||||||
required this.onSend,
|
required this.onSend,
|
||||||
required this.onClear,
|
required this.onClear,
|
||||||
|
required this.onPickFile,
|
||||||
required this.messageReplyingTo,
|
required this.messageReplyingTo,
|
||||||
required this.messageForwardingTo,
|
required this.messageForwardingTo,
|
||||||
required this.messageEditingTo,
|
required this.messageEditingTo,
|
||||||
|
required this.attachments,
|
||||||
|
required this.onUploadAttachment,
|
||||||
|
required this.onDeleteAttachment,
|
||||||
|
required this.onMoveAttachment,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -576,6 +648,24 @@ class _ChatInput extends StatelessWidget {
|
|||||||
color: Theme.of(context).colorScheme.surface,
|
color: Theme.of(context).colorScheme.surface,
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
|
if (attachments.isNotEmpty)
|
||||||
|
SizedBox(
|
||||||
|
height: 280,
|
||||||
|
child: ListView.separated(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 12),
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
itemCount: attachments.length,
|
||||||
|
itemBuilder: (context, idx) {
|
||||||
|
return AttachmentPreview(
|
||||||
|
item: attachments[idx],
|
||||||
|
onRequestUpload: () => onUploadAttachment(idx),
|
||||||
|
onDelete: () => onDeleteAttachment(idx),
|
||||||
|
onMove: (delta) => onMoveAttachment(idx, delta),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
separatorBuilder: (_, __) => const Gap(8),
|
||||||
|
),
|
||||||
|
),
|
||||||
if (messageReplyingTo != null ||
|
if (messageReplyingTo != null ||
|
||||||
messageForwardingTo != null ||
|
messageForwardingTo != null ||
|
||||||
messageEditingTo != null)
|
messageEditingTo != null)
|
||||||
@ -614,7 +704,9 @@ class _ChatInput extends StatelessWidget {
|
|||||||
icon: const Icon(Icons.close, size: 20),
|
icon: const Icon(Icons.close, size: 20),
|
||||||
onPressed: onClear,
|
onPressed: onClear,
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
constraints: const BoxConstraints(),
|
style: ButtonStyle(
|
||||||
|
minimumSize: WidgetStatePropertyAll(Size(28, 28)),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -623,6 +715,32 @@ class _ChatInput extends StatelessWidget {
|
|||||||
padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 8),
|
padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 8),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
|
PopupMenuButton(
|
||||||
|
icon: const Icon(Symbols.photo_library),
|
||||||
|
itemBuilder:
|
||||||
|
(context) => [
|
||||||
|
PopupMenuItem(
|
||||||
|
onTap: () => onPickFile(true),
|
||||||
|
child: Row(
|
||||||
|
spacing: 12,
|
||||||
|
children: [
|
||||||
|
const Icon(Symbols.photo),
|
||||||
|
Text('addPhoto').tr(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
PopupMenuItem(
|
||||||
|
onTap: () => onPickFile(true),
|
||||||
|
child: Row(
|
||||||
|
spacing: 12,
|
||||||
|
children: [
|
||||||
|
const Icon(Symbols.video_call),
|
||||||
|
Text('addVideo').tr(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: TextField(
|
child: TextField(
|
||||||
controller: messageController,
|
controller: messageController,
|
||||||
@ -773,6 +891,10 @@ class _MessageBubble extends HookConsumerWidget {
|
|||||||
message.toRemoteMessage().content ?? '',
|
message.toRemoteMessage().content ?? '',
|
||||||
style: TextStyle(color: textColor),
|
style: TextStyle(color: textColor),
|
||||||
),
|
),
|
||||||
|
if (message.toRemoteMessage().attachments.isNotEmpty)
|
||||||
|
CloudFileList(
|
||||||
|
files: message.toRemoteMessage().attachments,
|
||||||
|
).padding(top: 4),
|
||||||
const Gap(4),
|
const Gap(4),
|
||||||
Row(
|
Row(
|
||||||
spacing: 4,
|
spacing: 4,
|
||||||
|
@ -308,7 +308,7 @@ class PostComposeScreen extends HookConsumerWidget {
|
|||||||
idx < attachments.value.length;
|
idx < attachments.value.length;
|
||||||
idx++
|
idx++
|
||||||
)
|
)
|
||||||
_AttachmentPreview(
|
AttachmentPreview(
|
||||||
item: attachments.value[idx],
|
item: attachments.value[idx],
|
||||||
progress: attachmentProgress.value[idx],
|
progress: attachmentProgress.value[idx],
|
||||||
onRequestUpload: () => uploadAttachment(idx),
|
onRequestUpload: () => uploadAttachment(idx),
|
||||||
@ -374,13 +374,14 @@ class PostComposeScreen extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _AttachmentPreview extends StatelessWidget {
|
class AttachmentPreview extends StatelessWidget {
|
||||||
final UniversalFile item;
|
final UniversalFile item;
|
||||||
final double? progress;
|
final double? progress;
|
||||||
final Function(int)? onMove;
|
final Function(int)? onMove;
|
||||||
final Function? onDelete;
|
final Function? onDelete;
|
||||||
final Function? onRequestUpload;
|
final Function? onRequestUpload;
|
||||||
const _AttachmentPreview({
|
const AttachmentPreview({
|
||||||
|
super.key,
|
||||||
required this.item,
|
required this.item,
|
||||||
this.progress,
|
this.progress,
|
||||||
this.onRequestUpload,
|
this.onRequestUpload,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user