Skip to content

Commit c339c0a

Browse files
committed
msglist: Handle updated events in MessageListView (#118).
Processes an UpdateMessageEvent and hands it off to the MessageListView to update, if the message is visible in the MessageListView. This completes the changes required for issue #118.
1 parent 2465701 commit c339c0a

File tree

6 files changed

+278
-6
lines changed

6 files changed

+278
-6
lines changed

lib/api/model/model.dart

+4-4
Original file line numberDiff line numberDiff line change
@@ -250,13 +250,13 @@ class Subscription {
250250
sealed class Message {
251251
final String? avatarUrl;
252252
final String client;
253-
final String content;
253+
String content;
254254
final String contentType;
255255

256256
// final List<MessageEditHistory> editHistory; // TODO handle
257257
final int id;
258-
final bool isMeMessage;
259-
final int? lastEditTimestamp;
258+
bool isMeMessage;
259+
int? lastEditTimestamp;
260260

261261
// final List<Reaction> reactions; // TODO handle
262262
final int recipientId;
@@ -271,7 +271,7 @@ sealed class Message {
271271

272272
// final List<TopicLink> topicLinks; // TODO handle
273273
// final string type; // handled by runtime type of object
274-
final List<String> flags; // TODO enum
274+
List<String> flags; // TODO enum
275275
final String? matchContent;
276276
final String? matchSubject;
277277

lib/model/message_list.dart

+66
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
import 'package:collection/collection.dart';
12
import 'package:flutter/foundation.dart';
23

4+
import '../api/model/events.dart';
35
import '../api/model/model.dart';
46
import '../api/route/messages.dart';
57
import 'content.dart';
@@ -86,6 +88,70 @@ class MessageListView extends ChangeNotifier {
8688
notifyListeners();
8789
}
8890

91+
_applyChangesToMessage(UpdateMessageEvent event, Message message) {
92+
//In earlier server versions, omitting the userId indicates that this is a rendering-only update.
93+
//That means this change was initiated by the server, not the user.
94+
final isRenderingOnly = (event.renderingOnly != null && event.renderingOnly == true) ||
95+
event.userId == null;
96+
97+
if (event.editTimestamp != null && !isRenderingOnly) {
98+
//Only update the timestamp if this was a user-led update,
99+
//not a server-only update
100+
message.lastEditTimestamp = event.editTimestamp;
101+
}
102+
103+
if (!event.flags.equals(message.flags)) {
104+
message.flags = event.flags;
105+
}
106+
107+
if (event.renderedContent != null) {
108+
assert(message.contentType == 'text/html', "Expected message to have html contentType. Instead, got ${message.contentType}");
109+
message.content = event.renderedContent!;
110+
}
111+
112+
if (event.isMeMessage != null) {
113+
message.isMeMessage = event.isMeMessage!;
114+
}
115+
116+
}
117+
118+
//This is almost directly copied from package:collection/src/algorithms.dart.
119+
//The way that package was set up doesn't allow us to search
120+
//for a message ID among a bunch of message objects - this is a quick
121+
//modification of that method to work here for us.
122+
@visibleForTesting
123+
int findMessageWithId(int messageId) {
124+
var min = 0;
125+
var max = messages.length;
126+
127+
while (min < max) {
128+
var mid = min + ((max - min) >> 1);
129+
final message = messages[mid];
130+
var comp = message.id.compareTo(messageId);
131+
if (comp == 0) return mid;
132+
if (comp < 0) {
133+
min = mid + 1;
134+
} else {
135+
max = mid;
136+
}
137+
}
138+
return -1;
139+
}
140+
141+
void maybeUpdateMessage(UpdateMessageEvent event) {
142+
final idx = findMessageWithId(event.messageId);
143+
144+
if (idx == -1) {
145+
return;
146+
}
147+
148+
final message = messages[idx];
149+
_applyChangesToMessage(event, message);
150+
151+
contents[idx] = parseContent(message.content);
152+
notifyListeners();
153+
}
154+
89155
/// Called when the app is reassembled during debugging, e.g. for hot reload.
90156
///
91157
/// This will redo from scratch any computations we can, such as parsing

lib/model/store.dart

+3-1
Original file line numberDiff line numberDiff line change
@@ -272,7 +272,9 @@ class PerAccountStore extends ChangeNotifier {
272272
}
273273
} else if (event is UpdateMessageEvent) {
274274
assert(debugLog("server event: update_message ${event.messageId}"));
275-
// TODO handle
275+
for (final view in _messageListViews) {
276+
view.maybeUpdateMessage(event);
277+
}
276278
} else if (event is DeleteMessageEvent) {
277279
assert(debugLog("server event: delete_message ${event.messageIds}"));
278280
// TODO handle

test/api/model/model_checks.dart

+3
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ extension MessageChecks on Subject<Message> {
99
}
1010

1111
Subject<List<String>> get flags => has((e) => e.flags, 'flags');
12+
Subject<String> get content => has((e) => e.content, 'content');
13+
Subject<bool> get isMeMessage => has((e) => e.isMeMessage, 'isMeMessage');
14+
Subject<int?> get lastEditTimestamp => has((e) => e.lastEditTimestamp, 'lastEditTimestamp');
1215

1316
// TODO accessors for other fields
1417
}

test/example_data.dart

+2-1
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ StreamMessage streamMessage({
127127
String? topic,
128128
String? content,
129129
String? contentMarkdown,
130+
List<String>? flags,
130131
}) {
131132
final effectiveStream = stream ?? _stream();
132133
// The use of JSON here is convenient in order to delegate parts of the data
@@ -140,7 +141,7 @@ StreamMessage streamMessage({
140141
..._messagePropertiesFromContent(content, contentMarkdown),
141142
'display_recipient': effectiveStream.name,
142143
'stream_id': effectiveStream.streamId,
143-
'flags': [],
144+
'flags': flags ?? [],
144145
'id': id ?? 1234567, // TODO generate example IDs
145146
'subject': topic ?? 'example topic',
146147
'timestamp': 1678139636,

test/model/message_list_test.dart

+200
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
import 'package:checks/checks.dart';
2+
import 'package:test/scaffolding.dart';
3+
import 'package:zulip/api/model/events.dart';
4+
import 'package:zulip/api/model/model.dart';
5+
import 'package:zulip/api/route/messages.dart';
6+
import 'package:zulip/model/message_list.dart';
7+
import 'package:zulip/model/narrow.dart';
8+
import 'package:zulip/model/store.dart';
9+
import '../api/fake_api.dart';
10+
import '../api/model/model_checks.dart';
11+
import '../model/binding.dart';
12+
import '../model/test_store.dart';
13+
import '../example_data.dart' as eg;
14+
15+
const int userId = 1;
16+
const int streamId = 2;
17+
18+
Future<PerAccountStore> setupStore(ZulipStream stream) async {
19+
await TestZulipBinding.instance.globalStore.add(eg.selfAccount, eg.initialSnapshot());
20+
PerAccountStore store = await TestZulipBinding.instance.globalStore.perAccount(eg.selfAccount.id);
21+
store.addUser(eg.user(userId: userId));
22+
store.addStream(stream);
23+
return store;
24+
}
25+
26+
Future<MessageListView> messageListViewWithMessages(List<Message> messages, PerAccountStore store, Narrow narrow) async {
27+
MessageListView messageList = MessageListView.init(store: store, narrow: narrow);
28+
29+
final connection = store.connection as FakeApiConnection;
30+
31+
connection.prepare(json: GetMessagesResult(
32+
anchor: messages.first.id,
33+
foundNewest: true,
34+
foundOldest: true,
35+
foundAnchor: true,
36+
historyLimited: false,
37+
messages: messages,
38+
).toJson());
39+
40+
await messageList.fetch();
41+
42+
check(messageList.messages.length).equals(messages.length);
43+
44+
return messageList;
45+
}
46+
47+
void main() async {
48+
TestZulipBinding.ensureInitialized();
49+
const narrow = StreamNarrow(streamId);
50+
51+
final ZulipStream stream = eg.stream(streamId: streamId);
52+
PerAccountStore store = await setupStore(stream);
53+
54+
group('update message tests', () {
55+
56+
test('find message in message list returns index of message', () async {
57+
Message m1 = eg.streamMessage(id: 792, stream: stream);
58+
Message m2 = eg.streamMessage(id: 793, stream: stream);
59+
Message m3 = eg.streamMessage(id: 794, stream: stream);
60+
61+
MessageListView messageList = await messageListViewWithMessages([m1, m2, m3], store, narrow);
62+
63+
int idx = messageList.findMessageWithId(793);
64+
check(idx).equals(1);
65+
66+
idx = messageList.findMessageWithId(999);
67+
check(idx).equals(-1);
68+
});
69+
70+
test('update events are correctly applied to message when it is in the stream', () async {
71+
String oldContent = "<p>Hello, world</p>";
72+
String newContent = "<p>Hello, edited</p>";
73+
int newTimestamp = 99999;
74+
75+
List<String> oldFlags = [];
76+
List<String> newFlags = ["starred"];
77+
78+
Message mockMessage = eg.streamMessage(id: 243, stream: stream, content: oldContent, flags: oldFlags);
79+
MessageListView messageList = await messageListViewWithMessages([mockMessage], store, narrow);
80+
81+
UpdateMessageEvent updateEvent = UpdateMessageEvent(
82+
id: 1,
83+
messageId: mockMessage.id,
84+
messageIds: [mockMessage.id],
85+
flags: newFlags,
86+
renderedContent: newContent,
87+
editTimestamp: newTimestamp,
88+
isMeMessage: true,
89+
userId: userId
90+
);
91+
92+
Message message = messageList.messages[0];
93+
check(message)
94+
..content.equals(oldContent)
95+
..flags.deepEquals(oldFlags)
96+
..isMeMessage.equals(false);
97+
98+
bool listenersNotified = false;
99+
100+
messageList.addListener(() { listenersNotified = true; });
101+
messageList.maybeUpdateMessage(updateEvent);
102+
103+
Message updatedMessage = messageList.messages[0];
104+
check(updatedMessage).identicalTo(message);
105+
check(listenersNotified).equals(true);
106+
107+
check(message)
108+
..content.equals(newContent)
109+
..lastEditTimestamp.equals(newTimestamp)
110+
..flags.equals(newFlags)
111+
..isMeMessage.equals(true);
112+
});
113+
114+
test('update event is ignored when message is not in the message list', () async {
115+
String oldContent = "<p>Hello, world</p>";
116+
String newContent = "<p>Hello, edited</p>";
117+
int newTimestamp = 99999;
118+
119+
Message mockMessage = eg.streamMessage(id: 243, stream: stream, content: oldContent);
120+
MessageListView messageList = await messageListViewWithMessages([mockMessage], store, narrow);
121+
122+
UpdateMessageEvent updateEvent = UpdateMessageEvent(
123+
id: 1,
124+
messageId: 972,
125+
messageIds: [972],
126+
flags: mockMessage.flags,
127+
renderedContent: newContent,
128+
editTimestamp: newTimestamp,
129+
userId: userId,
130+
);
131+
132+
Message message = messageList.messages[0];
133+
check(message).content.equals(oldContent);
134+
135+
bool listenersNotified = false;
136+
137+
messageList.addListener(() { listenersNotified = true; });
138+
messageList.maybeUpdateMessage(updateEvent);
139+
140+
Message updatedMessage = messageList.messages[0];
141+
142+
check(listenersNotified).equals(false);
143+
check(updatedMessage).identicalTo(message);
144+
check(message).content.equals(oldContent);
145+
146+
});
147+
test('rendering-only update does not change timestamp', () async {
148+
String oldContent = "<p>Hello, world</p>";
149+
String newContent = "<p>Hello, world</p> <div>Some link preview</div>";
150+
int newTimestamp = 99999;
151+
152+
Message mockMessage = eg.streamMessage(id: 972, stream: stream, content: oldContent);
153+
MessageListView messageList = await messageListViewWithMessages([mockMessage], store, narrow);
154+
155+
UpdateMessageEvent updateEvent = UpdateMessageEvent(
156+
id: 1,
157+
messageId: 972,
158+
messageIds: [972],
159+
flags: mockMessage.flags,
160+
renderedContent: newContent,
161+
editTimestamp: newTimestamp,
162+
renderingOnly: true,
163+
userId: null,
164+
);
165+
166+
Message message = messageList.messages[0];
167+
messageList.maybeUpdateMessage(updateEvent);
168+
check(message)
169+
..content.equals(newContent)
170+
..lastEditTimestamp.isNull();
171+
});
172+
173+
test('rendering-only update does not change timestamp (for old server versions)', () async {
174+
String oldContent = "<p>Hello, world</p>";
175+
String newContent = "<p>Hello, world</p> <div>Some link preview</div>";
176+
int newTimestamp = 99999;
177+
178+
Message mockMessage = eg.streamMessage(id: 972, stream: stream, content: oldContent);
179+
MessageListView messageList = await messageListViewWithMessages([mockMessage], store, narrow);
180+
181+
UpdateMessageEvent updateEvent = UpdateMessageEvent(
182+
id: 1,
183+
messageId: 972,
184+
messageIds: [972],
185+
flags: mockMessage.flags,
186+
renderedContent: newContent,
187+
editTimestamp: newTimestamp,
188+
userId: null,
189+
);
190+
191+
Message message = messageList.messages[0];
192+
messageList.maybeUpdateMessage(updateEvent);
193+
check(message)
194+
..content.equals(newContent)
195+
..lastEditTimestamp.isNull();
196+
});
197+
198+
199+
});
200+
}

0 commit comments

Comments
 (0)