@ttt-productions/chat-react 0.13.1 → 0.13.3

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 (58) hide show
  1. package/README.md +13 -11
  2. package/dist/context/ChatNameResolverContext.d.ts +3 -3
  3. package/dist/context/ChatNameResolverContext.js +3 -3
  4. package/dist/context/ChatNameResolverContext.js.map +1 -1
  5. package/dist/hooks/useChatMessages.d.ts.map +1 -1
  6. package/dist/hooks/useChatMessages.js +4 -13
  7. package/dist/hooks/useChatMessages.js.map +1 -1
  8. package/dist/index.d.ts +2 -4
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +1 -3
  11. package/dist/index.js.map +1 -1
  12. package/dist/realtime/channel-client.d.ts +0 -4
  13. package/dist/realtime/channel-client.d.ts.map +1 -1
  14. package/dist/realtime/channel-client.js +0 -8
  15. package/dist/realtime/channel-client.js.map +1 -1
  16. package/dist/realtime/map.d.ts +3 -5
  17. package/dist/realtime/map.d.ts.map +1 -1
  18. package/dist/realtime/map.js +3 -18
  19. package/dist/realtime/map.js.map +1 -1
  20. package/dist/realtime/useRealtimeChatMessages.d.ts +1 -4
  21. package/dist/realtime/useRealtimeChatMessages.d.ts.map +1 -1
  22. package/dist/realtime/useRealtimeChatMessages.js +1 -3
  23. package/dist/realtime/useRealtimeChatMessages.js.map +1 -1
  24. package/dist/realtime/wire.d.ts +6 -5
  25. package/dist/realtime/wire.d.ts.map +1 -1
  26. package/dist/realtime/wire.js.map +1 -1
  27. package/dist/types.d.ts +1 -52
  28. package/dist/types.d.ts.map +1 -1
  29. package/dist/types.js.map +1 -1
  30. package/dist/ui/ChatShell.d.ts +2 -2
  31. package/dist/ui/ChatShell.d.ts.map +1 -1
  32. package/dist/ui/ChatShell.js +6 -10
  33. package/dist/ui/ChatShell.js.map +1 -1
  34. package/dist/ui/Composer.d.ts +7 -6
  35. package/dist/ui/Composer.d.ts.map +1 -1
  36. package/dist/ui/Composer.js +2 -18
  37. package/dist/ui/Composer.js.map +1 -1
  38. package/dist/ui/MessageItemDefault.d.ts.map +1 -1
  39. package/dist/ui/MessageItemDefault.js +10 -13
  40. package/dist/ui/MessageItemDefault.js.map +1 -1
  41. package/dist/ui/MessageText.d.ts +19 -0
  42. package/dist/ui/MessageText.d.ts.map +1 -0
  43. package/dist/ui/MessageText.js +7 -0
  44. package/dist/ui/MessageText.js.map +1 -0
  45. package/package.json +5 -5
  46. package/src/styles/chat.css +5 -37
  47. package/dist/mentions/MentionAutocomplete.d.ts +0 -22
  48. package/dist/mentions/MentionAutocomplete.d.ts.map +0 -1
  49. package/dist/mentions/MentionAutocomplete.js +0 -21
  50. package/dist/mentions/MentionAutocomplete.js.map +0 -1
  51. package/dist/mentions/MessageText.d.ts +0 -27
  52. package/dist/mentions/MessageText.d.ts.map +0 -1
  53. package/dist/mentions/MessageText.js +0 -20
  54. package/dist/mentions/MessageText.js.map +0 -1
  55. package/dist/mentions/use-mention-autocomplete.d.ts +0 -82
  56. package/dist/mentions/use-mention-autocomplete.d.ts.map +0 -1
  57. package/dist/mentions/use-mention-autocomplete.js +0 -323
  58. package/dist/mentions/use-mention-autocomplete.js.map +0 -1
@@ -25,8 +25,6 @@ export function useRealtimeChatMessages(client) {
25
25
  const set = new Set();
26
26
  for (const m of state.messages) {
27
27
  set.add(m.senderId);
28
- if (m.replyTo?.senderId)
29
- set.add(m.replyTo.senderId);
30
28
  }
31
29
  return Array.from(set);
32
30
  }, [state.messages]);
