Skip to content

Commit a8397e9

Browse files
lightbox: Add "share" button in bottom app bar
Add a share button to the lightbox that allows users to share image URLs. The button appears in the bottom app bar with a share icon and tooltip. Test coverage includes verifying the share button's UI elements (icon and tooltip). Fixes: zulip#43
1 parent 28b3536 commit a8397e9

10 files changed

+122
-2
lines changed

assets/l10n/app_en.arb

+8
Original file line numberDiff line numberDiff line change
@@ -387,6 +387,10 @@
387387
"@errorDialogTitle": {
388388
"description": "Generic title for error dialog."
389389
},
390+
"errorShareFailed": "Failed to share the image",
391+
"@errorShareFailed": {
392+
"description": "Title for sharing image error dialog."
393+
},
390394
"snackBarDetails": "Details",
391395
"@snackBarDetails": {
392396
"description": "Button label for snack bar button that opens a dialog with more details."
@@ -395,6 +399,10 @@
395399
"@lightboxCopyLinkTooltip": {
396400
"description": "Tooltip in lightbox for the copy link action."
397401
},
402+
"lightboxShareImageTooltip": "Share Image",
403+
"@lightboxShareImageTooltip": {
404+
"description": "Tooltip in lightbox for the Share Image action."
405+
},
398406
"loginPageTitle": "Log in",
399407
"@loginPageTitle": {
400408
"description": "Title for login page."

lib/generated/l10n/zulip_localizations.dart

+12
Original file line numberDiff line numberDiff line change
@@ -619,6 +619,12 @@ abstract class ZulipLocalizations {
619619
/// **'Error'**
620620
String get errorDialogTitle;
621621

622+
/// Title for sharing image error dialog.
623+
///
624+
/// In en, this message translates to:
625+
/// **'Failed to share the image'**
626+
String get errorShareFailed;
627+
622628
/// Button label for snack bar button that opens a dialog with more details.
623629
///
624630
/// In en, this message translates to:
@@ -631,6 +637,12 @@ abstract class ZulipLocalizations {
631637
/// **'Copy link'**
632638
String get lightboxCopyLinkTooltip;
633639

640+
/// Tooltip in lightbox for the Share Image action.
641+
///
642+
/// In en, this message translates to:
643+
/// **'Share Image'**
644+
String get lightboxShareImageTooltip;
645+
634646
/// Title for login page.
635647
///
636648
/// In en, this message translates to:

lib/generated/l10n/zulip_localizations_ar.dart

+6
Original file line numberDiff line numberDiff line change
@@ -304,12 +304,18 @@ class ZulipLocalizationsAr extends ZulipLocalizations {
304304
@override
305305
String get errorDialogTitle => 'Error';
306306

307+
@override
308+
String get errorShareFailed => 'Failed to share the image';
309+
307310
@override
308311
String get snackBarDetails => 'Details';
309312

310313
@override
311314
String get lightboxCopyLinkTooltip => 'Copy link';
312315

316+
@override
317+
String get lightboxShareImageTooltip => 'Share Image';
318+
313319
@override
314320
String get loginPageTitle => 'Log in';
315321

lib/generated/l10n/zulip_localizations_en.dart

+6
Original file line numberDiff line numberDiff line change
@@ -304,12 +304,18 @@ class ZulipLocalizationsEn extends ZulipLocalizations {
304304
@override
305305
String get errorDialogTitle => 'Error';
306306

307+
@override
308+
String get errorShareFailed => 'Failed to share the image';
309+
307310
@override
308311
String get snackBarDetails => 'Details';
309312

310313
@override
311314
String get lightboxCopyLinkTooltip => 'Copy link';
312315

316+
@override
317+
String get lightboxShareImageTooltip => 'Share Image';
318+
313319
@override
314320
String get loginPageTitle => 'Log in';
315321

lib/generated/l10n/zulip_localizations_fr.dart

+6
Original file line numberDiff line numberDiff line change
@@ -304,12 +304,18 @@ class ZulipLocalizationsFr extends ZulipLocalizations {
304304
@override
305305
String get errorDialogTitle => 'Error';
306306

307+
@override
308+
String get errorShareFailed => 'Failed to share the image';
309+
307310
@override
308311
String get snackBarDetails => 'Details';
309312

310313
@override
311314
String get lightboxCopyLinkTooltip => 'Copy link';
312315

316+
@override
317+
String get lightboxShareImageTooltip => 'Share Image';
318+
313319
@override
314320
String get loginPageTitle => 'Log in';
315321

lib/generated/l10n/zulip_localizations_ja.dart

+6
Original file line numberDiff line numberDiff line change
@@ -304,12 +304,18 @@ class ZulipLocalizationsJa extends ZulipLocalizations {
304304
@override
305305
String get errorDialogTitle => 'Error';
306306

307+
@override
308+
String get errorShareFailed => 'Failed to share the image';
309+
307310
@override
308311
String get snackBarDetails => 'Details';
309312

310313
@override
311314
String get lightboxCopyLinkTooltip => 'Copy link';
312315

316+
@override
317+
String get lightboxShareImageTooltip => 'Share Image';
318+
313319
@override
314320
String get loginPageTitle => 'Log in';
315321

lib/generated/l10n/zulip_localizations_pl.dart

+6
Original file line numberDiff line numberDiff line change
@@ -304,12 +304,18 @@ class ZulipLocalizationsPl extends ZulipLocalizations {
304304
@override
305305
String get errorDialogTitle => 'Błąd';
306306

307+
@override
308+
String get errorShareFailed => 'Failed to share the image';
309+
307310
@override
308311
String get snackBarDetails => 'Szczegóły';
309312

310313
@override
311314
String get lightboxCopyLinkTooltip => 'Skopiuj odnośnik';
312315

316+
@override
317+
String get lightboxShareImageTooltip => 'Share Image';
318+
313319
@override
314320
String get loginPageTitle => 'Zaloguj';
315321

lib/generated/l10n/zulip_localizations_ru.dart

+6
Original file line numberDiff line numberDiff line change
@@ -304,12 +304,18 @@ class ZulipLocalizationsRu extends ZulipLocalizations {
304304
@override
305305
String get errorDialogTitle => 'Error';
306306

307+
@override
308+
String get errorShareFailed => 'Failed to share the image';
309+
307310
@override
308311
String get snackBarDetails => 'Details';
309312

310313
@override
311314
String get lightboxCopyLinkTooltip => 'Copy link';
312315

316+
@override
317+
String get lightboxShareImageTooltip => 'Share Image';
318+
313319
@override
314320
String get loginPageTitle => 'Log in';
315321

lib/widgets/lightbox.dart

+43-2
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@ import 'package:flutter/material.dart';
22
import 'package:flutter/scheduler.dart';
33
import 'package:flutter/services.dart';
44
import 'package:intl/intl.dart';
5+
import 'package:share_plus/share_plus.dart';
56
import 'package:video_player/video_player.dart';
6-
7+
import 'package:http/http.dart' as http;
78
import '../api/core.dart';
89
import '../api/model/model.dart';
910
import '../generated/l10n/zulip_localizations.dart';
@@ -89,6 +90,46 @@ class _CopyLinkButton extends StatelessWidget {
8990
}
9091
}
9192

93+
Future<XFile> _downloadImage(Uri url, Map<String, String> headers) async {
94+
final response = await http.get(url, headers: headers);
95+
final bytes = response.bodyBytes;
96+
return XFile.fromData(bytes,
97+
name: url.pathSegments.last,
98+
mimeType: response.headers['content-type']);
99+
}
100+
101+
class _ShareButton extends StatelessWidget {
102+
const _ShareButton({required this.url});
103+
104+
final Uri url;
105+
106+
@override
107+
Widget build(BuildContext context) {
108+
final zulipLocalizations = ZulipLocalizations.of(context);
109+
return IconButton(
110+
tooltip: zulipLocalizations.lightboxShareImageTooltip,
111+
icon: const Icon(Icons.share),
112+
onPressed: () async {
113+
try {
114+
final store = PerAccountStoreWidget.of(context);
115+
final headers = {
116+
if (url.origin == store.account.realmUrl.origin)
117+
...authHeader(email: store.account.email, apiKey: store.account.apiKey),
118+
...userAgentHeader()
119+
};
120+
final xFile = await _downloadImage(url, headers);
121+
await Share.shareXFiles([xFile]);
122+
} catch (error) {
123+
if (!context.mounted) return;
124+
showErrorDialog(
125+
context: context,
126+
title: zulipLocalizations.errorDialogTitle,
127+
message: zulipLocalizations.errorShareFailed);
128+
}
129+
});
130+
}
131+
}
132+
92133
class _LightboxPageLayout extends StatefulWidget {
93134
const _LightboxPageLayout({
94135
required this.routeEntranceAnimation,
@@ -258,7 +299,7 @@ class _ImageLightboxPageState extends State<_ImageLightboxPage> {
258299
elevation: elevation,
259300
child: Row(children: [
260301
_CopyLinkButton(url: widget.src),
261-
// TODO(#43): Share image
302+
_ShareButton(url: widget.src),
262303
// TODO(#42): Download image
263304
]),
264305
);

test/widgets/lightbox_test.dart

+23
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,29 @@ void main() {
275275
debugNetworkImageHttpClientProvider = null;
276276
});
277277

278+
testWidgets('share button shows correct icon and downloads image', (tester) async {
279+
prepareBoringImageHttpClient();
280+
final message = eg.streamMessage();
281+
await setupPage(tester, message: message, thumbnailUrl: null);
282+
283+
// Verify share icon exists
284+
final shareIcon = find.descendant(
285+
of: find.byType(BottomAppBar),
286+
matching: find.byIcon(Icons.share),
287+
skipOffstage: false);
288+
check(tester.widget<Icon>(shareIcon).icon).equals(Icons.share);
289+
290+
// Verify tooltip
291+
final button = tester.widget<IconButton>(find.ancestor(
292+
of: shareIcon,
293+
matching: find.byType(IconButton)));
294+
final zulipLocalizations = GlobalLocalizations.zulipLocalizations;
295+
check(button.tooltip).equals(zulipLocalizations.lightboxShareImageTooltip);
296+
check(button.tooltip).equals(zulipLocalizations.lightboxShareImageTooltip);
297+
298+
debugNetworkImageHttpClientProvider = null;
299+
});
300+
278301
// TODO test _CopyLinkButton
279302
// TODO test thumbnail gets shown, then gets replaced when main image loads
280303
// TODO test image is scaled down to fit, but not up

0 commit comments

Comments
 (0)