From 9ac3d16e95fd0952d6e4d562f7d21c63e1a3b3bd Mon Sep 17 00:00:00 2001 From: Shivansh Sharma Date: Fri, 13 Dec 2024 13:53:51 +0530 Subject: [PATCH] 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 --- assets/l10n/app_en.arb | 8 ++++ lib/generated/l10n/zulip_localizations.dart | 12 +++++ .../l10n/zulip_localizations_ar.dart | 6 +++ .../l10n/zulip_localizations_en.dart | 6 +++ .../l10n/zulip_localizations_ja.dart | 6 +++ lib/widgets/lightbox.dart | 45 ++++++++++++++++++- test/widgets/lightbox_test.dart | 22 +++++++++ 7 files changed, 103 insertions(+), 2 deletions(-) diff --git a/assets/l10n/app_en.arb b/assets/l10n/app_en.arb index cf816f50c4..8e2222168d 100644 --- a/assets/l10n/app_en.arb +++ b/assets/l10n/app_en.arb @@ -338,6 +338,10 @@ "@errorDialogTitle": { "description": "Generic title for error dialog." }, + "errorShareFailed": "Error Sharing the Image", + "@errorShareFailed": { + "description": "Title for sharing image error dialog." + }, "snackBarDetails": "Details", "@snackBarDetails": { "description": "Button label for snack bar button that opens a dialog with more details." @@ -346,6 +350,10 @@ "@lightboxCopyLinkTooltip": { "description": "Tooltip in lightbox for the copy link action." }, + "lightboxShareImageTooltip": "Share Image", + "@lightboxShareImageTooltip": { + "description": "Tooltip in lightbox for the Share Image action." + }, "loginPageTitle": "Log in", "@loginPageTitle": { "description": "Page title for login page." diff --git a/lib/generated/l10n/zulip_localizations.dart b/lib/generated/l10n/zulip_localizations.dart index 972b1e1ad2..50c49cca38 100644 --- a/lib/generated/l10n/zulip_localizations.dart +++ b/lib/generated/l10n/zulip_localizations.dart @@ -541,6 +541,12 @@ abstract class ZulipLocalizations { /// **'Error'** String get errorDialogTitle; + /// Title for sharing image error dialog. + /// + /// In en, this message translates to: + /// **'Error Sharing the Image'** + String get errorShareFailed; + /// Button label for snack bar button that opens a dialog with more details. /// /// In en, this message translates to: @@ -553,6 +559,12 @@ abstract class ZulipLocalizations { /// **'Copy link'** String get lightboxCopyLinkTooltip; + /// Tooltip in lightbox for the Share Image action. + /// + /// In en, this message translates to: + /// **'Share Image'** + String get lightboxShareImageTooltip; + /// Page title for login page. /// /// In en, this message translates to: diff --git a/lib/generated/l10n/zulip_localizations_ar.dart b/lib/generated/l10n/zulip_localizations_ar.dart index 95ff1d0aea..e6bf55988a 100644 --- a/lib/generated/l10n/zulip_localizations_ar.dart +++ b/lib/generated/l10n/zulip_localizations_ar.dart @@ -266,12 +266,18 @@ class ZulipLocalizationsAr extends ZulipLocalizations { @override String get errorDialogTitle => 'Error'; + @override + String get errorShareFailed => 'Error Sharing the Image'; + @override String get snackBarDetails => 'Details'; @override String get lightboxCopyLinkTooltip => 'Copy link'; + @override + String get lightboxShareImageTooltip => 'Share Image'; + @override String get loginPageTitle => 'Log in'; diff --git a/lib/generated/l10n/zulip_localizations_en.dart b/lib/generated/l10n/zulip_localizations_en.dart index d440ed2b10..3bcc5ffa08 100644 --- a/lib/generated/l10n/zulip_localizations_en.dart +++ b/lib/generated/l10n/zulip_localizations_en.dart @@ -266,12 +266,18 @@ class ZulipLocalizationsEn extends ZulipLocalizations { @override String get errorDialogTitle => 'Error'; + @override + String get errorShareFailed => 'Error Sharing the Image'; + @override String get snackBarDetails => 'Details'; @override String get lightboxCopyLinkTooltip => 'Copy link'; + @override + String get lightboxShareImageTooltip => 'Share Image'; + @override String get loginPageTitle => 'Log in'; diff --git a/lib/generated/l10n/zulip_localizations_ja.dart b/lib/generated/l10n/zulip_localizations_ja.dart index 42128ba024..a9be7bf53f 100644 --- a/lib/generated/l10n/zulip_localizations_ja.dart +++ b/lib/generated/l10n/zulip_localizations_ja.dart @@ -266,12 +266,18 @@ class ZulipLocalizationsJa extends ZulipLocalizations { @override String get errorDialogTitle => 'Error'; + @override + String get errorShareFailed => 'Error Sharing the Image'; + @override String get snackBarDetails => 'Details'; @override String get lightboxCopyLinkTooltip => 'Copy link'; + @override + String get lightboxShareImageTooltip => 'Share Image'; + @override String get loginPageTitle => 'Log in'; diff --git a/lib/widgets/lightbox.dart b/lib/widgets/lightbox.dart index 7e4141db63..d607543538 100644 --- a/lib/widgets/lightbox.dart +++ b/lib/widgets/lightbox.dart @@ -2,8 +2,9 @@ import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; import 'package:intl/intl.dart'; +import 'package:share_plus/share_plus.dart'; import 'package:video_player/video_player.dart'; - +import 'package:http/http.dart' as httpClient; import '../api/core.dart'; import '../api/model/model.dart'; import '../generated/l10n/zulip_localizations.dart'; @@ -89,6 +90,46 @@ class _CopyLinkButton extends StatelessWidget { } } +Future _downloadImage(Uri url, Map headers) async { + final response = await httpClient.get(url, headers: headers); + final bytes = response.bodyBytes; + return XFile.fromData(bytes, + name: url.pathSegments.last, + mimeType: response.headers['content-type']); +} + +class _ShareButton extends StatelessWidget { + const _ShareButton({required this.url}); + + final Uri url; + + @override + Widget build(BuildContext context) { + final zulipLocalizations = ZulipLocalizations.of(context); + return IconButton( + tooltip: zulipLocalizations.lightboxShareImageTooltip, + icon: const Icon(Icons.share), + onPressed: () async { + try { + final store = PerAccountStoreWidget.of(context); + final headers = { + if (url.origin == store.account.realmUrl.origin) + ...authHeader(email: store.account.email, apiKey: store.account.apiKey), + ...userAgentHeader() + }; + final xFile = await _downloadImage(url, headers); + await Share.shareXFiles([xFile]); + } catch (error) { + if (!context.mounted) return; + showErrorDialog( + context: context, + title: zulipLocalizations.errorDialogTitle, + message: zulipLocalizations.errorShareFailed); + } + }); + } +} + class _LightboxPageLayout extends StatefulWidget { const _LightboxPageLayout({ required this.routeEntranceAnimation, @@ -258,7 +299,7 @@ class _ImageLightboxPageState extends State<_ImageLightboxPage> { elevation: elevation, child: Row(children: [ _CopyLinkButton(url: widget.src), - // TODO(#43): Share image + _ShareButton(url: widget.src), // TODO(#42): Download image ]), ); diff --git a/test/widgets/lightbox_test.dart b/test/widgets/lightbox_test.dart index 4a84f79b27..8a7ea7c94a 100644 --- a/test/widgets/lightbox_test.dart +++ b/test/widgets/lightbox_test.dart @@ -275,6 +275,28 @@ void main() { debugNetworkImageHttpClientProvider = null; }); + testWidgets('share button shows correct icon and downloads image', (tester) async { + prepareBoringImageHttpClient(); + final message = eg.streamMessage(); + await setupPage(tester, message: message, thumbnailUrl: null); + + // Verify share icon exists + final shareIcon = find.descendant( + of: find.byType(BottomAppBar), + matching: find.byIcon(Icons.share), + skipOffstage: false); + check(tester.widget(shareIcon).icon).equals(Icons.share); + + // Verify tooltip + final button = tester.widget(find.ancestor( + of: shareIcon, + matching: find.byType(IconButton))); + final zulipLocalizations = GlobalLocalizations.zulipLocalizations; + check(button.tooltip).equals(zulipLocalizations.lightboxShareImageTooltip); + + debugNetworkImageHttpClientProvider = null; + }); + // TODO test _CopyLinkButton // TODO test thumbnail gets shown, then gets replaced when main image loads // TODO test image is scaled down to fit, but not up