tempest-react-sdk 0.63.0 → 0.64.0

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 (51) hide show
  1. package/README.md +1 -1
  2. package/dist/components/Chat/Chat.cjs +1 -1
  3. package/dist/components/Chat/Chat.cjs.map +1 -1
  4. package/dist/components/Chat/Chat.js +71 -96
  5. package/dist/components/Chat/Chat.js.map +1 -1
  6. package/dist/components/Chat/Chat.module.cjs +1 -1
  7. package/dist/components/Chat/Chat.module.cjs.map +1 -1
  8. package/dist/components/Chat/Chat.module.js +30 -8
  9. package/dist/components/Chat/Chat.module.js.map +1 -1
  10. package/dist/components/Chat/ChatActionsMenu.cjs +2 -0
  11. package/dist/components/Chat/ChatActionsMenu.cjs.map +1 -0
  12. package/dist/components/Chat/ChatActionsMenu.js +14 -0
  13. package/dist/components/Chat/ChatActionsMenu.js.map +1 -0
  14. package/dist/components/Chat/ChatAttachments.cjs +2 -0
  15. package/dist/components/Chat/ChatAttachments.cjs.map +1 -0
  16. package/dist/components/Chat/ChatAttachments.js +80 -0
  17. package/dist/components/Chat/ChatAttachments.js.map +1 -0
  18. package/dist/components/Chat/ChatBubble.cjs +2 -0
  19. package/dist/components/Chat/ChatBubble.cjs.map +1 -0
  20. package/dist/components/Chat/ChatBubble.js +155 -0
  21. package/dist/components/Chat/ChatBubble.js.map +1 -0
  22. package/dist/components/Chat/chat-groups.cjs +1 -1
  23. package/dist/components/Chat/chat-groups.cjs.map +1 -1
  24. package/dist/components/Chat/chat-groups.js +42 -2
  25. package/dist/components/Chat/chat-groups.js.map +1 -1
  26. package/dist/components/Chat/chat-receipt.cjs +2 -0
  27. package/dist/components/Chat/chat-receipt.cjs.map +1 -0
  28. package/dist/components/Chat/chat-receipt.js +14 -0
  29. package/dist/components/Chat/chat-receipt.js.map +1 -0
  30. package/dist/components/ContextMenu/ContextMenu.cjs +1 -1
  31. package/dist/components/ContextMenu/ContextMenu.cjs.map +1 -1
  32. package/dist/components/ContextMenu/ContextMenu.js +100 -48
  33. package/dist/components/ContextMenu/ContextMenu.js.map +1 -1
  34. package/dist/components/ContextMenu/ContextMenu.module.cjs.map +1 -1
  35. package/dist/components/ContextMenu/ContextMenu.module.js.map +1 -1
  36. package/dist/styles/advanced.css +2 -0
  37. package/dist/styles/chat.css +28 -0
  38. package/dist/styles/component/Chat.css +28 -0
  39. package/dist/styles/component/ContextMenu.css +2 -0
  40. package/dist/styles/manifest.json +12 -0
  41. package/dist/styles.css +1 -1
  42. package/dist/tempest-react-sdk.cjs +1 -1
  43. package/dist/tempest-react-sdk.d.ts +253 -10
  44. package/dist/tempest-react-sdk.js +169 -167
  45. package/dist/theme/create-theme.cjs +3 -3
  46. package/dist/theme/create-theme.cjs.map +1 -1
  47. package/dist/theme/create-theme.js +109 -57
  48. package/dist/theme/create-theme.js.map +1 -1
  49. package/loader/css-loader-hooks.mjs +37 -0
  50. package/loader/css-loader.mjs +46 -0
  51. package/package.json +3 -1
