@ttt-productions/chat-react 0.13.14 → 0.13.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/dist/context/ChatNameResolverContext.d.ts +3 -3
- package/dist/context/ChatNameResolverContext.d.ts.map +1 -1
- package/dist/context/ChatNameResolverContext.js +3 -3
- package/dist/context/ChatNameResolverContext.js.map +1 -1
- package/dist/firestore/queries.js +1 -1
- package/dist/firestore/queries.js.map +1 -1
- package/dist/hooks/useChatMessages.d.ts +3 -3
- package/dist/hooks/useChatMessages.d.ts.map +1 -1
- package/dist/hooks/useChatMessages.js +20 -12
- package/dist/hooks/useChatMessages.js.map +1 -1
- package/dist/index.d.ts +32 -18
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +18 -11
- package/dist/index.js.map +1 -1
- package/dist/realtime/channel-client.d.ts.map +1 -1
- package/dist/realtime/channel-client.js +97 -32
- package/dist/realtime/channel-client.js.map +1 -1
- package/dist/realtime/inbox-client.d.ts.map +1 -1
- package/dist/realtime/inbox-client.js +29 -11
- package/dist/realtime/inbox-client.js.map +1 -1
- package/dist/realtime/initial-load-budget.d.ts.map +1 -1
- package/dist/realtime/initial-load-budget.js.map +1 -1
- package/dist/realtime/map.d.ts +0 -2
- package/dist/realtime/map.d.ts.map +1 -1
- package/dist/realtime/map.js +3 -4
- package/dist/realtime/map.js.map +1 -1
- package/dist/realtime/shared.d.ts +0 -1
- package/dist/realtime/shared.d.ts.map +1 -1
- package/dist/realtime/shared.js +2 -6
- package/dist/realtime/shared.js.map +1 -1
- package/dist/realtime/socket.d.ts.map +1 -1
- package/dist/realtime/socket.js +1 -11
- package/dist/realtime/socket.js.map +1 -1
- package/dist/realtime/transport.d.ts +1 -2
- package/dist/realtime/transport.d.ts.map +1 -1
- package/dist/realtime/transport.js +1 -1
- package/dist/realtime/transport.js.map +1 -1
- package/dist/realtime/useRealtimeChatMessages.d.ts +4 -4
- package/dist/realtime/useRealtimeChatMessages.d.ts.map +1 -1
- package/dist/realtime/useRealtimeChatMessages.js +5 -5
- package/dist/realtime/useRealtimeChatMessages.js.map +1 -1
- package/dist/realtime/wire.d.ts +0 -2
- package/dist/realtime/wire.d.ts.map +1 -1
- package/dist/realtime/wire.js +0 -6
- package/dist/realtime/wire.js.map +1 -1
- package/dist/types.d.ts +3 -3
- package/dist/types.js.map +1 -1
- package/dist/ui/ChatShell.d.ts +5 -5
- package/dist/ui/ChatShell.d.ts.map +1 -1
- package/dist/ui/ChatShell.js +37 -34
- package/dist/ui/ChatShell.js.map +1 -1
- package/dist/ui/Composer.d.ts +1 -1
- package/dist/ui/Composer.js +12 -12
- package/dist/ui/Composer.js.map +1 -1
- package/dist/ui/MessageItemDefault.d.ts +2 -2
- package/dist/ui/MessageItemDefault.d.ts.map +1 -1
- package/dist/ui/MessageItemDefault.js +26 -24
- package/dist/ui/MessageItemDefault.js.map +1 -1
- package/dist/ui/MessageList.d.ts +3 -3
- package/dist/ui/MessageList.d.ts.map +1 -1
- package/dist/ui/MessageList.js +13 -16
- package/dist/ui/MessageList.js.map +1 -1
- package/dist/ui/SenderName.js +3 -3
- package/dist/ui/SenderName.js.map +1 -1
- package/dist/ui/menus.d.ts +1 -1
- package/dist/ui/menus.d.ts.map +1 -1
- package/dist/ui/menus.js +4 -4
- package/dist/ui/menus.js.map +1 -1
- package/package.json +10 -10
- package/dist/realtime/index.d.ts +0 -19
- package/dist/realtime/index.d.ts.map +0 -1
- package/dist/realtime/index.js +0 -15
- package/dist/realtime/index.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"transport.js","sourceRoot":"","sources":["../../src/realtime/transport.ts"],"names":[],"mappings":"AAAA,gFAAgF;AAChF,6DAA6D;AAC7D,gFAAgF;AAChF,2EAA2E;AAC3E,oFAAoF;AACpF,EAAE;AACF,sEAAsE;AACtE,+EAA+E;AAC/E,kFAAkF;AAClF,mFAAmF;AACnF,sDAAsD;AAEtD,OAAO,
|
|
1
|
+
{"version":3,"file":"transport.js","sourceRoot":"","sources":["../../src/realtime/transport.ts"],"names":[],"mappings":"AAAA,gFAAgF;AAChF,6DAA6D;AAC7D,gFAAgF;AAChF,2EAA2E;AAC3E,oFAAoF;AACpF,EAAE;AACF,sEAAsE;AACtE,+EAA+E;AAC/E,kFAAkF;AAClF,mFAAmF;AACnF,sDAAsD;AAEtD,OAAO,EACL,aAAa,GAGd,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,WAAW,EAA0B,MAAM,mBAAmB,CAAC;AACxE,OAAO,EAAE,oBAAoB,EAAsB,MAAM,aAAa,CAAC;AAgDvE,4FAA4F;AAC5F,MAAM,UAAU,wBAAwB,CAAC,MAA+B;IACtE,MAAM,OAAO,GAAG,IAAI,aAAa,CAAC;QAChC,QAAQ,EAAE,MAAM,CAAC,QAAQ;QACzB,QAAQ,EAAE,MAAM,CAAC,QAAQ;QACzB,aAAa,EAAE,MAAM,CAAC,aAAa;QACnC,aAAa,EAAE,MAAM,CAAC,aAAa;QACnC,aAAa,EAAE,MAAM,CAAC,aAAa,IAAI,oBAAoB;QAC3D,MAAM,EAAE,MAAM,CAAC,MAAM;QACrB,SAAS,EAAE,MAAM,CAAC,SAAS;QAC3B,WAAW,EAAE,MAAM,CAAC,WAAW;KAChC,CAAC,CAAC;IAEH,OAAO;QACL,OAAO;QACP,UAAU,EAAE,MAAM,CAAC,UAAU;QAC7B,aAAa,EAAE,MAAM,CAAC,aAAa;QACnC,OAAO,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,OAAO,EAAE;QAChC,KAAK,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,QAAQ,EAAE;QAC/B,KAAK,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,KAAK,EAAE;QAC5B,QAAQ,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,QAAQ,EAAE;QAClC,SAAS,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,OAAO,CAAC,SAAS,CAAC,EAAE,CAAC;KACzC,CAAC;AACJ,CAAC;AAED,iFAAiF;AACjF,MAAM,UAAU,iBAAiB,CAAC,MASjC;IACC,OAAO,IAAI,WAAW,CAAC;QACrB,QAAQ,EAAE,MAAM,CAAC,QAAQ;QACzB,aAAa,EAAE,MAAM,CAAC,aAAa;QACnC,aAAa,EAAE,MAAM,CAAC,aAAa;QACnC,aAAa,EAAE,MAAM,CAAC,aAAa,IAAI,oBAAoB;QAC3D,MAAM,EAAE,MAAM,CAAC,MAAM;QACrB,SAAS,EAAE,MAAM,CAAC,SAAS;QAC3B,WAAW,EAAE,MAAM,CAAC,WAAW;KAChC,CAAC,CAAC;AACL,CAAC","sourcesContent":["// The realtime transport HANDLE the chat-react config carries on the `realtime`\n// transport (`ChatRealtimeTransportConfig.client`). It owns:\n// - a ChannelClient for the active thread (send/read/typing/presence/history)\n// - the grant-mint wiring (channel scope) via the injected grantProvider\n// - auth-user-switch teardown (close every socket before connecting as a new uid)\n//\n// The INBOX socket is created SEPARATELY (one per user, app-wide) via\n// `createInboxClient` — it is not tied to a single thread, so the app owns its\n// lifetime (mount it once at the dock, not per ChatShell). This keeps the channel\n// transport (per-thread) and the inbox transport (per-user) cleanly split, exactly\n// as the worker splits Channel DOs from the Inbox DO.\n\nimport {\n ChannelClient,\n type ChannelClientConfig,\n type ChannelClientState,\n} from './channel-client.js';\nimport { InboxClient, type InboxClientConfig } from './inbox-client.js';\nimport { browserSocketFactory, type SocketFactory } from './socket.js';\nimport type { GrantProvider, TransportTimers } from './shared.js';\nimport type { ChatClientDiagnosticsOption } from './diagnostics.js';\nimport type { ChatConversationRef } from '@ttt-productions/chat-schemas';\n\n/**\n * Everything the channel transport needs. The app supplies `grantProvider`\n * (its `mintChatGrant` callable, channel scope) + `endpoint`; `socketFactory`\n * defaults to the real browser WebSocket and is overridden in tests.\n */\nexport interface RealtimeTransportConfig {\n endpoint: string;\n /** The conversation the socket is for, as the app maps it onto the neutral reference. */\n channelRef: ChatConversationRef;\n threadId: string;\n currentUserId: string;\n grantProvider: GrantProvider;\n socketFactory?: SocketFactory;\n timers?: TransportTimers;\n reconnect?: ChannelClientConfig['reconnect'];\n /**\n * OPT-IN structured client diagnostics, forwarded to the ChannelClient. Absent\n * or `false` (the default) changes nothing and costs one nullish check; `true`\n * emits one single-line `console.debug` entry per client DECISION with a stable\n * `chat_client_*` event name; a function routes those entries into the app's own\n * logger. Content-free by construction (ids/seqs/states/counts/codes only).\n */\n diagnostics?: ChatClientDiagnosticsOption;\n}\n\n/**\n * The opaque realtime client handle stored on `ChatRealtimeTransportConfig.client`.\n * The UI hook (`useRealtimeChatMessages`) drives it; the UI never reaches into the\n * socket directly.\n */\nexport interface RealtimeChatClient {\n readonly channel: ChannelClient;\n readonly channelRef: ChatConversationRef;\n readonly currentUserId: string;\n connect(): Promise<void>;\n /** Retry the connection now (the first-open failure's Retry) — see `ChannelClient.retryNow`. */\n retry(): void;\n /** Tear down every socket (auth-user switch / unmount). Idempotent. */\n close(): void;\n getState(): ChannelClientState;\n subscribe(fn: (s: ChannelClientState) => void): () => void;\n}\n\n/** Create the per-thread realtime client (the value placed on `config.realtime.client`). */\nexport function createRealtimeChatClient(config: RealtimeTransportConfig): RealtimeChatClient {\n const channel = new ChannelClient({\n endpoint: config.endpoint,\n threadId: config.threadId,\n currentUserId: config.currentUserId,\n grantProvider: config.grantProvider,\n socketFactory: config.socketFactory ?? browserSocketFactory,\n timers: config.timers,\n reconnect: config.reconnect,\n diagnostics: config.diagnostics,\n });\n\n return {\n channel,\n channelRef: config.channelRef,\n currentUserId: config.currentUserId,\n connect: () => channel.connect(),\n retry: () => channel.retryNow(),\n close: () => channel.close(),\n getState: () => channel.getState(),\n subscribe: (fn) => channel.subscribe(fn),\n };\n}\n\n/** Create the per-USER inbox client (mount once at the dock; not per-thread). */\nexport function createInboxClient(config: {\n endpoint: string;\n currentUserId: string;\n grantProvider: GrantProvider;\n socketFactory?: SocketFactory;\n timers?: TransportTimers;\n reconnect?: InboxClientConfig['reconnect'];\n /** OPT-IN structured diagnostics, forwarded to the InboxClient (default OFF). */\n diagnostics?: ChatClientDiagnosticsOption;\n}): InboxClient {\n return new InboxClient({\n endpoint: config.endpoint,\n currentUserId: config.currentUserId,\n grantProvider: config.grantProvider,\n socketFactory: config.socketFactory ?? browserSocketFactory,\n timers: config.timers,\n reconnect: config.reconnect,\n diagnostics: config.diagnostics,\n });\n}\n"]}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { ChatMessageV1 } from
|
|
2
|
-
import type { RealtimeChatClient } from
|
|
3
|
-
import type { ChannelClientState } from
|
|
1
|
+
import type { ChatMessageV1 } from '@ttt-productions/chat-core';
|
|
2
|
+
import type { RealtimeChatClient } from './transport.js';
|
|
3
|
+
import type { ChannelClientState } from './channel-client.js';
|
|
4
4
|
export type UseRealtimeChatMessagesResult = {
|
|
5
5
|
allowed: boolean;
|
|
6
6
|
isInitialLoading: boolean;
|
|
@@ -15,7 +15,7 @@ export type UseRealtimeChatMessagesResult = {
|
|
|
15
15
|
/** Retry the connection now — the first-open failure's Retry. */
|
|
16
16
|
retry: () => void;
|
|
17
17
|
/** Realtime extras (not present on the firestore result) — optional for the UI. */
|
|
18
|
-
status: ChannelClientState[
|
|
18
|
+
status: ChannelClientState['status'];
|
|
19
19
|
/** The last structured error code the DO sent (e.g. a close reason). Drives the
|
|
20
20
|
* disconnected-banner detail (R13). */
|
|
21
21
|
lastErrorCode: string | null;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useRealtimeChatMessages.d.ts","sourceRoot":"","sources":["../../src/realtime/useRealtimeChatMessages.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAChE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AACzD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAI9D,MAAM,MAAM,6BAA6B,GAAG;IAC1C,OAAO,EAAE,OAAO,CAAC;IACjB,gBAAgB,EAAE,OAAO,CAAC;IAC1B,QAAQ,EAAE,aAAa,EAAE,CAAC;IAC1B,UAAU,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAChC,QAAQ,EAAE,OAAO,CAAC;IAClB,eAAe,EAAE,OAAO,CAAC;IACzB,wFAAwF;IACxF,iBAAiB,EAAE,OAAO,CAAC;IAC3B,oGAAoG;IACpG,QAAQ,EAAE,OAAO,CAAC;IAClB,iEAAiE;IACjE,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,mFAAmF;IACnF,MAAM,EAAE,kBAAkB,CAAC,QAAQ,CAAC,CAAC;IACrC;4CACwC;IACxC,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,iHAAiH;IACjH,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC;IAChC;;;;;uCAKmC;IACnC,SAAS,EAAE,CAAC,eAAe,EAAE,MAAM,KAAK,OAAO,CAAC;IAChD,sIAAsI;IACtI,OAAO,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC;IACxD,YAAY,EAAE,MAAM,IAAI,CAAC;IACzB;;yEAEqE;IACrE,iBAAiB,EAAE,MAAM,IAAI,CAAC;IAC9B,mBAAmB,EAAE,MAAM,IAAI,CAAC;CACjC,CAAC;AAEF,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,kBAAkB,GAAG,6BAA6B,
|
|
1
|
+
{"version":3,"file":"useRealtimeChatMessages.d.ts","sourceRoot":"","sources":["../../src/realtime/useRealtimeChatMessages.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAChE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AACzD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAI9D,MAAM,MAAM,6BAA6B,GAAG;IAC1C,OAAO,EAAE,OAAO,CAAC;IACjB,gBAAgB,EAAE,OAAO,CAAC;IAC1B,QAAQ,EAAE,aAAa,EAAE,CAAC;IAC1B,UAAU,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAChC,QAAQ,EAAE,OAAO,CAAC;IAClB,eAAe,EAAE,OAAO,CAAC;IACzB,wFAAwF;IACxF,iBAAiB,EAAE,OAAO,CAAC;IAC3B,oGAAoG;IACpG,QAAQ,EAAE,OAAO,CAAC;IAClB,iEAAiE;IACjE,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,mFAAmF;IACnF,MAAM,EAAE,kBAAkB,CAAC,QAAQ,CAAC,CAAC;IACrC;4CACwC;IACxC,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,iHAAiH;IACjH,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC;IAChC;;;;;uCAKmC;IACnC,SAAS,EAAE,CAAC,eAAe,EAAE,MAAM,KAAK,OAAO,CAAC;IAChD,sIAAsI;IACtI,OAAO,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC;IACxD,YAAY,EAAE,MAAM,IAAI,CAAC;IACzB;;yEAEqE;IACrE,iBAAiB,EAAE,MAAM,IAAI,CAAC;IAC9B,mBAAmB,EAAE,MAAM,IAAI,CAAC;CACjC,CAAC;AAEF,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,kBAAkB,GAAG,6BAA6B,CAoFjG"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
'use client';
|
|
2
2
|
// React hook for the REALTIME transport, returning the SAME shape as
|
|
3
3
|
// `useChatMessages` (the firestore path) so `ChatShell` is transport-agnostic.
|
|
4
4
|
// It SUBSCRIBES to a `RealtimeChatClient`'s observable state. It does NOT own the
|
|
@@ -8,9 +8,9 @@
|
|
|
8
8
|
// (passed via config) so its identity is the subscription key — a new client for a
|
|
9
9
|
// new uid replaces the old one (Contract A auth-user-switch teardown happens in the
|
|
10
10
|
// owning hook).
|
|
11
|
-
import * as React from
|
|
12
|
-
import { useOptionalChatNameResolver } from
|
|
13
|
-
import { newCorrelationId } from
|
|
11
|
+
import * as React from 'react';
|
|
12
|
+
import { useOptionalChatNameResolver } from '../context/ChatNameResolverContext.js';
|
|
13
|
+
import { newCorrelationId } from './shared.js';
|
|
14
14
|
export function useRealtimeChatMessages(client) {
|
|
15
15
|
const [state, setState] = React.useState(() => client.getState());
|
|
16
16
|
// Subscribe only; the owning hook drives connect/close (single lifecycle owner, C-B7).
|
|
@@ -52,7 +52,7 @@ export function useRealtimeChatMessages(client) {
|
|
|
52
52
|
// maps to allowed:false so ChatShell renders its no-access surface instead of an
|
|
53
53
|
// eternal loader; a transient close / 4403-revoke keeps allowed:true and rides the
|
|
54
54
|
// reconnect/disconnected banner. Otherwise accessMode is presentation-only here.
|
|
55
|
-
allowed: state.lastErrorCode !==
|
|
55
|
+
allowed: state.lastErrorCode !== 'access-denied',
|
|
56
56
|
// Derive initial loading from authoritative-data completion, NOT connection status:
|
|
57
57
|
// a socket can be 'open' with no snapshot yet, and a post-load reconnect must not
|
|
58
58
|
// fall back into the opening state. `hasLoadedInitialData` encodes the full table.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useRealtimeChatMessages.js","sourceRoot":"","sources":["../../src/realtime/useRealtimeChatMessages.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb,qEAAqE;AACrE,+EAA+E;AAC/E,kFAAkF;AAClF,2EAA2E;AAC3E,oFAAoF;AACpF,iFAAiF;AACjF,mFAAmF;AACnF,oFAAoF;AACpF,gBAAgB;AAEhB,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAI/B,OAAO,EAAE,2BAA2B,EAAE,MAAM,uCAAuC,CAAC;AACpF,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAyC/C,MAAM,UAAU,uBAAuB,CAAC,MAA0B;IAChE,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,KAAK,CAAC,QAAQ,CAAqB,GAAG,EAAE,CAAC,MAAM,CAAC,QAAQ,EAAE,CAAC,CAAC;IAEtF,uFAAuF;IACvF,8DAA8D;IAC9D,KAAK,CAAC,SAAS,CAAC,GAAG,EAAE;QACnB,MAAM,KAAK,GAAG,MAAM,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC;QACzC,QAAQ,CAAC,MAAM,CAAC,QAAQ,EAAE,CAAC,CAAC;QAC5B,OAAO,KAAK,CAAC;IACf,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC;IAEb,gFAAgF;IAChF,MAAM,WAAW,GAAG,2BAA2B,EAAE,CAAC;IAClD,MAAM,SAAS,GAAG,KAAK,CAAC,OAAO,CAAC,GAAG,EAAE;QACnC,MAAM,GAAG,GAAG,IAAI,GAAG,EAAU,CAAC;QAC9B,KAAK,MAAM,CAAC,IAAI,KAAK,CAAC,QAAQ,EAAE,CAAC;YAC/B,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC;YACpB,KAAK,MAAM,GAAG,IAAI,CAAC,CAAC,cAAc,IAAI,EAAE;gBAAE,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACzD,CAAC;QACD,OAAO,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IACzB,CAAC,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC;IAErB,KAAK,CAAC,SAAS,CAAC,GAAG,EAAE;QACnB,IAAI,WAAW,EAAE,OAAO,IAAI,SAAS,CAAC,MAAM,GAAG,CAAC;YAAE,WAAW,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;IACnF,CAAC,EAAE,CAAC,WAAW,EAAE,SAAS,CAAC,CAAC,CAAC;IAE7B,MAAM,UAAU,GAAG,KAAK,CAAC,WAAW,CAAC,KAAK,IAAI,EAAE;QAC9C,MAAM,CAAC,OAAO,CAAC,UAAU,EAAE,CAAC;IAC9B,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC;IAEb,MAAM,IAAI,GAAG,KAAK,CAAC,WAAW,CAC5B,CAAC,IAAY,EAAW,EAAE,
|
|
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';\nimport { newCorrelationId } from './shared.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 /** True once the first open has failed (see `ChannelClientState.initialLoadFailed`). */\n initialLoadFailed: boolean;\n /** The channel is current (see `ChannelClientState.isSynced`): act on its terms only while true. */\n isSynced: boolean;\n /** Retry the connection now — the first-open failure's Retry. */\n retry: () => void;\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 for (const uid of m.referencedUids ?? []) set.add(uid);\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 => client.channel.send({ clientMessageId: newCorrelationId(), 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 retry = React.useCallback(() => client.retry(), [client]);\n const presenceSubscribe = React.useCallback(() => client.channel.presenceSubscribe(), [client]);\n const presenceUnsubscribe = React.useCallback(\n () => client.channel.presenceUnsubscribe(),\n [client],\n );\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 initialLoadFailed: state.initialLoadFailed,\n // Current: open, loaded, and resynced since this socket opened (never sticky).\n isSynced: state.isSynced,\n retry,\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"]}
|
package/dist/realtime/wire.d.ts
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
export { CHAT_SUBPROTOCOL, CHAT_WIRE_VERSION, CHAT_CLOSE_CODES, CLIENT_KINDS as CLIENT_FRAME, SERVER_KINDS as SERVER_FRAME, ChatSendRejectedPayloadSchema, CHAT_SEND_REJECTION_CODES, CHAT_SEND_REJECTION_RETRYABLE, } from '@ttt-productions/chat-schemas';
|
|
2
|
-
export type { ChatCloseCode, ChatSendRejectedPayload, ChatSendRejectionCode, } from '@ttt-productions/chat-schemas';
|
|
3
1
|
import type { ChatConversationRef, ChatMarkReadResultPayload, ChatMessageRevisionKind, ChatSendRejectedPayload } from '@ttt-productions/chat-schemas';
|
|
4
2
|
/** A revision's kind (chat-schemas `ChatMessageRevisionKind`), under this package's historical name. */
|
|
5
3
|
export type RevisionKind = ChatMessageRevisionKind;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"wire.d.ts","sourceRoot":"","sources":["../../src/realtime/wire.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"wire.d.ts","sourceRoot":"","sources":["../../src/realtime/wire.ts"],"names":[],"mappings":"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"}
|
package/dist/realtime/wire.js
CHANGED
|
@@ -11,12 +11,6 @@
|
|
|
11
11
|
// `JSON.stringify({ v: 1, type, payload })` and read `env.type` + `env.payload`.
|
|
12
12
|
// We keep that field name here to match; realtime-core's envelope schema is the
|
|
13
13
|
// generic transport primitive, the chat protocol chose `type` for its frames.
|
|
14
|
-
// Re-export the canonical wire contract so this package's public surface is
|
|
15
|
-
// unchanged. `CLIENT_FRAME`/`SERVER_FRAME` are this package's historical names
|
|
16
|
-
// for the frame-kind maps (`CLIENT_KINDS`/`SERVER_KINDS` in the contract).
|
|
17
|
-
export { CHAT_SUBPROTOCOL, CHAT_WIRE_VERSION, CHAT_CLOSE_CODES, CLIENT_KINDS as CLIENT_FRAME, SERVER_KINDS as SERVER_FRAME,
|
|
18
|
-
// Correlated send-rejection contract (SERVER_FRAME.SEND_REJECTED payload).
|
|
19
|
-
ChatSendRejectedPayloadSchema, CHAT_SEND_REJECTION_CODES, CHAT_SEND_REJECTION_RETRYABLE, } from '@ttt-productions/chat-schemas';
|
|
20
14
|
import { CHAT_WIRE_VERSION } from '@ttt-productions/chat-schemas';
|
|
21
15
|
/** Build an outbound `{ v, type, payload }` frame (the wire format the DO reads). */
|
|
22
16
|
export function buildFrame(type, payload) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"wire.js","sourceRoot":"","sources":["../../src/realtime/wire.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,mFAAmF;AACnF,iFAAiF;AACjF,gIAAgI;AAChI,+EAA+E;AAC/E,6EAA6E;AAC7E,0DAA0D;AAC1D,EAAE;AACF,2EAA2E;AAC3E,sEAAsE;AACtE,iFAAiF;AACjF,gFAAgF;AAChF,8EAA8E;AAE9E,
|
|
1
|
+
{"version":3,"file":"wire.js","sourceRoot":"","sources":["../../src/realtime/wire.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,mFAAmF;AACnF,iFAAiF;AACjF,gIAAgI;AAChI,+EAA+E;AAC/E,6EAA6E;AAC7E,0DAA0D;AAC1D,EAAE;AACF,2EAA2E;AAC3E,sEAAsE;AACtE,iFAAiF;AACjF,gFAAgF;AAChF,8EAA8E;AAE9E,OAAO,EAAE,iBAAiB,EAAE,MAAM,+BAA+B,CAAC;AAiIlE,qFAAqF;AACrF,MAAM,UAAU,UAAU,CAAC,IAAY,EAAE,OAAgC;IACvE,OAAO,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC,EAAE,iBAAiB,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC;AACjE,CAAC;AAED,kFAAkF;AAClF,MAAM,UAAU,UAAU,CACxB,GAAW;IAEX,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAY,CAAC;QAC1C,IAAI,CAAC,MAAM,IAAI,OAAO,MAAM,KAAK,QAAQ;YAAE,OAAO,IAAI,CAAC;QACvD,OAAO,MAA0E,CAAC;IACpF,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,wGAAwG;AACxG,MAAM,UAAU,eAAe,CAC7B,CAA0C;IAE1C,OAAO,KAAK,CAAC,OAAO,CAAE,CAAuB,CAAC,QAAQ,CAAC,CAAC;AAC1D,CAAC","sourcesContent":["// Chat realtime WIRE PROTOCOL (Contract C) — the client side of the Channel +\n// Inbox Durable Object protocol. The canonical protocol declarations (subprotocol,\n// frame-kind maps, close codes, the neutral conversation reference, grant scope,\n// the send and mark-read contracts, client-agreed limits) are owned by `@ttt-productions/chat-schemas`; this file consumes them\n// and adds the client-side raw row/frame shapes the transport maps into the UI\n// message shape. The client and the worker runtime agree on the wire by both\n// importing that one contract; neither imports the other.\n//\n// Frame shape on the socket is `{ v, type, payload }` (NOT realtime-core's\n// `{ v, kind, payload }`): the worker runtime `send`/`broadcast` emit\n// `JSON.stringify({ v: 1, type, payload })` and read `env.type` + `env.payload`.\n// We keep that field name here to match; realtime-core's envelope schema is the\n// generic transport primitive, the chat protocol chose `type` for its frames.\n\nimport { CHAT_WIRE_VERSION } from '@ttt-productions/chat-schemas';\nimport type {\n ChatConversationRef,\n ChatMarkReadResultPayload,\n ChatMessageRevisionKind,\n ChatSendRejectedPayload,\n} from '@ttt-productions/chat-schemas';\n\n// ---- raw wire row shapes (what the DO serializes; opaque-but-typed here) ----\n\n/** A revision's kind (chat-schemas `ChatMessageRevisionKind`), under this package's historical name. */\nexport type RevisionKind = ChatMessageRevisionKind;\n\n/**\n * A message row as the Channel DO stores + broadcasts it (the worker runtime's\n * message row), declared as the fields the client actually CONSUMES. The adapter\n * mapping in `map.ts` turns it into the UI `ChatMessageV1` shape.\n *\n * The row is TEXT-only: there is no attachment lifecycle on a chat message, and\n * no reply pointer — chat has no reply-authoring affordance, so the client\n * neither sends nor reads one (DJ ruling 2026-07-29). A DO that still broadcasts\n * a legacy stringified `replyTo` key is simply ignored here.\n */\nexport interface WireMessageRow {\n seq: number;\n senderUid: string;\n clientMessageId: string;\n text: string;\n createdAt: number;\n epoch: number;\n /** Account ids a server-written row's text refers to (absent on a member's own message). */\n referencedUids?: string[];\n /**\n * The effective (max-`messageRevision`) moderation kind for this row, or null\n * when never moderated. Populated by the DO's getHistory/getDeltaSince overlay\n * merge so a hidden/deleted/edited row arrives already-moderated and its\n * ORIGINAL text never reaches the renderer. Optional/absent on pre-overlay rows.\n */\n moderationKind?: RevisionKind | null;\n /**\n * The effective `messageRevision` that `moderationKind` reflects (the DO's\n * max-revision). Used to break ties against a live `revision` frame: a higher\n * `messageRevision` wins; a stale/older one is ignored. Optional/absent on\n * pre-overlay rows (treated as revision 0).\n */\n messageRevision?: number;\n}\n\n/**\n * The channel resume snapshot the DO returns for a `resume` frame (Contract C).\n * `resync` / `delta` are the durable gap-repair half of the contract:\n * - `resync: false` + `delta` = the contiguous ≤500-message backlog after the\n * client's cursor (oldest-first); the client APPLIES the delta and advances its\n * cursor to `lastMessageSeq` — a missed broadcast is never permanent.\n * - `resync: true` = the gap exceeds the backlog (or the client sent no cursor);\n * the client DROPS its local tail and re-pages history from the top.\n * Both are OPTIONAL/absent on a pre-contract DO (treated as `resync: true`, empty\n * delta — the safe re-page path) so a stale worker can't leave a client stuck.\n */\nexport interface WireChannelSnapshot {\n lastMessageSeq: number;\n readSeq: number;\n resync?: boolean;\n delta?: WireMessageRow[];\n}\n\n/** A registry entry in the inbox snapshot (the worker runtime's registry entry). */\nexport interface WireRegistryEntry {\n channelRef: string;\n kind: string;\n state: 'active' | 'tombstoned';\n registryVersion: number;\n /**\n * The conversation this entry is — the OPENABLE identity for the inbox view (the\n * `channelRef` is an opaque registry key). The app maps it back to its own\n * conversation; absent, the entry offers no open action.\n */\n ref?: ChatConversationRef | null;\n /**\n * Per-entry unread flag (drives the per-row dot). Optional/absent until the inbox\n * DO populates it; `deriveUnreadRefs` reads it as a typed field (no cast).\n */\n unread?: boolean;\n /**\n * Archived state — DISTINCT from `tombstoned`. An archived row is still an ACTIVE\n * membership (`state: 'active'`, history readable) but is grouped under the inbox\n * view's Archived toggle instead of the active list. The client must NOT count\n * archived rows in the unread roll-up (archive = done; the DO clears unread on\n * archive). Optional/absent on a pre-archive DO (treated as not-archived).\n */\n archived?: boolean;\n}\n\n/** The inbox snapshot payload pushed on connect / `resume` / live delta. */\nexport interface WireInboxSnapshot {\n registry: WireRegistryEntry[];\n hasUnread: boolean;\n}\n\n// ---- server → client frame payloads (discriminated on `type`) ----\n\nexport type ServerFrame =\n | { type: 'message'; payload: { message: WireMessageRow } }\n | { type: 'ack'; payload: { clientMessageId?: string; seq?: number; readSeq?: number } }\n | { type: 'history-page'; payload: { messages: WireMessageRow[] } }\n | { type: 'presence'; payload: { online: string[] } }\n | { type: 'typing'; payload: { uid: string; at: number } }\n | { type: 'snapshot'; payload: WireChannelSnapshot | WireInboxSnapshot }\n // A standalone unread push (inbox scope): either a full inbox snapshot or a\n // lightweight dock-dot patch. Represented in the union so the inbox client routes\n // it instead of silently dropping it (forward-compatible with a future DO push).\n | { type: 'unread'; payload: WireInboxSnapshot | { hasUnread: boolean } }\n | { type: 'error'; payload: { code: string; retryAfterMs?: number } }\n // The inbox runtime's correlated answer to one `mark-read`.\n | { type: 'mark-read-result'; payload: ChatMarkReadResultPayload }\n // A CORRELATED send rejection (SERVER_KINDS.SEND_REJECTED): names the SAME\n // clientMessageId as the rejected send + the canonical retry classification, so\n // exactly one optimistic bubble is failed/retried. See ChatSendRejectedPayload.\n | { type: 'send-rejected'; payload: ChatSendRejectedPayload }\n // A moderation revision was applied to one message. The Channel DO broadcasts\n // this on a NEWLY-applied revision (the worker runtime's moderation apply) so live\n // viewers hide/blank/edit/restore the message immediately with no extra fetch.\n // `messageSeq` is the target message id; `kind` is the applied revision kind;\n // `messageRevision` is its per-message revision number (max-revision wins).\n | {\n type: 'revision';\n payload: { messageSeq: number; kind: RevisionKind; messageRevision: number };\n };\n\n/** Build an outbound `{ v, type, payload }` frame (the wire format the DO reads). */\nexport function buildFrame(type: string, payload: Record<string, unknown>): string {\n return JSON.stringify({ v: CHAT_WIRE_VERSION, type, payload });\n}\n\n/** Parse an inbound frame, tolerating malformed JSON. Returns null on garbage. */\nexport function parseFrame(\n raw: string,\n): { v?: number; type?: string; payload?: Record<string, unknown> } | null {\n try {\n const parsed = JSON.parse(raw) as unknown;\n if (!parsed || typeof parsed !== 'object') return null;\n return parsed as { v?: number; type?: string; payload?: Record<string, unknown> };\n } catch {\n return null;\n }\n}\n\n/** A snapshot with `registry` is an inbox snapshot; one with `lastMessageSeq` is a channel snapshot. */\nexport function isInboxSnapshot(\n p: WireChannelSnapshot | WireInboxSnapshot,\n): p is WireInboxSnapshot {\n return Array.isArray((p as WireInboxSnapshot).registry);\n}\n"]}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { ReactNode } from
|
|
2
|
-
import type { ChatMessageV1 } from
|
|
3
|
-
import type { ChatConversationRef } from
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import type { ChatMessageV1 } from '@ttt-productions/chat-core';
|
|
3
|
+
import type { ChatConversationRef } from '@ttt-productions/chat-schemas';
|
|
4
4
|
/**
|
|
5
5
|
* Which transport backs a chat thread.
|
|
6
6
|
* - `firestore` — the Firestore newest-window + cursor path (default).
|
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
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,6EAA6E;AAC7E,4DAA4D","sourcesContent":["// React- and Firebase-client-coupled chat types. These were previously on the\n// chat-core root; they live here so the pure chat-core package stays free of\n// react / firebase/firestore / firebase/storage references.\n\nimport type { ReactNode } from 'react';\nimport type { ChatMessageV1 } from '@ttt-productions/chat-core';\nimport type { ChatConversationRef } from '@ttt-productions/chat-schemas';\n\n// ============================================\n// CONFIG\n// ============================================\n//\n// Chat carries PLAIN TEXT only. There is no upload adapter, attachment config,\n// or mention/token config here: a conversation's files are owned by the\n// consuming app's Conversation Files surface, which runs the canonical upload\n// pipeline outside the chat timeline.\n\n/**\n * Which transport backs a chat thread.\n * - `firestore` — the Firestore newest-window + cursor path (default).\n * - `realtime` — the Cloudflare Durable Object socket; the grant is the data-access\n * authority there.\n */\nexport type ChatTransportMode = 'firestore' | 'realtime';\n\n/**\n * Realtime (Durable Object) transport handle. The concrete socket client is built\n * in this package (`realtime/transport.ts`); chat-react consumes it through this\n * config so the UI stays transport-agnostic. The app builds the `client` via\n * `createRealtimeChatClient({ endpoint, channelRef, grantProvider, ... })` and\n * passes it here. `client` stays structurally typed (`unknown` at the config seam)\n * so `ChatCoreConfig` does not pull the realtime module's types into every firestore\n * consumer — the realtime hook narrows it at the call site.\n */\nexport interface ChatRealtimeTransportConfig {\n /** The conversation the socket is scoped to, as the app maps it onto the neutral reference. */\n channelRef: ChatConversationRef;\n /**\n * The realtime client handle from `createRealtimeChatClient(...)`. Drives the\n * channel socket (subscribe / send / ack / typing / presence / history). The UI\n * never reaches into the socket directly — it goes through `useRealtimeChatMessages`.\n */\n client: unknown;\n}\n\nexport type ChatCoreConfig = {\n chatCollectionPath: string | string[];\n messagesSubcollection?: string; // default: \"messages\"\n threadId: string;\n currentUserId: string;\n currentUserDisplayName?: string;\n isAdmin: boolean;\n /**\n * Which transport backs this thread. Defaults to `'firestore'` (the current\n * path) so existing call sites are unchanged. See {@link ChatTransportMode}.\n */\n transport?: ChatTransportMode;\n /** Required iff `transport === 'realtime'`; ignored on the firestore transport. */\n realtime?: ChatRealtimeTransportConfig;\n /**\n * Whether the current user may read this conversation — the app's ONE access fact,\n * decided by the app's own rule for the conversation's kind (the package never decides\n * access). False renders the no-access state and reads nothing. On the `realtime`\n * transport the grant is the data authority, and a terminal denial from the grant\n * closes the conversation even when this is true.\n */\n allowed: boolean;\n createdAtField?: string; // default: \"createdAt\"\n pageSize?: number;\n /**\n * Firestore transport only: the app's own per-message facts, read from the stored message\n * document and merged onto the mapped message's `meta`. The package never reads an app field\n * itself; the app parses its own stored shape here and reads the result back from `meta`.\n */\n messageMeta?: (doc: Record<string, unknown>) => Record<string, unknown> | undefined;\n};\n\n// ============================================\n// RENDERING\n// ============================================\n\nexport type MessageRenderer = (m: ChatMessageV1) => ReactNode;\n\nexport type MessageRendererRegistry = Record<string, MessageRenderer>;\n"]}
|
package/dist/ui/ChatShell.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import * as React from
|
|
2
|
-
import type { ChatMessageGroupKey, ChatMessageV1, ModerationHandlers } from
|
|
3
|
-
import type { ChatCoreConfig, MessageRendererRegistry } from
|
|
4
|
-
import { type ChatScrollContainerProps } from
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import type { ChatMessageGroupKey, ChatMessageV1, ModerationHandlers } from '@ttt-productions/chat-core';
|
|
3
|
+
import type { ChatCoreConfig, MessageRendererRegistry } from '../types.js';
|
|
4
|
+
import { type ChatScrollContainerProps } from './MessageList.js';
|
|
5
5
|
/**
|
|
6
6
|
* A read the chat could not complete, handed to the app's `renderLoadError`:
|
|
7
7
|
* - `initial` — nothing loaded: the first open failed (the realtime client's attempts
|
|
@@ -18,7 +18,7 @@ import { type ChatScrollContainerProps } from "./MessageList.js";
|
|
|
18
18
|
* gives way to the opening state instead.
|
|
19
19
|
*/
|
|
20
20
|
export type ChatLoadFailure = {
|
|
21
|
-
scope:
|
|
21
|
+
scope: 'initial' | 'live' | 'older';
|
|
22
22
|
error: Error | null;
|
|
23
23
|
retry: () => void;
|
|
24
24
|
retrying: boolean;
|
|
@@ -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,
|
|
1
|
+
{"version":3,"file":"ChatShell.d.ts","sourceRoot":"","sources":["../../src/ui/ChatShell.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,KAAK,EACV,mBAAmB,EACnB,aAAa,EACb,kBAAkB,EACnB,MAAM,4BAA4B,CAAC;AACpC,OAAO,KAAK,EAAE,cAAc,EAAE,uBAAuB,EAAE,MAAM,aAAa,CAAC;AAa3E,OAAO,EAAe,KAAK,wBAAwB,EAAE,MAAM,kBAAkB,CAAC;AAI9E;;;;;;;;;;;;;;GAcG;AACH,MAAM,MAAM,eAAe,GAAG;IAC5B,KAAK,EAAE,SAAS,GAAG,MAAM,GAAG,OAAO,CAAC;IACpC,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IACpB,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,QAAQ,EAAE,OAAO,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG;IAC3B,MAAM,EAAE,cAAc,CAAC;IAEvB;;;OAGG;IACH,eAAe,EAAE,CAAC,OAAO,EAAE,eAAe,KAAK,KAAK,CAAC,SAAS,CAAC;IAG/D,MAAM,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAKzB,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAGzC,aAAa,CAAC,EAAE,CAAC,CAAC,EAAE,GAAG,KAAK,KAAK,CAAC,SAAS,CAAC;IAC5C,gBAAgB,CAAC,EAAE,uBAAuB,CAAC;IAC3C,QAAQ,CAAC,EAAE,kBAAkB,CAAC;IAE9B,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,SAAS,CAAC,EAAE,OAAO,CAAC;IAGpB,mBAAmB,CAAC,EAAE,MAAM,KAAK,CAAC,SAAS,CAAC;IAC5C,mBAAmB,CAAC,EAAE,MAAM,KAAK,CAAC,SAAS,CAAC;IAC5C,kDAAkD;IAClD,YAAY,CAAC,EAAE,MAAM,KAAK,CAAC,SAAS,CAAC;IAGrC,aAAa,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IAC3C,oGAAoG;IACpG,eAAe,CAAC,EAAE,mBAAmB,CAAC;IACtC,qBAAqB,CAAC,EAAE,CAAC,CAAC,EAAE,aAAa,KAAK,KAAK,CAAC,SAAS,CAAC;IAG9D,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAE3B;;;6FAGyF;IACzF,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;;;;OAMG;IACH,iBAAiB,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;IAC7C;qDACiD;IACjD,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;iEAC6D;IAC7D,eAAe,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC,wBAAwB,CAAC,CAAC;CACjE,CAAC;AAgCF;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,cAAc,qBAK9C"}
|
package/dist/ui/ChatShell.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
|
|
1
|
+
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import * as React from
|
|
4
|
-
import { Card, CardHeader, CardContent, CardFooter, Skeleton, Spinner } from
|
|
5
|
-
import { KeyboardAvoidingView } from
|
|
6
|
-
import { useChatMessages } from
|
|
7
|
-
import { useRealtimeChatMessages } from
|
|
8
|
-
import { MessageList } from
|
|
9
|
-
import { Composer } from
|
|
10
|
-
import { ThreadActions } from
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import { Card, CardHeader, CardContent, CardFooter, Skeleton, Spinner, } from '@ttt-productions/ui-core/react';
|
|
5
|
+
import { KeyboardAvoidingView } from '@ttt-productions/mobile-core/react';
|
|
6
|
+
import { useChatMessages } from '../hooks/useChatMessages.js';
|
|
7
|
+
import { useRealtimeChatMessages } from '../realtime/useRealtimeChatMessages.js';
|
|
8
|
+
import { MessageList } from './MessageList.js';
|
|
9
|
+
import { Composer } from './Composer.js';
|
|
10
|
+
import { ThreadActions } from './menus.js';
|
|
11
11
|
/**
|
|
12
12
|
* ChatShell dispatches on `config.transport`. Hooks must be unconditional, so the
|
|
13
13
|
* two transports live in separate inner components (each calls exactly ONE data
|
|
@@ -15,7 +15,7 @@ import { ThreadActions } from "./menus.js";
|
|
|
15
15
|
* (undefined / 'firestore') transport is byte-for-byte the previous behavior.
|
|
16
16
|
*/
|
|
17
17
|
export function ChatShell(props) {
|
|
18
|
-
if (props.config.transport ===
|
|
18
|
+
if (props.config.transport === 'realtime') {
|
|
19
19
|
return _jsx(RealtimeChatShell, { ...props });
|
|
20
20
|
}
|
|
21
21
|
return _jsx(FirestoreChatShell, { ...props });
|
|
@@ -28,8 +28,10 @@ function FirestoreChatShell(props) {
|
|
|
28
28
|
await onSend(text);
|
|
29
29
|
}, [onSend]);
|
|
30
30
|
const { error, olderError, retry, messages, isFetchingOlder } = r;
|
|
31
|
-
const loadFailure = React.useMemo(() =>
|
|
32
|
-
|
|
31
|
+
const loadFailure = React.useMemo(() => error
|
|
32
|
+
? { scope: messages.length > 0 ? 'live' : 'initial', error, retry, retrying: false }
|
|
33
|
+
: null, [error, messages.length, retry]);
|
|
34
|
+
const olderFailure = React.useMemo(() => olderError ? { scope: 'older', error: olderError, retry, retrying: isFetchingOlder } : null, [olderError, retry, isFetchingOlder]);
|
|
33
35
|
return (_jsx(ChatShellView, { ...props, resolved: {
|
|
34
36
|
allowed: r.allowed,
|
|
35
37
|
isInitialLoading: r.isInitialLoading,
|
|
@@ -51,13 +53,13 @@ function RealtimeChatShell(props) {
|
|
|
51
53
|
}
|
|
52
54
|
const r = useRealtimeChatMessages(client);
|
|
53
55
|
const { initialLoadFailed, retry } = r;
|
|
54
|
-
const loadFailure = React.useMemo(() => (initialLoadFailed ? { scope:
|
|
56
|
+
const loadFailure = React.useMemo(() => (initialLoadFailed ? { scope: 'initial', error: null, retry, retrying: false } : null), [initialLoadFailed, retry]);
|
|
55
57
|
const send = React.useCallback(async (text) => {
|
|
56
58
|
const ok = r.send(text);
|
|
57
59
|
// C-B8: surface a closed-socket failure so the Composer keeps the user's text
|
|
58
60
|
// instead of clearing it on a no-op send.
|
|
59
61
|
if (!ok)
|
|
60
|
-
throw new Error(
|
|
62
|
+
throw new Error('chat-send-failed');
|
|
61
63
|
if (onSend)
|
|
62
64
|
await onSend(text); // optional mirror
|
|
63
65
|
}, [r, onSend]);
|
|
@@ -82,9 +84,9 @@ function RealtimeChatShell(props) {
|
|
|
82
84
|
}
|
|
83
85
|
function ChatShellView(props) {
|
|
84
86
|
const { config, header, renderMessage, messageRenderers, handlers, composerPlaceholder, autoFocus = false, renderAboveMessages, renderBelowMessages, renderFooter, onSenderClick, messageGroupKey, renderSenderAdornment, composerDisabled, fillHeight = false, scrollClassName, scrollContainer, renderLoadError, onReadinessChange, resolved, } = props;
|
|
85
|
-
const { allowed, isInitialLoading, messages, fetchOlder, hasOlder, isFetchingOlder, loadFailure, olderFailure, send, retrySend, readAck, status, typing, signalTyping } = resolved;
|
|
87
|
+
const { allowed, isInitialLoading, messages, fetchOlder, hasOlder, isFetchingOlder, loadFailure, olderFailure, send, retrySend, readAck, status, typing, signalTyping, } = resolved;
|
|
86
88
|
// Nothing loaded and the read failed: the failure takes the list's place.
|
|
87
|
-
const initialFailure = loadFailure?.scope ===
|
|
89
|
+
const initialFailure = loadFailure?.scope === 'initial' ? loadFailure : null;
|
|
88
90
|
const ready = allowed && !isInitialLoading && loadFailure === null && resolved.transportCurrent;
|
|
89
91
|
const readinessListener = React.useRef(onReadinessChange);
|
|
90
92
|
React.useEffect(() => {
|
|
@@ -103,7 +105,7 @@ function ChatShellView(props) {
|
|
|
103
105
|
const latestSeq = React.useMemo(() => {
|
|
104
106
|
let max = 0;
|
|
105
107
|
for (const m of messages) {
|
|
106
|
-
const seq = typeof m.meta?.seq ===
|
|
108
|
+
const seq = typeof m.meta?.seq === 'number' ? m.meta?.seq : 0;
|
|
107
109
|
if (seq > max)
|
|
108
110
|
max = seq;
|
|
109
111
|
}
|
|
@@ -111,17 +113,18 @@ function ChatShellView(props) {
|
|
|
111
113
|
}, [messages]);
|
|
112
114
|
// Track tab focus/visibility so the read-ack "focused" lease releases on blur/hide.
|
|
113
115
|
React.useEffect(() => {
|
|
114
|
-
const compute = () => typeof document ===
|
|
115
|
-
(document.visibilityState !==
|
|
116
|
+
const compute = () => typeof document === 'undefined' ||
|
|
117
|
+
(document.visibilityState !== 'hidden' &&
|
|
118
|
+
(typeof document.hasFocus !== 'function' || document.hasFocus()));
|
|
116
119
|
setFocused(compute());
|
|
117
120
|
const onChange = () => setFocused(compute());
|
|
118
|
-
window.addEventListener(
|
|
119
|
-
window.addEventListener(
|
|
120
|
-
document.addEventListener(
|
|
121
|
+
window.addEventListener('focus', onChange);
|
|
122
|
+
window.addEventListener('blur', onChange);
|
|
123
|
+
document.addEventListener('visibilitychange', onChange);
|
|
121
124
|
return () => {
|
|
122
|
-
window.removeEventListener(
|
|
123
|
-
window.removeEventListener(
|
|
124
|
-
document.removeEventListener(
|
|
125
|
+
window.removeEventListener('focus', onChange);
|
|
126
|
+
window.removeEventListener('blur', onChange);
|
|
127
|
+
document.removeEventListener('visibilitychange', onChange);
|
|
125
128
|
};
|
|
126
129
|
}, []);
|
|
127
130
|
// C-B6: advance the authoritative read cursor when the reader is actually at the
|
|
@@ -147,8 +150,8 @@ function ChatShellView(props) {
|
|
|
147
150
|
}, [readAck, latestSeq, atBottom, focused]);
|
|
148
151
|
// In fill mode the Card flexes to its parent's height and the message list fills the
|
|
149
152
|
// space between header and composer; otherwise it's the default natural-height card.
|
|
150
|
-
const cardClassName = fillHeight ?
|
|
151
|
-
const contentClassName = fillHeight ?
|
|
153
|
+
const cardClassName = fillHeight ? 'w-full flex flex-col h-full min-h-0' : 'w-full';
|
|
154
|
+
const contentClassName = fillHeight ? 'p-0 flex-1 min-h-0 flex flex-col' : 'p-0';
|
|
152
155
|
if (!allowed) {
|
|
153
156
|
return _jsx("div", { className: "p-4 text-sm opacity-70", children: "You don't have access to this thread." });
|
|
154
157
|
}
|
|
@@ -166,9 +169,9 @@ function ChatShellView(props) {
|
|
|
166
169
|
// conversation-files section) must not paint an empty bordered strip.
|
|
167
170
|
const aboveMessages = renderAboveMessages?.() ?? null;
|
|
168
171
|
return aboveMessages != null && aboveMessages !== false ? (_jsx("div", { className: "border-b", children: aboveMessages })) : null;
|
|
169
|
-
})(), _jsx(CardContent, { className: contentClassName, children: initialFailure ? (renderLoadError(initialFailure)) : (_jsxs("div", { className: "flex flex-col items-center justify-center gap-2 py-12 text-sm text-muted-foreground", role: "status", "aria-live": "polite", children: [_jsx(Spinner, { size: "sm" }), _jsx("span", { children: "Opening chat\u2026" })] })) }), renderBelowMessages &&
|
|
172
|
+
})(), _jsx(CardContent, { className: contentClassName, children: initialFailure ? (renderLoadError(initialFailure)) : (_jsxs("div", { className: "flex flex-col items-center justify-center gap-2 py-12 text-sm text-muted-foreground", role: "status", "aria-live": "polite", children: [_jsx(Spinner, { size: "sm" }), _jsx("span", { children: "Opening chat\u2026" })] })) }), renderBelowMessages && _jsx("div", { className: "border-t", children: renderBelowMessages() }), renderFooter ? (_jsx(CardFooter, { className: "border-t", children: renderFooter() })) : (_jsx(CardFooter, { className: "border-t", children: _jsx(Skeleton, { className: "h-10 w-full" }) }))] }));
|
|
170
173
|
}
|
|
171
|
-
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 &&
|
|
174
|
+
return (_jsxs(Card, { className: cardClassName, children: [(header || handlers) && (_jsxs(CardHeader, { className: "flex flex-row items-start justify-between gap-4 space-y-0", children: [_jsx("div", { children: header }), _jsx("div", { className: "flex flex-col items-end gap-2", children: _jsx(ThreadActions, { threadId: config.threadId, isAdmin: config.isAdmin, handlers: handlers }) })] })), _jsx(ChatConnectionBanner, { status: status }), renderAboveMessages && _jsx("div", { className: "border-b", children: renderAboveMessages() }), loadFailure?.scope === 'live' && (_jsx("div", { className: "border-b", children: renderLoadError(loadFailure) })), _jsx(CardContent, { className: contentClassName, children: _jsx(MessageList, { messages: messages, currentUserId: config.currentUserId, isAdmin: config.isAdmin, isFetchingOlder: isFetchingOlder, hasOlder: hasOlder, onLoadOlder: () => fetchOlder(), renderMessage: renderMessage, messageRenderers: messageRenderers, showScrollToBottom: showScrollToBottom, onScrollToBottom: () => setShowScrollToBottom(false), onAtBottomChange: setAtBottom, fillHeight: fillHeight, scrollClassName: scrollClassName, scrollContainer: scrollContainer, handlers: handlers, onSenderClick: onSenderClick, messageGroupKey: messageGroupKey, renderSenderAdornment: renderSenderAdornment, onRetrySend: retrySend, olderLoadErrorRow: olderFailure ? renderLoadError(olderFailure) : null }) }), renderBelowMessages && _jsx("div", { className: "border-t", children: renderBelowMessages() }), renderFooter ? (_jsx(CardFooter, { className: "border-t", children: renderFooter() })) : (_jsxs(CardFooter, { className: "border-t flex-col items-stretch gap-1", children: [_jsx(TypingIndicator, { typing: typing }), _jsx(KeyboardAvoidingView, { padding: true, offset: 8, className: "w-full", children: _jsx(Composer, { onSend: send, onTyping: signalTyping, disabled: composerDisabled, autoFocus: autoFocus, placeholder: composerPlaceholder }) })] }))] }));
|
|
172
175
|
}
|
|
173
176
|
/**
|
|
174
177
|
* R13 — connection-state banner. The realtime path sets `status`; once the first
|
|
@@ -177,17 +180,17 @@ function ChatShellView(props) {
|
|
|
177
180
|
* firestore path (`status` undefined) or while live (`open`).
|
|
178
181
|
*/
|
|
179
182
|
function ChatConnectionBanner({ status }) {
|
|
180
|
-
if (status !==
|
|
183
|
+
if (status !== 'reconnecting' && status !== 'closed')
|
|
181
184
|
return null;
|
|
182
|
-
return (_jsx("div", { className: "px-3 py-1.5 text-xs text-center bg-muted text-muted-foreground border-b", role: "status", "aria-live": "polite", children: status ===
|
|
183
|
-
?
|
|
184
|
-
:
|
|
185
|
+
return (_jsx("div", { className: "px-3 py-1.5 text-xs text-center bg-muted text-muted-foreground border-b", role: "status", "aria-live": "polite", children: status === 'reconnecting'
|
|
186
|
+
? 'Reconnecting… messages may be delayed.'
|
|
187
|
+
: 'Disconnected — messages may be out of date.' }));
|
|
185
188
|
}
|
|
186
189
|
/** R14 — typing indicator. `typing` is the set of OTHER users typing (the transport
|
|
187
190
|
* excludes the local user). Generic copy (no name resolution) for v1. */
|
|
188
191
|
function TypingIndicator({ typing }) {
|
|
189
192
|
if (!typing || typing.length === 0)
|
|
190
193
|
return null;
|
|
191
|
-
return (_jsx("div", { className: "text-xs italic text-muted-foreground", "aria-live": "polite", children: typing.length === 1 ?
|
|
194
|
+
return (_jsx("div", { className: "text-xs italic text-muted-foreground", "aria-live": "polite", children: typing.length === 1 ? 'Someone is typing…' : 'Several people are typing…' }));
|
|
192
195
|
}
|
|
193
196
|
//# sourceMappingURL=ChatShell.js.map
|