message_edit: Show typing indicator for message editing.

This commit adds typing indicators for message editing in stream
as well as in dm, if the send typing notification
for corresponding is enabled.

Based on earlier work in #28585.

Co-authored-by: Rohan Gudimetla <rohan.gudimetla07@gmail.com>

Fixes #25719.
This commit is contained in:
opmkumar
2025-02-07 00:54:43 +05:30
committed by Tim Abbott
parent c2821ef0f4
commit 2a15da47d9
28 changed files with 1477 additions and 38 deletions

View File

@@ -14,13 +14,24 @@ export type Recipient =
| (StreamTopic & {
message_type: "stream";
notification_event_type: "typing";
});
})
| {
notification_event_type: "typing_message_edit";
message_id: number;
};
type TypingStatusWorker = {
get_current_time: () => number;
notify_server_start: (recipient: Recipient) => void;
notify_server_stop: (recipient: Recipient) => void;
};
export type EditingStatusWorker = {
get_current_time: () => number;
notify_server_editing_start: (recipient: Recipient) => void;
notify_server_editing_stop: (recipient: Recipient) => void;
};
type TypingStatusState = {
current_recipient: Recipient;
next_send_start_time: number;
@@ -41,19 +52,21 @@ function same_recipient(a: Recipient | null, b: Recipient | null): boolean {
return false;
}
if (a.message_type === "direct" && b.message_type === "direct") {
// direct message recipients
return _.isEqual(a.ids, b.ids);
} else if (a.message_type === "stream" && b.message_type === "stream") {
// stream recipients
return same_stream_and_topic(a, b);
if (a.notification_event_type === "typing" && b.notification_event_type === "typing") {
if (a.message_type === "direct" && b.message_type === "direct") {
// direct message recipients
return _.isEqual(a.ids, b.ids);
} else if (a.message_type === "stream" && b.message_type === "stream") {
// stream recipients
return same_stream_and_topic(a, b);
}
}
return false;
}
/** Exported only for tests. */
export let state: TypingStatusState | null = null;
export const editing_state = new Map<number, TypingStatusState>();
export function rewire_state(value: typeof state): void {
state = value;
@@ -67,6 +80,18 @@ export let stop_last_notification = (worker: TypingStatusWorker): void => {
state = null;
};
export function stop_notification_for_message_edit(
worker: EditingStatusWorker,
message_id: number,
): void {
const state = editing_state.get(message_id);
if (state !== undefined) {
clearTimeout(state.idle_timer);
worker.notify_server_editing_stop(state.current_recipient);
editing_state.delete(message_id);
}
}
export function rewire_stop_last_notification(value: typeof stop_last_notification): void {
stop_last_notification = value;
}
@@ -90,6 +115,26 @@ export let start_or_extend_idle_timer = (
return setTimeout(on_idle_timeout, typing_stopped_wait_period);
};
function start_or_extend_idle_timer_for_message_edit(
worker: EditingStatusWorker,
message_id: number,
typing_stopped_wait_period: number,
): ReturnType<typeof setTimeout> {
function on_idle_timeout(): void {
// We don't do any real error checking here, because
// if we've been idle, we need to tell folks, and if
// our current recipients has changed, previous code will
// have stopped the timer.
stop_notification_for_message_edit(worker, message_id);
}
const state = editing_state.get(message_id);
if (state?.idle_timer) {
clearTimeout(state.idle_timer);
}
return setTimeout(on_idle_timeout, typing_stopped_wait_period);
}
export function rewire_start_or_extend_idle_timer(value: typeof start_or_extend_idle_timer): void {
start_or_extend_idle_timer = value;
}
@@ -99,6 +144,17 @@ function set_next_start_time(current_time: number, typing_started_wait_period: n
state.next_send_start_time = current_time + typing_started_wait_period;
}
function set_next_start_time_for_message_edit(
current_time: number,
typing_started_wait_period: number,
message_id: number,
): void {
const state = editing_state.get(message_id);
assert(state !== undefined);
state.next_send_start_time = current_time + typing_started_wait_period;
editing_state.set(message_id, state);
}
// Exported for tests
export let actually_ping_server = (
worker: TypingStatusWorker,
@@ -110,6 +166,21 @@ export let actually_ping_server = (
set_next_start_time(current_time, typing_started_wait_period);
};
function actually_ping_server_for_message_edit(
worker: EditingStatusWorker,
recipient: Recipient,
current_time: number,
typing_started_wait_period: number,
): void {
assert(recipient.notification_event_type === "typing_message_edit");
worker.notify_server_editing_start(recipient);
set_next_start_time_for_message_edit(
current_time,
typing_started_wait_period,
recipient.message_id,
);
}
export function rewire_actually_ping_server(value: typeof actually_ping_server): void {
actually_ping_server = value;
}
@@ -127,6 +198,24 @@ export let maybe_ping_server = (
}
};
export function maybe_ping_server_for_message_edit(
worker: EditingStatusWorker,
recipient: Recipient,
typing_started_wait_period: number,
): void {
assert(recipient.notification_event_type === "typing_message_edit");
const state = editing_state.get(recipient.message_id);
assert(state !== undefined);
const current_time = worker.get_current_time();
if (current_time > state.next_send_start_time) {
actually_ping_server_for_message_edit(
worker,
recipient,
current_time,
typing_started_wait_period,
);
}
}
export function rewire_maybe_ping_server(value: typeof maybe_ping_server): void {
maybe_ping_server = value;
}
@@ -195,3 +284,56 @@ export function update(
const current_time = worker.get_current_time();
actually_ping_server(worker, new_recipient, current_time, typing_started_wait_period);
}
export function update_editing_status(
edit_box_worker: EditingStatusWorker,
new_recipient: Recipient,
new_status: "start" | "stop",
typing_started_wait_period: number,
typing_stopped_wait_period: number,
): void {
assert(new_recipient.notification_event_type === "typing_message_edit");
const message_id = new_recipient.message_id;
if (new_status === "stop") {
stop_notification_for_message_edit(edit_box_worker, message_id);
return;
}
if (editing_state.has(message_id)) {
// Nothing has really changed, except we may need to extend out our idle time.
const state = editing_state.get(message_id)!;
state.idle_timer = start_or_extend_idle_timer_for_message_edit(
edit_box_worker,
message_id,
typing_stopped_wait_period,
);
// We may need to send a ping to the server too.
maybe_ping_server_for_message_edit(
edit_box_worker,
new_recipient,
typing_started_wait_period,
);
return;
}
const edit_state: TypingStatusState = {
current_recipient: new_recipient,
next_send_start_time: 0,
idle_timer: start_or_extend_idle_timer_for_message_edit(
edit_box_worker,
message_id,
typing_stopped_wait_period,
),
};
editing_state.set(message_id, edit_state);
const current_time = edit_box_worker.get_current_time();
actually_ping_server_for_message_edit(
edit_box_worker,
new_recipient,
current_time,
typing_started_wait_period,
);
}