From 3585ecd72ce36d3703b8eabbc1adea328588f31f Mon Sep 17 00:00:00 2001 From: Vatsal Date: Fri, 29 Mar 2024 12:38:02 +0530 Subject: [PATCH] launch_url: Deduplicate launchUrl functions - Deduplicate logic for realm-based and non-realm URLs. - Utilize i18n for consistent error messaging. - Refactor error handling into a private function. - Move shared functionality to a new file: ```dart lib/widgets/launch_url.dart ```. --- lib/widgets/content.dart | 55 ++------------------------------ lib/widgets/launch_url.dart | 62 +++++++++++++++++++++++++++++++++++++ lib/widgets/login.dart | 29 ++--------------- 3 files changed, 66 insertions(+), 80 deletions(-) create mode 100644 lib/widgets/launch_url.dart diff --git a/lib/widgets/content.dart b/lib/widgets/content.dart index c023d6d7bec..53f9a61f9a6 100644 --- a/lib/widgets/content.dart +++ b/lib/widgets/content.dart @@ -1,7 +1,5 @@ -import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:html/dom.dart' as dom; import 'package:intl/intl.dart'; import 'package:flutter_gen/gen_l10n/zulip_localizations.dart'; @@ -9,14 +7,11 @@ import 'package:flutter_gen/gen_l10n/zulip_localizations.dart'; import '../api/core.dart'; import '../api/model/model.dart'; import '../model/avatar_url.dart'; -import '../model/binding.dart'; import '../model/content.dart'; -import '../model/internal_link.dart'; import 'code_block.dart'; -import 'dialog.dart'; import 'icons.dart'; +import 'launch_url.dart'; import 'lightbox.dart'; -import 'message_list.dart'; import 'store.dart'; import 'text.dart'; @@ -507,7 +502,7 @@ class _BlockInlineContainerState extends State<_BlockInlineContainer> { void _prepareRecognizers() { _recognizers.addEntries(widget.links.map((node) => MapEntry(node, - TapGestureRecognizer()..onTap = () => _launchUrl(context, node.url)))); + TapGestureRecognizer()..onTap = () => launchUrlWithRealm(context, node.url)))); } void _disposeRecognizers() { @@ -868,52 +863,6 @@ class GlobalTime extends StatelessWidget { } } -void _launchUrl(BuildContext context, String urlString) async { - Future showError(BuildContext context, String? message) { - return showErrorDialog(context: context, - title: 'Unable to open link', - message: [ - 'Link could not be opened: $urlString', - if (message != null) message, - ].join("\n\n")); - } - - final store = PerAccountStoreWidget.of(context); - final url = store.tryResolveUrl(urlString); - if (url == null) { // TODO(log) - await showError(context, null); - return; - } - - final internalNarrow = parseInternalLink(url, store); - if (internalNarrow != null) { - Navigator.push(context, - MessageListPage.buildRoute(context: context, - narrow: internalNarrow)); - return; - } - - bool launched = false; - String? errorMessage; - try { - launched = await ZulipBinding.instance.launchUrl(url, - mode: switch (defaultTargetPlatform) { - // On iOS we prefer LaunchMode.externalApplication because (for - // HTTP URLs) LaunchMode.platformDefault uses SFSafariViewController, - // which gives an awkward UX as described here: - // https://chat.zulip.org/#narrow/stream/48-mobile/topic/in-app.20browser/near/1169118 - TargetPlatform.iOS => UrlLaunchMode.externalApplication, - _ => UrlLaunchMode.platformDefault, - }); - } on PlatformException catch (e) { - errorMessage = e.message; - } - if (!launched) { // TODO(log) - if (!context.mounted) return; - await showError(context, errorMessage); - } -} - /// Like [Image.network], but includes [authHeader] if [src] is on-realm. /// /// Use this to present image content in the ambient realm: avatars, images in diff --git a/lib/widgets/launch_url.dart b/lib/widgets/launch_url.dart new file mode 100644 index 00000000000..3c162c30bbe --- /dev/null +++ b/lib/widgets/launch_url.dart @@ -0,0 +1,62 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_gen/gen_l10n/zulip_localizations.dart'; + +import '../model/binding.dart'; +import '../model/internal_link.dart'; +import 'dialog.dart'; +import 'message_list.dart'; +import 'store.dart'; + +/// Handles showing an error dialog with a customizable message. +Future _showError(BuildContext context, String? message, String urlString) { + return showErrorDialog( + context: context, + title: ZulipLocalizations.of(context).errorUnableToOpenLinkTitle, + message: [ + ZulipLocalizations.of(context).errorLinkCouldNotBeOpened(urlString), + if (message != null) message, + ].join("\n\n")); +} + +/// Launches a URL without considering a realm base URL. +void launchUrlWithoutRealm(BuildContext context, String urlString) async { + bool launched = false; + String? errorMessage; + try { + launched = await ZulipBinding.instance.launchUrl(Uri.parse(urlString), + mode: switch (defaultTargetPlatform) { + // On iOS we prefer LaunchMode.externalApplication because (for + // HTTP URLs) LaunchMode.platformDefault uses SFSafariViewController, + // which gives an awkward UX as described here: + // https://chat.zulip.org/#narrow/stream/48-mobile/topic/in-app.20browser/near/1169118 + TargetPlatform.iOS => UrlLaunchMode.externalApplication, + _ => UrlLaunchMode.platformDefault, + }); + } on PlatformException catch (e) { + errorMessage = e.message; + } + if (!launched) { + if (!context.mounted) return; + await _showError(context, errorMessage, urlString); + } +} + +/// Launches a URL considering a realm base URL (if available). +void launchUrlWithRealm(BuildContext context, String urlString) async { + final store = PerAccountStoreWidget.of(context); + final url = store.tryResolveUrl(urlString); + if (url == null) { // TODO(log) + await _showError(context, null, urlString); + return; + } + + final internalNarrow = parseInternalLink(url, store); + if (internalNarrow != null) { + Navigator.push(context, MessageListPage.buildRoute(context: context, narrow: internalNarrow)); + return; + } + + launchUrlWithoutRealm(context, url.toString()); +} diff --git a/lib/widgets/login.dart b/lib/widgets/login.dart index 70296ba176a..0996dc0bf2b 100644 --- a/lib/widgets/login.dart +++ b/lib/widgets/login.dart @@ -1,16 +1,15 @@ import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_gen/gen_l10n/zulip_localizations.dart'; import '../api/exception.dart'; import '../api/route/account.dart'; import '../api/route/realm.dart'; import '../api/route/users.dart'; -import '../model/binding.dart'; import '../model/store.dart'; import 'app.dart'; import 'dialog.dart'; import 'input.dart'; +import 'launch_url.dart'; import 'page.dart'; import 'store.dart'; @@ -228,7 +227,7 @@ class _AddAccountPageState extends State { errorText: errorText, helper: GestureDetector( onTap: () { - _launchUrl(context); + launchUrlWithoutRealm(context, AddAccountPage.serverUrlHelpUrl); }, child: Text( zulipLocalizations.serverUrlDocLinkLabel, @@ -245,30 +244,6 @@ class _AddAccountPageState extends State { } } -void _launchUrl(BuildContext context) async { - Future showError(BuildContext context, String? message) { - return showErrorDialog( - context: context, - title: ZulipLocalizations.of(context).errorUnableToOpenLinkTitle, - message: [ - ZulipLocalizations.of(context).errorLinkCouldNotBeOpened(AddAccountPage.serverUrlHelpUrl), - if (message != null) message, - ].join("\n\n")); - } - - bool launched = false; - String? errorMessage; - try { - launched = await ZulipBinding.instance.launchUrl(Uri.parse(AddAccountPage.serverUrlHelpUrl)); - } on PlatformException catch (e) { - errorMessage = e.message; - } - if (!launched) { - if (!context.mounted) return; - await showError(context, errorMessage); - } -} - class PasswordLoginPage extends StatefulWidget { const PasswordLoginPage({super.key, required this.serverSettings});