zulip/static/shared/js/typing_status.js

132 lines
3.9 KiB
JavaScript
Raw Normal View History

import _ from "underscore";
// See docs/subsystems/typing-indicators.md for details on typing indicators.
2017-09-25 20:33:29 +02:00
// The following constants are tuned to work with
// TYPING_STARTED_EXPIRY_PERIOD, which is what the other
// users will use to time out our messages. (Or us,
2017-03-22 15:11:41 +01:00
// depending on your perspective.) See typing_events.js.
// How frequently 'still typing' notifications are sent
// to extend the expiry
var TYPING_STARTED_WAIT_PERIOD = 10000; // 10s
// How long after someone stops editing in the compose box
// do we send a 'stopped typing' notification
var TYPING_STOPPED_WAIT_PERIOD = 5000; // 5s
/*
Our parent should pass in a worker object with the following
callbacks:
notify_server_start
notify_server_stop
get_current_time
See typing.js for the implementations of the above. (Our
node tests also act as workers and will stub those functions
appropriately.)
*/
/** Exported only for tests. */
export const state = {};
/** Exported only for tests. */
export function initialize_state() {
state.current_recipient = undefined;
state.next_send_start_time = undefined;
state.idle_timer = undefined;
}
initialize_state();
/** Exported only for tests. */
export function stop_last_notification(worker) {
if (state.idle_timer) {
clearTimeout(state.idle_timer);
}
worker.notify_server_stop(state.current_recipient);
initialize_state();
}
/** Exported only for tests. */
export function start_or_extend_idle_timer(worker) {
function on_idle_timeout() {
// We don't do any real error checking here, because
// if we've been idle, we need to tell folks, and if
// our current recipient has changed, previous code will
// have stopped the timer.
stop_last_notification(worker);
}
if (state.idle_timer) {
clearTimeout(state.idle_timer);
}
state.idle_timer = setTimeout(
on_idle_timeout,
TYPING_STOPPED_WAIT_PERIOD
);
}
function set_next_start_time(current_time) {
state.next_send_start_time = current_time + TYPING_STARTED_WAIT_PERIOD;
}
function actually_ping_server(worker, recipient, current_time) {
worker.notify_server_start(recipient);
set_next_start_time(current_time);
}
/** Exported only for tests. */
export function maybe_ping_server(worker, recipient) {
var current_time = worker.get_current_time();
if (current_time > state.next_send_start_time) {
actually_ping_server(worker, recipient, current_time);
}
}
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
export function handle_text_input(worker, new_recipient) {
var current_recipient = state.current_recipient;
if (current_recipient) {
// We need to use _.isEqual for comparisons; === doesn't work
// on arrays.
if (_.isEqual(new_recipient, current_recipient)) {
// Nothing has really changed, except we may need
// to send a ping to the server.
maybe_ping_server(worker, new_recipient);
// We can also extend out our idle time.
start_or_extend_idle_timer(worker);
return;
}
// We apparently stopped talking to our old recipient,
// so we must stop the old notification. Don't return
// yet, because we may have a new recipient.
stop_last_notification(worker);
}
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
if (!new_recipient) {
// If we are not talking to somebody we care about,
// then there is no more action to take.
return;
}
// We just started talking to this recipient, so notify
// the server.
state.current_recipient = new_recipient;
var current_time = worker.get_current_time();
actually_ping_server(worker, new_recipient, current_time);
start_or_extend_idle_timer(worker);
}
export function stop(worker) {
// We get this if somebody closes the compose box, but
// it doesn't necessarily mean we had typing indicators
// active before this.
if (state.current_recipient) {
stop_last_notification(worker);
}
}