/
githubmirror
/
zulip
Обзор
Документация
Войти
/
githubmirror
/
zulip
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
web/src/submessage.ts
270 строк
7 KB
Aman Agrawal
submessage: Rerender message on live poll edit events.
15 апр 2026, 01:47
15 апр 2026, 01:47
50e8c6b
Код
Авторство
О чём код?
import * as z from "zod/mini"; import * as blueslip from "./blueslip.ts"; import * as channel from "./channel.ts"; import type {MessageList} from "./message_list.ts"; import * as message_lists from "./message_lists.ts"; import * as message_store from "./message_store.ts"; import type {Message} from "./message_store.ts"; import {any_widget_data_schema} from "./widget_schema.ts"; import type {WidgetOutboundData} from "./widget_schema.ts"; import * as widgetize from "./widgetize.ts"; export type Submessage = z.infer<typeof message_store.submessage_schema>; const widget_data_event_schema = z.object({ sender_id: z.number(), data: any_widget_data_schema, }); const inbound_data_event_schema = z.object({ sender_id: z.number(), data: z.intersection( z.object({ type: z.string(), }), z.record(z.string(), z.unknown()), ), }); const submessages_event_schema = z.tuple([widget_data_event_schema], inbound_data_event_schema); type SubmessageEvents = z.infer<typeof submessages_event_schema>; export function get_message_events(message: Message): SubmessageEvents | undefined { if (message.locally_echoed) { return undefined; } if (message.submessages.length === 0) { return undefined; } message.submessages.sort((m1, m2) => m1.id - m2.id); const events: {sender_id: number; data: unknown}[] = []; for (const obj of message.submessages) { let data: unknown; try { data = JSON.parse(obj.content); } catch { return undefined; } events.push({sender_id: obj.sender_id, data}); } const parsed = submessages_event_schema.safeParse(events); if (!parsed.success) { return undefined; } return parsed.data; } export function is_poll_message(message: Message): boolean { if (message.submessages === undefined || message.submessages.length === 0) { return false; } try { const first_data: unknown = JSON.parse(message.submessages[0]!.content); return ( typeof first_data === "object" && first_data !== null && "widget_type" in first_data && first_data.widget_type === "poll" ); } catch { return false; } } export function is_widget_edited(message: Message): boolean { if (message.has_widget_edits !== undefined) { return message.has_widget_edits; } if (message.submessages === undefined || message.submessages.length === 0) { message.has_widget_edits = false; return false; } if (!is_poll_message(message)) { message.has_widget_edits = false; return false; } // Check if any subsequent submessages are edits (question changes // or new options added after poll creation). Votes don't count. for (let i = 1; i < message.submessages.length; i += 1) { try { const data: unknown = JSON.parse(message.submessages[i]!.content); if ( typeof data === "object" && data !== null && "type" in data && (data.type === "question" || data.type === "new_option") ) { message.has_widget_edits = true; return true; } } catch { continue; } } message.has_widget_edits = false; return false; } export function render_widget_rows_in_list(list: MessageList | undefined): void { for (const message_id of widgetize.get_message_ids()) { const $row = list?.get_row(message_id); if ($row && $row.length > 0) { render_submessage({message_id, $row}); } } } export function process_submessages(message: Message): void { // This happens in our rendering path, so we try to limit any // damage that may be triggered by one rogue message. try { do_process_submessages(message); return; } catch (error) { blueslip.error("Failed to do_process_submessages", undefined, error); return; } } export function do_process_submessages(message: Message): void { const events = get_message_events(message); if (!events) { return; } const [widget_event, ...inbound_events] = events; if (widget_event.sender_id !== message.sender_id) { blueslip.warn(`User ${widget_event.sender_id} tried to hijack message ${message.id}`); return; } // Right now, our only use of submessages is widgets. const any_data = widget_event.data; widgetize.activate({ any_data, events: inbound_events, message, }); } export function render_submessage(in_opts: {$row: JQuery; message_id: number}): void { const message_id = in_opts.message_id; const message = message_store.get(message_id); if (!message) { return; } const $row = in_opts.$row; const post_to_server = make_server_callback(message_id); widgetize.render({ $row, message, post_to_server, }); } export function update_message(message: Message, submsg: Submessage): void { const existing = message.submessages.find((sm) => sm.id === submsg.id); if (existing !== undefined) { blueslip.warn("Got submessage multiple times: " + submsg.id); return; } message.submessages.push(submsg); } export function handle_event(submsg: Submessage): void { // Update message.submessages in case we haven't actually // activated the widget yet, so that when the message does // come in view, the data will be complete. const message = message_store.get(submsg.message_id); if (message === undefined) { // This is generally not a problem--the server // can send us events without us having received // the original message, since the server doesn't // track that. return; } update_message(message, submsg); // Right now, our only use of submessages is widgets. const msg_type = submsg.msg_type; if (msg_type !== "widget") { blueslip.warn("unknown msg_type: " + msg_type); return; } let data: unknown; try { data = JSON.parse(submsg.content); } catch { blueslip.warn("server sent us invalid json in handle_event: " + submsg.content); return; } const post_to_server = make_server_callback(submsg.message_id); widgetize.handle_event({ sender_id: submsg.sender_id, message, post_to_server, data, }); // If a poll question was changed or a new option was added, // update the widget-edited cache and rerender so the EDITED // marker appears immediately. if ( typeof data === "object" && data !== null && "type" in data && (data.type === "question" || data.type === "new_option") && !message.has_widget_edits ) { message.has_widget_edits = true; if (message_lists.current !== undefined) { message_lists.current.view.rerender_messages([message]); } } } export function make_server_callback( message_id: number, ): (opts: {msg_type: string; data: WidgetOutboundData}) => void { return function (opts: {msg_type: string; data: WidgetOutboundData}) { const url = "/json/submessage"; void channel.post({ url, data: { message_id, msg_type: opts.msg_type, content: JSON.stringify(opts.data), }, }); }; }