Skip to content

Commit 626be61

Browse files
committed
compose: Add narrowLink helper, to write links to Narrows
We'll use this soon, for quote-and-reply #116. With the recent commit 4e11ea7, PerAccountStoreTestExtension now has `addStream`, which we use in the tests.
1 parent df6772a commit 626be61

File tree

2 files changed

+130
-0
lines changed

2 files changed

+130
-0
lines changed

lib/model/compose.dart

+59
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import 'dart:math';
22

3+
import '../api/model/narrow.dart';
4+
import 'narrow.dart';
5+
import 'store.dart';
6+
37
//
48
// Put functions for nontrivial message-content generation in this file.
59
//
@@ -99,5 +103,60 @@ String wrapWithBacktickFence({required String content, String? infoString}) {
99103
return resultBuffer.toString();
100104
}
101105

106+
const hashReplacements = {
107+
"%": ".",
108+
"(": ".28",
109+
")": ".29",
110+
".": ".2E",
111+
};
112+
113+
// Corresponds to encodeHashComponent in Zulip web;
114+
// see web/shared/src/internal_url.js.
115+
String encodeHashComponent(String str) {
116+
return Uri.encodeComponent(str)
117+
.replaceAllMapped(RegExp(r'[%().]'), (Match m) => hashReplacements[m[0]!]!);
118+
}
119+
120+
/// A URL to the given [Narrow], on `store`'s realm.
121+
Uri narrowLink(PerAccountStore store, Narrow narrow) {
122+
final apiNarrow = narrow.apiEncode();
123+
final resultBuffer = StringBuffer('#narrow');
124+
for (final element in apiNarrow) {
125+
resultBuffer.write('/');
126+
if (element.negated) {
127+
resultBuffer.write('-');
128+
}
129+
// TODO(server-7) remove special-casing
130+
if (element is ApiNarrowDm) {
131+
final supportsOperatorDm = store.connection.zulipFeatureLevel! >= 177; // TODO(server-7)
132+
final resolved = element.resolve(legacy: !supportsOperatorDm);
133+
final operator = resolved.operator;
134+
final operand = resolved.operand;
135+
final operandSuffix = operand.length >= 3
136+
? 'group'
137+
: (supportsOperatorDm ? 'dm' : 'pm'); // TODO(?) CZO actually gives a slugified full_name here…??
138+
resultBuffer.write('$operator/${operand.join(',')}-$operandSuffix');
139+
continue;
140+
}
141+
142+
resultBuffer.write('${element.operator}/');
143+
144+
switch (element) {
145+
case ApiNarrowStream():
146+
final streamId = element.operand;
147+
final name = store.streams[streamId]?.name ?? 'unknown';
148+
final slugifiedName = encodeHashComponent(name.replaceAll(' ', '-'));
149+
resultBuffer.write('$streamId-$slugifiedName');
150+
case ApiNarrowTopic():
151+
resultBuffer.write(encodeHashComponent(element.operand));
152+
case ApiNarrowDm():
153+
// do nothing; handled as special case above
154+
case ApiNarrowMessageId():
155+
resultBuffer.write(element.operand.toString());
156+
}
157+
}
158+
return store.account.realmUrl.resolve(resultBuffer.toString());
159+
}
160+
102161
// TODO more, like /near links to messages in conversations
103162
// (also to be used in quote-and-reply)

test/model/compose_test.dart

+71
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import 'package:checks/checks.dart';
22
import 'package:test/scaffolding.dart';
33
import 'package:zulip/model/compose.dart';
4+
import 'package:zulip/model/narrow.dart';
5+
6+
import '../example_data.dart' as eg;
7+
import 'test_store.dart';
48