@@ -37,7 +35,7 @@ export function useRealtimeChatMessages(client) {
37
35
  const fetchOlder = React.useCallback(async () => {
38
36
  client.channel.fetchOlder();
39
37
  }, [client]);
40
- const send = React.useCallback((text, replyTo) => client.channel.send({ clientMessageId: makeClientMessageId(), text, replyTo: replyTo ?? null }), [client]);
38
+ const send = React.useCallback((text) => client.channel.send({ clientMessageId: makeClientMessageId(), text }), [client]);
41
39
  const retrySend = React.useCallback((clientMessageId) => client.channel.retrySend(clientMessageId), [client]);
42
40
  const readAck = React.useCallback((readSeq, focused) => client.channel.readAck(readSeq, focused), [client]);
43
41
  const signalTyping = React.useCallback(() => client.channel.typing(), [client]);
@@ -1 +1 @@
1
- {"version":3,"file":"useRealtimeChatMessages.js","sourceRoot":"","sources":["../../src/realtime/useRealtimeChatMessages.ts"],"names":[],"mappings":"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","sourcesContent":["\"use client\";\n\n// React hook for the REALTIME transport, returning the SAME shape as\n// `useChatMessages` (the firestore path) so `ChatShell` is transport-agnostic.\n// It SUBSCRIBES to a `RealtimeChatClient`'s observable state. It does NOT own the\n// socket lifecycle: the caller (the app hook that CREATES the client, e.g.\n// `useRealtimeChannelClient`) is the single connect/close owner (C-B7) — connecting\n// here too opened a second WebSocket. The client instance is owned by the caller\n// (passed via config) so its identity is the subscription key — a new client for a\n// new uid replaces the old one (Contract A auth-user-switch teardown happens in the\n// owning hook).\n\nimport * as React from \"react\";\nimport type { ChatMessageV1 } from \"@ttt-productions/chat-core\";\nimport type { RealtimeChatClient } from \"./transport.js\";\nimport type { ChannelClientState } from \"./channel-client.js\";\nimport { useOptionalChatNameResolver } from \"../context/ChatNameResolverContext.js\";\n\nexport type UseRealtimeChatMessagesResult = {\n allowed: boolean;\n isInitialLoading: boolean;\n messages: ChatMessageV1[];\n fetchOlder: () => Promise<void>;\n hasOlder: boolean;\n isFetchingOlder: boolean;\n /** Realtime extras (not present on the firestore result) — optional for the UI. */\n status: ChannelClientState[\"status\"];\n /** The last structured error code the DO sent (e.g. a close reason). Drives the\n * disconnected-banner detail (R13). */\n lastErrorCode: string | null;\n typing: string[];\n presence: string[];\n /** Returns false when the socket was closed and nothing was sent (C-B8) — the caller keeps the composer text. */\n send: (text: string, replyTo?: { messageSeq: number; preview: string } | null) => boolean;\n /** Retry a failed/un-acked send by its ORIGINAL clientMessageId (read from the\n * failed row's `meta.clientMessageId`) — the transport re-queues the tracked\n * pending send with the SAME id so the DO's send-idempotency dedups any copy\n * that already landed. Returns false for an unknown id (already acked/\n * reconciled). ChatShell wires this to MessageItemDefault's failed-bubble\n * retry affordance by default. */\n retrySend: (clientMessageId: string) => boolean;\n /** Returns whether the ack frame was sent (false on a closed socket) so the caller advances its local cursor only on success (M2). */\n readAck: (readSeq: number, focused: boolean) => boolean;\n signalTyping: () => void;\n /** Watched-only presence (R14): the UI subscribes ONLY while a member-list / online\n * indicator is open, and unsubscribes when it closes, so an unwatched channel runs\n * zero presence alarms. `presence` stays empty until subscribed. */\n presenceSubscribe: () => void;\n presenceUnsubscribe: () => void;\n};\n\nexport function useRealtimeChatMessages(client: RealtimeChatClient): UseRealtimeChatMessagesResult {\n const [state, setState] = React.useState<ChannelClientState>(() => client.getState());\n\n // Subscribe only; the owning hook drives connect/close (single lifecycle owner, C-B7).\n // Re-subscribes on client identity change (auth-user switch).\n React.useEffect(() => {\n const unsub = client.subscribe(setState);\n setState(client.getState());\n return unsub;\n }, [client]);\n\n // Pre-warm the name cache for visible senders (parity with the firestore hook).\n const resolverCtx = useOptionalChatNameResolver();\n const senderIds = React.useMemo(() => {\n const set = new Set<string>();\n for (const m of state.messages) {\n set.add(m.senderId);\n if (m.replyTo?.senderId) set.add(m.replyTo.senderId);\n }\n return Array.from(set);\n }, [state.messages]);\n\n React.useEffect(() => {\n if (resolverCtx?.prewarm && senderIds.length > 0) resolverCtx.prewarm(senderIds);\n }, [resolverCtx, senderIds]);\n\n const fetchOlder = React.useCallback(async () => {\n client.channel.fetchOlder();\n }, [client]);\n\n const send = React.useCallback(\n (text: string, replyTo?: { messageSeq: number; preview: string } | null): boolean =>\n client.channel.send({ clientMessageId: makeClientMessageId(), text, replyTo: replyTo ?? null }),\n [client],\n );\n\n const retrySend = React.useCallback(\n (clientMessageId: string): boolean => client.channel.retrySend(clientMessageId),\n [client],\n );\n\n const readAck = React.useCallback(\n (readSeq: number, focused: boolean) => client.channel.readAck(readSeq, focused),\n [client],\n );\n\n const signalTyping = React.useCallback(() => client.channel.typing(), [client]);\n const presenceSubscribe = React.useCallback(() => client.channel.presenceSubscribe(), [client]);\n const presenceUnsubscribe = React.useCallback(() => client.channel.presenceUnsubscribe(), [client]);\n\n return {\n // On the realtime transport DATA ACCESS is enforced by the DO grant, so the hook\n // is \"allowed\" client-side UNLESS the grant provider surfaced a TERMINAL access\n // denial (ChatAccessDeniedError → stable 'access-denied' code). That single case\n // maps to allowed:false so ChatShell renders its no-access surface instead of an\n // eternal loader; a transient close / 4403-revoke keeps allowed:true and rides the\n // reconnect/disconnected banner. Otherwise accessMode is presentation-only here.\n allowed: state.lastErrorCode !== \"access-denied\",\n // Derive initial loading from authoritative-data completion, NOT connection status:\n // a socket can be 'open' with no snapshot yet, and a post-load reconnect must not\n // fall back into the opening state. `hasLoadedInitialData` encodes the full table.\n isInitialLoading: !state.hasLoadedInitialData,\n messages: state.messages,\n fetchOlder,\n hasOlder: state.hasOlder,\n isFetchingOlder: state.isFetchingOlder,\n status: state.status,\n lastErrorCode: state.lastErrorCode,\n typing: state.typing,\n presence: state.presence,\n send,\n retrySend,\n readAck,\n signalTyping,\n presenceSubscribe,\n presenceUnsubscribe,\n };\n}\n\n/** A client message id for optimistic-send idempotency (SQLite unique (senderUid, clientMessageId)). */\nfunction makeClientMessageId(): string {\n const c = (globalThis as { crypto?: { randomUUID?: () => string } }).crypto;\n if (c?.randomUUID) return c.randomUUID();\n return `cmid-${Date.now()}-${Math.random().toString(36).slice(2)}`;\n}\n"]}
1
+ {"version":3,"file":"useRealtimeChatMessages.js","sourceRoot":"","sources":["../../src/realtime/useRealtimeChatMessages.ts"],"names":[],"mappings":"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","sourcesContent":["\"use client\";\n\n// React hook for the REALTIME transport, returning the SAME shape as\n// `useChatMessages` (the firestore path) so `ChatShell` is transport-agnostic.\n// It SUBSCRIBES to a `RealtimeChatClient`'s observable state. It does NOT own the\n// socket lifecycle: the caller (the app hook that CREATES the client, e.g.\n// `useRealtimeChannelClient`) is the single connect/close owner (C-B7) — connecting\n// here too opened a second WebSocket. The client instance is owned by the caller\n// (passed via config) so its identity is the subscription key — a new client for a\n// new uid replaces the old one (Contract A auth-user-switch teardown happens in the\n// owning hook).\n\nimport * as React from \"react\";\nimport type { ChatMessageV1 } from \"@ttt-productions/chat-core\";\nimport type { RealtimeChatClient } from \"./transport.js\";\nimport type { ChannelClientState } from \"./channel-client.js\";\nimport { useOptionalChatNameResolver } from \"../context/ChatNameResolverContext.js\";\n\nexport type UseRealtimeChatMessagesResult = {\n allowed: boolean;\n isInitialLoading: boolean;\n messages: ChatMessageV1[];\n fetchOlder: () => Promise<void>;\n hasOlder: boolean;\n isFetchingOlder: boolean;\n /** Realtime extras (not present on the firestore result) — optional for the UI. */\n status: ChannelClientState[\"status\"];\n /** The last structured error code the DO sent (e.g. a close reason). Drives the\n * disconnected-banner detail (R13). */\n lastErrorCode: string | null;\n typing: string[];\n presence: string[];\n /** Returns false when the socket was closed and nothing was sent (C-B8) — the caller keeps the composer text. */\n send: (text: string) => boolean;\n /** Retry a failed/un-acked send by its ORIGINAL clientMessageId (read from the\n * failed row's `meta.clientMessageId`) — the transport re-queues the tracked\n * pending send with the SAME id so the DO's send-idempotency dedups any copy\n * that already landed. Returns false for an unknown id (already acked/\n * reconciled). ChatShell wires this to MessageItemDefault's failed-bubble\n * retry affordance by default. */\n retrySend: (clientMessageId: string) => boolean;\n /** Returns whether the ack frame was sent (false on a closed socket) so the caller advances its local cursor only on success (M2). */\n readAck: (readSeq: number, focused: boolean) => boolean;\n signalTyping: () => void;\n /** Watched-only presence (R14): the UI subscribes ONLY while a member-list / online\n * indicator is open, and unsubscribes when it closes, so an unwatched channel runs\n * zero presence alarms. `presence` stays empty until subscribed. */\n presenceSubscribe: () => void;\n presenceUnsubscribe: () => void;\n};\n\nexport function useRealtimeChatMessages(client: RealtimeChatClient): UseRealtimeChatMessagesResult {\n const [state, setState] = React.useState<ChannelClientState>(() => client.getState());\n\n // Subscribe only; the owning hook drives connect/close (single lifecycle owner, C-B7).\n // Re-subscribes on client identity change (auth-user switch).\n React.useEffect(() => {\n const unsub = client.subscribe(setState);\n setState(client.getState());\n return unsub;\n }, [client]);\n\n // Pre-warm the name cache for visible senders (parity with the firestore hook).\n const resolverCtx = useOptionalChatNameResolver();\n const senderIds = React.useMemo(() => {\n const set = new Set<string>();\n for (const m of state.messages) {\n set.add(m.senderId);\n }\n return Array.from(set);\n }, [state.messages]);\n\n React.useEffect(() => {\n if (resolverCtx?.prewarm && senderIds.length > 0) resolverCtx.prewarm(senderIds);\n }, [resolverCtx, senderIds]);\n\n const fetchOlder = React.useCallback(async () => {\n client.channel.fetchOlder();\n }, [client]);\n\n const send = React.useCallback(\n (text: string): boolean =>\n client.channel.send({ clientMessageId: makeClientMessageId(), text }),\n [client],\n );\n\n const retrySend = React.useCallback(\n (clientMessageId: string): boolean => client.channel.retrySend(clientMessageId),\n [client],\n );\n\n const readAck = React.useCallback(\n (readSeq: number, focused: boolean) => client.channel.readAck(readSeq, focused),\n [client],\n );\n\n const signalTyping = React.useCallback(() => client.channel.typing(), [client]);\n const presenceSubscribe = React.useCallback(() => client.channel.presenceSubscribe(), [client]);\n const presenceUnsubscribe = React.useCallback(() => client.channel.presenceUnsubscribe(), [client]);\n\n return {\n // On the realtime transport DATA ACCESS is enforced by the DO grant, so the hook\n // is \"allowed\" client-side UNLESS the grant provider surfaced a TERMINAL access\n // denial (ChatAccessDeniedError → stable 'access-denied' code). That single case\n // maps to allowed:false so ChatShell renders its no-access surface instead of an\n // eternal loader; a transient close / 4403-revoke keeps allowed:true and rides the\n // reconnect/disconnected banner. Otherwise accessMode is presentation-only here.\n allowed: state.lastErrorCode !== \"access-denied\",\n // Derive initial loading from authoritative-data completion, NOT connection status:\n // a socket can be 'open' with no snapshot yet, and a post-load reconnect must not\n // fall back into the opening state. `hasLoadedInitialData` encodes the full table.\n isInitialLoading: !state.hasLoadedInitialData,\n messages: state.messages,\n fetchOlder,\n hasOlder: state.hasOlder,\n isFetchingOlder: state.isFetchingOlder,\n status: state.status,\n lastErrorCode: state.lastErrorCode,\n typing: state.typing,\n presence: state.presence,\n send,\n retrySend,\n readAck,\n signalTyping,\n presenceSubscribe,\n presenceUnsubscribe,\n };\n}\n\n/** A client message id for optimistic-send idempotency (SQLite unique (senderUid, clientMessageId)). */\nfunction makeClientMessageId(): string {\n const c = (globalThis as { crypto?: { randomUUID?: () => string } }).crypto;\n if (c?.randomUUID) return c.randomUUID();\n return `cmid-${Date.now()}-${Math.random().toString(36).slice(2)}`;\n}\n"]}
@@ -11,18 +11,19 @@ import type { ChannelRefTuple, ChatSendRejectedPayload } from '@ttt-productions/
11
11
  export type RevisionKind = 'delete' | 'moderate' | 'edit' | 'restore';
12
12
  /**
13
13
  * A message row as the Channel DO stores + broadcasts it (the worker runtime's
14
- * message row). `replyTo` arrives as a JSON STRING (or null) — the DO stores it
15
- * stringified and does not re-parse before broadcast. The adapter mapping in
16
- * `map.ts` parses it into the UI `ChatMessageV1` shape.
14
+ * message row), declared as the fields the client actually CONSUMES. The adapter
15
+ * mapping in `map.ts` turns it into the UI `ChatMessageV1` shape.
17
16
  *
18
- * The row is TEXT-only: there is no attachment lifecycle on a chat message.
17
+ * The row is TEXT-only: there is no attachment lifecycle on a chat message, and
18
+ * no reply pointer — chat has no reply-authoring affordance, so the client
19
+ * neither sends nor reads one (DJ ruling 2026-07-29). A DO that still broadcasts
20
+ * a legacy stringified `replyTo` key is simply ignored here.
19
21
  */
20
22
  export interface WireMessageRow {
21
23
  seq: number;
22
24
  senderUid: string;
23
25
  clientMessageId: string;
24
26
  text: string;
25
- replyTo: string | null;
26
27
  createdAt: number;
27
28
  epoch: number;
28
29
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"wire.d.ts","sourceRoot":"","sources":["../../src/realtime/wire.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"wire.d.ts","sourceRoot":"","sources":["../../src/realtime/wire.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"wire.js","sourceRoot":"","sources":["../../src/realtime/wire.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,mFAAmF;AACnF,8EAA8E;AAC9E,gFAAgF;AAChF,+EAA+E;AAC/E,6EAA6E;AAC7E,0DAA0D;AAC1D,EAAE;AACF,2EAA2E;AAC3E,sEAAsE;AACtE,iFAAiF;AACjF,gFAAgF;AAChF,8EAA8E;AAE9E,4EAA4E;AAC5E,+EAA+E;AAC/E,2EAA2E;AAC3E,OAAO,EACL,gBAAgB,EAChB,iBAAiB,EACjB,gBAAgB,EAChB,YAAY,IAAI,YAAY,EAC5B,YAAY,IAAI,YAAY;AAC5B,2EAA2E;AAC3E,6BAA6B,EAC7B,yBAAyB,EACzB,6BAA6B,GAC9B,MAAM,+BAA+B,CAAC;AAQvC,OAAO,EAAE,iBAAiB,EAAE,MAAM,+BAA+B,CAAC;AA0HlE,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,CAAC,GAAW;IACpC,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,CAAC,CAA0C;IACxE,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, channel-ref tuple, grant scope, client-agreed\n// 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\n// Re-export the canonical wire contract so this package's public surface is\n// unchanged. `CLIENT_FRAME`/`SERVER_FRAME` are this package's historical names\n// for the frame-kind maps (`CLIENT_KINDS`/`SERVER_KINDS` in the contract).\nexport {\n CHAT_SUBPROTOCOL,\n CHAT_WIRE_VERSION,\n CHAT_CLOSE_CODES,\n CLIENT_KINDS as CLIENT_FRAME,\n SERVER_KINDS as SERVER_FRAME,\n // Correlated send-rejection contract (SERVER_FRAME.SEND_REJECTED payload).\n ChatSendRejectedPayloadSchema,\n CHAT_SEND_REJECTION_CODES,\n CHAT_SEND_REJECTION_RETRYABLE,\n} from '@ttt-productions/chat-schemas';\nexport type {\n ChatCloseCode,\n ChannelRefTuple,\n ChatSendRejectedPayload,\n ChatSendRejectionCode,\n} from '@ttt-productions/chat-schemas';\n\nimport { CHAT_WIRE_VERSION } from '@ttt-productions/chat-schemas';\nimport type { ChannelRefTuple, ChatSendRejectedPayload } from '@ttt-productions/chat-schemas';\n\n// ---- raw wire row shapes (what the DO serializes; opaque-but-typed here) ----\n\n/**\n * The moderation revision `kind` (the worker runtime's revision input + the\n * `revisions.kind` column). The EFFECTIVE state of a message is its\n * MAX-`messageRevision` row's kind, so a `restore` (a later revision) supersedes\n * an earlier `moderate`/`delete`. `moderate`/`delete` blank the original text;\n * `edit` overlays new text; `restore` reverts to the original.\n */\nexport type RevisionKind = 'delete' | 'moderate' | 'edit' | 'restore';\n\n/**\n * A message row as the Channel DO stores + broadcasts it (the worker runtime's\n * message row). `replyTo` arrives as a JSON STRING (or null) — the DO stores it\n * stringified and does not re-parse before broadcast. The adapter mapping in\n * `map.ts` parses it into the UI `ChatMessageV1` shape.\n *\n * The row is TEXT-only: there is no attachment lifecycle on a chat message.\n */\nexport interface WireMessageRow {\n seq: number;\n senderUid: string;\n clientMessageId: string;\n text: string;\n replyTo: string | null;\n createdAt: number;\n epoch: number;\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 typed channel-ref tuple — the OPENABLE identity for the inbox view (the\n * `channelRef` is an opaque DO-id dedup key). Null/absent on legacy or pre-tuple\n * rows; the consumer falls back to hiding the open action for those entries.\n */\n ref?: ChannelRefTuple | 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 // A CORRELATED send rejection (SERVER_FRAME.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 | { type: 'revision'; payload: { messageSeq: number; kind: RevisionKind; messageRevision: number } };\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(raw: string): { 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(p: WireChannelSnapshot | WireInboxSnapshot): 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,8EAA8E;AAC9E,mFAAmF;AACnF,8EAA8E;AAC9E,gFAAgF;AAChF,+EAA+E;AAC/E,6EAA6E;AAC7E,0DAA0D;AAC1D,EAAE;AACF,2EAA2E;AAC3E,sEAAsE;AACtE,iFAAiF;AACjF,gFAAgF;AAChF,8EAA8E;AAE9E,4EAA4E;AAC5E,+EAA+E;AAC/E,2EAA2E;AAC3E,OAAO,EACL,gBAAgB,EAChB,iBAAiB,EACjB,gBAAgB,EAChB,YAAY,IAAI,YAAY,EAC5B,YAAY,IAAI,YAAY;AAC5B,2EAA2E;AAC3E,6BAA6B,EAC7B,yBAAyB,EACzB,6BAA6B,GAC9B,MAAM,+BAA+B,CAAC;AAQvC,OAAO,EAAE,iBAAiB,EAAE,MAAM,+BAA+B,CAAC;AA2HlE,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,CAAC,GAAW;IACpC,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,CAAC,CAA0C;IACxE,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, channel-ref tuple, grant scope, client-agreed\n// 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\n// Re-export the canonical wire contract so this package's public surface is\n// unchanged. `CLIENT_FRAME`/`SERVER_FRAME` are this package's historical names\n// for the frame-kind maps (`CLIENT_KINDS`/`SERVER_KINDS` in the contract).\nexport {\n CHAT_SUBPROTOCOL,\n CHAT_WIRE_VERSION,\n CHAT_CLOSE_CODES,\n CLIENT_KINDS as CLIENT_FRAME,\n SERVER_KINDS as SERVER_FRAME,\n // Correlated send-rejection contract (SERVER_FRAME.SEND_REJECTED payload).\n ChatSendRejectedPayloadSchema,\n CHAT_SEND_REJECTION_CODES,\n CHAT_SEND_REJECTION_RETRYABLE,\n} from '@ttt-productions/chat-schemas';\nexport type {\n ChatCloseCode,\n ChannelRefTuple,\n ChatSendRejectedPayload,\n ChatSendRejectionCode,\n} from '@ttt-productions/chat-schemas';\n\nimport { CHAT_WIRE_VERSION } from '@ttt-productions/chat-schemas';\nimport type { ChannelRefTuple, ChatSendRejectedPayload } from '@ttt-productions/chat-schemas';\n\n// ---- raw wire row shapes (what the DO serializes; opaque-but-typed here) ----\n\n/**\n * The moderation revision `kind` (the worker runtime's revision input + the\n * `revisions.kind` column). The EFFECTIVE state of a message is its\n * MAX-`messageRevision` row's kind, so a `restore` (a later revision) supersedes\n * an earlier `moderate`/`delete`. `moderate`/`delete` blank the original text;\n * `edit` overlays new text; `restore` reverts to the original.\n */\nexport type RevisionKind = 'delete' | 'moderate' | 'edit' | 'restore';\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 /**\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 typed channel-ref tuple — the OPENABLE identity for the inbox view (the\n * `channelRef` is an opaque DO-id dedup key). Null/absent on legacy or pre-tuple\n * rows; the consumer falls back to hiding the open action for those entries.\n */\n ref?: ChannelRefTuple | 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 // A CORRELATED send rejection (SERVER_FRAME.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 | { type: 'revision'; payload: { messageSeq: number; kind: RevisionKind; messageRevision: number } };\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(raw: string): { 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(p: WireChannelSnapshot | WireInboxSnapshot): p is WireInboxSnapshot {\n return Array.isArray((p as WireInboxSnapshot).registry);\n}\n"]}
package/dist/types.d.ts CHANGED
@@ -1,50 +1,5 @@
1
1
  import type { ReactNode } from "react";
2
- import type { ChatAccessMode, ChatMessageV1, MentionProvider, MentionRef, RecentMentionsAdapter } from "@ttt-productions/chat-core";
3
- /** Custom renderer for a single autocomplete row / mention result. */
4
- export type MentionResultRenderer<TKind extends string = string> = (ref: MentionRef<TKind>) => ReactNode;
5
- /**
6
- * A pure {@link MentionProvider} augmented with optional React row rendering.
7
- * The autocomplete UI consumes this; the pure provider contract (search) lives
8
- * in @ttt-productions/chat-core.
9
- */
10
- export type RenderableMentionProvider<TKind extends string = string, TContext = unknown> = MentionProvider<TKind, TContext> & {
11
- /**
12
- * Optional custom row UI. When omitted, the autocomplete renders the
13
- * `displayText` plain.
14
- */
15
- renderResult?: MentionResultRenderer<TKind>;
16
- };
17
- /**
18
- * Pluggable mention system config. When attached to ChatCoreConfig, the
19
- * composer enables `@`-trigger autocomplete and the message renderer parses
20
- * mention tokens out of `text`.
21
- *
22
- * Generic over `TKind` and `TContext` is intentionally NOT exposed here at the
23
- * type level — `ChatCoreConfig` would have to become generic too, which
24
- * cascades. Instead this type accepts permissive `string`-keyed providers;
25
- * consumers binding stricter kind unions narrow at the call site via their
26
- * provider definitions.
27
- */
28
- export type ChatMentionConfig = {
29
- /** Providers in display order. */
30
- providers: RenderableMentionProvider<string, unknown>[];
31
- /** Context object forwarded to every provider's `search`. */
32
- context: unknown;
33
- /** Optional recent-mentions adapter. */
34
- recent?: RecentMentionsAdapter<string>;
35
- /** Trigger character. Defaults to `'@'`. */
36
- trigger?: string;
37
- /** Minimum query length before search fires. Default: 0. */
38
- minQueryLength?: number;
39
- /** Search debounce window in ms. Default: 200. */
40
- searchDebounceMs?: number;
41
- /**
42
- * Optional custom renderer for mentions inside rendered message text.
43
- * When omitted, the default chip (`<span class="chat-mention-chip">`) is
44
- * used. Receives the resolved MentionRef.
45
- */
46
- renderMention?: (ref: MentionRef) => ReactNode;
47
- };
2
+ import type { ChatAccessMode, ChatMessageV1 } from "@ttt-productions/chat-core";
48
3
  /**
49
4
  * Which transport backs a chat thread.
50
5
  * - `firestore` — the current Firestore newest-window + cursor path (default).
@@ -99,12 +54,6 @@ export type ChatCoreConfig = {
99
54
  threadAllowedUserIds?: string[];
100
55
  createdAtField?: string;
101
56
  pageSize?: number;
102
- /**
103
- * Optional mention system configuration. When present, the composer enables
104
- * `@`-trigger autocomplete using the supplied providers and renders mention
105
- * chips in messages. Each provider corresponds to one mention kind.
106
- */
107
- mentionConfig?: ChatMentionConfig;
108
57
  };
109
58
  export type MessageRenderer = (m: ChatMessageV1) => ReactNode;
110
59
  export type MessageRendererRegistry = Record<string, MessageRenderer>;
@@ -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,EACV,cAAc,EACd,aAAa,EACb,eAAe,EACf,UAAU,EACV,qBAAqB,EACtB,MAAM,4BAA4B,CAAC;AAMpC,sEAAsE;AACtE,MAAM,MAAM,qBAAqB,CAAC,KAAK,SAAS,MAAM,GAAG,MAAM,IAAI,CACjE,GAAG,EAAE,UAAU,CAAC,KAAK,CAAC,KACnB,SAAS,CAAC;AAEf;;;;GAIG;AACH,MAAM,MAAM,yBAAyB,CACnC,KAAK,SAAS,MAAM,GAAG,MAAM,EAC7B,QAAQ,GAAG,OAAO,IAChB,eAAe,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG;IACrC;;;OAGG;IACH,YAAY,CAAC,EAAE,qBAAqB,CAAC,KAAK,CAAC,CAAC;CAC7C,CAAC;AAUF;;;;;;;;;;GAUG;AACH,MAAM,MAAM,iBAAiB,GAAG;IAC9B,kCAAkC;IAClC,SAAS,EAAE,yBAAyB,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC;IACxD,6DAA6D;IAC7D,OAAO,EAAE,OAAO,CAAC;IACjB,wCAAwC;IACxC,MAAM,CAAC,EAAE,qBAAqB,CAAC,MAAM,CAAC,CAAC;IACvC,4CAA4C;IAC5C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,4DAA4D;IAC5D,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,kDAAkD;IAClD,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B;;;;OAIG;IACH,aAAa,CAAC,EAAE,CAAC,GAAG,EAAE,UAAU,KAAK,SAAS,CAAC;CAChD,CAAC;AAMF;;;;;;;GAOG;AACH,MAAM,MAAM,iBAAiB,GAAG,WAAW,GAAG,UAAU,CAAC;AAEzD;;;;;;;;GAQG;AACH,MAAM,WAAW,2BAA2B;IAC1C,wFAAwF;IACxF,UAAU,EAAE,OAAO,CAAC;IACpB;;;;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;;;;OAIG;IACH,UAAU,EAAE,cAAc,CAAC;IAC3B;;OAEG;IACH,oBAAoB,CAAC,EAAE,MAAM,EAAE,CAAC;IAChC,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;OAIG;IACH,aAAa,CAAC,EAAE,iBAAiB,CAAC;CACnC,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":"AAIA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AACvC,OAAO,KAAK,EACV,cAAc,EACd,aAAa,EACd,MAAM,4BAA4B,CAAC;AAWpC;;;;;;;GAOG;AACH,MAAM,MAAM,iBAAiB,GAAG,WAAW,GAAG,UAAU,CAAC;AAEzD;;;;;;;;GAQG;AACH,MAAM,WAAW,2BAA2B;IAC1C,wFAAwF;IACxF,UAAU,EAAE,OAAO,CAAC;IACpB;;;;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;;;;OAIG;IACH,UAAU,EAAE,cAAc,CAAC;IAC3B;;OAEG;IACH,oBAAoB,CAAC,EAAE,MAAM,EAAE,CAAC;IAChC,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,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"}
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 {\n ChatAccessMode,\n ChatMessageV1,\n MentionProvider,\n MentionRef,\n RecentMentionsAdapter,\n} from \"@ttt-productions/chat-core\";\n\n// ============================================\n// MENTION RENDER TYPES (React-coupled)\n// ============================================\n\n/** Custom renderer for a single autocomplete row / mention result. */\nexport type MentionResultRenderer<TKind extends string = string> = (\n ref: MentionRef<TKind>,\n) => ReactNode;\n\n/**\n * A pure {@link MentionProvider} augmented with optional React row rendering.\n * The autocomplete UI consumes this; the pure provider contract (search) lives\n * in @ttt-productions/chat-core.\n */\nexport type RenderableMentionProvider<\n TKind extends string = string,\n TContext = unknown,\n> = MentionProvider<TKind, TContext> & {\n /**\n * Optional custom row UI. When omitted, the autocomplete renders the\n * `displayText` plain.\n */\n renderResult?: MentionResultRenderer<TKind>;\n};\n\n// ============================================\n// MENTION SYSTEM CONFIG (passed through ChatShell → Composer)\n// ============================================\n//\n// Chat carries TEXT only. There is no upload adapter or attachment config here:\n// a conversation's files are owned by the consuming app's Conversation Files\n// surface, which runs the canonical upload pipeline outside the chat timeline.\n\n/**\n * Pluggable mention system config. When attached to ChatCoreConfig, the\n * composer enables `@`-trigger autocomplete and the message renderer parses\n * mention tokens out of `text`.\n *\n * Generic over `TKind` and `TContext` is intentionally NOT exposed here at the\n * type level — `ChatCoreConfig` would have to become generic too, which\n * cascades. Instead this type accepts permissive `string`-keyed providers;\n * consumers binding stricter kind unions narrow at the call site via their\n * provider definitions.\n */\nexport type ChatMentionConfig = {\n /** Providers in display order. */\n providers: RenderableMentionProvider<string, unknown>[];\n /** Context object forwarded to every provider's `search`. */\n context: unknown;\n /** Optional recent-mentions adapter. */\n recent?: RecentMentionsAdapter<string>;\n /** Trigger character. Defaults to `'@'`. */\n trigger?: string;\n /** Minimum query length before search fires. Default: 0. */\n minQueryLength?: number;\n /** Search debounce window in ms. Default: 200. */\n searchDebounceMs?: number;\n /**\n * Optional custom renderer for mentions inside rendered message text.\n * When omitted, the default chip (`<span class=\"chat-mention-chip\">`) is\n * used. Receives the resolved MentionRef.\n */\n renderMention?: (ref: MentionRef) => ReactNode;\n};\n\n// ============================================\n// CONFIG\n// ============================================\n\n/**\n * Which transport backs a chat thread.\n * - `firestore` — the current Firestore newest-window + cursor path (default).\n * - `realtime` — the Cloudflare Durable Object socket (chat-edge-rebuild). On\n * this transport DATA ACCESS is enforced by the DO grant, so `accessMode`\n * becomes PRESENTATION-ONLY (UI affordances like read-only), never a\n * data-access gate.\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. `channelRef`/`client` stay structurally typed (`unknown` at the\n * config seam) so `ChatCoreConfig` does not pull the realtime module's types into\n * every firestore consumer — the realtime hook narrows them at the call site.\n */\nexport interface ChatRealtimeTransportConfig {\n /** Opaque channel reference the socket is scoped to (guild channel / invite thread). */\n channelRef: unknown;\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 * The Firestore data-access gate (see ChatAccessMode docs in\n * @ttt-productions/chat-core). REQUIRED. On the `realtime` transport this is\n * PRESENTATION-ONLY — the DO grant is the real data-access authority.\n */\n accessMode: ChatAccessMode;\n /**\n * Required iff accessMode === \"explicit-allowlist\". Ignored otherwise.\n */\n threadAllowedUserIds?: string[];\n createdAtField?: string; // default: \"createdAt\"\n pageSize?: number;\n /**\n * Optional mention system configuration. When present, the composer enables\n * `@`-trigger autocomplete using the supplied providers and renders mention\n * chips in messages. Each provider corresponds to one mention kind.\n */\n mentionConfig?: ChatMentionConfig;\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":"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 {\n ChatAccessMode,\n ChatMessageV1,\n} from \"@ttt-productions/chat-core\";\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 current Firestore newest-window + cursor path (default).\n * - `realtime` — the Cloudflare Durable Object socket (chat-edge-rebuild). On\n * this transport DATA ACCESS is enforced by the DO grant, so `accessMode`\n * becomes PRESENTATION-ONLY (UI affordances like read-only), never a\n * data-access gate.\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. `channelRef`/`client` stay structurally typed (`unknown` at the\n * config seam) so `ChatCoreConfig` does not pull the realtime module's types into\n * every firestore consumer — the realtime hook narrows them at the call site.\n */\nexport interface ChatRealtimeTransportConfig {\n /** Opaque channel reference the socket is scoped to (guild channel / invite thread). */\n channelRef: unknown;\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 * The Firestore data-access gate (see ChatAccessMode docs in\n * @ttt-productions/chat-core). REQUIRED. On the `realtime` transport this is\n * PRESENTATION-ONLY — the DO grant is the real data-access authority.\n */\n accessMode: ChatAccessMode;\n /**\n * Required iff accessMode === \"explicit-allowlist\". Ignored otherwise.\n */\n threadAllowedUserIds?: string[];\n createdAtField?: string; // default: \"createdAt\"\n pageSize?: number;\n};\n\n// ============================================\n// RENDERING\n// ============================================\n\nexport type MessageRenderer = (m: ChatMessageV1) => ReactNode;\n\nexport type MessageRendererRegistry = Record<string, MessageRenderer>;\n"]}
@@ -1,10 +1,10 @@
1
1
  import * as React from "react";
