-
Notifications
You must be signed in to change notification settings - Fork 343
model: Introduce data structures for "recent senders criterion" of user-mention autocomplete #692
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
e2b989d
2ff2406
76d1404
e875302
3a834c4
00ebe6e
f74933b
d3d3774
73c827c
eca8af9
dbb2dcf
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||
---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,149 @@ | ||||||||
import 'package:collection/collection.dart'; | ||||||||
import 'package:flutter/foundation.dart'; | ||||||||
|
||||||||
import '../api/model/events.dart'; | ||||||||
import '../api/model/model.dart'; | ||||||||
import 'algorithms.dart'; | ||||||||
|
||||||||
/// Tracks the latest messages sent by each user, in each stream and topic. | ||||||||
/// | ||||||||
/// Use [latestMessageIdOfSenderInStream] and [latestMessageIdOfSenderInTopic] | ||||||||
/// for queries. | ||||||||
class RecentSenders { | ||||||||
// streamSenders[streamId][senderId] = MessageIdTracker | ||||||||
@visibleForTesting | ||||||||
final Map<int, Map<int, MessageIdTracker>> streamSenders = {}; | ||||||||
|
||||||||
// topicSenders[streamId][topic][senderId] = MessageIdTracker | ||||||||
@visibleForTesting | ||||||||
final Map<int, Map<String, Map<int, MessageIdTracker>>> topicSenders = {}; | ||||||||
|
||||||||
/// The latest message the given user sent to the given stream, | ||||||||
/// or null if no such message is known. | ||||||||
int? latestMessageIdOfSenderInStream({ | ||||||||
required int streamId, | ||||||||
required int senderId, | ||||||||
}) => streamSenders[streamId]?[senderId]?.maxId; | ||||||||
|
||||||||
/// The latest message the given user sent to the given topic, | ||||||||
/// or null if no such message is known. | ||||||||
int? latestMessageIdOfSenderInTopic({ | ||||||||
required int streamId, | ||||||||
required String topic, | ||||||||
required int senderId, | ||||||||
}) => topicSenders[streamId]?[topic]?[senderId]?.maxId; | ||||||||
|
||||||||
/// Records the necessary data from a batch of just-fetched messages. | ||||||||
/// | ||||||||
/// The messages must be sorted by [Message.id] ascending. | ||||||||
void handleMessages(List<Message> messages) { | ||||||||
final messagesByUserInStream = <(int, int), QueueList<int>>{}; | ||||||||
final messagesByUserInTopic = <(int, String, int), QueueList<int>>{}; | ||||||||
for (final message in messages) { | ||||||||
if (message is! StreamMessage) continue; | ||||||||
final StreamMessage(:streamId, :topic, :senderId, id: int messageId) = message; | ||||||||
(messagesByUserInStream[(streamId, senderId)] ??= QueueList()).add(messageId); | ||||||||
(messagesByUserInTopic[(streamId, topic, senderId)] ??= QueueList()).add(messageId); | ||||||||
} | ||||||||
|
||||||||
for (final entry in messagesByUserInStream.entries) { | ||||||||
final (streamId, senderId) = entry.key; | ||||||||
((streamSenders[streamId] ??= {}) | ||||||||
[senderId] ??= MessageIdTracker()).addAll(entry.value); | ||||||||
} | ||||||||
for (final entry in messagesByUserInTopic.entries) { | ||||||||
final (streamId, topic, senderId) = entry.key; | ||||||||
(((topicSenders[streamId] ??= {})[topic] ??= {}) | ||||||||
[senderId] ??= MessageIdTracker()).addAll(entry.value); | ||||||||
} | ||||||||
} | ||||||||
|
||||||||
/// Records the necessary data from a new message. | ||||||||
void handleMessage(Message message) { | ||||||||
|
void handleMessage(Message message) { | |
void handleMessageEvent(MessageEvent event) { | |
final message = event.message; |
Matches RecentDmConversationsView.handleMessageEvent
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This change would match it with other somewhat similar parts of the codebase, but there are places this method is called where there is no MessageEvent
available, such as in model/message_list.dart
file. In contrast, RecentDmConversationsView.handleMessageEvent
is only called in PerAccountStore.handleEvent
where the MessageEvent
is available.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That makes sense.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Zulip Web seems not to delete the recently deleted message id from the tracker list, but I am unsure whether to follow them exactly!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, good question. Let's go ahead and do it, since we have all the infrastructure handy for it and it makes the behavior more correct.
Similar to the handleMessages
case, though, let's do it with just one method call to each affected MessageIdTracker
per handleDeleteMessageEvent
call. After you do that for handleMessages
, I think most of the code can be copied to use for this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So as this field on DeleteMessageEvent suggests, the set of messages in the event can't be totally arbitary — they're either all stream/channel messages or all DMs.
Then in fact there are more fields on the event and they specify things more precisely: if it's stream messages, they all have the same stream and topic, and those are given on the event too.
So that can be used to simplify this function quite a bit.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also started an #api documentation
thread to confirm and clarify the fact I stated above:
https://chat.zulip.org/#narrow/stream/19-documentation/topic/delete_message.20events/near/1895069
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Making this class private would cause the following lint message, thus failing the CI:
Invalid use of a private type in a public API.
Try making the private type public, or making the API that uses the private type also be private.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, because the type appears in RecentSenders.streamSenders
and RecentSenders.topicSenders
.
Still we can keep a @visibleForTesting
annotation on this, right? It looks like that went away in the latest revision.
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -18,6 +18,7 @@ import '../api/model/model_checks.dart'; | |||||
import '../example_data.dart' as eg; | ||||||
import '../stdlib_checks.dart'; | ||||||
import 'content_checks.dart'; | ||||||
import 'recent_senders_test.dart' as recent_senders_test; | ||||||
import 'test_store.dart'; | ||||||
|
||||||
void main() { | ||||||
|
@@ -141,6 +142,25 @@ void main() { | |||||
..haveOldest.isTrue(); | ||||||
}); | ||||||
|
||||||
// TODO(#824): move this test | ||||||
test('fetchInitial, recent senders track all the messages', () async { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The need for importing from another test file is a sign that the layers here are getting a little tangled. But I think that's OK for merging this PR — it's really the same thing that #824 is about. So let's just add a todo-comment, to help find this to clean it up when we take care of that issue:
Suggested change
Similarly the fetchOlder test below. |
||||||
const narrow = CombinedFeedNarrow(); | ||||||
await prepare(narrow: narrow); | ||||||
final messages = [ | ||||||
eg.streamMessage(), | ||||||
// Not subscribed to the stream with id 10. | ||||||
eg.streamMessage(stream: eg.stream(streamId: 10)), | ||||||
]; | ||||||
connection.prepare(json: newestResult( | ||||||
foundOldest: false, | ||||||
messages: messages, | ||||||
).toJson()); | ||||||
await model.fetchInitial(); | ||||||
|
||||||
check(model).messages.length.equals(1); | ||||||
recent_senders_test.checkMatchesMessages(store.recentSenders, messages); | ||||||
}); | ||||||
|
||||||
test('fetchOlder', () async { | ||||||
const narrow = CombinedFeedNarrow(); | ||||||
await prepare(narrow: narrow); | ||||||
|
@@ -233,6 +253,27 @@ void main() { | |||||
..messages.length.equals(200); | ||||||
}); | ||||||
|
||||||
// TODO(#824): move this test | ||||||
test('fetchOlder, recent senders track all the messages', () async { | ||||||
const narrow = CombinedFeedNarrow(); | ||||||
await prepare(narrow: narrow); | ||||||
final initialMessages = List.generate(10, (i) => eg.streamMessage(id: 100 + i)); | ||||||
await prepareMessages(foundOldest: false, messages: initialMessages); | ||||||
|
||||||
final oldMessages = List.generate(10, (i) => eg.streamMessage(id: 89 + i)) | ||||||
// Not subscribed to the stream with id 10. | ||||||
..add(eg.streamMessage(id: 99, stream: eg.stream(streamId: 10))); | ||||||
connection.prepare(json: olderResult( | ||||||
anchor: 100, foundOldest: false, | ||||||
messages: oldMessages, | ||||||
).toJson()); | ||||||
await model.fetchOlder(); | ||||||
|
||||||
check(model).messages.length.equals(20); | ||||||
recent_senders_test.checkMatchesMessages(store.recentSenders, | ||||||
[...initialMessages, ...oldMessages]); | ||||||
}); | ||||||
|
||||||
test('MessageEvent', () async { | ||||||
final stream = eg.stream(); | ||||||
await prepare(narrow: StreamNarrow(stream.streamId)); | ||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit in commit message:
Use forward slashes to separate path components; that's the Internet standard (e.g. in URLs).
Also helpful is to give a GitHub permalink to the file. (Once you're looking at a GitHub page like
…/blob/main/web/src/recent_senders.ts
, you can hit they
keyboard shortcut to turn it into a permalink.) That encodes the commit ID, so that a reader in the future can quickly and unambiguously find the file you were talking about even if in the repo it's been renamed since then, or its contents substantially changed.