feat: панель перессылки
This commit is contained in:
@@ -28,6 +28,7 @@ class ComposerInputBar extends StatelessWidget {
|
||||
this.backdropKey,
|
||||
required this.attachAnim,
|
||||
required this.replyTo,
|
||||
required this.forwardMessages,
|
||||
required this.myId,
|
||||
required this.hasText,
|
||||
required this.uploadStatus,
|
||||
@@ -42,6 +43,7 @@ class ComposerInputBar extends StatelessWidget {
|
||||
required this.onOpenAttachScheduled,
|
||||
required this.onSendHistory,
|
||||
required this.onCancelReply,
|
||||
required this.onCancelForward,
|
||||
this.onPickReplyChat,
|
||||
required this.formatElapsed,
|
||||
required this.contextMenuBuilder,
|
||||
@@ -64,6 +66,7 @@ class ComposerInputBar extends StatelessWidget {
|
||||
final BackdropKey? backdropKey;
|
||||
final Animation<double> attachAnim;
|
||||
final ValueListenable<CachedMessage?> replyTo;
|
||||
final ValueListenable<List<CachedMessage>> forwardMessages;
|
||||
final int myId;
|
||||
final ValueListenable<bool> hasText;
|
||||
final ValueListenable<UploadStatus> uploadStatus;
|
||||
@@ -78,6 +81,7 @@ class ComposerInputBar extends StatelessWidget {
|
||||
final VoidCallback onOpenAttachScheduled;
|
||||
final Future<void> Function(FileHistoryEntry entry) onSendHistory;
|
||||
final VoidCallback onCancelReply;
|
||||
final VoidCallback onCancelForward;
|
||||
final VoidCallback? onPickReplyChat;
|
||||
final String Function(int ms) formatElapsed;
|
||||
final Widget Function(BuildContext, EditableTextState) contextMenuBuilder;
|
||||
@@ -94,10 +98,18 @@ class ComposerInputBar extends StatelessWidget {
|
||||
|
||||
@override
|
||||
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 mutedIcon = cs.onSurfaceVariant.withValues(alpha: 0.85);
|
||||
final hasForward = forwards.isNotEmpty;
|
||||
|
||||
if (chatType == "CHANNEL") {
|
||||
if (chatType == "CHANNEL" && !hasForward) {
|
||||
if (!channelSubscribed) {
|
||||
return SafeArea(
|
||||
child: Padding(
|
||||
@@ -180,7 +192,7 @@ class ComposerInputBar extends StatelessWidget {
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_replyPreview(cs),
|
||||
_messagePreview(cs, forwards),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12.0,
|
||||
@@ -242,7 +254,11 @@ class ComposerInputBar extends StatelessWidget {
|
||||
!HardwareKeyboard
|
||||
.instance
|
||||
.isShiftPressed) {
|
||||
if (hasText.value) onSendText();
|
||||
if (hasText.value ||
|
||||
hasForward ||
|
||||
forceSend) {
|
||||
onSendText();
|
||||
}
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
return KeyEventResult.ignored;
|
||||
@@ -383,6 +399,7 @@ class ComposerInputBar extends StatelessWidget {
|
||||
builder: (context, videoMode, _) {
|
||||
final sendMode =
|
||||
hasText ||
|
||||
hasForward ||
|
||||
locked ||
|
||||
forceSend;
|
||||
final pill = _actionSurface(
|
||||
@@ -395,7 +412,10 @@ class ComposerInputBar extends StatelessWidget {
|
||||
: _frost
|
||||
? AppFrost.inputTint(cs)
|
||||
: cs.surfaceContainerHighest,
|
||||
onTap: (hasText || forceSend)
|
||||
onTap:
|
||||
(hasText ||
|
||||
hasForward ||
|
||||
forceSend)
|
||||
? onSendText
|
||||
: locked
|
||||
? () => voiceRec.stop(
|
||||
@@ -403,7 +423,9 @@ class ComposerInputBar extends StatelessWidget {
|
||||
)
|
||||
: null,
|
||||
onLongPress:
|
||||
(hasText && !forceSend)
|
||||
(hasText &&
|
||||
!forceSend &&
|
||||
!hasForward)
|
||||
? onScheduleMessage
|
||||
: null,
|
||||
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) {
|
||||
return ValueListenableBuilder<CachedMessage?>(
|
||||
valueListenable: replyTo,
|
||||
@@ -625,19 +722,23 @@ class ComposerInputBar extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
);
|
||||
if (_flat && _translucent) return 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,
|
||||
);
|
||||
return _previewSurface(cs, 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({
|
||||
required Widget pill,
|
||||
required ColorScheme cs,
|
||||
|
||||
@@ -198,9 +198,26 @@ class _MeasureSizeState extends State<_MeasureSize> {
|
||||
|
||||
class ForwardRequest {
|
||||
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 {
|
||||
@@ -459,8 +476,13 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
int? _participantsCount;
|
||||
|
||||
final ValueNotifier<CachedMessage?> _replyTo = ValueNotifier(null);
|
||||
final ValueNotifier<List<CachedMessage>> _pendingForwards = ValueNotifier(
|
||||
const [],
|
||||
);
|
||||
static const bool _crossChatReplySupported = false;
|
||||
int? _replySourceChatId;
|
||||
ForwardRequest? _forwardRequest;
|
||||
bool _forwardSending = false;
|
||||
final ValueNotifier<String?> _highlightMessageId = ValueNotifier(null);
|
||||
Timer? _highlightTimer;
|
||||
final ValueNotifier<double?> _jumpCacheExtent = ValueNotifier<double?>(null);
|
||||
@@ -493,8 +515,6 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
bool _previewChat = false;
|
||||
bool _subscribing = false;
|
||||
String? _channelLink;
|
||||
bool _forwardRequestDone = false;
|
||||
|
||||
final ChatController _chatController = ChatController();
|
||||
|
||||
List<CachedMessage> get _messages => _chatController.messages;
|
||||
@@ -667,6 +687,11 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
? null
|
||||
: incomingReply.sourceChatId;
|
||||
}
|
||||
final incomingForward = widget.forwardRequest;
|
||||
if (incomingForward != null) {
|
||||
_forwardRequest = incomingForward;
|
||||
_pendingForwards.value = incomingForward.messages;
|
||||
}
|
||||
_pushSub = api.pushStream
|
||||
.where(
|
||||
(p) =>
|
||||
@@ -718,11 +743,7 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
reverseCurve: Curves.easeIn,
|
||||
);
|
||||
|
||||
unawaited(
|
||||
_fastPreloadCache().then((_) {
|
||||
if (mounted) unawaited(_runForwardRequest());
|
||||
}),
|
||||
);
|
||||
unawaited(_fastPreloadCache());
|
||||
unawaited(_loadParticipantsCount());
|
||||
WidgetsBinding.instance.addPostFrameCallback(_onFirstFrameRendered);
|
||||
}
|
||||
@@ -1935,6 +1956,7 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
_shimmerStartTimer?.cancel();
|
||||
_shimmerController.dispose();
|
||||
_replyTo.dispose();
|
||||
_pendingForwards.dispose();
|
||||
_highlightTimer?.cancel();
|
||||
_highlightMessageId.dispose();
|
||||
_goToMessageSettleTimer?.cancel();
|
||||
@@ -2213,7 +2235,9 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
}
|
||||
|
||||
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) {
|
||||
showCustomNotification(context, 'Нечего пересылать');
|
||||
return;
|
||||
@@ -2225,21 +2249,20 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
);
|
||||
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 request = ForwardRequest(
|
||||
sourceChatId: widget.chatId,
|
||||
sourceChatName: widget.name,
|
||||
sourceChatIconUrl: widget.imageUrl,
|
||||
sourceChatType: widget.chatType,
|
||||
messages: ordered,
|
||||
);
|
||||
|
||||
if (target.chatId == widget.chatId) {
|
||||
await _forwardIntoCurrentChat(ordered);
|
||||
_setForwardRequest(request);
|
||||
return;
|
||||
}
|
||||
|
||||
final optimistic = await _seedForwardsToChat(target, ordered);
|
||||
if (!mounted) return;
|
||||
Haptics.send();
|
||||
pushSwipeable(
|
||||
context,
|
||||
(_) => ChatScreen(
|
||||
@@ -2247,164 +2270,127 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
name: target.name,
|
||||
imageUrl: target.imageUrl,
|
||||
chatType: target.chatType,
|
||||
forwardRequest: ForwardRequest(
|
||||
sourceChatId: widget.chatId,
|
||||
optimistic: optimistic,
|
||||
),
|
||||
forwardRequest: request,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _forwardIntoCurrentChat(List<CachedMessage> sources) async {
|
||||
final now = DateTime.now().millisecondsSinceEpoch;
|
||||
final optimistic = <CachedMessage>[];
|
||||
var i = 0;
|
||||
for (final src in sources) {
|
||||
final msg = MessagesModule.buildForwardMessage(
|
||||
void _setForwardRequest(ForwardRequest request) {
|
||||
_cancelReply();
|
||||
_forwardRequest = request;
|
||||
_pendingForwards.value = request.messages;
|
||||
}
|
||||
|
||||
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,
|
||||
targetChatId: widget.chatId,
|
||||
sourceChatId: widget.chatId,
|
||||
source: src,
|
||||
sourceChatId: request.sourceChatId,
|
||||
source: source,
|
||||
tempId: _nextTempId(),
|
||||
time: now + i,
|
||||
time: DateTime.now().millisecondsSinceEpoch,
|
||||
status: 'sending',
|
||||
sourceChatName: widget.name,
|
||||
sourceChatIconUrl: widget.imageUrl,
|
||||
sourceChatType: widget.chatType,
|
||||
sourceChatName: request.sourceChatName,
|
||||
sourceChatIconUrl: request.sourceChatIconUrl,
|
||||
sourceChatType: request.sourceChatType,
|
||||
);
|
||||
optimistic.add(msg);
|
||||
_messages.add(msg);
|
||||
unawaited(_persistOutgoing(msg));
|
||||
i++;
|
||||
}
|
||||
_bumpMessages();
|
||||
Haptics.send();
|
||||
_scrollToBottom();
|
||||
if (optimistic.isNotEmpty) {
|
||||
final last = optimistic.last;
|
||||
unawaited(
|
||||
chats.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);
|
||||
_messages.add(optimistic);
|
||||
_bumpMessages();
|
||||
_scrollToBottom();
|
||||
await _syncForwardOutgoing(optimistic);
|
||||
final sent = await _sendOneForward(optimistic, request.sourceChatId);
|
||||
if (!sent || !mounted) return false;
|
||||
if (!identical(_forwardRequest, request)) return false;
|
||||
final remaining = request.messages.skip(1).toList(growable: false);
|
||||
if (remaining.isEmpty) {
|
||||
_cancelForward();
|
||||
return true;
|
||||
}
|
||||
request = request.withMessages(remaining);
|
||||
_forwardRequest = request;
|
||||
_pendingForwards.value = request.messages;
|
||||
}
|
||||
_cancelForward();
|
||||
return true;
|
||||
}
|
||||
|
||||
Future<List<CachedMessage>> _seedForwardsToChat(
|
||||
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(
|
||||
Future<bool> _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;
|
||||
if (wireId == null) return false;
|
||||
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();
|
||||
if (mounted) {
|
||||
final index = _messages.indexWhere((m) => m.id == optimistic.id);
|
||||
if (index != -1) {
|
||||
_messages[index] = sent;
|
||||
_bumpMessages();
|
||||
}
|
||||
}
|
||||
unawaited(_persistOutgoing(sent, removeId: optimistic.id));
|
||||
unawaited(
|
||||
chats.applyOutgoing(
|
||||
_myId,
|
||||
widget.chatId,
|
||||
messageId: sent.id,
|
||||
time: sent.time,
|
||||
text: MessagesModule.forwardPreviewText(sent),
|
||||
status: 'sent',
|
||||
),
|
||||
);
|
||||
await _syncForwardOutgoing(sent, removeId: optimistic.id);
|
||||
return true;
|
||||
} 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));
|
||||
try {
|
||||
await AppDatabase.deleteMessage(_myId, widget.chatId, optimistic.id);
|
||||
} catch (_) {}
|
||||
if (mounted) {
|
||||
Haptics.error();
|
||||
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) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
final content = Column(
|
||||
@@ -2472,6 +2458,7 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
backdropKey: _pillBackdrop,
|
||||
attachAnim: _attachAnim,
|
||||
replyTo: _replyTo,
|
||||
forwardMessages: _pendingForwards,
|
||||
myId: _myId,
|
||||
hasText: _hasText,
|
||||
uploadStatus: _uploadStatus,
|
||||
@@ -2486,6 +2473,7 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
onOpenAttachScheduled: _openAttachmentSheetScheduled,
|
||||
onSendHistory: _sendHistoryFile,
|
||||
onCancelReply: _cancelReply,
|
||||
onCancelForward: _cancelForward,
|
||||
onPickReplyChat: _commentsMode || !_crossChatReplySupported
|
||||
? null
|
||||
: () => unawaited(_pickReplyChat()),
|
||||
@@ -3609,6 +3597,23 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
}
|
||||
|
||||
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 rawText = content.text;
|
||||
final text = rawText.trim();
|
||||
@@ -4260,6 +4265,7 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
}
|
||||
|
||||
void _startReply(CachedMessage message) {
|
||||
_cancelForward();
|
||||
_replyTo.value = message;
|
||||
_replySourceChatId = null;
|
||||
_messageFocusNode.requestFocus();
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user