Skip to content

Commit 0847f10

Browse files
committed
content: Handle internal links
Integrates [parseInternalLink] into content link nodes so that recognized narrows are navigated to within the app, rather than opened in a browser. Fixes: #73
1 parent e3a2dcc commit 0847f10

File tree

2 files changed

+62
-0
lines changed

2 files changed

+62
-0
lines changed

lib/widgets/content.dart

+9
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import '../model/store.dart';
1212
import 'code_block.dart';
1313
import 'dialog.dart';
1414
import 'lightbox.dart';
15+
import 'message_list.dart';
1516
import 'store.dart';
1617
import 'text.dart';
1718

@@ -668,6 +669,14 @@ void _launchUrl(BuildContext context, String urlString) async {
668669
return;
669670
}
670671

672+
final internalNarrow = parseInternalLink(url, store);
673+
if (internalNarrow != null) {
674+
Navigator.push(context,
675+
MessageListPage.buildRoute(context: context,
676+
narrow: internalNarrow));
677+
return;
678+
}
679+
671680
bool launched = false;
672681
String? errorMessage;
673682
try {

test/widgets/content_test.dart

+53
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,19 @@ import 'package:flutter_test/flutter_test.dart';
88
import 'package:url_launcher/url_launcher.dart';
99
import 'package:zulip/api/core.dart';
1010
import 'package:zulip/model/content.dart';
11+
import 'package:zulip/model/narrow.dart';
1112
import 'package:zulip/widgets/content.dart';
13+
import 'package:zulip/widgets/message_list.dart';
14+
import 'package:zulip/widgets/page.dart';
1215
import 'package:zulip/widgets/store.dart';
1316

1417
import '../example_data.dart' as eg;
1518
import '../model/binding.dart';
1619
import '../test_images.dart';
20+
import '../test_navigation.dart';
1721
import 'dialog_checks.dart';
22+
import 'message_list_checks.dart';
23+
import 'page_checks.dart';
1824

1925
void main() {
2026
TestZulipBinding.ensureInitialized();
@@ -158,6 +164,53 @@ void main() {
158164
});
159165
});
160166

167+
group('LinkNode on internal links', () {
168+
Future<List<Route<dynamic>>> prepareContent(WidgetTester tester, {
169+
required String html,
170+
}) async {
171+
await testBinding.globalStore.add(eg.selfAccount, eg.initialSnapshot(
172+
streams: [eg.stream(streamId: 1, name: 'check')],
173+
));
174+
addTearDown(testBinding.reset);
175+
final pushedRoutes = <Route<dynamic>>[];
176+
final testNavObserver = TestNavigatorObserver()
177+
..onPushed = (route, prevRoute) => pushedRoutes.add(route);
178+
await tester.pumpWidget(GlobalStoreWidget(child: MaterialApp(
179+
navigatorObservers: [testNavObserver],
180+
home: PerAccountStoreWidget(accountId: eg.selfAccount.id,
181+
child: BlockContentList(nodes: parseContent(html).nodes)))));
182+
await tester.pump(); // global store
183+
await tester.pump(); // per-account store
184+
// `tester.pumpWidget` introduces an initial route, remove so
185+
// consumers only have newly pushed routes.
186+
assert(pushedRoutes.length == 1);
187+
pushedRoutes.removeLast();
188+
return pushedRoutes;
189+
}
190+
191+
testWidgets('valid internal links are navigated to within app', (tester) async {
192+
final pushedRoutes = await prepareContent(tester,
193+
html: '<p><a href="/#narrow/stream/1-check">stream</a></p>');
194+
195+
await tester.tap(find.text('stream'));
196+
check(testBinding.takeLaunchUrlCalls()).isEmpty();
197+
check(pushedRoutes).single.isA<WidgetRoute>()
198+
.page.isA<MessageListPage>().narrow.equals(const StreamNarrow(1));
199+
});
200+
201+
testWidgets('invalid internal links are opened in browser', (tester) async {
202+
// Link is invalid due to `topic` operator missing an operand.
203+
final pushedRoutes = await prepareContent(tester,
204+
html: '<p><a href="/#narrow/stream/1-check/topic">invalid</a></p>');
205+
206+
await tester.tap(find.text('invalid'));
207+
final expectedUrl = eg.realmUrl.resolve('/#narrow/stream/1-check/topic');
208+
check(testBinding.takeLaunchUrlCalls())
209+
.single.equals((url: expectedUrl, mode: LaunchMode.externalApplication));
210+
check(pushedRoutes).isEmpty();
211+
});
212+
});
213+
161214
group('UnicodeEmoji', () {
162215
Future<void> prepareContent(WidgetTester tester, String html) async {
163216
await tester.pumpWidget(MaterialApp(home: BlockContentList(nodes: parseContent(html).nodes)));

0 commit comments

Comments
 (0)