Skip to content

Commit ba6047e

Browse files
committed
internal_links: Add variable realmUrlWithSlash to add '/'
Updates the URL construction to include a trailing slash before the fragment identifier. This ensures that the URL is properly formatted and makes the url linkified. Co-authored-by: Greg Price <[email protected]> Fixes: #845
1 parent 6979b50 commit ba6047e

File tree

2 files changed

+87
-4
lines changed

2 files changed

+87
-4
lines changed

lib/model/internal_link.dart

+5-1
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,11 @@ Uri narrowLink(PerAccountStore store, Narrow narrow, {int? nearMessageId}) {
9696
fragment.write('/near/$nearMessageId');
9797
}
9898

99-
return store.realmUrl.replace(fragment: fragment.toString());
99+
final realmUrlWithSlash = store.realmUrl.path.isNotEmpty
100+
? store.realmUrl
101+
: store.realmUrl.replace(path: '${store.realmUrl.path}/');
102+
103+
return realmUrlWithSlash.replace(fragment: fragment.toString());
100104
}
101105

102106
/// A [Narrow] from a given URL, on `store`'s realm.

test/widgets/action_sheet_test.dart

+82-3
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,14 @@ late FakeApiConnection connection;
3838
Future<void> setupToMessageActionSheet(WidgetTester tester, {
3939
required Message message,
4040
required Narrow narrow,
41+
Account? account,
4142
}) async {
4243
addTearDown(testBinding.reset);
4344

44-
await testBinding.globalStore.add(eg.selfAccount, eg.initialSnapshot());
45-
final store = await testBinding.globalStore.perAccount(eg.selfAccount.id);
45+
account ??= eg.selfAccount;
46+
final initialSnapshot = eg.initialSnapshot();
47+
await testBinding.globalStore.add(account, initialSnapshot);
48+
final store = await testBinding.globalStore.perAccount(account.id);
4649
await store.addUser(eg.user(userId: message.senderId));
4750
if (message is StreamMessage) {
4851
final stream = eg.stream(streamId: message.streamId);
@@ -61,7 +64,7 @@ Future<void> setupToMessageActionSheet(WidgetTester tester, {
6164
messages: [message],
6265
).toJson());
6366

64-
await tester.pumpWidget(TestZulipApp(accountId: eg.selfAccount.id,
67+
await tester.pumpWidget(TestZulipApp(accountId: account.id,
6568
child: MessageListPage(initNarrow: narrow)));
6669

6770
// global store, per-account store, and message list get loaded
@@ -553,6 +556,82 @@ void main() {
553556
});
554557
});
555558

559+
group('Message link verbatim URL tests', () {
560+
Future<void> tapCopyMessageLinkButton(WidgetTester tester) async {
561+
await tester.ensureVisible(find.byIcon(Icons.link, skipOffstage: false));
562+
await tester.tap(find.byIcon(Icons.link));
563+
await tester.pump(); // [MenuItemButton.onPressed] called in a post-frame callback: flutter/flutter@e4a39fa2e
564+
}
565+
566+
Future<void> testMessageLink({
567+
required WidgetTester tester,
568+
required String realmUrl,
569+
required String topic,
570+
required String expectedLink,
571+
}) async {
572+
final stream = eg.stream(streamId: 42, name: 'stream 42');
573+
final message = eg.streamMessage(id: 12345, stream: stream, topic: topic);
574+
final narrow = TopicNarrow.ofMessage(message);
575+
final account = eg.selfAccount.copyWith(realmUrl: Uri.parse(realmUrl));
576+
await setupToMessageActionSheet(
577+
tester, message: message, narrow: narrow, account: account);
578+
579+
final store = await testBinding.globalStore.perAccount(account.id);
580+
if (!store.streams.containsKey(stream.streamId)) {
581+
await store.addStream(stream);
582+
}
583+
584+
await tapCopyMessageLinkButton(tester);
585+
await tester.pump(Duration.zero);
586+
check(await Clipboard.getData('text/plain')).isNotNull().text.equals(expectedLink);
587+
}
588+
589+
testWidgets('Copies link with custom realm path', (tester) async {
590+
await testMessageLink(
591+
tester: tester,
592+
realmUrl: 'https://chat.example/foo',
593+
topic: 'test topic',
594+
expectedLink: 'https://chat.example/foo/#narrow/stream/42-stream-42/topic/test.20topic/near/12345',
595+
);
596+
});
597+
598+
testWidgets('Copies link with realm URL without path', (tester) async {
599+
await testMessageLink(
600+
tester: tester,
601+
realmUrl: 'https://chat.zulip.org',
602+
topic: 'test topic',
603+
expectedLink: 'https://chat.zulip.org/#narrow/stream/42-stream-42/topic/test.20topic/near/12345',
604+
);
605+
});
606+
607+
testWidgets('Copies link with special characters in topic', (tester) async {
608+
await testMessageLink(
609+
tester: tester,
610+
realmUrl: 'https://chat.zulip.org',
611+
topic: 'What\'s up? 123!',
612+
expectedLink: 'https://chat.zulip.org/#narrow/stream/42-stream-42/topic/What.27s.20up.3F.20123.21/near/12345',
613+
);
614+
});
615+
616+
testWidgets('Copies link with realm URL ending in path', (tester) async {
617+
await testMessageLink(
618+
tester: tester,
619+
realmUrl: 'https://chat.zulip.org/path',
620+
topic: 'Numbers & Symbols: 100%',
621+
expectedLink: 'https://chat.zulip.org/path/#narrow/stream/42-stream-42/topic/Numbers.20.26.20Symbols.3A.20100.25/near/12345',
622+
);
623+
});
624+
625+
testWidgets('Copies link with realm URL ending in slash', (tester) async {
626+
await testMessageLink(
627+
tester: tester,
628+
realmUrl: 'https://chat.zulip.org/',
629+
topic: 'Regular topic',
630+
expectedLink: 'https://chat.zulip.org/#narrow/stream/42-stream-42/topic/Regular.20topic/near/12345',
631+
);
632+
});
633+
});
634+
556635
group('ShareButton', () {
557636
// Tests should call this.
558637
MockSharePlus setupMockSharePlus() {

0 commit comments

Comments
 (0)