@ttt-productions/chat-react 0.13.9 → 0.13.10

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.
@@ -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,MAAM,MAAM,aAAa,GAAG;IAC1B;;;;;;;OAOG;IACH,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAExC;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IAEtB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF,wBAAgB,QAAQ,CAAC,KAAK,EAAE,aAAa,qBAuG5C"}
1
+ {"version":3,"file":"Composer.d.ts","sourceRoot":"","sources":["../../src/ui/Composer.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAc/B,MAAM,MAAM,aAAa,GAAG;IAC1B;;;;;;;OAOG;IACH,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAExC;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IAEtB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF,wBAAgB,QAAQ,CAAC,KAAK,EAAE,aAAa,qBAyG5C"}
@@ -4,7 +4,7 @@ import * as React from "react";
4
4
  import { Button, Textarea } from "@ttt-productions/ui-core/react";
5
5
  import { cn } from "@ttt-productions/ui-core";
6
6
  import { useOptionalLocalUploadGuard } from "@ttt-productions/upload-ui/react/guard";
7
- import { CHAT_MESSAGE_TEXT_MAX_LENGTH } from "@ttt-productions/chat-schemas";
7
+ import { CHAT_MESSAGE_TEXT_INPUT, CHAT_MESSAGE_TEXT_MAX_LENGTH, judgeChatMessageText, } from "@ttt-productions/chat-schemas";
8
8
  function genId() {
9
9
  return `${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`;
10
10
  }
@@ -28,16 +28,17 @@ export function Composer(props) {
28
28
  return;
29
29
  ref.current?.focus();
30
30
  }, [autoFocus]);
31
+ // The same judgement the Worker applies: a blank text is never sent, and what is sent is trimmed.
32
+ const judged = judgeChatMessageText(text);
31
33
  const send = async () => {
32
- const v = text.trim();
33
- if (!v)
34
+ if (!judged.ok)
34
35
  return;
35
36
  setIsSending(true);
36
37
  setSendError(null);
37
38
  const sendGuardId = `chat-send-${genId()}`;
38
39
  navigationGuard?.registerUpload(sendGuardId);
39
40
  try {
40
- await onSend(text);
41
+ await onSend(judged.text);
41
42
  setText("");
42
43
  }
43
44
  catch (err) {
@@ -54,7 +55,7 @@ export function Composer(props) {
54
55
  }
55
56
  };
56
57
  const isDisabled = disabled || isSending;
57
- return (_jsxs("div", { className: "chat-composer", children: [sendError && (_jsx("div", { className: "px-1 pb-1 text-sm text-destructive", role: "alert", children: sendError })), _jsxs("div", { className: "flex items-end gap-2", children: [_jsx(Textarea, { ref: ref, value: text, maxLength: CHAT_MESSAGE_TEXT_MAX_LENGTH, "aria-describedby": text.length > 0 ? counterId : undefined, onChange: (e) => {
58
+ return (_jsxs("div", { className: "chat-composer", children: [sendError && (_jsx("div", { className: "px-1 pb-1 text-sm text-destructive", role: "alert", children: sendError })), _jsxs("div", { className: "flex items-end gap-2", children: [_jsx(Textarea, { ref: ref, value: text, inputFormat: CHAT_MESSAGE_TEXT_INPUT, "aria-describedby": text.length > 0 ? counterId : undefined, onChange: (e) => {
58
59
  setText(e.target.value);
59
60
  if (sendError)
60
61
  setSendError(null);
@@ -66,6 +67,6 @@ export function Composer(props) {
66
67
  e.preventDefault();
67
68
  send();
68
69
  }
69
- } }), _jsx(Button, { type: "button", variant: "default", disabled: isDisabled || !text.trim(), pending: isSending, onClick: send, children: "Send" })] }), text.length > 0 && (_jsxs("div", { id: counterId, className: "px-1 pt-1 text-xs text-right text-muted-foreground", children: [text.length, "/", CHAT_MESSAGE_TEXT_MAX_LENGTH] }))] }));
70
+ } }), _jsx(Button, { type: "button", variant: "default", disabled: isDisabled || !judged.ok, pending: isSending, onClick: send, children: "Send" })] }), text.length > 0 && (_jsxs("div", { id: counterId, className: "px-1 pt-1 text-xs text-right text-muted-foreground", children: [text.length, "/", CHAT_MESSAGE_TEXT_MAX_LENGTH] }))] }));
70
71
  }
