-
Notifications
You must be signed in to change notification settings - Fork 353
Keep track of the known topics in store #1951
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
base: main
Are you sure you want to change the base?
Conversation
1019746 to
2e0fcfd
Compare
chrisbobbe
left a comment
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.
Thanks! Glad to be maintaining a data structure for this. 🙂
Comments below, from reading the implementation in the first commit (I haven't yet read the tests):
d6b2242 channel: Keep track of channel topics, and keep up-to-date with events
lib/model/channel.dart
Outdated
| import 'store.dart'; | ||
| import 'user.dart'; | ||
|
|
||
| final _apiGetChannelTopics = getStreamTopics; // similar to _apiSendMessage in lib/model/message.dart |
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: too-long line; put comment on line above
lib/model/channel.dart
Outdated
| /// can be retrieved with [getChannelTopics]. | ||
| Future<void> fetchTopics(int channelId); | ||
|
|
||
| /// Pairs of the known topics and its latest message ID, in the given channel. |
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.
| /// Pairs of the known topics and its latest message ID, in the given channel. | |
| /// The topics in the given channel, along with their latest message ID. |
lib/model/channel.dart
Outdated
|
|
||
| /// Pairs of the known topics and its latest message ID, in the given channel. | ||
| /// | ||
| /// Returns null if the data has never been fetched yet. |
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:
| /// Returns null if the data has never been fetched yet. | |
| /// Returns null if the data has not been fetched yet. |
lib/model/channel.dart
Outdated
| /// The result is guaranteed to be sorted by [GetStreamTopicsEntry.maxId] | ||
| /// descending, and the topics are guaranteed to be distinct. |
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, omit needless words:
| /// The result is guaranteed to be sorted by [GetStreamTopicsEntry.maxId] | |
| /// descending, and the topics are guaranteed to be distinct. | |
| /// The result is sorted by [GetStreamTopicsEntry.maxId] descending, | |
| /// and the topics are distinct. |
lib/model/channel.dart
Outdated
| /// In some cases, the same maxId affected by message moves can be present in | ||
| /// multiple [GetStreamTopicsEntry] entries. For this reason, the caller | ||
| /// should not rely on [getChannelTopics] to determine which topic the message | ||
| /// is in. Instead, refer to [PerAccountStore.messages]. | ||
| /// See [handleUpdateMessageEvent] on how this could happen. |
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.
It seems worth highlighting in general that maxId might be…incorrect, basically. (Not just that two topics might have the same maxId.) Maybe something like:
(Also, isn't message deletion another reason maxId could be wrong?)
| /// In some cases, the same maxId affected by message moves can be present in | |
| /// multiple [GetStreamTopicsEntry] entries. For this reason, the caller | |
| /// should not rely on [getChannelTopics] to determine which topic the message | |
| /// is in. Instead, refer to [PerAccountStore.messages]. | |
| /// See [handleUpdateMessageEvent] on how this could happen. | |
| /// Occasionally, [GetStreamTopicsEntry.maxId] will refer to a message | |
| /// that doesn't exist or is no longer in the topic. | |
| /// This happens when a topic's latest message is moved or deleted | |
| /// and we don't have enough information | |
| /// to replace [GetStreamTopicsEntry.maxId] accurately. | |
| /// (We don't keep a snapshot of all messages.) | |
| /// Use [PerAccountStore.messages] to check a message's topic accurately. |
lib/model/channel.dart
Outdated
| /// Handle a [MessageEvent], returning whether listeners should be notified. | ||
| bool handleMessageEvent(MessageEvent event) { | ||
| if (event.message is! StreamMessage) return false; | ||
| final StreamMessage(:streamId, :topic) = event.message as StreamMessage; |
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:
| final StreamMessage(:streamId, :topic) = event.message as StreamMessage; | |
| final StreamMessage(streamId: channelId, :topic) = event.message as StreamMessage; |
lib/model/channel.dart
Outdated
| // If this message is already the latest message in the topic because it was | ||
| // received through fetch in fetch/event race, or it is a message sent even | ||
| // before the latest message of the fetch, we don't do the update. |
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.
I'm getting confused trying to parse this sentence. Instead, how about, inside the if just below this, say something like:
// The event raced with a message fetch.
lib/model/channel.dart
Outdated
| // If we don't already know about the list of topics of the channel this | ||
| // message belongs to, we don't want to proceed and put one entry about the | ||
| // topic of this message, otherwise [fetchTopics] and the callers of | ||
| // [getChannelTopics] would assume that the channel only has this one topic | ||
| // and would never fetch the complete list of topics for that matter. | ||
| if (latestMessageIdsByTopic == null) return false; |
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.
How about:
| // If we don't already know about the list of topics of the channel this | |
| // message belongs to, we don't want to proceed and put one entry about the | |
| // topic of this message, otherwise [fetchTopics] and the callers of | |
| // [getChannelTopics] would assume that the channel only has this one topic | |
| // and would never fetch the complete list of topics for that matter. | |
| if (latestMessageIdsByTopic == null) return false; | |
| if (latestMessageIdsByTopic == null) { | |
| // We're not tracking this channel's topics yet. | |
| // We start doing that when [fetchTopics] is called, | |
| // and we fill in all the topics at that time. | |
| return false; | |
| } |
| final origLatestMessageIdsByTopics = _latestMessageIdsByChannelTopic[origStreamId]; | ||
| // We only handle the case where all the messages of [origTopic] are | ||
| // moved to [newTopic]; in that case we can remove [origTopic] safely. | ||
| // But if only one messsage is moved (`PropagateMode.changeOne`) or a few | ||
| // messages are moved (`PropagateMode.changeLater`), we cannot do anything | ||
| // about [origTopic] here as we cannot determine the new `maxId` for it. | ||
| // (This is the case where there could be multiple channel-topic keys with | ||
| // the same `maxId`) | ||
| if (propagateMode == PropagateMode.changeAll | ||
| && origLatestMessageIdsByTopics != null) { | ||
| shouldNotify = origLatestMessageIdsByTopics.remove(origTopic) != null; | ||
| } |
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.
With a switch/case on propagateMode, I think we can be less verbose in the comment, e.g.:
| final origLatestMessageIdsByTopics = _latestMessageIdsByChannelTopic[origStreamId]; | |
| // We only handle the case where all the messages of [origTopic] are | |
| // moved to [newTopic]; in that case we can remove [origTopic] safely. | |
| // But if only one messsage is moved (`PropagateMode.changeOne`) or a few | |
| // messages are moved (`PropagateMode.changeLater`), we cannot do anything | |
| // about [origTopic] here as we cannot determine the new `maxId` for it. | |
| // (This is the case where there could be multiple channel-topic keys with | |
| // the same `maxId`) | |
| if (propagateMode == PropagateMode.changeAll | |
| && origLatestMessageIdsByTopics != null) { | |
| shouldNotify = origLatestMessageIdsByTopics.remove(origTopic) != null; | |
| } | |
| final origLatestMessageIdsByTopics = _latestMessageIdsByChannelTopic[origStreamId]; | |
| switch (propagateMode) { | |
| case PropagateMode.changeOne: | |
| case PropagateMode.changeLater: | |
| // We can't know the new `maxId` for the original topic. | |
| // Shrug; leave it unchanged. (See dartdoc of [getChannelTopics], | |
| // where we call out this possibility that `maxId` is incorrect. | |
| break; | |
| case PropagateMode.changeAll: | |
| if (origLatestMessageIdsByTopics != null) { | |
| origLatestMessageIdsByTopics.remove(origTopic); | |
| shouldNotify = true; | |
| } | |
| } |
| final newLatestMessageIdsByTopics = _latestMessageIdsByChannelTopic[newStreamId]; | ||
| if (newLatestMessageIdsByTopics != null) { | ||
| final movedMaxId = event.messageIds.max; | ||
| if (!newLatestMessageIdsByTopics.containsKey(newTopic) | ||
| || newLatestMessageIdsByTopics[newTopic]! < movedMaxId) { | ||
| newLatestMessageIdsByTopics[newTopic] = movedMaxId; | ||
| shouldNotify = true; | ||
| } | ||
| } |
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.
A bit easier to read, I think:
| final newLatestMessageIdsByTopics = _latestMessageIdsByChannelTopic[newStreamId]; | |
| if (newLatestMessageIdsByTopics != null) { | |
| final movedMaxId = event.messageIds.max; | |
| if (!newLatestMessageIdsByTopics.containsKey(newTopic) | |
| || newLatestMessageIdsByTopics[newTopic]! < movedMaxId) { | |
| newLatestMessageIdsByTopics[newTopic] = movedMaxId; | |
| shouldNotify = true; | |
| } | |
| } | |
| final newLatestMessageIdsByTopics = _latestMessageIdsByChannelTopic[newStreamId]; | |
| if (newLatestMessageIdsByTopics != null) { | |
| final movedMaxId = event.messageIds.max; | |
| final currentMaxId = newLatestMessageIdsByTopics[newTopic]; | |
| if (currentMaxId == null || currentMaxId < movedMaxId) { | |
| newLatestMessageIdsByTopics[newTopic] = movedMaxId; | |
| shouldNotify = true; | |
| } | |
| } |
2e0fcfd to
c37de07
Compare
|
Thanks @chrisbobbe for the review. Pushed new revision. |
c37de07 to
02ed253
Compare
Co-authored-by: Sayed Mahmood Sayedi <[email protected]>
Fixes: zulip#1499 Co-authored-by: Sayed Mahmood Sayedi <[email protected]>
02ed253 to
7fca9bb
Compare
chrisbobbe
left a comment
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.
Thanks, and sorry for the delay! Comments below, this time from a full review.
| }); | ||
|
|
||
| testWidgets('fetch again when navigating away and back', (tester) async { | ||
| testWidgets("dont't fetch again when navigating away and back", (tester) async { |
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: spelling of "don't"
| Subject<Uri> get realmUrl => has((e) => e.realmUrl, 'realmUrl'); | ||
| } | ||
|
|
||
| extension GetStreamTopicEntryChecks on Subject<GetStreamTopicsEntry> { |
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.
"GetStreamTopicsEntryChecks"
| Subject<Map<int, ZulipStream>> get streams => has((x) => x.streams, 'streams'); | ||
| Subject<Map<String, ZulipStream>> get streamsByName => has((x) => x.streamsByName, 'streamsByName'); | ||
| Subject<Map<int, Subscription>> get subscriptions => has((x) => x.subscriptions, 'subscriptions'); | ||
| Subject<List<GetStreamTopicsEntry>?> getStreamTopics(int streamId) => has((x) => x.getChannelTopics(streamId), 'getStreamTopics'); |
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.
getChannelTopics and 'getChannelTopics', to match PerAccountStore.getChannelTopics
| import 'user.dart'; | ||
|
|
||
| // similar to _apiSendMessage in lib/model/message.dart | ||
| final _apiGetChannelTopics = getStreamTopics; |
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.
Could you add a prep commit that renames the existing binding getStreamTopics to getChannelTopics, then consistently use "channel topics" naming instead of "stream topics" naming?
| /// can be retrieved with [getChannelTopics]. | ||
| Future<void> fetchTopics(int channelId); | ||
|
|
||
| /// The topics in the given channel, along with their latest message ID. |
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.
| /// The topics in the given channel, along with their latest message ID. | |
| /// The topics the user can access, along with their latest message ID, | |
| /// reflecting updates from events that arrived since the data was fetched. |
| connection.prepare(json: GetStreamTopicsResult( | ||
| topics: [eg.getStreamTopicsEntry(name: 'topic B')]).toJson()); |
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.
If we don't expect to fetch again, we shouldn't prepare a response, right?
| ]); | ||
| testWidgets('show topic action sheet before and after moves', (tester) async { | ||
| final channel = eg.stream(); | ||
| final message = eg.streamMessage(id: 123, stream: channel, topic: 'foo'); |
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.
Is 123 chosen because it equals eg.defaultStreamMessageStreamId? We should use eg.defaultStreamMessageStreamId instead, if so, or else pass 123 as streamId to the eg.stream() call.
| // After the move, the message with maxId moved away from "foo". The topic | ||
| // actions that require `someMessageIdInTopic` is no longer 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.
nit: "are no longer available"
| await tester.tap(find.text('Cancel')); | ||
| await tester.pump(); | ||
|
|
||
| // Topic actions that require `someMessageIdInTopic` 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.
nit: "are available"
| check(find.descendant(of: topicItemFinder, | ||
| matching: find.text('foo'))).findsOne(); |
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.
How about:
check(findInTopicItemAt(0, find.text('foo'))).findsOne();?
#1508 but rebased on top of main with the review of @gnprice addressed.
(Thanks @PIG208 for all of your previous work in #1508)
Fixes: #1499