@@ -0,0 +1,80 @@
1
+ import { formatBytes as e } from "../../utils/numbers.js";
2
+ import { VideoPlayer as t } from "../VideoPlayer/VideoPlayer.js";
3
+ import { Lightbox as n } from "../Lightbox/Lightbox.js";
4
+ import r from "./Chat.module.js";
5
+ import { AudioPlayer as i } from "../AudioPlayer/AudioPlayer.js";
6
+ import { useState as a } from "react";
7
+ import { jsx as o, jsxs as s } from "react/jsx-runtime";
8
+ //#region src/components/Chat/ChatAttachments.tsx
9
+ var c = Array.from({ length: 28 }, () => .25);
10
+ function l(e) {
11
+ let t = Math.round(e / 1e3), n = Math.floor(t / 60), r = t % 60;
12
+ return `${n}:${String(r).padStart(2, "0")}`;
13
+ }
14
+ function u({ attachments: u, strings: d }) {
15
+ let f = u.filter((e) => e.kind === "image"), [p, m] = a(null);
16
+ return /* @__PURE__ */ s("div", {
17
+ className: r.attachments,
18
+ children: [u.map((n, a) => {
19
+ let u = `${n.kind}-${n.url}-${a}`;
20
+ if (n.kind === "image") {
21
+ let e = f.indexOf(n);
22
+ return /* @__PURE__ */ o("button", {
23
+ type: "button",
24
+ className: r.imageAttachment,
25
+ onClick: () => m(e),
26
+ children: /* @__PURE__ */ o("img", {
27
+ src: n.thumbnailUrl ?? n.url,
28
+ alt: n.alt ?? n.name ?? "",
29
+ loading: "lazy"
30
+ })
31
+ }, u);
32
+ }
33
+ return n.kind === "video" ? /* @__PURE__ */ o(t, {
34
+ src: n.url,
35
+ durationMs: n.durationMs,
36
+ poster: n.thumbnailUrl,
37
+ className: r.videoAttachment
38
+ }, u) : n.kind === "voice" ? /* @__PURE__ */ s("div", {
39
+ className: r.voiceAttachment,
40
+ children: [/* @__PURE__ */ o("div", {
41
+ className: r.waveform,
42
+ "aria-hidden": "true",
43
+ children: (n.waveform ?? c).map((e, t) => /* @__PURE__ */ o("span", { style: { height: `${Math.max(8, Math.min(100, e * 100))}%` } }, t))
44
+ }), /* @__PURE__ */ o(i, {
45
+ src: n.url,
46
+ durationMs: n.durationMs,
47
+ "aria-label": d.voiceNote
48
+ })]
49
+ }, u) : n.kind === "audio" ? /* @__PURE__ */ o(i, {
50
+ src: n.url,
51
+ durationMs: n.durationMs
52
+ }, u) : /* @__PURE__ */ s("a", {
53
+ className: r.fileAttachment,
54
+ href: n.url,
55
+ download: n.name ?? !0,
56
+ children: [/* @__PURE__ */ o("span", {
57
+ className: r.fileName,
58
+ children: n.name ?? d.download
59
+ }), /* @__PURE__ */ o("span", {
60
+ className: r.fileMeta,
61
+ children: [n.sizeBytes === void 0 ? null : e(n.sizeBytes), n.durationMs === void 0 ? null : l(n.durationMs)].filter(Boolean).join(" · ")
62
+ })]
63
+ }, u);
64
+ }), f.length > 0 && /* @__PURE__ */ o(n, {
65
+ items: f.map((e) => ({
66
+ src: e.url,
67
+ alt: e.alt ?? e.name ?? "",
68
+ thumbnail: e.thumbnailUrl
69
+ })),
70
+ open: p !== null,
71
+ index: p ?? 0,
72
+ onIndexChange: m,
73
+ onClose: () => m(null)
74
+ })]
75
+ });
76
+ }
77
+ //#endregion
78
+ export { u as ChatAttachments };
79
+
80
+ //# sourceMappingURL=ChatAttachments.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ChatAttachments.js","names":[],"sources":["../../../src/components/Chat/ChatAttachments.tsx"],"sourcesContent":["import { useState } from \"react\";\n\nimport { formatBytes } from \"@/utils/numbers\";\n\nimport { AudioPlayer } from \"../AudioPlayer\";\nimport { Lightbox } from \"../Lightbox\";\nimport { VideoPlayer } from \"../VideoPlayer\";\nimport type { ChatAttachment } from \"./chat-groups\";\nimport styles from \"./Chat.module.css\";\n\n/** Strings this view needs, passed in so the locale table stays in one file. */\nexport interface ChatAttachmentStrings {\n voiceNote: string;\n download: string;\n}\n\nexport interface ChatAttachmentsProps {\n /** What this message carries. */\n attachments: readonly ChatAttachment[];\n /** Labels, from `chatStrings(locale)`. */\n strings: ChatAttachmentStrings;\n}\n\n/** Bars a voice note draws when the app gave no peaks. */\nconst FLAT_WAVEFORM = Array.from({ length: 28 }, () => 0.25);\n\n/** `1:04` from milliseconds — a duration, never a clock time. */\nfunction durationLabel(durationMs: number): string {\n const total = Math.round(durationMs / 1000);\n const minutes = Math.floor(total / 60);\n const seconds = total % 60;\n return `${minutes}:${String(seconds).padStart(2, \"0\")}`;\n}\n\n/**\n * The media half of a bubble.\n *\n * Kept in its own module, and reached through `lazy()` from the thread, because\n * it pulls three of the heaviest components in the SDK — `Lightbox`,\n * `VideoPlayer`, `AudioPlayer` — and a support thread of plain text should not\n * pay for a photo viewer it never opens. The thread renders it only for messages\n * that actually carry an attachment, so that chunk is requested the first time\n * one appears.\n *\n * Nothing here is new UI: an image opens the SDK's lightbox, a video and an audio\n * clip use the SDK's players, and a file is a download row. That composition is\n * the whole point — every app was writing it again, the same way.\n *\n * @param props - The attachments and the locale strings.\n * @returns The rendered attachment list.\n */\nexport function ChatAttachments({ attachments, strings }: ChatAttachmentsProps) {\n const images = attachments.filter((attachment) => attachment.kind === \"image\");\n const [lightboxIndex, setLightboxIndex] = useState<number | null>(null);\n\n return (\n <div className={styles.attachments}>\n {attachments.map((attachment, index) => {\n const key = `${attachment.kind}-${attachment.url}-${index}`;\n if (attachment.kind === \"image\") {\n const position = images.indexOf(attachment);\n return (\n <button\n key={key}\n type=\"button\"\n className={styles.imageAttachment}\n onClick={() => setLightboxIndex(position)}\n >\n <img\n src={attachment.thumbnailUrl ?? attachment.url}\n alt={attachment.alt ?? attachment.name ?? \"\"}\n loading=\"lazy\"\n />\n </button>\n );\n }\n if (attachment.kind === \"video\") {\n return (\n <VideoPlayer\n key={key}\n src={attachment.url}\n durationMs={attachment.durationMs}\n poster={attachment.thumbnailUrl}\n className={styles.videoAttachment}\n />\n );\n }\n if (attachment.kind === \"voice\") {\n return (\n <div key={key} className={styles.voiceAttachment}>\n <div className={styles.waveform} aria-hidden=\"true\">\n {(attachment.waveform ?? FLAT_WAVEFORM).map((peak, bar) => (\n <span\n key={bar}\n style={{\n height: `${Math.max(8, Math.min(100, peak * 100))}%`,\n }}\n />\n ))}\n </div>\n <AudioPlayer\n src={attachment.url}\n durationMs={attachment.durationMs}\n aria-label={strings.voiceNote}\n />\n </div>\n );\n }\n if (attachment.kind === \"audio\") {\n return (\n <AudioPlayer\n key={key}\n src={attachment.url}\n durationMs={attachment.durationMs}\n />\n );\n }\n return (\n <a\n key={key}\n className={styles.fileAttachment}\n href={attachment.url}\n download={attachment.name ?? true}\n >\n <span className={styles.fileName}>\n {attachment.name ?? strings.download}\n </span>\n <span className={styles.fileMeta}>\n {[\n attachment.sizeBytes !== undefined\n ? formatBytes(attachment.sizeBytes)\n : null,\n attachment.durationMs !== undefined\n ? durationLabel(attachment.durationMs)\n : null,\n ]\n .filter(Boolean)\n .join(\" · \")}\n </span>\n </a>\n );\n })}\n\n {images.length > 0 && (\n <Lightbox\n items={images.map((image) => ({\n src: image.url,\n alt: image.alt ?? image.name ?? \"\",\n thumbnail: image.thumbnailUrl,\n }))}\n open={lightboxIndex !== null}\n index={lightboxIndex ?? 0}\n onIndexChange={setLightboxIndex}\n onClose={() => setLightboxIndex(null)}\n />\n )}\n </div>\n );\n}\n"],"mappings":";;;;;;;;AAwBA,IAAM,IAAgB,MAAM,KAAK,EAAE,QAAQ,GAAG,SAAS,GAAI;AAG3D,SAAS,EAAc,GAA4B;CAC/C,IAAM,IAAQ,KAAK,MAAM,IAAa,GAAI,GACpC,IAAU,KAAK,MAAM,IAAQ,EAAE,GAC/B,IAAU,IAAQ;CACxB,OAAO,GAAG,EAAQ,GAAG,OAAO,CAAO,CAAC,CAAC,SAAS,GAAG,GAAG;AACxD;AAmBA,SAAgB,EAAgB,EAAE,gBAAa,cAAiC;CAC5E,IAAM,IAAS,EAAY,QAAQ,MAAe,EAAW,SAAS,OAAO,GACvE,CAAC,GAAe,KAAoB,EAAwB,IAAI;CAEtE,OACI,kBAAC,OAAD;EAAK,WAAW,EAAO;EAAvB,UAAA,CACK,EAAY,KAAK,GAAY,MAAU;GACpC,IAAM,IAAM,GAAG,EAAW,KAAK,GAAG,EAAW,IAAI,GAAG;GACpD,IAAI,EAAW,SAAS,SAAS;IAC7B,IAAM,IAAW,EAAO,QAAQ,CAAU;IAC1C,OACI,kBAAC,UAAD;KAEI,MAAK;KACL,WAAW,EAAO;KAClB,eAAe,EAAiB,CAAQ;KAExC,UAAA,kBAAC,OAAD;MACI,KAAK,EAAW,gBAAgB,EAAW;MAC3C,KAAK,EAAW,OAAO,EAAW,QAAQ;MAC1C,SAAQ;KACX,CAAA;IACG,GAVC,CAUD;GAEhB;GA0CA,OAzCI,EAAW,SAAS,UAEhB,kBAAC,GAAD;IAEI,KAAK,EAAW;IAChB,YAAY,EAAW;IACvB,QAAQ,EAAW;IACnB,WAAW,EAAO;GACrB,GALQ,CAKR,IAGL,EAAW,SAAS,UAEhB,kBAAC,OAAD;IAAe,WAAW,EAAO;IAAjC,UAAA,CACI,kBAAC,OAAD;KAAK,WAAW,EAAO;KAAU,eAAY;KACvC,WAAA,EAAW,YAAY,EAAA,CAAe,KAAK,GAAM,MAC/C,kBAAC,QAAD,EAEI,OAAO,EACH,QAAQ,GAAG,KAAK,IAAI,GAAG,KAAK,IAAI,KAAK,IAAO,GAAG,CAAC,EAAE,GACtD,EACH,GAJQ,CAIR,CACJ;IACA,CAAA,GACL,kBAAC,GAAD;KACI,KAAK,EAAW;KAChB,YAAY,EAAW;KACvB,cAAY,EAAQ;IACvB,CAAA,CACA;GAhBK,GAAA,CAgBL,IAGT,EAAW,SAAS,UAEhB,kBAAC,GAAD;IAEI,KAAK,EAAW;IAChB,YAAY,EAAW;GAC1B,GAHQ,CAGR,IAIL,kBAAC,KAAD;IAEI,WAAW,EAAO;IAClB,MAAM,EAAW;IACjB,UAAU,EAAW,QAAQ;IAJjC,UAAA,CAMI,kBAAC,QAAD;KAAM,WAAW,EAAO;KACnB,UAAA,EAAW,QAAQ,EAAQ;IAC1B,CAAA,GACN,kBAAC,QAAD;KAAM,WAAW,EAAO;KACnB,UAAA,CACG,EAAW,cAAc,KAAA,IAEnB,OADA,EAAY,EAAW,SAAS,GAEtC,EAAW,eAAe,KAAA,IAEpB,OADA,EAAc,EAAW,UAAU,CAE7C,CAAC,CACI,OAAO,OAAO,CAAC,CACf,KAAK,KAAK;IACb,CAAA,CACP;GApBM,GAAA,CAoBN;EAEX,CAAC,GAEA,EAAO,SAAS,KACb,kBAAC,GAAD;GACI,OAAO,EAAO,KAAK,OAAW;IAC1B,KAAK,EAAM;IACX,KAAK,EAAM,OAAO,EAAM,QAAQ;IAChC,WAAW,EAAM;GACrB,EAAE;GACF,MAAM,MAAkB;GACxB,OAAO,KAAiB;GACxB,eAAe;GACf,eAAe,EAAiB,IAAI;EACvC,CAAA,CAEJ;;AAEb"}
@@ -0,0 +1,2 @@
1
+ const e=require("../../utils/cn.cjs"),t=require("../VisuallyHidden/VisuallyHidden.cjs"),n=require("./chat-groups.cjs"),r=require("./chat-receipt.cjs"),i=require("./Chat.module.cjs");let a=require("react"),o=require("react/jsx-runtime");var s=(0,a.lazy)(()=>Promise.resolve().then(()=>require("./ChatAttachments.cjs")).then(e=>({default:e.ChatAttachments}))),c=(0,a.lazy)(()=>Promise.resolve().then(()=>require("./ChatActionsMenu.cjs")).then(e=>({default:e.ChatActionsMenu}))),l={sending:`◌`,sent:`✓`,delivered:`✓✓`,read:`✓✓`,failed:`!`};function u({message:l,own:u,locale:p,onRetry:m,onReact:h,messageActions:g,onQuoteClick:_}){let v=n.chatStrings(p),y=u?r.resolveReceiptState(l):null,b=l.deleted?[]:g?.(l)??[],x=(0,o.jsxs)(`div`,{className:e.cn(i.default.bubble,u&&i.default.ownBubble,l.deleted&&i.default.deletedBubble,l.status===`failed`&&i.default.failedBubble),children:[l.quote&&!l.deleted&&(0,o.jsx)(d,{message:l,locale:p,onQuoteClick:_}),l.deleted?(0,o.jsxs)(`div`,{className:i.default.tombstone,children:[(0,o.jsx)(`span`,{"aria-hidden":`true`,children:`🚫`}),` `,v.deleted]}):(0,o.jsxs)(o.Fragment,{children:[l.attachments&&l.attachments.length>0&&(0,o.jsx)(a.Suspense,{fallback:(0,o.jsx)(`div`,{className:i.default.attachmentsFallback}),children:(0,o.jsx)(s,{attachments:l.attachments,strings:v})}),l.body!==void 0&&l.body!==null&&l.body!==``&&(0,o.jsx)(`div`,{className:i.default.body,children:l.body})]}),(0,o.jsxs)(`div`,{className:i.default.meta,children:[l.edited&&!l.deleted&&(0,o.jsx)(`span`,{className:i.default.edited,children:v.edited}),(0,o.jsx)(`time`,{dateTime:new Date(l.sentAt).toISOString(),children:n.timeLabel(l.sentAt,p)}),y&&(0,o.jsx)(f,{message:l,state:y,locale:p})]})]});return(0,o.jsxs)(`li`,{className:i.default.bubbleRow,children:[b.length>0?(0,o.jsx)(a.Suspense,{fallback:x,children:(0,o.jsx)(c,{items:b,trigger:`contextmenu`,children:x})}):x,b.length>0&&(0,o.jsx)(a.Suspense,{fallback:null,children:(0,o.jsx)(c,{items:b,trigger:`click`,children:(0,o.jsx)(`button`,{type:`button`,className:i.default.actionsButton,"aria-label":v.messageActions,children:(0,o.jsx)(`span`,{"aria-hidden":`true`,children:`⋮`})})})}),l.reactions&&l.reactions.length>0&&(0,o.jsx)(`ul`,{className:i.default.reactions,"aria-label":v.reactions,children:l.reactions.map(n=>(0,o.jsx)(`li`,{children:(0,o.jsxs)(`button`,{type:`button`,className:e.cn(i.default.reaction,n.reacted&&i.default.reactionMine),"aria-pressed":n.reacted??!1,title:n.names?.join(`, `),disabled:!h,onClick:()=>h?.(l,n.emoji),children:[(0,o.jsxs)(`span`,{"aria-hidden":`true`,children:[n.emoji,` `,n.count]}),(0,o.jsx)(t.VisuallyHidden,{children:v.react(n.emoji,n.count)})]})},n.emoji))}),l.status===`failed`&&m&&(0,o.jsx)(`button`,{type:`button`,className:i.default.retry,onClick:()=>m(l),children:v.retry})]})}function d({message:t,locale:r,onQuoteClick:a}){let s=n.chatStrings(r),c=t.quote;if(!c)return null;let l=c.revoked?s.quoteRevoked:c.excerpt??(c.kind&&c.kind!==`text`?s.quoteKind[c.kind]:``),u=(0,o.jsxs)(o.Fragment,{children:[(0,o.jsx)(`span`,{className:i.default.quoteAuthor,children:c.senderName??s.replyingTo(c.messageId)}),(0,o.jsx)(`span`,{className:e.cn(i.default.quoteText,c.revoked&&i.default.quoteRevoked),children:l})]});return a?(0,o.jsx)(`button`,{type:`button`,className:e.cn(i.default.quote,i.default.quoteButton),onClick:()=>a(t),children:u}):(0,o.jsx)(`div`,{className:i.default.quote,children:u})}function f({message:a,state:s,locale:c}){let u=n.chatStrings(c),d=a.receipt&&a.receipt.totalRecipients>0&&s!==`failed`?r.receiptLabel(a.receipt,u):u[s===`delivered`?`sent`:s];return(0,o.jsxs)(`span`,{className:e.cn(i.default.status,s===`read`&&i.default.statusRead,s===`failed`&&i.default.statusFailed),title:d,children:[(0,o.jsx)(`span`,{"aria-hidden":`true`,children:l[s]}),(0,o.jsx)(t.VisuallyHidden,{children:d})]})}exports.ChatBubble=u;
2
+ //# sourceMappingURL=ChatBubble.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ChatBubble.cjs","names":[],"sources":["../../../src/components/Chat/ChatBubble.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines props-count function-lines — one bubble is one unit of\n * layout, and every part of it reads from the same message: the quote above the\n * body, the body or its tombstone, the attachments, the reactions under it, and the\n * timestamp with its ticks. Splitting them would pass the same message to six\n * components and put the bubble's geometry in none of them.\n */\nimport { Suspense, lazy, type ReactNode } from \"react\";\n\nimport { cn } from \"@/utils/cn\";\n\nimport type { ContextMenuItem } from \"../ContextMenu\";\nimport { VisuallyHidden } from \"../VisuallyHidden\";\nimport { chatStrings, timeLabel, type ChatMessage } from \"./chat-groups\";\nimport { receiptLabel, resolveReceiptState } from \"./chat-receipt\";\nimport styles from \"./Chat.module.css\";\n\nconst ChatAttachments = lazy(() =>\n import(\"./ChatAttachments\").then((module) => ({ default: module.ChatAttachments })),\n);\n\nconst ChatActionsMenu = lazy(() =>\n import(\"./ChatActionsMenu\").then((module) => ({ default: module.ChatActionsMenu })),\n);\n\n/** Glyph per tick state. `delivered` is the one a boolean `status` cannot say. */\nconst RECEIPT_GLYPH = {\n sending: \"◌\",\n sent: \"✓\",\n delivered: \"✓✓\",\n read: \"✓✓\",\n failed: \"!\",\n} as const;\n\nexport interface ChatBubbleProps {\n /** The message this bubble renders. */\n message: ChatMessage;\n /** Whether it belongs to the current user — side, colour and ticks. */\n own: boolean;\n /** Locale for labels. */\n locale: \"pt-BR\" | \"en\";\n /** Retry handler for a `\"failed\"` message. */\n onRetry?: (message: ChatMessage) => void;\n /** Toggles a reaction. Receives the emoji that was pressed. */\n onReact?: (message: ChatMessage, emoji: string) => void;\n /** Per-message actions, opened by right click, long press or the ⋮ button. */\n messageActions?: (message: ChatMessage) => ContextMenuItem[];\n /** Called when the quote stub is activated — scroll to the quoted message. */\n onQuoteClick?: (message: ChatMessage) => void;\n}\n\n/**\n * One message bubble.\n *\n * Everything a messenger needs on a single message lives here, because every one\n * of these was a thing each app rebuilt on top of a text-only bubble: the reply\n * stub, attachments, reactions, per-recipient ticks, the actions menu, and the\n * tombstone of a deleted message.\n *\n * @param props - The message and its handlers.\n * @returns The bubble, wrapped in a context menu when actions are given.\n */\nexport function ChatBubble({\n message,\n own,\n locale,\n onRetry,\n onReact,\n messageActions,\n onQuoteClick,\n}: ChatBubbleProps) {\n const strings = chatStrings(locale);\n const receiptState = own ? resolveReceiptState(message) : null;\n const actions = message.deleted ? [] : (messageActions?.(message) ?? []);\n\n const bubble = (\n <div\n className={cn(\n styles.bubble,\n own && styles.ownBubble,\n message.deleted && styles.deletedBubble,\n message.status === \"failed\" && styles.failedBubble,\n )}\n >\n {message.quote && !message.deleted && (\n <QuoteStub message={message} locale={locale} onQuoteClick={onQuoteClick} />\n )}\n\n {message.deleted ? (\n <div className={styles.tombstone}>\n <span aria-hidden=\"true\">🚫</span> {strings.deleted}\n </div>\n ) : (\n <>\n {message.attachments && message.attachments.length > 0 && (\n <Suspense fallback={<div className={styles.attachmentsFallback} />}>\n <ChatAttachments attachments={message.attachments} strings={strings} />\n </Suspense>\n )}\n {message.body !== undefined && message.body !== null && message.body !== \"\" && (\n <div className={styles.body}>{message.body}</div>\n )}\n </>\n )}\n\n <div className={styles.meta}>\n {message.edited && !message.deleted && (\n <span className={styles.edited}>{strings.edited}</span>\n )}\n <time dateTime={new Date(message.sentAt).toISOString()}>\n {timeLabel(message.sentAt, locale)}\n </time>\n {receiptState && <Receipt message={message} state={receiptState} locale={locale} />}\n </div>\n </div>\n );\n\n return (\n <li className={styles.bubbleRow}>\n {actions.length > 0 ? (\n <Suspense fallback={bubble}>\n <ChatActionsMenu items={actions} trigger=\"contextmenu\">\n {bubble}\n </ChatActionsMenu>\n </Suspense>\n ) : (\n bubble\n )}\n\n {actions.length > 0 && (\n <Suspense fallback={null}>\n <ChatActionsMenu items={actions} trigger=\"click\">\n <button\n type=\"button\"\n className={styles.actionsButton}\n aria-label={strings.messageActions}\n >\n <span aria-hidden=\"true\">⋮</span>\n </button>\n </ChatActionsMenu>\n </Suspense>\n )}\n\n {message.reactions && message.reactions.length > 0 && (\n <ul className={styles.reactions} aria-label={strings.reactions}>\n {message.reactions.map((reaction) => (\n <li key={reaction.emoji}>\n <button\n type=\"button\"\n className={cn(\n styles.reaction,\n reaction.reacted && styles.reactionMine,\n )}\n aria-pressed={reaction.reacted ?? false}\n title={reaction.names?.join(\", \")}\n disabled={!onReact}\n onClick={() => onReact?.(message, reaction.emoji)}\n >\n <span aria-hidden=\"true\">\n {reaction.emoji} {reaction.count}\n </span>\n <VisuallyHidden>\n {strings.react(reaction.emoji, reaction.count)}\n </VisuallyHidden>\n </button>\n </li>\n ))}\n </ul>\n )}\n\n {message.status === \"failed\" && onRetry && (\n <button type=\"button\" className={styles.retry} onClick={() => onRetry(message)}>\n {strings.retry}\n </button>\n )}\n </li>\n );\n}\n\n/** The reply stub above a body, or its blanked form once the target is gone. */\nfunction QuoteStub({\n message,\n locale,\n onQuoteClick,\n}: {\n message: ChatMessage;\n locale: \"pt-BR\" | \"en\";\n onQuoteClick?: (message: ChatMessage) => void;\n}): ReactNode {\n const strings = chatStrings(locale);\n const quote = message.quote;\n if (!quote) return null;\n const label = quote.revoked\n ? strings.quoteRevoked\n : (quote.excerpt ??\n (quote.kind && quote.kind !== \"text\" ? strings.quoteKind[quote.kind] : \"\"));\n\n const content = (\n <>\n <span className={styles.quoteAuthor}>\n {quote.senderName ?? strings.replyingTo(quote.messageId)}\n </span>\n <span className={cn(styles.quoteText, quote.revoked && styles.quoteRevoked)}>\n {label}\n </span>\n </>\n );\n\n return onQuoteClick ? (\n <button\n type=\"button\"\n className={cn(styles.quote, styles.quoteButton)}\n onClick={() => onQuoteClick(message)}\n >\n {content}\n </button>\n ) : (\n <div className={styles.quote}>{content}</div>\n );\n}\n\n/** The ticks, and the sentence behind them. */\nfunction Receipt({\n message,\n state,\n locale,\n}: {\n message: ChatMessage;\n state: keyof typeof RECEIPT_GLYPH;\n locale: \"pt-BR\" | \"en\";\n}): ReactNode {\n const strings = chatStrings(locale);\n const label =\n message.receipt && message.receipt.totalRecipients > 0 && state !== \"failed\"\n ? receiptLabel(message.receipt, strings)\n : strings[state === \"delivered\" ? \"sent\" : state];\n\n return (\n <span\n className={cn(\n styles.status,\n state === \"read\" && styles.statusRead,\n state === \"failed\" && styles.statusFailed,\n )}\n title={label}\n >\n <span aria-hidden=\"true\">{RECEIPT_GLYPH[state]}</span>\n <VisuallyHidden>{label}</VisuallyHidden>\n </span>\n );\n}\n"],"mappings":"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"}
@@ -0,0 +1,155 @@
1
+ import { cn as e } from "../../utils/cn.js";
2
+ import { VisuallyHidden as t } from "../VisuallyHidden/VisuallyHidden.js";
3
+ import { chatStrings as n, timeLabel as r } from "./chat-groups.js";
4
+ import { receiptLabel as i, resolveReceiptState as a } from "./chat-receipt.js";
5
+ import o from "./Chat.module.js";
6
+ import { Suspense as s, lazy as c } from "react";
7
+ import { Fragment as l, jsx as u, jsxs as d } from "react/jsx-runtime";
8
+ //#region src/components/Chat/ChatBubble.tsx
9
+ var f = c(() => import("./ChatAttachments.js").then((e) => ({ default: e.ChatAttachments }))), p = c(() => import("./ChatActionsMenu.js").then((e) => ({ default: e.ChatActionsMenu }))), m = {
10
+ sending: "◌",
11
+ sent: "✓",
12
+ delivered: "✓✓",
13
+ read: "✓✓",
14
+ failed: "!"
15
+ };
16
+ function h({ message: i, own: c, locale: m, onRetry: h, onReact: v, messageActions: y, onQuoteClick: b }) {
17
+ let x = n(m), S = c ? a(i) : null, C = i.deleted ? [] : y?.(i) ?? [], w = /* @__PURE__ */ d("div", {
18
+ className: e(o.bubble, c && o.ownBubble, i.deleted && o.deletedBubble, i.status === "failed" && o.failedBubble),
19
+ children: [
20
+ i.quote && !i.deleted && /* @__PURE__ */ u(g, {
21
+ message: i,
22
+ locale: m,
23
+ onQuoteClick: b
24
+ }),
25
+ i.deleted ? /* @__PURE__ */ d("div", {
26
+ className: o.tombstone,
27
+ children: [
28
+ /* @__PURE__ */ u("span", {
29
+ "aria-hidden": "true",
30
+ children: "🚫"
31
+ }),
32
+ " ",
33
+ x.deleted
34
+ ]
35
+ }) : /* @__PURE__ */ d(l, { children: [i.attachments && i.attachments.length > 0 && /* @__PURE__ */ u(s, {
36
+ fallback: /* @__PURE__ */ u("div", { className: o.attachmentsFallback }),
37
+ children: /* @__PURE__ */ u(f, {
38
+ attachments: i.attachments,
39
+ strings: x
40
+ })
41
+ }), i.body !== void 0 && i.body !== null && i.body !== "" && /* @__PURE__ */ u("div", {
42
+ className: o.body,
43
+ children: i.body
44
+ })] }),
45
+ /* @__PURE__ */ d("div", {
46
+ className: o.meta,
47
+ children: [
48
+ i.edited && !i.deleted && /* @__PURE__ */ u("span", {
49
+ className: o.edited,
50
+ children: x.edited
51
+ }),
52
+ /* @__PURE__ */ u("time", {
53
+ dateTime: new Date(i.sentAt).toISOString(),
54
+ children: r(i.sentAt, m)
55
+ }),
56
+ S && /* @__PURE__ */ u(_, {
57
+ message: i,
58
+ state: S,
59
+ locale: m
60
+ })
61
+ ]
62
+ })
63
+ ]
64
+ });
65
+ return /* @__PURE__ */ d("li", {
66
+ className: o.bubbleRow,
67
+ children: [
68
+ C.length > 0 ? /* @__PURE__ */ u(s, {
69
+ fallback: w,
70
+ children: /* @__PURE__ */ u(p, {
71
+ items: C,
72
+ trigger: "contextmenu",
73
+ children: w
74
+ })
75
+ }) : w,
76
+ C.length > 0 && /* @__PURE__ */ u(s, {
77
+ fallback: null,
78
+ children: /* @__PURE__ */ u(p, {
79
+ items: C,
80
+ trigger: "click",
81
+ children: /* @__PURE__ */ u("button", {
82
+ type: "button",
83
+ className: o.actionsButton,
84
+ "aria-label": x.messageActions,
85
+ children: /* @__PURE__ */ u("span", {
86
+ "aria-hidden": "true",
87
+ children: "⋮"
88
+ })
89
+ })
90
+ })
91
+ }),
92
+ i.reactions && i.reactions.length > 0 && /* @__PURE__ */ u("ul", {
93
+ className: o.reactions,
94
+ "aria-label": x.reactions,
95
+ children: i.reactions.map((n) => /* @__PURE__ */ u("li", { children: /* @__PURE__ */ d("button", {
96
+ type: "button",
97
+ className: e(o.reaction, n.reacted && o.reactionMine),
98
+ "aria-pressed": n.reacted ?? !1,
99
+ title: n.names?.join(", "),
100
+ disabled: !v,
101
+ onClick: () => v?.(i, n.emoji),
102
+ children: [/* @__PURE__ */ d("span", {
103
+ "aria-hidden": "true",
104
+ children: [
105
+ n.emoji,
106
+ " ",
107
+ n.count
108
+ ]
109
+ }), /* @__PURE__ */ u(t, { children: x.react(n.emoji, n.count) })]
110
+ }) }, n.emoji))
111
+ }),
112
+ i.status === "failed" && h && /* @__PURE__ */ u("button", {
113
+ type: "button",
114
+ className: o.retry,
115
+ onClick: () => h(i),
116
+ children: x.retry
117
+ })
118
+ ]
119
+ });
120
+ }
121
+ function g({ message: t, locale: r, onQuoteClick: i }) {
122
+ let a = n(r), s = t.quote;
123
+ if (!s) return null;
124
+ let c = s.revoked ? a.quoteRevoked : s.excerpt ?? (s.kind && s.kind !== "text" ? a.quoteKind[s.kind] : ""), f = /* @__PURE__ */ d(l, { children: [/* @__PURE__ */ u("span", {
125
+ className: o.quoteAuthor,
126
+ children: s.senderName ?? a.replyingTo(s.messageId)
127
+ }), /* @__PURE__ */ u("span", {
128
+ className: e(o.quoteText, s.revoked && o.quoteRevoked),
129
+ children: c
130
+ })] });
131
+ return i ? /* @__PURE__ */ u("button", {
132
+ type: "button",
133
+ className: e(o.quote, o.quoteButton),
134
+ onClick: () => i(t),
135
+ children: f
136
+ }) : /* @__PURE__ */ u("div", {
137
+ className: o.quote,
138
+ children: f
139
+ });
140
+ }
141
+ function _({ message: r, state: a, locale: s }) {
142
+ let c = n(s), l = r.receipt && r.receipt.totalRecipients > 0 && a !== "failed" ? i(r.receipt, c) : c[a === "delivered" ? "sent" : a];
143
+ return /* @__PURE__ */ d("span", {
144
+ className: e(o.status, a === "read" && o.statusRead, a === "failed" && o.statusFailed),
145
+ title: l,
146
+ children: [/* @__PURE__ */ u("span", {
147
+ "aria-hidden": "true",
148
+ children: m[a]
149
+ }), /* @__PURE__ */ u(t, { children: l })]
150
+ });
151
+ }
152
+ //#endregion
153
+ export { h as ChatBubble };
154
+
155
+ //# sourceMappingURL=ChatBubble.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ChatBubble.js","names":[],"sources":["../../../src/components/Chat/ChatBubble.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines props-count function-lines — one bubble is one unit of\n * layout, and every part of it reads from the same message: the quote above the\n * body, the body or its tombstone, the attachments, the reactions under it, and the\n * timestamp with its ticks. Splitting them would pass the same message to six\n * components and put the bubble's geometry in none of them.\n */\nimport { Suspense, lazy, type ReactNode } from \"react\";\n\nimport { cn } from \"@/utils/cn\";\n\nimport type { ContextMenuItem } from \"../ContextMenu\";\nimport { VisuallyHidden } from \"../VisuallyHidden\";\nimport { chatStrings, timeLabel, type ChatMessage } from \"./chat-groups\";\nimport { receiptLabel, resolveReceiptState } from \"./chat-receipt\";\nimport styles from \"./Chat.module.css\";\n\nconst ChatAttachments = lazy(() =>\n import(\"./ChatAttachments\").then((module) => ({ default: module.ChatAttachments })),\n);\n\nconst ChatActionsMenu = lazy(() =>\n import(\"./ChatActionsMenu\").then((module) => ({ default: module.ChatActionsMenu })),\n);\n\n/** Glyph per tick state. `delivered` is the one a boolean `status` cannot say. */\nconst RECEIPT_GLYPH = {\n sending: \"◌\",\n sent: \"✓\",\n delivered: \"✓✓\",\n read: \"✓✓\",\n failed: \"!\",\n} as const;\n\nexport interface ChatBubbleProps {\n /** The message this bubble renders. */\n message: ChatMessage;\n /** Whether it belongs to the current user — side, colour and ticks. */\n own: boolean;\n /** Locale for labels. */\n locale: \"pt-BR\" | \"en\";\n /** Retry handler for a `\"failed\"` message. */\n onRetry?: (message: ChatMessage) => void;\n /** Toggles a reaction. Receives the emoji that was pressed. */\n onReact?: (message: ChatMessage, emoji: string) => void;\n /** Per-message actions, opened by right click, long press or the ⋮ button. */\n messageActions?: (message: ChatMessage) => ContextMenuItem[];\n /** Called when the quote stub is activated — scroll to the quoted message. */\n onQuoteClick?: (message: ChatMessage) => void;\n}\n\n/**\n * One message bubble.\n *\n * Everything a messenger needs on a single message lives here, because every one\n * of these was a thing each app rebuilt on top of a text-only bubble: the reply\n * stub, attachments, reactions, per-recipient ticks, the actions menu, and the\n * tombstone of a deleted message.\n *\n * @param props - The message and its handlers.\n * @returns The bubble, wrapped in a context menu when actions are given.\n */\nexport function ChatBubble({\n message,\n own,\n locale,\n onRetry,\n onReact,\n messageActions,\n onQuoteClick,\n}: ChatBubbleProps) {\n const strings = chatStrings(locale);\n const receiptState = own ? resolveReceiptState(message) : null;\n const actions = message.deleted ? [] : (messageActions?.(message) ?? []);\n\n const bubble = (\n <div\n className={cn(\n styles.bubble,\n own && styles.ownBubble,\n message.deleted && styles.deletedBubble,\n message.status === \"failed\" && styles.failedBubble,\n )}\n >\n {message.quote && !message.deleted && (\n <QuoteStub message={message} locale={locale} onQuoteClick={onQuoteClick} />\n )}\n\n {message.deleted ? (\n <div className={styles.tombstone}>\n <span aria-hidden=\"true\">🚫</span> {strings.deleted}\n </div>\n ) : (\n <>\n {message.attachments && message.attachments.length > 0 && (\n <Suspense fallback={<div className={styles.attachmentsFallback} />}>\n <ChatAttachments attachments={message.attachments} strings={strings} />\n </Suspense>\n )}\n {message.body !== undefined && message.body !== null && message.body !== \"\" && (\n <div className={styles.body}>{message.body}</div>\n )}\n </>\n )}\n\n <div className={styles.meta}>\n {message.edited && !message.deleted && (\n <span className={styles.edited}>{strings.edited}</span>\n )}\n <time dateTime={new Date(message.sentAt).toISOString()}>\n {timeLabel(message.sentAt, locale)}\n </time>\n {receiptState && <Receipt message={message} state={receiptState} locale={locale} />}\n </div>\n </div>\n );\n\n return (\n <li className={styles.bubbleRow}>\n {actions.length > 0 ? (\n <Suspense fallback={bubble}>\n <ChatActionsMenu items={actions} trigger=\"contextmenu\">\n {bubble}\n </ChatActionsMenu>\n </Suspense>\n ) : (\n bubble\n )}\n\n {actions.length > 0 && (\n <Suspense fallback={null}>\n <ChatActionsMenu items={actions} trigger=\"click\">\n <button\n type=\"button\"\n className={styles.actionsButton}\n aria-label={strings.messageActions}\n >\n <span aria-hidden=\"true\">⋮</span>\n </button>\n </ChatActionsMenu>\n </Suspense>\n )}\n\n {message.reactions && message.reactions.length > 0 && (\n <ul className={styles.reactions} aria-label={strings.reactions}>\n {message.reactions.map((reaction) => (\n <li key={reaction.emoji}>\n <button\n type=\"button\"\n className={cn(\n styles.reaction,\n reaction.reacted && styles.reactionMine,\n )}\n aria-pressed={reaction.reacted ?? false}\n title={reaction.names?.join(\", \")}\n disabled={!onReact}\n onClick={() => onReact?.(message, reaction.emoji)}\n >\n <span aria-hidden=\"true\">\n {reaction.emoji} {reaction.count}\n </span>\n <VisuallyHidden>\n {strings.react(reaction.emoji, reaction.count)}\n </VisuallyHidden>\n </button>\n </li>\n ))}\n </ul>\n )}\n\n {message.status === \"failed\" && onRetry && (\n <button type=\"button\" className={styles.retry} onClick={() => onRetry(message)}>\n {strings.retry}\n </button>\n )}\n </li>\n );\n}\n\n/** The reply stub above a body, or its blanked form once the target is gone. */\nfunction QuoteStub({\n message,\n locale,\n onQuoteClick,\n}: {\n message: ChatMessage;\n locale: \"pt-BR\" | \"en\";\n onQuoteClick?: (message: ChatMessage) => void;\n}): ReactNode {\n const strings = chatStrings(locale);\n const quote = message.quote;\n if (!quote) return null;\n const label = quote.revoked\n ? strings.quoteRevoked\n : (quote.excerpt ??\n (quote.kind && quote.kind !== \"text\" ? strings.quoteKind[quote.kind] : \"\"));\n\n const content = (\n <>\n <span className={styles.quoteAuthor}>\n {quote.senderName ?? strings.replyingTo(quote.messageId)}\n </span>\n <span className={cn(styles.quoteText, quote.revoked && styles.quoteRevoked)}>\n {label}\n </span>\n </>\n );\n\n return onQuoteClick ? (\n <button\n type=\"button\"\n className={cn(styles.quote, styles.quoteButton)}\n onClick={() => onQuoteClick(message)}\n >\n {content}\n </button>\n ) : (\n <div className={styles.quote}>{content}</div>\n );\n}\n\n/** The ticks, and the sentence behind them. */\nfunction Receipt({\n message,\n state,\n locale,\n}: {\n message: ChatMessage;\n state: keyof typeof RECEIPT_GLYPH;\n locale: \"pt-BR\" | \"en\";\n}): ReactNode {\n const strings = chatStrings(locale);\n const label =\n message.receipt && message.receipt.totalRecipients > 0 && state !== \"failed\"\n ? receiptLabel(message.receipt, strings)\n : strings[state === \"delivered\" ? \"sent\" : state];\n\n return (\n <span\n className={cn(\n styles.status,\n state === \"read\" && styles.statusRead,\n state === \"failed\" && styles.statusFailed,\n )}\n title={label}\n >\n <span aria-hidden=\"true\">{RECEIPT_GLYPH[state]}</span>\n <VisuallyHidden>{label}</VisuallyHidden>\n </span>\n );\n}\n"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- var e=3e5;function t(e){let t=new Date(e);return t.setHours(0,0,0,0),t.getTime()}function n({messages:n,currentUserId:r,windowMs:i=e}){let a=[],o=null,s=null;for(let e of n){let n=t(e.sentAt);n!==o&&(o=n,s=null,a.push({kind:`day`,key:`day-${n}`,date:n}));let c=s?.messages[s.messages.length-1];if(s!==null&&c!==void 0&&s.authorId===e.authorId&&e.sentAt-c.sentAt<=i&&s){s.messages.push(e);continue}s={kind:`run`,key:`${e.authorId}-${e.id}`,authorId:e.authorId,authorName:e.authorName,own:r!==void 0&&e.authorId===r,messages:[e]},a.push(s)}return a}var r={thread:`Conversa`,today:`Hoje`,yesterday:`Ontem`,you:`Você`,typingOne:e=>`${e} está digitando…`,typingTwo:(e,t)=>`${e} e ${t} estão digitando…`,typingMany:e=>`${e} pessoas estão digitando…`,sending:`Enviando`,sent:`Enviada`,read:`Lida`,failed:`Falhou ao enviar`,retry:`Tentar de novo`,empty:`Nenhuma mensagem ainda`,placeholder:`Escreva uma mensagem`,send:`Enviar`},i={thread:`Conversation`,today:`Today`,yesterday:`Yesterday`,you:`You`,typingOne:e=>`${e} is typing…`,typingTwo:(e,t)=>`${e} and ${t} are typing…`,typingMany:e=>`${e} people are typing…`,sending:`Sending`,sent:`Sent`,read:`Read`,failed:`Failed to send`,retry:`Try again`,empty:`No messages yet`,placeholder:`Write a message`,send:`Send`};function a(e){return e===`en`?i:r}function o(e,{locale:n=`pt-BR`,now:r}={}){let i=a(n),o=t(r??Date.now()),s=864e5;return e===o?i.today:e===o-s?i.yesterday:new Date(e).toLocaleDateString(n===`en`?`en-US`:`pt-BR`,{day:`2-digit`,month:`short`,year:e<o-300*s?`numeric`:void 0})}function s(e,t=`pt-BR`){return new Date(e).toLocaleTimeString(t===`en`?`en-US`:`pt-BR`,{hour:`2-digit`,minute:`2-digit`})}function c(e,t=`pt-BR`){let n=a(t);return e.length===0?null:e.length===1?n.typingOne(e[0]):e.length===2?n.typingTwo(e[0],e[1]):n.typingMany(e.length)}exports.DEFAULT_GROUP_WINDOW_MS=e,exports.chatStrings=a,exports.dayLabel=o,exports.groupMessages=n,exports.timeLabel=s,exports.typingLabel=c;
1
+ var e=3e5;function t(e){let t=new Date(e);return t.setHours(0,0,0,0),t.getTime()}function n({messages:n,currentUserId:r,windowMs:i=e}){let a=[],o=null,s=null;for(let e of n){let n=t(e.sentAt);n!==o&&(o=n,s=null,a.push({kind:`day`,key:`day-${n}`,date:n}));let c=s?.messages[s.messages.length-1];if(s!==null&&c!==void 0&&s.authorId===e.authorId&&e.sentAt-c.sentAt<=i&&s){s.messages.push(e);continue}s={kind:`run`,key:`${e.authorId}-${e.id}`,authorId:e.authorId,authorName:e.authorName,own:r!==void 0&&e.authorId===r,messages:[e]},a.push(s)}return a}var r={thread:`Conversa`,today:`Hoje`,yesterday:`Ontem`,you:`Você`,typingOne:e=>`${e} está digitando…`,typingTwo:(e,t)=>`${e} e ${t} estão digitando…`,typingMany:e=>`${e} pessoas estão digitando…`,sending:`Enviando`,sent:`Enviada`,read:`Lida`,failed:`Falhou ao enviar`,retry:`Tentar de novo`,empty:`Nenhuma mensagem ainda`,placeholder:`Escreva uma mensagem`,send:`Enviar`,deleted:`Esta mensagem foi apagada`,edited:`editada`,replyingTo:e=>`Em resposta a ${e}`,quoteRevoked:`Mensagem apagada`,quoteKind:{image:`Foto`,video:`Vídeo`,audio:`Áudio`,voice:`Mensagem de voz`,file:`Documento`},reactions:`Reações`,react:(e,t)=>`${e}, ${t} ${t===1?`pessoa`:`pessoas`}`,deliveredAll:`Entregue a todos`,deliveredSome:(e,t)=>`Entregue a ${e} de ${t}`,readAll:`Lida por todos`,readSome:(e,t)=>`Lida por ${e} de ${t}`,messageActions:`Ações da mensagem`,voiceNote:`Mensagem de voz`,download:`Baixar`},i={thread:`Conversation`,today:`Today`,yesterday:`Yesterday`,you:`You`,typingOne:e=>`${e} is typing…`,typingTwo:(e,t)=>`${e} and ${t} are typing…`,typingMany:e=>`${e} people are typing…`,sending:`Sending`,sent:`Sent`,read:`Read`,failed:`Failed to send`,retry:`Try again`,empty:`No messages yet`,placeholder:`Write a message`,send:`Send`,deleted:`This message was deleted`,edited:`edited`,replyingTo:e=>`Replying to ${e}`,quoteRevoked:`Message deleted`,quoteKind:{image:`Photo`,video:`Video`,audio:`Audio`,voice:`Voice message`,file:`Document`},reactions:`Reactions`,react:(e,t)=>`${e}, ${t} ${t===1?`person`:`people`}`,deliveredAll:`Delivered to everyone`,deliveredSome:(e,t)=>`Delivered to ${e} of ${t}`,readAll:`Read by everyone`,readSome:(e,t)=>`Read by ${e} of ${t}`,messageActions:`Message actions`,voiceNote:`Voice message`,download:`Download`};function a(e){return e===`en`?i:r}function o(e,{locale:n=`pt-BR`,now:r}={}){let i=a(n),o=t(r??Date.now()),s=864e5;return e===o?i.today:e===o-s?i.yesterday:new Date(e).toLocaleDateString(n===`en`?`en-US`:`pt-BR`,{day:`2-digit`,month:`short`,year:e<o-300*s?`numeric`:void 0})}function s(e,t=`pt-BR`){return new Date(e).toLocaleTimeString(t===`en`?`en-US`:`pt-BR`,{hour:`2-digit`,minute:`2-digit`})}function c(e,t=`pt-BR`){let n=a(t);return e.length===0?null:e.length===1?n.typingOne(e[0]):e.length===2?n.typingTwo(e[0],e[1]):n.typingMany(e.length)}exports.DEFAULT_GROUP_WINDOW_MS=e,exports.chatStrings=a,exports.dayLabel=o,exports.groupMessages=n,exports.timeLabel=s,exports.typingLabel=c;
2
2
  //# sourceMappingURL=chat-groups.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"chat-groups.cjs","names":[],"sources":["../../../src/components/Chat/chat-groups.ts"],"sourcesContent":["import type { ReactNode } from \"react\";\n\n/** One entry in a thread. */\nexport interface ChatMessage {\n /** Stable identity. Used as the React key and by `onRetry`. */\n id: string;\n /** What was said. A node, so an app can render a link, an image or a quote. */\n body: ReactNode;\n /** Who said it. Compared against `currentUserId` to decide sides. */\n authorId: string;\n /** Display name. Falls back to `authorId` in the header of a run. */\n authorName?: string;\n /** Epoch milliseconds. */\n sentAt: number;\n /**\n * Delivery state of an outgoing message.\n *\n * `\"failed\"` is the one that matters: without it an app has to invent its own\n * way to say \"this never left\", and the user re-types a message that is\n * sitting right there.\n */\n status?: \"sending\" | \"sent\" | \"read\" | \"failed\";\n /** Anything the app wants to carry through to its own renderers. */\n data?: Record<string, unknown>;\n}\n\n/** A run of consecutive messages from one author, under one day. */\nexport interface ChatRun {\n kind: \"run\";\n /** `${authorId}-${first message id}` — stable across re-renders. */\n key: string;\n authorId: string;\n authorName?: string;\n /** Whether this run belongs to the current user. */\n own: boolean;\n messages: ChatMessage[];\n}\n\n/** A date heading between runs. */\nexport interface ChatDay {\n kind: \"day\";\n key: string;\n /** Midnight of that local day, epoch ms — the label is formatted by the view. */\n date: number;\n}\n\nexport type ChatSection = ChatDay | ChatRun;\n\n/** Default window in which consecutive messages from one author stay in a run. */\nexport const DEFAULT_GROUP_WINDOW_MS = 5 * 60 * 1000;\n\n/** Local midnight of an instant, epoch ms. */\nfunction startOfDay(timestamp: number): number {\n const date = new Date(timestamp);\n date.setHours(0, 0, 0, 0);\n return date.getTime();\n}\n\n/**\n * Turn a flat message list into the sections a thread renders: a date heading\n * whenever the local day changes, and runs of consecutive messages from the same\n * author.\n *\n * Grouping is what makes a thread readable — repeating the avatar and the name on\n * every line of a five-line burst turns a conversation into a list of receipts.\n * The run breaks on a different author, a different day, or a gap longer than\n * `windowMs`: a reply an hour later is a new beat in the conversation even when\n * nobody else spoke, and joining it to the earlier burst would put one timestamp\n * on messages an hour apart.\n *\n * Order is taken as given, oldest first, and never sorted here: a thread that\n * reorders what the server sent would fight optimistic inserts, where the\n * pending message is deliberately last.\n *\n * @param params.messages - Oldest first.\n * @param params.currentUserId - Author id treated as \"own\".\n * @param params.windowMs - Gap that still keeps a run together. Default 5 min.\n * @returns Sections in render order.\n */\nexport function groupMessages({\n messages,\n currentUserId,\n windowMs = DEFAULT_GROUP_WINDOW_MS,\n}: {\n messages: readonly ChatMessage[];\n currentUserId?: string;\n windowMs?: number;\n}): ChatSection[] {\n const sections: ChatSection[] = [];\n let day: number | null = null;\n let run: ChatRun | null = null;\n\n for (const message of messages) {\n const messageDay = startOfDay(message.sentAt);\n if (messageDay !== day) {\n day = messageDay;\n run = null;\n sections.push({ kind: \"day\", key: `day-${messageDay}`, date: messageDay });\n }\n\n const previous = run?.messages[run.messages.length - 1];\n const continues =\n run !== null &&\n previous !== undefined &&\n run.authorId === message.authorId &&\n message.sentAt - previous.sentAt <= windowMs;\n\n if (continues && run) {\n run.messages.push(message);\n continue;\n }\n\n run = {\n kind: \"run\",\n key: `${message.authorId}-${message.id}`,\n authorId: message.authorId,\n authorName: message.authorName,\n own: currentUserId !== undefined && message.authorId === currentUserId,\n messages: [message],\n };\n sections.push(run);\n }\n\n return sections;\n}\n\n/** Labels the thread needs, per locale. */\ninterface ChatStrings {\n thread: string;\n today: string;\n yesterday: string;\n you: string;\n typingOne: (name: string) => string;\n typingTwo: (a: string, b: string) => string;\n typingMany: (n: number) => string;\n sending: string;\n sent: string;\n read: string;\n failed: string;\n retry: string;\n empty: string;\n placeholder: string;\n send: string;\n}\n\nconst PT_BR: ChatStrings = {\n thread: \"Conversa\",\n today: \"Hoje\",\n yesterday: \"Ontem\",\n you: \"Você\",\n typingOne: (name) => `${name} está digitando…`,\n typingTwo: (a, b) => `${a} e ${b} estão digitando…`,\n typingMany: (n) => `${n} pessoas estão digitando…`,\n sending: \"Enviando\",\n sent: \"Enviada\",\n read: \"Lida\",\n failed: \"Falhou ao enviar\",\n retry: \"Tentar de novo\",\n empty: \"Nenhuma mensagem ainda\",\n placeholder: \"Escreva uma mensagem\",\n send: \"Enviar\",\n};\n\nconst EN: ChatStrings = {\n thread: \"Conversation\",\n today: \"Today\",\n yesterday: \"Yesterday\",\n you: \"You\",\n typingOne: (name) => `${name} is typing…`,\n typingTwo: (a, b) => `${a} and ${b} are typing…`,\n typingMany: (n) => `${n} people are typing…`,\n sending: \"Sending\",\n sent: \"Sent\",\n read: \"Read\",\n failed: \"Failed to send\",\n retry: \"Try again\",\n empty: \"No messages yet\",\n placeholder: \"Write a message\",\n send: \"Send\",\n};\n\n/** Locale strings for the thread. */\nexport function chatStrings(locale: \"pt-BR\" | \"en\"): ChatStrings {\n return locale === \"en\" ? EN : PT_BR;\n}\n\n/**\n * Label for a date heading: `\"Hoje\"`, `\"Ontem\"`, or the formatted date.\n *\n * @param date - Local midnight of the day being labelled.\n * @param params.now - Reference instant, so tests and SSR-free renders are stable.\n */\nexport function dayLabel(\n date: number,\n { locale = \"pt-BR\", now }: { locale?: \"pt-BR\" | \"en\"; now?: number } = {},\n): string {\n const strings = chatStrings(locale);\n const today = startOfDay(now ?? Date.now());\n const dayMs = 24 * 60 * 60 * 1000;\n if (date === today) return strings.today;\n if (date === today - dayMs) return strings.yesterday;\n return new Date(date).toLocaleDateString(locale === \"en\" ? \"en-US\" : \"pt-BR\", {\n day: \"2-digit\",\n month: \"short\",\n year: date < today - 300 * dayMs ? \"numeric\" : undefined,\n });\n}\n\n/** Clock label for a single message — the time, not a relative phrase. */\nexport function timeLabel(timestamp: number, locale: \"pt-BR\" | \"en\" = \"pt-BR\"): string {\n return new Date(timestamp).toLocaleTimeString(locale === \"en\" ? \"en-US\" : \"pt-BR\", {\n hour: \"2-digit\",\n minute: \"2-digit\",\n });\n}\n\n/** Sentence for the typing indicator, or `null` when nobody is typing. */\nexport function typingLabel(\n names: readonly string[],\n locale: \"pt-BR\" | \"en\" = \"pt-BR\",\n): string | null {\n const strings = chatStrings(locale);\n if (names.length === 0) return null;\n if (names.length === 1) return strings.typingOne(names[0]);\n if (names.length === 2) return strings.typingTwo(names[0], names[1]);\n return strings.typingMany(names.length);\n}\n"],"mappings":"AAiDA,IAAa,EAA0B,IAGvC,SAAS,EAAW,EAA2B,CAC3C,IAAM,EAAO,IAAI,KAAK,CAAS,EAE/B,OADA,EAAK,SAAS,EAAG,EAAG,EAAG,CAAC,EACjB,EAAK,QAAQ,CACxB,CAuBA,SAAgB,EAAc,CAC1B,WACA,gBACA,WAAW,GAKG,CACd,IAAM,EAA0B,CAAC,EAC7B,EAAqB,KACrB,EAAsB,KAE1B,IAAK,IAAM,KAAW,EAAU,CAC5B,IAAM,EAAa,EAAW,EAAQ,MAAM,EACxC,IAAe,IACf,EAAM,EACN,EAAM,KACN,EAAS,KAAK,CAAE,KAAM,MAAO,IAAK,OAAO,IAAc,KAAM,CAAW,CAAC,GAG7E,IAAM,EAAW,GAAK,SAAS,EAAI,SAAS,OAAS,GAOrD,GALI,IAAQ,MACR,IAAa,IAAA,IACb,EAAI,WAAa,EAAQ,UACzB,EAAQ,OAAS,EAAS,QAAU,GAEvB,EAAK,CAClB,EAAI,SAAS,KAAK,CAAO,EACzB,QACJ,CAEA,EAAM,CACF,KAAM,MACN,IAAK,GAAG,EAAQ,SAAS,GAAG,EAAQ,KACpC,SAAU,EAAQ,SAClB,WAAY,EAAQ,WACpB,IAAK,IAAkB,IAAA,IAAa,EAAQ,WAAa,EACzD,SAAU,CAAC,CAAO,CACtB,EACA,EAAS,KAAK,CAAG,CACrB,CAEA,OAAO,CACX,CAqBA,IAAM,EAAqB,CACvB,OAAQ,WACR,MAAO,OACP,UAAW,QACX,IAAK,OACL,UAAY,GAAS,GAAG,EAAK,kBAC7B,WAAY,EAAG,IAAM,GAAG,EAAE,KAAK,EAAE,mBACjC,WAAa,GAAM,GAAG,EAAE,2BACxB,QAAS,WACT,KAAM,UACN,KAAM,OACN,OAAQ,mBACR,MAAO,iBACP,MAAO,yBACP,YAAa,uBACb,KAAM,QACV,EAEM,EAAkB,CACpB,OAAQ,eACR,MAAO,QACP,UAAW,YACX,IAAK,MACL,UAAY,GAAS,GAAG,EAAK,aAC7B,WAAY,EAAG,IAAM,GAAG,EAAE,OAAO,EAAE,cACnC,WAAa,GAAM,GAAG,EAAE,qBACxB,QAAS,UACT,KAAM,OACN,KAAM,OACN,OAAQ,iBACR,MAAO,YACP,MAAO,kBACP,YAAa,kBACb,KAAM,MACV,EAGA,SAAgB,EAAY,EAAqC,CAC7D,OAAO,IAAW,KAAO,EAAK,CAClC,CAQA,SAAgB,EACZ,EACA,CAAE,SAAS,QAAS,OAAmD,CAAC,EAClE,CACN,IAAM,EAAU,EAAY,CAAM,EAC5B,EAAQ,EAAW,GAAO,KAAK,IAAI,CAAC,EACpC,EAAQ,MAGd,OAFI,IAAS,EAAc,EAAQ,MAC/B,IAAS,EAAQ,EAAc,EAAQ,UACpC,IAAI,KAAK,CAAI,CAAC,CAAC,mBAAmB,IAAW,KAAO,QAAU,QAAS,CAC1E,IAAK,UACL,MAAO,QACP,KAAM,EAAO,EAAQ,IAAM,EAAQ,UAAY,IAAA,EACnD,CAAC,CACL,CAGA,SAAgB,EAAU,EAAmB,EAAyB,QAAiB,CACnF,OAAO,IAAI,KAAK,CAAS,CAAC,CAAC,mBAAmB,IAAW,KAAO,QAAU,QAAS,CAC/E,KAAM,UACN,OAAQ,SACZ,CAAC,CACL,CAGA,SAAgB,EACZ,EACA,EAAyB,QACZ,CACb,IAAM,EAAU,EAAY,CAAM,EAIlC,OAHI,EAAM,SAAW,EAAU,KAC3B,EAAM,SAAW,EAAU,EAAQ,UAAU,EAAM,EAAE,EACrD,EAAM,SAAW,EAAU,EAAQ,UAAU,EAAM,GAAI,EAAM,EAAE,EAC5D,EAAQ,WAAW,EAAM,MAAM,CAC1C"}
