Skip to content

Commit a383afe

Browse files
committed
action_sheet: Add "Copy to clipboard" button
Fixes: #132
1 parent cadc37e commit a383afe

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

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

0 commit comments

Comments
 (0)