diff --git a/components/chat/message-group.tsx b/components/chat/message-group.tsx
index 6a308819..6d4dce3b 100644
--- a/components/chat/message-group.tsx
+++ b/components/chat/message-group.tsx
@@ -190,7 +190,7 @@ export function MessageGroup({
{/* An optimistic bubble for a captionless attachment has
nothing to draw until the server answers — say so rather
than render an empty box. */}
- {m.pending && !m.body && m.attachments.length === 0 && (
+ {m.pending && !m.body && !m.attachments?.length && (
Sending attachment…
diff --git a/components/notifications/broadcast-modal.tsx b/components/notifications/broadcast-modal.tsx
index b1662a25..1d8441e0 100644
--- a/components/notifications/broadcast-modal.tsx
+++ b/components/notifications/broadcast-modal.tsx
@@ -60,7 +60,9 @@ export function BroadcastModal() {
}
}, [item, dismissing]);
- if (!item) return null;
+ // Guard on the shape, not just presence: a malformed payload must not put
+ // an empty modal over every page in the app.
+ if (!item?.id || !item.title) return null;
return (
(path: string, init?: RequestInit): Promise {
});
if (!res.ok) throw new Error(`broadcast request failed: ${res.status}`);
if (res.status === 204) return undefined as T;
- const json = await res.json();
- return (json?.data ?? json) as T;
+ const body = (await res.json().catch(() => ({}))) as {
+ data?: T;
+ success?: boolean;
+ };
+ // NOT `body.data ?? body`: /pending answers {data: null} when nothing is
+ // waiting, and `??` would fall through and hand back the whole envelope —
+ // which is truthy, so the modal would try to render it as an announcement.
+ return (body.data ?? (null as unknown as T)) as T;
}
export type Broadcast = {
diff --git a/lib/chat-format.ts b/lib/chat-format.ts
index 120839b6..86555414 100644
--- a/lib/chat-format.ts
+++ b/lib/chat-format.ts
@@ -29,10 +29,15 @@ export function buildEntityTree(
body: string,
entities: MessageEntity[],
): EntityNode[] {
+ // Defensive on both inputs: this renders content from the network, and a
+ // missing body should show nothing rather than take down the screen it is
+ // on. The broadcast modal renders through here too, and it mounts on every
+ // page — so a crash here is not confined to chat.
+ const text = body ?? "";
const sorted = [...(entities ?? [])].sort(
(a, b) => a.offset - b.offset || b.length - a.length,
);
- return build(body, 0, body.length, sorted);
+ return build(text, 0, text.length, sorted);
}
function build(