71
72
  //# sourceMappingURL=Composer.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"Composer.js","sourceRoot":"","sources":["../../src/ui/Composer.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAEb,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,gCAAgC,CAAC;AAClE,OAAO,EAAE,EAAE,EAAE,MAAM,0BAA0B,CAAC;AAC9C,OAAO,EAAE,2BAA2B,EAAE,MAAM,wCAAwC,CAAC;AACrF,OAAO,EAAE,4BAA4B,EAAE,MAAM,+BAA+B,CAAC;AAE7E,SAAS,KAAK;IACZ,OAAO,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,EAAE,CAAC;AAC5E,CAAC;AAyBD,MAAM,UAAU,QAAQ,CAAC,KAAoB;IAC3C,MAAM,EACJ,MAAM,EACN,QAAQ,EACR,QAAQ,EACR,SAAS,GAAG,KAAK,EACjB,WAAW,GAAG,mBAAmB,GAClC,GAAG,KAAK,CAAC;IAEV,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,GAAG,KAAK,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;IAC3C,MAAM,CAAC,SAAS,EAAE,YAAY,CAAC,GAAG,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;IACxD,MAAM,CAAC,SAAS,EAAE,YAAY,CAAC,GAAG,KAAK,CAAC,QAAQ,CAAgB,IAAI,CAAC,CAAC;IACtE,MAAM,GAAG,GAAG,KAAK,CAAC,MAAM,CAAsB,IAAI,CAAC,CAAC;IACpD,6EAA6E;IAC7E,6EAA6E;IAC7E,+EAA+E;IAC/E,8EAA8E;IAC9E,yEAAyE;IACzE,8EAA8E;IAC9E,MAAM,eAAe,GAAG,2BAA2B,EAAE,CAAC;IACtD,MAAM,SAAS,GAAG,KAAK,CAAC,KAAK,EAAE,CAAC;IAEhC,+DAA+D;IAC/D,KAAK,CAAC,SAAS,CAAC,GAAG,EAAE;QACnB,IAAI,CAAC,SAAS;YAAE,OAAO;QACvB,GAAG,CAAC,OAAO,EAAE,KAAK,EAAE,CAAC;IACvB,CAAC,EAAE,CAAC,SAAS,CAAC,CAAC,CAAC;IAEhB,MAAM,IAAI,GAAG,KAAK,IAAI,EAAE;QACtB,MAAM,CAAC,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;QACtB,IAAI,CAAC,CAAC;YAAE,OAAO;QAEf,YAAY,CAAC,IAAI,CAAC,CAAC;QACnB,YAAY,CAAC,IAAI,CAAC,CAAC;QACnB,MAAM,WAAW,GAAG,aAAa,KAAK,EAAE,EAAE,CAAC;QAC3C,eAAe,EAAE,cAAc,CAAC,WAAW,CAAC,CAAC;QAE7C,IAAI,CAAC;YACH,MAAM,MAAM,CAAC,IAAI,CAAC,CAAC;YACnB,OAAO,CAAC,EAAE,CAAC,CAAC;QACd,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,+EAA+E;YAC/E,iFAAiF;YACjF,8EAA8E;YAC9E,yCAAyC;YACzC,OAAO,CAAC,KAAK,CAAC,yBAAyB,EAAE,GAAG,CAAC,CAAC;YAC9C,YAAY,CAAC,qDAAqD,CAAC,CAAC;QACtE,CAAC;gBAAS,CAAC;YACT,eAAe,EAAE,gBAAgB,CAAC,WAAW,CAAC,CAAC;YAC/C,YAAY,CAAC,KAAK,CAAC,CAAC;QACtB,CAAC;IACH,CAAC,CAAC;IAEF,MAAM,UAAU,GAAG,QAAQ,IAAI,SAAS,CAAC;IAEzC,OAAO,CACL,eAAK,SAAS,EAAC,eAAe,aAC3B,SAAS,IAAI,CACZ,cAAK,SAAS,EAAC,oCAAoC,EAAC,IAAI,EAAC,OAAO,YAC7D,SAAS,GACN,CACP,EAED,eAAK,SAAS,EAAC,sBAAsB,aACnC,KAAC,QAAQ,IACP,GAAG,EAAE,GAAG,EACR,KAAK,EAAE,IAAI,EACX,SAAS,EAAE,4BAA4B,sBACrB,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,EACzD,QAAQ,EAAE,CAAC,CAAC,EAAE,EAAE;4BACd,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;4BACxB,IAAI,SAAS;gCAAE,YAAY,CAAC,IAAI,CAAC,CAAC;4BAClC,+EAA+E;4BAC/E,IAAI,QAAQ,IAAI,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC;gCAAE,QAAQ,EAAE,CAAC;wBAC/D,CAAC,EACD,WAAW,EAAE,WAAW,EACxB,QAAQ,EAAE,UAAU,EACpB,IAAI,EAAE,CAAC,EACP,SAAS,EAAE,EAAE,CAAC,0BAA0B,CAAC,EACzC,SAAS,EAAE,CAAC,CAAC,EAAE,EAAE;4BACf,IAAI,CAAC,CAAC,GAAG,KAAK,OAAO,IAAI,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC;gCACrC,CAAC,CAAC,cAAc,EAAE,CAAC;gCACnB,IAAI,EAAE,CAAC;4BACT,CAAC;wBACH,CAAC,GACD,EACF,KAAC,MAAM,IACL,IAAI,EAAC,QAAQ,EACb,OAAO,EAAC,SAAS,EACjB,QAAQ,EAAE,UAAU,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,EACpC,OAAO,EAAE,SAAS,EAClB,OAAO,EAAE,IAAI,qBAGN,IACL,EACL,IAAI,CAAC,MAAM,GAAG,CAAC,IAAI,CAClB,eAAK,EAAE,EAAE,SAAS,EAAE,SAAS,EAAC,oDAAoD,aAC/E,IAAI,CAAC,MAAM,OAAG,4BAA4B,IACvC,CACP,IACG,CACP,CAAC;AACJ,CAAC","sourcesContent":["\"use client\";\n\nimport * as React from \"react\";\nimport { Button, Textarea } from \"@ttt-productions/ui-core/react\";\nimport { cn } from \"@ttt-productions/ui-core\";\nimport { useOptionalLocalUploadGuard } from \"@ttt-productions/upload-ui/react/guard\";\nimport { CHAT_MESSAGE_TEXT_MAX_LENGTH } from \"@ttt-productions/chat-schemas\";\n\nfunction genId(): string {\n return `${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`;\n}\n\nexport type ComposerProps = {\n /**\n * Text send — the composer's ONE argument. The composer is PLAIN-TEXT-ONLY:\n * files are shared through the conversation's Conversation Files surface,\n * never through the message timeline, so there is no attachment path here;\n * message text carries no inline token grammar, so what the user typed is what\n * is sent; and there is no reply-target picker, so a send never names another\n * message (DJ ruling 2026-07-29).\n */\n onSend: (text: string) => Promise<void>;\n\n /**\n * Signal the local user is typing (R14). Called as the user edits the textarea; the\n * realtime transport coalesces to ≤1 frame per ~2s, so calling per keystroke is safe.\n * Optional — absent on the firestore transport (no typing broadcast).\n */\n onTyping?: () => void;\n\n disabled?: boolean;\n autoFocus?: boolean;\n placeholder?: string;\n};\n\nexport function Composer(props: ComposerProps) {\n const {\n onSend,\n onTyping,\n disabled,\n autoFocus = false,\n placeholder = \"Type a message...\",\n } = props;\n\n const [text, setText] = React.useState(\"\");\n const [isSending, setIsSending] = React.useState(false);\n const [sendError, setSendError] = React.useState<string | null>(null);\n const ref = React.useRef<HTMLTextAreaElement>(null);\n // Navigation guard for the in-flight send: a send that has left the composer\n // but not yet committed is killable by navigation/sign-out with zero warning\n // (observed live: the callable aborted, the message silently never delivered).\n // Registering with the app's one guarded-navigation system makes beforeunload\n // fire on hard navs and lets GuardedLink-style navigation confirm first.\n // Optional accessor — consumers without the provider degrade to old behavior.\n const navigationGuard = useOptionalLocalUploadGuard();\n const counterId = React.useId();\n\n // focus stability: never steal focus unless explicitly enabled\n React.useEffect(() => {\n if (!autoFocus) return;\n ref.current?.focus();\n }, [autoFocus]);\n\n const send = async () => {\n const v = text.trim();\n if (!v) return;\n\n setIsSending(true);\n setSendError(null);\n const sendGuardId = `chat-send-${genId()}`;\n navigationGuard?.registerUpload(sendGuardId);\n\n try {\n await onSend(text);\n setText(\"\");\n } catch (err) {\n // C-B8: a failed send (e.g. the realtime socket was closed) must NOT clear the\n // user's text — `setText(\"\")` only runs on the success path above — and must NOT\n // re-throw into the click handler (an unhandled rejection). Surface it so the\n // user can retry with their text intact.\n console.error(\"[Composer] Send failed:\", err);\n setSendError(\"Couldn't send. Check your connection and try again.\");\n } finally {\n navigationGuard?.unregisterUpload(sendGuardId);\n setIsSending(false);\n }\n };\n\n const isDisabled = disabled || isSending;\n\n return (\n <div className=\"chat-composer\">\n {sendError && (\n <div className=\"px-1 pb-1 text-sm text-destructive\" role=\"alert\">\n {sendError}\n </div>\n )}\n\n <div className=\"flex items-end gap-2\">\n <Textarea\n ref={ref}\n value={text}\n maxLength={CHAT_MESSAGE_TEXT_MAX_LENGTH}\n aria-describedby={text.length > 0 ? counterId : undefined}\n onChange={(e) => {\n setText(e.target.value);\n if (sendError) setSendError(null);\n // R14: broadcast a typing signal (transport-coalesced) while there is content.\n if (onTyping && e.target.value.trim().length > 0) onTyping();\n }}\n placeholder={placeholder}\n disabled={isDisabled}\n rows={1}\n className={cn(\"min-h-[40px] resize-none\")}\n onKeyDown={(e) => {\n if (e.key === \"Enter\" && !e.shiftKey) {\n e.preventDefault();\n send();\n }\n }}\n />\n <Button\n type=\"button\"\n variant=\"default\"\n disabled={isDisabled || !text.trim()}\n pending={isSending}\n onClick={send}\n >\n Send\n </Button>\n </div>\n {text.length > 0 && (\n <div id={counterId} className=\"px-1 pt-1 text-xs text-right text-muted-foreground\">\n {text.length}/{CHAT_MESSAGE_TEXT_MAX_LENGTH}\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 { useOptionalLocalUploadGuard } from \"@ttt-productions/upload-ui/react/guard\";\nimport {\n CHAT_MESSAGE_TEXT_INPUT,\n CHAT_MESSAGE_TEXT_MAX_LENGTH,\n judgeChatMessageText,\n} from \"@ttt-productions/chat-schemas\";\n\nfunction genId(): string {\n return `${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`;\n}\n\nexport type ComposerProps = {\n /**\n * Text send — the composer's ONE argument. The composer is PLAIN-TEXT-ONLY:\n * files are shared through the conversation's Conversation Files surface,\n * never through the message timeline, so there is no attachment path here;\n * message text carries no inline token grammar, so what the user typed is what\n * is sent; and there is no reply-target picker, so a send never names another\n * message (DJ ruling 2026-07-29).\n */\n onSend: (text: string) => Promise<void>;\n\n /**\n * Signal the local user is typing (R14). Called as the user edits the textarea; the\n * realtime transport coalesces to ≤1 frame per ~2s, so calling per keystroke is safe.\n * Optional — absent on the firestore transport (no typing broadcast).\n */\n onTyping?: () => void;\n\n disabled?: boolean;\n autoFocus?: boolean;\n placeholder?: string;\n};\n\nexport function Composer(props: ComposerProps) {\n const {\n onSend,\n onTyping,\n disabled,\n autoFocus = false,\n placeholder = \"Type a message...\",\n } = props;\n\n const [text, setText] = React.useState(\"\");\n const [isSending, setIsSending] = React.useState(false);\n const [sendError, setSendError] = React.useState<string | null>(null);\n const ref = React.useRef<HTMLTextAreaElement>(null);\n // Navigation guard for the in-flight send: a send that has left the composer\n // but not yet committed is killable by navigation/sign-out with zero warning\n // (observed live: the callable aborted, the message silently never delivered).\n // Registering with the app's one guarded-navigation system makes beforeunload\n // fire on hard navs and lets GuardedLink-style navigation confirm first.\n // Optional accessor — consumers without the provider degrade to old behavior.\n const navigationGuard = useOptionalLocalUploadGuard();\n const counterId = React.useId();\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 // The same judgement the Worker applies: a blank text is never sent, and what is sent is trimmed.\n const judged = judgeChatMessageText(text);\n\n const send = async () => {\n if (!judged.ok) return;\n\n setIsSending(true);\n setSendError(null);\n const sendGuardId = `chat-send-${genId()}`;\n navigationGuard?.registerUpload(sendGuardId);\n\n try {\n await onSend(judged.text);\n setText(\"\");\n } catch (err) {\n // C-B8: a failed send (e.g. the realtime socket was closed) must NOT clear the\n // user's text — `setText(\"\")` only runs on the success path above — and must NOT\n // re-throw into the click handler (an unhandled rejection). Surface it so the\n // user can retry with their text intact.\n console.error(\"[Composer] Send failed:\", err);\n setSendError(\"Couldn't send. Check your connection and try again.\");\n } finally {\n navigationGuard?.unregisterUpload(sendGuardId);\n setIsSending(false);\n }\n };\n\n const isDisabled = disabled || isSending;\n\n return (\n <div className=\"chat-composer\">\n {sendError && (\n <div className=\"px-1 pb-1 text-sm text-destructive\" role=\"alert\">\n {sendError}\n </div>\n )}\n\n <div className=\"flex items-end gap-2\">\n <Textarea\n ref={ref}\n value={text}\n inputFormat={CHAT_MESSAGE_TEXT_INPUT}\n aria-describedby={text.length > 0 ? counterId : undefined}\n onChange={(e) => {\n setText(e.target.value);\n if (sendError) setSendError(null);\n // R14: broadcast a typing signal (transport-coalesced) while there is content.\n if (onTyping && e.target.value.trim().length > 0) onTyping();\n }}\n placeholder={placeholder}\n disabled={isDisabled}\n rows={1}\n className={cn(\"min-h-[40px] resize-none\")}\n onKeyDown={(e) => {\n if (e.key === \"Enter\" && !e.shiftKey) {\n e.preventDefault();\n send();\n }\n }}\n />\n <Button\n type=\"button\"\n variant=\"default\"\n disabled={isDisabled || !judged.ok}\n pending={isSending}\n onClick={send}\n >\n Send\n </Button>\n </div>\n {text.length > 0 && (\n <div id={counterId} className=\"px-1 pt-1 text-xs text-right text-muted-foreground\">\n {text.length}/{CHAT_MESSAGE_TEXT_MAX_LENGTH}\n </div>\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.9",
3
+ "version": "0.13.10",
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",
@@ -61,10 +61,10 @@
61
61
  },
62
62
  "dependencies": {
63
63
  "@ttt-productions/chat-core": "^0.11.2",
64
- "@ttt-productions/chat-schemas": "^0.10.4",
64
+ "@ttt-productions/chat-schemas": "^0.10.5",
65
65
  "@ttt-productions/realtime-core": "^0.1.3",
66
- "@ttt-productions/ui-core": "^0.11.20",
67
- "@ttt-productions/upload-ui": "^0.7.12",
66
+ "@ttt-productions/ui-core": "^0.11.21",
67
+ "@ttt-productions/upload-ui": "^0.7.13",
68
68
  "@ttt-productions/mobile-core": "^0.9.3",
69
69
  "@ttt-productions/firebase-helpers": "^0.12.10"
70
70
  },