typing: Add typing constants to the post register api response.

Adds typing notification constants to the response given by
`POST /register`. Until now, these were hardcoded by clients
based on the documentation for implementing typing notifications
in the main endpoint description for `api/set-typing-status`.

This change also reflects updating the web-app frontend code
to use the new constants from the register response.

Co-authored-by: Samuel Kabuya <samuel.mwangikabuya@kibo.school>
Co-authored-by: Wilhelmina Asante <wilhelmina.asante@kibo.school>
This commit is contained in:
Samuel 2023-08-17 13:42:41 +01:00 committed by Tim Abbott
parent d26a94a0db
commit 3ce7b77092
10 changed files with 146 additions and 40 deletions

View File

@ -20,6 +20,15 @@ format used by the Zulip server that they are interacting with.
## Changes in Zulip 8.0 ## Changes in Zulip 8.0
**Feature level 204**
* [`POST /register`](/api/register-queue): Added
`server_typing_started_wait_period_milliseconds`,
`server_typing_stopped_wait_period_milliseconds`, and
`server_typing_started_expiry_period_milliseconds` fields
for clients to use when implementing [typing
notifications](/api/set-typing-status) protocol.
**Feature level 203** **Feature level 203**
* [`POST /register`](/api/register-queue): Add * [`POST /register`](/api/register-queue): Add

View File

@ -33,7 +33,7 @@ DESKTOP_WARNING_VERSION = "5.9.3"
# Changes should be accompanied by documentation explaining what the # Changes should be accompanied by documentation explaining what the
# new level means in api_docs/changelog.md, as well as "**Changes**" # new level means in api_docs/changelog.md, as well as "**Changes**"
# entries in the endpoint's documentation in `zulip.yaml`. # entries in the endpoint's documentation in `zulip.yaml`.
API_FEATURE_LEVEL = 203 API_FEATURE_LEVEL = 204
# Bump the minor PROVISION_VERSION to indicate that folks should provision # Bump the minor PROVISION_VERSION to indicate that folks should provision
# only when going from an old version of the code to a newer version. Bump # only when going from an old version of the code to a newer version. Bump

View File

