Skip to content

Commit 9a06553

Browse files
chrisbobbegnprice
authored andcommitted
action_sheet: Add "Copy message text" button
Fixes: #132
1 parent e1ca6a1 commit 9a06553

File tree

2 files changed

+82
-0
lines changed

2 files changed

+82
-0
lines changed

lib/widgets/action_sheet.dart

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import 'package:flutter/material.dart';
2+
import 'package:flutter/services.dart';
23
import 'package:share_plus/share_plus.dart';
34

45
import '../api/exception.dart';
56
import '../api/model/model.dart';
67
import '../api/route/messages.dart';
8+
import 'clipboard.dart';
79
import 'compose_box.dart';
810
import 'dialog.dart';
911
import 'draggable_scrollable_modal_bottom_sheet.dart';
@@ -28,6 +30,7 @@ void showMessageActionSheet({required BuildContext context, required Message mes
2830
message: message,
2931
messageListContext: context,
3032
),
33+
CopyButton(message: message, messageListContext: context),
3134
]);
3235
});
3336
}
@@ -190,3 +193,36 @@ class QuoteAndReplyButton extends MessageActionSheetMenuItemButton {
190193
}
191194
};
192195
}
196+
197+
class CopyButton extends MessageActionSheetMenuItemButton {
198+
CopyButton({
199+
super.key,
200+
required super.message,
201+
required super.messageListContext,
202+
});
203+
204+
@override get icon => Icons.copy;
205+
206+
@override get label => 'Copy message text';
207+
208+
@override get onPressed => (BuildContext context) async {
209+
// Close the message action sheet. We won't be showing request progress,
210+
// but hopefully it won't take long at all, and
211+
// fetchRawContentWithFeedback has a TODO for giving feedback if it does.
212+
Navigator.of(context).pop();
213+
214+
final rawContent = await fetchRawContentWithFeedback(
215+
context: messageListContext,
216+
messageId: message.id,
217+
errorDialogTitle: 'Copying failed',
218+
);
219+
220+
if (rawContent == null) return;
221+
222+
if (!messageListContext.mounted) return;
223+
224+
// TODO(i18n)
225+
copyWithPopup(context: context, successContent: const Text('Message copied'),
226+
data: ClipboardData(text: rawContent));
227+
};
228+
}

test/widgets/action_sheet_test.dart

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import 'package:checks/checks.dart';
22
import 'package:flutter/material.dart';
3+
import 'package:flutter/services.dart';
34
import 'package:flutter_test/flutter_test.dart';
45
import 'package:zulip/api/model/model.dart';
56
import 'package:zulip/api/route/messages.dart';
@@ -16,6 +17,7 @@ import '../example_data.dart' as eg;
1617
import '../flutter_checks.dart';
1718
import '../model/binding.dart';
1819
import '../model/test_store.dart';
20+
import '../test_clipboard.dart';
1921
import 'compose_box_checks.dart';
2022
import 'dialog_checks.dart';
2123

@@ -221,4 +223,48 @@ void main() {
221223
check(findQuoteAndReplyButton(tester)).isNull();
222224
});
223225
});
226+
227+
group('CopyButton', () {
228+
setUp(() async {
229+
TestZulipBinding.ensureInitialized();
230+
TestWidgetsFlutterBinding.ensureInitialized();
231+
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(
232+
SystemChannels.platform,
233+
MockClipboard().handleMethodCall,
234+
);
235+
});
236+
237+
tearDown(() async {
238+
TestZulipBinding.instance.reset();
239+
});
240+
241+
testWidgets('success', (WidgetTester tester) async {
242+
final message = eg.streamMessage();
243+
await setupToMessageActionSheet(tester, message: message, narrow: TopicNarrow.ofMessage(message));
244+
final store = await TestZulipBinding.instance.globalStore.perAccount(eg.selfAccount.id);
245+
246+
await tester.ensureVisible(find.byIcon(Icons.copy, skipOffstage: false));
247+
prepareRawContentResponseSuccess(store, message: message, rawContent: 'Hello world');
248+
await tester.tap(find.byIcon(Icons.copy));
249+
await tester.pump(Duration.zero);
250+
check(await Clipboard.getData('text/plain')).isNotNull().text.equals('Hello world');
251+
});
252+
253+
testWidgets('request has an error', (WidgetTester tester) async {
254+
final message = eg.streamMessage();
255+
await setupToMessageActionSheet(tester, message: message, narrow: TopicNarrow.ofMessage(message));
256+
final store = await TestZulipBinding.instance.globalStore.perAccount(eg.selfAccount.id);
257+
258+
await tester.ensureVisible(find.byIcon(Icons.copy, skipOffstage: false));
259+
prepareRawContentResponseError(store);
260+
await tester.tap(find.byIcon(Icons.copy));
261+
await tester.pump(Duration.zero); // error arrives; error dialog shows
262+
263+
await tester.tap(find.byWidget(checkErrorDialog(tester,
264+
expectedTitle: 'Copying failed',
265+
expectedMessage: 'That message does not seem to exist.',
266+
)));
267+
check(await Clipboard.getData('text/plain')).isNull();
268+
});
269+
});
224270
}

0 commit comments

Comments
 (0)