Skip to content

Commit b1ac761

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: #43
1 parent f7421bf commit b1ac761

File tree

7 files changed

+103
-2
lines changed

7 files changed

+103
-2
lines changed

assets/l10n/app_en.arb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,10 @@
338338
"@errorDialogTitle": {
339339
"description": "Generic title for error dialog."
340340
},
341+
"errorShareFailed": "Error Sharing the Image",
342+
"@errorShareFailed": {
343+
"description": "Title for sharing image error dialog."
344+
},
341345
"snackBarDetails": "Details",
342346
"@snackBarDetails": {
343347
"description": "Button label for snack bar button that opens a dialog with more details."
@@ -346,6 +350,10 @@
346350
"@lightboxCopyLinkTooltip": {
347351
"description": "Tooltip in lightbox for the copy link action."
348352
},
353+
"lightboxShareImageTooltip": "Share Image",
354+
"@lightboxShareImageTooltip": {
355+
"description": "Tooltip in lightbox for the Share Image action."
356+
},
349357
"loginPageTitle": "Log in",
350358
"@loginPageTitle": {
351359
"description": "Page title for login page."

lib/generated/l10n/zulip_localizations.dart

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -541,6 +541,12 @@ abstract class ZulipLocalizations {
541541
/// **'Error'**
542542
String get errorDialogTitle;
543543

544+
/// Title for sharing image error dialog.
545+
///
546+
/// In en, this message translates to:
547+
/// **'Error Sharing the Image'**
548+
String get errorShareFailed;
549+
544550
/// Button label for snack bar button that opens a dialog with more details.
545551
///
546552
/// In en, this message translates to:
@@ -553,6 +559,12 @@ abstract class ZulipLocalizations {
553559
/// **'Copy link'**
554560
String get lightboxCopyLinkTooltip;
555561

562+
/// Tooltip in lightbox for the Share Image action.
563+
///
564+
/// In en, this message translates to:
565+
/// **'Share Image'**
566+
String get lightboxShareImageTooltip;
567+
556568
/// Page title for login page.
557569
///
558570
/// In en, this message translates to:

lib/generated/l10n/zulip_localizations_ar.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,12 +266,18 @@ class ZulipLocalizationsAr extends ZulipLocalizations {
266266
@override
267267
String get errorDialogTitle => 'Error';
268268

269+
@override
270+
String get errorShareFailed => 'Error Sharing the Image';
271+
269272
@override
270273
String get snackBarDetails => 'Details';
271274

272275
@override
273276
String get lightboxCopyLinkTooltip => 'Copy link';
274277

278+
@override
279+
String get lightboxShareImageTooltip => 'Share Image';
280+
275281
@override
276282
String get loginPageTitle => 'Log in';
277283

lib/generated/l10n/zulip_localizations_en.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,12 +266,18 @@ class ZulipLocalizationsEn extends ZulipLocalizations {
266266
@override
267267
String get errorDialogTitle => 'Error';
268268

269+
@override
270+
String get errorShareFailed => 'Error Sharing the Image';
271+
269272
@override
270273
String get snackBarDetails => 'Details';
271274

272275
@override
273276
String get lightboxCopyLinkTooltip => 'Copy link';
274277

278+
@override
279+
String get lightboxShareImageTooltip => 'Share Image';
280+
275281
@override
276282
String get loginPageTitle => 'Log in';
277283

lib/generated/l10n/zulip_localizations_ja.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,12 +266,18 @@ class ZulipLocalizationsJa extends ZulipLocalizations {
266266
@override
267267
String get errorDialogTitle => 'Error';
268268

269+
@override
270+
String get errorShareFailed => 'Error Sharing the Image';
271+
269272
@override
270273
String get snackBarDetails => 'Details';
271274

272275
@override
273276
String get lightboxCopyLinkTooltip => 'Copy link';
274277

278+
@override
279+
String get lightboxShareImageTooltip => 'Share Image';
280+
275281
@override
276282
String get loginPageTitle => 'Log in';
277283

lib/widgets/lightbox.dart

Lines changed: 43 additions & 2 deletions
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 httpClient;
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 httpClient.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

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,28 @@ 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+
297+
debugNetworkImageHttpClientProvider = null;
298+
});
299+
278300
// TODO test _CopyLinkButton
279301
// TODO test thumbnail gets shown, then gets replaced when main image loads
280302
// TODO test image is scaled down to fit, but not up

0 commit comments

Comments
 (0)