@ttt-productions/chat-react 0.13.10 → 0.13.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -9,6 +9,7 @@ export type { RealtimeChatClient, RealtimeTransportConfig, ChatClientDiagnostics
9
9
  export type { ChatShellProps, ChatLoadFailure } from "./ui/ChatShell.js";
10
10
  export { ChatShell } from "./ui/ChatShell.js";
11
11
  export { MessageList } from "./ui/MessageList.js";
12
+ export type { ChatScrollContainerProps } from "./ui/MessageList.js";
12
13
  export type { ComposerProps } from "./ui/Composer.js";
13
14
  export { Composer } from "./ui/Composer.js";
14
15
  export type { MessageItemDefaultProps } from "./ui/MessageItemDefault.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AASA,YAAY,EACV,cAAc,EACd,iBAAiB,EACjB,2BAA2B,EAC3B,eAAe,EACf,uBAAuB,GACxB,MAAM,YAAY,CAAC;AAGpB,YAAY,EACV,6BAA6B,EAC7B,kBAAkB,GACnB,MAAM,sCAAsC,CAAC;AAC9C,OAAO,EACL,wBAAwB,EACxB,mBAAmB,EACnB,2BAA2B,EAC3B,uBAAuB,GACxB,MAAM,sCAAsC,CAAC;AAC9C,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAEhD,YAAY,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAC;AACxE,OAAO,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAK7D,OAAO,EACL,wBAAwB,EACxB,iBAAiB,EACjB,aAAa,EACb,WAAW,EACX,uBAAuB,EACvB,oBAAoB,EACpB,qBAAqB,EACrB,uBAAuB,EACvB,mCAAmC,EACnC,gBAAgB,EAChB,gBAAgB,EAChB,YAAY,EACZ,YAAY,EACZ,UAAU,EACV,UAAU,EACV,6BAA6B,GAC9B,MAAM,qBAAqB,CAAC;AAC7B,YAAY,EACV,kBAAkB,EAClB,uBAAuB,EACvB,2BAA2B,EAC3B,yBAAyB,EACzB,6BAA6B,EAC7B,kBAAkB,EAClB,mBAAmB,EACnB,gBAAgB,EAChB,iBAAiB,EACjB,mBAAmB,EACnB,6BAA6B,EAC7B,aAAa,EACb,eAAe,EACf,cAAc,EACd,cAAc,EACd,aAAa,EACb,cAAc,EACd,cAAc,EACd,iBAAiB,EACjB,iBAAiB,EACjB,WAAW,GACZ,MAAM,qBAAqB,CAAC;AAU7B,YAAY,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACzE,OAAO,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAC9C,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAClD,YAAY,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACtD,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,YAAY,EAAE,uBAAuB,EAAE,MAAM,4BAA4B,CAAC;AAC1E,OAAO,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAChE,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9D,OAAO,EAAE,WAAW,EAAE,KAAK,gBAAgB,EAAE,MAAM,qBAAqB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AASA,YAAY,EACV,cAAc,EACd,iBAAiB,EACjB,2BAA2B,EAC3B,eAAe,EACf,uBAAuB,GACxB,MAAM,YAAY,CAAC;AAGpB,YAAY,EACV,6BAA6B,EAC7B,kBAAkB,GACnB,MAAM,sCAAsC,CAAC;AAC9C,OAAO,EACL,wBAAwB,EACxB,mBAAmB,EACnB,2BAA2B,EAC3B,uBAAuB,GACxB,MAAM,sCAAsC,CAAC;AAC9C,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAEhD,YAAY,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAC;AACxE,OAAO,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAK7D,OAAO,EACL,wBAAwB,EACxB,iBAAiB,EACjB,aAAa,EACb,WAAW,EACX,uBAAuB,EACvB,oBAAoB,EACpB,qBAAqB,EACrB,uBAAuB,EACvB,mCAAmC,EACnC,gBAAgB,EAChB,gBAAgB,EAChB,YAAY,EACZ,YAAY,EACZ,UAAU,EACV,UAAU,EACV,6BAA6B,GAC9B,MAAM,qBAAqB,CAAC;AAC7B,YAAY,EACV,kBAAkB,EAClB,uBAAuB,EACvB,2BAA2B,EAC3B,yBAAyB,EACzB,6BAA6B,EAC7B,kBAAkB,EAClB,mBAAmB,EACnB,gBAAgB,EAChB,iBAAiB,EACjB,mBAAmB,EACnB,6BAA6B,EAC7B,aAAa,EACb,eAAe,EACf,cAAc,EACd,cAAc,EACd,aAAa,EACb,cAAc,EACd,cAAc,EACd,iBAAiB,EACjB,iBAAiB,EACjB,WAAW,GACZ,MAAM,qBAAqB,CAAC;AAU7B,YAAY,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACzE,OAAO,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAC9C,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAClD,YAAY,EAAE,wBAAwB,EAAE,MAAM,qBAAqB,CAAC;AACpE,YAAY,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACtD,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,YAAY,EAAE,uBAAuB,EAAE,MAAM,4BAA4B,CAAC;AAC1E,OAAO,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAChE,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9D,OAAO,EAAE,WAAW,EAAE,KAAK,gBAAgB,EAAE,MAAM,qBAAqB,CAAC"}
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAsBb,OAAO,EACL,wBAAwB,EACxB,mBAAmB,EACnB,2BAA2B,EAC3B,uBAAuB,GACxB,MAAM,sCAAsC,CAAC;AAC9C,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAGhD,OAAO,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAE7D,+EAA+E;AAC/E,kFAAkF;AAClF,oFAAoF;AACpF,OAAO,EACL,wBAAwB,EACxB,iBAAiB,EACjB,aAAa,EACb,WAAW,EACX,uBAAuB,EACvB,oBAAoB,EACpB,qBAAqB,EACrB,uBAAuB,EACvB,mCAAmC,EACnC,gBAAgB,EAChB,gBAAgB,EAChB,YAAY,EACZ,YAAY,EACZ,UAAU,EACV,UAAU,EACV,6BAA6B,GAC9B,MAAM,qBAAqB,CAAC;AAkC7B,OAAO,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAC9C,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAElD,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAE5C,OAAO,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAChE,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9D,OAAO,EAAE,WAAW,EAAyB,MAAM,qBAAqB,CAAC","sourcesContent":["\"use client\";\n\n// @ttt-productions/chat-react — chat React UI, hooks, the Firebase-client\n// adapter config types, and React render types. Depends on the pure\n// @ttt-productions/chat-core for contracts, parser, and grouping.\n//\n// Apps import chat CSS via \"@ttt-productions/chat-react/styles\".\n\n// React-coupled config + render types (previously on chat-core root).\nexport type {\n ChatCoreConfig,\n ChatTransportMode,\n ChatRealtimeTransportConfig,\n MessageRenderer,\n MessageRendererRegistry,\n} from \"./types.js\";\n\n// Name-resolver context.\nexport type {\n ChatNameResolverProviderProps,\n UnresolvedChatName,\n} from \"./context/ChatNameResolverContext.js\";\nexport {\n ChatNameResolverProvider,\n useChatNameResolver,\n useOptionalChatNameResolver,\n useSenderNameResolution,\n} from \"./context/ChatNameResolverContext.js\";\nexport { SenderName } from \"./ui/SenderName.js\";\n\nexport type { UseChatMessagesResult } from \"./hooks/useChatMessages.js\";\nexport { useChatMessages } from \"./hooks/useChatMessages.js\";\n\n// Realtime (Cloudflare Durable Object) transport — the client half of the chat\n// worker wire protocol. Opt-in via `ChatCoreConfig.transport === 'realtime'`; the\n// firestore transport stays the unchanged default. See docs/packages/chat-react.md.\nexport {\n createRealtimeChatClient,\n createInboxClient,\n ChannelClient,\n InboxClient,\n useRealtimeChatMessages,\n browserSocketFactory,\n ChatAccessDeniedError,\n isChatAccessDeniedError,\n CHAT_INITIAL_LOAD_FAILURE_BUDGET_MS,\n CHAT_SUBPROTOCOL,\n CHAT_CLOSE_CODES,\n CLIENT_FRAME,\n SERVER_FRAME,\n buildFrame,\n parseFrame,\n CHAT_CLIENT_DIAGNOSTIC_EVENTS,\n} from \"./realtime/index.js\";\nexport type {\n RealtimeChatClient,\n RealtimeTransportConfig,\n ChatClientDiagnosticsOption,\n ChatClientDiagnosticsSink,\n ChatClientDiagnosticEventName,\n ChannelClientState,\n ChannelClientConfig,\n InboxClientState,\n InboxClientConfig,\n ChatMarkReadOutcome,\n UseRealtimeChatMessagesResult,\n GrantProvider,\n TransportTimers,\n RealtimeStatus,\n RealtimeSocket,\n SocketFactory,\n SocketHandlers,\n WireMessageRow,\n WireInboxSnapshot,\n WireRegistryEntry,\n ServerFrame,\n} from \"./realtime/index.js\";\n\n// TODO (re-export when a consumer needs them): the moderation-overlay helpers\n// `RevisionKind`, `ModerationOverlay`, `applyModerationOverlay`, `overlayFromRow`,\n// and `MODERATION_REDACTED_TEXT` live in `./realtime` but are intentionally NOT\n// re-exported from this package root yet — the consuming app's renderer reads moderation\n// state off `message.meta` (the realtime client applies the overlay internally), so\n// nothing imports them from here today. Add them to the block above when a consumer\n// genuinely needs to apply the overlay directly. (Comment-only; rides the next publish.)\n\nexport type { ChatShellProps, ChatLoadFailure } from \"./ui/ChatShell.js\";\nexport { ChatShell } from \"./ui/ChatShell.js\";\nexport { MessageList } from \"./ui/MessageList.js\";\nexport type { ComposerProps } from \"./ui/Composer.js\";\nexport { Composer } from \"./ui/Composer.js\";\nexport type { MessageItemDefaultProps } from \"./ui/MessageItemDefault.js\";\nexport { MessageItemDefault } from \"./ui/MessageItemDefault.js\";\nexport { MessageActions, ThreadActions } from \"./ui/menus.js\";\nexport { MessageText, type MessageTextProps } from \"./ui/MessageText.js\";\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAsBb,OAAO,EACL,wBAAwB,EACxB,mBAAmB,EACnB,2BAA2B,EAC3B,uBAAuB,GACxB,MAAM,sCAAsC,CAAC;AAC9C,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAGhD,OAAO,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAE7D,+EAA+E;AAC/E,kFAAkF;AAClF,oFAAoF;AACpF,OAAO,EACL,wBAAwB,EACxB,iBAAiB,EACjB,aAAa,EACb,WAAW,EACX,uBAAuB,EACvB,oBAAoB,EACpB,qBAAqB,EACrB,uBAAuB,EACvB,mCAAmC,EACnC,gBAAgB,EAChB,gBAAgB,EAChB,YAAY,EACZ,YAAY,EACZ,UAAU,EACV,UAAU,EACV,6BAA6B,GAC9B,MAAM,qBAAqB,CAAC;AAkC7B,OAAO,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAC9C,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAGlD,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAE5C,OAAO,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAChE,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9D,OAAO,EAAE,WAAW,EAAyB,MAAM,qBAAqB,CAAC","sourcesContent":["\"use client\";\n\n// @ttt-productions/chat-react — chat React UI, hooks, the Firebase-client\n// adapter config types, and React render types. Depends on the pure\n// @ttt-productions/chat-core for contracts, parser, and grouping.\n//\n// Apps import chat CSS via \"@ttt-productions/chat-react/styles\".\n\n// React-coupled config + render types (previously on chat-core root).\nexport type {\n ChatCoreConfig,\n ChatTransportMode,\n ChatRealtimeTransportConfig,\n MessageRenderer,\n MessageRendererRegistry,\n} from \"./types.js\";\n\n// Name-resolver context.\nexport type {\n ChatNameResolverProviderProps,\n UnresolvedChatName,\n} from \"./context/ChatNameResolverContext.js\";\nexport {\n ChatNameResolverProvider,\n useChatNameResolver,\n useOptionalChatNameResolver,\n useSenderNameResolution,\n} from \"./context/ChatNameResolverContext.js\";\nexport { SenderName } from \"./ui/SenderName.js\";\n\nexport type { UseChatMessagesResult } from \"./hooks/useChatMessages.js\";\nexport { useChatMessages } from \"./hooks/useChatMessages.js\";\n\n// Realtime (Cloudflare Durable Object) transport — the client half of the chat\n// worker wire protocol. Opt-in via `ChatCoreConfig.transport === 'realtime'`; the\n// firestore transport stays the unchanged default. See docs/packages/chat-react.md.\nexport {\n createRealtimeChatClient,\n createInboxClient,\n ChannelClient,\n InboxClient,\n useRealtimeChatMessages,\n browserSocketFactory,\n ChatAccessDeniedError,\n isChatAccessDeniedError,\n CHAT_INITIAL_LOAD_FAILURE_BUDGET_MS,\n CHAT_SUBPROTOCOL,\n CHAT_CLOSE_CODES,\n CLIENT_FRAME,\n SERVER_FRAME,\n buildFrame,\n parseFrame,\n CHAT_CLIENT_DIAGNOSTIC_EVENTS,\n} from \"./realtime/index.js\";\nexport type {\n RealtimeChatClient,\n RealtimeTransportConfig,\n ChatClientDiagnosticsOption,\n ChatClientDiagnosticsSink,\n ChatClientDiagnosticEventName,\n ChannelClientState,\n ChannelClientConfig,\n InboxClientState,\n InboxClientConfig,\n ChatMarkReadOutcome,\n UseRealtimeChatMessagesResult,\n GrantProvider,\n TransportTimers,\n RealtimeStatus,\n RealtimeSocket,\n SocketFactory,\n SocketHandlers,\n WireMessageRow,\n WireInboxSnapshot,\n WireRegistryEntry,\n ServerFrame,\n} from \"./realtime/index.js\";\n\n// TODO (re-export when a consumer needs them): the moderation-overlay helpers\n// `RevisionKind`, `ModerationOverlay`, `applyModerationOverlay`, `overlayFromRow`,\n// and `MODERATION_REDACTED_TEXT` live in `./realtime` but are intentionally NOT\n// re-exported from this package root yet — the consuming app's renderer reads moderation\n// state off `message.meta` (the realtime client applies the overlay internally), so\n// nothing imports them from here today. Add them to the block above when a consumer\n// genuinely needs to apply the overlay directly. (Comment-only; rides the next publish.)\n\nexport type { ChatShellProps, ChatLoadFailure } from \"./ui/ChatShell.js\";\nexport { ChatShell } from \"./ui/ChatShell.js\";\nexport { MessageList } from \"./ui/MessageList.js\";\nexport type { ChatScrollContainerProps } from \"./ui/MessageList.js\";\nexport type { ComposerProps } from \"./ui/Composer.js\";\nexport { Composer } from \"./ui/Composer.js\";\nexport type { MessageItemDefaultProps } from \"./ui/MessageItemDefault.js\";\nexport { MessageItemDefault } from \"./ui/MessageItemDefault.js\";\nexport { MessageActions, ThreadActions } from \"./ui/menus.js\";\nexport { MessageText, type MessageTextProps } from \"./ui/MessageText.js\";\n"]}
@@ -1,6 +1,7 @@
1
1
  import * as React from "react";
