mirror of
https://github.com/zulip/zulip.git
synced 2025-11-06 15:03:34 +00:00
Previously, we showed "View source" icon if content editing was not allowed, now we show the move message icon instead if moving message is allowed but content editing is not. We remove the hotkey from tooltip for now and would add a different hotkey for moving messages in further commits. We also add a common class for all the three icons in this commit to avoid duplication and use different class if required for click handlers.
425 lines
12 KiB
JavaScript
425 lines
12 KiB
JavaScript
import autosize from "autosize";
|
|
import $ from "jquery";
|
|
|
|
import * as blueslip from "./blueslip";
|
|
import {MessageListData} from "./message_list_data";
|
|
import {MessageListView} from "./message_list_view";
|
|
import * as narrow_banner from "./narrow_banner";
|
|
import * as narrow_state from "./narrow_state";
|
|
import {page_params} from "./page_params";
|
|
import * as stream_data from "./stream_data";
|
|
|
|
export let narrowed;
|
|
|
|
export function set_narrowed(value) {
|
|
narrowed = value;
|
|
}
|
|
|
|
export class MessageList {
|
|
constructor(opts) {
|
|
if (opts.data) {
|
|
this.data = opts.data;
|
|
} else {
|
|
const filter = opts.filter;
|
|
|
|
this.data = new MessageListData({
|
|
excludes_muted_topics: opts.excludes_muted_topics,
|
|
filter,
|
|
});
|
|
}
|
|
|
|
const collapse_messages = this.data.filter.supports_collapsing_recipients();
|
|
const table_name = opts.table_name;
|
|
this.view = new MessageListView(this, table_name, collapse_messages);
|
|
this.table_name = table_name;
|
|
this.narrowed = this.table_name === "zfilt";
|
|
this.num_appends = 0;
|
|
this.reading_prevented = false;
|
|
|
|
return this;
|
|
}
|
|
|
|
prevent_reading() {
|
|
this.reading_prevented = true;
|
|
}
|
|
|
|
resume_reading() {
|
|
this.reading_prevented = false;
|
|
}
|
|
|
|
add_messages(messages, opts) {
|
|
// This adds all messages to our data, but only returns
|
|
// the currently viewable ones.
|
|
const info = this.data.add_messages(messages);
|
|
|
|
const top_messages = info.top_messages;
|
|
const bottom_messages = info.bottom_messages;
|
|
const interior_messages = info.interior_messages;
|
|
|
|
// Currently we only need data back from rendering to
|
|
// tell us whether users needs to scroll, which only
|
|
// applies for `append_to_view`, but this may change over
|
|
// time.
|
|
let render_info;
|
|
|
|
if (interior_messages.length > 0) {
|
|
this.view.rerender_preserving_scrolltop(true);
|
|
return true;
|
|
}
|
|
if (top_messages.length > 0) {
|
|
this.view.prepend(top_messages);
|
|
}
|
|
|
|
if (bottom_messages.length > 0) {
|
|
render_info = this.append_to_view(bottom_messages, opts);
|
|
}
|
|
|
|
if (this === narrowed && !this.empty()) {
|
|
// If adding some new messages to the message tables caused
|
|
// our current narrow to no longer be empty, hide the empty
|
|
// feed placeholder text.
|
|
narrow_banner.hide_empty_narrow_message();
|
|
}
|
|
|
|
if (this === narrowed && !this.empty() && this.selected_id() === -1) {
|
|
// And also select the newly arrived message.
|
|
this.select_id(this.selected_id(), {then_scroll: true, use_closest: true});
|
|
}
|
|
|
|
return render_info;
|
|
}
|
|
|
|
get(id) {
|
|
return this.data.get(id);
|
|
}
|
|
|
|
num_items() {
|
|
return this.data.num_items();
|
|
}
|
|
|
|
empty() {
|
|
return this.data.empty();
|
|
}
|
|
|
|
first() {
|
|
return this.data.first();
|
|
}
|
|
|
|
last() {
|
|
return this.data.last();
|
|
}
|
|
|
|
ids_greater_or_equal_than(id) {
|
|
return this.data.ids_greater_or_equal_than(id);
|
|
}
|
|
|
|
prev() {
|
|
return this.data.prev();
|
|
}
|
|
|
|
next() {
|
|
return this.data.next();
|
|
}
|
|
|
|
is_at_end() {
|
|
return this.data.is_at_end();
|
|
}
|
|
|
|
nth_most_recent_id(n) {
|
|
return this.data.nth_most_recent_id(n);
|
|
}
|
|
|
|
is_search() {
|
|
return this.data.is_search();
|
|
}
|
|
|
|
can_mark_messages_read() {
|
|
/* Automatically marking messages as read can be disabled for
|
|
two different reasons:
|
|
* The view is structurally a search view, encoded in the
|
|
properties of the message_list_data object.
|
|
* The user recently marked messages in the view as unread, and
|
|
we don't want to lose that state.
|
|
*/
|
|
return this.data.can_mark_messages_read() && !this.reading_prevented;
|
|
}
|
|
|
|
clear({clear_selected_id = true} = {}) {
|
|
this.data.clear();
|
|
this.view.clear_rendering_state(true);
|
|
|
|
if (clear_selected_id) {
|
|
this.data.clear_selected_id();
|
|
}
|
|
}
|
|
|
|
selected_id() {
|
|
return this.data.selected_id();
|
|
}
|
|
|
|
select_id(id, opts) {
|
|
opts = {
|
|
then_scroll: false,
|
|
target_scroll_offset: undefined,
|
|
use_closest: false,
|
|
empty_ok: false,
|
|
mark_read: true,
|
|
force_rerender: false,
|
|
...opts,
|
|
id,
|
|
msg_list: this,
|
|
previously_selected_id: this.data.selected_id(),
|
|
};
|
|
|
|
const convert_id = (str_id) => {
|
|
const id = Number.parseFloat(str_id);
|
|
if (Number.isNaN(id)) {
|
|
throw new TypeError("Bad message id " + str_id);
|
|
}
|
|
return id;
|
|
};
|
|
|
|
id = convert_id(id);
|
|
|
|
const closest_id = this.closest_id(id);
|
|
|
|
let error_data;
|
|
|
|
// The name "use_closest" option is a bit legacy. We
|
|
// are always gonna move to the closest visible id; the flag
|
|
// just says whether we call blueslip.error or not. The caller
|
|
// sets use_closest to true when it expects us to move the
|
|
// pointer as needed, so only generate an error if the flag is
|
|
// false.
|
|
if (!opts.use_closest && closest_id !== id) {
|
|
error_data = {
|
|
table_name: this.table_name,
|
|
id,
|
|
closest_id,
|
|
};
|
|
blueslip.error("Selected message id not in MessageList", error_data);
|
|
}
|
|
|
|
if (closest_id === -1 && !opts.empty_ok) {
|
|
error_data = {
|
|
table_name: this.table_name,
|
|
id,
|
|
items_length: this.data.num_items(),
|
|
};
|
|
blueslip.error("Cannot select id -1", error_data);
|
|
throw new Error("Cannot select id -1");
|
|
}
|
|
|
|
id = closest_id;
|
|
opts.id = id;
|
|
this.data.set_selected_id(id);
|
|
|
|
if (opts.force_rerender) {
|
|
this.rerender();
|
|
} else if (!opts.from_rendering) {
|
|
this.view.maybe_rerender();
|
|
}
|
|
|
|
$(document).trigger(new $.Event("message_selected.zulip", opts));
|
|
}
|
|
|
|
selected_message() {
|
|
return this.get(this.data.selected_id());
|
|
}
|
|
|
|
selected_row() {
|
|
return this.get_row(this.data.selected_id());
|
|
}
|
|
|
|
closest_id(id) {
|
|
return this.data.closest_id(id);
|
|
}
|
|
|
|
advance_past_messages(msg_ids) {
|
|
return this.data.advance_past_messages(msg_ids);
|
|
}
|
|
|
|
selected_idx() {
|
|
return this.data.selected_idx();
|
|
}
|
|
|
|
// Maintains a trailing bookend element explaining any changes in
|
|
// your subscribed/unsubscribed status at the bottom of the
|
|
// message list.
|
|
update_trailing_bookend() {
|
|
this.view.clear_trailing_bookend();
|
|
if (!this.narrowed) {
|
|
return;
|
|
}
|
|
const stream_name = narrow_state.stream();
|
|
if (stream_name === undefined) {
|
|
return;
|
|
}
|
|
|
|
let deactivated = false;
|
|
let just_unsubscribed = false;
|
|
const subscribed = stream_data.is_subscribed_by_name(stream_name);
|
|
const sub = stream_data.get_sub(stream_name);
|
|
const can_toggle_subscription =
|
|
sub !== undefined && stream_data.can_toggle_subscription(sub);
|
|
if (sub === undefined) {
|
|
deactivated = true;
|
|
} else if (!subscribed && !this.last_message_historical) {
|
|
just_unsubscribed = true;
|
|
}
|
|
this.view.render_trailing_bookend(
|
|
stream_name,
|
|
subscribed,
|
|
deactivated,
|
|
just_unsubscribed,
|
|
can_toggle_subscription,
|
|
page_params.is_spectator,
|
|
);
|
|
}
|
|
|
|
unmuted_messages(messages) {
|
|
return this.data.unmuted_messages(messages);
|
|
}
|
|
|
|
append(messages, opts) {
|
|
const viewable_messages = this.data.append(messages);
|
|
this.append_to_view(viewable_messages, opts);
|
|
}
|
|
|
|
append_to_view(messages, {messages_are_new = false} = {}) {
|
|
this.num_appends += 1;
|
|
const render_info = this.view.append(messages, messages_are_new);
|
|
return render_info;
|
|
}
|
|
|
|
remove_and_rerender(message_ids) {
|
|
this.data.remove(message_ids);
|
|
this.rerender();
|
|
}
|
|
|
|
show_edit_message($row, edit_obj) {
|
|
if ($row.find(".message_edit_form form").length !== 0) {
|
|
return;
|
|
}
|
|
$row.find(".message_edit_form").append(edit_obj.$form);
|
|
$row.find(".message_content, .status-message, .message_controls").hide();
|
|
$row.find(".message_edit").css("display", "block");
|
|
autosize($row.find(".message_edit_content"));
|
|
}
|
|
|
|
hide_edit_message($row) {
|
|
$row.find(".message_content, .status-message, .message_controls").show();
|
|
$row.find(".message_edit_form").empty();
|
|
$row.find(".message_edit").hide();
|
|
$row.trigger("mouseleave");
|
|
}
|
|
|
|
show_edit_topic_on_recipient_row($recipient_row, $form) {
|
|
$recipient_row.find(".topic_edit_form").append($form);
|
|
$recipient_row.find(".on_hover_topic_edit").hide();
|
|
$recipient_row.find(".edit_message_button").hide();
|
|
$recipient_row.find(".stream_topic").hide();
|
|
$recipient_row.find(".topic_edit").show();
|
|
$recipient_row.find(".always_visible_topic_edit").hide();
|
|
}
|
|
|
|
hide_edit_topic_on_recipient_row($recipient_row) {
|
|
$recipient_row.find(".stream_topic").show();
|
|
$recipient_row.find(".on_hover_topic_edit").show();
|
|
$recipient_row.find(".edit_message_button").show();
|
|
$recipient_row.find(".topic_edit_form").empty();
|
|
$recipient_row.find(".topic_edit").hide();
|
|
$recipient_row.find(".always_visible_topic_edit").show();
|
|
}
|
|
|
|
show_message_as_read(message, options) {
|
|
const $row = this.get_row(message.id);
|
|
if (options.from === "pointer" || options.from === "server") {
|
|
$row.find(".unread_marker").addClass("fast_fade");
|
|
} else {
|
|
$row.find(".unread_marker").addClass("slow_fade");
|
|
}
|
|
$row.removeClass("unread");
|
|
}
|
|
|
|
reselect_selected_id() {
|
|
const selected_id = this.data.selected_id();
|
|
|
|
if (selected_id !== -1) {
|
|
this.select_id(this.data.selected_id(), {from_rendering: true, mark_read: false});
|
|
}
|
|
}
|
|
|
|
rerender_view() {
|
|
this.view.rerender_preserving_scrolltop();
|
|
this.reselect_selected_id();
|
|
}
|
|
|
|
rerender() {
|
|
// We need to clear the rendering state, rather than just
|
|
// doing clear_table, since we want to potentially recollapse
|
|
// things.
|
|
this.data.reset_select_to_closest();
|
|
this.view.clear_rendering_state(false);
|
|
this.view.update_render_window(this.selected_idx(), false);
|
|
|
|
if (this === narrowed) {
|
|
if (this.empty()) {
|
|
narrow_banner.show_empty_narrow_message();
|
|
} else {
|
|
narrow_banner.hide_empty_narrow_message();
|
|
}
|
|
}
|
|
this.rerender_view();
|
|
}
|
|
|
|
update_muting_and_rerender() {
|
|
this.data.update_items_for_muting();
|
|
// We need to rerender whether or not the narrow hides muted
|
|
// topics, because we need to update recipient bars for topics
|
|
// we've muted when we are displaying those topics.
|
|
//
|
|
// We could avoid a rerender if we can provide that this
|
|
// narrow cannot have contained messages to muted topics
|
|
// either before or after the state change. The right place
|
|
// to do this is in the message_events.js code path for
|
|
// processing topic edits, since that's the only place we'll
|
|
// call this frequently anyway.
|
|
//
|
|
// But in any case, we need to rerender the list for user muting,
|
|
// to make sure only the right messages are hidden.
|
|
this.rerender();
|
|
}
|
|
|
|
all_messages() {
|
|
return this.data.all_messages();
|
|
}
|
|
|
|
first_unread_message_id() {
|
|
return this.data.first_unread_message_id();
|
|
}
|
|
|
|
has_unread_messages() {
|
|
return this.data.has_unread_messages();
|
|
}
|
|
|
|
message_range(start, end) {
|
|
return this.data.message_range(start, end);
|
|
}
|
|
|
|
get_row(id) {
|
|
return this.view.get_row(id);
|
|
}
|
|
|
|
change_message_id(old_id, new_id) {
|
|
const require_rerender = this.data.change_message_id(old_id, new_id);
|
|
if (require_rerender) {
|
|
this.rerender_view();
|
|
}
|
|
}
|
|
|
|
get_last_message_sent_by_me() {
|
|
return this.data.get_last_message_sent_by_me();
|
|
}
|
|
}
|