Render messages as groups of divs instead of individual rows

Now that we no longer use tables for our message list, we can
more logically group messages together.

(imported from commit 9923a092f91a45fe3ef06f2f00e23e4e3fb62a37)
This commit is contained in:
Leo Franchi 2014-02-05 10:55:24 -05:00 committed by Jessica McKellar
parent 748e5b6da6
commit 568c33f294
11 changed files with 417 additions and 358 deletions

View File

@ -64,7 +64,7 @@ function _fade_messages() {
for (i = 0; i < visible_messages.length; i++) {
should_fade_message = !fade_heuristic(focused_recipient, visible_messages[i]);
var elt = current_msg_list.get_row(visible_messages[i].id);
var recipient_row = $(elt).prevAll(".recipient_row").first().expectOne();
var recipient_row = rows.get_message_recipient_row(elt);
change_fade_state(elt, should_fade_message);
change_fade_state(recipient_row, should_fade_message);
@ -74,7 +74,7 @@ function _fade_messages() {
// Defer updating all messages so that the compose box can open sooner
setTimeout(function (expected_msg_list, expected_recipient) {
var all_elts = rows.get_table(current_msg_list.table_name).find(".recipient_row, .message_row");
var all_groups = rows.get_table(current_msg_list.table_name).find(".recipient_row");
if (current_msg_list !== expected_msg_list ||
!compose.composing() ||
@ -86,13 +86,10 @@ function _fade_messages() {
// Note: The below algorithm relies on the fact that all_elts is
// sorted as it would be displayed in the message view
for (i = 0; i < all_elts.length; i++) {
var elt = $(all_elts[i]);
if (elt.hasClass("recipient_row")) {
should_fade_message = !fade_heuristic(focused_recipient, current_msg_list.get(rows.id(elt)));
}
change_fade_state(elt, should_fade_message);
for (i = 0; i < all_groups.length; i++) {
var group_elt = $(all_groups[i]);
should_fade_message = !fade_heuristic(focused_recipient, rows.recipient_from_group(group_elt));
change_fade_state(group_elt, should_fade_message);
}
}, 0, current_msg_list, compose.recipient());
}

View File

@ -278,6 +278,18 @@ MessageList.prototype = {
function (a, b) { return a.id < b; });
},
subscribed_bookend_content: function (stream_name) {
return "--- Subscribed to stream " + stream_name + " ---";
},
unsubscribed_bookend_content: function (stream_name) {
return "--- Unsubscribed from stream " + stream_name + " ---";
},
not_subscribed_bookend_content: function (stream_name) {
return "--- Not subscribed to stream " + stream_name + " ---";
},
// Maintains a trailing bookend element explaining any changes in
// your subscribed/unsubscribed status at the bottom of the
// message list.
@ -293,13 +305,13 @@ MessageList.prototype = {
var trailing_bookend_content, subscribed = stream_data.is_subscribed(stream);
if (subscribed) {
if (this.last_message_historical) {
trailing_bookend_content = "--- Subscribed to stream " + stream + " ---";
trailing_bookend_content = this.subscribed_bookend_content(stream);
}
} else {
if (!this.last_message_historical) {
trailing_bookend_content = "--- Unsubscribed from stream " + stream + " ---";
trailing_bookend_content = this.unsubscribed_bookend_content(stream);
} else {
trailing_bookend_content = "--- Not subscribed to stream " + stream + " ---";
trailing_bookend_content = this.not_subscribed_bookend_content(stream);
}
}
if (trailing_bookend_content !== undefined) {

View File

@ -22,21 +22,24 @@ function stringify_time(time) {
return time.toString('h:mm TT');
}
function add_display_time(message, prev) {
function same_day(earlier_msg, later_msg) {
var earlier_time = new XDate(earlier_msg.timestamp * 1000);
var later_time = new XDate(later_msg.timestamp * 1000);
return earlier_time.toDateString() === later_time.toDateString();
}
function add_display_time(group, message, prev) {
var time = new XDate(message.timestamp * 1000);
if (prev !== undefined) {
var prev_time = new XDate(prev.timestamp * 1000);
if (time.toDateString() !== prev_time.toDateString()) {
// NB: show_date is HTML, inserted into the document without escaping.
message.show_date = (timerender.render_date(time, prev_time))[0].outerHTML;
} else {
// This is run on re-render, and must remove the date if a message
// from the same day is added above this one when scrolling up.
message.show_date = undefined;
group.show_date = (timerender.render_date(time, prev_time))[0].outerHTML;
}
} else {
message.show_date = (timerender.render_date(time))[0].outerHTML;
group.show_date = (timerender.render_date(time))[0].outerHTML;
}
if (message.timestr === undefined) {
@ -61,16 +64,20 @@ MessageListView.prototype = {
return;
}
function start_group() {
return {messages: []};
}
var table_name = this.table_name;
var table = rows.get_table(table_name);
// we we record if last_message_was_selected before updating the table
var last_message_was_selected = rows.id(rows.last_visible()) === list.selected_id();
var messages_to_render = [];
var ids_where_next_is_same_sender = {};
var prev;
var orig_scrolltop_offset, last_message_id;
var combined_messages, first_msg, last_msg;
var current_group = [];
var current_group = start_group();
var new_message_groups = [];
var self = this;
@ -83,26 +90,21 @@ MessageListView.prototype = {
}
if (where === 'top' && self.collapse_messages && this._message_groups.length > 0) {
// Remove the top date_row, we'll re-add it after rendering
$('.date_row:first', table).remove();
// 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
// This means we redraw the entire view on each backfill-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 = self._message_groups[0];
var top_messages = [];
_.each(top_group, function (id) {
self.get_row(id).remove();
// Remove any date row headers for these messages
$('.date_row[data-zid=' + id + ']').remove();
top_messages.push(self.get_message(id));
});
messages = messages.concat(top_messages);
rows.get_message_recipient_row(this.get_row(top_group.messages[0].id)).remove();
messages = messages.concat(top_group.messages);
}
// Delete the leftover recipient label.
table.find('.recipient_row:first').remove();
} else {
if (where !== 'top') {
var last_row = table.find('div[zid]:last');
last_message_id = rows.id(last_row);
prev = self.get_message(last_message_id);
@ -116,51 +118,73 @@ MessageListView.prototype = {
}
}
function add_message_to_group(message) {
current_group.messages.push(message);
}
function finish_group() {
if (current_group.length > 0) {
var message_ids = _.pluck(current_group, 'id');
current_group[0].message_ids = message_ids;
current_group[0].collapsible = feature_flags.collapsible;
new_message_groups.push(message_ids);
if (current_group.messages.length > 0) {
var first_message = current_group.messages[0];
current_group.message_ids = _.pluck(current_group.messages, 'id');
current_group.is_stream = first_message.is_stream;
current_group.is_private = first_message.is_private;
if (current_group.is_stream) {
current_group.background_color = stream_data.get_color(first_message.stream);
current_group.invite_only = stream_data.get_invite_only(first_message.stream);
current_group.subject = first_message.subject;
} else if (current_group.is_private) {
current_group.pm_with_url = first_message.pm_with_url;
current_group.display_reply_to = first_message.display_reply_to;
}
current_group.display_recipient = first_message.display_recipient;
current_group.always_visible_topic_edit = first_message.always_visible_topic_edit;
current_group.subject_links = first_message.subject_links;
current_group.messages[current_group.messages.length - 1].include_footer = true;
new_message_groups.push(current_group);
}
}
function add_subscription_marker(group, last_msg, first_msg) {
if (last_msg !== undefined &&
first_msg.historical !== last_msg.historical) {
group.bookend_top = true;
if (first_msg.historical) {
group.unsubscribed = first_msg.stream;
group.bookend_content = self.list.unsubscribed_bookend_content(first_msg.stream);
} else {
group.subscribed = first_msg.stream;
group.bookend_content = self.list.subscribed_bookend_content(first_msg.stream);
}
}
}
_.each(messages, function (message) {
message.include_recipient = false;
message.include_bookend = false;
message.include_footer = false;
add_display_time(message, prev);
if (util.same_recipient(prev, message) && self.collapse_messages &&
prev.historical === message.historical && !message.show_date) {
current_group.push(message);
// prev is no longer the last element in this block
prev.include_footer = false;
prev.historical === message.historical && same_day(prev, message)) {
add_message_to_group(message);
} else {
finish_group();
current_group = [message];
current_group = start_group();
add_message_to_group(message);
// Add a space to the table, but not for the first element.
message.include_recipient = true;
message.include_bookend = (prev !== undefined);
if (prev !== undefined) {
prev.include_footer = true;
}
message.subscribed = false;
message.unsubscribed = false;
if (message.include_bookend &&
// This home_msg_list condition can be removed
// once we filter historical messages from the
// home view on the server side (which requires
// having an index on UserMessage.flags)
self.list !== home_msg_list &&
message.historical !== prev.historical) {
if (message.historical) {
message.unsubscribed = message.stream;
} else {
message.subscribed = message.stream;
}
// This home_msg_list condition can be removed
// once we filter historical messages from the
// home view on the server side (which requires
// having an index on UserMessage.flags)
if (self.list !== home_msg_list) {
add_subscription_marker(current_group, prev, message);
}
if (message.stream) {
@ -171,6 +195,8 @@ MessageListView.prototype = {
}
}
add_display_time(current_group, message, prev);
message.include_sender = true;
if (!message.include_recipient &&
!prev.status_message &&
@ -179,13 +205,7 @@ MessageListView.prototype = {
ids_where_next_is_same_sender[prev.id] = true;
}
if (message.last_edit_timestamp !== undefined) {
// Add or update the last_edit_timestr
var last_edit_time = new XDate(message.last_edit_timestamp * 1000);
message.last_edit_timestr =
(timerender.render_date(last_edit_time))[0].innerText
+ " at " + stringify_time(last_edit_time);
}
self._add_msg_timestring(message);
message.dom_id = table_name + message.id;
@ -205,7 +225,6 @@ MessageListView.prototype = {
message.status_message = false;
}
messages_to_render.push(message);
prev = message;
// This home_msg_list condition can be removed
// once we filter historical messages from the
@ -216,52 +235,49 @@ MessageListView.prototype = {
}
});
if (prev) {
prev.include_footer = true;
}
finish_group();
if (messages_to_render.length === 0) {
if (new_message_groups.length === 0 ||
new_message_groups[new_message_groups.length - 1].messages.length === 0) {
return;
}
finish_group();
if (where === 'top') {
self._message_groups = new_message_groups.concat(self._message_groups);
} else {
self._message_groups = self._message_groups.concat(new_message_groups);
// If we prepended messages that are historical, show the subscription message
if (self._message_groups.length === 0 ||
(prev.historical !== self._message_groups[0].messages[0].historical)) {
if (self.list !== home_msg_list) {
current_group.bookend_bottom = true;
add_subscription_marker(current_group, prev, self._message_groups[0].messages[0]);
}
}
}
var rendered_elems = $(templates.render('message', {
messages: messages_to_render,
include_layout_row: (table.find('div:first').length === 0),
use_match_properties: list.filter.is_search()
var rendered_groups = $(templates.render('message_group', {
message_groups: new_message_groups
}));
_.each(rendered_elems, function (elem) {
var row = $(elem);
var rendered_messages = [];
_.each(rendered_groups, function (group) {
_.each($('div.message_row', group), function (message_row) {
var row = $(message_row);
// Save DOM elements by id into self._rows for O(1) lookup
if (row.hasClass('message_row')) {
self._rows[row.attr('zid')] = elem;
}
// Save DOM elements by id into self._rows for O(1) lookup
if (row.hasClass('message_row')) {
self._rows[row.attr('zid')] = message_row;
}
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 (row.hasClass('mention')) {
row.find('.user-mention').each(function () {
var email = $(this).attr('data-user-email');
if (email === '*' || email === page_params.email) {
$(this).addClass('user-mention-me');
}
});
}
if (row.hasClass('mention')) {
row.find('.user-mention').each(function () {
var email = $(this).attr('data-user-email');
if (email === '*' || email === page_params.email) {
$(this).addClass('user-mention-me');
}
});
}
rendered_messages.push(message_row);
});
});
// The message that was last before this batch came in has to be
@ -274,35 +290,69 @@ MessageListView.prototype = {
if (ids_where_next_is_same_sender[last_message_id]) {
row.find('.messagebox').addClass("next_is_same_sender");
}
// We didn't actually rerender the original last message,
// but we might have set .include_footer=false for it in
// the above loop since it was the previous message for
// messages[0]. If so, we need to update the DOM.
if (self.get_message(last_message_id) && ! self.get_message(last_message_id).include_footer) {
row.removeClass('last_message');
}
}
_.each(rendered_elems, function (elem){
_.each(rendered_messages, function (elem){
var e = $.Event('message_rendered.zulip', {target: elem});
$(document).trigger(e);
});
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);
list.update_trailing_bookend();
function first_group_message(groups) {
var first_group = groups[0];
return first_group.messages[0];
}
_.each(rendered_elems, function (elem) {
var row = $(elem);
if (! row.hasClass('message_row')) {
return;
function last_group_message(groups) {
var last_group = groups[groups.length - 1];
return last_group.messages[last_group.messages.length - 1];
}
function combine_adjacent_groups(before_list, after_list) {
// Given two lists of message groups,
// returns: one list that has the abutting groups' message list messages
var combined_messages = before_list[before_list.length - 1].messages.concat(after_list[0].messages);
before_list[before_list.length - 1].messages = combined_messages;
return before_list.concat(after_list.slice(1));
}
if (self._message_groups.length === 0) {
self._message_groups = new_message_groups;
table.append(rendered_groups);
} else {
if (where === 'top') {
self._message_groups = new_message_groups.concat(self._message_groups);
table.prepend(rendered_groups);
} else {
// When appending messages, since we're not re-rendering the whole existing last block of messages,
// we may have to insert the messages in the existing block. We do this if the messages would normally
// have been in the same group originally
last_msg = last_group_message(self._message_groups);
first_msg = first_group_message(new_message_groups);
if (self.collapse_messages && util.same_recipient(last_msg, first_msg) && same_day(last_msg, first_msg)) {
self._message_groups = combine_adjacent_groups(self._message_groups, new_message_groups);
// Pluck the merged messages out of our rendered group list, and insert them
// into the existing group div
var last_group = rows.get_message_recipient_row(self._rows[last_msg.id]);
last_group.append($('.message_row', rendered_groups[0]).remove());
rendered_groups.splice(0, 1);
} else {
self._message_groups = self._message_groups.concat(new_message_groups);
}
// append the rest of the groups
table.append(rendered_groups);
}
}
list.update_trailing_bookend();
_.each(rendered_messages, function (elem) {
var row = $(elem);
var id = rows.id(row);
message_edit.maybe_show_edit(row, id);
});
@ -311,7 +361,7 @@ MessageListView.prototype = {
// getBoundingClientRect to work.
// Also, the list must actually be visible.
if (list === current_msg_list) {
ui.condense_and_collapse(rendered_elems);
ui.condense_and_collapse(rendered_messages);
}
// Must happen after anything that changes the height of messages has
@ -343,7 +393,17 @@ MessageListView.prototype = {
}
if (list === current_msg_list && messages_are_new) {
self._maybe_autoscroll(rendered_elems, last_message_was_selected);
self._maybe_autoscroll(rendered_messages, last_message_was_selected);
}
},
_add_msg_timestring: function MessageListView__add_msg_timestring(message) {
if (message.last_edit_timestamp !== undefined) {
// Add or update the last_edit_timestr
var last_edit_time = new XDate(message.last_edit_timestamp * 1000);
message.last_edit_timestr =
(timerender.render_date(last_edit_time))[0].innerText
+ " at " + stringify_time(last_edit_time);
}
},
@ -356,7 +416,7 @@ MessageListView.prototype = {
var id_of_last_message_sent_by_us = -1;
// C++ iterators would have made this less painful
_.each(rendered_elems.toArray().reverse(), function (elem) {
_.each(rendered_elems.reverse(), function (elem) {
// Sometimes there are non-DOM elements in rendered_elems; only
// try to get the heights of actual trs.
if ($(elem).is("div")) {
@ -545,7 +605,8 @@ MessageListView.prototype = {
var cur_window_size = this._render_win_end - this._render_win_start;
if (cur_window_size < this._RENDER_WINDOW_SIZE) {
var slice_to_render = messages.slice(0, this._RENDER_WINDOW_SIZE - cur_window_size);
var msgs_to_render_count = this._RENDER_WINDOW_SIZE - cur_window_size;
var slice_to_render = messages.slice(messages.length - msgs_to_render_count);
this.render(slice_to_render, 'top', false);
this._render_win_start -= slice_to_render.length;
}
@ -575,13 +636,14 @@ MessageListView.prototype = {
},
clear_trailing_bookend: function MessageListView_clear_trailing_bookend() {
var trailing_bookend = rows.get_table(this.table_name).find('#trailing_bookend');
var trailing_bookend = rows.get_table(this.table_name).find('.trailing_bookend');
trailing_bookend.remove();
},
render_trailing_bookend: function MessageListView_render_trailng_bookend(trailing_bookend_content) {
var rendered_trailing_bookend = $(templates.render('trailing_bookend', {
trailing_bookend: trailing_bookend_content
render_trailing_bookend: function MessageListView_render_trailing_bookend(trailing_bookend_content) {
var rendered_trailing_bookend = $(templates.render('bookend', {
bookend_content: trailing_bookend_content,
trailing: true
}));
rows.get_table(this.table_name).append(rendered_trailing_bookend);
},

View File

@ -13,7 +13,12 @@ exports.next_visible = function (message_row) {
if (row.length !== 0) {
return row;
}
return message_row.nextUntil('.selectable_row').next('.selectable_row');
var recipient_row = exports.get_message_recipient_row(message_row);
var next_recipient_rows = $(recipient_row).nextAll('.recipient_row');
if (next_recipient_rows.length === 0) {
return $();
}
return $('.selectable_row:first', next_recipient_rows[0]);
};
exports.prev_visible = function (message_row) {
@ -24,7 +29,12 @@ exports.prev_visible = function (message_row) {
if (row.length !== 0) {
return row;
}
return message_row.prevUntil('.selectable_row').prev('.selectable_row');
var recipient_row = exports.get_message_recipient_row(message_row);
var prev_recipient_rows = $(recipient_row).prevAll('.recipient_row');
if (prev_recipient_rows.length === 0) {
return $();
}
return $('.selectable_row:last', prev_recipient_rows[0]);
};
exports.first_visible = function () {
@ -52,11 +62,27 @@ exports.get_table = function (table_name) {
return $('#' + table_name);
};
exports.get_closest_row = function (element) {
exports.get_closest_group = function (element) {
// This gets the closest message row to an element, whether it's
// a recipient bar or message. With our current markup,
// this is the most reliable way to do it.
return $(element).closest("div.message_row, div.recipient_row");
return $(element).closest("div.recipient_row");
};
exports.first_message_in_group = function (message_group) {
return $('div.message_row:first', message_group);
};
exports.get_message_recipient_row = function (message_row) {
return $(message_row).parent('.recipient_row').expectOne();
};
exports.get_message_recipient_header = function (message_row) {
return $(message_row).parent('.recipient_row').find('.message_header').expectOne();
};
exports.recipient_from_group = function (message_group) {
return message_store.get(exports.id($(message_group).children('.message_row').first().expectOne()));
};
return exports;

View File

@ -262,13 +262,13 @@ function box(x, y, width, height) {
$("#clear-screen").css({opacity: 0.0, width: doc_width, height: doc_height});
}
function messages_in_viewport() {
function message_groups_in_viewport() {
var vp = viewport.message_viewport_info();
var top = vp.visible_top;
var height = vp.visible_height;
var last_row = rows.last_visible();
var last_group = rows.get_message_recipient_row(rows.last_visible());
return $.merge(last_row, last_row.prevAll()).filter(function (idx, row) {
return $.merge(last_group, last_group.prevAll()).filter(function (idx, row) {
var row_offset = $(row).offset();
return (row_offset.top > top && row_offset.top < top + height);
});
@ -392,29 +392,31 @@ function update_popover_info(popover_func) {
function box_first_message() {
var spotlight_message = rows.first_visible();
var bar = spotlight_message.prev(".recipient_row");
var bar = rows.get_message_recipient_row(spotlight_message);
var header = bar.find('.message_header');
var x = bar.offset().left;
var y = bar.offset().top;
var message_height = bar.height() + spotlight_message.height();
var message_height = header.height() + spotlight_message.height();
var message_width = bar.width();
box(x, y, message_width, message_height);
}
function box_messagelist() {
var spotlight_message = rows.first_visible().prev(".recipient_row");
var x = spotlight_message.offset().left;
var y = spotlight_message.offset().top;
var spotlight_message_row = rows.get_message_recipient_row(rows.first_visible());
var x = spotlight_message_row.offset().left;
var y = spotlight_message_row.offset().top;
var height = 0;
_.each(messages_in_viewport(), function (row) {
_.each(message_groups_in_viewport(), function (row) {
height += $(row).height();
});
box(x, y, spotlight_message.width(), height);
box(x, y, spotlight_message_row.width(), height);
}
function reply() {
var spotlight_message = rows.first_visible().prev(".recipient_row");
var spotlight_message = rows.get_message_recipient_row(rows.first_visible());
box_messagelist();
create_and_show_popover(spotlight_message, maybe_tweak_placement("left"),
"Replying", "tutorial_reply");
@ -430,7 +432,7 @@ function reply() {
}
function home() {
var spotlight_message = rows.first_visible().prev(".recipient_row");
var spotlight_message = rows.get_message_recipient_header(rows.first_visible());
box_messagelist();
create_and_show_popover(spotlight_message, maybe_tweak_placement("left"),
"Narrowing", "tutorial_home");
@ -447,7 +449,7 @@ function home() {
function subject() {
var spotlight_message = rows.first_visible();
var bar = spotlight_message.prev(".recipient_row");
var bar = rows.get_message_recipient_header(spotlight_message);
var placement = maybe_tweak_placement("bottom");
box_first_message();
create_and_show_popover(bar, placement, "Topics", "tutorial_subject");
@ -467,7 +469,7 @@ function subject() {
}
function stream() {
var bar = rows.first_visible().prev(".recipient_row");
var bar = rows.get_message_recipient_header(rows.first_visible());
var placement = maybe_tweak_placement("bottom");
box_first_message();
create_and_show_popover(bar, placement, "Streams", "tutorial_stream");
@ -491,7 +493,7 @@ function welcome() {
$('#top-screen').css({opacity: 0.7, width: $(document).width(),
height: $(document).height()});
var spotlight_message = rows.first_visible();
var bar = spotlight_message.prev(".recipient_row");
var bar = rows.get_message_recipient_header(spotlight_message);
box_first_message();
create_and_show_popover(bar, maybe_tweak_placement("left"), "Welcome",
"tutorial_message");

View File

@ -627,22 +627,28 @@ exports.update_floating_recipient_bar = function () {
// Find the last message where the top of the recipient
// row is at least partially occluded by our box.
// Start with the pointer's current location.
var candidate = current_msg_list.selected_row();
var selected_row = current_msg_list.selected_row();
if (selected_row === undefined || selected_row.length === 0) {
return;
}
var candidate = rows.get_message_recipient_row(selected_row);
if (candidate === undefined) {
return;
}
while (true) {
candidate = candidate.prev();
if (candidate.length === 0) {
// We're at the top of the page and no labels are above us.
hide_floating_recipient_bar();
return;
}
if (candidate.is(".focused_table .recipient_row")) {
if (candidate.is(".recipient_row")) {
if (candidate.offset().top < floating_recipient_bar_bottom) {
break;
}
}
candidate = candidate.prev();
}
var current_label = candidate;
@ -652,27 +658,13 @@ exports.update_floating_recipient_bar = function () {
// Hide if the bottom of our floating stream/subject label is not
// lower than the bottom of current_label (since that means we're
// covering up a label that already exists).
var header_height = $(current_label).find('.message_header').outerHeight();
if (floating_recipient_bar_bottom <=
(current_label.offset().top + current_label.outerHeight())) {
(current_label.offset().top + header_height)) {
hide_floating_recipient_bar();
return;
}
// Hide if our bottom is in our bookend (or one bookend-height
// above it). This means we're not showing any useful part of the
// message above us, so why bother showing the label?
var current_bookend = current_label.nextUntil(".bookend_tr")
.andSelf()
.next(".bookend_tr:first");
// (The last message currently doesn't have a bookend, which is why this might be 0).
if (current_bookend.length > 0) {
if (floating_recipient_bar_bottom >
(current_bookend.offset().top - current_bookend.outerHeight())) {
hide_floating_recipient_bar();
return;
}
}
replace_floating_recipient_bar(current_label);
};
@ -1292,9 +1284,10 @@ $(function () {
});
function get_row_id_for_narrowing(narrow_link_elem) {
var row = rows.get_closest_row(narrow_link_elem);
var group = rows.get_closest_group(narrow_link_elem);
var msg_row = rows.first_message_in_group(group);
var nearest = current_msg_list.get(rows.id(row));
var nearest = current_msg_list.get(rows.id(msg_row));
var selected = current_msg_list.selected_message();
if (util.same_recipient(nearest, selected)) {
return selected.id;

View File

@ -0,0 +1,7 @@
{{! Client-side Mustache template for rendering the trailing bookend.}}
{{#if bookend_content}}
<div class="{{#if trailing}}trailing_bookend{{/if}} bookend sub-unsub-message">
<span>{{bookend_content}}</span>
</div>
{{/if}}

View File

@ -1,168 +0,0 @@
{{! Client-side Mustache template for rendering messages.}}
{{! Because we use table-layout: fixed for the Message table,
all the column widths are computed from the first row;
these CSS classes specify the widths for that first,
collapsed row. (Otherwise, colspan breaks everything).}}
{{#include_layout_row}}
<div class="ztable_layout_row">
</div>
{{/include_layout_row}}
{{#each messages}}
{{#with this}}
{{#include_bookend}}
<div class="bookend_tr">
<div class="bookend{{#if subscribed}} sub-unsub-message{{/if}}{{#if unsubscribed}} sub-unsub-message{{/if}}">
{{#if subscribed}}
<span>--- Subscribed to stream {{subscribed}} ---</span>
{{/if}}
{{#if unsubscribed}}
<span>--- Unsubscribed from stream {{unsubscribed}} ---</span>
{{/if}}
</div>
</div>
{{/include_bookend}}
{{#if show_date}}
<div class="date_row" data-zid="{{id}}"><div colspan="4">{{{show_date}}}</div></div>
{{/if}}
{{#include_recipient}}
<div zid="{{id}}" class="recipient_row" data-messages="{{message_ids}}">
{{#if is_stream}}
<div class="message_header message_header_stream right_part">
<div class="message-header-wrapper">
<div class="message-header-contents">
{{! [-] }}
{{#if collapsible}}
<i class="messages-collapse icon-vector-collapse-alt"></i>
{{/if}}
{{! stream link }}
<a class="message_label_clickable narrows_by_recipient stream_label {{color_class}}"
style="background: {{background_color}}; border-left-color: {{background_color}};"
href="{{stream_url}}"
title="Narrow to stream &quot;{{display_recipient}}&quot;">
{{! invite only lock }}
{{#if invite_only}}
<i class="icon-vector-lock invite-stream-icon" title="This is an invite-only stream"></i>
{{/if}}
{{display_recipient}}
</a>
{{! hidden narrow icon for copy-pasting }}
<span class="copy-paste-text">&gt;</span>
{{! topic stuff }}
<span class="stream_topic">
{{! topic link }}
<a class="message_label_clickable narrows_by_subject"
href="{{topic_url}}"
title="Narrow to stream &quot;{{display_recipient}}&quot;, topic &quot;{{subject}}&quot;">
{{#if ../../../../../use_match_properties}}
{{{match_subject}}}
{{else}}
{{subject}}
{{/if}}
</a>
{{! edit subject pencil icon }}
{{#if always_visible_topic_edit}}
<i class="icon-vector-pencil always_visible_topic_edit"></i>
{{else}}
{{#if on_hover_topic_edit}}
<i class="icon-vector-pencil on_hover_topic_edit"></i>
{{/if}}
{{/if}}
{{! exterior links (e.g. to a trac ticket) }}
{{#each subject_links}}
<a href="{{this}}" target="_blank">
<i class="icon-vector-external-link-sign"></i>
</a>
{{/each}}
</span>
<span class="topic_edit">
<span class="topic_edit_form" id="{{id}}"></span>
</span>
</div>
</div>
</div>
{{else}}
<div class="message_header message_header_private_message dark_background">
<div class="message-header-wrapper">
<div class="message-header-contents">
{{#if collapsible}}
<i class="messages-collapse icon-vector-collapse-alt"></i>
{{/if}}
<a class="message_label_clickable narrows_by_recipient"
href="{{pm_with_url}}"
title="Narrow to your private messages with {{display_reply_to}}">
You and {{display_reply_to}}
</a>
</div>
</div>
</div>
{{/if}}
</div>
{{/include_recipient}}
<div zid="{{id}}" id="{{dom_id}}"
class="message_row{{^is_stream}} private-message{{/is_stream}}{{#include_sender}} include-sender{{/include_sender}}{{#contains_mention}} mention{{/contains_mention}}{{#include_footer}} last_message{{/include_footer}}{{#unread}} unread{{/unread}} {{#if local_id}}local{{/if}} selectable_row">
<div class="unread_marker"><div class="unread-marker-fill"></div></div>
<div class="messagebox{{^include_sender}} prev_is_same_sender{{/include_sender}}{{^is_stream}} private-message{{/is_stream}}">
<div class="messagebox-border">
<div class="messagebox-content">
<div class="message_top_line">
{{#include_sender}}
<span class="message_sender{{^status_message}} sender_info_hover{{/status_message}}">
{{! See ../js/notifications.js for another user of avatar_url. }}
<div class="inline_profile_picture{{#status_message}} sender_info_hover{{/status_message}}"
style="background-image: url('{{small_avatar_url}}');"/><span class="{{^status_message}}sender_name{{/status_message}}{{#status_message}}sender-status{{/status_message}}">{{#unless status_message}}{{sender_full_name}}{{else}}<span class="sender_name sender_info_hover">{{sender_full_name}}</span>{{{ status_message }}}{{/unless}}</span>
</span>
{{/include_sender}}
<span class="message_time{{#if local_id}} notvisible{{/if}}{{#if status_message}} status-time{{/if}}">{{timestr}}</span>
{{#if_and last_edit_timestr include_sender}}
<div class="message_edit_notice" title="Edited ({{last_edit_timestr}})">EDITED</div>
{{/if_and}}
<div class="message_controls">
<div class="star">
<span class="message_star {{#if starred}}icon-vector-star{{else}}icon-vector-star-empty empty-star{{/if}}"
title="{{#if starred}}Unstar{{else}}Star{{/if}} this message"></span>
</div>
<div class="info actions_hover">
<i class="icon-vector-chevron-down"></i>
</div>
<div class="message_failed {{#unless failed_request}}notvisible{{/unless}}">
<span class="failed_text">Not delivered </span><i class="icon-vector-refresh refresh-failed-message"></i><i class="icon-vector-pencil edit-failed-message"></i><i class="icon-vector-remove-sign remove-failed-message"></i>
</div>
</div>
</div>
<div class="message_content">{{#unless status_message}}{{#if ../../../../use_match_properties}}{{{match_content}}}{{else}}{{{content}}}{{/if}}{{/unless}}</div>
{{#if last_edit_timestr}}
{{#unless include_sender}}
<div class="message_edit_notice" title="Edited ({{last_edit_timestr}})">EDITED</div>
{{/unless}}
{{/if}}
<div class="message_edit">
<div class="message_edit_form" id="{{id}}"></div>
</div>
<div class="message_expander message_length_controller" title="See the rest of this message">[More...]</div>
<div class="message_condenser message_length_controller" title="Make this message take up less space on the screen">[Condense this message]</div>
</div>
</div>
</div>
</div>
{{/with}}
{{/each}}
{{#if trailing_bookend}}
<div id="trailing_bookend" class="bookend_tr">
<div class="bookend">
<center>{{trailing_bookend}}</center>
<span class="tiny"><p></p></span>
</div>
</div>
{{/if}}

View File

@ -0,0 +1,140 @@
{{! Client-side Mustache template for rendering messages.}}
{{#each message_groups}}
{{#with this}}
{{#if show_date}}
<div class="date_row">{{{show_date}}}</div>
{{/if}}
{{#if bookend_top}}
{{partial "bookend"}}
{{/if}}
<div class="recipient_row" data-messages="{{message_ids}}">
{{#if is_stream}}
<div class="message_header message_header_stream right_part">
<div class="message-header-wrapper">
<div class="message-header-contents">
{{! stream link }}
<a class="message_label_clickable narrows_by_recipient stream_label"
style="background: {{background_color}}; border-left-color: {{background_color}};"
href="{{stream_url}}"
title="Narrow to stream &quot;{{display_recipient}}&quot;">
{{! invite only lock }}
{{#if invite_only}}
<i class="icon-vector-lock invite-stream-icon" title="This is an invite-only stream"></i>
{{/if}}
{{display_recipient}}
</a>
{{! hidden narrow icon for copy-pasting }}
<span class="copy-paste-text">&gt;</span>
{{! topic stuff }}
<span class="stream_topic">
{{! topic link }}
<a class="message_label_clickable narrows_by_subject"
href="{{topic_url}}"
title="Narrow to stream &quot;{{display_recipient}}&quot;, topic &quot;{{subject}}&quot;">
{{#if ../../../../../use_match_properties}}
{{{match_subject}}}
{{else}}
{{subject}}
{{/if}}
</a>
{{! edit subject pencil icon }}
{{#if always_visible_topic_edit}}
<i class="icon-vector-pencil always_visible_topic_edit"></i>
{{else}}
{{#if on_hover_topic_edit}}
<i class="icon-vector-pencil on_hover_topic_edit"></i>
{{/if}}
{{/if}}
{{! exterior links (e.g. to a trac ticket) }}
{{#each subject_links}}
<a href="{{this}}" target="_blank">
<i class="icon-vector-external-link-sign"></i>
</a>
{{/each}}
</span>
<span class="topic_edit">
<span class="topic_edit_form" id="{{id}}"></span>
</span>
</div>
</div>
</div>
{{else}}
<div class="message_header message_header_private_message dark_background">
<div class="message-header-wrapper">
<div class="message-header-contents">
<a class="message_label_clickable narrows_by_recipient"
href="{{pm_with_url}}"
title="Narrow to your private messages with {{display_reply_to}}">
You and {{display_reply_to}}
</a>
</div>
</div>
</div>
{{/if}}
{{#each messages}}
{{#with this}}
<div zid="{{id}}" id="{{dom_id}}"
class="message_row{{^is_stream}} private-message{{/is_stream}}{{#include_sender}} include-sender{{/include_sender}}{{#contains_mention}} mention{{/contains_mention}}{{#include_footer}} last_message{{/include_footer}}{{#unread}} unread{{/unread}} {{#if local_id}}local{{/if}} selectable_row">
<div class="unread_marker"><div class="unread-marker-fill"></div></div>
<div class="messagebox{{^include_sender}} prev_is_same_sender{{/include_sender}}{{^is_stream}} private-message{{/is_stream}}">
<div class="messagebox-border">
<div class="messagebox-content">
<div class="message_top_line">
{{#include_sender}}
<span class="message_sender{{^status_message}} sender_info_hover{{/status_message}}">
{{! See ../js/notifications.js for another user of avatar_url. }}
<div class="inline_profile_picture{{#status_message}} sender_info_hover{{/status_message}}"
style="background-image: url('{{small_avatar_url}}');"/><span class="{{^status_message}}sender_name{{/status_message}}{{#status_message}}sender-status{{/status_message}}">{{#unless status_message}}{{sender_full_name}}{{else}}<span class="sender_name sender_info_hover">{{sender_full_name}}</span>{{{ status_message }}}{{/unless}}</span>
</span>
{{/include_sender}}
<span class="message_time{{#if local_id}} notvisible{{/if}}{{#if status_message}} status-time{{/if}}">{{timestr}}</span>
{{#if_and last_edit_timestr include_sender}}
<div class="message_edit_notice" title="Edited ({{last_edit_timestr}})">EDITED</div>
{{/if_and}}
<div class="message_controls">
<div class="star">
<span class="message_star {{#if starred}}icon-vector-star{{else}}icon-vector-star-empty empty-star{{/if}}"
title="{{#if starred}}Unstar{{else}}Star{{/if}} this message"></span>
</div>
<div class="info actions_hover">
<i class="icon-vector-chevron-down"></i>
</div>
<div class="message_failed {{#unless failed_request}}notvisible{{/unless}}">
<span class="failed_text">Not delivered </span><i class="icon-vector-refresh refresh-failed-message"></i><i class="icon-vector-pencil edit-failed-message"></i><i class="icon-vector-remove-sign remove-failed-message"></i>
</div>
</div>
</div>
<div class="message_content">{{#unless status_message}}{{#if ../../../../use_match_properties}}{{{match_content}}}{{else}}{{{content}}}{{/if}}{{/unless}}</div>
{{#if last_edit_timestr}}
{{#unless include_sender}}
<div class="message_edit_notice" title="Edited ({{last_edit_timestr}})">EDITED</div>
{{/unless}}
{{/if}}
<div class="message_edit">
<div class="message_edit_form" id="{{id}}"></div>
</div>
<div class="message_expander message_length_controller" title="See the rest of this message">[More...]</div>
<div class="message_condenser message_length_controller" title="Make this message take up less space on the screen">[Condense this message]</div>
</div>
</div>
</div>
</div>
{{/with}}
{{/each}}
</div>
{{#if bookend_bottom}}
{{partial "bookend"}}
{{/if}}
{{/with}}
{{/each}}

View File

@ -1,10 +0,0 @@
{{! Client-side Mustache template for rendering the trailing bookend.}}
{{#if trailing_bookend}}
<tr id="trailing_bookend" class="bookend_tr">
<td colspan="4" class="bookend">
<center>{{trailing_bookend}}</center>
<span class="tiny"><p></p></span>
</td>
</tr>
{{/if}}

View File

@ -559,17 +559,15 @@ function render(template_name, args) {
(function trailing_bookend() {
var args = {
trailing_bookend: "subscribed to stream"
bookend_content: "subscribed to stream",
trailing: true
};
var html = '';
html += '<table>';
html += render('trailing_bookend', args);
html += '</table>';
html += render('bookend', args);
global.write_test_output("trailing_bookend.handlebars", html);
global.write_test_output("bookend.handlebars", html);
var td = $(html).find("td:first");
assert.equal(td.text().trim(), 'subscribed to stream');
assert.equal($(html).text().trim(), 'subscribed to stream');
}());
(function tutorial() {