From 8bc8556f06200b6b071e74200617f21a712a94c1 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Wed, 25 Jun 2025 23:02:14 +0800 Subject: [PATCH] :sparkles: Share to chat --- assets/i18n/en-US.json | 7 +- lib/widgets/share/share_sheet.dart | 236 ++++++++++++++++++++++++++--- 2 files changed, 220 insertions(+), 23 deletions(-) diff --git a/assets/i18n/en-US.json b/assets/i18n/en-US.json index d986c18..f9a2216 100644 --- a/assets/i18n/en-US.json +++ b/assets/i18n/en-US.json @@ -561,5 +561,10 @@ "noChatRoomsAvailable": "No chat rooms available", "failedToLoadChats": "Failed to load chats", "contentToShare": "Content to share:", - "unknownChat": "Unknown Chat" + "unknownChat": "Unknown Chat", + "addAdditionalMessage": "Add additional message...", + "uploadingFiles": "Uploading files...", + "sharedSuccessfully": "Shared successfully!", + "navigateToChat": "Navigate to Chat", + "wouldYouLikeToNavigateToChat": "Would you like to navigate to the chat?" } diff --git a/lib/widgets/share/share_sheet.dart b/lib/widgets/share/share_sheet.dart index 56ca0cc..78262e8 100644 --- a/lib/widgets/share/share_sheet.dart +++ b/lib/widgets/share/share_sheet.dart @@ -10,6 +10,10 @@ import 'package:island/route.gr.dart'; import 'package:island/screens/posts/compose.dart'; import 'package:island/models/file.dart'; import 'package:island/pods/link_preview.dart'; +import 'package:island/pods/network.dart'; +import 'package:island/pods/config.dart'; +import 'package:island/pods/userinfo.dart'; +import 'package:island/services/file.dart'; import 'dart:io'; import 'package:path/path.dart' as path; @@ -120,6 +124,14 @@ class ShareSheet extends ConsumerStatefulWidget { class _ShareSheetState extends ConsumerState { bool _isLoading = false; + final TextEditingController _messageController = TextEditingController(); + final Map> _fileUploadProgress = {}; + + @override + void dispose() { + _messageController.dispose(); + super.dispose(); + } void _handleClose() { if (widget.onClose != null) { @@ -185,33 +197,162 @@ class _ShareSheetState extends ConsumerState { } } - Future _shareToChat() async { - setState(() => _isLoading = true); - try {} catch (e) { - showErrorAlert(e); - } finally { - setState(() => _isLoading = false); - } - } - Future _shareToSpecificChat(SnChatRoom chatRoom) async { setState(() => _isLoading = true); try { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - 'shareToSpecificChatComingSoon'.tr( - args: [chatRoom.name ?? 'directChat'.tr()], + final apiClient = ref.read(apiClientProvider); + final userInfo = ref.read(userInfoProvider.notifier); + final serverUrl = ref.read(serverUrlProvider); + + String content = _messageController.text.trim(); + List attachmentIds = []; + + // Handle different content types + switch (widget.content.type) { + case ShareContentType.text: + if (content.isEmpty) { + content = widget.content.text ?? ''; + } else if (widget.content.text?.isNotEmpty == true) { + content = '$content\n\n${widget.content.text}'; + } + break; + case ShareContentType.link: + if (content.isEmpty) { + content = widget.content.link ?? ''; + } else if (widget.content.link?.isNotEmpty == true) { + content = '$content\n\n${widget.content.link}'; + } + break; + case ShareContentType.file: + // Upload files to cloud storage + if (widget.content.files?.isNotEmpty == true) { + final token = await userInfo.getAccessToken(); + if (token == null) { + throw Exception('Authentication required'); + } + + final universalFiles = + widget.content.files!.map((file) { + UniversalFileType fileType; + if (file.mimeType?.startsWith('image/') == true) { + fileType = UniversalFileType.image; + } else if (file.mimeType?.startsWith('video/') == true) { + fileType = UniversalFileType.video; + } else if (file.mimeType?.startsWith('audio/') == true) { + fileType = UniversalFileType.audio; + } else { + fileType = UniversalFileType.file; + } + return UniversalFile(data: file, type: fileType); + }).toList(); + + // Initialize progress tracking + final messageId = DateTime.now().millisecondsSinceEpoch.toString(); + _fileUploadProgress[messageId] = List.filled( + universalFiles.length, + 0.0, + ); + + // Upload each file + for (var idx = 0; idx < universalFiles.length; idx++) { + final file = universalFiles[idx]; + final cloudFile = + await putMediaToCloud( + fileData: file, + atk: token, + baseUrl: serverUrl, + filename: file.data.name ?? 'Shared file', + mimetype: + file.data.mimeType ?? + switch (file.type) { + UniversalFileType.image => 'image/unknown', + UniversalFileType.video => 'video/unknown', + UniversalFileType.audio => 'audio/unknown', + UniversalFileType.file => 'application/octet-stream', + }, + onProgress: (progress, _) { + if (mounted) { + setState(() { + _fileUploadProgress[messageId]?[idx] = progress; + }); + } + }, + ).future; + + if (cloudFile == null) { + throw Exception('Failed to upload file: ${file.data.name}'); + } + attachmentIds.add(cloudFile.id); + } + } + break; + } + + if (content.isEmpty && attachmentIds.isEmpty) { + throw Exception('No content to share'); + } + + // Send message to chat room + await apiClient.post( + '/chat/${chatRoom.id}/messages', + data: {'content': content, 'attachments_id': attachmentIds, 'meta': {}}, + ); + + if (mounted) { + // Show success message + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + 'shareToSpecificChatSuccess'.tr( + args: [chatRoom.name ?? 'directChat'.tr()], + ), ), ), - ), - ); + ); + + // Show navigation prompt + final shouldNavigate = await showDialog( + context: context, + builder: + (context) => AlertDialog( + title: Text('shareSuccess'.tr()), + content: Text('wouldYouLikeToGoToChat'.tr()), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(false), + child: Text('no'.tr()), + ), + TextButton( + onPressed: () => Navigator.of(context).pop(true), + child: Text('yes'.tr()), + ), + ], + ), + ); + + // Close the share sheet + if (mounted) { + Navigator.of(context).pop(); + } + + // Navigate to chat if requested + if (shouldNavigate == true && mounted) { + context.router.pushPath('/chat/$chatRoom'); + } + } } catch (e) { - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text('Failed to share to chat: $e'))); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Failed to share to chat: $e'), + backgroundColor: Theme.of(context).colorScheme.error, + ), + ); + } } finally { - setState(() => _isLoading = false); + if (mounted) { + setState(() => _isLoading = false); + } } } @@ -370,6 +511,28 @@ class _ShareSheetState extends ConsumerState { ), ), const SizedBox(height: 12), + + // Additional message input + Container( + margin: const EdgeInsets.only(bottom: 16), + child: TextField( + controller: _messageController, + decoration: InputDecoration( + hintText: 'addAdditionalMessage'.tr(), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + ), + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), + ), + maxLines: 3, + minLines: 1, + enabled: !_isLoading, + ), + ), + _ChatRoomsList( onChatSelected: _isLoading ? null : _shareToSpecificChat, @@ -384,11 +547,40 @@ class _ShareSheetState extends ConsumerState { ), ), - // Loading indicator + // Loading indicator and file upload progress if (_isLoading) Container( padding: const EdgeInsets.all(16), - child: const CircularProgressIndicator(), + child: Column( + children: [ + const CircularProgressIndicator(), + const SizedBox(height: 8), + if (_fileUploadProgress.isNotEmpty) + ..._fileUploadProgress.entries.map((entry) { + final progress = entry.value; + final averageProgress = + progress.isEmpty + ? 0.0 + : progress.reduce((a, b) => a + b) / + progress.length; + return Column( + children: [ + Text( + 'uploadingFiles'.tr(), + style: Theme.of(context).textTheme.bodySmall, + ), + const SizedBox(height: 4), + LinearProgressIndicator(value: averageProgress), + const SizedBox(height: 4), + Text( + '${(averageProgress * 100).toInt()}%', + style: Theme.of(context).textTheme.bodySmall, + ), + ], + ); + }), + ], + ), ), ], ),