@ttt-productions/chat-react 0.13.16 → 0.13.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/README.md +3 -3
  2. package/dist/context/ChatNameResolverContext.d.ts +0 -23
  3. package/dist/context/ChatNameResolverContext.d.ts.map +1 -1
  4. package/dist/context/ChatNameResolverContext.js +0 -16
  5. package/dist/context/ChatNameResolverContext.js.map +1 -1
  6. package/dist/hooks/useChatMessages.d.ts +0 -4
  7. package/dist/hooks/useChatMessages.d.ts.map +1 -1
  8. package/dist/hooks/useChatMessages.js +0 -10
  9. package/dist/hooks/useChatMessages.js.map +1 -1
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +0 -3
  12. package/dist/index.js.map +1 -1
  13. package/dist/realtime/channel-client.d.ts +0 -248
  14. package/dist/realtime/channel-client.d.ts.map +1 -1
  15. package/dist/realtime/channel-client.js +5 -432
  16. package/dist/realtime/channel-client.js.map +1 -1
  17. package/dist/realtime/diagnostics.d.ts +0 -62
  18. package/dist/realtime/diagnostics.d.ts.map +1 -1
  19. package/dist/realtime/diagnostics.js +4 -89
  20. package/dist/realtime/diagnostics.js.map +1 -1
  21. package/dist/realtime/inbox-client.d.ts +0 -71
  22. package/dist/realtime/inbox-client.d.ts.map +1 -1
  23. package/dist/realtime/inbox-client.js +0 -98
  24. package/dist/realtime/inbox-client.js.map +1 -1
  25. package/dist/realtime/initial-load-budget.d.ts +1 -16
  26. package/dist/realtime/initial-load-budget.d.ts.map +1 -1
  27. package/dist/realtime/initial-load-budget.js +1 -20
  28. package/dist/realtime/initial-load-budget.js.map +1 -1
  29. package/dist/realtime/map.d.ts +0 -26
  30. package/dist/realtime/map.d.ts.map +1 -1
  31. package/dist/realtime/map.js +0 -33
  32. package/dist/realtime/map.js.map +1 -1
  33. package/dist/realtime/shared.d.ts +0 -78
  34. package/dist/realtime/shared.d.ts.map +1 -1
  35. package/dist/realtime/shared.js +2 -76
  36. package/dist/realtime/shared.js.map +1 -1
  37. package/dist/realtime/socket.d.ts +0 -15
  38. package/dist/realtime/socket.d.ts.map +1 -1
  39. package/dist/realtime/socket.js +0 -4
  40. package/dist/realtime/socket.js.map +1 -1
  41. package/dist/realtime/transport.d.ts +0 -23
  42. package/dist/realtime/transport.d.ts.map +1 -1
  43. package/dist/realtime/transport.js +0 -13
  44. package/dist/realtime/transport.js.map +1 -1
  45. package/dist/realtime/useRealtimeChatMessages.d.ts +0 -17
  46. package/dist/realtime/useRealtimeChatMessages.d.ts.map +1 -1
  47. package/dist/realtime/useRealtimeChatMessages.js +0 -22
  48. package/dist/realtime/useRealtimeChatMessages.js.map +1 -1
  49. package/dist/realtime/wire.d.ts +0 -56
  50. package/dist/realtime/wire.d.ts.map +1 -1
  51. package/dist/realtime/wire.js +0 -16
  52. package/dist/realtime/wire.js.map +1 -1
  53. package/dist/types.d.ts +0 -39
  54. package/dist/types.d.ts.map +1 -1
  55. package/dist/types.js +0 -3
  56. package/dist/types.js.map +1 -1
  57. package/dist/ui/ChatShell.d.ts +0 -42
  58. package/dist/ui/ChatShell.d.ts.map +1 -1
  59. package/dist/ui/ChatShell.js +4 -41
  60. package/dist/ui/ChatShell.js.map +1 -1
  61. package/dist/ui/Composer.d.ts +0 -13
  62. package/dist/ui/Composer.d.ts.map +1 -1
  63. package/dist/ui/Composer.js +0 -13
  64. package/dist/ui/Composer.js.map +1 -1
  65. package/dist/ui/MessageItemDefault.d.ts +0 -5
  66. package/dist/ui/MessageItemDefault.d.ts.map +1 -1
  67. package/dist/ui/MessageItemDefault.js +2 -34
  68. package/dist/ui/MessageItemDefault.js.map +1 -1
  69. package/dist/ui/MessageList.d.ts +0 -24
  70. package/dist/ui/MessageList.d.ts.map +1 -1
  71. package/dist/ui/MessageList.js +4 -14
  72. package/dist/ui/MessageList.js.map +1 -1
  73. package/dist/ui/MessageText.d.ts +0 -13
  74. package/dist/ui/MessageText.d.ts.map +1 -1
  75. package/dist/ui/MessageText.js.map +1 -1
  76. package/dist/ui/SenderName.d.ts +0 -5
  77. package/dist/ui/SenderName.d.ts.map +1 -1
  78. package/dist/ui/SenderName.js +0 -5
  79. package/dist/ui/SenderName.js.map +1 -1
  80. package/dist/ui/menus.d.ts +0 -2
  81. package/dist/ui/menus.d.ts.map +1 -1
  82. package/dist/ui/menus.js +6 -10
  83. package/dist/ui/menus.js.map +1 -1
  84. package/package.json +10 -10
  85. package/src/styles/chat.css +2 -15
@@ -7,17 +7,6 @@ import { AlertTriangle } from 'lucide-react';
7
7
  import { MessageActions } from './menus.js';
8
8
  import { SenderName } from './SenderName.js';
9
9
  import { useSenderNameResolution } from '../context/ChatNameResolverContext.js';
