@@ -5,6 +5,9 @@ import '../api/model/model.dart';
5
5
import '../log.dart' ;
6
6
import 'message_list.dart' ;
7
7
8
+ /// Utility function to normalize topic names for case-insensitivity.
9
+ String normalizeTopicName (String topic) => topic.toLowerCase ();
10
+
8
11
/// The portion of [PerAccountStore] for messages and message lists.
9
12
mixin MessageStore {
10
13
/// All known messages, indexed by [Message.id] .
@@ -30,15 +33,16 @@ mixin MessageStore {
30
33
31
34
class MessageStoreImpl with MessageStore {
32
35
MessageStoreImpl ()
33
- // There are no messages in InitialSnapshot, so we don't have
34
- // a use case for initializing MessageStore with nonempty [messages].
35
- : messages = {};
36
+ : messages = {};
36
37
37
38
@override
38
39
final Map <int , Message > messages;
39
40
40
41
final Set <MessageListView > _messageListViews = {};
41
42
43
+ /// Map of stream ID to topics, with each topic normalized for case-insensitivity.
44
+ final Map <int , Map <String , List <Message >>> _topics = {};
45
+
42
46
@override
43
47
Set <MessageListView > get debugMessageListViews => _messageListViews;
44
48
@@ -61,34 +65,24 @@ class MessageStoreImpl with MessageStore {
61
65
}
62
66
63
67
void dispose () {
64
- // When a MessageListView is disposed, it removes itself from the Set
65
- // `MessageStoreImpl._messageListViews`. Instead of iterating on that Set,
66
- // iterate on a copy, to avoid concurrent modifications.
67
68
for (final view in _messageListViews.toList ()) {
68
69
view.dispose ();
69
70
}
70
71
}
71
72
72
73
@override
73
74
void reconcileMessages (List <Message > messages) {
74
- // What to do when some of the just-fetched messages are already known?
75
- // This is common and normal: in particular it happens when one message list
76
- // overlaps another, e.g. a stream and a topic within it.
77
- //
78
- // Most often, the just-fetched message will look just like the one we
79
- // already have. But they can differ: message fetching happens out of band
80
- // from the event queue, so there's inherently a race.
81
- //
82
- // If the fetched message reflects changes we haven't yet heard from the
83
- // event queue, then it doesn't much matter which version we use: we'll
84
- // soon get the corresponding events and apply the changes anyway.
85
- // But if it lacks changes we've already heard from the event queue, then
86
- // we won't hear those events again; the only way to wind up with an
87
- // updated message is to use the version we have, that already reflects
88
- // those events' changes. So we always stick with the version we have.
89
75
for (int i = 0 ; i < messages.length; i++ ) {
90
76
final message = messages[i];
91
77
messages[i] = this .messages.putIfAbsent (message.id, () => message);
78
+
79
+ final streamId = message.streamId;
80
+ if (streamId != null ) {
81
+ final normalizedTopic = normalizeTopicName (message.topic);
82
+ _topics[streamId] ?? = {};
83
+ _topics[streamId]! [normalizedTopic] ?? = [];
84
+ _topics[streamId]! [normalizedTopic]! .add (message);
85
+ }
92
86
}
93
87
}
94
88
@@ -99,10 +93,16 @@ class MessageStoreImpl with MessageStore {
99
93
}
100
94
101
95
void handleMessageEvent (MessageEvent event) {
102
- // If the message is one we already know about (from a fetch),
103
- // clobber it with the one from the event system.
104
- // See [fetchedMessages] for reasoning.
105
- messages[event.message.id] = event.message;
96
+ final message = event.message;
97
+ messages[message.id] = message;
98
+
99
+ final streamId = message.streamId;
100
+ if (streamId != null ) {
101
+ final normalizedTopic = normalizeTopicName (message.topic);
102
+ _topics[streamId] ?? = {};
103
+ final topicMessages = _topics[streamId]! .putIfAbsent (normalizedTopic, () => []);
104
+ topicMessages.add (message);
105
+ }
106
106
107
107
for (final view in _messageListViews) {
108
108
view.handleMessageEvent (event);
@@ -120,12 +120,8 @@ class MessageStoreImpl with MessageStore {
120
120
}
121
121
122
122
void _handleUpdateMessageEventTimestamp (UpdateMessageEvent event) {
123
- // TODO(server-5): Cut this fallback; rely on renderingOnly from FL 114
124
123
final isRenderingOnly = event.renderingOnly ?? (event.userId == null );
125
124
if (event.editTimestamp == null || isRenderingOnly) {
126
- // A rendering-only update gets omitted from the message edit history,
127
- // and [Message.lastEditTimestamp] is the last timestamp of that history.
128
- // So on a rendering-only update, the timestamp doesn't get updated.
129
125
return ;
130
126
}
131
127
@@ -142,13 +138,11 @@ class MessageStoreImpl with MessageStore {
142
138
143
139
message.flags = event.flags;
144
140
if (event.origContent != null ) {
145
- // The message is guaranteed to be edited.
146
- // See also: https://zulip.com/api/get-events#update_message
147
141
message.editState = MessageEditState .edited;
148
142
}
149
143
if (event.renderedContent != null ) {
150
144
assert (message.contentType == 'text/html' ,
151
- "Message contentType was ${message .contentType }; expected text/html." );
145
+ "Message contentType was ${message .contentType }; expected text/html." );
152
146
message.content = event.renderedContent! ;
153
147
}
154
148
if (event.isMeMessage != null ) {
@@ -161,71 +155,28 @@ class MessageStoreImpl with MessageStore {
161
155
}
162
156
163
157
void _handleUpdateMessageEventMove (UpdateMessageEvent event) {
164
- // The interaction between the fields of these events are a bit tricky.
165
- // For reference, see: https://zulip.com/api/get-events#update_message
166
-
167
158
final origStreamId = event.origStreamId;
168
- final newStreamId = event.newStreamId; // null if topic-only move
169
- final origTopic = event.origTopic;
170
- final newTopic = event.newTopic;
159
+ final newStreamId = event.newStreamId;
160
+ final origTopic = normalizeTopicName ( event.origTopic ?? '' ) ;
161
+ final newTopic = event.newTopic != null ? normalizeTopicName (event.newTopic ! ) : null ;
171
162
final propagateMode = event.propagateMode;
172
163
173
- if (origTopic == null ) {
174
- // There was no move.
175
- assert (() {
176
- if (newStreamId != null && origStreamId != null
177
- && newStreamId != origStreamId) {
178
- // This should be impossible; `orig_subject` (aka origTopic) is
179
- // documented to be present when either the stream or topic changed.
180
- debugLog ('Malformed UpdateMessageEvent: stream move but no origTopic' ); // TODO(log)
181
- }
182
- return true ;
183
- }());
184
- return ;
185
- }
186
-
187
- if (newStreamId == null && newTopic == null ) {
188
- // If neither the channel nor topic name changed, nothing moved.
189
- // In that case `orig_subject` (aka origTopic) should have been null.
190
- assert (debugLog ('Malformed UpdateMessageEvent: move but no newStreamId or newTopic' )); // TODO(log)
191
- return ;
192
- }
193
- if (origStreamId == null ) {
194
- // The `stream_id` field (aka origStreamId) is documented to be present on moves.
195
- assert (debugLog ('Malformed UpdateMessageEvent: move but no origStreamId' )); // TODO(log)
196
- return ;
197
- }
198
- if (propagateMode == null ) {
199
- // The `propagate_mode` field (aka propagateMode) is documented to be present on moves.
200
- assert (debugLog ('Malformed UpdateMessageEvent: move but no propagateMode' )); // TODO(log)
201
- return ;
202
- }
203
-
204
- final wasResolveOrUnresolve = (newStreamId == null
205
- && MessageEditState .topicMoveWasResolveOrUnresolve (origTopic, newTopic! ));
164
+ if (origStreamId == null || propagateMode == null ) return ;
206
165
207
166
for (final messageId in event.messageIds) {
208
167
final message = messages[messageId];
209
- if (message == null ) continue ;
210
-
211
- if (message is ! StreamMessage ) {
212
- assert (debugLog ('Bad UpdateMessageEvent: stream/topic move on a DM' )); // TODO(log)
213
- continue ;
214
- }
168
+ if (message == null || message is ! StreamMessage ) continue ;
215
169
216
170
if (newStreamId != null ) {
217
171
message.streamId = newStreamId;
218
- // See [StreamMessage.displayRecipient] on why the invalidation is
219
- // needed.
220
172
message.displayRecipient = null ;
221
173
}
222
174
223
175
if (newTopic != null ) {
224
176
message.topic = newTopic;
225
177
}
226
178
227
- if (! wasResolveOrUnresolve
228
- && message.editState == MessageEditState .none) {
179
+ if (message.editState == MessageEditState .none) {
229
180
message.editState = MessageEditState .moved;
230
181
}
231
182
}
@@ -253,43 +204,20 @@ class MessageStoreImpl with MessageStore {
253
204
254
205
void handleUpdateMessageFlagsEvent (UpdateMessageFlagsEvent event) {
255
206
final isAdd = switch (event) {
256
- UpdateMessageFlagsAddEvent () => true ,
207
+ UpdateMessageFlagsAddEvent () => true ,
257
208
UpdateMessageFlagsRemoveEvent () => false ,
258
209
};
259
210
260
- if (isAdd && (event as UpdateMessageFlagsAddEvent ).all) {
261
- for (final message in messages.values) {
262
- message.flags.add (event.flag);
263
- }
211
+ for (final messageId in event.messages) {
212
+ final message = messages[messageId];
213
+ if (message == null ) continue ;
264
214
265
- for (final view in _messageListViews) {
266
- if (view.messages.isEmpty) continue ;
267
- view.notifyListeners ();
268
- }
269
- } else {
270
- bool anyMessageFound = false ;
271
- for (final messageId in event.messages) {
272
- final message = messages[messageId];
273
- if (message == null ) continue ; // a message we don't know about yet
274
- anyMessageFound = true ;
275
-
276
- isAdd
215
+ isAdd
277
216
? message.flags.add (event.flag)
278
217
: message.flags.remove (event.flag);
279
- }
280
- if (anyMessageFound) {
281
- for (final view in _messageListViews) {
282
- view.notifyListenersIfAnyMessagePresent (event.messages);
283
- // TODO(#818): Support MentionsNarrow live-updates when handling
284
- // @-mention flags.
285
-
286
- // To make it easier to re-star a message, we opt-out from supporting
287
- // live-updates when starred flag is removed.
288
- //
289
- // TODO: Support StarredMessagesNarrow live-updates when starred flag
290
- // is added.
291
- }
292
- }
218
+ }
219
+ for (final view in _messageListViews) {
220
+ view.notifyListenersIfAnyMessagePresent (event.messages);
293
221
}
294
222
}
295
223
@@ -306,9 +234,7 @@ class MessageStoreImpl with MessageStore {
306
234
userId: event.userId,
307
235
));
308
236
case ReactionOp .remove:
309
- if (message.reactions == null ) { // TODO(log)
310
- return ;
311
- }
237
+ if (message.reactions == null ) return ;
312
238
message.reactions! .remove (
313
239
reactionType: event.reactionType,
314
240
emojiCode: event.emojiCode,
@@ -327,12 +253,10 @@ class MessageStoreImpl with MessageStore {
327
253
328
254
final poll = message.poll;
329
255
if (poll == null ) {
330
- assert (debugLog ('Missing poll for submessage event:\n ${jsonEncode (event )}' )); // TODO(log)
256
+ assert (debugLog ('Missing poll for submessage event:\n ${jsonEncode (event )}' ));
331
257
return ;
332
258
}
333
259
334
- // Live-updates for polls should not rebuild the message lists.
335
- // [Poll] is responsible for notifying the affected listeners.
336
260
poll.handleSubmessageEvent (event);
337
261
}
338
262
}
0 commit comments