events: Add support for processing modern presence event in web client.

This commit is contained in:
Vector73
2025-08-15 10:30:57 +00:00
committed by Tim Abbott
parent 396f4d004a
commit 92fbe47a3b
10 changed files with 72 additions and 69 deletions

View File

@@ -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

View File

@@ -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();
}

View File

@@ -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<typeof presence_info_from_event_schema>;
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<typeof presence_schema> | 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);

View File

@@ -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":

View File

@@ -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);
});

View File

@@ -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);

View File

@@ -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}) => {

View File

@@ -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: {

View File

@@ -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);

View File

@@ -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: