mirror of
https://github.com/zulip/zulip.git
synced 2025-11-06 15:03:34 +00:00
The most expensive part of adding the display time to messages is calling time.toLocaleDateString() and time.toLocaleTimeString(). Most of the time, this information never gets seen, so we now delay calculating it until just before the user would see it. This cuts the time to render a chunk of messages from >1s to ~200ms. (imported from commit 6167e7a8e1c3b4ca77471fa346292be4ffa67ec8)
387 lines
14 KiB
JavaScript
387 lines
14 KiB
JavaScript
/*jslint nomen: true */
|
|
function MessageList(table_name, opts) {
|
|
$.extend(this, {collapse_messages: true}, opts);
|
|
this._items = [];
|
|
this._hash = {};
|
|
this.table_name = table_name;
|
|
this._selected_id = -1;
|
|
this._message_groups = [];
|
|
|
|
if (this.table_name) {
|
|
this._clear_table();
|
|
}
|
|
return this;
|
|
}
|
|
|
|
(function () {
|
|
|
|
function add_display_time(message, prev) {
|
|
if (message.timestr !== undefined) {
|
|
return;
|
|
}
|
|
var two_digits = function (x) { return ('0' + x).slice(-2); };
|
|
var time = new XDate(message.timestamp * 1000);
|
|
var include_date = message.include_recipient;
|
|
|
|
if (prev !== undefined) {
|
|
var prev_time = new XDate(prev.timestamp * 1000);
|
|
if (time.toDateString() !== prev_time.toDateString()) {
|
|
include_date = true;
|
|
}
|
|
}
|
|
|
|
// NB: timestr is HTML, inserted into the document without escaping.
|
|
if (include_date) {
|
|
message.timestr = (timerender.render_time(time))[0].outerHTML;
|
|
} else {
|
|
message.timestr = time.toString("HH:mm");
|
|
}
|
|
}
|
|
|
|
MessageList.prototype = {
|
|
get: function MessageList_get(id) {
|
|
id = parseInt(id, 10);
|
|
if (isNaN(id)) {
|
|
return undefined;
|
|
}
|
|
return this._hash[id];
|
|
},
|
|
|
|
empty: function MessageList_empty() {
|
|
return this._items.length === 0;
|
|
},
|
|
|
|
first: function MessageList_first() {
|
|
return this._items[0];
|
|
},
|
|
|
|
last: function MessageList_last() {
|
|
return this._items[this._items.length - 1];
|
|
},
|
|
|
|
_clear_table: function MessageList__clear_table() {
|
|
// We do not want to call .empty() because that also clears
|
|
// jQuery data. This does mean, however, that we need to be
|
|
// mindful of memory leaks.
|
|
rows.get_table(this.table_name).children().detach();
|
|
},
|
|
|
|
clear: function MessageList_clear(opts) {
|
|
opts = $.extend({}, {clear_selected_id: true}, opts);
|
|
|
|
this._items = [];
|
|
this._hash = {};
|
|
this._message_groups = [];
|
|
this._clear_table();
|
|
|
|
if (opts.clear_selected_id) {
|
|
this._selected_id = -1;
|
|
}
|
|
},
|
|
|
|
selected_id: function MessageList_selected_id() {
|
|
return this._selected_id;
|
|
},
|
|
|
|
select_id: function MessageList_select_id(id, opts) {
|
|
opts = $.extend({then_scroll: false, use_closest: false}, opts, {id: id, msg_list: this});
|
|
|
|
id = parseInt(id, 10);
|
|
if (isNaN(id)) {
|
|
throw (new Error("Bad message id"));
|
|
}
|
|
if (this.get(id) === undefined) {
|
|
if (!opts.use_closest) {
|
|
throw (new Error("Selected message id not in MessageList"));
|
|
} else {
|
|
id = this.closest_id(id);
|
|
opts.id = id;
|
|
}
|
|
}
|
|
this._selected_id = id;
|
|
|
|
// This is the number of pixels between the top of the
|
|
// viewable window and the newly selected message
|
|
var scrolltop_offset;
|
|
var selected_row = rows.get(id, this.table_name);
|
|
var new_msg_in_view = (selected_row.length > 0);
|
|
if (new_msg_in_view) {
|
|
scrolltop_offset = viewport.scrollTop() - selected_row.offset().top;
|
|
}
|
|
if (this._maybe_rerender()) {
|
|
// If we could see the newly selected message, scroll the
|
|
// window such that the newly selected message is at the
|
|
// same location as it would have been before we
|
|
// re-rendered.
|
|
if (new_msg_in_view) {
|
|
viewport.scrollTop(rows.get(id, this.table_name).offset().top + scrolltop_offset);
|
|
}
|
|
}
|
|
$(document).trigger($.Event('message_selected.zephyr', opts));
|
|
},
|
|
|
|
selected_message: function MessageList_selected_message() {
|
|
return this.get(this._selected_id);
|
|
},
|
|
|
|
selected_row: function MessageList_selected_row() {
|
|
return rows.get(this._selected_id, this.table_name);
|
|
},
|
|
|
|
closest_id: function MessageList_closest_id(id) {
|
|
if (this._items.length === 0) {
|
|
return -1;
|
|
}
|
|
var closest = util.lower_bound(this._items, id,
|
|
function (a, b) {
|
|
return a.id < b;
|
|
});
|
|
if (closest === this._items.length
|
|
|| (closest !== 0
|
|
&& (id - this._items[closest - 1].id <
|
|
this._items[closest].id - id)))
|
|
{
|
|
closest = closest - 1;
|
|
}
|
|
return this._items[closest].id;
|
|
},
|
|
|
|
_add_to_hash: function MessageList__add_to_hash(messages) {
|
|
var self = this;
|
|
messages.forEach(function (elem) {
|
|
var id = parseInt(elem.id, 10);
|
|
if (isNaN(id)) {
|
|
throw (new Error("Bad message id"));
|
|
}
|
|
if (self._hash[id] !== undefined) {
|
|
throw (new Error("Duplicate message added to MessageList"));
|
|
}
|
|
self._hash[id] = elem;
|
|
});
|
|
},
|
|
|
|
// Number of messages to render at a time
|
|
_RENDER_WINDOW_SIZE: 400,
|
|
// Number of messages away from edge of render window at which we
|
|
// trigger a re-render
|
|
_RENDER_THRESHOLD: 50,
|
|
|
|
_maybe_rerender: function MessageList__maybe_rerender() {
|
|
if (this.table_name === undefined) {
|
|
return false;
|
|
}
|
|
|
|
var selected_idx = util.lower_bound(this._items, this._selected_id,
|
|
function (a, b) { return a.id < b; });
|
|
var new_min_idx = -1;
|
|
|
|
// We rerender under the following conditions:
|
|
// * This is the first render
|
|
// * The selected message is within this._RENDER_THRESHOLD messages
|
|
// of the top of the currently rendered window and the top
|
|
// of the window does not abut the beginning of the message
|
|
// list
|
|
// * The selected message is within this._RENDER_THRESHOLD messages
|
|
// of the bottom of the currently rendered window and the
|
|
// bottom of the window does not abut the end of the
|
|
// message list
|
|
if (! (this._min_rendered_idx === undefined
|
|
|| ((selected_idx - this._min_rendered_idx < this._RENDER_THRESHOLD)
|
|
&& (this._min_rendered_idx !== 0))
|
|
|| ((this._max_rendered_idx - selected_idx < this._RENDER_THRESHOLD)
|
|
&& (this._max_rendered_idx !== this._items.length - 1))))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
new_min_idx = Math.max(selected_idx - this._RENDER_WINDOW_SIZE / 2, 0);
|
|
if (new_min_idx === this._min_rendered_idx) {
|
|
return false;
|
|
}
|
|
|
|
this._min_rendered_idx = new_min_idx;
|
|
this._max_rendered_idx = Math.max(Math.min(this._min_rendered_idx + this._RENDER_WINDOW_SIZE - 1,
|
|
this._items.length - 1),
|
|
0);
|
|
|
|
this._clear_table();
|
|
this._render(this._items.slice(this._min_rendered_idx,
|
|
this._max_rendered_idx + 1),
|
|
'bottom', true);
|
|
return true;
|
|
},
|
|
|
|
_render: function MessageList__render(messages, where) {
|
|
if (messages.length === 0 || this.table_name === undefined)
|
|
return;
|
|
|
|
var self = this;
|
|
var table_name = this.table_name;
|
|
var table = rows.get_table(table_name);
|
|
var messages_to_render = [];
|
|
var ids_where_next_is_same_sender = {};
|
|
var prev;
|
|
var last_message_id;
|
|
|
|
var current_group = [];
|
|
var new_message_groups = [];
|
|
|
|
if (where === 'top' && this.collapse_messages && this._message_groups.length > 0) {
|
|
// Delete the current top message group, and add it back in with these
|
|
// messages, in order to collapse properly.
|
|
//
|
|
// This means we redraw the entire view on each update when narrowed by
|
|
// subject, which could be a problem down the line. For now we hope
|
|
// that subject views will not be very big.
|
|
|
|
var top_group = this._message_groups[0];
|
|
var top_messages = [];
|
|
$.each(top_group, function (index, id) {
|
|
rows.get(id, table_name).remove();
|
|
top_messages.push(self.get(id));
|
|
});
|
|
messages = messages.concat(top_messages);
|
|
|
|
// Delete the leftover recipient label.
|
|
table.find('.recipient_row:first').remove();
|
|
} else {
|
|
last_message_id = rows.id(table.find('tr[zid]:last'));
|
|
prev = this.get(last_message_id);
|
|
}
|
|
|
|
$.each(messages, function (index, message) {
|
|
message.include_recipient = false;
|
|
message.include_bookend = false;
|
|
if (util.same_recipient(prev, message) && self.collapse_messages) {
|
|
current_group.push(message.id);
|
|
} else {
|
|
if (current_group.length > 0)
|
|
new_message_groups.push(current_group);
|
|
current_group = [message.id];
|
|
|
|
// Add a space to the table, but not for the first element.
|
|
message.include_recipient = true;
|
|
message.include_bookend = (prev !== undefined);
|
|
}
|
|
|
|
message.include_sender = true;
|
|
if (!message.include_recipient &&
|
|
util.same_sender(prev, message) &&
|
|
(Math.abs(message.timestamp - prev.timestamp) < 60*10)) {
|
|
message.include_sender = false;
|
|
ids_where_next_is_same_sender[prev.id] = true;
|
|
}
|
|
|
|
add_display_time(message, prev);
|
|
|
|
message.dom_id = table_name + message.id;
|
|
|
|
if (message.sender_email === email) {
|
|
message.stamp = ui.get_gravatar_stamp();
|
|
}
|
|
|
|
if (message.is_stream) {
|
|
message.background_color = subs.get_color(message.display_recipient);
|
|
message.color_class = subs.get_color_class(message.background_color);
|
|
message.invite_only = subs.get_invite_only(message.display_recipient);
|
|
}
|
|
|
|
messages_to_render.push(message);
|
|
prev = message;
|
|
});
|
|
|
|
if (messages_to_render.length === 0) {
|
|
return;
|
|
}
|
|
|
|
if (current_group.length > 0)
|
|
new_message_groups.push(current_group);
|
|
|
|
if (where === 'top') {
|
|
this._message_groups = new_message_groups.concat(this._message_groups);
|
|
} else {
|
|
this._message_groups = this._message_groups.concat(new_message_groups);
|
|
}
|
|
|
|
var rendered_elems = $(templates.message({
|
|
messages: messages_to_render,
|
|
include_layout_row: (table.find('tr:first').length === 0)
|
|
}));
|
|
|
|
$.each(rendered_elems, function (index, elem) {
|
|
var row = $(elem);
|
|
if (! row.hasClass('message_row')) {
|
|
return;
|
|
}
|
|
var id = rows.id(row);
|
|
if (ids_where_next_is_same_sender[id]) {
|
|
row.find('.messagebox').addClass("next_is_same_sender");
|
|
}
|
|
if (this === narrowed_msg_list) {
|
|
// If narrowed, we may need to highlight the message
|
|
search.maybe_highlight_message(row);
|
|
}
|
|
});
|
|
|
|
// The message that was last before this batch came in has to be
|
|
// handled specially because we didn't just render it and
|
|
// therefore have to lookup its associated element
|
|
if (last_message_id !== undefined
|
|
&& ids_where_next_is_same_sender[last_message_id])
|
|
{
|
|
var row = rows.get(last_message_id, table_name);
|
|
row.find('.messagebox').addClass("next_is_same_sender");
|
|
}
|
|
|
|
if (where === 'top' && table.find('.ztable_layout_row').length > 0) {
|
|
// If we have a totally empty narrow, there may not
|
|
// be a .ztable_layout_row.
|
|
table.find('.ztable_layout_row').after(rendered_elems);
|
|
} else {
|
|
table.append(rendered_elems);
|
|
|
|
// XXX: This is absolutely awful. There is a firefox bug
|
|
// where when table rows as DOM elements are appended (as
|
|
// opposed to as a string) a border is sometimes added to the
|
|
// row. This border goes away if we add a dummy row to the
|
|
// top of the table (it doesn't go away on any reflow,
|
|
// though, as resizing the window doesn't make them go away).
|
|
// So, we add an empty row and then garbage collect them
|
|
// later when the user is idle.
|
|
var dummy = $("<tr></tr>");
|
|
table.find('.ztable_layout_row').after(dummy);
|
|
$(document).idle({'idle': 1000*10,
|
|
'onIdle': function () {
|
|
dummy.remove();
|
|
}});
|
|
}
|
|
},
|
|
|
|
append: function MessageList_append(messages) {
|
|
this._items = this._items.concat(messages);
|
|
this._add_to_hash(messages);
|
|
|
|
var cur_window_size = this._max_rendered_idx - this._min_rendered_idx + 1;
|
|
if (cur_window_size < this._RENDER_WINDOW_SIZE) {
|
|
this._render(messages.slice(0, this._RENDER_WINDOW_SIZE - cur_window_size),
|
|
'bottom');
|
|
}
|
|
},
|
|
|
|
prepend: function MessageList_prepend(messages) {
|
|
this._items = messages.concat(this._items);
|
|
this._add_to_hash(messages);
|
|
|
|
if (this._min_rendered_idx !== undefined) {
|
|
this._min_rendered_idx += messages.length;
|
|
this._max_rendered_idx += messages.length;
|
|
}
|
|
},
|
|
|
|
all: function MessageList_all() {
|
|
return this._items;
|
|
}
|
|
};
|
|
}());
|
|
/*jslint nomen: false */
|