Skip to content

Commit 3585ecd

Browse files
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 ```.
1 parent f0c82eb commit 3585ecd

File tree

3 files changed

+66
-80
lines changed

3 files changed

+66
-80
lines changed

lib/widgets/content.dart

+2-53
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,17 @@
1-
import 'package:flutter/foundation.dart';
21
import 'package:flutter/gestures.dart';
32
import 'package:flutter/material.dart';
4-
import 'package:flutter/services.dart';
53
import 'package:html/dom.dart' as dom;
64
import 'package:intl/intl.dart';
75
import 'package:flutter_gen/gen_l10n/zulip_localizations.dart';
86

97
import '../api/core.dart';
108
import '../api/model/model.dart';
119
import '../model/avatar_url.dart';
12-
import '../model/binding.dart';
1310
import '../model/content.dart';
14-
import '../model/internal_link.dart';
1511
import 'code_block.dart';
16-
import 'dialog.dart';
1712
import 'icons.dart';
13+
import 'launch_url.dart';
1814
import 'lightbox.dart';
19-
import 'message_list.dart';
2015
import 'store.dart';
2116
import 'text.dart';
2217

@@ -507,7 +502,7 @@ class _BlockInlineContainerState extends State<_BlockInlineContainer> {
507502

508503
void _prepareRecognizers() {
509504
_recognizers.addEntries(widget.links.map((node) => MapEntry(node,
510-
TapGestureRecognizer()..onTap = () => _launchUrl(context, node.url))));
505+
TapGestureRecognizer()..onTap = () => launchUrlWithRealm(context, node.url))));
511506
}
512507

513508
void _disposeRecognizers() {
@@ -868,52 +863,6 @@ class GlobalTime extends StatelessWidget {
868863
}
869864
}
870865

871-
void _launchUrl(BuildContext context, String urlString) async {
872-
Future<void> showError(BuildContext context, String? message) {
873-
return showErrorDialog(context: context,
874-
title: 'Unable to open link',
875-
message: [
876-
'Link could not be opened: $urlString',
877-
if (message != null) message,
878-
].join("\n\n"));
879-
}
880-
881-
final store = PerAccountStoreWidget.of(context);
882-
final url = store.tryResolveUrl(urlString);
883-
if (url == null) { // TODO(log)
884-
await showError(context, null);
885-
return;
886-
}
887-
888-
final internalNarrow = parseInternalLink(url, store);
889-
if (internalNarrow != null) {
890-
Navigator.push(context,
891-
MessageListPage.buildRoute(context: context,
892-
narrow: internalNarrow));
893-
return;
894-
}
895-
896-
bool launched = false;
897-
String? errorMessage;
898-
try {
899-
launched = await ZulipBinding.instance.launchUrl(url,
900-
mode: switch (defaultTargetPlatform) {
901-
// On iOS we prefer LaunchMode.externalApplication because (for
902-
// HTTP URLs) LaunchMode.platformDefault uses SFSafariViewController,
903-
// which gives an awkward UX as described here:
904-
// https://chat.zulip.org/#narrow/stream/48-mobile/topic/in-app.20browser/near/1169118
905-
TargetPlatform.iOS => UrlLaunchMode.externalApplication,
906-
_ => UrlLaunchMode.platformDefault,
907-
});
908-
} on PlatformException catch (e) {
909-
errorMessage = e.message;
910-
}
911-
if (!launched) { // TODO(log)
912-
if (!context.mounted) return;
913-
await showError(context, errorMessage);
914-
}
915-
}
916-
917866
/// Like [Image.network], but includes [authHeader] if [src] is on-realm.
918867
///
919868
/// Use this to present image content in the ambient realm: avatars, images in

lib/widgets/launch_url.dart

+62
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import 'package:flutter/foundation.dart';
2+
import 'package:flutter/material.dart';
3+
import 'package:flutter/services.dart';
4+
import 'package:flutter_gen/gen_l10n/zulip_localizations.dart';
5+
6+
import '../model/binding.dart';
7+
import '../model/internal_link.dart';
8+
import 'dialog.dart';
9+
import 'message_list.dart';
10+
import 'store.dart';
11+
12+
/// Handles showing an error dialog with a customizable message.
13+
Future<void> _showError(BuildContext context, String? message, String urlString) {
14+
return showErrorDialog(
15+
context: context,
16+
title: ZulipLocalizations.of(context).errorUnableToOpenLinkTitle,
17+
message: [
18+
ZulipLocalizations.of(context).errorLinkCouldNotBeOpened(urlString),
19+
if (message != null) message,
20+
].join("\n\n"));
21+
}
22+
23+
/// Launches a URL without considering a realm base URL.
24+
void launchUrlWithoutRealm(BuildContext context, String urlString) async {
25+
bool launched = false;
26+
String? errorMessage;
27+
try {
28+
launched = await ZulipBinding.instance.launchUrl(Uri.parse(urlString),
29+
mode: switch (defaultTargetPlatform) {
30+
// On iOS we prefer LaunchMode.externalApplication because (for
31+
// HTTP URLs) LaunchMode.platformDefault uses SFSafariViewController,
32+
// which gives an awkward UX as described here:
33+
// https://chat.zulip.org/#narrow/stream/48-mobile/topic/in-app.20browser/near/1169118
34+
TargetPlatform.iOS => UrlLaunchMode.externalApplication,
35+
_ => UrlLaunchMode.platformDefault,
36+
});
37+
} on PlatformException catch (e) {
38+
errorMessage = e.message;
39+
}
40+
if (!launched) {
41+
if (!context.mounted) return;
42+
await _showError(context, errorMessage, urlString);
43+
}
44+
}
45+
46+
/// Launches a URL considering a realm base URL (if available).
47+
void launchUrlWithRealm(BuildContext context, String urlString) async {
48+
final store = PerAccountStoreWidget.of(context);
49+
final url = store.tryResolveUrl(urlString);
50+
if (url == null) { // TODO(log)
51+
await _showError(context, null, urlString);
52+
return;
53+
}
54+
55+
final internalNarrow = parseInternalLink(url, store);
56+
if (internalNarrow != null) {
57+
Navigator.push(context, MessageListPage.buildRoute(context: context, narrow: internalNarrow));
58+
return;
59+
}
60+
61+
launchUrlWithoutRealm(context, url.toString());
62+
}

lib/widgets/login.dart

+2-27
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,15 @@
11
import 'package:flutter/material.dart';
2-
import 'package:flutter/services.dart';
32
import 'package:flutter_gen/gen_l10n/zulip_localizations.dart';
43

54
import '../api/exception.dart';
65
import '../api/route/account.dart';
76
import '../api/route/realm.dart';
87
import '../api/route/users.dart';
9-
import '../model/binding.dart';
108
import '../model/store.dart';
119
import 'app.dart';
1210
import 'dialog.dart';
1311
import 'input.dart';
12+
import 'launch_url.dart';
1413
import 'page.dart';
1514
import 'store.dart';
1615

@@ -228,7 +227,7 @@ class _AddAccountPageState extends State<AddAccountPage> {
228227
errorText: errorText,
229228
helper: GestureDetector(
230229
onTap: () {
231-
_launchUrl(context);
230+
launchUrlWithoutRealm(context, AddAccountPage.serverUrlHelpUrl);
232231
},
233232
child: Text(
234233
zulipLocalizations.serverUrlDocLinkLabel,
@@ -245,30 +244,6 @@ class _AddAccountPageState extends State<AddAccountPage> {
245244
}
246245
}
247246

248-
void _launchUrl(BuildContext context) async {
249-
Future<void> showError(BuildContext context, String? message) {
250-
return showErrorDialog(
251-
context: context,
252-
title: ZulipLocalizations.of(context).errorUnableToOpenLinkTitle,
253-
message: [
254-
ZulipLocalizations.of(context).errorLinkCouldNotBeOpened(AddAccountPage.serverUrlHelpUrl),
255-
if (message != null) message,
256-
].join("\n\n"));
257-
}
258-
259-
bool launched = false;
260-
String? errorMessage;
261-
try {
262-
launched = await ZulipBinding.instance.launchUrl(Uri.parse(AddAccountPage.serverUrlHelpUrl));
263-
} on PlatformException catch (e) {
264-
errorMessage = e.message;
265-
}
266-
if (!launched) {
267-
if (!context.mounted) return;
268-
await showError(context, errorMessage);
269-
}
270-
}
271-
272247
class PasswordLoginPage extends StatefulWidget {
273248
const PasswordLoginPage({super.key, required this.serverSettings});
274249

0 commit comments

Comments
 (0)