2020-02-13 22:34:29 +01:00
|
|
|
const util = require("./util");
|
2020-07-15 01:29:15 +02:00
|
|
|
const rendered_markdown = require("./rendered_markdown");
|
|
|
|
const render_bookend = require("../templates/bookend.hbs");
|
|
|
|
const render_message_group = require("../templates/message_group.hbs");
|
|
|
|
const render_recipient_row = require("../templates/recipient_row.hbs");
|
|
|
|
const render_single_message = require("../templates/single_message.hbs");
|
2019-07-09 21:24:00 +02:00
|
|
|
|
2013-08-16 17:10:22 +02:00
|
|
|
function MessageListView(list, table_name, collapse_messages) {
|
|
|
|
this.list = list;
|
|
|
|
this.collapse_messages = collapse_messages;
|
2020-02-12 06:38:21 +01:00
|
|
|
this._rows = new Map();
|
2020-02-12 06:39:40 +01:00
|
|
|
this.message_containers = new Map();
|
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;
|
|
|
|
}
|
|
|
|
|
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;
|
|
|
|
}
|
2019-11-02 00:06:25 +01:00
|
|
|
const earlier_time = new XDate(earlier_msg.msg.timestamp * 1000);
|
|
|
|
const 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);
|
|
|
|
}
|
|
|
|
|
2019-02-08 20:43:45 +01:00
|
|
|
function render_group_display_date(group, message_container) {
|
2019-11-02 00:06:25 +01:00
|
|
|
const time = new XDate(message_container.msg.timestamp * 1000);
|
|
|
|
const today = new XDate();
|
|
|
|
const date_element = timerender.render_date(time, undefined, today)[0];
|
2019-02-08 20:43:45 +01:00
|
|
|
|
|
|
|
group.date = date_element.outerHTML;
|
|
|
|
}
|
|
|
|
|
2019-02-08 20:30:58 +01:00
|
|
|
function update_group_date_divider(group, message_container, prev) {
|
2019-11-02 00:06:25 +01:00
|
|
|
const time = new XDate(message_container.msg.timestamp * 1000);
|
|
|
|
const today = new XDate();
|
2013-08-16 17:10:22 +02:00
|
|
|
|
|
|
|
if (prev !== undefined) {
|
2019-11-02 00:06:25 +01:00
|
|
|
const prev_time = new XDate(prev.msg.timestamp * 1000);
|
2013-08-16 17:10:22 +02:00
|
|
|
if (time.toDateString() !== prev_time.toDateString()) {
|
2019-02-08 20:11:21 +01:00
|
|
|
// NB: group_date_divider_html is HTML, inserted into the document without escaping.
|
2020-07-15 00:34:28 +02:00
|
|
|
group.group_date_divider_html = timerender.render_date(
|
|
|
|
time,
|
|
|
|
prev_time,
|
|
|
|
today,
|
|
|
|
)[0].outerHTML;
|
2019-02-08 20:24:01 +01:00
|
|
|
group.show_group_date_divider = 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.
|
2019-02-08 20:24:01 +01:00
|
|
|
group.show_group_date_divider = false;
|
2019-02-08 20:11:21 +01:00
|
|
|
group.group_date_divider_html = timerender.render_date(time, undefined, today)[0].outerHTML;
|
2013-08-16 17:10:22 +02:00
|
|
|
}
|
2018-11-12 23:43:37 +01:00
|
|
|
}
|
2013-08-16 17:10:22 +02:00
|
|
|
|
2019-02-08 20:07:25 +01:00
|
|
|
function clear_group_date_divider(group) {
|
2019-02-08 20:24:01 +01:00
|
|
|
group.show_group_date_divider = false;
|
2019-02-08 20:11:21 +01:00
|
|
|
group.group_date_divider_html = undefined;
|
2019-02-08 20:07:25 +01:00
|
|
|
}
|
|
|
|
|
2019-02-08 21:11:55 +01:00
|
|
|
function clear_message_date_divider(msg) {
|
|
|
|
// see update_message_date_divider for how
|
|
|
|
// these get set
|
|
|
|
msg.want_date_divider = false;
|
|
|
|
msg.date_divider_html = undefined;
|
|
|
|
}
|
|
|
|
|
|
|
|
function update_message_date_divider(opts) {
|
2019-11-02 00:06:25 +01:00
|
|
|
const prev_msg_container = opts.prev_msg_container;
|
|
|
|
const curr_msg_container = opts.curr_msg_container;
|
2019-02-08 21:11:55 +01:00
|
|
|
|
|
|
|
if (!prev_msg_container || same_day(curr_msg_container, prev_msg_container)) {
|
|
|
|
clear_message_date_divider(curr_msg_container);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2019-11-02 00:06:25 +01:00
|
|
|
const prev_time = new XDate(prev_msg_container.msg.timestamp * 1000);
|
|
|
|
const curr_time = new XDate(curr_msg_container.msg.timestamp * 1000);
|
|
|
|
const today = new XDate();
|
2019-02-08 21:11:55 +01:00
|
|
|
|
|
|
|
curr_msg_container.want_date_divider = true;
|
2020-07-15 00:34:28 +02:00
|
|
|
curr_msg_container.date_divider_html = timerender.render_date(
|
|
|
|
curr_time,
|
|
|
|
prev_time,
|
|
|
|
today,
|
|
|
|
)[0].outerHTML;
|
2019-02-08 21:11:55 +01:00
|
|
|
}
|
|
|
|
|
2019-02-08 23:49:48 +01:00
|
|
|
function set_timestr(message_container) {
|
2019-11-02 00:06:25 +01:00
|
|
|
const time = new XDate(message_container.msg.timestamp * 1000);
|
2019-02-08 23:49:48 +01:00
|
|
|
message_container.timestr = timerender.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) {
|
2018-03-05 19:52:18 +01:00
|
|
|
group.realm_allow_message_editing = page_params.realm_allow_message_editing;
|
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
|
|
|
group.always_visible_topic_edit = false;
|
|
|
|
group.on_hover_topic_edit = false;
|
|
|
|
|
|
|
|
// Messages with no topics should always have an edit icon visible
|
|
|
|
// to encourage updating them. Admins can also edit any topic.
|
2020-02-19 00:04:12 +01:00
|
|
|
if (message.topic === compose.empty_topic_placeholder()) {
|
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
|
|
|
group.always_visible_topic_edit = true;
|
2018-10-09 09:54:57 +02:00
|
|
|
} else if (message_edit.is_topic_editable(message)) {
|
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
|
|
|
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);
|
2020-02-19 00:04:12 +01:00
|
|
|
group.topic = message_container.msg.topic;
|
2018-11-15 16:59:41 +01:00
|
|
|
group.match_topic = util.get_match_topic(message_container.msg);
|
2014-03-17 19:38:35 +01:00
|
|
|
group.stream_url = message_container.stream_url;
|
|
|
|
group.topic_url = message_container.topic_url;
|
2020-06-20 22:06:49 +02:00
|
|
|
const sub = stream_data.get_sub_by_id(message_container.msg.stream_id);
|
2016-11-22 09:20:20 +01:00
|
|
|
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;
|
2020-02-14 13:39:04 +01:00
|
|
|
group.topic_links = message_container.msg.topic_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);
|
2019-02-08 20:43:45 +01:00
|
|
|
render_group_display_date(group, message_container);
|
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,
|
|
|
|
|
2020-07-20 22:18:43 +02:00
|
|
|
_get_msg_timestring(message_container) {
|
2019-04-22 20:13:23 +02:00
|
|
|
let last_edit_timestamp;
|
|
|
|
if (message_container.msg.local_edit_timestamp !== undefined) {
|
|
|
|
last_edit_timestamp = message_container.msg.local_edit_timestamp;
|
|
|
|
} else {
|
|
|
|
last_edit_timestamp = message_container.msg.last_edit_timestamp;
|
|
|
|
}
|
|
|
|
if (last_edit_timestamp !== undefined) {
|
|
|
|
const last_edit_time = new XDate(last_edit_timestamp * 1000);
|
2019-11-02 00:06:25 +01:00
|
|
|
const today = new XDate();
|
2020-07-15 00:34:28 +02:00
|
|
|
return (
|
|
|
|
timerender.render_date(last_edit_time, undefined, today)[0].textContent +
|
|
|
|
" at " +
|
|
|
|
timerender.stringify_time(last_edit_time)
|
|
|
|
);
|
2019-03-13 11:19:45 +01:00
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2020-07-20 22:18:43 +02:00
|
|
|
_add_msg_edited_vars(message_container) {
|
2019-03-15 19:42:24 +01:00
|
|
|
// This adds variables to message_container object which calculate bools for
|
|
|
|
// checking position of "(EDITED)" label as well as the edited timestring
|
|
|
|
// The bools can be defined only when the message is edited
|
|
|
|
// (or when the `last_edit_timestr` is defined). The bools are:
|
|
|
|
// * `edited_in_left_col` -- when label appears in left column.
|
|
|
|
// * `edited_alongside_sender` -- when label appears alongside sender info.
|
|
|
|
// * `edited_status_msg` -- when label appears for a "/me" message.
|
2019-11-02 00:06:25 +01:00
|
|
|
const last_edit_timestr = this._get_msg_timestring(message_container);
|
|
|
|
const include_sender = message_container.include_sender;
|
|
|
|
const status_message = Boolean(message_container.status_message);
|
2019-03-13 11:19:45 +01:00
|
|
|
if (last_edit_timestr !== undefined) {
|
|
|
|
message_container.last_edit_timestr = last_edit_timestr;
|
2019-03-15 19:42:24 +01:00
|
|
|
message_container.edited_in_left_col = !include_sender;
|
|
|
|
message_container.edited_alongside_sender = include_sender && !status_message;
|
|
|
|
message_container.edited_status_msg = include_sender && status_message;
|
2019-04-22 20:13:23 +02:00
|
|
|
} else {
|
|
|
|
delete message_container.last_edit_timestr;
|
|
|
|
message_container.edited_in_left_col = false;
|
|
|
|
message_container.edited_alongside_sender = false;
|
|
|
|
message_container.edited_status_msg = false;
|
2014-03-06 23:11:03 +01:00
|
|
|
}
|
|
|
|
},
|
2013-08-16 17:10:22 +02:00
|
|
|
|
2020-07-20 22:18:43 +02:00
|
|
|
add_subscription_marker(group, last_msg_container, first_msg_container) {
|
2017-12-22 12:37:26 +01:00
|
|
|
if (last_msg_container === undefined) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2019-11-02 00:06:25 +01:00
|
|
|
const last_subscribed = !last_msg_container.msg.historical;
|
|
|
|
const first_subscribed = !first_msg_container.msg.historical;
|
|
|
|
const stream = first_msg_container.msg.stream;
|
2017-12-22 12:37:26 +01:00
|
|
|
|
|
|
|
if (!last_subscribed && first_subscribed) {
|
|
|
|
group.bookend_top = true;
|
|
|
|
group.subscribed = stream;
|
|
|
|
group.bookend_content = this.list.subscribed_bookend_content(stream);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (last_subscribed && !first_subscribed) {
|
2014-03-06 23:11:03 +01:00
|
|
|
group.bookend_top = true;
|
2017-12-22 12:37:26 +01:00
|
|
|
group.unsubscribed = stream;
|
|
|
|
group.bookend_content = this.list.unsubscribed_bookend_content(stream);
|
|
|
|
return;
|
2013-08-16 17:10:22 +02:00
|
|
|
}
|
2014-03-06 23:11:03 +01:00
|
|
|
},
|
2013-08-16 17:10:22 +02:00
|
|
|
|
2020-07-20 22:18:43 +02:00
|
|
|
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: [],
|
2020-07-15 01:29:15 +02:00
|
|
|
message_group_id: _.uniqueId("message_group_"),
|
2014-03-08 08:59:38 +01:00
|
|
|
};
|
2014-02-05 16:55:24 +01:00
|
|
|
}
|
|
|
|
|
2019-11-02 00:06:25 +01:00
|
|
|
const self = this;
|
|
|
|
let current_group = start_group();
|
|
|
|
const new_message_groups = [];
|
|
|
|
let 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) {
|
2020-07-15 00:34:28 +02: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);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
js: Automatically convert _.each to for…of.
This commit was automatically generated by the following script,
followed by lint --fix and a few small manual lint-related cleanups.
import * as babelParser from "recast/parsers/babel";
import * as recast from "recast";
import * as tsParser from "recast/parsers/typescript";
import { builders as b, namedTypes as n } from "ast-types";
import { Context } from "ast-types/lib/path-visitor";
import K from "ast-types/gen/kinds";
import { NodePath } from "ast-types/lib/node-path";
import assert from "assert";
import fs from "fs";
import path from "path";
import process from "process";
const checkExpression = (node: n.Node): node is K.ExpressionKind =>
n.Expression.check(node);
const checkStatement = (node: n.Node): node is K.StatementKind =>
n.Statement.check(node);
for (const file of process.argv.slice(2)) {
console.log("Parsing", file);
const ast = recast.parse(fs.readFileSync(file, { encoding: "utf8" }), {
parser: path.extname(file) === ".ts" ? tsParser : babelParser,
});
let changed = false;
let inLoop = false;
let replaceReturn = false;
const visitLoop = (...args: string[]) =>
function(this: Context, path: NodePath) {
for (const arg of args) {
this.visit(path.get(arg));
}
const old = { inLoop };
inLoop = true;
this.visit(path.get("body"));
inLoop = old.inLoop;
return false;
};
recast.visit(ast, {
visitDoWhileStatement: visitLoop("test"),
visitExpressionStatement(path) {
const { expression, comments } = path.node;
let valueOnly;
if (
n.CallExpression.check(expression) &&
n.MemberExpression.check(expression.callee) &&
!expression.callee.computed &&
n.Identifier.check(expression.callee.object) &&
expression.callee.object.name === "_" &&
n.Identifier.check(expression.callee.property) &&
["each", "forEach"].includes(expression.callee.property.name) &&
[2, 3].includes(expression.arguments.length) &&
checkExpression(expression.arguments[0]) &&
(n.FunctionExpression.check(expression.arguments[1]) ||
n.ArrowFunctionExpression.check(expression.arguments[1])) &&
[1, 2].includes(expression.arguments[1].params.length) &&
n.Identifier.check(expression.arguments[1].params[0]) &&
((valueOnly = expression.arguments[1].params[1] === undefined) ||
n.Identifier.check(expression.arguments[1].params[1])) &&
(expression.arguments[2] === undefined ||
n.ThisExpression.check(expression.arguments[2]))
) {
const old = { inLoop, replaceReturn };
inLoop = false;
replaceReturn = true;
this.visit(
path
.get("expression")
.get("arguments")
.get(1)
.get("body")
);
inLoop = old.inLoop;
replaceReturn = old.replaceReturn;
const [right, { body, params }] = expression.arguments;
const loop = b.forOfStatement(
b.variableDeclaration("let", [
b.variableDeclarator(
valueOnly ? params[0] : b.arrayPattern([params[1], params[0]])
),
]),
valueOnly
? right
: b.callExpression(
b.memberExpression(right, b.identifier("entries")),
[]
),
checkStatement(body) ? body : b.expressionStatement(body)
);
loop.comments = comments;
path.replace(loop);
changed = true;
}
this.traverse(path);
},
visitForStatement: visitLoop("init", "test", "update"),
visitForInStatement: visitLoop("left", "right"),
visitForOfStatement: visitLoop("left", "right"),
visitFunction(path) {
this.visit(path.get("params"));
const old = { replaceReturn };
replaceReturn = false;
this.visit(path.get("body"));
replaceReturn = old.replaceReturn;
return false;
},
visitReturnStatement(path) {
if (replaceReturn) {
assert(!inLoop); // could use labeled continue if this ever fires
const { argument, comments } = path.node;
if (argument === null) {
const s = b.continueStatement();
s.comments = comments;
path.replace(s);
} else {
const s = b.expressionStatement(argument);
s.comments = comments;
path.replace(s, b.continueStatement());
}
return false;
}
this.traverse(path);
},
visitWhileStatement: visitLoop("test"),
});
if (changed) {
console.log("Writing", file);
fs.writeFileSync(file, recast.print(ast).code, { encoding: "utf8" });
}
}
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-02-06 06:19:47 +01:00
|
|
|
for (const message_container of message_containers) {
|
2019-11-02 00:06:25 +01:00
|
|
|
const message_reactions = reactions.get_message_reactions(message_container.msg);
|
2016-12-02 13:23:23 +01:00
|
|
|
message_container.msg.message_reactions = message_reactions;
|
2014-03-17 19:38:35 +01:00
|
|
|
message_container.include_recipient = false;
|
2020-07-16 23:29:01 +02:00
|
|
|
message_container.include_footer = false;
|
2013-08-16 17:10:22 +02:00
|
|
|
|
2020-07-15 00:34:28 +02:00
|
|
|
if (
|
|
|
|
same_recipient(prev, message_container) &&
|
|
|
|
self.collapse_messages &&
|
|
|
|
prev.msg.historical === message_container.msg.historical
|
|
|
|
) {
|
2014-03-17 19:38:35 +01:00
|
|
|
add_message_container_to_group(message_container);
|
2019-02-08 21:11:55 +01:00
|
|
|
update_message_date_divider({
|
|
|
|
prev_msg_container: prev,
|
|
|
|
curr_msg_container: 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
|
|
|
|
2019-02-08 23:54:40 +01:00
|
|
|
update_group_date_divider(current_group, message_container, prev);
|
2019-02-08 21:11:55 +01:00
|
|
|
clear_message_date_divider(message_container);
|
2019-02-08 23:54:40 +01: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) {
|
2020-07-15 00:34:28 +02:00
|
|
|
message_container.stream_url = hash_util.by_stream_uri(
|
|
|
|
message_container.msg.stream_id,
|
|
|
|
);
|
|
|
|
message_container.topic_url = hash_util.by_stream_topic_uri(
|
|
|
|
message_container.msg.stream_id,
|
|
|
|
message_container.msg.topic,
|
|
|
|
);
|
2013-10-03 23:05:46 +02:00
|
|
|
} else {
|
2020-07-15 00:34:28 +02:00
|
|
|
message_container.pm_with_url = message_container.msg.pm_with_url;
|
2013-10-03 23:05:46 +02:00
|
|
|
}
|
2013-08-16 17:10:22 +02:00
|
|
|
}
|
|
|
|
|
2019-02-08 23:49:48 +01:00
|
|
|
set_timestr(message_container);
|
2014-02-05 16:55:24 +01:00
|
|
|
|
2014-03-17 19:38:35 +01:00
|
|
|
message_container.include_sender = true;
|
2020-07-15 00:34:28 +02:00
|
|
|
if (
|
|
|
|
!message_container.include_recipient &&
|
2013-11-08 02:00:12 +01:00
|
|
|
!prev.status_message &&
|
2019-02-08 21:11:55 +01:00
|
|
|
same_day(prev, message_container) &&
|
2020-07-15 00:34:28 +02:00
|
|
|
same_sender(prev, message_container)
|
|
|
|
) {
|
2014-03-17 19:38:35 +01:00
|
|
|
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);
|
2018-10-31 18:15:29 +01:00
|
|
|
message_container.sender_is_guest = people.sender_is_guest(message_container.msg);
|
2017-01-11 16:45:06 +01:00
|
|
|
|
2017-01-21 20:29:39 +01:00
|
|
|
message_container.small_avatar_url = people.small_avatar_url(message_container.msg);
|
2019-04-01 19:48:47 +02:00
|
|
|
if (message_container.msg.stream) {
|
2020-07-15 00:34:28 +02: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);
|
2019-03-15 19:42:24 +01:00
|
|
|
// Once all other variables are updated
|
|
|
|
self._add_msg_edited_vars(message_container);
|
2013-11-08 02:00:12 +01:00
|
|
|
|
2014-03-17 19:38:35 +01:00
|
|
|
prev = message_container;
|
js: Automatically convert _.each to for…of.
This commit was automatically generated by the following script,
followed by lint --fix and a few small manual lint-related cleanups.
import * as babelParser from "recast/parsers/babel";
import * as recast from "recast";
import * as tsParser from "recast/parsers/typescript";
import { builders as b, namedTypes as n } from "ast-types";
import { Context } from "ast-types/lib/path-visitor";
import K from "ast-types/gen/kinds";
import { NodePath } from "ast-types/lib/node-path";
import assert from "assert";
import fs from "fs";
import path from "path";
import process from "process";
const checkExpression = (node: n.Node): node is K.ExpressionKind =>
n.Expression.check(node);
const checkStatement = (node: n.Node): node is K.StatementKind =>
n.Statement.check(node);
for (const file of process.argv.slice(2)) {
console.log("Parsing", file);
const ast = recast.parse(fs.readFileSync(file, { encoding: "utf8" }), {
parser: path.extname(file) === ".ts" ? tsParser : babelParser,
});
let changed = false;
let inLoop = false;
let replaceReturn = false;
const visitLoop = (...args: string[]) =>
function(this: Context, path: NodePath) {
for (const arg of args) {
this.visit(path.get(arg));
}
const old = { inLoop };
inLoop = true;
this.visit(path.get("body"));
inLoop = old.inLoop;
return false;
};
recast.visit(ast, {
visitDoWhileStatement: visitLoop("test"),
visitExpressionStatement(path) {
const { expression, comments } = path.node;
let valueOnly;
if (
n.CallExpression.check(expression) &&
n.MemberExpression.check(expression.callee) &&
!expression.callee.computed &&
n.Identifier.check(expression.callee.object) &&
expression.callee.object.name === "_" &&
n.Identifier.check(expression.callee.property) &&
["each", "forEach"].includes(expression.callee.property.name) &&
[2, 3].includes(expression.arguments.length) &&
checkExpression(expression.arguments[0]) &&
(n.FunctionExpression.check(expression.arguments[1]) ||
n.ArrowFunctionExpression.check(expression.arguments[1])) &&
[1, 2].includes(expression.arguments[1].params.length) &&
n.Identifier.check(expression.arguments[1].params[0]) &&
((valueOnly = expression.arguments[1].params[1] === undefined) ||
n.Identifier.check(expression.arguments[1].params[1])) &&
(expression.arguments[2] === undefined ||
n.ThisExpression.check(expression.arguments[2]))
) {
const old = { inLoop, replaceReturn };
inLoop = false;
replaceReturn = true;
this.visit(
path
.get("expression")
.get("arguments")
.get(1)
.get("body")
);
inLoop = old.inLoop;
replaceReturn = old.replaceReturn;
const [right, { body, params }] = expression.arguments;
const loop = b.forOfStatement(
b.variableDeclaration("let", [
b.variableDeclarator(
valueOnly ? params[0] : b.arrayPattern([params[1], params[0]])
),
]),
valueOnly
? right
: b.callExpression(
b.memberExpression(right, b.identifier("entries")),
[]
),
checkStatement(body) ? body : b.expressionStatement(body)
);
loop.comments = comments;
path.replace(loop);
changed = true;
}
this.traverse(path);
},
visitForStatement: visitLoop("init", "test", "update"),
visitForInStatement: visitLoop("left", "right"),
visitForOfStatement: visitLoop("left", "right"),
visitFunction(path) {
this.visit(path.get("params"));
const old = { replaceReturn };
replaceReturn = false;
this.visit(path.get("body"));
replaceReturn = old.replaceReturn;
return false;
},
visitReturnStatement(path) {
if (replaceReturn) {
assert(!inLoop); // could use labeled continue if this ever fires
const { argument, comments } = path.node;
if (argument === null) {
const s = b.continueStatement();
s.comments = comments;
path.replace(s);
} else {
const s = b.expressionStatement(argument);
s.comments = comments;
path.replace(s, b.continueStatement());
}
return false;
}
this.traverse(path);
},
visitWhileStatement: visitLoop("test"),
});
if (changed) {
console.log("Writing", file);
fs.writeFileSync(file, recast.print(ast).code, { encoding: "utf8" });
}
}
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-02-06 06:19:47 +01:00
|
|
|
}
|
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;
|
|
|
|
},
|
|
|
|
|
2020-07-20 22:18:43 +02:00
|
|
|
join_message_groups(first_group, second_group) {
|
2014-03-08 08:59:38 +01:00
|
|
|
// join_message_groups will combine groups if they have the
|
2019-02-08 21:11:55 +01:00
|
|
|
// same_recipient 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.
|
2014-03-08 08:59:38 +01:00
|
|
|
if (first_group === undefined || second_group === undefined) {
|
|
|
|
return false;
|
|
|
|
}
|
2019-11-02 00:06:25 +01:00
|
|
|
const last_msg_container = _.last(first_group.message_containers);
|
|
|
|
const first_msg_container = _.first(second_group.message_containers);
|
2014-03-08 08:59:38 +01:00
|
|
|
|
|
|
|
// Join two groups into one.
|
2020-07-15 00:34:28 +02:00
|
|
|
if (
|
|
|
|
this.collapse_messages &&
|
|
|
|
same_recipient(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_day(last_msg_container, first_msg_container) &&
|
|
|
|
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
|
|
|
}
|
2020-07-15 00:34:28 +02:00
|
|
|
first_group.message_containers = first_group.message_containers.concat(
|
|
|
|
second_group.message_containers,
|
|
|
|
);
|
2014-03-08 08:59:38 +01:00
|
|
|
return true;
|
2020-07-15 00:34:28 +02:00
|
|
|
// Add a subscription marker
|
|
|
|
} 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;
|
|
|
|
},
|
|
|
|
|
2020-07-20 22:18:43 +02:00
|
|
|
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
|
|
|
|
|
2019-11-02 00:06:25 +01:00
|
|
|
const message_actions = {
|
2014-03-08 08:59:38 +01:00
|
|
|
append_groups: [],
|
|
|
|
prepend_groups: [],
|
|
|
|
rerender_groups: [],
|
|
|
|
append_messages: [],
|
2019-02-25 18:59:44 +01:00
|
|
|
rerender_messages_next_same_sender: [],
|
2014-03-08 08:59:38 +01:00
|
|
|
};
|
2019-11-02 00:06:25 +01:00
|
|
|
let first_group;
|
|
|
|
let second_group;
|
|
|
|
let curr_msg_container;
|
|
|
|
let prev_msg_container;
|
2014-03-08 08:59:38 +01:00
|
|
|
|
2020-07-15 01:29:15 +02:00
|
|
|
if (where === "top") {
|
2014-03-08 08:59:38 +01:00
|
|
|
first_group = _.last(new_message_groups);
|
|
|
|
second_group = _.first(this._message_groups);
|
2018-12-25 19:24:47 +01:00
|
|
|
} else {
|
|
|
|
first_group = _.last(this._message_groups);
|
|
|
|
second_group = _.first(new_message_groups);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (first_group) {
|
|
|
|
prev_msg_container = _.last(first_group.message_containers);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (second_group) {
|
|
|
|
curr_msg_container = _.first(second_group.message_containers);
|
|
|
|
}
|
|
|
|
|
2019-11-02 00:06:25 +01:00
|
|
|
const was_joined = this.join_message_groups(first_group, second_group);
|
2019-02-08 21:11:55 +01:00
|
|
|
if (was_joined) {
|
|
|
|
update_message_date_divider({
|
2020-07-20 22:18:43 +02:00
|
|
|
prev_msg_container,
|
|
|
|
curr_msg_container,
|
2019-02-08 21:11:55 +01:00
|
|
|
});
|
|
|
|
} else {
|
|
|
|
clear_message_date_divider(curr_msg_container);
|
|
|
|
}
|
2018-12-25 19:24:47 +01:00
|
|
|
|
2020-07-15 01:29:15 +02:00
|
|
|
if (where === "top") {
|
2018-12-25 19:24:47 +01:00
|
|
|
if (was_joined) {
|
2014-03-08 08:59:38 +01:00
|
|
|
// 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);
|
2020-07-15 00:34:28 +02: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
|
2019-02-08 20:30:58 +01:00
|
|
|
update_group_date_divider(second_group, curr_msg_container, prev_msg_container);
|
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 {
|
2018-12-25 19:24:47 +01:00
|
|
|
if (was_joined) {
|
2014-03-08 08:59:38 +01:00
|
|
|
// rerender the last message
|
2019-02-25 18:59:44 +01:00
|
|
|
message_actions.rerender_messages_next_same_sender.push(prev_msg_container);
|
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) {
|
2018-11-12 23:43:37 +01:00
|
|
|
if (same_day(prev_msg_container, curr_msg_container)) {
|
2019-02-08 20:07:25 +01:00
|
|
|
clear_group_date_divider(second_group);
|
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.
|
2019-02-08 20:30:58 +01:00
|
|
|
update_group_date_divider(second_group, curr_msg_container, prev_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;
|
|
|
|
},
|
|
|
|
|
2020-07-20 22:18:43 +02:00
|
|
|
_put_row(row) {
|
Use jQuery objects in MessageListView._rows.
The values of this dictionary used to be raw DOM elements,
but get_row() wraps them again, so there's not a huge
reason to store them as raw DOM elements internally. It
is slightly easier to reason about the code if everything
stays at the jQuery level.
To preserve the old behavior here, we have to do something
that is kind of ugly, but at least it's explicit now. In
the old code, our cache was DOM elements, and if an id
wasn't in the cache, we would sneakily return $(undefined)
with this code in get_row():
return $(this._rows[id]);
And it turns out that $(undefined) is basically just a
zero-element jQuery object. A lot of our code depends
on this behavior and just works around the zero-element
objects as needed with checks like this:
if (this.selected_row()).length === 0) {
// don't try to get offset
}
For now we just preserve this behavior. We could eventually
be more strict here, or at least have aggressive warnings
on cache misses, but we'd need to retrofit code to be
able to call something like `has_rendered_selection()`
and/or deal with `undefined` as the return value for the case
where the selection hasn't been rendered.
Here is some example code that would cause tracebacks if
we just returned `undefined` for cache misses:
rerender_preserving_scrolltop: function () {
// old_offset is the number of pixels between the top of the
// viewable window and the selected message
var old_offset;
var selected_row = this.selected_row();
var selected_in_view = selected_row.length > 0;
if (selected_in_view) {
old_offset = selected_row.offset().top;
}
return this.rerender_with_target_scrolltop(selected_row,
old_offset);
},
2018-07-12 23:11:59 +02:00
|
|
|
// row is a jQuery object wrapping one message row
|
2020-07-15 01:29:15 +02:00
|
|
|
if (row.hasClass("message_row")) {
|
2020-02-12 06:38:21 +01:00
|
|
|
this._rows.set(rows.id(row), row);
|
2018-07-12 18:29:29 +02:00
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2020-07-20 22:18:43 +02:00
|
|
|
_post_process($message_rows) {
|
2018-07-12 17:41:05 +02:00
|
|
|
// $message_rows wraps one or more message rows
|
|
|
|
|
|
|
|
if ($message_rows.constructor !== jQuery) {
|
|
|
|
// An assertion check that we're calling this properly
|
2020-07-15 01:29:15 +02:00
|
|
|
blueslip.error("programming error--pass in jQuery objects");
|
2018-07-12 17:41:05 +02:00
|
|
|
}
|
2014-03-08 08:59:38 +01:00
|
|
|
|
2019-11-02 00:06:25 +01:00
|
|
|
const self = this;
|
js: Automatically convert _.each to for…of.
This commit was automatically generated by the following script,
followed by lint --fix and a few small manual lint-related cleanups.
import * as babelParser from "recast/parsers/babel";
import * as recast from "recast";
import * as tsParser from "recast/parsers/typescript";
import { builders as b, namedTypes as n } from "ast-types";
import { Context } from "ast-types/lib/path-visitor";
import K from "ast-types/gen/kinds";
import { NodePath } from "ast-types/lib/node-path";
import assert from "assert";
import fs from "fs";
import path from "path";
import process from "process";
const checkExpression = (node: n.Node): node is K.ExpressionKind =>
n.Expression.check(node);
const checkStatement = (node: n.Node): node is K.StatementKind =>
n.Statement.check(node);
for (const file of process.argv.slice(2)) {
console.log("Parsing", file);
const ast = recast.parse(fs.readFileSync(file, { encoding: "utf8" }), {
parser: path.extname(file) === ".ts" ? tsParser : babelParser,
});
let changed = false;
let inLoop = false;
let replaceReturn = false;
const visitLoop = (...args: string[]) =>
function(this: Context, path: NodePath) {
for (const arg of args) {
this.visit(path.get(arg));
}
const old = { inLoop };
inLoop = true;
this.visit(path.get("body"));
inLoop = old.inLoop;
return false;
};
recast.visit(ast, {
visitDoWhileStatement: visitLoop("test"),
visitExpressionStatement(path) {
const { expression, comments } = path.node;
let valueOnly;
if (
n.CallExpression.check(expression) &&
n.MemberExpression.check(expression.callee) &&
!expression.callee.computed &&
n.Identifier.check(expression.callee.object) &&
expression.callee.object.name === "_" &&
n.Identifier.check(expression.callee.property) &&
["each", "forEach"].includes(expression.callee.property.name) &&
[2, 3].includes(expression.arguments.length) &&
checkExpression(expression.arguments[0]) &&
(n.FunctionExpression.check(expression.arguments[1]) ||
n.ArrowFunctionExpression.check(expression.arguments[1])) &&
[1, 2].includes(expression.arguments[1].params.length) &&
n.Identifier.check(expression.arguments[1].params[0]) &&
((valueOnly = expression.arguments[1].params[1] === undefined) ||
n.Identifier.check(expression.arguments[1].params[1])) &&
(expression.arguments[2] === undefined ||
n.ThisExpression.check(expression.arguments[2]))
) {
const old = { inLoop, replaceReturn };
inLoop = false;
replaceReturn = true;
this.visit(
path
.get("expression")
.get("arguments")
.get(1)
.get("body")
);
inLoop = old.inLoop;
replaceReturn = old.replaceReturn;
const [right, { body, params }] = expression.arguments;
const loop = b.forOfStatement(
b.variableDeclaration("let", [
b.variableDeclarator(
valueOnly ? params[0] : b.arrayPattern([params[1], params[0]])
),
]),
valueOnly
? right
: b.callExpression(
b.memberExpression(right, b.identifier("entries")),
[]
),
checkStatement(body) ? body : b.expressionStatement(body)
);
loop.comments = comments;
path.replace(loop);
changed = true;
}
this.traverse(path);
},
visitForStatement: visitLoop("init", "test", "update"),
visitForInStatement: visitLoop("left", "right"),
visitForOfStatement: visitLoop("left", "right"),
visitFunction(path) {
this.visit(path.get("params"));
const old = { replaceReturn };
replaceReturn = false;
this.visit(path.get("body"));
replaceReturn = old.replaceReturn;
return false;
},
visitReturnStatement(path) {
if (replaceReturn) {
assert(!inLoop); // could use labeled continue if this ever fires
const { argument, comments } = path.node;
if (argument === null) {
const s = b.continueStatement();
s.comments = comments;
path.replace(s);
} else {
const s = b.expressionStatement(argument);
s.comments = comments;
path.replace(s, b.continueStatement());
}
return false;
}
this.traverse(path);
},
visitWhileStatement: visitLoop("test"),
});
if (changed) {
console.log("Writing", file);
fs.writeFileSync(file, recast.print(ast).code, { encoding: "utf8" });
}
}
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-02-06 06:19:47 +01:00
|
|
|
|
|
|
|
for (const dom_row of $message_rows) {
|
2019-11-02 00:06:25 +01:00
|
|
|
const row = $(dom_row);
|
Use jQuery objects in MessageListView._rows.
The values of this dictionary used to be raw DOM elements,
but get_row() wraps them again, so there's not a huge
reason to store them as raw DOM elements internally. It
is slightly easier to reason about the code if everything
stays at the jQuery level.
To preserve the old behavior here, we have to do something
that is kind of ugly, but at least it's explicit now. In
the old code, our cache was DOM elements, and if an id
wasn't in the cache, we would sneakily return $(undefined)
with this code in get_row():
return $(this._rows[id]);
And it turns out that $(undefined) is basically just a
zero-element jQuery object. A lot of our code depends
on this behavior and just works around the zero-element
objects as needed with checks like this:
if (this.selected_row()).length === 0) {
// don't try to get offset
}
For now we just preserve this behavior. We could eventually
be more strict here, or at least have aggressive warnings
on cache misses, but we'd need to retrofit code to be
able to call something like `has_rendered_selection()`
and/or deal with `undefined` as the return value for the case
where the selection hasn't been rendered.
Here is some example code that would cause tracebacks if
we just returned `undefined` for cache misses:
rerender_preserving_scrolltop: function () {
// old_offset is the number of pixels between the top of the
// viewable window and the selected message
var old_offset;
var selected_row = this.selected_row();
var selected_in_view = selected_row.length > 0;
if (selected_in_view) {
old_offset = selected_row.offset().top;
}
return this.rerender_with_target_scrolltop(selected_row,
old_offset);
},
2018-07-12 23:11:59 +02:00
|
|
|
self._put_row(row);
|
2018-07-12 18:42:27 +02:00
|
|
|
self._post_process_single_row(row);
|
js: Automatically convert _.each to for…of.
This commit was automatically generated by the following script,
followed by lint --fix and a few small manual lint-related cleanups.
import * as babelParser from "recast/parsers/babel";
import * as recast from "recast";
import * as tsParser from "recast/parsers/typescript";
import { builders as b, namedTypes as n } from "ast-types";
import { Context } from "ast-types/lib/path-visitor";
import K from "ast-types/gen/kinds";
import { NodePath } from "ast-types/lib/node-path";
import assert from "assert";
import fs from "fs";
import path from "path";
import process from "process";
const checkExpression = (node: n.Node): node is K.ExpressionKind =>
n.Expression.check(node);
const checkStatement = (node: n.Node): node is K.StatementKind =>
n.Statement.check(node);
for (const file of process.argv.slice(2)) {
console.log("Parsing", file);
const ast = recast.parse(fs.readFileSync(file, { encoding: "utf8" }), {
parser: path.extname(file) === ".ts" ? tsParser : babelParser,
});
let changed = false;
let inLoop = false;
let replaceReturn = false;
const visitLoop = (...args: string[]) =>
function(this: Context, path: NodePath) {
for (const arg of args) {
this.visit(path.get(arg));
}
const old = { inLoop };
inLoop = true;
this.visit(path.get("body"));
inLoop = old.inLoop;
return false;
};
recast.visit(ast, {
visitDoWhileStatement: visitLoop("test"),
visitExpressionStatement(path) {
const { expression, comments } = path.node;
let valueOnly;
if (
n.CallExpression.check(expression) &&
n.MemberExpression.check(expression.callee) &&
!expression.callee.computed &&
n.Identifier.check(expression.callee.object) &&
expression.callee.object.name === "_" &&
n.Identifier.check(expression.callee.property) &&
["each", "forEach"].includes(expression.callee.property.name) &&
[2, 3].includes(expression.arguments.length) &&
checkExpression(expression.arguments[0]) &&
(n.FunctionExpression.check(expression.arguments[1]) ||
n.ArrowFunctionExpression.check(expression.arguments[1])) &&
[1, 2].includes(expression.arguments[1].params.length) &&
n.Identifier.check(expression.arguments[1].params[0]) &&
((valueOnly = expression.arguments[1].params[1] === undefined) ||
n.Identifier.check(expression.arguments[1].params[1])) &&
(expression.arguments[2] === undefined ||
n.ThisExpression.check(expression.arguments[2]))
) {
const old = { inLoop, replaceReturn };
inLoop = false;
replaceReturn = true;
this.visit(
path
.get("expression")
.get("arguments")
.get(1)
.get("body")
);
inLoop = old.inLoop;
replaceReturn = old.replaceReturn;
const [right, { body, params }] = expression.arguments;
const loop = b.forOfStatement(
b.variableDeclaration("let", [
b.variableDeclarator(
valueOnly ? params[0] : b.arrayPattern([params[1], params[0]])
),
]),
valueOnly
? right
: b.callExpression(
b.memberExpression(right, b.identifier("entries")),
[]
),
checkStatement(body) ? body : b.expressionStatement(body)
);
loop.comments = comments;
path.replace(loop);
changed = true;
}
this.traverse(path);
},
visitForStatement: visitLoop("init", "test", "update"),
visitForInStatement: visitLoop("left", "right"),
visitForOfStatement: visitLoop("left", "right"),
visitFunction(path) {
this.visit(path.get("params"));
const old = { replaceReturn };
replaceReturn = false;
this.visit(path.get("body"));
replaceReturn = old.replaceReturn;
return false;
},
visitReturnStatement(path) {
if (replaceReturn) {
assert(!inLoop); // could use labeled continue if this ever fires
const { argument, comments } = path.node;
if (argument === null) {
const s = b.continueStatement();
s.comments = comments;
path.replace(s);
} else {
const s = b.expressionStatement(argument);
s.comments = comments;
path.replace(s, b.continueStatement());
}
return false;
}
this.traverse(path);
},
visitWhileStatement: visitLoop("test"),
});
if (changed) {
console.log("Writing", file);
fs.writeFileSync(file, recast.print(ast).code, { encoding: "utf8" });
}
}
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-02-06 06:19:47 +01:00
|
|
|
}
|
2018-07-12 18:42:27 +02:00
|
|
|
},
|
2018-07-05 12:17:20 +02:00
|
|
|
|
2020-07-20 22:18:43 +02:00
|
|
|
_post_process_single_row(row) {
|
2018-07-12 18:42:27 +02:00
|
|
|
// For message formatting that requires some post-processing
|
|
|
|
// (and is not possible to handle solely via CSS), this is
|
|
|
|
// where we modify the content. It is a goal to minimize how
|
|
|
|
// much logic is present in this function; wherever possible,
|
|
|
|
// we should implement features with the markdown processor,
|
|
|
|
// HTML and CSS.
|
2014-03-08 08:59:38 +01:00
|
|
|
|
2018-07-12 18:42:27 +02:00
|
|
|
if (row.length !== 1) {
|
2020-07-15 01:29:15 +02:00
|
|
|
blueslip.error("programming error--expected single element");
|
2018-07-12 18:42:27 +02:00
|
|
|
}
|
2014-03-08 08:59:38 +01:00
|
|
|
|
2020-07-15 01:29:15 +02:00
|
|
|
const content = row.find(".message_content");
|
2017-10-23 13:54:20 +02:00
|
|
|
|
2020-05-21 00:53:14 +02:00
|
|
|
rendered_markdown.update_elements(content);
|
2018-07-12 18:42:27 +02:00
|
|
|
|
2019-11-02 00:06:25 +01:00
|
|
|
const id = rows.id(row);
|
2018-07-12 18:42:27 +02:00
|
|
|
message_edit.maybe_show_edit(row, id);
|
|
|
|
|
|
|
|
submessage.process_submessages({
|
2020-07-20 22:18:43 +02:00
|
|
|
row,
|
2018-07-12 18:42:27 +02:00
|
|
|
message_id: id,
|
2014-03-08 08:59:38 +01:00
|
|
|
});
|
|
|
|
},
|
|
|
|
|
2020-07-20 22:18:43 +02:00
|
|
|
_get_message_template(message_container) {
|
2019-11-02 00:06:25 +01:00
|
|
|
const msg_reactions = reactions.get_message_reactions(message_container.msg);
|
2017-02-02 22:19:39 +01:00
|
|
|
message_container.msg.message_reactions = msg_reactions;
|
2020-02-09 04:15:38 +01:00
|
|
|
const msg_to_render = {
|
|
|
|
...message_container,
|
2017-02-02 22:19:39 +01:00
|
|
|
table_name: this.table_name,
|
2020-02-09 04:15:38 +01:00
|
|
|
};
|
2019-07-09 21:24:00 +02:00
|
|
|
return render_single_message(msg_to_render);
|
2017-02-02 22:19:39 +01:00
|
|
|
},
|
|
|
|
|
2020-07-20 22:18:43 +02:00
|
|
|
_render_group(opts) {
|
2019-11-02 00:06:25 +01:00
|
|
|
const message_groups = opts.message_groups;
|
|
|
|
const use_match_properties = opts.use_match_properties;
|
|
|
|
const table_name = opts.table_name;
|
2019-02-23 14:57:25 +01:00
|
|
|
|
2020-07-15 00:34:28 +02:00
|
|
|
return $(
|
|
|
|
render_message_group({
|
2020-07-20 22:18:43 +02:00
|
|
|
message_groups,
|
|
|
|
use_match_properties,
|
|
|
|
table_name,
|
2020-07-15 00:34:28 +02:00
|
|
|
}),
|
|
|
|
);
|
2019-02-23 14:57:25 +01:00
|
|
|
},
|
|
|
|
|
2020-07-20 22:18:43 +02:00
|
|
|
render(messages, where, messages_are_new) {
|
2014-03-06 23:11:03 +01:00
|
|
|
// This function processes messages into chunks with separators between them,
|
|
|
|
// and templates them to be inserted as table rows into the DOM.
|
|
|
|
|
2019-03-02 01:25:13 +01:00
|
|
|
// Store this in a separate variable so it doesn't get
|
|
|
|
// confusingly masked in upcoming loops.
|
2019-11-02 00:06:25 +01:00
|
|
|
const self = this;
|
2019-03-02 01:25:13 +01:00
|
|
|
|
|
|
|
if (messages.length === 0 || self.table_name === undefined) {
|
2014-03-06 23:11:03 +01:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2019-11-02 00:06:25 +01:00
|
|
|
const list = self.list; // for convenience
|
|
|
|
const table_name = self.table_name;
|
|
|
|
const table = rows.get_table(table_name);
|
|
|
|
let orig_scrolltop_offset;
|
2014-03-06 23:11:03 +01:00
|
|
|
|
2019-01-06 17:44:48 +01:00
|
|
|
// If we start with the message feed scrolled up (i.e.
|
|
|
|
// the bottom message is not visible), then we will respect
|
|
|
|
// the user's current position after rendering, rather
|
|
|
|
// than auto-scrolling.
|
2019-11-02 00:06:25 +01:00
|
|
|
const started_scrolled_up = message_viewport.is_scrolled_up();
|
2019-01-06 17:44:48 +01:00
|
|
|
|
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.
|
2020-07-02 01:39:34 +02:00
|
|
|
const message_containers = messages.map((message) => {
|
2017-06-29 08:33:10 +02:00
|
|
|
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) {
|
2018-04-14 01:10:22 +02:00
|
|
|
list.view.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
|
|
|
|
2019-03-02 01:25:13 +01:00
|
|
|
if (message_containers.length === 0 || self.table_name === undefined) {
|
2013-08-20 22:05:56 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2019-11-02 00:06:25 +01:00
|
|
|
const new_message_groups = self.build_message_groups(message_containers, self.table_name);
|
|
|
|
const message_actions = self.merge_message_groups(new_message_groups, where);
|
|
|
|
let new_dom_elements = [];
|
|
|
|
let rendered_groups;
|
|
|
|
let dom_messages;
|
|
|
|
let last_message_row;
|
|
|
|
let last_group_row;
|
2014-03-08 08:59:38 +01:00
|
|
|
|
js: Automatically convert _.each to for…of.
This commit was automatically generated by the following script,
followed by lint --fix and a few small manual lint-related cleanups.
import * as babelParser from "recast/parsers/babel";
import * as recast from "recast";
import * as tsParser from "recast/parsers/typescript";
import { builders as b, namedTypes as n } from "ast-types";
import { Context } from "ast-types/lib/path-visitor";
import K from "ast-types/gen/kinds";
import { NodePath } from "ast-types/lib/node-path";
import assert from "assert";
import fs from "fs";
import path from "path";
import process from "process";
const checkExpression = (node: n.Node): node is K.ExpressionKind =>
n.Expression.check(node);
const checkStatement = (node: n.Node): node is K.StatementKind =>
n.Statement.check(node);
for (const file of process.argv.slice(2)) {
console.log("Parsing", file);
const ast = recast.parse(fs.readFileSync(file, { encoding: "utf8" }), {
parser: path.extname(file) === ".ts" ? tsParser : babelParser,
});
let changed = false;
let inLoop = false;
let replaceReturn = false;
const visitLoop = (...args: string[]) =>
function(this: Context, path: NodePath) {
for (const arg of args) {
this.visit(path.get(arg));
}
const old = { inLoop };
inLoop = true;
this.visit(path.get("body"));
inLoop = old.inLoop;
return false;
};
recast.visit(ast, {
visitDoWhileStatement: visitLoop("test"),
visitExpressionStatement(path) {
const { expression, comments } = path.node;
let valueOnly;
if (
n.CallExpression.check(expression) &&
n.MemberExpression.check(expression.callee) &&
!expression.callee.computed &&
n.Identifier.check(expression.callee.object) &&
expression.callee.object.name === "_" &&
n.Identifier.check(expression.callee.property) &&
["each", "forEach"].includes(expression.callee.property.name) &&
[2, 3].includes(expression.arguments.length) &&
checkExpression(expression.arguments[0]) &&
(n.FunctionExpression.check(expression.arguments[1]) ||
n.ArrowFunctionExpression.check(expression.arguments[1])) &&
[1, 2].includes(expression.arguments[1].params.length) &&
n.Identifier.check(expression.arguments[1].params[0]) &&
((valueOnly = expression.arguments[1].params[1] === undefined) ||
n.Identifier.check(expression.arguments[1].params[1])) &&
(expression.arguments[2] === undefined ||
n.ThisExpression.check(expression.arguments[2]))
) {
const old = { inLoop, replaceReturn };
inLoop = false;
replaceReturn = true;
this.visit(
path
.get("expression")
.get("arguments")
.get(1)
.get("body")
);
inLoop = old.inLoop;
replaceReturn = old.replaceReturn;
const [right, { body, params }] = expression.arguments;
const loop = b.forOfStatement(
b.variableDeclaration("let", [
b.variableDeclarator(
valueOnly ? params[0] : b.arrayPattern([params[1], params[0]])
),
]),
valueOnly
? right
: b.callExpression(
b.memberExpression(right, b.identifier("entries")),
[]
),
checkStatement(body) ? body : b.expressionStatement(body)
);
loop.comments = comments;
path.replace(loop);
changed = true;
}
this.traverse(path);
},
visitForStatement: visitLoop("init", "test", "update"),
visitForInStatement: visitLoop("left", "right"),
visitForOfStatement: visitLoop("left", "right"),
visitFunction(path) {
this.visit(path.get("params"));
const old = { replaceReturn };
replaceReturn = false;
this.visit(path.get("body"));
replaceReturn = old.replaceReturn;
return false;
},
visitReturnStatement(path) {
if (replaceReturn) {
assert(!inLoop); // could use labeled continue if this ever fires
const { argument, comments } = path.node;
if (argument === null) {
const s = b.continueStatement();
s.comments = comments;
path.replace(s);
} else {
const s = b.expressionStatement(argument);
s.comments = comments;
path.replace(s, b.continueStatement());
}
return false;
}
this.traverse(path);
},
visitWhileStatement: visitLoop("test"),
});
if (changed) {
console.log("Writing", file);
fs.writeFileSync(file, recast.print(ast).code, { encoding: "utf8" });
}
}
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-02-06 06:19:47 +01:00
|
|
|
for (const message_container of message_containers) {
|
2020-02-12 06:39:40 +01:00
|
|
|
self.message_containers.set(message_container.msg.id, message_container);
|
js: Automatically convert _.each to for…of.
This commit was automatically generated by the following script,
followed by lint --fix and a few small manual lint-related cleanups.
import * as babelParser from "recast/parsers/babel";
import * as recast from "recast";
import * as tsParser from "recast/parsers/typescript";
import { builders as b, namedTypes as n } from "ast-types";
import { Context } from "ast-types/lib/path-visitor";
import K from "ast-types/gen/kinds";
import { NodePath } from "ast-types/lib/node-path";
import assert from "assert";
import fs from "fs";
import path from "path";
import process from "process";
const checkExpression = (node: n.Node): node is K.ExpressionKind =>
n.Expression.check(node);
const checkStatement = (node: n.Node): node is K.StatementKind =>
n.Statement.check(node);
for (const file of process.argv.slice(2)) {
console.log("Parsing", file);
const ast = recast.parse(fs.readFileSync(file, { encoding: "utf8" }), {
parser: path.extname(file) === ".ts" ? tsParser : babelParser,
});
let changed = false;
let inLoop = false;
let replaceReturn = false;
const visitLoop = (...args: string[]) =>
function(this: Context, path: NodePath) {
for (const arg of args) {
this.visit(path.get(arg));
}
const old = { inLoop };
inLoop = true;
this.visit(path.get("body"));
inLoop = old.inLoop;
return false;
};
recast.visit(ast, {
visitDoWhileStatement: visitLoop("test"),
visitExpressionStatement(path) {
const { expression, comments } = path.node;
let valueOnly;
if (
n.CallExpression.check(expression) &&
n.MemberExpression.check(expression.callee) &&
!expression.callee.computed &&
n.Identifier.check(expression.callee.object) &&
expression.callee.object.name === "_" &&
n.Identifier.check(expression.callee.property) &&
["each", "forEach"].includes(expression.callee.property.name) &&
[2, 3].includes(expression.arguments.length) &&
checkExpression(expression.arguments[0]) &&
(n.FunctionExpression.check(expression.arguments[1]) ||
n.ArrowFunctionExpression.check(expression.arguments[1])) &&
[1, 2].includes(expression.arguments[1].params.length) &&
n.Identifier.check(expression.arguments[1].params[0]) &&
((valueOnly = expression.arguments[1].params[1] === undefined) ||
n.Identifier.check(expression.arguments[1].params[1])) &&
(expression.arguments[2] === undefined ||
n.ThisExpression.check(expression.arguments[2]))
) {
const old = { inLoop, replaceReturn };
inLoop = false;
replaceReturn = true;
this.visit(
path
.get("expression")
.get("arguments")
.get(1)
.get("body")
);
inLoop = old.inLoop;
replaceReturn = old.replaceReturn;
const [right, { body, params }] = expression.arguments;
const loop = b.forOfStatement(
b.variableDeclaration("let", [
b.variableDeclarator(
valueOnly ? params[0] : b.arrayPattern([params[1], params[0]])
),
]),
valueOnly
? right
: b.callExpression(
b.memberExpression(right, b.identifier("entries")),
[]
),
checkStatement(body) ? body : b.expressionStatement(body)
);
loop.comments = comments;
path.replace(loop);
changed = true;
}
this.traverse(path);
},
visitForStatement: visitLoop("init", "test", "update"),
visitForInStatement: visitLoop("left", "right"),
visitForOfStatement: visitLoop("left", "right"),
visitFunction(path) {
this.visit(path.get("params"));
const old = { replaceReturn };
replaceReturn = false;
this.visit(path.get("body"));
replaceReturn = old.replaceReturn;
return false;
},
visitReturnStatement(path) {
if (replaceReturn) {
assert(!inLoop); // could use labeled continue if this ever fires
const { argument, comments } = path.node;
if (argument === null) {
const s = b.continueStatement();
s.comments = comments;
path.replace(s);
} else {
const s = b.expressionStatement(argument);
s.comments = comments;
path.replace(s, b.continueStatement());
}
return false;
}
this.traverse(path);
},
visitWhileStatement: visitLoop("test"),
});
if (changed) {
console.log("Writing", file);
fs.writeFileSync(file, recast.print(ast).code, { encoding: "utf8" });
}
}
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-02-06 06:19:47 +01:00
|
|
|
}
|
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();
|
|
|
|
|
2019-02-23 14:57:25 +01:00
|
|
|
rendered_groups = self._render_group({
|
2014-03-30 03:42:48 +02:00
|
|
|
message_groups: message_actions.prepend_groups,
|
2018-05-04 18:51:09 +02:00
|
|
|
use_match_properties: self.list.is_search(),
|
2017-01-12 00:17:43 +01:00
|
|
|
table_name: self.table_name,
|
2019-02-23 14:57:25 +01:00
|
|
|
});
|
2014-03-30 03:42:48 +02:00
|
|
|
|
2020-07-15 01:29:15 +02:00
|
|
|
dom_messages = rendered_groups.find(".message_row");
|
2014-03-30 03:42:48 +02:00
|
|
|
new_dom_elements = new_dom_elements.concat(rendered_groups);
|
|
|
|
|
2018-07-12 17:41:05 +02:00
|
|
|
self._post_process(dom_messages);
|
2014-03-30 03:42:48 +02:00
|
|
|
|
|
|
|
// The date row will be included in the message groups or will be
|
|
|
|
// added in a rerenderd in the group below
|
2020-07-15 01:29:15 +02:00
|
|
|
table.find(".recipient_row").first().prev(".date_row").remove();
|
2014-03-30 03:42:48 +02:00
|
|
|
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();
|
|
|
|
|
js: Automatically convert _.each to for…of.
This commit was automatically generated by the following script,
followed by lint --fix and a few small manual lint-related cleanups.
import * as babelParser from "recast/parsers/babel";
import * as recast from "recast";
import * as tsParser from "recast/parsers/typescript";
import { builders as b, namedTypes as n } from "ast-types";
import { Context } from "ast-types/lib/path-visitor";
import K from "ast-types/gen/kinds";
import { NodePath } from "ast-types/lib/node-path";
import assert from "assert";
import fs from "fs";
import path from "path";
import process from "process";
const checkExpression = (node: n.Node): node is K.ExpressionKind =>
n.Expression.check(node);
const checkStatement = (node: n.Node): node is K.StatementKind =>
n.Statement.check(node);
for (const file of process.argv.slice(2)) {
console.log("Parsing", file);
const ast = recast.parse(fs.readFileSync(file, { encoding: "utf8" }), {
parser: path.extname(file) === ".ts" ? tsParser : babelParser,
});
let changed = false;
let inLoop = false;
let replaceReturn = false;
const visitLoop = (...args: string[]) =>
function(this: Context, path: NodePath) {
for (const arg of args) {
this.visit(path.get(arg));
}
const old = { inLoop };
inLoop = true;
this.visit(path.get("body"));
inLoop = old.inLoop;
return false;
};
recast.visit(ast, {
visitDoWhileStatement: visitLoop("test"),
visitExpressionStatement(path) {
const { expression, comments } = path.node;
let valueOnly;
if (
n.CallExpression.check(expression) &&
n.MemberExpression.check(expression.callee) &&
!expression.callee.computed &&
n.Identifier.check(expression.callee.object) &&
expression.callee.object.name === "_" &&
n.Identifier.check(expression.callee.property) &&
["each", "forEach"].includes(expression.callee.property.name) &&
[2, 3].includes(expression.arguments.length) &&
checkExpression(expression.arguments[0]) &&
(n.FunctionExpression.check(expression.arguments[1]) ||
n.ArrowFunctionExpression.check(expression.arguments[1])) &&
[1, 2].includes(expression.arguments[1].params.length) &&
n.Identifier.check(expression.arguments[1].params[0]) &&
((valueOnly = expression.arguments[1].params[1] === undefined) ||
n.Identifier.check(expression.arguments[1].params[1])) &&
(expression.arguments[2] === undefined ||
n.ThisExpression.check(expression.arguments[2]))
) {
const old = { inLoop, replaceReturn };
inLoop = false;
replaceReturn = true;
this.visit(
path
.get("expression")
.get("arguments")
.get(1)
.get("body")
);
inLoop = old.inLoop;
replaceReturn = old.replaceReturn;
const [right, { body, params }] = expression.arguments;
const loop = b.forOfStatement(
b.variableDeclaration("let", [
b.variableDeclarator(
valueOnly ? params[0] : b.arrayPattern([params[1], params[0]])
),
]),
valueOnly
? right
: b.callExpression(
b.memberExpression(right, b.identifier("entries")),
[]
),
checkStatement(body) ? body : b.expressionStatement(body)
);
loop.comments = comments;
path.replace(loop);
changed = true;
}
this.traverse(path);
},
visitForStatement: visitLoop("init", "test", "update"),
visitForInStatement: visitLoop("left", "right"),
visitForOfStatement: visitLoop("left", "right"),
visitFunction(path) {
this.visit(path.get("params"));
const old = { replaceReturn };
replaceReturn = false;
this.visit(path.get("body"));
replaceReturn = old.replaceReturn;
return false;
},
visitReturnStatement(path) {
if (replaceReturn) {
assert(!inLoop); // could use labeled continue if this ever fires
const { argument, comments } = path.node;
if (argument === null) {
const s = b.continueStatement();
s.comments = comments;
path.replace(s);
} else {
const s = b.expressionStatement(argument);
s.comments = comments;
path.replace(s, b.continueStatement());
}
return false;
}
this.traverse(path);
},
visitWhileStatement: visitLoop("test"),
});
if (changed) {
console.log("Writing", file);
fs.writeFileSync(file, recast.print(ast).code, { encoding: "utf8" });
}
}
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-02-06 06:19:47 +01:00
|
|
|
for (const message_group of message_actions.rerender_groups) {
|
2020-07-15 01:29:15 +02:00
|
|
|
const old_message_group = $("#" + message_group.message_group_id);
|
2014-03-08 08:59:38 +01:00
|
|
|
// Remove the top date_row, we'll re-add it after rendering
|
2020-07-15 01:29:15 +02:00
|
|
|
old_message_group.prev(".date_row").remove();
|
2014-03-08 08:59:38 +01:00
|
|
|
|
2019-02-23 14:57:25 +01:00
|
|
|
rendered_groups = self._render_group({
|
2014-03-08 08:59:38 +01:00
|
|
|
message_groups: [message_group],
|
2018-05-04 18:51:09 +02:00
|
|
|
use_match_properties: self.list.is_search(),
|
2017-01-12 00:17:43 +01:00
|
|
|
table_name: self.table_name,
|
2019-02-23 14:57:25 +01:00
|
|
|
});
|
2014-03-08 08:59:38 +01:00
|
|
|
|
2020-07-15 01:29:15 +02:00
|
|
|
dom_messages = rendered_groups.find(".message_row");
|
2014-03-08 08:59:38 +01:00
|
|
|
// Not adding to new_dom_elements it is only used for autoscroll
|
|
|
|
|
2018-07-12 17:41:05 +02:00
|
|
|
self._post_process(dom_messages);
|
2014-03-08 08:59:38 +01:00
|
|
|
old_message_group.replaceWith(rendered_groups);
|
|
|
|
condense.condense_and_collapse(dom_messages);
|
js: Automatically convert _.each to for…of.
This commit was automatically generated by the following script,
followed by lint --fix and a few small manual lint-related cleanups.
import * as babelParser from "recast/parsers/babel";
import * as recast from "recast";
import * as tsParser from "recast/parsers/typescript";
import { builders as b, namedTypes as n } from "ast-types";
import { Context } from "ast-types/lib/path-visitor";
import K from "ast-types/gen/kinds";
import { NodePath } from "ast-types/lib/node-path";
import assert from "assert";
import fs from "fs";
import path from "path";
import process from "process";
const checkExpression = (node: n.Node): node is K.ExpressionKind =>
n.Expression.check(node);
const checkStatement = (node: n.Node): node is K.StatementKind =>
n.Statement.check(node);
for (const file of process.argv.slice(2)) {
console.log("Parsing", file);
const ast = recast.parse(fs.readFileSync(file, { encoding: "utf8" }), {
parser: path.extname(file) === ".ts" ? tsParser : babelParser,
});
let changed = false;
let inLoop = false;
let replaceReturn = false;
const visitLoop = (...args: string[]) =>
function(this: Context, path: NodePath) {
for (const arg of args) {
this.visit(path.get(arg));
}
const old = { inLoop };
inLoop = true;
this.visit(path.get("body"));
inLoop = old.inLoop;
return false;
};
recast.visit(ast, {
visitDoWhileStatement: visitLoop("test"),
visitExpressionStatement(path) {
const { expression, comments } = path.node;
let valueOnly;
if (
n.CallExpression.check(expression) &&
n.MemberExpression.check(expression.callee) &&
!expression.callee.computed &&
n.Identifier.check(expression.callee.object) &&
expression.callee.object.name === "_" &&
n.Identifier.check(expression.callee.property) &&
["each", "forEach"].includes(expression.callee.property.name) &&
[2, 3].includes(expression.arguments.length) &&
checkExpression(expression.arguments[0]) &&
(n.FunctionExpression.check(expression.arguments[1]) ||
n.ArrowFunctionExpression.check(expression.arguments[1])) &&
[1, 2].includes(expression.arguments[1].params.length) &&
n.Identifier.check(expression.arguments[1].params[0]) &&
((valueOnly = expression.arguments[1].params[1] === undefined) ||
n.Identifier.check(expression.arguments[1].params[1])) &&
(expression.arguments[2] === undefined ||
n.ThisExpression.check(expression.arguments[2]))
) {
const old = { inLoop, replaceReturn };
inLoop = false;
replaceReturn = true;
this.visit(
path
.get("expression")
.get("arguments")
.get(1)
.get("body")
);
inLoop = old.inLoop;
replaceReturn = old.replaceReturn;
const [right, { body, params }] = expression.arguments;
const loop = b.forOfStatement(
b.variableDeclaration("let", [
b.variableDeclarator(
valueOnly ? params[0] : b.arrayPattern([params[1], params[0]])
),
]),
valueOnly
? right
: b.callExpression(
b.memberExpression(right, b.identifier("entries")),
[]
),
checkStatement(body) ? body : b.expressionStatement(body)
);
loop.comments = comments;
path.replace(loop);
changed = true;
}
this.traverse(path);
},
visitForStatement: visitLoop("init", "test", "update"),
visitForInStatement: visitLoop("left", "right"),
visitForOfStatement: visitLoop("left", "right"),
visitFunction(path) {
this.visit(path.get("params"));
const old = { replaceReturn };
replaceReturn = false;
this.visit(path.get("body"));
replaceReturn = old.replaceReturn;
return false;
},
visitReturnStatement(path) {
if (replaceReturn) {
assert(!inLoop); // could use labeled continue if this ever fires
const { argument, comments } = path.node;
if (argument === null) {
const s = b.continueStatement();
s.comments = comments;
path.replace(s);
} else {
const s = b.expressionStatement(argument);
s.comments = comments;
path.replace(s, b.continueStatement());
}
return false;
}
this.traverse(path);
},
visitWhileStatement: visitLoop("test"),
});
if (changed) {
console.log("Writing", file);
fs.writeFileSync(file, recast.print(ast).code, { encoding: "utf8" });
}
}
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-02-06 06:19:47 +01:00
|
|
|
}
|
2013-08-16 17:10:22 +02:00
|
|
|
}
|
|
|
|
|
2019-02-25 18:59:44 +01:00
|
|
|
// Update the rendering for message rows which used to be last
|
|
|
|
// and now know whether the following message has the same
|
|
|
|
// sender.
|
|
|
|
//
|
|
|
|
// It is likely the case that we can just remove the block
|
|
|
|
// entirely, since it appears the next_is_same_sender CSS
|
|
|
|
// class doesn't do anything.
|
|
|
|
if (message_actions.rerender_messages_next_same_sender.length > 0) {
|
2019-11-02 00:06:25 +01:00
|
|
|
const targets = message_actions.rerender_messages_next_same_sender;
|
js: Automatically convert _.each to for…of.
This commit was automatically generated by the following script,
followed by lint --fix and a few small manual lint-related cleanups.
import * as babelParser from "recast/parsers/babel";
import * as recast from "recast";
import * as tsParser from "recast/parsers/typescript";
import { builders as b, namedTypes as n } from "ast-types";
import { Context } from "ast-types/lib/path-visitor";
import K from "ast-types/gen/kinds";
import { NodePath } from "ast-types/lib/node-path";
import assert from "assert";
import fs from "fs";
import path from "path";
import process from "process";
const checkExpression = (node: n.Node): node is K.ExpressionKind =>
n.Expression.check(node);
const checkStatement = (node: n.Node): node is K.StatementKind =>
n.Statement.check(node);
for (const file of process.argv.slice(2)) {
console.log("Parsing", file);
const ast = recast.parse(fs.readFileSync(file, { encoding: "utf8" }), {
parser: path.extname(file) === ".ts" ? tsParser : babelParser,
});
let changed = false;
let inLoop = false;
let replaceReturn = false;
const visitLoop = (...args: string[]) =>
function(this: Context, path: NodePath) {
for (const arg of args) {
this.visit(path.get(arg));
}
const old = { inLoop };
inLoop = true;
this.visit(path.get("body"));
inLoop = old.inLoop;
return false;
};
recast.visit(ast, {
visitDoWhileStatement: visitLoop("test"),
visitExpressionStatement(path) {
const { expression, comments } = path.node;
let valueOnly;
if (
n.CallExpression.check(expression) &&
n.MemberExpression.check(expression.callee) &&
!expression.callee.computed &&
n.Identifier.check(expression.callee.object) &&
expression.callee.object.name === "_" &&
n.Identifier.check(expression.callee.property) &&
["each", "forEach"].includes(expression.callee.property.name) &&
[2, 3].includes(expression.arguments.length) &&
checkExpression(expression.arguments[0]) &&
(n.FunctionExpression.check(expression.arguments[1]) ||
n.ArrowFunctionExpression.check(expression.arguments[1])) &&
[1, 2].includes(expression.arguments[1].params.length) &&
n.Identifier.check(expression.arguments[1].params[0]) &&
((valueOnly = expression.arguments[1].params[1] === undefined) ||
n.Identifier.check(expression.arguments[1].params[1])) &&
(expression.arguments[2] === undefined ||
n.ThisExpression.check(expression.arguments[2]))
) {
const old = { inLoop, replaceReturn };
inLoop = false;
replaceReturn = true;
this.visit(
path
.get("expression")
.get("arguments")
.get(1)
.get("body")
);
inLoop = old.inLoop;
replaceReturn = old.replaceReturn;
const [right, { body, params }] = expression.arguments;
const loop = b.forOfStatement(
b.variableDeclaration("let", [
b.variableDeclarator(
valueOnly ? params[0] : b.arrayPattern([params[1], params[0]])
),
]),
valueOnly
? right
: b.callExpression(
b.memberExpression(right, b.identifier("entries")),
[]
),
checkStatement(body) ? body : b.expressionStatement(body)
);
loop.comments = comments;
path.replace(loop);
changed = true;
}
this.traverse(path);
},
visitForStatement: visitLoop("init", "test", "update"),
visitForInStatement: visitLoop("left", "right"),
visitForOfStatement: visitLoop("left", "right"),
visitFunction(path) {
this.visit(path.get("params"));
const old = { replaceReturn };
replaceReturn = false;
this.visit(path.get("body"));
replaceReturn = old.replaceReturn;
return false;
},
visitReturnStatement(path) {
if (replaceReturn) {
assert(!inLoop); // could use labeled continue if this ever fires
const { argument, comments } = path.node;
if (argument === null) {
const s = b.continueStatement();
s.comments = comments;
path.replace(s);
} else {
const s = b.expressionStatement(argument);
s.comments = comments;
path.replace(s, b.continueStatement());
}
return false;
}
this.traverse(path);
},
visitWhileStatement: visitLoop("test"),
});
if (changed) {
console.log("Writing", file);
fs.writeFileSync(file, recast.print(ast).code, { encoding: "utf8" });
}
}
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-02-06 06:19:47 +01:00
|
|
|
|
|
|
|
for (const message_container of targets) {
|
2019-11-02 00:06:25 +01:00
|
|
|
const row = self.get_row(message_container.msg.id);
|
2020-07-15 00:34:28 +02:00
|
|
|
$(row)
|
|
|
|
.find("div.messagebox")
|
|
|
|
.toggleClass("next_is_same_sender", message_container.next_is_same_sender);
|
js: Automatically convert _.each to for…of.
This commit was automatically generated by the following script,
followed by lint --fix and a few small manual lint-related cleanups.
import * as babelParser from "recast/parsers/babel";
import * as recast from "recast";
import * as tsParser from "recast/parsers/typescript";
import { builders as b, namedTypes as n } from "ast-types";
import { Context } from "ast-types/lib/path-visitor";
import K from "ast-types/gen/kinds";
import { NodePath } from "ast-types/lib/node-path";
import assert from "assert";
import fs from "fs";
import path from "path";
import process from "process";
const checkExpression = (node: n.Node): node is K.ExpressionKind =>
n.Expression.check(node);
const checkStatement = (node: n.Node): node is K.StatementKind =>
n.Statement.check(node);
for (const file of process.argv.slice(2)) {
console.log("Parsing", file);
const ast = recast.parse(fs.readFileSync(file, { encoding: "utf8" }), {
parser: path.extname(file) === ".ts" ? tsParser : babelParser,
});
let changed = false;
let inLoop = false;
let replaceReturn = false;
const visitLoop = (...args: string[]) =>
function(this: Context, path: NodePath) {
for (const arg of args) {
this.visit(path.get(arg));
}
const old = { inLoop };
inLoop = true;
this.visit(path.get("body"));
inLoop = old.inLoop;
return false;
};
recast.visit(ast, {
visitDoWhileStatement: visitLoop("test"),
visitExpressionStatement(path) {
const { expression, comments } = path.node;
let valueOnly;
if (
n.CallExpression.check(expression) &&
n.MemberExpression.check(expression.callee) &&
!expression.callee.computed &&
n.Identifier.check(expression.callee.object) &&
expression.callee.object.name === "_" &&
n.Identifier.check(expression.callee.property) &&
["each", "forEach"].includes(expression.callee.property.name) &&
[2, 3].includes(expression.arguments.length) &&
checkExpression(expression.arguments[0]) &&
(n.FunctionExpression.check(expression.arguments[1]) ||
n.ArrowFunctionExpression.check(expression.arguments[1])) &&
[1, 2].includes(expression.arguments[1].params.length) &&
n.Identifier.check(expression.arguments[1].params[0]) &&
((valueOnly = expression.arguments[1].params[1] === undefined) ||
n.Identifier.check(expression.arguments[1].params[1])) &&
(expression.arguments[2] === undefined ||
n.ThisExpression.check(expression.arguments[2]))
) {
const old = { inLoop, replaceReturn };
inLoop = false;
replaceReturn = true;
this.visit(
path
.get("expression")
.get("arguments")
.get(1)
.get("body")
);
inLoop = old.inLoop;
replaceReturn = old.replaceReturn;
const [right, { body, params }] = expression.arguments;
const loop = b.forOfStatement(
b.variableDeclaration("let", [
b.variableDeclarator(
valueOnly ? params[0] : b.arrayPattern([params[1], params[0]])
),
]),
valueOnly
? right
: b.callExpression(
b.memberExpression(right, b.identifier("entries")),
[]
),
checkStatement(body) ? body : b.expressionStatement(body)
);
loop.comments = comments;
path.replace(loop);
changed = true;
}
this.traverse(path);
},
visitForStatement: visitLoop("init", "test", "update"),
visitForInStatement: visitLoop("left", "right"),
visitForOfStatement: visitLoop("left", "right"),
visitFunction(path) {
this.visit(path.get("params"));
const old = { replaceReturn };
replaceReturn = false;
this.visit(path.get("body"));
replaceReturn = old.replaceReturn;
return false;
},
visitReturnStatement(path) {
if (replaceReturn) {
assert(!inLoop); // could use labeled continue if this ever fires
const { argument, comments } = path.node;
if (argument === null) {
const s = b.continueStatement();
s.comments = comments;
path.replace(s);
} else {
const s = b.expressionStatement(argument);
s.comments = comments;
path.replace(s, b.continueStatement());
}
return false;
}
this.traverse(path);
},
visitWhileStatement: visitLoop("test"),
});
if (changed) {
console.log("Writing", file);
fs.writeFileSync(file, recast.print(ast).code, { encoding: "utf8" });
}
}
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-02-06 06:19:47 +01:00
|
|
|
}
|
2014-03-08 08:59:38 +01:00
|
|
|
}
|
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) {
|
2020-07-15 01:29:15 +02: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);
|
2020-07-15 00:34:28 +02:00
|
|
|
dom_messages = $(
|
|
|
|
message_actions.append_messages
|
|
|
|
.map((message_container) => self._get_message_template(message_container))
|
|
|
|
.join(""),
|
|
|
|
).filter(".message_row");
|
2014-02-05 16:55:24 +01:00
|
|
|
|
2018-07-12 17:41:05 +02:00
|
|
|
self._post_process(dom_messages);
|
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
|
|
|
|
2019-02-23 14:57:25 +01:00
|
|
|
rendered_groups = self._render_group({
|
2014-03-08 08:59:38 +01:00
|
|
|
message_groups: message_actions.append_groups,
|
2018-05-04 18:51:09 +02:00
|
|
|
use_match_properties: self.list.is_search(),
|
2017-01-12 00:17:43 +01:00
|
|
|
table_name: self.table_name,
|
2019-02-23 14:57:25 +01:00
|
|
|
});
|
2014-02-05 16:55:24 +01:00
|
|
|
|
2020-07-15 01:29:15 +02:00
|
|
|
dom_messages = rendered_groups.find(".message_row");
|
2014-03-08 08:59:38 +01:00
|
|
|
new_dom_elements = new_dom_elements.concat(rendered_groups);
|
2014-02-05 16:55:24 +01:00
|
|
|
|
2018-07-12 17:41:05 +02:00
|
|
|
self._post_process(dom_messages);
|
2018-03-21 23:50:47 +01:00
|
|
|
|
|
|
|
// This next line is a workaround for a weird scrolling
|
|
|
|
// bug on Chrome. Basically, in Chrome 64, we had a
|
|
|
|
// highly reproducible bug where if you hit the "End" key
|
|
|
|
// 5 times in a row in a `near:1` narrow (or any other
|
|
|
|
// narrow with enough content below to try this), the 5th
|
|
|
|
// time (because RENDER_WINDOW_SIZE / batch_size = 4,
|
|
|
|
// i.e. the first time we need to rerender to show the
|
|
|
|
// message "End" jumps to) would trigger an unexpected
|
|
|
|
// scroll, resulting in some chaotic scrolling and
|
|
|
|
// additional fetches (from bottom_whitespace ending up in
|
|
|
|
// the view). During debugging, we found that this adding
|
|
|
|
// this next line seems to prevent the Chrome bug from firing.
|
|
|
|
message_viewport.scrollTop();
|
|
|
|
|
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
|
|
|
|
2019-11-02 00:06:25 +01:00
|
|
|
const last_message_group = _.last(self._message_groups);
|
2014-03-08 08:59:38 +01:00
|
|
|
if (last_message_group !== undefined) {
|
2020-07-15 00:34:28 +02: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
|
|
|
|
2019-11-02 00:06:25 +01:00
|
|
|
const 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.
|
2019-11-02 00:06:25 +01:00
|
|
|
const 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.
|
|
|
|
|
2019-11-02 00:06:25 +01:00
|
|
|
const get_element = function (message_group) {
|
2014-02-21 21:10:09 +01:00
|
|
|
// 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.
|
2019-11-02 00:06:25 +01:00
|
|
|
const 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) {
|
2019-02-06 01:12:14 +01:00
|
|
|
// First, in single-recipient narrows, potentially
|
|
|
|
// auto-scroll to the latest message if it was sent by us.
|
|
|
|
if (narrow_state.narrowed_by_reply()) {
|
2019-11-02 00:06:25 +01:00
|
|
|
const selected_id = list.selected_id();
|
|
|
|
let i;
|
2019-02-06 01:12:14 +01:00
|
|
|
|
|
|
|
// Iterate backwards to find the last message
|
|
|
|
// sent_by_me, stopping at the pointer position.
|
|
|
|
// There's a reasonable argument that this search
|
|
|
|
// should be limited in how far offscreen it's willing
|
|
|
|
// to go.
|
|
|
|
for (i = messages.length - 1; i >= 0; i -= 1) {
|
2019-11-02 00:06:25 +01:00
|
|
|
const id = messages[i].id;
|
2019-02-06 01:12:14 +01:00
|
|
|
if (id <= selected_id) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
if (messages[i].sent_by_me && list.get(id) !== undefined) {
|
|
|
|
// If this is a reply we just sent, advance the pointer to it.
|
|
|
|
list.select_id(messages[i].id, {then_scroll: true, from_scroll: true});
|
|
|
|
return {
|
|
|
|
need_user_to_scroll: false,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-01-06 17:44:48 +01:00
|
|
|
if (started_scrolled_up) {
|
message scrolling: Fix "Scroll down to view" warning.
We recently added a feature to warn users that they
may need to scroll down to view messages that they
just sent, but it was broken due to various complexities
in the rendering code path.
Now we compute it a bit more rigorously.
It requires us to pass some info about rendering up
and down the stack, which is why it's kind of a long
commit, but the bulk of the logic is in these JS files:
* message_list_view.js
* notifications.js
I choose to pass structs around instead of booleans,
because I anticipate we may eventually add more metadata
about rendering to it, plus bools are just kinda brittle.
(The exceptions are that `_maybe_autoscroll`, which
is at the bottom of the stack, just passes back a simple
boolean, and `notify_local_mixes`, also at the bottom
of the stack, just accepts a simple boolean.)
This errs on the side of warning the user, even if the
new message is partially visible.
Fixes #11138
2019-01-07 21:00:03 +01:00
|
|
|
return {
|
|
|
|
need_user_to_scroll: true,
|
|
|
|
};
|
2019-01-06 17:44:48 +01:00
|
|
|
}
|
2019-11-02 00:06:25 +01:00
|
|
|
const new_messages_height = self._new_messages_height(new_dom_elements);
|
|
|
|
const need_user_to_scroll = self._maybe_autoscroll(new_messages_height);
|
message scrolling: Fix "Scroll down to view" warning.
We recently added a feature to warn users that they
may need to scroll down to view messages that they
just sent, but it was broken due to various complexities
in the rendering code path.
Now we compute it a bit more rigorously.
It requires us to pass some info about rendering up
and down the stack, which is why it's kind of a long
commit, but the bulk of the logic is in these JS files:
* message_list_view.js
* notifications.js
I choose to pass structs around instead of booleans,
because I anticipate we may eventually add more metadata
about rendering to it, plus bools are just kinda brittle.
(The exceptions are that `_maybe_autoscroll`, which
is at the bottom of the stack, just passes back a simple
boolean, and `notify_local_mixes`, also at the bottom
of the stack, just accepts a simple boolean.)
This errs on the side of warning the user, even if the
new message is partially visible.
Fixes #11138
2019-01-07 21:00:03 +01:00
|
|
|
|
|
|
|
if (need_user_to_scroll) {
|
|
|
|
return {
|
|
|
|
need_user_to_scroll: true,
|
|
|
|
};
|
|
|
|
}
|
2014-02-05 16:55:24 +01:00
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2020-07-20 22:18:43 +02:00
|
|
|
_new_messages_height(rendered_elems) {
|
2019-11-02 00:06:25 +01:00
|
|
|
let new_messages_height = 0;
|
2013-12-05 22:45:38 +01:00
|
|
|
|
js: Automatically convert _.each to for…of.
This commit was automatically generated by the following script,
followed by lint --fix and a few small manual lint-related cleanups.
import * as babelParser from "recast/parsers/babel";
import * as recast from "recast";
import * as tsParser from "recast/parsers/typescript";
import { builders as b, namedTypes as n } from "ast-types";
import { Context } from "ast-types/lib/path-visitor";
import K from "ast-types/gen/kinds";
import { NodePath } from "ast-types/lib/node-path";
import assert from "assert";
import fs from "fs";
import path from "path";
import process from "process";
const checkExpression = (node: n.Node): node is K.ExpressionKind =>
n.Expression.check(node);
const checkStatement = (node: n.Node): node is K.StatementKind =>
n.Statement.check(node);
for (const file of process.argv.slice(2)) {
console.log("Parsing", file);
const ast = recast.parse(fs.readFileSync(file, { encoding: "utf8" }), {
parser: path.extname(file) === ".ts" ? tsParser : babelParser,
});
let changed = false;
let inLoop = false;
let replaceReturn = false;
const visitLoop = (...args: string[]) =>
function(this: Context, path: NodePath) {
for (const arg of args) {
this.visit(path.get(arg));
}
const old = { inLoop };
inLoop = true;
this.visit(path.get("body"));
inLoop = old.inLoop;
return false;
};
recast.visit(ast, {
visitDoWhileStatement: visitLoop("test"),
visitExpressionStatement(path) {
const { expression, comments } = path.node;
let valueOnly;
if (
n.CallExpression.check(expression) &&
n.MemberExpression.check(expression.callee) &&
!expression.callee.computed &&
n.Identifier.check(expression.callee.object) &&
expression.callee.object.name === "_" &&
n.Identifier.check(expression.callee.property) &&
["each", "forEach"].includes(expression.callee.property.name) &&
[2, 3].includes(expression.arguments.length) &&
checkExpression(expression.arguments[0]) &&
(n.FunctionExpression.check(expression.arguments[1]) ||
n.ArrowFunctionExpression.check(expression.arguments[1])) &&
[1, 2].includes(expression.arguments[1].params.length) &&
n.Identifier.check(expression.arguments[1].params[0]) &&
((valueOnly = expression.arguments[1].params[1] === undefined) ||
n.Identifier.check(expression.arguments[1].params[1])) &&
(expression.arguments[2] === undefined ||
n.ThisExpression.check(expression.arguments[2]))
) {
const old = { inLoop, replaceReturn };
inLoop = false;
replaceReturn = true;
this.visit(
path
.get("expression")
.get("arguments")
.get(1)
.get("body")
);
inLoop = old.inLoop;
replaceReturn = old.replaceReturn;
const [right, { body, params }] = expression.arguments;
const loop = b.forOfStatement(
b.variableDeclaration("let", [
b.variableDeclarator(
valueOnly ? params[0] : b.arrayPattern([params[1], params[0]])
),
]),
valueOnly
? right
: b.callExpression(
b.memberExpression(right, b.identifier("entries")),
[]
),
checkStatement(body) ? body : b.expressionStatement(body)
);
loop.comments = comments;
path.replace(loop);
changed = true;
}
this.traverse(path);
},
visitForStatement: visitLoop("init", "test", "update"),
visitForInStatement: visitLoop("left", "right"),
visitForOfStatement: visitLoop("left", "right"),
visitFunction(path) {
this.visit(path.get("params"));
const old = { replaceReturn };
replaceReturn = false;
this.visit(path.get("body"));
replaceReturn = old.replaceReturn;
return false;
},
visitReturnStatement(path) {
if (replaceReturn) {
assert(!inLoop); // could use labeled continue if this ever fires
const { argument, comments } = path.node;
if (argument === null) {
const s = b.continueStatement();
s.comments = comments;
path.replace(s);
} else {
const s = b.expressionStatement(argument);
s.comments = comments;
path.replace(s, b.continueStatement());
}
return false;
}
this.traverse(path);
},
visitWhileStatement: visitLoop("test"),
});
if (changed) {
console.log("Writing", file);
fs.writeFileSync(file, recast.print(ast).code, { encoding: "utf8" });
}
}
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-02-06 06:19:47 +01:00
|
|
|
for (const elem of rendered_elems.reverse()) {
|
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
|
|
|
}
|
js: Automatically convert _.each to for…of.
This commit was automatically generated by the following script,
followed by lint --fix and a few small manual lint-related cleanups.
import * as babelParser from "recast/parsers/babel";
import * as recast from "recast";
import * as tsParser from "recast/parsers/typescript";
import { builders as b, namedTypes as n } from "ast-types";
import { Context } from "ast-types/lib/path-visitor";
import K from "ast-types/gen/kinds";
import { NodePath } from "ast-types/lib/node-path";
import assert from "assert";
import fs from "fs";
import path from "path";
import process from "process";
const checkExpression = (node: n.Node): node is K.ExpressionKind =>
n.Expression.check(node);
const checkStatement = (node: n.Node): node is K.StatementKind =>
n.Statement.check(node);
for (const file of process.argv.slice(2)) {
console.log("Parsing", file);
const ast = recast.parse(fs.readFileSync(file, { encoding: "utf8" }), {
parser: path.extname(file) === ".ts" ? tsParser : babelParser,
});
let changed = false;
let inLoop = false;
let replaceReturn = false;
const visitLoop = (...args: string[]) =>
function(this: Context, path: NodePath) {
for (const arg of args) {
this.visit(path.get(arg));
}
const old = { inLoop };
inLoop = true;
this.visit(path.get("body"));
inLoop = old.inLoop;
return false;
};
recast.visit(ast, {
visitDoWhileStatement: visitLoop("test"),
visitExpressionStatement(path) {
const { expression, comments } = path.node;
let valueOnly;
if (
n.CallExpression.check(expression) &&
n.MemberExpression.check(expression.callee) &&
!expression.callee.computed &&
n.Identifier.check(expression.callee.object) &&
expression.callee.object.name === "_" &&
n.Identifier.check(expression.callee.property) &&
["each", "forEach"].includes(expression.callee.property.name) &&
[2, 3].includes(expression.arguments.length) &&
checkExpression(expression.arguments[0]) &&
(n.FunctionExpression.check(expression.arguments[1]) ||
n.ArrowFunctionExpression.check(expression.arguments[1])) &&
[1, 2].includes(expression.arguments[1].params.length) &&
n.Identifier.check(expression.arguments[1].params[0]) &&
((valueOnly = expression.arguments[1].params[1] === undefined) ||
n.Identifier.check(expression.arguments[1].params[1])) &&
(expression.arguments[2] === undefined ||
n.ThisExpression.check(expression.arguments[2]))
) {
const old = { inLoop, replaceReturn };
inLoop = false;
replaceReturn = true;
this.visit(
path
.get("expression")
.get("arguments")
.get(1)
.get("body")
);
inLoop = old.inLoop;
replaceReturn = old.replaceReturn;
const [right, { body, params }] = expression.arguments;
const loop = b.forOfStatement(
b.variableDeclaration("let", [
b.variableDeclarator(
valueOnly ? params[0] : b.arrayPattern([params[1], params[0]])
),
]),
valueOnly
? right
: b.callExpression(
b.memberExpression(right, b.identifier("entries")),
[]
),
checkStatement(body) ? body : b.expressionStatement(body)
);
loop.comments = comments;
path.replace(loop);
changed = true;
}
this.traverse(path);
},
visitForStatement: visitLoop("init", "test", "update"),
visitForInStatement: visitLoop("left", "right"),
visitForOfStatement: visitLoop("left", "right"),
visitFunction(path) {
this.visit(path.get("params"));
const old = { replaceReturn };
replaceReturn = false;
this.visit(path.get("body"));
replaceReturn = old.replaceReturn;
return false;
},
visitReturnStatement(path) {
if (replaceReturn) {
assert(!inLoop); // could use labeled continue if this ever fires
const { argument, comments } = path.node;
if (argument === null) {
const s = b.continueStatement();
s.comments = comments;
path.replace(s);
} else {
const s = b.expressionStatement(argument);
s.comments = comments;
path.replace(s, b.continueStatement());
}
return false;
}
this.traverse(path);
},
visitWhileStatement: visitLoop("test"),
});
if (changed) {
console.log("Writing", file);
fs.writeFileSync(file, recast.print(ast).code, { encoding: "utf8" });
}
}
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-02-06 06:19:47 +01:00
|
|
|
}
|
2013-12-05 22:45:38 +01:00
|
|
|
|
2019-01-05 16:25:21 +01:00
|
|
|
return new_messages_height;
|
|
|
|
},
|
|
|
|
|
2020-07-20 22:18:43 +02:00
|
|
|
_scroll_limit(selected_row, viewport_info) {
|
2019-01-05 16:49:23 +01:00
|
|
|
// This scroll limit is driven by the TOP of the feed, and
|
|
|
|
// it's the max amount that we can scroll down (or "skooch
|
|
|
|
// up" the messages) before knocking the selected message
|
|
|
|
// out of the feed.
|
2019-11-02 00:06:25 +01:00
|
|
|
const selected_row_top = selected_row.offset().top;
|
|
|
|
let scroll_limit = selected_row_top - viewport_info.visible_top;
|
2019-01-05 16:49:23 +01:00
|
|
|
|
|
|
|
if (scroll_limit < 0) {
|
|
|
|
// This shouldn't happen, but if we're off by a pixel or
|
|
|
|
// something, we can deal with it, and just warn.
|
2020-07-15 01:29:15 +02:00
|
|
|
blueslip.warn("Selected row appears too high on screen.");
|
2019-01-05 16:49:23 +01:00
|
|
|
scroll_limit = 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
return scroll_limit;
|
|
|
|
},
|
|
|
|
|
2020-07-20 22:18:43 +02:00
|
|
|
_maybe_autoscroll(new_messages_height) {
|
2019-01-05 16:25:21 +01: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.
|
message scrolling: Fix "Scroll down to view" warning.
We recently added a feature to warn users that they
may need to scroll down to view messages that they
just sent, but it was broken due to various complexities
in the rendering code path.
Now we compute it a bit more rigorously.
It requires us to pass some info about rendering up
and down the stack, which is why it's kind of a long
commit, but the bulk of the logic is in these JS files:
* message_list_view.js
* notifications.js
I choose to pass structs around instead of booleans,
because I anticipate we may eventually add more metadata
about rendering to it, plus bools are just kinda brittle.
(The exceptions are that `_maybe_autoscroll`, which
is at the bottom of the stack, just passes back a simple
boolean, and `notify_local_mixes`, also at the bottom
of the stack, just accepts a simple boolean.)
This errs on the side of warning the user, even if the
new message is partially visible.
Fixes #11138
2019-01-07 21:00:03 +01:00
|
|
|
//
|
|
|
|
// returns `true` if we need the user to scroll
|
2019-01-05 16:25:21 +01:00
|
|
|
|
2019-11-02 00:06:25 +01:00
|
|
|
const selected_row = this.selected_row();
|
|
|
|
const last_visible = rows.last_visible();
|
2013-08-16 17:10:22 +02:00
|
|
|
|
|
|
|
// Make sure we have a selected row and last visible row. (defensive)
|
2018-06-06 18:50:09 +02:00
|
|
|
if (!(selected_row && selected_row.length > 0 && last_visible)) {
|
message scrolling: Fix "Scroll down to view" warning.
We recently added a feature to warn users that they
may need to scroll down to view messages that they
just sent, but it was broken due to various complexities
in the rendering code path.
Now we compute it a bit more rigorously.
It requires us to pass some info about rendering up
and down the stack, which is why it's kind of a long
commit, but the bulk of the logic is in these JS files:
* message_list_view.js
* notifications.js
I choose to pass structs around instead of booleans,
because I anticipate we may eventually add more metadata
about rendering to it, plus bools are just kinda brittle.
(The exceptions are that `_maybe_autoscroll`, which
is at the bottom of the stack, just passes back a simple
boolean, and `notify_local_mixes`, also at the bottom
of the stack, just accepts a simple boolean.)
This errs on the side of warning the user, even if the
new message is partially visible.
Fixes #11138
2019-01-07 21:00:03 +01:00
|
|
|
return false;
|
2013-08-16 17:10:22 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if (new_messages_height <= 0) {
|
message scrolling: Fix "Scroll down to view" warning.
We recently added a feature to warn users that they
may need to scroll down to view messages that they
just sent, but it was broken due to various complexities
in the rendering code path.
Now we compute it a bit more rigorously.
It requires us to pass some info about rendering up
and down the stack, which is why it's kind of a long
commit, but the bulk of the logic is in these JS files:
* message_list_view.js
* notifications.js
I choose to pass structs around instead of booleans,
because I anticipate we may eventually add more metadata
about rendering to it, plus bools are just kinda brittle.
(The exceptions are that `_maybe_autoscroll`, which
is at the bottom of the stack, just passes back a simple
boolean, and `notify_local_mixes`, also at the bottom
of the stack, just accepts a simple boolean.)
This errs on the side of warning the user, even if the
new message is partially visible.
Fixes #11138
2019-01-07 21:00:03 +01:00
|
|
|
return false;
|
2013-08-16 17:10:22 +02:00
|
|
|
}
|
|
|
|
|
2019-06-29 02:52:15 +02:00
|
|
|
if (!activity.client_is_active) {
|
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
|
|
|
// 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");
|
message scrolling: Fix "Scroll down to view" warning.
We recently added a feature to warn users that they
may need to scroll down to view messages that they
just sent, but it was broken due to various complexities
in the rendering code path.
Now we compute it a bit more rigorously.
It requires us to pass some info about rendering up
and down the stack, which is why it's kind of a long
commit, but the bulk of the logic is in these JS files:
* message_list_view.js
* notifications.js
I choose to pass structs around instead of booleans,
because I anticipate we may eventually add more metadata
about rendering to it, plus bools are just kinda brittle.
(The exceptions are that `_maybe_autoscroll`, which
is at the bottom of the stack, just passes back a simple
boolean, and `notify_local_mixes`, also at the bottom
of the stack, just accepts a simple boolean.)
This errs on the side of warning the user, even if the
new message is partially visible.
Fixes #11138
2019-01-07 21:00:03 +01:00
|
|
|
return false;
|
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
|
|
|
}
|
|
|
|
|
2017-11-07 22:20:48 +01:00
|
|
|
// do not scroll if there are any active popovers.
|
|
|
|
if (popovers.any_active()) {
|
message scrolling: Fix "Scroll down to view" warning.
We recently added a feature to warn users that they
may need to scroll down to view messages that they
just sent, but it was broken due to various complexities
in the rendering code path.
Now we compute it a bit more rigorously.
It requires us to pass some info about rendering up
and down the stack, which is why it's kind of a long
commit, but the bulk of the logic is in these JS files:
* message_list_view.js
* notifications.js
I choose to pass structs around instead of booleans,
because I anticipate we may eventually add more metadata
about rendering to it, plus bools are just kinda brittle.
(The exceptions are that `_maybe_autoscroll`, which
is at the bottom of the stack, just passes back a simple
boolean, and `notify_local_mixes`, also at the bottom
of the stack, just accepts a simple boolean.)
This errs on the side of warning the user, even if the
new message is partially visible.
Fixes #11138
2019-01-07 21:00:03 +01:00
|
|
|
// If a popover is active, then we are pretty sure the
|
|
|
|
// incoming message is not from the user themselves, so
|
|
|
|
// we don't need to tell users to scroll down.
|
|
|
|
return false;
|
2017-11-07 22:20:48 +01:00
|
|
|
}
|
|
|
|
|
2019-11-02 00:06:25 +01:00
|
|
|
const info = message_viewport.message_viewport_info();
|
|
|
|
const scroll_limit = this._scroll_limit(selected_row, info);
|
2019-01-05 17:08:53 +01:00
|
|
|
|
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
|
2019-11-02 00:06:25 +01:00
|
|
|
let scroll_amount;
|
|
|
|
let need_user_to_scroll;
|
message scrolling: Fix "Scroll down to view" warning.
We recently added a feature to warn users that they
may need to scroll down to view messages that they
just sent, but it was broken due to various complexities
in the rendering code path.
Now we compute it a bit more rigorously.
It requires us to pass some info about rendering up
and down the stack, which is why it's kind of a long
commit, but the bulk of the logic is in these JS files:
* message_list_view.js
* notifications.js
I choose to pass structs around instead of booleans,
because I anticipate we may eventually add more metadata
about rendering to it, plus bools are just kinda brittle.
(The exceptions are that `_maybe_autoscroll`, which
is at the bottom of the stack, just passes back a simple
boolean, and `notify_local_mixes`, also at the bottom
of the stack, just accepts a simple boolean.)
This errs on the side of warning the user, even if the
new message is partially visible.
Fixes #11138
2019-01-07 21:00:03 +01:00
|
|
|
|
|
|
|
if (new_messages_height <= scroll_limit) {
|
|
|
|
// This is the happy path where we can just scroll
|
|
|
|
// automatically, and the user will see the new message.
|
|
|
|
scroll_amount = new_messages_height;
|
|
|
|
need_user_to_scroll = false;
|
|
|
|
} else {
|
|
|
|
// Sometimes we don't want to scroll the entire height of
|
|
|
|
// the message, but our callers can give appropriate
|
|
|
|
// warnings if the message is gonna be offscreen.
|
|
|
|
// (Even if we are somewhat constrained here, the message may
|
|
|
|
// still end up being visible, so we do some arithmetic.)
|
2019-01-05 16:49:23 +01:00
|
|
|
scroll_amount = scroll_limit;
|
2019-11-02 00:06:25 +01:00
|
|
|
const offset = message_viewport.offset_from_bottom(last_visible);
|
2019-02-05 22:12:28 +01:00
|
|
|
|
|
|
|
// For determining whether we need to show the user a "you
|
|
|
|
// need to scroll down" notification, the obvious check
|
|
|
|
// would be `offset > scroll_amount`, and that is indeed
|
|
|
|
// correct with a 1-line message in the compose box.
|
|
|
|
// However, the compose box is open with the content of
|
|
|
|
// the message just sent when this code runs, and
|
|
|
|
// `offset_from_bottom` if an offset from the top of the
|
|
|
|
// compose box, which is about to be reset to empty. So
|
|
|
|
// to compute the offset at the time the user might see
|
|
|
|
// this notification, we need to adjust by the amount that
|
|
|
|
// the current compose is bigger than the empty, open
|
|
|
|
// compose box.
|
2019-11-02 00:06:25 +01:00
|
|
|
const compose_textarea_default_height = 42;
|
|
|
|
const compose_textarea_current_height = $("#compose-textarea").height();
|
|
|
|
const expected_change =
|
2019-10-26 00:21:32 +02:00
|
|
|
compose_textarea_current_height - compose_textarea_default_height;
|
2019-11-02 00:06:25 +01:00
|
|
|
const expected_offset = offset - expected_change;
|
2019-02-05 22:12:28 +01:00
|
|
|
need_user_to_scroll = expected_offset > scroll_amount;
|
2013-08-16 17:10:22 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// Ok, we are finally ready to actually scroll.
|
2019-01-08 00:14:15 +01:00
|
|
|
if (scroll_amount > 0) {
|
|
|
|
message_viewport.system_initiated_animate_scroll(scroll_amount);
|
|
|
|
}
|
message scrolling: Fix "Scroll down to view" warning.
We recently added a feature to warn users that they
may need to scroll down to view messages that they
just sent, but it was broken due to various complexities
in the rendering code path.
Now we compute it a bit more rigorously.
It requires us to pass some info about rendering up
and down the stack, which is why it's kind of a long
commit, but the bulk of the logic is in these JS files:
* message_list_view.js
* notifications.js
I choose to pass structs around instead of booleans,
because I anticipate we may eventually add more metadata
about rendering to it, plus bools are just kinda brittle.
(The exceptions are that `_maybe_autoscroll`, which
is at the bottom of the stack, just passes back a simple
boolean, and `notify_local_mixes`, also at the bottom
of the stack, just accepts a simple boolean.)
This errs on the side of warning the user, even if the
new message is partially visible.
Fixes #11138
2019-01-07 21:00:03 +01:00
|
|
|
|
|
|
|
return need_user_to_scroll;
|
2013-08-16 17:10:22 +02:00
|
|
|
},
|
|
|
|
|
2020-07-20 22:18:43 +02:00
|
|
|
clear_rendering_state(clear_table) {
|
2013-08-16 17:10:22 +02:00
|
|
|
if (clear_table) {
|
|
|
|
this.clear_table();
|
|
|
|
}
|
|
|
|
this.list.last_message_historical = false;
|
|
|
|
|
|
|
|
this._render_win_start = 0;
|
|
|
|
this._render_win_end = 0;
|
|
|
|
},
|
|
|
|
|
2020-07-20 22:18:43 +02:00
|
|
|
update_render_window(selected_idx, check_for_changed) {
|
2019-11-02 00:06:25 +01:00
|
|
|
const 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;
|
2020-07-15 00:34:28 +02: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;
|
|
|
|
},
|
|
|
|
|
2020-07-20 22:18:43 +02:00
|
|
|
maybe_rerender() {
|
2013-08-16 17:10:22 +02:00
|
|
|
if (this.table_name === undefined) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2019-11-02 00:06:25 +01:00
|
|
|
const selected_idx = this.list.selected_idx();
|
2013-08-16 17:10:22 +02:00
|
|
|
|
|
|
|
// 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
|
2020-07-15 00:34:28 +02:00
|
|
|
if (
|
|
|
|
!(
|
|
|
|
(selected_idx - this._render_win_start < this._RENDER_THRESHOLD &&
|
|
|
|
this._render_win_start !== 0) ||
|
|
|
|
(this._render_win_end - selected_idx <= this._RENDER_THRESHOLD &&
|
|
|
|
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;
|
|
|
|
},
|
|
|
|
|
2020-07-20 22:18:43 +02:00
|
|
|
rerender_preserving_scrolltop(discard_rendering_state) {
|
2018-03-11 19:09:17 +01:00
|
|
|
// old_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
|
2019-11-02 00:06:25 +01:00
|
|
|
let old_offset;
|
|
|
|
const selected_row = this.selected_row();
|
|
|
|
const selected_in_view = selected_row.length > 0;
|
2013-08-16 17:10:22 +02:00
|
|
|
if (selected_in_view) {
|
2018-03-11 19:09:17 +01:00
|
|
|
old_offset = selected_row.offset().top;
|
2013-08-16 17:10:22 +02:00
|
|
|
}
|
2019-09-18 20:38:07 +02:00
|
|
|
if (discard_rendering_state) {
|
|
|
|
// If we know that the existing render is invalid way
|
|
|
|
// (typically because messages appear out-of-order), then
|
|
|
|
// we discard the message_list rendering state entirely.
|
|
|
|
this.clear_rendering_state(true);
|
|
|
|
this.update_render_window(this.list.selected_idx(), false);
|
|
|
|
}
|
2018-04-14 00:47:02 +02:00
|
|
|
return this.rerender_with_target_scrolltop(selected_row, old_offset);
|
|
|
|
},
|
2013-08-16 17:10:22 +02:00
|
|
|
|
2020-07-20 22:18:43 +02:00
|
|
|
set_message_offset(offset) {
|
2019-11-02 00:06:25 +01:00
|
|
|
const msg = this.selected_row();
|
2018-04-14 01:10:22 +02:00
|
|
|
message_viewport.scrollTop(message_viewport.scrollTop() + msg.offset().top - offset);
|
|
|
|
},
|
|
|
|
|
2020-07-20 22:18:43 +02:00
|
|
|
rerender_with_target_scrolltop(selected_row, target_offset) {
|
2018-04-14 00:47:02 +02:00
|
|
|
// target_offset is the target number of pixels between the top of the
|
|
|
|
// viewable window and the selected message
|
2013-08-16 17:10:22 +02:00
|
|
|
this.clear_table();
|
2020-07-15 00:34:28 +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.
|
2018-04-14 00:47:02 +02:00
|
|
|
if (target_offset !== undefined) {
|
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});
|
|
|
|
}
|
2018-03-11 19:09:17 +01:00
|
|
|
|
2018-04-14 01:10:22 +02:00
|
|
|
this.set_message_offset(target_offset);
|
2013-08-16 17:10:22 +02:00
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2020-07-20 22:18:43 +02:00
|
|
|
_find_message_group(message_group_id) {
|
2019-02-21 01:16:54 +01:00
|
|
|
// Ideally, we'd maintain this data structure with a hash
|
|
|
|
// table or at least a pointer from the message containers (in
|
|
|
|
// either case, updating the data structure when message
|
|
|
|
// groups are merged etc.) , but we only call this from flows
|
|
|
|
// like message editing, so it's not a big performance
|
|
|
|
// problem.
|
2020-02-08 05:31:13 +01:00
|
|
|
return this._message_groups.find(
|
2019-02-21 01:16:54 +01:00
|
|
|
// Since we don't have a way to get a message group from
|
|
|
|
// the containing message container, we just do a search
|
|
|
|
// to find it.
|
2020-07-02 02:16:03 +02:00
|
|
|
(message_group) => message_group.message_group_id === message_group_id,
|
2020-02-08 05:31:13 +01:00
|
|
|
);
|
2019-02-21 01:16:54 +01:00
|
|
|
},
|
|
|
|
|
2020-07-20 22:18:43 +02:00
|
|
|
_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;
|
|
|
|
}
|
|
|
|
|
2019-11-02 00:06:25 +01:00
|
|
|
const 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;
|
|
|
|
}
|
|
|
|
|
2019-11-02 00:06:25 +01:00
|
|
|
const recipient_row = rows.get_message_recipient_row(first_row);
|
2020-07-15 01:29:15 +02:00
|
|
|
const header = recipient_row.find(".message_header");
|
2019-11-02 00:06:25 +01:00
|
|
|
const message_group_id = recipient_row.attr("id");
|
2019-02-21 01:16:54 +01:00
|
|
|
|
|
|
|
// Since there might be multiple dates within the message
|
|
|
|
// group, it's important to lookup the original/full message
|
|
|
|
// group rather than doing an artificial rerendering of the
|
|
|
|
// message header from the set of message containers passed in
|
|
|
|
// here.
|
2019-11-02 00:06:25 +01:00
|
|
|
const group = this._find_message_group(message_group_id);
|
2019-02-21 01:16:54 +01:00
|
|
|
if (group === undefined) {
|
|
|
|
blueslip.error("Could not find message group for rerendering headers");
|
|
|
|
return;
|
|
|
|
}
|
2014-03-03 22:00:42 +01:00
|
|
|
|
2019-02-21 01:16:54 +01:00
|
|
|
// TODO: It's possible that we no longer need this populate
|
|
|
|
// call; it was introduced in an earlier version of this code
|
|
|
|
// where we constructed an artificial message group for this
|
|
|
|
// rerendering rather than looking up the original version.
|
|
|
|
populate_group_from_message_container(group, group.message_containers[0]);
|
2014-03-03 22:00:42 +01:00
|
|
|
|
2019-11-02 00:06:25 +01:00
|
|
|
const rendered_recipient_row = $(render_recipient_row(group));
|
2014-03-03 22:00:42 +01:00
|
|
|
|
|
|
|
header.replaceWith(rendered_recipient_row);
|
|
|
|
},
|
|
|
|
|
2020-07-20 22:18:43 +02:00
|
|
|
_rerender_message(message_container, message_content_edited) {
|
2019-11-02 00:06:25 +01:00
|
|
|
const row = this.get_row(message_container.msg.id);
|
|
|
|
const was_selected = this.list.selected_message() === message_container.msg;
|
2014-02-11 16:19:42 +01:00
|
|
|
|
|
|
|
// Re-render just this one message
|
2016-05-31 14:24:58 +02:00
|
|
|
this._maybe_format_me_message(message_container);
|
2019-03-15 19:42:24 +01:00
|
|
|
this._add_msg_edited_vars(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;
|
|
|
|
|
2019-11-02 00:06:25 +01:00
|
|
|
const 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");
|
|
|
|
}
|
2018-07-12 17:41:05 +02:00
|
|
|
this._post_process(rendered_msg);
|
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
|
|
|
},
|
|
|
|
|
2020-07-20 22:18:43 +02:00
|
|
|
rerender_messages(messages, message_content_edited) {
|
2019-11-02 00:06:25 +01:00
|
|
|
const self = this;
|
2014-03-04 16:38:12 +01:00
|
|
|
|
2014-03-14 16:28:54 +01:00
|
|
|
// Convert messages to list messages
|
2020-07-02 01:39:34 +02:00
|
|
|
let message_containers = messages.map((message) => self.message_containers.get(message.id));
|
2014-03-18 20:46:32 +01:00
|
|
|
// We may not have the message_container if the stream or topic was muted
|
2020-02-08 03:51:18 +01:00
|
|
|
message_containers = message_containers.filter(
|
2020-07-02 02:16:03 +02:00
|
|
|
(message_container) => message_container !== undefined,
|
2020-02-08 03:51:18 +01:00
|
|
|
);
|
2014-03-04 16:38:12 +01:00
|
|
|
|
2019-11-02 00:06:25 +01:00
|
|
|
const message_groups = [];
|
|
|
|
let current_group = [];
|
js: Automatically convert _.each to for…of.
This commit was automatically generated by the following script,
followed by lint --fix and a few small manual lint-related cleanups.
import * as babelParser from "recast/parsers/babel";
import * as recast from "recast";
import * as tsParser from "recast/parsers/typescript";
import { builders as b, namedTypes as n } from "ast-types";
import { Context } from "ast-types/lib/path-visitor";
import K from "ast-types/gen/kinds";
import { NodePath } from "ast-types/lib/node-path";
import assert from "assert";
import fs from "fs";
import path from "path";
import process from "process";
const checkExpression = (node: n.Node): node is K.ExpressionKind =>
n.Expression.check(node);
const checkStatement = (node: n.Node): node is K.StatementKind =>
n.Statement.check(node);
for (const file of process.argv.slice(2)) {
console.log("Parsing", file);
const ast = recast.parse(fs.readFileSync(file, { encoding: "utf8" }), {
parser: path.extname(file) === ".ts" ? tsParser : babelParser,
});
let changed = false;
let inLoop = false;
let replaceReturn = false;
const visitLoop = (...args: string[]) =>
function(this: Context, path: NodePath) {
for (const arg of args) {
this.visit(path.get(arg));
}
const old = { inLoop };
inLoop = true;
this.visit(path.get("body"));
inLoop = old.inLoop;
return false;
};
recast.visit(ast, {
visitDoWhileStatement: visitLoop("test"),
visitExpressionStatement(path) {
const { expression, comments } = path.node;
let valueOnly;
if (
n.CallExpression.check(expression) &&
n.MemberExpression.check(expression.callee) &&
!expression.callee.computed &&
n.Identifier.check(expression.callee.object) &&
expression.callee.object.name === "_" &&
n.Identifier.check(expression.callee.property) &&
["each", "forEach"].includes(expression.callee.property.name) &&
[2, 3].includes(expression.arguments.length) &&
checkExpression(expression.arguments[0]) &&
(n.FunctionExpression.check(expression.arguments[1]) ||
n.ArrowFunctionExpression.check(expression.arguments[1])) &&
[1, 2].includes(expression.arguments[1].params.length) &&
n.Identifier.check(expression.arguments[1].params[0]) &&
((valueOnly = expression.arguments[1].params[1] === undefined) ||
n.Identifier.check(expression.arguments[1].params[1])) &&
(expression.arguments[2] === undefined ||
n.ThisExpression.check(expression.arguments[2]))
) {
const old = { inLoop, replaceReturn };
inLoop = false;
replaceReturn = true;
this.visit(
path
.get("expression")
.get("arguments")
.get(1)
.get("body")
);
inLoop = old.inLoop;
replaceReturn = old.replaceReturn;
const [right, { body, params }] = expression.arguments;
const loop = b.forOfStatement(
b.variableDeclaration("let", [
b.variableDeclarator(
valueOnly ? params[0] : b.arrayPattern([params[1], params[0]])
),
]),
valueOnly
? right
: b.callExpression(
b.memberExpression(right, b.identifier("entries")),
[]
),
checkStatement(body) ? body : b.expressionStatement(body)
);
loop.comments = comments;
path.replace(loop);
changed = true;
}
this.traverse(path);
},
visitForStatement: visitLoop("init", "test", "update"),
visitForInStatement: visitLoop("left", "right"),
visitForOfStatement: visitLoop("left", "right"),
visitFunction(path) {
this.visit(path.get("params"));
const old = { replaceReturn };
replaceReturn = false;
this.visit(path.get("body"));
replaceReturn = old.replaceReturn;
return false;
},
visitReturnStatement(path) {
if (replaceReturn) {
assert(!inLoop); // could use labeled continue if this ever fires
const { argument, comments } = path.node;
if (argument === null) {
const s = b.continueStatement();
s.comments = comments;
path.replace(s);
} else {
const s = b.expressionStatement(argument);
s.comments = comments;
path.replace(s, b.continueStatement());
}
return false;
}
this.traverse(path);
},
visitWhileStatement: visitLoop("test"),
});
if (changed) {
console.log("Writing", file);
fs.writeFileSync(file, recast.print(ast).code, { encoding: "utf8" });
}
}
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-02-06 06:19:47 +01:00
|
|
|
|
|
|
|
for (const message_container of message_containers) {
|
2020-07-15 00:34:28 +02: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);
|
js: Automatically convert _.each to for…of.
This commit was automatically generated by the following script,
followed by lint --fix and a few small manual lint-related cleanups.
import * as babelParser from "recast/parsers/babel";
import * as recast from "recast";
import * as tsParser from "recast/parsers/typescript";
import { builders as b, namedTypes as n } from "ast-types";
import { Context } from "ast-types/lib/path-visitor";
import K from "ast-types/gen/kinds";
import { NodePath } from "ast-types/lib/node-path";
import assert from "assert";
import fs from "fs";
import path from "path";
import process from "process";
const checkExpression = (node: n.Node): node is K.ExpressionKind =>
n.Expression.check(node);
const checkStatement = (node: n.Node): node is K.StatementKind =>
n.Statement.check(node);
for (const file of process.argv.slice(2)) {
console.log("Parsing", file);
const ast = recast.parse(fs.readFileSync(file, { encoding: "utf8" }), {
parser: path.extname(file) === ".ts" ? tsParser : babelParser,
});
let changed = false;
let inLoop = false;
let replaceReturn = false;
const visitLoop = (...args: string[]) =>
function(this: Context, path: NodePath) {
for (const arg of args) {
this.visit(path.get(arg));
}
const old = { inLoop };
inLoop = true;
this.visit(path.get("body"));
inLoop = old.inLoop;
return false;
};
recast.visit(ast, {
visitDoWhileStatement: visitLoop("test"),
visitExpressionStatement(path) {
const { expression, comments } = path.node;
let valueOnly;
if (
n.CallExpression.check(expression) &&
n.MemberExpression.check(expression.callee) &&
!expression.callee.computed &&
n.Identifier.check(expression.callee.object) &&
expression.callee.object.name === "_" &&
n.Identifier.check(expression.callee.property) &&
["each", "forEach"].includes(expression.callee.property.name) &&
[2, 3].includes(expression.arguments.length) &&
checkExpression(expression.arguments[0]) &&
(n.FunctionExpression.check(expression.arguments[1]) ||
n.ArrowFunctionExpression.check(expression.arguments[1])) &&
[1, 2].includes(expression.arguments[1].params.length) &&
n.Identifier.check(expression.arguments[1].params[0]) &&
((valueOnly = expression.arguments[1].params[1] === undefined) ||
n.Identifier.check(expression.arguments[1].params[1])) &&
(expression.arguments[2] === undefined ||
n.ThisExpression.check(expression.arguments[2]))
) {
const old = { inLoop, replaceReturn };
inLoop = false;
replaceReturn = true;
this.visit(
path
.get("expression")
.get("arguments")
.get(1)
.get("body")
);
inLoop = old.inLoop;
replaceReturn = old.replaceReturn;
const [right, { body, params }] = expression.arguments;
const loop = b.forOfStatement(
b.variableDeclaration("let", [
b.variableDeclarator(
valueOnly ? params[0] : b.arrayPattern([params[1], params[0]])
),
]),
valueOnly
? right
: b.callExpression(
b.memberExpression(right, b.identifier("entries")),
[]
),
checkStatement(body) ? body : b.expressionStatement(body)
);
loop.comments = comments;
path.replace(loop);
changed = true;
}
this.traverse(path);
},
visitForStatement: visitLoop("init", "test", "update"),
visitForInStatement: visitLoop("left", "right"),
visitForOfStatement: visitLoop("left", "right"),
visitFunction(path) {
this.visit(path.get("params"));
const old = { replaceReturn };
replaceReturn = false;
this.visit(path.get("body"));
replaceReturn = old.replaceReturn;
return false;
},
visitReturnStatement(path) {
if (replaceReturn) {
assert(!inLoop); // could use labeled continue if this ever fires
const { argument, comments } = path.node;
if (argument === null) {
const s = b.continueStatement();
s.comments = comments;
path.replace(s);
} else {
const s = b.expressionStatement(argument);
s.comments = comments;
path.replace(s, b.continueStatement());
}
return false;
}
this.traverse(path);
},
visitWhileStatement: visitLoop("test"),
});
if (changed) {
console.log("Writing", file);
fs.writeFileSync(file, recast.print(ast).code, { encoding: "utf8" });
}
}
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-02-06 06:19:47 +01:00
|
|
|
}
|
|
|
|
|
2014-03-03 22:00:42 +01:00
|
|
|
if (current_group.length !== 0) {
|
|
|
|
message_groups.push(current_group);
|
|
|
|
}
|
js: Automatically convert _.each to for…of.
This commit was automatically generated by the following script,
followed by lint --fix and a few small manual lint-related cleanups.
import * as babelParser from "recast/parsers/babel";
import * as recast from "recast";
import * as tsParser from "recast/parsers/typescript";
import { builders as b, namedTypes as n } from "ast-types";
import { Context } from "ast-types/lib/path-visitor";
import K from "ast-types/gen/kinds";
import { NodePath } from "ast-types/lib/node-path";
import assert from "assert";
import fs from "fs";
import path from "path";
import process from "process";
const checkExpression = (node: n.Node): node is K.ExpressionKind =>
n.Expression.check(node);
const checkStatement = (node: n.Node): node is K.StatementKind =>
n.Statement.check(node);
for (const file of process.argv.slice(2)) {
console.log("Parsing", file);
const ast = recast.parse(fs.readFileSync(file, { encoding: "utf8" }), {
parser: path.extname(file) === ".ts" ? tsParser : babelParser,
});
let changed = false;
let inLoop = false;
let replaceReturn = false;
const visitLoop = (...args: string[]) =>
function(this: Context, path: NodePath) {
for (const arg of args) {
this.visit(path.get(arg));
}
const old = { inLoop };
inLoop = true;
this.visit(path.get("body"));
inLoop = old.inLoop;
return false;
};
recast.visit(ast, {
visitDoWhileStatement: visitLoop("test"),
visitExpressionStatement(path) {
const { expression, comments } = path.node;
let valueOnly;
if (
n.CallExpression.check(expression) &&
n.MemberExpression.check(expression.callee) &&
!expression.callee.computed &&
n.Identifier.check(expression.callee.object) &&
expression.callee.object.name === "_" &&
n.Identifier.check(expression.callee.property) &&
["each", "forEach"].includes(expression.callee.property.name) &&
[2, 3].includes(expression.arguments.length) &&
checkExpression(expression.arguments[0]) &&
(n.FunctionExpression.check(expression.arguments[1]) ||
n.ArrowFunctionExpression.check(expression.arguments[1])) &&
[1, 2].includes(expression.arguments[1].params.length) &&
n.Identifier.check(expression.arguments[1].params[0]) &&
((valueOnly = expression.arguments[1].params[1] === undefined) ||
n.Identifier.check(expression.arguments[1].params[1])) &&
(expression.arguments[2] === undefined ||
n.ThisExpression.check(expression.arguments[2]))
) {
const old = { inLoop, replaceReturn };
inLoop = false;
replaceReturn = true;
this.visit(
path
.get("expression")
.get("arguments")
.get(1)
.get("body")
);
inLoop = old.inLoop;
replaceReturn = old.replaceReturn;
const [right, { body, params }] = expression.arguments;
const loop = b.forOfStatement(
b.variableDeclaration("let", [
b.variableDeclarator(
valueOnly ? params[0] : b.arrayPattern([params[1], params[0]])
),
]),
valueOnly
? right
: b.callExpression(
b.memberExpression(right, b.identifier("entries")),
[]
),
checkStatement(body) ? body : b.expressionStatement(body)
);
loop.comments = comments;
path.replace(loop);
changed = true;
}
this.traverse(path);
},
visitForStatement: visitLoop("init", "test", "update"),
visitForInStatement: visitLoop("left", "right"),
visitForOfStatement: visitLoop("left", "right"),
visitFunction(path) {
this.visit(path.get("params"));
const old = { replaceReturn };
replaceReturn = false;
this.visit(path.get("body"));
replaceReturn = old.replaceReturn;
return false;
},
visitReturnStatement(path) {
if (replaceReturn) {
assert(!inLoop); // could use labeled continue if this ever fires
const { argument, comments } = path.node;
if (argument === null) {
const s = b.continueStatement();
s.comments = comments;
path.replace(s);
} else {
const s = b.expressionStatement(argument);
s.comments = comments;
path.replace(s, b.continueStatement());
}
return false;
}
this.traverse(path);
},
visitWhileStatement: visitLoop("test"),
});
if (changed) {
console.log("Writing", file);
fs.writeFileSync(file, recast.print(ast).code, { encoding: "utf8" });
}
}
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-02-06 06:19:47 +01:00
|
|
|
|
|
|
|
for (const messages_in_group of message_groups) {
|
2017-04-21 20:27:45 +02:00
|
|
|
self._rerender_header(messages_in_group, message_content_edited);
|
js: Automatically convert _.each to for…of.
This commit was automatically generated by the following script,
followed by lint --fix and a few small manual lint-related cleanups.
import * as babelParser from "recast/parsers/babel";
import * as recast from "recast";
import * as tsParser from "recast/parsers/typescript";
import { builders as b, namedTypes as n } from "ast-types";
import { Context } from "ast-types/lib/path-visitor";
import K from "ast-types/gen/kinds";
import { NodePath } from "ast-types/lib/node-path";
import assert from "assert";
import fs from "fs";
import path from "path";
import process from "process";
const checkExpression = (node: n.Node): node is K.ExpressionKind =>
n.Expression.check(node);
const checkStatement = (node: n.Node): node is K.StatementKind =>
n.Statement.check(node);
for (const file of process.argv.slice(2)) {
console.log("Parsing", file);
const ast = recast.parse(fs.readFileSync(file, { encoding: "utf8" }), {
parser: path.extname(file) === ".ts" ? tsParser : babelParser,
});
let changed = false;
let inLoop = false;
let replaceReturn = false;
const visitLoop = (...args: string[]) =>
function(this: Context, path: NodePath) {
for (const arg of args) {
this.visit(path.get(arg));
}
const old = { inLoop };
inLoop = true;
this.visit(path.get("body"));
inLoop = old.inLoop;
return false;
};
recast.visit(ast, {
visitDoWhileStatement: visitLoop("test"),
visitExpressionStatement(path) {
const { expression, comments } = path.node;
let valueOnly;
if (
n.CallExpression.check(expression) &&
n.MemberExpression.check(expression.callee) &&
!expression.callee.computed &&
n.Identifier.check(expression.callee.object) &&
expression.callee.object.name === "_" &&
n.Identifier.check(expression.callee.property) &&
["each", "forEach"].includes(expression.callee.property.name) &&
[2, 3].includes(expression.arguments.length) &&
checkExpression(expression.arguments[0]) &&
(n.FunctionExpression.check(expression.arguments[1]) ||
n.ArrowFunctionExpression.check(expression.arguments[1])) &&
[1, 2].includes(expression.arguments[1].params.length) &&
n.Identifier.check(expression.arguments[1].params[0]) &&
((valueOnly = expression.arguments[1].params[1] === undefined) ||
n.Identifier.check(expression.arguments[1].params[1])) &&
(expression.arguments[2] === undefined ||
n.ThisExpression.check(expression.arguments[2]))
) {
const old = { inLoop, replaceReturn };
inLoop = false;
replaceReturn = true;
this.visit(
path
.get("expression")
.get("arguments")
.get(1)
.get("body")
);
inLoop = old.inLoop;
replaceReturn = old.replaceReturn;
const [right, { body, params }] = expression.arguments;
const loop = b.forOfStatement(
b.variableDeclaration("let", [
b.variableDeclarator(
valueOnly ? params[0] : b.arrayPattern([params[1], params[0]])
),
]),
valueOnly
? right
: b.callExpression(
b.memberExpression(right, b.identifier("entries")),
[]
),
checkStatement(body) ? body : b.expressionStatement(body)
);
loop.comments = comments;
path.replace(loop);
changed = true;
}
this.traverse(path);
},
visitForStatement: visitLoop("init", "test", "update"),
visitForInStatement: visitLoop("left", "right"),
visitForOfStatement: visitLoop("left", "right"),
visitFunction(path) {
this.visit(path.get("params"));
const old = { replaceReturn };
replaceReturn = false;
this.visit(path.get("body"));
replaceReturn = old.replaceReturn;
return false;
},
visitReturnStatement(path) {
if (replaceReturn) {
assert(!inLoop); // could use labeled continue if this ever fires
const { argument, comments } = path.node;
if (argument === null) {
const s = b.continueStatement();
s.comments = comments;
path.replace(s);
} else {
const s = b.expressionStatement(argument);
s.comments = comments;
path.replace(s, b.continueStatement());
}
return false;
}
this.traverse(path);
},
visitWhileStatement: visitLoop("test"),
});
if (changed) {
console.log("Writing", file);
fs.writeFileSync(file, recast.print(ast).code, { encoding: "utf8" });
}
}
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-02-06 06:19:47 +01:00
|
|
|
}
|
2014-02-11 16:19:42 +01:00
|
|
|
},
|
|
|
|
|
2020-07-20 22:18:43 +02:00
|
|
|
append(messages, messages_are_new) {
|
2019-11-02 00:06:25 +01:00
|
|
|
const cur_window_size = this._render_win_end - this._render_win_start;
|
|
|
|
let render_info;
|
message scrolling: Fix "Scroll down to view" warning.
We recently added a feature to warn users that they
may need to scroll down to view messages that they
just sent, but it was broken due to various complexities
in the rendering code path.
Now we compute it a bit more rigorously.
It requires us to pass some info about rendering up
and down the stack, which is why it's kind of a long
commit, but the bulk of the logic is in these JS files:
* message_list_view.js
* notifications.js
I choose to pass structs around instead of booleans,
because I anticipate we may eventually add more metadata
about rendering to it, plus bools are just kinda brittle.
(The exceptions are that `_maybe_autoscroll`, which
is at the bottom of the stack, just passes back a simple
boolean, and `notify_local_mixes`, also at the bottom
of the stack, just accepts a simple boolean.)
This errs on the side of warning the user, even if the
new message is partially visible.
Fixes #11138
2019-01-07 21:00:03 +01:00
|
|
|
|
2013-08-16 17:10:22 +02:00
|
|
|
if (cur_window_size < this._RENDER_WINDOW_SIZE) {
|
2019-11-02 00:06:25 +01:00
|
|
|
const slice_to_render = messages.slice(0, this._RENDER_WINDOW_SIZE - cur_window_size);
|
2020-07-15 01:29:15 +02:00
|
|
|
render_info = this.render(slice_to_render, "bottom", messages_are_new);
|
2013-08-16 17:10:22 +02:00
|
|
|
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.
|
2019-11-02 00:06:25 +01:00
|
|
|
const needed_rerender = this.maybe_rerender();
|
message scrolling: Fix "Scroll down to view" warning.
We recently added a feature to warn users that they
may need to scroll down to view messages that they
just sent, but it was broken due to various complexities
in the rendering code path.
Now we compute it a bit more rigorously.
It requires us to pass some info about rendering up
and down the stack, which is why it's kind of a long
commit, but the bulk of the logic is in these JS files:
* message_list_view.js
* notifications.js
I choose to pass structs around instead of booleans,
because I anticipate we may eventually add more metadata
about rendering to it, plus bools are just kinda brittle.
(The exceptions are that `_maybe_autoscroll`, which
is at the bottom of the stack, just passes back a simple
boolean, and `notify_local_mixes`, also at the bottom
of the stack, just accepts a simple boolean.)
This errs on the side of warning the user, even if the
new message is partially visible.
Fixes #11138
2019-01-07 21:00:03 +01:00
|
|
|
|
|
|
|
if (needed_rerender) {
|
|
|
|
render_info = {need_user_to_scroll: true};
|
|
|
|
}
|
|
|
|
|
|
|
|
return render_info;
|
2013-08-16 17:10:22 +02:00
|
|
|
},
|
|
|
|
|
2020-07-20 22:18:43 +02:00
|
|
|
prepend(messages) {
|
2013-08-16 17:10:22 +02:00
|
|
|
this._render_win_start += messages.length;
|
|
|
|
this._render_win_end += messages.length;
|
|
|
|
|
2019-11-02 00:06:25 +01:00
|
|
|
const cur_window_size = this._render_win_end - this._render_win_start;
|
2013-11-07 16:44:17 +01:00
|
|
|
if (cur_window_size < this._RENDER_WINDOW_SIZE) {
|
2019-11-02 00:06:25 +01:00
|
|
|
const msgs_to_render_count = this._RENDER_WINDOW_SIZE - cur_window_size;
|
|
|
|
const slice_to_render = messages.slice(messages.length - msgs_to_render_count);
|
2020-07-15 01:29:15 +02:00
|
|
|
this.render(slice_to_render, "top", false);
|
2013-11-07 16:44:17 +01:00
|
|
|
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
|
|
|
},
|
|
|
|
|
2020-07-20 22:18:43 +02:00
|
|
|
clear_table() {
|
2013-08-16 17:10:22 +02:00
|
|
|
// 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();
|
2020-02-12 06:38:21 +01:00
|
|
|
this._rows.clear();
|
2014-03-18 18:20:29 +01:00
|
|
|
this._message_groups = [];
|
2020-02-12 06:39:40 +01:00
|
|
|
this.message_containers.clear();
|
2013-08-16 17:10:22 +02:00
|
|
|
},
|
|
|
|
|
2020-07-20 22:18:43 +02:00
|
|
|
get_row(id) {
|
2020-02-12 06:38:21 +01:00
|
|
|
const row = this._rows.get(id);
|
Use jQuery objects in MessageListView._rows.
The values of this dictionary used to be raw DOM elements,
but get_row() wraps them again, so there's not a huge
reason to store them as raw DOM elements internally. It
is slightly easier to reason about the code if everything
stays at the jQuery level.
To preserve the old behavior here, we have to do something
that is kind of ugly, but at least it's explicit now. In
the old code, our cache was DOM elements, and if an id
wasn't in the cache, we would sneakily return $(undefined)
with this code in get_row():
return $(this._rows[id]);
And it turns out that $(undefined) is basically just a
zero-element jQuery object. A lot of our code depends
on this behavior and just works around the zero-element
objects as needed with checks like this:
if (this.selected_row()).length === 0) {
// don't try to get offset
}
For now we just preserve this behavior. We could eventually
be more strict here, or at least have aggressive warnings
on cache misses, but we'd need to retrofit code to be
able to call something like `has_rendered_selection()`
and/or deal with `undefined` as the return value for the case
where the selection hasn't been rendered.
Here is some example code that would cause tracebacks if
we just returned `undefined` for cache misses:
rerender_preserving_scrolltop: function () {
// old_offset is the number of pixels between the top of the
// viewable window and the selected message
var old_offset;
var selected_row = this.selected_row();
var selected_in_view = selected_row.length > 0;
if (selected_in_view) {
old_offset = selected_row.offset().top;
}
return this.rerender_with_target_scrolltop(selected_row,
old_offset);
},
2018-07-12 23:11:59 +02:00
|
|
|
|
|
|
|
if (row === undefined) {
|
|
|
|
// For legacy reasons we need to return an empty
|
|
|
|
// jQuery object here.
|
|
|
|
return $(undefined);
|
|
|
|
}
|
|
|
|
|
|
|
|
return row;
|
2013-08-16 17:10:22 +02:00
|
|
|
},
|
|
|
|
|
2020-07-20 22:18:43 +02:00
|
|
|
clear_trailing_bookend() {
|
2020-07-15 01:29:15 +02:00
|
|
|
const trailing_bookend = rows.get_table(this.table_name).find(".trailing_bookend");
|
2013-08-16 17:10:22 +02:00
|
|
|
trailing_bookend.remove();
|
|
|
|
},
|
|
|
|
|
2020-07-20 22:18:43 +02:00
|
|
|
render_trailing_bookend(trailing_bookend_content, subscribed, show_button) {
|
2020-07-15 00:34:28 +02:00
|
|
|
const rendered_trailing_bookend = $(
|
|
|
|
render_bookend({
|
|
|
|
bookend_content: trailing_bookend_content,
|
|
|
|
trailing: show_button,
|
2020-07-20 22:18:43 +02:00
|
|
|
subscribed,
|
2020-07-15 00:34:28 +02:00
|
|
|
}),
|
|
|
|
);
|
2013-08-16 17:10:22 +02:00
|
|
|
rows.get_table(this.table_name).append(rendered_trailing_bookend);
|
|
|
|
},
|
|
|
|
|
2020-07-20 22:18:43 +02:00
|
|
|
selected_row() {
|
2013-08-16 17:10:22 +02:00
|
|
|
return this.get_row(this.list.selected_id());
|
|
|
|
},
|
|
|
|
|
2020-07-20 22:18:43 +02:00
|
|
|
get_message(id) {
|
2013-08-16 17:10:22 +02:00
|
|
|
return this.list.get(id);
|
2013-12-19 17:03:08 +01:00
|
|
|
},
|
|
|
|
|
2020-07-20 22:18:43 +02:00
|
|
|
change_message_id(old_id, new_id) {
|
2020-02-12 06:38:21 +01:00
|
|
|
if (this._rows.has(old_id)) {
|
|
|
|
const row = this._rows.get(old_id);
|
|
|
|
this._rows.delete(old_id);
|
2013-12-19 17:03:08 +01:00
|
|
|
|
2020-07-15 01:29:15 +02:00
|
|
|
row.attr("zid", new_id);
|
|
|
|
row.attr("id", this.table_name + new_id);
|
|
|
|
row.removeClass("local");
|
2020-02-12 06:38:21 +01:00
|
|
|
this._rows.set(new_id, row);
|
2013-12-19 17:03:08 +01:00
|
|
|
}
|
2014-03-14 16:28:54 +01:00
|
|
|
|
2020-02-12 06:39:40 +01:00
|
|
|
if (this.message_containers.has(old_id)) {
|
|
|
|
const message_container = this.message_containers.get(old_id);
|
|
|
|
this.message_containers.delete(old_id);
|
|
|
|
this.message_containers.set(new_id, message_container);
|
2014-03-14 16:28:54 +01:00
|
|
|
}
|
2016-05-31 14:24:58 +02:00
|
|
|
},
|
|
|
|
|
2020-07-20 22:18:43 +02:00
|
|
|
_maybe_format_me_message(message_container) {
|
2016-05-31 14:24:58 +02:00
|
|
|
if (message_container.msg.is_me_message) {
|
2018-12-29 11:07:27 +01:00
|
|
|
// Slice the '<p>/me ' off the front, and '</p>' off the first line
|
|
|
|
// 'p' tag is sliced off to get sender in the same line as the
|
|
|
|
// first line of the message
|
2019-11-02 00:06:25 +01:00
|
|
|
const msg_content = message_container.msg.content;
|
2020-07-15 01:29:15 +02:00
|
|
|
const p_index = msg_content.indexOf("</p>");
|
2020-07-15 00:34:28 +02:00
|
|
|
message_container.status_message =
|
|
|
|
msg_content.slice("<p>/me ".length, p_index) +
|
|
|
|
msg_content.slice(p_index + "</p>".length);
|
2016-05-31 14:24:58 +02:00
|
|
|
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
|
|
|
};
|
|
|
|
|
2019-10-25 09:45:13 +02:00
|
|
|
module.exports = MessageListView;
|
2018-05-28 08:04:36 +02:00
|
|
|
window.MessageListView = MessageListView;
|