diff --git a/lib/model/compose.dart b/lib/model/compose.dart index cec844e162e..ec8fdcb3ddd 100644 --- a/lib/model/compose.dart +++ b/lib/model/compose.dart @@ -1,7 +1,7 @@ import 'dart:math'; import '../api/model/model.dart'; -import '../api/model/narrow.dart'; +import 'internal_link.dart'; import 'narrow.dart'; import 'store.dart'; @@ -101,82 +101,6 @@ String wrapWithBacktickFence({required String content, String? infoString}) { return resultBuffer.toString(); } -const _hashReplacements = { - "%": ".", - "(": ".28", - ")": ".29", - ".": ".2E", -}; - -final _encodeHashComponentRegex = RegExp(r'[%().]'); - -// Corresponds to encodeHashComponent in Zulip web; -// see web/shared/src/internal_url.ts. -String _encodeHashComponent(String str) { - return Uri.encodeComponent(str) - .replaceAllMapped(_encodeHashComponentRegex, (Match m) => _hashReplacements[m[0]!]!); -} - -/// A URL to the given [Narrow], on `store`'s realm. -/// -/// To include /near/{messageId} in the link, pass a non-null [nearMessageId]. -// Why take [nearMessageId] in a param, instead of looking for it in [narrow]? -// -// A reasonable question: after all, the "near" part of a near link (e.g., for -// quote-and-reply) does take the same form as other operator/operand pairs -// that we represent with [ApiNarrowElement]s, like "/stream/48-mobile". -// -// But unlike those other elements, we choose not to give the "near" element -// an [ApiNarrowElement] representation, because it doesn't have quite that role: -// it says where to look in a list of messages, but it doesn't filter the list down. -// In fact, from a brief look at server code, it seems to be *ignored* -// if you include it in the `narrow` param in get-messages requests. -// When you want to point the server to a location in a message list, you -// you do so by passing the `anchor` param. -Uri narrowLink(PerAccountStore store, Narrow narrow, {int? nearMessageId}) { - final apiNarrow = narrow.apiEncode(); - final fragment = StringBuffer('narrow'); - for (ApiNarrowElement element in apiNarrow) { - fragment.write('/'); - if (element.negated) { - fragment.write('-'); - } - - if (element is ApiNarrowDm) { - final supportsOperatorDm = store.connection.zulipFeatureLevel! >= 177; // TODO(server-7) - element = element.resolve(legacy: !supportsOperatorDm); - } - - fragment.write('${element.operator}/'); - - switch (element) { - case ApiNarrowStream(): - final streamId = element.operand; - final name = store.streams[streamId]?.name ?? 'unknown'; - final slugifiedName = _encodeHashComponent(name.replaceAll(' ', '-')); - fragment.write('$streamId-$slugifiedName'); - case ApiNarrowTopic(): - fragment.write(_encodeHashComponent(element.operand)); - case ApiNarrowDmModern(): - final suffix = element.operand.length >= 3 ? 'group' : 'dm'; - fragment.write('${element.operand.join(',')}-$suffix'); - case ApiNarrowPmWith(): - final suffix = element.operand.length >= 3 ? 'group' : 'pm'; - fragment.write('${element.operand.join(',')}-$suffix'); - case ApiNarrowDm(): - assert(false, 'ApiNarrowDm should have been resolved'); - case ApiNarrowMessageId(): - fragment.write(element.operand.toString()); - } - } - - if (nearMessageId != null) { - fragment.write('/near/$nearMessageId'); - } - - return store.account.realmUrl.replace(fragment: fragment.toString()); -} - /// An @-mention, like @**Chris Bobbe|13313**. /// /// To omit the user ID part ("|13313") whenever the name part is unambiguous, diff --git a/lib/model/internal_link.dart b/lib/model/internal_link.dart new file mode 100644 index 00000000000..79c4ea2f4a3 --- /dev/null +++ b/lib/model/internal_link.dart @@ -0,0 +1,81 @@ + +import 'store.dart'; + +import '../api/model/narrow.dart'; +import 'narrow.dart'; + +const _hashReplacements = { + "%": ".", + "(": ".28", + ")": ".29", + ".": ".2E", +}; + +final _encodeHashComponentRegex = RegExp(r'[%().]'); + +// Corresponds to encodeHashComponent in Zulip web; +// see web/shared/src/internal_url.ts. +String _encodeHashComponent(String str) { + return Uri.encodeComponent(str) + .replaceAllMapped(_encodeHashComponentRegex, (Match m) => _hashReplacements[m[0]!]!); +} + +/// A URL to the given [Narrow], on `store`'s realm. +/// +/// To include /near/{messageId} in the link, pass a non-null [nearMessageId]. +// Why take [nearMessageId] in a param, instead of looking for it in [narrow]? +// +// A reasonable question: after all, the "near" part of a near link (e.g., for +// quote-and-reply) does take the same form as other operator/operand pairs +// that we represent with [ApiNarrowElement]s, like "/stream/48-mobile". +// +// But unlike those other elements, we choose not to give the "near" element +// an [ApiNarrowElement] representation, because it doesn't have quite that role: +// it says where to look in a list of messages, but it doesn't filter the list down. +// In fact, from a brief look at server code, it seems to be *ignored* +// if you include it in the `narrow` param in get-messages requests. +// When you want to point the server to a location in a message list, you +// you do so by passing the `anchor` param. +Uri narrowLink(PerAccountStore store, Narrow narrow, {int? nearMessageId}) { + final apiNarrow = narrow.apiEncode(); + final fragment = StringBuffer('narrow'); + for (ApiNarrowElement element in apiNarrow) { + fragment.write('/'); + if (element.negated) { + fragment.write('-'); + } + + if (element is ApiNarrowDm) { + final supportsOperatorDm = store.connection.zulipFeatureLevel! >= 177; // TODO(server-7) + element = element.resolve(legacy: !supportsOperatorDm); + } + + fragment.write('${element.operator}/'); + + switch (element) { + case ApiNarrowStream(): + final streamId = element.operand; + final name = store.streams[streamId]?.name ?? 'unknown'; + final slugifiedName = _encodeHashComponent(name.replaceAll(' ', '-')); + fragment.write('$streamId-$slugifiedName'); + case ApiNarrowTopic(): + fragment.write(_encodeHashComponent(element.operand)); + case ApiNarrowDmModern(): + final suffix = element.operand.length >= 3 ? 'group' : 'dm'; + fragment.write('${element.operand.join(',')}-$suffix'); + case ApiNarrowPmWith(): + final suffix = element.operand.length >= 3 ? 'group' : 'pm'; + fragment.write('${element.operand.join(',')}-$suffix'); + case ApiNarrowDm(): + assert(false, 'ApiNarrowDm should have been resolved'); + case ApiNarrowMessageId(): + fragment.write(element.operand.toString()); + } + } + + if (nearMessageId != null) { + fragment.write('/near/$nearMessageId'); + } + + return store.account.realmUrl.replace(fragment: fragment.toString()); +} diff --git a/test/model/compose_test.dart b/test/model/compose_test.dart index 4f223347783..ad68a70d105 100644 --- a/test/model/compose_test.dart +++ b/test/model/compose_test.dart @@ -2,6 +2,7 @@ import 'package:checks/checks.dart'; import 'package:test/scaffolding.dart'; import 'package:zulip/model/compose.dart'; import 'package:zulip/model/narrow.dart'; +import 'package:zulip/model/internal_link.dart'; import '../example_data.dart' as eg; import 'test_store.dart';