@ttt-productions/chat-react 0.13.16 → 0.13.17

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.
Files changed (84) hide show
  1. package/README.md +3 -3
  2. package/dist/context/ChatNameResolverContext.d.ts +0 -23
  3. package/dist/context/ChatNameResolverContext.d.ts.map +1 -1
  4. package/dist/context/ChatNameResolverContext.js +0 -16
  5. package/dist/context/ChatNameResolverContext.js.map +1 -1
  6. package/dist/hooks/useChatMessages.d.ts +0 -4
  7. package/dist/hooks/useChatMessages.d.ts.map +1 -1
  8. package/dist/hooks/useChatMessages.js +0 -10
  9. package/dist/hooks/useChatMessages.js.map +1 -1
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +0 -3
  12. package/dist/index.js.map +1 -1
  13. package/dist/realtime/channel-client.d.ts +0 -248
  14. package/dist/realtime/channel-client.d.ts.map +1 -1
  15. package/dist/realtime/channel-client.js +5 -432
  16. package/dist/realtime/channel-client.js.map +1 -1
  17. package/dist/realtime/diagnostics.d.ts +0 -62
  18. package/dist/realtime/diagnostics.d.ts.map +1 -1
  19. package/dist/realtime/diagnostics.js +4 -89
  20. package/dist/realtime/diagnostics.js.map +1 -1
  21. package/dist/realtime/inbox-client.d.ts +0 -71
  22. package/dist/realtime/inbox-client.d.ts.map +1 -1
  23. package/dist/realtime/inbox-client.js +0 -98
  24. package/dist/realtime/inbox-client.js.map +1 -1
  25. package/dist/realtime/initial-load-budget.d.ts +1 -16
  26. package/dist/realtime/initial-load-budget.d.ts.map +1 -1
  27. package/dist/realtime/initial-load-budget.js +1 -20
  28. package/dist/realtime/initial-load-budget.js.map +1 -1
  29. package/dist/realtime/map.d.ts +0 -26
  30. package/dist/realtime/map.d.ts.map +1 -1
  31. package/dist/realtime/map.js +0 -33
  32. package/dist/realtime/map.js.map +1 -1
  33. package/dist/realtime/shared.d.ts +0 -78
  34. package/dist/realtime/shared.d.ts.map +1 -1
  35. package/dist/realtime/shared.js +2 -76
  36. package/dist/realtime/shared.js.map +1 -1
  37. package/dist/realtime/socket.d.ts +0 -15
  38. package/dist/realtime/socket.d.ts.map +1 -1
  39. package/dist/realtime/socket.js +0 -4
  40. package/dist/realtime/socket.js.map +1 -1
  41. package/dist/realtime/transport.d.ts +0 -23
  42. package/dist/realtime/transport.d.ts.map +1 -1
  43. package/dist/realtime/transport.js +0 -13
  44. package/dist/realtime/transport.js.map +1 -1
  45. package/dist/realtime/useRealtimeChatMessages.d.ts +0 -17
  46. package/dist/realtime/useRealtimeChatMessages.d.ts.map +1 -1
  47. package/dist/realtime/useRealtimeChatMessages.js +0 -22
  48. package/dist/realtime/useRealtimeChatMessages.js.map +1 -1
  49. package/dist/realtime/wire.d.ts +0 -56
  50. package/dist/realtime/wire.d.ts.map +1 -1
  51. package/dist/realtime/wire.js +0 -16
  52. package/dist/realtime/wire.js.map +1 -1
  53. package/dist/types.d.ts +0 -38
  54. package/dist/types.d.ts.map +1 -1
  55. package/dist/types.js +0 -3
  56. package/dist/types.js.map +1 -1
  57. package/dist/ui/ChatShell.d.ts +0 -42
  58. package/dist/ui/ChatShell.d.ts.map +1 -1
  59. package/dist/ui/ChatShell.js +2 -39
  60. package/dist/ui/ChatShell.js.map +1 -1
  61. package/dist/ui/Composer.d.ts +0 -13
  62. package/dist/ui/Composer.d.ts.map +1 -1
  63. package/dist/ui/Composer.js +0 -13
  64. package/dist/ui/Composer.js.map +1 -1
  65. package/dist/ui/MessageItemDefault.d.ts +0 -4
  66. package/dist/ui/MessageItemDefault.d.ts.map +1 -1
  67. package/dist/ui/MessageItemDefault.js +0 -32
  68. package/dist/ui/MessageItemDefault.js.map +1 -1
  69. package/dist/ui/MessageList.d.ts +0 -23
  70. package/dist/ui/MessageList.d.ts.map +1 -1
  71. package/dist/ui/MessageList.js +2 -12
  72. package/dist/ui/MessageList.js.map +1 -1
  73. package/dist/ui/MessageText.d.ts +0 -13
  74. package/dist/ui/MessageText.d.ts.map +1 -1
  75. package/dist/ui/MessageText.js.map +1 -1
  76. package/dist/ui/SenderName.d.ts +0 -5
  77. package/dist/ui/SenderName.d.ts.map +1 -1
  78. package/dist/ui/SenderName.js +0 -5
  79. package/dist/ui/SenderName.js.map +1 -1
  80. package/dist/ui/menus.d.ts.map +1 -1
  81. package/dist/ui/menus.js +1 -3
  82. package/dist/ui/menus.js.map +1 -1
  83. package/package.json +10 -10
  84. package/src/styles/chat.css +2 -15
