mirror of
				https://github.com/zulip/zulip.git
				synced 2025-11-03 21:43:21 +00:00 
			
		
		
		
	There is a bug and race issue that occurs when a message is selected while we are in the process of reifying a locally echoed message, raising the "Selected message id not in MessageList" error. The code flow to get the exception is as follows: * A user sends a message to the current narrow we are in. * Before the new message event is received, we sent a message to the same message list which renders it with a locally echoed id. * One of the ways of getting the exception is to already have the locally sent message selected, before receiving an acknowledgment from the server. * Thus the Message List Data's `selected_id` now points to the new message id. The exception is raised on entering the `was_selected` if block inside `message_list_view` which tries to re-select the message. Updating the `_rerender_message` code for this special case won't fix the entire bug because, as mentioned above there are other ways of getting the exception: Ideally, after all our synchronous work (`echo.process_from_server`) has completed we would expect the re-order and re-render work of the `change_message_id` would occur first, due to the timer of the setTimeout being set to 0. However as evident from the race condition existing, this isn't always the case. `change_message_id` function is responsible for 3 things: updation, re-ordering and re-rendering. The first one which is responsible for updating the message list's local cache, occurs synchronously while for the latter two, they both occur asynchronously. Before the setTimeout which is responsible for the latter two actions, is encountered the user might select the message by clicking or more commonly by scrolling, which causes this message selection event to be ahead of the setTimeout in the callback queue. During this time frame, our race condition takes place. And even though the message id is updated it's Message List is not in the correct sort order, which leads to `closest_id` !== `id` in `MessageList_select_id` being true and raising the exception. Now, we only asynchronously call the re_render function, to guarantee the data is always correct and UI updates should be done at the end. Extended by tabbott to comment the setTimeout call. Fixes #15346.
		
			
				
	
	
		
			576 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			576 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
const util = require("./util");
 | 
						|
function MessageListData(opts) {
 | 
						|
    this.muting_enabled = opts.muting_enabled;
 | 
						|
    if (this.muting_enabled) {
 | 
						|
        this._all_items = [];
 | 
						|
    }
 | 
						|
    this._items = [];
 | 
						|
    this._hash = new Map();
 | 
						|
    this._local_only = new Set();
 | 
						|
    this._selected_id = -1;
 | 
						|
 | 
						|
    let filter = opts.filter;
 | 
						|
    if (filter === undefined) {
 | 
						|
        filter = new Filter();
 | 
						|
    }
 | 
						|
 | 
						|
    this.filter = filter;
 | 
						|
    this.fetch_status = FetchStatus();
 | 
						|
}
 | 
						|
 | 
						|
