2021-03-11 05:43:45 +01:00
|
|
|
import $ from "jquery";
|
|
|
|
|
2021-02-28 00:50:52 +01:00
|
|
|
import * as compose_pm_pill from "./compose_pm_pill";
|
2023-10-02 22:56:07 +02:00
|
|
|
import {$t} from "./i18n";
|
2023-10-06 09:37:10 +02:00
|
|
|
import * as people from "./people";
|
2023-05-18 15:53:21 +02:00
|
|
|
import * as sub_store from "./sub_store";
|
2021-02-28 00:38:58 +01:00
|
|
|
|
2024-01-07 23:28:42 +01:00
|
|
|
let message_type: "stream" | "private" | undefined;
|
2023-01-09 16:45:16 +01:00
|
|
|
let recipient_edited_manually = false;
|
2024-05-25 23:34:26 +02:00
|
|
|
let is_content_unedited_restored_draft = false;
|
2024-05-15 01:35:33 +02:00
|
|
|
let last_focused_compose_type_input: HTMLTextAreaElement | undefined;
|
2023-01-09 16:45:16 +01:00
|
|
|
|
2023-02-02 05:43:24 +01:00
|
|
|
// We use this variable to keep track of whether user has viewed the topic resolved
|
|
|
|
// banner for the current compose session, for a narrow. This prevents the banner
|
|
|
|
// from popping up for every keystroke while composing.
|
|
|
|
// The variable is reset on sending a message, closing the compose box and changing
|
|
|
|
// the narrow and the user should still be able to see the banner once after
|
|
|
|
// performing these actions
|
|
|
|
let recipient_viewed_topic_resolved_banner = false;
|
|
|
|
|
2023-11-02 19:27:34 +01:00
|
|
|
export function set_recipient_edited_manually(flag: boolean): void {
|
2023-01-09 16:45:16 +01:00
|
|
|
recipient_edited_manually = flag;
|
|
|
|
}
|
|
|
|
|
2023-11-02 19:27:34 +01:00
|
|
|
export function is_recipient_edited_manually(): boolean {
|
2023-01-09 16:45:16 +01:00
|
|
|
return recipient_edited_manually;
|
|
|
|
}
|
2017-04-15 01:15:59 +02:00
|
|
|
|
2024-05-25 23:34:26 +02:00
|
|
|
export function set_is_content_unedited_restored_draft(flag: boolean): void {
|
|
|
|
is_content_unedited_restored_draft = flag;
|
|
|
|
}
|
|
|
|
|
|
|
|
export function get_is_content_unedited_restored_draft(): boolean {
|
|
|
|
return is_content_unedited_restored_draft;
|
|
|
|
}
|
|
|
|
|
2024-05-15 01:35:33 +02:00
|
|
|
export function set_last_focused_compose_type_input(element: HTMLTextAreaElement): void {
|
2023-10-31 16:02:06 +01:00
|
|
|
last_focused_compose_type_input = element;
|
|
|
|
}
|
|
|
|
|
2024-05-15 01:35:33 +02:00
|
|
|
export function get_last_focused_compose_type_input(): HTMLTextAreaElement | undefined {
|
2023-10-31 16:02:06 +01:00
|
|
|
return last_focused_compose_type_input;
|
|
|
|
}
|
|
|
|
|
2024-01-07 23:28:42 +01:00
|
|
|
export function set_message_type(msg_type: "stream" | "private" | undefined): void {
|
2017-04-15 01:15:59 +02:00
|
|
|
message_type = msg_type;
|
2021-02-28 00:50:52 +01:00
|
|
|
}
|
2017-04-15 01:15:59 +02:00
|
|
|
|
2024-01-07 23:28:42 +01:00
|
|
|
export function get_message_type(): "stream" | "private" | undefined {
|
2017-04-15 01:15:59 +02:00
|
|
|
return message_type;
|
2021-02-28 00:50:52 +01:00
|
|
|
}
|
2017-04-15 01:15:59 +02:00
|
|
|
|
2023-11-02 19:27:34 +01:00
|
|
|
export function set_recipient_viewed_topic_resolved_banner(flag: boolean): void {
|
2023-02-02 05:43:24 +01:00
|
|
|
recipient_viewed_topic_resolved_banner = flag;
|
|
|
|
}
|
|
|
|
|
2023-11-02 19:27:34 +01:00
|
|
|
export function has_recipient_viewed_topic_resolved_banner(): boolean {
|
2023-02-02 05:43:24 +01:00
|
|
|
return recipient_viewed_topic_resolved_banner;
|
|
|
|
}
|
|
|
|
|
2023-11-02 19:27:34 +01:00
|
|
|
export function composing(): boolean {
|
2017-04-24 20:35:26 +02:00
|
|
|
// This is very similar to get_message_type(), but it returns
|
|
|
|
// a boolean.
|
2020-10-07 12:54:16 +02:00
|
|
|
return Boolean(message_type);
|
2021-02-28 00:50:52 +01:00
|
|
|
}
|
2017-04-15 01:15:59 +02:00
|
|
|
|
2023-11-02 19:27:34 +01:00
|
|
|
function get_or_set(
|
|
|
|
input_selector: string,
|
|
|
|
keep_leading_whitespace?: boolean,
|
|
|
|
no_trim?: boolean,
|
|
|
|
): (newval?: string) => string {
|
2022-01-25 11:36:19 +01:00
|
|
|
// We can't hoist the assignment of '$elem' out of this lambda,
|
2017-04-15 01:15:59 +02:00
|
|
|
// because the DOM element might not exist yet when get_or_set
|
|
|
|
// is called.
|
|
|
|
return function (newval) {
|
2023-11-02 19:27:34 +01:00
|
|
|
const $elem = $<HTMLInputElement | HTMLTextAreaElement>(input_selector);
|
|
|
|
const oldval = $elem.val()!;
|
2017-04-15 01:15:59 +02:00
|
|
|
if (newval !== undefined) {
|
2022-01-25 11:36:19 +01:00
|
|
|
$elem.val(newval);
|
2017-04-15 01:15:59 +02:00
|
|
|
}
|
2023-01-08 18:45:18 +01:00
|
|
|
if (no_trim) {
|
|
|
|
return oldval;
|
|
|
|
} else if (keep_leading_whitespace) {
|
|
|
|
return oldval.trimEnd();
|
|
|
|
}
|
|
|
|
return oldval.trim();
|
2017-04-15 01:15:59 +02:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2023-10-05 08:04:23 +02:00
|
|
|
// selected_recipient_id is the current state for the stream picker widget:
|
|
|
|
// "" -> stream message but no stream is selected
|
|
|
|
// integer -> stream id of the selected stream.
|
|
|
|
// "direct" -> Direct message is selected.
|
2023-11-02 19:27:34 +01:00
|
|
|
export let selected_recipient_id: number | "direct" | "" = "";
|
2024-03-29 00:11:41 +01:00
|
|
|
export const DIRECT_MESSAGE_ID = "direct";
|
2023-10-05 08:04:23 +02:00
|
|
|
|
2023-11-02 19:27:34 +01:00
|
|
|
export function set_selected_recipient_id(recipient_id: number | "direct" | ""): void {
|
2023-10-05 08:04:23 +02:00
|
|
|
selected_recipient_id = recipient_id;
|
|
|
|
}
|
|
|
|
|
2023-11-02 19:27:34 +01:00
|
|
|
export function stream_id(): number | undefined {
|
2023-10-05 08:04:23 +02:00
|
|
|
const stream_id = selected_recipient_id;
|
2023-05-26 02:48:11 +02:00
|
|
|
if (typeof stream_id === "number") {
|
|
|
|
return stream_id;
|
|
|
|
}
|
2023-09-26 20:28:39 +02:00
|
|
|
return undefined;
|
2023-05-26 02:48:11 +02:00
|
|
|
}
|
|
|
|
|
2023-11-02 19:27:34 +01:00
|
|
|
export function stream_name(): string {
|
2023-10-05 08:04:23 +02:00
|
|
|
const stream_id = selected_recipient_id;
|
2023-05-07 14:45:04 +02:00
|
|
|
if (typeof stream_id === "number") {
|
2023-12-22 01:46:30 +01:00
|
|
|
return sub_store.maybe_get_stream_name(stream_id) ?? "";
|
2023-05-07 14:45:04 +02:00
|
|
|
}
|
|
|
|
return "";
|
|
|
|
}
|
|
|
|
|
2023-11-02 19:27:34 +01:00
|
|
|
export function set_stream_id(stream_id: number | ""): void {
|
2023-10-05 08:04:23 +02:00
|
|
|
set_selected_recipient_id(stream_id);
|
2022-10-27 00:22:29 +02:00
|
|
|
}
|
|
|
|
|
2023-11-02 19:27:34 +01:00
|
|
|
export function set_compose_recipient_id(recipient_id: number | "direct"): void {
|
2023-10-05 08:04:23 +02:00
|
|
|
set_selected_recipient_id(recipient_id);
|
2022-10-27 00:22:29 +02:00
|
|
|
}
|
2021-02-28 00:50:52 +01:00
|
|
|
|
2022-10-27 00:22:29 +02:00
|
|
|
// TODO: Break out setter and getter into their own functions.
|
2023-11-02 19:29:54 +01:00
|
|
|
export const topic = get_or_set("input#stream_message_recipient_topic");
|
2021-02-28 00:50:52 +01:00
|
|
|
|
2023-11-02 19:27:34 +01:00
|
|
|
export function empty_topic_placeholder(): string {
|
2023-10-02 22:56:07 +02:00
|
|
|
return $t({defaultMessage: "(no topic)"});
|
|
|
|
}
|
|
|
|
|
2017-11-26 20:37:44 +01:00
|
|
|
// We can't trim leading whitespace in `compose_textarea` because
|
2017-04-15 01:15:59 +02:00
|
|
|
// of the indented syntax for multi-line code blocks.
|
2023-11-02 19:29:54 +01:00
|
|
|
export const message_content = get_or_set("textarea#compose-textarea", true);
|
2021-02-27 18:19:48 +01:00
|
|
|
|
2023-11-02 19:29:54 +01:00
|
|
|
const untrimmed_message_content = get_or_set("textarea#compose-textarea", true, true);
|
2023-01-08 18:45:18 +01:00
|
|
|
|
2023-11-02 19:27:34 +01:00
|
|
|
function cursor_at_start_of_whitespace_in_compose(): boolean {
|
2023-11-02 19:29:54 +01:00
|
|
|
const cursor_position = $("textarea#compose-textarea").caret();
|
2023-01-08 18:45:18 +01:00
|
|
|
return message_content() === "" && cursor_position === 0;
|
|
|
|
}
|
|
|
|
|
2023-11-02 19:27:34 +01:00
|
|
|
export function focus_in_empty_compose(
|
|
|
|
consider_start_of_whitespace_message_empty = false,
|
|
|
|
): boolean {
|
2022-01-07 10:21:55 +01:00
|
|
|
// A user trying to press arrow keys in an empty compose is mostly
|
|
|
|
// likely trying to navigate messages. This helper function
|
2023-01-08 18:45:18 +01:00
|
|
|
// decides whether the compose box is empty for this purpose.
|
2023-02-06 12:24:57 +01:00
|
|
|
if (!composing()) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// We treat the compose box as empty if it's completely empty, or
|
|
|
|
// if the caller requested, if it contains only whitespace and we're
|
|
|
|
// at the start of te compose box.
|
|
|
|
const treat_compose_as_empty =
|
|
|
|
untrimmed_message_content() === "" ||
|
|
|
|
(consider_start_of_whitespace_message_empty && cursor_at_start_of_whitespace_in_compose());
|
|
|
|
if (!treat_compose_as_empty) {
|
2022-01-07 10:21:55 +01:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2023-11-02 19:27:34 +01:00
|
|
|
const focused_element_id = document.activeElement?.id;
|
2022-01-07 10:21:55 +01:00
|
|
|
if (focused_element_id === "compose-textarea") {
|
|
|
|
// Focus will be in the compose textarea after sending a
|
|
|
|
// message; this is the most common situation.
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
// If the current focus is in one of the recipient inputs, we need
|
|
|
|
// to check whether the input is empty, to avoid accidentally
|
|
|
|
// overriding the browser feature where the Up/Down arrow keys jump
|
|
|
|
// you to the start/end of a non-empty text input.
|
|
|
|
//
|
|
|
|
// Check whether the current input element is empty for each input type.
|
|
|
|
switch (focused_element_id) {
|
|
|
|
case "private_message_recipient":
|
|
|
|
return private_message_recipient().length === 0;
|
|
|
|
case "stream_message_recipient_topic":
|
2023-02-07 01:30:29 +01:00
|
|
|
return topic() === "";
|
2023-07-19 16:09:53 +02:00
|
|
|
case "compose_select_recipient_widget_wrapper":
|
2023-09-26 20:28:39 +02:00
|
|
|
return stream_id() === undefined;
|
2022-01-07 10:21:55 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
2021-02-28 00:50:52 +01:00
|
|
|
}
|
2021-02-27 18:19:48 +01:00
|
|
|
|
2023-11-02 19:27:34 +01:00
|
|
|
export function private_message_recipient(): string;
|
|
|
|
export function private_message_recipient(value: string): undefined;
|
|
|
|
export function private_message_recipient(value?: string): string | undefined {
|
2018-03-06 15:07:55 +01:00
|
|
|
if (typeof value === "string") {
|
|
|
|
compose_pm_pill.set_from_emails(value);
|
2020-09-24 07:50:36 +02:00
|
|
|
return undefined;
|
2018-03-06 15:07:55 +01:00
|
|
|
}
|
2020-09-24 07:50:36 +02:00
|
|
|
return compose_pm_pill.get_emails();
|
2021-02-28 00:50:52 +01:00
|
|
|
}
|
2017-04-15 01:15:59 +02:00
|
|
|
|
2023-11-02 19:27:34 +01:00
|
|
|
export function has_message_content(): boolean {
|
2021-02-28 00:50:52 +01:00
|
|
|
return message_content() !== "";
|
|
|
|
}
|
2022-03-09 18:29:01 +01:00
|
|
|
|
2024-05-25 23:34:26 +02:00
|
|
|
export function has_novel_message_content(): boolean {
|
|
|
|
return message_content() !== "" && !get_is_content_unedited_restored_draft();
|
|
|
|
}
|
|
|
|
|
2024-04-20 22:28:36 +02:00
|
|
|
const MINIMUM_MESSAGE_LENGTH_TO_SAVE_DRAFT = 2;
|
2024-01-19 23:12:26 +01:00
|
|
|
export function has_savable_message_content(): boolean {
|
|
|
|
return message_content().length > MINIMUM_MESSAGE_LENGTH_TO_SAVE_DRAFT;
|
|
|
|
}
|
|
|
|
|
2023-11-02 19:27:34 +01:00
|
|
|
export function has_full_recipient(): boolean {
|
2022-09-26 21:01:43 +02:00
|
|
|
if (message_type === "stream") {
|
2023-09-26 20:28:39 +02:00
|
|
|
return stream_id() !== undefined && topic() !== "";
|
2022-09-26 21:01:43 +02:00
|
|
|
}
|
|
|
|
return private_message_recipient() !== "";
|
2022-03-09 18:29:01 +01:00
|
|
|
}
|
2023-10-06 09:37:10 +02:00
|
|
|
|
2023-11-02 19:27:34 +01:00
|
|
|
export function update_email(user_id: number, new_email: string): void {
|
2023-10-06 09:37:10 +02:00
|
|
|
let reply_to = private_message_recipient();
|
|
|
|
|
|
|
|
if (!reply_to) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
reply_to = people.update_email_in_reply_to(reply_to, user_id, new_email);
|
|
|
|
|
|
|
|
private_message_recipient(reply_to);
|
|
|
|
}
|
2024-01-19 23:12:26 +01:00
|
|
|
|
|
|
|
let _can_restore_drafts = true;
|
|
|
|
export function prevent_draft_restoring(): void {
|
|
|
|
_can_restore_drafts = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
export function allow_draft_restoring(): void {
|
|
|
|
_can_restore_drafts = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
export function can_restore_drafts(): boolean {
|
|
|
|
return _can_restore_drafts;
|
|
|
|
}
|