Skip to content

Commit 2561775

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

File tree

2 files changed

+131
-0
lines changed

2 files changed

+131
-0
lines changed

lib/model/compose.dart

+60
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
//
@@ -96,5 +100,61 @@ String wrapWithBacktickFence({required String content, String? infoString}) {
96100
return resultBuffer.toString();
97101
}
98102

103+
const hashReplacements = {
104+
"%": ".",
105+
"(": ".28",
106+
")": ".29",
107+
".": ".2E",
108+
};
109+
110+
final encodeHashComponentRegex = RegExp(r'[%().]');
111+
112+
// Corresponds to encodeHashComponent in Zulip web;
113+
// see web/shared/src/internal_url.ts.
114+
String encodeHashComponent(String str) {
115+
return Uri.encodeComponent(str)
116+
.replaceAllMapped(encodeHashComponentRegex, (Match m) => hashReplacements[m[0]!]!);
117+
}
118+
119+
/// A URL to the given [Narrow], on `store`'s realm.
120+
Uri narrowLink(PerAccountStore store, Narrow narrow) {
121+
final apiNarrow = narrow.apiEncode();
122+
final fragment = StringBuffer('narrow');
123+
for (ApiNarrowElement element in apiNarrow) {
124+
fragment.write('/');
125+
if (element.negated) {
126+
fragment.write('-');
127+
}
128+
129+
if (element is ApiNarrowDm) {
130+
final supportsOperatorDm = store.connection.zulipFeatureLevel! >= 177; // TODO(server-7)
131+
element = element.resolve(legacy: !supportsOperatorDm);
132+
}
133+
134+
fragment.write('${element.operator}/');
135+
136+
switch (element) {
137+
case ApiNarrowStream():
138+
final streamId = element.operand;
139+
final name = store.streams[streamId]?.name ?? 'unknown';
140+
final slugifiedName = encodeHashComponent(name.replaceAll(' ', '-'));
141+
fragment.write('$streamId-$slugifiedName');
142+
case ApiNarrowTopic():
143+
fragment.write(encodeHashComponent(element.operand));
144+
case ApiNarrowDmModern():
145+
final suffix = element.operand.length >= 3 ? 'group' : 'dm';
146+
fragment.write('${element.operand.join(',')}-$suffix');
147+
case ApiNarrowPmWith():
148+
final suffix = element.operand.length >= 3 ? 'group' : 'pm';
149+
fragment.write('${element.operand.join(',')}-$suffix');
150+
case ApiNarrowDm():
151+
assert(false, 'ApiNarrowDm should have been resolved');
152+
case ApiNarrowMessageId():
153+
fragment.write(element.operand.toString());
154+
}
155+
}
156+
return store.account.realmUrl.replace(fragment: fragment.toString());
157+
}
158+
99159
// TODO more, like /near links to messages in conversations
100160
// (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', () {
@@ -225,4 +229,71 @@ hello
225229
''');
226230
});
227231
});
232+
233+
group('narrowLink', () {
234+
test('AllMessagesNarrow', () {
235+
final store = eg.store();
236+
check(narrowLink(store, const AllMessagesNarrow())).equals(store.account.realmUrl.resolve('#narrow'));
237+
});
238+
239+
test('StreamNarrow / TopicNarrow', () {
240+
void checkNarrow(String expectedFragment, {
241+
required int streamId,
242+
required String name,
243+
String? topic,
244+
}) {
245+
assert(expectedFragment.startsWith('#'), 'wrong-looking expectedFragment');
246+
final store = eg.store();
247+
store.addStream(eg.stream(streamId: streamId, name: name));
248+
final narrow = topic == null
249+
? StreamNarrow(streamId)
250+
: TopicNarrow(streamId, topic);
251+
check(narrowLink(store, narrow)).equals(store.account.realmUrl.resolve(expectedFragment));
252+
}
253+
254+
checkNarrow(streamId: 1, name: 'announce', '#narrow/stream/1-announce');
255+
checkNarrow(streamId: 378, name: 'api design', '#narrow/stream/378-api-design');
256+
checkNarrow(streamId: 391, name: 'Outreachy', '#narrow/stream/391-Outreachy');
257+
checkNarrow(streamId: 415, name: 'chat.zulip.org', '#narrow/stream/415-chat.2Ezulip.2Eorg');
258+
checkNarrow(streamId: 419, name: 'français', '#narrow/stream/419-fran.C3.A7ais');
259+
checkNarrow(streamId: 403, name: 'Hshs[™~}(.', '#narrow/stream/403-Hshs.5B.E2.84.A2~.7D.28.2E');
260+
261+
checkNarrow(streamId: 48, name: 'mobile', topic: 'Welcome screen UI',
262+
'#narrow/stream/48-mobile/topic/Welcome.20screen.20UI');
263+
checkNarrow(streamId: 243, name: 'mobile-team', topic: 'Podfile.lock clash #F92',
264+
'#narrow/stream/243-mobile-team/topic/Podfile.2Elock.20clash.20.23F92');
265+
checkNarrow(streamId: 377, name: 'translation/zh_tw', topic: '翻譯 "stream"',
266+
'#narrow/stream/377-translation.2Fzh_tw/topic/.E7.BF.BB.E8.AD.AF.20.22stream.22');
267+
});
268+
269+
test('DmNarrow', () {
270+
void checkNarrow(String expectedFragment, String legacyExpectedFragment, {
271+
required List<int> allRecipientIds,
272+
required int selfUserId,
273+
}) {
274+
assert(expectedFragment.startsWith('#'), 'wrong-looking expectedFragment');
275+
final store = eg.store();
276+
final narrow = DmNarrow(allRecipientIds: allRecipientIds, selfUserId: selfUserId);
277+
check(narrowLink(store, narrow)).equals(store.account.realmUrl.resolve(expectedFragment));
278+
store.connection.zulipFeatureLevel = 176;
279+
check(narrowLink(store, narrow)).equals(store.account.realmUrl.resolve(legacyExpectedFragment));
280+
}
281+
282+
checkNarrow(allRecipientIds: [1], selfUserId: 1,
283+
'#narrow/dm/1-dm',
284+
'#narrow/pm-with/1-pm');
285+
checkNarrow(allRecipientIds: [1, 2], selfUserId: 1,
286+
'#narrow/dm/1,2-dm',
287+
'#narrow/pm-with/1,2-pm');
288+
checkNarrow(allRecipientIds: [1, 2, 3], selfUserId: 1,
289+
'#narrow/dm/1,2,3-group',
290+
'#narrow/pm-with/1,2,3-group');
291+
checkNarrow(allRecipientIds: [1, 2, 3, 4], selfUserId: 4,
292+
'#narrow/dm/1,2,3,4-group',
293+
'#narrow/pm-with/1,2,3,4-group');
294+
});
295+
296+
// TODO other Narrow subclasses as we add them:
297+
// starred, mentioned; searches; arbitrary
298+
});
228299
}

0 commit comments

Comments
 (0)