Skip to content

Commit

Permalink
compose: Add narrowLink helper, to write links to Narrows
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
chrisbobbe authored and gnprice committed Jun 16, 2023
1 parent aab6cad commit 4ee0341
Show file tree
Hide file tree
Showing 2 changed files with 131 additions and 0 deletions.
60 changes: 60 additions & 0 deletions lib/model/compose.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import 'dart:math';

import '../api/model/narrow.dart';
import 'narrow.dart';
import 'store.dart';

//
// Put functions for nontrivial message-content generation in this file.
//
Expand Down Expand Up @@ -96,5 +100,61 @@ 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.
Uri narrowLink(PerAccountStore store, Narrow narrow) {
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());
}
}
return store.account.realmUrl.replace(fragment: fragment.toString());
}

// TODO more, like /near links to messages in conversations
// (also to be used in quote-and-reply)
71 changes: 71 additions & 0 deletions test/model/compose_test.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import 'package:checks/checks.dart';
import 'package:test/scaffolding.dart';
import 'package:zulip/model/compose.dart';
import 'package:zulip/model/narrow.dart';

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

void main() {
group('wrapWithBacktickFence', () {
Expand Down Expand Up @@ -215,4 +219,71 @@ hello
''');
});
});

group('narrowLink', () {
test('AllMessagesNarrow', () {
final store = eg.store();
check(narrowLink(store, const AllMessagesNarrow())).equals(store.account.realmUrl.resolve('#narrow'));
});

test('StreamNarrow / TopicNarrow', () {
void checkNarrow(String expectedFragment, {
required int streamId,
required String name,
String? topic,
}) {
assert(expectedFragment.startsWith('#'), 'wrong-looking expectedFragment');
final store = eg.store();
store.addStream(eg.stream(streamId: streamId, name: name));
final narrow = topic == null
? StreamNarrow(streamId)
: TopicNarrow(streamId, topic);
check(narrowLink(store, narrow)).equals(store.account.realmUrl.resolve(expectedFragment));
}

checkNarrow(streamId: 1, name: 'announce', '#narrow/stream/1-announce');
checkNarrow(streamId: 378, name: 'api design', '#narrow/stream/378-api-design');
checkNarrow(streamId: 391, name: 'Outreachy', '#narrow/stream/391-Outreachy');
checkNarrow(streamId: 415, name: 'chat.zulip.org', '#narrow/stream/415-chat.2Ezulip.2Eorg');
checkNarrow(streamId: 419, name: 'français', '#narrow/stream/419-fran.C3.A7ais');
checkNarrow(streamId: 403, name: 'Hshs[™~}(.', '#narrow/stream/403-Hshs.5B.E2.84.A2~.7D.28.2E');

checkNarrow(streamId: 48, name: 'mobile', topic: 'Welcome screen UI',
'#narrow/stream/48-mobile/topic/Welcome.20screen.20UI');
checkNarrow(streamId: 243, name: 'mobile-team', topic: 'Podfile.lock clash #F92',
'#narrow/stream/243-mobile-team/topic/Podfile.2Elock.20clash.20.23F92');
checkNarrow(streamId: 377, name: 'translation/zh_tw', topic: '翻譯 "stream"',
'#narrow/stream/377-translation.2Fzh_tw/topic/.E7.BF.BB.E8.AD.AF.20.22stream.22');
});

test('DmNarrow', () {
void checkNarrow(String expectedFragment, String legacyExpectedFragment, {
required List<int> allRecipientIds,
required int selfUserId,
}) {
assert(expectedFragment.startsWith('#'), 'wrong-looking expectedFragment');
final store = eg.store();
final narrow = DmNarrow(allRecipientIds: allRecipientIds, selfUserId: selfUserId);
check(narrowLink(store, narrow)).equals(store.account.realmUrl.resolve(expectedFragment));
store.connection.zulipFeatureLevel = 176;
check(narrowLink(store, narrow)).equals(store.account.realmUrl.resolve(legacyExpectedFragment));
}

checkNarrow(allRecipientIds: [1], selfUserId: 1,
'#narrow/dm/1-dm',
'#narrow/pm-with/1-pm');
checkNarrow(allRecipientIds: [1, 2], selfUserId: 1,
'#narrow/dm/1,2-dm',
'#narrow/pm-with/1,2-pm');
checkNarrow(allRecipientIds: [1, 2, 3], selfUserId: 1,
'#narrow/dm/1,2,3-group',
'#narrow/pm-with/1,2,3-group');
checkNarrow(allRecipientIds: [1, 2, 3, 4], selfUserId: 4,
'#narrow/dm/1,2,3,4-group',
'#narrow/pm-with/1,2,3,4-group');
});

// TODO other Narrow subclasses as we add them:
// starred, mentioned; searches; arbitrary
});
}

0 comments on commit 4ee0341

Please sign in to comment.