-
Notifications
You must be signed in to change notification settings - Fork 307
store: Add RecentDmConversationsView view-model #210
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
b75dc7d
d7af63e
68d30ee
8a7d664
ac11ef8
a7e3529
606975b
3550d87
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,126 @@ | ||
import 'package:collection/collection.dart'; | ||
import 'package:flutter/foundation.dart'; | ||
|
||
import '../api/model/initial_snapshot.dart'; | ||
import '../api/model/model.dart'; | ||
import '../api/model/events.dart'; | ||
import 'narrow.dart'; | ||
|
||
/// A view-model for the recent-DM-conversations UI. | ||
/// | ||
/// This maintains the list of recent DM conversations, | ||
/// plus additional data in order to efficiently maintain the list. | ||
class RecentDmConversationsView extends ChangeNotifier { | ||
factory RecentDmConversationsView({ | ||
required List<RecentDmConversation> initial, | ||
required selfUserId, | ||
}) { | ||
final entries = initial.map((conversation) => MapEntry( | ||
DmNarrow.ofRecentDmConversation(conversation, selfUserId: selfUserId), | ||
conversation.maxMessageId, | ||
)).toList()..sort((a, b) => -a.value.compareTo(b.value)); | ||
return RecentDmConversationsView._( | ||
map: Map.fromEntries(entries), | ||
sorted: QueueList.from(entries.map((e) => e.key)), | ||
selfUserId: selfUserId, | ||
); | ||
} | ||
|
||
RecentDmConversationsView._({ | ||
required this.map, | ||
required this.sorted, | ||
required this.selfUserId, | ||
}); | ||
|
||
/// The latest message ID in each conversation. | ||
final Map<DmNarrow, int> map; | ||
|
||
/// The [DmNarrow] keys of the map, sorted by latest message descending. | ||
final QueueList<DmNarrow> sorted; | ||
|
||
final int selfUserId; | ||
|
||
/// Insert the key at the proper place in [sorted]. | ||
/// | ||
/// Optimized, taking O(1) time, for the case where that place is the start, | ||
/// because that's the common case for a new message. | ||
/// May take O(n) time in general. | ||
void _insertSorted(DmNarrow key, int msgId) { | ||
final i = sorted.indexWhere((k) => map[k]! < msgId); | ||
// QueueList is a deque, with O(1) to add at start or end. | ||
switch (i) { | ||
case == 0: | ||
sorted.addFirst(key); | ||
case < 0: | ||
sorted.addLast(key); | ||
default: | ||
sorted.insert(i, key); | ||
} | ||
} | ||
|
||
/// Handle [MessageEvent], updating [map] and [sorted]. | ||
/// | ||
/// Can take linear time in general. That sounds inefficient... | ||
/// but it's what the webapp does, so must not be catastrophic. 🤷 | ||
/// (In fact the webapp calls `Array#sort`, | ||
/// which takes at *least* linear time, and may be 𝛳(N log N).) | ||
/// | ||
/// The point of the event is that we're learning about the message | ||
/// in real time immediately after it was sent -- | ||
/// so the overwhelmingly common case is that the message | ||
/// is newer than any existing message we know about. (*) | ||
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. dangling asterisk 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. Hmm, I'm guessing the reason you've omitted the footnote is because it's about a race with FETCH_MESSAGES_COMPLETE, and we don't yet have an analogue of that in zulip-flutter; but the same reason means that the case where this is the newest message is also not just the overwhelmingly common case, but the only possible case. I think the logic is probably actually cleanest to read and understand if that caveat is left in and the wording just adjusted to reflect that it's not yet present. I'll suggest a version. |
||
/// That's therefore the case we optimize for, | ||
/// particularly in the helper _insertSorted. | ||
/// | ||
/// (*) In fact at present that's the only possible case. | ||
/// The alternative will become possible when we have the analogue of | ||
/// zulip-mobile's FETCH_MESSAGES_COMPLETE, with fetches done for a | ||
/// [MessageListView] reporting their messages back via the [PerAccountStore] | ||
/// to all our central data structures. | ||
/// Then that can race with a new-message event: for example, | ||
/// say we get a fetch-messages result that includes the just-sent | ||
/// message 1002, and only after that get the event about message 1001, | ||
/// sent moments earlier. The event queue always delivers events in order, so | ||
/// even the race is possible only because we fetch messages outside of the | ||
/// event queue. | ||
void handleMessageEvent(MessageEvent event) { | ||
final message = event.message; | ||
if (message is! DmMessage) { | ||
return; | ||
} | ||
final key = DmNarrow.ofMessage(message, selfUserId: selfUserId); | ||
final prev = map[key]; | ||
if (prev == null) { | ||
// The conversation is new. Add to both `map` and `sorted`. | ||
map[key] = message.id; | ||
_insertSorted(key, message.id); | ||
} else if (prev >= message.id) { | ||
// The conversation already has a newer message. | ||
// This should be impossible as long as we only listen for messages coming | ||
// through the event system, which sends events in order. | ||
// Anyway, do nothing. | ||
} else { | ||
// The conversation needs to be (a) updated in `map`... | ||
map[key] = message.id; | ||
|
||
// ... and (b) possibly moved around in `sorted` to keep the list sorted. | ||
final i = sorted.indexOf(key); | ||
assert(i >= 0, 'key in map should be in sorted'); | ||
if (i == 0) { | ||
// The conversation was already the latest, so no reordering needed. | ||
// (This is likely a common case in practice -- happens every time | ||
// the user gets several DMs in a row in the same thread -- so good to | ||
// optimize.) | ||
} else { | ||
// It wasn't the latest. Just handle the general case. | ||
sorted.removeAt(i); // linear time, ouch | ||
_insertSorted(key, message.id); | ||
} | ||
} | ||
notifyListeners(); | ||
} | ||
|
||
// TODO update from messages loaded in message lists. When doing so, | ||
// review handleMessageEvent so it acknowledges the subtle races that can | ||
// happen when taking data from outside the event system. | ||
} |
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 think the comment on the corresponding function in zulip-mobile is useful to propagate:
In particular the point about when it's O(1) and why.