2021-03-11 05:43:45 +01:00
|
|
|
import $ from "jquery";
|
2024-08-10 22:13:48 +02:00
|
|
|
import assert from "minimalistic-assert";
|
|
|
|
import {z} from "zod";
|
2021-03-11 05:43:45 +01:00
|
|
|
|
2024-09-11 05:31:26 +02:00
|
|
|
import render_message_controls from "../templates/message_controls.hbs";
|
|
|
|
import render_message_controls_failed_msg from "../templates/message_controls_failed_msg.hbs";
|
|
|
|
|
2021-02-10 17:01:31 +01:00
|
|
|
import * as alert_words from "./alert_words";
|
2021-03-16 23:38:59 +01:00
|
|
|
import * as blueslip from "./blueslip";
|
2023-10-06 22:36:26 +02:00
|
|
|
import * as compose_notifications from "./compose_notifications";
|
2021-09-23 17:43:30 +02:00
|
|
|
import * as compose_ui from "./compose_ui";
|
2024-08-03 14:13:29 +02:00
|
|
|
import * as echo_state from "./echo_state";
|
2021-02-28 00:49:57 +01:00
|
|
|
import * as local_message from "./local_message";
|
2021-02-28 00:49:36 +01:00
|
|
|
import * as markdown from "./markdown";
|
2024-07-15 16:23:45 +02:00
|
|
|
import * as message_events_util from "./message_events_util";
|
2021-03-30 02:21:21 +02:00
|
|
|
import * as message_lists from "./message_lists";
|
2023-04-25 05:36:10 +02:00
|
|
|
import * as message_live_update from "./message_live_update";
|
2021-02-28 01:10:03 +01:00
|
|
|
import * as message_store from "./message_store";
|
2024-08-10 22:13:48 +02:00
|
|
|
import type {DisplayRecipientUser, Message, RawMessage} from "./message_store";
|
2024-07-08 19:00:08 +02:00
|
|
|
import * as message_util from "./message_util";
|
2021-02-10 16:53:37 +01:00
|
|
|
import * as people from "./people";
|
2021-02-28 00:44:12 +01:00
|
|
|
import * as pm_list from "./pm_list";
|
2023-09-06 23:22:20 +02:00
|
|
|
import * as recent_view_data from "./recent_view_data";
|
2021-02-28 00:42:30 +01:00
|
|
|
import * as rows from "./rows";
|
2021-02-28 00:50:19 +01:00
|
|
|
import * as sent_messages from "./sent_messages";
|
2024-02-13 02:08:16 +01:00
|
|
|
import {current_user} from "./state_data";
|
2023-07-26 22:07:21 +02:00
|
|
|
import * as stream_data from "./stream_data";
|
2021-02-28 21:31:02 +01:00
|
|
|
import * as stream_list from "./stream_list";
|
2021-02-28 00:54:32 +01:00
|
|
|
import * as stream_topic_history from "./stream_topic_history";
|
2024-08-10 22:13:48 +02:00
|
|
|
import type {TopicLink} from "./types";
|
2021-02-10 16:53:37 +01:00
|
|
|
import * as util from "./util";
|
2020-08-01 03:43:15 +02:00
|
|
|
|
2018-11-30 00:48:13 +01:00
|
|
|
// Docs: https://zulip.readthedocs.io/en/latest/subsystems/sending-messages.html
|
2013-12-04 17:16:08 +01:00
|
|
|
|
2024-08-10 22:13:48 +02:00
|
|
|
type ServerMessage = RawMessage & {local_id?: string};
|
|
|
|
|
|
|
|
const send_message_api_response_schema = z.object({
|
|
|
|
id: z.number(),
|
|
|
|
automatic_new_visibility_policy: z.number().optional(),
|
|
|
|
});
|
|
|
|
|
|
|
|
type MessageRequestObject = {
|
|
|
|
sender_id: number;
|
|
|
|
queue_id: null | string;
|
|
|
|
topic: string;
|
|
|
|
content: string;
|
|
|
|
to: string;
|
|
|
|
draft_id: string | undefined;
|
|
|
|
};
|
|
|
|
|
|
|
|
type PrivateMessageObject = {
|
|
|
|
type: "private";
|
|
|
|
reply_to: string;
|
|
|
|
private_message_recipient: string;
|
|
|
|
to_user_ids: string | undefined;
|
|
|
|
};
|
|
|
|
|
|
|
|
type StreamMessageObject = {
|
|
|
|
type: "stream";
|
|
|
|
stream_id: number;
|
|
|
|
};
|
|
|
|
|
|
|
|
type MessageRequest = MessageRequestObject & (PrivateMessageObject | StreamMessageObject);
|
|
|
|
|
|
|
|
type LocalEditRequest = Partial<{
|
|
|
|
raw_content: string | undefined;
|
|
|
|
content: string;
|
|
|
|
orig_content: string;
|
|
|
|
orig_raw_content: string | undefined;
|
|
|
|
new_topic: string;
|
|
|
|
new_stream_id: number;
|
|
|
|
starred: boolean;
|
|
|
|
historical: boolean;
|
|
|
|
collapsed: boolean;
|
|
|
|
alerted: boolean;
|
|
|
|
mentioned: boolean;
|
|
|
|
mentioned_me_directly: boolean;
|
|
|
|
}>;
|
|
|
|
|
|
|
|
type LocalMessage = MessageRequestObject & {
|
|
|
|
raw_content: string;
|
|
|
|
flags: string[];
|
|
|
|
is_me_message: boolean;
|
|
|
|
content_type: string;
|
|
|
|
sender_email: string;
|
|
|
|
sender_full_name: string;
|
|
|
|
avatar_url?: string | null | undefined;
|
|
|
|
timestamp: number;
|
|
|
|
local_id: string;
|
|
|
|
locally_echoed: boolean;
|
|
|
|
resend: boolean;
|
|
|
|
id: number;
|
|
|
|
topic_links: TopicLink[];
|
|
|
|
} & (
|
|
|
|
| (StreamMessageObject & {display_recipient?: string})
|
|
|
|
| (PrivateMessageObject & {display_recipient?: DisplayRecipientUser[]})
|
|
|
|
);
|
|
|
|
|
|
|
|
type PostMessageAPIData = z.output<typeof send_message_api_response_schema>;
|
|
|
|
|
2021-05-07 08:49:29 +02:00
|
|
|
// These retry spinner functions return true if and only if the
|
|
|
|
// spinner already is in the requested state, which can be used to
|
|
|
|
// avoid sending duplicate requests.
|
2024-08-10 22:13:48 +02:00
|
|
|
function show_retry_spinner($row: JQuery): boolean {
|
2022-01-25 11:36:19 +01:00
|
|
|
const $retry_spinner = $row.find(".refresh-failed-message");
|
2021-05-07 08:49:29 +02:00
|
|
|
|
2022-01-25 11:36:19 +01:00
|
|
|
if (!$retry_spinner.hasClass("rotating")) {
|
|
|
|
$retry_spinner.toggleClass("rotating", true);
|
2021-05-07 08:49:29 +02:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2024-08-10 22:13:48 +02:00
|
|
|
function hide_retry_spinner($row: JQuery): boolean {
|
2022-01-25 11:36:19 +01:00
|
|
|
const $retry_spinner = $row.find(".refresh-failed-message");
|
2021-05-07 08:49:29 +02:00
|
|
|
|
2022-01-25 11:36:19 +01:00
|
|
|
if ($retry_spinner.hasClass("rotating")) {
|
|
|
|
$retry_spinner.toggleClass("rotating", false);
|
2021-05-07 08:49:29 +02:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2024-09-11 05:31:26 +02:00
|
|
|
function show_message_failed(message_id: number, _failed_msg: string): void {
|
2023-04-25 05:36:10 +02:00
|
|
|
// Failed to send message, so display inline retry/cancel
|
|
|
|
message_live_update.update_message_in_all_views(message_id, ($row) => {
|
2024-01-19 09:03:45 +01:00
|
|
|
$row.find(".slow-send-spinner").addClass("hidden");
|
2024-09-11 05:31:26 +02:00
|
|
|
const $message_controls = $row.find(".message_controls");
|
|
|
|
$message_controls.html(render_message_controls_failed_msg());
|
2023-04-25 05:36:10 +02:00
|
|
|
});
|
2024-09-11 05:31:26 +02:00
|
|
|
// TODO: Show the `_failed_msg` in the UI, describing the reason for the failure.
|
2023-04-25 05:36:10 +02:00
|
|
|
}
|
|
|
|
|
2024-08-10 22:13:48 +02:00
|
|
|
function show_failed_message_success(message_id: number): void {
|
2023-04-25 05:36:10 +02:00
|
|
|
// Previously failed message succeeded
|
2024-09-11 05:31:26 +02:00
|
|
|
const msg = message_store.get(message_id);
|
2023-04-25 05:36:10 +02:00
|
|
|
message_live_update.update_message_in_all_views(message_id, ($row) => {
|
2024-09-11 05:31:26 +02:00
|
|
|
const $message_controls = $row.find(".message_controls");
|
|
|
|
$message_controls.html(render_message_controls({msg}));
|
2023-04-25 05:36:10 +02:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2024-08-10 22:13:48 +02:00
|
|
|
function failed_message_success(message_id: number): void {
|
|
|
|
message_store.get(message_id)!.failed_request = false;
|
2023-04-25 05:36:10 +02:00
|
|
|
show_failed_message_success(message_id);
|
2020-07-12 23:21:05 +02:00
|
|
|
}
|
|
|
|
|
2024-08-10 22:13:48 +02:00
|
|
|
function resend_message(
|
|
|
|
message: Message,
|
|
|
|
$row: JQuery,
|
|
|
|
{
|
|
|
|
on_send_message_success,
|
|
|
|
send_message,
|
|
|
|
}: {
|
|
|
|
on_send_message_success: (request: Message, data: PostMessageAPIData) => void;
|
|
|
|
send_message: (
|
|
|
|
request: Message,
|
|
|
|
on_success: (raw_data: unknown) => void,
|
|
|
|
error: (response: string, _server_error_code: string) => void,
|
|
|
|
) => void;
|
|
|
|
},
|
|
|
|
): void {
|
|
|
|
message.content = message.raw_content!;
|
2022-01-25 11:36:19 +01:00
|
|
|
if (show_retry_spinner($row)) {
|
2021-05-07 08:49:29 +02:00
|
|
|
// retry already in in progress
|
|
|
|
return;
|
|
|
|
}
|
sending messages: Extract sent_messages.js.
This commit extract send_messages.js to clean up code related
to the following things:
* sending data to /json/report_send_time
* restarting the event loop if events don't arrive on time
The code related to /json/report changes the following ways:
* We track the state almost completely in the new
send_messages.js module, with other modules just
making one-line calls.
* We no longer send "displayed" times to the servers, since
we were kind of lying about them anyway.
* We now explicitly track the state of each single sent
message in its own object.
* We now look up data related to the messages by local_id,
instead of message_id. The problem with message_id was
that is was mutable. Now we use local_id, and we extend
the local_id concept to messages that don't get rendered
client side. We no longer need to react to the
'message_id_changed' event to change our hash key.
* The code used to live in many places:
* various big chunks were scattered among compose.js,
and those were all moved or reduced to one-line
calls into the new module
* echo.js continues to make basically one-line calls,
but it no longer calls compose.report_as_received(),
nor does it set the "start" time.
* message_util.js used to report received events, but
only when they finally got drawn in the home view;
this code is gone now
The code related to restarting the event loop if events don't arrive
changes as follows:
* The timer now gets set up from within
send_messages.message_state.report_server_ack,
where we can easily inspect the current state of the
possibly-still-in-flight message.
* The code to confirm that an event was received happens now
in server_events.js, rather than later, so that we don't
falsely blame the event loop for a downstream bug. (Plus
it's easier to just do it one place.)
This change removes a fair amount of code from our node tests. Some
of the removal is good stuff related to us completing killing off
unnecessary code. Other removals are more expediency-driven, and
we should make another sweep at ramping up our coverage on compose.js,
with possibly a little more mocking of the new `send_messages` code
layer, since it's now abstracted better.
There is also some minor cleanup to echo.resend_message() in this
commit.
See #5968 for a detailed breakdown of the changes.
2017-07-30 12:56:46 +02:00
|
|
|
|
2023-05-03 00:01:10 +02:00
|
|
|
message.resend = true;
|
2014-01-03 20:39:12 +01:00
|
|
|
|
2024-08-10 22:13:48 +02:00
|
|
|
function on_success(raw_data: unknown): void {
|
|
|
|
const data = send_message_api_response_schema.parse(raw_data);
|
2019-11-02 00:06:25 +01:00
|
|
|
const message_id = data.id;
|
2014-01-03 20:39:12 +01:00
|
|
|
|
2022-01-25 11:36:19 +01:00
|
|
|
hide_retry_spinner($row);
|
sending messages: Extract sent_messages.js.
This commit extract send_messages.js to clean up code related
to the following things:
* sending data to /json/report_send_time
* restarting the event loop if events don't arrive on time
The code related to /json/report changes the following ways:
* We track the state almost completely in the new
send_messages.js module, with other modules just
making one-line calls.
* We no longer send "displayed" times to the servers, since
we were kind of lying about them anyway.
* We now explicitly track the state of each single sent
message in its own object.
* We now look up data related to the messages by local_id,
instead of message_id. The problem with message_id was
that is was mutable. Now we use local_id, and we extend
the local_id concept to messages that don't get rendered
client side. We no longer need to react to the
'message_id_changed' event to change our hash key.
* The code used to live in many places:
* various big chunks were scattered among compose.js,
and those were all moved or reduced to one-line
calls into the new module
* echo.js continues to make basically one-line calls,
but it no longer calls compose.report_as_received(),
nor does it set the "start" time.
* message_util.js used to report received events, but
only when they finally got drawn in the home view;
this code is gone now
The code related to restarting the event loop if events don't arrive
changes as follows:
* The timer now gets set up from within
send_messages.message_state.report_server_ack,
where we can easily inspect the current state of the
possibly-still-in-flight message.
* The code to confirm that an event was received happens now
in server_events.js, rather than later, so that we don't
falsely blame the event loop for a downstream bug. (Plus
it's easier to just do it one place.)
This change removes a fair amount of code from our node tests. Some
of the removal is good stuff related to us completing killing off
unnecessary code. Other removals are more expediency-driven, and
we should make another sweep at ramping up our coverage on compose.js,
with possibly a little more mocking of the new `send_messages` code
layer, since it's now abstracted better.
There is also some minor cleanup to echo.resend_message() in this
commit.
See #5968 for a detailed breakdown of the changes.
2017-07-30 12:56:46 +02:00
|
|
|
|
2023-10-16 12:55:24 +02:00
|
|
|
on_send_message_success(message, data);
|
2014-01-03 20:39:12 +01:00
|
|
|
|
|
|
|
// Resend succeeded, so mark as no longer failed
|
2020-07-12 23:21:05 +02:00
|
|
|
failed_message_success(message_id);
|
sending messages: Extract sent_messages.js.
This commit extract send_messages.js to clean up code related
to the following things:
* sending data to /json/report_send_time
* restarting the event loop if events don't arrive on time
The code related to /json/report changes the following ways:
* We track the state almost completely in the new
send_messages.js module, with other modules just
making one-line calls.
* We no longer send "displayed" times to the servers, since
we were kind of lying about them anyway.
* We now explicitly track the state of each single sent
message in its own object.
* We now look up data related to the messages by local_id,
instead of message_id. The problem with message_id was
that is was mutable. Now we use local_id, and we extend
the local_id concept to messages that don't get rendered
client side. We no longer need to react to the
'message_id_changed' event to change our hash key.
* The code used to live in many places:
* various big chunks were scattered among compose.js,
and those were all moved or reduced to one-line
calls into the new module
* echo.js continues to make basically one-line calls,
but it no longer calls compose.report_as_received(),
nor does it set the "start" time.
* message_util.js used to report received events, but
only when they finally got drawn in the home view;
this code is gone now
The code related to restarting the event loop if events don't arrive
changes as follows:
* The timer now gets set up from within
send_messages.message_state.report_server_ack,
where we can easily inspect the current state of the
possibly-still-in-flight message.
* The code to confirm that an event was received happens now
in server_events.js, rather than later, so that we don't
falsely blame the event loop for a downstream bug. (Plus
it's easier to just do it one place.)
This change removes a fair amount of code from our node tests. Some
of the removal is good stuff related to us completing killing off
unnecessary code. Other removals are more expediency-driven, and
we should make another sweep at ramping up our coverage on compose.js,
with possibly a little more mocking of the new `send_messages` code
layer, since it's now abstracted better.
There is also some minor cleanup to echo.resend_message() in this
commit.
See #5968 for a detailed breakdown of the changes.
2017-07-30 12:56:46 +02:00
|
|
|
}
|
|
|
|
|
2024-08-10 22:13:48 +02:00
|
|
|
function on_error(response: string, _server_error_code: string): void {
|
2021-02-10 16:53:37 +01:00
|
|
|
message_send_error(message.id, response);
|
2020-07-02 01:45:54 +02:00
|
|
|
setTimeout(() => {
|
2022-01-25 11:36:19 +01:00
|
|
|
hide_retry_spinner($row);
|
2019-03-19 21:29:53 +01:00
|
|
|
}, 300);
|
2014-01-03 20:39:12 +01:00
|
|
|
blueslip.log("Manual resend of message failed");
|
sending messages: Extract sent_messages.js.
This commit extract send_messages.js to clean up code related
to the following things:
* sending data to /json/report_send_time
* restarting the event loop if events don't arrive on time
The code related to /json/report changes the following ways:
* We track the state almost completely in the new
send_messages.js module, with other modules just
making one-line calls.
* We no longer send "displayed" times to the servers, since
we were kind of lying about them anyway.
* We now explicitly track the state of each single sent
message in its own object.
* We now look up data related to the messages by local_id,
instead of message_id. The problem with message_id was
that is was mutable. Now we use local_id, and we extend
the local_id concept to messages that don't get rendered
client side. We no longer need to react to the
'message_id_changed' event to change our hash key.
* The code used to live in many places:
* various big chunks were scattered among compose.js,
and those were all moved or reduced to one-line
calls into the new module
* echo.js continues to make basically one-line calls,
but it no longer calls compose.report_as_received(),
nor does it set the "start" time.
* message_util.js used to report received events, but
only when they finally got drawn in the home view;
this code is gone now
The code related to restarting the event loop if events don't arrive
changes as follows:
* The timer now gets set up from within
send_messages.message_state.report_server_ack,
where we can easily inspect the current state of the
possibly-still-in-flight message.
* The code to confirm that an event was received happens now
in server_events.js, rather than later, so that we don't
falsely blame the event loop for a downstream bug. (Plus
it's easier to just do it one place.)
This change removes a fair amount of code from our node tests. Some
of the removal is good stuff related to us completing killing off
unnecessary code. Other removals are more expediency-driven, and
we should make another sweep at ramping up our coverage on compose.js,
with possibly a little more mocking of the new `send_messages` code
layer, since it's now abstracted better.
There is also some minor cleanup to echo.resend_message() in this
commit.
See #5968 for a detailed breakdown of the changes.
2017-07-30 12:56:46 +02:00
|
|
|
}
|
|
|
|
|
2023-10-06 19:30:38 +02:00
|
|
|
send_message(message, on_success, on_error);
|
2014-01-03 20:39:12 +01:00
|
|
|
}
|
|
|
|
|
2024-08-10 22:13:48 +02:00
|
|
|
export function build_display_recipient(message: LocalMessage): DisplayRecipientUser[] | string {
|
2020-07-15 01:29:15 +02:00
|
|
|
if (message.type === "stream") {
|
2023-07-26 22:07:21 +02:00
|
|
|
return stream_data.get_stream_name_from_id(message.stream_id);
|
2013-12-19 17:03:08 +01:00
|
|
|
}
|
|
|
|
|
2020-01-23 21:44:23 +01:00
|
|
|
// Build a display recipient with the full names of each
|
|
|
|
// recipient. Note that it's important that use
|
|
|
|
// util.extract_pm_recipients, which filters out any spurious
|
|
|
|
// ", " at the end of the recipient list
|
|
|
|
const emails = util.extract_pm_recipients(message.private_message_recipient);
|
|
|
|
|
2020-01-31 21:46:03 +01:00
|
|
|
let sender_in_display_recipients = false;
|
2020-07-02 01:39:34 +02:00
|
|
|
const display_recipient = emails.map((email) => {
|
2020-01-23 21:44:23 +01:00
|
|
|
email = email.trim();
|
|
|
|
const person = people.get_by_email(email);
|
2024-08-10 22:13:48 +02:00
|
|
|
assert(person !== undefined);
|
2020-01-23 21:44:23 +01:00
|
|
|
|
2020-01-31 21:46:03 +01:00
|
|
|
if (person.user_id === message.sender_id) {
|
|
|
|
sender_in_display_recipients = true;
|
2020-01-23 07:52:08 +01:00
|
|
|
}
|
|
|
|
|
2020-01-23 21:44:23 +01:00
|
|
|
// NORMAL PATH
|
|
|
|
//
|
|
|
|
// This should match the format of display_recipient
|
2023-12-15 21:59:38 +01:00
|
|
|
// objects generated by the backend code in display_recipient.py,
|
2020-01-23 21:44:23 +01:00
|
|
|
// which is why we create a new object with a `.id` field
|
|
|
|
// rather than a `.user_id` field.
|
|
|
|
return {
|
|
|
|
id: person.user_id,
|
|
|
|
email: person.email,
|
|
|
|
full_name: person.full_name,
|
|
|
|
};
|
|
|
|
});
|
2020-01-23 07:52:08 +01:00
|
|
|
|
2020-01-31 21:46:03 +01:00
|
|
|
if (!sender_in_display_recipients) {
|
2020-01-23 07:52:08 +01:00
|
|
|
// Ensure that the current user is included in
|
2023-06-16 13:23:39 +02:00
|
|
|
// display_recipient for group direct messages.
|
2020-01-23 07:52:08 +01:00
|
|
|
display_recipient.push({
|
|
|
|
id: message.sender_id,
|
|
|
|
email: message.sender_email,
|
|
|
|
full_name: message.sender_full_name,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
return display_recipient;
|
2021-02-10 16:53:37 +01:00
|
|
|
}
|
2020-01-22 11:42:05 +01:00
|
|
|
|
2024-08-20 21:12:43 +02:00
|
|
|
export function track_local_message(message: Message): void {
|
|
|
|
assert(message.local_id !== undefined);
|
|
|
|
echo_state.set_message_waiting_for_id(message.local_id, message);
|
|
|
|
echo_state.set_message_waiting_for_ack(message.local_id, message);
|
|
|
|
}
|
|
|
|
|
2024-08-10 22:13:48 +02:00
|
|
|
export function insert_local_message(
|
|
|
|
message_request: MessageRequest,
|
|
|
|
local_id_float: number,
|
|
|
|
insert_new_messages: (
|
|
|
|
messages: LocalMessage[],
|
|
|
|
send_by_this_client: boolean,
|
|
|
|
deliver_locally: boolean,
|
|
|
|
) => Message[],
|
|
|
|
): Message {
|
2020-01-22 11:42:05 +01:00
|
|
|
// Shallow clone of message request object that is turned into something suitable
|
|
|
|
// for zulip.js:add_message
|
|
|
|
// Keep this in sync with changes to compose.create_message_object
|
2024-08-10 22:13:48 +02:00
|
|
|
const raw_content = message_request.content;
|
|
|
|
const topic = message_request.topic;
|
|
|
|
|
|
|
|
const local_message: LocalMessage = {
|
|
|
|
...message_request,
|
|
|
|
...markdown.render(raw_content),
|
|
|
|
raw_content,
|
|
|
|
content_type: "text/html",
|
|
|
|
sender_email: people.my_current_email(),
|
|
|
|
sender_full_name: people.my_full_name(),
|
|
|
|
avatar_url: current_user.avatar_url,
|
|
|
|
timestamp: Date.now() / 1000,
|
|
|
|
local_id: local_id_float.toString(),
|
|
|
|
locally_echoed: true,
|
|
|
|
id: local_id_float,
|
|
|
|
resend: false,
|
|
|
|
is_me_message: false,
|
|
|
|
topic_links: topic ? markdown.get_topic_links(topic) : [],
|
2024-01-22 07:55:55 +01:00
|
|
|
};
|
2018-02-24 13:42:27 +01:00
|
|
|
|
2024-08-10 22:13:48 +02:00
|
|
|
local_message.display_recipient = build_display_recipient(local_message);
|
2024-06-03 20:39:19 +02:00
|
|
|
|
2024-08-10 22:13:48 +02:00
|
|
|
const [message] = insert_new_messages([local_message], true, true);
|
|
|
|
assert(message !== undefined);
|
2023-06-28 13:02:28 +02:00
|
|
|
|
2020-04-09 19:22:30 +02:00
|
|
|
return message;
|
2021-02-10 16:53:37 +01:00
|
|
|
}
|
2014-01-16 20:18:55 +01:00
|
|
|
|
2024-08-10 22:13:48 +02:00
|
|
|
export function is_slash_command(content: string): boolean {
|
2020-07-15 01:29:15 +02:00
|
|
|
return !content.startsWith("/me") && content.startsWith("/");
|
2021-02-10 16:53:37 +01:00
|
|
|
}
|
2018-05-16 22:05:11 +02:00
|
|
|
|
2024-08-10 22:13:48 +02:00
|
|
|
export function try_deliver_locally(
|
|
|
|
message_request: MessageRequest,
|
|
|
|
insert_new_messages: (
|
|
|
|
messages: LocalMessage[],
|
|
|
|
send_by_this_client: boolean,
|
|
|
|
deliver_locally: boolean,
|
|
|
|
) => Message[],
|
|
|
|
): Message | undefined {
|
echo: Remove checks on current message list.
Originally, we only wanted to do local echo in the current
view. However, now that we're looking at navigating the user to the
conversation that they send a new message to, it's going to be quite
common that we immediately visit a destination different from the
current view, where local echo in that different view would be
valuable.
The most interesting block was added in
af188205cb015d7d14cf3e0f28ae3084dd306c83 / #8989. But in
6637f2dbb7128dae30e9fb78f5c2ebea759c0be8, the key logic for checking
`msg_list.data.fetch_status.has_found_newest` was duplicated in the
`add_new_messages` code path, which critically also updates
`update_expected_max_message_id` and thus may close a race with
fetching message history for a view we're being navigated to, where
the locally echoed message might fail to appear at all.
This change does come with a slight regression: If we are looking at a
search view where the filter is one that we cannot apply locally, a
newly sent message will now be locally echoed (returning the compose
box for drafting another) even though it cannot be displayed in the
current view, which means that the message will not appear in either
the compose box or the current view for the brief period before we get
a reply from the server in this scenario. This is a minor detail,
likely not worth troubling ourselves over.
Given our intent to experiment with navigating the user out of the
search view in this scenario, this is likely not important.
Co-authored-by: Tim Abbott <tabbott@zulip.com>
2024-05-02 04:35:38 +02:00
|
|
|
// Checks if the message request can be locally echoed, and if so,
|
|
|
|
// adds a local echoed copy of the message to appropriate message lists.
|
|
|
|
//
|
|
|
|
// Returns the message object, or undefined if it cannot be
|
|
|
|
// echoed; in that case, the compose box will remain in the
|
|
|
|
// sending state rather than being cleared to allow composing a
|
|
|
|
// next message.
|
|
|
|
//
|
|
|
|
// Notably, this algorithm will allow locally echoing a message in
|
|
|
|
// cases where we are currently looking at a search view where
|
|
|
|
// `!filter.can_apply_locally(message)`; so it is possible for a
|
|
|
|
// message to be locally echoed but not appear in the current
|
|
|
|
// view; this is useful to ensure it will be visible in other
|
|
|
|
// views that we might navigate to before we get a response from
|
|
|
|
// the server.
|
2024-07-08 19:00:08 +02:00
|
|
|
if (
|
2024-08-10 22:13:48 +02:00
|
|
|
message_request.type === "private" &&
|
2024-07-08 19:00:08 +02:00
|
|
|
message_request.to_user_ids &&
|
|
|
|
!people.user_can_initiate_direct_message_thread(message_request.to_user_ids) &&
|
|
|
|
!message_util.get_direct_message_permission_hints(message_request.to_user_ids)
|
|
|
|
.is_local_echo_safe
|
|
|
|
) {
|
|
|
|
return undefined;
|
|
|
|
}
|
2017-07-29 02:51:33 +02:00
|
|
|
if (markdown.contains_backend_only_syntax(message_request.content)) {
|
2020-09-24 07:50:36 +02:00
|
|
|
return undefined;
|
2014-01-16 20:18:55 +01:00
|
|
|
}
|
|
|
|
|
2021-02-10 16:53:37 +01:00
|
|
|
if (is_slash_command(message_request.content)) {
|
2020-09-24 07:50:36 +02:00
|
|
|
return undefined;
|
2018-05-16 22:05:11 +02:00
|
|
|
}
|
|
|
|
|
2020-02-12 09:32:25 +01:00
|
|
|
const local_id_float = local_message.get_next_id_float();
|
2017-07-17 22:25:25 +02:00
|
|
|
|
2020-02-12 09:32:25 +01:00
|
|
|
if (!local_id_float) {
|
2017-07-17 22:25:25 +02:00
|
|
|
// This can happen for legit reasons.
|
2020-09-24 07:50:36 +02:00
|
|
|
return undefined;
|
2014-01-24 17:15:35 +01:00
|
|
|
}
|
|
|
|
|
2021-09-23 17:43:30 +02:00
|
|
|
// Now that we've committed to delivering the message locally, we
|
2024-06-12 01:01:10 +02:00
|
|
|
// shrink the compose-box if it is in an expanded state. This
|
2021-09-23 17:43:30 +02:00
|
|
|
// would have happened anyway in clear_compose_box, however, we
|
|
|
|
// need to this operation before inserting the local message into
|
|
|
|
// the feed. Otherwise, the out-of-view notification will be
|
|
|
|
// always triggered on the top of compose-box, regardless of
|
|
|
|
// whether the message would be visible after shrinking compose,
|
2024-06-12 01:01:10 +02:00
|
|
|
// because compose occludes the whole screen in full size state.
|
|
|
|
if (compose_ui.is_expanded()) {
|
2021-09-23 17:43:30 +02:00
|
|
|
compose_ui.make_compose_box_original_size();
|
|
|
|
}
|
|
|
|
|
2023-06-28 13:02:28 +02:00
|
|
|
const message = insert_local_message(message_request, local_id_float, insert_new_messages);
|
2020-04-09 19:22:30 +02:00
|
|
|
return message;
|
2021-02-10 16:53:37 +01:00
|
|
|
}
|
2013-12-19 17:03:08 +01:00
|
|
|
|
2024-08-10 22:13:48 +02:00
|
|
|
export function edit_locally(message: Message, request: LocalEditRequest): Message {
|
2019-04-22 20:13:23 +02:00
|
|
|
// Responsible for doing the rendering work of locally editing the
|
2022-02-08 00:13:33 +01:00
|
|
|
// content of a message. This is used in several code paths:
|
2019-04-22 20:13:23 +02:00
|
|
|
// * Editing a message where a message was locally echoed but
|
|
|
|
// it got an error back from the server
|
|
|
|
// * Locally echoing any content-only edits to fully sent messages
|
|
|
|
// * Restoring the original content should the server return an
|
|
|
|
// error after having locally echoed content-only messages.
|
|
|
|
// The details of what should be changed are encoded in the request.
|
2019-11-20 23:20:39 +01:00
|
|
|
const raw_content = request.raw_content;
|
2019-11-02 00:06:25 +01:00
|
|
|
const message_content_edited = raw_content !== undefined && message.raw_content !== raw_content;
|
2018-05-03 21:12:39 +02:00
|
|
|
|
2020-05-06 12:07:34 +02:00
|
|
|
if (request.new_topic !== undefined || request.new_stream_id !== undefined) {
|
2024-08-10 22:13:48 +02:00
|
|
|
assert(message.type === "stream");
|
2020-05-06 12:07:34 +02:00
|
|
|
const new_stream_id = request.new_stream_id;
|
2019-11-20 23:20:39 +01:00
|
|
|
const new_topic = request.new_topic;
|
2020-06-15 19:52:00 +02:00
|
|
|
stream_topic_history.remove_messages({
|
2017-07-26 14:05:25 +02:00
|
|
|
stream_id: message.stream_id,
|
2020-02-19 00:04:12 +01:00
|
|
|
topic_name: message.topic,
|
2020-06-15 19:52:00 +02:00
|
|
|
num_messages: 1,
|
2020-08-04 11:12:42 +02:00
|
|
|
max_removed_msg_id: message.id,
|
2017-07-26 14:05:25 +02:00
|
|
|
});
|
|
|
|
|
2020-05-06 12:07:34 +02:00
|
|
|
if (new_stream_id !== undefined) {
|
|
|
|
message.stream_id = new_stream_id;
|
|
|
|
}
|
|
|
|
if (new_topic !== undefined) {
|
|
|
|
message.topic = new_topic;
|
|
|
|
}
|
2017-07-26 14:05:25 +02:00
|
|
|
|
2020-03-22 18:40:05 +01:00
|
|
|
stream_topic_history.add_message({
|
2017-07-26 14:05:25 +02:00
|
|
|
stream_id: message.stream_id,
|
2020-02-19 00:04:12 +01:00
|
|
|
topic_name: message.topic,
|
2017-07-26 14:05:25 +02:00
|
|
|
message_id: message.id,
|
|
|
|
});
|
2014-01-02 19:39:22 +01:00
|
|
|
}
|
|
|
|
|
2018-05-03 21:12:39 +02:00
|
|
|
if (message_content_edited) {
|
|
|
|
message.raw_content = raw_content;
|
2019-04-22 20:13:23 +02:00
|
|
|
if (request.content !== undefined) {
|
|
|
|
// This happens in the code path where message editing
|
|
|
|
// failed and we're trying to undo the local echo. We use
|
|
|
|
// the saved content and flags rather than rendering; this
|
|
|
|
// is important in case
|
|
|
|
// markdown.contains_backend_only_syntax(message) is true.
|
|
|
|
message.content = request.content;
|
2024-08-10 22:13:48 +02:00
|
|
|
message.mentioned = request.mentioned ?? false;
|
|
|
|
message.mentioned_me_directly = request.mentioned_me_directly ?? false;
|
|
|
|
message.alerted = request.alerted ?? false;
|
2019-04-22 20:13:23 +02:00
|
|
|
} else {
|
2020-08-11 01:47:49 +02:00
|
|
|
// Otherwise, we Markdown-render the message; this resets
|
2019-04-22 20:13:23 +02:00
|
|
|
// all flags, so we need to restore those flags that are
|
|
|
|
// properties of how the user has interacted with the
|
|
|
|
// message, and not its rendering.
|
message_edit: Fix local echo of message edit.
Previously, when a message is edited, it is locally echoed with its
pre-edit content.
This is because previously, when we tried to render the edited
message of the edit box during local echo, in order to update
the content, flags, and is_me_message properties of the message
object with that of those returned is markdown.render(), we used
the spread operator and created a new message object, and updated
the existing message object with this new one.
This was misconverted, since edit_locally() method already has a
fully-rendered message object to start with, and is just doing a
rerendering, it should be mutating what message it received, rather
than constructing a new local variable.
2024-03-20 18:32:21 +01:00
|
|
|
const {content, flags, is_me_message} = markdown.render(message.raw_content);
|
|
|
|
message.content = content;
|
|
|
|
message.flags = flags;
|
|
|
|
message.is_me_message = is_me_message;
|
2019-04-22 20:13:23 +02:00
|
|
|
if (request.starred !== undefined) {
|
|
|
|
message.starred = request.starred;
|
|
|
|
}
|
|
|
|
if (request.historical !== undefined) {
|
|
|
|
message.historical = request.historical;
|
|
|
|
}
|
|
|
|
if (request.collapsed !== undefined) {
|
|
|
|
message.collapsed = request.collapsed;
|
|
|
|
}
|
|
|
|
}
|
2018-05-03 21:12:39 +02:00
|
|
|
}
|
2017-01-20 15:21:28 +01:00
|
|
|
|
2019-04-22 20:13:23 +02:00
|
|
|
// We don't have logic to adjust unread counts, because message
|
|
|
|
// reaching this code path must either have been sent by us or the
|
|
|
|
// topic isn't being edited, so unread counts can't have changed.
|
2022-08-17 07:10:19 +02:00
|
|
|
for (const msg_list of message_lists.all_rendered_message_lists()) {
|
|
|
|
msg_list.view.rerender_messages([message]);
|
2014-01-02 19:39:22 +01:00
|
|
|
}
|
|
|
|
stream_list.update_streams_sidebar();
|
2016-11-29 16:53:43 +01:00
|
|
|
pm_list.update_private_messages();
|
2024-01-22 07:55:55 +01:00
|
|
|
return message;
|
2021-02-10 16:53:37 +01:00
|
|
|
}
|
2014-01-02 19:39:22 +01:00
|
|
|
|
2024-08-10 22:13:48 +02:00
|
|
|
export function reify_message_id(local_id: string, server_id: number): void {
|
2024-08-03 14:13:29 +02:00
|
|
|
const message = echo_state.get_message_waiting_for_id(local_id);
|
|
|
|
echo_state.remove_message_from_waiting_for_id(local_id);
|
2013-12-19 17:03:08 +01:00
|
|
|
|
|
|
|
// reify_message_id is called both on receiving a self-sent message
|
|
|
|
// from the server, and on receiving the response to the send request
|
|
|
|
// Reification is only needed the first time the server id is found
|
|
|
|
if (message === undefined) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
message.id = server_id;
|
2017-07-17 16:52:57 +02:00
|
|
|
message.locally_echoed = false;
|
2013-12-19 17:03:08 +01:00
|
|
|
|
2020-10-07 09:17:30 +02:00
|
|
|
const opts = {old_id: Number.parseFloat(local_id), new_id: server_id};
|
2017-07-19 12:49:49 +02:00
|
|
|
|
|
|
|
message_store.reify_message_id(opts);
|
2021-03-28 19:08:25 +02:00
|
|
|
update_message_lists(opts);
|
2023-10-06 22:36:26 +02:00
|
|
|
compose_notifications.reify_message_id(opts);
|
2023-09-06 23:22:20 +02:00
|
|
|
recent_view_data.reify_message_id_if_available(opts);
|
2024-08-03 14:16:14 +02:00
|
|
|
|
|
|
|
// We add the message to stream_topic_history only after we receive
|
|
|
|
// it from the server i.e., is acked, so as to maintain integer
|
|
|
|
// message id values there.
|
|
|
|
if (message.type === "stream") {
|
|
|
|
stream_topic_history.add_message({
|
|
|
|
stream_id: message.stream_id,
|
|
|
|
topic_name: message.topic,
|
|
|
|
message_id: message.id,
|
|
|
|
});
|
|
|
|
}
|
2021-02-10 16:53:37 +01:00
|
|
|
}
|
2013-12-19 17:03:08 +01:00
|
|
|
|
2024-08-10 22:13:48 +02:00
|
|
|
export function update_message_lists({old_id, new_id}: {old_id: number; new_id: number}): void {
|
2024-08-12 11:23:25 +02:00
|
|
|
// Update the rendered data first since it is most user visible.
|
2022-08-17 07:10:19 +02:00
|
|
|
for (const msg_list of message_lists.all_rendered_message_lists()) {
|
|
|
|
msg_list.change_message_id(old_id, new_id);
|
2022-08-18 00:03:29 +02:00
|
|
|
msg_list.view.change_message_id(old_id, new_id);
|
2021-03-28 19:08:25 +02:00
|
|
|
}
|
2024-08-12 11:23:25 +02:00
|
|
|
|
|
|
|
for (const msg_list_data of message_lists.non_rendered_data()) {
|
|
|
|
msg_list_data.change_message_id(old_id, new_id);
|
|
|
|
}
|
2021-03-28 19:08:25 +02:00
|
|
|
}
|
|
|
|
|
2024-08-10 22:13:48 +02:00
|
|
|
export function process_from_server(messages: ServerMessage[]): ServerMessage[] {
|
2024-07-15 16:23:45 +02:00
|
|
|
const msgs_to_rerender_or_add_to_narrow = [];
|
2024-08-10 22:13:48 +02:00
|
|
|
// For messages that weren't locally echoed, we go through the
|
|
|
|
// "main" codepath that doesn't have to id reconciliation. We
|
|
|
|
// simply return non-echo messages to our caller.
|
2019-11-02 00:06:25 +01:00
|
|
|
const non_echo_messages = [];
|
2017-09-26 19:22:52 +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 message of messages) {
|
2013-12-19 17:03:08 +01:00
|
|
|
// In case we get the sent message before we get the send ACK, reify here
|
|
|
|
|
2020-04-09 21:09:17 +02:00
|
|
|
const local_id = message.local_id;
|
2024-08-10 22:13:48 +02:00
|
|
|
|
|
|
|
if (local_id === undefined) {
|
|
|
|
// The server only returns local_id to the client whose
|
|
|
|
// queue_id was in the message send request, aka the
|
|
|
|
// client that sent it. Messages sent by another client,
|
|
|
|
// or where we didn't pass a local ID to the server,
|
|
|
|
// cannot have been locally echoed.
|
|
|
|
non_echo_messages.push(message);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2024-08-03 14:13:29 +02:00
|
|
|
const client_message = echo_state.get_message_waiting_for_ack(local_id);
|
2017-09-26 19:22:52 +02:00
|
|
|
if (client_message === undefined) {
|
|
|
|
non_echo_messages.push(message);
|
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;
|
2017-09-26 19:22:52 +02:00
|
|
|
}
|
|
|
|
|
2021-02-10 16:53:37 +01:00
|
|
|
reify_message_id(local_id, message.id);
|
2017-09-26 19:22:52 +02:00
|
|
|
|
2024-08-10 22:13:48 +02:00
|
|
|
if (message_store.get(message.id)?.failed_request) {
|
2020-07-12 23:21:05 +02:00
|
|
|
failed_message_success(message.id);
|
|
|
|
}
|
|
|
|
|
2017-09-26 19:22:52 +02:00
|
|
|
if (client_message.content !== message.content) {
|
|
|
|
client_message.content = message.content;
|
2020-04-09 21:09:17 +02:00
|
|
|
sent_messages.mark_disparity(local_id);
|
2013-12-19 17:03:08 +01:00
|
|
|
}
|
2023-05-03 00:01:10 +02:00
|
|
|
sent_messages.report_event_received(local_id);
|
2017-09-26 21:12:27 +02:00
|
|
|
|
2017-12-16 23:25:31 +01:00
|
|
|
message_store.update_booleans(client_message, message.flags);
|
2017-10-12 22:11:43 +02:00
|
|
|
|
|
|
|
// We don't try to highlight alert words locally, so we have to
|
|
|
|
// do it now. (Note that we will indeed highlight alert words in
|
|
|
|
// messages that we sent to ourselves, since we might want to test
|
|
|
|
// that our alert words are set up correctly.)
|
|
|
|
alert_words.process_message(client_message);
|
|
|
|
|
2017-09-26 22:01:39 +02:00
|
|
|
// Previously, the message had the "local echo" timestamp set
|
|
|
|
// by the browser; if there was some round-trip delay to the
|
|
|
|
// server, the actual server-side timestamp could be slightly
|
|
|
|
// different. This corrects the frontend timestamp to match
|
|
|
|
// the backend.
|
2017-09-26 21:12:27 +02:00
|
|
|
client_message.timestamp = message.timestamp;
|
|
|
|
|
2024-08-10 22:13:48 +02:00
|
|
|
client_message.topic_links = message.topic_links ?? [];
|
2019-06-21 06:31:52 +02:00
|
|
|
client_message.is_me_message = message.is_me_message;
|
2018-02-12 15:12:40 +01:00
|
|
|
client_message.submessages = message.submessages;
|
|
|
|
|
2024-07-15 16:23:45 +02:00
|
|
|
msgs_to_rerender_or_add_to_narrow.push(client_message);
|
2024-08-03 14:13:29 +02:00
|
|
|
echo_state.remove_message_from_waiting_for_ack(local_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
|
|
|
}
|
2013-12-19 17:03:08 +01:00
|
|
|
|
2024-07-15 16:23:45 +02:00
|
|
|
if (msgs_to_rerender_or_add_to_narrow.length > 0) {
|
2022-08-17 07:10:19 +02:00
|
|
|
for (const msg_list of message_lists.all_rendered_message_lists()) {
|
2024-07-15 16:23:45 +02:00
|
|
|
if (!msg_list.data.filter.can_apply_locally()) {
|
2024-07-10 14:03:58 +02:00
|
|
|
// If this message list is a search filter that we
|
|
|
|
// cannot apply locally, we will not have locally
|
|
|
|
// echoed echoed the message at all originally, and
|
|
|
|
// must the server now whether to add it to the view.
|
2024-07-15 16:23:45 +02:00
|
|
|
message_events_util.maybe_add_narrowed_messages(
|
|
|
|
msgs_to_rerender_or_add_to_narrow,
|
|
|
|
msg_list,
|
|
|
|
message_util.add_new_messages,
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
// In theory, we could just rerender messages where there were
|
|
|
|
// changes in either the rounded timestamp we display or the
|
|
|
|
// message content, but in practice, there's no harm to just
|
|
|
|
// doing it unconditionally.
|
|
|
|
msg_list.view.rerender_messages(msgs_to_rerender_or_add_to_narrow);
|
|
|
|
}
|
2013-12-19 17:03:08 +01:00
|
|
|
}
|
|
|
|
}
|
2017-09-26 21:04:23 +02:00
|
|
|
|
2017-09-26 19:22:52 +02:00
|
|
|
return non_echo_messages;
|
2021-02-10 16:53:37 +01:00
|
|
|
}
|
2013-12-19 17:03:08 +01:00
|
|
|
|
2024-08-10 22:13:48 +02:00
|
|
|
export function message_send_error(message_id: number, error_response: string): void {
|
2013-12-19 17:03:08 +01:00
|
|
|
// Error sending message, show inline
|
2024-08-10 22:13:48 +02:00
|
|
|
const message = message_store.get(message_id)!;
|
2024-01-19 09:03:45 +01:00
|
|
|
message.failed_request = true;
|
|
|
|
message.show_slow_send_spinner = false;
|
|
|
|
|
2023-04-25 05:36:10 +02:00
|
|
|
show_message_failed(message_id, error_response);
|
2021-02-10 16:53:37 +01:00
|
|
|
}
|
2013-12-19 17:03:08 +01:00
|
|
|
|
2024-08-10 22:13:48 +02:00
|
|
|
function abort_message(message: Message): void {
|
2024-08-12 11:23:25 +02:00
|
|
|
// Update the rendered data first since it is most user visible.
|
2024-02-05 19:03:29 +01:00
|
|
|
for (const msg_list of message_lists.all_rendered_message_lists()) {
|
2020-11-12 22:03:45 +01:00
|
|
|
msg_list.remove_and_rerender([message.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
|
|
|
}
|
2024-08-12 11:23:25 +02:00
|
|
|
|
|
|
|
for (const msg_list_data of message_lists.non_rendered_data()) {
|
|
|
|
msg_list_data.remove([message.id]);
|
|
|
|
}
|
2013-12-19 17:03:08 +01:00
|
|
|
}
|
|
|
|
|
2024-08-10 22:13:48 +02:00
|
|
|
export function display_slow_send_loading_spinner(message: Message): void {
|
2024-01-19 07:37:02 +01:00
|
|
|
const $rows = message_lists.all_rendered_row_for_message_id(message.id);
|
2023-03-17 19:54:09 +01:00
|
|
|
if (message.locally_echoed && !message.failed_request) {
|
2024-01-19 09:03:45 +01:00
|
|
|
message.show_slow_send_spinner = true;
|
2024-01-19 07:37:02 +01:00
|
|
|
$rows.find(".slow-send-spinner").removeClass("hidden");
|
2023-03-17 19:54:09 +01:00
|
|
|
// We don't need to do anything special to ensure this gets
|
|
|
|
// cleaned up if the message is delivered, because the
|
|
|
|
// message's HTML gets replaced once the message is
|
|
|
|
// successfully sent.
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-08-10 22:13:48 +02:00
|
|
|
export function initialize({
|
|
|
|
on_send_message_success,
|
|
|
|
send_message,
|
|
|
|
}: {
|
|
|
|
on_send_message_success: (request: Message, data: PostMessageAPIData) => void;
|
|
|
|
send_message: (
|
|
|
|
request: Message,
|
|
|
|
on_success: (raw_data: unknown) => void,
|
|
|
|
error: (response: string, _server_error_code: string) => void,
|
|
|
|
) => void;
|
|
|
|
}): void {
|
|
|
|
function on_failed_action(
|
|
|
|
selector: string,
|
|
|
|
callback: (
|
|
|
|
message: Message,
|
|
|
|
$row: JQuery,
|
|
|
|
{
|
|
|
|
on_send_message_success,
|
|
|
|
send_message,
|
|
|
|
}: {
|
|
|
|
on_send_message_success: (request: Message, data: PostMessageAPIData) => void;
|
|
|
|
send_message: (
|
|
|
|
request: Message,
|
|
|
|
on_success: (raw_data: unknown) => void,
|
|
|
|
error: (response: string, _server_error_code: string) => void,
|
|
|
|
) => void;
|
|
|
|
},
|
|
|
|
) => void,
|
|
|
|
): void {
|
|
|
|
$("#main_div").on("click", selector, function (this: HTMLElement, e) {
|
2013-12-19 17:03:08 +01:00
|
|
|
e.stopPropagation();
|
2022-01-25 11:36:19 +01:00
|
|
|
const $row = $(this).closest(".message_row");
|
|
|
|
const local_id = rows.local_echo_id($row);
|
2013-12-19 17:03:08 +01:00
|
|
|
// Message should be waiting for ack and only have a local id,
|
|
|
|
// otherwise send would not have failed
|
2024-08-03 14:13:29 +02:00
|
|
|
const message = echo_state.get_message_waiting_for_ack(local_id);
|
2013-12-19 17:03:08 +01:00
|
|
|
if (message === undefined) {
|
2020-07-15 00:34:28 +02:00
|
|
|
blueslip.warn(
|
|
|
|
"Got resend or retry on failure request but did not find message in ack list " +
|
|
|
|
local_id,
|
|
|
|
);
|
2013-12-19 17:03:08 +01:00
|
|
|
return;
|
|
|
|
}
|
2023-10-06 19:30:38 +02:00
|
|
|
callback(message, $row, {on_send_message_success, send_message});
|
2013-12-19 17:03:08 +01:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2021-02-03 23:23:32 +01:00
|
|
|
on_failed_action(".remove-failed-message", abort_message);
|
|
|
|
on_failed_action(".refresh-failed-message", resend_message);
|
2021-02-10 16:53:37 +01:00
|
|
|
}
|