Skip to content

Commit c0f4c78

Browse files
committed
WIP
1 parent 638f8c0 commit c0f4c78

File tree

7 files changed

+98
-19
lines changed

7 files changed

+98
-19
lines changed

lib/api/model/narrow.dart

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,20 +10,30 @@ typedef ApiNarrow = List<ApiNarrowElement>;
1010
// TODO(server-7) remove [ApiNarrowDm] reference in dartdoc
1111
ApiNarrow resolveApiNarrowElements(ApiNarrow narrow, int zulipFeatureLevel) {
1212
bool hasDmElement = false;
13+
bool hasWithElement = false;
1314
for (final element in narrow) {
1415
switch (element) {
15-
case ApiNarrowDm(): hasDmElement = true;
16+
case ApiNarrowDm(): hasDmElement = true;
17+
case ApiNarrowWith(): hasWithElement = true;
1618
default:
1719
}
1820
}
19-
if (!hasDmElement) return narrow;
21+
if (!hasDmElement && !hasWithElement) return narrow;
2022

2123
final supportsOperatorDm = zulipFeatureLevel >= 177; // TODO(server-7)
22-
23-
return narrow.map((element) => switch (element) {
24-
ApiNarrowDm() => element.resolve(legacy: !supportsOperatorDm),
25-
_ => element,
26-
}).toList();
24+
final supportsOperatorWith = zulipFeatureLevel >= 271; // TODO(server-9)
25+
26+
ApiNarrow result = narrow;
27+
if (hasDmElement) {
28+
result = narrow.map((element) => switch (element) {
29+
ApiNarrowDm() => element.resolve(legacy: !supportsOperatorDm),
30+
_ => element,
31+
}).toList();
32+
}
33+
if (hasWithElement && !supportsOperatorWith) {
34+
result.removeWhere((element) => element is ApiNarrowWith);
35+
}
36+
return result;
2737
}
2838

2939
/// An element in the list representing a narrow in the Zulip API.
@@ -72,6 +82,22 @@ class ApiNarrowTopic extends ApiNarrowElement {
7282
);
7383
}
7484

85+
/// An [ApiNarrowElement] with the 'with' operator.
86+
///
87+
/// If part of [ApiNarrow] use [resolveApiNarrowElements].
88+
class ApiNarrowWith extends ApiNarrowElement {
89+
@override String get operator => 'with';
90+
91+
@override final int operand;
92+
93+
ApiNarrowWith(this.operand, {super.negated});
94+
95+
factory ApiNarrowWith.fromJson(Map<String, dynamic> json) => ApiNarrowWith(
96+
json['operand'] as int,
97+
negated: json['negated'] as bool? ?? false,
98+
);
99+
}
100+
75101
/// An [ApiNarrowElement] with the 'dm', or legacy 'pm-with', operator.
76102
///
77103
/// An instance directly of this class must not be serialized with [jsonEncode],

