Skip to content

Commit 0454607

Browse files
committed
lib [nfc]: refactor narrowLink out of compose into a new internal_link package
1 parent 67dff51 commit 0454607

File tree

3 files changed

+83
-77
lines changed

3 files changed

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

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)