@@ -1 +1 @@
1
- {"version":3,"file":"wire.js","sourceRoot":"","sources":["../../src/realtime/wire.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,mFAAmF;AACnF,iFAAiF;AACjF,gIAAgI;AAChI,+EAA+E;AAC/E,6EAA6E;AAC7E,0DAA0D;AAC1D,EAAE;AACF,2EAA2E;AAC3E,sEAAsE;AACtE,iFAAiF;AACjF,gFAAgF;AAChF,8EAA8E;AAE9E,OAAO,EAAE,iBAAiB,EAAE,MAAM,+BAA+B,CAAC;AAiIlE,qFAAqF;AACrF,MAAM,UAAU,UAAU,CAAC,IAAY,EAAE,OAAgC;IACvE,OAAO,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC,EAAE,iBAAiB,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC;AACjE,CAAC;AAED,kFAAkF;AAClF,MAAM,UAAU,UAAU,CACxB,GAAW;IAEX,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAY,CAAC;QAC1C,IAAI,CAAC,MAAM,IAAI,OAAO,MAAM,KAAK,QAAQ;YAAE,OAAO,IAAI,CAAC;QACvD,OAAO,MAA0E,CAAC;IACpF,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,wGAAwG;AACxG,MAAM,UAAU,eAAe,CAC7B,CAA0C;IAE1C,OAAO,KAAK,CAAC,OAAO,CAAE,CAAuB,CAAC,QAAQ,CAAC,CAAC;AAC1D,CAAC","sourcesContent":["// Chat realtime WIRE PROTOCOL (Contract C) — the client side of the Channel +\n// Inbox Durable Object protocol. The canonical protocol declarations (subprotocol,\n// frame-kind maps, close codes, the neutral conversation reference, grant scope,\n// the send and mark-read contracts, client-agreed limits) are owned by `@ttt-productions/chat-schemas`; this file consumes them\n// and adds the client-side raw row/frame shapes the transport maps into the UI\n// message shape. The client and the worker runtime agree on the wire by both\n// importing that one contract; neither imports the other.\n//\n// Frame shape on the socket is `{ v, type, payload }` (NOT realtime-core's\n// `{ v, kind, payload }`): the worker runtime `send`/`broadcast` emit\n// `JSON.stringify({ v: 1, type, payload })` and read `env.type` + `env.payload`.\n// We keep that field name here to match; realtime-core's envelope schema is the\n// generic transport primitive, the chat protocol chose `type` for its frames.\n\nimport { CHAT_WIRE_VERSION } from '@ttt-productions/chat-schemas';\nimport type {\n ChatConversationRef,\n ChatMarkReadResultPayload,\n ChatMessageRevisionKind,\n ChatSendRejectedPayload,\n} from '@ttt-productions/chat-schemas';\n\n// ---- raw wire row shapes (what the DO serializes; opaque-but-typed here) ----\n\n/** A revision's kind (chat-schemas `ChatMessageRevisionKind`), under this package's historical name. */\nexport type RevisionKind = ChatMessageRevisionKind;\n\n/**\n * A message row as the Channel DO stores + broadcasts it (the worker runtime's\n * message row), declared as the fields the client actually CONSUMES. The adapter\n * mapping in `map.ts` turns it into the UI `ChatMessageV1` shape.\n *\n * The row is TEXT-only: there is no attachment lifecycle on a chat message, and\n * no reply pointer — chat has no reply-authoring affordance, so the client\n * neither sends nor reads one (DJ ruling 2026-07-29). A DO that still broadcasts\n * a legacy stringified `replyTo` key is simply ignored here.\n */\nexport interface WireMessageRow {\n seq: number;\n senderUid: string;\n clientMessageId: string;\n text: string;\n createdAt: number;\n epoch: number;\n /** Account ids a server-written row's text refers to (absent on a member's own message). */\n referencedUids?: string[];\n /**\n * The effective (max-`messageRevision`) moderation kind for this row, or null\n * when never moderated. Populated by the DO's getHistory/getDeltaSince overlay\n * merge so a hidden/deleted/edited row arrives already-moderated and its\n * ORIGINAL text never reaches the renderer. Optional/absent on pre-overlay rows.\n */\n moderationKind?: RevisionKind | null;\n /**\n * The effective `messageRevision` that `moderationKind` reflects (the DO's\n * max-revision). Used to break ties against a live `revision` frame: a higher\n * `messageRevision` wins; a stale/older one is ignored. Optional/absent on\n * pre-overlay rows (treated as revision 0).\n */\n messageRevision?: number;\n}\n\n/**\n * The channel resume snapshot the DO returns for a `resume` frame (Contract C).\n * `resync` / `delta` are the durable gap-repair half of the contract:\n * - `resync: false` + `delta` = the contiguous ≤500-message backlog after the\n * client's cursor (oldest-first); the client APPLIES the delta and advances its\n * cursor to `lastMessageSeq` — a missed broadcast is never permanent.\n * - `resync: true` = the gap exceeds the backlog (or the client sent no cursor);\n * the client DROPS its local tail and re-pages history from the top.\n * Both are OPTIONAL/absent on a pre-contract DO (treated as `resync: true`, empty\n * delta — the safe re-page path) so a stale worker can't leave a client stuck.\n */\nexport interface WireChannelSnapshot {\n lastMessageSeq: number;\n readSeq: number;\n resync?: boolean;\n delta?: WireMessageRow[];\n}\n\n/** A registry entry in the inbox snapshot (the worker runtime's registry entry). */\nexport interface WireRegistryEntry {\n channelRef: string;\n kind: string;\n state: 'active' | 'tombstoned';\n registryVersion: number;\n /**\n * The conversation this entry is — the OPENABLE identity for the inbox view (the\n * `channelRef` is an opaque registry key). The app maps it back to its own\n * conversation; absent, the entry offers no open action.\n */\n ref?: ChatConversationRef | null;\n /**\n * Per-entry unread flag (drives the per-row dot). Optional/absent until the inbox\n * DO populates it; `deriveUnreadRefs` reads it as a typed field (no cast).\n */\n unread?: boolean;\n /**\n * Archived state — DISTINCT from `tombstoned`. An archived row is still an ACTIVE\n * membership (`state: 'active'`, history readable) but is grouped under the inbox\n * view's Archived toggle instead of the active list. The client must NOT count\n * archived rows in the unread roll-up (archive = done; the DO clears unread on\n * archive). Optional/absent on a pre-archive DO (treated as not-archived).\n */\n archived?: boolean;\n}\n\n/** The inbox snapshot payload pushed on connect / `resume` / live delta. */\nexport interface WireInboxSnapshot {\n registry: WireRegistryEntry[];\n hasUnread: boolean;\n}\n\n// ---- server → client frame payloads (discriminated on `type`) ----\n\nexport type ServerFrame =\n | { type: 'message'; payload: { message: WireMessageRow } }\n | { type: 'ack'; payload: { clientMessageId?: string; seq?: number; readSeq?: number } }\n | { type: 'history-page'; payload: { messages: WireMessageRow[] } }\n | { type: 'presence'; payload: { online: string[] } }\n | { type: 'typing'; payload: { uid: string; at: number } }\n | { type: 'snapshot'; payload: WireChannelSnapshot | WireInboxSnapshot }\n // A standalone unread push (inbox scope): either a full inbox snapshot or a\n // lightweight dock-dot patch. Represented in the union so the inbox client routes\n // it instead of silently dropping it (forward-compatible with a future DO push).\n | { type: 'unread'; payload: WireInboxSnapshot | { hasUnread: boolean } }\n | { type: 'error'; payload: { code: string; retryAfterMs?: number } }\n // The inbox runtime's correlated answer to one `mark-read`.\n | { type: 'mark-read-result'; payload: ChatMarkReadResultPayload }\n // A CORRELATED send rejection (SERVER_KINDS.SEND_REJECTED): names the SAME\n // clientMessageId as the rejected send + the canonical retry classification, so\n // exactly one optimistic bubble is failed/retried. See ChatSendRejectedPayload.\n | { type: 'send-rejected'; payload: ChatSendRejectedPayload }\n // A moderation revision was applied to one message. The Channel DO broadcasts\n // this on a NEWLY-applied revision (the worker runtime's moderation apply) so live\n // viewers hide/blank/edit/restore the message immediately with no extra fetch.\n // `messageSeq` is the target message id; `kind` is the applied revision kind;\n // `messageRevision` is its per-message revision number (max-revision wins).\n | {\n type: 'revision';\n payload: { messageSeq: number; kind: RevisionKind; messageRevision: number };\n };\n\n/** Build an outbound `{ v, type, payload }` frame (the wire format the DO reads). */\nexport function buildFrame(type: string, payload: Record<string, unknown>): string {\n return JSON.stringify({ v: CHAT_WIRE_VERSION, type, payload });\n}\n\n/** Parse an inbound frame, tolerating malformed JSON. Returns null on garbage. */\nexport function parseFrame(\n raw: string,\n): { v?: number; type?: string; payload?: Record<string, unknown> } | null {\n try {\n const parsed = JSON.parse(raw) as unknown;\n if (!parsed || typeof parsed !== 'object') return null;\n return parsed as { v?: number; type?: string; payload?: Record<string, unknown> };\n } catch {\n return null;\n }\n}\n\n/** A snapshot with `registry` is an inbox snapshot; one with `lastMessageSeq` is a channel snapshot. */\nexport function isInboxSnapshot(\n p: WireChannelSnapshot | WireInboxSnapshot,\n): p is WireInboxSnapshot {\n return Array.isArray((p as WireInboxSnapshot).registry);\n}\n"]}
1
+ {"version":3,"file":"wire.js","sourceRoot":"","sources":["../../src/realtime/wire.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,+BAA+B,CAAC;AA6DlE,MAAM,UAAU,UAAU,CAAC,IAAY,EAAE,OAAgC;IACvE,OAAO,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC,EAAE,iBAAiB,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC;AACjE,CAAC;AAED,MAAM,UAAU,UAAU,CACxB,GAAW;IAEX,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAY,CAAC;QAC1C,IAAI,CAAC,MAAM,IAAI,OAAO,MAAM,KAAK,QAAQ;YAAE,OAAO,IAAI,CAAC;QACvD,OAAO,MAA0E,CAAC;IACpF,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,MAAM,UAAU,eAAe,CAC7B,CAA0C;IAE1C,OAAO,KAAK,CAAC,OAAO,CAAE,CAAuB,CAAC,QAAQ,CAAC,CAAC;AAC1D,CAAC","sourcesContent":["import { CHAT_WIRE_VERSION } from '@ttt-productions/chat-schemas';\nimport type {\n ChatConversationRef,\n ChatMarkReadResultPayload,\n ChatMessageRevisionKind,\n ChatSendRejectedPayload,\n} from '@ttt-productions/chat-schemas';\n\nexport type RevisionKind = ChatMessageRevisionKind;\n\nexport interface WireMessageRow {\n seq: number;\n senderUid: string;\n clientMessageId: string;\n text: string;\n createdAt: number;\n epoch: number;\n referencedUids?: string[];\n moderationKind?: RevisionKind | null;\n messageRevision?: number;\n}\n\nexport interface WireChannelSnapshot {\n lastMessageSeq: number;\n readSeq: number;\n resync?: boolean;\n delta?: WireMessageRow[];\n}\n\nexport interface WireRegistryEntry {\n channelRef: string;\n kind: string;\n state: 'active' | 'tombstoned';\n registryVersion: number;\n ref?: ChatConversationRef | null;\n unread?: boolean;\n archived?: boolean;\n}\n\nexport interface WireInboxSnapshot {\n registry: WireRegistryEntry[];\n hasUnread: boolean;\n}\n\n// Chat frames carry `type`, not realtime-core's `kind`: the deployed Worker emits and reads `type`.\nexport type ServerFrame =\n | { type: 'message'; payload: { message: WireMessageRow } }\n | { type: 'ack'; payload: { clientMessageId?: string; seq?: number; readSeq?: number } }\n | { type: 'history-page'; payload: { messages: WireMessageRow[] } }\n | { type: 'presence'; payload: { online: string[] } }\n | { type: 'typing'; payload: { uid: string; at: number } }\n | { type: 'snapshot'; payload: WireChannelSnapshot | WireInboxSnapshot }\n | { type: 'unread'; payload: WireInboxSnapshot | { hasUnread: boolean } }\n | { type: 'error'; payload: { code: string; retryAfterMs?: number } }\n | { type: 'mark-read-result'; payload: ChatMarkReadResultPayload }\n | { type: 'send-rejected'; payload: ChatSendRejectedPayload }\n | {\n type: 'revision';\n payload: { messageSeq: number; kind: RevisionKind; messageRevision: number };\n };\n\nexport function buildFrame(type: string, payload: Record<string, unknown>): string {\n return JSON.stringify({ v: CHAT_WIRE_VERSION, type, payload });\n}\n\nexport function parseFrame(\n raw: string,\n): { v?: number; type?: string; payload?: Record<string, unknown> } | null {\n try {\n const parsed = JSON.parse(raw) as unknown;\n if (!parsed || typeof parsed !== 'object') return null;\n return parsed as { v?: number; type?: string; payload?: Record<string, unknown> };\n } catch {\n return null;\n }\n}\n\nexport function isInboxSnapshot(\n p: WireChannelSnapshot | WireInboxSnapshot,\n): p is WireInboxSnapshot {\n return Array.isArray((p as WireInboxSnapshot).registry);\n}\n"]}
package/dist/types.d.ts CHANGED
@@ -1,30 +1,9 @@
1
1
  import type { ReactNode } from 'react';
2
2
  import type { ChatMessageV1 } from '@ttt-productions/chat-core';
3
3
  import type { ChatConversationRef } from '@ttt-productions/chat-schemas';
4
- /**
5
- * Which transport backs a chat thread.
6
- * - `firestore` — the Firestore newest-window + cursor path (default).
7
- * - `realtime` — the Cloudflare Durable Object socket; the grant is the data-access
8
- * authority there.
9
- */
10
4
  export type ChatTransportMode = 'firestore' | 'realtime';
11
- /**
12
- * Realtime (Durable Object) transport handle. The concrete socket client is built
13
- * in this package (`realtime/transport.ts`); chat-react consumes it through this
14
- * config so the UI stays transport-agnostic. The app builds the `client` via
15
- * `createRealtimeChatClient({ endpoint, channelRef, grantProvider, ... })` and
16
- * passes it here. `client` stays structurally typed (`unknown` at the config seam)
17
- * so `ChatCoreConfig` does not pull the realtime module's types into every firestore
18
- * consumer — the realtime hook narrows it at the call site.
19
- */
20
5
  export interface ChatRealtimeTransportConfig {
21
- /** The conversation the socket is scoped to, as the app maps it onto the neutral reference. */
22
6
  channelRef: ChatConversationRef;
23
- /**
24
- * The realtime client handle from `createRealtimeChatClient(...)`. Drives the
25
- * channel socket (subscribe / send / ack / typing / presence / history). The UI
26
- * never reaches into the socket directly — it goes through `useRealtimeChatMessages`.
27
- */
28
7
  client: unknown;
29
8
  }
30
9
  export type ChatCoreConfig = {
@@ -34,28 +13,11 @@ export type ChatCoreConfig = {
34
13
  currentUserId: string;
35
14
  currentUserDisplayName?: string;
36
15
  isAdmin: boolean;
37
- /**
38
- * Which transport backs this thread. Defaults to `'firestore'` (the current
39
- * path) so existing call sites are unchanged. See {@link ChatTransportMode}.
40
- */
41
16
  transport?: ChatTransportMode;
42
- /** Required iff `transport === 'realtime'`; ignored on the firestore transport. */
43
17
  realtime?: ChatRealtimeTransportConfig;
44
- /**
45
- * Whether the current user may read this conversation — the app's ONE access fact,
46
- * decided by the app's own rule for the conversation's kind (the package never decides
47
- * access). False renders the no-access state and reads nothing. On the `realtime`
48
- * transport the grant is the data authority, and a terminal denial from the grant
49
- * closes the conversation even when this is true.
50
- */
51
18
  allowed: boolean;
52
19
  createdAtField?: string;
53
20
  pageSize?: number;
54
- /**
55
- * Firestore transport only: the app's own per-message facts, read from the stored message
56
- * document and merged onto the mapped message's `meta`. The package never reads an app field
57
- * itself; the app parses its own stored shape here and reads the result back from `meta`.
58
- */
59
21
  messageMeta?: (doc: Record<string, unknown>) => Record<string, unknown> | undefined;
60
22
  };
61
23
  export type MessageRenderer = (m: ChatMessageV1) => ReactNode;
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AACvC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAChE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,+BAA+B,CAAC;AAWzE;;;;;GAKG;AACH,MAAM,MAAM,iBAAiB,GAAG,WAAW,GAAG,UAAU,CAAC;AAEzD;;;;;;;;GAQG;AACH,MAAM,WAAW,2BAA2B;IAC1C,+FAA+F;IAC/F,UAAU,EAAE,mBAAmB,CAAC;IAChC;;;;OAIG;IACH,MAAM,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,MAAM,cAAc,GAAG;IAC3B,kBAAkB,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IACtC,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;IACtB,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,OAAO,EAAE,OAAO,CAAC;IACjB;;;OAGG;IACH,SAAS,CAAC,EAAE,iBAAiB,CAAC;IAC9B,mFAAmF;IACnF,QAAQ,CAAC,EAAE,2BAA2B,CAAC;IACvC;;;;;;OAMG;IACH,OAAO,EAAE,OAAO,CAAC;IACjB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;OAIG;IACH,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAC;CACrF,CAAC;AAMF,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,EAAE,aAAa,KAAK,SAAS,CAAC;AAE9D,MAAM,MAAM,uBAAuB,GAAG,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AACvC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAChE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,+BAA+B,CAAC;AAEzE,MAAM,MAAM,iBAAiB,GAAG,WAAW,GAAG,UAAU,CAAC;AAEzD,MAAM,WAAW,2BAA2B;IAC1C,UAAU,EAAE,mBAAmB,CAAC;IAChC,MAAM,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,MAAM,cAAc,GAAG;IAC3B,kBAAkB,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IACtC,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;IACtB,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,OAAO,EAAE,OAAO,CAAC;IACjB,SAAS,CAAC,EAAE,iBAAiB,CAAC;IAC9B,QAAQ,CAAC,EAAE,2BAA2B,CAAC;IACvC,OAAO,EAAE,OAAO,CAAC;IACjB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAC;CACrF,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,EAAE,aAAa,KAAK,SAAS,CAAC;AAE9D,MAAM,MAAM,uBAAuB,GAAG,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC"}
package/dist/types.js CHANGED
@@ -1,5 +1,2 @@
1
- // React- and Firebase-client-coupled chat types. These were previously on the
2
- // chat-core root; they live here so the pure chat-core package stays free of
3
- // react / firebase/firestore / firebase/storage references.
4
1
  export {};
5
2
  //# sourceMappingURL=types.js.map
package/dist/types.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,6EAA6E;AAC7E,4DAA4D","sourcesContent":["// React- and Firebase-client-coupled chat types. These were previously on the\n// chat-core root; they live here so the pure chat-core package stays free of\n// react / firebase/firestore / firebase/storage references.\n\nimport type { ReactNode } from 'react';\nimport type { ChatMessageV1 } from '@ttt-productions/chat-core';\nimport type { ChatConversationRef } from '@ttt-productions/chat-schemas';\n\n// ============================================\n// CONFIG\n// ============================================\n//\n// Chat carries PLAIN TEXT only. There is no upload adapter, attachment config,\n// or mention/token config here: a conversation's files are owned by the\n// consuming app's Conversation Files surface, which runs the canonical upload\n// pipeline outside the chat timeline.\n\n/**\n * Which transport backs a chat thread.\n * - `firestore` — the Firestore newest-window + cursor path (default).\n * - `realtime` — the Cloudflare Durable Object socket; the grant is the data-access\n * authority there.\n */\nexport type ChatTransportMode = 'firestore' | 'realtime';\n\n/**\n * Realtime (Durable Object) transport handle. The concrete socket client is built\n * in this package (`realtime/transport.ts`); chat-react consumes it through this\n * config so the UI stays transport-agnostic. The app builds the `client` via\n * `createRealtimeChatClient({ endpoint, channelRef, grantProvider, ... })` and\n * passes it here. `client` stays structurally typed (`unknown` at the config seam)\n * so `ChatCoreConfig` does not pull the realtime module's types into every firestore\n * consumer — the realtime hook narrows it at the call site.\n */\nexport interface ChatRealtimeTransportConfig {\n /** The conversation the socket is scoped to, as the app maps it onto the neutral reference. */\n channelRef: ChatConversationRef;\n /**\n * The realtime client handle from `createRealtimeChatClient(...)`. Drives the\n * channel socket (subscribe / send / ack / typing / presence / history). The UI\n * never reaches into the socket directly — it goes through `useRealtimeChatMessages`.\n */\n client: unknown;\n}\n\nexport type ChatCoreConfig = {\n chatCollectionPath: string | string[];\n messagesSubcollection?: string; // default: \"messages\"\n threadId: string;\n currentUserId: string;\n currentUserDisplayName?: string;\n isAdmin: boolean;\n /**\n * Which transport backs this thread. Defaults to `'firestore'` (the current\n * path) so existing call sites are unchanged. See {@link ChatTransportMode}.\n */\n transport?: ChatTransportMode;\n /** Required iff `transport === 'realtime'`; ignored on the firestore transport. */\n realtime?: ChatRealtimeTransportConfig;\n /**\n * Whether the current user may read this conversation — the app's ONE access fact,\n * decided by the app's own rule for the conversation's kind (the package never decides\n * access). False renders the no-access state and reads nothing. On the `realtime`\n * transport the grant is the data authority, and a terminal denial from the grant\n * closes the conversation even when this is true.\n */\n allowed: boolean;\n createdAtField?: string; // default: \"createdAt\"\n pageSize?: number;\n /**\n * Firestore transport only: the app's own per-message facts, read from the stored message\n * document and merged onto the mapped message's `meta`. The package never reads an app field\n * itself; the app parses its own stored shape here and reads the result back from `meta`.\n */\n messageMeta?: (doc: Record<string, unknown>) => Record<string, unknown> | undefined;\n};\n\n// ============================================\n// RENDERING\n// ============================================\n\nexport type MessageRenderer = (m: ChatMessageV1) => ReactNode;\n\nexport type MessageRendererRegistry = Record<string, MessageRenderer>;\n"]}
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"","sourcesContent":["import type { ReactNode } from 'react';\nimport type { ChatMessageV1 } from '@ttt-productions/chat-core';\nimport type { ChatConversationRef } from '@ttt-productions/chat-schemas';\n\nexport type ChatTransportMode = 'firestore' | 'realtime';\n\nexport interface ChatRealtimeTransportConfig {\n channelRef: ChatConversationRef;\n client: unknown;\n}\n\nexport type ChatCoreConfig = {\n chatCollectionPath: string | string[];\n messagesSubcollection?: string;\n threadId: string;\n currentUserId: string;\n currentUserDisplayName?: string;\n isAdmin: boolean;\n transport?: ChatTransportMode;\n realtime?: ChatRealtimeTransportConfig;\n allowed: boolean;\n createdAtField?: string;\n pageSize?: number;\n messageMeta?: (doc: Record<string, unknown>) => Record<string, unknown> | undefined;\n};\n\nexport type MessageRenderer = (m: ChatMessageV1) => ReactNode;\n\nexport type MessageRendererRegistry = Record<string, MessageRenderer>;\n"]}
@@ -2,21 +2,6 @@ import * as React from 'react';
2
2
  import type { ChatMessageGroupKey, ChatMessageV1, ModerationHandlers } from '@ttt-productions/chat-core';
3
3
  import type { ChatCoreConfig, MessageRendererRegistry } from '../types.js';
4
4
  import { type ChatScrollContainerProps } from './MessageList.js';
5
- /**
6
- * A read the chat could not complete, handed to the app's `renderLoadError`:
7
- * - `initial` — nothing loaded: the first open failed (the realtime client's attempts
8
- * failed for its budget, or the Firestore listener failed before any message). It
9
- * takes the place of the message list.
10
- * - `live` — the Firestore listener failed after messages loaded. The messages stay,
11
- * with this band above them.
12
- * - `older` — an older page failed to load. It renders at the top of the list, which
13
- * stops asking for older pages until `retry`.
14
- * `error` is the read's own error (null on the realtime transport); `retry` re-runs what
15
- * failed — the realtime transport also keeps retrying in the background. `retrying` is true
16
- * while a retry of this failure is running and the failure is still shown (an older page being
17
- * read again), so the app's Retry shows its pending state. A retried initial or live failure
18
- * gives way to the opening state instead.
19
- */
20
5
  export type ChatLoadFailure = {
21
6
  scope: 'initial' | 'live' | 'older';
22
7
  error: Error | null;
@@ -25,10 +10,6 @@ export type ChatLoadFailure = {
25
10
  };
26
11
  export type ChatShellProps = {
27
12
  config: ChatCoreConfig;
28
- /**
29
- * Renders every failed read (see {@link ChatLoadFailure}) — the app's own error state and
30
- * copy. Required: a chat that cannot load must say so, and the words are the app's.
31
- */
32
13
  renderLoadError: (failure: ChatLoadFailure) => React.ReactNode;
33
14
  header?: React.ReactNode;
34
15
  onSend?: (text: string) => Promise<void>;
@@ -39,38 +20,15 @@ export type ChatShellProps = {
39
20
  autoFocus?: boolean;
40
21
  renderAboveMessages?: () => React.ReactNode;
41
22
  renderBelowMessages?: () => React.ReactNode;
42
- /** If provided, replaces the Composer entirely */
43
23
  renderFooter?: () => React.ReactNode;
44
24
  onSenderClick?: (senderId: string) => void;
45
- /** The app's per-message grouping fact and its sender-line adornment (forwarded to MessageList). */
46
25
  messageGroupKey?: ChatMessageGroupKey;
47
26
  renderSenderAdornment?: (m: ChatMessageV1) => React.ReactNode;
48
27
  composerDisabled?: boolean;
49
- /** Fill the parent's height instead of the default fixed-height card. Lays the Card out
50
- * as `flex flex-col h-full` with the message list flexing to fill (scrolls inside), so a
51
- * full-height page panel doesn't grow unbounded or waste space on a fixed 400px box.
52
- * The consumer must give ChatShell a bounded-height parent (e.g. an `h-full` panel). */
53
28
  fillHeight?: boolean;
54
- /**
55
- * Called with whether the conversation is READY — its access allowed, its initial data loaded
56
- * with no failure, and (realtime) the channel current since its socket last opened — each time
57
- * that changes, and with `false` when the shell unmounts. A consumer that lets someone act on
58
- * what the conversation says (an agreement, an offer) offers and accepts that action only while
59
- * it is true; reading the conversation needs no readiness.
60
- */
61
29
  onReadinessChange?: (ready: boolean) => void;
62
- /** Advanced override for the scrollable region's class (forwarded to MessageList).
63
- * Usually unnecessary — prefer `fillHeight`. */
64
30
  scrollClassName?: string;
65
- /** Renders the scrollable message region (forwarded to MessageList) in place of the default div,
66
- * so the consumer can give it its own surface component. */
67
31
  scrollContainer?: React.ComponentType<ChatScrollContainerProps>;
68
32
  };
69
- /**
70
- * ChatShell dispatches on `config.transport`. Hooks must be unconditional, so the
71
- * two transports live in separate inner components (each calls exactly ONE data
72
- * hook) and both render the shared presentational `ChatShellView`. The default
73
- * (undefined / 'firestore') transport is byte-for-byte the previous behavior.
74
- */
75
33
  export declare function ChatShell(props: ChatShellProps): React.JSX.Element;
76
34
  //# sourceMappingURL=ChatShell.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ChatShell.d.ts","sourceRoot":"","sources":["../../src/ui/ChatShell.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,KAAK,EACV,mBAAmB,EACnB,aAAa,EACb,kBAAkB,EACnB,MAAM,4BAA4B,CAAC;AACpC,OAAO,KAAK,EAAE,cAAc,EAAE,uBAAuB,EAAE,MAAM,aAAa,CAAC;AAa3E,OAAO,EAAe,KAAK,wBAAwB,EAAE,MAAM,kBAAkB,CAAC;AAI9E;;;;;;;;;;;;;;GAcG;AACH,MAAM,MAAM,eAAe,GAAG;IAC5B,KAAK,EAAE,SAAS,GAAG,MAAM,GAAG,OAAO,CAAC;IACpC,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IACpB,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,QAAQ,EAAE,OAAO,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG;IAC3B,MAAM,EAAE,cAAc,CAAC;IAEvB;;;OAGG;IACH,eAAe,EAAE,CAAC,OAAO,EAAE,eAAe,KAAK,KAAK,CAAC,SAAS,CAAC;IAG/D,MAAM,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAKzB,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAGzC,aAAa,CAAC,EAAE,CAAC,CAAC,EAAE,GAAG,KAAK,KAAK,CAAC,SAAS,CAAC;IAC5C,gBAAgB,CAAC,EAAE,uBAAuB,CAAC;IAC3C,QAAQ,CAAC,EAAE,kBAAkB,CAAC;IAE9B,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,SAAS,CAAC,EAAE,OAAO,CAAC;IAGpB,mBAAmB,CAAC,EAAE,MAAM,KAAK,CAAC,SAAS,CAAC;IAC5C,mBAAmB,CAAC,EAAE,MAAM,KAAK,CAAC,SAAS,CAAC;IAC5C,kDAAkD;IAClD,YAAY,CAAC,EAAE,MAAM,KAAK,CAAC,SAAS,CAAC;IAGrC,aAAa,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IAC3C,oGAAoG;IACpG,eAAe,CAAC,EAAE,mBAAmB,CAAC;IACtC,qBAAqB,CAAC,EAAE,CAAC,CAAC,EAAE,aAAa,KAAK,KAAK,CAAC,SAAS,CAAC;IAG9D,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAE3B;;;6FAGyF;IACzF,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;;;;OAMG;IACH,iBAAiB,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;IAC7C;qDACiD;IACjD,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;iEAC6D;IAC7D,eAAe,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC,wBAAwB,CAAC,CAAC;CACjE,CAAC;AAgCF;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,cAAc,qBAK9C"}
1
+ {"version":3,"file":"ChatShell.d.ts","sourceRoot":"","sources":["../../src/ui/ChatShell.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,KAAK,EACV,mBAAmB,EACnB,aAAa,EACb,kBAAkB,EACnB,MAAM,4BAA4B,CAAC;AACpC,OAAO,KAAK,EAAE,cAAc,EAAE,uBAAuB,EAAE,MAAM,aAAa,CAAC;AAa3E,OAAO,EAAe,KAAK,wBAAwB,EAAE,MAAM,kBAAkB,CAAC;AAI9E,MAAM,MAAM,eAAe,GAAG;IAC5B,KAAK,EAAE,SAAS,GAAG,MAAM,GAAG,OAAO,CAAC;IACpC,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IACpB,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,QAAQ,EAAE,OAAO,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG;IAC3B,MAAM,EAAE,cAAc,CAAC;IAEvB,eAAe,EAAE,CAAC,OAAO,EAAE,eAAe,KAAK,KAAK,CAAC,SAAS,CAAC;IAE/D,MAAM,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAEzB,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAEzC,aAAa,CAAC,EAAE,CAAC,CAAC,EAAE,GAAG,KAAK,KAAK,CAAC,SAAS,CAAC;IAC5C,gBAAgB,CAAC,EAAE,uBAAuB,CAAC;IAC3C,QAAQ,CAAC,EAAE,kBAAkB,CAAC;IAE9B,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,SAAS,CAAC,EAAE,OAAO,CAAC;IAEpB,mBAAmB,CAAC,EAAE,MAAM,KAAK,CAAC,SAAS,CAAC;IAC5C,mBAAmB,CAAC,EAAE,MAAM,KAAK,CAAC,SAAS,CAAC;IAC5C,YAAY,CAAC,EAAE,MAAM,KAAK,CAAC,SAAS,CAAC;IAErC,aAAa,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IAC3C,eAAe,CAAC,EAAE,mBAAmB,CAAC;IACtC,qBAAqB,CAAC,EAAE,CAAC,CAAC,EAAE,aAAa,KAAK,KAAK,CAAC,SAAS,CAAC;IAE9D,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAE3B,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,iBAAiB,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;IAC7C,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,eAAe,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC,wBAAwB,CAAC,CAAC;CACjE,CAAC;AAqBF,wBAAgB,SAAS,CAAC,KAAK,EAAE,cAAc,qBAK9C"}
@@ -8,12 +8,7 @@ import { useRealtimeChatMessages } from '../realtime/useRealtimeChatMessages.js'
8
8
  import { MessageList } from './MessageList.js';
9
9
  import { Composer } from './Composer.js';
10
10
  import { ThreadActions } from './menus.js';
11
- /**
12
- * ChatShell dispatches on `config.transport`. Hooks must be unconditional, so the
13
- * two transports live in separate inner components (each calls exactly ONE data
14
- * hook) and both render the shared presentational `ChatShellView`. The default
15
- * (undefined / 'firestore') transport is byte-for-byte the previous behavior.
16
- */
11
+ // Hooks must be unconditional, so each transport is a separate component with one data hook.
17
12
  export function ChatShell(props) {
18
13
  if (props.config.transport === 'realtime') {
19
14
  return _jsx(RealtimeChatShell, { ...props });
@@ -56,15 +51,12 @@ function RealtimeChatShell(props) {
56
51
  const loadFailure = React.useMemo(() => (initialLoadFailed ? { scope: 'initial', error: null, retry, retrying: false } : null), [initialLoadFailed, retry]);
57
52
  const send = React.useCallback(async (text) => {
58
53
  const ok = r.send(text);
59
- // C-B8: surface a closed-socket failure so the Composer keeps the user's text
60
- // instead of clearing it on a no-op send.
61
54
  if (!ok)
62
55
  throw new Error('chat-send-failed');
63
56
  if (onSend)
64
- await onSend(text); // optional mirror
57
+ await onSend(text);
65
58
  }, [r, onSend]);
66
59
  return (_jsx(ChatShellView, { ...props, resolved: {
67
- // The app's access fact AND the grant's: a terminal denial closes it either way.
68
60
  allowed: config.allowed && r.allowed,
69
61
  isInitialLoading: r.isInitialLoading,
70
62
  transportCurrent: r.isSynced,
@@ -85,7 +77,6 @@ function RealtimeChatShell(props) {
85
77
  function ChatShellView(props) {
86
78
  const { config, header, renderMessage, messageRenderers, handlers, composerPlaceholder, autoFocus = false, renderAboveMessages, renderBelowMessages, renderFooter, onSenderClick, messageGroupKey, renderSenderAdornment, composerDisabled, fillHeight = false, scrollClassName, scrollContainer, renderLoadError, onReadinessChange, resolved, } = props;
87
79
  const { allowed, isInitialLoading, messages, fetchOlder, hasOlder, isFetchingOlder, loadFailure, olderFailure, send, retrySend, readAck, status, typing, signalTyping, } = resolved;
88
- // Nothing loaded and the read failed: the failure takes the list's place.
89
80
  const initialFailure = loadFailure?.scope === 'initial' ? loadFailure : null;
90
81
  const ready = allowed && !isInitialLoading && loadFailure === null && resolved.transportCurrent;
91
82
  const readinessListener = React.useRef(onReadinessChange);
@@ -101,7 +92,6 @@ function ChatShellView(props) {
101
92
  const [focused, setFocused] = React.useState(true);
102
93
  const lastAckedRef = React.useRef(0);
103
94
  const leaseHeldRef = React.useRef(false);
104
- // The latest authoritative seq currently rendered (optimistic rows have no seq).
105
95
  const latestSeq = React.useMemo(() => {
106
96
  let max = 0;
107
97
  for (const m of messages) {
@@ -111,7 +101,6 @@ function ChatShellView(props) {
111
101
  }
112
102
  return max;
113
103
  }, [messages]);
114
- // Track tab focus/visibility so the read-ack "focused" lease releases on blur/hide.
115
104
  React.useEffect(() => {
116
105
  const compute = () => typeof document === 'undefined' ||
117
106
  (document.visibilityState !== 'hidden' &&
@@ -127,18 +116,12 @@ function ChatShellView(props) {
127
116
  document.removeEventListener('visibilitychange', onChange);
128
117
  };
129
118
  }, []);
130
- // C-B6: advance the authoritative read cursor when the reader is actually at the
131
- // latest message (channel mounted + tab focused + scrolled to bottom); release the
132
- // focus lease exactly once when they stop. No-op on the firestore path (no readAck).
133
119
  React.useEffect(() => {
134
120
  if (!readAck || latestSeq <= 0)
135
121
  return;
136
122
  if (focused && atBottom) {
137
123
  leaseHeldRef.current = true;
138
124
  if (latestSeq > lastAckedRef.current) {
139
- // Advance the local marker ONLY if the frame was actually sent — a drop during a
140
- // disconnect window must stay un-acked so it retries (the client also re-sends the
141
- // cursor on reconnect). (M2)
142
125
  if (readAck(latestSeq, true))
143
126
  lastAckedRef.current = latestSeq;
144
127
  }
@@ -148,37 +131,19 @@ function ChatShellView(props) {
148
131
  readAck(latestSeq, false);
149
132
  }
150
133
  }, [readAck, latestSeq, atBottom, focused]);
151
- // In fill mode the Card flexes to its parent's height and the message list fills the
152
- // space between header and composer; otherwise it's the default natural-height card.
153
134
  const cardClassName = fillHeight ? 'w-full flex flex-col h-full min-h-0' : 'w-full';
154
135
  const contentClassName = fillHeight ? 'p-0 flex-1 min-h-0 flex flex-col' : 'p-0';
155
136
  if (!allowed) {
156
137
  return _jsx("div", { className: "p-4 text-sm opacity-70", children: "You don't have access to this thread." });
157
138
  }
158
139
  if (isInitialLoading || initialFailure) {
159
- // Before the first authoritative snapshot/history result, show an HONEST working
160
- // state — a visible "Opening chat…" spinner in a polite live region — NOT a blank
161
- // parchment/skeleton box (which read as an empty chat) and NOT the reconnect banner
162
- // (which must not compete with the initial-opening message). Only the MessageList
163
- // region is replaced: the consumer's header + actions + the three render-slots +
164
- // footer still render, and `onReadinessChange` tells a consumer whose slots carry
165
- // actions that the conversation is not ready to act on.
166
140
  return (_jsxs(Card, { className: cardClassName, children: [(header || handlers) && (_jsxs(CardHeader, { className: "flex flex-row items-start justify-between gap-4 space-y-0", children: [_jsx("div", { children: header }), _jsx("div", { className: "flex flex-col items-end gap-2", children: _jsx(ThreadActions, { threadId: config.threadId, isAdmin: config.isAdmin, handlers: handlers }) })] })), (() => {
167
- // The wrapper (and its border) renders only when the slot actually
168
- // produced content — a slot returning null (e.g. a collapsed
169
- // conversation-files section) must not paint an empty bordered strip.
170
141
  const aboveMessages = renderAboveMessages?.() ?? null;
171
142
  return aboveMessages != null && aboveMessages !== false ? (_jsx("div", { className: "border-b", children: aboveMessages })) : null;
172
143
  })(), _jsx(CardContent, { className: contentClassName, children: initialFailure ? (renderLoadError(initialFailure)) : (_jsxs("div", { className: "flex flex-col items-center justify-center gap-2 py-12 text-sm text-muted-foreground", role: "status", "aria-live": "polite", children: [_jsx(Spinner, { size: "sm" }), _jsx("span", { children: "Opening chat\u2026" })] })) }), renderBelowMessages && _jsx("div", { className: "border-t", children: renderBelowMessages() }), renderFooter ? (_jsx(CardFooter, { className: "border-t", children: renderFooter() })) : (_jsx(CardFooter, { className: "border-t", children: _jsx(Skeleton, { className: "h-10 w-full" }) }))] }));
173
144
  }
174
145
  return (_jsxs(Card, { className: cardClassName, children: [(header || handlers) && (_jsxs(CardHeader, { className: "flex flex-row items-start justify-between gap-4 space-y-0", children: [_jsx("div", { children: header }), _jsx("div", { className: "flex flex-col items-end gap-2", children: _jsx(ThreadActions, { threadId: config.threadId, isAdmin: config.isAdmin, handlers: handlers }) })] })), _jsx(ChatConnectionBanner, { status: status }), renderAboveMessages && _jsx("div", { className: "border-b", children: renderAboveMessages() }), loadFailure?.scope === 'live' && (_jsx("div", { className: "border-b", children: renderLoadError(loadFailure) })), _jsx(CardContent, { className: contentClassName, children: _jsx(MessageList, { messages: messages, currentUserId: config.currentUserId, isAdmin: config.isAdmin, isFetchingOlder: isFetchingOlder, hasOlder: hasOlder, onLoadOlder: () => fetchOlder(), renderMessage: renderMessage, messageRenderers: messageRenderers, showScrollToBottom: showScrollToBottom, onScrollToBottom: () => setShowScrollToBottom(false), onAtBottomChange: setAtBottom, fillHeight: fillHeight, scrollClassName: scrollClassName, scrollContainer: scrollContainer, handlers: handlers, onSenderClick: onSenderClick, messageGroupKey: messageGroupKey, renderSenderAdornment: renderSenderAdornment, onRetrySend: retrySend, olderLoadErrorRow: olderFailure ? renderLoadError(olderFailure) : null }) }), renderBelowMessages && _jsx("div", { className: "border-t", children: renderBelowMessages() }), renderFooter ? (_jsx(CardFooter, { className: "border-t", children: renderFooter() })) : (_jsxs(CardFooter, { className: "border-t flex-col items-stretch gap-1", children: [_jsx(TypingIndicator, { typing: typing }), _jsx(KeyboardAvoidingView, { padding: true, offset: 8, className: "w-full", children: _jsx(Composer, { onSend: send, onTyping: signalTyping, disabled: composerDisabled, autoFocus: autoFocus, placeholder: composerPlaceholder }) })] }))] }));
175
146
  }
176
- /**
177
- * R13 — connection-state banner. The realtime path sets `status`; once the first
178
- * snapshot has loaded (ChatShellView is past `isInitialLoading`), a dropped/unreachable
179
- * socket surfaces here instead of a silently-empty thread. Renders nothing on the
180
- * firestore path (`status` undefined) or while live (`open`).
181
- */
182
147
  function ChatConnectionBanner({ status }) {
183
148
  if (status !== 'reconnecting' && status !== 'closed')
184
149
  return null;
@@ -186,8 +151,6 @@ function ChatConnectionBanner({ status }) {
186
151
  ? 'Reconnecting… messages may be delayed.'
187
152
  : 'Disconnected — messages may be out of date.' }));
188
153
  }
189
- /** R14 — typing indicator. `typing` is the set of OTHER users typing (the transport
190
- * excludes the local user). Generic copy (no name resolution) for v1. */
191
154
  function TypingIndicator({ typing }) {
192
155
  if (!typing || typing.length === 0)
193
156
  return null;
@@ -1 +1 @@
1
- {"version":3,"file":"ChatShell.js","sourceRoot":"","sources":["../../src/ui/ChatShell.tsx"],"names":[],"mappings":"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","sourcesContent":["'use client';\n\nimport * as React from 'react';\nimport type {\n ChatMessageGroupKey,\n ChatMessageV1,\n ModerationHandlers,\n} from '@ttt-productions/chat-core';\nimport type { ChatCoreConfig, MessageRendererRegistry } from '../types.js';\nimport {\n Card,\n CardHeader,\n CardContent,\n CardFooter,\n Skeleton,\n Spinner,\n} from '@ttt-productions/ui-core/react';\nimport { KeyboardAvoidingView } from '@ttt-productions/mobile-core/react';\nimport { useChatMessages } from '../hooks/useChatMessages.js';\nimport { useRealtimeChatMessages } from '../realtime/useRealtimeChatMessages.js';\nimport type { RealtimeChatClient } from '../realtime/transport.js';\nimport { MessageList, type ChatScrollContainerProps } from './MessageList.js';\nimport { Composer } from './Composer.js';\nimport { ThreadActions } from './menus.js';\n\n/**\n * A read the chat could not complete, handed to the app's `renderLoadError`:\n * - `initial` — nothing loaded: the first open failed (the realtime client's attempts\n * failed for its budget, or the Firestore listener failed before any message). It\n * takes the place of the message list.\n * - `live` — the Firestore listener failed after messages loaded. The messages stay,\n * with this band above them.\n * - `older` — an older page failed to load. It renders at the top of the list, which\n * stops asking for older pages until `retry`.\n * `error` is the read's own error (null on the realtime transport); `retry` re-runs what\n * failed — the realtime transport also keeps retrying in the background. `retrying` is true\n * while a retry of this failure is running and the failure is still shown (an older page being\n * read again), so the app's Retry shows its pending state. A retried initial or live failure\n * gives way to the opening state instead.\n */\nexport type ChatLoadFailure = {\n scope: 'initial' | 'live' | 'older';\n error: Error | null;\n retry: () => void;\n retrying: boolean;\n};\n\nexport type ChatShellProps = {\n config: ChatCoreConfig;\n\n /**\n * Renders every failed read (see {@link ChatLoadFailure}) — the app's own error state and\n * copy. Required: a chat that cannot load must say so, and the words are the app's.\n */\n renderLoadError: (failure: ChatLoadFailure) => React.ReactNode;\n\n // Header\n header?: React.ReactNode;\n\n // Send handler — text-only. On the realtime transport this is OPTIONAL: the\n // shell sends through the DO socket. If provided, it is still called (so a\n // consumer can mirror to analytics), but the socket send is authoritative.\n onSend?: (text: string) => Promise<void>;\n\n // Message rendering\n renderMessage?: (m: any) => React.ReactNode;\n messageRenderers?: MessageRendererRegistry;\n handlers?: ModerationHandlers;\n\n composerPlaceholder?: string;\n autoFocus?: boolean;\n\n // Three render slots\n renderAboveMessages?: () => React.ReactNode;\n renderBelowMessages?: () => React.ReactNode;\n /** If provided, replaces the Composer entirely */\n renderFooter?: () => React.ReactNode;\n\n // Sender interaction\n onSenderClick?: (senderId: string) => void;\n /** The app's per-message grouping fact and its sender-line adornment (forwarded to MessageList). */\n messageGroupKey?: ChatMessageGroupKey;\n renderSenderAdornment?: (m: ChatMessageV1) => React.ReactNode;\n\n // Disable\n composerDisabled?: boolean;\n\n /** Fill the parent's height instead of the default fixed-height card. Lays the Card out\n * as `flex flex-col h-full` with the message list flexing to fill (scrolls inside), so a\n * full-height page panel doesn't grow unbounded or waste space on a fixed 400px box.\n * The consumer must give ChatShell a bounded-height parent (e.g. an `h-full` panel). */\n fillHeight?: boolean;\n /**\n * Called with whether the conversation is READY — its access allowed, its initial data loaded\n * with no failure, and (realtime) the channel current since its socket last opened — each time\n * that changes, and with `false` when the shell unmounts. A consumer that lets someone act on\n * what the conversation says (an agreement, an offer) offers and accepts that action only while\n * it is true; reading the conversation needs no readiness.\n */\n onReadinessChange?: (ready: boolean) => void;\n /** Advanced override for the scrollable region's class (forwarded to MessageList).\n * Usually unnecessary — prefer `fillHeight`. */\n scrollClassName?: string;\n /** Renders the scrollable message region (forwarded to MessageList) in place of the default div,\n * so the consumer can give it its own surface component. */\n scrollContainer?: React.ComponentType<ChatScrollContainerProps>;\n};\n\n/** The resolved data a transport hook hands the presentational view. */\ntype ResolvedChat = {\n allowed: boolean;\n isInitialLoading: boolean;\n /** The transport's own readiness beyond loaded-and-allowed (realtime: the channel is current). */\n transportCurrent: boolean;\n messages: ChatMessageV1[];\n fetchOlder: () => Promise<void>;\n hasOlder: boolean;\n isFetchingOlder: boolean;\n /** The failed read that replaces or annotates the list (initial / live), or null. */\n loadFailure: ChatLoadFailure | null;\n /** The failed older page, or null. */\n olderFailure: ChatLoadFailure | null;\n /** The send handler the Composer calls (socket send on realtime, prop on firestore). */\n send: (text: string) => Promise<void>;\n /** Retry a failed realtime send by its original clientMessageId (realtime only;\n * absent on firestore). Wired to MessageItemDefault's failed-bubble retry. */\n retrySend?: (clientMessageId: string) => void;\n /** Advance the authoritative read cursor (realtime only; absent on the firestore path).\n * Returns whether the ack was actually sent so the caller advances its local cursor only on success (M2). */\n readAck?: (latestSeq: number, focused: boolean) => boolean;\n /** Realtime connection status — drives the disconnected banner (R13). Absent on firestore. */\n status?: string;\n /** uids currently typing (realtime only) — drives the typing indicator (R14). */\n typing?: string[];\n /** Signal the local user is typing (coalesced by the transport). Wired to the Composer (R14). */\n signalTyping?: () => void;\n};\n\n/**\n * ChatShell dispatches on `config.transport`. Hooks must be unconditional, so the\n * two transports live in separate inner components (each calls exactly ONE data\n * hook) and both render the shared presentational `ChatShellView`. The default\n * (undefined / 'firestore') transport is byte-for-byte the previous behavior.\n */\nexport function ChatShell(props: ChatShellProps) {\n if (props.config.transport === 'realtime') {\n return <RealtimeChatShell {...props} />;\n }\n return <FirestoreChatShell {...props} />;\n}\n\nfunction FirestoreChatShell(props: ChatShellProps) {\n const { config, onSend } = props;\n const r = useChatMessages(config);\n const send = React.useCallback(\n async (text: string) => {\n if (onSend) await onSend(text);\n },\n [onSend],\n );\n const { error, olderError, retry, messages, isFetchingOlder } = r;\n const loadFailure = React.useMemo<ChatLoadFailure | null>(\n () =>\n error\n ? { scope: messages.length > 0 ? 'live' : 'initial', error, retry, retrying: false }\n : null,\n [error, messages.length, retry],\n );\n const olderFailure = React.useMemo<ChatLoadFailure | null>(\n () =>\n olderError ? { scope: 'older', error: olderError, retry, retrying: isFetchingOlder } : null,\n [olderError, retry, isFetchingOlder],\n );\n return (\n <ChatShellView\n {...props}\n resolved={{\n allowed: r.allowed,\n isInitialLoading: r.isInitialLoading,\n transportCurrent: true,\n messages: r.messages,\n fetchOlder: r.fetchOlder,\n hasOlder: r.hasOlder,\n isFetchingOlder: r.isFetchingOlder,\n loadFailure,\n olderFailure,\n send,\n }}\n />\n );\n}\n\nfunction RealtimeChatShell(props: ChatShellProps) {\n const { config, onSend } = props;\n const client = config.realtime?.client as RealtimeChatClient | undefined;\n if (!client) {\n throw new Error(\n \"[ChatShell] transport 'realtime' requires config.realtime.client (createRealtimeChatClient(...))\",\n );\n }\n const r = useRealtimeChatMessages(client);\n const { initialLoadFailed, retry } = r;\n const loadFailure = React.useMemo<ChatLoadFailure | null>(\n () => (initialLoadFailed ? { scope: 'initial', error: null, retry, retrying: false } : null),\n [initialLoadFailed, retry],\n );\n const send = React.useCallback(\n async (text: string) => {\n const ok = r.send(text);\n // C-B8: surface a closed-socket failure so the Composer keeps the user's text\n // instead of clearing it on a no-op send.\n if (!ok) throw new Error('chat-send-failed');\n if (onSend) await onSend(text); // optional mirror\n },\n [r, onSend],\n );\n return (\n <ChatShellView\n {...props}\n resolved={{\n // The app's access fact AND the grant's: a terminal denial closes it either way.\n allowed: config.allowed && r.allowed,\n isInitialLoading: r.isInitialLoading,\n transportCurrent: r.isSynced,\n messages: r.messages,\n fetchOlder: r.fetchOlder,\n hasOlder: r.hasOlder,\n isFetchingOlder: r.isFetchingOlder,\n loadFailure,\n olderFailure: null,\n send,\n retrySend: r.retrySend,\n readAck: r.readAck,\n status: r.status,\n typing: r.typing,\n signalTyping: r.signalTyping,\n }}\n />\n );\n}\n\nfunction ChatShellView(props: ChatShellProps & { resolved: ResolvedChat }) {\n const {\n config,\n header,\n renderMessage,\n messageRenderers,\n handlers,\n composerPlaceholder,\n autoFocus = false,\n renderAboveMessages,\n renderBelowMessages,\n renderFooter,\n onSenderClick,\n messageGroupKey,\n renderSenderAdornment,\n composerDisabled,\n fillHeight = false,\n scrollClassName,\n scrollContainer,\n renderLoadError,\n onReadinessChange,\n resolved,\n } = props;\n\n const {\n allowed,\n isInitialLoading,\n messages,\n fetchOlder,\n hasOlder,\n isFetchingOlder,\n loadFailure,\n olderFailure,\n send,\n retrySend,\n readAck,\n status,\n typing,\n signalTyping,\n } = resolved;\n // Nothing loaded and the read failed: the failure takes the list's place.\n const initialFailure = loadFailure?.scope === 'initial' ? loadFailure : null;\n\n const ready = allowed && !isInitialLoading && loadFailure === null && resolved.transportCurrent;\n const readinessListener = React.useRef(onReadinessChange);\n React.useEffect(() => {\n readinessListener.current = onReadinessChange;\n });\n React.useEffect(() => {\n readinessListener.current?.(ready);\n }, [ready]);\n React.useEffect(() => () => readinessListener.current?.(false), []);\n\n const [showScrollToBottom, setShowScrollToBottom] = React.useState(false);\n const [atBottom, setAtBottom] = React.useState(true);\n const [focused, setFocused] = React.useState(true);\n const lastAckedRef = React.useRef(0);\n const leaseHeldRef = React.useRef(false);\n\n // The latest authoritative seq currently rendered (optimistic rows have no seq).\n const latestSeq = React.useMemo(() => {\n let max = 0;\n for (const m of messages) {\n const seq = typeof m.meta?.seq === 'number' ? (m.meta?.seq as number) : 0;\n if (seq > max) max = seq;\n }\n return max;\n }, [messages]);\n\n // Track tab focus/visibility so the read-ack \"focused\" lease releases on blur/hide.\n React.useEffect(() => {\n const compute = () =>\n typeof document === 'undefined' ||\n (document.visibilityState !== 'hidden' &&\n (typeof document.hasFocus !== 'function' || document.hasFocus()));\n setFocused(compute());\n const onChange = () => setFocused(compute());\n window.addEventListener('focus', onChange);\n window.addEventListener('blur', onChange);\n document.addEventListener('visibilitychange', onChange);\n return () => {\n window.removeEventListener('focus', onChange);\n window.removeEventListener('blur', onChange);\n document.removeEventListener('visibilitychange', onChange);\n };\n }, []);\n\n // C-B6: advance the authoritative read cursor when the reader is actually at the\n // latest message (channel mounted + tab focused + scrolled to bottom); release the\n // focus lease exactly once when they stop. No-op on the firestore path (no readAck).\n React.useEffect(() => {\n if (!readAck || latestSeq <= 0) return;\n if (focused && atBottom) {\n leaseHeldRef.current = true;\n if (latestSeq > lastAckedRef.current) {\n // Advance the local marker ONLY if the frame was actually sent — a drop during a\n // disconnect window must stay un-acked so it retries (the client also re-sends the\n // cursor on reconnect). (M2)\n if (readAck(latestSeq, true)) lastAckedRef.current = latestSeq;\n }\n } else if (leaseHeldRef.current) {\n leaseHeldRef.current = false;\n readAck(latestSeq, false);\n }\n }, [readAck, latestSeq, atBottom, focused]);\n\n // In fill mode the Card flexes to its parent's height and the message list fills the\n // space between header and composer; otherwise it's the default natural-height card.\n const cardClassName = fillHeight ? 'w-full flex flex-col h-full min-h-0' : 'w-full';\n const contentClassName = fillHeight ? 'p-0 flex-1 min-h-0 flex flex-col' : 'p-0';\n\n if (!allowed) {\n return <div className=\"p-4 text-sm opacity-70\">You don&apos;t have access to this thread.</div>;\n }\n\n if (isInitialLoading || initialFailure) {\n // Before the first authoritative snapshot/history result, show an HONEST working\n // state — a visible \"Opening chat…\" spinner in a polite live region — NOT a blank\n // parchment/skeleton box (which read as an empty chat) and NOT the reconnect banner\n // (which must not compete with the initial-opening message). Only the MessageList\n // region is replaced: the consumer's header + actions + the three render-slots +\n // footer still render, and `onReadinessChange` tells a consumer whose slots carry\n // actions that the conversation is not ready to act on.\n return (\n <Card className={cardClassName}>\n {(header || handlers) && (\n <CardHeader className=\"flex flex-row items-start justify-between gap-4 space-y-0\">\n <div>{header}</div>\n <div className=\"flex flex-col items-end gap-2\">\n <ThreadActions\n threadId={config.threadId}\n isAdmin={config.isAdmin}\n handlers={handlers}\n />\n </div>\n </CardHeader>\n )}\n {(() => {\n // The wrapper (and its border) renders only when the slot actually\n // produced content — a slot returning null (e.g. a collapsed\n // conversation-files section) must not paint an empty bordered strip.\n const aboveMessages = renderAboveMessages?.() ?? null;\n return aboveMessages != null && aboveMessages !== false ? (\n <div className=\"border-b\">{aboveMessages}</div>\n ) : null;\n })()}\n <CardContent className={contentClassName}>\n {initialFailure ? (\n renderLoadError(initialFailure)\n ) : (\n <div\n className=\"flex flex-col items-center justify-center gap-2 py-12 text-sm text-muted-foreground\"\n role=\"status\"\n aria-live=\"polite\"\n >\n <Spinner size=\"sm\" />\n <span>Opening chat…</span>\n </div>\n )}\n </CardContent>\n {renderBelowMessages && <div className=\"border-t\">{renderBelowMessages()}</div>}\n {renderFooter ? (\n <CardFooter className=\"border-t\">{renderFooter()}</CardFooter>\n ) : (\n <CardFooter className=\"border-t\">\n <Skeleton className=\"h-10 w-full\" />\n </CardFooter>\n )}\n </Card>\n );\n }\n\n return (\n <Card className={cardClassName}>\n {(header || handlers) && (\n <CardHeader className=\"flex flex-row items-start justify-between gap-4 space-y-0\">\n <div>{header}</div>\n <div className=\"flex flex-col items-end gap-2\">\n <ThreadActions\n threadId={config.threadId}\n isAdmin={config.isAdmin}\n handlers={handlers}\n />\n </div>\n </CardHeader>\n )}\n\n {/* R13: a disconnected/reconnecting banner so a configured-but-unreachable Worker\n never presents a healthy-looking empty thread. Only the realtime path sets `status`. */}\n <ChatConnectionBanner status={status} />\n\n {renderAboveMessages && <div className=\"border-b\">{renderAboveMessages()}</div>}\n\n {/* The listener failed after messages loaded: they stay, with the failure above them. */}\n {loadFailure?.scope === 'live' && (\n <div className=\"border-b\">{renderLoadError(loadFailure)}</div>\n )}\n\n <CardContent className={contentClassName}>\n <MessageList\n messages={messages}\n currentUserId={config.currentUserId}\n isAdmin={config.isAdmin}\n isFetchingOlder={isFetchingOlder}\n hasOlder={hasOlder}\n onLoadOlder={() => fetchOlder()}\n renderMessage={renderMessage}\n messageRenderers={messageRenderers}\n showScrollToBottom={showScrollToBottom}\n onScrollToBottom={() => setShowScrollToBottom(false)}\n onAtBottomChange={setAtBottom}\n fillHeight={fillHeight}\n scrollClassName={scrollClassName}\n scrollContainer={scrollContainer}\n handlers={handlers}\n onSenderClick={onSenderClick}\n messageGroupKey={messageGroupKey}\n renderSenderAdornment={renderSenderAdornment}\n onRetrySend={retrySend}\n olderLoadErrorRow={olderFailure ? renderLoadError(olderFailure) : null}\n />\n </CardContent>\n\n {renderBelowMessages && <div className=\"border-t\">{renderBelowMessages()}</div>}\n\n {renderFooter ? (\n <CardFooter className=\"border-t\">{renderFooter()}</CardFooter>\n ) : (\n <CardFooter className=\"border-t flex-col items-stretch gap-1\">\n {/* R14: typing dots above the composer (realtime only — `typing` is undefined on firestore). */}\n <TypingIndicator typing={typing} />\n <KeyboardAvoidingView padding offset={8} className=\"w-full\">\n <Composer\n onSend={send}\n onTyping={signalTyping}\n disabled={composerDisabled}\n autoFocus={autoFocus}\n placeholder={composerPlaceholder}\n />\n </KeyboardAvoidingView>\n </CardFooter>\n )}\n </Card>\n );\n}\n\n/**\n * R13 — connection-state banner. The realtime path sets `status`; once the first\n * snapshot has loaded (ChatShellView is past `isInitialLoading`), a dropped/unreachable\n * socket surfaces here instead of a silently-empty thread. Renders nothing on the\n * firestore path (`status` undefined) or while live (`open`).\n */\nfunction ChatConnectionBanner({ status }: { status?: string }) {\n if (status !== 'reconnecting' && status !== 'closed') return null;\n return (\n <div\n className=\"px-3 py-1.5 text-xs text-center bg-muted text-muted-foreground border-b\"\n role=\"status\"\n aria-live=\"polite\"\n >\n {status === 'reconnecting'\n ? 'Reconnecting… messages may be delayed.'\n : 'Disconnected — messages may be out of date.'}\n </div>\n );\n}\n\n/** R14 — typing indicator. `typing` is the set of OTHER users typing (the transport\n * excludes the local user). Generic copy (no name resolution) for v1. */\nfunction TypingIndicator({ typing }: { typing?: string[] }) {\n if (!typing || typing.length === 0) return null;\n return (\n <div className=\"text-xs italic text-muted-foreground\" aria-live=\"polite\">\n {typing.length === 1 ? 'Someone is typing…' : 'Several people are typing…'}\n </div>\n );\n}\n"]}
1
+ {"version":3,"file":"ChatShell.js","sourceRoot":"","sources":["../../src/ui/ChatShell.tsx"],"names":[],"mappings":"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","sourcesContent":["'use client';\n\nimport * as React from 'react';\nimport type {\n ChatMessageGroupKey,\n ChatMessageV1,\n ModerationHandlers,\n} from '@ttt-productions/chat-core';\nimport type { ChatCoreConfig, MessageRendererRegistry } from '../types.js';\nimport {\n Card,\n CardHeader,\n CardContent,\n CardFooter,\n Skeleton,\n Spinner,\n} from '@ttt-productions/ui-core/react';\nimport { KeyboardAvoidingView } from '@ttt-productions/mobile-core/react';\nimport { useChatMessages } from '../hooks/useChatMessages.js';\nimport { useRealtimeChatMessages } from '../realtime/useRealtimeChatMessages.js';\nimport type { RealtimeChatClient } from '../realtime/transport.js';\nimport { MessageList, type ChatScrollContainerProps } from './MessageList.js';\nimport { Composer } from './Composer.js';\nimport { ThreadActions } from './menus.js';\n\nexport type ChatLoadFailure = {\n scope: 'initial' | 'live' | 'older';\n error: Error | null;\n retry: () => void;\n retrying: boolean;\n};\n\nexport type ChatShellProps = {\n config: ChatCoreConfig;\n\n renderLoadError: (failure: ChatLoadFailure) => React.ReactNode;\n\n header?: React.ReactNode;\n\n onSend?: (text: string) => Promise<void>;\n\n renderMessage?: (m: any) => React.ReactNode;\n messageRenderers?: MessageRendererRegistry;\n handlers?: ModerationHandlers;\n\n composerPlaceholder?: string;\n autoFocus?: boolean;\n\n renderAboveMessages?: () => React.ReactNode;\n renderBelowMessages?: () => React.ReactNode;\n renderFooter?: () => React.ReactNode;\n\n onSenderClick?: (senderId: string) => void;\n messageGroupKey?: ChatMessageGroupKey;\n renderSenderAdornment?: (m: ChatMessageV1) => React.ReactNode;\n\n composerDisabled?: boolean;\n\n fillHeight?: boolean;\n onReadinessChange?: (ready: boolean) => void;\n scrollClassName?: string;\n scrollContainer?: React.ComponentType<ChatScrollContainerProps>;\n};\n\ntype ResolvedChat = {\n allowed: boolean;\n isInitialLoading: boolean;\n transportCurrent: boolean;\n messages: ChatMessageV1[];\n fetchOlder: () => Promise<void>;\n hasOlder: boolean;\n isFetchingOlder: boolean;\n loadFailure: ChatLoadFailure | null;\n olderFailure: ChatLoadFailure | null;\n send: (text: string) => Promise<void>;\n retrySend?: (clientMessageId: string) => void;\n readAck?: (latestSeq: number, focused: boolean) => boolean;\n status?: string;\n typing?: string[];\n signalTyping?: () => void;\n};\n\n// Hooks must be unconditional, so each transport is a separate component with one data hook.\nexport function ChatShell(props: ChatShellProps) {\n if (props.config.transport === 'realtime') {\n return <RealtimeChatShell {...props} />;\n }\n return <FirestoreChatShell {...props} />;\n}\n\nfunction FirestoreChatShell(props: ChatShellProps) {\n const { config, onSend } = props;\n const r = useChatMessages(config);\n const send = React.useCallback(\n async (text: string) => {\n if (onSend) await onSend(text);\n },\n [onSend],\n );\n const { error, olderError, retry, messages, isFetchingOlder } = r;\n const loadFailure = React.useMemo<ChatLoadFailure | null>(\n () =>\n error\n ? { scope: messages.length > 0 ? 'live' : 'initial', error, retry, retrying: false }\n : null,\n [error, messages.length, retry],\n );\n const olderFailure = React.useMemo<ChatLoadFailure | null>(\n () =>\n olderError ? { scope: 'older', error: olderError, retry, retrying: isFetchingOlder } : null,\n [olderError, retry, isFetchingOlder],\n );\n return (\n <ChatShellView\n {...props}\n resolved={{\n allowed: r.allowed,\n isInitialLoading: r.isInitialLoading,\n transportCurrent: true,\n messages: r.messages,\n fetchOlder: r.fetchOlder,\n hasOlder: r.hasOlder,\n isFetchingOlder: r.isFetchingOlder,\n loadFailure,\n olderFailure,\n send,\n }}\n />\n );\n}\n\nfunction RealtimeChatShell(props: ChatShellProps) {\n const { config, onSend } = props;\n const client = config.realtime?.client as RealtimeChatClient | undefined;\n if (!client) {\n throw new Error(\n \"[ChatShell] transport 'realtime' requires config.realtime.client (createRealtimeChatClient(...))\",\n );\n }\n const r = useRealtimeChatMessages(client);\n const { initialLoadFailed, retry } = r;\n const loadFailure = React.useMemo<ChatLoadFailure | null>(\n () => (initialLoadFailed ? { scope: 'initial', error: null, retry, retrying: false } : null),\n [initialLoadFailed, retry],\n );\n const send = React.useCallback(\n async (text: string) => {\n const ok = r.send(text);\n if (!ok) throw new Error('chat-send-failed');\n if (onSend) await onSend(text);\n },\n [r, onSend],\n );\n return (\n <ChatShellView\n {...props}\n resolved={{\n allowed: config.allowed && r.allowed,\n isInitialLoading: r.isInitialLoading,\n transportCurrent: r.isSynced,\n messages: r.messages,\n fetchOlder: r.fetchOlder,\n hasOlder: r.hasOlder,\n isFetchingOlder: r.isFetchingOlder,\n loadFailure,\n olderFailure: null,\n send,\n retrySend: r.retrySend,\n readAck: r.readAck,\n status: r.status,\n typing: r.typing,\n signalTyping: r.signalTyping,\n }}\n />\n );\n}\n\nfunction ChatShellView(props: ChatShellProps & { resolved: ResolvedChat }) {\n const {\n config,\n header,\n renderMessage,\n messageRenderers,\n handlers,\n composerPlaceholder,\n autoFocus = false,\n renderAboveMessages,\n renderBelowMessages,\n renderFooter,\n onSenderClick,\n messageGroupKey,\n renderSenderAdornment,\n composerDisabled,\n fillHeight = false,\n scrollClassName,\n scrollContainer,\n renderLoadError,\n onReadinessChange,\n resolved,\n } = props;\n\n const {\n allowed,\n isInitialLoading,\n messages,\n fetchOlder,\n hasOlder,\n isFetchingOlder,\n loadFailure,\n olderFailure,\n send,\n retrySend,\n readAck,\n status,\n typing,\n signalTyping,\n } = resolved;\n const initialFailure = loadFailure?.scope === 'initial' ? loadFailure : null;\n\n const ready = allowed && !isInitialLoading && loadFailure === null && resolved.transportCurrent;\n const readinessListener = React.useRef(onReadinessChange);\n React.useEffect(() => {\n readinessListener.current = onReadinessChange;\n });\n React.useEffect(() => {\n readinessListener.current?.(ready);\n }, [ready]);\n React.useEffect(() => () => readinessListener.current?.(false), []);\n\n const [showScrollToBottom, setShowScrollToBottom] = React.useState(false);\n const [atBottom, setAtBottom] = React.useState(true);\n const [focused, setFocused] = React.useState(true);\n const lastAckedRef = React.useRef(0);\n const leaseHeldRef = React.useRef(false);\n\n const latestSeq = React.useMemo(() => {\n let max = 0;\n for (const m of messages) {\n const seq = typeof m.meta?.seq === 'number' ? (m.meta?.seq as number) : 0;\n if (seq > max) max = seq;\n }\n return max;\n }, [messages]);\n\n React.useEffect(() => {\n const compute = () =>\n typeof document === 'undefined' ||\n (document.visibilityState !== 'hidden' &&\n (typeof document.hasFocus !== 'function' || document.hasFocus()));\n setFocused(compute());\n const onChange = () => setFocused(compute());\n window.addEventListener('focus', onChange);\n window.addEventListener('blur', onChange);\n document.addEventListener('visibilitychange', onChange);\n return () => {\n window.removeEventListener('focus', onChange);\n window.removeEventListener('blur', onChange);\n document.removeEventListener('visibilitychange', onChange);\n };\n }, []);\n\n React.useEffect(() => {\n if (!readAck || latestSeq <= 0) return;\n if (focused && atBottom) {\n leaseHeldRef.current = true;\n if (latestSeq > lastAckedRef.current) {\n if (readAck(latestSeq, true)) lastAckedRef.current = latestSeq;\n }\n } else if (leaseHeldRef.current) {\n leaseHeldRef.current = false;\n readAck(latestSeq, false);\n }\n }, [readAck, latestSeq, atBottom, focused]);\n\n const cardClassName = fillHeight ? 'w-full flex flex-col h-full min-h-0' : 'w-full';\n const contentClassName = fillHeight ? 'p-0 flex-1 min-h-0 flex flex-col' : 'p-0';\n\n if (!allowed) {\n return <div className=\"p-4 text-sm opacity-70\">You don&apos;t have access to this thread.</div>;\n }\n\n if (isInitialLoading || initialFailure) {\n return (\n <Card className={cardClassName}>\n {(header || handlers) && (\n <CardHeader className=\"flex flex-row items-start justify-between gap-4 space-y-0\">\n <div>{header}</div>\n <div className=\"flex flex-col items-end gap-2\">\n <ThreadActions\n threadId={config.threadId}\n isAdmin={config.isAdmin}\n handlers={handlers}\n />\n </div>\n </CardHeader>\n )}\n {(() => {\n const aboveMessages = renderAboveMessages?.() ?? null;\n return aboveMessages != null && aboveMessages !== false ? (\n <div className=\"border-b\">{aboveMessages}</div>\n ) : null;\n })()}\n <CardContent className={contentClassName}>\n {initialFailure ? (\n renderLoadError(initialFailure)\n ) : (\n <div\n className=\"flex flex-col items-center justify-center gap-2 py-12 text-sm text-muted-foreground\"\n role=\"status\"\n aria-live=\"polite\"\n >\n <Spinner size=\"sm\" />\n <span>Opening chat…</span>\n </div>\n )}\n </CardContent>\n {renderBelowMessages && <div className=\"border-t\">{renderBelowMessages()}</div>}\n {renderFooter ? (\n <CardFooter className=\"border-t\">{renderFooter()}</CardFooter>\n ) : (\n <CardFooter className=\"border-t\">\n <Skeleton className=\"h-10 w-full\" />\n </CardFooter>\n )}\n </Card>\n );\n }\n\n return (\n <Card className={cardClassName}>\n {(header || handlers) && (\n <CardHeader className=\"flex flex-row items-start justify-between gap-4 space-y-0\">\n <div>{header}</div>\n <div className=\"flex flex-col items-end gap-2\">\n <ThreadActions\n threadId={config.threadId}\n isAdmin={config.isAdmin}\n handlers={handlers}\n />\n </div>\n </CardHeader>\n )}\n\n <ChatConnectionBanner status={status} />\n\n {renderAboveMessages && <div className=\"border-b\">{renderAboveMessages()}</div>}\n\n {loadFailure?.scope === 'live' && (\n <div className=\"border-b\">{renderLoadError(loadFailure)}</div>\n )}\n\n <CardContent className={contentClassName}>\n <MessageList\n messages={messages}\n currentUserId={config.currentUserId}\n isAdmin={config.isAdmin}\n isFetchingOlder={isFetchingOlder}\n hasOlder={hasOlder}\n onLoadOlder={() => fetchOlder()}\n renderMessage={renderMessage}\n messageRenderers={messageRenderers}\n showScrollToBottom={showScrollToBottom}\n onScrollToBottom={() => setShowScrollToBottom(false)}\n onAtBottomChange={setAtBottom}\n fillHeight={fillHeight}\n scrollClassName={scrollClassName}\n scrollContainer={scrollContainer}\n handlers={handlers}\n onSenderClick={onSenderClick}\n messageGroupKey={messageGroupKey}\n renderSenderAdornment={renderSenderAdornment}\n onRetrySend={retrySend}\n olderLoadErrorRow={olderFailure ? renderLoadError(olderFailure) : null}\n />\n </CardContent>\n\n {renderBelowMessages && <div className=\"border-t\">{renderBelowMessages()}</div>}\n\n {renderFooter ? (\n <CardFooter className=\"border-t\">{renderFooter()}</CardFooter>\n ) : (\n <CardFooter className=\"border-t flex-col items-stretch gap-1\">\n <TypingIndicator typing={typing} />\n <KeyboardAvoidingView padding offset={8} className=\"w-full\">\n <Composer\n onSend={send}\n onTyping={signalTyping}\n disabled={composerDisabled}\n autoFocus={autoFocus}\n placeholder={composerPlaceholder}\n />\n </KeyboardAvoidingView>\n </CardFooter>\n )}\n </Card>\n );\n}\n\nfunction ChatConnectionBanner({ status }: { status?: string }) {\n if (status !== 'reconnecting' && status !== 'closed') return null;\n return (\n <div\n className=\"px-3 py-1.5 text-xs text-center bg-muted text-muted-foreground border-b\"\n role=\"status\"\n aria-live=\"polite\"\n >\n {status === 'reconnecting'\n ? 'Reconnecting… messages may be delayed.'\n : 'Disconnected — messages may be out of date.'}\n </div>\n );\n}\n\nfunction TypingIndicator({ typing }: { typing?: string[] }) {\n if (!typing || typing.length === 0) return null;\n return (\n <div className=\"text-xs italic text-muted-foreground\" aria-live=\"polite\">\n {typing.length === 1 ? 'Someone is typing…' : 'Several people are typing…'}\n </div>\n );\n}\n"]}
@@ -1,19 +1,6 @@
1
1
  import * as React from 'react';
2
2
  export type ComposerProps = {
3
- /**
4
- * Text send — the composer's ONE argument. The composer is PLAIN-TEXT-ONLY:
5
- * files are shared through the conversation's Conversation Files surface,
6
- * never through the message timeline, so there is no attachment path here;
7
- * message text carries no inline token grammar, so what the user typed is what
8
- * is sent; and there is no reply-target picker, so a send never names another
9
- * message (DJ ruling 2026-07-29).
10
- */
11
3
  onSend: (text: string) => Promise<void>;
12
- /**
13
- * Signal the local user is typing (R14). Called as the user edits the textarea; the
14
- * realtime transport coalesces to ≤1 frame per ~2s, so calling per keystroke is safe.
15
- * Optional — absent on the firestore transport (no typing broadcast).
16
- */
17
4
  onTyping?: () => void;
18
5
  disabled?: boolean;
19
6
  autoFocus?: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"Composer.d.ts","sourceRoot":"","sources":["../../src/ui/Composer.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAc/B,MAAM,MAAM,aAAa,GAAG;IAC1B;;;;;;;OAOG;IACH,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAExC;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IAEtB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF,wBAAgB,QAAQ,CAAC,KAAK,EAAE,aAAa,qBAyG5C"}
1
+ {"version":3,"file":"Composer.d.ts","sourceRoot":"","sources":["../../src/ui/Composer.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAc/B,MAAM,MAAM,aAAa,GAAG;IAC1B,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAExC,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IAEtB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF,wBAAgB,QAAQ,CAAC,KAAK,EAAE,aAAa,qBA4F5C"}
@@ -14,21 +14,13 @@ export function Composer(props) {
14
14
  const [isSending, setIsSending] = React.useState(false);
15
15
  const [sendError, setSendError] = React.useState(null);
16
16
  const ref = React.useRef(null);
17
- // Navigation guard for the in-flight send: a send that has left the composer
18
- // but not yet committed is killable by navigation/sign-out with zero warning
19
- // (observed live: the callable aborted, the message silently never delivered).
20
- // Registering with the app's one guarded-navigation system makes beforeunload
21
- // fire on hard navs and lets GuardedLink-style navigation confirm first.
22
- // Optional accessor — consumers without the provider degrade to old behavior.
23
17
  const navigationGuard = useOptionalLocalUploadGuard();
24
18
  const counterId = React.useId();
25
- // focus stability: never steal focus unless explicitly enabled
26
19
  React.useEffect(() => {
27
20
  if (!autoFocus)
28
21
  return;
29
22
  ref.current?.focus();
30
23
  }, [autoFocus]);
31
- // The same judgement the Worker applies: a blank text is never sent, and what is sent is trimmed.
32
24
  const judged = judgeChatMessageText(text);
33
25
  const send = async () => {
34
26
  if (!judged.ok)
@@ -42,10 +34,6 @@ export function Composer(props) {
42
34
  setText('');
43
35
  }
44
36
  catch (err) {
45
- // C-B8: a failed send (e.g. the realtime socket was closed) must NOT clear the
46
- // user's text — `setText("")` only runs on the success path above — and must NOT
47
- // re-throw into the click handler (an unhandled rejection). Surface it so the
48
- // user can retry with their text intact.
49
37
  console.error('[Composer] Send failed:', err);
50
38
  setSendError("Couldn't send. Check your connection and try again.");
51
39
  }
@@ -59,7 +47,6 @@ export function Composer(props) {
59
47
  setText(e.target.value);
60
48
  if (sendError)
61
49
  setSendError(null);
62
- // R14: broadcast a typing signal (transport-coalesced) while there is content.
63
50
  if (onTyping && e.target.value.trim().length > 0)
64
51
  onTyping();
65
52
  }, placeholder: placeholder, disabled: isDisabled, rows: 1, className: cn('min-h-[40px] resize-none'), onKeyDown: (e) => {
@@ -1 +1 @@
1
- {"version":3,"file":"Composer.js","sourceRoot":"","sources":["../../src/ui/Composer.tsx"],"names":[],"mappings":"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","sourcesContent":["'use client';\n\nimport * as React from 'react';\nimport { Button, Textarea } from '@ttt-productions/ui-core/react';\nimport { cn } from '@ttt-productions/ui-core';\nimport { useOptionalLocalUploadGuard } from '@ttt-productions/upload-ui/react/guard';\nimport {\n CHAT_MESSAGE_TEXT_INPUT,\n CHAT_MESSAGE_TEXT_MAX_LENGTH,\n judgeChatMessageText,\n} from '@ttt-productions/chat-schemas';\n\nfunction genId(): string {\n return `${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`;\n}\n\nexport type ComposerProps = {\n /**\n * Text send — the composer's ONE argument. The composer is PLAIN-TEXT-ONLY:\n * files are shared through the conversation's Conversation Files surface,\n * never through the message timeline, so there is no attachment path here;\n * message text carries no inline token grammar, so what the user typed is what\n * is sent; and there is no reply-target picker, so a send never names another\n * message (DJ ruling 2026-07-29).\n */\n onSend: (text: string) => Promise<void>;\n\n /**\n * Signal the local user is typing (R14). Called as the user edits the textarea; the\n * realtime transport coalesces to ≤1 frame per ~2s, so calling per keystroke is safe.\n * Optional — absent on the firestore transport (no typing broadcast).\n */\n onTyping?: () => void;\n\n disabled?: boolean;\n autoFocus?: boolean;\n placeholder?: string;\n};\n\nexport function Composer(props: ComposerProps) {\n const {\n onSend,\n onTyping,\n disabled,\n autoFocus = false,\n placeholder = 'Type a message...',\n } = props;\n\n const [text, setText] = React.useState('');\n const [isSending, setIsSending] = React.useState(false);\n const [sendError, setSendError] = React.useState<string | null>(null);\n const ref = React.useRef<HTMLTextAreaElement>(null);\n // Navigation guard for the in-flight send: a send that has left the composer\n // but not yet committed is killable by navigation/sign-out with zero warning\n // (observed live: the callable aborted, the message silently never delivered).\n // Registering with the app's one guarded-navigation system makes beforeunload\n // fire on hard navs and lets GuardedLink-style navigation confirm first.\n // Optional accessor — consumers without the provider degrade to old behavior.\n const navigationGuard = useOptionalLocalUploadGuard();\n const counterId = React.useId();\n\n // focus stability: never steal focus unless explicitly enabled\n React.useEffect(() => {\n if (!autoFocus) return;\n ref.current?.focus();\n }, [autoFocus]);\n\n // The same judgement the Worker applies: a blank text is never sent, and what is sent is trimmed.\n const judged = judgeChatMessageText(text);\n\n const send = async () => {\n if (!judged.ok) return;\n\n setIsSending(true);\n setSendError(null);\n const sendGuardId = `chat-send-${genId()}`;\n navigationGuard?.registerUpload(sendGuardId);\n\n try {\n await onSend(judged.text);\n setText('');\n } catch (err) {\n // C-B8: a failed send (e.g. the realtime socket was closed) must NOT clear the\n // user's text — `setText(\"\")` only runs on the success path above — and must NOT\n // re-throw into the click handler (an unhandled rejection). Surface it so the\n // user can retry with their text intact.\n console.error('[Composer] Send failed:', err);\n setSendError(\"Couldn't send. Check your connection and try again.\");\n } finally {\n navigationGuard?.unregisterUpload(sendGuardId);\n setIsSending(false);\n }\n };\n\n const isDisabled = disabled || isSending;\n\n return (\n <div className=\"chat-composer\">\n {sendError && (\n <div className=\"px-1 pb-1 text-sm text-destructive\" role=\"alert\">\n {sendError}\n </div>\n )}\n\n <div className=\"flex items-end gap-2\">\n <Textarea\n ref={ref}\n value={text}\n inputFormat={CHAT_MESSAGE_TEXT_INPUT}\n aria-describedby={text.length > 0 ? counterId : undefined}\n onChange={(e) => {\n setText(e.target.value);\n if (sendError) setSendError(null);\n // R14: broadcast a typing signal (transport-coalesced) while there is content.\n if (onTyping && e.target.value.trim().length > 0) onTyping();\n }}\n placeholder={placeholder}\n disabled={isDisabled}\n rows={1}\n className={cn('min-h-[40px] resize-none')}\n onKeyDown={(e) => {\n if (e.key === 'Enter' && !e.shiftKey) {\n e.preventDefault();\n send();\n }\n }}\n />\n <Button\n type=\"button\"\n variant=\"default\"\n disabled={isDisabled || !judged.ok}\n pending={isSending}\n onClick={send}\n >\n Send\n </Button>\n </div>\n {text.length > 0 && (\n <div id={counterId} className=\"px-1 pt-1 text-xs text-right text-muted-foreground\">\n {text.length}/{CHAT_MESSAGE_TEXT_MAX_LENGTH}\n </div>\n )}\n </div>\n );\n}\n"]}
1
+ {"version":3,"file":"Composer.js","sourceRoot":"","sources":["../../src/ui/Composer.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAEb,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,gCAAgC,CAAC;AAClE,OAAO,EAAE,EAAE,EAAE,MAAM,0BAA0B,CAAC;AAC9C,OAAO,EAAE,2BAA2B,EAAE,MAAM,wCAAwC,CAAC;AACrF,OAAO,EACL,uBAAuB,EACvB,4BAA4B,EAC5B,oBAAoB,GACrB,MAAM,+BAA+B,CAAC;AAEvC,SAAS,KAAK;IACZ,OAAO,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,EAAE,CAAC;AAC5E,CAAC;AAYD,MAAM,UAAU,QAAQ,CAAC,KAAoB;IAC3C,MAAM,EACJ,MAAM,EACN,QAAQ,EACR,QAAQ,EACR,SAAS,GAAG,KAAK,EACjB,WAAW,GAAG,mBAAmB,GAClC,GAAG,KAAK,CAAC;IAEV,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,GAAG,KAAK,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;IAC3C,MAAM,CAAC,SAAS,EAAE,YAAY,CAAC,GAAG,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;IACxD,MAAM,CAAC,SAAS,EAAE,YAAY,CAAC,GAAG,KAAK,CAAC,QAAQ,CAAgB,IAAI,CAAC,CAAC;IACtE,MAAM,GAAG,GAAG,KAAK,CAAC,MAAM,CAAsB,IAAI,CAAC,CAAC;IACpD,MAAM,eAAe,GAAG,2BAA2B,EAAE,CAAC;IACtD,MAAM,SAAS,GAAG,KAAK,CAAC,KAAK,EAAE,CAAC;IAEhC,KAAK,CAAC,SAAS,CAAC,GAAG,EAAE;QACnB,IAAI,CAAC,SAAS;YAAE,OAAO;QACvB,GAAG,CAAC,OAAO,EAAE,KAAK,EAAE,CAAC;IACvB,CAAC,EAAE,CAAC,SAAS,CAAC,CAAC,CAAC;IAEhB,MAAM,MAAM,GAAG,oBAAoB,CAAC,IAAI,CAAC,CAAC;IAE1C,MAAM,IAAI,GAAG,KAAK,IAAI,EAAE;QACtB,IAAI,CAAC,MAAM,CAAC,EAAE;YAAE,OAAO;QAEvB,YAAY,CAAC,IAAI,CAAC,CAAC;QACnB,YAAY,CAAC,IAAI,CAAC,CAAC;QACnB,MAAM,WAAW,GAAG,aAAa,KAAK,EAAE,EAAE,CAAC;QAC3C,eAAe,EAAE,cAAc,CAAC,WAAW,CAAC,CAAC;QAE7C,IAAI,CAAC;YACH,MAAM,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;YAC1B,OAAO,CAAC,EAAE,CAAC,CAAC;QACd,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,OAAO,CAAC,KAAK,CAAC,yBAAyB,EAAE,GAAG,CAAC,CAAC;YAC9C,YAAY,CAAC,qDAAqD,CAAC,CAAC;QACtE,CAAC;gBAAS,CAAC;YACT,eAAe,EAAE,gBAAgB,CAAC,WAAW,CAAC,CAAC;YAC/C,YAAY,CAAC,KAAK,CAAC,CAAC;QACtB,CAAC;IACH,CAAC,CAAC;IAEF,MAAM,UAAU,GAAG,QAAQ,IAAI,SAAS,CAAC;IAEzC,OAAO,CACL,eAAK,SAAS,EAAC,eAAe,aAC3B,SAAS,IAAI,CACZ,cAAK,SAAS,EAAC,oCAAoC,EAAC,IAAI,EAAC,OAAO,YAC7D,SAAS,GACN,CACP,EAED,eAAK,SAAS,EAAC,sBAAsB,aACnC,KAAC,QAAQ,IACP,GAAG,EAAE,GAAG,EACR,KAAK,EAAE,IAAI,EACX,WAAW,EAAE,uBAAuB,sBAClB,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,EACzD,QAAQ,EAAE,CAAC,CAAC,EAAE,EAAE;4BACd,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;4BACxB,IAAI,SAAS;gCAAE,YAAY,CAAC,IAAI,CAAC,CAAC;4BAClC,IAAI,QAAQ,IAAI,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC;gCAAE,QAAQ,EAAE,CAAC;wBAC/D,CAAC,EACD,WAAW,EAAE,WAAW,EACxB,QAAQ,EAAE,UAAU,EACpB,IAAI,EAAE,CAAC,EACP,SAAS,EAAE,EAAE,CAAC,0BAA0B,CAAC,EACzC,SAAS,EAAE,CAAC,CAAC,EAAE,EAAE;4BACf,IAAI,CAAC,CAAC,GAAG,KAAK,OAAO,IAAI,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC;gCACrC,CAAC,CAAC,cAAc,EAAE,CAAC;gCACnB,IAAI,EAAE,CAAC;4BACT,CAAC;wBACH,CAAC,GACD,EACF,KAAC,MAAM,IACL,IAAI,EAAC,QAAQ,EACb,OAAO,EAAC,SAAS,EACjB,QAAQ,EAAE,UAAU,IAAI,CAAC,MAAM,CAAC,EAAE,EAClC,OAAO,EAAE,SAAS,EAClB,OAAO,EAAE,IAAI,qBAGN,IACL,EACL,IAAI,CAAC,MAAM,GAAG,CAAC,IAAI,CAClB,eAAK,EAAE,EAAE,SAAS,EAAE,SAAS,EAAC,oDAAoD,aAC/E,IAAI,CAAC,MAAM,OAAG,4BAA4B,IACvC,CACP,IACG,CACP,CAAC;AACJ,CAAC","sourcesContent":["'use client';\n\nimport * as React from 'react';\nimport { Button, Textarea } from '@ttt-productions/ui-core/react';\nimport { cn } from '@ttt-productions/ui-core';\nimport { useOptionalLocalUploadGuard } from '@ttt-productions/upload-ui/react/guard';\nimport {\n CHAT_MESSAGE_TEXT_INPUT,\n CHAT_MESSAGE_TEXT_MAX_LENGTH,\n judgeChatMessageText,\n} from '@ttt-productions/chat-schemas';\n\nfunction genId(): string {\n return `${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`;\n}\n\nexport type ComposerProps = {\n onSend: (text: string) => Promise<void>;\n\n onTyping?: () => void;\n\n disabled?: boolean;\n autoFocus?: boolean;\n placeholder?: string;\n};\n\nexport function Composer(props: ComposerProps) {\n const {\n onSend,\n onTyping,\n disabled,\n autoFocus = false,\n placeholder = 'Type a message...',\n } = props;\n\n const [text, setText] = React.useState('');\n const [isSending, setIsSending] = React.useState(false);\n const [sendError, setSendError] = React.useState<string | null>(null);\n const ref = React.useRef<HTMLTextAreaElement>(null);\n const navigationGuard = useOptionalLocalUploadGuard();\n const counterId = React.useId();\n\n React.useEffect(() => {\n if (!autoFocus) return;\n ref.current?.focus();\n }, [autoFocus]);\n\n const judged = judgeChatMessageText(text);\n\n const send = async () => {\n if (!judged.ok) return;\n\n setIsSending(true);\n setSendError(null);\n const sendGuardId = `chat-send-${genId()}`;\n navigationGuard?.registerUpload(sendGuardId);\n\n try {\n await onSend(judged.text);\n setText('');\n } catch (err) {\n console.error('[Composer] Send failed:', err);\n setSendError(\"Couldn't send. Check your connection and try again.\");\n } finally {\n navigationGuard?.unregisterUpload(sendGuardId);\n setIsSending(false);\n }\n };\n\n const isDisabled = disabled || isSending;\n\n return (\n <div className=\"chat-composer\">\n {sendError && (\n <div className=\"px-1 pb-1 text-sm text-destructive\" role=\"alert\">\n {sendError}\n </div>\n )}\n\n <div className=\"flex items-end gap-2\">\n <Textarea\n ref={ref}\n value={text}\n inputFormat={CHAT_MESSAGE_TEXT_INPUT}\n aria-describedby={text.length > 0 ? counterId : undefined}\n onChange={(e) => {\n setText(e.target.value);\n if (sendError) setSendError(null);\n if (onTyping && e.target.value.trim().length > 0) onTyping();\n }}\n placeholder={placeholder}\n disabled={isDisabled}\n rows={1}\n className={cn('min-h-[40px] resize-none')}\n onKeyDown={(e) => {\n if (e.key === 'Enter' && !e.shiftKey) {\n e.preventDefault();\n send();\n }\n }}\n />\n <Button\n type=\"button\"\n variant=\"default\"\n disabled={isDisabled || !judged.ok}\n pending={isSending}\n onClick={send}\n >\n Send\n </Button>\n </div>\n {text.length > 0 && (\n <div id={counterId} className=\"px-1 pt-1 text-xs text-right text-muted-foreground\">\n {text.length}/{CHAT_MESSAGE_TEXT_MAX_LENGTH}\n </div>\n )}\n </div>\n );\n}\n"]}
@@ -7,11 +7,7 @@ export type MessageItemDefaultProps = {
7
7
  handlers?: ModerationHandlers;
8
8
  isContinuation?: boolean;
9
9
  onSenderClick?: (senderId: string) => void;
10
- /** The app's adornment beside the sender name; shown with the sender line, never on a continuation. */
11
10
  senderAdornment?: ReactNode;
12
- /** Retry a failed realtime send by its original clientMessageId (wired by
13
- * ChatShell on the realtime transport; absent on firestore). Enables the
14
- * retry affordance on a `meta.sendFailed` bubble. */
15
11
  onRetrySend?: (clientMessageId: string) => void;
16
12
  };
17
13
  export declare function MessageItemDefault(props: MessageItemDefaultProps): import("react").JSX.Element;
@@ -1 +1 @@
1
- {"version":3,"file":"MessageItemDefault.d.ts","sourceRoot":"","sources":["../../src/ui/MessageItemDefault.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AACvC,OAAO,KAAK,EAAE,aAAa,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAoEpF,MAAM,MAAM,uBAAuB,GAAG;IACpC,CAAC,EAAE,aAAa,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,CAAC,EAAE,kBAAkB,CAAC;IAG9B,cAAc,CAAC,EAAE,OAAO,CAAC;IAGzB,aAAa,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IAC3C,uGAAuG;IACvG,eAAe,CAAC,EAAE,SAAS,CAAC;IAE5B;;0DAEsD;IACtD,WAAW,CAAC,EAAE,CAAC,eAAe,EAAE,MAAM,KAAK,IAAI,CAAC;CACjD,CAAC;AAEF,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,uBAAuB,+BAsHhE"}
1
+ {"version":3,"file":"MessageItemDefault.d.ts","sourceRoot":"","sources":["../../src/ui/MessageItemDefault.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AACvC,OAAO,KAAK,EAAE,aAAa,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAsCpF,MAAM,MAAM,uBAAuB,GAAG;IACpC,CAAC,EAAE,aAAa,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,CAAC,EAAE,kBAAkB,CAAC;IAE9B,cAAc,CAAC,EAAE,OAAO,CAAC;IAEzB,aAAa,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IAC3C,eAAe,CAAC,EAAE,SAAS,CAAC;IAE5B,WAAW,CAAC,EAAE,CAAC,eAAe,EAAE,MAAM,KAAK,IAAI,CAAC;CACjD,CAAC;AAEF,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,uBAAuB,+BA2GhE"}