MessageListData.prototype = {
 | 
						|
    all_messages: function () {
 | 
						|
        return this._items;
 | 
						|
    },
 | 
						|
 | 
						|
    num_items: function () {
 | 
						|
        return this._items.length;
 | 
						|
    },
 | 
						|
 | 
						|
    empty: function () {
 | 
						|
        return this._items.length === 0;
 | 
						|
    },
 | 
						|
 | 
						|
    first: function () {
 | 
						|
        return this._items[0];
 | 
						|
    },
 | 
						|
 | 
						|
    last: function () {
 | 
						|
        return this._items[this._items.length - 1];
 | 
						|
    },
 | 
						|
 | 
						|
    select_idx: function () {
 | 
						|
        if (this._selected_id === -1) {
 | 
						|
            return;
 | 
						|
        }
 | 
						|
        const ids = this._items.map((message) => message.id);
 | 
						|
 | 
						|
        const i = ids.indexOf(this._selected_id);
 | 
						|
        if (i === -1) {
 | 
						|
            return;
 | 
						|
        }
 | 
						|
        return i;
 | 
						|
    },
 | 
						|
 | 
						|
    prev: function () {
 | 
						|
        const i = this.select_idx();
 | 
						|
 | 
						|
        if (i === undefined) {
 | 
						|
            return;
 | 
						|
        }
 | 
						|
 | 
						|
        if (i === 0) {
 | 
						|
            return;
 | 
						|
        }
 | 
						|
 | 
						|
        return this._items[i - 1].id;
 | 
						|
    },
 | 
						|
 | 
						|
    next: function () {
 | 
						|
        const i = this.select_idx();
 | 
						|
 | 
						|
        if (i === undefined) {
 | 
						|
            return;
 | 
						|
        }
 | 
						|
 | 
						|
        if (i + 1 >= this._items.length) {
 | 
						|
            return;
 | 
						|
        }
 | 
						|
 | 
						|
        return this._items[i + 1].id;
 | 
						|
    },
 | 
						|
 | 
						|
    is_at_end: function () {
 | 
						|
        if (this._selected_id === -1) {
 | 
						|
            return false;
 | 
						|
        }
 | 
						|
 | 
						|
        const n = this._items.length;
 | 
						|
 | 
						|
        if (n === 0) {
 | 
						|
            return false;
 | 
						|
        }
 | 
						|
 | 
						|
        const last_msg = this._items[n - 1];
 | 
						|
 | 
						|
        return last_msg.id === this._selected_id;
 | 
						|
    },
 | 
						|
 | 
						|
    nth_most_recent_id: function (n) {
 | 
						|
        const i = this._items.length - n;
 | 
						|
        if (i < 0) {
 | 
						|
            return -1;
 | 
						|
        }
 | 
						|
        return this._items[i].id;
 | 
						|
    },
 | 
						|
 | 
						|
    clear: function () {
 | 
						|
        if (this.muting_enabled) {
 | 
						|
            this._all_items = [];
 | 
						|
        }
 | 
						|
 | 
						|
        this._items = [];
 | 
						|
        this._hash.clear();
 | 
						|
    },
 | 
						|
 | 
						|
    get: function (id) {
 | 
						|
        id = parseFloat(id);
 | 
						|
        if (isNaN(id)) {
 | 
						|
            return;
 | 
						|
        }
 | 
						|
        return this._hash.get(id);
 | 
						|
    },
 | 
						|
 | 
						|
    clear_selected_id: function () {
 | 
						|
        this._selected_id = -1;
 | 
						|
    },
 | 
						|
 | 
						|
    selected_id: function () {
 | 
						|
        return this._selected_id;
 | 
						|
    },
 | 
						|
 | 
						|
    set_selected_id: function (id) {
 | 
						|
        this._selected_id = id;
 | 
						|
    },
 | 
						|
 | 
						|
    selected_idx: function () {
 | 
						|
        return this._lower_bound(this._selected_id);
 | 
						|
    },
 | 
						|
 | 
						|
    reset_select_to_closest: function () {
 | 
						|
        this._selected_id = this.closest_id(this._selected_id);
 | 
						|
    },
 | 
						|
 | 
						|
    is_search: function () {
 | 
						|
        return this.filter.is_search();
 | 
						|
    },
 | 
						|
    can_mark_messages_read: function () {
 | 
						|
        return this.filter.can_mark_messages_read();
 | 
						|
    },
 | 
						|
    _get_predicate: function () {
 | 
						|
        // We cache this.
 | 
						|
        if (!this.predicate) {
 | 
						|
            this.predicate = this.filter.predicate();
 | 
						|
        }
 | 
						|
        return this.predicate;
 | 
						|
    },
 | 
						|
 | 
						|
    valid_non_duplicated_messages: function (messages) {
 | 
						|
        const predicate = this._get_predicate();
 | 
						|
        const self = this;
 | 
						|
        return messages.filter((msg) => self.get(msg.id) === undefined && predicate(msg));
 | 
						|
    },
 | 
						|
 | 
						|
    filter_incoming: function (messages) {
 | 
						|
        const predicate = this._get_predicate();
 | 
						|
        return messages.filter(predicate);
 | 
						|
    },
 | 
						|
 | 
						|
    unmuted_messages: function (messages) {
 | 
						|
        return messages.filter(
 | 
						|
            (message) =>
 | 
						|
                !muting.is_topic_muted(message.stream_id, message.topic) ||
 | 
						|
                message.mentioned,
 | 
						|
        );
 | 
						|
    },
 | 
						|
 | 
						|
    update_items_for_muting: function () {
 | 
						|
        if (!this.muting_enabled) {
 | 
						|
            return;
 | 
						|
        }
 | 
						|
        this._items = this.unmuted_messages(this._all_items);
 | 
						|
    },
 | 
						|
 | 
						|
    first_unread_message_id: function () {
 | 
						|
        const first_unread = this._items.find((message) => unread.message_unread(message));
 | 
						|
 | 
						|
        if (first_unread) {
 | 
						|
            return first_unread.id;
 | 
						|
        }
 | 
						|
 | 
						|
        // if no unread, return the bottom message
 | 
						|
        return this.last().id;
 | 
						|
    },
 | 
						|
 | 
						|
    update_user_full_name: function (user_id, full_name) {
 | 
						|
        for (const item of this._items) {
 | 
						|
            if (item.sender_id && item.sender_id === user_id) {
 | 
						|
                item.sender_full_name = full_name;
 | 
						|
            }
 | 
						|
        }
 | 
						|
    },
 | 
						|
 | 
						|
    update_user_avatar: function (user_id, avatar_url) {
 | 
						|
        // TODO:
 | 
						|
        // We may want to de-dup some logic with update_user_full_name,
 | 
						|
        // especially if we want to optimize this with some kind of
 | 
						|
        // hash that maps sender_id -> messages.
 | 
						|
        for (const item of this._items) {
 | 
						|
            if (item.sender_id && item.sender_id === user_id) {
 | 
						|
                item.small_avatar_url = avatar_url;
 | 
						|
            }
 | 
						|
        }
 | 
						|
    },
 | 
						|
 | 
						|
    update_stream_name: function (stream_id, new_stream_name) {
 | 
						|
        for (const item of this._items) {
 | 
						|
            if (item.stream_id && item.stream_id === stream_id) {
 | 
						|
                item.display_recipient = new_stream_name;
 | 
						|
                item.stream = new_stream_name;
 | 
						|
            }
 | 
						|
        }
 | 
						|
    },
 | 
						|
 | 
						|
    add_messages: function (messages) {
 | 
						|
        const self = this;
 | 
						|
        let top_messages = [];
 | 
						|
        let bottom_messages = [];
 | 
						|
        let interior_messages = [];
 | 
						|
 | 
						|
        // If we're initially populating the list, save the messages in
 | 
						|
        // bottom_messages regardless
 | 
						|
        if (self.selected_id() === -1 && self.empty()) {
 | 
						|
            const narrow_messages = self.filter_incoming(messages);
 | 
						|
            bottom_messages = narrow_messages.filter((msg) => !self.get(msg.id));
 | 
						|
        } else {
 | 
						|
            // Filter out duplicates that are already in self, and all messages
 | 
						|
            // that fail our filter predicate
 | 
						|
            messages = self.valid_non_duplicated_messages(messages);
 | 
						|
 | 
						|
            for (const msg of messages) {
 | 
						|
                // Put messages in correct order on either side of the
 | 
						|
                // message list.  This code path assumes that messages
 | 
						|
                // is a (1) sorted, and (2) consecutive block of
 | 
						|
                // messages that belong in this message list; those
 | 
						|
                // facts should be ensured by the caller.
 | 
						|
                if (self.empty() || msg.id > self.last().id) {
 | 
						|
                    bottom_messages.push(msg);
 | 
						|
                } else if (msg.id < self.first().id) {
 | 
						|
                    top_messages.push(msg);
 | 
						|
                } else {
 | 
						|
                    interior_messages.push(msg);
 | 
						|
                }
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        if (interior_messages.length > 0) {
 | 
						|
            interior_messages = self.add_anywhere(interior_messages);
 | 
						|
        }
 | 
						|
 | 
						|
        if (top_messages.length > 0) {
 | 
						|
            top_messages = self.prepend(top_messages);
 | 
						|
        }
 | 
						|
 | 
						|
        if (bottom_messages.length > 0) {
 | 
						|
            bottom_messages = self.append(bottom_messages);
 | 
						|
        }
 | 
						|
 | 
						|
        const info = {
 | 
						|
            top_messages: top_messages,
 | 
						|
            bottom_messages: bottom_messages,
 | 
						|
            interior_messages: interior_messages,
 | 
						|
        };
 | 
						|
 | 
						|
        return info;
 | 
						|
    },
 | 
						|
 | 
						|
    add_anywhere: function (messages) {
 | 
						|
        // Caller should have already filtered messages.
 | 
						|
        // This should be used internally when we have
 | 
						|
        // "interior" messages to add and can't optimize
 | 
						|
        // things by only doing prepend or only doing append.
 | 
						|
        let viewable_messages;
 | 
						|
        if (this.muting_enabled) {
 | 
						|
            this._all_items = messages.concat(this._all_items);
 | 
						|
            this._all_items.sort((a, b) => a.id - b.id);
 | 
						|
 | 
						|
            viewable_messages = this.unmuted_messages(messages);
 | 
						|
            this._items = viewable_messages.concat(this._items);
 | 
						|
 | 
						|
        } else {
 | 
						|
            viewable_messages = messages;
 | 
						|
            this._items = messages.concat(this._items);
 | 
						|
        }
 | 
						|
 | 
						|
        this._items.sort((a, b) => a.id - b.id);
 | 
						|
        this._add_to_hash(messages);
 | 
						|
        return viewable_messages;
 | 
						|
    },
 | 
						|
 | 
						|
    append: function (messages) {
 | 
						|
        // Caller should have already filtered
 | 
						|
        let viewable_messages;
 | 
						|
        if (this.muting_enabled) {
 | 
						|
            this._all_items = this._all_items.concat(messages);
 | 
						|
            viewable_messages = this.unmuted_messages(messages);
 | 
						|
        } else {
 | 
						|
            viewable_messages = messages;
 | 
						|
        }
 | 
						|
        this._items = this._items.concat(viewable_messages);
 | 
						|
        this._add_to_hash(messages);
 | 
						|
        return viewable_messages;
 | 
						|
    },
 | 
						|
 | 
						|
    prepend: function (messages) {
 | 
						|
        // Caller should have already filtered
 | 
						|
        let viewable_messages;
 | 
						|
        if (this.muting_enabled) {
 | 
						|
            this._all_items = messages.concat(this._all_items);
 | 
						|
            viewable_messages = this.unmuted_messages(messages);
 | 
						|
        } else {
 | 
						|
            viewable_messages = messages;
 | 
						|
        }
 | 
						|
        this._items = viewable_messages.concat(this._items);
 | 
						|
        this._add_to_hash(messages);
 | 
						|
        return viewable_messages;
 | 
						|
    },
 | 
						|
 | 
						|
    remove: function (messages) {
 | 
						|
        const self = this;
 | 
						|
 | 
						|
        for (const message of messages) {
 | 
						|
            const stored_message = self._hash.get(message.id);
 | 
						|
            if (stored_message !== undefined) {
 | 
						|
                self._hash.delete(stored_message);
 | 
						|
            }
 | 
						|
            self._local_only.delete(message.id);
 | 
						|
        }
 | 
						|
 | 
						|
        const msg_ids_to_remove = new Set();
 | 
						|
 | 
						|
        for (const message of messages) {
 | 
						|
            msg_ids_to_remove.add(message.id);
 | 
						|
        }
 | 
						|
 | 
						|
        this._items = this._items.filter((message) => !msg_ids_to_remove.has(message.id));
 | 
						|
        if (this.muting_enabled) {
 | 
						|
            this._all_items = this._all_items.filter(
 | 
						|
                (message) => !msg_ids_to_remove.has(message.id),
 | 
						|
            );
 | 
						|
        }
 | 
						|
    },
 | 
						|
 | 
						|
    // Returns messages from the given message list in the specified range, inclusive
 | 
						|
    message_range: function (start, end) {
 | 
						|
        if (start === -1) {
 | 
						|
            blueslip.error("message_range given a start of -1");
 | 
						|
        }
 | 
						|
 | 
						|
        const start_idx = this._lower_bound(start);
 | 
						|
        const end_idx   = this._lower_bound(end);
 | 
						|
        return this._items.slice(start_idx, end_idx + 1);
 | 
						|
    },
 | 
						|
 | 
						|
    // Returns the index where you could insert the desired ID
 | 
						|
    // into the message list, without disrupting the sort order
 | 
						|
    // This takes into account the potentially-unsorted
 | 
						|
    // nature of local message IDs in the message list
 | 
						|
    _lower_bound: function (id) {
 | 
						|
        const self = this;
 | 
						|
        function less_func(msg, ref_id, a_idx) {
 | 
						|
            if (self._is_localonly_id(msg.id)) {
 | 
						|
                // First non-local message before this one
 | 
						|
                const effective = self._next_nonlocal_message(self._items, a_idx,
 | 
						|
                                                              (idx) => idx - 1);
 | 
						|
                if (effective) {
 | 
						|
                    // Turn the 10.02 in [11, 10.02, 12] into 11.02
 | 
						|
                    const decimal = parseFloat((msg.id % 1).toFixed(0.02));
 | 
						|
                    const effective_id = effective.id + decimal;
 | 
						|
                    return effective_id < ref_id;
 | 
						|
                }
 | 
						|
            }
 | 
						|
            return msg.id < ref_id;
 | 
						|
        }
 | 
						|
 | 
						|
        return util.lower_bound(self._items, id, less_func);
 | 
						|
    },
 | 
						|
 | 
						|
    closest_id: function (id) {
 | 
						|
        // We directly keep track of local-only messages,
 | 
						|
        // so if we're asked for one that we know we have,
 | 
						|
        // just return it directly
 | 
						|
        if (this._local_only.has(id)) {
 | 
						|
            return id;
 | 
						|
        }
 | 
						|
 | 
						|
        const items = this._items;
 | 
						|
 | 
						|
        if (items.length === 0) {
 | 
						|
            return -1;
 | 
						|
        }
 | 
						|
 | 
						|
        let closest = this._lower_bound(id);
 | 
						|
 | 
						|
        if (closest < items.length && id === items[closest].id) {
 | 
						|
            return items[closest].id;
 | 
						|
        }
 | 
						|
 | 
						|
        const potential_closest_matches = [];
 | 
						|
        if (closest > 0 && this._is_localonly_id(items[closest - 1].id)) {
 | 
						|
            // Since we treated all blocks of local ids as their left-most-non-local message
 | 
						|
            // for lower_bound purposes, find the real leftmost index (first non-local id)
 | 
						|
            do {
 | 
						|
                potential_closest_matches.push(closest);
 | 
						|
                closest -= 1;
 | 
						|
            } while (closest > 0 && this._is_localonly_id(items[closest - 1].id));
 | 
						|
        }
 | 
						|
        potential_closest_matches.push(closest);
 | 
						|
 | 
						|
        if (closest === items.length) {
 | 
						|
            closest = closest - 1;
 | 
						|
        } else {
 | 
						|
            // Any of the ids that we skipped over (due to them being local-only) might be the
 | 
						|
            // closest ID to the desired one, in case there is no exact match.
 | 
						|
            potential_closest_matches.unshift(_.last(potential_closest_matches) - 1);
 | 
						|
            let best_match = items[closest].id;
 | 
						|
 | 
						|
            for (const potential_idx of potential_closest_matches) {
 | 
						|
                if (potential_idx < 0) {
 | 
						|
                    continue;
 | 
						|
                }
 | 
						|
                const item = items[potential_idx];
 | 
						|
 | 
						|
                if (item === undefined) {
 | 
						|
                    blueslip.warn('Invalid potential_idx: ' + potential_idx);
 | 
						|
                    continue;
 | 
						|
                }
 | 
						|
 | 
						|
                const potential_match = item.id;
 | 
						|
                // If the potential id is the closest to the requested, save that one
 | 
						|
                if (Math.abs(id - potential_match) < Math.abs(best_match - id)) {
 | 
						|
                    best_match = potential_match;
 | 
						|
                    closest = potential_idx;
 | 
						|
                }
 | 
						|
            }
 | 
						|
        }
 | 
						|
        return items[closest].id;
 | 
						|
    },
 | 
						|
 | 
						|
    advance_past_messages: function (msg_ids) {
 | 
						|
        // Start with the current pointer, but then keep advancing the
 | 
						|
        // pointer while the next message's id is in msg_ids.  See trac #1555
 | 
						|
        // for more context, but basically we are skipping over contiguous
 | 
						|
        // messages that we have recently visited.
 | 
						|
        let next_msg_id = 0;
 | 
						|
 | 
						|
        const id_set = new Set(msg_ids);
 | 
						|
 | 
						|
        let idx = this.selected_idx() + 1;
 | 
						|
        while (idx < this._items.length) {
 | 
						|
            const msg_id = this._items[idx].id;
 | 
						|
            if (!id_set.has(msg_id)) {
 | 
						|
                break;
 | 
						|
            }
 | 
						|
            next_msg_id = msg_id;
 | 
						|
            idx += 1;
 | 
						|
        }
 | 
						|
 | 
						|
        if (next_msg_id > 0) {
 | 
						|
            this.set_selected_id(next_msg_id);
 | 
						|
        }
 | 
						|
    },
 | 
						|
 | 
						|
    _add_to_hash: function (messages) {
 | 
						|
        const self = this;
 | 
						|
        messages.forEach((elem) => {
 | 
						|
            const id = parseFloat(elem.id);
 | 
						|
            if (isNaN(id)) {
 | 
						|
                blueslip.fatal("Bad message id");
 | 
						|
            }
 | 
						|
            if (self._is_localonly_id(id)) {
 | 
						|
                self._local_only.add(id);
 | 
						|
            }
 | 
						|
            if (self._hash.has(id)) {
 | 
						|
                blueslip.error("Duplicate message added to MessageListData");
 | 
						|
                return;
 | 
						|
            }
 | 
						|
            self._hash.set(id, elem);
 | 
						|
        });
 | 
						|
    },
 | 
						|
 | 
						|
    _is_localonly_id: function (id) {
 | 
						|
        return id % 1 !== 0;
 | 
						|
    },
 | 
						|
 | 
						|
    _next_nonlocal_message: function (item_list, start_index, op) {
 | 
						|
        let cur_idx = start_index;
 | 
						|
        do {
 | 
						|
            cur_idx = op(cur_idx);
 | 
						|
        } while (item_list[cur_idx] !== undefined && this._is_localonly_id(item_list[cur_idx].id));
 | 
						|
        return item_list[cur_idx];
 | 
						|
    },
 | 
						|
 | 
						|
    change_message_id: function (old_id, new_id, opts) {
 | 
						|
        // Update our local cache that uses the old id to the new id
 | 
						|
        if (this._hash.has(old_id)) {
 | 
						|
            const msg = this._hash.get(old_id);
 | 
						|
            this._hash.delete(old_id);
 | 
						|
            this._hash.set(new_id, msg);
 | 
						|
        } else {
 | 
						|
            return;
 | 
						|
        }
 | 
						|
 | 
						|
        if (this._local_only.has(old_id)) {
 | 
						|
            if (this._is_localonly_id(new_id)) {
 | 
						|
                this._local_only.add(new_id);
 | 
						|
            }
 | 
						|
            this._local_only.delete(old_id);
 | 
						|
        }
 | 
						|
 | 
						|
        if (this._selected_id === old_id) {
 | 
						|
            this._selected_id = new_id;
 | 
						|
        }
 | 
						|
 | 
						|
        this.reorder_messages(new_id, opts);
 | 
						|
    },
 | 
						|
 | 
						|
    reorder_messages: function (new_id, opts) {
 | 
						|
        function message_sort_func(a, b) {return a.id - b.id;}
 | 
						|
        // If this message is now out of order, re-order and re-render
 | 
						|
        const self = this;
 | 
						|
        const current_message = self._hash.get(new_id);
 | 
						|
        const index = self._items.indexOf(current_message);
 | 
						|
 | 
						|
        if (index === -1) {
 | 
						|
            if (!self.muting_enabled && opts.is_current_list()) {
 | 
						|
                blueslip.error("Trying to re-order message but can't find message with new_id in _items!");
 | 
						|
            }
 | 
						|
            return;
 | 
						|
        }
 | 
						|
 | 
						|
        const next = self._next_nonlocal_message(self._items, index,
 | 
						|
                                                 (idx) => idx + 1);
 | 
						|
        const prev = self._next_nonlocal_message(self._items, index,
 | 
						|
                                                 (idx) => idx - 1);
 | 
						|
 | 
						|
        if (next !== undefined && current_message.id > next.id ||
 | 
						|
            prev !== undefined && current_message.id < prev.id) {
 | 
						|
            blueslip.debug("Changed message ID from server caused out-of-order list, reordering");
 | 
						|
            self._items.sort(message_sort_func);
 | 
						|
            if (self.muting_enabled) {
 | 
						|
                self._all_items.sort(message_sort_func);
 | 
						|
            }
 | 
						|
 | 
						|
            // The data updates above need to happen synchronously,
 | 
						|
            // but the rerendering can be deferred.  It's unclear
 | 
						|
            // whether this deferral is necessary; it was present in
 | 
						|
            // the original 2013 local echo implementation but we
 | 
						|
            // don't have records for why we added it then.
 | 
						|
            setTimeout(opts.re_render, 0);
 | 
						|
        }
 | 
						|
    },
 | 
						|
 | 
						|
    get_last_message_sent_by_me: function () {
 | 
						|
        const msg_index = _.findLastIndex(this._items, {sender_id: page_params.user_id});
 | 
						|
        if (msg_index === -1) {
 | 
						|
            return;
 | 
						|
        }
 | 
						|
        const msg = this._items[msg_index];
 | 
						|
        return msg;
 | 
						|
    },
 | 
						|
};
 | 
						|
 | 
						|
module.exports = MessageListData;
 | 
						|
 | 
						|
window.MessageListData = MessageListData;
 |