1
+ {"version":3,"file":"chat-groups.cjs","names":[],"sources":["../../../src/components/Chat/chat-groups.ts"],"sourcesContent":["import type { ReactNode } from \"react\";\n\n/** What a bubble can carry besides text. */\nexport type ChatAttachmentKind = \"image\" | \"video\" | \"audio\" | \"voice\" | \"file\";\n\n/** One attachment on a message. */\nexport interface ChatAttachment {\n /** Decides how it renders: a picture, a player, a voice note, a download row. */\n kind: ChatAttachmentKind;\n /** Where the bytes are. A blob URL works for a message still uploading. */\n url: string;\n /** File name, shown for `\"file\"` and used as the download name. */\n name?: string;\n /** MIME type, when the app knows it. */\n mimeType?: string;\n /** Size in bytes, shown next to a file. */\n sizeBytes?: number;\n /** Duration in ms, for audio, voice and video. */\n durationMs?: number;\n /** Poster for a video, or a smaller copy of an image. */\n thumbnailUrl?: string;\n /**\n * Normalised peaks, `0`–`1`, for a voice note.\n *\n * A voice note without one is a grey rectangle: the waveform is what tells\n * somebody whether this is a word or a two-minute monologue.\n */\n waveform?: readonly number[];\n /** Alternative text for an image. */\n alt?: string;\n}\n\n/**\n * The stub of the message being replied to, shown above the body.\n *\n * `revoked` is not decoration: deleting the quoted message has to blank the\n * quoted text, or it leaks through everyone who replied to it.\n */\nexport interface ChatQuote {\n /** Id of the quoted message, so the app can scroll to it. */\n messageId: string;\n /** Who wrote it. */\n senderName?: string;\n /** A short piece of what it said. The component never truncates for you. */\n excerpt?: string;\n /** What the quoted message was, when it was not text. */\n kind?: \"text\" | ChatAttachmentKind;\n /** The quoted message was deleted — the excerpt is not shown. */\n revoked?: boolean;\n}\n\n/** One emoji on a message, with its tally. */\nexport interface ChatReaction {\n /** The emoji itself. */\n emoji: string;\n /** How many people reacted with it. */\n count: number;\n /** Whether the current user is one of them — the chip reads as pressed. */\n reacted?: boolean;\n /** Who reacted, for the chip's tooltip. */\n names?: readonly string[];\n}\n\n/**\n * Per-recipient delivery, which is what a group needs.\n *\n * `status` is a boolean for the sender: it cannot say \"delivered to everyone\"\n * versus \"read by everyone\", and only the second one turns the ticks blue.\n */\nexport interface ChatReceipt {\n /** How many recipients have received it. */\n deliveredTo: number;\n /** How many have read it. */\n readBy: number;\n /** How many recipients there are. */\n totalRecipients: number;\n}\n\n/** One entry in a thread. */\nexport interface ChatMessage {\n /** Stable identity. Used as the React key and by `onRetry`. */\n id: string;\n /**\n * What was said. A node, so an app can render a link, an image or a quote.\n *\n * Optional, because a message can be all attachment, or be a tombstone: a\n * deleted message has no body to show, and requiring one made every app\n * invent a placeholder string for the state the component now owns.\n */\n body?: ReactNode;\n /** Who said it. Compared against `currentUserId` to decide sides. */\n authorId: string;\n /** Display name. Falls back to `authorId` in the header of a run. */\n authorName?: string;\n /** Epoch milliseconds. */\n sentAt: number;\n /**\n * Delivery state of an outgoing message.\n *\n * `\"failed\"` is the one that matters: without it an app has to invent its own\n * way to say \"this never left\", and the user re-types a message that is\n * sitting right there.\n */\n status?: \"sending\" | \"sent\" | \"read\" | \"failed\";\n /**\n * Per-recipient delivery. Takes precedence over `status` for the ticks.\n *\n * Use it in a group, where \"delivered to all\" and \"read by all\" are different\n * states and `status` can only say one thing.\n */\n receipt?: ChatReceipt;\n /** Media, voice notes and documents carried by this message. */\n attachments?: readonly ChatAttachment[];\n /** The message this one replies to. */\n quote?: ChatQuote;\n /** Emoji reactions, already tallied by the app. */\n reactions?: readonly ChatReaction[];\n /**\n * The message was deleted — it renders as a tombstone.\n *\n * A state, not a `body` the app swaps for a string: as a state the quote of\n * it can be blanked too, and every app stops writing its own wording.\n */\n deleted?: boolean;\n /** The message was edited after it was sent. */\n edited?: boolean;\n /** Anything the app wants to carry through to its own renderers. */\n data?: Record<string, unknown>;\n}\n\n/** A run of consecutive messages from one author, under one day. */\nexport interface ChatRun {\n kind: \"run\";\n /** `${authorId}-${first message id}` — stable across re-renders. */\n key: string;\n authorId: string;\n authorName?: string;\n /** Whether this run belongs to the current user. */\n own: boolean;\n messages: ChatMessage[];\n}\n\n/** A date heading between runs. */\nexport interface ChatDay {\n kind: \"day\";\n key: string;\n /** Midnight of that local day, epoch ms — the label is formatted by the view. */\n date: number;\n}\n\nexport type ChatSection = ChatDay | ChatRun;\n\n/** Default window in which consecutive messages from one author stay in a run. */\nexport const DEFAULT_GROUP_WINDOW_MS = 5 * 60 * 1000;\n\n/** Local midnight of an instant, epoch ms. */\nfunction startOfDay(timestamp: number): number {\n const date = new Date(timestamp);\n date.setHours(0, 0, 0, 0);\n return date.getTime();\n}\n\n/**\n * Turn a flat message list into the sections a thread renders: a date heading\n * whenever the local day changes, and runs of consecutive messages from the same\n * author.\n *\n * Grouping is what makes a thread readable — repeating the avatar and the name on\n * every line of a five-line burst turns a conversation into a list of receipts.\n * The run breaks on a different author, a different day, or a gap longer than\n * `windowMs`: a reply an hour later is a new beat in the conversation even when\n * nobody else spoke, and joining it to the earlier burst would put one timestamp\n * on messages an hour apart.\n *\n * Order is taken as given, oldest first, and never sorted here: a thread that\n * reorders what the server sent would fight optimistic inserts, where the\n * pending message is deliberately last.\n *\n * @param params.messages - Oldest first.\n * @param params.currentUserId - Author id treated as \"own\".\n * @param params.windowMs - Gap that still keeps a run together. Default 5 min.\n * @returns Sections in render order.\n */\nexport function groupMessages({\n messages,\n currentUserId,\n windowMs = DEFAULT_GROUP_WINDOW_MS,\n}: {\n messages: readonly ChatMessage[];\n currentUserId?: string;\n windowMs?: number;\n}): ChatSection[] {\n const sections: ChatSection[] = [];\n let day: number | null = null;\n let run: ChatRun | null = null;\n\n for (const message of messages) {\n const messageDay = startOfDay(message.sentAt);\n if (messageDay !== day) {\n day = messageDay;\n run = null;\n sections.push({ kind: \"day\", key: `day-${messageDay}`, date: messageDay });\n }\n\n const previous = run?.messages[run.messages.length - 1];\n const continues =\n run !== null &&\n previous !== undefined &&\n run.authorId === message.authorId &&\n message.sentAt - previous.sentAt <= windowMs;\n\n if (continues && run) {\n run.messages.push(message);\n continue;\n }\n\n run = {\n kind: \"run\",\n key: `${message.authorId}-${message.id}`,\n authorId: message.authorId,\n authorName: message.authorName,\n own: currentUserId !== undefined && message.authorId === currentUserId,\n messages: [message],\n };\n sections.push(run);\n }\n\n return sections;\n}\n\n/** Labels the thread needs, per locale. */\ninterface ChatStrings {\n thread: string;\n today: string;\n yesterday: string;\n you: string;\n typingOne: (name: string) => string;\n typingTwo: (a: string, b: string) => string;\n typingMany: (n: number) => string;\n sending: string;\n sent: string;\n read: string;\n failed: string;\n retry: string;\n empty: string;\n placeholder: string;\n send: string;\n deleted: string;\n edited: string;\n replyingTo: (name: string) => string;\n quoteRevoked: string;\n quoteKind: Record<ChatAttachmentKind, string>;\n reactions: string;\n react: (emoji: string, count: number) => string;\n deliveredAll: string;\n deliveredSome: (delivered: number, total: number) => string;\n readAll: string;\n readSome: (read: number, total: number) => string;\n messageActions: string;\n voiceNote: string;\n download: string;\n}\n\nconst PT_BR: ChatStrings = {\n thread: \"Conversa\",\n today: \"Hoje\",\n yesterday: \"Ontem\",\n you: \"Você\",\n typingOne: (name) => `${name} está digitando…`,\n typingTwo: (a, b) => `${a} e ${b} estão digitando…`,\n typingMany: (n) => `${n} pessoas estão digitando…`,\n sending: \"Enviando\",\n sent: \"Enviada\",\n read: \"Lida\",\n failed: \"Falhou ao enviar\",\n retry: \"Tentar de novo\",\n empty: \"Nenhuma mensagem ainda\",\n placeholder: \"Escreva uma mensagem\",\n send: \"Enviar\",\n deleted: \"Esta mensagem foi apagada\",\n edited: \"editada\",\n replyingTo: (name) => `Em resposta a ${name}`,\n quoteRevoked: \"Mensagem apagada\",\n quoteKind: {\n image: \"Foto\",\n video: \"Vídeo\",\n audio: \"Áudio\",\n voice: \"Mensagem de voz\",\n file: \"Documento\",\n },\n reactions: \"Reações\",\n react: (emoji, count) => `${emoji}, ${count} ${count === 1 ? \"pessoa\" : \"pessoas\"}`,\n deliveredAll: \"Entregue a todos\",\n deliveredSome: (delivered, total) => `Entregue a ${delivered} de ${total}`,\n readAll: \"Lida por todos\",\n readSome: (read, total) => `Lida por ${read} de ${total}`,\n messageActions: \"Ações da mensagem\",\n voiceNote: \"Mensagem de voz\",\n download: \"Baixar\",\n};\n\nconst EN: ChatStrings = {\n thread: \"Conversation\",\n today: \"Today\",\n yesterday: \"Yesterday\",\n you: \"You\",\n typingOne: (name) => `${name} is typing…`,\n typingTwo: (a, b) => `${a} and ${b} are typing…`,\n typingMany: (n) => `${n} people are typing…`,\n sending: \"Sending\",\n sent: \"Sent\",\n read: \"Read\",\n failed: \"Failed to send\",\n retry: \"Try again\",\n empty: \"No messages yet\",\n placeholder: \"Write a message\",\n send: \"Send\",\n deleted: \"This message was deleted\",\n edited: \"edited\",\n replyingTo: (name) => `Replying to ${name}`,\n quoteRevoked: \"Message deleted\",\n quoteKind: {\n image: \"Photo\",\n video: \"Video\",\n audio: \"Audio\",\n voice: \"Voice message\",\n file: \"Document\",\n },\n reactions: \"Reactions\",\n react: (emoji, count) => `${emoji}, ${count} ${count === 1 ? \"person\" : \"people\"}`,\n deliveredAll: \"Delivered to everyone\",\n deliveredSome: (delivered, total) => `Delivered to ${delivered} of ${total}`,\n readAll: \"Read by everyone\",\n readSome: (read, total) => `Read by ${read} of ${total}`,\n messageActions: \"Message actions\",\n voiceNote: \"Voice message\",\n download: \"Download\",\n};\n\n/** Locale strings for the thread. */\nexport function chatStrings(locale: \"pt-BR\" | \"en\"): ChatStrings {\n return locale === \"en\" ? EN : PT_BR;\n}\n\n/**\n * Label for a date heading: `\"Hoje\"`, `\"Ontem\"`, or the formatted date.\n *\n * @param date - Local midnight of the day being labelled.\n * @param params.now - Reference instant, so tests and SSR-free renders are stable.\n */\nexport function dayLabel(\n date: number,\n { locale = \"pt-BR\", now }: { locale?: \"pt-BR\" | \"en\"; now?: number } = {},\n): string {\n const strings = chatStrings(locale);\n const today = startOfDay(now ?? Date.now());\n const dayMs = 24 * 60 * 60 * 1000;\n if (date === today) return strings.today;\n if (date === today - dayMs) return strings.yesterday;\n return new Date(date).toLocaleDateString(locale === \"en\" ? \"en-US\" : \"pt-BR\", {\n day: \"2-digit\",\n month: \"short\",\n year: date < today - 300 * dayMs ? \"numeric\" : undefined,\n });\n}\n\n/** Clock label for a single message — the time, not a relative phrase. */\nexport function timeLabel(timestamp: number, locale: \"pt-BR\" | \"en\" = \"pt-BR\"): string {\n return new Date(timestamp).toLocaleTimeString(locale === \"en\" ? \"en-US\" : \"pt-BR\", {\n hour: \"2-digit\",\n minute: \"2-digit\",\n });\n}\n\n/** Sentence for the typing indicator, or `null` when nobody is typing. */\nexport function typingLabel(\n names: readonly string[],\n locale: \"pt-BR\" | \"en\" = \"pt-BR\",\n): string | null {\n const strings = chatStrings(locale);\n if (names.length === 0) return null;\n if (names.length === 1) return strings.typingOne(names[0]);\n if (names.length === 2) return strings.typingTwo(names[0], names[1]);\n return strings.typingMany(names.length);\n}\n"],"mappings":"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"}
@@ -44,7 +44,27 @@ var r = {
44
44
  retry: "Tentar de novo",
45
45
  empty: "Nenhuma mensagem ainda",
46
46
  placeholder: "Escreva uma mensagem",
47
- send: "Enviar"
47
+ send: "Enviar",
48
+ deleted: "Esta mensagem foi apagada",
49
+ edited: "editada",
50
+ replyingTo: (e) => `Em resposta a ${e}`,
51
+ quoteRevoked: "Mensagem apagada",
52
+ quoteKind: {
53
+ image: "Foto",
54
+ video: "Vídeo",
55
+ audio: "Áudio",
56
+ voice: "Mensagem de voz",
57
+ file: "Documento"
58
+ },
59
+ reactions: "Reações",
60
+ react: (e, t) => `${e}, ${t} ${t === 1 ? "pessoa" : "pessoas"}`,
61
+ deliveredAll: "Entregue a todos",
62
+ deliveredSome: (e, t) => `Entregue a ${e} de ${t}`,
63
+ readAll: "Lida por todos",
64
+ readSome: (e, t) => `Lida por ${e} de ${t}`,
65
+ messageActions: "Ações da mensagem",
66
+ voiceNote: "Mensagem de voz",
67
+ download: "Baixar"
48
68
  }, i = {
49
69
  thread: "Conversation",
50
70
  today: "Today",
@@ -60,7 +80,27 @@ var r = {
60
80
  retry: "Try again",
61
81
  empty: "No messages yet",
62
82
  placeholder: "Write a message",
63
- send: "Send"
83
+ send: "Send",
84
+ deleted: "This message was deleted",
85
+ edited: "edited",
86
+ replyingTo: (e) => `Replying to ${e}`,
87
+ quoteRevoked: "Message deleted",
88
+ quoteKind: {
89
+ image: "Photo",
90
+ video: "Video",
91
+ audio: "Audio",
92
+ voice: "Voice message",
93
+ file: "Document"
94
+ },
95
+ reactions: "Reactions",
96
+ react: (e, t) => `${e}, ${t} ${t === 1 ? "person" : "people"}`,
97
+ deliveredAll: "Delivered to everyone",
98
+ deliveredSome: (e, t) => `Delivered to ${e} of ${t}`,
99
+ readAll: "Read by everyone",
100
+ readSome: (e, t) => `Read by ${e} of ${t}`,
101
+ messageActions: "Message actions",
102
+ voiceNote: "Voice message",
103
+ download: "Download"
64
104
  };
65
105
  function a(e) {
66
106
  return e === "en" ? i : r;
@@ -1 +1 @@
1
- {"version":3,"file":"chat-groups.js","names":[],"sources":["../../../src/components/Chat/chat-groups.ts"],"sourcesContent":["import type { ReactNode } from \"react\";\n\n/** One entry in a thread. */\nexport interface ChatMessage {\n /** Stable identity. Used as the React key and by `onRetry`. */\n id: string;\n /** What was said. A node, so an app can render a link, an image or a quote. */\n body: ReactNode;\n /** Who said it. Compared against `currentUserId` to decide sides. */\n authorId: string;\n /** Display name. Falls back to `authorId` in the header of a run. */\n authorName?: string;\n /** Epoch milliseconds. */\n sentAt: number;\n /**\n * Delivery state of an outgoing message.\n *\n * `\"failed\"` is the one that matters: without it an app has to invent its own\n * way to say \"this never left\", and the user re-types a message that is\n * sitting right there.\n */\n status?: \"sending\" | \"sent\" | \"read\" | \"failed\";\n /** Anything the app wants to carry through to its own renderers. */\n data?: Record<string, unknown>;\n}\n\n/** A run of consecutive messages from one author, under one day. */\nexport interface ChatRun {\n kind: \"run\";\n /** `${authorId}-${first message id}` — stable across re-renders. */\n key: string;\n authorId: string;\n authorName?: string;\n /** Whether this run belongs to the current user. */\n own: boolean;\n messages: ChatMessage[];\n}\n\n/** A date heading between runs. */\nexport interface ChatDay {\n kind: \"day\";\n key: string;\n /** Midnight of that local day, epoch ms — the label is formatted by the view. */\n date: number;\n}\n\nexport type ChatSection = ChatDay | ChatRun;\n\n/** Default window in which consecutive messages from one author stay in a run. */\nexport const DEFAULT_GROUP_WINDOW_MS = 5 * 60 * 1000;\n\n/** Local midnight of an instant, epoch ms. */\nfunction startOfDay(timestamp: number): number {\n const date = new Date(timestamp);\n date.setHours(0, 0, 0, 0);\n return date.getTime();\n}\n\n/**\n * Turn a flat message list into the sections a thread renders: a date heading\n * whenever the local day changes, and runs of consecutive messages from the same\n * author.\n *\n * Grouping is what makes a thread readable — repeating the avatar and the name on\n * every line of a five-line burst turns a conversation into a list of receipts.\n * The run breaks on a different author, a different day, or a gap longer than\n * `windowMs`: a reply an hour later is a new beat in the conversation even when\n * nobody else spoke, and joining it to the earlier burst would put one timestamp\n * on messages an hour apart.\n *\n * Order is taken as given, oldest first, and never sorted here: a thread that\n * reorders what the server sent would fight optimistic inserts, where the\n * pending message is deliberately last.\n *\n * @param params.messages - Oldest first.\n * @param params.currentUserId - Author id treated as \"own\".\n * @param params.windowMs - Gap that still keeps a run together. Default 5 min.\n * @returns Sections in render order.\n */\nexport function groupMessages({\n messages,\n currentUserId,\n windowMs = DEFAULT_GROUP_WINDOW_MS,\n}: {\n messages: readonly ChatMessage[];\n currentUserId?: string;\n windowMs?: number;\n}): ChatSection[] {\n const sections: ChatSection[] = [];\n let day: number | null = null;\n let run: ChatRun | null = null;\n\n for (const message of messages) {\n const messageDay = startOfDay(message.sentAt);\n if (messageDay !== day) {\n day = messageDay;\n run = null;\n sections.push({ kind: \"day\", key: `day-${messageDay}`, date: messageDay });\n }\n\n const previous = run?.messages[run.messages.length - 1];\n const continues =\n run !== null &&\n previous !== undefined &&\n run.authorId === message.authorId &&\n message.sentAt - previous.sentAt <= windowMs;\n\n if (continues && run) {\n run.messages.push(message);\n continue;\n }\n\n run = {\n kind: \"run\",\n key: `${message.authorId}-${message.id}`,\n authorId: message.authorId,\n authorName: message.authorName,\n own: currentUserId !== undefined && message.authorId === currentUserId,\n messages: [message],\n };\n sections.push(run);\n }\n\n return sections;\n}\n\n/** Labels the thread needs, per locale. */\ninterface ChatStrings {\n thread: string;\n today: string;\n yesterday: string;\n you: string;\n typingOne: (name: string) => string;\n typingTwo: (a: string, b: string) => string;\n typingMany: (n: number) => string;\n sending: string;\n sent: string;\n read: string;\n failed: string;\n retry: string;\n empty: string;\n placeholder: string;\n send: string;\n}\n\nconst PT_BR: ChatStrings = {\n thread: \"Conversa\",\n today: \"Hoje\",\n yesterday: \"Ontem\",\n you: \"Você\",\n typingOne: (name) => `${name} está digitando…`,\n typingTwo: (a, b) => `${a} e ${b} estão digitando…`,\n typingMany: (n) => `${n} pessoas estão digitando…`,\n sending: \"Enviando\",\n sent: \"Enviada\",\n read: \"Lida\",\n failed: \"Falhou ao enviar\",\n retry: \"Tentar de novo\",\n empty: \"Nenhuma mensagem ainda\",\n placeholder: \"Escreva uma mensagem\",\n send: \"Enviar\",\n};\n\nconst EN: ChatStrings = {\n thread: \"Conversation\",\n today: \"Today\",\n yesterday: \"Yesterday\",\n you: \"You\",\n typingOne: (name) => `${name} is typing…`,\n typingTwo: (a, b) => `${a} and ${b} are typing…`,\n typingMany: (n) => `${n} people are typing…`,\n sending: \"Sending\",\n sent: \"Sent\",\n read: \"Read\",\n failed: \"Failed to send\",\n retry: \"Try again\",\n empty: \"No messages yet\",\n placeholder: \"Write a message\",\n send: \"Send\",\n};\n\n/** Locale strings for the thread. */\nexport function chatStrings(locale: \"pt-BR\" | \"en\"): ChatStrings {\n return locale === \"en\" ? EN : PT_BR;\n}\n\n/**\n * Label for a date heading: `\"Hoje\"`, `\"Ontem\"`, or the formatted date.\n *\n * @param date - Local midnight of the day being labelled.\n * @param params.now - Reference instant, so tests and SSR-free renders are stable.\n */\nexport function dayLabel(\n date: number,\n { locale = \"pt-BR\", now }: { locale?: \"pt-BR\" | \"en\"; now?: number } = {},\n): string {\n const strings = chatStrings(locale);\n const today = startOfDay(now ?? Date.now());\n const dayMs = 24 * 60 * 60 * 1000;\n if (date === today) return strings.today;\n if (date === today - dayMs) return strings.yesterday;\n return new Date(date).toLocaleDateString(locale === \"en\" ? \"en-US\" : \"pt-BR\", {\n day: \"2-digit\",\n month: \"short\",\n year: date < today - 300 * dayMs ? \"numeric\" : undefined,\n });\n}\n\n/** Clock label for a single message — the time, not a relative phrase. */\nexport function timeLabel(timestamp: number, locale: \"pt-BR\" | \"en\" = \"pt-BR\"): string {\n return new Date(timestamp).toLocaleTimeString(locale === \"en\" ? \"en-US\" : \"pt-BR\", {\n hour: \"2-digit\",\n minute: \"2-digit\",\n });\n}\n\n/** Sentence for the typing indicator, or `null` when nobody is typing. */\nexport function typingLabel(\n names: readonly string[],\n locale: \"pt-BR\" | \"en\" = \"pt-BR\",\n): string | null {\n const strings = chatStrings(locale);\n if (names.length === 0) return null;\n if (names.length === 1) return strings.typingOne(names[0]);\n if (names.length === 2) return strings.typingTwo(names[0], names[1]);\n return strings.typingMany(names.length);\n}\n"],"mappings":";AAiDA,IAAa,IAA0B;AAGvC,SAAS,EAAW,GAA2B;CAC3C,IAAM,IAAO,IAAI,KAAK,CAAS;CAE/B,OADA,EAAK,SAAS,GAAG,GAAG,GAAG,CAAC,GACjB,EAAK,QAAQ;AACxB;AAuBA,SAAgB,EAAc,EAC1B,aACA,kBACA,cAAW,KAKG;CACd,IAAM,IAA0B,CAAC,GAC7B,IAAqB,MACrB,IAAsB;CAE1B,KAAK,IAAM,KAAW,GAAU;EAC5B,IAAM,IAAa,EAAW,EAAQ,MAAM;EAC5C,AAAI,MAAe,MACf,IAAM,GACN,IAAM,MACN,EAAS,KAAK;GAAE,MAAM;GAAO,KAAK,OAAO;GAAc,MAAM;EAAW,CAAC;EAG7E,IAAM,IAAW,GAAK,SAAS,EAAI,SAAS,SAAS;EAOrD,IALI,MAAQ,QACR,MAAa,KAAA,KACb,EAAI,aAAa,EAAQ,YACzB,EAAQ,SAAS,EAAS,UAAU,KAEvB,GAAK;GAClB,EAAI,SAAS,KAAK,CAAO;GACzB;EACJ;EAUA,AARA,IAAM;GACF,MAAM;GACN,KAAK,GAAG,EAAQ,SAAS,GAAG,EAAQ;GACpC,UAAU,EAAQ;GAClB,YAAY,EAAQ;GACpB,KAAK,MAAkB,KAAA,KAAa,EAAQ,aAAa;GACzD,UAAU,CAAC,CAAO;EACtB,GACA,EAAS,KAAK,CAAG;CACrB;CAEA,OAAO;AACX;AAqBA,IAAM,IAAqB;CACvB,QAAQ;CACR,OAAO;CACP,WAAW;CACX,KAAK;CACL,YAAY,MAAS,GAAG,EAAK;CAC7B,YAAY,GAAG,MAAM,GAAG,EAAE,KAAK,EAAE;CACjC,aAAa,MAAM,GAAG,EAAE;CACxB,SAAS;CACT,MAAM;CACN,MAAM;CACN,QAAQ;CACR,OAAO;CACP,OAAO;CACP,aAAa;CACb,MAAM;AACV,GAEM,IAAkB;CACpB,QAAQ;CACR,OAAO;CACP,WAAW;CACX,KAAK;CACL,YAAY,MAAS,GAAG,EAAK;CAC7B,YAAY,GAAG,MAAM,GAAG,EAAE,OAAO,EAAE;CACnC,aAAa,MAAM,GAAG,EAAE;CACxB,SAAS;CACT,MAAM;CACN,MAAM;CACN,QAAQ;CACR,OAAO;CACP,OAAO;CACP,aAAa;CACb,MAAM;AACV;AAGA,SAAgB,EAAY,GAAqC;CAC7D,OAAO,MAAW,OAAO,IAAK;AAClC;AAQA,SAAgB,EACZ,GACA,EAAE,YAAS,SAAS,WAAmD,CAAC,GAClE;CACN,IAAM,IAAU,EAAY,CAAM,GAC5B,IAAQ,EAAW,KAAO,KAAK,IAAI,CAAC,GACpC,IAAQ;CAGd,OAFI,MAAS,IAAc,EAAQ,QAC/B,MAAS,IAAQ,IAAc,EAAQ,YACpC,IAAI,KAAK,CAAI,CAAC,CAAC,mBAAmB,MAAW,OAAO,UAAU,SAAS;EAC1E,KAAK;EACL,OAAO;EACP,MAAM,IAAO,IAAQ,MAAM,IAAQ,YAAY,KAAA;CACnD,CAAC;AACL;AAGA,SAAgB,EAAU,GAAmB,IAAyB,SAAiB;CACnF,OAAO,IAAI,KAAK,CAAS,CAAC,CAAC,mBAAmB,MAAW,OAAO,UAAU,SAAS;EAC/E,MAAM;EACN,QAAQ;CACZ,CAAC;AACL;AAGA,SAAgB,EACZ,GACA,IAAyB,SACZ;CACb,IAAM,IAAU,EAAY,CAAM;CAIlC,OAHI,EAAM,WAAW,IAAU,OAC3B,EAAM,WAAW,IAAU,EAAQ,UAAU,EAAM,EAAE,IACrD,EAAM,WAAW,IAAU,EAAQ,UAAU,EAAM,IAAI,EAAM,EAAE,IAC5D,EAAQ,WAAW,EAAM,MAAM;AAC1C"}
1
+ {"version":3,"file":"chat-groups.js","names":[],"sources":["../../../src/components/Chat/chat-groups.ts"],"sourcesContent":["import type { ReactNode } from \"react\";\n\n/** What a bubble can carry besides text. */\nexport type ChatAttachmentKind = \"image\" | \"video\" | \"audio\" | \"voice\" | \"file\";\n\n/** One attachment on a message. */\nexport interface ChatAttachment {\n /** Decides how it renders: a picture, a player, a voice note, a download row. */\n kind: ChatAttachmentKind;\n /** Where the bytes are. A blob URL works for a message still uploading. */\n url: string;\n /** File name, shown for `\"file\"` and used as the download name. */\n name?: string;\n /** MIME type, when the app knows it. */\n mimeType?: string;\n /** Size in bytes, shown next to a file. */\n sizeBytes?: number;\n /** Duration in ms, for audio, voice and video. */\n durationMs?: number;\n /** Poster for a video, or a smaller copy of an image. */\n thumbnailUrl?: string;\n /**\n * Normalised peaks, `0`–`1`, for a voice note.\n *\n * A voice note without one is a grey rectangle: the waveform is what tells\n * somebody whether this is a word or a two-minute monologue.\n */\n waveform?: readonly number[];\n /** Alternative text for an image. */\n alt?: string;\n}\n\n/**\n * The stub of the message being replied to, shown above the body.\n *\n * `revoked` is not decoration: deleting the quoted message has to blank the\n * quoted text, or it leaks through everyone who replied to it.\n */\nexport interface ChatQuote {\n /** Id of the quoted message, so the app can scroll to it. */\n messageId: string;\n /** Who wrote it. */\n senderName?: string;\n /** A short piece of what it said. The component never truncates for you. */\n excerpt?: string;\n /** What the quoted message was, when it was not text. */\n kind?: \"text\" | ChatAttachmentKind;\n /** The quoted message was deleted — the excerpt is not shown. */\n revoked?: boolean;\n}\n\n/** One emoji on a message, with its tally. */\nexport interface ChatReaction {\n /** The emoji itself. */\n emoji: string;\n /** How many people reacted with it. */\n count: number;\n /** Whether the current user is one of them — the chip reads as pressed. */\n reacted?: boolean;\n /** Who reacted, for the chip's tooltip. */\n names?: readonly string[];\n}\n\n/**\n * Per-recipient delivery, which is what a group needs.\n *\n * `status` is a boolean for the sender: it cannot say \"delivered to everyone\"\n * versus \"read by everyone\", and only the second one turns the ticks blue.\n */\nexport interface ChatReceipt {\n /** How many recipients have received it. */\n deliveredTo: number;\n /** How many have read it. */\n readBy: number;\n /** How many recipients there are. */\n totalRecipients: number;\n}\n\n/** One entry in a thread. */\nexport interface ChatMessage {\n /** Stable identity. Used as the React key and by `onRetry`. */\n id: string;\n /**\n * What was said. A node, so an app can render a link, an image or a quote.\n *\n * Optional, because a message can be all attachment, or be a tombstone: a\n * deleted message has no body to show, and requiring one made every app\n * invent a placeholder string for the state the component now owns.\n */\n body?: ReactNode;\n /** Who said it. Compared against `currentUserId` to decide sides. */\n authorId: string;\n /** Display name. Falls back to `authorId` in the header of a run. */\n authorName?: string;\n /** Epoch milliseconds. */\n sentAt: number;\n /**\n * Delivery state of an outgoing message.\n *\n * `\"failed\"` is the one that matters: without it an app has to invent its own\n * way to say \"this never left\", and the user re-types a message that is\n * sitting right there.\n */\n status?: \"sending\" | \"sent\" | \"read\" | \"failed\";\n /**\n * Per-recipient delivery. Takes precedence over `status` for the ticks.\n *\n * Use it in a group, where \"delivered to all\" and \"read by all\" are different\n * states and `status` can only say one thing.\n */\n receipt?: ChatReceipt;\n /** Media, voice notes and documents carried by this message. */\n attachments?: readonly ChatAttachment[];\n /** The message this one replies to. */\n quote?: ChatQuote;\n /** Emoji reactions, already tallied by the app. */\n reactions?: readonly ChatReaction[];\n /**\n * The message was deleted — it renders as a tombstone.\n *\n * A state, not a `body` the app swaps for a string: as a state the quote of\n * it can be blanked too, and every app stops writing its own wording.\n */\n deleted?: boolean;\n /** The message was edited after it was sent. */\n edited?: boolean;\n /** Anything the app wants to carry through to its own renderers. */\n data?: Record<string, unknown>;\n}\n\n/** A run of consecutive messages from one author, under one day. */\nexport interface ChatRun {\n kind: \"run\";\n /** `${authorId}-${first message id}` — stable across re-renders. */\n key: string;\n authorId: string;\n authorName?: string;\n /** Whether this run belongs to the current user. */\n own: boolean;\n messages: ChatMessage[];\n}\n\n/** A date heading between runs. */\nexport interface ChatDay {\n kind: \"day\";\n key: string;\n /** Midnight of that local day, epoch ms — the label is formatted by the view. */\n date: number;\n}\n\nexport type ChatSection = ChatDay | ChatRun;\n\n/** Default window in which consecutive messages from one author stay in a run. */\nexport const DEFAULT_GROUP_WINDOW_MS = 5 * 60 * 1000;\n\n/** Local midnight of an instant, epoch ms. */\nfunction startOfDay(timestamp: number): number {\n const date = new Date(timestamp);\n date.setHours(0, 0, 0, 0);\n return date.getTime();\n}\n\n/**\n * Turn a flat message list into the sections a thread renders: a date heading\n * whenever the local day changes, and runs of consecutive messages from the same\n * author.\n *\n * Grouping is what makes a thread readable — repeating the avatar and the name on\n * every line of a five-line burst turns a conversation into a list of receipts.\n * The run breaks on a different author, a different day, or a gap longer than\n * `windowMs`: a reply an hour later is a new beat in the conversation even when\n * nobody else spoke, and joining it to the earlier burst would put one timestamp\n * on messages an hour apart.\n *\n * Order is taken as given, oldest first, and never sorted here: a thread that\n * reorders what the server sent would fight optimistic inserts, where the\n * pending message is deliberately last.\n *\n * @param params.messages - Oldest first.\n * @param params.currentUserId - Author id treated as \"own\".\n * @param params.windowMs - Gap that still keeps a run together. Default 5 min.\n * @returns Sections in render order.\n */\nexport function groupMessages({\n messages,\n currentUserId,\n windowMs = DEFAULT_GROUP_WINDOW_MS,\n}: {\n messages: readonly ChatMessage[];\n currentUserId?: string;\n windowMs?: number;\n}): ChatSection[] {\n const sections: ChatSection[] = [];\n let day: number | null = null;\n let run: ChatRun | null = null;\n\n for (const message of messages) {\n const messageDay = startOfDay(message.sentAt);\n if (messageDay !== day) {\n day = messageDay;\n run = null;\n sections.push({ kind: \"day\", key: `day-${messageDay}`, date: messageDay });\n }\n\n const previous = run?.messages[run.messages.length - 1];\n const continues =\n run !== null &&\n previous !== undefined &&\n run.authorId === message.authorId &&\n message.sentAt - previous.sentAt <= windowMs;\n\n if (continues && run) {\n run.messages.push(message);\n continue;\n }\n\n run = {\n kind: \"run\",\n key: `${message.authorId}-${message.id}`,\n authorId: message.authorId,\n authorName: message.authorName,\n own: currentUserId !== undefined && message.authorId === currentUserId,\n messages: [message],\n };\n sections.push(run);\n }\n\n return sections;\n}\n\n/** Labels the thread needs, per locale. */\ninterface ChatStrings {\n thread: string;\n today: string;\n yesterday: string;\n you: string;\n typingOne: (name: string) => string;\n typingTwo: (a: string, b: string) => string;\n typingMany: (n: number) => string;\n sending: string;\n sent: string;\n read: string;\n failed: string;\n retry: string;\n empty: string;\n placeholder: string;\n send: string;\n deleted: string;\n edited: string;\n replyingTo: (name: string) => string;\n quoteRevoked: string;\n quoteKind: Record<ChatAttachmentKind, string>;\n reactions: string;\n react: (emoji: string, count: number) => string;\n deliveredAll: string;\n deliveredSome: (delivered: number, total: number) => string;\n readAll: string;\n readSome: (read: number, total: number) => string;\n messageActions: string;\n voiceNote: string;\n download: string;\n}\n\nconst PT_BR: ChatStrings = {\n thread: \"Conversa\",\n today: \"Hoje\",\n yesterday: \"Ontem\",\n you: \"Você\",\n typingOne: (name) => `${name} está digitando…`,\n typingTwo: (a, b) => `${a} e ${b} estão digitando…`,\n typingMany: (n) => `${n} pessoas estão digitando…`,\n sending: \"Enviando\",\n sent: \"Enviada\",\n read: \"Lida\",\n failed: \"Falhou ao enviar\",\n retry: \"Tentar de novo\",\n empty: \"Nenhuma mensagem ainda\",\n placeholder: \"Escreva uma mensagem\",\n send: \"Enviar\",\n deleted: \"Esta mensagem foi apagada\",\n edited: \"editada\",\n replyingTo: (name) => `Em resposta a ${name}`,\n quoteRevoked: \"Mensagem apagada\",\n quoteKind: {\n image: \"Foto\",\n video: \"Vídeo\",\n audio: \"Áudio\",\n voice: \"Mensagem de voz\",\n file: \"Documento\",\n },\n reactions: \"Reações\",\n react: (emoji, count) => `${emoji}, ${count} ${count === 1 ? \"pessoa\" : \"pessoas\"}`,\n deliveredAll: \"Entregue a todos\",\n deliveredSome: (delivered, total) => `Entregue a ${delivered} de ${total}`,\n readAll: \"Lida por todos\",\n readSome: (read, total) => `Lida por ${read} de ${total}`,\n messageActions: \"Ações da mensagem\",\n voiceNote: \"Mensagem de voz\",\n download: \"Baixar\",\n};\n\nconst EN: ChatStrings = {\n thread: \"Conversation\",\n today: \"Today\",\n yesterday: \"Yesterday\",\n you: \"You\",\n typingOne: (name) => `${name} is typing…`,\n typingTwo: (a, b) => `${a} and ${b} are typing…`,\n typingMany: (n) => `${n} people are typing…`,\n sending: \"Sending\",\n sent: \"Sent\",\n read: \"Read\",\n failed: \"Failed to send\",\n retry: \"Try again\",\n empty: \"No messages yet\",\n placeholder: \"Write a message\",\n send: \"Send\",\n deleted: \"This message was deleted\",\n edited: \"edited\",\n replyingTo: (name) => `Replying to ${name}`,\n quoteRevoked: \"Message deleted\",\n quoteKind: {\n image: \"Photo\",\n video: \"Video\",\n audio: \"Audio\",\n voice: \"Voice message\",\n file: \"Document\",\n },\n reactions: \"Reactions\",\n react: (emoji, count) => `${emoji}, ${count} ${count === 1 ? \"person\" : \"people\"}`,\n deliveredAll: \"Delivered to everyone\",\n deliveredSome: (delivered, total) => `Delivered to ${delivered} of ${total}`,\n readAll: \"Read by everyone\",\n readSome: (read, total) => `Read by ${read} of ${total}`,\n messageActions: \"Message actions\",\n voiceNote: \"Voice message\",\n download: \"Download\",\n};\n\n/** Locale strings for the thread. */\nexport function chatStrings(locale: \"pt-BR\" | \"en\"): ChatStrings {\n return locale === \"en\" ? EN : PT_BR;\n}\n\n/**\n * Label for a date heading: `\"Hoje\"`, `\"Ontem\"`, or the formatted date.\n *\n * @param date - Local midnight of the day being labelled.\n * @param params.now - Reference instant, so tests and SSR-free renders are stable.\n */\nexport function dayLabel(\n date: number,\n { locale = \"pt-BR\", now }: { locale?: \"pt-BR\" | \"en\"; now?: number } = {},\n): string {\n const strings = chatStrings(locale);\n const today = startOfDay(now ?? Date.now());\n const dayMs = 24 * 60 * 60 * 1000;\n if (date === today) return strings.today;\n if (date === today - dayMs) return strings.yesterday;\n return new Date(date).toLocaleDateString(locale === \"en\" ? \"en-US\" : \"pt-BR\", {\n day: \"2-digit\",\n month: \"short\",\n year: date < today - 300 * dayMs ? \"numeric\" : undefined,\n });\n}\n\n/** Clock label for a single message — the time, not a relative phrase. */\nexport function timeLabel(timestamp: number, locale: \"pt-BR\" | \"en\" = \"pt-BR\"): string {\n return new Date(timestamp).toLocaleTimeString(locale === \"en\" ? \"en-US\" : \"pt-BR\", {\n hour: \"2-digit\",\n minute: \"2-digit\",\n });\n}\n\n/** Sentence for the typing indicator, or `null` when nobody is typing. */\nexport function typingLabel(\n names: readonly string[],\n locale: \"pt-BR\" | \"en\" = \"pt-BR\",\n): string | null {\n const strings = chatStrings(locale);\n if (names.length === 0) return null;\n if (names.length === 1) return strings.typingOne(names[0]);\n if (names.length === 2) return strings.typingTwo(names[0], names[1]);\n return strings.typingMany(names.length);\n}\n"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ function e(e){if(e.status===`failed`||e.status===`sending`)return e.status;let t=e.receipt;return!t||t.totalRecipients<=0?e.status??null:t.readBy>=t.totalRecipients?`read`:t.deliveredTo>=t.totalRecipients?`delivered`:e.status??`sent`}function t(e,t){let{deliveredTo:n,readBy:r,totalRecipients:i}=e;return r>=i?t.readAll:r>0?t.readSome(r,i):n>=i?t.deliveredAll:t.deliveredSome(n,i)}exports.receiptLabel=t,exports.resolveReceiptState=e;
2
+ //# sourceMappingURL=chat-receipt.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chat-receipt.cjs","names":[],"sources":["../../../src/components/Chat/chat-receipt.ts"],"sourcesContent":["import type { ChatMessage, ChatReceipt } from \"./chat-groups\";\n\n/**\n * What the ticks on an outgoing message say.\n *\n * `\"read\"` is the only one that turns them blue, and in a group that means\n * **everyone** has read it — the distinction `status: \"read\"` cannot make.\n */\nexport type ChatReceiptState = \"sending\" | \"sent\" | \"delivered\" | \"read\" | \"failed\";\n\n/**\n * Resolve the tick state of an outgoing message.\n *\n * A group needs three states where a single message has two: sent to the server,\n * delivered to the recipients, read by the recipients. A boolean `status` collapses\n * the middle one, so an app that has per-recipient counts either invents its own\n * glyph or shows a message as read when one person out of nine opened it.\n *\n * The counts win when they are there, and the thresholds are deliberately \"all\":\n * blue ticks in every messenger mean the whole room, and reporting \"read\" for a\n * partial count is the failure that makes people distrust the indicator entirely.\n * A `totalRecipients` of zero — a note to self, a thread whose membership the app\n * does not track — falls back to `status`, because \"read by all of nobody\" is not\n * a claim worth making.\n *\n * `\"failed\"` and `\"sending\"` come from `status` regardless: they are about this\n * side of the wire, and no recipient count exists yet.\n *\n * @param message - The message being rendered.\n * @returns The tick state, or `null` when there is nothing to show.\n */\nexport function resolveReceiptState(message: ChatMessage): ChatReceiptState | null {\n if (message.status === \"failed\" || message.status === \"sending\") return message.status;\n const receipt = message.receipt;\n if (!receipt || receipt.totalRecipients <= 0) return message.status ?? null;\n if (receipt.readBy >= receipt.totalRecipients) return \"read\";\n if (receipt.deliveredTo >= receipt.totalRecipients) return \"delivered\";\n return message.status ?? \"sent\";\n}\n\n/**\n * The sentence a reader gets for a receipt, for the tooltip and the screen reader.\n *\n * Partial counts are spelled out rather than rounded to a state: \"delivered to 3\n * of 9\" is the information the sender actually wants when the ticks are not blue\n * yet, and it is the part a glyph cannot carry.\n *\n * @param receipt - Per-recipient counts.\n * @param strings - Locale strings for the thread.\n * @returns One sentence describing delivery and reading.\n */\nexport function receiptLabel(\n receipt: ChatReceipt,\n strings: {\n deliveredAll: string;\n deliveredSome: (delivered: number, total: number) => string;\n readAll: string;\n readSome: (read: number, total: number) => string;\n },\n): string {\n const { deliveredTo, readBy, totalRecipients } = receipt;\n if (readBy >= totalRecipients) return strings.readAll;\n if (readBy > 0) return strings.readSome(readBy, totalRecipients);\n if (deliveredTo >= totalRecipients) return strings.deliveredAll;\n return strings.deliveredSome(deliveredTo, totalRecipients);\n}\n"],"mappings":"AA+BA,SAAgB,EAAoB,EAA+C,CAC/E,GAAI,EAAQ,SAAW,UAAY,EAAQ,SAAW,UAAW,OAAO,EAAQ,OAChF,IAAM,EAAU,EAAQ,QAIxB,MAHI,CAAC,GAAW,EAAQ,iBAAmB,EAAU,EAAQ,QAAU,KACnE,EAAQ,QAAU,EAAQ,gBAAwB,OAClD,EAAQ,aAAe,EAAQ,gBAAwB,YACpD,EAAQ,QAAU,MAC7B,CAaA,SAAgB,EACZ,EACA,EAMM,CACN,GAAM,CAAE,cAAa,SAAQ,mBAAoB,EAIjD,OAHI,GAAU,EAAwB,EAAQ,QAC1C,EAAS,EAAU,EAAQ,SAAS,EAAQ,CAAe,EAC3D,GAAe,EAAwB,EAAQ,aAC5C,EAAQ,cAAc,EAAa,CAAe,CAC7D"}
@@ -0,0 +1,14 @@
1
+ //#region src/components/Chat/chat-receipt.ts
2
+ function e(e) {
3
+ if (e.status === "failed" || e.status === "sending") return e.status;
4
+ let t = e.receipt;
5
+ return !t || t.totalRecipients <= 0 ? e.status ?? null : t.readBy >= t.totalRecipients ? "read" : t.deliveredTo >= t.totalRecipients ? "delivered" : e.status ?? "sent";
6
+ }
7
+ function t(e, t) {
8
+ let { deliveredTo: n, readBy: r, totalRecipients: i } = e;
9
+ return r >= i ? t.readAll : r > 0 ? t.readSome(r, i) : n >= i ? t.deliveredAll : t.deliveredSome(n, i);
10
+ }
11
+ //#endregion
12
+ export { t as receiptLabel, e as resolveReceiptState };
13
+
14
+ //# sourceMappingURL=chat-receipt.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chat-receipt.js","names":[],"sources":["../../../src/components/Chat/chat-receipt.ts"],"sourcesContent":["import type { ChatMessage, ChatReceipt } from \"./chat-groups\";\n\n/**\n * What the ticks on an outgoing message say.\n *\n * `\"read\"` is the only one that turns them blue, and in a group that means\n * **everyone** has read it — the distinction `status: \"read\"` cannot make.\n */\nexport type ChatReceiptState = \"sending\" | \"sent\" | \"delivered\" | \"read\" | \"failed\";\n\n/**\n * Resolve the tick state of an outgoing message.\n *\n * A group needs three states where a single message has two: sent to the server,\n * delivered to the recipients, read by the recipients. A boolean `status` collapses\n * the middle one, so an app that has per-recipient counts either invents its own\n * glyph or shows a message as read when one person out of nine opened it.\n *\n * The counts win when they are there, and the thresholds are deliberately \"all\":\n * blue ticks in every messenger mean the whole room, and reporting \"read\" for a\n * partial count is the failure that makes people distrust the indicator entirely.\n * A `totalRecipients` of zero — a note to self, a thread whose membership the app\n * does not track — falls back to `status`, because \"read by all of nobody\" is not\n * a claim worth making.\n *\n * `\"failed\"` and `\"sending\"` come from `status` regardless: they are about this\n * side of the wire, and no recipient count exists yet.\n *\n * @param message - The message being rendered.\n * @returns The tick state, or `null` when there is nothing to show.\n */\nexport function resolveReceiptState(message: ChatMessage): ChatReceiptState | null {\n if (message.status === \"failed\" || message.status === \"sending\") return message.status;\n const receipt = message.receipt;\n if (!receipt || receipt.totalRecipients <= 0) return message.status ?? null;\n if (receipt.readBy >= receipt.totalRecipients) return \"read\";\n if (receipt.deliveredTo >= receipt.totalRecipients) return \"delivered\";\n return message.status ?? \"sent\";\n}\n\n/**\n * The sentence a reader gets for a receipt, for the tooltip and the screen reader.\n *\n * Partial counts are spelled out rather than rounded to a state: \"delivered to 3\n * of 9\" is the information the sender actually wants when the ticks are not blue\n * yet, and it is the part a glyph cannot carry.\n *\n * @param receipt - Per-recipient counts.\n * @param strings - Locale strings for the thread.\n * @returns One sentence describing delivery and reading.\n */\nexport function receiptLabel(\n receipt: ChatReceipt,\n strings: {\n deliveredAll: string;\n deliveredSome: (delivered: number, total: number) => string;\n readAll: string;\n readSome: (read: number, total: number) => string;\n },\n): string {\n const { deliveredTo, readBy, totalRecipients } = receipt;\n if (readBy >= totalRecipients) return strings.readAll;\n if (readBy > 0) return strings.readSome(readBy, totalRecipients);\n if (deliveredTo >= totalRecipients) return strings.deliveredAll;\n return strings.deliveredSome(deliveredTo, totalRecipients);\n}\n"],"mappings":";AA+BA,SAAgB,EAAoB,GAA+C;CAC/E,IAAI,EAAQ,WAAW,YAAY,EAAQ,WAAW,WAAW,OAAO,EAAQ;CAChF,IAAM,IAAU,EAAQ;CAIxB,OAHI,CAAC,KAAW,EAAQ,mBAAmB,IAAU,EAAQ,UAAU,OACnE,EAAQ,UAAU,EAAQ,kBAAwB,SAClD,EAAQ,eAAe,EAAQ,kBAAwB,cACpD,EAAQ,UAAU;AAC7B;AAaA,SAAgB,EACZ,GACA,GAMM;CACN,IAAM,EAAE,gBAAa,WAAQ,uBAAoB;CAIjD,OAHI,KAAU,IAAwB,EAAQ,UAC1C,IAAS,IAAU,EAAQ,SAAS,GAAQ,CAAe,IAC3D,KAAe,IAAwB,EAAQ,eAC5C,EAAQ,cAAc,GAAa,CAAe;AAC7D"}
@@ -1,2 +1,2 @@
1
- const e=require("../../utils/cn.cjs"),t=require("../Portal/Portal.cjs"),n=require("./ContextMenu.module.cjs");let r=require("react"),i=require("react/jsx-runtime");function a(e){return`separator`in e&&e.separator===!0}function o({items:o,children:s,className:c}){let[l,u]=(0,r.useState)(!1),[d,f]=(0,r.useState)({x:0,y:0}),[p,m]=(0,r.useState)(-1),h=(0,r.useId)(),g=(0,r.useRef)(null),_=(0,r.useRef)([]),v=o.map((e,t)=>!a(e)&&!e.disabled?t:-1).filter(e=>e!==-1),y=(0,r.useCallback)(()=>{u(!1),m(-1)},[]),b=e=>{e.preventDefault(),f({x:e.clientX,y:e.clientY}),m(-1),u(!0)};(0,r.useEffect)(()=>{if(!l)return;let e=e=>{if(e.key===`Escape`){y();return}if(e.key===`ArrowDown`){e.preventDefault();let t=v.indexOf(p),n=v[(t+1)%v.length]??-1;m(n),_.current[n]?.focus()}if(e.key===`ArrowUp`){e.preventDefault();let t=v.indexOf(p),n=v[(t-1+v.length)%v.length]??-1;m(n),_.current[n]?.focus()}},t=e=>{g.current&&!g.current.contains(e.target)&&y()};return window.addEventListener(`keydown`,e),window.addEventListener(`mousedown`,t),()=>{window.removeEventListener(`keydown`,e),window.removeEventListener(`mousedown`,t)}},[l,p,v,y]);let x=e=>{e.onSelect?.(),y()};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(`span`,{className:n.default.root,onContextMenu:b,children:s}),l&&(0,i.jsx)(t.Portal,{children:(0,i.jsx)(`ul`,{ref:g,id:h,role:`menu`,className:e.cn(n.default.menu,c),style:{top:d.y,left:d.x},children:o.map((t,r)=>a(t)?(0,i.jsx)(`li`,{role:`separator`,className:n.default.separator,"aria-hidden":!0},`separator-${r}`):(0,i.jsx)(`li`,{role:`none`,children:(0,i.jsx)(`button`,{ref:e=>{_.current[r]=e},type:`button`,role:`menuitem`,className:e.cn(n.default.item,t.danger&&n.default.danger,p===r&&n.default.active),disabled:t.disabled,onClick:()=>x(t),onMouseEnter:()=>m(r),children:t.label})},`item-${r}`))})})]})}exports.ContextMenu=o;
1
+ const e=require("../../utils/cn.cjs"),t=require("../Portal/Portal.cjs"),n=require("./ContextMenu.module.cjs");let r=require("react"),i=require("react/jsx-runtime");var a=10;function o(e){return`separator`in e&&e.separator===!0}function s({items:s,children:l,className:u,trigger:d=`contextmenu`,longPressDelay:f=500,viewportMargin:p=8}){let[m,h]=(0,r.useState)(!1),[g,_]=(0,r.useState)({x:0,y:0}),[v,y]=(0,r.useState)(-1),b=(0,r.useId)(),[x,S]=(0,r.useState)(null),C=(0,r.useRef)([]),w=(0,r.useRef)(null),T=(0,r.useRef)(null),E=s.map((e,t)=>!o(e)&&!e.disabled?t:-1).filter(e=>e!==-1),D=(0,r.useCallback)(()=>{h(!1),y(-1)},[]),O=(0,r.useCallback)(e=>{_(e),y(-1),h(!0)},[]),k=(0,r.useCallback)(()=>{w.current!==null&&(clearTimeout(w.current),w.current=null),T.current=null},[]);(0,r.useEffect)(()=>k,[k]);let A=e=>{d!==`click`&&(e.preventDefault(),O({x:e.clientX,y:e.clientY}))},j=e=>{d!==`contextmenu`&&O({x:e.clientX,y:e.clientY})},M=e=>{if(f<=0||e.pointerType===`mouse`)return;let t={x:e.clientX,y:e.clientY};T.current=t,k(),T.current=t,w.current=setTimeout(()=>{w.current=null,T.current=null,c(),O(t)},f)},N=e=>{let t=T.current;t&&w.current!==null&&Math.hypot(e.clientX-t.x,e.clientY-t.y)>a&&k()};(0,r.useEffect)(()=>{if(!m)return;let e=e=>{if(e.key===`Escape`){D();return}let t=t=>{e.preventDefault(),y(t),C.current[t]?.focus()};if(E.length===0)return;let n=E.indexOf(v);e.key===`ArrowDown`&&t(E[(n+1)%E.length]??-1),e.key===`ArrowUp`&&t(E[(n-1+E.length)%E.length]??-1),e.key===`Home`&&t(E[0]??-1),e.key===`End`&&t(E[E.length-1]??-1)},t=e=>{x&&!x.contains(e.target)&&D()};return window.addEventListener(`keydown`,e),window.addEventListener(`mousedown`,t),window.addEventListener(`resize`,D),window.addEventListener(`scroll`,D,!0),()=>{window.removeEventListener(`keydown`,e),window.removeEventListener(`mousedown`,t),window.removeEventListener(`resize`,D),window.removeEventListener(`scroll`,D,!0)}},[m,v,E,D,x]),(0,r.useLayoutEffect)(()=>{if(!m||!x)return;x.focus({preventScroll:!0});let{offsetWidth:e,offsetHeight:t}=x;if(e===0&&t===0)return;let n=window.innerWidth-e-p,r=window.innerHeight-t-p,i=Math.max(p,Math.min(g.x,n)),a=Math.max(p,Math.min(g.y,r));(i!==g.x||a!==g.y)&&_({x:i,y:a})},[m,x,g.x,g.y,p]);let P=e=>{e.onSelect?.(),D()};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(`span`,{className:n.default.root,onContextMenu:A,onClick:j,onPointerDown:M,onPointerMove:N,onPointerUp:k,onPointerCancel:k,children:l}),m&&(0,i.jsx)(t.Portal,{children:(0,i.jsx)(`ul`,{ref:S,id:b,role:`menu`,"aria-orientation":`vertical`,tabIndex:-1,className:e.cn(n.default.menu,u),style:{top:g.y,left:g.x},children:s.map((t,r)=>o(t)?(0,i.jsx)(`li`,{role:`separator`,className:n.default.separator,"aria-hidden":!0},`separator-${r}`):(0,i.jsx)(`li`,{role:`none`,children:(0,i.jsx)(`button`,{ref:e=>{C.current[r]=e},type:`button`,role:`menuitem`,className:e.cn(n.default.item,t.danger&&n.default.danger,v===r&&n.default.active),disabled:t.disabled,onClick:()=>P(t),onMouseEnter:()=>y(r),children:t.label})},`item-${r}`))})})]})}function c(){typeof window>`u`||window.addEventListener(`click`,e=>{e.preventDefault(),e.stopPropagation()},{capture:!0,once:!0})}exports.ContextMenu=s;
2
2
  //# sourceMappingURL=ContextMenu.cjs.map