2
2
  import type { ModerationHandlers } from "@ttt-productions/chat-core";
3
3
  import type { ChatCoreConfig, MessageRendererRegistry } from "../types.js";
4
+ import { type ChatScrollContainerProps } from "./MessageList.js";
4
5
  /**
5
6
  * A read the chat could not complete, handed to the app's `renderLoadError`:
6
7
  * - `initial` — nothing loaded: the first open failed (the realtime client's attempts
@@ -50,6 +51,9 @@ export type ChatShellProps = {
50
51
  /** Advanced override for the scrollable region's class (forwarded to MessageList).
51
52
  * Usually unnecessary — prefer `fillHeight`. */
52
53
  scrollClassName?: string;
54
+ /** Renders the scrollable message region (forwarded to MessageList) in place of the default div,
55
+ * so the consumer can give it its own surface component. */
56
+ scrollContainer?: React.ComponentType<ChatScrollContainerProps>;
53
57
  };
54
58
  /**
55
59
  * ChatShell dispatches on `config.transport`. Hooks must be unconditional, so the
@@ -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,EAAiB,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AACpF,OAAO,KAAK,EACV,cAAc,EACd,uBAAuB,EACxB,MAAM,aAAa,CAAC;AAUrB;;;;;;;;;;;;;;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;IAG3C,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAE3B;;;6FAGyF;IACzF,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;qDACiD;IACjD,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B,CAAC;AA8BF;;;;;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;AAMrB,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;IAG3C,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAE3B;;;6FAGyF;IACzF,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;qDACiD;IACjD,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;iEAC6D;IAC7D,eAAe,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC,wBAAwB,CAAC,CAAC;CACjE,CAAC;AA8BF;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,cAAc,qBAK9C"}
@@ -79,7 +79,7 @@ function RealtimeChatShell(props) {
79
79
  } }));
80
80
  }
81
81
  function ChatShellView(props) {
82
- const { config, header, renderMessage, messageRenderers, handlers, composerPlaceholder, autoFocus = false, renderAboveMessages, renderBelowMessages, renderFooter, onSenderClick, composerDisabled, fillHeight = false, scrollClassName, renderLoadError, resolved, } = props;
82
+ const { config, header, renderMessage, messageRenderers, handlers, composerPlaceholder, autoFocus = false, renderAboveMessages, renderBelowMessages, renderFooter, onSenderClick, composerDisabled, fillHeight = false, scrollClassName, scrollContainer, renderLoadError, resolved, } = props;
83
83
  const { allowed, isInitialLoading, messages, fetchOlder, hasOlder, isFetchingOlder, loadFailure, olderFailure, send, retrySend, readAck, status, typing, signalTyping } = resolved;
84
84
  // Nothing loaded and the read failed: the failure takes the list's place.
85
85
  const initialFailure = loadFailure?.scope === "initial" ? loadFailure : null;
@@ -158,7 +158,7 @@ function ChatShellView(props) {
158
158
  return aboveMessages != null && aboveMessages !== false ? (_jsx("div", { className: "border-b", children: aboveMessages })) : null;
159
159
  })(), _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" }) }))] }));
160
160
  }
161
- 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, handlers: handlers, onSenderClick: onSenderClick, 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 }) })] }))] }));
161
+ 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, 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 }) })] }))] }));
162
162
  }
163
163
  /**
164
164
  * 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, Spinner } from \"@ttt-productions/ui-core/react\";\nimport { KeyboardAvoidingView } from \"@ttt-productions/mobile-core/react\";\nimport { useChatMessages } from \"../hooks/useChatMessages.js\";\nimport { useRealtimeChatMessages } from \"../realtime/useRealtimeChatMessages.js\";\nimport type { RealtimeChatClient } from \"../realtime/transport.js\";\nimport { MessageList } from \"./MessageList.js\";\nimport { Composer } from \"./Composer.js\";\nimport { ThreadActions } from \"./menus.js\";\n\n/**\n * A read the chat could not complete, handed to the app's `renderLoadError`:\n * - `initial` — nothing loaded: the first open failed (the realtime client's attempts\n * failed for its budget, or the Firestore listener failed before any message). It\n * takes the place of the message list.\n * - `live` — the Firestore listener failed after messages loaded. The messages stay,\n * with this band above them.\n * - `older` — an older page failed to load. It renders at the top of the list, which\n * stops asking for older pages until `retry`.\n * `error` is the read's own error (null on the realtime transport); `retry` re-runs what\n * failed — the realtime transport also keeps retrying in the background. `retrying` is true\n * while a retry of this failure is running and the failure is still shown (an older page being\n * read again), so the app's Retry shows its pending state. A retried initial or live failure\n * gives way to the opening state instead.\n */\nexport type ChatLoadFailure = {\n scope: \"initial\" | \"live\" | \"older\";\n error: Error | null;\n retry: () => void;\n retrying: boolean;\n};\n\nexport type ChatShellProps = {\n config: ChatCoreConfig;\n\n /**\n * Renders every failed read (see {@link ChatLoadFailure}) — the app's own error state and\n * copy. Required: a chat that cannot load must say so, and the words are the app's.\n */\n renderLoadError: (failure: ChatLoadFailure) => React.ReactNode;\n\n // Header\n header?: React.ReactNode;\n\n // Send handler — text-only. On the realtime transport this is OPTIONAL: the\n // shell sends through the DO socket. If provided, it is still called (so a\n // consumer can mirror to analytics), but the socket send is authoritative.\n onSend?: (text: string) => Promise<void>;\n\n // Message rendering\n renderMessage?: (m: any) => React.ReactNode;\n messageRenderers?: MessageRendererRegistry;\n handlers?: ModerationHandlers;\n\n composerPlaceholder?: string;\n autoFocus?: boolean;\n\n // Three render slots\n renderAboveMessages?: () => React.ReactNode;\n renderBelowMessages?: () => React.ReactNode;\n /** If provided, replaces the Composer entirely */\n renderFooter?: () => React.ReactNode;\n\n // Sender interaction\n onSenderClick?: (senderId: string) => void;\n\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 failed read that replaces or annotates the list (initial / live), or null. */\n loadFailure: ChatLoadFailure | null;\n /** The failed older page, or null. */\n olderFailure: ChatLoadFailure | null;\n /** The send handler the Composer calls (socket send on realtime, prop on firestore). */\n send: (text: string) => Promise<void>;\n /** Retry a failed realtime send by its original clientMessageId (realtime only;\n * absent on firestore). Wired to MessageItemDefault's failed-bubble retry. */\n retrySend?: (clientMessageId: string) => void;\n /** Advance the authoritative read cursor (realtime only; absent on the firestore path).\n * Returns whether the ack was actually sent so the caller advances its local cursor only on success (M2). */\n readAck?: (latestSeq: number, focused: boolean) => boolean;\n /** Realtime connection status — drives the disconnected banner (R13). Absent on firestore. */\n status?: string;\n /** uids currently typing (realtime only) — drives the typing indicator (R14). */\n typing?: string[];\n /** Signal the local user is typing (coalesced by the transport). Wired to the Composer (R14). */\n signalTyping?: () => void;\n};\n\n/**\n * ChatShell dispatches on `config.transport`. Hooks must be unconditional, so the\n * two transports live in separate inner components (each calls exactly ONE data\n * hook) and both render the shared presentational `ChatShellView`. The default\n * (undefined / 'firestore') transport is byte-for-byte the previous behavior.\n */\nexport function ChatShell(props: ChatShellProps) {\n if (props.config.transport === \"realtime\") {\n return <RealtimeChatShell {...props} />;\n }\n return <FirestoreChatShell {...props} />;\n}\n\nfunction FirestoreChatShell(props: ChatShellProps) {\n const { config, onSend } = props;\n const r = useChatMessages(config);\n const send = React.useCallback(\n async (text: string) => {\n if (onSend) await onSend(text);\n },\n [onSend],\n );\n const { error, olderError, retry, messages, isFetchingOlder } = r;\n const loadFailure = React.useMemo<ChatLoadFailure | null>(\n () => (error ? { scope: messages.length > 0 ? \"live\" : \"initial\", error, retry, retrying: false } : null),\n [error, messages.length, retry],\n );\n const olderFailure = React.useMemo<ChatLoadFailure | null>(\n () => (olderError ? { scope: \"older\", error: olderError, retry, retrying: isFetchingOlder } : null),\n [olderError, retry, isFetchingOlder],\n );\n return (\n <ChatShellView\n {...props}\n resolved={{\n allowed: r.allowed,\n isInitialLoading: r.isInitialLoading,\n messages: r.messages,\n fetchOlder: r.fetchOlder,\n hasOlder: r.hasOlder,\n isFetchingOlder: r.isFetchingOlder,\n loadFailure,\n olderFailure,\n send,\n }}\n />\n );\n}\n\nfunction RealtimeChatShell(props: ChatShellProps) {\n const { config, onSend } = props;\n const client = config.realtime?.client as RealtimeChatClient | undefined;\n if (!client) {\n throw new Error(\n \"[ChatShell] transport 'realtime' requires config.realtime.client (createRealtimeChatClient(...))\",\n );\n }\n const r = useRealtimeChatMessages(client);\n const { initialLoadFailed, retry } = r;\n const loadFailure = React.useMemo<ChatLoadFailure | null>(\n () => (initialLoadFailed ? { scope: \"initial\", error: null, retry, retrying: false } : null),\n [initialLoadFailed, retry],\n );\n const send = React.useCallback(\n async (text: string) => {\n const ok = r.send(text);\n // C-B8: surface a closed-socket failure so the Composer keeps the user's text\n // instead of clearing it on a no-op send.\n if (!ok) throw new Error(\"chat-send-failed\");\n if (onSend) await onSend(text); // optional mirror\n },\n [r, onSend],\n );\n return (\n <ChatShellView\n {...props}\n resolved={{\n // The app's access fact AND the grant's: a terminal denial closes it either way.\n allowed: config.allowed && r.allowed,\n isInitialLoading: r.isInitialLoading,\n messages: r.messages,\n fetchOlder: r.fetchOlder,\n hasOlder: r.hasOlder,\n isFetchingOlder: r.isFetchingOlder,\n loadFailure,\n olderFailure: null,\n send,\n retrySend: r.retrySend,\n readAck: r.readAck,\n status: r.status,\n typing: r.typing,\n signalTyping: r.signalTyping,\n }}\n />\n );\n}\n\nfunction ChatShellView(props: ChatShellProps & { resolved: ResolvedChat }) {\n const {\n config,\n header,\n renderMessage,\n messageRenderers,\n handlers,\n composerPlaceholder,\n autoFocus = false,\n renderAboveMessages,\n renderBelowMessages,\n renderFooter,\n onSenderClick,\n composerDisabled,\n fillHeight = false,\n scrollClassName,\n renderLoadError,\n resolved,\n } = props;\n\n const { allowed, isInitialLoading, messages, fetchOlder, hasOlder, isFetchingOlder, loadFailure, olderFailure, send, retrySend, readAck, status, typing, signalTyping } = resolved;\n // Nothing loaded and the read failed: the failure takes the list's place.\n const initialFailure = loadFailure?.scope === \"initial\" ? loadFailure : null;\n\n const [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 || initialFailure) {\n // Before the first authoritative snapshot/history result, show an HONEST working\n // state — a visible \"Opening chat…\" spinner in a polite live region — NOT a blank\n // parchment/skeleton box (which read as an empty chat) and NOT the reconnect banner\n // (which must not compete with the initial-opening message). Only the MessageList\n // region is replaced: the consumer's header + actions + the three render-slots +\n // footer 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 {initialFailure ? (\n renderLoadError(initialFailure)\n ) : (\n <div\n className=\"flex flex-col items-center justify-center gap-2 py-12 text-sm text-muted-foreground\"\n role=\"status\"\n aria-live=\"polite\"\n >\n <Spinner size=\"sm\" />\n <span>Opening chat…</span>\n </div>\n )}\n </CardContent>\n {renderBelowMessages && (\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 {/* The listener failed after messages loaded: they stay, with the failure above them. */}\n {loadFailure?.scope === \"live\" && <div className=\"border-b\">{renderLoadError(loadFailure)}</div>}\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 olderLoadErrorRow={olderFailure ? renderLoadError(olderFailure) : null}\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
+ {"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, Spinner } from \"@ttt-productions/ui-core/react\";\nimport { KeyboardAvoidingView } from \"@ttt-productions/mobile-core/react\";\nimport { useChatMessages } from \"../hooks/useChatMessages.js\";\nimport { useRealtimeChatMessages } from \"../realtime/useRealtimeChatMessages.js\";\nimport type { RealtimeChatClient } from \"../realtime/transport.js\";\nimport { MessageList, type ChatScrollContainerProps } from \"./MessageList.js\";\nimport { Composer } from \"./Composer.js\";\nimport { ThreadActions } from \"./menus.js\";\n\n/**\n * A read the chat could not complete, handed to the app's `renderLoadError`:\n * - `initial` — nothing loaded: the first open failed (the realtime client's attempts\n * failed for its budget, or the Firestore listener failed before any message). It\n * takes the place of the message list.\n * - `live` — the Firestore listener failed after messages loaded. The messages stay,\n * with this band above them.\n * - `older` — an older page failed to load. It renders at the top of the list, which\n * stops asking for older pages until `retry`.\n * `error` is the read's own error (null on the realtime transport); `retry` re-runs what\n * failed — the realtime transport also keeps retrying in the background. `retrying` is true\n * while a retry of this failure is running and the failure is still shown (an older page being\n * read again), so the app's Retry shows its pending state. A retried initial or live failure\n * gives way to the opening state instead.\n */\nexport type ChatLoadFailure = {\n scope: \"initial\" | \"live\" | \"older\";\n error: Error | null;\n retry: () => void;\n retrying: boolean;\n};\n\nexport type ChatShellProps = {\n config: ChatCoreConfig;\n\n /**\n * Renders every failed read (see {@link ChatLoadFailure}) — the app's own error state and\n * copy. Required: a chat that cannot load must say so, and the words are the app's.\n */\n renderLoadError: (failure: ChatLoadFailure) => React.ReactNode;\n\n // Header\n header?: React.ReactNode;\n\n // Send handler — text-only. On the realtime transport this is OPTIONAL: the\n // shell sends through the DO socket. If provided, it is still called (so a\n // consumer can mirror to analytics), but the socket send is authoritative.\n onSend?: (text: string) => Promise<void>;\n\n // Message rendering\n renderMessage?: (m: any) => React.ReactNode;\n messageRenderers?: MessageRendererRegistry;\n handlers?: ModerationHandlers;\n\n composerPlaceholder?: string;\n autoFocus?: boolean;\n\n // Three render slots\n renderAboveMessages?: () => React.ReactNode;\n renderBelowMessages?: () => React.ReactNode;\n /** If provided, replaces the Composer entirely */\n renderFooter?: () => React.ReactNode;\n\n // Sender interaction\n onSenderClick?: (senderId: string) => void;\n\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 /** Renders the scrollable message region (forwarded to MessageList) in place of the default div,\n * so the consumer can give it its own surface component. */\n scrollContainer?: React.ComponentType<ChatScrollContainerProps>;\n};\n\n/** The resolved data a transport hook hands the presentational view. */\ntype ResolvedChat = {\n allowed: boolean;\n isInitialLoading: boolean;\n messages: ChatMessageV1[];\n fetchOlder: () => Promise<void>;\n hasOlder: boolean;\n isFetchingOlder: boolean;\n /** The failed read that replaces or annotates the list (initial / live), or null. */\n loadFailure: ChatLoadFailure | null;\n /** The failed older page, or null. */\n olderFailure: ChatLoadFailure | null;\n /** The send handler the Composer calls (socket send on realtime, prop on firestore). */\n send: (text: string) => Promise<void>;\n /** Retry a failed realtime send by its original clientMessageId (realtime only;\n * absent on firestore). Wired to MessageItemDefault's failed-bubble retry. */\n retrySend?: (clientMessageId: string) => void;\n /** Advance the authoritative read cursor (realtime only; absent on the firestore path).\n * Returns whether the ack was actually sent so the caller advances its local cursor only on success (M2). */\n readAck?: (latestSeq: number, focused: boolean) => boolean;\n /** Realtime connection status — drives the disconnected banner (R13). Absent on firestore. */\n status?: string;\n /** uids currently typing (realtime only) — drives the typing indicator (R14). */\n typing?: string[];\n /** Signal the local user is typing (coalesced by the transport). Wired to the Composer (R14). */\n signalTyping?: () => void;\n};\n\n/**\n * ChatShell dispatches on `config.transport`. Hooks must be unconditional, so the\n * two transports live in separate inner components (each calls exactly ONE data\n * hook) and both render the shared presentational `ChatShellView`. The default\n * (undefined / 'firestore') transport is byte-for-byte the previous behavior.\n */\nexport function ChatShell(props: ChatShellProps) {\n if (props.config.transport === \"realtime\") {\n return <RealtimeChatShell {...props} />;\n }\n return <FirestoreChatShell {...props} />;\n}\n\nfunction FirestoreChatShell(props: ChatShellProps) {\n const { config, onSend } = props;\n const r = useChatMessages(config);\n const send = React.useCallback(\n async (text: string) => {\n if (onSend) await onSend(text);\n },\n [onSend],\n );\n const { error, olderError, retry, messages, isFetchingOlder } = r;\n const loadFailure = React.useMemo<ChatLoadFailure | null>(\n () => (error ? { scope: messages.length > 0 ? \"live\" : \"initial\", error, retry, retrying: false } : null),\n [error, messages.length, retry],\n );\n const olderFailure = React.useMemo<ChatLoadFailure | null>(\n () => (olderError ? { scope: \"older\", error: olderError, retry, retrying: isFetchingOlder } : null),\n [olderError, retry, isFetchingOlder],\n );\n return (\n <ChatShellView\n {...props}\n resolved={{\n allowed: r.allowed,\n isInitialLoading: r.isInitialLoading,\n messages: r.messages,\n fetchOlder: r.fetchOlder,\n hasOlder: r.hasOlder,\n isFetchingOlder: r.isFetchingOlder,\n loadFailure,\n olderFailure,\n send,\n }}\n />\n );\n}\n\nfunction RealtimeChatShell(props: ChatShellProps) {\n const { config, onSend } = props;\n const client = config.realtime?.client as RealtimeChatClient | undefined;\n if (!client) {\n throw new Error(\n \"[ChatShell] transport 'realtime' requires config.realtime.client (createRealtimeChatClient(...))\",\n );\n }\n const r = useRealtimeChatMessages(client);\n const { initialLoadFailed, retry } = r;\n const loadFailure = React.useMemo<ChatLoadFailure | null>(\n () => (initialLoadFailed ? { scope: \"initial\", error: null, retry, retrying: false } : null),\n [initialLoadFailed, retry],\n );\n const send = React.useCallback(\n async (text: string) => {\n const ok = r.send(text);\n // C-B8: surface a closed-socket failure so the Composer keeps the user's text\n // instead of clearing it on a no-op send.\n if (!ok) throw new Error(\"chat-send-failed\");\n if (onSend) await onSend(text); // optional mirror\n },\n [r, onSend],\n );\n return (\n <ChatShellView\n {...props}\n resolved={{\n // The app's access fact AND the grant's: a terminal denial closes it either way.\n allowed: config.allowed && r.allowed,\n isInitialLoading: r.isInitialLoading,\n messages: r.messages,\n fetchOlder: r.fetchOlder,\n hasOlder: r.hasOlder,\n isFetchingOlder: r.isFetchingOlder,\n loadFailure,\n olderFailure: null,\n send,\n retrySend: r.retrySend,\n readAck: r.readAck,\n status: r.status,\n typing: r.typing,\n signalTyping: r.signalTyping,\n }}\n />\n );\n}\n\nfunction ChatShellView(props: ChatShellProps & { resolved: ResolvedChat }) {\n const {\n config,\n header,\n renderMessage,\n messageRenderers,\n handlers,\n composerPlaceholder,\n autoFocus = false,\n renderAboveMessages,\n renderBelowMessages,\n renderFooter,\n onSenderClick,\n composerDisabled,\n fillHeight = false,\n scrollClassName,\n scrollContainer,\n renderLoadError,\n resolved,\n } = props;\n\n const { allowed, isInitialLoading, messages, fetchOlder, hasOlder, isFetchingOlder, loadFailure, olderFailure, send, retrySend, readAck, status, typing, signalTyping } = resolved;\n // Nothing loaded and the read failed: the failure takes the list's place.\n const initialFailure = loadFailure?.scope === \"initial\" ? loadFailure : null;\n\n const [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 || initialFailure) {\n // Before the first authoritative snapshot/history result, show an HONEST working\n // state — a visible \"Opening chat…\" spinner in a polite live region — NOT a blank\n // parchment/skeleton box (which read as an empty chat) and NOT the reconnect banner\n // (which must not compete with the initial-opening message). Only the MessageList\n // region is replaced: the consumer's header + actions + the three render-slots +\n // footer 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 {initialFailure ? (\n renderLoadError(initialFailure)\n ) : (\n <div\n className=\"flex flex-col items-center justify-center gap-2 py-12 text-sm text-muted-foreground\"\n role=\"status\"\n aria-live=\"polite\"\n >\n <Spinner size=\"sm\" />\n <span>Opening chat…</span>\n </div>\n )}\n </CardContent>\n {renderBelowMessages && (\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 {/* The listener failed after messages loaded: they stay, with the failure above them. */}\n {loadFailure?.scope === \"live\" && <div className=\"border-b\">{renderLoadError(loadFailure)}</div>}\n\n <CardContent className={contentClassName}>\n <MessageList\n messages={messages}\n currentUserId={config.currentUserId}\n isAdmin={config.isAdmin}\n isFetchingOlder={isFetchingOlder}\n hasOlder={hasOlder}\n onLoadOlder={() => fetchOlder()}\n renderMessage={renderMessage}\n messageRenderers={messageRenderers}\n showScrollToBottom={showScrollToBottom}\n onScrollToBottom={() => setShowScrollToBottom(false)}\n onAtBottomChange={setAtBottom}\n fillHeight={fillHeight}\n scrollClassName={scrollClassName}\n scrollContainer={scrollContainer}\n handlers={handlers}\n onSenderClick={onSenderClick}\n onRetrySend={retrySend}\n olderLoadErrorRow={olderFailure ? renderLoadError(olderFailure) : null}\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,6 +1,14 @@
1
1
  import * as React from "react";
2
2
  import type { ChatMessageV1, ModerationHandlers } from "@ttt-productions/chat-core";
3
3
  import type { MessageRendererRegistry } from "../types.js";
4
+ /** What `MessageList` hands a custom scroll container: the element it must render as the scrollable
5
+ * message region, with this ref, class, scroll handler, and children. */
6
+ export type ChatScrollContainerProps = {
7
+ ref: React.Ref<HTMLDivElement>;
8
+ className: string;
9
+ onScroll: React.UIEventHandler<HTMLDivElement>;
10
+ children: React.ReactNode;
11
+ };
4
12
  export declare function MessageList(props: {
5
13
  messages: ChatMessageV1[];
6
14
  currentUserId: string;
@@ -19,6 +27,10 @@ export declare function MessageList(props: {
19
27
  * `h-[400px]` (fixed-height card) unless `fillHeight` is set, in which case it is
20
28
  * `flex-1 min-h-0`. */
21
29
  scrollClassName?: string;
30
+ /** Renders the scrollable message region in place of the default div, so the consumer can give it
31
+ * its own surface component. It must render one element carrying the given ref, class, scroll
32
+ * handler, and children. */
33
+ scrollContainer?: React.ComponentType<ChatScrollContainerProps>;
22
34
  /** Fill the parent's height instead of a fixed `h-[400px]`. Makes the list a flex-col
23
35
  * that flexes to fill its container, so a bounded-height page panel scrolls inside.
24
36
  * The consuming layout must give MessageList's ancestor a bounded height (ChatShell's
@@ -1 +1 @@
1
- {"version":3,"file":"MessageList.d.ts","sourceRoot":"","sources":["../../src/ui/MessageList.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,KAAK,EAAE,aAAa,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAEpF,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,aAAa,CAAC;AAI3D,wBAAgB,WAAW,CAAC,KAAK,EAAE;IACjC,QAAQ,EAAE,aAAa,EAAE,CAAC;IAC1B,aAAa,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,OAAO,CAAC;IAEjB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,IAAI,CAAC;IAEzB,aAAa,CAAC,EAAE,CAAC,CAAC,EAAE,aAAa,KAAK,KAAK,CAAC,SAAS,CAAC;IACtD,gBAAgB,CAAC,EAAE,uBAAuB,CAAC;IAE3C,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,gBAAgB,CAAC,EAAE,MAAM,IAAI,CAAC;IAC9B;4FACwF;IACxF,gBAAgB,CAAC,EAAE,CAAC,QAAQ,EAAE,OAAO,KAAK,IAAI,CAAC;IAC/C;;4BAEwB;IACxB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;;yEAGqE;IACrE,UAAU,CAAC,EAAE,OAAO,CAAC;IAErB,QAAQ,CAAC,EAAE,kBAAkB,CAAC;IAC9B,aAAa,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IAC3C,iFAAiF;IACjF,WAAW,CAAC,EAAE,CAAC,eAAe,EAAE,MAAM,KAAK,IAAI,CAAC;IAChD;;;;OAIG;IACH,iBAAiB,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;CACrC,qBAyMA"}
1
+ {"version":3,"file":"MessageList.d.ts","sourceRoot":"","sources":["../../src/ui/MessageList.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,KAAK,EAAE,aAAa,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAEpF,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,aAAa,CAAC;AAI3D;0EAC0E;AAC1E,MAAM,MAAM,wBAAwB,GAAG;IACrC,GAAG,EAAE,KAAK,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,KAAK,CAAC,cAAc,CAAC,cAAc,CAAC,CAAC;IAC/C,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;CAC3B,CAAC;AAWF,wBAAgB,WAAW,CAAC,KAAK,EAAE;IACjC,QAAQ,EAAE,aAAa,EAAE,CAAC;IAC1B,aAAa,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,OAAO,CAAC;IAEjB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,IAAI,CAAC;IAEzB,aAAa,CAAC,EAAE,CAAC,CAAC,EAAE,aAAa,KAAK,KAAK,CAAC,SAAS,CAAC;IACtD,gBAAgB,CAAC,EAAE,uBAAuB,CAAC;IAE3C,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,gBAAgB,CAAC,EAAE,MAAM,IAAI,CAAC;IAC9B;4FACwF;IACxF,gBAAgB,CAAC,EAAE,CAAC,QAAQ,EAAE,OAAO,KAAK,IAAI,CAAC;IAC/C;;4BAEwB;IACxB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;iCAE6B;IAC7B,eAAe,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC,wBAAwB,CAAC,CAAC;IAChE;;;yEAGqE;IACrE,UAAU,CAAC,EAAE,OAAO,CAAC;IAErB,QAAQ,CAAC,EAAE,kBAAkB,CAAC;IAC9B,aAAa,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IAC3C,iFAAiF;IACjF,WAAW,CAAC,EAAE,CAAC,eAAe,EAAE,MAAM,KAAK,IAAI,CAAC;IAChD;;;;OAIG;IACH,iBAAiB,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;CACrC,qBA0MA"}
@@ -4,8 +4,12 @@ import * as React from "react";
4
4
  import { isContinuation } from "@ttt-productions/chat-core";
5
5
  import { Button, Spinner } from "@ttt-productions/ui-core/react";
6
6
  import { MessageItemDefault } from "./MessageItemDefault.js";
7
+ /** The default scroll container: a plain div. */
8
+ function DefaultScrollContainer({ ref, className, onScroll, children }) {
9
+ return (_jsx("div", { ref: ref, className: className, onScroll: onScroll, children: children }));
10
+ }
7
11
  export function MessageList(props) {
8
- const { messages, currentUserId, isAdmin, isFetchingOlder, hasOlder, onLoadOlder, renderMessage, messageRenderers, showScrollToBottom, onScrollToBottom, onAtBottomChange, scrollClassName, fillHeight = false, handlers, onSenderClick, onRetrySend, olderLoadErrorRow, } = props;
12
+ const { messages, currentUserId, isAdmin, isFetchingOlder, hasOlder, onLoadOlder, renderMessage, messageRenderers, showScrollToBottom, onScrollToBottom, onAtBottomChange, scrollClassName, scrollContainer: ScrollContainer = DefaultScrollContainer, fillHeight = false, handlers, onSenderClick, onRetrySend, olderLoadErrorRow, } = props;
9
13
  const olderLoadFailed = olderLoadErrorRow != null;
10
14
  const scrollClass = scrollClassName ?? (fillHeight ? "flex-1 min-h-0" : "h-[400px]");
11
15
  const outerClass = fillHeight ? "relative flex flex-col flex-1 min-h-0" : "relative";
@@ -99,7 +103,7 @@ export function MessageList(props) {
99
103
  setUnseenCount(0);
100
104
  };
101
105
  let lastDay = null;
102
- return (_jsxs("div", { className: outerClass, children: [_jsxs("div", { ref: scrollRef, className: `${scrollClass} overflow-y-auto p-4`, onScroll: onScroll, children: [olderLoadFailed ? (_jsx("div", { className: "mb-2", children: olderLoadErrorRow })) : isFetchingOlder ? (_jsx("div", { className: "flex justify-center mb-2", children: _jsx(Spinner, { size: "xs", label: "Loading older messages" }) })) : null, _jsx("div", { ref: topSentinelRef }), messages.length === 0 ? (_jsx("div", { className: "h-full flex items-center justify-center text-sm opacity-70", children: "No messages yet" })) : (_jsx("div", { className: "flex flex-col", children: messages.map((m, idx) => {
106
+ return (_jsxs("div", { className: outerClass, children: [_jsxs(ScrollContainer, { ref: scrollRef, className: `${scrollClass} overflow-y-auto p-4`, onScroll: onScroll, children: [olderLoadFailed ? (_jsx("div", { className: "mb-2", children: olderLoadErrorRow })) : isFetchingOlder ? (_jsx("div", { className: "flex justify-center mb-2", children: _jsx(Spinner, { size: "xs", label: "Loading older messages" }) })) : null, _jsx("div", { ref: topSentinelRef }), messages.length === 0 ? (_jsx("div", { className: "h-full flex items-center justify-center text-sm opacity-70", children: "No messages yet" })) : (_jsx("div", { className: "flex flex-col", children: messages.map((m, idx) => {
103
107
  const day = new Date(m.createdAt).toDateString();
104
108
  const showDay = day !== lastDay;
105
109
  lastDay = day;
@@ -1 +1 @@
1
- {"version":3,"file":"MessageList.js","sourceRoot":"","sources":["../../src/ui/MessageList.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 { isContinuation } from \"@ttt-productions/chat-core\";\nimport type { MessageRendererRegistry } from \"../types.js\";\nimport { Button, Spinner } from \"@ttt-productions/ui-core/react\";\nimport { MessageItemDefault } from \"./MessageItemDefault.js\";\n\nexport function MessageList(props: {\n messages: ChatMessageV1[];\n currentUserId: string;\n isAdmin: boolean;\n\n isFetchingOlder?: boolean;\n hasOlder?: boolean;\n onLoadOlder?: () => void;\n\n renderMessage?: (m: ChatMessageV1) => React.ReactNode;\n messageRenderers?: MessageRendererRegistry;\n\n showScrollToBottom?: boolean;\n onScrollToBottom?: () => void;\n /** Reports whether the list is scrolled to the bottom so the consumer can advance\n * authoritative read state only when the latest message is actually in view (C-B6). */\n onAtBottomChange?: (atBottom: boolean) => void;\n /** Class for the scrollable message region. Overrides the default. The default is\n * `h-[400px]` (fixed-height card) unless `fillHeight` is set, in which case it is\n * `flex-1 min-h-0`. */\n scrollClassName?: string;\n /** Fill the parent's height instead of a fixed `h-[400px]`. Makes the list a flex-col\n * that flexes to fill its container, so a bounded-height page panel scrolls inside.\n * The consuming layout must give MessageList's ancestor a bounded height (ChatShell's\n * `fillHeight` wires Card → CardContent → MessageList for this). */\n fillHeight?: boolean;\n\n handlers?: ModerationHandlers;\n onSenderClick?: (senderId: string) => void;\n /** Retry a failed realtime send by clientMessageId (realtime transport only). */\n onRetrySend?: (clientMessageId: string) => void;\n /**\n * The failed older-page row (the consumer's error state with its retry). While set, it\n * renders at the top of the list and the list stops requesting older pages — a failed\n * page is retried only through the row, never by scrolling into it again.\n */\n olderLoadErrorRow?: React.ReactNode;\n}) {\n const {\n messages,\n currentUserId,\n isAdmin,\n isFetchingOlder,\n hasOlder,\n onLoadOlder,\n renderMessage,\n messageRenderers,\n showScrollToBottom,\n onScrollToBottom,\n onAtBottomChange,\n scrollClassName,\n fillHeight = false,\n handlers,\n onSenderClick,\n onRetrySend,\n olderLoadErrorRow,\n } = props;\n const olderLoadFailed = olderLoadErrorRow != null;\n\n const scrollClass = scrollClassName ?? (fillHeight ? \"flex-1 min-h-0\" : \"h-[400px]\");\n const outerClass = fillHeight ? \"relative flex flex-col flex-1 min-h-0\" : \"relative\";\n\n const scrollRef = React.useRef<HTMLDivElement>(null);\n const topSentinelRef = React.useRef<HTMLDivElement>(null);\n\n const isAtBottomRef = React.useRef(true);\n const prevScrollHeightRef = React.useRef<number | null>(null);\n const prevCountRef = React.useRef(0);\n\n // Count of messages that arrived while the user was scrolled up — drives the\n // \"new messages\" pill. Reset when the user returns to the bottom.\n const [unseenCount, setUnseenCount] = React.useState(0);\n\n const scrollToBottom = React.useCallback(() => {\n const el = scrollRef.current;\n if (!el) return;\n el.scrollTop = el.scrollHeight;\n isAtBottomRef.current = true;\n setUnseenCount(0);\n onAtBottomChange?.(true);\n }, [onAtBottomChange]);\n\n // IntersectionObserver for infinite scroll (older messages)\n React.useEffect(() => {\n const root = scrollRef.current;\n const target = topSentinelRef.current;\n if (!root || !target) return;\n if (typeof IntersectionObserver === \"undefined\") return;\n\n const io = new IntersectionObserver(\n (entries) => {\n const e = entries[0];\n if (!e?.isIntersecting) return;\n if (!hasOlder || isFetchingOlder || olderLoadFailed) return;\n\n prevScrollHeightRef.current = root.scrollHeight;\n onLoadOlder?.();\n },\n { root, threshold: 0 }\n );\n\n io.observe(target);\n return () => io.disconnect();\n }, [hasOlder, isFetchingOlder, olderLoadFailed, onLoadOlder]);\n\n // Scroll management — FIXES accordion scroll jump\n React.useLayoutEffect(() => {\n const el = scrollRef.current;\n if (!el) return;\n\n const count = messages.length;\n const prev = prevCountRef.current;\n\n if (prev === 0 && count > 0) {\n // Initial load — scroll to bottom WITHOUT triggering parent scroll\n requestAnimationFrame(() => {\n const savedScrollY = window.scrollY;\n el.scrollTop = el.scrollHeight;\n // Restore page scroll if the browser moved it\n if (window.scrollY !== savedScrollY) {\n window.scrollTo({ top: savedScrollY, behavior: \"instant\" as ScrollBehavior });\n }\n });\n isAtBottomRef.current = true;\n } else if (count > prev && prevScrollHeightRef.current != null) {\n // Older messages prepended — maintain scroll position\n const diff = el.scrollHeight - prevScrollHeightRef.current;\n el.scrollTop += diff;\n prevScrollHeightRef.current = null;\n } else if (count > prev && isAtBottomRef.current) {\n // New message at bottom, user is at bottom — scroll down\n // Use scrollTop assignment, NOT scrollTo with behavior:\"smooth\" (causes parent scroll jump)\n requestAnimationFrame(() => {\n const savedScrollY = window.scrollY;\n el.scrollTop = el.scrollHeight;\n if (window.scrollY !== savedScrollY) {\n window.scrollTo({ top: savedScrollY, behavior: \"instant\" as ScrollBehavior });\n }\n });\n } else if (count > prev) {\n // New message(s) arrived while the user was scrolled up — surface the pill\n // instead of yanking their scroll position.\n setUnseenCount((c) => c + (count - prev));\n }\n\n prevCountRef.current = count;\n }, [messages]);\n\n const onScroll = (e: React.UIEvent<HTMLDivElement>) => {\n const { scrollTop, scrollHeight, clientHeight } = e.currentTarget;\n const atBottom = scrollHeight - (scrollTop + clientHeight) < 50;\n if (atBottom !== isAtBottomRef.current) onAtBottomChange?.(atBottom);\n isAtBottomRef.current = atBottom;\n if (atBottom && unseenCount !== 0) setUnseenCount(0);\n };\n\n let lastDay: string | null = null;\n\n return (\n <div className={outerClass}>\n <div ref={scrollRef} className={`${scrollClass} overflow-y-auto p-4`} onScroll={onScroll}>\n {olderLoadFailed ? (\n <div className=\"mb-2\">{olderLoadErrorRow}</div>\n ) : isFetchingOlder ? (\n <div className=\"flex justify-center mb-2\">\n <Spinner size=\"xs\" label=\"Loading older messages\" />\n </div>\n ) : null}\n\n <div ref={topSentinelRef} />\n\n {messages.length === 0 ? (\n <div className=\"h-full flex items-center justify-center text-sm opacity-70\">No messages yet</div>\n ) : (\n <div className=\"flex flex-col\">\n {messages.map((m, idx) => {\n const day = new Date(m.createdAt).toDateString();\n const showDay = day !== lastDay;\n lastDay = day;\n\n const prevMsg = idx > 0 ? messages[idx - 1] : undefined;\n // Date separators always break grouping\n const continuation = showDay ? false : isContinuation(prevMsg, m);\n\n const byType =\n m.type && messageRenderers?.[m.type] ? messageRenderers[m.type]!(m) : null;\n\n const body =\n renderMessage?.(m) ??\n byType ?? (\n <MessageItemDefault\n m={m}\n currentUserId={currentUserId}\n isAdmin={isAdmin}\n handlers={handlers}\n isContinuation={continuation}\n onSenderClick={onSenderClick}\n onRetrySend={onRetrySend}\n />\n );\n\n return (\n <React.Fragment key={m.messageId}>\n {showDay && (\n <div className=\"my-2 flex justify-center\">\n {/* theme-core: components.css */}\n <div className=\"chat-date-separator\">\n {new Date(m.createdAt).toLocaleDateString()}\n </div>\n </div>\n )}\n {body}\n </React.Fragment>\n );\n })}\n </div>\n )}\n </div>\n\n {(unseenCount > 0 || showScrollToBottom) && (\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n aria-label={unseenCount > 0 ? `${unseenCount} new messages, scroll to latest` : \"Scroll to latest\"}\n className=\"absolute bottom-4 left-1/2 -translate-x-1/2 rounded-full elevation-raised gap-1\"\n onClick={() => {\n scrollToBottom();\n onScrollToBottom?.();\n }}\n >\n {unseenCount > 0\n ? `${unseenCount} new message${unseenCount > 1 ? \"s\" : \"\"} ↓`\n : \"↓\"}\n </Button>\n )}\n </div>\n );\n}\n"]}
1
+ {"version":3,"file":"MessageList.js","sourceRoot":"","sources":["../../src/ui/MessageList.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 { isContinuation } from \"@ttt-productions/chat-core\";\nimport type { MessageRendererRegistry } from \"../types.js\";\nimport { Button, Spinner } from \"@ttt-productions/ui-core/react\";\nimport { MessageItemDefault } from \"./MessageItemDefault.js\";\n\n/** What `MessageList` hands a custom scroll container: the element it must render as the scrollable\n * message region, with this ref, class, scroll handler, and children. */\nexport type ChatScrollContainerProps = {\n ref: React.Ref<HTMLDivElement>;\n className: string;\n onScroll: React.UIEventHandler<HTMLDivElement>;\n children: React.ReactNode;\n};\n\n/** The default scroll container: a plain div. */\nfunction DefaultScrollContainer({ ref, className, onScroll, children }: ChatScrollContainerProps) {\n return (\n <div ref={ref} className={className} onScroll={onScroll}>\n {children}\n </div>\n );\n}\n\nexport function MessageList(props: {\n messages: ChatMessageV1[];\n currentUserId: string;\n isAdmin: boolean;\n\n isFetchingOlder?: boolean;\n hasOlder?: boolean;\n onLoadOlder?: () => void;\n\n renderMessage?: (m: ChatMessageV1) => React.ReactNode;\n messageRenderers?: MessageRendererRegistry;\n\n showScrollToBottom?: boolean;\n onScrollToBottom?: () => void;\n /** Reports whether the list is scrolled to the bottom so the consumer can advance\n * authoritative read state only when the latest message is actually in view (C-B6). */\n onAtBottomChange?: (atBottom: boolean) => void;\n /** Class for the scrollable message region. Overrides the default. The default is\n * `h-[400px]` (fixed-height card) unless `fillHeight` is set, in which case it is\n * `flex-1 min-h-0`. */\n scrollClassName?: string;\n /** Renders the scrollable message region in place of the default div, so the consumer can give it\n * its own surface component. It must render one element carrying the given ref, class, scroll\n * handler, and children. */\n scrollContainer?: React.ComponentType<ChatScrollContainerProps>;\n /** Fill the parent's height instead of a fixed `h-[400px]`. Makes the list a flex-col\n * that flexes to fill its container, so a bounded-height page panel scrolls inside.\n * The consuming layout must give MessageList's ancestor a bounded height (ChatShell's\n * `fillHeight` wires Card → CardContent → MessageList for this). */\n fillHeight?: boolean;\n\n handlers?: ModerationHandlers;\n onSenderClick?: (senderId: string) => void;\n /** Retry a failed realtime send by clientMessageId (realtime transport only). */\n onRetrySend?: (clientMessageId: string) => void;\n /**\n * The failed older-page row (the consumer's error state with its retry). While set, it\n * renders at the top of the list and the list stops requesting older pages — a failed\n * page is retried only through the row, never by scrolling into it again.\n */\n olderLoadErrorRow?: React.ReactNode;\n}) {\n const {\n messages,\n currentUserId,\n isAdmin,\n isFetchingOlder,\n hasOlder,\n onLoadOlder,\n renderMessage,\n messageRenderers,\n showScrollToBottom,\n onScrollToBottom,\n onAtBottomChange,\n scrollClassName,\n scrollContainer: ScrollContainer = DefaultScrollContainer,\n fillHeight = false,\n handlers,\n onSenderClick,\n onRetrySend,\n olderLoadErrorRow,\n } = props;\n const olderLoadFailed = olderLoadErrorRow != null;\n\n const scrollClass = scrollClassName ?? (fillHeight ? \"flex-1 min-h-0\" : \"h-[400px]\");\n const outerClass = fillHeight ? \"relative flex flex-col flex-1 min-h-0\" : \"relative\";\n\n const scrollRef = React.useRef<HTMLDivElement>(null);\n const topSentinelRef = React.useRef<HTMLDivElement>(null);\n\n const isAtBottomRef = React.useRef(true);\n const prevScrollHeightRef = React.useRef<number | null>(null);\n const prevCountRef = React.useRef(0);\n\n // Count of messages that arrived while the user was scrolled up — drives the\n // \"new messages\" pill. Reset when the user returns to the bottom.\n const [unseenCount, setUnseenCount] = React.useState(0);\n\n const scrollToBottom = React.useCallback(() => {\n const el = scrollRef.current;\n if (!el) return;\n el.scrollTop = el.scrollHeight;\n isAtBottomRef.current = true;\n setUnseenCount(0);\n onAtBottomChange?.(true);\n }, [onAtBottomChange]);\n\n // IntersectionObserver for infinite scroll (older messages)\n React.useEffect(() => {\n const root = scrollRef.current;\n const target = topSentinelRef.current;\n if (!root || !target) return;\n if (typeof IntersectionObserver === \"undefined\") return;\n\n const io = new IntersectionObserver(\n (entries) => {\n const e = entries[0];\n if (!e?.isIntersecting) return;\n if (!hasOlder || isFetchingOlder || olderLoadFailed) return;\n\n prevScrollHeightRef.current = root.scrollHeight;\n onLoadOlder?.();\n },\n { root, threshold: 0 }\n );\n\n io.observe(target);\n return () => io.disconnect();\n }, [hasOlder, isFetchingOlder, olderLoadFailed, onLoadOlder]);\n\n // Scroll management — FIXES accordion scroll jump\n React.useLayoutEffect(() => {\n const el = scrollRef.current;\n if (!el) return;\n\n const count = messages.length;\n const prev = prevCountRef.current;\n\n if (prev === 0 && count > 0) {\n // Initial load — scroll to bottom WITHOUT triggering parent scroll\n requestAnimationFrame(() => {\n const savedScrollY = window.scrollY;\n el.scrollTop = el.scrollHeight;\n // Restore page scroll if the browser moved it\n if (window.scrollY !== savedScrollY) {\n window.scrollTo({ top: savedScrollY, behavior: \"instant\" as ScrollBehavior });\n }\n });\n isAtBottomRef.current = true;\n } else if (count > prev && prevScrollHeightRef.current != null) {\n // Older messages prepended — maintain scroll position\n const diff = el.scrollHeight - prevScrollHeightRef.current;\n el.scrollTop += diff;\n prevScrollHeightRef.current = null;\n } else if (count > prev && isAtBottomRef.current) {\n // New message at bottom, user is at bottom — scroll down\n // Use scrollTop assignment, NOT scrollTo with behavior:\"smooth\" (causes parent scroll jump)\n requestAnimationFrame(() => {\n const savedScrollY = window.scrollY;\n el.scrollTop = el.scrollHeight;\n if (window.scrollY !== savedScrollY) {\n window.scrollTo({ top: savedScrollY, behavior: \"instant\" as ScrollBehavior });\n }\n });\n } else if (count > prev) {\n // New message(s) arrived while the user was scrolled up — surface the pill\n // instead of yanking their scroll position.\n setUnseenCount((c) => c + (count - prev));\n }\n\n prevCountRef.current = count;\n }, [messages]);\n\n const onScroll = (e: React.UIEvent<HTMLDivElement>) => {\n const { scrollTop, scrollHeight, clientHeight } = e.currentTarget;\n const atBottom = scrollHeight - (scrollTop + clientHeight) < 50;\n if (atBottom !== isAtBottomRef.current) onAtBottomChange?.(atBottom);\n isAtBottomRef.current = atBottom;\n if (atBottom && unseenCount !== 0) setUnseenCount(0);\n };\n\n let lastDay: string | null = null;\n\n return (\n <div className={outerClass}>\n <ScrollContainer ref={scrollRef} className={`${scrollClass} overflow-y-auto p-4`} onScroll={onScroll}>\n {olderLoadFailed ? (\n <div className=\"mb-2\">{olderLoadErrorRow}</div>\n ) : isFetchingOlder ? (\n <div className=\"flex justify-center mb-2\">\n <Spinner size=\"xs\" label=\"Loading older messages\" />\n </div>\n ) : null}\n\n <div ref={topSentinelRef} />\n\n {messages.length === 0 ? (\n <div className=\"h-full flex items-center justify-center text-sm opacity-70\">No messages yet</div>\n ) : (\n <div className=\"flex flex-col\">\n {messages.map((m, idx) => {\n const day = new Date(m.createdAt).toDateString();\n const showDay = day !== lastDay;\n lastDay = day;\n\n const prevMsg = idx > 0 ? messages[idx - 1] : undefined;\n // Date separators always break grouping\n const continuation = showDay ? false : isContinuation(prevMsg, m);\n\n const byType =\n m.type && messageRenderers?.[m.type] ? messageRenderers[m.type]!(m) : null;\n\n const body =\n renderMessage?.(m) ??\n byType ?? (\n <MessageItemDefault\n m={m}\n currentUserId={currentUserId}\n isAdmin={isAdmin}\n handlers={handlers}\n isContinuation={continuation}\n onSenderClick={onSenderClick}\n onRetrySend={onRetrySend}\n />\n );\n\n return (\n <React.Fragment key={m.messageId}>\n {showDay && (\n <div className=\"my-2 flex justify-center\">\n {/* theme-core: components.css */}\n <div className=\"chat-date-separator\">\n {new Date(m.createdAt).toLocaleDateString()}\n </div>\n </div>\n )}\n {body}\n </React.Fragment>\n );\n })}\n </div>\n )}\n </ScrollContainer>\n\n {(unseenCount > 0 || showScrollToBottom) && (\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n aria-label={unseenCount > 0 ? `${unseenCount} new messages, scroll to latest` : \"Scroll to latest\"}\n className=\"absolute bottom-4 left-1/2 -translate-x-1/2 rounded-full elevation-raised gap-1\"\n onClick={() => {\n scrollToBottom();\n onScrollToBottom?.();\n }}\n >\n {unseenCount > 0\n ? `${unseenCount} new message${unseenCount > 1 ? \"s\" : \"\"} ↓`\n : \"↓\"}\n </Button>\n )}\n </div>\n );\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ttt-productions/chat-react",
3
- "version": "0.13.10",
3
+ "version": "0.13.11",
4
4
  "description": "Chat React UI, hooks, and Firebase-client adapter for TTT Productions apps (realtime newest window + infinite older pagination)",
5
5
  "repository": {
6
6
  "type": "git",
@@ -33,7 +33,7 @@
33
33
  },
34
34
  "peerDependencies": {
35
35
  "@tanstack/react-query": ">=5.0.0",
36
- "@ttt-productions/query-core": "^0.14.6",
36
+ "@ttt-productions/query-core": "^0.14.7",
37
37
  "firebase": ">=12.0.0",
38
38
  "lucide-react": "^1.30.0",
39
39
  "react": ">=19.0.0",
@@ -63,13 +63,13 @@
63
63
  "@ttt-productions/chat-core": "^0.11.2",
64
64
  "@ttt-productions/chat-schemas": "^0.10.5",
65
65
  "@ttt-productions/realtime-core": "^0.1.3",
66
- "@ttt-productions/ui-core": "^0.11.21",
66
+ "@ttt-productions/ui-core": "^0.11.23",
67
67
  "@ttt-productions/upload-ui": "^0.7.13",
68
68
  "@ttt-productions/mobile-core": "^0.9.3",
69
69
  "@ttt-productions/firebase-helpers": "^0.12.10"
70
70
  },
71
71
  "devDependencies": {
72
- "@ttt-productions/query-core": "^0.14.6",
72
+ "@ttt-productions/query-core": "^0.14.7",
73
73
  "@types/react": "^19.0.0",
74
74
  "@types/react-dom": "^19.0.0",
75
75
  "lucide-react": "^1.30.0",