2
- import type { ChatMessageV1, ModerationHandlers } from "@ttt-productions/chat-core";
2
+ import type { ModerationHandlers } from "@ttt-productions/chat-core";
3
3
  import type { ChatCoreConfig, MessageRendererRegistry } from "../types.js";
4
4
  export type ChatShellProps = {
5
5
  config: ChatCoreConfig;
6
6
  header?: React.ReactNode;
7
- onSend?: (text: string, replyTo?: ChatMessageV1["replyTo"]) => Promise<void>;
7
+ onSend?: (text: string) => Promise<void>;
8
8
  renderMessage?: (m: any) => React.ReactNode;
9
9
  messageRenderers?: MessageRendererRegistry;
10
10
  handlers?: ModerationHandlers;
@@ -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,EAAE,aAAa,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AACpF,OAAO,KAAK,EACV,cAAc,EACd,uBAAuB,EACxB,MAAM,aAAa,CAAC;AAWrB,MAAM,MAAM,cAAc,GAAG;IAC3B,MAAM,EAAE,cAAc,CAAC;IAGvB,MAAM,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAKzB,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,aAAa,CAAC,SAAS,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAG7E,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,EAAE,WAAW,EAAE,MAAM,KAAK,IAAI,CAAC;IAGhE,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAE3B;;;6FAGyF;IACzF,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;qDACiD;IACjD,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B,CAAC;AA0BF;;;;;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,EAAiB,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AACpF,OAAO,KAAK,EACV,cAAc,EACd,uBAAuB,EACxB,MAAM,aAAa,CAAC;AAWrB,MAAM,MAAM,cAAc,GAAG;IAC3B,MAAM,EAAE,cAAc,CAAC;IAGvB,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,EAAE,WAAW,EAAE,MAAM,KAAK,IAAI,CAAC;IAGhE,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAE3B;;;6FAGyF;IACzF,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;qDACiD;IACjD,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B,CAAC;AA0BF;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,cAAc,qBAK9C"}
@@ -24,9 +24,9 @@ export function ChatShell(props) {
24
24
  function FirestoreChatShell(props) {
25
25
  const { config, onSend } = props;
26
26
  const r = useChatMessages(config);
27
- const send = React.useCallback(async (text, replyTo) => {
27
+ const send = React.useCallback(async (text) => {
28
28
  if (onSend)
29
- await onSend(text, replyTo);
29
+ await onSend(text);
30
30
  }, [onSend]);
31
31
  return _jsx(ChatShellView, { ...props, resolved: { ...r, send } });
32
32
  }
@@ -37,18 +37,14 @@ function RealtimeChatShell(props) {
37
37
  throw new Error("[ChatShell] transport 'realtime' requires config.realtime.client (createRealtimeChatClient(...))");
38
38
  }
39
39
  const r = useRealtimeChatMessages(client);
40
- const send = React.useCallback(async (text, replyTo) => {
41
- // Map the UI replyTo (messageId-based) to the wire replyTo (seq + preview).
42
- const wireReply = replyTo && replyTo.messageId
43
- ? { messageSeq: Number(replyTo.messageId), preview: replyTo.messagePreview ?? "" }
44
- : null;
45
- const ok = r.send(text, Number.isFinite(wireReply?.messageSeq) ? wireReply : null);
40
+ const send = React.useCallback(async (text) => {
41
+ const ok = r.send(text);
46
42
  // C-B8: surface a closed-socket failure so the Composer keeps the user's text
47
43
  // instead of clearing it on a no-op send.
48
44
  if (!ok)
49
45
  throw new Error("chat-send-failed");
50
46
  if (onSend)
51
- await onSend(text, replyTo); // optional mirror
47
+ await onSend(text); // optional mirror
52
48
  }, [r, onSend]);
53
49
  return (_jsx(ChatShellView, { ...props, resolved: {
54
50
  allowed: r.allowed,
@@ -143,7 +139,7 @@ function ChatShellView(props) {
143
139
  return aboveMessages != null && aboveMessages !== false ? (_jsx("div", { className: "border-b", children: aboveMessages })) : null;
144
140
  })(), _jsx(CardContent, { className: contentClassName, children: _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(Loader2, { className: "h-5 w-5 animate-spin", "aria-hidden": "true" }), _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" }) }))] }));
145
141
  }
146
- 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() })), _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, handlers: handlers, onSenderClick: onSenderClick, onRetrySend: retrySend }) }), 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, mentionConfig: config.mentionConfig }) })] }))] }));
142
+ 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() })), _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, handlers: handlers, onSenderClick: onSenderClick, onRetrySend: retrySend }) }), 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 }) })] }))] }));
147
143
  }