@ -13,18 +13,6 @@ type TypingStatusState = {
idle_timer: ReturnType<typeof setTimeout>; idle_timer: ReturnType<typeof setTimeout>;
}; };
// 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,
// depending on your perspective.) See typing_events.js.
// How frequently 'still typing' notifications are sent
// to extend the expiry
const TYPING_STARTED_WAIT_PERIOD = 10000; // 10s
// How long after someone stops editing in the compose box
// do we send a 'stopped typing' notification
const TYPING_STOPPED_WAIT_PERIOD = 5000; // 5s
/** Exported only for tests. */ /** Exported only for tests. */
export let state: TypingStatusState | null = null; export let state: TypingStatusState | null = null;
@ -39,6 +27,7 @@ export function stop_last_notification(worker: TypingStatusWorker): void {
/** Exported only for tests. */ /** Exported only for tests. */
export function start_or_extend_idle_timer( export function start_or_extend_idle_timer(
worker: TypingStatusWorker, worker: TypingStatusWorker,
typing_stopped_wait_period: number,
): ReturnType<typeof setTimeout> { ): ReturnType<typeof setTimeout> {
function on_idle_timeout(): void { function on_idle_timeout(): void {
// We don't do any real error checking here, because // We don't do any real error checking here, because
@ -51,29 +40,34 @@ export function start_or_extend_idle_timer(
if (state?.idle_timer) { if (state?.idle_timer) {
clearTimeout(state.idle_timer); clearTimeout(state.idle_timer);
} }
return setTimeout(on_idle_timeout, TYPING_STOPPED_WAIT_PERIOD); return setTimeout(on_idle_timeout, typing_stopped_wait_period);
} }
function set_next_start_time(current_time: number): void { function set_next_start_time(current_time: number, typing_started_wait_period: number): void {
assert(state !== null, "State object should not be null here."); assert(state !== null, "State object should not be null here.");
state.next_send_start_time = current_time + TYPING_STARTED_WAIT_PERIOD; state.next_send_start_time = current_time + typing_started_wait_period;
} }
function actually_ping_server( function actually_ping_server(
worker: TypingStatusWorker, worker: TypingStatusWorker,
recipient_ids: number[], recipient_ids: number[],
current_time: number, current_time: number,
typing_started_wait_period: number,
): void { ): void {
worker.notify_server_start(recipient_ids); worker.notify_server_start(recipient_ids);
set_next_start_time(current_time); set_next_start_time(current_time, typing_started_wait_period);
} }
/** Exported only for tests. */ /** Exported only for tests. */
export function maybe_ping_server(worker: TypingStatusWorker, recipient_ids: number[]): void { export function maybe_ping_server(
worker: TypingStatusWorker,
recipient_ids: number[],
typing_started_wait_period: number,
): void {
assert(state !== null, "State object should not be null here."); assert(state !== null, "State object should not be null here.");
const current_time = worker.get_current_time(); const current_time = worker.get_current_time();
if (current_time > state.next_send_start_time) { if (current_time > state.next_send_start_time) {
actually_ping_server(worker, recipient_ids, current_time); actually_ping_server(worker, recipient_ids, current_time, typing_started_wait_period);
} }
} }
@ -103,17 +97,22 @@ export function maybe_ping_server(worker: TypingStatusWorker, recipient_ids: num
* addressed to, as a sorted array of user IDs; or `null` if no direct message * addressed to, as a sorted array of user IDs; or `null` if no direct message
* is being composed anymore. * is being composed anymore.
*/ */
export function update(worker: TypingStatusWorker, new_recipient_ids: number[] | null): void { export function update(
worker: TypingStatusWorker,
new_recipient_ids: number[] | null,
typing_started_wait_period: number,
typing_stopped_wait_period: number,
): void {
if (state !== null) { if (state !== null) {
// We need to use _.isEqual for comparisons; === doesn't work // We need to use _.isEqual for comparisons; === doesn't work
// on arrays. // on arrays.
if (_.isEqual(new_recipient_ids, state.current_recipient_ids)) { if (_.isEqual(new_recipient_ids, state.current_recipient_ids)) {
// Nothing has really changed, except we may need // Nothing has really changed, except we may need
// to send a ping to the server. // to send a ping to the server.
maybe_ping_server(worker, new_recipient_ids!); maybe_ping_server(worker, new_recipient_ids!, typing_started_wait_period);
// We can also extend out our idle time. // We can also extend out our idle time.
state.idle_timer = start_or_extend_idle_timer(worker); state.idle_timer = start_or_extend_idle_timer(worker, typing_stopped_wait_period);
return; return;
} }
@ -135,8 +134,8 @@ export function update(worker: TypingStatusWorker, new_recipient_ids: number[] |
state = { state = {
current_recipient_ids: new_recipient_ids, current_recipient_ids: new_recipient_ids,
next_send_start_time: 0, next_send_start_time: 0,
idle_timer: start_or_extend_idle_timer(worker), idle_timer: start_or_extend_idle_timer(worker, typing_stopped_wait_period),
}; };
const current_time = worker.get_current_time(); const current_time = worker.get_current_time();
actually_ping_server(worker, new_recipient_ids, current_time); actually_ping_server(worker, new_recipient_ids, current_time, typing_started_wait_period);
} }

View File

@ -6,14 +6,21 @@ import * as blueslip from "./blueslip";
import * as channel from "./channel"; import * as channel from "./channel";
import * as compose_pm_pill from "./compose_pm_pill"; import * as compose_pm_pill from "./compose_pm_pill";
import * as compose_state from "./compose_state"; import * as compose_state from "./compose_state";
import {page_params} from "./page_params";
import * as people from "./people"; import * as people from "./people";
import {user_settings} from "./user_settings"; import {user_settings} from "./user_settings";
// This module handles the outbound side of typing indicators. // This module handles the outbound side of typing indicators.
// We detect changes in the compose box and notify the server // We detect changes in the compose box and notify the server
// when we are typing. For the inbound side see typing_events.js. // when we are typing. For the inbound side see typing_events.js.
// // See docs/subsystems/typing-indicators.md for more details.
// See docs/subsystems/typing-indicators.md for details on typing indicators.
// 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(user_ids_array, operation) { function send_typing_notification_ajax(user_ids_array, operation) {
channel.post({ channel.post({
@ -78,12 +85,17 @@ export function initialize() {
// If our previous state was no typing notification, send a // If our previous state was no typing notification, send a
// start-typing notice immediately. // start-typing notice immediately.
const new_recipient = is_valid_conversation() ? get_recipient() : null; const new_recipient = is_valid_conversation() ? get_recipient() : null;
typing_status.update(worker, new_recipient); typing_status.update(
worker,
new_recipient,
typing_started_wait_period,
typing_stopped_wait_period,
);
}); });
// We send a stop-typing notification immediately when compose is // We send a stop-typing notification immediately when compose is
// closed/cancelled // closed/cancelled
$(document).on("compose_canceled.zulip compose_finished.zulip", () => { $(document).on("compose_canceled.zulip compose_finished.zulip", () => {
typing_status.update(worker, null); typing_status.update(worker, null, typing_started_wait_period, typing_stopped_wait_period);
}); });
} }

View File

@ -14,10 +14,10 @@ import * as typing_data from "./typing_data";
// //
// We also handle the local event of re-narrowing. // We also handle the local event of re-narrowing.
// (For the outbound code, see typing.js.) // (For the outbound code, see typing.js.)
//
// How long before we assume a client has gone away // How long before we assume a client has gone away
// and expire its typing status // and expire the active typing indicator.
const TYPING_STARTED_EXPIRY_PERIOD = 15000; // 15s const typing_started_expiry_period = page_params.server_typing_started_expiry_period_milliseconds;
// If number of users typing exceed this, // If number of users typing exceed this,
// we render "Several people are typing..." // we render "Several people are typing..."
@ -93,7 +93,7 @@ export function display_notification(event) {
render_notifications_for_narrow(); render_notifications_for_narrow();
typing_data.kickstart_inbound_timer(recipients, TYPING_STARTED_EXPIRY_PERIOD, () => { typing_data.kickstart_inbound_timer(recipients, typing_started_expiry_period, () => {
hide_notification(event); hide_notification(event);
}); });
} }

View File

@ -10,6 +10,9 @@ const compose_pm_pill = mock_esm("../src/compose_pm_pill");
const typing = zrequire("typing"); const typing = zrequire("typing");
const typing_status = zrequire("../shared/src/typing_status"); const typing_status = zrequire("../shared/src/typing_status");
const TYPING_STARTED_WAIT_PERIOD = 10000;
const TYPING_STOPPED_WAIT_PERIOD = 5000;
function make_time(secs) { function make_time(secs) {
// make times semi-realistic // make times semi-realistic
return 1000000 + 1000 * secs; return 1000000 + 1000 * secs;
@ -26,7 +29,7 @@ run_test("basics", ({override, override_rewire}) => {
// invalid conversation basically does nothing // invalid conversation basically does nothing
let worker = {}; let worker = {};
typing_status.update(worker, null); typing_status.update(worker, null, TYPING_STARTED_WAIT_PERIOD, TYPING_STOPPED_WAIT_PERIOD);
// Start setting up more testing state. // Start setting up more testing state.
const events = {}; const events = {};
@ -63,7 +66,12 @@ run_test("basics", ({override, override_rewire}) => {
function call_handler(new_recipient) { function call_handler(new_recipient) {
clear_events(); clear_events();
typing_status.update(worker, new_recipient); typing_status.update(
worker,
new_recipient,
TYPING_STARTED_WAIT_PERIOD,
TYPING_STOPPED_WAIT_PERIOD,
);
} }
worker = { worker = {
@ -263,12 +271,22 @@ run_test("basics", ({override, override_rewire}) => {
// User ids of people in compose narrow doesn't change and is same as stat.current_recipient_ids // User ids of people in compose narrow doesn't change and is same as stat.current_recipient_ids
// so counts of function should increase except stop_last_notification // so counts of function should increase except stop_last_notification
typing_status.update(worker, typing.get_recipient()); typing_status.update(
worker,
typing.get_recipient(),
TYPING_STARTED_WAIT_PERIOD,
TYPING_STOPPED_WAIT_PERIOD,
);
assert.deepEqual(call_count.maybe_ping_server, 1); assert.deepEqual(call_count.maybe_ping_server, 1);
assert.deepEqual(call_count.start_or_extend_idle_timer, 1); assert.deepEqual(call_count.start_or_extend_idle_timer, 1);
assert.deepEqual(call_count.stop_last_notification, 0); assert.deepEqual(call_count.stop_last_notification, 0);
typing_status.update(worker, typing.get_recipient()); typing_status.update(
worker,
typing.get_recipient(),
TYPING_STARTED_WAIT_PERIOD,
TYPING_STOPPED_WAIT_PERIOD,
);
assert.deepEqual(call_count.maybe_ping_server, 2); assert.deepEqual(call_count.maybe_ping_server, 2);
assert.deepEqual(call_count.start_or_extend_idle_timer, 2); assert.deepEqual(call_count.start_or_extend_idle_timer, 2);
assert.deepEqual(call_count.stop_last_notification, 0); assert.deepEqual(call_count.stop_last_notification, 0);
@ -276,14 +294,24 @@ run_test("basics", ({override, override_rewire}) => {
// change in recipient and new_recipient should make us // change in recipient and new_recipient should make us
// call typing_status.stop_last_notification // call typing_status.stop_last_notification
override(compose_pm_pill, "get_user_ids_string", () => "2,3,4"); override(compose_pm_pill, "get_user_ids_string", () => "2,3,4");
typing_status.update(worker, typing.get_recipient()); typing_status.update(
worker,
typing.get_recipient(),
TYPING_STARTED_WAIT_PERIOD,
TYPING_STOPPED_WAIT_PERIOD,
);
assert.deepEqual(call_count.maybe_ping_server, 2); assert.deepEqual(call_count.maybe_ping_server, 2);
assert.deepEqual(call_count.start_or_extend_idle_timer, 3); assert.deepEqual(call_count.start_or_extend_idle_timer, 3);
assert.deepEqual(call_count.stop_last_notification, 1); assert.deepEqual(call_count.stop_last_notification, 1);
// Stream messages are represented as get_user_ids_string being empty // Stream messages are represented as get_user_ids_string being empty
override(compose_pm_pill, "get_user_ids_string", () => ""); override(compose_pm_pill, "get_user_ids_string", () => "");
typing_status.update(worker, typing.get_recipient()); typing_status.update(
worker,
typing.get_recipient(),
TYPING_STARTED_WAIT_PERIOD,
TYPING_STOPPED_WAIT_PERIOD,
);
assert.deepEqual(call_count.maybe_ping_server, 2); assert.deepEqual(call_count.maybe_ping_server, 2);
assert.deepEqual(call_count.start_or_extend_idle_timer, 3); assert.deepEqual(call_count.start_or_extend_idle_timer, 3);
assert.deepEqual(call_count.stop_last_notification, 2); assert.deepEqual(call_count.stop_last_notification, 2);

View File

@ -364,7 +364,16 @@ def fetch_initial_state_data(
# Presence system parameters for client behavior. # Presence system parameters for client behavior.
state["server_presence_ping_interval_seconds"] = settings.PRESENCE_PING_INTERVAL_SECS state["server_presence_ping_interval_seconds"] = settings.PRESENCE_PING_INTERVAL_SECS
state["server_presence_offline_threshold_seconds"] = settings.OFFLINE_THRESHOLD_SECS state["server_presence_offline_threshold_seconds"] = settings.OFFLINE_THRESHOLD_SECS
# Typing notifications protocol parameters for client behavior.
state[
"server_typing_started_expiry_period_milliseconds"
] = settings.TYPING_STARTED_EXPIRY_PERIOD_MILLISECONDS
state[
"server_typing_stopped_wait_period_milliseconds"
] = settings.TYPING_STOPPED_WAIT_PERIOD_MILLISECONDS
state[
"server_typing_started_wait_period_milliseconds"
] = settings.TYPING_STARTED_WAIT_PERIOD_MILLISECONDS
if want("realm_user_settings_defaults"): if want("realm_user_settings_defaults"):
realm_user_default = RealmUserDefault.objects.get(realm=realm) realm_user_default = RealmUserDefault.objects.get(realm=realm)
state["realm_user_settings_defaults"] = {} state["realm_user_settings_defaults"] = {}

View File

@ -11240,7 +11240,7 @@ paths:
**Changes**: New in Zulip 7.0 (feature level 164). Clients should use 60 **Changes**: New in Zulip 7.0 (feature level 164). Clients should use 60
for older Zulip servers, since that's the value that was hardcoded in the for older Zulip servers, since that's the value that was hardcoded in the
the Zulip mobile apps prior to this parameter being introduced. Zulip mobile apps prior to this parameter being introduced.
server_presence_offline_threshold_seconds: server_presence_offline_threshold_seconds:
type: integer type: integer
description: | description: |
@ -11251,6 +11251,39 @@ paths:
**Changes**: New in Zulip 7.0 (feature level 164). Clients should use 140 **Changes**: New in Zulip 7.0 (feature level 164). Clients should use 140
for older Zulip servers, since that's the value that was hardcoded in the for older Zulip servers, since that's the value that was hardcoded in the
Zulip client apps prior to this parameter being introduced. Zulip client apps prior to this parameter being introduced.
server_typing_started_expiry_period_milliseconds:
type: integer
description: |
For clients implementing [typing notifications](/api/set-typing-status)
protocol, the time interval in milliseconds that the client should wait
for additional [typing start](/api/get-events#typing-start) events from
the server before removing an active typing indicator.
**Changes**: New in Zulip 8.0 (feature level 204). Clients should use 15000
for older Zulip servers, since that's the value that was hardcoded in the
Zulip apps prior to this parameter being introduced.
server_typing_stopped_wait_period_milliseconds:
type: integer
description: |
For clients implementing [typing notifications](/api/set-typing-status)
protocol, the time interval in milliseconds that the client should wait
when a user stops interacting with the compose UI before sending a stop
notification to the server.
**Changes**: New in Zulip 8.0 (feature level 204). Clients should use 5000
for older Zulip servers, since that's the value that was hardcoded in the
Zulip apps prior to this parameter being introduced.
server_typing_started_wait_period_milliseconds:
type: integer
description: |
For clients implementing [typing notifications](/api/set-typing-status)
protocol, the time interval in milliseconds that the client should use
to send regular start notifications to the server to indicate that the
user is still actively interacting with the compose UI.
**Changes**: New in Zulip 8.0 (feature level 204). Clients should use 10000
for older Zulip servers, since that's the value that was hardcoded in the
Zulip apps prior to this parameter being introduced.
scheduled_messages: scheduled_messages:
type: array type: array
description: | description: |

View File

@ -199,6 +199,9 @@ class HomeTest(ZulipTestCase):
"server_presence_ping_interval_seconds", "server_presence_ping_interval_seconds",
"server_sentry_dsn", "server_sentry_dsn",
"server_timestamp", "server_timestamp",
"server_typing_started_expiry_period_milliseconds",
"server_typing_started_wait_period_milliseconds",
"server_typing_stopped_wait_period_milliseconds",
"server_web_public_streams_enabled", "server_web_public_streams_enabled",
"settings_send_digest_emails", "settings_send_digest_emails",
"show_billing", "show_billing",

View File

@ -574,3 +574,16 @@ MAX_MESSAGE_LENGTH = 10000
# More drafts, should they exist for some crazy reason, could be # More drafts, should they exist for some crazy reason, could be
# fetched in a separate request. # fetched in a separate request.
MAX_DRAFTS_IN_REGISTER_RESPONSE = 1000 MAX_DRAFTS_IN_REGISTER_RESPONSE = 1000
# How long before a client should assume that another client sending
# typing notifications has gone away and expire the active typing
# indicator.
TYPING_STARTED_EXPIRY_PERIOD_MILLISECONDS = 15000
# How long after a user has stopped interacting with the compose UI
# that a client should send a stop notification to the server.
TYPING_STOPPED_WAIT_PERIOD_MILLISECONDS = 5000
# How often a client should send start notifications to the server to
# indicate that the user is still interacting with the compose UI.
TYPING_STARTED_WAIT_PERIOD_MILLISECONDS = 10000