@ttt-productions/chat-react 0.12.6 → 0.12.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/realtime/map.d.ts
CHANGED
|
@@ -28,12 +28,12 @@ export declare function seqToMessageId(seq: number): string;
|
|
|
28
28
|
* - `messageId` = the server seq (string) — stable + dedup key.
|
|
29
29
|
* - `replyTo` JSON `{ messageSeq, preview }` → UI `{ messageId, senderId:'', messagePreview }`.
|
|
30
30
|
* - The DO row carries only the attachment LIFECYCLE (`attachmentState` +
|
|
31
|
-
* `attachmentMeta
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
31
|
+
* `attachmentMeta` with `mediaAssetId`/`attachmentKind`/`failureReason`), never a
|
|
32
|
+
* full stored `ChatAttachment` — the projection below rebuilds the renderable
|
|
33
|
+
* `attachment` from that lifecycle, and the render-time URL comes from
|
|
34
|
+
* `mediaAssetId` via the attachment-URL resolver context (identity/media
|
|
35
|
+
* invariant: no URLs are ever stored). The raw lifecycle also stays on
|
|
36
|
+
* `meta.attachment*` for consumers that key on it.
|
|
37
37
|
*/
|
|
38
38
|
export declare function wireRowToMessage(row: WireMessageRow, threadId: string): ChatMessageV1;
|
|
39
39
|
/** The optimistic local echo a `send` produces before the server `ack` reconciles the seq. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"map.d.ts","sourceRoot":"","sources":["../../src/realtime/map.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"map.d.ts","sourceRoot":"","sources":["../../src/realtime/map.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAkB,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAChF,OAAO,EAAE,wBAAwB,EAAE,MAAM,+BAA+B,CAAC;AACzE,OAAO,KAAK,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAI9D,OAAO,EAAE,wBAAwB,EAAE,CAAC;AAEpC,sFAAsF;AACtF,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,YAAY,CAAC;IACnB,eAAe,EAAE,MAAM,CAAC;CACzB;AAED;;;;;;;;;GASG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,aAAa,EAAE,OAAO,EAAE,iBAAiB,GAAG,IAAI,GAAG,aAAa,CAc/G;AAED,4FAA4F;AAC5F,wBAAgB,cAAc,CAAC,GAAG,EAAE,cAAc,GAAG,iBAAiB,GAAG,IAAI,CAG5E;AAED,qFAAqF;AACrF,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAElD;AA0BD;;;;;;;;;;;;GAYG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,cAAc,EAAE,QAAQ,EAAE,MAAM,GAAG,aAAa,CAuDrF;AAED,8FAA8F;AAC9F,wBAAgB,iBAAiB,CAAC,IAAI,EAAE;IACtC,eAAe,EAAE,MAAM,CAAC;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,cAAc,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;CACjE,GAAG,aAAa,CAYhB"}
|
package/dist/realtime/map.js
CHANGED
|
@@ -61,22 +61,53 @@ function parseJson(raw) {
|
|
|
61
61
|
* - `messageId` = the server seq (string) — stable + dedup key.
|
|
62
62
|
* - `replyTo` JSON `{ messageSeq, preview }` → UI `{ messageId, senderId:'', messagePreview }`.
|
|
63
63
|
* - The DO row carries only the attachment LIFECYCLE (`attachmentState` +
|
|
64
|
-
* `attachmentMeta
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
64
|
+
* `attachmentMeta` with `mediaAssetId`/`attachmentKind`/`failureReason`), never a
|
|
65
|
+
* full stored `ChatAttachment` — the projection below rebuilds the renderable
|
|
66
|
+
* `attachment` from that lifecycle, and the render-time URL comes from
|
|
67
|
+
* `mediaAssetId` via the attachment-URL resolver context (identity/media
|
|
68
|
+
* invariant: no URLs are ever stored). The raw lifecycle also stays on
|
|
69
|
+
* `meta.attachment*` for consumers that key on it.
|
|
70
70
|
*/
|
|
71
71
|
export function wireRowToMessage(row, threadId) {
|
|
72
72
|
const reply = parseJson(row.replyTo);
|
|
73
73
|
const meta = parseJson(row.attachmentMeta);
|
|
74
|
+
// Project the DO's attachment LIFECYCLE into the renderable `attachment` object —
|
|
75
|
+
// the renderer keys on `m.attachment` (AttachmentView: pending spinner / failed
|
|
76
|
+
// bubble / ready media via the attachment-URL resolver + `mediaAssetId`). Before
|
|
77
|
+
// this projection existed, a wire-delivered attachment message (live broadcast,
|
|
78
|
+
// resume snapshot, history page) mapped to a message with NO `attachment` and
|
|
79
|
+
// rendered as an empty bubble — the recipient never saw the media at all (found
|
|
80
|
+
// live 2026-07-20, hosted path 11.3). `name`/`size`/`storagePath` are wire-absent
|
|
81
|
+
// and render-irrelevant (labels are kind-scrubbed; storagePath is never a serving
|
|
82
|
+
// contract) — placeholders keep the ChatAttachment shape without inventing data.
|
|
83
|
+
const state = row.attachmentState === 'pending' || row.attachmentState === 'ready' || row.attachmentState === 'failed'
|
|
84
|
+
? row.attachmentState
|
|
85
|
+
: null;
|
|
86
|
+
const kind = meta?.attachmentKind === 'image' || meta?.attachmentKind === 'video' || meta?.attachmentKind === 'audio'
|
|
87
|
+
? meta.attachmentKind
|
|
88
|
+
: // Rows written before the DO stored `attachmentKind` carry none — 'image' keeps
|
|
89
|
+
// them rendering through the media path (the resolver serves whatever the asset
|
|
90
|
+
// is); new rows always carry the real kind.
|
|
91
|
+
'image';
|
|
92
|
+
const attachment = state
|
|
93
|
+
? {
|
|
94
|
+
id: row.clientMessageId,
|
|
95
|
+
name: '',
|
|
96
|
+
type: kind,
|
|
97
|
+
size: 0,
|
|
98
|
+
storagePath: '',
|
|
99
|
+
...(meta?.mediaAssetId ? { mediaAssetId: meta.mediaAssetId } : {}),
|
|
100
|
+
status: state,
|
|
101
|
+
...(meta?.failureReason ? { failureReason: meta.failureReason } : {}),
|
|
102
|
+
}
|
|
103
|
+
: null;
|
|
74
104
|
return {
|
|
75
105
|
messageId: seqToMessageId(row.seq),
|
|
76
106
|
threadId,
|
|
77
107
|
createdAt: row.createdAt,
|
|
78
108
|
senderId: row.senderUid,
|
|
79
109
|
text: row.text,
|
|
110
|
+
...(attachment ? { attachment } : {}),
|
|
80
111
|
replyTo: reply
|
|
81
112
|
? { messageId: seqToMessageId(reply.messageSeq), senderId: '', messagePreview: reply.preview }
|
|
82
113
|
: undefined,
|
package/dist/realtime/map.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"map.js","sourceRoot":"","sources":["../../src/realtime/map.ts"],"names":[],"mappings":"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","sourcesContent":["// Map the DO wire row (`WireMessageRow`) into the UI `ChatMessageV1` shape so the\n// UI stays transport-agnostic — the MessageList renders the same shape whether\n// the bytes came from Firestore or the DO socket.\n//\n// Identity invariant: the DO stores uid references only (`senderUid`), never a\n// display name — names resolve at render time through the ChatNameResolver\n// context, identical to the Firestore path.\n\nimport type { ChatMessageV1 } from '@ttt-productions/chat-core';\nimport { MODERATION_REDACTED_TEXT } from '@ttt-productions/chat-schemas';\nimport type { RevisionKind, WireMessageRow } from './wire.js';\n\n// The redacted-text constant lives in the wire contract; re-export to keep this\n// module's public surface stable.\nexport { MODERATION_REDACTED_TEXT };\n\n/** The effective moderation overlay for a message: the max-`messageRevision` kind. */\nexport interface ModerationOverlay {\n kind: RevisionKind;\n messageRevision: number;\n}\n\n/**\n * Apply a moderation overlay to a mapped message so the ORIGINAL content never\n * renders for a hidden/deleted message. Pure — returns a new message; the input\n * is not mutated. The overlay state rides on `meta` (`meta` is opaque to\n * chat-core), so the renderer can branch on `meta.moderated` / `meta.moderationKind`.\n * - `delete` / `moderate` → blank/replace the text + mark moderated.\n * - `edit` → the row already carries the overlaid text (the DO rewrites `text`),\n * so only the marker is stamped.\n * - `restore` → original content (the DO row is the original); no redaction.\n */\nexport function applyModerationOverlay(message: ChatMessageV1, overlay: ModerationOverlay | null): ChatMessageV1 {\n if (!overlay) return message;\n const redacted = overlay.kind === 'delete' || overlay.kind === 'moderate';\n return {\n ...message,\n ...(redacted ? { text: MODERATION_REDACTED_TEXT } : {}),\n meta: {\n ...message.meta,\n moderationKind: overlay.kind,\n messageRevision: overlay.messageRevision,\n // `restore` is the absence of active moderation — it reverts to the original.\n moderated: overlay.kind !== 'restore',\n },\n };\n}\n\n/** The overlay carried inline on a DO row (the getHistory/getDeltaSince merge), or null. */\nexport function overlayFromRow(row: WireMessageRow): ModerationOverlay | null {\n if (row.moderationKind == null) return null;\n return { kind: row.moderationKind, messageRevision: row.messageRevision ?? 0 };\n}\n\n/** The DO `seq` is the stable per-channel message id (monotonic, epoch-spanning). */\nexport function seqToMessageId(seq: number): string {\n return String(seq);\n}\n\ninterface WireReplyTo {\n messageSeq: number;\n preview: string;\n}\n\ninterface WireAttachmentMeta {\n senderOnly?: boolean;\n mediaAssetId?: string;\n failureReason?: string;\n [k: string]: unknown;\n}\n\nfunction parseJson<T>(raw: string | null): T | null {\n if (raw == null) return null;\n try {\n return JSON.parse(raw) as T;\n } catch {\n return null;\n }\n}\n\n/**\n * Convert a DO message row to the UI message. `threadId` is the caller's logical\n * thread id (the UI's thread key); the DO row carries no threadId of its own.\n * - `messageId` = the server seq (string) — stable + dedup key.\n * - `replyTo` JSON `{ messageSeq, preview }` → UI `{ messageId, senderId:'', messagePreview }`.\n * - The DO row carries only the attachment LIFECYCLE (`attachmentState` +\n * `attachmentMeta.mediaAssetId`), not the full `ChatAttachment` (id/name/size/\n * storagePath). The full attachment-saga projection is the next chain's work\n * (P5/P6); until then the placeholder lifecycle rides in `meta.attachment*` so\n * the renderer can show the pending/ready/failed bubble without a stored URL —\n * the render-time URL comes from `mediaAssetId` via the attachment-URL resolver\n * context (identity/media invariant: no URLs are ever stored).\n */\nexport function wireRowToMessage(row: WireMessageRow, threadId: string): ChatMessageV1 {\n const reply = parseJson<WireReplyTo>(row.replyTo);\n const meta = parseJson<WireAttachmentMeta>(row.attachmentMeta);\n\n return {\n messageId: seqToMessageId(row.seq),\n threadId,\n createdAt: row.createdAt,\n senderId: row.senderUid,\n text: row.text,\n replyTo: reply\n ? { messageId: seqToMessageId(reply.messageSeq), senderId: '', messagePreview: reply.preview }\n : undefined,\n isSystemMessage: row.senderUid === 'system' || undefined,\n meta: {\n seq: row.seq,\n epoch: row.epoch,\n ...(row.attachmentState ? { attachmentState: row.attachmentState } : {}),\n ...(meta ? { attachmentMeta: meta } : {}),\n },\n };\n}\n\n/** The optimistic local echo a `send` produces before the server `ack` reconciles the seq. */\nexport function optimisticMessage(args: {\n clientMessageId: string;\n threadId: string;\n senderId: string;\n text: string;\n createdAt: number;\n replyTo?: { messageId: string; messagePreview?: string } | null;\n}): ChatMessageV1 {\n return {\n messageId: `optimistic:${args.clientMessageId}`,\n threadId: args.threadId,\n createdAt: args.createdAt,\n senderId: args.senderId,\n text: args.text,\n replyTo: args.replyTo\n ? { messageId: args.replyTo.messageId, senderId: '', messagePreview: args.replyTo.messagePreview ?? '' }\n : undefined,\n meta: { clientMessageId: args.clientMessageId, optimistic: true },\n };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"map.js","sourceRoot":"","sources":["../../src/realtime/map.ts"],"names":[],"mappings":"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","sourcesContent":["// Map the DO wire row (`WireMessageRow`) into the UI `ChatMessageV1` shape so the\n// UI stays transport-agnostic — the MessageList renders the same shape whether\n// the bytes came from Firestore or the DO socket.\n//\n// Identity invariant: the DO stores uid references only (`senderUid`), never a\n// display name — names resolve at render time through the ChatNameResolver\n// context, identical to the Firestore path.\n\nimport type { ChatAttachment, ChatMessageV1 } from '@ttt-productions/chat-core';\nimport { MODERATION_REDACTED_TEXT } from '@ttt-productions/chat-schemas';\nimport type { RevisionKind, WireMessageRow } from './wire.js';\n\n// The redacted-text constant lives in the wire contract; re-export to keep this\n// module's public surface stable.\nexport { MODERATION_REDACTED_TEXT };\n\n/** The effective moderation overlay for a message: the max-`messageRevision` kind. */\nexport interface ModerationOverlay {\n kind: RevisionKind;\n messageRevision: number;\n}\n\n/**\n * Apply a moderation overlay to a mapped message so the ORIGINAL content never\n * renders for a hidden/deleted message. Pure — returns a new message; the input\n * is not mutated. The overlay state rides on `meta` (`meta` is opaque to\n * chat-core), so the renderer can branch on `meta.moderated` / `meta.moderationKind`.\n * - `delete` / `moderate` → blank/replace the text + mark moderated.\n * - `edit` → the row already carries the overlaid text (the DO rewrites `text`),\n * so only the marker is stamped.\n * - `restore` → original content (the DO row is the original); no redaction.\n */\nexport function applyModerationOverlay(message: ChatMessageV1, overlay: ModerationOverlay | null): ChatMessageV1 {\n if (!overlay) return message;\n const redacted = overlay.kind === 'delete' || overlay.kind === 'moderate';\n return {\n ...message,\n ...(redacted ? { text: MODERATION_REDACTED_TEXT } : {}),\n meta: {\n ...message.meta,\n moderationKind: overlay.kind,\n messageRevision: overlay.messageRevision,\n // `restore` is the absence of active moderation — it reverts to the original.\n moderated: overlay.kind !== 'restore',\n },\n };\n}\n\n/** The overlay carried inline on a DO row (the getHistory/getDeltaSince merge), or null. */\nexport function overlayFromRow(row: WireMessageRow): ModerationOverlay | null {\n if (row.moderationKind == null) return null;\n return { kind: row.moderationKind, messageRevision: row.messageRevision ?? 0 };\n}\n\n/** The DO `seq` is the stable per-channel message id (monotonic, epoch-spanning). */\nexport function seqToMessageId(seq: number): string {\n return String(seq);\n}\n\ninterface WireReplyTo {\n messageSeq: number;\n preview: string;\n}\n\ninterface WireAttachmentMeta {\n senderOnly?: boolean;\n mediaAssetId?: string;\n failureReason?: string;\n /** 'image' | 'video' | 'audio' — stored by the Channel DO from the placeholder\n * command's validated contentType kind; inherited by the ready flip. */\n attachmentKind?: string;\n [k: string]: unknown;\n}\n\nfunction parseJson<T>(raw: string | null): T | null {\n if (raw == null) return null;\n try {\n return JSON.parse(raw) as T;\n } catch {\n return null;\n }\n}\n\n/**\n * Convert a DO message row to the UI message. `threadId` is the caller's logical\n * thread id (the UI's thread key); the DO row carries no threadId of its own.\n * - `messageId` = the server seq (string) — stable + dedup key.\n * - `replyTo` JSON `{ messageSeq, preview }` → UI `{ messageId, senderId:'', messagePreview }`.\n * - The DO row carries only the attachment LIFECYCLE (`attachmentState` +\n * `attachmentMeta` with `mediaAssetId`/`attachmentKind`/`failureReason`), never a\n * full stored `ChatAttachment` — the projection below rebuilds the renderable\n * `attachment` from that lifecycle, and the render-time URL comes from\n * `mediaAssetId` via the attachment-URL resolver context (identity/media\n * invariant: no URLs are ever stored). The raw lifecycle also stays on\n * `meta.attachment*` for consumers that key on it.\n */\nexport function wireRowToMessage(row: WireMessageRow, threadId: string): ChatMessageV1 {\n const reply = parseJson<WireReplyTo>(row.replyTo);\n const meta = parseJson<WireAttachmentMeta>(row.attachmentMeta);\n\n // Project the DO's attachment LIFECYCLE into the renderable `attachment` object —\n // the renderer keys on `m.attachment` (AttachmentView: pending spinner / failed\n // bubble / ready media via the attachment-URL resolver + `mediaAssetId`). Before\n // this projection existed, a wire-delivered attachment message (live broadcast,\n // resume snapshot, history page) mapped to a message with NO `attachment` and\n // rendered as an empty bubble — the recipient never saw the media at all (found\n // live 2026-07-20, hosted path 11.3). `name`/`size`/`storagePath` are wire-absent\n // and render-irrelevant (labels are kind-scrubbed; storagePath is never a serving\n // contract) — placeholders keep the ChatAttachment shape without inventing data.\n const state =\n row.attachmentState === 'pending' || row.attachmentState === 'ready' || row.attachmentState === 'failed'\n ? row.attachmentState\n : null;\n const kind =\n meta?.attachmentKind === 'image' || meta?.attachmentKind === 'video' || meta?.attachmentKind === 'audio'\n ? meta.attachmentKind\n : // Rows written before the DO stored `attachmentKind` carry none — 'image' keeps\n // them rendering through the media path (the resolver serves whatever the asset\n // is); new rows always carry the real kind.\n 'image';\n const attachment: ChatAttachment | null = state\n ? {\n id: row.clientMessageId,\n name: '',\n type: kind,\n size: 0,\n storagePath: '',\n ...(meta?.mediaAssetId ? { mediaAssetId: meta.mediaAssetId } : {}),\n status: state,\n ...(meta?.failureReason ? { failureReason: meta.failureReason } : {}),\n }\n : null;\n\n return {\n messageId: seqToMessageId(row.seq),\n threadId,\n createdAt: row.createdAt,\n senderId: row.senderUid,\n text: row.text,\n ...(attachment ? { attachment } : {}),\n replyTo: reply\n ? { messageId: seqToMessageId(reply.messageSeq), senderId: '', messagePreview: reply.preview }\n : undefined,\n isSystemMessage: row.senderUid === 'system' || undefined,\n meta: {\n seq: row.seq,\n epoch: row.epoch,\n ...(row.attachmentState ? { attachmentState: row.attachmentState } : {}),\n ...(meta ? { attachmentMeta: meta } : {}),\n },\n };\n}\n\n/** The optimistic local echo a `send` produces before the server `ack` reconciles the seq. */\nexport function optimisticMessage(args: {\n clientMessageId: string;\n threadId: string;\n senderId: string;\n text: string;\n createdAt: number;\n replyTo?: { messageId: string; messagePreview?: string } | null;\n}): ChatMessageV1 {\n return {\n messageId: `optimistic:${args.clientMessageId}`,\n threadId: args.threadId,\n createdAt: args.createdAt,\n senderId: args.senderId,\n text: args.text,\n replyTo: args.replyTo\n ? { messageId: args.replyTo.messageId, senderId: '', messagePreview: args.replyTo.messagePreview ?? '' }\n : undefined,\n meta: { clientMessageId: args.clientMessageId, optimistic: true },\n };\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ttt-productions/chat-react",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.7",
|
|
4
4
|
"description": "Chat React UI, hooks, and Firebase-client adapter for TTT Productions apps (realtime newest window + infinite older pagination)",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -64,10 +64,10 @@
|
|
|
64
64
|
"@ttt-productions/chat-schemas": "^0.9.2",
|
|
65
65
|
"@ttt-productions/realtime-core": "^0.1.3",
|
|
66
66
|
"@ttt-productions/media-schemas": "^0.14.2",
|
|
67
|
-
"@ttt-productions/ui-core": "^0.11.
|
|
68
|
-
"@ttt-productions/file-input": "^0.11.
|
|
67
|
+
"@ttt-productions/ui-core": "^0.11.8",
|
|
68
|
+
"@ttt-productions/file-input": "^0.11.5",
|
|
69
69
|
"@ttt-productions/upload-ui": "^0.7.5",
|
|
70
|
-
"@ttt-productions/media-viewer": "^0.10.
|
|
70
|
+
"@ttt-productions/media-viewer": "^0.10.5",
|
|
71
71
|
"@ttt-productions/mobile-core": "^0.9.1",
|
|
72
72
|
"@ttt-productions/firebase-helpers": "^0.12.2"
|
|
73
73
|
},
|