Skip to content

Commit 2f944a0

Browse files
committed
internal_link [nfc]: Move narrowLink out of compose into a new internal_link file
1 parent 5dbf1e6 commit 2f944a0

File tree

3 files changed

+82
-77
lines changed

3 files changed

+82
-77
lines changed

lib/model/compose.dart

Lines changed: 1 addition & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import 'dart:math';
22

33
import '../api/model/model.dart';
4-
import '../api/model/narrow.dart';
4+
import 'internal_link.dart';
55
import 'narrow.dart';
66
import 'store.dart';
77

@@ -101,82 +101,6 @@ String wrapWithBacktickFence({required String content, String? infoString}) {
101101
return resultBuffer.toString();
102102
}
103103

104-
const _hashReplacements = {
105-
"%": ".",
106-
"(": ".28",
107-
")": ".29",
108-
".": ".2E",
109-
};
110-
111-
final _encodeHashComponentRegex = RegExp(r'[%().]');
112-
113-
// Corresponds to encodeHashComponent in Zulip web;
114-
// see web/shared/src/internal_url.ts.
115-
String _encodeHashComponent(String str) {
116-
return Uri.encodeComponent(str)
117-
.replaceAllMapped(_encodeHashComponentRegex, (Match m) => _hashReplacements[m[0]!]!);
118-
}
119-
120-
/// A URL to the given [Narrow], on `store`'s realm.
121-
///
122-
/// To include /near/{messageId} in the link, pass a non-null [nearMessageId].
123-
// Why take [nearMessageId] in a param, instead of looking for it in [narrow]?
124-
//
125-
// A reasonable question: after all, the "near" part of a near link (e.g., for
126-
// quote-and-reply) does take the same form as other operator/operand pairs
127-
// that we represent with [ApiNarrowElement]s, like "/stream/48-mobile".
128-
//
129-
// But unlike those other elements, we choose not to give the "near" element
130-
// an [ApiNarrowElement] representation, because it doesn't have quite that role:
131-
// it says where to look in a list of messages, but it doesn't filter the list down.
132-
// In fact, from a brief look at server code, it seems to be *ignored*
133-
// if you include it in the `narrow` param in get-messages requests.
134-
// When you want to point the server to a location in a message list, you
135-
// you do so by passing the `anchor` param.
136-
Uri narrowLink(PerAccountStore store, Narrow narrow, {int? nearMessageId}) {
137-
final apiNarrow = narrow.apiEncode();
138-
final fragment = StringBuffer('narrow');
139-
for (ApiNarrowElement element in apiNarrow) {
140-
fragment.write('/');
141-
if (element.negated) {
142-
fragment.write('-');
143-
}
144-
145-
if (element is ApiNarrowDm) {
146-
final supportsOperatorDm = store.connection.zulipFeatureLevel! >= 177; // TODO(server-7)
147-
element = element.resolve(legacy: !supportsOperatorDm);
148-
}
149-
150-
fragment.write('${element.operator}/');
151-
152-
switch (element) {
153-
case ApiNarrowStream():
154-
final streamId = element.operand;
155-
final name = store.streams[streamId]?.name ?? 'unknown';
156-
final slugifiedName = _encodeHashComponent(name.replaceAll(' ', '-'));
157-
fragment.write('$streamId-$slugifiedName');
158-
case ApiNarrowTopic():
159-
fragment.write(_encodeHashComponent(element.operand));
160-
case ApiNarrowDmModern():
161-
final suffix = element.operand.length >= 3 ? 'group' : 'dm';
162-
fragment.write('${element.operand.join(',')}-$suffix');
163-
case ApiNarrowPmWith():
164-
final suffix = element.operand.length >= 3 ? 'group' : 'pm';
165-
fragment.write('${element.operand.join(',')}-$suffix');
166-
case ApiNarrowDm():
167-
assert(false, 'ApiNarrowDm should have been resolved');
168-
case ApiNarrowMessageId():
169-
fragment.write(element.operand.toString());
170-
}
171-
}
172-
173-
if (nearMessageId != null) {
174-
fragment.write('/near/$nearMessageId');
175-
}
176-
177-
return store.account.realmUrl.replace(fragment: fragment.toString());
178-
}
179-
180104
/// An @-mention, like @**Chris Bobbe|13313**.
181105
///
182106
/// To omit the user ID part ("|13313") whenever the name part is unambiguous,

