fix: fix issue #75

This commit is contained in:
Jganenokk
2026-08-20 17:45:27 +07:00
parent 0030ded1f4
commit 3cfe3a3965
4 changed files with 118 additions and 32 deletions
@@ -8,10 +8,10 @@ class SelectionTopBar extends StatelessWidget {
final ColorScheme cs;
final Set<String> selected;
final bool glossy;
final CachedMessage? copyMsg;
final List<CachedMessage> copyMsgs;
final CachedMessage? editMsg;
final VoidCallback onClear;
final void Function(CachedMessage) onCopy;
final void Function(List<CachedMessage>) onCopy;
final void Function(CachedMessage) onEdit;
final VoidCallback onDelete;
@@ -20,7 +20,7 @@ class SelectionTopBar extends StatelessWidget {
required this.cs,
required this.selected,
required this.glossy,
required this.copyMsg,
required this.copyMsgs,
required this.editMsg,
required this.onClear,
required this.onCopy,
@@ -56,10 +56,10 @@ class SelectionTopBar extends StatelessWidget {
),
),
),
if (copyMsg != null)
if (copyMsgs.isNotEmpty)
IconButton(
icon: Icon(Symbols.content_copy, color: cs.onSurface),
onPressed: () => onCopy(copyMsg!),
onPressed: () => onCopy(copyMsgs),
),
if (editMsg != null)
IconButton(
@@ -130,8 +130,8 @@ class SelectionTopBar extends StatelessWidget {
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (copyMsg != null)
actionBtn(Symbols.content_copy, () => onCopy(copyMsg!)),
if (copyMsgs.isNotEmpty)
actionBtn(Symbols.content_copy, () => onCopy(copyMsgs)),
if (editMsg != null)
actionBtn(Symbols.edit, () => onEdit(editMsg!)),
actionBtn(Symbols.delete, onDelete),
+13 -20
View File
@@ -2383,17 +2383,10 @@ class _ChatScreenState extends State<ChatScreen>
List<CachedMessage> _selectedMessages(Set<String> ids) =>
_messages.where((m) => ids.contains(m.id)).toList();
CachedMessage? _singleCopyableText(Set<String> ids) {
CachedMessage? found;
var textCount = 0;
for (final m in _messages) {
if (!ids.contains(m.id)) continue;
if ((m.text ?? '').isEmpty) continue;
if (++textCount > 1) return null;
found = m;
}
return found;
}
List<CachedMessage> _copyableSelection(Set<String> ids) => [
for (final m in _messages)
if (ids.contains(m.id) && (m.selectableText ?? '').isNotEmpty) m,
];
CachedMessage? _singleEditable(Set<String> ids) {
if (ids.length != 1) return null;
@@ -2402,9 +2395,9 @@ class _ChatScreenState extends State<ChatScreen>
return _canEditMessage(list.first) ? list.first : null;
}
void _copySelected(CachedMessage message) {
final text = message.text;
if (text == null || text.isEmpty) return;
void _copySelected(List<CachedMessage> messages) {
if (messages.isEmpty) return;
final text = messages.map((m) => m.selectableText!).join('\n\n');
Clipboard.setData(ClipboardData(text: text));
Haptics.tap();
showCustomNotification(context, 'Скопировано');
@@ -3223,7 +3216,7 @@ class _ChatScreenState extends State<ChatScreen>
cs: cs,
selected: selected,
glossy: glossy,
copyMsg: _singleCopyableText(selected),
copyMsgs: _copyableSelection(selected),
editMsg: _singleEditable(selected),
onClear: _clearSelection,
onCopy: _copySelected,
@@ -6330,11 +6323,9 @@ class _ChatScreenState extends State<ChatScreen>
if (info.durationMs > 0) durationMs = info.durationMs;
if (info.width > 0 && info.height > 0) dims = (info.width, info.height);
}
final frames = await VideoTranscoder.frames(
edited.path,
const [0],
size: 512,
);
final frames = await VideoTranscoder.frames(edited.path, const [
0,
], size: 512);
if (frames.isNotEmpty) thumbBytes = frames.first;
}
if (durationMs == null && DesktopVideoProbe.supported) {
@@ -7212,6 +7203,7 @@ class _SelectableMessageRowState extends State<_SelectableMessageRow> {
tapPoint: _lastTapDown ?? rect.center,
isMe: widget.isMe,
messageText: widget.message.text,
copyText: widget.message.selectableText,
controller: controller,
style: AppMessageActionsStyle.current.value,
interaction: MessageActionsInteraction.tap,
@@ -7281,6 +7273,7 @@ class _SelectableMessageRowState extends State<_SelectableMessageRow> {
tapPoint: details.globalPosition,
isMe: widget.isMe,
messageText: widget.message.text,
copyText: widget.message.selectableText,
controller: controller,
style: MessageActionsStyle.list,
interaction: MessageActionsInteraction.click,
@@ -100,6 +100,7 @@ void showMessageActions({
required Offset tapPoint,
required bool isMe,
required String? messageText,
required String? copyText,
required MessageActionsController controller,
required MessageActionsStyle style,
required VoidCallback onDispose,
@@ -140,6 +141,7 @@ void showMessageActions({
tapPoint: tapPoint,
isMe: isMe,
messageText: messageText,
copyText: copyText,
controller: controller,
style: style,
interaction: interaction,
@@ -176,6 +178,7 @@ class _MessageActionsLayer extends StatefulWidget {
final Offset tapPoint;
final bool isMe;
final String? messageText;
final String? copyText;
final MessageActionsController controller;
final MessageActionsStyle style;
final MessageActionsInteraction interaction;
@@ -205,6 +208,7 @@ class _MessageActionsLayer extends StatefulWidget {
required this.tapPoint,
required this.isMe,
required this.messageText,
required this.copyText,
required this.controller,
required this.style,
required this.interaction,
@@ -496,15 +500,14 @@ class _MessageActionsLayerState extends State<_MessageActionsLayer>
List<_Action> _buildActions() {
final l10n = AppLocalizations.of(context)!;
final hasText =
widget.messageText != null && widget.messageText!.isNotEmpty;
final copyText = widget.copyText;
final canCopy = widget.allowCopy && copyText != null && copyText.isNotEmpty;
return <_Action>[
if (widget.onReply != null)
_Action(Symbols.reply, l10n.msgActionsReply, _reply),
if (widget.onForward != null)
_Action(Symbols.forward, l10n.msgActionsForward, _forward),
if (hasText && widget.allowCopy)
_Action(Symbols.content_copy, l10n.msgActionsCopy, _copy),
if (canCopy) _Action(Symbols.content_copy, l10n.msgActionsCopy, _copy),
if (widget.isMe && widget.onEdit != null)
_Action(Symbols.edit, l10n.msgActionsEdit, _edit),
if (widget.onPin != null)
@@ -642,7 +645,7 @@ class _MessageActionsLayerState extends State<_MessageActionsLayer>
}
Future<void> _copy() async {
final text = widget.messageText;
final text = widget.copyText;
if (text != null && text.isNotEmpty) {
await Clipboard.setData(ClipboardData(text: text));
if (!mounted) return;
+90
View File
@@ -0,0 +1,90 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:komet/backend/modules/messages.dart';
import 'package:komet/frontend/screens/chats/chat/view/selection_bar.dart';
import 'package:material_symbols_icons/symbols.dart';
const int _me = 1;
const int _chatId = 2;
CachedMessage _text(String id, String? text) => CachedMessage(
id: id,
accountId: _me,
chatId: _chatId,
senderId: _me,
text: text,
time: DateTime(2026, 1, 1, 12).millisecondsSinceEpoch,
);
CachedMessage _forwarded(String id, String originalText) =>
CachedMessage.fromPushPayload(_me, _chatId, {
'id': id,
'time': DateTime(2026, 1, 1, 12).millisecondsSinceEpoch,
'type': 'USER',
'sender': _me,
'link': {
'type': 'FORWARD',
'message': {
'id': '900',
'time': 1000,
'type': 'USER',
'sender': 5,
'text': originalText,
'attaches': const [],
},
'chatId': -30,
'chatName': 'Synthetic Channel',
},
});
Future<List<CachedMessage>?> _tapCopy(
WidgetTester tester,
List<CachedMessage> copyMsgs, {
required bool glossy,
}) async {
List<CachedMessage>? copied;
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: SelectionTopBar(
cs: ThemeData.light().colorScheme,
selected: copyMsgs.map((m) => m.id).toSet(),
glossy: glossy,
copyMsgs: copyMsgs,
editMsg: null,
onClear: () {},
onCopy: (msgs) => copied = msgs,
onEdit: (_) {},
onDelete: () {},
),
),
),
);
final copyButton = find.widgetWithIcon(IconButton, Symbols.content_copy);
if (copyButton.evaluate().isEmpty) return null;
await tester.tap(copyButton);
await tester.pump();
return copied;
}
void main() {
testWidgets('copy stays available for several selected messages', (
tester,
) async {
final msgs = [_text('1', 'первое'), _text('2', 'второе')];
for (final glossy in [false, true]) {
expect(await _tapCopy(tester, msgs, glossy: glossy), msgs);
}
});
testWidgets('copy is hidden when nothing carries text', (tester) async {
expect(await _tapCopy(tester, const [], glossy: false), isNull);
});
testWidgets('a forwarded message exposes its original text', (tester) async {
final forwarded = _forwarded('3', 'исходный текст');
expect(forwarded.text, isNull);
expect(forwarded.selectableText, 'исходный текст');
expect(await _tapCopy(tester, [forwarded], glossy: false), [forwarded]);
});
}