diff --git a/assets/l10n/app_en.arb b/assets/l10n/app_en.arb index 58822303fd..bde8798cfa 100644 --- a/assets/l10n/app_en.arb +++ b/assets/l10n/app_en.arb @@ -387,6 +387,10 @@ "@errorDialogTitle": { "description": "Generic title for error dialog." }, + "errorShareFailed": "Failed to share 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." @@ -395,6 +399,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": "Title for login page." diff --git a/lib/generated/l10n/zulip_localizations.dart b/lib/generated/l10n/zulip_localizations.dart index 00d7cfde72..0f6b7f5a3d 100644 --- a/lib/generated/l10n/zulip_localizations.dart +++ b/lib/generated/l10n/zulip_localizations.dart @@ -619,6 +619,12 @@ abstract class ZulipLocalizations { /// **'Error'** String get errorDialogTitle; + /// Title for sharing image error dialog. + /// + /// In en, this message translates to: + /// **'Failed to share the image'** + String get errorShareFailed; + /// Button label for snack bar button that opens a dialog with more details. /// /// In en, this message translates to: @@ -631,6 +637,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; + /// 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 542b85031b..7cb59a0cc2 100644 --- a/lib/generated/l10n/zulip_localizations_ar.dart +++ b/lib/generated/l10n/zulip_localizations_ar.dart @@ -304,12 +304,18 @@ class ZulipLocalizationsAr extends ZulipLocalizations { @override String get errorDialogTitle => 'Error'; + @override + String get errorShareFailed => 'Failed to share 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 b6bc9f72e7..fc2983b788 100644 --- a/lib/generated/l10n/zulip_localizations_en.dart +++ b/lib/generated/l10n/zulip_localizations_en.dart @@ -304,12 +304,18 @@ class ZulipLocalizationsEn extends ZulipLocalizations { @override String get errorDialogTitle => 'Error'; + @override + String get errorShareFailed => 'Failed to share 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_fr.dart b/lib/generated/l10n/zulip_localizations_fr.dart index c857da2c82..dbb1c3b0fb 100644 --- a/lib/generated/l10n/zulip_localizations_fr.dart +++ b/lib/generated/l10n/zulip_localizations_fr.dart @@ -304,12 +304,18 @@ class ZulipLocalizationsFr extends ZulipLocalizations { @override String get errorDialogTitle => 'Error'; + @override + String get errorShareFailed => 'Failed to share 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 7adbc9ae8a..b679d1c5e9 100644 --- a/lib/generated/l10n/zulip_localizations_ja.dart +++ b/lib/generated/l10n/zulip_localizations_ja.dart @@ -304,12 +304,18 @@ class ZulipLocalizationsJa extends ZulipLocalizations { @override String get errorDialogTitle => 'Error'; + @override + String get errorShareFailed => 'Failed to share 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_pl.dart b/lib/generated/l10n/zulip_localizations_pl.dart index 07746b3f27..3798cb622b 100644 --- a/lib/generated/l10n/zulip_localizations_pl.dart +++ b/lib/generated/l10n/zulip_localizations_pl.dart @@ -304,12 +304,18 @@ class ZulipLocalizationsPl extends ZulipLocalizations { @override String get errorDialogTitle => 'Błąd'; + @override + String get errorShareFailed => 'Failed to share the image'; + @override String get snackBarDetails => 'Szczegóły'; @override String get lightboxCopyLinkTooltip => 'Skopiuj odnośnik'; + @override + String get lightboxShareImageTooltip => 'Share Image'; + @override String get loginPageTitle => 'Zaloguj'; diff --git a/lib/generated/l10n/zulip_localizations_ru.dart b/lib/generated/l10n/zulip_localizations_ru.dart index 9c2065376b..16b02e4154 100644 --- a/lib/generated/l10n/zulip_localizations_ru.dart +++ b/lib/generated/l10n/zulip_localizations_ru.dart @@ -304,12 +304,18 @@ class ZulipLocalizationsRu extends ZulipLocalizations { @override String get errorDialogTitle => 'Error'; + @override + String get errorShareFailed => 'Failed to share 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..55a1fef748 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 http; 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 http.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..ff0336a291 100644 --- a/test/widgets/lightbox_test.dart +++ b/test/widgets/lightbox_test.dart @@ -275,6 +275,29 @@ 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); + 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