148
144
  /**
149
145
  * R13 — connection-state banner. The realtime path sets `status`; once the first
@@ -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 { ChatMessageV1, ModerationHandlers } from \"@ttt-productions/chat-core\";\nimport type {\n ChatCoreConfig,\n MessageRendererRegistry,\n} from \"../types.js\";\nimport { Card, CardHeader, CardContent, CardFooter, Skeleton } from \"@ttt-productions/ui-core/react\";\nimport { KeyboardAvoidingView } from \"@ttt-productions/mobile-core/react\";\nimport { Loader2 } from \"lucide-react\";\nimport { useChatMessages } from \"../hooks/useChatMessages.js\";\nimport { useRealtimeChatMessages } from \"../realtime/useRealtimeChatMessages.js\";\nimport type { RealtimeChatClient } from \"../realtime/transport.js\";\nimport { MessageList } from \"./MessageList.js\";\nimport { Composer } from \"./Composer.js\";\nimport { ThreadActions } from \"./menus.js\";\n\nexport type ChatShellProps = {\n config: ChatCoreConfig;\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, replyTo?: ChatMessageV1[\"replyTo\"]) => 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, displayName: string) => void;\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 /** Advanced override for the scrollable region's class (forwarded to MessageList).\n * Usually unnecessary — prefer `fillHeight`. */\n scrollClassName?: string;\n};\n\n/** The resolved data a transport hook hands the presentational view. */\ntype ResolvedChat = {\n allowed: boolean;\n isInitialLoading: boolean;\n messages: ChatMessageV1[];\n fetchOlder: () => Promise<void>;\n hasOlder: boolean;\n isFetchingOlder: boolean;\n /** The send handler the Composer calls (socket send on realtime, prop on firestore). */\n send: (text: string, replyTo?: ChatMessageV1[\"replyTo\"]) => 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, replyTo?: ChatMessageV1[\"replyTo\"]) => {\n if (onSend) await onSend(text, replyTo);\n },\n [onSend],\n );\n return <ChatShellView {...props} resolved={{ ...r, send }} />;\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 send = React.useCallback(\n async (text: string, replyTo?: ChatMessageV1[\"replyTo\"]) => {\n // Map the UI replyTo (messageId-based) to the wire replyTo (seq + preview).\n const wireReply =\n replyTo && replyTo.messageId\n ? { messageSeq: Number(replyTo.messageId), preview: replyTo.messagePreview ?? \"\" }\n : null;\n const ok = r.send(text, Number.isFinite(wireReply?.messageSeq) ? wireReply : null);\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, replyTo); // optional mirror\n },\n [r, onSend],\n );\n return (\n <ChatShellView\n {...props}\n resolved={{\n allowed: r.allowed,\n isInitialLoading: r.isInitialLoading,\n messages: r.messages,\n fetchOlder: r.fetchOlder,\n hasOlder: r.hasOlder,\n isFetchingOlder: r.isFetchingOlder,\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 composerDisabled,\n fillHeight = false,\n scrollClassName,\n resolved,\n } = props;\n\n const { allowed, isInitialLoading, messages, fetchOlder, hasOlder, isFetchingOlder, send, retrySend, readAck, status, typing, signalTyping } = resolved;\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\" && (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) {\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 all still render, because a consumer that hangs critical controls off those\n // slots (e.g. the guild-invite binding-agreement Agree/Decline/stake-share controls)\n // must work whether or not realtime chat has connected yet (F4.1).\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 threadId={config.threadId} isAdmin={config.isAdmin} handlers={handlers} />\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 <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 <Loader2 className=\"h-5 w-5 animate-spin\" aria-hidden=\"true\" />\n <span>Opening chat…</span>\n </div>\n </CardContent>\n {renderBelowMessages && (\n <div className=\"border-t\">{renderBelowMessages()}</div>\n )}\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 threadId={config.threadId} isAdmin={config.isAdmin} handlers={handlers} />\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 && (\n <div className=\"border-b\">{renderAboveMessages()}</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 handlers={handlers}\n onSenderClick={onSenderClick}\n onRetrySend={retrySend}\n />\n </CardContent>\n\n {renderBelowMessages && (\n <div className=\"border-t\">{renderBelowMessages()}</div>\n )}\n\n {renderFooter ? (\n <CardFooter className=\"border-t\">\n {renderFooter()}\n </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 mentionConfig={config.mentionConfig}\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 { ChatMessageV1, ModerationHandlers } from \"@ttt-productions/chat-core\";\nimport type {\n ChatCoreConfig,\n MessageRendererRegistry,\n} from \"../types.js\";\nimport { Card, CardHeader, CardContent, CardFooter, Skeleton } from \"@ttt-productions/ui-core/react\";\nimport { KeyboardAvoidingView } from \"@ttt-productions/mobile-core/react\";\nimport { Loader2 } from \"lucide-react\";\nimport { useChatMessages } from \"../hooks/useChatMessages.js\";\nimport { useRealtimeChatMessages } from \"../realtime/useRealtimeChatMessages.js\";\nimport type { RealtimeChatClient } from \"../realtime/transport.js\";\nimport { MessageList } from \"./MessageList.js\";\nimport { Composer } from \"./Composer.js\";\nimport { ThreadActions } from \"./menus.js\";\n\nexport type ChatShellProps = {\n config: ChatCoreConfig;\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, displayName: string) => void;\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 /** Advanced override for the scrollable region's class (forwarded to MessageList).\n * Usually unnecessary — prefer `fillHeight`. */\n scrollClassName?: string;\n};\n\n/** The resolved data a transport hook hands the presentational view. */\ntype ResolvedChat = {\n allowed: boolean;\n isInitialLoading: boolean;\n messages: ChatMessageV1[];\n fetchOlder: () => Promise<void>;\n hasOlder: boolean;\n isFetchingOlder: boolean;\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 return <ChatShellView {...props} resolved={{ ...r, send }} />;\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 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 allowed: r.allowed,\n isInitialLoading: r.isInitialLoading,\n messages: r.messages,\n fetchOlder: r.fetchOlder,\n hasOlder: r.hasOlder,\n isFetchingOlder: r.isFetchingOlder,\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 composerDisabled,\n fillHeight = false,\n scrollClassName,\n resolved,\n } = props;\n\n const { allowed, isInitialLoading, messages, fetchOlder, hasOlder, isFetchingOlder, send, retrySend, readAck, status, typing, signalTyping } = resolved;\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\" && (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) {\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 all still render, because a consumer that hangs critical controls off those\n // slots (e.g. the guild-invite binding-agreement Agree/Decline/stake-share controls)\n // must work whether or not realtime chat has connected yet (F4.1).\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 threadId={config.threadId} isAdmin={config.isAdmin} handlers={handlers} />\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 <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 <Loader2 className=\"h-5 w-5 animate-spin\" aria-hidden=\"true\" />\n <span>Opening chat…</span>\n </div>\n </CardContent>\n {renderBelowMessages && (\n <div className=\"border-t\">{renderBelowMessages()}</div>\n )}\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 threadId={config.threadId} isAdmin={config.isAdmin} handlers={handlers} />\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 && (\n <div className=\"border-b\">{renderAboveMessages()}</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 handlers={handlers}\n onSenderClick={onSenderClick}\n onRetrySend={retrySend}\n />\n </CardContent>\n\n {renderBelowMessages && (\n <div className=\"border-t\">{renderBelowMessages()}</div>\n )}\n\n {renderFooter ? (\n <CardFooter className=\"border-t\">\n {renderFooter()}\n </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,12 +1,14 @@
1
1
  import * as React from "react";
2
- import type { ChatMessageV1 } from "@ttt-productions/chat-core";
3
2
  export type ComposerProps = {
4
3
  /**
5
- * Text send. The composer is TEXT-ONLY — files are shared through the
6
- * conversation's Conversation Files surface, never through the message
7
- * timeline, so there is no attachment path here.
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).
8
10
  */
9
- onSend: (text: string, replyTo?: ChatMessageV1["replyTo"]) => Promise<void>;
11
+ onSend: (text: string) => Promise<void>;
10
12
  /**
11
13
  * Signal the local user is typing (R14). Called as the user edits the textarea; the
12
14
  * realtime transport coalesces to ≤1 frame per ~2s, so calling per keystroke is safe.
@@ -16,7 +18,6 @@ export type ComposerProps = {
16
18
  disabled?: boolean;
17
19
  autoFocus?: boolean;
18
20
  placeholder?: string;
19
- mentionConfig?: import('../types.js').ChatMentionConfig;
20
21
  };
21
22
  export declare function Composer(props: ComposerProps): React.JSX.Element;
22
23
  //# sourceMappingURL=Composer.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"Composer.d.ts","sourceRoot":"","sources":["../../src/ui/Composer.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAK/B,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAQhE,MAAM,MAAM,aAAa,GAAG;IAC1B;;;;OAIG;IACH,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,aAAa,CAAC,SAAS,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAE5E;;;;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;IACrB,aAAa,CAAC,EAAE,OAAO,aAAa,EAAE,iBAAiB,CAAC;CACzD,CAAC;AAEF,wBAAgB,QAAQ,CAAC,KAAK,EAAE,aAAa,qBAuH5C"}
1
+ {"version":3,"file":"Composer.d.ts","sourceRoot":"","sources":["../../src/ui/Composer.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAU/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,qBA8F5C"}
@@ -5,8 +5,6 @@ import { Button, Textarea } from "@ttt-productions/ui-core/react";
5
5
  import { cn } from "@ttt-productions/ui-core";
6
6
  import { useOptionalLocalUploadGuard } from "@ttt-productions/upload-ui/react/guard";
7
7
  import { Loader2 } from "lucide-react";
8
- import { useMentionAutocomplete } from "../mentions/use-mention-autocomplete.js";
9
- import { MentionAutocomplete } from "../mentions/MentionAutocomplete.js";
10
8
  function genId() {
11
9
  return `${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`;
12
10
  }
@@ -23,17 +21,6 @@ export function Composer(props) {
23
21
  // fire on hard navs and lets GuardedLink-style navigation confirm first.
24
22
  // Optional accessor — consumers without the provider degrade to old behavior.
25
23
  const navigationGuard = useOptionalLocalUploadGuard();
26
- const mentionApi = useMentionAutocomplete({
27
- textareaRef: ref,
28
- value: text,
29
- onChange: setText,
30
- providers: props.mentionConfig?.providers ?? [],
31
- context: props.mentionConfig?.context,
32
- recent: props.mentionConfig?.recent,
33
- trigger: props.mentionConfig?.trigger,
34
- minQueryLength: props.mentionConfig?.minQueryLength,
35
- searchDebounceMs: props.mentionConfig?.searchDebounceMs,
36
- });
37
24
  // focus stability: never steal focus unless explicitly enabled
38
25
  React.useEffect(() => {
39
26
  if (!autoFocus)
@@ -49,9 +36,8 @@ export function Composer(props) {
49
36
  const sendGuardId = `chat-send-${genId()}`;
50
37
  navigationGuard?.registerUpload(sendGuardId);
51
38
  try {
52
- await onSend(mentionApi.getValueWithTokens());
39
+ await onSend(text);
53
40
  setText("");
54
- mentionApi.close();
55
41
  }
56
42
  catch (err) {
57
43
  // C-B8: a failed send (e.g. the realtime socket was closed) must NOT clear the
@@ -67,7 +53,7 @@ export function Composer(props) {
67
53
  }
68
54
  };
69
55
  const isDisabled = disabled || isSending;
70
- return (_jsxs("div", { className: "chat-composer", children: [props.mentionConfig && mentionApi.state.open && (_jsx("div", { className: "relative", children: _jsx("div", { className: "absolute bottom-full left-0 mb-1 z-10", children: _jsx(MentionAutocomplete, { state: mentionApi.state, onSelect: (ref) => mentionApi.insertMention(ref) }) }) })), sendError && (_jsx("div", { className: "px-1 pb-1 text-sm text-destructive", role: "alert", children: sendError })), _jsxs("div", { className: "flex items-end gap-2", children: [_jsx(Textarea, { ref: ref, value: text, onChange: (e) => {
56
+ return (_jsxs("div", { className: "chat-composer", children: [sendError && (_jsx("div", { className: "px-1 pb-1 text-sm text-destructive", role: "alert", children: sendError })), _jsxs("div", { className: "flex items-end gap-2", children: [_jsx(Textarea, { ref: ref, value: text, onChange: (e) => {
71
57
  setText(e.target.value);
72
58
  if (sendError)
73
59
  setSendError(null);
@@ -75,8 +61,6 @@ export function Composer(props) {
75
61
  if (onTyping && e.target.value.trim().length > 0)
76
62
  onTyping();
77
63
  }, placeholder: placeholder, disabled: isDisabled, rows: 1, className: cn("min-h-[40px] resize-none"), onKeyDown: (e) => {
78
- if (mentionApi.handleKeyDown(e))
79
- return;
80
64
  if (e.key === "Enter" && !e.shiftKey) {
81
65
  e.preventDefault();
82
66
  send();
@@ -1 +1 @@
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,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAEvC,OAAO,EAAE,sBAAsB,EAAE,MAAM,yCAAyC,CAAC;AACjF,OAAO,EAAE,mBAAmB,EAAE,MAAM,oCAAoC,CAAC;AAEzE,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;AAuBD,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,6EAA6E;IAC7E,6EAA6E;IAC7E,+EAA+E;IAC/E,8EAA8E;IAC9E,yEAAyE;IACzE,8EAA8E;IAC9E,MAAM,eAAe,GAAG,2BAA2B,EAAE,CAAC;IAEtD,MAAM,UAAU,GAAG,sBAAsB,CAAC;QACxC,WAAW,EAAE,GAA2C;QACxD,KAAK,EAAE,IAAI;QACX,QAAQ,EAAE,OAAO;QACjB,SAAS,EAAE,KAAK,CAAC,aAAa,EAAE,SAAS,IAAI,EAAE;QAC/C,OAAO,EAAE,KAAK,CAAC,aAAa,EAAE,OAAO;QACrC,MAAM,EAAE,KAAK,CAAC,aAAa,EAAE,MAAM;QACnC,OAAO,EAAE,KAAK,CAAC,aAAa,EAAE,OAAO;QACrC,cAAc,EAAE,KAAK,CAAC,aAAa,EAAE,cAAc;QACnD,gBAAgB,EAAE,KAAK,CAAC,aAAa,EAAE,gBAAgB;KACxD,CAAC,CAAC;IAEH,+DAA+D;IAC/D,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,IAAI,GAAG,KAAK,IAAI,EAAE;QACtB,MAAM,CAAC,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;QACtB,IAAI,CAAC,CAAC;YAAE,OAAO;QAEf,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,UAAU,CAAC,kBAAkB,EAAE,CAAC,CAAC;YAC9C,OAAO,CAAC,EAAE,CAAC,CAAC;YACZ,UAAU,CAAC,KAAK,EAAE,CAAC;QACrB,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,+EAA+E;YAC/E,iFAAiF;YACjF,8EAA8E;YAC9E,yCAAyC;YACzC,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,KAAK,CAAC,aAAa,IAAI,UAAU,CAAC,KAAK,CAAC,IAAI,IAAI,CAC/C,cAAK,SAAS,EAAC,UAAU,YACvB,cAAK,SAAS,EAAC,uCAAuC,YACpD,KAAC,mBAAmB,IAClB,KAAK,EAAE,UAAU,CAAC,KAAK,EACvB,QAAQ,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,UAAU,CAAC,aAAa,CAAC,GAAG,CAAC,GAChD,GACE,GACF,CACP,EAEA,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,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,+EAA+E;4BAC/E,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,UAAU,CAAC,aAAa,CAAC,CAAC,CAAC;gCAAE,OAAO;4BACxC,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,IAAI,CAAC,IAAI,EAAE,EACpC,OAAO,EAAE,IAAI,YAEZ,SAAS,CAAC,CAAC,CAAC,KAAC,OAAO,IAAC,SAAS,EAAC,sBAAsB,GAAG,CAAC,CAAC,CAAC,MAAM,GAC3D,IACL,IACF,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 { Loader2 } from \"lucide-react\";\nimport type { ChatMessageV1 } from \"@ttt-productions/chat-core\";\nimport { useMentionAutocomplete } from \"../mentions/use-mention-autocomplete.js\";\nimport { MentionAutocomplete } from \"../mentions/MentionAutocomplete.js\";\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 is TEXT-ONLY — files are shared through the\n * conversation's Conversation Files surface, never through the message\n * timeline, so there is no attachment path here.\n */\n onSend: (text: string, replyTo?: ChatMessageV1[\"replyTo\"]) => 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 mentionConfig?: import('../types.js').ChatMentionConfig;\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\n const mentionApi = useMentionAutocomplete({\n textareaRef: ref as React.RefObject<HTMLTextAreaElement>,\n value: text,\n onChange: setText,\n providers: props.mentionConfig?.providers ?? [],\n context: props.mentionConfig?.context,\n recent: props.mentionConfig?.recent,\n trigger: props.mentionConfig?.trigger,\n minQueryLength: props.mentionConfig?.minQueryLength,\n searchDebounceMs: props.mentionConfig?.searchDebounceMs,\n });\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 const send = async () => {\n const v = text.trim();\n if (!v) return;\n\n setIsSending(true);\n setSendError(null);\n const sendGuardId = `chat-send-${genId()}`;\n navigationGuard?.registerUpload(sendGuardId);\n\n try {\n await onSend(mentionApi.getValueWithTokens());\n setText(\"\");\n mentionApi.close();\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 {props.mentionConfig && mentionApi.state.open && (\n <div className=\"relative\">\n <div className=\"absolute bottom-full left-0 mb-1 z-10\">\n <MentionAutocomplete\n state={mentionApi.state}\n onSelect={(ref) => mentionApi.insertMention(ref)}\n />\n </div>\n </div>\n )}\n\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 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 (mentionApi.handleKeyDown(e)) return;\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 || !text.trim()}\n onClick={send}\n >\n {isSending ? <Loader2 className=\"h-4 w-4 animate-spin\" /> : \"Send\"}\n </Button>\n </div>\n </div>\n );\n}\n"]}
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 { Loader2 } from \"lucide-react\";\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\n // focus stability: never steal focus unless explicitly enabled\n React.useEffect(() => {\n if (!autoFocus) return;\n ref.current?.focus();\n }, [autoFocus]);\n\n const send = async () => {\n const v = text.trim();\n if (!v) return;\n\n setIsSending(true);\n setSendError(null);\n const sendGuardId = `chat-send-${genId()}`;\n navigationGuard?.registerUpload(sendGuardId);\n\n try {\n await onSend(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 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 || !text.trim()}\n onClick={send}\n >\n {isSending ? <Loader2 className=\"h-4 w-4 animate-spin\" /> : \"Send\"}\n </Button>\n </div>\n </div>\n );\n}\n"]}