diff --git a/lib/widgets/content.dart b/lib/widgets/content.dart index 9dd59afafc..294a308bbc 100644 --- a/lib/widgets/content.dart +++ b/lib/widgets/content.dart @@ -12,6 +12,7 @@ import '../model/store.dart'; import 'code_block.dart'; import 'dialog.dart'; import 'lightbox.dart'; +import 'message_list.dart'; import 'store.dart'; import 'text.dart'; @@ -668,6 +669,14 @@ void _launchUrl(BuildContext context, String urlString) async { 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 { diff --git a/test/widgets/content_test.dart b/test/widgets/content_test.dart index cd9324ebe9..9cf54705f1 100644 --- a/test/widgets/content_test.dart +++ b/test/widgets/content_test.dart @@ -8,13 +8,19 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:zulip/api/core.dart'; import 'package:zulip/model/content.dart'; +import 'package:zulip/model/narrow.dart'; import 'package:zulip/widgets/content.dart'; +import 'package:zulip/widgets/message_list.dart'; +import 'package:zulip/widgets/page.dart'; import 'package:zulip/widgets/store.dart'; import '../example_data.dart' as eg; import '../model/binding.dart'; import '../test_images.dart'; +import '../test_navigation.dart'; import 'dialog_checks.dart'; +import 'message_list_checks.dart'; +import 'page_checks.dart'; void main() { TestZulipBinding.ensureInitialized(); @@ -158,6 +164,52 @@ void main() { }); }); + group('LinkNode on internal links', () { + Future>> prepareContent(WidgetTester tester, { + required String html, + }) async { + await testBinding.globalStore.add(eg.selfAccount, eg.initialSnapshot( + streams: [eg.stream(streamId: 1, name: 'check')], + )); + addTearDown(testBinding.reset); + final pushedRoutes = >[]; + final testNavObserver = TestNavigatorObserver() + ..onPushed = (route, prevRoute) => pushedRoutes.add(route); + await tester.pumpWidget(GlobalStoreWidget(child: MaterialApp( + navigatorObservers: [testNavObserver], + home: PerAccountStoreWidget(accountId: eg.selfAccount.id, + child: BlockContentList(nodes: parseContent(html).nodes))))); + await tester.pump(); // global store + await tester.pump(); // pre-account store + // `tester.pumpWidget` introduces an initial route, remove so + // consumers only have newly pushed routes. + assert(pushedRoutes.length == 1); + pushedRoutes.removeLast(); + return pushedRoutes; + } + + testWidgets('valid internal links result in a pushed route', (tester) async { + final pushedRoutes = await prepareContent(tester, + html: '

stream

'); + + await tester.tap(find.text('stream')); + check(testBinding.takeLaunchUrlCalls()).isEmpty(); + check(pushedRoutes).single.isA() + .page.isA().narrow.equals(const StreamNarrow(1)); + }); + + testWidgets('invalid internal links result in launchUrl call', (tester) async { + final pushedRoutes = await prepareContent(tester, + html: '

invalid

'); + + await tester.tap(find.text('invalid')); + final expectedUrl = Uri.parse('${eg.realmUrl}#narrow/stream/1-check/topic'); + check(testBinding.takeLaunchUrlCalls()) + .single.equals((url: expectedUrl, mode: LaunchMode.externalApplication)); + check(pushedRoutes).isEmpty(); + }); + }); + group('UnicodeEmoji', () { Future prepareContent(WidgetTester tester, String html) async { await tester.pumpWidget(MaterialApp(home: BlockContentList(nodes: parseContent(html).nodes)));