From c80499db0308419771bceb1efe8c2a5d05ea8cd2 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Fri, 31 Jan 2025 22:52:21 +0800 Subject: [PATCH] :sparkles: Share to chat channel --- assets/translations/en-US.json | 1 + assets/translations/zh-CN.json | 1 + assets/translations/zh-HK.json | 1 + assets/translations/zh-TW.json | 1 + lib/router.dart | 3 +- lib/screens/chat/room.dart | 34 +++- lib/screens/post/post_editor.dart | 6 +- lib/screens/sharing.dart | 232 ++++++++++++++++++++++- lib/widgets/chat/chat_message_input.dart | 10 + lib/widgets/notify_indicator.dart | 3 - 10 files changed, 274 insertions(+), 18 deletions(-) diff --git a/assets/translations/en-US.json b/assets/translations/en-US.json index aecc9a6..c24d9c7 100644 --- a/assets/translations/en-US.json +++ b/assets/translations/en-US.json @@ -562,6 +562,7 @@ "shareIntent": "Share", "shareIntentDescription": "What do you want to do with the content you are sharing?", "shareIntentPostStory": "Post a Story", + "shareIntentSendChannel": "Share to Channel", "updateAvailable": "Update Available", "updateOngoing": "Updating, please wait...", "custom": "Custom", diff --git a/assets/translations/zh-CN.json b/assets/translations/zh-CN.json index 228299e..3f85876 100644 --- a/assets/translations/zh-CN.json +++ b/assets/translations/zh-CN.json @@ -560,6 +560,7 @@ "shareIntent": "分享", "shareIntentDescription": "您想对您分享的内容做些什么?", "shareIntentPostStory": "发布动态", + "shareIntentSendChannel": "分享到聊天频道", "updateAvailable": "检测到更新可用", "updateOngoing": "正在更新,请稍后……", "custom": "自定义", diff --git a/assets/translations/zh-HK.json b/assets/translations/zh-HK.json index a8724fd..9e9ef86 100644 --- a/assets/translations/zh-HK.json +++ b/assets/translations/zh-HK.json @@ -560,6 +560,7 @@ "shareIntent": "分享", "shareIntentDescription": "您想對您分享的內容做些什麼?", "shareIntentPostStory": "發佈動態", + "shareIntentSendChannel": "分享到聊天頻道", "updateAvailable": "檢測到更新可用", "updateOngoing": "正在更新,請稍後……", "custom": "自定義", diff --git a/assets/translations/zh-TW.json b/assets/translations/zh-TW.json index 9c238b2..f2ff86e 100644 --- a/assets/translations/zh-TW.json +++ b/assets/translations/zh-TW.json @@ -560,6 +560,7 @@ "shareIntent": "分享", "shareIntentDescription": "您想對您分享的內容做些什麼?", "shareIntentPostStory": "發佈動態", + "shareIntentSendChannel": "分享到聊天頻道", "updateAvailable": "檢測到更新可用", "updateOngoing": "正在更新,請稍後……", "custom": "自定義", diff --git a/lib/router.dart b/lib/router.dart index b6f6a75..611e766 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -73,7 +73,7 @@ final _appRoutes = [ postRepostId: int.tryParse( state.uri.queryParameters['reposting'] ?? '', ), - extraProps: state.extra as PostEditorExtraProps?, + extraProps: state.extra as PostEditorExtra?, ), ), GoRoute( @@ -156,6 +156,7 @@ final _appRoutes = [ builder: (context, state) => ChatRoomScreen( scope: state.pathParameters['scope']!, alias: state.pathParameters['alias']!, + extra: state.extra as ChatRoomScreenExtra?, ), ), GoRoute( diff --git a/lib/screens/chat/room.dart b/lib/screens/chat/room.dart index c9f8f3a..39fa609 100644 --- a/lib/screens/chat/room.dart +++ b/lib/screens/chat/room.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:developer'; import 'package:dio/dio.dart'; import 'package:easy_localization/easy_localization.dart'; @@ -9,9 +10,12 @@ import 'package:material_symbols_icons/symbols.dart'; import 'package:provider/provider.dart'; import 'package:styled_widget/styled_widget.dart'; import 'package:surface/controllers/chat_message_controller.dart'; +import 'package:surface/controllers/post_write_controller.dart'; import 'package:surface/providers/channel.dart'; import 'package:surface/providers/chat_call.dart'; import 'package:surface/providers/sn_network.dart'; +import 'package:surface/providers/user_directory.dart'; +import 'package:surface/providers/userinfo.dart'; import 'package:surface/providers/websocket.dart'; import 'package:surface/types/chat.dart'; import 'package:surface/widgets/chat/call/call_prejoin.dart'; @@ -23,14 +27,19 @@ import 'package:surface/widgets/loading_indicator.dart'; import 'package:surface/widgets/navigation/app_scaffold.dart'; import 'package:very_good_infinite_list/very_good_infinite_list.dart'; -import '../../providers/user_directory.dart'; -import '../../providers/userinfo.dart'; +class ChatRoomScreenExtra { + final String? initialText; + final List? initialAttachments; + + ChatRoomScreenExtra({this.initialText, this.initialAttachments}); +} class ChatRoomScreen extends StatefulWidget { final String scope; final String alias; + final ChatRoomScreenExtra? extra; - const ChatRoomScreen({super.key, required this.scope, required this.alias}); + const ChatRoomScreen({super.key, required this.scope, required this.alias, this.extra}); @override State createState() => _ChatRoomScreenState(); @@ -177,8 +186,23 @@ class _ChatRoomScreenState extends State { _messageController = ChatMessageController(context); _fetchChannel().then((_) async { await _messageController.initialize(_channel!); - await _messageController.checkUpdate(); - await _fetchOngoingCall(); + + if (widget.extra != null) { + WidgetsBinding.instance.addPostFrameCallback((_) { + log('[ChatInput] Setting initial text and attachments...'); + if (widget.extra!.initialText != null) { + _inputGlobalKey.currentState?.setInitialText(widget.extra!.initialText!); + } + if (widget.extra!.initialAttachments != null) { + _inputGlobalKey.currentState?.setInitialAttachments(widget.extra!.initialAttachments!); + } + }); + } + + await Future.wait([ + _messageController.checkUpdate(), + _fetchOngoingCall(), + ]); }); final ws = context.read(); diff --git a/lib/screens/post/post_editor.dart b/lib/screens/post/post_editor.dart index 12cc4ff..6ade9fc 100644 --- a/lib/screens/post/post_editor.dart +++ b/lib/screens/post/post_editor.dart @@ -20,13 +20,13 @@ import 'package:surface/widgets/post/post_meta_editor.dart'; import 'package:surface/widgets/dialog.dart'; import 'package:provider/provider.dart'; -class PostEditorExtraProps { +class PostEditorExtra { final String? text; final String? title; final String? description; final List? attachments; - const PostEditorExtraProps({ + const PostEditorExtra({ this.text, this.title, this.description, @@ -39,7 +39,7 @@ class PostEditorScreen extends StatefulWidget { final int? postEditId; final int? postReplyId; final int? postRepostId; - final PostEditorExtraProps? extraProps; + final PostEditorExtra? extraProps; const PostEditorScreen({ super.key, diff --git a/lib/screens/sharing.dart b/lib/screens/sharing.dart index 7c9c217..f78e242 100644 --- a/lib/screens/sharing.dart +++ b/lib/screens/sharing.dart @@ -8,9 +8,20 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:gap/gap.dart'; import 'package:go_router/go_router.dart'; +import 'package:material_symbols_icons/material_symbols_icons.dart'; +import 'package:provider/provider.dart'; import 'package:receive_sharing_intent/receive_sharing_intent.dart'; +import 'package:styled_widget/styled_widget.dart'; import 'package:surface/controllers/post_write_controller.dart'; +import 'package:surface/providers/channel.dart'; +import 'package:surface/providers/user_directory.dart'; +import 'package:surface/providers/userinfo.dart'; +import 'package:surface/screens/chat/room.dart'; import 'package:surface/screens/post/post_editor.dart'; +import 'package:surface/types/chat.dart'; +import 'package:surface/widgets/account/account_image.dart'; +import 'package:surface/widgets/dialog.dart'; +import 'package:surface/widgets/loading_indicator.dart'; class AppSharingListener extends StatefulWidget { final Widget child; @@ -51,20 +62,39 @@ class _AppSharingListenerState extends State { pathParameters: { 'mode': 'stories', }, - extra: PostEditorExtraProps( + extra: PostEditorExtra( text: value .where((e) => [SharedMediaType.text, SharedMediaType.url].contains(e.type)) - .map((e) => e.path).join('\n'), + .map((e) => e.path) + .join('\n'), attachments: value - .where((e) => [SharedMediaType.video, SharedMediaType.file, SharedMediaType.image].contains(e.type)) - .map((e) => PostWriteMedia.fromFile(XFile(e.path))).toList(), + .where((e) => [SharedMediaType.video, SharedMediaType.file, SharedMediaType.image] + .contains(e.type)) + .map((e) => PostWriteMedia.fromFile(XFile(e.path))) + .toList(), ), ); Navigator.pop(context); }, ), + ListTile( + contentPadding: const EdgeInsets.symmetric(horizontal: 24), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + leading: Icon(Icons.chat_outlined), + trailing: const Icon(Icons.chevron_right), + title: Text('shareIntentSendChannel').tr(), + onTap: () { + showModalBottomSheet( + context: context, + builder: (context) => _ShareIntentChannelSelect(value: value), + ).then((val) { + if (!context.mounted) return; + if (val == true) Navigator.pop(context); + }); + }, + ), ], - ), + ).width(280), ) ], ), @@ -103,7 +133,7 @@ class _AppSharingListenerState extends State { @override void initState() { super.initState(); - if(!kIsWeb && (Platform.isAndroid || Platform.isIOS)) { + if (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) { _initialize(); _initialHandle(); } @@ -120,3 +150,193 @@ class _AppSharingListenerState extends State { return widget.child; } } + +class _ShareIntentChannelSelect extends StatefulWidget { + final Iterable value; + + const _ShareIntentChannelSelect({required this.value}); + + @override + State<_ShareIntentChannelSelect> createState() => _ShareIntentChannelSelectState(); +} + +class _ShareIntentChannelSelectState extends State<_ShareIntentChannelSelect> { + bool _isBusy = true; + + List? _channels; + Map? _lastMessages; + + void _refreshChannels() { + final ua = context.read(); + if (!ua.isAuthorized) { + setState(() => _isBusy = false); + return; + } + + final chan = context.read(); + chan.fetchChannels().listen((channels) async { + final lastMessages = await chan.getLastMessages(channels); + _lastMessages = {for (final val in lastMessages) val.channelId: val}; + channels.sort((a, b) { + if (_lastMessages!.containsKey(a.id) && _lastMessages!.containsKey(b.id)) { + return _lastMessages![b.id]!.createdAt.compareTo(_lastMessages![a.id]!.createdAt); + } + if (_lastMessages!.containsKey(a.id)) return -1; + if (_lastMessages!.containsKey(b.id)) return 1; + return 0; + }); + + if (!mounted) return; + final ud = context.read(); + for (final channel in channels) { + if (channel.type == 1) { + await ud.listAccount( + channel.members + ?.cast() + .map((ele) => ele?.accountId) + .where((ele) => ele != null) + .toSet() ?? + {}, + ); + } + } + + if (mounted) setState(() => _channels = channels); + }) + ..onError((err) { + if (!mounted) return; + context.showErrorDialog(err); + setState(() => _isBusy = false); + }) + ..onDone(() { + if (!mounted) return; + setState(() => _isBusy = false); + }); + } + + @override + void initState() { + super.initState(); + _refreshChannels(); + } + + @override + Widget build(BuildContext context) { + final ua = context.read(); + final ud = context.read(); + + return Column( + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Icon(Symbols.chat, size: 24), + const Gap(16), + Text('shareIntentSendChannel', style: Theme.of(context).textTheme.titleLarge).tr(), + ], + ).padding(horizontal: 20, top: 16, bottom: 12), + LoadingIndicator(isActive: _isBusy), + Expanded( + child: MediaQuery.removePadding( + context: context, + removeTop: true, + child: RefreshIndicator( + onRefresh: () => Future.sync(() => _refreshChannels()), + child: ListView.builder( + itemCount: _channels?.length ?? 0, + itemBuilder: (context, idx) { + final channel = _channels![idx]; + final lastMessage = _lastMessages?[channel.id]; + + if (channel.type == 1) { + final otherMember = channel.members?.cast().firstWhere( + (ele) => ele?.accountId != ua.user?.id, + orElse: () => null, + ); + + return ListTile( + title: Text(ud.getAccountFromCache(otherMember?.accountId)?.nick ?? channel.name), + subtitle: lastMessage != null + ? Text( + '${ud.getAccountFromCache(lastMessage.sender.accountId)?.nick}: ${lastMessage.body['text'] ?? 'Unable preview'}', + maxLines: 1, + overflow: TextOverflow.ellipsis, + ) + : Text( + 'channelDirectMessageDescription'.tr(args: [ + '@${ud.getAccountFromCache(otherMember?.accountId)?.name}', + ]), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + contentPadding: const EdgeInsets.symmetric(horizontal: 16), + leading: AccountImage( + content: ud.getAccountFromCache(otherMember?.accountId)?.avatar, + ), + onTap: () { + GoRouter.of(context).pushNamed( + 'chatRoom', + pathParameters: { + 'scope': channel.realm?.alias ?? 'global', + 'alias': channel.alias, + }, + ).then((value) { + if (mounted) _refreshChannels(); + }); + }, + ); + } + + return ListTile( + title: Text(channel.name), + subtitle: lastMessage != null + ? Text( + '${ud.getAccountFromCache(lastMessage.sender.accountId)?.nick}: ${lastMessage.body['text'] ?? 'Unable preview'}', + maxLines: 1, + overflow: TextOverflow.ellipsis, + ) + : Text( + channel.description, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + contentPadding: const EdgeInsets.symmetric(horizontal: 16), + leading: AccountImage( + content: null, + fallbackWidget: const Icon(Symbols.chat, size: 20), + ), + onTap: () { + Navigator.pop(context, true); + GoRouter.of(context) + .pushNamed( + 'chatRoom', + pathParameters: { + 'scope': channel.realm?.alias ?? 'global', + 'alias': channel.alias, + }, + extra: ChatRoomScreenExtra( + initialText: widget.value + .where((e) => [SharedMediaType.text, SharedMediaType.url].contains(e.type)) + .map((e) => e.path) + .join('\n'), + initialAttachments: widget.value + .where((e) => + [SharedMediaType.video, SharedMediaType.file, SharedMediaType.image].contains(e.type)) + .map((e) => PostWriteMedia.fromFile(XFile(e.path))) + .toList(), + ), + ) + .then((value) { + if (value == true) _refreshChannels(); + }); + }, + ); + }, + ), + ), + ), + ), + ], + ); + } +} diff --git a/lib/widgets/chat/chat_message_input.dart b/lib/widgets/chat/chat_message_input.dart index e56c76b..2ff5412 100644 --- a/lib/widgets/chat/chat_message_input.dart +++ b/lib/widgets/chat/chat_message_input.dart @@ -46,6 +46,16 @@ class ChatMessageInputState extends State { setState(() => _replyingMessage = value); } + void setInitialText(String? value) { + _contentController.text = value ?? ''; + setState(() {}); + } + + void setInitialAttachments(List? value) { + _attachments.addAll(value ?? []); + setState(() {}); + } + void setEdit(SnChatMessage? value) { _contentController.text = value?.body['text'] ?? ''; _attachments.clear(); diff --git a/lib/widgets/notify_indicator.dart b/lib/widgets/notify_indicator.dart index 9299580..dfbc191 100644 --- a/lib/widgets/notify_indicator.dart +++ b/lib/widgets/notify_indicator.dart @@ -1,5 +1,3 @@ -import 'dart:math' show min; - import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_animate/flutter_animate.dart'; @@ -8,7 +6,6 @@ import 'package:material_symbols_icons/material_symbols_icons.dart'; import 'package:provider/provider.dart'; import 'package:responsive_framework/responsive_framework.dart'; import 'package:styled_widget/styled_widget.dart'; -import 'package:surface/providers/config.dart'; import 'package:surface/providers/notification.dart'; import 'package:surface/providers/sn_network.dart'; import 'package:surface/providers/userinfo.dart';