Skip to content

Commit 385fb81

Browse files
committed
api: Use "channel" operator over the legacy "stream", where supported
This change affects a few places in the app, for example, generating internal links and sending api request with the new "channel" operator. Fixes: #633
1 parent e7cd777 commit 385fb81

File tree

10 files changed

+137
-55
lines changed

10 files changed

+137
-55
lines changed

lib/api/model/narrow.dart

Lines changed: 49 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,24 +14,29 @@ typedef ApiNarrow = List<ApiNarrowElement>;
1414
/// reasonably be omitted will be omitted.
1515
ApiNarrow resolveApiNarrowForServer(ApiNarrow narrow, int zulipFeatureLevel) {
1616
final supportsOperatorDm = zulipFeatureLevel >= 177; // TODO(server-7)
17+
final supportsOperatorChannel = zulipFeatureLevel >= 250; // TODO(server-9)
1718
final supportsOperatorWith = zulipFeatureLevel >= 271; // TODO(server-9)
1819

1920
bool hasDmElement = false;
21+
bool hasChannelElement = false;
2022
bool hasWithElement = false;
2123
for (final element in narrow) {
2224
switch (element) {
23-
case ApiNarrowDm(): hasDmElement = true;
24-
case ApiNarrowWith(): hasWithElement = true;
25+
case ApiNarrowChannel(): hasChannelElement = true;
26+
case ApiNarrowDm(): hasDmElement = true;
27+
case ApiNarrowWith(): hasWithElement = true;
2528
default:
2629
}
2730
}
28-
if (!(hasDmElement || (hasWithElement && !supportsOperatorWith))) {
31+
if (!(hasChannelElement || hasDmElement || (hasWithElement && !supportsOperatorWith))) {
2932
return narrow;
3033
}
3134

3235
final result = <ApiNarrowElement>[];
3336
for (final element in narrow) {
3437
switch (element) {
38+
case ApiNarrowChannel():
39+
result.add(element.resolve(legacy: !supportsOperatorChannel));
3540
case ApiNarrowDm():
3641
result.add(element.resolve(legacy: !supportsOperatorDm));
3742
case ApiNarrowWith() when !supportsOperatorWith:
@@ -93,17 +98,51 @@ sealed class ApiNarrowElement {
9398
};
9499
}
95100

96-
class ApiNarrowStream extends ApiNarrowElement {
97-
@override String get operator => 'stream';
101+
class ApiNarrowChannel extends ApiNarrowElement {
102+
@override String get operator {
103+
assert(false,
104+
"The [operator] getter was called on a plain [ApiNarrowChannel]. "
105+
"Before passing to [jsonEncode] or otherwise getting [operator], "
106+
"the [ApiNarrowChannel] must be replaced by the result of [ApiNarrowChannel.resolve]."
107+
);
108+
return "channel";
109+
}
98110

99111
@override final int operand;
100112

101-
ApiNarrowStream(this.operand, {super.negated});
113+
ApiNarrowChannel(this.operand, {super.negated});
102114

103-
factory ApiNarrowStream.fromJson(Map<String, dynamic> json) => ApiNarrowStream(
104-
json['operand'] as int,
105-
negated: json['negated'] as bool? ?? false,
106-
);
115+
factory ApiNarrowChannel.fromJson(Map<String, dynamic> json) {
116+
var operand = (json['operand'] as int);
117+
var negated = json['negated'] as bool? ?? false;
118+
return json['operator'] == 'stream'
119+
? ApiNarrowStream._(operand, negated: negated)
120+
: ApiNarrowChannelModern._(operand, negated: negated);
121+
}
122+
123+
/// This element resolved, as either an [ApiNarrowChannelModern] or an [ApiNarrowStream].
124+
ApiNarrowChannel resolve({required bool legacy}) {
125+
return legacy ? ApiNarrowStream._(operand, negated: negated)
126+
: ApiNarrowChannelModern._(operand, negated: negated);
127+
}
128+
}
129+
130+
/// An [ApiNarrowElement] with the 'channel' operator (and not the legacy 'stream').
131+
///
132+
/// To construct one of these, use [ApiNarrowChannel.resolve].
133+
class ApiNarrowChannelModern extends ApiNarrowChannel {
134+
@override String get operator => 'channel';
135+
136+
ApiNarrowChannelModern._(super.operand, {super.negated});
137+
}
138+
139+
/// An [ApiNarrowElement] with the legacy 'stream' operator.
140+
///
141+
/// To construct one of these, use [ApiNarrowChannel.resolve].
142+
class ApiNarrowStream extends ApiNarrowChannel {
143+
@override String get operator => 'stream';
144+
145+
ApiNarrowStream._(super.operand, {super.negated});
107146
}
108147

109148
class ApiNarrowTopic extends ApiNarrowElement {

lib/model/internal_link.dart

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ Uri narrowLink(PerAccountStore store, Narrow narrow, {int? nearMessageId}) {
7171
fragment.write('${element.operator}/');
7272

7373
switch (element) {
74-
case ApiNarrowStream():
74+
case ApiNarrowChannel():
7575
final streamId = element.operand;
7676
final name = store.streams[streamId]?.name ?? 'unknown';
7777
final slugifiedName = _encodeHashComponent(name.replaceAll(' ', '-'));
@@ -182,7 +182,7 @@ NarrowLink? _interpretNarrowSegments(List<String> segments, PerAccountStore stor
182182
assert(segments.isNotEmpty);
183183
assert(segments.length.isEven);
184184

185-
ApiNarrowStream? streamElement;
185+
ApiNarrowChannel? channelElement;
186186
ApiNarrowTopic? topicElement;
187187
ApiNarrowDm? dmElement;
188188
ApiNarrowWith? withElement;
@@ -196,10 +196,10 @@ NarrowLink? _interpretNarrowSegments(List<String> segments, PerAccountStore stor
196196
switch (operator) {
197197
case _NarrowOperator.stream:
198198
case _NarrowOperator.channel:
199-
if (streamElement != null) return null;
199+
if (channelElement != null) return null;
200200
final streamId = _parseStreamOperand(operand, store);
201201
if (streamId == null) return null;
202-
streamElement = ApiNarrowStream(streamId, negated: negated);
202+
channelElement = ApiNarrowChannel(streamId, negated: negated);
203203

204204
case _NarrowOperator.topic:
205205
case _NarrowOperator.subject:
@@ -238,7 +238,7 @@ NarrowLink? _interpretNarrowSegments(List<String> segments, PerAccountStore stor
238238

239239
final Narrow? narrow;
240240
if (isElementOperands.isNotEmpty) {
241-
if (streamElement != null || topicElement != null || dmElement != null || withElement != null) {
241+
if (channelElement != null || topicElement != null || dmElement != null || withElement != null) {
242242
return null;
243243
}
244244
if (isElementOperands.length > 1) return null;
@@ -257,10 +257,10 @@ NarrowLink? _interpretNarrowSegments(List<String> segments, PerAccountStore stor
257257
return null;
258258
}
259259
} else if (dmElement != null) {
260-
if (streamElement != null || topicElement != null || withElement != null) return null;
260+
if (channelElement != null || topicElement != null || withElement != null) return null;
261261
narrow = DmNarrow.withUsers(dmElement.operand, selfUserId: store.selfUserId);
262-
} else if (streamElement != null) {
263-
final streamId = streamElement.operand;
262+
} else if (channelElement != null) {
263+
final streamId = channelElement.operand;
264264
if (topicElement != null) {
265265
narrow = TopicNarrow(streamId, topicElement.operand, with_: withElement?.operand);
266266
} else {

lib/model/narrow.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ class ChannelNarrow extends Narrow {
8080
}
8181

8282
@override
83-
ApiNarrow apiEncode() => [ApiNarrowStream(streamId)];
83+
ApiNarrow apiEncode() => [ApiNarrowChannel(streamId)];
8484

8585
@override
8686
String toString() => 'ChannelNarrow($streamId)';
@@ -117,7 +117,7 @@ class TopicNarrow extends Narrow implements SendableNarrow {
117117

118118
@override
119119
ApiNarrow apiEncode() => [
120-
ApiNarrowStream(streamId),
120+
ApiNarrowChannel(streamId),
121121
ApiNarrowTopic(topic),
122122
if (with_ != null) ApiNarrowWith(with_!),
123123
];

test/api/route/messages_test.dart

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -92,14 +92,14 @@ void main() {
9292

9393
checkNarrow(const CombinedFeedNarrow().apiEncode(), jsonEncode([]));
9494
checkNarrow(const ChannelNarrow(12).apiEncode(), jsonEncode([
95-
{'operator': 'stream', 'operand': 12},
95+
{'operator': 'channel', 'operand': 12},
9696
]));
9797
checkNarrow(eg.topicNarrow(12, 'stuff').apiEncode(), jsonEncode([
98-
{'operator': 'stream', 'operand': 12},
98+
{'operator': 'channel', 'operand': 12},
9999
{'operator': 'topic', 'operand': 'stuff'},
100100
]));
101101
checkNarrow(eg.topicNarrow(12, 'stuff', with_: 1).apiEncode(), jsonEncode([
102-
{'operator': 'stream', 'operand': 12},
102+
{'operator': 'channel', 'operand': 12},
103103
{'operator': 'topic', 'operand': 'stuff'},
104104
{'operator': 'with', 'operand': 1},
105105
]));
@@ -113,7 +113,7 @@ void main() {
113113

114114
connection.zulipFeatureLevel = 270;
115115
checkNarrow(eg.topicNarrow(12, 'stuff', with_: 1).apiEncode(), jsonEncode([
116-
{'operator': 'stream', 'operand': 12},
116+
{'operator': 'channel', 'operand': 12},
117117
{'operator': 'topic', 'operand': 'stuff'},
118118
]));
119119
checkNarrow([ApiNarrowDm([123, 234])], jsonEncode([
@@ -123,6 +123,19 @@ void main() {
123123
{'operator': 'dm', 'operand': [123, 234]},
124124
]));
125125

126+
connection.zulipFeatureLevel = 249;
127+
checkNarrow(const ChannelNarrow(12).apiEncode(), jsonEncode([
128+
{'operator': 'stream', 'operand': 12},
129+
]));
130+
checkNarrow(eg.topicNarrow(12, 'stuff').apiEncode(), jsonEncode([
131+
{'operator': 'stream', 'operand': 12},
132+
{'operator': 'topic', 'operand': 'stuff'},
133+
]));
134+
checkNarrow(eg.topicNarrow(12, 'stuff', with_: 1).apiEncode(), jsonEncode([
135+
{'operator': 'stream', 'operand': 12},
136+
{'operator': 'topic', 'operand': 'stuff'},
137+
]));
138+
126139
connection.zulipFeatureLevel = 176;
127140
checkNarrow(eg.topicNarrow(12, 'stuff', with_: 1).apiEncode(), jsonEncode([
128141
{'operator': 'stream', 'operand': 12},

test/model/compose_test.dart

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,19 @@ hello
349349
await store.addStream(stream);
350350
await store.addUser(sender);
351351

352+
check(quoteAndReplyPlaceholder(
353+
GlobalLocalizations.zulipLocalizations, store, message: message)).equals('''
354+
@_**Full Name|123** [said](${eg.selfAccount.realmUrl}#narrow/channel/1-test-here/topic/some.20topic/near/${message.id}): *(loading message ${message.id})*
355+
''');
356+
357+
check(quoteAndReply(store, message: message, rawContent: 'Hello world!')).equals('''
358+
@_**Full Name|123** [said](${eg.selfAccount.realmUrl}#narrow/channel/1-test-here/topic/some.20topic/near/${message.id}):
359+
```quote
360+
Hello world!
361+
```
362+
''');
363+
364+
store.connection.zulipFeatureLevel = 249;
352365
check(quoteAndReplyPlaceholder(
353366
GlobalLocalizations.zulipLocalizations, store, message: message)).equals('''
354367
@_**Full Name|123** [said](${eg.selfAccount.realmUrl}#narrow/stream/1-test-here/topic/some.20topic/near/${message.id}): *(loading message ${message.id})*

test/model/internal_link_test.dart

Lines changed: 29 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -60,15 +60,16 @@ void main() {
6060
.equals(store.realmUrl.resolve('#narrow/is/starred/near/1'));
6161
});
6262

63-
test('ChannelNarrow / TopicNarrow', () {
63+
group('ChannelNarrow / TopicNarrow', () {
6464
void checkNarrow(String expectedFragment, {
6565
required int streamId,
6666
required String name,
6767
String? topic,
6868
int? nearMessageId,
69+
int? zulipFeatureLevel = eg.futureZulipFeatureLevel,
6970
}) async {
7071
assert(expectedFragment.startsWith('#'), 'wrong-looking expectedFragment');
71-
final store = eg.store();
72+
final store = eg.store()..connection.zulipFeatureLevel = zulipFeatureLevel;
7273
await store.addStream(eg.stream(streamId: streamId, name: name));
7374
final narrow = topic == null
7475
? ChannelNarrow(streamId)
@@ -77,22 +78,32 @@ void main() {
7778
.equals(store.realmUrl.resolve(expectedFragment));
7879
}
7980

80-
checkNarrow(streamId: 1, name: 'announce', '#narrow/stream/1-announce');
81-
checkNarrow(streamId: 378, name: 'api design', '#narrow/stream/378-api-design');
82-
checkNarrow(streamId: 391, name: 'Outreachy', '#narrow/stream/391-Outreachy');
83-
checkNarrow(streamId: 415, name: 'chat.zulip.org', '#narrow/stream/415-chat.2Ezulip.2Eorg');
84-
checkNarrow(streamId: 419, name: 'français', '#narrow/stream/419-fran.C3.A7ais');
85-
checkNarrow(streamId: 403, name: 'Hshs[™~}(.', '#narrow/stream/403-Hshs.5B.E2.84.A2~.7D.28.2E');
86-
checkNarrow(streamId: 60, name: 'twitter', nearMessageId: 1570686, '#narrow/stream/60-twitter/near/1570686');
87-
88-
checkNarrow(streamId: 48, name: 'mobile', topic: 'Welcome screen UI',
89-
'#narrow/stream/48-mobile/topic/Welcome.20screen.20UI');
90-
checkNarrow(streamId: 243, name: 'mobile-team', topic: 'Podfile.lock clash #F92',
91-
'#narrow/stream/243-mobile-team/topic/Podfile.2Elock.20clash.20.23F92');
92-
checkNarrow(streamId: 377, name: 'translation/zh_tw', topic: '翻譯 "stream"',
93-
'#narrow/stream/377-translation.2Fzh_tw/topic/.E7.BF.BB.E8.AD.AF.20.22stream.22');
94-
checkNarrow(streamId: 42, name: 'Outreachy 2016-2017', topic: '2017-18 Stream?', nearMessageId: 302690,
95-
'#narrow/stream/42-Outreachy-2016-2017/topic/2017-18.20Stream.3F/near/302690');
81+
test('modern including "channel" operator', () {
82+
checkNarrow(streamId: 1, name: 'announce', '#narrow/channel/1-announce');
83+
checkNarrow(streamId: 378, name: 'api design', '#narrow/channel/378-api-design');
84+
checkNarrow(streamId: 391, name: 'Outreachy', '#narrow/channel/391-Outreachy');
85+
checkNarrow(streamId: 415, name: 'chat.zulip.org', '#narrow/channel/415-chat.2Ezulip.2Eorg');
86+
checkNarrow(streamId: 419, name: 'français', '#narrow/channel/419-fran.C3.A7ais');
87+
checkNarrow(streamId: 403, name: 'Hshs[™~}(.', '#narrow/channel/403-Hshs.5B.E2.84.A2~.7D.28.2E');
88+
checkNarrow(streamId: 60, name: 'twitter', nearMessageId: 1570686, '#narrow/channel/60-twitter/near/1570686');
89+
90+
checkNarrow(streamId: 48, name: 'mobile', topic: 'Welcome screen UI',
91+
'#narrow/channel/48-mobile/topic/Welcome.20screen.20UI');
92+
checkNarrow(streamId: 243, name: 'mobile-team', topic: 'Podfile.lock clash #F92',
93+
'#narrow/channel/243-mobile-team/topic/Podfile.2Elock.20clash.20.23F92');
94+
checkNarrow(streamId: 377, name: 'translation/zh_tw', topic: '翻譯 "stream"',
95+
'#narrow/channel/377-translation.2Fzh_tw/topic/.E7.BF.BB.E8.AD.AF.20.22stream.22');
96+
checkNarrow(streamId: 42, name: 'Outreachy 2016-2017', topic: '2017-18 Stream?', nearMessageId: 302690,
97+
'#narrow/channel/42-Outreachy-2016-2017/topic/2017-18.20Stream.3F/near/302690');
98+
});
99+
100+
test('legacy including "stream" operator', () {
101+
checkNarrow(streamId: 1, name: 'announce', zulipFeatureLevel: 249,
102+
'#narrow/stream/1-announce');
103+
checkNarrow(streamId: 48, name: 'mobile-team', topic: 'Welcome screen UI',
104+
zulipFeatureLevel: 249,
105+
'#narrow/stream/48-mobile-team/topic/Welcome.20screen.20UI');
106+
});
96107
});
97108

98109
test('DmNarrow', () {

test/model/message_list_test.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ void main() {
156156
..method.equals('GET')
157157
..url.path.equals('/api/v1/messages')
158158
..url.queryParameters.deepEquals({
159-
'narrow': jsonEncode(narrow),
159+
'narrow': jsonEncode(resolveApiNarrowForServer(narrow, connection.zulipFeatureLevel!)),
160160
'anchor': anchor,
161161
if (includeAnchor != null) 'include_anchor': includeAnchor.toString(),
162162
'num_before': numBefore.toString(),

test/widgets/action_sheet_test.dart

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -350,7 +350,7 @@ void main() {
350350
'num_before': '0',
351351
'num_after': '1000',
352352
'narrow': jsonEncode([
353-
{'operator': 'stream', 'operand': channelId},
353+
{'operator': 'channel', 'operand': channelId},
354354
{'operator': 'is', 'operand': 'unread'},
355355
]),
356356
'op': 'add',
@@ -1014,7 +1014,9 @@ void main() {
10141014
check(connection.lastRequest).isA<http.Request>()
10151015
..url.path.equals('/api/v1/messages/flags/narrow')
10161016
..bodyFields['narrow'].equals(jsonEncode([
1017-
...eg.topicNarrow(someChannel.streamId, someTopic).apiEncode(),
1017+
...resolveApiNarrowForServer(
1018+
eg.topicNarrow(someChannel.streamId, someTopic).apiEncode(),
1019+
connection.zulipFeatureLevel!),
10181020
ApiNarrowIs(IsOperand.unread),
10191021
]))
10201022
..bodyFields['op'].equals('add')
@@ -1656,7 +1658,9 @@ void main() {
16561658
'include_anchor': 'true',
16571659
'num_before': '0',
16581660
'num_after': '1000',
1659-
'narrow': jsonEncode(TopicNarrow.ofMessage(message).apiEncode()),
1661+
'narrow': jsonEncode(resolveApiNarrowForServer(
1662+
TopicNarrow.ofMessage(message).apiEncode(),
1663+
connection.zulipFeatureLevel!)),
16601664
'op': 'remove',
16611665
'flag': 'read',
16621666
});
@@ -1701,7 +1705,9 @@ void main() {
17011705
..method.equals('POST')
17021706
..url.path.equals('/api/v1/messages/flags/narrow')
17031707
..bodyFields['narrow'].equals(
1704-
jsonEncode(eg.topicNarrow(newStream.streamId, newTopic).apiEncode()));
1708+
jsonEncode(resolveApiNarrowForServer(
1709+
eg.topicNarrow(newStream.streamId, newTopic).apiEncode(),
1710+
connection.zulipFeatureLevel!)));
17051711
});
17061712

17071713
testWidgets('shows error when fails', (tester) async {

test/widgets/actions_test.dart

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ void main() {
7474
'include_anchor': 'false',
7575
'num_before': '0',
7676
'num_after': '1000',
77-
'narrow': jsonEncode(apiNarrow),
77+
'narrow': jsonEncode(resolveApiNarrowForServer(apiNarrow, connection.zulipFeatureLevel!)),
7878
'op': 'add',
7979
'flag': 'read',
8080
});
@@ -155,7 +155,7 @@ void main() {
155155
'include_anchor': 'false',
156156
'num_before': '0',
157157
'num_after': '1000',
158-
'narrow': jsonEncode(apiNarrow),
158+
'narrow': jsonEncode(resolveApiNarrowForServer(apiNarrow, connection.zulipFeatureLevel!)),
159159
'op': 'add',
160160
'flag': 'read',
161161
});
@@ -180,7 +180,7 @@ void main() {
180180
'include_anchor': 'false',
181181
'num_before': '0',
182182
'num_after': '1000',
183-
'narrow': jsonEncode(apiNarrow),
183+
'narrow': jsonEncode(resolveApiNarrowForServer(apiNarrow, connection.zulipFeatureLevel!)),
184184
'op': 'add',
185185
'flag': 'read',
186186
});
@@ -199,7 +199,7 @@ void main() {
199199
'include_anchor': 'false',
200200
'num_before': '0',
201201
'num_after': '1000',
202-
'narrow': jsonEncode(apiNarrow),
202+
'narrow': jsonEncode(resolveApiNarrowForServer(apiNarrow, connection.zulipFeatureLevel!)),
203203
'op': 'add',
204204
'flag': 'read',
205205
});
@@ -223,7 +223,7 @@ void main() {
223223
'include_anchor': 'false',
224224
'num_before': '0',
225225
'num_after': '1000',
226-
'narrow': jsonEncode(apiNarrow),
226+
'narrow': jsonEncode(resolveApiNarrowForServer(apiNarrow, connection.zulipFeatureLevel!)),
227227
'op': 'add',
228228
'flag': 'read',
229229
});

0 commit comments

Comments
 (0)