mirror of
				https://github.com/zulip/zulip.git
				synced 2025-11-04 05:53:43 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			306 lines
		
	
	
		
			8.8 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			306 lines
		
	
	
		
			8.8 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
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 is_complete_for_stream_id(stream_id) {
 | 
						|
    if (fetched_stream_ids.has(stream_id)) {
 | 
						|
        return true;
 | 
						|
    }
 | 
						|
 | 
						|
    /*
 | 
						|
        TODO: We should possibly move all_topics_in_cache
 | 
						|
        from stream_data to here, since the function
 | 
						|
        mostly looks at message_list.all and has little
 | 
						|
        to do with typical stream_data stuff.  (We just
 | 
						|
        need sub.first_message_id.)
 | 
						|
    */
 | 
						|
    const sub = stream_data.get_sub_by_id(stream_id);
 | 
						|
    const in_cache = stream_data.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(opts) {
 | 
						|
        const topic_name = opts.topic_name;
 | 
						|
        let message_id = opts.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(opts.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();
 | 
						|
}
 |