From 92fbe47a3b64732b5166c2559a63f8280a48c439 Mon Sep 17 00:00:00 2001 From: Vector73 Date: Fri, 15 Aug 2025 10:30:57 +0000 Subject: [PATCH] events: Add support for processing modern presence event in web client. --- tools/check-schemas | 3 ++- web/src/activity_ui.ts | 13 +++++----- web/src/presence.ts | 43 ++++++++++++++----------------- web/src/server_events_dispatch.js | 2 +- web/tests/activity.test.cjs | 36 +++++++++++++++++++------- web/tests/buddy_data.test.cjs | 6 ++--- web/tests/dispatch.test.cjs | 6 ++--- web/tests/lib/events.cjs | 13 +++------- web/tests/presence.test.cjs | 18 +++++-------- zerver/lib/home.py | 1 + 10 files changed, 72 insertions(+), 69 deletions(-) diff --git a/tools/check-schemas b/tools/check-schemas index cb0c6cc910..3ece3ce3be 100755 --- a/tools/check-schemas +++ b/tools/check-schemas @@ -68,8 +68,9 @@ def get_event_checker(event: dict[str, Any]) -> Callable[[str, dict[str, Any]], # Change to CamelCase name = name.replace("_", " ").title().replace(" ", "") + # Use EventModernPresence type to check "presence" events if name == "Presence": - name = "Legacy" + name + name = "Modern" + name # And add the prefix. name = "Event" + name diff --git a/web/src/activity_ui.ts b/web/src/activity_ui.ts index cfcddbddcc..8ef083ec62 100644 --- a/web/src/activity_ui.ts +++ b/web/src/activity_ui.ts @@ -227,11 +227,12 @@ export function initialize(opts: {narrow_by_email: (email: string) => void}): vo activity.send_presence_to_server(); } -export function update_presence_info( - user_id: number, - info: PresenceInfoFromEvent, - server_time: number, -): void { +export function update_presence_info(info: PresenceInfoFromEvent): void { + const presence_entry = Object.entries(info)[0]; + assert(presence_entry !== undefined); + const [user_id_string, presence_info] = presence_entry; + const user_id = Number.parseInt(user_id_string, 10); + // There can be some case where the presence event // was set for an inaccessible user if // CAN_ACCESS_ALL_USERS_GROUP_LIMITS_PRESENCE is @@ -241,7 +242,7 @@ export function update_presence_info( return; } - presence.update_info_from_event(user_id, info, server_time); + presence.update_info_from_event(user_id, presence_info); redraw_user(user_id); pm_list.update_private_messages(); } diff --git a/web/src/presence.ts b/web/src/presence.ts index e0c1bc5ecb..84bca78dbc 100644 --- a/web/src/presence.ts +++ b/web/src/presence.ts @@ -15,14 +15,13 @@ export type PresenceStatus = { last_active?: number | undefined; }; -export const presence_info_from_event_schema = z.object({ - website: z.object({ - client: z.literal("website"), - status: z.enum(["idle", "active"]), - timestamp: z.number(), - pushable: z.boolean(), +export const presence_info_from_event_schema = z.record( + z.string(), + z.object({ + active_timestamp: z.number(), + idle_timestamp: z.number(), }), -}); +); export type PresenceInfoFromEvent = z.output; export const user_last_seen_response_schema = z.object({ @@ -163,18 +162,16 @@ export function status_from_raw(raw: RawPresence, user: User | undefined): Prese export function update_info_from_event( user_id: number, - info: PresenceInfoFromEvent | null, - server_timestamp: number, + info: z.infer | null, + server_timestamp: number | undefined = undefined, ): void { /* Example of `info`: { - website: { - client: 'website', - pushable: false, - status: 'active', - timestamp: 1585745225 + "10": { + active_timestamp: 1585745133, + idle_timestamp: 1585745091 } } @@ -190,16 +187,16 @@ export function update_info_from_event( server_timestamp: 0, }; - raw.server_timestamp = server_timestamp; + if (server_timestamp !== undefined) { + // The event itself doesn't contain a server_timestamp. But + // since the event should be newer than our last polling + // response from the server, it should be safe to use that. + raw.server_timestamp = server_timestamp; + } - for (const rec of Object.values(info ?? {})) { - if (rec.status === "active" && rec.timestamp > (raw.active_timestamp ?? 0)) { - raw.active_timestamp = rec.timestamp; - } - - if (rec.status === "idle" && rec.timestamp > (raw.idle_timestamp ?? 0)) { - raw.idle_timestamp = rec.timestamp; - } + if (info !== null) { + raw.active_timestamp = info.active_timestamp; + raw.idle_timestamp = info.idle_timestamp; } raw_info.set(user_id, raw); diff --git a/web/src/server_events_dispatch.js b/web/src/server_events_dispatch.js index ccf2ede830..dd9b70e914 100644 --- a/web/src/server_events_dispatch.js +++ b/web/src/server_events_dispatch.js @@ -230,7 +230,7 @@ export function dispatch_normal_event(event) { break; case "presence": - activity_ui.update_presence_info(event.user_id, event.presence, event.server_timestamp); + activity_ui.update_presence_info(event.presences); break; case "restart": diff --git a/web/tests/activity.test.cjs b/web/tests/activity.test.cjs index aeba1ea240..ac0e29b6c4 100644 --- a/web/tests/activity.test.cjs +++ b/web/tests/activity.test.cjs @@ -694,11 +694,10 @@ test("update_presence_info", ({override, override_rewire}) => { override(realm, "server_presence_ping_interval_seconds", 60); override(realm, "server_presence_offline_threshold_seconds", 200); - const server_time = 500; - const info = { - website: { - status: "active", - timestamp: server_time, + let info = { + [me.user_id]: { + active_timestamp: 500, + idle_timestamp: 500, }, }; @@ -711,12 +710,19 @@ test("update_presence_info", ({override, override_rewire}) => { }); presence.presence_info.delete(me.user_id); - activity_ui.update_presence_info(me.user_id, info, server_time); + activity_ui.update_presence_info(info); assert.ok(inserted); assert.deepEqual(presence.presence_info.get(me.user_id).status, "active"); + info = { + [alice.user_id]: { + active_timestamp: 500, + idle_timestamp: 500, + }, + }; + presence.presence_info.delete(alice.user_id); - activity_ui.update_presence_info(alice.user_id, info, server_time); + activity_ui.update_presence_info(info); assert.ok(inserted); const expected = {status: "active", last_active: 500}; @@ -724,7 +730,13 @@ test("update_presence_info", ({override, override_rewire}) => { // Test invalid and inaccessible user IDs. const invalid_user_id = 99; - activity_ui.update_presence_info(invalid_user_id, info, server_time); + info = { + [invalid_user_id]: { + active_timestamp: 500, + idle_timestamp: 500, + }, + }; + activity_ui.update_presence_info(info); assert.equal(presence.presence_info.get(invalid_user_id), undefined); const inaccessible_user_id = 10; @@ -735,7 +747,13 @@ test("update_presence_info", ({override, override_rewire}) => { "Unknown user", ); people._add_user(inaccessible_user); - activity_ui.update_presence_info(inaccessible_user_id, info, server_time); + info = { + [inaccessible_user_id]: { + active_timestamp: 500, + idle_timestamp: 500, + }, + }; + activity_ui.update_presence_info(info); assert.equal(presence.presence_info.get(inaccessible_user_id), undefined); }); diff --git a/web/tests/buddy_data.test.cjs b/web/tests/buddy_data.test.cjs index 590db4bd0e..7afad96707 100644 --- a/web/tests/buddy_data.test.cjs +++ b/web/tests/buddy_data.test.cjs @@ -461,10 +461,8 @@ test("level", ({override}) => { const server_time = 9999; const info = { - website: { - status: "active", - timestamp: server_time, - }, + active_timestamp: 9999, + idle_timestamp: 9999, }; presence.update_info_from_event(me.user_id, info, server_time); presence.update_info_from_event(selma.user_id, info, server_time); diff --git a/web/tests/dispatch.test.cjs b/web/tests/dispatch.test.cjs index e50d2ea8d5..0d07517661 100644 --- a/web/tests/dispatch.test.cjs +++ b/web/tests/dispatch.test.cjs @@ -461,10 +461,8 @@ run_test("presence", ({override}) => { override(activity_ui, "update_presence_info", stub.f); dispatch(event); assert.equal(stub.num_calls, 1); - const args = stub.get_args("user_id", "presence", "server_time"); - assert_same(args.user_id, event.user_id); - assert_same(args.presence, event.presence); - assert_same(args.server_time, event.server_timestamp); + const args = stub.get_args("presences"); + assert_same(args.presences, event.presences); }); run_test("reaction", ({override}) => { diff --git a/web/tests/lib/events.cjs b/web/tests/lib/events.cjs index ede04aff05..aa00495e92 100644 --- a/web/tests/lib/events.cjs +++ b/web/tests/lib/events.cjs @@ -328,17 +328,12 @@ exports.fixtures = { presence: { type: "presence", - email: "alice@example.com", - user_id: 42, - presence: { - electron: { - status: "active", - timestamp: fake_now, - client: "electron", - pushable: false, + presences: { + 42: { + active_timestamp: fake_now, + idle_timestamp: fake_now, }, }, - server_timestamp: fake_now, }, reaction__add: { diff --git a/web/tests/presence.test.cjs b/web/tests/presence.test.cjs index d27d9e74ad..f230a7579d 100644 --- a/web/tests/presence.test.cjs +++ b/web/tests/presence.test.cjs @@ -315,10 +315,8 @@ test("update_info_from_event", () => { let info; info = { - website: { - status: "active", - timestamp: 500, - }, + active_timestamp: 500, + idle_timestamp: 500, }; presence.presence_info.delete(alice.user_id); @@ -330,10 +328,8 @@ test("update_info_from_event", () => { }); info = { - mobile: { - status: "idle", - timestamp: 510, - }, + active_timestamp: 500, + idle_timestamp: 500, }; presence.update_info_from_event(alice.user_id, info, 510); @@ -343,10 +339,8 @@ test("update_info_from_event", () => { }); info = { - mobile: { - status: "idle", - timestamp: 1000, - }, + active_timestamp: 500, + idle_timestamp: 1000, }; presence.update_info_from_event(alice.user_id, info, 1000); diff --git a/zerver/lib/home.py b/zerver/lib/home.py index aabcec6783..0fe9132d2b 100644 --- a/zerver/lib/home.py +++ b/zerver/lib/home.py @@ -105,6 +105,7 @@ def build_page_params_for_home_page_load( include_deactivated_groups=True, archived_channels=True, empty_topic_name=True, + simplified_presence_events=True, ) if user_profile is not None: