Skip to content

Commit

Permalink
content: handle internal links
Browse files Browse the repository at this point in the history
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
  • Loading branch information
sirpengi committed Sep 15, 2023
1 parent dcc28e0 commit 87df3a1
Show file tree
Hide file tree
Showing 2 changed files with 102 additions and 0 deletions.
10 changes: 10 additions & 0 deletions lib/widgets/content.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@ import '../api/core.dart';
import '../api/model/model.dart';
import '../model/binding.dart';
import '../model/content.dart';
import '../model/internal_link.dart';
import '../model/store.dart';
import 'code_block.dart';
import 'dialog.dart';
import 'lightbox.dart';
import 'message_list.dart';
import 'store.dart';
import 'text.dart';

Expand Down Expand Up @@ -670,6 +672,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 {
Expand Down
92 changes: 92 additions & 0 deletions test/widgets/content_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,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();
Expand Down Expand Up @@ -155,6 +163,90 @@ void main() {
});
});

group('Internal links', () {
Future<void> prepareContent(WidgetTester tester, {
NavigatorObserver? navigatorObserver,
required String html,
}) async {
addTearDown(testBinding.reset);
await testBinding.globalStore.add(eg.selfAccount, eg.initialSnapshot(
streams: [eg.stream(streamId: 1, name: 'check')],
));
final store = await testBinding.globalStore.perAccount(eg.selfAccount.id);
final connection = store.connection as FakeApiConnection;
connection.prepare(json: newestResult(
foundOldest: true,
messages: [],
).toJson());
await tester.pumpWidget(GlobalStoreWidget(child: MaterialApp(
navigatorObservers: navigatorObserver != null ? [navigatorObserver] : [],
home: PerAccountStoreWidget(accountId: eg.selfAccount.id,
child: BlockContentList(
nodes: parseContent(html).nodes)))));
await tester.pump();
await tester.pump();
}

testWidgets('internal links are resolved: stream', (tester) async {
final pushedRoutes = <Route<dynamic>>[];
final testNavObserver = TestNavigatorObserver()
..onPushed = (route, prevRoute) => pushedRoutes.add(route);
await prepareContent(tester,
navigatorObserver: testNavObserver,
html: '<p><a href="/#narrow/stream/1-check">stream</a></p>');

await tester.tap(find.text('stream'));
check(testBinding.takeLaunchUrlCalls()).isEmpty();
check(pushedRoutes).last.isA<WidgetRoute>()
.page.isA<MessageListPage>()
.narrow.equals(const StreamNarrow(1));
});

testWidgets('internal links are resolved: topic', (tester) async {
final pushedRoutes = <Route<dynamic>>[];
final testNavObserver = TestNavigatorObserver()
..onPushed = (route, prevRoute) => pushedRoutes.add(route);
await prepareContent(tester,
navigatorObserver: testNavObserver,
html: '<p><a href="/#narrow/stream/1-check/topic/my.20topic">topic</a></p>');

await tester.tap(find.text('topic'));
check(testBinding.takeLaunchUrlCalls()).isEmpty();
check(pushedRoutes).last.isA<WidgetRoute>()
.page.isA<MessageListPage>()
.narrow.equals(const TopicNarrow(1, 'my topic'));
});

testWidgets('internal links are resolved: dm', (tester) async {
final pushedRoutes = <Route<dynamic>>[];
final testNavObserver = TestNavigatorObserver()
..onPushed = (route, prevRoute) => pushedRoutes.add(route);
await prepareContent(tester,
navigatorObserver: testNavObserver,
html: '<p><a href="/#narrow/dm/1-123-group">dm</a></p>');

await tester.tap(find.text('dm'));
check(testBinding.takeLaunchUrlCalls()).isEmpty();
check(pushedRoutes).last.isA<WidgetRoute>()
.page.isA<MessageListPage>()
.narrow.equals(DmNarrow.withUser(1, selfUserId: eg.selfUser.userId));
});

testWidgets('invalid internal links are not followed', (tester) async {
final pushedRoutes = <Route<dynamic>>[];
final testNavObserver = TestNavigatorObserver()
..onPushed = (route, prevRoute) => pushedRoutes.add(route);
await prepareContent(tester,
navigatorObserver: testNavObserver,
html: '<p><a href="/#narrow/stream/1-check/topic">invalid</a></p>');
await tester.tap(find.text('invalid'));
final expectedUrl = '${eg.realmUrl}#narrow/stream/1-check/topic';
const expectedMode = LaunchMode.externalApplication;
check(testBinding.takeLaunchUrlCalls())
.single.equals((url: Uri.parse(expectedUrl), mode: expectedMode));
});
});

group('UnicodeEmoji', () {
Future<void> prepareContent(WidgetTester tester, String html) async {
await tester.pumpWidget(MaterialApp(home: BlockContentList(nodes: parseContent(html).nodes)));
Expand Down

0 comments on commit 87df3a1

Please sign in to comment.