From 1bb4c5026b108838169192712b8d6446e7f36683 Mon Sep 17 00:00:00 2001 From: Shu Chen Date: Fri, 15 Sep 2023 14:45:14 +0100 Subject: [PATCH] content: Handle internal links Integrates internal_links into link nodes so that urls that resolve to internal Narrows navigate to that instead of launching in an external browser. Fixes: #73 --- lib/widgets/content.dart | 9 ++++++ test/widgets/content_test.dart | 53 ++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+) 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..cb41a56c91 100644 --- a/test/widgets/content_test.dart +++ b/test/widgets/content_test.dart @@ -8,13 +8,21 @@ 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 '../api/fake_api.dart'; import '../example_data.dart' as eg; import '../model/binding.dart'; +import '../model/message_list_test.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 +166,51 @@ 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(); + await tester.pump(); + assert(pushedRoutes.length == 1); + pushedRoutes.removeLast(); + return pushedRoutes; + } + + testWidgets('valid internal links are resolved', (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 are not followed', (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)));