10
- // A chat message renders TEXT (plus its send-state rows). Files live in the
11
- // conversation's Conversation Files surface, never in the timeline — there is no
12
- // attachment body here. There is no reply-quote body either: no chat surface has
13
- // an authoring affordance for replying to a specific message, so a quote
14
- // renderer would be unreachable machinery.
15
- // ============================================
16
- // Correlated send-rejection copy + retry policy
17
- // ============================================
18
- // User-facing copy per correlated `send-rejected` code (stamped on meta.sendFailureCode
19
- // by the realtime channel client). A failed send with no code (a transport/exhausted
20
- // reconnect failure) falls back to the neutral "Couldn't send".
21
10
  const SEND_FAILURE_COPY = {
22
11
  'blocked-word': 'Message contains blocked language',
23
12
  archived: 'This channel is read-only',
@@ -30,44 +19,23 @@ const SEND_FAILURE_COPY = {
30
19
  function sendFailureCopy(code) {
31
20
  return (code && SEND_FAILURE_COPY[code]) || "Couldn't send";
32
21
  }
33
- // Retry policy comes from meta.sendRetryable, stamped by the channel client from
34
- // the wire contract's canonical retryability table (chat-schemas enforces the
35
- // frame's `retryable` matches the table, so this is the ONE source of truth —
36
- // never a second hardcoded code list here, which would have to move in lockstep
37
- // with the contract). `false` = terminal (an unchanged resend is guaranteed to
38
- // fail — no Retry action); `true` or ABSENT (a code-less transport/exhausted
39
- // reconnect failure) keeps Retry, reusing the ORIGINAL clientMessageId.
40
22
  function isRetryableFailure(meta) {
41
23
  return meta?.sendRetryable !== false;
42
24
  }
43
- // ============================================
44
- // System message
45
- // ============================================
46
- // System lines are the conversation's action record (agreed / retracted / offer updated /
47
- // finalized…), so each carries its time — same hour:minute format as the message bubbles.
48
25
  function SystemMessage({ m }) {
49
26
  return (_jsxs("div", { className: "chat-system-message", children: [m.text && _jsx("span", { children: m.text }), _jsx("span", { className: "chat-system-message-time", children: new Date(m.createdAt).toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' }) })] }));
50
27
  }
51
28
  export function MessageItemDefault(props) {
52
- const { m, currentUserId, isAdmin, handlers, isContinuation, onSenderClick: senderClick, senderAdornment, onRetrySend, } = props;
53
- // Only a resolved name is a click target: the app's unresolved state (a Retry included) is never
54
- // nested inside a button.
29
+ const { m, currentUserId, handlers, isContinuation, onSenderClick: senderClick, senderAdornment, onRetrySend, } = props;
55
30
  const nameResolved = useSenderNameResolution(m.senderId).status === 'resolved';
56
31
  const onSenderClick = nameResolved ? senderClick : undefined;
57
- // System messages render differently
58
32
  if (m.isSystemMessage) {
59
33
  return _jsx(SystemMessage, { m: m });
60
34
  }
61
35
  const mine = m.senderId === currentUserId;
62
- // Realtime optimistic-send lifecycle (meta is stamped by the channel client):
63
- // an un-acked echo renders subtly pending; one past the retry/age cap renders a
64
- // visibly failed bubble with a retry affordance (same clientMessageId — the DO
65
- // dedups, so a retry can never double-send).
66
36
  const sendFailed = m.meta?.sendFailed === true;
67
37
  const sendPending = m.meta?.optimistic === true && !sendFailed;
68
38
  const clientMessageId = typeof m.meta?.clientMessageId === 'string' ? m.meta.clientMessageId : null;
69
- // Correlated rejection reason (from the `send-rejected` frame), if any — drives
70
- // both the failure copy and whether a Retry affordance is offered.
71
39
  const sendFailureCode = typeof m.meta?.sendFailureCode === 'string' ? m.meta.sendFailureCode : null;
72
40
  const showRetry = sendFailed && isRetryableFailure(m.meta);
73
41
  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) : undefined, role: onSenderClick ? 'button' : undefined, tabIndex: onSenderClick ? 0 : undefined, onKeyDown: onSenderClick
@@ -75,6 +43,6 @@ export function MessageItemDefault(props) {
75
43
  if (e.key === 'Enter' || e.key === ' ')
76
44
  onSenderClick(m.senderId);
77
45
  }
78
- : undefined, children: _jsx(SenderName, { senderId: m.senderId }) }), senderAdornment, _jsx("span", { children: "\u00B7" }), _jsx("span", { children: new Date(m.createdAt).toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' }) })] })), m.text && (_jsx("p", { className: "text-sm whitespace-pre-wrap", children: _jsx(MessageText, { text: m.text }) }))] }), sendPending && (_jsxs("div", { className: "chat-send-status mt-0.5", role: "status", "aria-live": "polite", children: [_jsx(Spinner, { size: "xs", className: "shrink-0" }), _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 }) }))] }));
46
+ : undefined, children: _jsx(SenderName, { senderId: m.senderId }) }), senderAdornment, _jsx("span", { children: "\u00B7" }), _jsx("span", { children: new Date(m.createdAt).toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' }) })] })), m.text && (_jsx("p", { className: "text-sm whitespace-pre-wrap", children: _jsx(MessageText, { text: m.text }) }))] }), sendPending && (_jsxs("div", { className: "chat-send-status mt-0.5", role: "status", "aria-live": "polite", children: [_jsx(Spinner, { size: "xs", className: "shrink-0" }), _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, handlers: handlers }) }))] }));
79
47
  }
80
48
  //# 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 { ReactNode } from 'react';\nimport type { ChatMessageV1, ModerationHandlers } from '@ttt-productions/chat-core';\nimport { MessageText } from './MessageText.js';\nimport { cn } from '@ttt-productions/ui-core';\nimport { Button, Spinner } from '@ttt-productions/ui-core/react';\nimport { AlertTriangle } from 'lucide-react';\nimport { MessageActions } from './menus.js';\nimport { SenderName } from './SenderName.js';\nimport { useSenderNameResolution } from '../context/ChatNameResolverContext.js';\n\n// A chat message renders TEXT (plus its send-state rows). Files live in the\n// conversation's Conversation Files surface, never in the timeline — there is no\n// attachment body here. There is no reply-quote body either: no chat surface has\n// an authoring affordance for replying to a specific message, so a quote\n// renderer would be unreachable machinery.\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\n// ============================================\n// System message\n// ============================================\n\n// System lines are the conversation's action record (agreed / retracted / offer updated /\n// finalized…), so each carries its time — same hour:minute format as the message bubbles.\nfunction SystemMessage({ m }: { m: ChatMessageV1 }) {\n return (\n <div className=\"chat-system-message\">\n {m.text && <span>{m.text}</span>}\n <span className=\"chat-system-message-time\">\n {new Date(m.createdAt).toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' })}\n </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 — the app resolves anything it needs about the sender from the id.\n onSenderClick?: (senderId: string) => void;\n /** The app's adornment beside the sender name; shown with the sender line, never on a continuation. */\n senderAdornment?: ReactNode;\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\nexport function MessageItemDefault(props: MessageItemDefaultProps) {\n const {\n m,\n currentUserId,\n isAdmin,\n handlers,\n isContinuation,\n onSenderClick: senderClick,\n senderAdornment,\n onRetrySend,\n } = props;\n // Only a resolved name is a click target: the app's unresolved state (a Retry included) is never\n // nested inside a button.\n const nameResolved = useSenderNameResolution(m.senderId).status === 'resolved';\n const onSenderClick = nameResolved ? senderClick : undefined;\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) : undefined}\n role={onSenderClick ? 'button' : undefined}\n tabIndex={onSenderClick ? 0 : undefined}\n onKeyDown={\n onSenderClick\n ? (e) => {\n if (e.key === 'Enter' || e.key === ' ') onSenderClick(m.senderId);\n }\n : undefined\n }\n >\n <SenderName senderId={m.senderId} />\n </span>\n {senderAdornment}\n <span>·</span>\n <span>\n {new Date(m.createdAt).toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' })}\n </span>\n </div>\n )}\n\n {m.text && (\n <p className=\"text-sm whitespace-pre-wrap\">\n <MessageText text={m.text} />\n </p>\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 <Spinner size=\"xs\" className=\"shrink-0\" />\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 { ReactNode } from 'react';\nimport type { ChatMessageV1, ModerationHandlers } from '@ttt-productions/chat-core';\nimport { MessageText } from './MessageText.js';\nimport { cn } from '@ttt-productions/ui-core';\nimport { Button, Spinner } from '@ttt-productions/ui-core/react';\nimport { AlertTriangle } from 'lucide-react';\nimport { MessageActions } from './menus.js';\nimport { SenderName } from './SenderName.js';\nimport { useSenderNameResolution } from '../context/ChatNameResolverContext.js';\n\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\nfunction isRetryableFailure(meta: Record<string, unknown> | undefined): boolean {\n return meta?.sendRetryable !== false;\n}\n\nfunction SystemMessage({ m }: { m: ChatMessageV1 }) {\n return (\n <div className=\"chat-system-message\">\n {m.text && <span>{m.text}</span>}\n <span className=\"chat-system-message-time\">\n {new Date(m.createdAt).toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' })}\n </span>\n </div>\n );\n}\n\nexport type MessageItemDefaultProps = {\n m: ChatMessageV1;\n currentUserId: string;\n handlers?: ModerationHandlers;\n\n isContinuation?: boolean;\n\n onSenderClick?: (senderId: string) => void;\n senderAdornment?: ReactNode;\n\n onRetrySend?: (clientMessageId: string) => void;\n};\n\nexport function MessageItemDefault(props: MessageItemDefaultProps) {\n const {\n m,\n currentUserId,\n handlers,\n isContinuation,\n onSenderClick: senderClick,\n senderAdornment,\n onRetrySend,\n } = props;\n const nameResolved = useSenderNameResolution(m.senderId).status === 'resolved';\n const onSenderClick = nameResolved ? senderClick : undefined;\n\n if (m.isSystemMessage) {\n return <SystemMessage m={m} />;\n }\n\n const mine = m.senderId === currentUserId;\n\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 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) : undefined}\n role={onSenderClick ? 'button' : undefined}\n tabIndex={onSenderClick ? 0 : undefined}\n onKeyDown={\n onSenderClick\n ? (e) => {\n if (e.key === 'Enter' || e.key === ' ') onSenderClick(m.senderId);\n }\n : undefined\n }\n >\n <SenderName senderId={m.senderId} />\n </span>\n {senderAdornment}\n <span>·</span>\n <span>\n {new Date(m.createdAt).toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' })}\n </span>\n </div>\n )}\n\n {m.text && (\n <p className=\"text-sm whitespace-pre-wrap\">\n <MessageText text={m.text} />\n </p>\n )}\n </div>\n\n {sendPending && (\n <div className=\"chat-send-status mt-0.5\" role=\"status\" aria-live=\"polite\">\n <Spinner size=\"xs\" className=\"shrink-0\" />\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} handlers={handlers} />\n </div>\n )}\n </div>\n );\n}\n"]}
@@ -1,8 +1,6 @@
1
1
  import * as React from 'react';
2
2
  import type { ChatMessageGroupKey, ChatMessageV1, ModerationHandlers } from '@ttt-productions/chat-core';
3
3
  import type { MessageRendererRegistry } from '../types.js';
4
- /** What `MessageList` hands a custom scroll container: the element it must render as the scrollable
5
- * message region, with this ref, class, scroll handler, and children. */
6
4
  export type ChatScrollContainerProps = {
7
5
  ref: React.Ref<HTMLDivElement>;
8
6
  className: string;
@@ -12,7 +10,6 @@ export type ChatScrollContainerProps = {
12
10
  export declare function MessageList(props: {
13
11
  messages: ChatMessageV1[];
14
12
  currentUserId: string;
15
- isAdmin: boolean;
16
13
  isFetchingOlder?: boolean;
17
14
  hasOlder?: boolean;
18
15
  onLoadOlder?: () => void;
@@ -20,36 +17,15 @@ export declare function MessageList(props: {
20
17
  messageRenderers?: MessageRendererRegistry;
21
18
  showScrollToBottom?: boolean;
22
19
  onScrollToBottom?: () => void;
23
- /** Reports whether the list is scrolled to the bottom so the consumer can advance
24
- * authoritative read state only when the latest message is actually in view. */
25
20
  onAtBottomChange?: (atBottom: boolean) => void;
26
- /** Class for the scrollable message region. Overrides the default. The default is
27
- * `h-[400px]` (fixed-height card) unless `fillHeight` is set, in which case it is
28
- * `flex-1 min-h-0`. */
29
21
  scrollClassName?: string;
30
- /** Renders the scrollable message region in place of the default div, so the consumer can give it
31
- * its own surface component. It must render one element carrying the given ref, class, scroll
32
- * handler, and children. */
33
22
  scrollContainer?: React.ComponentType<ChatScrollContainerProps>;
34
- /** Fill the parent's height instead of a fixed `h-[400px]`. Makes the list a flex-col
35
- * that flexes to fill its container, so a bounded-height page panel scrolls inside.
36
- * The consuming layout must give MessageList's ancestor a bounded height (ChatShell's
37
- * `fillHeight` wires Card → CardContent → MessageList for this). */
38
23
  fillHeight?: boolean;
39
24
  handlers?: ModerationHandlers;
40
25
  onSenderClick?: (senderId: string) => void;
41
- /** The app's per-message grouping fact (chat-core `isContinuation`): a change in it breaks a
42
- * same-sender group, so the sender line — and its adornment — shows again. */
43
26
  messageGroupKey?: ChatMessageGroupKey;
44
- /** The app's adornment beside a message's sender name (default renderer only). */
45
27
  renderSenderAdornment?: (m: ChatMessageV1) => React.ReactNode;
46
- /** Retry a failed realtime send by clientMessageId (realtime transport only). */
47
28
  onRetrySend?: (clientMessageId: string) => void;
48
- /**
49
- * The failed older-page row (the consumer's error state with its retry). While set, it
50
- * renders at the top of the list and the list stops requesting older pages — a failed
51
- * page is retried only through the row, never by scrolling into it again.
52
- */
53
29
  olderLoadErrorRow?: React.ReactNode;
54
30
  }): React.JSX.Element;
55
31
  //# 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,EACV,mBAAmB,EACnB,aAAa,EACb,kBAAkB,EACnB,MAAM,4BAA4B,CAAC;AAEpC,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,aAAa,CAAC;AAI3D;0EAC0E;AAC1E,MAAM,MAAM,wBAAwB,GAAG;IACrC,GAAG,EAAE,KAAK,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,KAAK,CAAC,cAAc,CAAC,cAAc,CAAC,CAAC;IAC/C,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;CAC3B,CAAC;AAWF,wBAAgB,WAAW,CAAC,KAAK,EAAE;IACjC,QAAQ,EAAE,aAAa,EAAE,CAAC;IAC1B,aAAa,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,OAAO,CAAC;IAEjB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,IAAI,CAAC;IAEzB,aAAa,CAAC,EAAE,CAAC,CAAC,EAAE,aAAa,KAAK,KAAK,CAAC,SAAS,CAAC;IACtD,gBAAgB,CAAC,EAAE,uBAAuB,CAAC;IAE3C,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,gBAAgB,CAAC,EAAE,MAAM,IAAI,CAAC;IAC9B;qFACiF;IACjF,gBAAgB,CAAC,EAAE,CAAC,QAAQ,EAAE,OAAO,KAAK,IAAI,CAAC;IAC/C;;4BAEwB;IACxB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;iCAE6B;IAC7B,eAAe,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC,wBAAwB,CAAC,CAAC;IAChE;;;yEAGqE;IACrE,UAAU,CAAC,EAAE,OAAO,CAAC;IAErB,QAAQ,CAAC,EAAE,kBAAkB,CAAC;IAC9B,aAAa,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IAC3C;mFAC+E;IAC/E,eAAe,CAAC,EAAE,mBAAmB,CAAC;IACtC,kFAAkF;IAClF,qBAAqB,CAAC,EAAE,CAAC,CAAC,EAAE,aAAa,KAAK,KAAK,CAAC,SAAS,CAAC;IAC9D,iFAAiF;IACjF,WAAW,CAAC,EAAE,CAAC,eAAe,EAAE,MAAM,KAAK,IAAI,CAAC;IAChD;;;;OAIG;IACH,iBAAiB,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;CACrC,qBAiNA"}
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,EACV,mBAAmB,EACnB,aAAa,EACb,kBAAkB,EACnB,MAAM,4BAA4B,CAAC;AAEpC,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,aAAa,CAAC;AAI3D,MAAM,MAAM,wBAAwB,GAAG;IACrC,GAAG,EAAE,KAAK,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,KAAK,CAAC,cAAc,CAAC,cAAc,CAAC,CAAC;IAC/C,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;CAC3B,CAAC;AAUF,wBAAgB,WAAW,CAAC,KAAK,EAAE;IACjC,QAAQ,EAAE,aAAa,EAAE,CAAC;IAC1B,aAAa,EAAE,MAAM,CAAC;IAEtB,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,gBAAgB,CAAC,EAAE,CAAC,QAAQ,EAAE,OAAO,KAAK,IAAI,CAAC;IAC/C,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,eAAe,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC,wBAAwB,CAAC,CAAC;IAChE,UAAU,CAAC,EAAE,OAAO,CAAC;IAErB,QAAQ,CAAC,EAAE,kBAAkB,CAAC;IAC9B,aAAa,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IAC3C,eAAe,CAAC,EAAE,mBAAmB,CAAC;IACtC,qBAAqB,CAAC,EAAE,CAAC,CAAC,EAAE,aAAa,KAAK,KAAK,CAAC,SAAS,CAAC;IAC9D,WAAW,CAAC,EAAE,CAAC,eAAe,EAAE,MAAM,KAAK,IAAI,CAAC;IAChD,iBAAiB,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;CACrC,qBAqMA"}
@@ -4,12 +4,11 @@ import * as React from 'react';
4
4
  import { isContinuation } from '@ttt-productions/chat-core';
5
5
  import { Button, Spinner } from '@ttt-productions/ui-core/react';
6
6
  import { MessageItemDefault } from './MessageItemDefault.js';
7
- /** The default scroll container: a plain div. */
8
7
  function DefaultScrollContainer({ ref, className, onScroll, children }) {
9
8
  return (_jsx("div", { ref: ref, className: className, onScroll: onScroll, children: children }));
10
9
  }
11
10
  export function MessageList(props) {
12
- const { messages, currentUserId, isAdmin, isFetchingOlder, hasOlder, onLoadOlder, renderMessage, messageRenderers, showScrollToBottom, onScrollToBottom, onAtBottomChange, scrollClassName, scrollContainer: ScrollContainer = DefaultScrollContainer, fillHeight = false, handlers, onSenderClick, messageGroupKey, renderSenderAdornment, onRetrySend, olderLoadErrorRow, } = props;
11
+ const { messages, currentUserId, isFetchingOlder, hasOlder, onLoadOlder, renderMessage, messageRenderers, showScrollToBottom, onScrollToBottom, onAtBottomChange, scrollClassName, scrollContainer: ScrollContainer = DefaultScrollContainer, fillHeight = false, handlers, onSenderClick, messageGroupKey, renderSenderAdornment, onRetrySend, olderLoadErrorRow, } = props;
13
12
  const olderLoadFailed = olderLoadErrorRow != null;
14
13
  const scrollClass = scrollClassName ?? (fillHeight ? 'flex-1 min-h-0' : 'h-[400px]');
15
14
  const outerClass = fillHeight ? 'relative flex flex-col flex-1 min-h-0' : 'relative';
@@ -18,8 +17,6 @@ export function MessageList(props) {
18
17
  const isAtBottomRef = React.useRef(true);
19
18
  const prevScrollHeightRef = React.useRef(null);
20
19
  const prevCountRef = React.useRef(0);
21
- // Count of messages that arrived while the user was scrolled up — drives the
22
- // "new messages" pill. Reset when the user returns to the bottom.
23
20
  const [unseenCount, setUnseenCount] = React.useState(0);
24
21
  const scrollToBottom = React.useCallback(() => {
25
22
  const el = scrollRef.current;
@@ -30,7 +27,6 @@ export function MessageList(props) {
30
27
  setUnseenCount(0);
31
28
  onAtBottomChange?.(true);
32
29
  }, [onAtBottomChange]);
33
- // IntersectionObserver for infinite scroll (older messages)
34
30
  React.useEffect(() => {
35
31
  const root = scrollRef.current;
36
32
  const target = topSentinelRef.current;
@@ -50,7 +46,8 @@ export function MessageList(props) {
50
46
  io.observe(target);
51
47
  return () => io.disconnect();
52
48
  }, [hasOlder, isFetchingOlder, olderLoadFailed, onLoadOlder]);
53
- // Scroll management — FIXES accordion scroll jump
49
+ // Scroll management assigns scrollTop (never a smooth scrollTo), so a scrolling parent such as an
50
+ // accordion never jumps.
54
51
  React.useLayoutEffect(() => {
55
52
  const el = scrollRef.current;
56
53
  if (!el)
@@ -58,7 +55,6 @@ export function MessageList(props) {
58
55
  const count = messages.length;
59
56
  const prev = prevCountRef.current;
60
57
  if (prev === 0 && count > 0) {
61
- // Initial load — scroll to bottom WITHOUT triggering parent scroll
62
58
  requestAnimationFrame(() => {
63
59
  const savedScrollY = window.scrollY;
64
60
  el.scrollTop = el.scrollHeight;
@@ -70,14 +66,11 @@ export function MessageList(props) {
70
66
  isAtBottomRef.current = true;
71
67
  }
72
68
  else if (count > prev && prevScrollHeightRef.current != null) {
73
- // Older messages prepended — maintain scroll position
74
69
  const diff = el.scrollHeight - prevScrollHeightRef.current;
75
70
  el.scrollTop += diff;
76
71
  prevScrollHeightRef.current = null;
77
72
  }
78
73
  else if (count > prev && isAtBottomRef.current) {
79
- // New message at bottom, user is at bottom — scroll down
80
- // Use scrollTop assignment, NOT scrollTo with behavior:"smooth" (causes parent scroll jump)
81
74
  requestAnimationFrame(() => {
82
75
  const savedScrollY = window.scrollY;
83
76
  el.scrollTop = el.scrollHeight;
@@ -87,8 +80,6 @@ export function MessageList(props) {
87
80
  });
88
81
  }
89
82
  else if (count > prev) {
90
- // New message(s) arrived while the user was scrolled up — surface the pill
91
- // instead of yanking their scroll position.
92
83
  setUnseenCount((c) => c + (count - prev));
93
84
  }
94
85
  prevCountRef.current = count;
@@ -108,10 +99,9 @@ export function MessageList(props) {
108
99
  const showDay = day !== lastDay;
109
100
  lastDay = day;
110
101
  const prevMsg = idx > 0 ? messages[idx - 1] : undefined;
111
- // Date separators always break grouping
112
102
  const continuation = showDay ? false : isContinuation(prevMsg, m, messageGroupKey);
113
103
  const byType = m.type && messageRenderers?.[m.type] ? messageRenderers[m.type](m) : null;
114
- const body = renderMessage?.(m) ?? byType ?? (_jsx(MessageItemDefault, { m: m, currentUserId: currentUserId, isAdmin: isAdmin, handlers: handlers, isContinuation: continuation, onSenderClick: onSenderClick, senderAdornment: renderSenderAdornment?.(m), onRetrySend: onRetrySend }));
104
+ const body = renderMessage?.(m) ?? byType ?? (_jsx(MessageItemDefault, { m: m, currentUserId: currentUserId, handlers: handlers, isContinuation: continuation, onSenderClick: onSenderClick, senderAdornment: renderSenderAdornment?.(m), onRetrySend: onRetrySend }));
115
105
  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));
116
106
  }) }))] }), (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 elevation-raised gap-1", onClick: () => {
117
107
  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 {\n ChatMessageGroupKey,\n ChatMessageV1,\n ModerationHandlers,\n} from '@ttt-productions/chat-core';\nimport { isContinuation } from '@ttt-productions/chat-core';\nimport type { MessageRendererRegistry } from '../types.js';\nimport { Button, Spinner } from '@ttt-productions/ui-core/react';\nimport { MessageItemDefault } from './MessageItemDefault.js';\n\n/** What `MessageList` hands a custom scroll container: the element it must render as the scrollable\n * message region, with this ref, class, scroll handler, and children. */\nexport type ChatScrollContainerProps = {\n ref: React.Ref<HTMLDivElement>;\n className: string;\n onScroll: React.UIEventHandler<HTMLDivElement>;\n children: React.ReactNode;\n};\n\n/** The default scroll container: a plain div. */\nfunction DefaultScrollContainer({ ref, className, onScroll, children }: ChatScrollContainerProps) {\n return (\n <div ref={ref} className={className} onScroll={onScroll}>\n {children}\n </div>\n );\n}\n\nexport function MessageList(props: {\n messages: ChatMessageV1[];\n currentUserId: string;\n isAdmin: boolean;\n\n isFetchingOlder?: boolean;\n hasOlder?: boolean;\n onLoadOlder?: () => void;\n\n renderMessage?: (m: ChatMessageV1) => React.ReactNode;\n messageRenderers?: MessageRendererRegistry;\n\n showScrollToBottom?: boolean;\n onScrollToBottom?: () => void;\n /** Reports whether the list is scrolled to the bottom so the consumer can advance\n * authoritative read state only when the latest message is actually in view. */\n onAtBottomChange?: (atBottom: boolean) => void;\n /** Class for the scrollable message region. Overrides the default. The default is\n * `h-[400px]` (fixed-height card) unless `fillHeight` is set, in which case it is\n * `flex-1 min-h-0`. */\n scrollClassName?: string;\n /** Renders the scrollable message region in place of the default div, so the consumer can give it\n * its own surface component. It must render one element carrying the given ref, class, scroll\n * handler, and children. */\n scrollContainer?: React.ComponentType<ChatScrollContainerProps>;\n /** Fill the parent's height instead of a fixed `h-[400px]`. Makes the list a flex-col\n * that flexes to fill its container, so a bounded-height page panel scrolls inside.\n * The consuming layout must give MessageList's ancestor a bounded height (ChatShell's\n * `fillHeight` wires Card → CardContent → MessageList for this). */\n fillHeight?: boolean;\n\n handlers?: ModerationHandlers;\n onSenderClick?: (senderId: string) => void;\n /** The app's per-message grouping fact (chat-core `isContinuation`): a change in it breaks a\n * same-sender group, so the sender line — and its adornment — shows again. */\n messageGroupKey?: ChatMessageGroupKey;\n /** The app's adornment beside a message's sender name (default renderer only). */\n renderSenderAdornment?: (m: ChatMessageV1) => React.ReactNode;\n /** Retry a failed realtime send by clientMessageId (realtime transport only). */\n onRetrySend?: (clientMessageId: string) => void;\n /**\n * The failed older-page row (the consumer's error state with its retry). While set, it\n * renders at the top of the list and the list stops requesting older pages — a failed\n * page is retried only through the row, never by scrolling into it again.\n */\n olderLoadErrorRow?: React.ReactNode;\n}) {\n const {\n messages,\n currentUserId,\n isAdmin,\n isFetchingOlder,\n hasOlder,\n onLoadOlder,\n renderMessage,\n messageRenderers,\n showScrollToBottom,\n onScrollToBottom,\n onAtBottomChange,\n scrollClassName,\n scrollContainer: ScrollContainer = DefaultScrollContainer,\n fillHeight = false,\n handlers,\n onSenderClick,\n messageGroupKey,\n renderSenderAdornment,\n onRetrySend,\n olderLoadErrorRow,\n } = props;\n const olderLoadFailed = olderLoadErrorRow != null;\n\n const scrollClass = scrollClassName ?? (fillHeight ? 'flex-1 min-h-0' : 'h-[400px]');\n const outerClass = fillHeight ? 'relative flex flex-col flex-1 min-h-0' : 'relative';\n\n const scrollRef = React.useRef<HTMLDivElement>(null);\n const topSentinelRef = React.useRef<HTMLDivElement>(null);\n\n const isAtBottomRef = React.useRef(true);\n const prevScrollHeightRef = React.useRef<number | null>(null);\n const prevCountRef = React.useRef(0);\n\n // Count of messages that arrived while the user was scrolled up — drives the\n // \"new messages\" pill. Reset when the user returns to the bottom.\n const [unseenCount, setUnseenCount] = React.useState(0);\n\n const scrollToBottom = React.useCallback(() => {\n const el = scrollRef.current;\n if (!el) return;\n el.scrollTop = el.scrollHeight;\n isAtBottomRef.current = true;\n setUnseenCount(0);\n onAtBottomChange?.(true);\n }, [onAtBottomChange]);\n\n // IntersectionObserver for infinite scroll (older messages)\n React.useEffect(() => {\n const root = scrollRef.current;\n const target = topSentinelRef.current;\n if (!root || !target) return;\n if (typeof IntersectionObserver === 'undefined') return;\n\n const io = new IntersectionObserver(\n (entries) => {\n const e = entries[0];\n if (!e?.isIntersecting) return;\n if (!hasOlder || isFetchingOlder || olderLoadFailed) return;\n\n prevScrollHeightRef.current = root.scrollHeight;\n onLoadOlder?.();\n },\n { root, threshold: 0 },\n );\n\n io.observe(target);\n return () => io.disconnect();\n }, [hasOlder, isFetchingOlder, olderLoadFailed, onLoadOlder]);\n\n // Scroll management — FIXES accordion scroll jump\n React.useLayoutEffect(() => {\n const el = scrollRef.current;\n if (!el) return;\n\n const count = messages.length;\n const prev = prevCountRef.current;\n\n if (prev === 0 && count > 0) {\n // Initial load — scroll to bottom WITHOUT triggering parent scroll\n requestAnimationFrame(() => {\n const savedScrollY = window.scrollY;\n el.scrollTop = el.scrollHeight;\n // Restore page scroll if the browser moved it\n if (window.scrollY !== savedScrollY) {\n window.scrollTo({ top: savedScrollY, behavior: 'instant' as ScrollBehavior });\n }\n });\n isAtBottomRef.current = true;\n } else if (count > prev && prevScrollHeightRef.current != null) {\n // Older messages prepended — maintain scroll position\n const diff = el.scrollHeight - prevScrollHeightRef.current;\n el.scrollTop += diff;\n prevScrollHeightRef.current = null;\n } else if (count > prev && isAtBottomRef.current) {\n // New message at bottom, user is at bottom — scroll down\n // Use scrollTop assignment, NOT scrollTo with behavior:\"smooth\" (causes parent scroll jump)\n requestAnimationFrame(() => {\n const savedScrollY = window.scrollY;\n el.scrollTop = el.scrollHeight;\n if (window.scrollY !== savedScrollY) {\n window.scrollTo({ top: savedScrollY, behavior: 'instant' as ScrollBehavior });\n }\n });\n } else if (count > prev) {\n // New message(s) arrived while the user was scrolled up — surface the pill\n // instead of yanking their scroll position.\n setUnseenCount((c) => c + (count - prev));\n }\n\n prevCountRef.current = count;\n }, [messages]);\n\n const onScroll = (e: React.UIEvent<HTMLDivElement>) => {\n const { scrollTop, scrollHeight, clientHeight } = e.currentTarget;\n const atBottom = scrollHeight - (scrollTop + clientHeight) < 50;\n if (atBottom !== isAtBottomRef.current) onAtBottomChange?.(atBottom);\n isAtBottomRef.current = atBottom;\n if (atBottom && unseenCount !== 0) setUnseenCount(0);\n };\n\n let lastDay: string | null = null;\n\n return (\n <div className={outerClass}>\n <ScrollContainer\n ref={scrollRef}\n className={`${scrollClass} overflow-y-auto p-4`}\n onScroll={onScroll}\n >\n {olderLoadFailed ? (\n <div className=\"mb-2\">{olderLoadErrorRow}</div>\n ) : isFetchingOlder ? (\n <div className=\"flex justify-center mb-2\">\n <Spinner size=\"xs\" label=\"Loading older messages\" />\n </div>\n ) : null}\n\n <div ref={topSentinelRef} />\n\n {messages.length === 0 ? (\n <div className=\"h-full flex items-center justify-center text-sm opacity-70\">\n No messages yet\n </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, messageGroupKey);\n\n const byType =\n m.type && messageRenderers?.[m.type] ? messageRenderers[m.type]!(m) : null;\n\n const body = renderMessage?.(m) ?? byType ?? (\n <MessageItemDefault\n m={m}\n currentUserId={currentUserId}\n isAdmin={isAdmin}\n handlers={handlers}\n isContinuation={continuation}\n onSenderClick={onSenderClick}\n senderAdornment={renderSenderAdornment?.(m)}\n onRetrySend={onRetrySend}\n />\n );\n\n return (\n <React.Fragment key={m.messageId}>\n {showDay && (\n <div className=\"my-2 flex justify-center\">\n {/* theme-core: components.css */}\n <div className=\"chat-date-separator\">\n {new Date(m.createdAt).toLocaleDateString()}\n </div>\n </div>\n )}\n {body}\n </React.Fragment>\n );\n })}\n </div>\n )}\n </ScrollContainer>\n\n {(unseenCount > 0 || showScrollToBottom) && (\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n aria-label={\n unseenCount > 0 ? `${unseenCount} new messages, scroll to latest` : 'Scroll to latest'\n }\n className=\"absolute bottom-4 left-1/2 -translate-x-1/2 rounded-full elevation-raised gap-1\"\n onClick={() => {\n scrollToBottom();\n onScrollToBottom?.();\n }}\n >\n {unseenCount > 0 ? `${unseenCount} new message${unseenCount > 1 ? 's' : ''} ↓` : '↓'}\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 {\n ChatMessageGroupKey,\n ChatMessageV1,\n ModerationHandlers,\n} from '@ttt-productions/chat-core';\nimport { isContinuation } from '@ttt-productions/chat-core';\nimport type { MessageRendererRegistry } from '../types.js';\nimport { Button, Spinner } from '@ttt-productions/ui-core/react';\nimport { MessageItemDefault } from './MessageItemDefault.js';\n\nexport type ChatScrollContainerProps = {\n ref: React.Ref<HTMLDivElement>;\n className: string;\n onScroll: React.UIEventHandler<HTMLDivElement>;\n children: React.ReactNode;\n};\n\nfunction DefaultScrollContainer({ ref, className, onScroll, children }: ChatScrollContainerProps) {\n return (\n <div ref={ref} className={className} onScroll={onScroll}>\n {children}\n </div>\n );\n}\n\nexport function MessageList(props: {\n messages: ChatMessageV1[];\n currentUserId: string;\n\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 onAtBottomChange?: (atBottom: boolean) => void;\n scrollClassName?: string;\n scrollContainer?: React.ComponentType<ChatScrollContainerProps>;\n fillHeight?: boolean;\n\n handlers?: ModerationHandlers;\n onSenderClick?: (senderId: string) => void;\n messageGroupKey?: ChatMessageGroupKey;\n renderSenderAdornment?: (m: ChatMessageV1) => React.ReactNode;\n onRetrySend?: (clientMessageId: string) => void;\n olderLoadErrorRow?: React.ReactNode;\n}) {\n const {\n messages,\n currentUserId,\n isFetchingOlder,\n hasOlder,\n onLoadOlder,\n renderMessage,\n messageRenderers,\n showScrollToBottom,\n onScrollToBottom,\n onAtBottomChange,\n scrollClassName,\n scrollContainer: ScrollContainer = DefaultScrollContainer,\n fillHeight = false,\n handlers,\n onSenderClick,\n messageGroupKey,\n renderSenderAdornment,\n onRetrySend,\n olderLoadErrorRow,\n } = props;\n const olderLoadFailed = olderLoadErrorRow != null;\n\n const scrollClass = scrollClassName ?? (fillHeight ? 'flex-1 min-h-0' : 'h-[400px]');\n const outerClass = fillHeight ? 'relative flex flex-col flex-1 min-h-0' : 'relative';\n\n const scrollRef = React.useRef<HTMLDivElement>(null);\n const topSentinelRef = React.useRef<HTMLDivElement>(null);\n\n const isAtBottomRef = React.useRef(true);\n const prevScrollHeightRef = React.useRef<number | null>(null);\n const prevCountRef = React.useRef(0);\n\n 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 React.useEffect(() => {\n const root = scrollRef.current;\n const target = topSentinelRef.current;\n if (!root || !target) return;\n if (typeof IntersectionObserver === 'undefined') return;\n\n const io = new IntersectionObserver(\n (entries) => {\n const e = entries[0];\n if (!e?.isIntersecting) return;\n if (!hasOlder || isFetchingOlder || olderLoadFailed) return;\n\n prevScrollHeightRef.current = root.scrollHeight;\n onLoadOlder?.();\n },\n { root, threshold: 0 },\n );\n\n io.observe(target);\n return () => io.disconnect();\n }, [hasOlder, isFetchingOlder, olderLoadFailed, onLoadOlder]);\n\n // Scroll management assigns scrollTop (never a smooth scrollTo), so a scrolling parent such as an\n // accordion never jumps.\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 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 const diff = el.scrollHeight - prevScrollHeightRef.current;\n el.scrollTop += diff;\n prevScrollHeightRef.current = null;\n } else if (count > prev && isAtBottomRef.current) {\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 setUnseenCount((c) => c + (count - prev));\n }\n\n prevCountRef.current = count;\n }, [messages]);\n\n const onScroll = (e: React.UIEvent<HTMLDivElement>) => {\n const { scrollTop, scrollHeight, clientHeight } = e.currentTarget;\n const atBottom = scrollHeight - (scrollTop + clientHeight) < 50;\n if (atBottom !== isAtBottomRef.current) onAtBottomChange?.(atBottom);\n isAtBottomRef.current = atBottom;\n if (atBottom && unseenCount !== 0) setUnseenCount(0);\n };\n\n let lastDay: string | null = null;\n\n return (\n <div className={outerClass}>\n <ScrollContainer\n ref={scrollRef}\n className={`${scrollClass} overflow-y-auto p-4`}\n onScroll={onScroll}\n >\n {olderLoadFailed ? (\n <div className=\"mb-2\">{olderLoadErrorRow}</div>\n ) : isFetchingOlder ? (\n <div className=\"flex justify-center mb-2\">\n <Spinner size=\"xs\" label=\"Loading older messages\" />\n </div>\n ) : null}\n\n <div ref={topSentinelRef} />\n\n {messages.length === 0 ? (\n <div className=\"h-full flex items-center justify-center text-sm opacity-70\">\n No messages yet\n </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 const continuation = showDay ? false : isContinuation(prevMsg, m, messageGroupKey);\n\n const byType =\n m.type && messageRenderers?.[m.type] ? messageRenderers[m.type]!(m) : null;\n\n const body = renderMessage?.(m) ?? byType ?? (\n <MessageItemDefault\n m={m}\n currentUserId={currentUserId}\n handlers={handlers}\n isContinuation={continuation}\n onSenderClick={onSenderClick}\n senderAdornment={renderSenderAdornment?.(m)}\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 <div className=\"chat-date-separator\">\n {new Date(m.createdAt).toLocaleDateString()}\n </div>\n </div>\n )}\n {body}\n </React.Fragment>\n );\n })}\n </div>\n )}\n </ScrollContainer>\n\n {(unseenCount > 0 || showScrollToBottom) && (\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n aria-label={\n unseenCount > 0 ? `${unseenCount} new messages, scroll to latest` : 'Scroll to latest'\n }\n className=\"absolute bottom-4 left-1/2 -translate-x-1/2 rounded-full elevation-raised gap-1\"\n onClick={() => {\n scrollToBottom();\n onScrollToBottom?.();\n }}\n >\n {unseenCount > 0 ? `${unseenCount} new message${unseenCount > 1 ? 's' : ''} ↓` : '↓'}\n </Button>\n )}\n </div>\n );\n}\n"]}
@@ -1,18 +1,5 @@
1
- /**
2
- * Renders message text.
3
- *
4
- * Chat text is PLAIN text — there is no inline token grammar, no mention
5
- * parsing, and no rich-text pass. The component exists so every chat text
6
- * surface (message bubbles) renders through one place with one wrapper element
7
- * and one optional className.
8
- *
9
- * Pure, no context. Safe to use inside MessageItem renderers or any other chat
10
- * text surface.
11
- */
12
1
  export interface MessageTextProps {
13
- /** Raw message text. Rendered verbatim. */
14
2
  text: string;
15
- /** Optional className applied to the wrapping `<span>`. */
16
3
  className?: string;
17
4
  }
18
5
  export declare function MessageText(props: MessageTextProps): import("react").JSX.Element;
@@ -1 +1 @@
1
- {"version":3,"file":"MessageText.d.ts","sourceRoot":"","sources":["../../src/ui/MessageText.tsx"],"names":[],"mappings":"AAEA;;;;;;;;;;GAUG;AAEH,MAAM,WAAW,gBAAgB;IAC/B,2CAA2C;IAC3C,IAAI,EAAE,MAAM,CAAC;IACb,2DAA2D;IAC3D,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,gBAAgB,+BAGlD"}
1
+ {"version":3,"file":"MessageText.d.ts","sourceRoot":"","sources":["../../src/ui/MessageText.tsx"],"names":[],"mappings":"AAEA,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,gBAAgB,+BAGlD"}
@@ -1 +1 @@
1
- {"version":3,"file":"MessageText.js","sourceRoot":"","sources":["../../src/ui/MessageText.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAqBb,MAAM,UAAU,WAAW,CAAC,KAAuB;IACjD,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,GAAG,KAAK,CAAC;IAClC,OAAO,eAAM,SAAS,EAAE,SAAS,YAAG,IAAI,GAAQ,CAAC;AACnD,CAAC","sourcesContent":["'use client';\n\n/**\n * Renders message text.\n *\n * Chat text is PLAIN text — there is no inline token grammar, no mention\n * parsing, and no rich-text pass. The component exists so every chat text\n * surface (message bubbles) renders through one place with one wrapper element\n * and one optional className.\n *\n * Pure, no context. Safe to use inside MessageItem renderers or any other chat\n * text surface.\n */\n\nexport interface MessageTextProps {\n /** Raw message text. Rendered verbatim. */\n text: string;\n /** Optional className applied to the wrapping `<span>`. */\n className?: string;\n}\n\nexport function MessageText(props: MessageTextProps) {\n const { text, className } = props;\n return <span className={className}>{text}</span>;\n}\n"]}
1
+ {"version":3,"file":"MessageText.js","sourceRoot":"","sources":["../../src/ui/MessageText.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAOb,MAAM,UAAU,WAAW,CAAC,KAAuB;IACjD,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,GAAG,KAAK,CAAC;IAClC,OAAO,eAAM,SAAS,EAAE,SAAS,YAAG,IAAI,GAAQ,CAAC;AACnD,CAAC","sourcesContent":["'use client';\n\nexport interface MessageTextProps {\n text: string;\n className?: string;\n}\n\nexport function MessageText(props: MessageTextProps) {\n const { text, className } = props;\n return <span className={className}>{text}</span>;\n}\n"]}
@@ -1,8 +1,3 @@
1
- /**
2
- * A message sender's name: the resolved name, or — when the app could not resolve it —
3
- * whatever the app's `renderUnresolvedName` renders for that resolution. Never a stand-in
4
- * name: an unresolved sender with no slot renders nothing.
5
- */
6
1
  export declare function SenderName({ senderId }: {
7
2
  senderId: string;
8
3
  }): import("react").JSX.Element;
@@ -1 +1 @@
1
- {"version":3,"file":"SenderName.d.ts","sourceRoot":"","sources":["../../src/ui/SenderName.tsx"],"names":[],"mappings":"AAIA;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,EAAE,QAAQ,EAAE,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,+BAK5D"}
1
+ {"version":3,"file":"SenderName.d.ts","sourceRoot":"","sources":["../../src/ui/SenderName.tsx"],"names":[],"mappings":"AAIA,wBAAgB,UAAU,CAAC,EAAE,QAAQ,EAAE,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,+BAK5D"}
@@ -1,11 +1,6 @@
1
1
  'use client';
2
2
  import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
3
3
  import { useChatNameResolver } from '../context/ChatNameResolverContext.js';
4
- /**
5
- * A message sender's name: the resolved name, or — when the app could not resolve it —
6
- * whatever the app's `renderUnresolvedName` renders for that resolution. Never a stand-in
7
- * name: an unresolved sender with no slot renders nothing.
8
- */
9
4
  export function SenderName({ senderId }) {
10
5
  const { resolveName, renderUnresolvedName } = useChatNameResolver();
11
6
  const resolution = resolveName(senderId);
@@ -1 +1 @@
1
- {"version":3,"file":"SenderName.js","sourceRoot":"","sources":["../../src/ui/SenderName.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAEb,OAAO,EAAE,mBAAmB,EAAE,MAAM,uCAAuC,CAAC;AAE5E;;;;GAIG;AACH,MAAM,UAAU,UAAU,CAAC,EAAE,QAAQ,EAAwB;IAC3D,MAAM,EAAE,WAAW,EAAE,oBAAoB,EAAE,GAAG,mBAAmB,EAAE,CAAC;IACpE,MAAM,UAAU,GAAG,WAAW,CAAC,QAAQ,CAAC,CAAC;IACzC,IAAI,UAAU,CAAC,MAAM,KAAK,UAAU;QAAE,OAAO,4BAAG,UAAU,CAAC,IAAI,GAAI,CAAC;IACpE,OAAO,4BAAG,oBAAoB,EAAE,CAAC,UAAU,CAAC,IAAI,IAAI,GAAI,CAAC;AAC3D,CAAC","sourcesContent":["'use client';\n\nimport { useChatNameResolver } from '../context/ChatNameResolverContext.js';\n\n/**\n * A message sender's name: the resolved name, or — when the app could not resolve it —\n * whatever the app's `renderUnresolvedName` renders for that resolution. Never a stand-in\n * name: an unresolved sender with no slot renders nothing.\n */\nexport function SenderName({ senderId }: { senderId: string }) {\n const { resolveName, renderUnresolvedName } = useChatNameResolver();\n const resolution = resolveName(senderId);\n if (resolution.status === 'resolved') return <>{resolution.name}</>;\n return <>{renderUnresolvedName?.(resolution) ?? null}</>;\n}\n"]}
1
+ {"version":3,"file":"SenderName.js","sourceRoot":"","sources":["../../src/ui/SenderName.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAEb,OAAO,EAAE,mBAAmB,EAAE,MAAM,uCAAuC,CAAC;AAE5E,MAAM,UAAU,UAAU,CAAC,EAAE,QAAQ,EAAwB;IAC3D,MAAM,EAAE,WAAW,EAAE,oBAAoB,EAAE,GAAG,mBAAmB,EAAE,CAAC;IACpE,MAAM,UAAU,GAAG,WAAW,CAAC,QAAQ,CAAC,CAAC;IACzC,IAAI,UAAU,CAAC,MAAM,KAAK,UAAU;QAAE,OAAO,4BAAG,UAAU,CAAC,IAAI,GAAI,CAAC;IACpE,OAAO,4BAAG,oBAAoB,EAAE,CAAC,UAAU,CAAC,IAAI,IAAI,GAAI,CAAC;AAC3D,CAAC","sourcesContent":["'use client';\n\nimport { useChatNameResolver } from '../context/ChatNameResolverContext.js';\n\nexport function SenderName({ senderId }: { senderId: string }) {\n const { resolveName, renderUnresolvedName } = useChatNameResolver();\n const resolution = resolveName(senderId);\n if (resolution.status === 'resolved') return <>{resolution.name}</>;\n return <>{renderUnresolvedName?.(resolution) ?? null}</>;\n}\n"]}
@@ -1,12 +1,10 @@
1
1
  import type { ModerationHandlers } from '@ttt-productions/chat-core';
2
2
  export declare function MessageActions(props: {
3
3
  messageId: string;
4
- isAdmin: boolean;
5
4
  handlers?: ModerationHandlers;
6
5
  }): import("react").JSX.Element;
7
6
  export declare function ThreadActions(props: {
8
7
  threadId: string;
9
- isAdmin: boolean;
10
8
  handlers?: ModerationHandlers;
11
9
  }): import("react").JSX.Element;
12
10
  //# sourceMappingURL=menus.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"menus.d.ts","sourceRoot":"","sources":["../../src/ui/menus.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAmCrE,wBAAgB,cAAc,CAAC,KAAK,EAAE;IACpC,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,CAAC,EAAE,kBAAkB,CAAC;CAC/B,+BAaA;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE;IACnC,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,CAAC,EAAE,kBAAkB,CAAC;CAC/B,+BAeA"}
1
+ {"version":3,"file":"menus.d.ts","sourceRoot":"","sources":["../../src/ui/menus.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAiCrE,wBAAgB,cAAc,CAAC,KAAK,EAAE;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,kBAAkB,CAAA;CAAE,+BASzF;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,kBAAkB,CAAA;CAAE,+BAWvF"}
package/dist/ui/menus.js CHANGED
@@ -1,9 +1,7 @@
1
1
  'use client';
2
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import { Button, useAsyncAction } from '@ttt-productions/ui-core/react';
4
- // The consumer's handler owns its failure UX. The action only keeps a rejection
5
- // from vanishing: it re-raises it on the global error channel, where monitoring
6
- // sees it — exactly where an unawaited rejection would have gone.
4
+ // Rethrown on a timer so the rejection reaches the global error channel, as an unawaited one would.
7
5
  function reraise(error) {
8
6
  setTimeout(() => {
9
7
  throw error;
@@ -17,15 +15,13 @@ function ModerationActionButton(props) {
17
15
  return (_jsx(Button, { type: "button", variant: "link", size: "sm", className: "h-auto p-0 text-xs opacity-70 hover:opacity-100", onClick: () => void action.run(), pending: action.pending, children: label }));
18
16
  }
19
17
  export function MessageActions(props) {
20
- const { messageId, isAdmin, handlers } = props;
18
+ const { messageId, handlers } = props;
21
19
  const onReport = handlers?.onReportMessage;
22
- const onDelete = handlers?.onDeleteMessage;
23
- return (_jsxs("div", { className: "flex items-center gap-2", children: [onReport && _jsx(ModerationActionButton, { label: "Report", onAction: () => onReport(messageId) }), isAdmin && onDelete && (_jsx(ModerationActionButton, { label: "Delete", onAction: () => onDelete(messageId) }))] }));
20
+ return (_jsx("div", { className: "flex items-center gap-2", children: onReport && _jsx(ModerationActionButton, { label: "Report", onAction: () => onReport(messageId) }) }));
24
21
  }
25
22
  export function ThreadActions(props) {
26
- const { threadId, isAdmin, handlers } = props;
23
+ const { threadId, handlers } = props;
27
24
  const onReport = handlers?.onReportThread;
28
- const onDelete = handlers?.onDeleteThread;
29
- return (_jsxs("div", { className: "flex items-center gap-3", children: [onReport && (_jsx(ModerationActionButton, { label: "Report thread", onAction: () => onReport(threadId) })), isAdmin && onDelete && (_jsx(ModerationActionButton, { label: "Delete thread", onAction: () => onDelete(threadId) }))] }));
25
+ return (_jsx("div", { className: "flex items-center gap-3", children: onReport && (_jsx(ModerationActionButton, { label: "Report thread", onAction: () => onReport(threadId) })) }));
30
26
  }
31
27
  //# sourceMappingURL=menus.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"menus.js","sourceRoot":"","sources":["../../src/ui/menus.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAGb,OAAO,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,gCAAgC,CAAC;AAExE,gFAAgF;AAChF,gFAAgF;AAChF,kEAAkE;AAClE,SAAS,OAAO,CAAC,KAAc;IAC7B,UAAU,CAAC,GAAG,EAAE;QACd,MAAM,KAAK,CAAC;IACd,CAAC,CAAC,CAAC;AACL,CAAC;AAED,SAAS,sBAAsB,CAAC,KAA8D;IAC5F,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,GAAG,KAAK,CAAC;IAClC,MAAM,MAAM,GAAG,cAAc,CAC3B,KAAK,IAAI,EAAE;QACT,MAAM,QAAQ,EAAE,CAAC;IACnB,CAAC,EACD,EAAE,OAAO,EAAE,OAAO,EAAE,CACrB,CAAC;IAEF,OAAO,CACL,KAAC,MAAM,IACL,IAAI,EAAC,QAAQ,EACb,OAAO,EAAC,MAAM,EACd,IAAI,EAAC,IAAI,EACT,SAAS,EAAC,iDAAiD,EAC3D,OAAO,EAAE,GAAG,EAAE,CAAC,KAAK,MAAM,CAAC,GAAG,EAAE,EAChC,OAAO,EAAE,MAAM,CAAC,OAAO,YAEtB,KAAK,GACC,CACV,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,KAI9B;IACC,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE,QAAQ,EAAE,GAAG,KAAK,CAAC;IAC/C,MAAM,QAAQ,GAAG,QAAQ,EAAE,eAAe,CAAC;IAC3C,MAAM,QAAQ,GAAG,QAAQ,EAAE,eAAe,CAAC;IAE3C,OAAO,CACL,eAAK,SAAS,EAAC,yBAAyB,aACrC,QAAQ,IAAI,KAAC,sBAAsB,IAAC,KAAK,EAAC,QAAQ,EAAC,QAAQ,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,SAAS,CAAC,GAAI,EAC1F,OAAO,IAAI,QAAQ,IAAI,CACtB,KAAC,sBAAsB,IAAC,KAAK,EAAC,QAAQ,EAAC,QAAQ,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,SAAS,CAAC,GAAI,CAC/E,IACG,CACP,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,KAI7B;IACC,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,QAAQ,EAAE,GAAG,KAAK,CAAC;IAC9C,MAAM,QAAQ,GAAG,QAAQ,EAAE,cAAc,CAAC;IAC1C,MAAM,QAAQ,GAAG,QAAQ,EAAE,cAAc,CAAC;IAE1C,OAAO,CACL,eAAK,SAAS,EAAC,yBAAyB,aACrC,QAAQ,IAAI,CACX,KAAC,sBAAsB,IAAC,KAAK,EAAC,eAAe,EAAC,QAAQ,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,GAAI,CACrF,EACA,OAAO,IAAI,QAAQ,IAAI,CACtB,KAAC,sBAAsB,IAAC,KAAK,EAAC,eAAe,EAAC,QAAQ,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,GAAI,CACrF,IACG,CACP,CAAC;AACJ,CAAC","sourcesContent":["'use client';\n\nimport type { ModerationHandlers } from '@ttt-productions/chat-core';\nimport { Button, useAsyncAction } from '@ttt-productions/ui-core/react';\n\n// The consumer's handler owns its failure UX. The action only keeps a rejection\n// from vanishing: it re-raises it on the global error channel, where monitoring\n// sees it — exactly where an unawaited rejection would have gone.\nfunction reraise(error: unknown) {\n setTimeout(() => {\n throw error;\n });\n}\n\nfunction ModerationActionButton(props: { label: string; onAction: () => void | Promise<void> }) {\n const { label, onAction } = props;\n const action = useAsyncAction(\n async () => {\n await onAction();\n },\n { onError: reraise },\n );\n\n return (\n <Button\n type=\"button\"\n variant=\"link\"\n size=\"sm\"\n className=\"h-auto p-0 text-xs opacity-70 hover:opacity-100\"\n onClick={() => void action.run()}\n pending={action.pending}\n >\n {label}\n </Button>\n );\n}\n\nexport function MessageActions(props: {\n messageId: string;\n isAdmin: boolean;\n handlers?: ModerationHandlers;\n}) {\n const { messageId, isAdmin, handlers } = props;\n const onReport = handlers?.onReportMessage;\n const onDelete = handlers?.onDeleteMessage;\n\n return (\n <div className=\"flex items-center gap-2\">\n {onReport && <ModerationActionButton label=\"Report\" onAction={() => onReport(messageId)} />}\n {isAdmin && onDelete && (\n <ModerationActionButton label=\"Delete\" onAction={() => onDelete(messageId)} />\n )}\n </div>\n );\n}\n\nexport function ThreadActions(props: {\n threadId: string;\n isAdmin: boolean;\n handlers?: ModerationHandlers;\n}) {\n const { threadId, isAdmin, handlers } = props;\n const onReport = handlers?.onReportThread;\n const onDelete = handlers?.onDeleteThread;\n\n return (\n <div className=\"flex items-center gap-3\">\n {onReport && (\n <ModerationActionButton label=\"Report thread\" onAction={() => onReport(threadId)} />\n )}\n {isAdmin && onDelete && (\n <ModerationActionButton label=\"Delete thread\" onAction={() => onDelete(threadId)} />\n )}\n </div>\n );\n}\n"]}
1
+ {"version":3,"file":"menus.js","sourceRoot":"","sources":["../../src/ui/menus.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAGb,OAAO,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,gCAAgC,CAAC;AAExE,oGAAoG;AACpG,SAAS,OAAO,CAAC,KAAc;IAC7B,UAAU,CAAC,GAAG,EAAE;QACd,MAAM,KAAK,CAAC;IACd,CAAC,CAAC,CAAC;AACL,CAAC;AAED,SAAS,sBAAsB,CAAC,KAA8D;IAC5F,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,GAAG,KAAK,CAAC;IAClC,MAAM,MAAM,GAAG,cAAc,CAC3B,KAAK,IAAI,EAAE;QACT,MAAM,QAAQ,EAAE,CAAC;IACnB,CAAC,EACD,EAAE,OAAO,EAAE,OAAO,EAAE,CACrB,CAAC;IAEF,OAAO,CACL,KAAC,MAAM,IACL,IAAI,EAAC,QAAQ,EACb,OAAO,EAAC,MAAM,EACd,IAAI,EAAC,IAAI,EACT,SAAS,EAAC,iDAAiD,EAC3D,OAAO,EAAE,GAAG,EAAE,CAAC,KAAK,MAAM,CAAC,GAAG,EAAE,EAChC,OAAO,EAAE,MAAM,CAAC,OAAO,YAEtB,KAAK,GACC,CACV,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,KAA2D;IACxF,MAAM,EAAE,SAAS,EAAE,QAAQ,EAAE,GAAG,KAAK,CAAC;IACtC,MAAM,QAAQ,GAAG,QAAQ,EAAE,eAAe,CAAC;IAE3C,OAAO,CACL,cAAK,SAAS,EAAC,yBAAyB,YACrC,QAAQ,IAAI,KAAC,sBAAsB,IAAC,KAAK,EAAC,QAAQ,EAAC,QAAQ,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,SAAS,CAAC,GAAI,GACvF,CACP,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,KAA0D;IACtF,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,GAAG,KAAK,CAAC;IACrC,MAAM,QAAQ,GAAG,QAAQ,EAAE,cAAc,CAAC;IAE1C,OAAO,CACL,cAAK,SAAS,EAAC,yBAAyB,YACrC,QAAQ,IAAI,CACX,KAAC,sBAAsB,IAAC,KAAK,EAAC,eAAe,EAAC,QAAQ,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,GAAI,CACrF,GACG,CACP,CAAC;AACJ,CAAC","sourcesContent":["'use client';\n\nimport type { ModerationHandlers } from '@ttt-productions/chat-core';\nimport { Button, useAsyncAction } from '@ttt-productions/ui-core/react';\n\n// Rethrown on a timer so the rejection reaches the global error channel, as an unawaited one would.\nfunction reraise(error: unknown) {\n setTimeout(() => {\n throw error;\n });\n}\n\nfunction ModerationActionButton(props: { label: string; onAction: () => void | Promise<void> }) {\n const { label, onAction } = props;\n const action = useAsyncAction(\n async () => {\n await onAction();\n },\n { onError: reraise },\n );\n\n return (\n <Button\n type=\"button\"\n variant=\"link\"\n size=\"sm\"\n className=\"h-auto p-0 text-xs opacity-70 hover:opacity-100\"\n onClick={() => void action.run()}\n pending={action.pending}\n >\n {label}\n </Button>\n );\n}\n\nexport function MessageActions(props: { messageId: string; handlers?: ModerationHandlers }) {\n const { messageId, handlers } = props;\n const onReport = handlers?.onReportMessage;\n\n return (\n <div className=\"flex items-center gap-2\">\n {onReport && <ModerationActionButton label=\"Report\" onAction={() => onReport(messageId)} />}\n </div>\n );\n}\n\nexport function ThreadActions(props: { threadId: string; handlers?: ModerationHandlers }) {\n const { threadId, handlers } = props;\n const onReport = handlers?.onReportThread;\n\n return (\n <div className=\"flex items-center gap-3\">\n {onReport && (\n <ModerationActionButton label=\"Report thread\" onAction={() => onReport(threadId)} />\n )}\n </div>\n );\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ttt-productions/chat-react",
3
- "version": "0.13.16",
3
+ "version": "0.13.18",
4
4
  "description": "Chat React UI, hooks, and Firebase-client adapter for TTT Productions apps (realtime newest window + infinite older pagination)",
5
5
  "repository": {
6
6
  "type": "git",
@@ -33,7 +33,7 @@
33
33
  },
34
34
  "peerDependencies": {
35
35
  "@tanstack/react-query": ">=5.0.0",
36
- "@ttt-productions/query-core": "^0.14.22",
36
+ "@ttt-productions/query-core": "^0.14.24",
37
37
  "firebase": ">=12.0.0",
38
38
  "lucide-react": "^1.30.0",
39
39
  "react": ">=19.0.0",
@@ -60,16 +60,16 @@
60
60
  }
61
61
  },
62
62
  "dependencies": {
63
- "@ttt-productions/chat-core": "^0.11.4",
64
- "@ttt-productions/chat-schemas": "^0.10.7",
65
- "@ttt-productions/realtime-core": "^0.1.4",
66
- "@ttt-productions/ui-core": "^0.11.34",
67
- "@ttt-productions/upload-ui": "^0.7.17",
68
- "@ttt-productions/mobile-core": "^0.9.5",
69
- "@ttt-productions/firebase-helpers": "^0.12.13"
63
+ "@ttt-productions/chat-core": "^0.11.6",
64
+ "@ttt-productions/chat-schemas": "^0.10.9",
65
+ "@ttt-productions/realtime-core": "^0.1.5",
66
+ "@ttt-productions/ui-core": "^0.11.35",
67
+ "@ttt-productions/upload-ui": "^0.7.19",
68
+ "@ttt-productions/mobile-core": "^0.9.6",
69
+ "@ttt-productions/firebase-helpers": "^0.12.16"
70
70
  },
71
71
  "devDependencies": {
72
- "@ttt-productions/query-core": "^0.14.22",
72
+ "@ttt-productions/query-core": "^0.14.24",
73
73
  "@types/react": "^19.0.0",
74
74
  "@types/react-dom": "^19.0.0",
75
75
  "lucide-react": "^1.30.0",