zulip/web/src/typing.js

135 lines
4.2 KiB
JavaScript
Raw Normal View History

import $ from "jquery";
import * as typing_status from "../shared/src/typing_status";
import * as blueslip from "./blueslip";
import * as channel from "./channel";
import * as compose_pm_pill from "./compose_pm_pill";
import * as compose_state from "./compose_state";
import {page_params} from "./page_params";
import * as people from "./people";
import * as stream_data from "./stream_data";
import {user_settings} from "./user_settings";
2020-08-20 21:24:06 +02:00
2017-03-22 15:11:41 +01:00
// This module handles the outbound side of typing indicators.
// We detect changes in the compose box and notify the server
// when we are typing. For the inbound side see typing_events.js.
// See docs/subsystems/typing-indicators.md for more details.
// How frequently 'start' notifications are sent to extend
// the expiry of active typing indicators.
const typing_started_wait_period = page_params.server_typing_started_wait_period_milliseconds;
// How long after someone stops editing in the compose box
// do we send a 'stop' notification.
const typing_stopped_wait_period = page_params.server_typing_stopped_wait_period_milliseconds;
function send_typing_notification_ajax(data) {
channel.post({
url: "/json/typing",
data,
success() {},
error(xhr) {
if (xhr.readyState !== 0) {
blueslip.warn("Failed to send typing event: " + xhr.responseText);
}
},
});
}
function send_direct_message_typing_notification(user_ids_array, operation) {
const data = {
to: JSON.stringify(user_ids_array),
op: operation,
};
send_typing_notification_ajax(data);
}
function send_stream_typing_notification(stream_id, topic, operation) {
const data = {
type: "stream",
stream_id: JSON.stringify(stream_id),
topic,
op: operation,
};
send_typing_notification_ajax(data);
}
function send_typing_notification_based_on_message_type(to, operation) {
const message_type = to.stream_id ? "stream" : "direct";
if (message_type === "direct" && user_settings.send_private_typing_notifications) {
send_direct_message_typing_notification(to, operation);
} else if (message_type === "stream" && user_settings.send_stream_typing_notifications) {
send_stream_typing_notification(to.stream_id, to.topic, operation);
}
}
function get_user_ids_array() {
const user_ids_string = compose_pm_pill.get_user_ids_string();
if (user_ids_string === "") {
return null;
}
return people.user_ids_string_to_ids_array(user_ids_string);
}
typing_status: Combine two parameters into one, with a maybe-type. The main motivation for this change is to simplify this interface and make it easier to reason about. The case where it affects the behavior is when is_valid_conversation() returns false, while current_recipient and get_recipient() agree on some truthy value. This means the message-content textarea is empty -- in fact the user just cleared it, because we got here from an input event on it -- but the compose box is still open to some PM thread that we have a typing notification still outstanding for. The old behavior is that in this situation we would ignore the fact that the content was empty, and go ahead and prolong the typing notification, by updating our timer and possibly sending a "still typing" notice. This contrasts with the behavior (both old and new) in the case where the content is empty and we *don't* already have an outstanding typing notification, or we have one to some other thread. In that case, we cancel any existing notification and don't start a new one, exactly as if `stop` were called (e.g. because the user closed the compose box.) The new behavior is that we always treat clearing the input as "stopped typing": not only in those cases where we already did, but also in the case where we still have the same recipients. (Which seems like probably the common case.) That seems like the preferable behavior; indeed it's hard to see the point of the "compose_empty" logic if restricted to the other cases. It also makes the interface simpler. Those two properties don't seem like a coincidence, either: the complicated interface made it difficult to unpack exactly what logic we actually had, which made it easy for surprising wrinkles to hang out indefinitely.
2019-10-21 23:37:22 +02:00
function is_valid_conversation() {
const compose_empty = !compose_state.has_message_content();
if (compose_empty) {
return false;
}
return true;
}
function get_current_time() {
return Date.now();
}
function notify_server_start(to) {
send_typing_notification_based_on_message_type(to, "start");
}
function notify_server_stop(to) {
send_typing_notification_based_on_message_type(to, "stop");
}
export function get_recipient() {
const message_type = compose_state.get_message_type();
if (message_type === "private") {
return get_user_ids_array();
}
if (message_type === "stream") {
const stream_name = compose_state.stream_name();
const stream_id = stream_data.get_stream_id(stream_name);
const topic = compose_state.topic();
return {stream_id, topic};
}
return null;
}
export function initialize() {
const worker = {
get_current_time,
notify_server_start,
notify_server_stop,
};
$(document).on("input", "#compose-textarea", () => {
// If our previous state was no typing notification, send a
// start-typing notice immediately.
const new_recipient = is_valid_conversation() ? get_recipient() : null;
typing_status.update(
worker,
new_recipient,
typing_started_wait_period,
typing_stopped_wait_period,
);
});
// We send a stop-typing notification immediately when compose is
// closed/cancelled
$(document).on("compose_canceled.zulip compose_finished.zulip", () => {
typing_status.update(worker, null, typing_started_wait_period, typing_stopped_wait_period);
});
}