diff --git a/extensions/matrix/src/matrix/poll-types.ts b/extensions/matrix/src/matrix/poll-types.ts index 68ddc9412..96b29352b 100644 --- a/extensions/matrix/src/matrix/poll-types.ts +++ b/extensions/matrix/src/matrix/poll-types.ts @@ -7,15 +7,17 @@ * - m.poll.end - Closes a poll */ +import type { TimelineEvents } from "matrix-js-sdk/lib/@types/event.js"; +import type { ExtensibleAnyMessageEventContent } from "matrix-js-sdk/lib/@types/extensible_events.js"; import type { PollInput } from "../../../../src/polls.js"; -export const M_POLL_START = "m.poll.start"; -export const M_POLL_RESPONSE = "m.poll.response"; -export const M_POLL_END = "m.poll.end"; +export const M_POLL_START = "m.poll.start" as const; +export const M_POLL_RESPONSE = "m.poll.response" as const; +export const M_POLL_END = "m.poll.end" as const; -export const ORG_POLL_START = "org.matrix.msc3381.poll.start"; -export const ORG_POLL_RESPONSE = "org.matrix.msc3381.poll.response"; -export const ORG_POLL_END = "org.matrix.msc3381.poll.end"; +export const ORG_POLL_START = "org.matrix.msc3381.poll.start" as const; +export const ORG_POLL_RESPONSE = "org.matrix.msc3381.poll.response" as const; +export const ORG_POLL_END = "org.matrix.msc3381.poll.end" as const; export const POLL_EVENT_TYPES = [ M_POLL_START, @@ -32,9 +34,7 @@ export const POLL_END_TYPES = [M_POLL_END, ORG_POLL_END]; export type PollKind = "m.poll.disclosed" | "m.poll.undisclosed"; -export type TextContent = { - "m.text"?: string; - "org.matrix.msc1767.text"?: string; +export type TextContent = ExtensibleAnyMessageEventContent & { body?: string; }; @@ -42,24 +42,7 @@ export type PollAnswer = { id: string; } & TextContent; -export type PollStartContent = { - "m.poll"?: { - question: TextContent; - kind?: PollKind; - max_selections?: number; - answers: PollAnswer[]; - }; - "org.matrix.msc3381.poll.start"?: { - question: TextContent; - kind?: PollKind; - max_selections?: number; - answers: PollAnswer[]; - }; - "m.relates_to"?: { - rel_type: "m.reference"; - event_id: string; - }; -}; +export type PollStartContent = TimelineEvents[typeof M_POLL_START]; export type PollSummary = { eventId: string; @@ -82,7 +65,7 @@ export function getTextContent(text?: TextContent): string { } export function parsePollStartContent(content: PollStartContent): PollSummary | null { - const poll = content["m.poll"] ?? content["org.matrix.msc3381.poll.start"]; + const poll = content[M_POLL_START] ?? content[ORG_POLL_START]; if (!poll) return null; const question = getTextContent(poll.question); @@ -121,6 +104,11 @@ function buildTextContent(body: string): TextContent { }; } +function buildPollFallbackText(question: string, answers: string[]): string { + if (answers.length === 0) return question; + return `${question}\n${answers.map((answer, idx) => `${idx + 1}. ${answer}`).join("\n")}`; +} + export function buildPollStartContent(poll: PollInput): PollStartContent { const question = poll.question.trim(); const answers = poll.options @@ -132,13 +120,19 @@ export function buildPollStartContent(poll: PollInput): PollStartContent { })); const maxSelections = poll.multiple ? Math.max(1, answers.length) : 1; + const fallbackText = buildPollFallbackText( + question, + answers.map((answer) => getTextContent(answer)), + ); return { - "m.poll": { + [M_POLL_START]: { question: buildTextContent(question), kind: poll.multiple ? "m.poll.undisclosed" : "m.poll.disclosed", max_selections: maxSelections, answers, }, + "m.text": fallbackText, + "org.matrix.msc1767.text": fallbackText, }; } diff --git a/extensions/matrix/src/matrix/send.ts b/extensions/matrix/src/matrix/send.ts index 47b566eb3..3ac6d9d32 100644 --- a/extensions/matrix/src/matrix/send.ts +++ b/extensions/matrix/src/matrix/send.ts @@ -71,6 +71,12 @@ function normalizeTarget(raw: string): string { return trimmed; } +function normalizeThreadId(raw?: string | number | null): string | null { + if (raw === undefined || raw === null) return null; + const trimmed = String(raw).trim(); + return trimmed ? trimmed : null; +} + async function resolveDirectRoomId(client: MatrixClient, userId: string): Promise { const trimmed = userId.trim(); if (!trimmed.startsWith("@")) { @@ -238,14 +244,10 @@ export async function sendMessageMatrix( const textLimit = resolveTextChunkLimit(cfg, "matrix"); const chunkLimit = Math.min(textLimit, MATRIX_TEXT_LIMIT); const chunks = chunkMarkdownText(trimmedMessage, chunkLimit); - const rawThreadId = opts.threadId; - const threadId = - rawThreadId !== undefined && rawThreadId !== null - ? String(rawThreadId).trim() - : null; + const threadId = normalizeThreadId(opts.threadId); const relation = threadId ? undefined : buildReplyRelation(opts.replyToId); const sendContent = (content: RoomMessageEventContent) => - client.sendMessage(roomId, threadId ?? undefined, content); + threadId ? client.sendMessage(roomId, threadId, content) : client.sendMessage(roomId, content); let lastMessageId = ""; if (opts.mediaUrl) { @@ -316,17 +318,19 @@ export async function sendPollMatrix( try { const roomId = await resolveMatrixRoomId(client, to); const pollContent = buildPollStartContent(poll); - const rawThreadId = opts.threadId; - const threadId = - rawThreadId !== undefined && rawThreadId !== null - ? String(rawThreadId).trim() - : null; - const response = await client.sendEvent( - roomId, - threadId ?? undefined, - M_POLL_START as EventType.RoomMessage, - pollContent as unknown as RoomMessageEventContent, - ); + const threadId = normalizeThreadId(opts.threadId); + const response = threadId + ? await client.sendEvent( + roomId, + threadId, + M_POLL_START, + pollContent, + ) + : await client.sendEvent( + roomId, + M_POLL_START, + pollContent, + ); return { eventId: response.event_id ?? "unknown",