@ttt-productions/chat-react 0.12.10 → 0.12.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 +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/ui/ChatShell.d.ts.map +1 -1
- package/dist/ui/ChatShell.js +12 -1
- package/dist/ui/ChatShell.js.map +1 -1
- package/dist/ui/Composer.d.ts +15 -1
- package/dist/ui/Composer.d.ts.map +1 -1
- package/dist/ui/Composer.js +13 -3
- package/dist/ui/Composer.js.map +1 -1
- package/dist/ui/MessageItemDefault.d.ts +5 -0
- package/dist/ui/MessageItemDefault.d.ts.map +1 -1
- package/dist/ui/MessageItemDefault.js +25 -10
- package/dist/ui/MessageItemDefault.js.map +1 -1
- package/dist/ui/MessageList.d.ts +3 -0
- package/dist/ui/MessageList.d.ts.map +1 -1
- package/dist/ui/MessageList.js +2 -2
- package/dist/ui/MessageList.js.map +1 -1
- package/package.json +3 -3
- package/src/styles/chat.css +69 -5
package/dist/index.d.ts
CHANGED
|
@@ -13,7 +13,7 @@ export type { RealtimeChatClient, RealtimeTransportConfig, ChannelClientState, C
|
|
|
13
13
|
export type { ChatShellProps } from "./ui/ChatShell.js";
|
|
14
14
|
export { ChatShell } from "./ui/ChatShell.js";
|
|
15
15
|
export { MessageList } from "./ui/MessageList.js";
|
|
16
|
-
export type { ComposerProps } from "./ui/Composer.js";
|
|
16
|
+
export type { ComposerProps, ComposerHandle } from "./ui/Composer.js";
|
|
17
17
|
export { Composer } from "./ui/Composer.js";
|
|
18
18
|
export type { MessageItemDefaultProps } from "./ui/MessageItemDefault.js";
|
|
19
19
|
export { MessageItemDefault } from "./ui/MessageItemDefault.js";
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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,oBAAoB,EACpB,iBAAiB,EACjB,iBAAiB,EACjB,eAAe,EACf,uBAAuB,EACvB,yBAAyB,EACzB,qBAAqB,GACtB,MAAM,YAAY,CAAC;AAGpB,YAAY,EACV,yBAAyB,EACzB,8BAA8B,GAC/B,MAAM,uCAAuC,CAAC;AAC/C,OAAO,EACL,yBAAyB,EACzB,4BAA4B,GAC7B,MAAM,uCAAuC,CAAC;AAM/C,YAAY,EACV,4BAA4B,EAC5B,gCAAgC,GACjC,MAAM,yCAAyC,CAAC;AACjD,OAAO,EACL,2BAA2B,EAC3B,+BAA+B,GAChC,MAAM,yCAAyC,CAAC;AAGjD,YAAY,EAAE,6BAA6B,EAAE,MAAM,sCAAsC,CAAC;AAC1F,OAAO,EACL,wBAAwB,EACxB,mBAAmB,EACnB,2BAA2B,EAC3B,qBAAqB,GACtB,MAAM,sCAAsC,CAAC;AAE9C,YAAY,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAC;AACxE,OAAO,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAC7D,OAAO,EAAE,eAAe,EAAE,MAAM,gCAAgC,CAAC;AAKjE,OAAO,EACL,wBAAwB,EACxB,iBAAiB,EACjB,aAAa,EACb,WAAW,EACX,uBAAuB,EACvB,oBAAoB,EACpB,qBAAqB,EACrB,uBAAuB,EACvB,gBAAgB,EAChB,gBAAgB,EAChB,YAAY,EACZ,YAAY,EACZ,UAAU,EACV,UAAU,GACX,MAAM,qBAAqB,CAAC;AAC7B,YAAY,EACV,kBAAkB,EAClB,uBAAuB,EACvB,kBAAkB,EAClB,mBAAmB,EACnB,gBAAgB,EAChB,iBAAiB,EACjB,6BAA6B,EAC7B,aAAa,EACb,eAAe,EACf,cAAc,EACd,cAAc,EACd,aAAa,EACb,cAAc,EACd,eAAe,EACf,cAAc,EACd,iBAAiB,EACjB,iBAAiB,EACjB,WAAW,GACZ,MAAM,qBAAqB,CAAC;AAU7B,YAAY,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACxD,OAAO,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAC9C,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAClD,YAAY,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AASA,YAAY,EACV,cAAc,EACd,iBAAiB,EACjB,2BAA2B,EAC3B,oBAAoB,EACpB,iBAAiB,EACjB,iBAAiB,EACjB,eAAe,EACf,uBAAuB,EACvB,yBAAyB,EACzB,qBAAqB,GACtB,MAAM,YAAY,CAAC;AAGpB,YAAY,EACV,yBAAyB,EACzB,8BAA8B,GAC/B,MAAM,uCAAuC,CAAC;AAC/C,OAAO,EACL,yBAAyB,EACzB,4BAA4B,GAC7B,MAAM,uCAAuC,CAAC;AAM/C,YAAY,EACV,4BAA4B,EAC5B,gCAAgC,GACjC,MAAM,yCAAyC,CAAC;AACjD,OAAO,EACL,2BAA2B,EAC3B,+BAA+B,GAChC,MAAM,yCAAyC,CAAC;AAGjD,YAAY,EAAE,6BAA6B,EAAE,MAAM,sCAAsC,CAAC;AAC1F,OAAO,EACL,wBAAwB,EACxB,mBAAmB,EACnB,2BAA2B,EAC3B,qBAAqB,GACtB,MAAM,sCAAsC,CAAC;AAE9C,YAAY,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAC;AACxE,OAAO,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAC7D,OAAO,EAAE,eAAe,EAAE,MAAM,gCAAgC,CAAC;AAKjE,OAAO,EACL,wBAAwB,EACxB,iBAAiB,EACjB,aAAa,EACb,WAAW,EACX,uBAAuB,EACvB,oBAAoB,EACpB,qBAAqB,EACrB,uBAAuB,EACvB,gBAAgB,EAChB,gBAAgB,EAChB,YAAY,EACZ,YAAY,EACZ,UAAU,EACV,UAAU,GACX,MAAM,qBAAqB,CAAC;AAC7B,YAAY,EACV,kBAAkB,EAClB,uBAAuB,EACvB,kBAAkB,EAClB,mBAAmB,EACnB,gBAAgB,EAChB,iBAAiB,EACjB,6BAA6B,EAC7B,aAAa,EACb,eAAe,EACf,cAAc,EACd,cAAc,EACd,aAAa,EACb,cAAc,EACd,eAAe,EACf,cAAc,EACd,iBAAiB,EACjB,iBAAiB,EACjB,WAAW,GACZ,MAAM,qBAAqB,CAAC;AAU7B,YAAY,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACxD,OAAO,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAC9C,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAClD,YAAY,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AACtE,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,2BAA2B,CAAC;AAC/E,OAAO,EACL,mBAAmB,EACnB,KAAK,wBAAwB,GAC9B,MAAM,mCAAmC,CAAC;AAC3C,OAAO,EACL,sBAAsB,EACtB,KAAK,0BAA0B,EAC/B,KAAK,4BAA4B,EACjC,KAAK,iBAAiB,EACtB,KAAK,uBAAuB,GAC7B,MAAM,wCAAwC,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;AA2Bb,OAAO,EACL,yBAAyB,EACzB,4BAA4B,GAC7B,MAAM,uCAAuC,CAAC;AAU/C,OAAO,EACL,2BAA2B,EAC3B,+BAA+B,GAChC,MAAM,yCAAyC,CAAC;AAIjD,OAAO,EACL,wBAAwB,EACxB,mBAAmB,EACnB,2BAA2B,EAC3B,qBAAqB,GACtB,MAAM,sCAAsC,CAAC;AAG9C,OAAO,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAC7D,OAAO,EAAE,eAAe,EAAE,MAAM,gCAAgC,CAAC;AAEjE,+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,gBAAgB,EAChB,gBAAgB,EAChB,YAAY,EACZ,YAAY,EACZ,UAAU,EACV,UAAU,GACX,MAAM,qBAAqB,CAAC;AA+B7B,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,2BAA2B,CAAC;AAC/E,OAAO,EACL,mBAAmB,GAEpB,MAAM,mCAAmC,CAAC;AAC3C,OAAO,EACL,sBAAsB,GAKvB,MAAM,wCAAwC,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 ChatAttachmentConfig,\n ChatUploadAdapter,\n ChatMentionConfig,\n MessageRenderer,\n MessageRendererRegistry,\n RenderableMentionProvider,\n MentionResultRenderer,\n} from \"./types.js\";\n\n// Attachment-URL resolver context (render-time URL building from mediaAssetId).\nexport type {\n ChatAttachmentUrlResolver,\n ChatAttachmentUrlProviderProps,\n} from \"./context/ChatAttachmentUrlContext.js\";\nexport {\n ChatAttachmentUrlProvider,\n useChatAttachmentUrlResolver,\n} from \"./context/ChatAttachmentUrlContext.js\";\n\n// Attachment media-component injection (optional) — the app hands chat its own\n// display-path wrapper (recovery adapter, diagnostics, telemetry) so chat\n// media rides the same pipeline as every other surface; default stays the\n// package MediaViewer.\nexport type {\n ChatAttachmentMediaComponent,\n ChatAttachmentMediaProviderProps,\n} from \"./context/ChatAttachmentMediaContext.js\";\nexport {\n ChatAttachmentMediaProvider,\n useChatAttachmentMediaComponent,\n} from \"./context/ChatAttachmentMediaContext.js\";\n\n// Name-resolver context.\nexport type { ChatNameResolverProviderProps } from \"./context/ChatNameResolverContext.js\";\nexport {\n ChatNameResolverProvider,\n useChatNameResolver,\n useOptionalChatNameResolver,\n useResolvedSenderName,\n} from \"./context/ChatNameResolverContext.js\";\n\nexport type { UseChatMessagesResult } from \"./hooks/useChatMessages.js\";\nexport { useChatMessages } from \"./hooks/useChatMessages.js\";\nexport { canAccessThread } from \"./hooks/useChatThreadAccess.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_SUBPROTOCOL,\n CHAT_CLOSE_CODES,\n CLIENT_FRAME,\n SERVER_FRAME,\n buildFrame,\n parseFrame,\n} from \"./realtime/index.js\";\nexport type {\n RealtimeChatClient,\n RealtimeTransportConfig,\n ChannelClientState,\n ChannelClientConfig,\n InboxClientState,\n InboxClientConfig,\n UseRealtimeChatMessagesResult,\n GrantProvider,\n TransportTimers,\n RealtimeStatus,\n RealtimeSocket,\n SocketFactory,\n SocketHandlers,\n ChannelRefTuple,\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 } 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 \"./mentions/MessageText.js\";\nexport {\n MentionAutocomplete,\n type MentionAutocompleteProps,\n} from \"./mentions/MentionAutocomplete.js\";\nexport {\n useMentionAutocomplete,\n type UseMentionAutocompleteArgs,\n type UseMentionAutocompleteResult,\n type AutocompleteState,\n type AutocompleteResultGroup,\n} from \"./mentions/use-mention-autocomplete.js\";\n"]}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AA2Bb,OAAO,EACL,yBAAyB,EACzB,4BAA4B,GAC7B,MAAM,uCAAuC,CAAC;AAU/C,OAAO,EACL,2BAA2B,EAC3B,+BAA+B,GAChC,MAAM,yCAAyC,CAAC;AAIjD,OAAO,EACL,wBAAwB,EACxB,mBAAmB,EACnB,2BAA2B,EAC3B,qBAAqB,GACtB,MAAM,sCAAsC,CAAC;AAG9C,OAAO,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAC7D,OAAO,EAAE,eAAe,EAAE,MAAM,gCAAgC,CAAC;AAEjE,+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,gBAAgB,EAChB,gBAAgB,EAChB,YAAY,EACZ,YAAY,EACZ,UAAU,EACV,UAAU,GACX,MAAM,qBAAqB,CAAC;AA+B7B,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,2BAA2B,CAAC;AAC/E,OAAO,EACL,mBAAmB,GAEpB,MAAM,mCAAmC,CAAC;AAC3C,OAAO,EACL,sBAAsB,GAKvB,MAAM,wCAAwC,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 ChatAttachmentConfig,\n ChatUploadAdapter,\n ChatMentionConfig,\n MessageRenderer,\n MessageRendererRegistry,\n RenderableMentionProvider,\n MentionResultRenderer,\n} from \"./types.js\";\n\n// Attachment-URL resolver context (render-time URL building from mediaAssetId).\nexport type {\n ChatAttachmentUrlResolver,\n ChatAttachmentUrlProviderProps,\n} from \"./context/ChatAttachmentUrlContext.js\";\nexport {\n ChatAttachmentUrlProvider,\n useChatAttachmentUrlResolver,\n} from \"./context/ChatAttachmentUrlContext.js\";\n\n// Attachment media-component injection (optional) — the app hands chat its own\n// display-path wrapper (recovery adapter, diagnostics, telemetry) so chat\n// media rides the same pipeline as every other surface; default stays the\n// package MediaViewer.\nexport type {\n ChatAttachmentMediaComponent,\n ChatAttachmentMediaProviderProps,\n} from \"./context/ChatAttachmentMediaContext.js\";\nexport {\n ChatAttachmentMediaProvider,\n useChatAttachmentMediaComponent,\n} from \"./context/ChatAttachmentMediaContext.js\";\n\n// Name-resolver context.\nexport type { ChatNameResolverProviderProps } from \"./context/ChatNameResolverContext.js\";\nexport {\n ChatNameResolverProvider,\n useChatNameResolver,\n useOptionalChatNameResolver,\n useResolvedSenderName,\n} from \"./context/ChatNameResolverContext.js\";\n\nexport type { UseChatMessagesResult } from \"./hooks/useChatMessages.js\";\nexport { useChatMessages } from \"./hooks/useChatMessages.js\";\nexport { canAccessThread } from \"./hooks/useChatThreadAccess.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_SUBPROTOCOL,\n CHAT_CLOSE_CODES,\n CLIENT_FRAME,\n SERVER_FRAME,\n buildFrame,\n parseFrame,\n} from \"./realtime/index.js\";\nexport type {\n RealtimeChatClient,\n RealtimeTransportConfig,\n ChannelClientState,\n ChannelClientConfig,\n InboxClientState,\n InboxClientConfig,\n UseRealtimeChatMessagesResult,\n GrantProvider,\n TransportTimers,\n RealtimeStatus,\n RealtimeSocket,\n SocketFactory,\n SocketHandlers,\n ChannelRefTuple,\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 } from \"./ui/ChatShell.js\";\nexport { ChatShell } from \"./ui/ChatShell.js\";\nexport { MessageList } from \"./ui/MessageList.js\";\nexport type { ComposerProps, ComposerHandle } 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 \"./mentions/MessageText.js\";\nexport {\n MentionAutocomplete,\n type MentionAutocompleteProps,\n} from \"./mentions/MentionAutocomplete.js\";\nexport {\n useMentionAutocomplete,\n type UseMentionAutocompleteArgs,\n type UseMentionAutocompleteResult,\n type AutocompleteState,\n type AutocompleteResultGroup,\n} from \"./mentions/use-mention-autocomplete.js\";\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ChatShell.d.ts","sourceRoot":"","sources":["../../src/ui/ChatShell.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,KAAK,EAAE,aAAa,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AACtG,OAAO,KAAK,EACV,cAAc,EACd,oBAAoB,EACpB,uBAAuB,EACxB,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"ChatShell.d.ts","sourceRoot":"","sources":["../../src/ui/ChatShell.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,KAAK,EAAE,aAAa,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AACtG,OAAO,KAAK,EACV,cAAc,EACd,oBAAoB,EACpB,uBAAuB,EACxB,MAAM,aAAa,CAAC;AAYrB,MAAM,MAAM,cAAc,GAAG;IAC3B,MAAM,EAAE,cAAc,CAAC;IAGvB,MAAM,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAKzB,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,aAAa,CAAC,SAAS,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAG7E,aAAa,CAAC,EAAE,CAAC,CAAC,EAAE,GAAG,KAAK,KAAK,CAAC,SAAS,CAAC;IAC5C,gBAAgB,CAAC,EAAE,uBAAuB,CAAC;IAC3C,QAAQ,CAAC,EAAE,kBAAkB,CAAC;IAG9B,gBAAgB,CAAC,EAAE,oBAAoB,CAAC;IACxC,cAAc,CAAC,EAAE,gBAAgB,CAAC;IAClC,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,SAAS,CAAC,EAAE,OAAO,CAAC;IAGpB,mBAAmB,CAAC,EAAE,MAAM,KAAK,CAAC,SAAS,CAAC;IAC5C,mBAAmB,CAAC,EAAE,MAAM,KAAK,CAAC,SAAS,CAAC;IAC5C,kDAAkD;IAClD,YAAY,CAAC,EAAE,MAAM,KAAK,CAAC,SAAS,CAAC;IAGrC,aAAa,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,KAAK,IAAI,CAAC;IAGhE,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAE3B;;;6FAGyF;IACzF,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;qDACiD;IACjD,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B,CAAC;AA0BF;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,cAAc,qBAK9C"}
|
package/dist/ui/ChatShell.js
CHANGED
|
@@ -73,6 +73,17 @@ function ChatShellView(props) {
|
|
|
73
73
|
const [focused, setFocused] = React.useState(true);
|
|
74
74
|
const lastAckedRef = React.useRef(0);
|
|
75
75
|
const leaseHeldRef = React.useRef(false);
|
|
76
|
+
// A terminally-failed attachment bubble re-opens the canonical picker through the
|
|
77
|
+
// Composer's imperative handle. The action is offered only when attachment support
|
|
78
|
+
// is actually configured AND the default Composer is rendered (a custom renderFooter
|
|
79
|
+
// replaces it, so the picker would not exist). The new file then rides the ordinary
|
|
80
|
+
// Composer → guarded-upload flow with a fresh id — never reusing a storage path or
|
|
81
|
+
// mutating the terminal row.
|
|
82
|
+
const composerRef = React.useRef(null);
|
|
83
|
+
const attachmentSupported = Boolean(attachmentConfig && sendAttachment && !renderFooter);
|
|
84
|
+
const handleRetryAttachment = React.useCallback(() => {
|
|
85
|
+
composerRef.current?.openAttachmentSelector();
|
|
86
|
+
}, []);
|
|
76
87
|
// The latest authoritative seq currently rendered (optimistic rows have no seq).
|
|
77
88
|
const latestSeq = React.useMemo(() => {
|
|
78
89
|
let max = 0;
|
|
@@ -137,7 +148,7 @@ function ChatShellView(props) {
|
|
|
137
148
|
// must work whether or not realtime chat has connected yet (F4.1).
|
|
138
149
|
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 }) })] })), renderAboveMessages && (_jsx("div", { className: "border-b", children: renderAboveMessages() })), _jsx(CardContent, { className: contentClassName, children: _jsxs("div", { className: "flex flex-col items-center justify-center gap-2 py-12 text-sm text-muted-foreground", role: "status", "aria-live": "polite", children: [_jsx(Loader2, { className: "h-5 w-5 animate-spin", "aria-hidden": "true" }), _jsx("span", { children: "Opening chat\u2026" })] }) }), renderBelowMessages && (_jsx("div", { className: "border-t", children: renderBelowMessages() })), renderFooter ? (_jsx(CardFooter, { className: "border-t", children: renderFooter() })) : (_jsx(CardFooter, { className: "border-t", children: _jsx(Skeleton, { className: "h-10 w-full" }) }))] }));
|
|
139
150
|
}
|
|
140
|
-
return (_jsxs(Card, { className: cardClassName, children: [(header || handlers) && (_jsxs(CardHeader, { className: "flex flex-row items-start justify-between gap-4 space-y-0", children: [_jsx("div", { children: header }), _jsx("div", { className: "flex flex-col items-end gap-2", children: _jsx(ThreadActions, { threadId: config.threadId, isAdmin: config.isAdmin, handlers: handlers }) })] })), _jsx(ChatConnectionBanner, { status: status }), renderAboveMessages && (_jsx("div", { className: "border-b", children: renderAboveMessages() })), _jsx(CardContent, { className: contentClassName, children: _jsx(MessageList, { messages: messages, currentUserId: config.currentUserId, isAdmin: config.isAdmin, isFetchingOlder: isFetchingOlder, hasOlder: hasOlder, onLoadOlder: () => fetchOlder(), renderMessage: renderMessage, messageRenderers: messageRenderers, showScrollToBottom: showScrollToBottom, onScrollToBottom: () => setShowScrollToBottom(false), onAtBottomChange: setAtBottom, fillHeight: fillHeight, scrollClassName: scrollClassName, handlers: handlers, onSenderClick: onSenderClick, onRetrySend: retrySend }) }), renderBelowMessages && (_jsx("div", { className: "border-t", children: renderBelowMessages() })), renderFooter ? (_jsx(CardFooter, { className: "border-t", children: renderFooter() })) : (_jsxs(CardFooter, { className: "border-t flex-col items-stretch gap-1", children: [_jsx(TypingIndicator, { typing: typing }), _jsx(KeyboardAvoidingView, { padding: true, offset: 8, className: "w-full", children: _jsx(Composer, { onSend: send, onTyping: signalTyping, attachmentConfig: attachmentConfig, sendAttachment: sendAttachment, disabled: composerDisabled, autoFocus: autoFocus, placeholder: composerPlaceholder, mentionConfig: config.mentionConfig }) })] }))] }));
|
|
151
|
+
return (_jsxs(Card, { className: cardClassName, children: [(header || handlers) && (_jsxs(CardHeader, { className: "flex flex-row items-start justify-between gap-4 space-y-0", children: [_jsx("div", { children: header }), _jsx("div", { className: "flex flex-col items-end gap-2", children: _jsx(ThreadActions, { threadId: config.threadId, isAdmin: config.isAdmin, handlers: handlers }) })] })), _jsx(ChatConnectionBanner, { status: status }), renderAboveMessages && (_jsx("div", { className: "border-b", children: renderAboveMessages() })), _jsx(CardContent, { className: contentClassName, children: _jsx(MessageList, { messages: messages, currentUserId: config.currentUserId, isAdmin: config.isAdmin, isFetchingOlder: isFetchingOlder, hasOlder: hasOlder, onLoadOlder: () => fetchOlder(), renderMessage: renderMessage, messageRenderers: messageRenderers, showScrollToBottom: showScrollToBottom, onScrollToBottom: () => setShowScrollToBottom(false), onAtBottomChange: setAtBottom, fillHeight: fillHeight, scrollClassName: scrollClassName, handlers: handlers, onSenderClick: onSenderClick, onRetrySend: retrySend, onRetryAttachment: attachmentSupported ? handleRetryAttachment : undefined }) }), 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, { ref: composerRef, onSend: send, onTyping: signalTyping, attachmentConfig: attachmentConfig, sendAttachment: sendAttachment, disabled: composerDisabled, autoFocus: autoFocus, placeholder: composerPlaceholder, mentionConfig: config.mentionConfig }) })] }))] }));
|
|
141
152
|
}
|
|
142
153
|
/**
|
|
143
154
|
* R13 — connection-state banner. The realtime path sets `status`; once the first
|
package/dist/ui/ChatShell.js.map
CHANGED
|
@@ -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, SendAttachmentFn, ModerationHandlers } from \"@ttt-productions/chat-core\";\nimport type {\n ChatCoreConfig,\n ChatAttachmentConfig,\n MessageRendererRegistry,\n} from \"../types.js\";\nimport { Card, CardHeader, CardContent, CardFooter, Skeleton } from \"@ttt-productions/ui-core/react\";\nimport { KeyboardAvoidingView } from \"@ttt-productions/mobile-core/react\";\nimport { Loader2 } from \"lucide-react\";\nimport { useChatMessages } from \"../hooks/useChatMessages.js\";\nimport { useRealtimeChatMessages } from \"../realtime/useRealtimeChatMessages.js\";\nimport type { RealtimeChatClient } from \"../realtime/transport.js\";\nimport { MessageList } from \"./MessageList.js\";\nimport { Composer } from \"./Composer.js\";\nimport { ThreadActions } from \"./menus.js\";\n\nexport type ChatShellProps = {\n config: ChatCoreConfig;\n\n // Header\n header?: React.ReactNode;\n\n // Send handler — text-only. On the realtime transport this is OPTIONAL: the\n // shell sends through the DO socket. If provided, it is still called (so a\n // consumer can mirror to analytics), but the socket send is authoritative.\n onSend?: (text: string, replyTo?: ChatMessageV1[\"replyTo\"]) => Promise<void>;\n\n // Message rendering\n renderMessage?: (m: any) => React.ReactNode;\n messageRenderers?: MessageRendererRegistry;\n handlers?: ModerationHandlers;\n\n // Composer attachment config\n attachmentConfig?: ChatAttachmentConfig;\n sendAttachment?: SendAttachmentFn;\n composerPlaceholder?: string;\n autoFocus?: boolean;\n\n // Three render slots\n renderAboveMessages?: () => React.ReactNode;\n renderBelowMessages?: () => React.ReactNode;\n /** If provided, replaces the Composer entirely */\n renderFooter?: () => React.ReactNode;\n\n // Sender interaction\n onSenderClick?: (senderId: string, displayName: string) => void;\n\n // Disable\n composerDisabled?: boolean;\n\n /** Fill the parent's height instead of the default fixed-height card. Lays the Card out\n * as `flex flex-col h-full` with the message list flexing to fill (scrolls inside), so a\n * full-height page panel doesn't grow unbounded or waste space on a fixed 400px box.\n * The consumer must give ChatShell a bounded-height parent (e.g. an `h-full` panel). */\n fillHeight?: boolean;\n /** Advanced override for the scrollable region's class (forwarded to MessageList).\n * Usually unnecessary — prefer `fillHeight`. */\n scrollClassName?: string;\n};\n\n/** The resolved data a transport hook hands the presentational view. */\ntype ResolvedChat = {\n allowed: boolean;\n isInitialLoading: boolean;\n messages: ChatMessageV1[];\n fetchOlder: () => Promise<void>;\n hasOlder: boolean;\n isFetchingOlder: boolean;\n /** The send handler the Composer calls (socket send on realtime, prop on firestore). */\n send: (text: string, replyTo?: ChatMessageV1[\"replyTo\"]) => Promise<void>;\n /** Retry a failed realtime send by its original clientMessageId (realtime only;\n * absent on firestore). Wired to MessageItemDefault's failed-bubble retry. */\n retrySend?: (clientMessageId: string) => void;\n /** Advance the authoritative read cursor (realtime only; absent on the firestore path).\n * Returns whether the ack was actually sent so the caller advances its local cursor only on success (M2). */\n readAck?: (latestSeq: number, focused: boolean) => boolean;\n /** Realtime connection status — drives the disconnected banner (R13). Absent on firestore. */\n status?: string;\n /** uids currently typing (realtime only) — drives the typing indicator (R14). */\n typing?: string[];\n /** Signal the local user is typing (coalesced by the transport). Wired to the Composer (R14). */\n signalTyping?: () => void;\n};\n\n/**\n * ChatShell dispatches on `config.transport`. Hooks must be unconditional, so the\n * two transports live in separate inner components (each calls exactly ONE data\n * hook) and both render the shared presentational `ChatShellView`. The default\n * (undefined / 'firestore') transport is byte-for-byte the previous behavior.\n */\nexport function ChatShell(props: ChatShellProps) {\n if (props.config.transport === \"realtime\") {\n return <RealtimeChatShell {...props} />;\n }\n return <FirestoreChatShell {...props} />;\n}\n\nfunction FirestoreChatShell(props: ChatShellProps) {\n const { config, onSend } = props;\n const r = useChatMessages(config);\n const send = React.useCallback(\n async (text: string, replyTo?: ChatMessageV1[\"replyTo\"]) => {\n if (onSend) await onSend(text, replyTo);\n },\n [onSend],\n );\n return <ChatShellView {...props} resolved={{ ...r, send }} />;\n}\n\nfunction RealtimeChatShell(props: ChatShellProps) {\n const { config, onSend } = props;\n const client = config.realtime?.client as RealtimeChatClient | undefined;\n if (!client) {\n throw new Error(\n \"[ChatShell] transport 'realtime' requires config.realtime.client (createRealtimeChatClient(...))\",\n );\n }\n const r = useRealtimeChatMessages(client);\n const send = React.useCallback(\n async (text: string, replyTo?: ChatMessageV1[\"replyTo\"]) => {\n // Map the UI replyTo (messageId-based) to the wire replyTo (seq + preview).\n const wireReply =\n replyTo && replyTo.messageId\n ? { messageSeq: Number(replyTo.messageId), preview: replyTo.messagePreview ?? \"\" }\n : null;\n const ok = r.send(text, Number.isFinite(wireReply?.messageSeq) ? wireReply : null);\n // C-B8: surface a closed-socket failure so the Composer keeps the user's text\n // instead of clearing it on a no-op send.\n if (!ok) throw new Error(\"chat-send-failed\");\n if (onSend) await onSend(text, replyTo); // optional mirror\n },\n [r, onSend],\n );\n return (\n <ChatShellView\n {...props}\n resolved={{\n allowed: r.allowed,\n isInitialLoading: r.isInitialLoading,\n messages: r.messages,\n fetchOlder: r.fetchOlder,\n hasOlder: r.hasOlder,\n isFetchingOlder: r.isFetchingOlder,\n send,\n retrySend: r.retrySend,\n readAck: r.readAck,\n status: r.status,\n typing: r.typing,\n signalTyping: r.signalTyping,\n }}\n />\n );\n}\n\nfunction ChatShellView(props: ChatShellProps & { resolved: ResolvedChat }) {\n const {\n config,\n header,\n renderMessage,\n messageRenderers,\n handlers,\n attachmentConfig,\n sendAttachment,\n composerPlaceholder,\n autoFocus = false,\n renderAboveMessages,\n renderBelowMessages,\n renderFooter,\n onSenderClick,\n composerDisabled,\n fillHeight = false,\n scrollClassName,\n resolved,\n } = props;\n\n const { allowed, isInitialLoading, messages, fetchOlder, hasOlder, isFetchingOlder, send, retrySend, readAck, status, typing, signalTyping } = resolved;\n\n const [showScrollToBottom, setShowScrollToBottom] = React.useState(false);\n const [atBottom, setAtBottom] = React.useState(true);\n const [focused, setFocused] = React.useState(true);\n const lastAckedRef = React.useRef(0);\n const leaseHeldRef = React.useRef(false);\n\n // The latest authoritative seq currently rendered (optimistic rows have no seq).\n const latestSeq = React.useMemo(() => {\n let max = 0;\n for (const m of messages) {\n const seq = typeof m.meta?.seq === \"number\" ? (m.meta?.seq as number) : 0;\n if (seq > max) max = seq;\n }\n return max;\n }, [messages]);\n\n // Track tab focus/visibility so the read-ack \"focused\" lease releases on blur/hide.\n React.useEffect(() => {\n const compute = () =>\n typeof document === \"undefined\" ||\n (document.visibilityState !== \"hidden\" && (typeof document.hasFocus !== \"function\" || document.hasFocus()));\n setFocused(compute());\n const onChange = () => setFocused(compute());\n window.addEventListener(\"focus\", onChange);\n window.addEventListener(\"blur\", onChange);\n document.addEventListener(\"visibilitychange\", onChange);\n return () => {\n window.removeEventListener(\"focus\", onChange);\n window.removeEventListener(\"blur\", onChange);\n document.removeEventListener(\"visibilitychange\", onChange);\n };\n }, []);\n\n // C-B6: advance the authoritative read cursor when the reader is actually at the\n // latest message (channel mounted + tab focused + scrolled to bottom); release the\n // focus lease exactly once when they stop. No-op on the firestore path (no readAck).\n React.useEffect(() => {\n if (!readAck || latestSeq <= 0) return;\n if (focused && atBottom) {\n leaseHeldRef.current = true;\n if (latestSeq > lastAckedRef.current) {\n // Advance the local marker ONLY if the frame was actually sent — a drop during a\n // disconnect window must stay un-acked so it retries (the client also re-sends the\n // cursor on reconnect). (M2)\n if (readAck(latestSeq, true)) lastAckedRef.current = latestSeq;\n }\n } else if (leaseHeldRef.current) {\n leaseHeldRef.current = false;\n readAck(latestSeq, false);\n }\n }, [readAck, latestSeq, atBottom, focused]);\n\n // In fill mode the Card flexes to its parent's height and the message list fills the\n // space between header and composer; otherwise it's the default natural-height card.\n const cardClassName = fillHeight ? \"w-full flex flex-col h-full min-h-0\" : \"w-full\";\n const contentClassName = fillHeight ? \"p-0 flex-1 min-h-0 flex flex-col\" : \"p-0\";\n\n if (!allowed) {\n return <div className=\"p-4 text-sm opacity-70\">You don't have access to this thread.</div>;\n }\n\n if (isInitialLoading) {\n // Before the first authoritative snapshot/history result, show an HONEST working\n // state — a visible \"Opening chat…\" spinner in a polite live region — NOT a blank\n // parchment/skeleton box (which read as an empty chat) and NOT the reconnect banner\n // (which must not compete with the initial-opening message). Only the MessageList\n // region is replaced: the consumer's header + actions + the three render-slots +\n // footer all still render, because a consumer that hangs critical controls off those\n // slots (e.g. the guild-invite binding-agreement Agree/Decline/stake-share controls)\n // must work whether or not realtime chat has connected yet (F4.1).\n return (\n <Card className={cardClassName}>\n {(header || handlers) && (\n <CardHeader className=\"flex flex-row items-start justify-between gap-4 space-y-0\">\n <div>{header}</div>\n <div className=\"flex flex-col items-end gap-2\">\n <ThreadActions threadId={config.threadId} isAdmin={config.isAdmin} handlers={handlers} />\n </div>\n </CardHeader>\n )}\n {renderAboveMessages && (\n <div className=\"border-b\">{renderAboveMessages()}</div>\n )}\n <CardContent className={contentClassName}>\n <div\n className=\"flex flex-col items-center justify-center gap-2 py-12 text-sm text-muted-foreground\"\n role=\"status\"\n aria-live=\"polite\"\n >\n <Loader2 className=\"h-5 w-5 animate-spin\" aria-hidden=\"true\" />\n <span>Opening chat…</span>\n </div>\n </CardContent>\n {renderBelowMessages && (\n <div className=\"border-t\">{renderBelowMessages()}</div>\n )}\n {renderFooter ? (\n <CardFooter className=\"border-t\">{renderFooter()}</CardFooter>\n ) : (\n <CardFooter className=\"border-t\">\n <Skeleton className=\"h-10 w-full\" />\n </CardFooter>\n )}\n </Card>\n );\n }\n\n return (\n <Card className={cardClassName}>\n {(header || handlers) && (\n <CardHeader className=\"flex flex-row items-start justify-between gap-4 space-y-0\">\n <div>{header}</div>\n <div className=\"flex flex-col items-end gap-2\">\n <ThreadActions threadId={config.threadId} isAdmin={config.isAdmin} handlers={handlers} />\n </div>\n </CardHeader>\n )}\n\n {/* R13: a disconnected/reconnecting banner so a configured-but-unreachable Worker\n never presents a healthy-looking empty thread. Only the realtime path sets `status`. */}\n <ChatConnectionBanner status={status} />\n\n {renderAboveMessages && (\n <div className=\"border-b\">{renderAboveMessages()}</div>\n )}\n\n <CardContent className={contentClassName}>\n <MessageList\n messages={messages}\n currentUserId={config.currentUserId}\n isAdmin={config.isAdmin}\n isFetchingOlder={isFetchingOlder}\n hasOlder={hasOlder}\n onLoadOlder={() => fetchOlder()}\n renderMessage={renderMessage}\n messageRenderers={messageRenderers}\n showScrollToBottom={showScrollToBottom}\n onScrollToBottom={() => setShowScrollToBottom(false)}\n onAtBottomChange={setAtBottom}\n fillHeight={fillHeight}\n scrollClassName={scrollClassName}\n handlers={handlers}\n onSenderClick={onSenderClick}\n onRetrySend={retrySend}\n />\n </CardContent>\n\n {renderBelowMessages && (\n <div className=\"border-t\">{renderBelowMessages()}</div>\n )}\n\n {renderFooter ? (\n <CardFooter className=\"border-t\">\n {renderFooter()}\n </CardFooter>\n ) : (\n <CardFooter className=\"border-t flex-col items-stretch gap-1\">\n {/* R14: typing dots above the composer (realtime only — `typing` is undefined on firestore). */}\n <TypingIndicator typing={typing} />\n <KeyboardAvoidingView padding offset={8} className=\"w-full\">\n <Composer\n onSend={send}\n onTyping={signalTyping}\n attachmentConfig={attachmentConfig}\n sendAttachment={sendAttachment}\n disabled={composerDisabled}\n autoFocus={autoFocus}\n placeholder={composerPlaceholder}\n mentionConfig={config.mentionConfig}\n />\n </KeyboardAvoidingView>\n </CardFooter>\n )}\n </Card>\n );\n}\n\n/**\n * R13 — connection-state banner. The realtime path sets `status`; once the first\n * snapshot has loaded (ChatShellView is past `isInitialLoading`), a dropped/unreachable\n * socket surfaces here instead of a silently-empty thread. Renders nothing on the\n * firestore path (`status` undefined) or while live (`open`).\n */\nfunction ChatConnectionBanner({ status }: { status?: string }) {\n if (status !== \"reconnecting\" && status !== \"closed\") return null;\n return (\n <div\n className=\"px-3 py-1.5 text-xs text-center bg-muted text-muted-foreground border-b\"\n role=\"status\"\n aria-live=\"polite\"\n >\n {status === \"reconnecting\"\n ? \"Reconnecting… messages may be delayed.\"\n : \"Disconnected — messages may be out of date.\"}\n </div>\n );\n}\n\n/** R14 — typing indicator. `typing` is the set of OTHER users typing (the transport\n * excludes the local user). Generic copy (no name resolution) for v1. */\nfunction TypingIndicator({ typing }: { typing?: string[] }) {\n if (!typing || typing.length === 0) return null;\n return (\n <div className=\"text-xs italic text-muted-foreground\" aria-live=\"polite\">\n {typing.length === 1 ? \"Someone is typing…\" : \"Several people are typing…\"}\n </div>\n );\n}\n"]}
|
|
1
|
+
{"version":3,"file":"ChatShell.js","sourceRoot":"","sources":["../../src/ui/ChatShell.tsx"],"names":[],"mappings":"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","sourcesContent":["\"use client\";\n\nimport * as React from \"react\";\nimport type { ChatMessageV1, SendAttachmentFn, ModerationHandlers } from \"@ttt-productions/chat-core\";\nimport type {\n ChatCoreConfig,\n ChatAttachmentConfig,\n MessageRendererRegistry,\n} from \"../types.js\";\nimport { Card, CardHeader, CardContent, CardFooter, Skeleton } from \"@ttt-productions/ui-core/react\";\nimport { KeyboardAvoidingView } from \"@ttt-productions/mobile-core/react\";\nimport { Loader2 } from \"lucide-react\";\nimport { useChatMessages } from \"../hooks/useChatMessages.js\";\nimport { useRealtimeChatMessages } from \"../realtime/useRealtimeChatMessages.js\";\nimport type { RealtimeChatClient } from \"../realtime/transport.js\";\nimport { MessageList } from \"./MessageList.js\";\nimport { Composer } from \"./Composer.js\";\nimport type { ComposerHandle } from \"./Composer.js\";\nimport { ThreadActions } from \"./menus.js\";\n\nexport type ChatShellProps = {\n config: ChatCoreConfig;\n\n // Header\n header?: React.ReactNode;\n\n // Send handler — text-only. On the realtime transport this is OPTIONAL: the\n // shell sends through the DO socket. If provided, it is still called (so a\n // consumer can mirror to analytics), but the socket send is authoritative.\n onSend?: (text: string, replyTo?: ChatMessageV1[\"replyTo\"]) => Promise<void>;\n\n // Message rendering\n renderMessage?: (m: any) => React.ReactNode;\n messageRenderers?: MessageRendererRegistry;\n handlers?: ModerationHandlers;\n\n // Composer attachment config\n attachmentConfig?: ChatAttachmentConfig;\n sendAttachment?: SendAttachmentFn;\n composerPlaceholder?: string;\n autoFocus?: boolean;\n\n // Three render slots\n renderAboveMessages?: () => React.ReactNode;\n renderBelowMessages?: () => React.ReactNode;\n /** If provided, replaces the Composer entirely */\n renderFooter?: () => React.ReactNode;\n\n // Sender interaction\n onSenderClick?: (senderId: string, displayName: string) => void;\n\n // Disable\n composerDisabled?: boolean;\n\n /** Fill the parent's height instead of the default fixed-height card. Lays the Card out\n * as `flex flex-col h-full` with the message list flexing to fill (scrolls inside), so a\n * full-height page panel doesn't grow unbounded or waste space on a fixed 400px box.\n * The consumer must give ChatShell a bounded-height parent (e.g. an `h-full` panel). */\n fillHeight?: boolean;\n /** Advanced override for the scrollable region's class (forwarded to MessageList).\n * Usually unnecessary — prefer `fillHeight`. */\n scrollClassName?: string;\n};\n\n/** The resolved data a transport hook hands the presentational view. */\ntype ResolvedChat = {\n allowed: boolean;\n isInitialLoading: boolean;\n messages: ChatMessageV1[];\n fetchOlder: () => Promise<void>;\n hasOlder: boolean;\n isFetchingOlder: boolean;\n /** The send handler the Composer calls (socket send on realtime, prop on firestore). */\n send: (text: string, replyTo?: ChatMessageV1[\"replyTo\"]) => Promise<void>;\n /** Retry a failed realtime send by its original clientMessageId (realtime only;\n * absent on firestore). Wired to MessageItemDefault's failed-bubble retry. */\n retrySend?: (clientMessageId: string) => void;\n /** Advance the authoritative read cursor (realtime only; absent on the firestore path).\n * Returns whether the ack was actually sent so the caller advances its local cursor only on success (M2). */\n readAck?: (latestSeq: number, focused: boolean) => boolean;\n /** Realtime connection status — drives the disconnected banner (R13). Absent on firestore. */\n status?: string;\n /** uids currently typing (realtime only) — drives the typing indicator (R14). */\n typing?: string[];\n /** Signal the local user is typing (coalesced by the transport). Wired to the Composer (R14). */\n signalTyping?: () => void;\n};\n\n/**\n * ChatShell dispatches on `config.transport`. Hooks must be unconditional, so the\n * two transports live in separate inner components (each calls exactly ONE data\n * hook) and both render the shared presentational `ChatShellView`. The default\n * (undefined / 'firestore') transport is byte-for-byte the previous behavior.\n */\nexport function ChatShell(props: ChatShellProps) {\n if (props.config.transport === \"realtime\") {\n return <RealtimeChatShell {...props} />;\n }\n return <FirestoreChatShell {...props} />;\n}\n\nfunction FirestoreChatShell(props: ChatShellProps) {\n const { config, onSend } = props;\n const r = useChatMessages(config);\n const send = React.useCallback(\n async (text: string, replyTo?: ChatMessageV1[\"replyTo\"]) => {\n if (onSend) await onSend(text, replyTo);\n },\n [onSend],\n );\n return <ChatShellView {...props} resolved={{ ...r, send }} />;\n}\n\nfunction RealtimeChatShell(props: ChatShellProps) {\n const { config, onSend } = props;\n const client = config.realtime?.client as RealtimeChatClient | undefined;\n if (!client) {\n throw new Error(\n \"[ChatShell] transport 'realtime' requires config.realtime.client (createRealtimeChatClient(...))\",\n );\n }\n const r = useRealtimeChatMessages(client);\n const send = React.useCallback(\n async (text: string, replyTo?: ChatMessageV1[\"replyTo\"]) => {\n // Map the UI replyTo (messageId-based) to the wire replyTo (seq + preview).\n const wireReply =\n replyTo && replyTo.messageId\n ? { messageSeq: Number(replyTo.messageId), preview: replyTo.messagePreview ?? \"\" }\n : null;\n const ok = r.send(text, Number.isFinite(wireReply?.messageSeq) ? wireReply : null);\n // C-B8: surface a closed-socket failure so the Composer keeps the user's text\n // instead of clearing it on a no-op send.\n if (!ok) throw new Error(\"chat-send-failed\");\n if (onSend) await onSend(text, replyTo); // optional mirror\n },\n [r, onSend],\n );\n return (\n <ChatShellView\n {...props}\n resolved={{\n allowed: r.allowed,\n isInitialLoading: r.isInitialLoading,\n messages: r.messages,\n fetchOlder: r.fetchOlder,\n hasOlder: r.hasOlder,\n isFetchingOlder: r.isFetchingOlder,\n send,\n retrySend: r.retrySend,\n readAck: r.readAck,\n status: r.status,\n typing: r.typing,\n signalTyping: r.signalTyping,\n }}\n />\n );\n}\n\nfunction ChatShellView(props: ChatShellProps & { resolved: ResolvedChat }) {\n const {\n config,\n header,\n renderMessage,\n messageRenderers,\n handlers,\n attachmentConfig,\n sendAttachment,\n composerPlaceholder,\n autoFocus = false,\n renderAboveMessages,\n renderBelowMessages,\n renderFooter,\n onSenderClick,\n composerDisabled,\n fillHeight = false,\n scrollClassName,\n resolved,\n } = props;\n\n const { allowed, isInitialLoading, messages, fetchOlder, hasOlder, isFetchingOlder, send, retrySend, readAck, status, typing, signalTyping } = resolved;\n\n const [showScrollToBottom, setShowScrollToBottom] = React.useState(false);\n const [atBottom, setAtBottom] = React.useState(true);\n const [focused, setFocused] = React.useState(true);\n const lastAckedRef = React.useRef(0);\n const leaseHeldRef = React.useRef(false);\n\n // A terminally-failed attachment bubble re-opens the canonical picker through the\n // Composer's imperative handle. The action is offered only when attachment support\n // is actually configured AND the default Composer is rendered (a custom renderFooter\n // replaces it, so the picker would not exist). The new file then rides the ordinary\n // Composer → guarded-upload flow with a fresh id — never reusing a storage path or\n // mutating the terminal row.\n const composerRef = React.useRef<ComposerHandle>(null);\n const attachmentSupported = Boolean(attachmentConfig && sendAttachment && !renderFooter);\n const handleRetryAttachment = React.useCallback(() => {\n composerRef.current?.openAttachmentSelector();\n }, []);\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't have access to this thread.</div>;\n }\n\n if (isInitialLoading) {\n // Before the first authoritative snapshot/history result, show an HONEST working\n // state — a visible \"Opening chat…\" spinner in a polite live region — NOT a blank\n // parchment/skeleton box (which read as an empty chat) and NOT the reconnect banner\n // (which must not compete with the initial-opening message). Only the MessageList\n // region is replaced: the consumer's header + actions + the three render-slots +\n // footer all still render, because a consumer that hangs critical controls off those\n // slots (e.g. the guild-invite binding-agreement Agree/Decline/stake-share controls)\n // must work whether or not realtime chat has connected yet (F4.1).\n return (\n <Card className={cardClassName}>\n {(header || handlers) && (\n <CardHeader className=\"flex flex-row items-start justify-between gap-4 space-y-0\">\n <div>{header}</div>\n <div className=\"flex flex-col items-end gap-2\">\n <ThreadActions threadId={config.threadId} isAdmin={config.isAdmin} handlers={handlers} />\n </div>\n </CardHeader>\n )}\n {renderAboveMessages && (\n <div className=\"border-b\">{renderAboveMessages()}</div>\n )}\n <CardContent className={contentClassName}>\n <div\n className=\"flex flex-col items-center justify-center gap-2 py-12 text-sm text-muted-foreground\"\n role=\"status\"\n aria-live=\"polite\"\n >\n <Loader2 className=\"h-5 w-5 animate-spin\" aria-hidden=\"true\" />\n <span>Opening chat…</span>\n </div>\n </CardContent>\n {renderBelowMessages && (\n <div className=\"border-t\">{renderBelowMessages()}</div>\n )}\n {renderFooter ? (\n <CardFooter className=\"border-t\">{renderFooter()}</CardFooter>\n ) : (\n <CardFooter className=\"border-t\">\n <Skeleton className=\"h-10 w-full\" />\n </CardFooter>\n )}\n </Card>\n );\n }\n\n return (\n <Card className={cardClassName}>\n {(header || handlers) && (\n <CardHeader className=\"flex flex-row items-start justify-between gap-4 space-y-0\">\n <div>{header}</div>\n <div className=\"flex flex-col items-end gap-2\">\n <ThreadActions threadId={config.threadId} isAdmin={config.isAdmin} handlers={handlers} />\n </div>\n </CardHeader>\n )}\n\n {/* R13: a disconnected/reconnecting banner so a configured-but-unreachable Worker\n never presents a healthy-looking empty thread. Only the realtime path sets `status`. */}\n <ChatConnectionBanner status={status} />\n\n {renderAboveMessages && (\n <div className=\"border-b\">{renderAboveMessages()}</div>\n )}\n\n <CardContent className={contentClassName}>\n <MessageList\n messages={messages}\n currentUserId={config.currentUserId}\n isAdmin={config.isAdmin}\n isFetchingOlder={isFetchingOlder}\n hasOlder={hasOlder}\n onLoadOlder={() => fetchOlder()}\n renderMessage={renderMessage}\n messageRenderers={messageRenderers}\n showScrollToBottom={showScrollToBottom}\n onScrollToBottom={() => setShowScrollToBottom(false)}\n onAtBottomChange={setAtBottom}\n fillHeight={fillHeight}\n scrollClassName={scrollClassName}\n handlers={handlers}\n onSenderClick={onSenderClick}\n onRetrySend={retrySend}\n onRetryAttachment={attachmentSupported ? handleRetryAttachment : undefined}\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 ref={composerRef}\n onSend={send}\n onTyping={signalTyping}\n attachmentConfig={attachmentConfig}\n sendAttachment={sendAttachment}\n disabled={composerDisabled}\n autoFocus={autoFocus}\n placeholder={composerPlaceholder}\n mentionConfig={config.mentionConfig}\n />\n </KeyboardAvoidingView>\n </CardFooter>\n )}\n </Card>\n );\n}\n\n/**\n * R13 — connection-state banner. The realtime path sets `status`; once the first\n * snapshot has loaded (ChatShellView is past `isInitialLoading`), a dropped/unreachable\n * socket surfaces here instead of a silently-empty thread. Renders nothing on the\n * firestore path (`status` undefined) or while live (`open`).\n */\nfunction ChatConnectionBanner({ status }: { status?: string }) {\n if (status !== \"reconnecting\" && status !== \"closed\") return null;\n return (\n <div\n className=\"px-3 py-1.5 text-xs text-center bg-muted text-muted-foreground border-b\"\n role=\"status\"\n aria-live=\"polite\"\n >\n {status === \"reconnecting\"\n ? \"Reconnecting… messages may be delayed.\"\n : \"Disconnected — messages may be out of date.\"}\n </div>\n );\n}\n\n/** R14 — typing indicator. `typing` is the set of OTHER users typing (the transport\n * excludes the local user). Generic copy (no name resolution) for v1. */\nfunction TypingIndicator({ typing }: { typing?: string[] }) {\n if (!typing || typing.length === 0) return null;\n return (\n <div className=\"text-xs italic text-muted-foreground\" aria-live=\"polite\">\n {typing.length === 1 ? \"Someone is typing…\" : \"Several people are typing…\"}\n </div>\n );\n}\n"]}
|
package/dist/ui/Composer.d.ts
CHANGED
|
@@ -27,5 +27,19 @@ export type ComposerProps = {
|
|
|
27
27
|
placeholder?: string;
|
|
28
28
|
mentionConfig?: import('../types.js').ChatMentionConfig;
|
|
29
29
|
};
|
|
30
|
-
|
|
30
|
+
/**
|
|
31
|
+
* Imperative handle exposed by {@link Composer} via `ref`. Additive — ref-less
|
|
32
|
+
* usage is unaffected. ChatShell holds this ref so a failed-attachment bubble's
|
|
33
|
+
* "Attach again" action can re-open the canonical picker.
|
|
34
|
+
*/
|
|
35
|
+
export type ComposerHandle = {
|
|
36
|
+
/**
|
|
37
|
+
* Open the attachment picker, delegating to the underlying MediaInput's
|
|
38
|
+
* `openSelection()`. Must be called synchronously from a user gesture. No-op
|
|
39
|
+
* when attachments are not configured (the MediaInput is not mounted) or the
|
|
40
|
+
* composer is disabled/sending (MediaInput's own gate no-ops it).
|
|
41
|
+
*/
|
|
42
|
+
openAttachmentSelector: () => void;
|
|
43
|
+
};
|
|
44
|
+
export declare const Composer: React.ForwardRefExoticComponent<ComposerProps & React.RefAttributes<ComposerHandle>>;
|
|
31
45
|
//# sourceMappingURL=Composer.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Composer.d.ts","sourceRoot":"","sources":["../../src/ui/Composer.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAU/B,OAAO,KAAK,EAAkB,aAAa,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAC;AAClG,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAiCxD,MAAM,MAAM,aAAa,GAAG;IAC1B;;;OAGG;IACH,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,aAAa,CAAC,SAAS,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAE5E;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IAGtB,gBAAgB,CAAC,EAAE,oBAAoB,CAAC;IACxC;;;;;;OAMG;IACH,cAAc,CAAC,EAAE,gBAAgB,CAAC;IAElC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,aAAa,CAAC,EAAE,OAAO,aAAa,EAAE,iBAAiB,CAAC;CACzD,CAAC;AAEF,
|
|
1
|
+
{"version":3,"file":"Composer.d.ts","sourceRoot":"","sources":["../../src/ui/Composer.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAU/B,OAAO,KAAK,EAAkB,aAAa,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAC;AAClG,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAiCxD,MAAM,MAAM,aAAa,GAAG;IAC1B;;;OAGG;IACH,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,aAAa,CAAC,SAAS,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAE5E;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IAGtB,gBAAgB,CAAC,EAAE,oBAAoB,CAAC;IACxC;;;;;;OAMG;IACH,cAAc,CAAC,EAAE,gBAAgB,CAAC;IAElC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,aAAa,CAAC,EAAE,OAAO,aAAa,EAAE,iBAAiB,CAAC;CACzD,CAAC;AAEF;;;;GAIG;AACH,MAAM,MAAM,cAAc,GAAG;IAC3B;;;;;OAKG;IACH,sBAAsB,EAAE,MAAM,IAAI,CAAC;CACpC,CAAC;AAEF,eAAO,MAAM,QAAQ,sFAyOnB,CAAC"}
|
package/dist/ui/Composer.js
CHANGED
|
@@ -38,7 +38,7 @@ function getSelectedAttachmentLabel(type) {
|
|
|
38
38
|
default: return "File selected";
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
|
-
export function Composer(props) {
|
|
41
|
+
export const Composer = React.forwardRef(function Composer(props, forwardedRef) {
|
|
42
42
|
const { onSend, onTyping, attachmentConfig, sendAttachment, disabled, autoFocus = false, placeholder = "Type a message...", } = props;
|
|
43
43
|
const [text, setText] = React.useState("");
|
|
44
44
|
const [pendingFile, setPendingFile] = React.useState(null);
|
|
@@ -47,6 +47,7 @@ export function Composer(props) {
|
|
|
47
47
|
const [sendError, setSendError] = React.useState(null);
|
|
48
48
|
const ref = React.useRef(null);
|
|
49
49
|
const abortControllerRef = React.useRef(null);
|
|
50
|
+
const mediaInputRef = React.useRef(null);
|
|
50
51
|
const guardedUpload = useGuardedUpload();
|
|
51
52
|
// Navigation guard for the in-flight send: a send that has left the composer
|
|
52
53
|
// but not yet committed is killable by navigation/sign-out with zero warning
|
|
@@ -83,6 +84,15 @@ export function Composer(props) {
|
|
|
83
84
|
const handleCancelUpload = React.useCallback(() => {
|
|
84
85
|
abortControllerRef.current?.abort();
|
|
85
86
|
}, []);
|
|
87
|
+
// Additive imperative handle: re-open the canonical attachment picker. Delegates
|
|
88
|
+
// straight to MediaInput's openSelection so onBeforeSelect / disabled / validation
|
|
89
|
+
// / crop are all honored (and the disabled/sending gate is MediaInput's — it
|
|
90
|
+
// receives `disabled`/`isLoading` below). No-op when attachments aren't configured
|
|
91
|
+
// (MediaInput is unmounted → ref is null).
|
|
92
|
+
const openAttachmentSelector = React.useCallback(() => {
|
|
93
|
+
mediaInputRef.current?.openSelection();
|
|
94
|
+
}, []);
|
|
95
|
+
React.useImperativeHandle(forwardedRef, () => ({ openAttachmentSelector }), [openAttachmentSelector]);
|
|
86
96
|
const send = async () => {
|
|
87
97
|
const v = text.trim();
|
|
88
98
|
if (!v && !pendingFile)
|
|
@@ -167,6 +177,6 @@ export function Composer(props) {
|
|
|
167
177
|
e.preventDefault();
|
|
168
178
|
send();
|
|
169
179
|
}
|
|
170
|
-
} }), _jsx(Button, { type: "button", variant: "default", disabled: isDisabled || (!text.trim() && !pendingFile), onClick: send, children: isSending ? _jsx(Loader2, { className: "h-4 w-4 animate-spin" }) : "Send" })] }), attachEnabled && attachmentConfig && (_jsx("div", { className: "mt-2", children: _jsx(MediaInput, { spec: attachmentConfig.attachmentSpec, onChange: handleFileSelected, selectedFile: pendingFile, isLoading: isSending, uploadState: uploadState, disabled: isDisabled, buttonLabel: "Attach file", onClear: () => setPendingFile(null), onCancel: handleCancelUpload, className: "w-full" }) }))] }));
|
|
171
|
-
}
|
|
180
|
+
} }), _jsx(Button, { type: "button", variant: "default", disabled: isDisabled || (!text.trim() && !pendingFile), onClick: send, children: isSending ? _jsx(Loader2, { className: "h-4 w-4 animate-spin" }) : "Send" })] }), attachEnabled && attachmentConfig && (_jsx("div", { className: "mt-2", children: _jsx(MediaInput, { ref: mediaInputRef, spec: attachmentConfig.attachmentSpec, onChange: handleFileSelected, selectedFile: pendingFile, isLoading: isSending, uploadState: uploadState, disabled: isDisabled, buttonLabel: "Attach file", onClear: () => setPendingFile(null), onCancel: handleCancelUpload, className: "w-full" }) }))] }));
|
|
181
|
+
});
|
|
172
182
|
//# sourceMappingURL=Composer.js.map
|
package/dist/ui/Composer.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Composer.js","sourceRoot":"","sources":["../../src/ui/Composer.tsx"],"names":[],"mappings":"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","sourcesContent":["\"use client\";\n\nimport * as React from \"react\";\nimport { Button, Textarea } from \"@ttt-productions/ui-core/react\";\nimport { cn } from \"@ttt-productions/ui-core\";\nimport { ensureFileWithContentType } from \"@ttt-productions/file-input\";\nimport { MediaInput } from \"@ttt-productions/file-input/react\";\nimport type { MediaInputChangePayload } from \"@ttt-productions/file-input\";\nimport type { UploadState } from \"@ttt-productions/media-schemas\";\nimport { useGuardedUpload } from \"@ttt-productions/upload-ui/react/upload\";\nimport { useOptionalLocalUploadGuard } from \"@ttt-productions/upload-ui/react/guard\";\nimport { Loader2, X, FileText, ImageIcon, VideoIcon, MicIcon } from \"lucide-react\";\nimport type { ChatAttachment, ChatMessageV1, SendAttachmentFn } from \"@ttt-productions/chat-core\";\nimport type { ChatAttachmentConfig } from \"../types.js\";\nimport { useMentionAutocomplete } from \"../mentions/use-mention-autocomplete.js\";\nimport { MentionAutocomplete } from \"../mentions/MentionAutocomplete.js\";\n\nfunction genId(): string {\n return `${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`;\n}\n\nfunction getAttachmentType(file: File): ChatAttachment[\"type\"] {\n if (file.type.startsWith(\"image/\")) return \"image\";\n if (file.type.startsWith(\"video/\")) return \"video\";\n if (file.type.startsWith(\"audio/\")) return \"audio\";\n return \"text\";\n}\n\nfunction AttachmentTypeIcon({ type }: { type: ChatAttachment[\"type\"] }) {\n switch (type) {\n case \"image\": return <ImageIcon className=\"h-4 w-4 shrink-0\" />;\n case \"video\": return <VideoIcon className=\"h-4 w-4 shrink-0\" />;\n case \"audio\": return <MicIcon className=\"h-4 w-4 shrink-0\" />;\n default: return <FileText className=\"h-4 w-4 shrink-0\" />;\n }\n}\n\nfunction getSelectedAttachmentLabel(type: ChatAttachment[\"type\"]): string {\n switch (type) {\n case \"image\": return \"Image selected\";\n case \"video\": return \"Video selected\";\n case \"audio\": return \"Audio selected\";\n default: return \"File selected\";\n }\n}\n\nexport type ComposerProps = {\n /**\n * Text-only send. Composer calls this when there is no pending attachment.\n * Consumer creates the message doc. Return value is ignored.\n */\n onSend: (text: string, replyTo?: ChatMessageV1[\"replyTo\"]) => Promise<void>;\n\n /**\n * Signal the local user is typing (R14). Called as the user edits the textarea; the\n * realtime transport coalesces to ≤1 frame per ~2s, so calling per keystroke is safe.\n * Optional — absent on the firestore transport (no typing broadcast).\n */\n onTyping?: () => void;\n\n // Attachment support\n attachmentConfig?: ChatAttachmentConfig;\n /**\n * Attachment send. Composer uploads the file to Storage first, then calls\n * this with the resulting metadata. Consumer wires it to a backend callable\n * (`startUpload`) that writes the pendingMedia doc. The processor creates\n * the message doc after moderation succeeds.\n * Required for attachment functionality.\n */\n sendAttachment?: SendAttachmentFn;\n\n disabled?: boolean;\n autoFocus?: boolean;\n placeholder?: string;\n mentionConfig?: import('../types.js').ChatMentionConfig;\n};\n\nexport function Composer(props: ComposerProps) {\n const {\n onSend,\n onTyping,\n attachmentConfig,\n sendAttachment,\n disabled,\n autoFocus = false,\n placeholder = \"Type a message...\",\n } = props;\n\n const [text, setText] = React.useState(\"\");\n const [pendingFile, setPendingFile] = React.useState<File | null>(null);\n const [isSending, setIsSending] = React.useState(false);\n const [uploadState, setUploadState] = React.useState<UploadState | null>(null);\n const [sendError, setSendError] = React.useState<string | null>(null);\n const ref = React.useRef<HTMLTextAreaElement>(null);\n const abortControllerRef = React.useRef<AbortController | null>(null);\n const guardedUpload = useGuardedUpload();\n // Navigation guard for the in-flight send: a send that has left the composer\n // but not yet committed is killable by navigation/sign-out with zero warning\n // (observed live: the callable aborted, the message silently never delivered).\n // Registering with the app's one guarded-navigation system makes beforeunload\n // fire on hard navs and lets GuardedLink-style navigation confirm first.\n // Optional accessor — consumers without the provider degrade to old behavior.\n const navigationGuard = useOptionalLocalUploadGuard();\n\n const mentionApi = useMentionAutocomplete({\n textareaRef: ref as React.RefObject<HTMLTextAreaElement>,\n value: text,\n onChange: setText,\n providers: props.mentionConfig?.providers ?? [],\n context: props.mentionConfig?.context,\n recent: props.mentionConfig?.recent,\n trigger: props.mentionConfig?.trigger,\n minQueryLength: props.mentionConfig?.minQueryLength,\n searchDebounceMs: props.mentionConfig?.searchDebounceMs,\n });\n\n const attachEnabled = Boolean(attachmentConfig && sendAttachment);\n\n // focus stability: never steal focus unless explicitly enabled\n React.useEffect(() => {\n if (!autoFocus) return;\n ref.current?.focus();\n }, [autoFocus]);\n\n const handleFileSelected = React.useCallback((payload: MediaInputChangePayload) => {\n if (payload.error || !payload.file) {\n setPendingFile(null);\n return;\n }\n setPendingFile(payload.file);\n }, []);\n\n const handleCancelUpload = React.useCallback(() => {\n abortControllerRef.current?.abort();\n }, []);\n\n const send = async () => {\n const v = text.trim();\n if (!v && !pendingFile) return;\n\n setIsSending(true);\n setSendError(null);\n const sendGuardId = `chat-send-${genId()}`;\n navigationGuard?.registerUpload(sendGuardId);\n\n try {\n if (pendingFile && attachmentConfig && sendAttachment) {\n // Attachment path: upload first via canonical guarded upload, then register.\n const uuid = genId();\n const { uploadAdapter, userId, storage } = attachmentConfig;\n const storagePath = uploadAdapter.buildUploadPath({ userId, attachmentId: uuid });\n const extraMetadata = uploadAdapter.buildUploadMetadata?.({ userId, attachmentId: uuid }) ?? {};\n\n const fileToUpload = ensureFileWithContentType(pendingFile);\n const attType = getAttachmentType(fileToUpload);\n\n const controller = new AbortController();\n abortControllerRef.current = controller;\n\n await guardedUpload({\n storage,\n path: storagePath,\n file: fileToUpload,\n metadata: { contentType: fileToUpload.type, ...extraMetadata },\n uploadId: uuid,\n onProgress: (state) => setUploadState(state),\n signal: controller.signal,\n });\n\n await sendAttachment({\n text: mentionApi.getValueWithTokens(),\n attachment: {\n attachmentId: uuid,\n storagePath,\n originalFileName: fileToUpload.name,\n type: attType,\n size: fileToUpload.size,\n },\n });\n } else {\n // Text-only path.\n await onSend(mentionApi.getValueWithTokens());\n }\n\n setText(\"\");\n setPendingFile(null);\n mentionApi.close();\n } catch (err) {\n // AbortError = user-initiated cancel via the MediaInput cancel button.\n // Clear local state and swallow — not a real failure.\n if (err instanceof Error && err.name === 'AbortError') {\n setPendingFile(null);\n return;\n }\n // C-B8: a failed send (e.g. the realtime socket was closed) must NOT clear the\n // user's text — `setText(\"\")` only runs on the success path above — and must NOT\n // re-throw into the click handler (an unhandled rejection). Surface it so the\n // user can retry with their text intact.\n console.error(\"[Composer] Send failed:\", err);\n setPendingFile(null);\n setSendError(\"Couldn't send. Check your connection and try again.\");\n } finally {\n navigationGuard?.unregisterUpload(sendGuardId);\n abortControllerRef.current = null;\n setIsSending(false);\n setUploadState(null);\n }\n };\n\n const isDisabled = disabled || isSending;\n\n return (\n <div className=\"chat-composer\">\n {pendingFile && !isSending && (\n <div className=\"chat-composer-file-preview\">\n <AttachmentTypeIcon type={getAttachmentType(pendingFile)} />\n <span className=\"chat-composer-file-name\">\n {getSelectedAttachmentLabel(getAttachmentType(pendingFile))}\n </span>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon\"\n className=\"h-6 w-6 shrink-0\"\n onClick={() => setPendingFile(null)}\n >\n <X className=\"h-3 w-3\" />\n </Button>\n </div>\n )}\n\n {props.mentionConfig && mentionApi.state.open && (\n <div className=\"relative\">\n <div className=\"absolute bottom-full left-0 mb-1 z-10\">\n <MentionAutocomplete\n state={mentionApi.state}\n onSelect={(ref) => mentionApi.insertMention(ref)}\n />\n </div>\n </div>\n )}\n\n {sendError && (\n <div className=\"px-1 pb-1 text-sm text-destructive\" role=\"alert\">\n {sendError}\n </div>\n )}\n\n <div className=\"flex items-end gap-2\">\n <Textarea\n ref={ref}\n value={text}\n onChange={(e) => {\n setText(e.target.value);\n if (sendError) setSendError(null);\n // R14: broadcast a typing signal (transport-coalesced) while there is content.\n if (onTyping && e.target.value.trim().length > 0) onTyping();\n }}\n placeholder={placeholder}\n disabled={isDisabled}\n rows={1}\n className={cn(\"min-h-[40px] resize-none\")}\n onKeyDown={(e) => {\n if (mentionApi.handleKeyDown(e)) return;\n if (e.key === \"Enter\" && !e.shiftKey) {\n e.preventDefault();\n send();\n }\n }}\n />\n <Button\n type=\"button\"\n variant=\"default\"\n disabled={isDisabled || (!text.trim() && !pendingFile)}\n onClick={send}\n >\n {isSending ? <Loader2 className=\"h-4 w-4 animate-spin\" /> : \"Send\"}\n </Button>\n </div>\n\n {attachEnabled && attachmentConfig && (\n <div className=\"mt-2\">\n <MediaInput\n spec={attachmentConfig.attachmentSpec}\n onChange={handleFileSelected}\n selectedFile={pendingFile}\n isLoading={isSending}\n uploadState={uploadState}\n disabled={isDisabled}\n buttonLabel=\"Attach file\"\n onClear={() => setPendingFile(null)}\n onCancel={handleCancelUpload}\n className=\"w-full\"\n />\n </div>\n )}\n </div>\n );\n}\n"]}
|
|
1
|
+
{"version":3,"file":"Composer.js","sourceRoot":"","sources":["../../src/ui/Composer.tsx"],"names":[],"mappings":"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","sourcesContent":["\"use client\";\n\nimport * as React from \"react\";\nimport { Button, Textarea } from \"@ttt-productions/ui-core/react\";\nimport { cn } from \"@ttt-productions/ui-core\";\nimport { ensureFileWithContentType } from \"@ttt-productions/file-input\";\nimport { MediaInput } from \"@ttt-productions/file-input/react\";\nimport type { MediaInputChangePayload, MediaInputHandle } from \"@ttt-productions/file-input\";\nimport type { UploadState } from \"@ttt-productions/media-schemas\";\nimport { useGuardedUpload } from \"@ttt-productions/upload-ui/react/upload\";\nimport { useOptionalLocalUploadGuard } from \"@ttt-productions/upload-ui/react/guard\";\nimport { Loader2, X, FileText, ImageIcon, VideoIcon, MicIcon } from \"lucide-react\";\nimport type { ChatAttachment, ChatMessageV1, SendAttachmentFn } from \"@ttt-productions/chat-core\";\nimport type { ChatAttachmentConfig } from \"../types.js\";\nimport { useMentionAutocomplete } from \"../mentions/use-mention-autocomplete.js\";\nimport { MentionAutocomplete } from \"../mentions/MentionAutocomplete.js\";\n\nfunction genId(): string {\n return `${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`;\n}\n\nfunction getAttachmentType(file: File): ChatAttachment[\"type\"] {\n if (file.type.startsWith(\"image/\")) return \"image\";\n if (file.type.startsWith(\"video/\")) return \"video\";\n if (file.type.startsWith(\"audio/\")) return \"audio\";\n return \"text\";\n}\n\nfunction AttachmentTypeIcon({ type }: { type: ChatAttachment[\"type\"] }) {\n switch (type) {\n case \"image\": return <ImageIcon className=\"h-4 w-4 shrink-0\" />;\n case \"video\": return <VideoIcon className=\"h-4 w-4 shrink-0\" />;\n case \"audio\": return <MicIcon className=\"h-4 w-4 shrink-0\" />;\n default: return <FileText className=\"h-4 w-4 shrink-0\" />;\n }\n}\n\nfunction getSelectedAttachmentLabel(type: ChatAttachment[\"type\"]): string {\n switch (type) {\n case \"image\": return \"Image selected\";\n case \"video\": return \"Video selected\";\n case \"audio\": return \"Audio selected\";\n default: return \"File selected\";\n }\n}\n\nexport type ComposerProps = {\n /**\n * Text-only send. Composer calls this when there is no pending attachment.\n * Consumer creates the message doc. Return value is ignored.\n */\n onSend: (text: string, replyTo?: ChatMessageV1[\"replyTo\"]) => Promise<void>;\n\n /**\n * Signal the local user is typing (R14). Called as the user edits the textarea; the\n * realtime transport coalesces to ≤1 frame per ~2s, so calling per keystroke is safe.\n * Optional — absent on the firestore transport (no typing broadcast).\n */\n onTyping?: () => void;\n\n // Attachment support\n attachmentConfig?: ChatAttachmentConfig;\n /**\n * Attachment send. Composer uploads the file to Storage first, then calls\n * this with the resulting metadata. Consumer wires it to a backend callable\n * (`startUpload`) that writes the pendingMedia doc. The processor creates\n * the message doc after moderation succeeds.\n * Required for attachment functionality.\n */\n sendAttachment?: SendAttachmentFn;\n\n disabled?: boolean;\n autoFocus?: boolean;\n placeholder?: string;\n mentionConfig?: import('../types.js').ChatMentionConfig;\n};\n\n/**\n * Imperative handle exposed by {@link Composer} via `ref`. Additive — ref-less\n * usage is unaffected. ChatShell holds this ref so a failed-attachment bubble's\n * \"Attach again\" action can re-open the canonical picker.\n */\nexport type ComposerHandle = {\n /**\n * Open the attachment picker, delegating to the underlying MediaInput's\n * `openSelection()`. Must be called synchronously from a user gesture. No-op\n * when attachments are not configured (the MediaInput is not mounted) or the\n * composer is disabled/sending (MediaInput's own gate no-ops it).\n */\n openAttachmentSelector: () => void;\n};\n\nexport const Composer = React.forwardRef<ComposerHandle, ComposerProps>(function Composer(props, forwardedRef) {\n const {\n onSend,\n onTyping,\n attachmentConfig,\n sendAttachment,\n disabled,\n autoFocus = false,\n placeholder = \"Type a message...\",\n } = props;\n\n const [text, setText] = React.useState(\"\");\n const [pendingFile, setPendingFile] = React.useState<File | null>(null);\n const [isSending, setIsSending] = React.useState(false);\n const [uploadState, setUploadState] = React.useState<UploadState | null>(null);\n const [sendError, setSendError] = React.useState<string | null>(null);\n const ref = React.useRef<HTMLTextAreaElement>(null);\n const abortControllerRef = React.useRef<AbortController | null>(null);\n const mediaInputRef = React.useRef<MediaInputHandle>(null);\n const guardedUpload = useGuardedUpload();\n // Navigation guard for the in-flight send: a send that has left the composer\n // but not yet committed is killable by navigation/sign-out with zero warning\n // (observed live: the callable aborted, the message silently never delivered).\n // Registering with the app's one guarded-navigation system makes beforeunload\n // fire on hard navs and lets GuardedLink-style navigation confirm first.\n // Optional accessor — consumers without the provider degrade to old behavior.\n const navigationGuard = useOptionalLocalUploadGuard();\n\n const mentionApi = useMentionAutocomplete({\n textareaRef: ref as React.RefObject<HTMLTextAreaElement>,\n value: text,\n onChange: setText,\n providers: props.mentionConfig?.providers ?? [],\n context: props.mentionConfig?.context,\n recent: props.mentionConfig?.recent,\n trigger: props.mentionConfig?.trigger,\n minQueryLength: props.mentionConfig?.minQueryLength,\n searchDebounceMs: props.mentionConfig?.searchDebounceMs,\n });\n\n const attachEnabled = Boolean(attachmentConfig && sendAttachment);\n\n // focus stability: never steal focus unless explicitly enabled\n React.useEffect(() => {\n if (!autoFocus) return;\n ref.current?.focus();\n }, [autoFocus]);\n\n const handleFileSelected = React.useCallback((payload: MediaInputChangePayload) => {\n if (payload.error || !payload.file) {\n setPendingFile(null);\n return;\n }\n setPendingFile(payload.file);\n }, []);\n\n const handleCancelUpload = React.useCallback(() => {\n abortControllerRef.current?.abort();\n }, []);\n\n // Additive imperative handle: re-open the canonical attachment picker. Delegates\n // straight to MediaInput's openSelection so onBeforeSelect / disabled / validation\n // / crop are all honored (and the disabled/sending gate is MediaInput's — it\n // receives `disabled`/`isLoading` below). No-op when attachments aren't configured\n // (MediaInput is unmounted → ref is null).\n const openAttachmentSelector = React.useCallback(() => {\n mediaInputRef.current?.openSelection();\n }, []);\n React.useImperativeHandle(forwardedRef, () => ({ openAttachmentSelector }), [openAttachmentSelector]);\n\n const send = async () => {\n const v = text.trim();\n if (!v && !pendingFile) return;\n\n setIsSending(true);\n setSendError(null);\n const sendGuardId = `chat-send-${genId()}`;\n navigationGuard?.registerUpload(sendGuardId);\n\n try {\n if (pendingFile && attachmentConfig && sendAttachment) {\n // Attachment path: upload first via canonical guarded upload, then register.\n const uuid = genId();\n const { uploadAdapter, userId, storage } = attachmentConfig;\n const storagePath = uploadAdapter.buildUploadPath({ userId, attachmentId: uuid });\n const extraMetadata = uploadAdapter.buildUploadMetadata?.({ userId, attachmentId: uuid }) ?? {};\n\n const fileToUpload = ensureFileWithContentType(pendingFile);\n const attType = getAttachmentType(fileToUpload);\n\n const controller = new AbortController();\n abortControllerRef.current = controller;\n\n await guardedUpload({\n storage,\n path: storagePath,\n file: fileToUpload,\n metadata: { contentType: fileToUpload.type, ...extraMetadata },\n uploadId: uuid,\n onProgress: (state) => setUploadState(state),\n signal: controller.signal,\n });\n\n await sendAttachment({\n text: mentionApi.getValueWithTokens(),\n attachment: {\n attachmentId: uuid,\n storagePath,\n originalFileName: fileToUpload.name,\n type: attType,\n size: fileToUpload.size,\n },\n });\n } else {\n // Text-only path.\n await onSend(mentionApi.getValueWithTokens());\n }\n\n setText(\"\");\n setPendingFile(null);\n mentionApi.close();\n } catch (err) {\n // AbortError = user-initiated cancel via the MediaInput cancel button.\n // Clear local state and swallow — not a real failure.\n if (err instanceof Error && err.name === 'AbortError') {\n setPendingFile(null);\n return;\n }\n // C-B8: a failed send (e.g. the realtime socket was closed) must NOT clear the\n // user's text — `setText(\"\")` only runs on the success path above — and must NOT\n // re-throw into the click handler (an unhandled rejection). Surface it so the\n // user can retry with their text intact.\n console.error(\"[Composer] Send failed:\", err);\n setPendingFile(null);\n setSendError(\"Couldn't send. Check your connection and try again.\");\n } finally {\n navigationGuard?.unregisterUpload(sendGuardId);\n abortControllerRef.current = null;\n setIsSending(false);\n setUploadState(null);\n }\n };\n\n const isDisabled = disabled || isSending;\n\n return (\n <div className=\"chat-composer\">\n {pendingFile && !isSending && (\n <div className=\"chat-composer-file-preview\">\n <AttachmentTypeIcon type={getAttachmentType(pendingFile)} />\n <span className=\"chat-composer-file-name\">\n {getSelectedAttachmentLabel(getAttachmentType(pendingFile))}\n </span>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon\"\n className=\"h-6 w-6 shrink-0\"\n onClick={() => setPendingFile(null)}\n >\n <X className=\"h-3 w-3\" />\n </Button>\n </div>\n )}\n\n {props.mentionConfig && mentionApi.state.open && (\n <div className=\"relative\">\n <div className=\"absolute bottom-full left-0 mb-1 z-10\">\n <MentionAutocomplete\n state={mentionApi.state}\n onSelect={(ref) => mentionApi.insertMention(ref)}\n />\n </div>\n </div>\n )}\n\n {sendError && (\n <div className=\"px-1 pb-1 text-sm text-destructive\" role=\"alert\">\n {sendError}\n </div>\n )}\n\n <div className=\"flex items-end gap-2\">\n <Textarea\n ref={ref}\n value={text}\n onChange={(e) => {\n setText(e.target.value);\n if (sendError) setSendError(null);\n // R14: broadcast a typing signal (transport-coalesced) while there is content.\n if (onTyping && e.target.value.trim().length > 0) onTyping();\n }}\n placeholder={placeholder}\n disabled={isDisabled}\n rows={1}\n className={cn(\"min-h-[40px] resize-none\")}\n onKeyDown={(e) => {\n if (mentionApi.handleKeyDown(e)) return;\n if (e.key === \"Enter\" && !e.shiftKey) {\n e.preventDefault();\n send();\n }\n }}\n />\n <Button\n type=\"button\"\n variant=\"default\"\n disabled={isDisabled || (!text.trim() && !pendingFile)}\n onClick={send}\n >\n {isSending ? <Loader2 className=\"h-4 w-4 animate-spin\" /> : \"Send\"}\n </Button>\n </div>\n\n {attachEnabled && attachmentConfig && (\n <div className=\"mt-2\">\n <MediaInput\n ref={mediaInputRef}\n spec={attachmentConfig.attachmentSpec}\n onChange={handleFileSelected}\n selectedFile={pendingFile}\n isLoading={isSending}\n uploadState={uploadState}\n disabled={isDisabled}\n buttonLabel=\"Attach file\"\n onClear={() => setPendingFile(null)}\n onCancel={handleCancelUpload}\n className=\"w-full\"\n />\n </div>\n )}\n </div>\n );\n});\n"]}
|
|
@@ -10,6 +10,11 @@ export type MessageItemDefaultProps = {
|
|
|
10
10
|
* ChatShell on the realtime transport; absent on firestore). Enables the
|
|
11
11
|
* retry affordance on a `meta.sendFailed` bubble. */
|
|
12
12
|
onRetrySend?: (clientMessageId: string) => void;
|
|
13
|
+
/** Re-open the attachment picker for a terminally-failed attachment (wired by
|
|
14
|
+
* ChatShell to the Composer's `openAttachmentSelector`; present only when
|
|
15
|
+
* attachment support is configured). Enables the "Attach again" action on a
|
|
16
|
+
* failed attachment bubble, shown only to the sender. */
|
|
17
|
+
onRetryAttachment?: () => void;
|
|
13
18
|
};
|
|
14
19
|
export declare function MessageItemDefault(props: MessageItemDefaultProps): import("react").JSX.Element;
|
|
15
20
|
//# sourceMappingURL=MessageItemDefault.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MessageItemDefault.d.ts","sourceRoot":"","sources":["../../src/ui/MessageItemDefault.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAkB,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;
|
|
1
|
+
{"version":3,"file":"MessageItemDefault.d.ts","sourceRoot":"","sources":["../../src/ui/MessageItemDefault.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAkB,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAsMpG,MAAM,MAAM,uBAAuB,GAAG;IACpC,CAAC,EAAE,aAAa,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,CAAC,EAAE,kBAAkB,CAAC;IAG9B,cAAc,CAAC,EAAE,OAAO,CAAC;IAGzB,aAAa,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,KAAK,IAAI,CAAC;IAEhE;;0DAEsD;IACtD,WAAW,CAAC,EAAE,CAAC,eAAe,EAAE,MAAM,KAAK,IAAI,CAAC;IAEhD;;;8DAG0D;IAC1D,iBAAiB,CAAC,EAAE,MAAM,IAAI,CAAC;CAEhC,CAAC;AAEF,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,uBAAuB,+BA2GhE"}
|
|
@@ -20,6 +20,17 @@ function getAttachmentLabel(type) {
|
|
|
20
20
|
default: return "File attachment";
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
|
+
// Safe, generic terminal-failure copy per kind. The backend `failureReason` is an
|
|
24
|
+
// INTERNAL diagnostic string and must never reach the UI (it could leak processing
|
|
25
|
+
// internals); it is mapped to this fixed copy instead.
|
|
26
|
+
function getAttachmentFailureCopy(type) {
|
|
27
|
+
switch (type) {
|
|
28
|
+
case "image": return "This image could not be processed.";
|
|
29
|
+
case "video": return "This video could not be processed.";
|
|
30
|
+
case "audio": return "This audio could not be processed.";
|
|
31
|
+
default: return "This file could not be processed.";
|
|
32
|
+
}
|
|
33
|
+
}
|
|
23
34
|
// ============================================
|
|
24
35
|
// Correlated send-rejection copy + retry policy
|
|
25
36
|
// ============================================
|
|
@@ -48,7 +59,7 @@ function sendFailureCopy(code) {
|
|
|
48
59
|
function isRetryableFailure(meta) {
|
|
49
60
|
return meta?.sendRetryable !== false;
|
|
50
61
|
}
|
|
51
|
-
function AttachmentView({ att }) {
|
|
62
|
+
function AttachmentView({ att, mine, onRetryAttachment, }) {
|
|
52
63
|
// Display URL is built at render time by the app-injected resolver from
|
|
53
64
|
// att.mediaAssetId — attachments never store URLs (protected-gateway model).
|
|
54
65
|
const resolveAttachmentUrl = useChatAttachmentUrlResolver();
|
|
@@ -56,15 +67,19 @@ function AttachmentView({ att }) {
|
|
|
56
67
|
// app's ONE display path (recovery adapter, diagnostics, telemetry). Falls
|
|
57
68
|
// back to the package's own MediaViewer when no provider is present.
|
|
58
69
|
const Media = useChatAttachmentMediaComponent() ?? MediaViewer;
|
|
59
|
-
// In-flight placeholder — bytes uploaded, processing/moderation pending.
|
|
60
|
-
// `mediaAssetId` yet. Rendered only to the sender (other participants can't
|
|
61
|
-
// read a pending doc — Firestore rules scope it until `status === 'ready'`).
|
|
70
|
+
// In-flight placeholder — bytes uploaded, backend processing/moderation pending.
|
|
71
|
+
// No `mediaAssetId` yet. Rendered only to the sender (other participants can't
|
|
72
|
+
// read a pending doc — Firestore rules scope it until `status === 'ready'`). The
|
|
73
|
+
// bytes are already uploaded, so the honest copy is "Processing…" (NOT "Sending…").
|
|
62
74
|
if (att.status === "pending") {
|
|
63
|
-
return (_jsxs("div", { className: "chat-attachment-pending", children: [_jsx(Loader2, { className: "h-4 w-4 shrink-0 animate-spin" }), _jsx("span", { className: "truncate", children: getAttachmentLabel(att.type) }), _jsx("span", { className: "chat-attachment-status-label", children: "
|
|
75
|
+
return (_jsxs("div", { className: "chat-attachment-pending", role: "status", "aria-live": "polite", children: [_jsx(Loader2, { className: "h-4 w-4 shrink-0 animate-spin" }), _jsx("span", { className: "truncate", children: getAttachmentLabel(att.type) }), _jsx("span", { className: "chat-attachment-status-label", children: "Processing\u2026" })] }));
|
|
64
76
|
}
|
|
65
|
-
// Processing/moderation rejected the attachment — sender-only.
|
|
77
|
+
// Processing/moderation rejected the attachment — sender-only. The backend
|
|
78
|
+
// `failureReason` is internal and NEVER rendered; a fixed safe per-kind copy is
|
|
79
|
+
// shown instead. The sender can pick a fresh file through the canonical picker
|
|
80
|
+
// ("Attach again"), offered only when the composer wired a retry handler.
|
|
66
81
|
if (att.status === "failed") {
|
|
67
|
-
return (_jsxs("div", { className: "chat-attachment-rejected", children: [_jsx(AlertTriangle, { className: "h-4 w-4 shrink-0" }), _jsx("span", { className: "truncate", children: getAttachmentLabel(att.type) }), _jsx("span", { className: "chat-attachment-
|
|
82
|
+
return (_jsxs("div", { className: "chat-attachment-rejected", role: "alert", children: [_jsx(AlertTriangle, { className: "h-4 w-4 shrink-0 chat-attachment-rejected-icon" }), _jsxs("div", { className: "chat-attachment-rejected-body", children: [_jsx("span", { className: "truncate", children: getAttachmentLabel(att.type) }), _jsx("span", { className: "chat-attachment-rejected-reason", children: getAttachmentFailureCopy(att.type) })] }), mine && onRetryAttachment && (_jsx(Button, { type: "button", variant: "ghost", size: "sm", className: "chat-attachment-retry", onClick: onRetryAttachment, children: "Attach again" }))] }));
|
|
68
83
|
}
|
|
69
84
|
// ready (or legacy/absent status) — resolve the display URL from the asset ref.
|
|
70
85
|
const url = resolveAttachmentUrl(att);
|
|
@@ -77,7 +92,7 @@ function AttachmentView({ att }) {
|
|
|
77
92
|
// "Sending…", NO filename) and do NOT mount the media/download renderer until a
|
|
78
93
|
// non-empty authorized URL exists. Sender-only pending/processing/failed states
|
|
79
94
|
// above are unchanged — this begins only after a terminal-ready row is received.
|
|
80
|
-
return (_jsxs("div", { className: "chat-attachment-pending", children: [_jsx(Loader2, { className: "h-4 w-4 shrink-0 animate-spin" }), _jsx("span", { className: "truncate", children: getAttachmentLabel(att.type) }), _jsx("span", { className: "chat-attachment-status-label", children: "Loading\u2026" })] }));
|
|
95
|
+
return (_jsxs("div", { className: "chat-attachment-pending", role: "status", "aria-live": "polite", children: [_jsx(Loader2, { className: "h-4 w-4 shrink-0 animate-spin" }), _jsx("span", { className: "truncate", children: getAttachmentLabel(att.type) }), _jsx("span", { className: "chat-attachment-status-label", children: "Loading\u2026" })] }));
|
|
81
96
|
}
|
|
82
97
|
if (att.type === "image") {
|
|
83
98
|
return _jsx(Media, { type: "image", url: url, alt: "Image attachment", className: "chat-attachment-media" });
|
|
@@ -105,7 +120,7 @@ function SystemMessage({ m }) {
|
|
|
105
120
|
return (_jsx("div", { className: "chat-system-message", children: m.text && _jsx("span", { children: m.text }) }));
|
|
106
121
|
}
|
|
107
122
|
export function MessageItemDefault(props) {
|
|
108
|
-
const { m, currentUserId, isAdmin, handlers, isContinuation, onSenderClick, onRetrySend } = props;
|
|
123
|
+
const { m, currentUserId, isAdmin, handlers, isContinuation, onSenderClick, onRetrySend, onRetryAttachment } = props;
|
|
109
124
|
const senderName = useResolvedSenderName(m.senderId);
|
|
110
125
|
// System messages render differently
|
|
111
126
|
if (m.isSystemMessage) {
|
|
@@ -126,6 +141,6 @@ export function MessageItemDefault(props) {
|
|
|
126
141
|
return (_jsxs("div", { className: cn("flex flex-col w-fit max-w-[85%]", mine ? "ml-auto items-end" : "mr-auto items-start", isContinuation ? "chat-continuation-gap" : "chat-group-gap"), children: [_jsxs("div", { className: cn("chat-bubble", mine ? "chat-bubble--mine" : "chat-bubble--theirs", sendPending && "chat-bubble--pending", sendFailed && "chat-bubble--failed"), children: [!isContinuation && (_jsxs("div", { className: "flex items-center gap-2 text-xs opacity-80 mb-1", children: [_jsx("span", { className: cn("font-medium", onSenderClick && "cursor-pointer hover:underline"), onClick: onSenderClick ? () => onSenderClick(m.senderId, senderName) : undefined, role: onSenderClick ? "button" : undefined, tabIndex: onSenderClick ? 0 : undefined, onKeyDown: onSenderClick ? (e) => {
|
|
127
142
|
if (e.key === "Enter" || e.key === " ")
|
|
128
143
|
onSenderClick(m.senderId, senderName);
|
|
129
|
-
} : undefined, children: senderName }), _jsx("span", { children: "\u00B7" }), _jsx("span", { children: new Date(m.createdAt).toLocaleTimeString([], { hour: "numeric", minute: "2-digit" }) })] })), m.replyTo && _jsx(ReplyQuote, { replyTo: m.replyTo }), m.text && (_jsx("p", { className: "text-sm whitespace-pre-wrap", children: _jsx(MessageText, { text: m.text }) })), m.attachment && (_jsx("div", { className: "mt-2", children: _jsx(AttachmentView, { att: m.attachment }) }))] }), sendPending && (_jsxs("div", { className: "chat-send-status mt-0.5", role: "status", "aria-live": "polite", children: [_jsx(Loader2, { className: "h-3 w-3 shrink-0 animate-spin" }), _jsx("span", { children: "Sending\u2026" })] })), sendFailed && (_jsxs("div", { className: "chat-send-status chat-send-status--failed mt-0.5", role: "alert", children: [_jsx(AlertTriangle, { className: "h-3 w-3 shrink-0" }), _jsx("span", { children: sendFailureCopy(sendFailureCode) }), showRetry && onRetrySend && clientMessageId && (_jsx(Button, { type: "button", variant: "ghost", size: "sm", className: "h-5 px-1.5 text-[0.6875rem]", onClick: () => onRetrySend(clientMessageId), children: "Retry" }))] })), !isContinuation && (_jsx("div", { className: "mt-1", children: _jsx(MessageActions, { messageId: m.messageId, isAdmin: isAdmin, handlers: handlers }) }))] }));
|
|
144
|
+
} : undefined, children: senderName }), _jsx("span", { children: "\u00B7" }), _jsx("span", { children: new Date(m.createdAt).toLocaleTimeString([], { hour: "numeric", minute: "2-digit" }) })] })), m.replyTo && _jsx(ReplyQuote, { replyTo: m.replyTo }), m.text && (_jsx("p", { className: "text-sm whitespace-pre-wrap", children: _jsx(MessageText, { text: m.text }) })), m.attachment && (_jsx("div", { className: "mt-2", children: _jsx(AttachmentView, { att: m.attachment, mine: mine, onRetryAttachment: onRetryAttachment }) }))] }), sendPending && (_jsxs("div", { className: "chat-send-status mt-0.5", role: "status", "aria-live": "polite", children: [_jsx(Loader2, { className: "h-3 w-3 shrink-0 animate-spin" }), _jsx("span", { children: "Sending\u2026" })] })), sendFailed && (_jsxs("div", { className: "chat-send-status chat-send-status--failed mt-0.5", role: "alert", children: [_jsx(AlertTriangle, { className: "h-3 w-3 shrink-0" }), _jsx("span", { children: sendFailureCopy(sendFailureCode) }), showRetry && onRetrySend && clientMessageId && (_jsx(Button, { type: "button", variant: "ghost", size: "sm", className: "h-5 px-1.5 text-[0.6875rem]", onClick: () => onRetrySend(clientMessageId), children: "Retry" }))] })), !isContinuation && (_jsx("div", { className: "mt-1", children: _jsx(MessageActions, { messageId: m.messageId, isAdmin: isAdmin, handlers: handlers }) }))] }));
|
|
130
145
|
}
|
|
131
146
|
//# sourceMappingURL=MessageItemDefault.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MessageItemDefault.js","sourceRoot":"","sources":["../../src/ui/MessageItemDefault.tsx"],"names":[],"mappings":"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","sourcesContent":["\"use client\";\n\nimport type { ChatMessageV1, ChatAttachment, ModerationHandlers } from \"@ttt-productions/chat-core\";\nimport { MessageText } from \"../mentions/MessageText.js\";\nimport { cn } from \"@ttt-productions/ui-core\";\nimport { Button } from \"@ttt-productions/ui-core/react\";\nimport { MediaViewer } from \"@ttt-productions/media-viewer/react\";\nimport { FileText, Loader2, AlertTriangle } from \"lucide-react\";\nimport { MessageActions } from \"./menus.js\";\nimport { useResolvedSenderName } from \"../context/ChatNameResolverContext.js\";\nimport { useChatAttachmentUrlResolver } from \"../context/ChatAttachmentUrlContext.js\";\nimport { useChatAttachmentMediaComponent } from \"../context/ChatAttachmentMediaContext.js\";\n\n// ============================================\n// Attachment rendering\n// ============================================\n\nfunction getAttachmentLabel(type: ChatAttachment[\"type\"]): string {\n switch (type) {\n case \"image\": return \"Image attachment\";\n case \"video\": return \"Video attachment\";\n case \"audio\": return \"Audio attachment\";\n default: return \"File attachment\";\n }\n}\n\n// ============================================\n// Correlated send-rejection copy + retry policy\n// ============================================\n\n// User-facing copy per correlated `send-rejected` code (stamped on meta.sendFailureCode\n// by the realtime channel client). A failed send with no code (a transport/exhausted\n// reconnect failure) falls back to the neutral \"Couldn't send\".\nconst SEND_FAILURE_COPY: Record<string, string> = {\n \"blocked-word\": \"Message contains blocked language\",\n \"archived\": \"This channel is read-only\",\n \"deleted\": \"This channel is read-only\",\n \"membership-pending\": \"Chat is still preparing\",\n \"wordlist-unavailable\": \"Chat safety check is temporarily unavailable\",\n \"flood\": \"Please wait before sending again\",\n \"slow-mode\": \"Please wait before sending again\",\n};\n\nfunction sendFailureCopy(code: string | null): string {\n return (code && SEND_FAILURE_COPY[code]) || \"Couldn't send\";\n}\n\n// Retry policy comes from meta.sendRetryable, stamped by the channel client from\n// the wire contract's canonical retryability table (chat-schemas enforces the\n// frame's `retryable` matches the table, so this is the ONE source of truth —\n// never a second hardcoded code list here, which would have to move in lockstep\n// with the contract). `false` = terminal (an unchanged resend is guaranteed to\n// fail — no Retry action); `true` or ABSENT (a code-less transport/exhausted\n// reconnect failure) keeps Retry, reusing the ORIGINAL clientMessageId.\nfunction isRetryableFailure(meta: Record<string, unknown> | undefined): boolean {\n return meta?.sendRetryable !== false;\n}\n\nfunction AttachmentView({ att }: { att: ChatAttachment }) {\n // Display URL is built at render time by the app-injected resolver from\n // att.mediaAssetId — attachments never store URLs (protected-gateway model).\n const resolveAttachmentUrl = useChatAttachmentUrlResolver();\n // App-injected media component (optional) — lets chat attachments ride the\n // app's ONE display path (recovery adapter, diagnostics, telemetry). Falls\n // back to the package's own MediaViewer when no provider is present.\n const Media = useChatAttachmentMediaComponent() ?? MediaViewer;\n // In-flight placeholder — bytes uploaded, processing/moderation pending. No\n // `mediaAssetId` yet. Rendered only to the sender (other participants can't\n // read a pending doc — Firestore rules scope it until `status === 'ready'`).\n if (att.status === \"pending\") {\n return (\n <div className=\"chat-attachment-pending\">\n <Loader2 className=\"h-4 w-4 shrink-0 animate-spin\" />\n <span className=\"truncate\">{getAttachmentLabel(att.type)}</span>\n <span className=\"chat-attachment-status-label\">Sending…</span>\n </div>\n );\n }\n // Processing/moderation rejected the attachment — sender-only.\n if (att.status === \"failed\") {\n return (\n <div className=\"chat-attachment-rejected\">\n <AlertTriangle className=\"h-4 w-4 shrink-0\" />\n <span className=\"truncate\">{getAttachmentLabel(att.type)}</span>\n <span className=\"chat-attachment-status-label\">\n {att.failureReason ?? \"Couldn't send this attachment\"}\n </span>\n </div>\n );\n }\n // ready (or legacy/absent status) — resolve the display URL from the asset ref.\n const url = resolveAttachmentUrl(att);\n if (!url) {\n // Terminal-ready row, but the authorized URL is still SETTLING (the grant/URL\n // resolver has nothing yet). Returning null left only the surrounding message\n // metadata → an observed empty bubble; a broken/grant-less <img> would flash a\n // 403. Render a neutral loading placeholder in the existing loading visual\n // language (spinner + kind-appropriate generic label + \"Loading…\", never\n // \"Sending…\", NO filename) and do NOT mount the media/download renderer until a\n // non-empty authorized URL exists. Sender-only pending/processing/failed states\n // above are unchanged — this begins only after a terminal-ready row is received.\n return (\n <div className=\"chat-attachment-pending\">\n <Loader2 className=\"h-4 w-4 shrink-0 animate-spin\" />\n <span className=\"truncate\">{getAttachmentLabel(att.type)}</span>\n <span className=\"chat-attachment-status-label\">Loading…</span>\n </div>\n );\n }\n if (att.type === \"image\") {\n return <Media type=\"image\" url={url} alt=\"Image attachment\" className=\"chat-attachment-media\" />;\n }\n if (att.type === \"video\") {\n return <Media type=\"video\" url={url} controls className=\"chat-attachment-media\" />;\n }\n if (att.type === \"audio\") {\n return <Media type=\"audio\" url={url} controls className=\"chat-attachment-media\" />;\n }\n // text/markdown — download link\n return (\n <a\n href={url}\n download={att.name}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"chat-attachment-text-link\"\n >\n <FileText className=\"h-4 w-4 shrink-0\" />\n <span className=\"truncate\">Download attachment</span>\n </a>\n );\n}\n\n// ============================================\n// Reply-to quote\n// ============================================\n\nfunction ReplyQuote({ replyTo }: { replyTo: NonNullable<ChatMessageV1[\"replyTo\"]> }) {\n const replyName = useResolvedSenderName(replyTo.senderId);\n return (\n <div className=\"chat-reply-quote\">\n <span className=\"chat-reply-quote-sender\">{replyName}</span>\n <span className=\"chat-reply-quote-preview\"><MessageText text={replyTo.messagePreview} /></span>\n </div>\n );\n}\n\n// ============================================\n// System message\n// ============================================\n\nfunction SystemMessage({ m }: { m: ChatMessageV1 }) {\n return (\n <div className=\"chat-system-message\">\n {m.text && <span>{m.text}</span>}\n </div>\n );\n}\n\n// ============================================\n// Default message item\n// ============================================\n\nexport type MessageItemDefaultProps = {\n m: ChatMessageV1;\n currentUserId: string;\n isAdmin: boolean;\n handlers?: ModerationHandlers;\n\n // Grouping\n isContinuation?: boolean;\n\n // Sender interaction\n onSenderClick?: (senderId: string, displayName: string) => void;\n\n /** Retry a failed realtime send by its original clientMessageId (wired by\n * ChatShell on the realtime transport; absent on firestore). Enables the\n * retry affordance on a `meta.sendFailed` bubble. */\n onRetrySend?: (clientMessageId: string) => void;\n\n};\n\nexport function MessageItemDefault(props: MessageItemDefaultProps) {\n const { m, currentUserId, isAdmin, handlers, isContinuation, onSenderClick, onRetrySend } = props;\n const senderName = useResolvedSenderName(m.senderId);\n\n // System messages render differently\n if (m.isSystemMessage) {\n return <SystemMessage m={m} />;\n }\n\n const mine = m.senderId === currentUserId;\n\n // Realtime optimistic-send lifecycle (meta is stamped by the channel client):\n // an un-acked echo renders subtly pending; one past the retry/age cap renders a\n // visibly failed bubble with a retry affordance (same clientMessageId — the DO\n // dedups, so a retry can never double-send).\n const sendFailed = m.meta?.sendFailed === true;\n const sendPending = m.meta?.optimistic === true && !sendFailed;\n const clientMessageId =\n typeof m.meta?.clientMessageId === \"string\" ? m.meta.clientMessageId : null;\n // Correlated rejection reason (from the `send-rejected` frame), if any — drives\n // both the failure copy and whether a Retry affordance is offered.\n const sendFailureCode =\n typeof m.meta?.sendFailureCode === \"string\" ? m.meta.sendFailureCode : null;\n const showRetry = sendFailed && isRetryableFailure(m.meta);\n\n return (\n <div\n className={cn(\n \"flex flex-col w-fit max-w-[85%]\",\n mine ? \"ml-auto items-end\" : \"mr-auto items-start\",\n isContinuation ? \"chat-continuation-gap\" : \"chat-group-gap\"\n )}\n >\n <div\n className={cn(\n \"chat-bubble\",\n mine ? \"chat-bubble--mine\" : \"chat-bubble--theirs\",\n sendPending && \"chat-bubble--pending\",\n sendFailed && \"chat-bubble--failed\"\n )}\n >\n {!isContinuation && (\n <div className=\"flex items-center gap-2 text-xs opacity-80 mb-1\">\n <span\n className={cn(\"font-medium\", onSenderClick && \"cursor-pointer hover:underline\")}\n onClick={onSenderClick ? () => onSenderClick(m.senderId, senderName) : undefined}\n role={onSenderClick ? \"button\" : undefined}\n tabIndex={onSenderClick ? 0 : undefined}\n onKeyDown={onSenderClick ? (e) => {\n if (e.key === \"Enter\" || e.key === \" \") onSenderClick(m.senderId, senderName);\n } : undefined}\n >\n {senderName}\n </span>\n <span>·</span>\n <span>{new Date(m.createdAt).toLocaleTimeString([], { hour: \"numeric\", minute: \"2-digit\" })}</span>\n </div>\n )}\n\n {m.replyTo && <ReplyQuote replyTo={m.replyTo} />}\n\n {m.text && (\n <p className=\"text-sm whitespace-pre-wrap\">\n <MessageText text={m.text} />\n </p>\n )}\n\n {m.attachment && (\n <div className=\"mt-2\">\n <AttachmentView att={m.attachment} />\n </div>\n )}\n </div>\n\n {/* Send-state status rows render regardless of grouping — a pending/failed\n send must never be indistinguishable from a delivered one. */}\n {sendPending && (\n <div className=\"chat-send-status mt-0.5\" role=\"status\" aria-live=\"polite\">\n <Loader2 className=\"h-3 w-3 shrink-0 animate-spin\" />\n <span>Sending…</span>\n </div>\n )}\n {sendFailed && (\n <div className=\"chat-send-status chat-send-status--failed mt-0.5\" role=\"alert\">\n <AlertTriangle className=\"h-3 w-3 shrink-0\" />\n <span>{sendFailureCopy(sendFailureCode)}</span>\n {showRetry && onRetrySend && clientMessageId && (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className=\"h-5 px-1.5 text-[0.6875rem]\"\n onClick={() => onRetrySend(clientMessageId)}\n >\n Retry\n </Button>\n )}\n </div>\n )}\n\n {!isContinuation && (\n <div className=\"mt-1\">\n <MessageActions messageId={m.messageId} isAdmin={isAdmin} handlers={handlers} />\n </div>\n )}\n </div>\n );\n}\n"]}
|
|
1
|
+
{"version":3,"file":"MessageItemDefault.js","sourceRoot":"","sources":["../../src/ui/MessageItemDefault.tsx"],"names":[],"mappings":"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","sourcesContent":["\"use client\";\n\nimport type { ChatMessageV1, ChatAttachment, ModerationHandlers } from \"@ttt-productions/chat-core\";\nimport { MessageText } from \"../mentions/MessageText.js\";\nimport { cn } from \"@ttt-productions/ui-core\";\nimport { Button } from \"@ttt-productions/ui-core/react\";\nimport { MediaViewer } from \"@ttt-productions/media-viewer/react\";\nimport { FileText, Loader2, AlertTriangle } from \"lucide-react\";\nimport { MessageActions } from \"./menus.js\";\nimport { useResolvedSenderName } from \"../context/ChatNameResolverContext.js\";\nimport { useChatAttachmentUrlResolver } from \"../context/ChatAttachmentUrlContext.js\";\nimport { useChatAttachmentMediaComponent } from \"../context/ChatAttachmentMediaContext.js\";\n\n// ============================================\n// Attachment rendering\n// ============================================\n\nfunction getAttachmentLabel(type: ChatAttachment[\"type\"]): string {\n switch (type) {\n case \"image\": return \"Image attachment\";\n case \"video\": return \"Video attachment\";\n case \"audio\": return \"Audio attachment\";\n default: return \"File attachment\";\n }\n}\n\n// Safe, generic terminal-failure copy per kind. The backend `failureReason` is an\n// INTERNAL diagnostic string and must never reach the UI (it could leak processing\n// internals); it is mapped to this fixed copy instead.\nfunction getAttachmentFailureCopy(type: ChatAttachment[\"type\"]): string {\n switch (type) {\n case \"image\": return \"This image could not be processed.\";\n case \"video\": return \"This video could not be processed.\";\n case \"audio\": return \"This audio could not be processed.\";\n default: return \"This file could not be processed.\";\n }\n}\n\n// ============================================\n// Correlated send-rejection copy + retry policy\n// ============================================\n\n// User-facing copy per correlated `send-rejected` code (stamped on meta.sendFailureCode\n// by the realtime channel client). A failed send with no code (a transport/exhausted\n// reconnect failure) falls back to the neutral \"Couldn't send\".\nconst SEND_FAILURE_COPY: Record<string, string> = {\n \"blocked-word\": \"Message contains blocked language\",\n \"archived\": \"This channel is read-only\",\n \"deleted\": \"This channel is read-only\",\n \"membership-pending\": \"Chat is still preparing\",\n \"wordlist-unavailable\": \"Chat safety check is temporarily unavailable\",\n \"flood\": \"Please wait before sending again\",\n \"slow-mode\": \"Please wait before sending again\",\n};\n\nfunction sendFailureCopy(code: string | null): string {\n return (code && SEND_FAILURE_COPY[code]) || \"Couldn't send\";\n}\n\n// Retry policy comes from meta.sendRetryable, stamped by the channel client from\n// the wire contract's canonical retryability table (chat-schemas enforces the\n// frame's `retryable` matches the table, so this is the ONE source of truth —\n// never a second hardcoded code list here, which would have to move in lockstep\n// with the contract). `false` = terminal (an unchanged resend is guaranteed to\n// fail — no Retry action); `true` or ABSENT (a code-less transport/exhausted\n// reconnect failure) keeps Retry, reusing the ORIGINAL clientMessageId.\nfunction isRetryableFailure(meta: Record<string, unknown> | undefined): boolean {\n return meta?.sendRetryable !== false;\n}\n\nfunction AttachmentView({\n att,\n mine,\n onRetryAttachment,\n}: {\n att: ChatAttachment;\n mine: boolean;\n onRetryAttachment?: () => void;\n}) {\n // Display URL is built at render time by the app-injected resolver from\n // att.mediaAssetId — attachments never store URLs (protected-gateway model).\n const resolveAttachmentUrl = useChatAttachmentUrlResolver();\n // App-injected media component (optional) — lets chat attachments ride the\n // app's ONE display path (recovery adapter, diagnostics, telemetry). Falls\n // back to the package's own MediaViewer when no provider is present.\n const Media = useChatAttachmentMediaComponent() ?? MediaViewer;\n // In-flight placeholder — bytes uploaded, backend processing/moderation pending.\n // No `mediaAssetId` yet. Rendered only to the sender (other participants can't\n // read a pending doc — Firestore rules scope it until `status === 'ready'`). The\n // bytes are already uploaded, so the honest copy is \"Processing…\" (NOT \"Sending…\").\n if (att.status === \"pending\") {\n return (\n <div className=\"chat-attachment-pending\" role=\"status\" aria-live=\"polite\">\n <Loader2 className=\"h-4 w-4 shrink-0 animate-spin\" />\n <span className=\"truncate\">{getAttachmentLabel(att.type)}</span>\n <span className=\"chat-attachment-status-label\">Processing…</span>\n </div>\n );\n }\n // Processing/moderation rejected the attachment — sender-only. The backend\n // `failureReason` is internal and NEVER rendered; a fixed safe per-kind copy is\n // shown instead. The sender can pick a fresh file through the canonical picker\n // (\"Attach again\"), offered only when the composer wired a retry handler.\n if (att.status === \"failed\") {\n return (\n <div className=\"chat-attachment-rejected\" role=\"alert\">\n <AlertTriangle className=\"h-4 w-4 shrink-0 chat-attachment-rejected-icon\" />\n <div className=\"chat-attachment-rejected-body\">\n <span className=\"truncate\">{getAttachmentLabel(att.type)}</span>\n <span className=\"chat-attachment-rejected-reason\">\n {getAttachmentFailureCopy(att.type)}\n </span>\n </div>\n {mine && onRetryAttachment && (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className=\"chat-attachment-retry\"\n onClick={onRetryAttachment}\n >\n Attach again\n </Button>\n )}\n </div>\n );\n }\n // ready (or legacy/absent status) — resolve the display URL from the asset ref.\n const url = resolveAttachmentUrl(att);\n if (!url) {\n // Terminal-ready row, but the authorized URL is still SETTLING (the grant/URL\n // resolver has nothing yet). Returning null left only the surrounding message\n // metadata → an observed empty bubble; a broken/grant-less <img> would flash a\n // 403. Render a neutral loading placeholder in the existing loading visual\n // language (spinner + kind-appropriate generic label + \"Loading…\", never\n // \"Sending…\", NO filename) and do NOT mount the media/download renderer until a\n // non-empty authorized URL exists. Sender-only pending/processing/failed states\n // above are unchanged — this begins only after a terminal-ready row is received.\n return (\n <div className=\"chat-attachment-pending\" role=\"status\" aria-live=\"polite\">\n <Loader2 className=\"h-4 w-4 shrink-0 animate-spin\" />\n <span className=\"truncate\">{getAttachmentLabel(att.type)}</span>\n <span className=\"chat-attachment-status-label\">Loading…</span>\n </div>\n );\n }\n if (att.type === \"image\") {\n return <Media type=\"image\" url={url} alt=\"Image attachment\" className=\"chat-attachment-media\" />;\n }\n if (att.type === \"video\") {\n return <Media type=\"video\" url={url} controls className=\"chat-attachment-media\" />;\n }\n if (att.type === \"audio\") {\n return <Media type=\"audio\" url={url} controls className=\"chat-attachment-media\" />;\n }\n // text/markdown — download link\n return (\n <a\n href={url}\n download={att.name}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"chat-attachment-text-link\"\n >\n <FileText className=\"h-4 w-4 shrink-0\" />\n <span className=\"truncate\">Download attachment</span>\n </a>\n );\n}\n\n// ============================================\n// Reply-to quote\n// ============================================\n\nfunction ReplyQuote({ replyTo }: { replyTo: NonNullable<ChatMessageV1[\"replyTo\"]> }) {\n const replyName = useResolvedSenderName(replyTo.senderId);\n return (\n <div className=\"chat-reply-quote\">\n <span className=\"chat-reply-quote-sender\">{replyName}</span>\n <span className=\"chat-reply-quote-preview\"><MessageText text={replyTo.messagePreview} /></span>\n </div>\n );\n}\n\n// ============================================\n// System message\n// ============================================\n\nfunction SystemMessage({ m }: { m: ChatMessageV1 }) {\n return (\n <div className=\"chat-system-message\">\n {m.text && <span>{m.text}</span>}\n </div>\n );\n}\n\n// ============================================\n// Default message item\n// ============================================\n\nexport type MessageItemDefaultProps = {\n m: ChatMessageV1;\n currentUserId: string;\n isAdmin: boolean;\n handlers?: ModerationHandlers;\n\n // Grouping\n isContinuation?: boolean;\n\n // Sender interaction\n onSenderClick?: (senderId: string, displayName: string) => void;\n\n /** Retry a failed realtime send by its original clientMessageId (wired by\n * ChatShell on the realtime transport; absent on firestore). Enables the\n * retry affordance on a `meta.sendFailed` bubble. */\n onRetrySend?: (clientMessageId: string) => void;\n\n /** Re-open the attachment picker for a terminally-failed attachment (wired by\n * ChatShell to the Composer's `openAttachmentSelector`; present only when\n * attachment support is configured). Enables the \"Attach again\" action on a\n * failed attachment bubble, shown only to the sender. */\n onRetryAttachment?: () => void;\n\n};\n\nexport function MessageItemDefault(props: MessageItemDefaultProps) {\n const { m, currentUserId, isAdmin, handlers, isContinuation, onSenderClick, onRetrySend, onRetryAttachment } = props;\n const senderName = useResolvedSenderName(m.senderId);\n\n // System messages render differently\n if (m.isSystemMessage) {\n return <SystemMessage m={m} />;\n }\n\n const mine = m.senderId === currentUserId;\n\n // Realtime optimistic-send lifecycle (meta is stamped by the channel client):\n // an un-acked echo renders subtly pending; one past the retry/age cap renders a\n // visibly failed bubble with a retry affordance (same clientMessageId — the DO\n // dedups, so a retry can never double-send).\n const sendFailed = m.meta?.sendFailed === true;\n const sendPending = m.meta?.optimistic === true && !sendFailed;\n const clientMessageId =\n typeof m.meta?.clientMessageId === \"string\" ? m.meta.clientMessageId : null;\n // Correlated rejection reason (from the `send-rejected` frame), if any — drives\n // both the failure copy and whether a Retry affordance is offered.\n const sendFailureCode =\n typeof m.meta?.sendFailureCode === \"string\" ? m.meta.sendFailureCode : null;\n const showRetry = sendFailed && isRetryableFailure(m.meta);\n\n return (\n <div\n className={cn(\n \"flex flex-col w-fit max-w-[85%]\",\n mine ? \"ml-auto items-end\" : \"mr-auto items-start\",\n isContinuation ? \"chat-continuation-gap\" : \"chat-group-gap\"\n )}\n >\n <div\n className={cn(\n \"chat-bubble\",\n mine ? \"chat-bubble--mine\" : \"chat-bubble--theirs\",\n sendPending && \"chat-bubble--pending\",\n sendFailed && \"chat-bubble--failed\"\n )}\n >\n {!isContinuation && (\n <div className=\"flex items-center gap-2 text-xs opacity-80 mb-1\">\n <span\n className={cn(\"font-medium\", onSenderClick && \"cursor-pointer hover:underline\")}\n onClick={onSenderClick ? () => onSenderClick(m.senderId, senderName) : undefined}\n role={onSenderClick ? \"button\" : undefined}\n tabIndex={onSenderClick ? 0 : undefined}\n onKeyDown={onSenderClick ? (e) => {\n if (e.key === \"Enter\" || e.key === \" \") onSenderClick(m.senderId, senderName);\n } : undefined}\n >\n {senderName}\n </span>\n <span>·</span>\n <span>{new Date(m.createdAt).toLocaleTimeString([], { hour: \"numeric\", minute: \"2-digit\" })}</span>\n </div>\n )}\n\n {m.replyTo && <ReplyQuote replyTo={m.replyTo} />}\n\n {m.text && (\n <p className=\"text-sm whitespace-pre-wrap\">\n <MessageText text={m.text} />\n </p>\n )}\n\n {m.attachment && (\n <div className=\"mt-2\">\n <AttachmentView att={m.attachment} mine={mine} onRetryAttachment={onRetryAttachment} />\n </div>\n )}\n </div>\n\n {/* Send-state status rows render regardless of grouping — a pending/failed\n send must never be indistinguishable from a delivered one. */}\n {sendPending && (\n <div className=\"chat-send-status mt-0.5\" role=\"status\" aria-live=\"polite\">\n <Loader2 className=\"h-3 w-3 shrink-0 animate-spin\" />\n <span>Sending…</span>\n </div>\n )}\n {sendFailed && (\n <div className=\"chat-send-status chat-send-status--failed mt-0.5\" role=\"alert\">\n <AlertTriangle className=\"h-3 w-3 shrink-0\" />\n <span>{sendFailureCopy(sendFailureCode)}</span>\n {showRetry && onRetrySend && clientMessageId && (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className=\"h-5 px-1.5 text-[0.6875rem]\"\n onClick={() => onRetrySend(clientMessageId)}\n >\n Retry\n </Button>\n )}\n </div>\n )}\n\n {!isContinuation && (\n <div className=\"mt-1\">\n <MessageActions messageId={m.messageId} isAdmin={isAdmin} handlers={handlers} />\n </div>\n )}\n </div>\n );\n}\n"]}
|
package/dist/ui/MessageList.d.ts
CHANGED
|
@@ -28,5 +28,8 @@ export declare function MessageList(props: {
|
|
|
28
28
|
onSenderClick?: (senderId: string, displayName: string) => void;
|
|
29
29
|
/** Retry a failed realtime send by clientMessageId (realtime transport only). */
|
|
30
30
|
onRetrySend?: (clientMessageId: string) => void;
|
|
31
|
+
/** Re-open the attachment picker for a terminally-failed attachment. Present only
|
|
32
|
+
* when attachment support is configured; forwarded to each MessageItemDefault. */
|
|
33
|
+
onRetryAttachment?: () => void;
|
|
31
34
|
}): React.JSX.Element;
|
|
32
35
|
//# sourceMappingURL=MessageList.d.ts.map
|
|
@@ -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,EAAE,WAAW,EAAE,MAAM,KAAK,IAAI,CAAC;IAChE,iFAAiF;IACjF,WAAW,CAAC,EAAE,CAAC,eAAe,EAAE,MAAM,KAAK,IAAI,CAAC;
|
|
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,EAAE,WAAW,EAAE,MAAM,KAAK,IAAI,CAAC;IAChE,iFAAiF;IACjF,WAAW,CAAC,EAAE,CAAC,eAAe,EAAE,MAAM,KAAK,IAAI,CAAC;IAChD;uFACmF;IACnF,iBAAiB,CAAC,EAAE,MAAM,IAAI,CAAC;CAChC,qBAmMA"}
|
package/dist/ui/MessageList.js
CHANGED
|
@@ -5,7 +5,7 @@ import { isContinuation } from "@ttt-productions/chat-core";
|
|
|
5
5
|
import { Button } from "@ttt-productions/ui-core/react";
|
|
6
6
|
import { MessageItemDefault } from "./MessageItemDefault.js";
|
|
7
7
|
export function MessageList(props) {
|
|
8
|
-
const { messages, currentUserId, isAdmin, isFetchingOlder, hasOlder, onLoadOlder, renderMessage, messageRenderers, showScrollToBottom, onScrollToBottom, onAtBottomChange, scrollClassName, fillHeight = false, handlers, onSenderClick, onRetrySend, } = props;
|
|
8
|
+
const { messages, currentUserId, isAdmin, isFetchingOlder, hasOlder, onLoadOlder, renderMessage, messageRenderers, showScrollToBottom, onScrollToBottom, onAtBottomChange, scrollClassName, fillHeight = false, handlers, onSenderClick, onRetrySend, onRetryAttachment, } = props;
|
|
9
9
|
const scrollClass = scrollClassName ?? (fillHeight ? "flex-1 min-h-0" : "h-[400px]");
|
|
10
10
|
const outerClass = fillHeight ? "relative flex flex-col flex-1 min-h-0" : "relative";
|
|
11
11
|
const scrollRef = React.useRef(null);
|
|
@@ -107,7 +107,7 @@ export function MessageList(props) {
|
|
|
107
107
|
const continuation = showDay ? false : isContinuation(prevMsg, m);
|
|
108
108
|
const byType = m.type && messageRenderers?.[m.type] ? messageRenderers[m.type](m) : null;
|
|
109
109
|
const body = renderMessage?.(m) ??
|
|
110
|
-
byType ?? (_jsx(MessageItemDefault, { m: m, currentUserId: currentUserId, isAdmin: isAdmin, handlers: handlers, isContinuation: continuation, onSenderClick: onSenderClick, onRetrySend: onRetrySend }));
|
|
110
|
+
byType ?? (_jsx(MessageItemDefault, { m: m, currentUserId: currentUserId, isAdmin: isAdmin, handlers: handlers, isContinuation: continuation, onSenderClick: onSenderClick, onRetrySend: onRetrySend, onRetryAttachment: onRetryAttachment }));
|
|
111
111
|
return (_jsxs(React.Fragment, { children: [showDay && (_jsx("div", { className: "my-2 flex justify-center", children: _jsx("div", { className: "chat-date-separator", children: new Date(m.createdAt).toLocaleDateString() }) })), body] }, m.messageId));
|
|
112
112
|
}) }))] }), (unseenCount > 0 || showScrollToBottom) && (_jsx(Button, { type: "button", variant: "outline", size: "sm", "aria-label": unseenCount > 0 ? `${unseenCount} new messages, scroll to latest` : "Scroll to latest", className: "absolute bottom-4 left-1/2 -translate-x-1/2 rounded-full shadow-sm gap-1", onClick: () => {
|
|
113
113
|
scrollToBottom();
|
|
@@ -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 } 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, displayName: string) => void;\n /** Retry a failed realtime send by clientMessageId (realtime transport only). */\n onRetrySend?: (clientMessageId: string) => void;\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 } = props;\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) 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, 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 {isFetchingOlder && <div className=\"text-center text-xs opacity-70 mb-2\">Loading…</div>}\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 shadow-sm 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 } 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, displayName: string) => void;\n /** Retry a failed realtime send by clientMessageId (realtime transport only). */\n onRetrySend?: (clientMessageId: string) => void;\n /** Re-open the attachment picker for a terminally-failed attachment. Present only\n * when attachment support is configured; forwarded to each MessageItemDefault. */\n onRetryAttachment?: () => void;\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 onRetryAttachment,\n } = props;\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) 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, 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 {isFetchingOlder && <div className=\"text-center text-xs opacity-70 mb-2\">Loading…</div>}\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 onRetryAttachment={onRetryAttachment}\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 shadow-sm 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.12.
|
|
3
|
+
"version": "0.12.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",
|
|
@@ -63,9 +63,9 @@
|
|
|
63
63
|
"@ttt-productions/chat-core": "^0.10.2",
|
|
64
64
|
"@ttt-productions/chat-schemas": "^0.9.3",
|
|
65
65
|
"@ttt-productions/realtime-core": "^0.1.3",
|
|
66
|
-
"@ttt-productions/media-schemas": "^0.14.
|
|
66
|
+
"@ttt-productions/media-schemas": "^0.14.3",
|
|
67
67
|
"@ttt-productions/ui-core": "^0.11.10",
|
|
68
|
-
"@ttt-productions/file-input": "^0.11.
|
|
68
|
+
"@ttt-productions/file-input": "^0.11.6",
|
|
69
69
|
"@ttt-productions/upload-ui": "^0.7.5",
|
|
70
70
|
"@ttt-productions/media-viewer": "^0.10.5",
|
|
71
71
|
"@ttt-productions/mobile-core": "^0.9.1",
|
package/src/styles/chat.css
CHANGED
|
@@ -97,16 +97,55 @@
|
|
|
97
97
|
}
|
|
98
98
|
|
|
99
99
|
/* --- Attachment states --- */
|
|
100
|
+
|
|
101
|
+
/* Pending / URL-settling placeholder. Parchment-safe: a lightly tinted surface
|
|
102
|
+
with a subtle tokenized border (NOT the old opaque muted slab, which read as a
|
|
103
|
+
dark rectangle on the parchment chat canvas). It is a small inline status row —
|
|
104
|
+
deliberately NOT a media-sized box, so it never implies the file is viewable.
|
|
105
|
+
All colors are theme tokens; a restrained shimmer conveys "in progress". */
|
|
100
106
|
.chat-attachment-pending {
|
|
107
|
+
position: relative;
|
|
108
|
+
overflow: hidden;
|
|
101
109
|
display: flex;
|
|
102
110
|
align-items: center;
|
|
103
111
|
gap: 0.5rem;
|
|
104
112
|
padding: 0.5rem 0.75rem;
|
|
105
113
|
border-radius: 0.375rem;
|
|
106
|
-
background-color: hsl(var(--muted));
|
|
114
|
+
background-color: hsl(var(--muted) / 0.2);
|
|
115
|
+
border: 1px solid hsl(var(--border) / 0.6);
|
|
116
|
+
color: hsl(var(--muted-foreground));
|
|
107
117
|
font-size: 0.75rem;
|
|
108
118
|
}
|
|
109
119
|
|
|
120
|
+
/* Restrained left-to-right shimmer over the tinted surface. Tokenized, low-opacity,
|
|
121
|
+
and disabled under prefers-reduced-motion. The spinner remains the primary cue. */
|
|
122
|
+
.chat-attachment-pending::after {
|
|
123
|
+
content: "";
|
|
124
|
+
position: absolute;
|
|
125
|
+
inset: 0;
|
|
126
|
+
background: linear-gradient(
|
|
127
|
+
90deg,
|
|
128
|
+
transparent 0%,
|
|
129
|
+
hsl(var(--muted-foreground) / 0.06) 50%,
|
|
130
|
+
transparent 100%
|
|
131
|
+
);
|
|
132
|
+
transform: translateX(-100%);
|
|
133
|
+
animation: chat-attachment-shimmer 1.8s ease-in-out infinite;
|
|
134
|
+
pointer-events: none;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
@keyframes chat-attachment-shimmer {
|
|
138
|
+
100% {
|
|
139
|
+
transform: translateX(100%);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
@media (prefers-reduced-motion: reduce) {
|
|
144
|
+
.chat-attachment-pending::after {
|
|
145
|
+
animation: none;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
110
149
|
.chat-attachment-media {
|
|
111
150
|
max-width: 320px;
|
|
112
151
|
border-radius: 0.375rem;
|
|
@@ -130,17 +169,42 @@
|
|
|
130
169
|
opacity: 0.8;
|
|
131
170
|
}
|
|
132
171
|
|
|
172
|
+
/* Terminal-failure placeholder. A restrained, tokenized surface — a subtle border
|
|
173
|
+
and faint muted tint, NOT a destructive-red block (the failure is localized to
|
|
174
|
+
this attachment, so it must not make the whole bubble read as an error). */
|
|
133
175
|
.chat-attachment-rejected {
|
|
134
176
|
display: flex;
|
|
135
|
-
align-items:
|
|
136
|
-
gap: 0.
|
|
177
|
+
align-items: flex-start;
|
|
178
|
+
gap: 0.5rem;
|
|
137
179
|
padding: 0.5rem 0.75rem;
|
|
138
180
|
border-radius: 0.375rem;
|
|
139
|
-
background-color: hsl(var(--
|
|
140
|
-
|
|
181
|
+
background-color: hsl(var(--muted) / 0.35);
|
|
182
|
+
border: 1px solid hsl(var(--border));
|
|
183
|
+
color: hsl(var(--muted-foreground));
|
|
141
184
|
font-size: 0.75rem;
|
|
142
185
|
}
|
|
143
186
|
|
|
187
|
+
.chat-attachment-rejected-icon {
|
|
188
|
+
margin-top: 0.05rem;
|
|
189
|
+
color: hsl(var(--muted-foreground));
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
.chat-attachment-rejected-body {
|
|
193
|
+
display: flex;
|
|
194
|
+
flex-direction: column;
|
|
195
|
+
gap: 0.05rem;
|
|
196
|
+
min-width: 0;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
.chat-attachment-rejected-reason {
|
|
200
|
+
opacity: 0.85;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
.chat-attachment-retry {
|
|
204
|
+
margin-left: auto;
|
|
205
|
+
flex-shrink: 0;
|
|
206
|
+
}
|
|
207
|
+
|
|
144
208
|
.chat-attachment-status-label {
|
|
145
209
|
margin-left: auto;
|
|
146
210
|
opacity: 0.7;
|