lib/model/internal_link.dart

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
2+
import '../api/model/narrow.dart';
3+
import 'narrow.dart';
4+
import 'store.dart';
5+
6+
const _hashReplacements = {
7+
"%": ".",
8+
"(": ".28",
9+
")": ".29",
10+
".": ".2E",
11+
};
12+
13+
final _encodeHashComponentRegex = RegExp(r'[%().]');
14+
15+
// Corresponds to encodeHashComponent in Zulip web;
16+
// see web/shared/src/internal_url.ts.
17+
String _encodeHashComponent(String str) {
18+
return Uri.encodeComponent(str)
19+
.replaceAllMapped(_encodeHashComponentRegex, (Match m) => _hashReplacements[m[0]!]!);
20+
}
21+
22+
/// A URL to the given [Narrow], on `store`'s realm.
23+
///
24+
/// To include /near/{messageId} in the link, pass a non-null [nearMessageId].
25+
// Why take [nearMessageId] in a param, instead of looking for it in [narrow]?
26+
//
27+
// A reasonable question: after all, the "near" part of a near link (e.g., for
28+
// quote-and-reply) does take the same form as other operator/operand pairs
29+
// that we represent with [ApiNarrowElement]s, like "/stream/48-mobile".
30+
//
31+
// But unlike those other elements, we choose not to give the "near" element
32+
// an [ApiNarrowElement] representation, because it doesn't have quite that role:
33+
// it says where to look in a list of messages, but it doesn't filter the list down.
34+
// In fact, from a brief look at server code, it seems to be *ignored*
35+
// if you include it in the `narrow` param in get-messages requests.
36+
// When you want to point the server to a location in a message list, you
37+
// you do so by passing the `anchor` param.
38+
Uri narrowLink(PerAccountStore store, Narrow narrow, {int? nearMessageId}) {
39+
final apiNarrow = narrow.apiEncode();
40+
final fragment = StringBuffer('narrow');
41+
for (ApiNarrowElement element in apiNarrow) {
42+
fragment.write('/');
43+
if (element.negated) {
44+
fragment.write('-');
45+
}
46+
47+
if (element is ApiNarrowDm) {
48+
final supportsOperatorDm = store.connection.zulipFeatureLevel! >= 177; // TODO(server-7)
49+
element = element.resolve(legacy: !supportsOperatorDm);
50+
}
51+
52+
fragment.write('${element.operator}/');
53+
54+
switch (element) {
55+
case ApiNarrowStream():
56+
final streamId = element.operand;
57+
final name = store.streams[streamId]?.name ?? 'unknown';
58+
final slugifiedName = _encodeHashComponent(name.replaceAll(' ', '-'));
59+
fragment.write('$streamId-$slugifiedName');
60+
case ApiNarrowTopic():
61+
fragment.write(_encodeHashComponent(element.operand));
62+
case ApiNarrowDmModern():
63+
final suffix = element.operand.length >= 3 ? 'group' : 'dm';
64+
fragment.write('${element.operand.join(',')}-$suffix');
65+
case ApiNarrowPmWith():
66+
final suffix = element.operand.length >= 3 ? 'group' : 'pm';
67+
fragment.write('${element.operand.join(',')}-$suffix');
68+
case ApiNarrowDm():
69+
assert(false, 'ApiNarrowDm should have been resolved');
70+
case ApiNarrowMessageId():
71+
fragment.write(element.operand.toString());
72+
}
73+
}
74+
75+
if (nearMessageId != null) {
76+
fragment.write('/near/$nearMessageId');
77+
}
78+
79+
return store.account.realmUrl.replace(fragment: fragment.toString());
80+
}

test/model/compose_test.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import 'package:checks/checks.dart';
22
import 'package:test/scaffolding.dart';
33
import 'package:zulip/model/compose.dart';
44
import 'package:zulip/model/narrow.dart';
5+
import 'package:zulip/model/internal_link.dart';
56

67
import '../example_data.dart' as eg;
78
import 'test_store.dart';

0 commit comments

Comments
 (0)