2013-08-16 17:10:22 +02:00
|
|
|
function MessageListView(list, table_name, collapse_messages) {
|
|
|
|
this.list = list;
|
|
|
|
this.collapse_messages = collapse_messages;
|
|
|
|
this._rows = {};
|
2014-03-17 19:38:35 +01:00
|
|
|
this.message_containers = {};
|
2013-08-16 17:10:22 +02:00
|
|
|
this.table_name = table_name;
|
|
|
|
if (this.table_name) {
|
|
|
|
this.clear_table();
|
|
|
|
}
|
|
|
|
this._message_groups = [];
|
|
|
|
|
|
|
|
// Half-open interval of the indices that define the current render window
|
|
|
|
this._render_win_start = 0;
|
|
|
|
this._render_win_end = 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
(function () {
|
|
|
|
|
2017-01-20 17:43:45 +01:00
|
|
|
function mention_button_refers_to_me(elem) {
|
|
|
|
var user_id = $(elem).attr('data-user-id');
|
|
|
|
if ((user_id === '*') || people.is_my_user_id(user_id)) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Handle legacy markdown that was rendered before we cut
|
|
|
|
// over to using data-user-id.
|
|
|
|
var email = $(elem).attr('data-user-email');
|
|
|
|
if (email === '*' || people.is_current_user(email)) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2013-08-23 19:55:23 +02:00
|
|
|
function stringify_time(time) {
|
2015-08-19 22:35:46 +02:00
|
|
|
if (page_params.twenty_four_hour_time) {
|
2013-08-23 19:55:23 +02:00
|
|
|
return time.toString('HH:mm');
|
|
|
|
}
|
2013-11-21 12:11:50 +01:00
|
|
|
return time.toString('h:mm TT');
|
2013-08-23 19:55:23 +02:00
|
|
|
}
|
|
|
|
|
2014-02-05 16:55:24 +01:00
|
|
|
function same_day(earlier_msg, later_msg) {
|
2014-03-30 03:42:48 +02:00
|
|
|
if (earlier_msg === undefined || later_msg === undefined) {
|
|
|
|
return false;
|
|
|
|
}
|
2014-03-14 16:28:54 +01:00
|
|
|
var earlier_time = new XDate(earlier_msg.msg.timestamp * 1000);
|
|
|
|
var later_time = new XDate(later_msg.msg.timestamp * 1000);
|
2014-02-05 16:55:24 +01:00
|
|
|
|
|
|
|
return earlier_time.toDateString() === later_time.toDateString();
|
|
|
|
}
|
|
|
|
|
2014-03-14 16:28:54 +01:00
|
|
|
function same_sender(a, b) {
|
|
|
|
if (a === undefined || b === undefined) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
return util.same_sender(a.msg, b.msg);
|
|
|
|
}
|
|
|
|
|
|
|
|
function same_recipient(a, b) {
|
|
|
|
if (a === undefined || b === undefined) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
return util.same_recipient(a.msg, b.msg);
|
|
|
|
}
|
|
|
|
|
2014-03-17 19:38:35 +01:00
|
|
|
function add_display_time(group, message_container, prev) {
|
|
|
|
var time = new XDate(message_container.msg.timestamp * 1000);
|
2017-05-18 21:18:11 +02:00
|
|
|
var today = new XDate();
|
2013-08-16 17:10:22 +02:00
|
|
|
|
|
|
|
if (prev !== undefined) {
|
2014-03-14 16:28:54 +01:00
|
|
|
var prev_time = new XDate(prev.msg.timestamp * 1000);
|
2013-08-16 17:10:22 +02:00
|
|
|
if (time.toDateString() !== prev_time.toDateString()) {
|
2013-08-29 01:34:10 +02:00
|
|
|
// NB: show_date is HTML, inserted into the document without escaping.
|
2017-05-18 21:18:11 +02:00
|
|
|
group.show_date = (timerender.render_date(time, prev_time, today))[0].outerHTML;
|
2017-05-16 23:19:57 +02:00
|
|
|
group.show_date_separator = true;
|
2013-08-16 17:10:22 +02:00
|
|
|
}
|
|
|
|
} else {
|
2017-05-16 23:19:57 +02:00
|
|
|
// Show the date in the recipient bar, but not a date separator bar.
|
|
|
|
group.show_date_separator = false;
|
2017-05-18 21:18:11 +02:00
|
|
|
group.show_date = (timerender.render_date(time, undefined, today))[0].outerHTML;
|
2013-08-16 17:10:22 +02:00
|
|
|
}
|
|
|
|
|
2014-03-17 19:38:35 +01:00
|
|
|
if (message_container.timestr === undefined) {
|
|
|
|
message_container.timestr = stringify_time(time);
|
2013-08-16 17:10:22 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
editing: Fix live update of ability to edit messages.
Previously, we didn't check the organization-level settings when
rendering a message list; instead, we only checked it when putting
messages into the message_store. That resulted in the state being
stale in the event that the setting controlling whether one can edit
messages was changed.
We remove some node tests, because revidving the node test for their
new home in message_list_view would be more work than we probably want
to do with an upcoming release. We basically need to be better about
exporting functions like populate_group_from_message_container and
set_topic_edit_properties, so we can do fine grained testing.
When we get around to the node tests, rather than exporting these
functions, it might make sense to create a new module with a name
like message_container.js, which would have all of these
last-second type of data manipulations on message objects. This
would be nice to split out of message_list_view.js. MLV is our
biggest module, and it's mostly cohesive, but it's real job
should be about assembling messages into a DOM list, which is
probably 80% of the code now. The 20% that I'd want to consider
splitting out is actually closer in spirit to message_store.js.
Thanks to Steve Howell for helping with the node tests.
2017-08-23 04:41:43 +02:00
|
|
|
function set_topic_edit_properties(group, message) {
|
|
|
|
group.always_visible_topic_edit = false;
|
|
|
|
group.on_hover_topic_edit = false;
|
|
|
|
if (!page_params.realm_allow_message_editing) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Messages with no topics should always have an edit icon visible
|
|
|
|
// to encourage updating them. Admins can also edit any topic.
|
|
|
|
if (message.subject === compose.empty_topic_placeholder()) {
|
|
|
|
group.always_visible_topic_edit = true;
|
|
|
|
} else if (page_params.is_admin) {
|
|
|
|
group.on_hover_topic_edit = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2014-03-17 19:38:35 +01:00
|
|
|
function populate_group_from_message_container(group, message_container) {
|
|
|
|
group.is_stream = message_container.msg.is_stream;
|
|
|
|
group.is_private = message_container.msg.is_private;
|
2014-03-03 22:00:42 +01:00
|
|
|
|
|
|
|
if (group.is_stream) {
|
2014-03-17 19:38:35 +01:00
|
|
|
group.background_color = stream_data.get_color(message_container.msg.stream);
|
2014-03-03 22:00:42 +01:00
|
|
|
group.color_class = stream_color.get_color_class(group.background_color);
|
2014-03-17 19:38:35 +01:00
|
|
|
group.invite_only = stream_data.get_invite_only(message_container.msg.stream);
|
|
|
|
group.subject = message_container.msg.subject;
|
2014-03-18 23:24:23 +01:00
|
|
|
group.match_subject = message_container.msg.match_subject;
|
2014-03-17 19:38:35 +01:00
|
|
|
group.stream_url = message_container.stream_url;
|
|
|
|
group.topic_url = message_container.topic_url;
|
2016-11-22 09:20:20 +01:00
|
|
|
var sub = stream_data.get_sub(message_container.msg.stream);
|
|
|
|
if (sub === undefined) {
|
|
|
|
// Hack to handle unusual cases like the tutorial where
|
|
|
|
// the streams used don't actually exist in the subs
|
|
|
|
// module. Ideally, we'd clean this up by making the
|
|
|
|
// tutorial populate subs.js "properly".
|
|
|
|
group.stream_id = -1;
|
|
|
|
} else {
|
|
|
|
group.stream_id = sub.stream_id;
|
|
|
|
}
|
2014-03-03 22:00:42 +01:00
|
|
|
} else if (group.is_private) {
|
2014-03-17 19:38:35 +01:00
|
|
|
group.pm_with_url = message_container.pm_with_url;
|
2017-01-25 02:16:33 +01:00
|
|
|
group.display_reply_to = message_store.get_pm_full_names(message_container.msg);
|
2014-03-03 22:00:42 +01:00
|
|
|
}
|
2014-03-17 19:38:35 +01:00
|
|
|
group.display_recipient = message_container.msg.display_recipient;
|
|
|
|
group.subject_links = message_container.msg.subject_links;
|
2016-08-18 22:18:33 +02:00
|
|
|
|
editing: Fix live update of ability to edit messages.
Previously, we didn't check the organization-level settings when
rendering a message list; instead, we only checked it when putting
messages into the message_store. That resulted in the state being
stale in the event that the setting controlling whether one can edit
messages was changed.
We remove some node tests, because revidving the node test for their
new home in message_list_view would be more work than we probably want
to do with an upcoming release. We basically need to be better about
exporting functions like populate_group_from_message_container and
set_topic_edit_properties, so we can do fine grained testing.
When we get around to the node tests, rather than exporting these
functions, it might make sense to create a new module with a name
like message_container.js, which would have all of these
last-second type of data manipulations on message objects. This
would be nice to split out of message_list_view.js. MLV is our
biggest module, and it's mostly cohesive, but it's real job
should be about assembling messages into a DOM list, which is
probably 80% of the code now. The 20% that I'd want to consider
splitting out is actually closer in spirit to message_store.js.
Thanks to Steve Howell for helping with the node tests.
2017-08-23 04:41:43 +02:00
|
|
|
set_topic_edit_properties(group, message_container.msg);
|
|
|
|
|
2016-08-18 22:18:33 +02:00
|
|
|
var time = new XDate(message_container.msg.timestamp * 1000);
|
2017-05-18 21:18:11 +02:00
|
|
|
var today = new XDate();
|
|
|
|
var date_element = timerender.render_date(time, undefined, today)[0];
|
2016-08-18 22:18:33 +02:00
|
|
|
|
|
|
|
group.date = date_element.outerHTML;
|
2014-03-03 22:00:42 +01:00
|
|
|
}
|
|
|
|
|
2013-08-16 17:10:22 +02:00
|
|
|
MessageListView.prototype = {
|
|
|
|
// 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,
|
|
|
|
|
2014-03-17 19:38:35 +01:00
|
|
|
_add_msg_timestring: function MessageListView___add_msg_timestring(message_container) {
|
|
|
|
if (message_container.msg.last_edit_timestamp !== undefined) {
|
2014-03-06 23:11:03 +01:00
|
|
|
// Add or update the last_edit_timestr
|
2014-03-17 19:38:35 +01:00
|
|
|
var last_edit_time = new XDate(message_container.msg.last_edit_timestamp * 1000);
|
2017-05-18 21:18:11 +02:00
|
|
|
var today = new XDate();
|
2014-03-17 19:38:35 +01:00
|
|
|
message_container.last_edit_timestr =
|
2017-05-18 21:18:11 +02:00
|
|
|
(timerender.render_date(last_edit_time, undefined, today))[0].textContent
|
2014-03-06 23:11:03 +01:00
|
|
|
+ " at " + stringify_time(last_edit_time);
|
|
|
|
}
|
|
|
|
},
|
2013-08-16 17:10:22 +02:00
|
|
|
|
2016-12-02 15:16:33 +01:00
|
|
|
add_subscription_marker: function MessageListView__add_subscription_marker(
|
|
|
|
group, last_msg_container, first_msg_container) {
|
2014-03-17 19:38:35 +01:00
|
|
|
if (last_msg_container !== undefined &&
|
|
|
|
first_msg_container.msg.historical !== last_msg_container.msg.historical) {
|
2014-03-06 23:11:03 +01:00
|
|
|
group.bookend_top = true;
|
2014-03-17 19:38:35 +01:00
|
|
|
if (first_msg_container.msg.historical) {
|
|
|
|
group.unsubscribed = first_msg_container.msg.stream;
|
2016-12-02 15:16:33 +01:00
|
|
|
group.bookend_content =
|
|
|
|
this.list.unsubscribed_bookend_content(first_msg_container.msg.stream);
|
2014-03-06 23:11:03 +01:00
|
|
|
} else {
|
2014-03-17 19:38:35 +01:00
|
|
|
group.subscribed = first_msg_container.msg.stream;
|
2016-12-02 15:16:33 +01:00
|
|
|
group.bookend_content =
|
|
|
|
this.list.subscribed_bookend_content(first_msg_container.msg.stream);
|
2014-03-06 23:11:03 +01:00
|
|
|
}
|
2013-08-16 17:10:22 +02:00
|
|
|
}
|
2014-03-06 23:11:03 +01:00
|
|
|
},
|
2013-08-16 17:10:22 +02:00
|
|
|
|
2016-12-02 14:06:06 +01:00
|
|
|
build_message_groups: function MessageListView__build_message_groups(message_containers) {
|
2014-02-05 16:55:24 +01:00
|
|
|
function start_group() {
|
2014-03-08 08:59:38 +01:00
|
|
|
return {
|
2014-03-17 19:38:35 +01:00
|
|
|
message_containers: [],
|
2017-01-12 00:17:43 +01:00
|
|
|
message_group_id: _.uniqueId('message_group_'),
|
2014-03-08 08:59:38 +01:00
|
|
|
};
|
2014-02-05 16:55:24 +01:00
|
|
|
}
|
|
|
|
|
2014-03-06 23:11:03 +01:00
|
|
|
var self = this;
|
2014-02-05 16:55:24 +01:00
|
|
|
var current_group = start_group();
|
2013-08-16 17:10:22 +02:00
|
|
|
var new_message_groups = [];
|
2014-03-06 23:11:03 +01:00
|
|
|
var prev;
|
2013-08-16 17:10:22 +02:00
|
|
|
|
2014-03-17 19:38:35 +01:00
|
|
|
function add_message_container_to_group(message_container) {
|
|
|
|
if (same_sender(prev, message_container)) {
|
2014-03-08 08:59:38 +01:00
|
|
|
prev.next_is_same_sender = true;
|
|
|
|
}
|
2014-03-17 19:38:35 +01:00
|
|
|
current_group.message_containers.push(message_container);
|
2014-02-05 16:55:24 +01:00
|
|
|
}
|
|
|
|
|
2013-08-23 00:14:11 +02:00
|
|
|
function finish_group() {
|
2014-03-17 19:38:35 +01:00
|
|
|
if (current_group.message_containers.length > 0) {
|
2016-12-02 15:16:33 +01:00
|
|
|
populate_group_from_message_container(current_group,
|
|
|
|
current_group.message_containers[0]);
|
|
|
|
current_group
|
|
|
|
.message_containers[current_group.message_containers.length - 1]
|
|
|
|
.include_footer = true;
|
2014-02-05 16:55:24 +01:00
|
|
|
new_message_groups.push(current_group);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2014-03-17 19:38:35 +01:00
|
|
|
_.each(message_containers, function (message_container) {
|
2016-12-02 13:23:23 +01:00
|
|
|
var message_reactions = reactions.get_message_reactions(message_container.msg);
|
|
|
|
message_container.msg.message_reactions = message_reactions;
|
2014-03-17 19:38:35 +01:00
|
|
|
message_container.include_recipient = false;
|
|
|
|
message_container.include_footer = false;
|
2013-08-16 17:10:22 +02:00
|
|
|
|
2016-08-18 22:18:33 +02:00
|
|
|
if (same_recipient(prev, message_container) && self.collapse_messages &&
|
2016-12-02 15:16:33 +01:00
|
|
|
prev.msg.historical === message_container.msg.historical &&
|
|
|
|
same_day(prev, message_container)) {
|
2014-03-17 19:38:35 +01:00
|
|
|
add_message_container_to_group(message_container);
|
2013-08-16 17:10:22 +02:00
|
|
|
} else {
|
2013-08-23 00:14:11 +02:00
|
|
|
finish_group();
|
2014-02-05 16:55:24 +01:00
|
|
|
current_group = start_group();
|
2014-03-17 19:38:35 +01:00
|
|
|
add_message_container_to_group(message_container);
|
2013-08-16 17:10:22 +02:00
|
|
|
|
2014-03-17 19:38:35 +01:00
|
|
|
message_container.include_recipient = true;
|
|
|
|
message_container.subscribed = false;
|
|
|
|
message_container.unsubscribed = false;
|
2014-02-05 16:55:24 +01:00
|
|
|
|
|
|
|
// 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) {
|
2014-03-17 19:38:35 +01:00
|
|
|
self.add_subscription_marker(current_group, prev, message_container);
|
2013-08-16 17:10:22 +02:00
|
|
|
}
|
2013-10-03 23:05:46 +02:00
|
|
|
|
2014-03-17 19:38:35 +01:00
|
|
|
if (message_container.msg.stream) {
|
2016-12-02 15:16:33 +01:00
|
|
|
message_container.stream_url =
|
|
|
|
narrow.by_stream_uri(message_container.msg.stream);
|
|
|
|
message_container.topic_url =
|
|
|
|
narrow.by_stream_subject_uri(message_container.msg.stream,
|
|
|
|
message_container.msg.subject);
|
2013-10-03 23:05:46 +02:00
|
|
|
} else {
|
2016-12-02 15:16:33 +01:00
|
|
|
message_container.pm_with_url =
|
2017-02-06 20:48:01 +01:00
|
|
|
message_container.msg.pm_with_url;
|
2013-10-03 23:05:46 +02:00
|
|
|
}
|
2013-08-16 17:10:22 +02:00
|
|
|
}
|
|
|
|
|
2014-03-17 19:38:35 +01:00
|
|
|
add_display_time(current_group, message_container, prev);
|
2014-02-05 16:55:24 +01:00
|
|
|
|
2014-03-17 19:38:35 +01:00
|
|
|
message_container.include_sender = true;
|
|
|
|
if (!message_container.include_recipient &&
|
2013-11-08 02:00:12 +01:00
|
|
|
!prev.status_message &&
|
2014-03-17 19:38:35 +01:00
|
|
|
same_sender(prev, message_container)) {
|
|
|
|
message_container.include_sender = false;
|
2013-08-16 17:10:22 +02:00
|
|
|
}
|
|
|
|
|
2017-01-11 16:45:06 +01:00
|
|
|
message_container.sender_is_bot = people.sender_is_bot(message_container.msg);
|
|
|
|
|
2014-03-17 19:38:35 +01:00
|
|
|
self._add_msg_timestring(message_container);
|
2013-08-16 17:10:22 +02:00
|
|
|
|
2017-01-21 20:29:39 +01:00
|
|
|
message_container.small_avatar_url = people.small_avatar_url(message_container.msg);
|
2014-03-17 19:38:35 +01:00
|
|
|
if (message_container.msg.stream !== undefined) {
|
2016-12-02 15:16:33 +01:00
|
|
|
message_container.background_color =
|
|
|
|
stream_data.get_color(message_container.msg.stream);
|
2014-02-24 19:18:38 +01:00
|
|
|
}
|
2013-08-16 17:10:22 +02:00
|
|
|
|
2017-08-25 08:47:55 +02:00
|
|
|
message_container.contains_mention = message_container.msg.mentioned;
|
2016-05-31 14:24:58 +02:00
|
|
|
self._maybe_format_me_message(message_container);
|
2013-11-08 02:00:12 +01:00
|
|
|
|
2014-03-17 19:38:35 +01:00
|
|
|
prev = message_container;
|
2013-08-16 17:10:22 +02:00
|
|
|
});
|
|
|
|
|
2014-02-05 16:55:24 +01:00
|
|
|
finish_group();
|
2013-08-16 17:10:22 +02:00
|
|
|
|
2014-03-06 23:11:03 +01:00
|
|
|
return new_message_groups;
|
|
|
|
},
|
|
|
|
|
2014-03-08 08:59:38 +01:00
|
|
|
join_message_groups: function MessageListView__join_message_groups(first_group, second_group) {
|
|
|
|
// join_message_groups will combine groups if they have the
|
|
|
|
// same_recipient on the same_day and the view supports collapsing
|
|
|
|
// otherwise it may add a subscription_marker if required.
|
|
|
|
// It returns true if the two groups were joined in to one and
|
|
|
|
// the second_group should be ignored.
|
|
|
|
if (first_group === undefined || second_group === undefined) {
|
|
|
|
return false;
|
|
|
|
}
|
2014-03-17 19:38:35 +01:00
|
|
|
var last_msg_container = _.last(first_group.message_containers);
|
|
|
|
var first_msg_container = _.first(second_group.message_containers);
|
2014-03-08 08:59:38 +01:00
|
|
|
|
|
|
|
// Join two groups into one.
|
2016-12-02 15:16:33 +01:00
|
|
|
if (this.collapse_messages && same_recipient(last_msg_container, first_msg_container) &&
|
|
|
|
same_day(last_msg_container, first_msg_container) &&
|
|
|
|
last_msg_container.msg.historical === first_msg_container.msg.historical) {
|
|
|
|
if (!last_msg_container.status_message && !first_msg_container.msg.is_me_message
|
|
|
|
&& same_sender(last_msg_container, first_msg_container)) {
|
2014-03-17 19:38:35 +01:00
|
|
|
first_msg_container.include_sender = false;
|
2014-03-08 08:59:38 +01:00
|
|
|
}
|
2014-03-17 19:38:35 +01:00
|
|
|
if (same_sender(last_msg_container, first_msg_container)) {
|
|
|
|
last_msg_container.next_is_same_sender = true;
|
2014-03-08 08:59:38 +01:00
|
|
|
}
|
2016-12-02 15:16:33 +01:00
|
|
|
first_group.message_containers =
|
|
|
|
first_group.message_containers.concat(second_group.message_containers);
|
2014-03-08 08:59:38 +01:00
|
|
|
return true;
|
|
|
|
// Add a subscription marker
|
2016-12-02 15:16:33 +01:00
|
|
|
} else if (this.list !== home_msg_list &&
|
|
|
|
last_msg_container.msg.historical !== first_msg_container.msg.historical) {
|
2017-06-06 01:43:26 +02:00
|
|
|
second_group.bookend_top = true;
|
|
|
|
this.add_subscription_marker(second_group, last_msg_container, first_msg_container);
|
2014-03-08 08:59:38 +01:00
|
|
|
}
|
|
|
|
return false;
|
|
|
|
},
|
|
|
|
|
2016-12-02 15:16:33 +01:00
|
|
|
merge_message_groups: function MessageListView__merge_message_groups(new_message_groups,
|
|
|
|
where) {
|
2014-03-08 08:59:38 +01:00
|
|
|
// merge_message_groups takes a list of new messages groups to add to
|
|
|
|
// this._message_groups and a location where to merge them currently
|
|
|
|
// top or bottom. It returns an object of changes which needed to be
|
|
|
|
// rendered in to the page. The types of actions are append_group,
|
|
|
|
// prepend_group, rerender_group, append_message.
|
|
|
|
//
|
|
|
|
// append_groups are groups to add to the top of the rendered DOM
|
|
|
|
// prepend_groups are group to add to the bottom of the rendered DOM
|
|
|
|
// rerender_groups are group that should be updated in place in the DOM
|
|
|
|
// append_messages are messages which should be added to the last group in the DOM
|
|
|
|
// rerender_messages are messages which should be updated in place in the DOM
|
|
|
|
|
|
|
|
var message_actions = {
|
|
|
|
append_groups: [],
|
|
|
|
prepend_groups: [],
|
|
|
|
rerender_groups: [],
|
|
|
|
append_messages: [],
|
2017-01-12 00:17:43 +01:00
|
|
|
rerender_messages: [],
|
2014-03-08 08:59:38 +01:00
|
|
|
};
|
2016-12-02 17:09:31 +01:00
|
|
|
var first_group;
|
|
|
|
var second_group;
|
2014-03-08 08:59:38 +01:00
|
|
|
|
|
|
|
if (where === 'top') {
|
|
|
|
first_group = _.last(new_message_groups);
|
|
|
|
second_group = _.first(this._message_groups);
|
|
|
|
if (this.join_message_groups(first_group, second_group)) {
|
|
|
|
// join_message_groups moved the old message to the end of the
|
|
|
|
// new group. We need to replace the old rendered message
|
|
|
|
// group. So we will reuse its ID.
|
|
|
|
|
|
|
|
first_group.message_group_id = second_group.message_group_id;
|
|
|
|
message_actions.rerender_groups.push(first_group);
|
|
|
|
|
|
|
|
// Swap the new group in
|
|
|
|
this._message_groups.shift();
|
|
|
|
this._message_groups.unshift(first_group);
|
|
|
|
|
|
|
|
new_message_groups = _.initial(new_message_groups);
|
2016-12-02 15:16:33 +01:00
|
|
|
} else if (!same_day(second_group.message_containers[0],
|
|
|
|
first_group.message_containers[0])) {
|
2014-03-30 03:42:48 +02:00
|
|
|
// The groups did not merge, so we need up update the date row for the old group
|
|
|
|
add_display_time(
|
|
|
|
second_group,
|
|
|
|
_.first(second_group.message_containers),
|
|
|
|
_.last(first_group.message_containers)
|
|
|
|
);
|
2016-12-02 15:16:33 +01:00
|
|
|
// We could add an action to update the date row, but for now rerender the group.
|
2014-03-30 03:42:48 +02:00
|
|
|
message_actions.rerender_groups.push(second_group);
|
2014-03-08 08:59:38 +01:00
|
|
|
}
|
|
|
|
message_actions.prepend_groups = new_message_groups;
|
|
|
|
this._message_groups = new_message_groups.concat(this._message_groups);
|
|
|
|
} else {
|
|
|
|
first_group = _.last(this._message_groups);
|
|
|
|
second_group = _.first(new_message_groups);
|
|
|
|
if (this.join_message_groups(first_group, second_group)) {
|
|
|
|
// rerender the last message
|
|
|
|
message_actions.rerender_messages.push(
|
2016-12-02 15:16:33 +01:00
|
|
|
first_group.message_containers[
|
|
|
|
first_group.message_containers.length
|
|
|
|
- second_group.message_containers.length - 1]
|
2014-03-08 08:59:38 +01:00
|
|
|
);
|
2014-03-17 19:38:35 +01:00
|
|
|
message_actions.append_messages = _.first(new_message_groups).message_containers;
|
2014-03-08 08:59:38 +01:00
|
|
|
new_message_groups = _.rest(new_message_groups);
|
|
|
|
} else if (first_group !== undefined && second_group !== undefined) {
|
2014-03-17 19:38:35 +01:00
|
|
|
var last_msg_container = _.last(first_group.message_containers);
|
|
|
|
var first_msg_container = _.first(second_group.message_containers);
|
2017-01-06 01:44:34 +01:00
|
|
|
|
2014-03-17 19:38:35 +01:00
|
|
|
if (same_day(last_msg_container, first_msg_container)) {
|
2014-03-08 08:59:38 +01:00
|
|
|
// Clear the date if it is the same as the last group
|
2017-05-16 23:19:57 +02:00
|
|
|
delete second_group.show_date;
|
|
|
|
delete second_group.show_date_separator;
|
2017-06-05 18:13:02 +02:00
|
|
|
} else {
|
|
|
|
// If we just sent the first message on a new day
|
|
|
|
// in a narrow, make sure we render a date separator.
|
|
|
|
add_display_time(second_group, first_msg_container,
|
|
|
|
last_msg_container);
|
2014-03-08 08:59:38 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
message_actions.append_groups = new_message_groups;
|
|
|
|
this._message_groups = this._message_groups.concat(new_message_groups);
|
|
|
|
}
|
|
|
|
|
|
|
|
return message_actions;
|
|
|
|
},
|
|
|
|
|
2014-03-17 19:38:35 +01:00
|
|
|
_post_process_dom_messages: function MessageListView___post_process_dom_messages(dom_messages) {
|
|
|
|
// _post_process_dom_messages adds applies some extra formating to messages
|
2014-03-08 08:59:38 +01:00
|
|
|
// and stores them in self._rows and sends an event that the message is
|
2014-03-17 19:38:35 +01:00
|
|
|
// complete. _post_process_dom_messages should be a list of DOM nodes not
|
2014-03-08 08:59:38 +01:00
|
|
|
// jQuery objects.
|
|
|
|
|
|
|
|
var self = this;
|
2014-03-17 19:38:35 +01:00
|
|
|
_.each(dom_messages, function (dom_message) {
|
|
|
|
if (!_.isElement(dom_message)) {
|
|
|
|
blueslip.warn('Only DOM nodes can be passed to _post_process_messages');
|
2014-03-08 08:59:38 +01:00
|
|
|
}
|
2014-03-17 19:38:35 +01:00
|
|
|
var row = $(dom_message);
|
2014-03-08 08:59:38 +01:00
|
|
|
|
|
|
|
// Save DOM elements by id into self._rows for O(1) lookup
|
|
|
|
if (row.hasClass('message_row')) {
|
2014-03-17 19:38:35 +01:00
|
|
|
self._rows[row.attr('zid')] = dom_message;
|
2014-03-08 08:59:38 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
if (row.hasClass('mention')) {
|
|
|
|
row.find('.user-mention').each(function () {
|
2017-01-20 17:43:45 +01:00
|
|
|
// We give special highlights to the mention buttons
|
|
|
|
// that refer to the current user.
|
|
|
|
if (mention_button_refers_to_me(this)) {
|
2014-03-08 08:59:38 +01:00
|
|
|
$(this).addClass('user-mention-me');
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2017-10-23 13:54:20 +02:00
|
|
|
// Display emoji (including realm emoji) as text if
|
|
|
|
// page_params.emoji_alt_code is set
|
|
|
|
if (page_params.emoji_alt_code) {
|
|
|
|
row.find(".emoji").replaceWith(function () {
|
|
|
|
var text = $(this).attr("title");
|
|
|
|
return ":" + text + ":";
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2014-03-08 08:59:38 +01:00
|
|
|
var id = rows.id(row);
|
|
|
|
message_edit.maybe_show_edit(row, id);
|
|
|
|
|
|
|
|
var e = $.Event('message_rendered.zulip', {target: row});
|
|
|
|
try {
|
|
|
|
$(document).trigger(e);
|
|
|
|
} catch (ex) {
|
|
|
|
blueslip.error('Problem with message rendering',
|
|
|
|
{message_id: rows.id($(row))},
|
|
|
|
ex.stack);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
},
|
|
|
|
|
2017-02-02 22:19:39 +01:00
|
|
|
_get_message_template: function MessageListView___get_message_template(message_container) {
|
|
|
|
var msg_reactions = reactions.get_message_reactions(message_container.msg);
|
|
|
|
message_container.msg.message_reactions = msg_reactions;
|
|
|
|
var msg_to_render = _.extend(message_container, {
|
|
|
|
table_name: this.table_name,
|
|
|
|
});
|
|
|
|
return templates.render('single_message', msg_to_render);
|
|
|
|
},
|
|
|
|
|
2014-03-06 23:11:03 +01:00
|
|
|
render: function MessageListView__render(messages, where, messages_are_new) {
|
|
|
|
// This function processes messages into chunks with separators between them,
|
|
|
|
// and templates them to be inserted as table rows into the DOM.
|
|
|
|
|
|
|
|
if (messages.length === 0 || this.table_name === undefined) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
var list = this.list; // for convenience
|
|
|
|
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();
|
2016-12-02 17:09:31 +01:00
|
|
|
var orig_scrolltop_offset;
|
|
|
|
var message_containers;
|
2014-03-06 23:11:03 +01:00
|
|
|
|
|
|
|
var self = this;
|
|
|
|
|
2014-03-14 16:28:54 +01:00
|
|
|
// The messages we are being asked to render are shared with between
|
|
|
|
// all messages lists. To prevent having both list views overwriting
|
|
|
|
// each others data we will make a new message object to add data to
|
|
|
|
// for rendering.
|
2017-06-29 08:33:10 +02:00
|
|
|
message_containers = _.map(messages, function (message) {
|
|
|
|
if (message.starred) {
|
|
|
|
message.starred_status = i18n.t("Unstar");
|
|
|
|
} else {
|
|
|
|
message.starred_status = i18n.t("Star");
|
|
|
|
}
|
|
|
|
|
|
|
|
return {msg: message};
|
|
|
|
});
|
2014-03-14 16:28:54 +01:00
|
|
|
|
2014-03-08 08:59:38 +01:00
|
|
|
function save_scroll_position() {
|
|
|
|
if (orig_scrolltop_offset === undefined && self.selected_row().length > 0) {
|
|
|
|
orig_scrolltop_offset = self.selected_row().offset().top;
|
2014-03-06 23:11:03 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2014-03-08 08:59:38 +01:00
|
|
|
function restore_scroll_position() {
|
|
|
|
if (list === current_msg_list && orig_scrolltop_offset !== undefined) {
|
2017-03-10 23:48:51 +01:00
|
|
|
message_viewport.set_message_offset(orig_scrolltop_offset);
|
2014-03-08 08:59:38 +01:00
|
|
|
list.reselect_selected_id();
|
|
|
|
}
|
2014-03-06 23:11:03 +01:00
|
|
|
}
|
|
|
|
|
2014-03-08 08:59:38 +01:00
|
|
|
// This function processes messages into chunks with separators between them,
|
|
|
|
// and templates them to be inserted as table rows into the DOM.
|
2014-03-06 23:11:03 +01:00
|
|
|
|
2014-03-17 19:38:35 +01:00
|
|
|
if (message_containers.length === 0 || this.table_name === undefined) {
|
2013-08-20 22:05:56 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2014-03-17 19:38:35 +01:00
|
|
|
var new_message_groups = this.build_message_groups(message_containers, this.table_name);
|
2014-03-08 08:59:38 +01:00
|
|
|
var message_actions = this.merge_message_groups(new_message_groups, where);
|
|
|
|
var new_dom_elements = [];
|
2016-12-02 17:09:31 +01:00
|
|
|
var rendered_groups;
|
|
|
|
var dom_messages;
|
|
|
|
var last_message_row;
|
|
|
|
var last_group_row;
|
2014-03-08 08:59:38 +01:00
|
|
|
|
2014-03-17 19:38:35 +01:00
|
|
|
_.each(message_containers, function (message_container) {
|
|
|
|
self.message_containers[message_container.msg.id] = message_container;
|
|
|
|
});
|
2014-03-14 16:28:54 +01:00
|
|
|
|
2014-03-30 03:42:48 +02:00
|
|
|
// Render new message groups on the top
|
|
|
|
if (message_actions.prepend_groups.length > 0) {
|
|
|
|
save_scroll_position();
|
|
|
|
|
|
|
|
rendered_groups = $(templates.render('message_group', {
|
|
|
|
message_groups: message_actions.prepend_groups,
|
|
|
|
use_match_properties: self.list.filter.is_search(),
|
2017-01-12 00:17:43 +01:00
|
|
|
table_name: self.table_name,
|
2014-03-30 03:42:48 +02:00
|
|
|
}));
|
|
|
|
|
|
|
|
dom_messages = rendered_groups.find('.message_row');
|
|
|
|
new_dom_elements = new_dom_elements.concat(rendered_groups);
|
|
|
|
|
|
|
|
self._post_process_dom_messages(dom_messages.get());
|
|
|
|
|
|
|
|
// The date row will be included in the message groups or will be
|
|
|
|
// added in a rerenderd in the group below
|
|
|
|
table.find('.recipient_row').first().prev('.date_row').remove();
|
|
|
|
table.prepend(rendered_groups);
|
|
|
|
condense.condense_and_collapse(dom_messages);
|
|
|
|
}
|
2014-03-14 16:28:54 +01:00
|
|
|
|
2014-03-08 08:59:38 +01:00
|
|
|
// Rerender message groups
|
|
|
|
if (message_actions.rerender_groups.length > 0) {
|
|
|
|
save_scroll_position();
|
|
|
|
|
|
|
|
_.each(message_actions.rerender_groups, function (message_group) {
|
|
|
|
var old_message_group = $('#' + message_group.message_group_id);
|
|
|
|
// Remove the top date_row, we'll re-add it after rendering
|
|
|
|
old_message_group.prev('.date_row').remove();
|
|
|
|
|
|
|
|
rendered_groups = $(templates.render('message_group', {
|
|
|
|
message_groups: [message_group],
|
|
|
|
use_match_properties: self.list.filter.is_search(),
|
2017-01-12 00:17:43 +01:00
|
|
|
table_name: self.table_name,
|
2014-03-08 08:59:38 +01:00
|
|
|
}));
|
|
|
|
|
|
|
|
dom_messages = rendered_groups.find('.message_row');
|
|
|
|
// Not adding to new_dom_elements it is only used for autoscroll
|
|
|
|
|
2014-03-17 19:38:35 +01:00
|
|
|
self._post_process_dom_messages(dom_messages.get());
|
2014-03-08 08:59:38 +01:00
|
|
|
old_message_group.replaceWith(rendered_groups);
|
|
|
|
condense.condense_and_collapse(dom_messages);
|
|
|
|
});
|
2013-08-16 17:10:22 +02:00
|
|
|
}
|
|
|
|
|
2014-03-08 08:59:38 +01:00
|
|
|
// Rerender message rows
|
|
|
|
if (message_actions.rerender_messages.length > 0) {
|
2014-03-17 19:38:35 +01:00
|
|
|
_.each(message_actions.rerender_messages, function (message_container) {
|
|
|
|
var old_row = self.get_row(message_container.msg.id);
|
2017-02-02 22:19:39 +01:00
|
|
|
var row = $(self._get_message_template(message_container));
|
2014-03-17 19:38:35 +01:00
|
|
|
self._post_process_dom_messages(row.get());
|
2014-03-08 08:59:38 +01:00
|
|
|
old_row.replaceWith(row);
|
|
|
|
condense.condense_and_collapse(row);
|
|
|
|
list.reselect_selected_id();
|
|
|
|
});
|
|
|
|
}
|
2014-01-17 20:11:54 +01:00
|
|
|
|
2014-03-08 08:59:38 +01:00
|
|
|
// Insert new messages in to the last message group
|
|
|
|
if (message_actions.append_messages.length > 0) {
|
2014-03-18 18:20:29 +01:00
|
|
|
last_message_row = table.find('.message_row:last').expectOne();
|
2014-03-08 08:59:38 +01:00
|
|
|
last_group_row = rows.get_message_recipient_row(last_message_row);
|
2014-03-17 19:38:35 +01:00
|
|
|
dom_messages = $(_.map(message_actions.append_messages, function (message_container) {
|
2017-02-02 22:19:39 +01:00
|
|
|
return self._get_message_template(message_container);
|
2014-03-18 15:05:20 +01:00
|
|
|
}).join('')).filter('.message_row');
|
2014-02-05 16:55:24 +01:00
|
|
|
|
2014-03-17 19:38:35 +01:00
|
|
|
self._post_process_dom_messages(dom_messages.get());
|
2014-03-08 08:59:38 +01:00
|
|
|
last_group_row.append(dom_messages);
|
2014-02-05 16:55:24 +01:00
|
|
|
|
2017-03-19 22:28:45 +01:00
|
|
|
condense.condense_and_collapse(dom_messages);
|
2014-03-08 08:59:38 +01:00
|
|
|
new_dom_elements = new_dom_elements.concat(dom_messages);
|
2014-02-05 16:55:24 +01:00
|
|
|
}
|
|
|
|
|
2014-03-08 08:59:38 +01:00
|
|
|
// Add new message groups to the end
|
|
|
|
if (message_actions.append_groups.length > 0) {
|
|
|
|
// Remove the trailing bookend; it'll be re-added after we do our rendering
|
|
|
|
self.clear_trailing_bookend();
|
2014-02-05 16:55:24 +01:00
|
|
|
|
2014-03-08 08:59:38 +01:00
|
|
|
rendered_groups = $(templates.render('message_group', {
|
|
|
|
message_groups: message_actions.append_groups,
|
|
|
|
use_match_properties: self.list.filter.is_search(),
|
2017-01-12 00:17:43 +01:00
|
|
|
table_name: self.table_name,
|
2014-03-08 08:59:38 +01:00
|
|
|
}));
|
2014-02-05 16:55:24 +01:00
|
|
|
|
2014-03-08 08:59:38 +01:00
|
|
|
dom_messages = rendered_groups.find('.message_row');
|
|
|
|
new_dom_elements = new_dom_elements.concat(rendered_groups);
|
2014-02-05 16:55:24 +01:00
|
|
|
|
2014-03-17 19:38:35 +01:00
|
|
|
self._post_process_dom_messages(dom_messages.get());
|
2014-02-05 16:55:24 +01:00
|
|
|
table.append(rendered_groups);
|
2014-03-08 08:59:38 +01:00
|
|
|
condense.condense_and_collapse(dom_messages);
|
2013-08-16 17:10:22 +02:00
|
|
|
}
|
|
|
|
|
2014-03-08 08:59:38 +01:00
|
|
|
restore_scroll_position();
|
2013-08-16 17:10:22 +02:00
|
|
|
|
2014-03-08 08:59:38 +01:00
|
|
|
var last_message_group = _.last(self._message_groups);
|
|
|
|
if (last_message_group !== undefined) {
|
2016-12-02 15:16:33 +01:00
|
|
|
list.last_message_historical =
|
|
|
|
_.last(last_message_group.message_containers).msg.historical;
|
2013-08-16 17:10:22 +02:00
|
|
|
}
|
2016-03-22 16:50:09 +01:00
|
|
|
|
2017-04-25 15:25:31 +02:00
|
|
|
var stream_name = narrow_state.stream();
|
2016-03-22 16:50:09 +01:00
|
|
|
if (stream_name !== undefined) {
|
|
|
|
// If user narrows to a stream, doesn't update
|
|
|
|
// trailing bookend if user is subscribed.
|
|
|
|
var sub = stream_data.get_sub(stream_name);
|
2016-09-20 07:09:15 +02:00
|
|
|
if (sub === undefined || !sub.subscribed) {
|
2016-03-22 16:50:09 +01:00
|
|
|
list.update_trailing_bookend();
|
|
|
|
}
|
|
|
|
}
|
2013-08-16 17:10:22 +02:00
|
|
|
|
|
|
|
if (list === current_msg_list) {
|
|
|
|
// Update the fade.
|
|
|
|
|
2014-02-21 21:10:09 +01:00
|
|
|
var get_element = function (message_group) {
|
|
|
|
// We don't have a MessageGroup class, but we can at least hide the messy details
|
2013-08-16 17:10:22 +02:00
|
|
|
// of rows.js from compose_fade. We provide a callback function to be lazy--
|
|
|
|
// compose_fade may not actually need the elements depending on its internal
|
|
|
|
// state.
|
2014-03-17 19:38:35 +01:00
|
|
|
var message_row = self.get_row(message_group.message_containers[0].msg.id);
|
2014-02-21 21:10:09 +01:00
|
|
|
return rows.get_message_recipient_row(message_row);
|
2013-08-16 17:10:22 +02:00
|
|
|
};
|
|
|
|
|
2014-02-21 21:10:09 +01:00
|
|
|
compose_fade.update_rendered_message_groups(new_message_groups, get_element);
|
2013-08-16 17:10:22 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if (list === current_msg_list && messages_are_new) {
|
2014-03-08 08:59:38 +01:00
|
|
|
self._maybe_autoscroll(new_dom_elements, last_message_was_selected);
|
2014-02-05 16:55:24 +01:00
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2013-08-16 17:10:22 +02:00
|
|
|
|
2016-12-02 15:16:33 +01:00
|
|
|
_maybe_autoscroll: function MessageListView__maybe_autoscroll(rendered_elems,
|
|
|
|
last_message_was_selected) {
|
2013-08-16 17:10:22 +02:00
|
|
|
// If we are near the bottom of our feed (the bottom is visible) and can
|
|
|
|
// scroll up without moving the pointer out of the viewport, do so, by
|
|
|
|
// up to the amount taken up by the new message.
|
2013-12-05 22:45:38 +01:00
|
|
|
var new_messages_height = 0;
|
|
|
|
var id_of_last_message_sent_by_us = -1;
|
|
|
|
|
|
|
|
// C++ iterators would have made this less painful
|
2014-02-05 16:55:24 +01:00
|
|
|
_.each(rendered_elems.reverse(), function (elem) {
|
2013-12-05 22:45:38 +01:00
|
|
|
// Sometimes there are non-DOM elements in rendered_elems; only
|
|
|
|
// try to get the heights of actual trs.
|
2014-03-08 08:59:38 +01:00
|
|
|
if (elem.is("div")) {
|
|
|
|
new_messages_height += elem.height();
|
2013-12-05 22:45:38 +01:00
|
|
|
// starting from the last message, ignore message heights that weren't sent by me.
|
2016-06-09 22:47:12 +02:00
|
|
|
if (id_of_last_message_sent_by_us > -1) {
|
2013-12-13 21:46:16 +01:00
|
|
|
return;
|
|
|
|
}
|
2014-03-08 08:59:38 +01:00
|
|
|
var row_id = rows.id(elem);
|
2013-12-13 21:46:16 +01:00
|
|
|
// check for `row_id` NaN in case we're looking at a date row or bookend row
|
|
|
|
if (row_id > -1 &&
|
2017-01-19 20:18:03 +01:00
|
|
|
people.is_current_user(this.get_message(row_id).sender_email)) {
|
2014-03-08 08:59:38 +01:00
|
|
|
id_of_last_message_sent_by_us = rows.id(elem);
|
2013-12-05 22:45:38 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}, this);
|
|
|
|
|
|
|
|
// autoscroll_forever: if we're on the last message, keep us on the last message
|
|
|
|
if (last_message_was_selected && page_params.autoscroll_forever) {
|
2014-03-19 14:38:10 +01:00
|
|
|
this.list.select_id(this.list.last().id, {from_rendering: true});
|
2016-05-25 13:26:57 +02:00
|
|
|
navigate.scroll_to_selected();
|
2013-12-05 22:45:38 +01:00
|
|
|
this.list.reselect_selected_id();
|
|
|
|
return;
|
|
|
|
}
|
2013-08-16 17:10:22 +02:00
|
|
|
|
|
|
|
var selected_row = this.selected_row();
|
|
|
|
var last_visible = rows.last_visible();
|
|
|
|
|
|
|
|
// Make sure we have a selected row and last visible row. (defensive)
|
|
|
|
if (!(selected_row && (selected_row.length > 0) && last_visible)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
var selected_row_offset = selected_row.offset().top;
|
2017-03-10 23:48:51 +01:00
|
|
|
var info = message_viewport.message_viewport_info();
|
2013-08-16 17:10:22 +02:00
|
|
|
var available_space_for_scroll = selected_row_offset - info.visible_top;
|
|
|
|
|
2016-12-02 15:16:33 +01:00
|
|
|
var rows_offset = rows.last_visible().offset().top - this.list.selected_row().offset().top;
|
|
|
|
|
2013-12-05 22:45:38 +01:00
|
|
|
// autoscroll_forever: if we've sent a message, move pointer at least that far.
|
2016-12-02 15:16:33 +01:00
|
|
|
if (page_params.autoscroll_forever && id_of_last_message_sent_by_us > -1 &&
|
2017-03-10 23:48:51 +01:00
|
|
|
rows_offset < (message_viewport.height())) {
|
2013-12-05 22:45:38 +01:00
|
|
|
this.list.select_id(id_of_last_message_sent_by_us, {from_rendering: true});
|
2016-05-25 13:26:57 +02:00
|
|
|
navigate.scroll_to_selected();
|
2013-12-05 22:45:38 +01:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2013-08-16 17:10:22 +02:00
|
|
|
// Don't scroll if we can't move the pointer up.
|
|
|
|
if (available_space_for_scroll <= 0) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (new_messages_height <= 0) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
message_list: Limit potential damage of auto-scrolling.
We've been getting reports for a few months of folks coming back to
their Zulip window after a night's sleep and finding it scrolled to
the bottom, past dozens or hundreds of messages that they haven't
read. Oddly, the pointer is actually still located where it should be
(verifiable by hitting the Up key), but it's too late: everything
below gets marked as read because bottom_whitespace is in view.
There's only a few places in the zulip codebase where we scroll the
page down, and this is the main one of them. My best theory for what
could be happening is that the browser is, in its overnight
power-saving mode, not granting the Zulip window the resources to
actually repaint the early scrolls. This, in turn, would cause
scrolling down to happen that is not limited by the need to keep the
pointer in view.
I don't think that this fully closes the issue; ideally, we'd have a
reproducer and much more precise detection logic for this situation,
but it should mostly resolve the problem with likely no user-facing
visible harm.
2017-10-04 22:38:31 +02:00
|
|
|
if (!activity.has_focus) {
|
|
|
|
// Don't autoscroll if the window hasn't had focus
|
|
|
|
// recently. This in intended to help protect us from
|
|
|
|
// auto-scrolling downwards when the window is in the
|
|
|
|
// background and might be having some functionality
|
|
|
|
// throttled by modern Chrome's aggressive power-saving
|
|
|
|
// features.
|
|
|
|
blueslip.log("Suppressing scrolldown due to inactivity");
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2017-11-07 22:20:48 +01:00
|
|
|
// do not scroll if there are any active popovers.
|
|
|
|
if (popovers.any_active()) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2013-08-16 17:10:22 +02:00
|
|
|
// This next decision is fairly debatable. For a big message that
|
|
|
|
// would push the pointer off the screen, we do a partial autoscroll,
|
|
|
|
// which has the following implications:
|
|
|
|
// a) user sees scrolling (good)
|
|
|
|
// b) user's pointer stays on screen (good)
|
|
|
|
// c) scroll amount isn't really tied to size of new messages (bad)
|
|
|
|
// d) all the bad things about scrolling for users who want messages
|
|
|
|
// to stay on the screen
|
|
|
|
var scroll_amount = new_messages_height;
|
|
|
|
|
|
|
|
if (scroll_amount > available_space_for_scroll) {
|
|
|
|
scroll_amount = available_space_for_scroll;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Let's work our way back to whether the user was already dealing
|
|
|
|
// with messages off the screen, in which case we shouldn't autoscroll.
|
|
|
|
var bottom_last_visible = last_visible.offset().top + last_visible.height();
|
|
|
|
var bottom_old_last_visible = bottom_last_visible - new_messages_height;
|
|
|
|
var bottom_viewport = info.visible_top + info.visible_height;
|
|
|
|
|
|
|
|
// Exit if the user was already past the bottom.
|
|
|
|
if (bottom_old_last_visible > bottom_viewport) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Ok, we are finally ready to actually scroll.
|
2017-03-10 23:48:51 +01:00
|
|
|
message_viewport.system_initiated_animate_scroll(scroll_amount);
|
2013-08-16 17:10:22 +02:00
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
clear_rendering_state: function MessageListView__clear_rendering_state(clear_table) {
|
|
|
|
if (clear_table) {
|
|
|
|
this.clear_table();
|
|
|
|
}
|
|
|
|
this.list.last_message_historical = false;
|
|
|
|
|
|
|
|
this._render_win_start = 0;
|
|
|
|
this._render_win_end = 0;
|
|
|
|
},
|
|
|
|
|
2016-12-02 15:16:33 +01:00
|
|
|
update_render_window: function MessageListView__update_render_window(selected_idx,
|
|
|
|
check_for_changed) {
|
2013-11-07 16:40:57 +01:00
|
|
|
var new_start = Math.max(selected_idx - this._RENDER_WINDOW_SIZE / 2, 0);
|
2013-08-16 17:10:22 +02:00
|
|
|
if (check_for_changed && new_start === this._render_win_start) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
this._render_win_start = new_start;
|
2013-11-07 16:40:57 +01:00
|
|
|
this._render_win_end = Math.min(this._render_win_start + this._RENDER_WINDOW_SIZE,
|
|
|
|
this.list.num_items());
|
2013-08-16 17:10:22 +02:00
|
|
|
return true;
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
maybe_rerender: function MessageListView__maybe_rerender() {
|
|
|
|
if (this.table_name === undefined) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
var selected_idx = this.list.selected_idx();
|
|
|
|
|
|
|
|
// We rerender under the following conditions:
|
2013-11-07 16:40:57 +01:00
|
|
|
// * 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 (! (((selected_idx - this._render_win_start < this._RENDER_THRESHOLD)
|
2013-08-16 17:10:22 +02:00
|
|
|
&& (this._render_win_start !== 0)) ||
|
2013-11-07 16:40:57 +01:00
|
|
|
((this._render_win_end - selected_idx <= this._RENDER_THRESHOLD)
|
2016-12-01 13:30:30 +01:00
|
|
|
&& (this._render_win_end !== this.list.num_items())))) {
|
2013-08-16 17:10:22 +02:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!this.update_render_window(selected_idx, true)) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.rerender_preserving_scrolltop();
|
|
|
|
return true;
|
|
|
|
},
|
|
|
|
|
|
|
|
rerender_preserving_scrolltop: function MessageListView__rerender_preserving_scrolltop() {
|
|
|
|
// scrolltop_offset is the number of pixels between the top of the
|
2015-01-16 00:26:25 +01:00
|
|
|
// viewable window and the selected message
|
2013-08-16 17:10:22 +02:00
|
|
|
var scrolltop_offset;
|
|
|
|
var selected_row = this.selected_row();
|
|
|
|
var selected_in_view = (selected_row.length > 0);
|
|
|
|
if (selected_in_view) {
|
2015-01-16 00:26:25 +01:00
|
|
|
scrolltop_offset = selected_row.offset().top;
|
2013-08-16 17:10:22 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
this.clear_table();
|
2016-04-23 00:56:44 +02:00
|
|
|
this.render(this.list.all_messages().slice(this._render_win_start, this._render_win_end), 'bottom');
|
2013-08-16 17:10:22 +02:00
|
|
|
|
|
|
|
// 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 (selected_in_view) {
|
2014-03-13 20:07:33 +01:00
|
|
|
if (this.selected_row().length === 0 && this.list.selected_id() > -1) {
|
|
|
|
this.list.select_id(this.list.selected_id(), {use_closest: true});
|
|
|
|
}
|
2013-08-16 17:10:22 +02:00
|
|
|
// Must get this.list.selected_row() again since it is now a new DOM element
|
2017-03-10 23:48:51 +01:00
|
|
|
message_viewport.scrollTop(
|
|
|
|
message_viewport.scrollTop() +
|
|
|
|
this.selected_row().offset().top - scrolltop_offset);
|
2013-08-16 17:10:22 +02:00
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2014-03-17 19:38:35 +01:00
|
|
|
_rerender_header: function MessageListView__maybe_rerender_header(message_containers) {
|
2014-03-03 22:00:42 +01:00
|
|
|
// Given a list of messages that are in the **same** message group,
|
|
|
|
// rerender the header / recipient bar of the messages
|
2014-03-17 19:38:35 +01:00
|
|
|
if (message_containers.length === 0) {
|
2014-03-03 22:00:42 +01:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2014-03-17 19:38:35 +01:00
|
|
|
var first_row = this.get_row(message_containers[0].msg.id);
|
2014-03-04 23:09:56 +01:00
|
|
|
|
|
|
|
// We may not have the row if the stream or topic was muted
|
|
|
|
if (first_row.length === 0) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2014-03-03 22:00:42 +01:00
|
|
|
var recipient_row = rows.get_message_recipient_row(first_row);
|
|
|
|
var header = recipient_row.find('.message_header');
|
|
|
|
|
2014-03-17 19:38:35 +01:00
|
|
|
var group = {message_containers: message_containers};
|
|
|
|
populate_group_from_message_container(group, message_containers[0]);
|
2014-03-03 22:00:42 +01:00
|
|
|
|
|
|
|
var rendered_recipient_row = $(templates.render('recipient_row', group));
|
|
|
|
|
|
|
|
header.replaceWith(rendered_recipient_row);
|
|
|
|
},
|
|
|
|
|
2017-04-21 20:27:45 +02:00
|
|
|
_rerender_message: function MessageListView___rerender_message(message_container,
|
|
|
|
message_content_edited) {
|
2014-03-17 19:38:35 +01:00
|
|
|
var row = this.get_row(message_container.msg.id);
|
|
|
|
var was_selected = this.list.selected_message() === message_container.msg;
|
2014-02-11 16:19:42 +01:00
|
|
|
|
|
|
|
// Re-render just this one message
|
2014-03-17 19:38:35 +01:00
|
|
|
this._add_msg_timestring(message_container);
|
2016-05-31 14:24:58 +02:00
|
|
|
this._maybe_format_me_message(message_container);
|
2014-03-13 03:39:11 +01:00
|
|
|
|
2017-10-21 01:36:08 +02:00
|
|
|
// Make sure the right thing happens if the message was edited to mention us.
|
|
|
|
message_container.contains_mention = message_container.msg.mentioned;
|
|
|
|
|
2017-02-02 22:19:39 +01:00
|
|
|
var rendered_msg = $(this._get_message_template(message_container));
|
2017-04-21 20:27:45 +02:00
|
|
|
if (message_content_edited) {
|
|
|
|
rendered_msg.addClass("fade-in-message");
|
|
|
|
}
|
2014-03-17 19:38:35 +01:00
|
|
|
this._post_process_dom_messages(rendered_msg.get());
|
2014-03-14 16:28:54 +01:00
|
|
|
row.replaceWith(rendered_msg);
|
2014-03-13 03:39:11 +01:00
|
|
|
|
2014-02-21 16:24:21 +01:00
|
|
|
if (was_selected) {
|
2014-03-17 19:38:35 +01:00
|
|
|
this.list.select_id(message_container.msg.id);
|
2014-02-21 16:24:21 +01:00
|
|
|
}
|
2014-02-11 16:19:42 +01:00
|
|
|
},
|
|
|
|
|
2017-04-21 20:27:45 +02:00
|
|
|
rerender_messages: function MessageListView__rerender_messages(messages,
|
|
|
|
message_content_edited) {
|
2014-02-11 16:19:42 +01:00
|
|
|
var self = this;
|
2014-03-04 16:38:12 +01:00
|
|
|
|
2014-03-14 16:28:54 +01:00
|
|
|
// Convert messages to list messages
|
2014-03-17 19:38:35 +01:00
|
|
|
var message_containers = _.map(messages, function (message) {
|
|
|
|
return self.message_containers[message.id];
|
2014-03-14 16:28:54 +01:00
|
|
|
});
|
2014-03-18 20:46:32 +01:00
|
|
|
// We may not have the message_container if the stream or topic was muted
|
|
|
|
message_containers = _.reject(message_containers, function (message_container) {
|
|
|
|
return message_container === undefined;
|
|
|
|
});
|
2014-03-04 16:38:12 +01:00
|
|
|
|
2014-03-03 22:00:42 +01:00
|
|
|
var message_groups = [];
|
|
|
|
var current_group = [];
|
2014-03-17 19:38:35 +01:00
|
|
|
_.each(message_containers, function (message_container) {
|
2016-12-02 15:16:33 +01:00
|
|
|
if (current_group.length === 0 ||
|
|
|
|
same_recipient(current_group[current_group.length - 1], message_container)) {
|
2014-03-17 19:38:35 +01:00
|
|
|
current_group.push(message_container);
|
2014-03-03 22:00:42 +01:00
|
|
|
} else {
|
|
|
|
message_groups.push(current_group);
|
|
|
|
current_group = [];
|
|
|
|
}
|
2017-04-21 20:27:45 +02:00
|
|
|
self._rerender_message(message_container, message_content_edited);
|
2014-02-11 16:19:42 +01:00
|
|
|
});
|
2014-03-03 22:00:42 +01:00
|
|
|
if (current_group.length !== 0) {
|
|
|
|
message_groups.push(current_group);
|
|
|
|
}
|
|
|
|
_.each(message_groups, function (messages_in_group) {
|
2017-04-21 20:27:45 +02:00
|
|
|
self._rerender_header(messages_in_group, message_content_edited);
|
2014-03-03 22:00:42 +01:00
|
|
|
});
|
2014-02-11 16:19:42 +01:00
|
|
|
},
|
|
|
|
|
2013-08-16 17:10:22 +02:00
|
|
|
append: function MessageListView__append(messages, messages_are_new) {
|
2013-11-07 16:40:57 +01:00
|
|
|
var cur_window_size = this._render_win_end - this._render_win_start;
|
2013-08-16 17:10:22 +02:00
|
|
|
if (cur_window_size < this._RENDER_WINDOW_SIZE) {
|
|
|
|
var slice_to_render = messages.slice(0, this._RENDER_WINDOW_SIZE - cur_window_size);
|
|
|
|
this.render(slice_to_render, 'bottom', messages_are_new);
|
|
|
|
this._render_win_end += slice_to_render.length;
|
|
|
|
}
|
|
|
|
|
|
|
|
// If the pointer is high on the page such that there is a
|
|
|
|
// lot of empty space below and the render window is full, a
|
2017-01-12 05:27:56 +01:00
|
|
|
// newly received message should trigger a rerender so that
|
2013-08-16 17:10:22 +02:00
|
|
|
// the new message, which will appear in the viewable area,
|
|
|
|
// is rendered.
|
|
|
|
this.maybe_rerender();
|
|
|
|
},
|
|
|
|
|
|
|
|
prepend: function MessageListView__prepend(messages) {
|
|
|
|
this._render_win_start += messages.length;
|
|
|
|
this._render_win_end += messages.length;
|
|
|
|
|
2013-11-07 16:44:17 +01:00
|
|
|
var cur_window_size = this._render_win_end - this._render_win_start;
|
|
|
|
if (cur_window_size < this._RENDER_WINDOW_SIZE) {
|
2014-02-05 16:55:24 +01:00
|
|
|
var msgs_to_render_count = this._RENDER_WINDOW_SIZE - cur_window_size;
|
|
|
|
var slice_to_render = messages.slice(messages.length - msgs_to_render_count);
|
2013-11-07 16:44:17 +01:00
|
|
|
this.render(slice_to_render, 'top', false);
|
|
|
|
this._render_win_start -= slice_to_render.length;
|
|
|
|
}
|
2017-09-29 21:33:12 +02:00
|
|
|
|
|
|
|
// See comment for maybe_rerender call in the append code path
|
|
|
|
this.maybe_rerender();
|
2013-08-16 17:10:22 +02:00
|
|
|
},
|
|
|
|
|
2016-12-02 14:06:06 +01:00
|
|
|
rerender_the_whole_thing: function MessageListView__rerender_the_whole_thing() {
|
2013-08-16 17:10:22 +02:00
|
|
|
// TODO: Figure out if we can unify this with this.list.rerender().
|
|
|
|
this.clear_rendering_state(true);
|
|
|
|
this.update_render_window(this.list.selected_idx(), false);
|
2016-04-23 00:56:44 +02:00
|
|
|
this.render(this.list.all_messages().slice(this._render_win_start, this._render_win_end), 'bottom');
|
2013-08-16 17:10:22 +02:00
|
|
|
},
|
|
|
|
|
|
|
|
clear_table: function MessageListView_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();
|
|
|
|
this._rows = {};
|
2014-03-18 18:20:29 +01:00
|
|
|
this._message_groups = [];
|
2014-03-17 19:38:35 +01:00
|
|
|
this.message_containers = {};
|
2013-08-16 17:10:22 +02:00
|
|
|
},
|
|
|
|
|
|
|
|
get_row: function MessageListView_get_row(id) {
|
|
|
|
return $(this._rows[id]);
|
|
|
|
},
|
|
|
|
|
|
|
|
clear_trailing_bookend: function MessageListView_clear_trailing_bookend() {
|
2014-02-05 16:55:24 +01:00
|
|
|
var trailing_bookend = rows.get_table(this.table_name).find('.trailing_bookend');
|
2013-08-16 17:10:22 +02:00
|
|
|
trailing_bookend.remove();
|
|
|
|
},
|
|
|
|
|
2016-12-02 15:16:33 +01:00
|
|
|
render_trailing_bookend: function MessageListView_render_trailing_bookend(
|
2017-06-21 02:20:33 +02:00
|
|
|
trailing_bookend_content, subscribed, show_button) {
|
2014-02-05 16:55:24 +01:00
|
|
|
var rendered_trailing_bookend = $(templates.render('bookend', {
|
|
|
|
bookend_content: trailing_bookend_content,
|
2017-06-21 02:20:33 +02:00
|
|
|
trailing: show_button,
|
2017-01-12 00:17:43 +01:00
|
|
|
subscribed: subscribed,
|
2013-08-16 17:10:22 +02:00
|
|
|
}));
|
|
|
|
rows.get_table(this.table_name).append(rendered_trailing_bookend);
|
|
|
|
},
|
|
|
|
|
|
|
|
selected_row: function MessageListView_selected_row() {
|
|
|
|
return this.get_row(this.list.selected_id());
|
|
|
|
},
|
|
|
|
|
|
|
|
get_message: function MessageListView_get_message(id) {
|
|
|
|
return this.list.get(id);
|
2013-12-19 17:03:08 +01:00
|
|
|
},
|
|
|
|
|
|
|
|
change_message_id: function MessageListView_change_message_id(old_id, new_id) {
|
|
|
|
if (this._rows[old_id] !== undefined) {
|
|
|
|
var row = this._rows[old_id];
|
|
|
|
delete this._rows[old_id];
|
|
|
|
|
|
|
|
row.setAttribute('zid', new_id);
|
|
|
|
row.setAttribute('id', this.table_name + new_id);
|
2014-01-24 18:01:12 +01:00
|
|
|
$(row).removeClass('local');
|
2013-12-19 17:03:08 +01:00
|
|
|
this._rows[new_id] = row;
|
|
|
|
}
|
2014-03-14 16:28:54 +01:00
|
|
|
|
2014-03-17 19:38:35 +01:00
|
|
|
if (this.message_containers[old_id] !== undefined) {
|
|
|
|
var message_container = this.message_containers[old_id];
|
|
|
|
delete this.message_containers[old_id];
|
|
|
|
this.message_containers[new_id] = message_container;
|
2014-03-14 16:28:54 +01:00
|
|
|
}
|
|
|
|
|
2016-05-31 14:24:58 +02:00
|
|
|
},
|
|
|
|
|
|
|
|
_maybe_format_me_message: function MessageListView__maybe_format_me_message(message_container) {
|
|
|
|
if (message_container.msg.is_me_message) {
|
|
|
|
// Slice the '<p>/me ' off the front, and '</p>' off the end
|
|
|
|
message_container.status_message = message_container.msg.content.slice(4 + 3, -4);
|
|
|
|
message_container.include_sender = true;
|
2016-06-09 23:02:49 +02:00
|
|
|
} else {
|
2016-05-31 14:24:58 +02:00
|
|
|
message_container.status_message = false;
|
|
|
|
}
|
2017-01-12 00:17:43 +01:00
|
|
|
},
|
2013-08-16 17:10:22 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
}());
|
|
|
|
|
|
|
|
if (typeof module !== 'undefined') {
|
|
|
|
module.exports = MessageListView;
|
|
|
|
}
|