lib/model/internal_link.dart

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,8 @@ Uri narrowLink(PerAccountStore store, Narrow narrow, {int? nearMessageId}) {
7777
fragment.write('$streamId-$slugifiedName');
7878
case ApiNarrowTopic():
7979
fragment.write(_encodeHashComponent(element.operand));
80+
case ApiNarrowWith():
81+
fragment.write(element.operand.toString());
8082
case ApiNarrowDmModern():
8183
final suffix = element.operand.length >= 3 ? 'group' : 'dm';
8284
fragment.write('${element.operand.join(',')}-$suffix');
@@ -151,6 +153,7 @@ Narrow? _interpretNarrowSegments(List<String> segments, PerAccountStore store) {
151153

152154
ApiNarrowStream? streamElement;
153155
ApiNarrowTopic? topicElement;
156+
ApiNarrowWith? withElement;
154157
ApiNarrowDm? dmElement;
155158
Set<IsOperand> isElementOperands = {};
156159

@@ -185,9 +188,12 @@ Narrow? _interpretNarrowSegments(List<String> segments, PerAccountStore store) {
185188
isElementOperands.add(IsOperand.fromRawString(operand));
186189

187190
case _NarrowOperator.near: // TODO(#82): support for near
188-
case _NarrowOperator.with_: // TODO(#683): support for with
189191
continue;
190192

193+
case _NarrowOperator.with_:
194+
if (withElement != null) return null;
195+
withElement = ApiNarrowWith(int.parse(operand, radix: 10));
196+
191197
case _NarrowOperator.unknown:
192198
return null;
193199
}
@@ -216,7 +222,7 @@ Narrow? _interpretNarrowSegments(List<String> segments, PerAccountStore store) {
216222
} else if (streamElement != null) {
217223
final streamId = streamElement.operand;
218224
if (topicElement != null) {
219-
return TopicNarrow(streamId, topicElement.operand);
225+
return TopicNarrow(streamId, topicElement.operand, with_: withElement?.operand);
220226
} else {
221227
return ChannelNarrow(streamId);
222228
}

lib/model/message_list.dart

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -490,14 +490,49 @@ class MessageListView with ChangeNotifier, _MessageSequence {
490490
_fetched = true;
491491
_haveOldest = result.foundOldest;
492492
_updateEndMarkers();
493+
_adjustTopicPermalinkNarrow(narrow, result.messages.firstOrNull);
493494
notifyListeners();
494495
}
495496

497+
/// Update [narrow] for the result of a "with" narrow (topic permalink) fetch.
498+
///
499+
/// To avoid an extra round trip, the server handles [ApiNarrowWith]
500+
/// by returning results from the indicated message's current stream/topic
501+
/// (if the user has access),
502+
/// even if that differs from the narrow's stream/topic filters
503+
/// because the message was moved.
504+
///
505+
/// If such a "redirect" happened, this helper updates the stream and topic
506+
/// in [narrow] to match the message's current conversation.
507+
/// This also removes the "with" component from [narrow]
508+
/// whether or not a redirect happened.
509+
///
510+
/// See API doc:
511+
/// https://zulip.com/api/construct-narrow#message-ids
512+
void _adjustTopicPermalinkNarrow(Narrow narrow_, Message? firstMessageOrNull) {
513+
if (narrow_ is! TopicNarrow || narrow_.with_ == null) return;
514+
515+
switch (firstMessageOrNull) {
516+
case null:
517+
// This can't be a redirect; a redirect can't produce an empty result.
518+
// (The server only redirects if the message is accessible to the user,
519+
// and if it is, it'll appear in the result, making it non-empty.)
520+
narrow = narrow_.sansWith();
521+
case StreamMessage(:final streamId, :final topic):
522+
narrow = TopicNarrow(streamId, topic);
523+
case DmMessage(): // TODO(log)
524+
assert(false);
525+
}
526+
}
527+
496528
/// Fetch the next batch of older messages, if applicable.
497529
Future<void> fetchOlder() async {
498530
if (haveOldest) return;
499531
if (fetchingOlder) return;
500532
assert(fetched);
533+
assert(narrow is! TopicNarrow
534+
// We only intend to send "with" in [fetchInitial]; see there.
535+
|| (narrow as TopicNarrow).with_ == null);
501536
assert(messages.isNotEmpty);
502537
_fetchingOlder = true;
503538
_updateEndMarkers();

lib/model/narrow.dart

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -92,14 +92,17 @@ class ChannelNarrow extends Narrow {
9292
}
9393

9494
class TopicNarrow extends Narrow implements SendableNarrow {
95-
const TopicNarrow(this.streamId, this.topic);
95+
const TopicNarrow(this.streamId, this.topic, {this.with_});
9696

9797
factory TopicNarrow.ofMessage(StreamMessage message) {
9898
return TopicNarrow(message.streamId, message.topic);
9999
}
100100

101101
final int streamId;
102102
final String topic;
103+
final int? with_;
104+
105+
TopicNarrow sansWith() => TopicNarrow(streamId, topic);
103106

104107
@override
105108
bool containsMessage(Message message) {
@@ -108,22 +111,26 @@ class TopicNarrow extends Narrow implements SendableNarrow {
108111
}
109112

110113
@override
111-
ApiNarrow apiEncode() => [ApiNarrowStream(streamId), ApiNarrowTopic(topic)];
114+
ApiNarrow apiEncode() => [
115+
ApiNarrowStream(streamId),
116+
ApiNarrowTopic(topic),
117+
if (with_ != null) ApiNarrowWith(with_!),
118+
];
112119

113120
@override
114121
StreamDestination get destination => StreamDestination(streamId, topic);
115122

116123
@override
117-
String toString() => 'TopicNarrow($streamId, $topic)';
124+
String toString() => 'TopicNarrow($streamId, $topic, with: $with_)';
118125

119126
@override
120127
bool operator ==(Object other) {
121128
if (other is! TopicNarrow) return false;
122-
return other.streamId == streamId && other.topic == topic;
129+
return other.streamId == streamId && other.topic == topic && other.with_ == with_;
123130
}
124131

125132
@override
126-
int get hashCode => Object.hash('TopicNarrow', streamId, topic);
133+
int get hashCode => Object.hash('TopicNarrow', streamId, topic, with_);
127134
}
128135

129136
/// The narrow for a direct-message conversation.

test/example_data.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ void _checkPositive(int? value, String description) {
2424
final Uri realmUrl = Uri.parse('https://chat.example/');
2525
Uri get _realmUrl => realmUrl;
2626

27-
const String recentZulipVersion = '8.0';
28-
const int recentZulipFeatureLevel = 185;
27+
const String recentZulipVersion = '9.0';
28+
const int recentZulipFeatureLevel = 271;
2929
const int futureZulipFeatureLevel = 9999;
3030

3131
GetServerSettingsResult serverSettings({

test/model/internal_link_test.dart

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -173,12 +173,12 @@ void main() {
173173
const testCases = [
174174
('/#narrow/stream/check/topic/test', TopicNarrow(1, 'test')),
175175
('/#narrow/stream/mobile/subject/topic/near/378333', TopicNarrow(3, 'topic')),
176-
('/#narrow/stream/mobile/subject/topic/with/1', TopicNarrow(3, 'topic')),
176+
('/#narrow/stream/mobile/subject/topic/with/1', TopicNarrow(3, 'topic', with_: 1)),
177177
('/#narrow/stream/mobile/topic/topic/', TopicNarrow(3, 'topic')),
178178
('/#narrow/stream/stream/topic/topic/near/1', TopicNarrow(5, 'topic')),
179-
('/#narrow/stream/stream/topic/topic/with/22', TopicNarrow(5, 'topic')),
179+
('/#narrow/stream/stream/topic/topic/with/22', TopicNarrow(5, 'topic', with_: 22)),
180180
('/#narrow/stream/stream/subject/topic/near/1', TopicNarrow(5, 'topic')),
181-
('/#narrow/stream/stream/subject/topic/with/333', TopicNarrow(5, 'topic')),
181+
('/#narrow/stream/stream/subject/topic/with/333', TopicNarrow(5, 'topic', with_: 333)),
182182
('/#narrow/stream/stream/subject/topic', TopicNarrow(5, 'topic')),
183183
];
184184
testExpectedNarrows(testCases, streams: streams);

test/model/message_list_test.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,11 @@ void main() {
103103
generateMessages: (i) => eg.streamMessage(
104104
stream: someStream,
105105
topic: someTopic)),
106+
(desc: 'topic permalink where server redirects to moved messages',
107+
narrow: TopicNarrow(someStream.streamId, someTopic, with_: 1),
108+
generateMessages: (i) => eg.streamMessage(
109+
stream: someStream,
110+
topic: '$someTopic moved')),
106111
];
107112

108113
for (final case_ in smokeCases) {

0 commit comments

Comments
 (0)