mirror of
https://github.com/zulip/zulip.git
synced 2025-11-03 13:33:24 +00:00
This data structure has never been one that we actually render into the DOM; instead, its role is to support clicking into view that contain muted streams and topics quickly. This downgrade makes that situation much more explicit, and is also useful refactoring to help simpify the upcoming changes in #16746. Signed-off-by: Anders Kaseorg <anders@zulip.com>
330 lines
9.8 KiB
JavaScript
330 lines
9.8 KiB
JavaScript
import {all_messages_data} from "./all_messages_data";
|
|
import * as channel from "./channel";
|
|
import {FoldDict} from "./fold_dict";
|
|
import * as message_util from "./message_util";
|
|
import * as stream_data from "./stream_data";
|
|
import * as unread from "./unread";
|
|
|
|
const stream_dict = new Map(); // stream_id -> PerStreamHistory object
|
|
const fetched_stream_ids = new Set();
|
|
|
|
export function all_topics_in_cache(sub) {
|
|
// Checks whether this browser's cache of contiguous messages
|
|
// (used to locally render narrows) in all_messages_data has all
|
|
// messages from a given stream, and thus all historical topics
|
|
// for it. Because all_messages_data is a range, we just need to
|
|
// compare it to the range of history on the stream.
|
|
|
|
// If the cache isn't initialized, it's a clear false.
|
|
if (all_messages_data === undefined || all_messages_data.empty()) {
|
|
return false;
|
|
}
|
|
|
|
// If the cache doesn't have the latest messages, we can't be sure
|
|
// we have all topics.
|
|
if (!all_messages_data.fetch_status.has_found_newest()) {
|
|
return false;
|
|
}
|
|
|
|
if (sub.first_message_id === null) {
|
|
// If the stream has no message history, we have it all
|
|
// vacuously. This should be a very rare condition, since
|
|
// stream creation sends a message.
|
|
return true;
|
|
}
|
|
|
|
// Now, we can just compare the first cached message to the first
|
|
// message ID in the stream; if it's older, we're good, otherwise,
|
|
// we might be missing the oldest topics in this stream in our
|
|
// cache.
|
|
const first_cached_message = all_messages_data.first();
|
|
return first_cached_message.id <= sub.first_message_id;
|
|
}
|
|
|
|
export function is_complete_for_stream_id(stream_id) {
|
|
if (fetched_stream_ids.has(stream_id)) {
|
|
return true;
|
|
}
|
|
|
|
const sub = stream_data.get_sub_by_id(stream_id);
|
|
const in_cache = all_topics_in_cache(sub);
|
|
|
|
if (in_cache) {
|
|
/*
|
|
If the stream is cached, we can add it to
|
|
fetched_stream_ids. Note that for the opposite
|
|
scenario, we don't delete from
|
|
fetched_stream_ids, because we may just be
|
|
waiting for the initial message fetch.
|
|
*/
|
|
fetched_stream_ids.add(stream_id);
|
|
}
|
|
|
|
return in_cache;
|
|
}
|
|
|
|
export function stream_has_topics(stream_id) {
|
|
if (!stream_dict.has(stream_id)) {
|
|
return false;
|
|
}
|
|
|
|
const history = stream_dict.get(stream_id);
|
|
|
|
return history.has_topics();
|
|
}
|
|
|
|
export class PerStreamHistory {
|
|
/*
|
|
For a given stream, this structure has a dictionary of topics.
|
|
The main getter of this object is get_recent_topic_names, and
|
|
we just sort on the fly every time we are called.
|
|
|
|
Attributes for a topic are:
|
|
* message_id: The latest message_id in the topic. Only usable
|
|
for imprecise applications like sorting. The message_id
|
|
cannot be fully accurate given message editing and deleting
|
|
(as we don't have a way to handle the latest message in a
|
|
stream having its stream edited or deleted).
|
|
|
|
TODO: We can probably fix this limitation by doing a
|
|
single-message `GET /messages` query with anchor="latest",
|
|
num_before=0, num_after=0, to update this field when its
|
|
value becomes ambiguous. Or probably better to avoid a
|
|
thundering herd (of a fast query), having the server send
|
|
the data needed to do this update in stream/topic-edit and
|
|
delete events (just the new max_message_id for the relevant
|
|
topic would likely suffice, though we need to think about
|
|
private stream corner cases).
|
|
* pretty_name: The topic_name, with original case.
|
|
* historical: Whether the user actually received any messages in
|
|
the topic (has UserMessage rows) or is just viewing the stream.
|
|
* count: Number of known messages in the topic. Used to detect
|
|
when the last messages in a topic were moved to other topics or
|
|
deleted.
|
|
*/
|
|
|
|
topics = new FoldDict();
|
|
// Most recent message ID for the stream.
|
|
max_message_id = 0;
|
|
|
|
constructor(stream_id) {
|
|
this.stream_id = stream_id;
|
|
}
|
|
|
|
has_topics() {
|
|
return this.topics.size !== 0;
|
|
}
|
|
|
|
update_stream_max_message_id(message_id) {
|
|
if (message_id > this.max_message_id) {
|
|
this.max_message_id = message_id;
|
|
}
|
|
}
|
|
|
|
add_or_update({topic_name, message_id = 0}) {
|
|
message_id = Number.parseInt(message_id, 10);
|
|
this.update_stream_max_message_id(message_id);
|
|
|
|
const existing = this.topics.get(topic_name);
|
|
|
|
if (!existing) {
|
|
this.topics.set(topic_name, {
|
|
message_id,
|
|
pretty_name: topic_name,
|
|
historical: false,
|
|
count: 1,
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (!existing.historical) {
|
|
existing.count += 1;
|
|
}
|
|
|
|
if (message_id > existing.message_id) {
|
|
existing.message_id = message_id;
|
|
existing.pretty_name = topic_name;
|
|
}
|
|
}
|
|
|
|
maybe_remove(topic_name, num_messages) {
|
|
const existing = this.topics.get(topic_name);
|
|
|
|
if (!existing) {
|
|
return;
|
|
}
|
|
|
|
if (existing.historical) {
|
|
// We can't trust that a topic rename applied to
|
|
// the entire history of historical topic, so we
|
|
// will always leave it in the sidebar.
|
|
return;
|
|
}
|
|
|
|
if (existing.count <= num_messages) {
|
|
this.topics.delete(topic_name);
|
|
return;
|
|
}
|
|
|
|
existing.count -= num_messages;
|
|
}
|
|
|
|
add_history(server_history) {
|
|
// This method populates historical topics from the
|
|
// server. We have less data about these than the
|
|
// client can maintain for newer topics.
|
|
|
|
for (const obj of server_history) {
|
|
const topic_name = obj.name;
|
|
const message_id = obj.max_id;
|
|
|
|
const existing = this.topics.get(topic_name);
|
|
|
|
if (existing && !existing.historical) {
|
|
// Trust out local data more, since it
|
|
// maintains counts.
|
|
continue;
|
|
}
|
|
|
|
// If we get here, we are either finding out about
|
|
// the topic for the first time, or we are getting
|
|
// more current data for it.
|
|
|
|
this.topics.set(topic_name, {
|
|
message_id,
|
|
pretty_name: topic_name,
|
|
historical: true,
|
|
});
|
|
this.update_stream_max_message_id(message_id);
|
|
}
|
|
}
|
|
|
|
get_recent_topic_names() {
|
|
const my_recents = Array.from(this.topics.values());
|
|
|
|
const missing_topics = unread.get_missing_topics({
|
|
stream_id: this.stream_id,
|
|
topic_dict: this.topics,
|
|
});
|
|
|
|
const recents = my_recents.concat(missing_topics);
|
|
|
|
recents.sort((a, b) => b.message_id - a.message_id);
|
|
|
|
const names = recents.map((obj) => obj.pretty_name);
|
|
|
|
return names;
|
|
}
|
|
|
|
get_max_message_id() {
|
|
return this.max_message_id;
|
|
}
|
|
}
|
|
|
|
export function remove_messages(opts) {
|
|
const stream_id = opts.stream_id;
|
|
const topic_name = opts.topic_name;
|
|
const num_messages = opts.num_messages;
|
|
const max_removed_msg_id = opts.max_removed_msg_id;
|
|
const history = stream_dict.get(stream_id);
|
|
|
|
// This is the special case of "removing" a message from
|
|
// a topic, which happens when we edit topics.
|
|
|
|
if (!history) {
|
|
return;
|
|
}
|
|
|
|
// This is the normal case of an incoming message.
|
|
history.maybe_remove(topic_name, num_messages);
|
|
|
|
const existing_topic = history.topics.get(topic_name);
|
|
if (!existing_topic) {
|
|
return;
|
|
}
|
|
|
|
// Update max_message_id in topic
|
|
if (existing_topic.message_id <= max_removed_msg_id) {
|
|
const msgs_in_topic = message_util.get_messages_in_topic(stream_id, topic_name);
|
|
let max_message_id = 0;
|
|
for (const msg of msgs_in_topic) {
|
|
if (msg.id > max_message_id) {
|
|
max_message_id = msg.id;
|
|
}
|
|
}
|
|
existing_topic.message_id = max_message_id;
|
|
}
|
|
|
|
// Update max_message_id in stream
|
|
if (history.max_message_id <= max_removed_msg_id) {
|
|
history.max_message_id = message_util.get_max_message_id_in_stream(stream_id);
|
|
}
|
|
}
|
|
|
|
export function find_or_create(stream_id) {
|
|
let history = stream_dict.get(stream_id);
|
|
|
|
if (!history) {
|
|
history = new PerStreamHistory(stream_id);
|
|
stream_dict.set(stream_id, history);
|
|
}
|
|
|
|
return history;
|
|
}
|
|
|
|
export function add_message(opts) {
|
|
const stream_id = opts.stream_id;
|
|
const message_id = opts.message_id;
|
|
const topic_name = opts.topic_name;
|
|
|
|
const history = find_or_create(stream_id);
|
|
|
|
history.add_or_update({
|
|
topic_name,
|
|
message_id,
|
|
});
|
|
}
|
|
|
|
export function add_history(stream_id, server_history) {
|
|
const history = find_or_create(stream_id);
|
|
history.add_history(server_history);
|
|
fetched_stream_ids.add(stream_id);
|
|
}
|
|
|
|
export function get_server_history(stream_id, on_success) {
|
|
if (fetched_stream_ids.has(stream_id)) {
|
|
on_success();
|
|
return;
|
|
}
|
|
|
|
const url = "/json/users/me/" + stream_id + "/topics";
|
|
|
|
channel.get({
|
|
url,
|
|
data: {},
|
|
success(data) {
|
|
const server_history = data.topics;
|
|
add_history(stream_id, server_history);
|
|
on_success();
|
|
},
|
|
});
|
|
}
|
|
|
|
export function get_recent_topic_names(stream_id) {
|
|
const history = find_or_create(stream_id);
|
|
|
|
return history.get_recent_topic_names();
|
|
}
|
|
|
|
export function get_max_message_id(stream_id) {
|
|
const history = find_or_create(stream_id);
|
|
|
|
return history.get_max_message_id();
|
|
}
|
|
|
|
export function reset() {
|
|
// This is only used by tests.
|
|
stream_dict.clear();
|
|
fetched_stream_ids.clear();
|
|
}
|