2019-10-12 04:48:15 +02:00
|
|
|
import _ from "underscore";
|
2017-03-22 00:41:09 +01:00
|
|
|
|
2017-11-08 17:55:36 +01:00
|
|
|
// See docs/subsystems/typing-indicators.md for details on typing indicators.
|
2017-09-25 20:33:29 +02:00
|
|
|
|
2017-03-22 00:41:09 +01: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.
|
2017-03-22 00:41:09 +01:00
|
|
|
|
|
|
|
// 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.)
|
|
|
|
*/
|
|
|
|
|
2019-10-04 23:37:28 +02:00
|
|
|
/** Exported only for tests. */
|
2019-10-12 04:48:15 +02:00
|
|
|
export const state = {};
|
2017-03-22 00:41:09 +01:00
|
|
|
|
2019-10-04 23:37:28 +02:00
|
|
|
/** Exported only for tests. */
|
2019-10-12 04:48:15 +02:00
|
|
|
export function initialize_state() {
|
|
|
|
state.current_recipient = undefined;
|
|
|
|
state.next_send_start_time = undefined;
|
|
|
|
state.idle_timer = undefined;
|
|
|
|
}
|
2017-03-22 00:41:09 +01:00
|
|
|
|
2019-10-12 04:48:15 +02:00
|
|
|
initialize_state();
|
2017-03-22 00:41:09 +01:00
|
|
|
|
2019-10-04 23:37:28 +02:00
|
|
|
/** Exported only for tests. */
|
2019-10-12 04:48:15 +02:00
|
|
|
export function stop_last_notification(worker) {
|
|
|
|
if (state.idle_timer) {
|
|
|
|
clearTimeout(state.idle_timer);
|
2017-03-22 00:41:09 +01:00
|
|
|
}
|
2019-10-12 04:48:15 +02:00
|
|
|
worker.notify_server_stop(state.current_recipient);
|
|
|
|
initialize_state();
|
|
|
|
}
|
2017-03-22 00:41:09 +01:00
|
|
|
|
2019-10-04 23:37:28 +02:00
|
|
|
/** Exported only for tests. */
|
2019-10-12 04:48:15 +02:00
|
|
|
export function start_or_extend_idle_timer(worker) {
|
2017-03-22 00:41:09 +01:00
|
|
|
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.
|
2019-10-12 04:48:15 +02:00
|
|
|
stop_last_notification(worker);
|
2017-03-22 00:41:09 +01:00
|
|
|
}
|
|
|
|
|
2019-10-12 04:48:15 +02:00
|
|
|
if (state.idle_timer) {
|
|
|
|
clearTimeout(state.idle_timer);
|
2017-03-22 00:41:09 +01:00
|
|
|
}
|
2019-10-12 04:48:15 +02:00
|
|
|
state.idle_timer = setTimeout(
|
2017-03-22 00:41:09 +01:00
|
|
|
on_idle_timeout,
|
|
|
|
TYPING_STOPPED_WAIT_PERIOD
|
|
|
|
);
|
2019-10-12 04:48:15 +02:00
|
|
|
}
|
2017-03-22 00:41:09 +01:00
|
|
|
|
|
|
|
function set_next_start_time(current_time) {
|
2019-10-12 04:48:15 +02:00
|
|
|
state.next_send_start_time = current_time + TYPING_STARTED_WAIT_PERIOD;
|
2017-03-22 00:41:09 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
function actually_ping_server(worker, recipient, current_time) {
|
|
|
|
worker.notify_server_start(recipient);
|
|
|
|
set_next_start_time(current_time);
|
|
|
|
}
|
|
|
|
|
2019-10-04 23:37:28 +02:00
|
|
|
/** Exported only for tests. */
|
2019-10-12 04:48:15 +02:00
|
|
|
export function maybe_ping_server(worker, recipient) {
|
2017-03-22 00:41:09 +01:00
|
|
|
var current_time = worker.get_current_time();
|
2019-10-12 04:48:15 +02:00
|
|
|
if (current_time > state.next_send_start_time) {
|
2017-03-22 00:41:09 +01:00
|
|
|
actually_ping_server(worker, recipient, current_time);
|
|
|
|
}
|
2019-10-12 04:48:15 +02:00
|
|
|
}
|
2017-03-22 00:41:09 +01:00
|
|
|
|
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) {
|
2019-10-21 22:41:26 +02:00
|
|
|
var current_recipient = state.current_recipient;
|
2017-03-22 00:41:09 +01:00
|
|
|
if (current_recipient) {
|
2019-06-06 21:54:55 +02:00
|
|
|
// We need to use _.isEqual for comparisons; === doesn't work
|
|
|
|
// on arrays.
|
|
|
|
if (_.isEqual(new_recipient, current_recipient)) {
|
2017-03-22 00:41:09 +01:00
|
|
|
// Nothing has really changed, except we may need
|
|
|
|
// to send a ping to the server.
|
2019-10-12 04:48:15 +02:00
|
|
|
maybe_ping_server(worker, new_recipient);
|
2017-03-22 00:41:09 +01:00
|
|
|
|
|
|
|
// We can also extend out our idle time.
|
2019-10-12 04:48:15 +02:00
|
|
|
start_or_extend_idle_timer(worker);
|
2017-03-22 00:41:09 +01:00
|
|
|
|
|
|
|
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.
|
2019-10-12 04:48:15 +02:00
|
|
|
stop_last_notification(worker);
|
2017-03-22 00:41:09 +01:00
|
|
|
}
|
|
|
|
|
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) {
|
2017-03-22 00:41:09 +01:00
|
|
|
// 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.
|
2019-10-12 04:48:15 +02:00
|
|
|
state.current_recipient = new_recipient;
|
2017-03-22 00:41:09 +01:00
|
|
|
var current_time = worker.get_current_time();
|
|
|
|
actually_ping_server(worker, new_recipient, current_time);
|
2019-10-12 04:48:15 +02:00
|
|
|
start_or_extend_idle_timer(worker);
|
|
|
|
}
|
2017-03-22 00:41:09 +01:00
|
|
|
|
2019-10-12 04:48:15 +02:00
|
|
|
export function stop(worker) {
|
2017-03-22 00:41:09 +01:00
|
|
|
// We get this if somebody closes the compose box, but
|
|
|
|
// it doesn't necessarily mean we had typing indicators
|
|
|
|
// active before this.
|
2019-10-12 04:48:15 +02:00
|
|
|
if (state.current_recipient) {
|
|
|
|
stop_last_notification(worker);
|
2017-03-22 00:41:09 +01:00
|
|
|
}
|
|
|
|
}
|