diff --git a/lib/model/internal_link.dart b/lib/model/internal_link.dart index c132eb0b665..5be2bd62f5b 100644 --- a/lib/model/internal_link.dart +++ b/lib/model/internal_link.dart @@ -96,7 +96,14 @@ Uri narrowLink(PerAccountStore store, Narrow narrow, {int? nearMessageId}) { fragment.write('/near/$nearMessageId'); } - return store.realmUrl.replace(fragment: fragment.toString()); + Uri result = store.realmUrl.replace(fragment: fragment.toString()); + if (result.path.isEmpty) { + // Always ensure that there is a '/' right after the hostname. + // A generated URL without '/' looks odd and does not linkify. + result = result.replace(path: '/'); + } + assert(result.path.startsWith('/')); + return result; } /// A [Narrow] from a given URL, on `store`'s realm. diff --git a/test/model/compose_test.dart b/test/model/compose_test.dart index 37c7c503228..4f9a99ed6dd 100644 --- a/test/model/compose_test.dart +++ b/test/model/compose_test.dart @@ -314,6 +314,28 @@ hello '#narrow/dm/1,2-dm/near/12345', '#narrow/pm-with/1,2-pm/near/12345'); }); + + test('normalize links to always include a "/" after hostname', () { + void checkGeneratedLink({required String realmUrl, required String expectedLink}) { + final account = eg.selfAccount.copyWith(realmUrl: Uri.parse(realmUrl)); + final store = eg.store(account: account); + check(narrowLink(store, const CombinedFeedNarrow())) + .equals(Uri.parse(expectedLink)); + } + + checkGeneratedLink( + realmUrl: 'http://chat.example.com', + expectedLink: 'http://chat.example.com/#narrow'); + checkGeneratedLink( + realmUrl: 'http://chat.example.com/', + expectedLink: 'http://chat.example.com/#narrow'); + checkGeneratedLink( + realmUrl: 'http://chat.example.com/path', + expectedLink: 'http://chat.example.com/path#narrow'); + checkGeneratedLink( + realmUrl: 'http://chat.example.com/path/', + expectedLink: 'http://chat.example.com/path/#narrow'); + }); }); group('mention', () {