2024-05-30 19:07:59 +02:00
|
|
|
import assert from "minimalistic-assert";
|
|
|
|
|
2024-11-12 03:59:37 +01:00
|
|
|
import * as blueslip from "./blueslip.ts";
|
|
|
|
import {FetchStatus} from "./fetch_status.ts";
|
|
|
|
import type {Filter} from "./filter.ts";
|
|
|
|
import type {Message} from "./message_store.ts";
|
|
|
|
import * as muted_users from "./muted_users.ts";
|
|
|
|
import {current_user} from "./state_data.ts";
|
|
|
|
import * as user_topics from "./user_topics.ts";
|
|
|
|
import * as util from "./util.ts";
|
2020-07-25 02:02:35 +02:00
|
|
|
|
2021-02-24 05:00:56 +01:00
|
|
|
export class MessageListData {
|
2024-04-17 11:26:31 +02:00
|
|
|
// The Filter object defines which messages match the narrow,
|
|
|
|
// and defines most of the configuration for the MessageListData.
|
2023-12-22 07:35:01 +01:00
|
|
|
filter: Filter;
|
2024-04-17 11:26:31 +02:00
|
|
|
// The FetchStatus object keeps track of our understanding of
|
|
|
|
// to what extent this MessageListData has all the messages
|
|
|
|
// that the server possesses matching this narrow, and whether
|
|
|
|
// we're in the progress of fetching more.
|
2023-12-22 07:35:01 +01:00
|
|
|
fetch_status: FetchStatus;
|
2024-04-17 11:26:31 +02:00
|
|
|
// _all_items is a sorted list of all message objects that
|
|
|
|
// match this.filter, regardless of muting.
|
|
|
|
//
|
|
|
|
// Most code will instead use _items, which contains
|
|
|
|
// only messages that should be displayed after excluding
|
|
|
|
// muted topics and messages sent by muted users.
|
2023-12-22 07:35:01 +01:00
|
|
|
_all_items: Message[];
|
|
|
|
_items: Message[];
|
2024-04-17 11:26:31 +02:00
|
|
|
// _hash contains the same messages as _all_items, mapped by
|
|
|
|
// message ID. It's used to efficiently query if a given
|
|
|
|
// message is present.
|
2023-12-22 07:35:01 +01:00
|
|
|
_hash: Map<number, Message>;
|
2024-04-17 11:26:31 +02:00
|
|
|
// Some views exclude muted topics.
|
|
|
|
//
|
|
|
|
// TODO: Refactor this to be a property of Filter, rather than
|
|
|
|
// a parameter that needs to be passed into the constructor.
|
2023-12-22 07:35:01 +01:00
|
|
|
excludes_muted_topics: boolean;
|
2024-04-17 11:26:31 +02:00
|
|
|
// Tracks any locally echoed messages, which we know aren't present on the server.
|
2023-12-22 07:35:01 +01:00
|
|
|
_local_only: Set<number>;
|
2024-04-17 11:26:31 +02:00
|
|
|
// The currently selected message ID. The special value -1
|
|
|
|
// there is no selected message. A common situation is when
|
|
|
|
// there are no messages matching the current filter.
|
2023-12-22 07:35:01 +01:00
|
|
|
_selected_id: number;
|
|
|
|
predicate?: (message: Message) => boolean;
|
2024-04-21 05:31:35 +02:00
|
|
|
// This is a callback that is called when messages are added to the message list.
|
2024-05-30 06:20:14 +02:00
|
|
|
add_messages_callback?: (messages: Message[], recipient_order_changed: boolean) => void;
|
2023-12-22 07:35:01 +01:00
|
|
|
|
2024-08-12 11:06:32 +02:00
|
|
|
// ID of the MessageList for which this MessageListData is the data.
|
|
|
|
// Using ID instead of the MessageList object itself to avoid circular dependencies
|
|
|
|
// and to allow MessageList objects to be easily garbage collected.
|
|
|
|
rendered_message_list_id: number | undefined;
|
|
|
|
|
2023-01-28 00:07:48 +01:00
|
|
|
// MessageListData is a core data structure for keeping track of a
|
|
|
|
// contiguous block of messages matching a given narrow that can
|
|
|
|
// be displayed in a Zulip message feed.
|
|
|
|
//
|
|
|
|
// See also MessageList and MessageListView, which are important
|
|
|
|
// to actually display a message list.
|
|
|
|
|
2023-12-22 07:35:01 +01:00
|
|
|
constructor({excludes_muted_topics, filter}: {excludes_muted_topics: boolean; filter: Filter}) {
|
2023-01-28 00:07:48 +01:00
|
|
|
this.filter = filter;
|
|
|
|
this.fetch_status = new FetchStatus();
|
2021-05-07 22:34:38 +02:00
|
|
|
this._all_items = [];
|
2020-07-23 00:26:13 +02:00
|
|
|
this._items = [];
|
|
|
|
this._hash = new Map();
|
2023-01-28 00:07:48 +01:00
|
|
|
this.excludes_muted_topics = excludes_muted_topics;
|
2020-07-23 00:26:13 +02:00
|
|
|
this._local_only = new Set();
|
2023-01-28 00:07:48 +01:00
|
|
|
this._selected_id = -1;
|
2020-07-23 00:26:13 +02:00
|
|
|
}
|
2018-05-04 12:44:28 +02:00
|
|
|
|
2024-08-12 11:06:32 +02:00
|
|
|
set_rendered_message_list_id(rendered_message_list_id: number | undefined): void {
|
|
|
|
this.rendered_message_list_id = rendered_message_list_id;
|
|
|
|
}
|
|
|
|
|
2024-06-15 15:26:40 +02:00
|
|
|
set_add_messages_callback(
|
|
|
|
callback: (messages: Message[], rows_order_changed: boolean) => void,
|
|
|
|
): void {
|
2024-04-21 05:31:35 +02:00
|
|
|
this.add_messages_callback = callback;
|
|
|
|
}
|
|
|
|
|
2023-12-22 07:35:01 +01:00
|
|
|
all_messages(): Message[] {
|
2018-05-04 12:44:28 +02:00
|
|
|
return this._items;
|
2020-07-23 00:26:13 +02:00
|
|
|
}
|
2018-05-04 12:44:28 +02:00
|
|
|
|
2023-12-22 07:35:01 +01:00
|
|
|
num_items(): number {
|
2018-05-04 12:44:28 +02:00
|
|
|
return this._items.length;
|
2020-07-23 00:26:13 +02:00
|
|
|
}
|
2018-05-04 12:44:28 +02:00
|
|
|
|
2023-05-02 04:04:46 +02:00
|
|
|
// The message list is completely empty.
|
2023-12-22 07:35:01 +01:00
|
|
|
empty(): boolean {
|
message_list: Fix incorrect usage of visible empty check.
The core logic for deciding whether newly fetched messages should be
prepended, appended, or inserted between existing messages was wrong
in the case that the message list was only visibly empty, but its
data structures contained some muted messages.
In particular, the _all_data data structure would end up having items
appended when they should be prepended; while this would eventually be
corrected if a rerender triggered a sort, it was a data corruption
with unknown secondary consequences, and in particular would mess up
any logic correctly using the first/last elements in _all_data.
Fix this by doing all of the logic using functions accessing
_all_items. While doing so, we simplify the logic by removing the
unnecessary special case for empty message lists, including the
parallel filter_incoming function, which added extra complexity that
should always produce the same result.
The message_list.empty helper wraps this method, and thus is corrected
as well.
2023-05-02 04:02:46 +02:00
|
|
|
return this._all_items.length === 0;
|
2023-05-02 04:04:46 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// The message list appears empty, but might contain messages that hidden by muting.
|
2023-12-22 07:35:01 +01:00
|
|
|
visibly_empty(): boolean {
|
2018-05-04 12:44:28 +02:00
|
|
|
return this._items.length === 0;
|
2020-07-23 00:26:13 +02:00
|
|
|
}
|
2018-05-04 12:44:28 +02:00
|
|
|
|
2024-05-24 01:17:25 +02:00
|
|
|
first(): Message | undefined {
|
2018-05-04 12:44:28 +02:00
|
|
|
return this._items[0];
|
2020-07-23 00:26:13 +02:00
|
|
|
}
|
2018-05-04 12:44:28 +02:00
|
|
|
|
2024-05-24 01:17:25 +02:00
|
|
|
first_including_muted(): Message | undefined {
|
message_list: Fix incorrect usage of visible empty check.
The core logic for deciding whether newly fetched messages should be
prepended, appended, or inserted between existing messages was wrong
in the case that the message list was only visibly empty, but its
data structures contained some muted messages.
In particular, the _all_data data structure would end up having items
appended when they should be prepended; while this would eventually be
corrected if a rerender triggered a sort, it was a data corruption
with unknown secondary consequences, and in particular would mess up
any logic correctly using the first/last elements in _all_data.
Fix this by doing all of the logic using functions accessing
_all_items. While doing so, we simplify the logic by removing the
unnecessary special case for empty message lists, including the
parallel filter_incoming function, which added extra complexity that
should always produce the same result.
The message_list.empty helper wraps this method, and thus is corrected
as well.
2023-05-02 04:02:46 +02:00
|
|
|
return this._all_items[0];
|
|
|
|
}
|
|
|
|
|
2023-12-22 07:35:01 +01:00
|
|
|
last(): Message | undefined {
|
2022-01-24 09:05:06 +01:00
|
|
|
return this._items.at(-1);
|
2020-07-23 00:26:13 +02:00
|
|
|
}
|
2018-05-04 12:44:28 +02:00
|
|
|
|
2023-12-22 07:35:01 +01:00
|
|
|
last_including_muted(): Message | undefined {
|
message_list: Fix incorrect usage of visible empty check.
The core logic for deciding whether newly fetched messages should be
prepended, appended, or inserted between existing messages was wrong
in the case that the message list was only visibly empty, but its
data structures contained some muted messages.
In particular, the _all_data data structure would end up having items
appended when they should be prepended; while this would eventually be
corrected if a rerender triggered a sort, it was a data corruption
with unknown secondary consequences, and in particular would mess up
any logic correctly using the first/last elements in _all_data.
Fix this by doing all of the logic using functions accessing
_all_items. While doing so, we simplify the logic by removing the
unnecessary special case for empty message lists, including the
parallel filter_incoming function, which added extra complexity that
should always produce the same result.
The message_list.empty helper wraps this method, and thus is corrected
as well.
2023-05-02 04:02:46 +02:00
|
|
|
return this._all_items.at(-1);
|
|
|
|
}
|
|
|
|
|
2024-05-21 05:57:30 +02:00
|
|
|
msg_id_in_fetched_range(msg_id: number): boolean {
|
|
|
|
if (this.empty()) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2024-05-24 01:17:25 +02:00
|
|
|
return this.first()!.id <= msg_id && msg_id <= this.last()!.id;
|
2024-05-21 05:57:30 +02:00
|
|
|
}
|
|
|
|
|
2023-12-22 07:35:01 +01:00
|
|
|
select_idx(): number | undefined {
|
2018-05-26 12:29:38 +02:00
|
|
|
if (this._selected_id === -1) {
|
2020-09-24 07:50:36 +02:00
|
|
|
return undefined;
|
2018-05-26 12:29:38 +02:00
|
|
|
}
|
2020-07-02 01:39:34 +02:00
|
|
|
const ids = this._items.map((message) => message.id);
|
2018-05-26 12:29:38 +02:00
|
|
|
|
2019-11-02 00:06:25 +01:00
|
|
|
const i = ids.indexOf(this._selected_id);
|
2018-05-26 12:29:38 +02:00
|
|
|
if (i === -1) {
|
2020-09-24 07:50:36 +02:00
|
|
|
return undefined;
|
2018-05-26 12:29:38 +02:00
|
|
|
}
|
|
|
|
return i;
|
2020-07-23 00:26:13 +02:00
|
|
|
}
|
2018-05-26 12:29:38 +02:00
|
|
|
|
2023-12-22 07:35:01 +01:00
|
|
|
prev(): number | undefined {
|
2019-11-02 00:06:25 +01:00
|
|
|
const i = this.select_idx();
|
2018-05-26 12:29:38 +02:00
|
|
|
|
|
|
|
if (i === undefined) {
|
2020-09-24 07:50:36 +02:00
|
|
|
return undefined;
|
2018-05-26 12:29:38 +02:00
|
|
|
}
|
2024-05-30 19:07:59 +02:00
|
|
|
return this._items[i - 1]?.id;
|
2020-07-23 00:26:13 +02:00
|
|
|
}
|
2018-05-26 12:29:38 +02:00
|
|
|
|
2023-12-22 07:35:01 +01:00
|
|
|
next(): number | undefined {
|
2019-11-02 00:06:25 +01:00
|
|
|
const i = this.select_idx();
|
2018-05-26 12:29:38 +02:00
|
|
|
|
|
|
|
if (i === undefined) {
|
2020-09-24 07:50:36 +02:00
|
|
|
return undefined;
|
2018-05-26 12:29:38 +02:00
|
|
|
}
|
2024-05-30 19:07:59 +02:00
|
|
|
return this._items[i + 1]?.id;
|
2020-07-23 00:26:13 +02:00
|
|
|
}
|
2018-05-26 12:29:38 +02:00
|
|
|
|
2023-12-22 07:35:01 +01:00
|
|
|
is_at_end(): boolean {
|
2018-05-29 00:18:27 +02:00
|
|
|
if (this._selected_id === -1) {
|
|
|
|
return false;
|
|
|
|
}
|
2024-05-30 19:07:59 +02:00
|
|
|
return this.last()?.id === this._selected_id;
|
2020-07-23 00:26:13 +02:00
|
|
|
}
|
2018-05-29 00:18:27 +02:00
|
|
|
|
2023-12-22 07:35:01 +01:00
|
|
|
clear(): void {
|
2021-05-07 22:34:38 +02:00
|
|
|
this._all_items = [];
|
2018-05-04 12:44:28 +02:00
|
|
|
this._items = [];
|
2020-02-12 06:32:42 +01:00
|
|
|
this._hash.clear();
|
2020-07-23 00:26:13 +02:00
|
|
|
}
|
2018-05-04 12:44:28 +02:00
|
|
|
|
2023-12-22 07:35:01 +01:00
|
|
|
// TODO(typescript): Ideally this should only take a number.
|
|
|
|
get(id: number | string): Message | undefined {
|
|
|
|
const number_id = typeof id === "number" ? id : Number.parseFloat(id);
|
|
|
|
if (Number.isNaN(number_id)) {
|
2020-09-24 07:50:36 +02:00
|
|
|
return undefined;
|
2018-05-04 12:44:28 +02:00
|
|
|
}
|
2023-12-22 07:35:01 +01:00
|
|
|
return this._hash.get(number_id);
|
2020-07-23 00:26:13 +02:00
|
|
|
}
|
2018-05-04 12:44:28 +02:00
|
|
|
|
2023-12-22 07:35:01 +01:00
|
|
|
clear_selected_id(): void {
|
2018-05-04 12:44:28 +02:00
|
|
|
this._selected_id = -1;
|
2020-07-23 00:26:13 +02:00
|
|
|
}
|
2018-05-04 12:44:28 +02:00
|
|
|
|
2023-12-22 07:35:01 +01:00
|
|
|
selected_id(): number {
|
2018-05-04 12:44:28 +02:00
|
|
|
return this._selected_id;
|
2020-07-23 00:26:13 +02:00
|
|
|
}
|
2018-05-04 12:44:28 +02:00
|
|
|
|
2023-12-22 07:35:01 +01:00
|
|
|
set_selected_id(id: number): void {
|
2018-05-04 12:44:28 +02:00
|
|
|
this._selected_id = id;
|
2020-07-23 00:26:13 +02:00
|
|
|
}
|
2018-05-04 12:44:28 +02:00
|
|
|
|
2023-12-22 07:35:01 +01:00
|
|
|
selected_idx(): number {
|
2018-05-04 12:44:28 +02:00
|
|
|
return this._lower_bound(this._selected_id);
|
2020-07-23 00:26:13 +02:00
|
|
|
}
|
2018-05-04 12:44:28 +02:00
|
|
|
|
2023-12-22 07:35:01 +01:00
|
|
|
reset_select_to_closest(): void {
|
2018-05-04 12:44:28 +02:00
|
|
|
this._selected_id = this.closest_id(this._selected_id);
|
2020-07-23 00:26:13 +02:00
|
|
|
}
|
2018-05-04 12:44:28 +02:00
|
|
|
|
2023-12-22 07:35:01 +01:00
|
|
|
is_keyword_search(): boolean {
|
2023-11-12 00:13:40 +01:00
|
|
|
return this.filter.is_keyword_search();
|
2020-07-23 00:26:13 +02:00
|
|
|
}
|
2023-12-22 07:35:01 +01:00
|
|
|
can_mark_messages_read(): boolean {
|
2019-07-10 02:03:41 +02:00
|
|
|
return this.filter.can_mark_messages_read();
|
2020-07-23 00:26:13 +02:00
|
|
|
}
|
2023-12-22 07:35:01 +01:00
|
|
|
_get_predicate(): (message: Message) => boolean {
|
2018-05-04 12:44:28 +02:00
|
|
|
// We cache this.
|
|
|
|
if (!this.predicate) {
|
|
|
|
this.predicate = this.filter.predicate();
|
|
|
|
}
|
|
|
|
return this.predicate;
|
2020-07-23 00:26:13 +02:00
|
|
|
}
|
2018-05-04 12:44:28 +02:00
|
|
|
|
2023-12-22 07:35:01 +01:00
|
|
|
valid_non_duplicated_messages(messages: Message[]): Message[] {
|
2019-11-02 00:06:25 +01:00
|
|
|
const predicate = this._get_predicate();
|
2020-07-23 00:26:13 +02:00
|
|
|
return messages.filter((msg) => this.get(msg.id) === undefined && predicate(msg));
|
|
|
|
}
|
2018-05-04 12:44:28 +02:00
|
|
|
|
2023-12-22 07:35:01 +01:00
|
|
|
messages_filtered_for_topic_mutes(messages: Message[]): Message[] {
|
2021-01-25 06:54:00 +01:00
|
|
|
if (!this.excludes_muted_topics) {
|
|
|
|
return [...messages];
|
|
|
|
}
|
|
|
|
|
|
|
|
return messages.filter((message) => {
|
|
|
|
if (message.type !== "stream") {
|
|
|
|
return true;
|
|
|
|
}
|
2021-06-28 20:39:04 +02:00
|
|
|
return (
|
2022-08-14 15:33:13 +02:00
|
|
|
!user_topics.is_topic_muted(message.stream_id, message.topic) || message.mentioned
|
2021-06-28 20:39:04 +02:00
|
|
|
);
|
2021-01-25 06:54:00 +01:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2023-12-22 07:35:01 +01:00
|
|
|
messages_filtered_for_user_mutes(messages: Message[]): Message[] {
|
2024-06-11 21:45:05 +02:00
|
|
|
if (this.filter.is_non_group_direct_message()) {
|
2023-06-16 13:23:39 +02:00
|
|
|
// We are in a 1:1 direct message narrow, so do not do any filtering.
|
2021-05-05 13:08:49 +02:00
|
|
|
return [...messages];
|
|
|
|
}
|
|
|
|
|
2021-05-07 22:34:38 +02:00
|
|
|
return messages.filter((message) => {
|
|
|
|
if (message.type !== "private") {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
const recipients = util.extract_pm_recipients(message.to_user_ids);
|
|
|
|
if (recipients.length > 1) {
|
2024-06-11 21:45:05 +02:00
|
|
|
// Direct message group message
|
2021-05-07 22:34:38 +02:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2024-08-20 07:18:02 +02:00
|
|
|
const recipient_id = Number.parseInt(util.the(recipients), 10);
|
2021-06-27 21:38:26 +02:00
|
|
|
return (
|
|
|
|
!muted_users.is_user_muted(recipient_id) &&
|
|
|
|
!muted_users.is_user_muted(message.sender_id)
|
|
|
|
);
|
2021-05-07 22:34:38 +02:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2023-12-22 07:35:01 +01:00
|
|
|
unmuted_messages(messages: Message[]): Message[] {
|
2021-05-07 22:34:38 +02:00
|
|
|
return this.messages_filtered_for_topic_mutes(
|
|
|
|
this.messages_filtered_for_user_mutes(messages),
|
|
|
|
);
|
2020-07-23 00:26:13 +02:00
|
|
|
}
|
2018-05-04 12:44:28 +02:00
|
|
|
|
2023-12-22 07:35:01 +01:00
|
|
|
update_items_for_muting(): void {
|
2018-05-04 12:44:28 +02:00
|
|
|
this._items = this.unmuted_messages(this._all_items);
|
2020-07-23 00:26:13 +02:00
|
|
|
}
|
2018-05-04 12:44:28 +02:00
|
|
|
|
2023-12-28 03:38:59 +01:00
|
|
|
first_unread_message_id(): number | undefined {
|
2024-05-21 05:57:30 +02:00
|
|
|
// NOTE: This function returns the first unread that was fetched and is not
|
|
|
|
// necessarily the first unread for the narrow. There could be more unread messages.
|
|
|
|
// See https://github.com/zulip/zulip/pull/30008#discussion_r1597279862 for how
|
|
|
|
// ideas on how to possibly improve this.
|
|
|
|
// before `first_unread` calculated below that we haven't fetched yet.
|
2024-09-28 00:46:01 +02:00
|
|
|
const first_unread = this._items.find((message) => message.unread);
|
2018-05-04 12:44:28 +02:00
|
|
|
|
|
|
|
if (first_unread) {
|
|
|
|
return first_unread.id;
|
|
|
|
}
|
|
|
|
|
2024-05-21 05:57:30 +02:00
|
|
|
// If no unread, return the bottom message
|
|
|
|
// NOTE: This is only valid if we have found the latest message for the narrow as
|
|
|
|
// there could be more message that we haven't fetched yet.
|
2023-12-28 03:38:59 +01:00
|
|
|
return this.last()?.id;
|
2020-07-23 00:26:13 +02:00
|
|
|
}
|
2018-05-04 12:44:28 +02:00
|
|
|
|
2023-12-22 07:35:01 +01:00
|
|
|
has_unread_messages(): boolean {
|
2024-09-28 00:46:01 +02:00
|
|
|
return this._items.some((message) => message.unread);
|
2022-02-16 01:30:23 +01:00
|
|
|
}
|
|
|
|
|
2023-12-22 07:35:01 +01:00
|
|
|
add_messages(messages: Message[]): {
|
|
|
|
top_messages: Message[];
|
|
|
|
bottom_messages: Message[];
|
|
|
|
interior_messages: Message[];
|
|
|
|
} {
|
2019-11-02 00:06:25 +01:00
|
|
|
let top_messages = [];
|
|
|
|
let bottom_messages = [];
|
|
|
|
let interior_messages = [];
|
2018-05-04 12:44:28 +02:00
|
|
|
|
message_list: Fix incorrect usage of visible empty check.
The core logic for deciding whether newly fetched messages should be
prepended, appended, or inserted between existing messages was wrong
in the case that the message list was only visibly empty, but its
data structures contained some muted messages.
In particular, the _all_data data structure would end up having items
appended when they should be prepended; while this would eventually be
corrected if a rerender triggered a sort, it was a data corruption
with unknown secondary consequences, and in particular would mess up
any logic correctly using the first/last elements in _all_data.
Fix this by doing all of the logic using functions accessing
_all_items. While doing so, we simplify the logic by removing the
unnecessary special case for empty message lists, including the
parallel filter_incoming function, which added extra complexity that
should always produce the same result.
The message_list.empty helper wraps this method, and thus is corrected
as well.
2023-05-02 04:02:46 +02:00
|
|
|
// Filter out duplicates that are already in self, and all messages
|
|
|
|
// that fail our filter predicate
|
|
|
|
messages = this.valid_non_duplicated_messages(messages);
|
|
|
|
|
|
|
|
for (const msg of messages) {
|
|
|
|
// Put messages in correct order on either side of the
|
|
|
|
// message list. This code path assumes that messages
|
|
|
|
// is a (1) sorted, and (2) consecutive block of
|
|
|
|
// messages that belong in this message list; those
|
|
|
|
// facts should be ensured by the caller.
|
2023-12-22 07:35:01 +01:00
|
|
|
const last = this.last_including_muted();
|
|
|
|
if (last === undefined || msg.id > last.id) {
|
message_list: Fix incorrect usage of visible empty check.
The core logic for deciding whether newly fetched messages should be
prepended, appended, or inserted between existing messages was wrong
in the case that the message list was only visibly empty, but its
data structures contained some muted messages.
In particular, the _all_data data structure would end up having items
appended when they should be prepended; while this would eventually be
corrected if a rerender triggered a sort, it was a data corruption
with unknown secondary consequences, and in particular would mess up
any logic correctly using the first/last elements in _all_data.
Fix this by doing all of the logic using functions accessing
_all_items. While doing so, we simplify the logic by removing the
unnecessary special case for empty message lists, including the
parallel filter_incoming function, which added extra complexity that
should always produce the same result.
The message_list.empty helper wraps this method, and thus is corrected
as well.
2023-05-02 04:02:46 +02:00
|
|
|
bottom_messages.push(msg);
|
2024-05-24 01:17:25 +02:00
|
|
|
} else if (msg.id < this.first_including_muted()!.id) {
|
message_list: Fix incorrect usage of visible empty check.
The core logic for deciding whether newly fetched messages should be
prepended, appended, or inserted between existing messages was wrong
in the case that the message list was only visibly empty, but its
data structures contained some muted messages.
In particular, the _all_data data structure would end up having items
appended when they should be prepended; while this would eventually be
corrected if a rerender triggered a sort, it was a data corruption
with unknown secondary consequences, and in particular would mess up
any logic correctly using the first/last elements in _all_data.
Fix this by doing all of the logic using functions accessing
_all_items. While doing so, we simplify the logic by removing the
unnecessary special case for empty message lists, including the
parallel filter_incoming function, which added extra complexity that
should always produce the same result.
The message_list.empty helper wraps this method, and thus is corrected
as well.
2023-05-02 04:02:46 +02:00
|
|
|
top_messages.push(msg);
|
|
|
|
} else {
|
|
|
|
interior_messages.push(msg);
|
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-05-04 12:44:28 +02:00
|
|
|
}
|
|
|
|
|
2018-05-14 12:39:34 +02:00
|
|
|
if (interior_messages.length > 0) {
|
2020-07-23 00:26:13 +02:00
|
|
|
interior_messages = this.add_anywhere(interior_messages);
|
2018-05-14 12:39:34 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if (top_messages.length > 0) {
|
2020-07-23 00:26:13 +02:00
|
|
|
top_messages = this.prepend(top_messages);
|
2018-05-14 12:39:34 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if (bottom_messages.length > 0) {
|
2020-07-23 00:26:13 +02:00
|
|
|
bottom_messages = this.append(bottom_messages);
|
2018-05-14 12:39:34 +02:00
|
|
|
}
|
|
|
|
|
2024-04-21 05:31:35 +02:00
|
|
|
if (this.add_messages_callback) {
|
2024-05-30 06:20:14 +02:00
|
|
|
// If we only added old messages, last message for any of the existing recipients didn't change.
|
|
|
|
// Although, it maybe have resulted in new recipients being added which should be handled by the caller.
|
|
|
|
const recipient_order_changed = !(
|
|
|
|
top_messages.length > 0 &&
|
|
|
|
bottom_messages.length === 0 &&
|
|
|
|
interior_messages.length === 0
|
|
|
|
);
|
|
|
|
this.add_messages_callback(messages, recipient_order_changed);
|
2024-04-21 05:31:35 +02:00
|
|
|
}
|
|
|
|
|
2019-11-02 00:06:25 +01:00
|
|
|
const info = {
|
2020-07-20 22:18:43 +02:00
|
|
|
top_messages,
|
|
|
|
bottom_messages,
|
|
|
|
interior_messages,
|
2018-06-04 22:39:25 +02:00
|
|
|
};
|
|
|
|
|
2018-05-14 12:39:34 +02:00
|
|
|
return info;
|
2020-07-23 00:26:13 +02:00
|
|
|
}
|
2018-05-04 12:44:28 +02:00
|
|
|
|
2023-12-22 07:35:01 +01:00
|
|
|
add_anywhere(messages: Message[]): Message[] {
|
2018-05-14 12:39:34 +02:00
|
|
|
// Caller should have already filtered messages.
|
|
|
|
// This should be used internally when we have
|
|
|
|
// "interior" messages to add and can't optimize
|
|
|
|
// things by only doing prepend or only doing append.
|
2021-01-25 06:54:00 +01:00
|
|
|
|
|
|
|
const viewable_messages = this.unmuted_messages(messages);
|
|
|
|
|
2023-03-02 01:58:25 +01:00
|
|
|
this._all_items = [...messages, ...this._all_items];
|
2021-05-07 22:34:38 +02:00
|
|
|
this._all_items.sort((a, b) => a.id - b.id);
|
2018-05-04 12:44:28 +02:00
|
|
|
|
2023-03-02 01:58:25 +01:00
|
|
|
this._items = [...viewable_messages, ...this._items];
|
2020-07-02 01:45:54 +02:00
|
|
|
this._items.sort((a, b) => a.id - b.id);
|
2021-01-25 06:54:00 +01:00
|
|
|
|
2018-05-04 12:44:28 +02:00
|
|
|
this._add_to_hash(messages);
|
|
|
|
return viewable_messages;
|
2020-07-23 00:26:13 +02:00
|
|
|
}
|
2018-05-04 12:44:28 +02:00
|
|
|
|
2023-12-22 07:35:01 +01:00
|
|
|
append(messages: Message[]): Message[] {
|
2018-05-04 12:44:28 +02:00
|
|
|
// Caller should have already filtered
|
2021-01-25 06:54:00 +01:00
|
|
|
const viewable_messages = this.unmuted_messages(messages);
|
|
|
|
|
2023-03-02 01:58:25 +01:00
|
|
|
this._all_items = [...this._all_items, ...messages];
|
|
|
|
this._items = [...this._items, ...viewable_messages];
|
2021-01-25 06:54:00 +01:00
|
|
|
|
2018-05-04 12:44:28 +02:00
|
|
|
this._add_to_hash(messages);
|
|
|
|
return viewable_messages;
|
2020-07-23 00:26:13 +02:00
|
|
|
}
|
2018-05-04 12:44:28 +02:00
|
|
|
|
2023-12-22 07:35:01 +01:00
|
|
|
prepend(messages: Message[]): Message[] {
|
2018-05-04 12:44:28 +02:00
|
|
|
// Caller should have already filtered
|
2021-01-25 06:54:00 +01:00
|
|
|
const viewable_messages = this.unmuted_messages(messages);
|
|
|
|
|
2023-03-02 01:58:25 +01:00
|
|
|
this._all_items = [...messages, ...this._all_items];
|
|
|
|
this._items = [...viewable_messages, ...this._items];
|
2021-01-25 06:54:00 +01:00
|
|
|
|
2018-05-04 12:44:28 +02:00
|
|
|
this._add_to_hash(messages);
|
|
|
|
return viewable_messages;
|
2020-07-23 00:26:13 +02:00
|
|
|
}
|
2018-05-04 12:44:28 +02:00
|
|
|
|
2023-12-22 07:35:01 +01:00
|
|
|
remove(message_ids: number[]): void {
|
2020-11-12 22:03:45 +01:00
|
|
|
const msg_ids_to_remove = new Set(message_ids);
|
|
|
|
for (const id of msg_ids_to_remove) {
|
|
|
|
this._hash.delete(id);
|
|
|
|
this._local_only.delete(id);
|
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
|
|
|
}
|
|
|
|
|
2021-01-23 02:36:54 +01:00
|
|
|
this._items = this._items.filter((msg) => !msg_ids_to_remove.has(msg.id));
|
2021-05-07 22:34:38 +02:00
|
|
|
this._all_items = this._all_items.filter((msg) => !msg_ids_to_remove.has(msg.id));
|
2020-07-23 00:26:13 +02:00
|
|
|
}
|
2018-05-04 12:44:28 +02:00
|
|
|
|
|
|
|
// Returns messages from the given message list in the specified range, inclusive
|
2023-12-22 07:35:01 +01:00
|
|
|
message_range(start: number, end: number): Message[] {
|
2018-05-04 12:44:28 +02:00
|
|
|
if (start === -1) {
|
|
|
|
blueslip.error("message_range given a start of -1");
|
|
|
|
}
|
|
|
|
|
2019-11-02 00:06:25 +01:00
|
|
|
const start_idx = this._lower_bound(start);
|
2020-07-16 23:29:01 +02:00
|
|
|
const end_idx = this._lower_bound(end);
|
2018-05-04 12:44:28 +02:00
|
|
|
return this._items.slice(start_idx, end_idx + 1);
|
2020-07-23 00:26:13 +02:00
|
|
|
}
|
2018-05-04 12:44:28 +02:00
|
|
|
|
|
|
|
// Returns the index where you could insert the desired ID
|
|
|
|
// into the message list, without disrupting the sort order
|
|
|
|
// This takes into account the potentially-unsorted
|
|
|
|
// nature of local message IDs in the message list
|
2023-12-22 07:35:01 +01:00
|
|
|
_lower_bound(id: number): number {
|
|
|
|
const less_func = (msg: Message, ref_id: number, a_idx: number): boolean => {
|
2020-07-23 00:26:13 +02:00
|
|
|
if (this._is_localonly_id(msg.id)) {
|
2018-05-04 12:44:28 +02:00
|
|
|
// First non-local message before this one
|
2020-07-23 00:26:13 +02:00
|
|
|
const effective = this._next_nonlocal_message(this._items, a_idx, (idx) => idx - 1);
|
2018-05-04 12:44:28 +02:00
|
|
|
if (effective) {
|
|
|
|
// Turn the 10.02 in [11, 10.02, 12] into 11.02
|
2020-10-07 09:17:30 +02:00
|
|
|
const decimal = Number.parseFloat((msg.id % 1).toFixed(0.02));
|
2019-11-02 00:06:25 +01:00
|
|
|
const effective_id = effective.id + decimal;
|
2018-05-04 12:44:28 +02:00
|
|
|
return effective_id < ref_id;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return msg.id < ref_id;
|
2020-07-23 00:26:13 +02:00
|
|
|
};
|
2018-05-04 12:44:28 +02:00
|
|
|
|
2020-07-23 00:26:13 +02:00
|
|
|
return util.lower_bound(this._items, id, less_func);
|
|
|
|
}
|
2018-05-04 12:44:28 +02:00
|
|
|
|
2023-12-22 07:35:01 +01:00
|
|
|
closest_id(id: number): number {
|
2018-05-04 12:44:28 +02:00
|
|
|
// We directly keep track of local-only messages,
|
|
|
|
// so if we're asked for one that we know we have,
|
|
|
|
// just return it directly
|
2020-02-12 06:34:41 +01:00
|
|
|
if (this._local_only.has(id)) {
|
2018-05-04 12:44:28 +02:00
|
|
|
return id;
|
|
|
|
}
|
|
|
|
|
2019-11-02 00:06:25 +01:00
|
|
|
const items = this._items;
|
2018-05-04 12:44:28 +02:00
|
|
|
|
|
|
|
if (items.length === 0) {
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
2019-11-02 00:06:25 +01:00
|
|
|
let closest = this._lower_bound(id);
|
2018-05-04 12:44:28 +02:00
|
|
|
|
2024-05-30 19:07:59 +02:00
|
|
|
if (id === items[closest]?.id) {
|
|
|
|
return id;
|
2018-05-04 12:44:28 +02:00
|
|
|
}
|
|
|
|
|
2019-11-02 00:06:25 +01:00
|
|
|
const potential_closest_matches = [];
|
2024-05-30 19:07:59 +02:00
|
|
|
let prev_item;
|
|
|
|
while (
|
|
|
|
(prev_item = items[closest - 1]) !== undefined &&
|
|
|
|
this._is_localonly_id(prev_item.id)
|
|
|
|
) {
|
2018-05-04 12:44:28 +02:00
|
|
|
// Since we treated all blocks of local ids as their left-most-non-local message
|
|
|
|
// for lower_bound purposes, find the real leftmost index (first non-local id)
|
2024-05-30 19:07:59 +02:00
|
|
|
potential_closest_matches.push(closest);
|
|
|
|
closest -= 1;
|
2018-05-04 12:44:28 +02:00
|
|
|
}
|
|
|
|
potential_closest_matches.push(closest);
|
|
|
|
|
2024-05-30 19:07:59 +02:00
|
|
|
let item = items[closest];
|
|
|
|
if (item === undefined) {
|
|
|
|
item = items[closest - 1];
|
2018-05-04 12:44:28 +02:00
|
|
|
} else {
|
|
|
|
// Any of the ids that we skipped over (due to them being local-only) might be the
|
|
|
|
// closest ID to the desired one, in case there is no exact match.
|
2022-01-24 09:20:01 +01:00
|
|
|
potential_closest_matches.unshift(closest - 1);
|
2024-05-30 19:07:59 +02:00
|
|
|
let best_match = item.id;
|
2018-05-04 12:44:28 +02: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 potential_idx of potential_closest_matches) {
|
2018-05-04 12:44:28 +02:00
|
|
|
if (potential_idx < 0) {
|
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
|
|
|
continue;
|
2018-05-04 12:44:28 +02:00
|
|
|
}
|
2024-05-30 19:07:59 +02:00
|
|
|
const potential_item = items[potential_idx];
|
2018-05-04 12:44:28 +02:00
|
|
|
|
2024-05-30 19:07:59 +02:00
|
|
|
if (potential_item === undefined) {
|
2020-07-15 01:29:15 +02:00
|
|
|
blueslip.warn("Invalid potential_idx: " + potential_idx);
|
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
|
|
|
continue;
|
2018-05-04 12:44:28 +02:00
|
|
|
}
|
|
|
|
|
2024-05-30 19:07:59 +02:00
|
|
|
const potential_match = potential_item.id;
|
2018-05-04 12:44:28 +02:00
|
|
|
// If the potential id is the closest to the requested, save that one
|
|
|
|
if (Math.abs(id - potential_match) < Math.abs(best_match - id)) {
|
|
|
|
best_match = potential_match;
|
2024-05-30 19:07:59 +02:00
|
|
|
item = potential_item;
|
2018-05-04 12:44:28 +02: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
|
|
|
}
|
2018-05-04 12:44:28 +02:00
|
|
|
}
|
2024-05-30 19:07:59 +02:00
|
|
|
assert(item !== undefined);
|
|
|
|
return item.id;
|
2020-07-23 00:26:13 +02:00
|
|
|
}
|
2018-05-04 12:44:28 +02:00
|
|
|
|
2023-12-22 07:35:01 +01:00
|
|
|
advance_past_messages(msg_ids: number[]): void {
|
2018-05-04 12:44:28 +02:00
|
|
|
// Start with the current pointer, but then keep advancing the
|
|
|
|
// pointer while the next message's id is in msg_ids. See trac #1555
|
|
|
|
// for more context, but basically we are skipping over contiguous
|
|
|
|
// messages that we have recently visited.
|
2019-11-02 00:06:25 +01:00
|
|
|
let next_msg_id = 0;
|
2018-05-04 12:44:28 +02:00
|
|
|
|
2020-02-12 06:36:52 +01:00
|
|
|
const id_set = new Set(msg_ids);
|
2018-05-04 12:44:28 +02:00
|
|
|
|
2019-11-02 00:06:25 +01:00
|
|
|
let idx = this.selected_idx() + 1;
|
2018-05-04 12:44:28 +02:00
|
|
|
while (idx < this._items.length) {
|
2024-05-24 00:02:38 +02:00
|
|
|
const msg_id = this._items[idx]!.id;
|
2020-02-12 06:36:52 +01:00
|
|
|
if (!id_set.has(msg_id)) {
|
2018-05-04 12:44:28 +02:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
next_msg_id = msg_id;
|
|
|
|
idx += 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (next_msg_id > 0) {
|
|
|
|
this.set_selected_id(next_msg_id);
|
|
|
|
}
|
2020-07-23 00:26:13 +02:00
|
|
|
}
|
2018-05-04 12:44:28 +02:00
|
|
|
|
2023-12-22 07:35:01 +01:00
|
|
|
_add_to_hash(messages: Message[]): void {
|
2021-01-22 22:29:08 +01:00
|
|
|
for (const elem of messages) {
|
2023-12-22 07:35:01 +01:00
|
|
|
const id = elem.id;
|
2020-07-23 00:26:13 +02:00
|
|
|
if (this._is_localonly_id(id)) {
|
|
|
|
this._local_only.add(id);
|
2018-05-04 12:44:28 +02:00
|
|
|
}
|
2020-07-23 00:26:13 +02:00
|
|
|
if (this._hash.has(id)) {
|
2018-05-04 12:44:28 +02:00
|
|
|
blueslip.error("Duplicate message added to MessageListData");
|
2021-01-22 22:29:08 +01:00
|
|
|
continue;
|
2018-05-04 12:44:28 +02:00
|
|
|
}
|
2020-07-23 00:26:13 +02:00
|
|
|
this._hash.set(id, elem);
|
2021-01-22 22:29:08 +01:00
|
|
|
}
|
2020-07-23 00:26:13 +02:00
|
|
|
}
|
2018-05-04 12:44:28 +02:00
|
|
|
|
2023-12-22 07:35:01 +01:00
|
|
|
_is_localonly_id(id: number): boolean {
|
2018-05-04 12:44:28 +02:00
|
|
|
return id % 1 !== 0;
|
2020-07-23 00:26:13 +02:00
|
|
|
}
|
2018-05-04 12:44:28 +02:00
|
|
|
|
2023-12-22 07:35:01 +01:00
|
|
|
_next_nonlocal_message(
|
|
|
|
item_list: Message[],
|
|
|
|
start_index: number,
|
|
|
|
op: (idx: number) => number,
|
2024-05-24 03:50:39 +02:00
|
|
|
): Message | undefined {
|
2019-11-02 00:06:25 +01:00
|
|
|
let cur_idx = start_index;
|
2024-05-24 03:50:39 +02:00
|
|
|
let message;
|
2018-05-04 12:44:28 +02:00
|
|
|
do {
|
|
|
|
cur_idx = op(cur_idx);
|
2024-05-24 03:50:39 +02:00
|
|
|
message = item_list[cur_idx];
|
|
|
|
} while (message !== undefined && this._is_localonly_id(message.id));
|
|
|
|
return message;
|
2020-07-23 00:26:13 +02:00
|
|
|
}
|
2018-05-04 12:44:28 +02:00
|
|
|
|
2023-12-22 07:35:01 +01:00
|
|
|
change_message_id(old_id: number, new_id: number): boolean {
|
2018-05-04 12:44:28 +02:00
|
|
|
// Update our local cache that uses the old id to the new id
|
2023-12-22 07:35:01 +01:00
|
|
|
const msg = this._hash.get(old_id);
|
|
|
|
if (msg !== undefined) {
|
2020-02-12 06:32:42 +01:00
|
|
|
this._hash.delete(old_id);
|
|
|
|
this._hash.set(new_id, msg);
|
2018-05-04 12:44:28 +02:00
|
|
|
} else {
|
2020-11-12 22:43:04 +01:00
|
|
|
return false;
|
2018-05-04 12:44:28 +02:00
|
|
|
}
|
|
|
|
|
2020-02-12 06:34:41 +01:00
|
|
|
if (this._local_only.has(old_id)) {
|
2018-05-04 12:44:28 +02:00
|
|
|
if (this._is_localonly_id(new_id)) {
|
2020-02-12 06:34:41 +01:00
|
|
|
this._local_only.add(new_id);
|
2018-05-04 12:44:28 +02:00
|
|
|
}
|
2020-02-12 06:34:41 +01:00
|
|
|
this._local_only.delete(old_id);
|
2018-05-04 12:44:28 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if (this._selected_id === old_id) {
|
|
|
|
this._selected_id = new_id;
|
|
|
|
}
|
|
|
|
|
2020-11-12 22:43:04 +01:00
|
|
|
return this.reorder_messages(new_id);
|
2020-07-23 00:26:13 +02:00
|
|
|
}
|
2020-07-04 20:45:29 +02:00
|
|
|
|
2023-12-22 07:35:01 +01:00
|
|
|
reorder_messages(new_id: number): boolean {
|
|
|
|
const message_sort_func = (a: Message, b: Message): number => a.id - b.id;
|
2018-05-04 12:44:28 +02:00
|
|
|
// If this message is now out of order, re-order and re-render
|
2020-07-23 00:26:13 +02:00
|
|
|
const current_message = this._hash.get(new_id);
|
2023-12-22 07:35:01 +01:00
|
|
|
if (current_message === undefined) {
|
|
|
|
return false;
|
|
|
|
}
|
2020-07-23 00:26:13 +02:00
|
|
|
const index = this._items.indexOf(current_message);
|
2018-05-04 12:44:28 +02:00
|
|
|
|
2020-07-23 00:26:13 +02:00
|
|
|
const next = this._next_nonlocal_message(this._items, index, (idx) => idx + 1);
|
|
|
|
const prev = this._next_nonlocal_message(this._items, index, (idx) => idx - 1);
|
2018-05-04 12:44:28 +02:00
|
|
|
|
2020-07-15 00:34:28 +02:00
|
|
|
if (
|
|
|
|
(next !== undefined && current_message.id > next.id) ||
|
|
|
|
(prev !== undefined && current_message.id < prev.id)
|
|
|
|
) {
|
2020-07-04 20:45:29 +02:00
|
|
|
blueslip.debug("Changed message ID from server caused out-of-order list, reordering");
|
2020-07-23 00:26:13 +02:00
|
|
|
this._items.sort(message_sort_func);
|
2021-05-07 22:34:38 +02:00
|
|
|
this._all_items.sort(message_sort_func);
|
2020-11-12 22:43:04 +01:00
|
|
|
return true;
|
2020-07-04 20:45:29 +02:00
|
|
|
}
|
2020-11-12 22:43:04 +01:00
|
|
|
|
|
|
|
return false;
|
2020-07-23 00:26:13 +02:00
|
|
|
}
|
2018-05-04 12:44:28 +02:00
|
|
|
|
2023-12-22 07:35:01 +01:00
|
|
|
get_messages_sent_by_user(user_id: number): Message[] {
|
2022-10-07 00:10:54 +02:00
|
|
|
const msgs = this._items.filter((msg) => msg.sender_id === user_id);
|
2022-02-09 17:07:20 +01:00
|
|
|
if (msgs.length === 0) {
|
|
|
|
return [];
|
|
|
|
}
|
|
|
|
return msgs;
|
|
|
|
}
|
|
|
|
|
2023-12-22 07:35:01 +01:00
|
|
|
get_last_message_sent_by_me(): Message | undefined {
|
2024-02-13 02:08:16 +01:00
|
|
|
const msg_index = this._items.findLastIndex(
|
|
|
|
(msg) => msg.sender_id === current_user.user_id,
|
|
|
|
);
|
2018-05-04 12:44:28 +02:00
|
|
|
if (msg_index === -1) {
|
2020-09-24 07:50:36 +02:00
|
|
|
return undefined;
|
2018-05-04 12:44:28 +02:00
|
|
|
}
|
2019-11-02 00:06:25 +01:00
|
|
|
const msg = this._items[msg_index];
|
2018-05-04 12:44:28 +02:00
|
|
|
return msg;
|
2020-07-23 00:26:13 +02:00
|
|
|
}
|
|
|
|
}
|