feat: панель перессылки

This commit is contained in:
Jganenokk
2026-07-28 19:42:51 +07:00
parent 8dccbda24b
commit f4f9e22be8
3 changed files with 384 additions and 148 deletions
@@ -28,6 +28,7 @@ class ComposerInputBar extends StatelessWidget {
this.backdropKey, this.backdropKey,
required this.attachAnim, required this.attachAnim,
required this.replyTo, required this.replyTo,
required this.forwardMessages,
required this.myId, required this.myId,
required this.hasText, required this.hasText,
required this.uploadStatus, required this.uploadStatus,
@@ -42,6 +43,7 @@ class ComposerInputBar extends StatelessWidget {
required this.onOpenAttachScheduled, required this.onOpenAttachScheduled,
required this.onSendHistory, required this.onSendHistory,
required this.onCancelReply, required this.onCancelReply,
required this.onCancelForward,
this.onPickReplyChat, this.onPickReplyChat,
required this.formatElapsed, required this.formatElapsed,
required this.contextMenuBuilder, required this.contextMenuBuilder,
@@ -64,6 +66,7 @@ class ComposerInputBar extends StatelessWidget {
final BackdropKey? backdropKey; final BackdropKey? backdropKey;
final Animation<double> attachAnim; final Animation<double> attachAnim;
final ValueListenable<CachedMessage?> replyTo; final ValueListenable<CachedMessage?> replyTo;
final ValueListenable<List<CachedMessage>> forwardMessages;
final int myId; final int myId;
final ValueListenable<bool> hasText; final ValueListenable<bool> hasText;
final ValueListenable<UploadStatus> uploadStatus; final ValueListenable<UploadStatus> uploadStatus;
@@ -78,6 +81,7 @@ class ComposerInputBar extends StatelessWidget {
final VoidCallback onOpenAttachScheduled; final VoidCallback onOpenAttachScheduled;
final Future<void> Function(FileHistoryEntry entry) onSendHistory; final Future<void> Function(FileHistoryEntry entry) onSendHistory;
final VoidCallback onCancelReply; final VoidCallback onCancelReply;
final VoidCallback onCancelForward;
final VoidCallback? onPickReplyChat; final VoidCallback? onPickReplyChat;
final String Function(int ms) formatElapsed; final String Function(int ms) formatElapsed;
final Widget Function(BuildContext, EditableTextState) contextMenuBuilder; final Widget Function(BuildContext, EditableTextState) contextMenuBuilder;
@@ -94,10 +98,18 @@ class ComposerInputBar extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ValueListenableBuilder<List<CachedMessage>>(
valueListenable: forwardMessages,
builder: (context, forwards, _) => _build(context, forwards),
);
}
Widget _build(BuildContext context, List<CachedMessage> forwards) {
final cs = Theme.of(context).colorScheme; final cs = Theme.of(context).colorScheme;
final mutedIcon = cs.onSurfaceVariant.withValues(alpha: 0.85); final mutedIcon = cs.onSurfaceVariant.withValues(alpha: 0.85);
final hasForward = forwards.isNotEmpty;
if (chatType == "CHANNEL") { if (chatType == "CHANNEL" && !hasForward) {
if (!channelSubscribed) { if (!channelSubscribed) {
return SafeArea( return SafeArea(
child: Padding( child: Padding(
@@ -180,7 +192,7 @@ class ComposerInputBar extends StatelessWidget {
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
_replyPreview(cs), _messagePreview(cs, forwards),
Padding( Padding(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 12.0, horizontal: 12.0,
@@ -242,7 +254,11 @@ class ComposerInputBar extends StatelessWidget {
!HardwareKeyboard !HardwareKeyboard
.instance .instance
.isShiftPressed) { .isShiftPressed) {
if (hasText.value) onSendText(); if (hasText.value ||
hasForward ||
forceSend) {
onSendText();
}
return KeyEventResult.handled; return KeyEventResult.handled;
} }
return KeyEventResult.ignored; return KeyEventResult.ignored;
@@ -383,6 +399,7 @@ class ComposerInputBar extends StatelessWidget {
builder: (context, videoMode, _) { builder: (context, videoMode, _) {
final sendMode = final sendMode =
hasText || hasText ||
hasForward ||
locked || locked ||
forceSend; forceSend;
final pill = _actionSurface( final pill = _actionSurface(
@@ -395,7 +412,10 @@ class ComposerInputBar extends StatelessWidget {
: _frost : _frost
? AppFrost.inputTint(cs) ? AppFrost.inputTint(cs)
: cs.surfaceContainerHighest, : cs.surfaceContainerHighest,
onTap: (hasText || forceSend) onTap:
(hasText ||
hasForward ||
forceSend)
? onSendText ? onSendText
: locked : locked
? () => voiceRec.stop( ? () => voiceRec.stop(
@@ -403,7 +423,9 @@ class ComposerInputBar extends StatelessWidget {
) )
: null, : null,
onLongPress: onLongPress:
(hasText && !forceSend) (hasText &&
!forceSend &&
!hasForward)
? onScheduleMessage ? onScheduleMessage
: null, : null,
child: SizedBox( child: SizedBox(
@@ -567,6 +589,81 @@ class ComposerInputBar extends StatelessWidget {
); );
} }
Widget _messagePreview(ColorScheme cs, List<CachedMessage> forwards) {
if (forwards.isNotEmpty) return _forwardPreview(cs, forwards);
return _replyPreview(cs);
}
Widget _forwardPreview(ColorScheme cs, List<CachedMessage> messages) {
final first = messages.first;
final senderName = ContactCache.get(first.senderId);
final info = ReplyInfo(
senderId: first.senderId,
text: first.text,
attachments: first.attachments,
);
final preview = info.previewText();
final title = messages.length == 1
? first.senderId == myId
? 'Пересылка от вас'
: senderName == null
? 'Пересылка сообщения'
: 'Пересылка от $senderName'
: 'Пересылка: ${_forwardCount(messages.length)}';
final row = Padding(
padding: const EdgeInsets.fromLTRB(16, 6, 8, 2),
child: Row(
children: [
Icon(Symbols.forward, size: 20, color: cs.primary),
const SizedBox(width: 10),
Container(width: 2, height: 34, color: cs.primary),
const SizedBox(width: 10),
Expanded(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: cs.primary,
fontSize: 13,
fontWeight: FontWeight.w600,
),
),
if (preview.isNotEmpty)
Text(
preview,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(color: cs.onSurfaceVariant, fontSize: 13),
),
],
),
),
IconButton(
icon: const Icon(Symbols.close, size: 20),
color: cs.onSurfaceVariant,
onPressed: onCancelForward,
),
],
),
);
return _previewSurface(cs, row);
}
String _forwardCount(int count) {
final last = count % 10;
final lastTwo = count % 100;
if (last == 1 && lastTwo != 11) return '$count сообщение';
if (last >= 2 && last <= 4 && (lastTwo < 12 || lastTwo > 14)) {
return '$count сообщения';
}
return '$count сообщений';
}
Widget _replyPreview(ColorScheme cs) { Widget _replyPreview(ColorScheme cs) {
return ValueListenableBuilder<CachedMessage?>( return ValueListenableBuilder<CachedMessage?>(
valueListenable: replyTo, valueListenable: replyTo,
@@ -625,19 +722,23 @@ class ComposerInputBar extends StatelessWidget {
], ],
), ),
); );
if (_flat && _translucent) return row; return _previewSurface(cs, row);
if (!_translucent && chrome != ChatChromeStyle.transparent) return row;
return GlassSurface(
liquid: _liquid,
frostTint: AppFrost.panelTint(cs),
border: Border(top: AppFrost.hairline(cs)),
backdropKey: backdropKey,
child: row,
);
}, },
); );
} }
Widget _previewSurface(ColorScheme cs, Widget child) {
if (_flat && _translucent) return child;
if (!_translucent && chrome != ChatChromeStyle.transparent) return child;
return GlassSurface(
liquid: _liquid,
frostTint: AppFrost.panelTint(cs),
border: Border(top: AppFrost.hairline(cs)),
backdropKey: backdropKey,
child: child,
);
}
Widget _recordingButtonVisual({ Widget _recordingButtonVisual({
required Widget pill, required Widget pill,
required ColorScheme cs, required ColorScheme cs,
+140 -134
View File
@@ -198,9 +198,26 @@ class _MeasureSizeState extends State<_MeasureSize> {
class ForwardRequest { class ForwardRequest {
final int sourceChatId; final int sourceChatId;
final List<CachedMessage> optimistic; final String sourceChatName;
final String sourceChatIconUrl;
final String sourceChatType;
final List<CachedMessage> messages;
const ForwardRequest({required this.sourceChatId, required this.optimistic}); ForwardRequest({
required this.sourceChatId,
required this.sourceChatName,
required this.sourceChatIconUrl,
required this.sourceChatType,
required List<CachedMessage> messages,
}) : messages = List.unmodifiable(messages);
ForwardRequest withMessages(List<CachedMessage> value) => ForwardRequest(
sourceChatId: sourceChatId,
sourceChatName: sourceChatName,
sourceChatIconUrl: sourceChatIconUrl,
sourceChatType: sourceChatType,
messages: value,
);
} }
class ReplyRequest { class ReplyRequest {
@@ -459,8 +476,13 @@ class _ChatScreenState extends State<ChatScreen>
int? _participantsCount; int? _participantsCount;
final ValueNotifier<CachedMessage?> _replyTo = ValueNotifier(null); final ValueNotifier<CachedMessage?> _replyTo = ValueNotifier(null);
final ValueNotifier<List<CachedMessage>> _pendingForwards = ValueNotifier(
const [],
);
static const bool _crossChatReplySupported = false; static const bool _crossChatReplySupported = false;
int? _replySourceChatId; int? _replySourceChatId;
ForwardRequest? _forwardRequest;
bool _forwardSending = false;
final ValueNotifier<String?> _highlightMessageId = ValueNotifier(null); final ValueNotifier<String?> _highlightMessageId = ValueNotifier(null);
Timer? _highlightTimer; Timer? _highlightTimer;
final ValueNotifier<double?> _jumpCacheExtent = ValueNotifier<double?>(null); final ValueNotifier<double?> _jumpCacheExtent = ValueNotifier<double?>(null);
@@ -493,8 +515,6 @@ class _ChatScreenState extends State<ChatScreen>
bool _previewChat = false; bool _previewChat = false;
bool _subscribing = false; bool _subscribing = false;
String? _channelLink; String? _channelLink;
bool _forwardRequestDone = false;
final ChatController _chatController = ChatController(); final ChatController _chatController = ChatController();
List<CachedMessage> get _messages => _chatController.messages; List<CachedMessage> get _messages => _chatController.messages;
@@ -667,6 +687,11 @@ class _ChatScreenState extends State<ChatScreen>
? null ? null
: incomingReply.sourceChatId; : incomingReply.sourceChatId;
} }
final incomingForward = widget.forwardRequest;
if (incomingForward != null) {
_forwardRequest = incomingForward;
_pendingForwards.value = incomingForward.messages;
}
_pushSub = api.pushStream _pushSub = api.pushStream
.where( .where(
(p) => (p) =>
@@ -718,11 +743,7 @@ class _ChatScreenState extends State<ChatScreen>
reverseCurve: Curves.easeIn, reverseCurve: Curves.easeIn,
); );
unawaited( unawaited(_fastPreloadCache());
_fastPreloadCache().then((_) {
if (mounted) unawaited(_runForwardRequest());
}),
);
unawaited(_loadParticipantsCount()); unawaited(_loadParticipantsCount());
WidgetsBinding.instance.addPostFrameCallback(_onFirstFrameRendered); WidgetsBinding.instance.addPostFrameCallback(_onFirstFrameRendered);
} }
@@ -1935,6 +1956,7 @@ class _ChatScreenState extends State<ChatScreen>
_shimmerStartTimer?.cancel(); _shimmerStartTimer?.cancel();
_shimmerController.dispose(); _shimmerController.dispose();
_replyTo.dispose(); _replyTo.dispose();
_pendingForwards.dispose();
_highlightTimer?.cancel(); _highlightTimer?.cancel();
_highlightMessageId.dispose(); _highlightMessageId.dispose();
_goToMessageSettleTimer?.cancel(); _goToMessageSettleTimer?.cancel();
@@ -2213,7 +2235,9 @@ class _ChatScreenState extends State<ChatScreen>
} }
Future<void> _forwardMessages(List<CachedMessage> msgs) async { Future<void> _forwardMessages(List<CachedMessage> msgs) async {
final forwardable = msgs.where((m) => !m.id.startsWith('temp_')).toList(); final forwardable = msgs
.where((message) => int.tryParse(message.id) != null)
.toList();
if (forwardable.isEmpty) { if (forwardable.isEmpty) {
showCustomNotification(context, 'Нечего пересылать'); showCustomNotification(context, 'Нечего пересылать');
return; return;
@@ -2225,21 +2249,20 @@ class _ChatScreenState extends State<ChatScreen>
); );
if (target == null || !mounted) return; if (target == null || !mounted) return;
if (api.state != SessionState.online) {
showCustomNotification(context, 'Нет соединения');
return;
}
final ordered = [...forwardable]..sort((a, b) => a.time.compareTo(b.time)); final ordered = [...forwardable]..sort((a, b) => a.time.compareTo(b.time));
final request = ForwardRequest(
sourceChatId: widget.chatId,
sourceChatName: widget.name,
sourceChatIconUrl: widget.imageUrl,
sourceChatType: widget.chatType,
messages: ordered,
);
if (target.chatId == widget.chatId) { if (target.chatId == widget.chatId) {
await _forwardIntoCurrentChat(ordered); _setForwardRequest(request);
return; return;
} }
final optimistic = await _seedForwardsToChat(target, ordered);
if (!mounted) return;
Haptics.send();
pushSwipeable( pushSwipeable(
context, context,
(_) => ChatScreen( (_) => ChatScreen(
@@ -2247,164 +2270,127 @@ class _ChatScreenState extends State<ChatScreen>
name: target.name, name: target.name,
imageUrl: target.imageUrl, imageUrl: target.imageUrl,
chatType: target.chatType, chatType: target.chatType,
forwardRequest: ForwardRequest( forwardRequest: request,
sourceChatId: widget.chatId,
optimistic: optimistic,
),
), ),
); );
} }
Future<void> _forwardIntoCurrentChat(List<CachedMessage> sources) async { void _setForwardRequest(ForwardRequest request) {
final now = DateTime.now().millisecondsSinceEpoch; _cancelReply();
final optimistic = <CachedMessage>[]; _forwardRequest = request;
var i = 0; _pendingForwards.value = request.messages;
for (final src in sources) { }
final msg = MessagesModule.buildForwardMessage(
void _cancelForward() {
_forwardRequest = null;
_pendingForwards.value = const [];
}
Future<bool> _sendForwardRequest() async {
var request = _forwardRequest;
if (request == null) return true;
if (api.state != SessionState.online) {
showCustomNotification(context, 'Нет соединения');
return false;
}
Haptics.send();
while (request != null && request.messages.isNotEmpty) {
if (!identical(_forwardRequest, request)) return false;
final source = request.messages.first;
final optimistic = MessagesModule.buildForwardMessage(
myId: _myId, myId: _myId,
targetChatId: widget.chatId, targetChatId: widget.chatId,
sourceChatId: widget.chatId, sourceChatId: request.sourceChatId,
source: src, source: source,
tempId: _nextTempId(), tempId: _nextTempId(),
time: now + i, time: DateTime.now().millisecondsSinceEpoch,
status: 'sending', status: 'sending',
sourceChatName: widget.name, sourceChatName: request.sourceChatName,
sourceChatIconUrl: widget.imageUrl, sourceChatIconUrl: request.sourceChatIconUrl,
sourceChatType: widget.chatType, sourceChatType: request.sourceChatType,
); );
optimistic.add(msg); _messages.add(optimistic);
_messages.add(msg); _bumpMessages();
unawaited(_persistOutgoing(msg)); _scrollToBottom();
i++; await _syncForwardOutgoing(optimistic);
} final sent = await _sendOneForward(optimistic, request.sourceChatId);
_bumpMessages(); if (!sent || !mounted) return false;
Haptics.send(); if (!identical(_forwardRequest, request)) return false;
_scrollToBottom(); final remaining = request.messages.skip(1).toList(growable: false);
if (optimistic.isNotEmpty) { if (remaining.isEmpty) {
final last = optimistic.last; _cancelForward();
unawaited( return true;
chats.applyOutgoing( }
_myId, request = request.withMessages(remaining);
widget.chatId, _forwardRequest = request;
messageId: last.id, _pendingForwards.value = request.messages;
time: last.time,
text: MessagesModule.forwardPreviewText(last),
status: 'sending',
),
);
}
for (final opt in optimistic) {
await _sendOneForward(opt, widget.chatId);
} }
_cancelForward();
return true;
} }
Future<List<CachedMessage>> _seedForwardsToChat( Future<bool> _sendOneForward(
ForwardTarget target,
List<CachedMessage> sources,
) async {
await chats.ensureChatCached(api, _myId, target.chatId);
final now = DateTime.now().millisecondsSinceEpoch;
final optimistic = <CachedMessage>[];
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',
sourceChatName: widget.name,
sourceChatIconUrl: widget.imageUrl,
sourceChatType: widget.chatType,
);
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(
chats.applyOutgoing(
_myId,
target.chatId,
messageId: last.id,
time: last.time,
text: MessagesModule.forwardPreviewText(last),
status: 'sending',
),
);
}
return optimistic;
}
Future<void> _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<void> _sendOneForward(
CachedMessage optimistic, CachedMessage optimistic,
int sourceChatId, int sourceChatId,
) async { ) async {
final link = optimistic.payload?['link']; final link = optimistic.payload?['link'];
final rawWireId = link is Map ? link['messageId'] : null; final rawWireId = link is Map ? link['messageId'] : null;
final wireId = rawWireId is int ? rawWireId : null; final wireId = rawWireId is int ? rawWireId : null;
if (wireId == null) return; if (wireId == null) return false;
try { try {
final realId = await messagesModule.forwardMessage( final realId = await messagesModule.forwardMessage(
widget.chatId, widget.chatId,
sourceChatId, sourceChatId,
wireId, wireId,
); );
if (!mounted) return;
final sent = MessagesModule.reidentifyMessage( final sent = MessagesModule.reidentifyMessage(
optimistic, optimistic,
realId.isNotEmpty ? realId : optimistic.id, realId.isNotEmpty ? realId : optimistic.id,
status: 'sent', status: 'sent',
); );
final index = _messages.indexWhere((m) => m.id == optimistic.id); if (mounted) {
if (index != -1) { final index = _messages.indexWhere((m) => m.id == optimistic.id);
_messages[index] = sent; if (index != -1) {
_bumpMessages(); _messages[index] = sent;
_bumpMessages();
}
} }
unawaited(_persistOutgoing(sent, removeId: optimistic.id)); await _syncForwardOutgoing(sent, removeId: optimistic.id);
unawaited( return true;
chats.applyOutgoing(
_myId,
widget.chatId,
messageId: sent.id,
time: sent.time,
text: MessagesModule.forwardPreviewText(sent),
status: 'sent',
),
);
} catch (_) { } catch (_) {
final index = _messages.indexWhere((m) => m.id == optimistic.id); final index = _messages.indexWhere((m) => m.id == optimistic.id);
if (index != -1 && mounted) { if (index != -1 && mounted) {
_messages.removeAt(index); _messages.removeAt(index);
_bumpMessages(); _bumpMessages();
} }
unawaited(AppDatabase.deleteMessage(_myId, widget.chatId, optimistic.id)); try {
await AppDatabase.deleteMessage(_myId, widget.chatId, optimistic.id);
} catch (_) {}
if (mounted) { if (mounted) {
Haptics.error(); Haptics.error();
showCustomNotification(context, 'Не удалось переслать'); showCustomNotification(context, 'Не удалось переслать');
} }
return false;
} }
} }
Future<void> _syncForwardOutgoing(
CachedMessage message, {
String? removeId,
}) async {
await _persistOutgoing(message, removeId: removeId);
try {
await chats.applyOutgoing(
_myId,
widget.chatId,
messageId: message.id,
time: message.time,
text: MessagesModule.forwardPreviewText(message),
status: message.status ?? 'sending',
);
} catch (_) {}
}
Widget _buildComposerArea(BuildContext context) { Widget _buildComposerArea(BuildContext context) {
final cs = Theme.of(context).colorScheme; final cs = Theme.of(context).colorScheme;
final content = Column( final content = Column(
@@ -2472,6 +2458,7 @@ class _ChatScreenState extends State<ChatScreen>
backdropKey: _pillBackdrop, backdropKey: _pillBackdrop,
attachAnim: _attachAnim, attachAnim: _attachAnim,
replyTo: _replyTo, replyTo: _replyTo,
forwardMessages: _pendingForwards,
myId: _myId, myId: _myId,
hasText: _hasText, hasText: _hasText,
uploadStatus: _uploadStatus, uploadStatus: _uploadStatus,
@@ -2486,6 +2473,7 @@ class _ChatScreenState extends State<ChatScreen>
onOpenAttachScheduled: _openAttachmentSheetScheduled, onOpenAttachScheduled: _openAttachmentSheetScheduled,
onSendHistory: _sendHistoryFile, onSendHistory: _sendHistoryFile,
onCancelReply: _cancelReply, onCancelReply: _cancelReply,
onCancelForward: _cancelForward,
onPickReplyChat: _commentsMode || !_crossChatReplySupported onPickReplyChat: _commentsMode || !_crossChatReplySupported
? null ? null
: () => unawaited(_pickReplyChat()), : () => unawaited(_pickReplyChat()),
@@ -3609,6 +3597,23 @@ class _ChatScreenState extends State<ChatScreen>
} }
Future<void> _sendMessage() async { Future<void> _sendMessage() async {
if (_forwardRequest == null) {
await _sendTextMessage();
return;
}
if (_forwardSending || _myId == 0) return;
_forwardSending = true;
try {
final forwarded = await _sendForwardRequest();
if (!forwarded || !mounted) return;
if (_messageController.text.trim().isEmpty) return;
await _sendTextMessage();
} finally {
_forwardSending = false;
}
}
Future<void> _sendTextMessage() async {
final content = _messageController.buildContent(); final content = _messageController.buildContent();
final rawText = content.text; final rawText = content.text;
final text = rawText.trim(); final text = rawText.trim();
@@ -4260,6 +4265,7 @@ class _ChatScreenState extends State<ChatScreen>
} }
void _startReply(CachedMessage message) { void _startReply(CachedMessage message) {
_cancelForward();
_replyTo.value = message; _replyTo.value = message;
_replySourceChatId = null; _replySourceChatId = null;
_messageFocusNode.requestFocus(); _messageFocusNode.requestFocus();
+129
View File
@@ -0,0 +1,129 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:komet/backend/modules/messages.dart';
import 'package:komet/core/config/app_chat_chrome.dart';
import 'package:komet/core/config/app_composer_background.dart';
import 'package:komet/core/config/app_composer_style.dart';
import 'package:komet/frontend/screens/chats/chat/upload_status.dart';
import 'package:komet/frontend/screens/chats/chat/video_note_controller.dart';
import 'package:komet/frontend/screens/chats/chat/view/composer_input.dart';
import 'package:komet/frontend/screens/chats/chat/voice_record_controller.dart';
import 'package:komet/frontend/widgets/rich_message_controller.dart';
import 'package:material_symbols_icons/symbols.dart';
CachedMessage _message() => const CachedMessage(
id: '101',
accountId: 7,
chatId: 70,
senderId: 7,
text: 'Synthetic forwarded text',
time: 1000,
status: 'sent',
);
void main() {
testWidgets('forward preview forces send mode and can be cancelled', (
tester,
) async {
final forwards = ValueNotifier<List<CachedMessage>>([_message()]);
final reply = ValueNotifier<CachedMessage?>(null);
final hasText = ValueNotifier(false);
final uploadStatus = ValueNotifier(const UploadStatus());
final messageController = RichMessageController();
final focusNode = FocusNode();
final attachAnimation = AnimationController(
vsync: const TestVSync(),
duration: const Duration(milliseconds: 1),
);
late BuildContext composerContext;
var sendCount = 0;
var cancelCount = 0;
final voice = VoiceRecordController(
contextOf: () => composerContext,
isMounted: () => true,
myId: () => 7,
onRecorded: (File file, int durationMs, List<double> amplitudes) async {},
);
final note = VideoNoteController(
contextOf: () => composerContext,
isMounted: () => true,
onRecorded: (File file, int durationMs) async {},
formatElapsed: (milliseconds) => '$milliseconds',
);
await tester.pumpWidget(
MaterialApp(
home: Builder(
builder: (context) {
composerContext = context;
return Scaffold(
bottomNavigationBar: ComposerInputBar(
chatType: 'CHAT',
chrome: ChatChromeStyle.color,
style: ComposerStyle.materialYou,
background: ComposerBackground.standard,
attachAnim: attachAnimation,
replyTo: reply,
forwardMessages: forwards,
myId: 7,
hasText: hasText,
uploadStatus: uploadStatus,
messageController: messageController,
messageFocusNode: focusNode,
voiceRec: voice,
note: note,
onToggleStickerPanel: () {},
onSendText: () => sendCount++,
onScheduleMessage: () {},
onOpenAttach: () {},
onOpenAttachScheduled: () {},
onSendHistory: (_) async {},
onCancelReply: () {},
onCancelForward: () {
cancelCount++;
forwards.value = const [];
},
formatElapsed: (milliseconds) => '$milliseconds',
contextMenuBuilder: (context, state) => const SizedBox.shrink(),
isMuted: false,
onToggleMute: () {},
showStickerButton: false,
showAttachButton: false,
),
);
},
),
),
);
expect(find.text('Пересылка от вас'), findsOneWidget);
expect(find.text('Synthetic forwarded text'), findsOneWidget);
expect(find.byIcon(Symbols.forward), findsOneWidget);
expect(find.byIcon(Symbols.send), findsOneWidget);
expect(find.byIcon(Symbols.mic), findsNothing);
await tester.tap(find.byIcon(Symbols.send));
expect(sendCount, 1);
await tester.tap(find.byIcon(Symbols.close));
await tester.pump();
expect(cancelCount, 1);
expect(find.text('Пересылка от вас'), findsNothing);
expect(find.byIcon(Symbols.send), findsNothing);
expect(find.byIcon(Symbols.mic), findsOneWidget);
await tester.pumpWidget(const SizedBox.shrink());
forwards.dispose();
reply.dispose();
hasText.dispose();
uploadStatus.dispose();
messageController.dispose();
focusNode.dispose();
attachAnimation.dispose();
voice.dispose();
note.dispose();
});
}