From c623315908e2f33536ae298c6a9158fb63a25cd4 Mon Sep 17 00:00:00 2001 From: torvalds Date: Fri, 3 Jul 2026 23:21:25 +0700 Subject: [PATCH] =?UTF-8?q?refactor:=20=D0=BF=D0=B5=D1=80=D0=B5=D0=B4?= =?UTF-8?q?=D0=B5=D0=BB=D0=B0=D0=BB=20=D0=BF=D0=B5=D1=80=D0=B5=D1=81=D1=81?= =?UTF-8?q?=D1=8B=D0=BB=D0=BA=D1=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/backend/modules/messages.dart | 77 ++++++ .../screens/chats/chat_list_screen.dart | 63 ++++- lib/frontend/screens/chats/chat_screen.dart | 212 ++++++++++++++-- .../screens/chats/forward_picker_screen.dart | 239 ------------------ lib/frontend/widgets/sticker_pack_sheet.dart | 10 +- 5 files changed, 323 insertions(+), 278 deletions(-) delete mode 100644 lib/frontend/screens/chats/forward_picker_screen.dart diff --git a/lib/backend/modules/messages.dart b/lib/backend/modules/messages.dart index 93cce58..773dd33 100644 --- a/lib/backend/modules/messages.dart +++ b/lib/backend/modules/messages.dart @@ -864,6 +864,83 @@ class MessagesModule { return ''; } + static CachedMessage buildForwardMessage({ + required int myId, + required int targetChatId, + required int sourceChatId, + required CachedMessage source, + required String tempId, + required int time, + required String status, + }) { + final srcPayload = source.payload; + final srcLink = srcPayload?['link']; + Map originalMsg; + if (srcLink is Map && + srcLink['type'] == 'FORWARD' && + srcLink['message'] is Map) { + originalMsg = Map.from(srcLink['message'] as Map); + } else { + originalMsg = { + 'id': int.tryParse(source.id) ?? source.id, + 'sender': source.senderId, + 'time': source.time, + 'text': source.text, + 'attaches': (srcPayload?['attaches'] as List?) ?? const [], + }; + } + final payload = { + 'elements': const [], + 'attaches': const [], + 'link': { + 'type': 'FORWARD', + 'chatId': sourceChatId, + 'messageId': int.tryParse(source.id) ?? source.id, + 'message': originalMsg, + }, + }; + return CachedMessage( + id: tempId, + accountId: myId, + chatId: targetChatId, + senderId: myId, + text: null, + time: time, + status: status, + payload: payload, + attachments: [ForwardedMessageAttachment.fromMap(payload)], + ); + } + + static CachedMessage reidentifyMessage( + CachedMessage message, + String newId, { + String? status, + }) => CachedMessage( + id: newId, + accountId: message.accountId, + chatId: message.chatId, + senderId: message.senderId, + text: message.text, + time: message.time, + status: status ?? message.status, + payload: message.payload, + attachments: message.attachments, + isControl: message.isControl, + deleted: message.deleted, + editHistory: message.editHistory, + ); + + static String forwardPreviewText(CachedMessage message) { + final link = message.payload?['link']; + if (link is Map && link['message'] is Map) { + final original = link['message'] as Map; + final text = original['text']; + if (text is String && text.trim().isNotEmpty) return text; + } + return 'Пересланное сообщение'; + } + Future sendLinkMessage(int chatId, String url) async { final message = { 'text': url, diff --git a/lib/frontend/screens/chats/chat_list_screen.dart b/lib/frontend/screens/chats/chat_list_screen.dart index a755b87..84651cd 100644 --- a/lib/frontend/screens/chats/chat_list_screen.dart +++ b/lib/frontend/screens/chats/chat_list_screen.dart @@ -80,10 +80,41 @@ class _StoriesScrollPhysics extends BouncingScrollPhysics { } } +class ForwardTarget { + final int chatId; + final String name; + final String imageUrl; + final String chatType; + + const ForwardTarget({ + required this.chatId, + required this.name, + required this.imageUrl, + required this.chatType, + }); +} + +Future openForwardScreen({ + required BuildContext context, + int messageCount = 1, +}) { + return pushSwipeable( + context, + (_) => ChatListScreen(forwardMode: true, forwardMessageCount: messageCount), + ); +} + class ChatListScreen extends StatefulWidget { final ValueChanged? onChatSelected; + final bool forwardMode; + final int forwardMessageCount; - const ChatListScreen({super.key, this.onChatSelected}); + const ChatListScreen({ + super.key, + this.onChatSelected, + this.forwardMode = false, + this.forwardMessageCount = 1, + }); @override State createState() => _ChatListScreenState(); @@ -1312,10 +1343,12 @@ class _ChatListScreenState extends State padding: const EdgeInsets.fromLTRB(20, 3, 20, 8), child: GestureDetector( behavior: HitTestBehavior.opaque, - onTap: () => pushSwipeable( - context, - (_) => const SearchScreen(), - ), + onTap: widget.forwardMode + ? null + : () => pushSwipeable( + context, + (_) => const SearchScreen(), + ), child: GlossyPill( color: cs.surfaceContainerHighest, borderRadius: BorderRadius.circular(50), @@ -1335,7 +1368,9 @@ class _ChatListScreenState extends State ), const SizedBox(width: 10), Text( - 'Поиск', + widget.forwardMode + ? 'Пересылка...' + : 'Поиск', style: TextStyle( color: cs.outline, fontSize: 15, @@ -1791,6 +1826,9 @@ class _ChatListScreenState extends State bottom: false, child: LayoutBuilder( builder: (context, constraints) { + if (widget.forwardMode) { + return _getChatsBody(); + } final bottomInset = MediaQuery.viewPaddingOf(context).bottom; final pageW = constraints.maxWidth; final pageH = constraints.maxHeight; @@ -2326,6 +2364,17 @@ class _ChatListScreenState extends State return InkWell( key: ValueKey('chat_$id'), onTap: () { + if (widget.forwardMode) { + Navigator.of(context).pop( + ForwardTarget( + chatId: int.parse(id), + name: name, + imageUrl: imageUrl, + chatType: chatType, + ), + ); + return; + } if (_isSelectionMode) { _toggleSelection(id); return; @@ -2363,7 +2412,7 @@ class _ChatListScreenState extends State ); } }, - onLongPress: () => _toggleSelection(id), + onLongPress: widget.forwardMode ? null : () => _toggleSelection(id), child: AnimatedContainer( duration: const Duration(milliseconds: 200), color: isSelected diff --git a/lib/frontend/screens/chats/chat_screen.dart b/lib/frontend/screens/chats/chat_screen.dart index 691af24..8ea92a9 100644 --- a/lib/frontend/screens/chats/chat_screen.dart +++ b/lib/frontend/screens/chats/chat_screen.dart @@ -21,7 +21,7 @@ import 'package:komet/core/utils/format.dart'; import 'package:komet/core/utils/logger.dart'; import 'package:komet/frontend/screens/chats/chat_info_screen.dart'; import 'package:komet/frontend/screens/contacts/contact_profile_screen.dart'; -import 'package:komet/frontend/screens/chats/forward_picker_screen.dart'; +import 'package:komet/frontend/screens/chats/chat_list_screen.dart'; import 'package:komet/frontend/screens/chats/poll_create_screen.dart'; import 'package:komet/frontend/widgets/custom_notification.dart'; import 'package:komet/frontend/widgets/chat_menu_overlay.dart'; @@ -70,6 +70,7 @@ import '../../widgets/attachment/attachment_sheet.dart'; import '../../widgets/sticker_panel.dart'; import '../../widgets/sticker_pack_sheet.dart'; import '../../widgets/swipe_to_pop.dart'; +import '../../widgets/swipe_route.dart'; import '../../widgets/schedule_time_picker.dart'; import '../../widgets/chat_wallpaper_sheet.dart'; import '../../widgets/chat_wallpaper_view.dart'; @@ -286,6 +287,16 @@ class _MeasureSizeState extends State<_MeasureSize> { } } +class ForwardRequest { + final int sourceChatId; + final List optimistic; + + const ForwardRequest({ + required this.sourceChatId, + required this.optimistic, + }); +} + class ChatScreen extends StatefulWidget { final int chatId; final String name; @@ -293,6 +304,7 @@ class ChatScreen extends StatefulWidget { final String chatType; final bool embedded; final VoidCallback? onClose; + final ForwardRequest? forwardRequest; const ChatScreen({ super.key, @@ -302,6 +314,7 @@ class ChatScreen extends StatefulWidget { required this.chatType, this.embedded = false, this.onClose, + this.forwardRequest, }); @override @@ -428,6 +441,7 @@ class _ChatScreenState extends State Timer? _shimmerStartTimer; bool _historyKickedOff = false; bool _previewChat = false; + bool _forwardRequestDone = false; List _messages = []; final ValueNotifier _messagesRev = ValueNotifier(0); final Set _deletingIds = {}; @@ -536,7 +550,11 @@ class _ChatScreenState extends State reverseCurve: Curves.easeIn, ); - unawaited(_fastPreloadCache()); + unawaited( + _fastPreloadCache().then((_) { + if (mounted) unawaited(_runForwardRequest()); + }), + ); unawaited(_loadParticipantsCount()); WidgetsBinding.instance.addPostFrameCallback(_onFirstFrameRendered); } @@ -1461,45 +1479,191 @@ class _ChatScreenState extends State } Future _forwardMessages(List msgs) async { - final forwardable = - msgs.where((m) => !m.id.startsWith('temp_')).toList(); + final forwardable = msgs.where((m) => !m.id.startsWith('temp_')).toList(); if (forwardable.isEmpty) { showCustomNotification(context, 'Нечего пересылать'); return; } - final target = await showForwardPicker( + final target = await openForwardScreen( context: context, - accountId: _myId, messageCount: forwardable.length, ); if (target == null || !mounted) return; - final ordered = [...forwardable]..sort((a, b) => a.time.compareTo(b.time)); - var ok = 0; - for (final m in ordered) { - final mid = int.tryParse(m.id); - if (mid == null) continue; - try { - await messagesModule.forwardMessage(target.chatId, widget.chatId, mid); - ok++; - } catch (_) {} - } - if (!mounted) return; - if (ok == 0) { - Haptics.error(); - showCustomNotification(context, 'Не удалось переслать'); + if (api.state != SessionState.online) { + showCustomNotification(context, 'Нет соединения'); return; } + + final ordered = [...forwardable]..sort((a, b) => a.time.compareTo(b.time)); + + if (target.chatId == widget.chatId) { + await _forwardIntoCurrentChat(ordered); + return; + } + + final optimistic = await _seedForwardsToChat(target, ordered); + if (!mounted) return; Haptics.send(); - showCustomNotification( + pushSwipeable( context, - target.chatId == widget.chatId - ? 'Переслано' - : 'Переслано в «${target.name}»', + (_) => ChatScreen( + chatId: target.chatId, + name: target.name, + imageUrl: target.imageUrl, + chatType: target.chatType, + forwardRequest: ForwardRequest( + sourceChatId: widget.chatId, + optimistic: optimistic, + ), + ), ); } + Future _forwardIntoCurrentChat(List sources) async { + final now = DateTime.now().millisecondsSinceEpoch; + final optimistic = []; + var i = 0; + for (final src in sources) { + final msg = MessagesModule.buildForwardMessage( + myId: _myId, + targetChatId: widget.chatId, + sourceChatId: widget.chatId, + source: src, + tempId: _nextTempId(), + time: now + i, + status: 'sending', + ); + optimistic.add(msg); + _messages.add(msg); + unawaited(_persistOutgoing(msg)); + i++; + } + _bumpMessages(); + Haptics.send(); + _scrollToBottom(); + if (optimistic.isNotEmpty) { + final last = optimistic.last; + unawaited( + ChatsModule.applyOutgoing( + _myId, + widget.chatId, + messageId: last.id, + time: last.time, + text: MessagesModule.forwardPreviewText(last), + status: 'sending', + ), + ); + } + for (final opt in optimistic) { + await _sendOneForward(opt, widget.chatId); + } + } + + Future> _seedForwardsToChat( + ForwardTarget target, + List sources, + ) async { + await ChatsModule.ensureChatCached(api, _myId, target.chatId); + final now = DateTime.now().millisecondsSinceEpoch; + final optimistic = []; + var i = 0; + for (final src in sources) { + final msg = MessagesModule.buildForwardMessage( + myId: _myId, + targetChatId: target.chatId, + sourceChatId: widget.chatId, + source: src, + tempId: _nextTempId(), + time: now + i, + status: 'sending', + ); + optimistic.add(msg); + await AppDatabase.saveMessages([msg.toDbRow()]); + i++; + } + final cached = MessageSessionCache.get(_myId, target.chatId); + if (cached != null) { + MessageSessionCache.save( + _myId, + target.chatId, + [...cached.messages, ...optimistic], + reachedStart: cached.reachedStart, + ); + } + if (optimistic.isNotEmpty) { + final last = optimistic.last; + unawaited( + ChatsModule.applyOutgoing( + _myId, + target.chatId, + messageId: last.id, + time: last.time, + text: MessagesModule.forwardPreviewText(last), + status: 'sending', + ), + ); + } + return optimistic; + } + + Future _runForwardRequest() async { + final req = widget.forwardRequest; + if (req == null || _forwardRequestDone) return; + _forwardRequestDone = true; + for (final opt in req.optimistic) { + await _sendOneForward(opt, req.sourceChatId); + } + } + + Future _sendOneForward(CachedMessage optimistic, int sourceChatId) async { + final link = optimistic.payload?['link']; + final rawWireId = link is Map ? link['messageId'] : null; + final wireId = rawWireId is int ? rawWireId : null; + if (wireId == null) return; + try { + final realId = await messagesModule.forwardMessage( + widget.chatId, + sourceChatId, + wireId, + ); + if (!mounted) return; + final sent = MessagesModule.reidentifyMessage( + optimistic, + realId.isNotEmpty ? realId : optimistic.id, + status: 'sent', + ); + final index = _messages.indexWhere((m) => m.id == optimistic.id); + if (index != -1) { + _messages[index] = sent; + _bumpMessages(); + } + unawaited(_persistOutgoing(sent, removeId: optimistic.id)); + unawaited( + ChatsModule.applyOutgoing( + _myId, + widget.chatId, + messageId: sent.id, + time: sent.time, + text: MessagesModule.forwardPreviewText(sent), + status: 'sent', + ), + ); + } catch (_) { + final index = _messages.indexWhere((m) => m.id == optimistic.id); + if (index != -1 && mounted) { + _messages.removeAt(index); + _bumpMessages(); + } + unawaited(AppDatabase.deleteMessage(_myId, widget.chatId, optimistic.id)); + if (mounted) { + Haptics.error(); + showCustomNotification(context, 'Не удалось переслать'); + } + } + } + Widget _buildComposerArea(BuildContext context) { final cs = Theme.of(context).colorScheme; final content = Column( diff --git a/lib/frontend/screens/chats/forward_picker_screen.dart b/lib/frontend/screens/chats/forward_picker_screen.dart deleted file mode 100644 index 793c2a5..0000000 --- a/lib/frontend/screens/chats/forward_picker_screen.dart +++ /dev/null @@ -1,239 +0,0 @@ -import 'package:cached_network_image/cached_network_image.dart'; -import 'package:flutter/material.dart'; -import 'package:material_symbols_icons/symbols.dart'; - -import 'package:komet/backend/modules/chats.dart'; -import 'package:komet/backend/modules/messages.dart' show ContactCache; - -class ForwardTarget { - final int chatId; - final String name; - final String imageUrl; - final String chatType; - - const ForwardTarget({ - required this.chatId, - required this.name, - required this.imageUrl, - required this.chatType, - }); -} - -Future showForwardPicker({ - required BuildContext context, - required int accountId, - int messageCount = 1, -}) { - return showModalBottomSheet( - context: context, - isScrollControlled: true, - useSafeArea: true, - backgroundColor: Colors.transparent, - builder: (_) => - _ForwardPickerSheet(accountId: accountId, messageCount: messageCount), - ); -} - -class _ForwardPickerSheet extends StatefulWidget { - final int accountId; - final int messageCount; - - const _ForwardPickerSheet({ - required this.accountId, - required this.messageCount, - }); - - @override - State<_ForwardPickerSheet> createState() => _ForwardPickerSheetState(); -} - -class _ForwardPickerSheetState extends State<_ForwardPickerSheet> { - final TextEditingController _searchController = TextEditingController(); - List _all = const []; - String _query = ''; - bool _loading = true; - - @override - void initState() { - super.initState(); - _searchController.addListener(() { - setState(() => _query = _searchController.text.trim().toLowerCase()); - }); - _load(); - } - - @override - void dispose() { - _searchController.dispose(); - super.dispose(); - } - - Future _load() async { - final chats = await ChatsModule.getChats(widget.accountId); - final targets = []; - for (final chat in chats) { - if (chat.type == 'CHANNEL' && chat.owner != widget.accountId) continue; - targets.add(_targetFor(chat)); - } - targets.sort((a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase())); - if (!mounted) return; - setState(() { - _all = targets; - _loading = false; - }); - } - - ForwardTarget _targetFor(CachedChat chat) { - if (chat.id == 0) { - return ForwardTarget( - chatId: 0, - name: 'Избранное', - imageUrl: '', - chatType: chat.type, - ); - } - if (chat.type == 'DIALOG') { - int otherId = widget.accountId; - for (final entry in chat.participants.entries) { - if (entry.key != widget.accountId) { - otherId = entry.key; - break; - } - } - return ForwardTarget( - chatId: chat.id, - name: ContactCache.get(otherId) ?? chat.title ?? 'Пользователь', - imageUrl: ContactCache.getAvatar(otherId) ?? chat.iconUrl ?? '', - chatType: chat.type, - ); - } - return ForwardTarget( - chatId: chat.id, - name: chat.title ?? 'Чат', - imageUrl: chat.iconUrl ?? '', - chatType: chat.type, - ); - } - - @override - Widget build(BuildContext context) { - final cs = Theme.of(context).colorScheme; - final filtered = _query.isEmpty - ? _all - : _all.where((t) => t.name.toLowerCase().contains(_query)).toList(); - - return Padding( - padding: EdgeInsets.only(bottom: MediaQuery.viewInsetsOf(context).bottom), - child: DraggableScrollableSheet( - initialChildSize: 0.6, - minChildSize: 0.4, - maxChildSize: 0.92, - expand: false, - builder: (context, scrollController) { - return Container( - decoration: BoxDecoration( - color: cs.surface, - borderRadius: const BorderRadius.vertical( - top: Radius.circular(20), - ), - ), - clipBehavior: Clip.antiAlias, - child: Column( - children: [ - const SizedBox(height: 10), - Container( - width: 40, - height: 4, - decoration: BoxDecoration( - color: cs.onSurfaceVariant.withValues(alpha: 0.4), - borderRadius: BorderRadius.circular(2), - ), - ), - Padding( - padding: const EdgeInsets.fromLTRB(20, 12, 20, 4), - child: Row( - children: [ - Text( - widget.messageCount > 1 - ? 'Переслать (${widget.messageCount})' - : 'Переслать', - style: TextStyle( - color: cs.onSurface, - fontSize: 18, - fontWeight: FontWeight.w600, - ), - ), - ], - ), - ), - Padding( - padding: const EdgeInsets.fromLTRB(16, 8, 16, 8), - child: TextField( - controller: _searchController, - decoration: InputDecoration( - hintText: 'Поиск чата', - prefixIcon: const Icon(Symbols.search), - filled: true, - fillColor: cs.surfaceContainerHighest, - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(14), - borderSide: BorderSide.none, - ), - contentPadding: EdgeInsets.zero, - ), - ), - ), - Expanded( - child: _loading - ? const Center(child: CircularProgressIndicator()) - : filtered.isEmpty - ? Center( - child: Text( - 'Ничего не найдено', - style: TextStyle(color: cs.onSurfaceVariant), - ), - ) - : ListView.builder( - controller: scrollController, - padding: const EdgeInsets.only(bottom: 8), - itemCount: filtered.length, - itemBuilder: (context, index) { - final t = filtered[index]; - return ListTile( - leading: CircleAvatar( - radius: 22, - backgroundColor: cs.surfaceContainerHighest, - backgroundImage: t.imageUrl.isNotEmpty - ? CachedNetworkImageProvider( - t.imageUrl, - maxWidth: 132, - maxHeight: 132, - ) - : null, - child: t.imageUrl.isEmpty - ? Icon( - t.chatId == 0 - ? Symbols.bookmark - : Symbols.person, - color: cs.onSurfaceVariant, - ) - : null, - ), - title: Text( - t.name, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - onTap: () => Navigator.of(context).pop(t), - ); - }, - ), - ), - ], - ), - ); - }, - ), - ); - } -} diff --git a/lib/frontend/widgets/sticker_pack_sheet.dart b/lib/frontend/widgets/sticker_pack_sheet.dart index a1e8067..fbfbff5 100644 --- a/lib/frontend/widgets/sticker_pack_sheet.dart +++ b/lib/frontend/widgets/sticker_pack_sheet.dart @@ -2,10 +2,9 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:material_symbols_icons/symbols.dart'; -import '../../core/storage/app_database.dart'; import '../../main.dart' show stickersModule, messagesModule; import '../../models/sticker.dart'; -import '../screens/chats/forward_picker_screen.dart'; +import '../screens/chats/chat_list_screen.dart'; import 'custom_notification.dart'; import 'sticker_image.dart'; import 'sticker_peek.dart'; @@ -123,12 +122,7 @@ class _StickerPackSheetState extends State<_StickerPackSheet> { showCustomNotification(context, 'Ссылка недоступна'); return; } - final profile = await AppDatabase.loadActiveProfile(); - if (!mounted) return; - final target = await showForwardPicker( - context: context, - accountId: profile?.id ?? 0, - ); + final target = await openForwardScreen(context: context); if (target == null || !mounted) return; final ok = await messagesModule.sendLinkMessage(target.chatId, link); if (!mounted) return;