59
void main() {
610
group('wrapWithBacktickFence', () {
@@ -187,4 +191,71 @@ hello
187191
''');
188192
});
189193
});
194+
195+
group('narrowLink', () {
196+
test('AllMessagesNarrow', () {
197+
final store = eg.store();
198+
check(narrowLink(store, const AllMessagesNarrow())).equals(store.account.realmUrl.resolve('#narrow'));
199+
});
200+
201+
test('StreamNarrow / TopicNarrow', () {
202+
void checkNarrow(String expectedFragment, {
203+
required int streamId,
204+
required String name,
205+
String? topic,
206+
}) {
207+
assert(expectedFragment.startsWith('#'), 'wrong-looking expectedFragment');
208+
final store = eg.store();
209+
store.addStream(eg.stream(streamId: streamId, name: name));
210+
final narrow = topic == null
211+
? StreamNarrow(streamId)
212+
: TopicNarrow(streamId, topic);
213+
check(narrowLink(store, narrow)).equals(store.account.realmUrl.resolve(expectedFragment));
214+
}
215+
216+
checkNarrow(streamId: 1, name: 'announce', '#narrow/stream/1-announce');
217+
checkNarrow(streamId: 378, name: 'api design', '#narrow/stream/378-api-design');
218+
checkNarrow(streamId: 391, name: 'Outreachy', '#narrow/stream/391-Outreachy');
219+
checkNarrow(streamId: 415, name: 'chat.zulip.org', '#narrow/stream/415-chat.2Ezulip.2Eorg');
220+
checkNarrow(streamId: 419, name: 'français', '#narrow/stream/419-fran.C3.A7ais');
221+
checkNarrow(streamId: 403, name: 'Hshs[™~}(.', '#narrow/stream/403-Hshs.5B.E2.84.A2~.7D.28.2E');
222+
223+
checkNarrow(streamId: 48, name: 'mobile', topic: 'Welcome screen UI',
224+
'#narrow/stream/48-mobile/topic/Welcome.20screen.20UI');
225+
checkNarrow(streamId: 243, name: 'mobile-team', topic: 'Podfile.lock clash #F92',
226+
'#narrow/stream/243-mobile-team/topic/Podfile.2Elock.20clash.20.23F92');
227+
checkNarrow(streamId: 377, name: 'translation/zh_tw', topic: '翻譯 "stream"',
228+
'#narrow/stream/377-translation.2Fzh_tw/topic/.E7.BF.BB.E8.AD.AF.20.22stream.22');
229+
});
230+
231+
test('DmNarrow', () {
232+
void checkNarrow(String expectedFragment, String legacyExpectedFragment, {
233+
required List<int> allRecipientIds,
234+
required int selfUserId,
235+
}) {
236+
assert(expectedFragment.startsWith('#'), 'wrong-looking expectedFragment');
237+
final store = eg.store();
238+
final narrow = DmNarrow(allRecipientIds: allRecipientIds, selfUserId: selfUserId);
239+
check(narrowLink(store, narrow)).equals(store.account.realmUrl.resolve(expectedFragment));
240+
store.connection.zulipFeatureLevel = 176;
241+
check(narrowLink(store, narrow)).equals(store.account.realmUrl.resolve(legacyExpectedFragment));
242+
}
243+
244+
checkNarrow(allRecipientIds: [1], selfUserId: 1,
245+
'#narrow/dm/1-dm',
246+
'#narrow/pm-with/1-pm');
247+
checkNarrow(allRecipientIds: [1, 2], selfUserId: 1,
248+
'#narrow/dm/1,2-dm',
249+
'#narrow/pm-with/1,2-pm');
250+
checkNarrow(allRecipientIds: [1, 2, 3], selfUserId: 1,
251+
'#narrow/dm/1,2,3-group',
252+
'#narrow/pm-with/1,2,3-group');
253+
checkNarrow(allRecipientIds: [1, 2, 3, 4], selfUserId: 4,
254+
'#narrow/dm/1,2,3,4-group',
255+
'#narrow/pm-with/1,2,3,4-group');
256+
});
257+
258+
// TODO other Narrow subclasses as we add them:
259+
// starred, mentioned; searches; arbitrary
260+
});
190261
}

0 commit comments

Comments
 (0)