@dxos/react-ui-thread 0.12.0-next-2800d0376abcc62d5544afdd744b16b2b960434e → 0.13.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.
@@ -2,15 +2,20 @@ import { format } from "date-fns/format";
2
2
  import { startOfDay } from "date-fns/startOfDay";
3
3
  import { forwardRef, useCallback, useEffect, useId, useImperativeHandle, useMemo, useRef, useState } from "react";
4
4
  import { Obj, Ref } from "@dxos/echo";
5
- import { Avatar, Icon, IconButton, ScrollArea, composable, composableProps, useOnTransition, useThemeContext, useTranslation } from "@dxos/react-ui";
6
5
  import { Mosaic } from "@dxos/react-ui-mosaic";
6
+ import * as Button from "@dxos/react-ui/Button";
7
+ import * as Hooks from "@dxos/react-ui/Hooks";
8
+ import * as Icon from "@dxos/react-ui/Icon";
9
+ import * as ScrollArea from "@dxos/react-ui/ScrollArea";
10
+ import * as Util from "@dxos/react-ui/Util";
7
11
  import { createBasicExtensions, createThemeExtensions, keymap, listener, tags } from "@dxos/ui-editor";
8
12
  import { hoverableControlItem, hoverableControls, hoverableFocusedWithinControls, mx } from "@dxos/ui-theme";
9
13
  import { translationKey } from "#translations";
10
14
  import { HighlightStyle, StreamLanguage, syntaxHighlighting } from "@codemirror/language";
11
15
  import { EditorView } from "@codemirror/view";
12
- import { createContext } from "@radix-ui/react-context";
16
+ import { createContext } from "@dxos/react-hooks";
13
17
  import { useTextEditor } from "@dxos/react-ui-editor";
18
+ import * as Avatar from "@dxos/react-ui/Avatar";
14
19
  import { hexToEmoji, hexToHue, isTruthy } from "@dxos/util";
15
20
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
16
21
  //#region src/command.ts
@@ -32,10 +37,10 @@ var parser = StreamLanguage.define({
32
37
  });
33
38
  var styles = HighlightStyle.define([{
34
39
  tag: tags.tagName,
35
- class: mx("dx-tag dx-tag--indigo")
40
+ class: mx("dx-tag dx-tag-inline dx-tag-indigo mx-0.5")
36
41
  }, {
37
42
  tag: tags.labelName,
38
- class: mx("dx-tag dx-tag--blue")
43
+ class: mx("dx-tag dx-tag-inline dx-tag-blue mx-0.5")
39
44
  }]);
40
45
  var theme = EditorView.theme({ ".dx-tag": { verticalAlign: "bottom" } });
41
46
  var command = [
@@ -48,11 +53,11 @@ var command = [
48
53
  var [ThreadContextProvider, useThreadContext] = createContext("Thread");
49
54
  //#endregion
50
55
  //#region src/Message/Message.tsx
51
- var avatarSize = 7;
56
+ var avatarSize = "md";
52
57
  var buttonGroupClassNames = "flex flex-row items-center gap-0.5 pe-2";
53
58
  var buttonClassNames = "p-1! transition-opacity";
54
59
  var MessageRoot = forwardRef(({ authorImgSrc, authorId, authorName, authorAvatarProps, continues = true, controls, children, classNames, ...rootProps }, forwardedRef) => {
55
- return /* @__PURE__ */ jsx(Avatar.Root, { children: /* @__PURE__ */ jsxs("div", {
60
+ return /* @__PURE__ */ jsxs("div", {
56
61
  "data-testid": "thread.message",
57
62
  ...rootProps,
58
63
  className: mx("grid grid-cols-[var(--dx-rail-size)_1fr_min-content] w-full", classNames),
@@ -60,11 +65,12 @@ var MessageRoot = forwardRef(({ authorImgSrc, authorId, authorName, authorAvatar
60
65
  children: [
61
66
  /* @__PURE__ */ jsxs("div", {
62
67
  className: "flex flex-col items-center gap-2 pt-1",
63
- children: [/* @__PURE__ */ jsx(Avatar.Content, {
68
+ children: [/* @__PURE__ */ jsx(Avatar.Root, {
64
69
  size: avatarSize,
65
70
  hue: authorAvatarProps?.hue || hexToHue(authorId ?? "0"),
66
71
  fallback: authorAvatarProps?.emoji || hexToEmoji(authorId ?? "0"),
67
- ...authorImgSrc && { imgSrc: authorImgSrc }
72
+ src: authorImgSrc,
73
+ label: authorName
68
74
  }), continues && /* @__PURE__ */ jsx("div", { className: "w-px grow bg-separator" })]
69
75
  }),
70
76
  /* @__PURE__ */ jsx("div", {
@@ -76,7 +82,7 @@ var MessageRoot = forwardRef(({ authorImgSrc, authorId, authorName, authorAvatar
76
82
  children: controls
77
83
  })
78
84
  ]
79
- }) });
85
+ });
80
86
  });
81
87
  MessageRoot.displayName = "Message.Root";
82
88
  var MessageHeading = ({ children, classNames, timestamp, authorName, ...props }) => {
@@ -91,18 +97,18 @@ var MessageHeading = ({ children, classNames, timestamp, authorName, ...props })
91
97
  };
92
98
  MessageHeading.displayName = "Message.Heading";
93
99
  var MessageAuthorName = ({ authorName }) => {
94
- const { t } = useTranslation(translationKey);
95
- return /* @__PURE__ */ jsx(Avatar.Label, {
96
- classNames: "block truncate min-w-0 shrink text-sm text-subdued",
100
+ const { t } = Hooks.useTranslation(translationKey);
101
+ return /* @__PURE__ */ jsx("span", {
102
+ className: "block truncate min-w-0 shrink text-sm text-fg-subtle",
97
103
  children: authorName ?? t("anonymous.label")
98
104
  });
99
105
  };
100
106
  MessageAuthorName.displayName = "Message.AuthorName";
101
107
  var MessageTime = ({ timestamp }) => {
102
- const { dtLocale } = useTranslation(translationKey);
108
+ const { dtLocale } = Hooks.useTranslation(translationKey);
103
109
  const dt = timestamp ? new Date(timestamp) : void 0;
104
110
  return /* @__PURE__ */ jsx("time", {
105
- className: "shrink-0 text-subdued text-xs",
111
+ className: "shrink-0 text-fg-subtle text-xs",
106
112
  dateTime: dt?.toISOString(),
107
113
  children: dt ? format(dt, "p", { locale: dtLocale }) : ""
108
114
  });
@@ -147,7 +153,7 @@ var MessageBody = ({ message, isAuthor, editing, onSave }) => {
147
153
  };
148
154
  MessageBody.displayName = "Message.Body";
149
155
  var TextBlock = ({ block, isAuthor, editing, onSave }) => {
150
- const { themeMode } = useThemeContext();
156
+ const themeMode = Hooks.useThemeMode();
151
157
  const inMemoryContentRef = useRef(block.text);
152
158
  const handleDocumentChange = useCallback((next) => {
153
159
  inMemoryContentRef.current = next;
@@ -155,7 +161,7 @@ var TextBlock = ({ block, isAuthor, editing, onSave }) => {
155
161
  const saveDocumentChange = useCallback(() => {
156
162
  onSave?.(inMemoryContentRef.current);
157
163
  }, [onSave]);
158
- useOnTransition(editing, true, false, saveDocumentChange);
164
+ Hooks.useOnTransition(editing, true, false, saveDocumentChange);
159
165
  const { parentRef, focusAttributes, view } = useTextEditor(() => ({
160
166
  initialValue: block.text,
161
167
  extensions: [
@@ -239,7 +245,7 @@ MessageTextbox.displayName = "Message.Textbox";
239
245
  * the unit rendered by `Thread.Messages`.
240
246
  */
241
247
  var MessageTile = ({ message, classNames, continues = true }) => {
242
- const { t } = useTranslation(translationKey);
248
+ const { t } = Hooks.useTranslation(translationKey);
243
249
  const { getMetadata, identityDid, editable, onMessageDelete, onAcceptProposal, onAcceptChange, onRejectChange, onMessageSelect, currentMessageId } = useThreadContext("Message.Tile");
244
250
  const [editing, setEditing] = useState(false);
245
251
  const metadata = getMetadata(message);
@@ -266,7 +272,7 @@ var MessageTile = ({ message, classNames, continues = true }) => {
266
272
  const controls = showEdit || showAccept || showAcceptChange || showRejectChange || showDelete ? /* @__PURE__ */ jsxs("div", {
267
273
  className: buttonGroupClassNames,
268
274
  children: [
269
- showEdit && /* @__PURE__ */ jsx(IconButton, {
275
+ showEdit && /* @__PURE__ */ jsx(Button.Root, {
270
276
  "data-testid": editing ? "thread.message.save" : "thread.message.edit",
271
277
  variant: "ghost",
272
278
  icon: editing ? "ph--check--regular" : "ph--pencil-simple--regular",
@@ -275,7 +281,7 @@ var MessageTile = ({ message, classNames, continues = true }) => {
275
281
  classNames: [buttonClassNames, hoverableControlItem],
276
282
  onClick: handleEdit
277
283
  }),
278
- showAccept && /* @__PURE__ */ jsx(IconButton, {
284
+ showAccept && /* @__PURE__ */ jsx(Button.Root, {
279
285
  "data-testid": "thread.message.accept",
280
286
  variant: "ghost",
281
287
  icon: "ph--check--regular",
@@ -284,7 +290,7 @@ var MessageTile = ({ message, classNames, continues = true }) => {
284
290
  classNames: [buttonClassNames, hoverableControlItem],
285
291
  onClick: handleAcceptProposal
286
292
  }),
287
- showAcceptChange && /* @__PURE__ */ jsx(IconButton, {
293
+ showAcceptChange && /* @__PURE__ */ jsx(Button.Root, {
288
294
  "data-testid": "thread.message.accept-change",
289
295
  variant: "ghost",
290
296
  icon: "ph--check--regular",
@@ -293,7 +299,7 @@ var MessageTile = ({ message, classNames, continues = true }) => {
293
299
  classNames: [buttonClassNames, hoverableControlItem],
294
300
  onClick: handleAcceptChange
295
301
  }),
296
- showRejectChange && /* @__PURE__ */ jsx(IconButton, {
302
+ showRejectChange && /* @__PURE__ */ jsx(Button.Root, {
297
303
  "data-testid": "thread.message.reject-change",
298
304
  variant: "ghost",
299
305
  icon: "ph--x--regular",
@@ -302,7 +308,7 @@ var MessageTile = ({ message, classNames, continues = true }) => {
302
308
  classNames: [buttonClassNames, hoverableControlItem],
303
309
  onClick: handleRejectChange
304
310
  }),
305
- showDelete && /* @__PURE__ */ jsx(IconButton, {
311
+ showDelete && /* @__PURE__ */ jsx(Button.Root, {
306
312
  "data-testid": "thread.message.delete",
307
313
  variant: "ghost",
308
314
  icon: "ph--x--regular",
@@ -347,7 +353,7 @@ MessageTile.displayName = "Message.Tile";
347
353
  * to a single message.
348
354
  */
349
355
  var MessageGroup = ({ messages, continues = true, classNames }) => {
350
- const { t } = useTranslation(translationKey);
356
+ const { t } = Hooks.useTranslation(translationKey);
351
357
  const { getMetadata, identityDid, editable, onMessageDelete, onAcceptProposal } = useThreadContext("Message.Group");
352
358
  const [editing, setEditing] = useState(false);
353
359
  const first = messages[0];
@@ -369,7 +375,7 @@ var MessageGroup = ({ messages, continues = true, classNames }) => {
369
375
  const controls = showEdit || showAccept || showDelete ? /* @__PURE__ */ jsxs("div", {
370
376
  className: buttonGroupClassNames,
371
377
  children: [
372
- showEdit && /* @__PURE__ */ jsx(IconButton, {
378
+ showEdit && /* @__PURE__ */ jsx(Button.Root, {
373
379
  "data-testid": editing ? "thread.message.save" : "thread.message.edit",
374
380
  variant: "ghost",
375
381
  icon: editing ? "ph--check--regular" : "ph--pencil-simple--regular",
@@ -378,7 +384,7 @@ var MessageGroup = ({ messages, continues = true, classNames }) => {
378
384
  classNames: [buttonClassNames, hoverableControlItem],
379
385
  onClick: handleEdit
380
386
  }),
381
- showAccept && /* @__PURE__ */ jsx(IconButton, {
387
+ showAccept && /* @__PURE__ */ jsx(Button.Root, {
382
388
  "data-testid": "thread.message.accept",
383
389
  variant: "ghost",
384
390
  icon: "ph--check--regular",
@@ -387,7 +393,7 @@ var MessageGroup = ({ messages, continues = true, classNames }) => {
387
393
  classNames: [buttonClassNames, hoverableControlItem],
388
394
  onClick: handleAcceptProposal
389
395
  }),
390
- showDelete && /* @__PURE__ */ jsx(IconButton, {
396
+ showDelete && /* @__PURE__ */ jsx(Button.Root, {
391
397
  "data-testid": "thread.message.delete",
392
398
  variant: "ghost",
393
399
  icon: "ph--x--regular",
@@ -431,7 +437,6 @@ var Message = {
431
437
  };
432
438
  //#endregion
433
439
  //#region src/Thread/Thread.tsx
434
- var getMessageId = (message) => Obj.getURI(message);
435
440
  /**
436
441
  * Headless root of a thread. Provides message-tile context (metadata resolver,
437
442
  * injected renderers, callbacks). Requires an ambient `Dnd.Root` ancestor,
@@ -462,9 +467,9 @@ var ThreadRoot = ({ children, getMetadata, components, identityDid, editable, on
462
467
  };
463
468
  ThreadRoot.displayName = "Thread.Root";
464
469
  /** Visible thread container (the attention surface that hosts header / messages / composer). */
465
- var ThreadContent = composable(({ children, current, id, ...props }, forwardedRef) => {
470
+ var ThreadContent = Util.composable(({ children, current, id, ...props }, forwardedRef) => {
466
471
  return /* @__PURE__ */ jsx("div", {
467
- ...composableProps(props, {
472
+ ...Util.composableProps(props, {
468
473
  role: "group",
469
474
  classNames: ["flex flex-col bg-[var(--surface-bg)] current-related dx-attention-surface [--controls-opacity:0]", hoverableFocusedWithinControls]
470
475
  }),
@@ -481,15 +486,15 @@ ThreadContent.displayName = "Thread.Content";
481
486
  * `[rail · 1fr · controls]` grid so the trailing controls align with message-tile
482
487
  * controls (which use the same template) — no grid is leaked in from the caller.
483
488
  */
484
- var ThreadHeader = composable(({ title, detached, current, controls, onSelect, ...props }, forwardedRef) => {
485
- const { t } = useTranslation(translationKey);
489
+ var ThreadHeader = Util.composable(({ title, detached, current, controls, onSelect, ...props }, forwardedRef) => {
490
+ const { t } = Hooks.useTranslation(translationKey);
486
491
  const { focusComposer } = useThreadContext("Thread.Header");
487
492
  const handleSelect = useCallback(() => {
488
493
  onSelect?.();
489
494
  focusComposer();
490
495
  }, [onSelect, focusComposer]);
491
496
  return /* @__PURE__ */ jsxs("div", {
492
- ...composableProps(props, { classNames: [
497
+ ...Util.composableProps(props, { classNames: [
493
498
  "grid grid-cols-[var(--dx-rail-size)_1fr_min-content] items-center",
494
499
  hoverableControls,
495
500
  hoverableFocusedWithinControls
@@ -498,13 +503,13 @@ var ThreadHeader = composable(({ title, detached, current, controls, onSelect, .
498
503
  children: [
499
504
  /* @__PURE__ */ jsx("div", {
500
505
  className: "flex items-center justify-center",
501
- children: /* @__PURE__ */ jsx(IconButton, {
506
+ children: /* @__PURE__ */ jsx(Button.Root, {
502
507
  iconOnly: true,
503
508
  variant: "ghost",
504
- density: "sm",
509
+ size: "sm",
505
510
  icon: "ph--caret-double-right--regular",
506
511
  label: t("select-thread.label"),
507
- classNames: "text-description",
512
+ classNames: "text-fg-muted",
508
513
  onClick: handleSelect
509
514
  })
510
515
  }),
@@ -513,7 +518,7 @@ var ThreadHeader = composable(({ title, detached, current, controls, onSelect, .
513
518
  children: /* @__PURE__ */ jsx("p", {
514
519
  role: "heading",
515
520
  "data-testid": "thread.heading",
516
- className: mx("me-2 font-medium truncate italic", current ? "text-accent-text" : "text-description", detached && "line-through decoration-1"),
521
+ className: mx("me-2 font-medium truncate italic", current ? "text-accent-text" : "text-fg-muted", detached && "line-through decoration-1"),
517
522
  children: title
518
523
  })
519
524
  }),
@@ -572,7 +577,7 @@ var groupMessages = (messages, { groupWindowMs, dayDivider, gapDividerMs, dtLoca
572
577
  else {
573
578
  currentGroup = {
574
579
  kind: "group",
575
- id: getMessageId(message),
580
+ id: Obj.getURI(message),
576
581
  messages: [message]
577
582
  };
578
583
  items.push(currentGroup);
@@ -585,7 +590,7 @@ var groupMessages = (messages, { groupWindowMs, dayDivider, gapDividerMs, dtLoca
585
590
  return items;
586
591
  };
587
592
  var ThreadDivider = ({ label }) => label ? /* @__PURE__ */ jsxs("div", {
588
- className: "flex items-center gap-2 px-2 py-2 text-xs text-description",
593
+ className: "flex items-center gap-2 px-2 py-2 text-xs text-fg-muted",
589
594
  children: [
590
595
  /* @__PURE__ */ jsx("div", { className: "h-px grow bg-separator" }),
591
596
  /* @__PURE__ */ jsx("span", {
@@ -615,7 +620,7 @@ var DEFAULT_GROUP_WINDOW_MS = 6e4;
615
620
  var DEFAULT_GAP_DIVIDER_MS = 108e5;
616
621
  /** Virtualized stack of message tiles (via Mosaic), within an internal scroll area. */
617
622
  var ThreadMessages = ({ messages, id, estimateSize = 80, currentId, groupWindowMs = DEFAULT_GROUP_WINDOW_MS, dayDivider = true, gapDividerMs = DEFAULT_GAP_DIVIDER_MS, classNames }) => {
618
- const { dtLocale } = useTranslation(translationKey);
623
+ const { dtLocale } = Hooks.useTranslation(translationKey);
619
624
  const [viewport, setViewport] = useState(null);
620
625
  const items = useMemo(() => groupMessages(messages.filter(Boolean), {
621
626
  groupWindowMs,
@@ -641,7 +646,7 @@ var ThreadMessages = ({ messages, id, estimateSize = 80, currentId, groupWindowM
641
646
  currentId,
642
647
  eventHandler,
643
648
  children: /* @__PURE__ */ jsx(ScrollArea.Root, {
644
- classNames: mx("col-span-2 flex-1 min-h-0", classNames),
649
+ classNames: mx("col-span-2 dx-grow", classNames),
645
650
  orientation: "vertical",
646
651
  children: /* @__PURE__ */ jsx(ScrollArea.Viewport, {
647
652
  ref: setViewport,
@@ -660,8 +665,8 @@ var ThreadMessages = ({ messages, id, estimateSize = 80, currentId, groupWindowM
660
665
  ThreadMessages.displayName = "Thread.Messages";
661
666
  /** Message composer pinned at the foot of a thread. */
662
667
  var ThreadTextbox = ({ placeholder, autoFocus, disabled, extensions, onSend, ...metadata }) => {
663
- const { t } = useTranslation(translationKey);
664
- const { themeMode } = useThemeContext();
668
+ const { t } = Hooks.useTranslation(translationKey);
669
+ const themeMode = Hooks.useThemeMode();
665
670
  const { registerComposerFocus } = useThreadContext("Thread.Textbox");
666
671
  const composerRef = useRef(null);
667
672
  const messageRef = useRef("");
@@ -702,13 +707,13 @@ var ThreadTextbox = ({ placeholder, autoFocus, disabled, extensions, onSend, ...
702
707
  };
703
708
  ThreadTextbox.displayName = "Thread.Textbox";
704
709
  var ThreadStatus = forwardRef(({ activity, classNames, children, ...props }, forwardedRef) => {
705
- const { t } = useTranslation(translationKey);
710
+ const { t } = Hooks.useTranslation(translationKey);
706
711
  return /* @__PURE__ */ jsxs("div", {
707
712
  ...props,
708
- className: mx("col-start-2 grid grid-cols-[min-content_1fr_max-content] pb-2 pe-2 text-xs text-description", classNames),
713
+ className: mx("col-start-2 grid grid-cols-[min-content_1fr_max-content] pb-2 pe-2 text-xs text-fg-muted", classNames),
709
714
  ref: forwardedRef,
710
715
  children: [
711
- /* @__PURE__ */ jsx(Icon, {
716
+ /* @__PURE__ */ jsx(Icon.Icon, {
712
717
  icon: "ph--spinner--bold",
713
718
  classNames: "w-6 h-4 invisible data-[visible=show]:visible animate-spin-slow",
714
719
  "data-visible": activity ? "show" : "hide"
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../../src/command.ts","../../src/context.ts","../../src/Message/Message.tsx","../../src/Thread/Thread.tsx"],"sourcesContent":["//\n// Copyright 2024 DXOS.org\n//\n\nimport { HighlightStyle, StreamLanguage, syntaxHighlighting } from '@codemirror/language';\nimport { type Extension } from '@codemirror/state';\nimport { EditorView } from '@codemirror/view';\n\nimport { tags } from '@dxos/ui-editor';\nimport { mx } from '@dxos/ui-theme';\n\n/**\n * Highlights leading slash-commands (e.g. `/resolve`) and `@mention` tags\n * (e.g. `@Kai`, anywhere) in the message composer.\n * https://github.com/codemirror/stream-parser/blob/main/test/test-stream-parser.ts\n */\nconst parser = StreamLanguage.define<{ count: number }>({\n startState: () => ({ count: 0 }),\n token: (stream, state) => {\n state.count++;\n if (stream.eatSpace()) {\n return null;\n }\n // Leading slash-command (first token only).\n if (state.count === 1 && stream.match(/^\\/\\w+/)) {\n return 'tagName';\n }\n // @mention (anywhere).\n if (stream.match(/^@\\w+/)) {\n return 'labelName';\n }\n stream.next();\n return null;\n },\n});\n\nconst styles = HighlightStyle.define([\n {\n tag: tags.tagName,\n class: mx('dx-tag dx-tag--indigo'),\n },\n {\n tag: tags.labelName,\n class: mx('dx-tag dx-tag--blue'),\n },\n]);\n\n// Center the inline `.dx-tag` pills on the text line. `.dx-tag` is `inline-block`\n// with vertical padding, so on a CodeMirror line it baseline-aligns and rides\n// high; overriding `vertical-align` recentres it (cf. the `cm-reference-pill`\n// precedent in `@dxos/react-ui-chat`).\nconst theme = EditorView.theme({\n '.dx-tag': {\n verticalAlign: 'bottom',\n },\n});\n\nexport const command: Extension = [parser, syntaxHighlighting(styles), theme];\n","//\n// Copyright 2026 DXOS.org\n//\n\nimport { createContext } from '@radix-ui/react-context';\nimport { type FC, type ReactNode } from 'react';\n\n// Imported so TypeScript can name this type in the emitted .d.ts (ThreadContext → Message).\n// eslint-disable-next-line unused-imports/no-unused-imports\nimport { type Message } from '@dxos/types';\n\nimport { type ThreadContextValue } from './types';\n\n/**\n * Context provided by `Thread.Root` and consumed by its parts. Extends the\n * consumer-facing {@link ThreadContextValue} with internal coordination (e.g.\n * focusing the composer from the header's select affordance).\n */\nexport type ThreadContext = ThreadContextValue & {\n /** Register the composer's focus handler (set by `Thread.Textbox`). */\n registerComposerFocus: (focus: (() => void) | undefined) => void;\n /** Focus the thread composer (used by `Thread.Header`'s select affordance). */\n focusComposer: () => void;\n};\n\nexport const [ThreadContextProvider, useThreadContext]: readonly [\n FC<ThreadContext & { children: ReactNode }>,\n (consumerName: string) => ThreadContext,\n] = createContext<ThreadContext>('Thread');\n","//\n// Copyright 2023 DXOS.org\n//\n\nimport { EditorView } from '@codemirror/view';\nimport { format } from 'date-fns/format';\nimport React, {\n type ComponentPropsWithoutRef,\n type ComponentPropsWithRef,\n type ReactNode,\n forwardRef,\n useCallback,\n useEffect,\n useImperativeHandle,\n useRef,\n useState,\n} from 'react';\n\nimport { Obj, Ref } from '@dxos/echo';\nimport {\n Avatar,\n IconButton,\n type ThemedClassName,\n useOnTransition,\n useThemeContext,\n useTranslation,\n} from '@dxos/react-ui';\nimport { type UseTextEditorProps, useTextEditor } from '@dxos/react-ui-editor';\nimport { type ContentBlock, type Message as MessageType } from '@dxos/types';\nimport { createBasicExtensions, createThemeExtensions, keymap, listener } from '@dxos/ui-editor';\nimport { hoverableControlItem, hoverableControls, hoverableFocusedWithinControls, mx } from '@dxos/ui-theme';\nimport { hexToEmoji, hexToHue, isTruthy } from '@dxos/util';\n\nimport { translationKey } from '#translations';\n\nimport { command } from '../command';\nimport { useThreadContext } from '../context';\nimport { type MessageMetadata } from '../types';\n\nconst avatarSize = 7;\n\nconst buttonGroupClassNames = 'flex flex-row items-center gap-0.5 pe-2';\nconst buttonClassNames = 'p-1! transition-opacity';\n\n//\n// Root\n//\n\nexport type MessageRootProps = ThemedClassName<\n ComponentPropsWithRef<'div'> & MessageMetadata & Partial<{ continues: boolean; controls: ReactNode }>\n>;\n\n// TODO(burdon): Show authorName on tooltip.\nconst MessageRoot = forwardRef<HTMLDivElement, MessageRootProps>(\n (\n {\n authorImgSrc,\n authorId,\n authorName,\n authorAvatarProps,\n continues = true,\n controls,\n children,\n classNames,\n ...rootProps\n },\n forwardedRef,\n ) => {\n // Must wrap the message since Avatar.Label may be used in the content.\n // Columns mirror Thread.Header (avatar/rail · content · controls) so trailing\n // controls align with the thread header's controls.\n return (\n <Avatar.Root>\n <div\n data-testid='thread.message'\n {...rootProps}\n className={mx('grid grid-cols-[var(--dx-rail-size)_1fr_min-content] w-full', classNames)}\n ref={forwardedRef}\n >\n <div className='flex flex-col items-center gap-2 pt-1'>\n <Avatar.Content\n size={avatarSize}\n hue={authorAvatarProps?.hue || hexToHue(authorId ?? '0')}\n fallback={authorAvatarProps?.emoji || hexToEmoji(authorId ?? '0')}\n {...(authorImgSrc && { imgSrc: authorImgSrc })}\n />\n {continues && <div className='w-px grow bg-separator' />}\n </div>\n <div className='py-1 min-w-0'>{children}</div>\n {controls && <div className='self-start'>{controls}</div>}\n </div>\n </Avatar.Root>\n );\n },\n);\n\nMessageRoot.displayName = 'Message.Root';\n\n//\n// Heading\n//\n\nexport type MessageHeadingProps = ThemedClassName<ComponentPropsWithoutRef<'div'>> &\n Pick<MessageMetadata, 'authorName' | 'timestamp'>;\n\nconst MessageHeading = ({ children, classNames, timestamp, authorName, ...props }: MessageHeadingProps) => {\n return (\n <div {...props} className={mx('flex gap-2 items-start', classNames)}>\n <p className='grow flex items-baseline gap-2 min-w-0'>\n <MessageAuthorName authorName={authorName} />\n {timestamp && <MessageTime timestamp={timestamp} />}\n </p>\n {children}\n </div>\n );\n};\n\nMessageHeading.displayName = 'Message.Heading';\n\nexport type MessageAuthorNameProps = Pick<MessageMetadata, 'authorName'>;\n\nconst MessageAuthorName = ({ authorName }: MessageAuthorNameProps) => {\n const { t } = useTranslation(translationKey);\n return (\n <Avatar.Label classNames='block truncate min-w-0 shrink text-sm text-subdued'>\n {authorName ?? t('anonymous.label')}\n </Avatar.Label>\n );\n};\n\nMessageAuthorName.displayName = 'Message.AuthorName';\n\nexport type MessageTimeProps = Pick<MessageMetadata, 'timestamp'>;\n\nconst MessageTime = ({ timestamp }: MessageTimeProps) => {\n const { dtLocale } = useTranslation(translationKey);\n const dt = timestamp ? new Date(timestamp) : undefined;\n return (\n <time className='shrink-0 text-subdued text-xs' dateTime={dt?.toISOString()}>\n {dt ? format(dt, 'p', { locale: dtLocale }) : ''}\n </time>\n );\n};\n\nMessageTime.displayName = 'Message.Time';\n\n//\n// Body\n//\n\nexport type MessageBodyProps = {\n /** The message whose content blocks are rendered. */\n message: MessageType.Message;\n /** When true, the local user authored this message. */\n isAuthor?: boolean;\n /** When true, the text block is editable. */\n editing?: boolean;\n /** Persist an edited text block. */\n onSave?: (text: string) => void;\n};\n\n/**\n * Renders a message's content blocks: text (via editor), proposal, and\n * object/reference tiles (delegated to the injected `Object` component).\n */\nconst MessageBody = ({ message, isAuthor, editing, onSave }: MessageBodyProps) => {\n const { components } = useThreadContext('Message.Body');\n const textBlockIndex = message.blocks.findIndex((block) => block._tag === 'text');\n const textBlock = textBlockIndex !== -1 ? (message.blocks[textBlockIndex] as ContentBlock.Text) : undefined;\n const proposalBlock = message.blocks.find((block) => block._tag === 'proposal') as ContentBlock.Proposal | undefined;\n const changeBlock = message.blocks.find((block) => block._tag === 'change') as ContentBlock.Change | undefined;\n const references = message.blocks\n .filter((block) => block._tag === 'reference')\n .map((block) => (block as ContentBlock.Reference).reference);\n const Object = components.Object;\n\n return (\n <>\n {textBlock && <TextBlock block={textBlock} isAuthor={isAuthor} editing={editing} onSave={onSave} />}\n {proposalBlock && <div className='me-4 italic'>{proposalBlock.text}</div>}\n {changeBlock && (\n <p className='me-4 text-sm break-words'>\n {changeBlock.before && <span className='line-through opacity-60'>{changeBlock.before}</span>}\n {changeBlock.before && changeBlock.after && ' '}\n {changeBlock.after && <span>{changeBlock.after}</span>}\n </p>\n )}\n {Object &&\n Ref.Array.targets(references).map((reference, index) => (\n <Object key={index} subject={reference as Obj.Unknown} />\n ))}\n </>\n );\n};\n\nMessageBody.displayName = 'Message.Body';\n\nconst TextBlock = ({\n block,\n isAuthor,\n editing,\n onSave,\n}: {\n block: ContentBlock.Text;\n isAuthor?: boolean;\n editing?: boolean;\n onSave?: (text: string) => void;\n}) => {\n const { themeMode } = useThemeContext();\n const inMemoryContentRef = useRef(block.text);\n\n const handleDocumentChange = useCallback((next: string) => {\n inMemoryContentRef.current = next;\n }, []);\n\n const saveDocumentChange = useCallback(() => {\n onSave?.(inMemoryContentRef.current);\n }, [onSave]);\n\n useOnTransition(editing, true, false, saveDocumentChange);\n\n const { parentRef, focusAttributes, view } = useTextEditor(\n () => ({\n initialValue: block.text,\n extensions: [\n // Edit mode is authorisation enough — the control that gets here is already gated on\n // `isAuthor` — and a mid-edit flip to read-only makes the editor drop input silently.\n createBasicExtensions({ readOnly: !editing }),\n createThemeExtensions({ themeMode }),\n command,\n EditorView.updateListener.of((update) => {\n if (update.docChanged) {\n handleDocumentChange(update.state.doc.toString());\n }\n }),\n ],\n }),\n // While editing, the editor owns its content and its authorisation: pinning both keeps an incoming\n // `block.text` update or member-list refresh from rebuilding the view being typed in. `editing` is\n // itself a dep, so the flip still rebuilds.\n [editing, editing ? undefined : block.text, editing ? undefined : isAuthor, themeMode, handleDocumentChange],\n );\n\n useEffect(() => {\n if (editing) {\n view?.focus();\n }\n }, [editing, view]);\n\n return <div ref={parentRef} className='me-4' {...focusAttributes} />;\n};\n\n//\n// Textbox\n//\n\nexport type MessageTextboxProps = {\n disabled?: boolean;\n onSend?: () => void;\n onClear?: () => void;\n onEditorFocus?: () => void;\n} & MessageMetadata &\n UseTextEditorProps;\n\nconst keyBindings = ({ onSend, onClear }: Pick<MessageTextboxProps, 'onSend' | 'onClear'>) => [\n {\n key: 'Enter',\n run: () => {\n if (onSend) {\n onSend();\n return true;\n }\n return false;\n },\n },\n {\n key: 'Meta+Backspace',\n run: () => {\n if (onClear) {\n onClear();\n return true;\n }\n return false;\n },\n },\n];\n\nexport type MessageTextboxHandle = { focus: () => void };\n\nconst MessageTextbox = forwardRef<MessageTextboxHandle, MessageTextboxProps>(\n (\n {\n id,\n authorId,\n authorName,\n authorImgSrc,\n authorAvatarProps,\n disabled,\n extensions,\n onSend,\n onClear,\n onEditorFocus,\n ...editorProps\n },\n forwardedRef,\n ) => {\n const { parentRef, focusAttributes, view } = useTextEditor(\n () => ({\n id,\n extensions: [\n keymap.of(keyBindings({ onSend, onClear })),\n listener({\n onFocus: ({ focusing }) => {\n if (focusing) {\n onEditorFocus?.();\n }\n },\n }),\n extensions,\n ].filter(isTruthy),\n ...editorProps,\n }),\n [id, extensions],\n );\n\n useImperativeHandle(forwardedRef, () => ({ focus: () => view?.focus() }), [view]);\n\n return (\n <MessageRoot {...{ id, authorId, authorName, authorImgSrc, authorAvatarProps }} continues={false}>\n {/*\n Addressable: the reply composer is the only textbox in a thread that is not a message body,\n so without a testid a test can only guess at it by editor order.\n */}\n <div\n ref={parentRef}\n data-testid='thread.reply'\n className={mx('py-0.5 me-1 rounded-xs dx-focus-ring', disabled && 'opacity-50')}\n {...focusAttributes}\n />\n </MessageRoot>\n );\n },\n);\n\nMessageTextbox.displayName = 'Message.Textbox';\n\n//\n// Tile\n//\n\nexport type MessageTileProps = {\n message: MessageType.Message;\n classNames?: MessageRootProps['classNames'];\n /** Whether the avatar-rail continuation line is drawn below the tile; false for the last tile. */\n continues?: boolean;\n};\n\n/**\n * Full message tile (frame + heading + controls + body). Reads `Thread.Root`\n * context for metadata resolution, injected renderers, and callbacks. This is\n * the unit rendered by `Thread.Messages`.\n */\nconst MessageTile = ({ message, classNames, continues = true }: MessageTileProps) => {\n const { t } = useTranslation(translationKey);\n const {\n getMetadata,\n identityDid,\n editable,\n onMessageDelete,\n onAcceptProposal,\n onAcceptChange,\n onRejectChange,\n onMessageSelect,\n currentMessageId,\n } = useThreadContext('Message.Tile');\n const [editing, setEditing] = useState(false);\n\n const metadata = getMetadata(message);\n const isAuthor = !!identityDid && identityDid === metadata.authorId;\n const hasProposal = message.blocks.some((block) => block._tag === 'proposal');\n const hasChange = message.blocks.some((block) => block._tag === 'change');\n\n const handleEdit = useCallback(() => setEditing((value) => !value), []);\n const handleDelete = useCallback(() => onMessageDelete?.(message.id), [onMessageDelete, message.id]);\n const handleAcceptProposal = useCallback(() => onAcceptProposal?.(message.id), [onAcceptProposal, message.id]);\n const handleAcceptChange = useCallback(() => onAcceptChange?.(message.id), [onAcceptChange, message.id]);\n const handleRejectChange = useCallback(() => onRejectChange?.(message.id), [onRejectChange, message.id]);\n const handleSelect = useCallback(() => onMessageSelect?.(message.id), [onMessageSelect, message.id]);\n const handleSave = useCallback(\n (text: string) => {\n Obj.update(message, (message) => {\n const block = message.blocks.find((block) => block._tag === 'text');\n if (block && block._tag === 'text') {\n block.text = text;\n }\n });\n },\n [message],\n );\n\n const showEdit = isAuthor && editable;\n const showAccept = hasProposal && !!onAcceptProposal;\n const showAcceptChange = hasChange && !!onAcceptChange;\n const showRejectChange = hasChange && !!onRejectChange;\n const showDelete = !!onMessageDelete;\n const controls =\n showEdit || showAccept || showAcceptChange || showRejectChange || showDelete ? (\n <div className={buttonGroupClassNames}>\n {showEdit && (\n <IconButton\n data-testid={editing ? 'thread.message.save' : 'thread.message.edit'}\n variant='ghost'\n icon={editing ? 'ph--check--regular' : 'ph--pencil-simple--regular'}\n iconOnly\n label={t(editing ? 'save-message.label' : 'edit-message.label')}\n classNames={[buttonClassNames, hoverableControlItem]}\n onClick={handleEdit}\n />\n )}\n {showAccept && (\n <IconButton\n data-testid='thread.message.accept'\n variant='ghost'\n icon='ph--check--regular'\n iconOnly\n label={t('accept-proposal.label')}\n classNames={[buttonClassNames, hoverableControlItem]}\n onClick={handleAcceptProposal}\n />\n )}\n {showAcceptChange && (\n <IconButton\n data-testid='thread.message.accept-change'\n variant='ghost'\n icon='ph--check--regular'\n iconOnly\n label={t('accept-change.label')}\n classNames={[buttonClassNames, hoverableControlItem]}\n onClick={handleAcceptChange}\n />\n )}\n {showRejectChange && (\n <IconButton\n data-testid='thread.message.reject-change'\n variant='ghost'\n icon='ph--x--regular'\n iconOnly\n label={t('reject-change.label')}\n classNames={[buttonClassNames, hoverableControlItem]}\n onClick={handleRejectChange}\n />\n )}\n {showDelete && (\n <IconButton\n data-testid='thread.message.delete'\n variant='ghost'\n icon='ph--x--regular'\n iconOnly\n label={t('delete-message.label')}\n classNames={[buttonClassNames, hoverableControlItem]}\n onClick={handleDelete}\n />\n )}\n </div>\n ) : undefined;\n\n return (\n <MessageRoot\n {...metadata}\n continues={continues}\n controls={controls}\n // Selecting a tile is how the host reveals what it refers to (a suggestion's range in the\n // document), so the whole tile is the target — the accent marks which one is showing.\n onClick={onMessageSelect ? handleSelect : undefined}\n aria-current={currentMessageId === message.id ? 'location' : undefined}\n classNames={[\n hoverableControls,\n hoverableFocusedWithinControls,\n onMessageSelect && 'cursor-pointer',\n currentMessageId === message.id && 'bg-current-surface',\n classNames,\n ]}\n >\n <MessageHeading authorName={metadata.authorName} timestamp={metadata.timestamp} />\n <MessageBody message={message} isAuthor={isAuthor} editing={editing} onSave={handleSave} />\n </MessageRoot>\n );\n};\n\nMessageTile.displayName = 'Message.Tile';\n\n//\n// Group\n//\n\nexport type MessageGroupProps = {\n /** Consecutive same-sender messages, in ascending time order, rendered as one tile. */\n messages: readonly MessageType.Message[];\n /** Whether the avatar-rail continuation line is drawn below the tile; false for the last tile. */\n continues?: boolean;\n classNames?: MessageRootProps['classNames'];\n};\n\n/**\n * Groups consecutive same-sender messages (see `Thread.Messages`'s grouping\n * window) into a single tile: one heading (author + first message's\n * timestamp) followed by one body per message, stacked in order. Per-message\n * edit/delete controls are not shown per-row in a group — v1 shows them for\n * the group's first message only, since the heading/controls layout is keyed\n * to a single message.\n */\nconst MessageGroup = ({ messages, continues = true, classNames }: MessageGroupProps) => {\n const { t } = useTranslation(translationKey);\n const { getMetadata, identityDid, editable, onMessageDelete, onAcceptProposal } = useThreadContext('Message.Group');\n const [editing, setEditing] = useState(false);\n\n const first = messages[0];\n const metadata = getMetadata(first);\n const isAuthor = !!identityDid && identityDid === metadata.authorId;\n const hasProposal = first.blocks.some((block) => block._tag === 'proposal');\n\n const handleEdit = useCallback(() => setEditing((value) => !value), []);\n const handleDelete = useCallback(() => onMessageDelete?.(first.id), [onMessageDelete, first.id]);\n const handleAcceptProposal = useCallback(() => onAcceptProposal?.(first.id), [onAcceptProposal, first.id]);\n const handleSave = useCallback(\n (text: string) => {\n Obj.update(first, (first) => {\n const block = first.blocks.find((block) => block._tag === 'text');\n if (block && block._tag === 'text') {\n block.text = text;\n }\n });\n },\n [first],\n );\n\n const showEdit = isAuthor && editable;\n const showAccept = hasProposal && !!onAcceptProposal;\n const showDelete = !!onMessageDelete;\n const controls =\n showEdit || showAccept || showDelete ? (\n <div className={buttonGroupClassNames}>\n {showEdit && (\n <IconButton\n data-testid={editing ? 'thread.message.save' : 'thread.message.edit'}\n variant='ghost'\n icon={editing ? 'ph--check--regular' : 'ph--pencil-simple--regular'}\n iconOnly\n label={t(editing ? 'save-message.label' : 'edit-message.label')}\n classNames={[buttonClassNames, hoverableControlItem]}\n onClick={handleEdit}\n />\n )}\n {showAccept && (\n <IconButton\n data-testid='thread.message.accept'\n variant='ghost'\n icon='ph--check--regular'\n iconOnly\n label={t('accept-proposal.label')}\n classNames={[buttonClassNames, hoverableControlItem]}\n onClick={handleAcceptProposal}\n />\n )}\n {showDelete && (\n <IconButton\n data-testid='thread.message.delete'\n variant='ghost'\n icon='ph--x--regular'\n iconOnly\n label={t('delete-message.label')}\n classNames={[buttonClassNames, hoverableControlItem]}\n onClick={handleDelete}\n />\n )}\n </div>\n ) : undefined;\n\n return (\n <MessageRoot\n {...metadata}\n continues={continues}\n controls={controls}\n classNames={[hoverableControls, hoverableFocusedWithinControls, classNames]}\n >\n <MessageHeading authorName={metadata.authorName} timestamp={metadata.timestamp} />\n {messages.map((message) => (\n <MessageBody\n key={message.id}\n message={message}\n isAuthor={isAuthor}\n editing={editing && message === first}\n onSave={handleSave}\n />\n ))}\n </MessageRoot>\n );\n};\n\nMessageGroup.displayName = 'Message.Group';\n\n//\n// Message\n//\n\nexport const Message = {\n Root: MessageRoot,\n Heading: MessageHeading,\n AuthorName: MessageAuthorName,\n Time: MessageTime,\n Body: MessageBody,\n Textbox: MessageTextbox,\n Tile: MessageTile,\n Group: MessageGroup,\n};\n","//\n// Copyright 2023 DXOS.org\n//\n\nimport { format } from 'date-fns/format';\nimport { type Locale } from 'date-fns/locale';\nimport { startOfDay } from 'date-fns/startOfDay';\nimport React, {\n type ComponentPropsWithRef,\n type PropsWithChildren,\n type ReactNode,\n forwardRef,\n useCallback,\n useEffect,\n useId,\n useMemo,\n useRef,\n useState,\n} from 'react';\n\nimport { Obj } from '@dxos/echo';\nimport {\n type ComposableProps,\n Icon,\n IconButton,\n ScrollArea,\n type ThemedClassName,\n composable,\n composableProps,\n useThemeContext,\n useTranslation,\n} from '@dxos/react-ui';\nimport { type DndContainerHandler } from '@dxos/react-ui-dnd';\nimport { Mosaic, type MosaicTileProps } from '@dxos/react-ui-mosaic';\nimport { type Message as MessageType } from '@dxos/types';\nimport { type Extension, createBasicExtensions, createThemeExtensions, listener } from '@dxos/ui-editor';\nimport { hoverableControlItem, hoverableControls, hoverableFocusedWithinControls, mx } from '@dxos/ui-theme';\n\nimport { translationKey } from '#translations';\n\nimport { command } from '../command';\nimport { ThreadContextProvider, useThreadContext } from '../context';\nimport { Message } from '../Message';\nimport { type MessageMetadata, type ThreadContextValue } from '../types';\n\nconst getMessageId = (message: MessageType.Message) => Obj.getURI(message);\n\n//\n// Root\n//\n\nexport type ThreadRootProps = PropsWithChildren<\n Omit<ThreadContextValue, 'components'> & Partial<Pick<ThreadContextValue, 'components'>>\n>;\n\n/**\n * Headless root of a thread. Provides message-tile context (metadata resolver,\n * injected renderers, callbacks). Requires an ambient `Dnd.Root` ancestor,\n * within which `Thread.Messages` renders its virtual stack. Renders no DOM of\n * its own — wrap the visible thread chrome in `Thread.Content`.\n */\nconst ThreadRoot = ({\n children,\n getMetadata,\n components,\n identityDid,\n editable,\n onMessageDelete,\n onAcceptProposal,\n onAcceptChange,\n onRejectChange,\n onMessageSelect,\n currentMessageId,\n}: ThreadRootProps) => {\n // Composer focus handler registered by Thread.Textbox; invoked by Thread.Header's caret.\n const composerFocus = useRef<(() => void) | undefined>(undefined);\n\n const registerComposerFocus = useCallback((focus: (() => void) | undefined) => {\n composerFocus.current = focus;\n }, []);\n\n const focusComposer = useCallback(() => composerFocus.current?.(), []);\n\n return (\n <ThreadContextProvider\n getMetadata={getMetadata}\n components={components ?? {}}\n identityDid={identityDid}\n editable={editable}\n registerComposerFocus={registerComposerFocus}\n focusComposer={focusComposer}\n onMessageDelete={onMessageDelete}\n onAcceptProposal={onAcceptProposal}\n onAcceptChange={onAcceptChange}\n onRejectChange={onRejectChange}\n onMessageSelect={onMessageSelect}\n currentMessageId={currentMessageId}\n >\n {children}\n </ThreadContextProvider>\n );\n};\n\nThreadRoot.displayName = 'Thread.Root';\n\n//\n// Content\n//\n\ntype ThreadContentExtra = {\n id?: string;\n current?: boolean | string;\n} & Pick<ComponentPropsWithRef<'div'>, 'onClickCapture' | 'onFocusCapture'>;\n\nexport type ThreadContentProps = ComposableProps<ThreadContentExtra>;\n\n/** Visible thread container (the attention surface that hosts header / messages / composer). */\nconst ThreadContent = composable<HTMLDivElement, ThreadContentExtra>(\n ({ children, current, id, ...props }, forwardedRef) => {\n return (\n <div\n {...composableProps(props, {\n role: 'group',\n classNames: [\n 'flex flex-col bg-[var(--surface-bg)] current-related dx-attention-surface [--controls-opacity:0]',\n hoverableFocusedWithinControls,\n ],\n })}\n data-testid='thread'\n id={id}\n aria-current={current ? 'location' : undefined}\n ref={forwardedRef}\n >\n {children}\n </div>\n );\n },\n);\n\nThreadContent.displayName = 'Thread.Content';\n\n//\n// Header\n//\n\nexport type ThreadHeaderProps = Omit<\n ComposableProps<{\n /** Snippet text rendered inside a `<p>` (a string, not a DOM element). */\n title?: string;\n detached?: boolean;\n /** Whether this is the current/selected thread; accents the title. */\n current?: boolean;\n /** Trailing controls rendered in the right column (aligns with message-tile controls). */\n controls?: ReactNode;\n /** Invoked when the caret affordance is activated to select/focus this thread. */\n onSelect?: () => void;\n }>,\n 'children'\n>;\n\n/**\n * Thread header row: caret (rail) · snippet (content) · controls. Owns its own\n * `[rail · 1fr · controls]` grid so the trailing controls align with message-tile\n * controls (which use the same template) — no grid is leaked in from the caller.\n */\nconst ThreadHeader = composable<HTMLDivElement, ThreadHeaderProps>(\n ({ title, detached, current, controls, onSelect, ...props }, forwardedRef) => {\n const { t } = useTranslation(translationKey);\n const { focusComposer } = useThreadContext('Thread.Header');\n const handleSelect = useCallback(() => {\n onSelect?.();\n focusComposer();\n }, [onSelect, focusComposer]);\n\n return (\n <div\n {...composableProps(props, {\n classNames: [\n 'grid grid-cols-[var(--dx-rail-size)_1fr_min-content] items-center',\n hoverableControls,\n hoverableFocusedWithinControls,\n ],\n })}\n ref={forwardedRef}\n >\n <div className='flex items-center justify-center'>\n <IconButton\n iconOnly\n variant='ghost'\n density='sm'\n icon='ph--caret-double-right--regular'\n label={t('select-thread.label')}\n classNames='text-description'\n onClick={handleSelect}\n />\n </div>\n <div className='flex items-center overflow-hidden'>\n <p\n role='heading'\n data-testid='thread.heading'\n className={mx(\n 'me-2 font-medium truncate italic',\n current ? 'text-accent-text' : 'text-description',\n detached && 'line-through decoration-1',\n )}\n >\n {title}\n </p>\n </div>\n {controls}\n </div>\n );\n },\n);\n\nThreadHeader.displayName = 'Thread.Header';\n\n//\n// Messages\n//\n\n/** A run of consecutive same-sender messages (within the grouping window) rendered as one tile. */\ntype MessageGroupItem = { kind: 'group'; id: string; messages: MessageType.Message[]; last?: boolean };\n\n/**\n * A divider row between groups: day-labeled (calendar-day boundary) or plain\n * (a same-day gap exceeding `gapDividerMs`). Day boundaries take priority\n * when both coincide on the same message — see `groupMessages`.\n */\ntype DividerItem = { kind: 'divider'; id: string; label?: string };\n\ntype ThreadItem = MessageGroupItem | DividerItem;\n\nconst getThreadItemId = (item: ThreadItem) => item.id;\n\n/** Stable per-sender key: prefers the DID, then the (deprecated) identity key, then name/email, else 'anon'. */\nconst senderKey = (message: MessageType.Message): string =>\n message.sender.identityDid ?? message.sender.identityKey ?? message.sender.name ?? message.sender.email ?? 'anon';\n\n/** Message send time in epoch ms; malformed/missing `created` sorts as if sent at the epoch. */\nconst messageTime = (message: MessageType.Message): number => {\n const time = Date.parse(message.created);\n return Number.isFinite(time) ? time : 0;\n};\n\n/**\n * Preprocesses ascending, time-ordered `messages` into a flat list of divider\n * and group items for `Thread.Messages`'s virtual stack:\n * - A day divider (labeled with the calendar date) precedes the first message\n * of each calendar day.\n * - A plain divider (no label) marks a same-day gap exceeding `gapDividerMs`.\n * - When both would fall on the same message, only the day divider is emitted.\n * - Consecutive messages from the same sender within `groupWindowMs` of the\n * previous message in the group are merged into one `MessageGroupItem`; a\n * divider always starts a new group.\n */\nconst groupMessages = (\n messages: readonly MessageType.Message[],\n {\n groupWindowMs,\n dayDivider,\n gapDividerMs,\n dtLocale,\n }: { groupWindowMs: number; dayDivider: boolean; gapDividerMs: number; dtLocale?: Locale },\n): ThreadItem[] => {\n const items: ThreadItem[] = [];\n let currentGroup: MessageGroupItem | undefined;\n // Tracks the last message appended to `currentGroup`, since `Array.prototype.at(-1)`\n // on `currentGroup.messages` would need a non-null assertion despite `currentGroup`\n // always being non-empty by construction.\n let lastGroupMessage: MessageType.Message | undefined;\n let prevTime: number | undefined;\n let prevDay: number | undefined;\n\n for (const message of messages) {\n const time = messageTime(message);\n const day = startOfDay(time).getTime();\n\n const dayBoundary = dayDivider && (prevDay === undefined || day !== prevDay);\n const gapBoundary = !dayBoundary && prevTime !== undefined && time - prevTime > gapDividerMs;\n\n if (dayBoundary) {\n items.push({\n kind: 'divider',\n id: `divider:day:${day}`,\n label: format(day, 'EEEE, MMMM d', { locale: dtLocale }),\n });\n currentGroup = undefined;\n } else if (gapBoundary) {\n items.push({ kind: 'divider', id: `divider:gap:${time}` });\n currentGroup = undefined;\n }\n\n const sameSenderAsGroup = currentGroup && lastGroupMessage && senderKey(lastGroupMessage) === senderKey(message);\n const withinGroupWindow = currentGroup && lastGroupMessage && time - messageTime(lastGroupMessage) <= groupWindowMs;\n\n if (currentGroup && sameSenderAsGroup && withinGroupWindow) {\n currentGroup.messages.push(message);\n } else {\n currentGroup = { kind: 'group', id: getMessageId(message), messages: [message] };\n items.push(currentGroup);\n }\n lastGroupMessage = message;\n\n prevTime = time;\n prevDay = day;\n }\n\n // The trailing group is the last message tile; suppress its avatar-rail continuation line.\n if (currentGroup) {\n currentGroup.last = true;\n }\n\n return items;\n};\n\n//\n// Divider\n//\n\nconst ThreadDivider = ({ label }: { label?: string }) =>\n label ? (\n <div className='flex items-center gap-2 px-2 py-2 text-xs text-description'>\n <div className='h-px grow bg-separator' />\n <span className='shrink-0'>{label}</span>\n <div className='h-px grow bg-separator' />\n </div>\n ) : (\n <div className='px-2 py-2'>\n <div className='h-px bg-separator' />\n </div>\n );\n\nThreadDivider.displayName = 'Thread.Divider';\n\nconst ThreadItemAdapter = ({ id, data, location, draggable, current, selected }: MosaicTileProps<ThreadItem>) => (\n <Mosaic.Tile id={id} data={data} location={location} draggable={draggable} current={current} selected={selected}>\n {data.kind === 'divider' ? (\n <ThreadDivider label={data.label} />\n ) : (\n <Message.Group messages={data.messages} continues={!data.last} />\n )}\n </Mosaic.Tile>\n);\n\nexport type ThreadMessagesProps = ThemedClassName<{\n messages: readonly MessageType.Message[];\n /** Stable id of the owning thread; scopes the Mosaic container so multiple threads don't collide. */\n id?: string;\n /** Estimated tile height for the virtualizer. */\n estimateSize?: number;\n currentId?: string;\n /** Consecutive same-sender messages within this window (ms) are merged into one tile. */\n groupWindowMs?: number;\n /** Whether to insert a labeled divider before the first message of each calendar day. */\n dayDivider?: boolean;\n /** A same-day gap exceeding this (ms) inserts an unlabeled divider between groups. */\n gapDividerMs?: number;\n}>;\n\nconst DEFAULT_GROUP_WINDOW_MS = 60_000;\nconst DEFAULT_GAP_DIVIDER_MS = 3 * 60 * 60 * 1000;\n\n/** Virtualized stack of message tiles (via Mosaic), within an internal scroll area. */\nconst ThreadMessages = ({\n messages,\n id,\n estimateSize = 80,\n currentId,\n groupWindowMs = DEFAULT_GROUP_WINDOW_MS,\n dayDivider = true,\n gapDividerMs = DEFAULT_GAP_DIVIDER_MS,\n classNames,\n}: ThreadMessagesProps) => {\n const { dtLocale } = useTranslation(translationKey);\n const [viewport, setViewport] = useState<HTMLElement | null>(null);\n const items = useMemo(\n () => groupMessages(messages.filter(Boolean), { groupWindowMs, dayDivider, gapDividerMs, dtLocale }),\n [messages, groupWindowMs, dayDivider, gapDividerMs, dtLocale],\n );\n // Per-instance id keeps concurrent threads (incl. the same thread mounted twice) distinct in the DnD registry.\n const instanceId = useId();\n const eventHandler = useMemo<DndContainerHandler>(\n () => ({ id: `thread:${id ?? 'anon'}:${instanceId}`, canDrop: () => false }),\n [id, instanceId],\n );\n\n return (\n <Mosaic.Container\n asChild\n orientation='vertical'\n autoScroll={viewport}\n currentId={currentId}\n eventHandler={eventHandler}\n >\n <ScrollArea.Root classNames={mx('col-span-2 flex-1 min-h-0', classNames)} orientation='vertical'>\n <ScrollArea.Viewport ref={setViewport}>\n <Mosaic.VirtualStack\n Tile={ThreadItemAdapter}\n items={items}\n getId={getThreadItemId}\n draggable={false}\n getScrollElement={() => viewport}\n estimateSize={() => estimateSize}\n />\n </ScrollArea.Viewport>\n </ScrollArea.Root>\n </Mosaic.Container>\n );\n};\n\nThreadMessages.displayName = 'Thread.Messages';\n\n//\n// Textbox\n//\n\nexport type ThreadTextboxProps = MessageMetadata & {\n placeholder?: string;\n autoFocus?: boolean;\n disabled?: boolean;\n extensions?: Extension;\n /**\n * Called with the composer content on send. Return `true` to accept (the\n * editor is cleared and remounted), `false` to reject.\n */\n onSend?: (text: string) => boolean;\n};\n\n/** Message composer pinned at the foot of a thread. */\nconst ThreadTextbox = ({ placeholder, autoFocus, disabled, extensions, onSend, ...metadata }: ThreadTextboxProps) => {\n const { t } = useTranslation(translationKey);\n const { themeMode } = useThemeContext();\n const { registerComposerFocus } = useThreadContext('Thread.Textbox');\n const composerRef = useRef<{ focus: () => void } | null>(null);\n const messageRef = useRef('');\n const [count, setCount] = useState(0);\n\n // Expose the composer's focus to Thread.Root so the header caret can focus it.\n useEffect(() => {\n registerComposerFocus(() => composerRef.current?.focus());\n return () => registerComposerFocus(undefined);\n }, [registerComposerFocus]);\n\n const editorExtensions = useMemo(\n () =>\n [\n createBasicExtensions({ placeholder: placeholder ?? t('message.placeholder') }),\n createThemeExtensions({ themeMode }),\n listener({ onChange: ({ text }) => (messageRef.current = text) }),\n command,\n extensions,\n ].filter(Boolean) as Extension[],\n [themeMode, placeholder, t, extensions, count],\n );\n\n const handleSend = useCallback(() => {\n const text = messageRef.current;\n if (!text?.length || !onSend) {\n return;\n }\n if (onSend(text)) {\n messageRef.current = '';\n setCount((value) => value + 1);\n }\n }, []);\n\n return (\n <Message.Textbox\n ref={composerRef}\n {...metadata}\n autoFocus={autoFocus}\n disabled={disabled}\n extensions={editorExtensions}\n onSend={handleSend}\n />\n );\n};\n\nThreadTextbox.displayName = 'Thread.Textbox';\n\n//\n// Status\n//\n\nexport type ThreadStatusProps = ThemedClassName<PropsWithChildren> & {\n activity?: boolean;\n};\n\nconst ThreadStatus = forwardRef<HTMLDivElement, ThreadStatusProps>(\n ({ activity, classNames, children, ...props }, forwardedRef) => {\n const { t } = useTranslation(translationKey);\n return (\n <div\n {...props}\n className={mx(\n 'col-start-2 grid grid-cols-[min-content_1fr_max-content] pb-2 pe-2 text-xs text-description',\n classNames,\n )}\n ref={forwardedRef}\n >\n <Icon\n icon='ph--spinner--bold'\n classNames='w-6 h-4 invisible data-[visible=show]:visible animate-spin-slow'\n data-visible={activity ? 'show' : 'hide'}\n />\n <span className='truncate min-w-0' aria-live='polite'>\n {activity ? children : null}\n </span>\n <span className={mx('text-end', hoverableControlItem)}>{t('enter-to-send.message')}</span>\n </div>\n );\n },\n);\n\nThreadStatus.displayName = 'Thread.Status';\n\n//\n// Thread\n//\n\nexport const Thread = {\n Root: ThreadRoot,\n Content: ThreadContent,\n Header: ThreadHeader,\n Messages: ThreadMessages,\n Textbox: ThreadTextbox,\n Status: ThreadStatus,\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAgBA,IAAM,SAAS,eAAe,OAA0B;CACtD,mBAAmB,EAAE,OAAO,EAAE;CAC9B,QAAQ,QAAQ,UAAU;EACxB,MAAM;EACN,IAAI,OAAO,SAAS,GAClB,OAAO;EAGT,IAAI,MAAM,UAAU,KAAK,OAAO,MAAM,QAAQ,GAC5C,OAAO;EAGT,IAAI,OAAO,MAAM,OAAO,GACtB,OAAO;EAET,OAAO,KAAK;EACZ,OAAO;CACT;AACF,CAAC;AAED,IAAM,SAAS,eAAe,OAAO,CACnC;CACE,KAAK,KAAK;CACV,OAAO,GAAG,uBAAuB;AACnC,GACA;CACE,KAAK,KAAK;CACV,OAAO,GAAG,qBAAqB;AACjC,CACF,CAAC;AAMD,IAAM,QAAQ,WAAW,MAAM,EAC7B,WAAW,EACT,eAAe,SACjB,EACF,CAAC;AAED,IAAa,UAAqB;CAAC;CAAQ,mBAAmB,MAAM;CAAG;AAAK;;;AChC5E,IAAa,CAAC,uBAAuB,oBAGjC,cAA6B,QAAQ;;;ACWzC,IAAM,aAAa;AAEnB,IAAM,wBAAwB;AAC9B,IAAM,mBAAmB;AAWzB,IAAM,cAAc,YAEhB,EACE,cACA,UACA,YACA,mBACA,YAAY,MACZ,UACA,UACA,YACA,GAAG,aAEL,iBACG;CAIH,OACE,oBAAC,OAAO,MAAR,EAAA,UACE,qBAAC,OAAD;EACE,eAAY;EACZ,GAAI;EACJ,WAAW,GAAG,+DAA+D,UAAU;EACvF,KAAK;EAJP,UAAA;GAME,qBAAC,OAAD;IAAK,WAAU;IAAf,UAAA,CACE,oBAAC,OAAO,SAAR;KACE,MAAM;KACN,KAAK,mBAAmB,OAAO,SAAS,YAAY,GAAG;KACvD,UAAU,mBAAmB,SAAS,WAAW,YAAY,GAAG;KAChE,GAAK,gBAAgB,EAAE,QAAQ,aAAa;IAC7C,CAAA,GACA,aAAa,oBAAC,OAAD,EAAK,WAAU,yBAA0B,CAAA,CACpD;;GACL,oBAAC,OAAD;IAAK,WAAU;IAAgB;GAAc,CAAA;GAC5C,YAAY,oBAAC,OAAD;IAAK,WAAU;IAAc,UAAA;GAAc,CAAA;EACrD;CACM,CAAA,EAAA,CAAA;AAEjB,CACF;AAEA,YAAY,cAAc;AAS1B,IAAM,kBAAkB,EAAE,UAAU,YAAY,WAAW,YAAY,GAAG,YAAiC;CACzG,OACE,qBAAC,OAAD;EAAK,GAAI;EAAO,WAAW,GAAG,0BAA0B,UAAU;EAAlE,UAAA,CACE,qBAAC,KAAD;GAAG,WAAU;GAAb,UAAA,CACE,oBAAC,mBAAD,EAA+B,WAAa,CAAA,GAC3C,aAAa,oBAAC,aAAD,EAAwB,UAAY,CAAA,CACjD;EACF,CAAA,GAAA,QACE;;AAET;AAEA,eAAe,cAAc;AAI7B,IAAM,qBAAqB,EAAE,iBAAyC;CACpE,MAAM,EAAE,MAAM,eAAe,cAAc;CAC3C,OACE,oBAAC,OAAO,OAAR;EAAc,YAAW;EACtB,UAAA,cAAc,EAAE,iBAAiB;CACtB,CAAA;AAElB;AAEA,kBAAkB,cAAc;AAIhC,IAAM,eAAe,EAAE,gBAAkC;CACvD,MAAM,EAAE,aAAa,eAAe,cAAc;CAClD,MAAM,KAAK,YAAY,IAAI,KAAK,SAAS,IAAI,KAAA;CAC7C,OACE,oBAAC,QAAD;EAAM,WAAU;EAAgC,UAAU,IAAI,YAAY;EACvE,UAAA,KAAK,OAAO,IAAI,KAAK,EAAE,QAAQ,SAAS,CAAC,IAAI;CAC1C,CAAA;AAEV;AAEA,YAAY,cAAc;;;;;AAqB1B,IAAM,eAAe,EAAE,SAAS,UAAU,SAAS,aAA+B;CAChF,MAAM,EAAE,eAAe,iBAAiB,cAAc;CACtD,MAAM,iBAAiB,QAAQ,OAAO,WAAW,UAAU,MAAM,SAAS,MAAM;CAChF,MAAM,YAAY,mBAAmB,KAAM,QAAQ,OAAO,kBAAwC,KAAA;CAClG,MAAM,gBAAgB,QAAQ,OAAO,MAAM,UAAU,MAAM,SAAS,UAAU;CAC9E,MAAM,cAAc,QAAQ,OAAO,MAAM,UAAU,MAAM,SAAS,QAAQ;CAC1E,MAAM,aAAa,QAAQ,OACxB,QAAQ,UAAU,MAAM,SAAS,WAAW,CAAA,CAC5C,KAAK,UAAW,MAAiC,SAAS;CAC7D,MAAM,SAAS,WAAW;CAE1B,OACE,qBAAA,UAAA,EAAA,UAAA;EACG,aAAa,oBAAC,WAAD;GAAW,OAAO;GAAqB;GAAmB;GAAiB;EAAS,CAAA;EACjG,iBAAiB,oBAAC,OAAD;GAAK,WAAU;GAAe,UAAA,cAAc;EAAU,CAAA;EACvE,eACC,qBAAC,KAAD;GAAG,WAAU;GAAb,UAAA;IACG,YAAY,UAAU,oBAAC,QAAD;KAAM,WAAU;KAA2B,UAAA,YAAY;IAAa,CAAA;IAC1F,YAAY,UAAU,YAAY,SAAS;IAC3C,YAAY,SAAS,oBAAC,QAAD,EAAA,UAAO,YAAY,MAAY,CAAA;GACpD;;EAEJ,UACC,IAAI,MAAM,QAAQ,UAAU,CAAC,CAAC,KAAK,WAAW,UAC5C,oBAAC,QAAD,EAAoB,SAAS,UAA2B,GAA3C,KAA2C,CACzD;CACH,EAAA,CAAA;AAEN;AAEA,YAAY,cAAc;AAE1B,IAAM,aAAa,EACjB,OACA,UACA,SACA,aAMI;CACJ,MAAM,EAAE,cAAc,gBAAgB;CACtC,MAAM,qBAAqB,OAAO,MAAM,IAAI;CAE5C,MAAM,uBAAuB,aAAa,SAAiB;EACzD,mBAAmB,UAAU;CAC/B,GAAG,CAAC,CAAC;CAEL,MAAM,qBAAqB,kBAAkB;EAC3C,SAAS,mBAAmB,OAAO;CACrC,GAAG,CAAC,MAAM,CAAC;CAEX,gBAAgB,SAAS,MAAM,OAAO,kBAAkB;CAExD,MAAM,EAAE,WAAW,iBAAiB,SAAS,qBACpC;EACL,cAAc,MAAM;EACpB,YAAY;GAGV,sBAAsB,EAAE,UAAU,CAAC,QAAQ,CAAC;GAC5C,sBAAsB,EAAE,UAAU,CAAC;GACnC;GACA,WAAW,eAAe,IAAI,WAAW;IACvC,IAAI,OAAO,YACT,qBAAqB,OAAO,MAAM,IAAI,SAAS,CAAC;GAEpD,CAAC;EACH;CACF,IAIA;EAAC;EAAS,UAAU,KAAA,IAAY,MAAM;EAAM,UAAU,KAAA,IAAY;EAAU;EAAW;CAAoB,CAC7G;CAEA,gBAAgB;EACd,IAAI,SACF,MAAM,MAAM;CAEhB,GAAG,CAAC,SAAS,IAAI,CAAC;CAElB,OAAO,oBAAC,OAAD;EAAK,KAAK;EAAW,WAAU;EAAO,GAAI;CAAkB,CAAA;AACrE;AAcA,IAAM,eAAe,EAAE,QAAQ,cAA+D,CAC5F;CACE,KAAK;CACL,WAAW;EACT,IAAI,QAAQ;GACV,OAAO;GACP,OAAO;EACT;EACA,OAAO;CACT;AACF,GACA;CACE,KAAK;CACL,WAAW;EACT,IAAI,SAAS;GACX,QAAQ;GACR,OAAO;EACT;EACA,OAAO;CACT;AACF,CACF;AAIA,IAAM,iBAAiB,YAEnB,EACE,IACA,UACA,YACA,cACA,mBACA,UACA,YACA,QACA,SACA,eACA,GAAG,eAEL,iBACG;CACH,MAAM,EAAE,WAAW,iBAAiB,SAAS,qBACpC;EACL;EACA,YAAY;GACV,OAAO,GAAG,YAAY;IAAE;IAAQ;GAAQ,CAAC,CAAC;GAC1C,SAAS,EACP,UAAU,EAAE,eAAe;IACzB,IAAI,UACF,gBAAgB;GAEpB,EACF,CAAC;GACD;EACF,CAAC,CAAC,OAAO,QAAQ;EACjB,GAAG;CACL,IACA,CAAC,IAAI,UAAU,CACjB;CAEA,oBAAoB,qBAAqB,EAAE,aAAa,MAAM,MAAM,EAAE,IAAI,CAAC,IAAI,CAAC;CAEhF,OACE,oBAAC,aAAD;EAAmB;EAAI;EAAU;EAAY;EAAc;EAAqB,WAAW;EAKzF,UAAA,oBAAC,OAAD;GACE,KAAK;GACL,eAAY;GACZ,WAAW,GAAG,wCAAwC,YAAY,YAAY;GAC9E,GAAI;EACL,CAAA;CACU,CAAA;AAEjB,CACF;AAEA,eAAe,cAAc;;;;;;AAkB7B,IAAM,eAAe,EAAE,SAAS,YAAY,YAAY,WAA6B;CACnF,MAAM,EAAE,MAAM,eAAe,cAAc;CAC3C,MAAM,EACJ,aACA,aACA,UACA,iBACA,kBACA,gBACA,gBACA,iBACA,qBACE,iBAAiB,cAAc;CACnC,MAAM,CAAC,SAAS,cAAc,SAAS,KAAK;CAE5C,MAAM,WAAW,YAAY,OAAO;CACpC,MAAM,WAAW,CAAC,CAAC,eAAe,gBAAgB,SAAS;CAC3D,MAAM,cAAc,QAAQ,OAAO,MAAM,UAAU,MAAM,SAAS,UAAU;CAC5E,MAAM,YAAY,QAAQ,OAAO,MAAM,UAAU,MAAM,SAAS,QAAQ;CAExE,MAAM,aAAa,kBAAkB,YAAY,UAAU,CAAC,KAAK,GAAG,CAAC,CAAC;CACtE,MAAM,eAAe,kBAAkB,kBAAkB,QAAQ,EAAE,GAAG,CAAC,iBAAiB,QAAQ,EAAE,CAAC;CACnG,MAAM,uBAAuB,kBAAkB,mBAAmB,QAAQ,EAAE,GAAG,CAAC,kBAAkB,QAAQ,EAAE,CAAC;CAC7G,MAAM,qBAAqB,kBAAkB,iBAAiB,QAAQ,EAAE,GAAG,CAAC,gBAAgB,QAAQ,EAAE,CAAC;CACvG,MAAM,qBAAqB,kBAAkB,iBAAiB,QAAQ,EAAE,GAAG,CAAC,gBAAgB,QAAQ,EAAE,CAAC;CACvG,MAAM,eAAe,kBAAkB,kBAAkB,QAAQ,EAAE,GAAG,CAAC,iBAAiB,QAAQ,EAAE,CAAC;CACnG,MAAM,aAAa,aAChB,SAAiB;EAChB,IAAI,OAAO,UAAU,YAAY;GAC/B,MAAM,QAAQ,QAAQ,OAAO,MAAM,UAAU,MAAM,SAAS,MAAM;GAClE,IAAI,SAAS,MAAM,SAAS,QAC1B,MAAM,OAAO;EAEjB,CAAC;CACH,GACA,CAAC,OAAO,CACV;CAEA,MAAM,WAAW,YAAY;CAC7B,MAAM,aAAa,eAAe,CAAC,CAAC;CACpC,MAAM,mBAAmB,aAAa,CAAC,CAAC;CACxC,MAAM,mBAAmB,aAAa,CAAC,CAAC;CACxC,MAAM,aAAa,CAAC,CAAC;CACrB,MAAM,WACJ,YAAY,cAAc,oBAAoB,oBAAoB,aAChE,qBAAC,OAAD;EAAK,WAAW;EAAhB,UAAA;GACG,YACC,oBAAC,YAAD;IACE,eAAa,UAAU,wBAAwB;IAC/C,SAAQ;IACR,MAAM,UAAU,uBAAuB;IACvC,UAAA;IACA,OAAO,EAAE,UAAU,uBAAuB,oBAAoB;IAC9D,YAAY,CAAC,kBAAkB,oBAAoB;IACnD,SAAS;GACV,CAAA;GAEF,cACC,oBAAC,YAAD;IACE,eAAY;IACZ,SAAQ;IACR,MAAK;IACL,UAAA;IACA,OAAO,EAAE,uBAAuB;IAChC,YAAY,CAAC,kBAAkB,oBAAoB;IACnD,SAAS;GACV,CAAA;GAEF,oBACC,oBAAC,YAAD;IACE,eAAY;IACZ,SAAQ;IACR,MAAK;IACL,UAAA;IACA,OAAO,EAAE,qBAAqB;IAC9B,YAAY,CAAC,kBAAkB,oBAAoB;IACnD,SAAS;GACV,CAAA;GAEF,oBACC,oBAAC,YAAD;IACE,eAAY;IACZ,SAAQ;IACR,MAAK;IACL,UAAA;IACA,OAAO,EAAE,qBAAqB;IAC9B,YAAY,CAAC,kBAAkB,oBAAoB;IACnD,SAAS;GACV,CAAA;GAEF,cACC,oBAAC,YAAD;IACE,eAAY;IACZ,SAAQ;IACR,MAAK;IACL,UAAA;IACA,OAAO,EAAE,sBAAsB;IAC/B,YAAY,CAAC,kBAAkB,oBAAoB;IACnD,SAAS;GACV,CAAA;EAEA;CACH,CAAA,IAAA,KAAA;CAEN,OACE,qBAAC,aAAD;EACE,GAAI;EACO;EACD;EAGV,SAAS,kBAAkB,eAAe,KAAA;EAC1C,gBAAc,qBAAqB,QAAQ,KAAK,aAAa,KAAA;EAC7D,YAAY;GACV;GACA;GACA,mBAAmB;GACnB,qBAAqB,QAAQ,MAAM;GACnC;EACF;EAdF,UAAA,CAgBE,oBAAC,gBAAD;GAAgB,YAAY,SAAS;GAAY,WAAW,SAAS;EAAY,CAAA,GACjF,oBAAC,aAAD;GAAsB;GAAmB;GAAmB;GAAS,QAAQ;EAAa,CAAA,CAC/E;;AAEjB;AAEA,YAAY,cAAc;;;;;;;;;AAsB1B,IAAM,gBAAgB,EAAE,UAAU,YAAY,MAAM,iBAAoC;CACtF,MAAM,EAAE,MAAM,eAAe,cAAc;CAC3C,MAAM,EAAE,aAAa,aAAa,UAAU,iBAAiB,qBAAqB,iBAAiB,eAAe;CAClH,MAAM,CAAC,SAAS,cAAc,SAAS,KAAK;CAE5C,MAAM,QAAQ,SAAS;CACvB,MAAM,WAAW,YAAY,KAAK;CAClC,MAAM,WAAW,CAAC,CAAC,eAAe,gBAAgB,SAAS;CAC3D,MAAM,cAAc,MAAM,OAAO,MAAM,UAAU,MAAM,SAAS,UAAU;CAE1E,MAAM,aAAa,kBAAkB,YAAY,UAAU,CAAC,KAAK,GAAG,CAAC,CAAC;CACtE,MAAM,eAAe,kBAAkB,kBAAkB,MAAM,EAAE,GAAG,CAAC,iBAAiB,MAAM,EAAE,CAAC;CAC/F,MAAM,uBAAuB,kBAAkB,mBAAmB,MAAM,EAAE,GAAG,CAAC,kBAAkB,MAAM,EAAE,CAAC;CACzG,MAAM,aAAa,aAChB,SAAiB;EAChB,IAAI,OAAO,QAAQ,UAAU;GAC3B,MAAM,QAAQ,MAAM,OAAO,MAAM,UAAU,MAAM,SAAS,MAAM;GAChE,IAAI,SAAS,MAAM,SAAS,QAC1B,MAAM,OAAO;EAEjB,CAAC;CACH,GACA,CAAC,KAAK,CACR;CAEA,MAAM,WAAW,YAAY;CAC7B,MAAM,aAAa,eAAe,CAAC,CAAC;CACpC,MAAM,aAAa,CAAC,CAAC;CACrB,MAAM,WACJ,YAAY,cAAc,aACxB,qBAAC,OAAD;EAAK,WAAW;EAAhB,UAAA;GACG,YACC,oBAAC,YAAD;IACE,eAAa,UAAU,wBAAwB;IAC/C,SAAQ;IACR,MAAM,UAAU,uBAAuB;IACvC,UAAA;IACA,OAAO,EAAE,UAAU,uBAAuB,oBAAoB;IAC9D,YAAY,CAAC,kBAAkB,oBAAoB;IACnD,SAAS;GACV,CAAA;GAEF,cACC,oBAAC,YAAD;IACE,eAAY;IACZ,SAAQ;IACR,MAAK;IACL,UAAA;IACA,OAAO,EAAE,uBAAuB;IAChC,YAAY,CAAC,kBAAkB,oBAAoB;IACnD,SAAS;GACV,CAAA;GAEF,cACC,oBAAC,YAAD;IACE,eAAY;IACZ,SAAQ;IACR,MAAK;IACL,UAAA;IACA,OAAO,EAAE,sBAAsB;IAC/B,YAAY,CAAC,kBAAkB,oBAAoB;IACnD,SAAS;GACV,CAAA;EAEA;CACH,CAAA,IAAA,KAAA;CAEN,OACE,qBAAC,aAAD;EACE,GAAI;EACO;EACD;EACV,YAAY;GAAC;GAAmB;GAAgC;EAAU;EAJ5E,UAAA,CAME,oBAAC,gBAAD;GAAgB,YAAY,SAAS;GAAY,WAAW,SAAS;EAAY,CAAA,GAChF,SAAS,KAAK,YACb,oBAAC,aAAD;GAEW;GACC;GACV,SAAS,WAAW,YAAY;GAChC,QAAQ;EACT,GALM,QAAQ,EAKd,CACF,CACU;;AAEjB;AAEA,aAAa,cAAc;AAM3B,IAAa,UAAU;CACrB,MAAM;CACN,SAAS;CACT,YAAY;CACZ,MAAM;CACN,MAAM;CACN,SAAS;CACT,MAAM;CACN,OAAO;AACT;;;ACzjBA,IAAM,gBAAgB,YAAiC,IAAI,OAAO,OAAO;;;;;;;AAgBzE,IAAM,cAAc,EAClB,UACA,aACA,YACA,aACA,UACA,iBACA,kBACA,gBACA,gBACA,iBACA,uBACqB;CAErB,MAAM,gBAAgB,OAAiC,KAAA,CAAS;CAEhE,MAAM,wBAAwB,aAAa,UAAoC;EAC7E,cAAc,UAAU;CAC1B,GAAG,CAAC,CAAC;CAEL,MAAM,gBAAgB,kBAAkB,cAAc,UAAU,GAAG,CAAC,CAAC;CAErE,OACE,oBAAC,uBAAD;EACe;EACb,YAAY,cAAc,CAAC;EACd;EACH;EACa;EACR;EACE;EACC;EACF;EACA;EACC;EACC;EAEjB;CACoB,CAAA;AAE3B;AAEA,WAAW,cAAc;;AAczB,IAAM,gBAAgB,YACnB,EAAE,UAAU,SAAS,IAAI,GAAG,SAAS,iBAAiB;CACrD,OACE,oBAAC,OAAD;EACE,GAAI,gBAAgB,OAAO;GACzB,MAAM;GACN,YAAY,CACV,oGACA,8BACF;EACF,CAAC;EACD,eAAY;EACR;EACJ,gBAAc,UAAU,aAAa,KAAA;EACrC,KAAK;EAEJ;CACE,CAAA;AAET,CACF;AAEA,cAAc,cAAc;;;;;;AA0B5B,IAAM,eAAe,YAClB,EAAE,OAAO,UAAU,SAAS,UAAU,UAAU,GAAG,SAAS,iBAAiB;CAC5E,MAAM,EAAE,MAAM,eAAe,cAAc;CAC3C,MAAM,EAAE,kBAAkB,iBAAiB,eAAe;CAC1D,MAAM,eAAe,kBAAkB;EACrC,WAAW;EACX,cAAc;CAChB,GAAG,CAAC,UAAU,aAAa,CAAC;CAE5B,OACE,qBAAC,OAAD;EACE,GAAI,gBAAgB,OAAO,EACzB,YAAY;GACV;GACA;GACA;EACF,EACF,CAAC;EACD,KAAK;EARP,UAAA;GAUE,oBAAC,OAAD;IAAK,WAAU;IACb,UAAA,oBAAC,YAAD;KACE,UAAA;KACA,SAAQ;KACR,SAAQ;KACR,MAAK;KACL,OAAO,EAAE,qBAAqB;KAC9B,YAAW;KACX,SAAS;IACV,CAAA;GACE,CAAA;GACL,oBAAC,OAAD;IAAK,WAAU;IACb,UAAA,oBAAC,KAAD;KACE,MAAK;KACL,eAAY;KACZ,WAAW,GACT,oCACA,UAAU,qBAAqB,oBAC/B,YAAY,2BACd;KAEC,UAAA;IACA,CAAA;GACA,CAAA;GACJ;EACE;;AAET,CACF;AAEA,aAAa,cAAc;AAkB3B,IAAM,mBAAmB,SAAqB,KAAK;;AAGnD,IAAM,aAAa,YACjB,QAAQ,OAAO,eAAe,QAAQ,OAAO,eAAe,QAAQ,OAAO,QAAQ,QAAQ,OAAO,SAAS;;AAG7G,IAAM,eAAe,YAAyC;CAC5D,MAAM,OAAO,KAAK,MAAM,QAAQ,OAAO;CACvC,OAAO,OAAO,SAAS,IAAI,IAAI,OAAO;AACxC;;;;;;;;;;;;AAaA,IAAM,iBACJ,UACA,EACE,eACA,YACA,cACA,eAEe;CACjB,MAAM,QAAsB,CAAC;CAC7B,IAAI;CAIJ,IAAI;CACJ,IAAI;CACJ,IAAI;CAEJ,KAAK,MAAM,WAAW,UAAU;EAC9B,MAAM,OAAO,YAAY,OAAO;EAChC,MAAM,MAAM,WAAW,IAAI,CAAC,CAAC,QAAQ;EAErC,MAAM,cAAc,eAAe,YAAY,KAAA,KAAa,QAAQ;EACpE,MAAM,cAAc,CAAC,eAAe,aAAa,KAAA,KAAa,OAAO,WAAW;EAEhF,IAAI,aAAa;GACf,MAAM,KAAK;IACT,MAAM;IACN,IAAI,eAAe;IACnB,OAAO,OAAO,KAAK,gBAAgB,EAAE,QAAQ,SAAS,CAAC;GACzD,CAAC;GACD,eAAe,KAAA;EACjB,OAAO,IAAI,aAAa;GACtB,MAAM,KAAK;IAAE,MAAM;IAAW,IAAI,eAAe;GAAO,CAAC;GACzD,eAAe,KAAA;EACjB;EAEA,MAAM,oBAAoB,gBAAgB,oBAAoB,UAAU,gBAAgB,MAAM,UAAU,OAAO;EAC/G,MAAM,oBAAoB,gBAAgB,oBAAoB,OAAO,YAAY,gBAAgB,KAAK;EAEtG,IAAI,gBAAgB,qBAAqB,mBACvC,aAAa,SAAS,KAAK,OAAO;OAC7B;GACL,eAAe;IAAE,MAAM;IAAS,IAAI,aAAa,OAAO;IAAG,UAAU,CAAC,OAAO;GAAE;GAC/E,MAAM,KAAK,YAAY;EACzB;EACA,mBAAmB;EAEnB,WAAW;EACX,UAAU;CACZ;CAGA,IAAI,cACF,aAAa,OAAO;CAGtB,OAAO;AACT;AAMA,IAAM,iBAAiB,EAAE,YACvB,QACE,qBAAC,OAAD;CAAK,WAAU;CAAf,UAAA;EACE,oBAAC,OAAD,EAAK,WAAU,yBAA0B,CAAA;EACzC,oBAAC,QAAD;GAAM,WAAU;GAAY,UAAA;EAAY,CAAA;EACxC,oBAAC,OAAD,EAAK,WAAU,yBAA0B,CAAA;CACtC;AAEL,CAAA,IAAA,oBAAC,OAAD;CAAK,WAAU;CACb,UAAA,oBAAC,OAAD,EAAK,WAAU,oBAAqB,CAAA;AACjC,CAAA;AAGT,cAAc,cAAc;AAE5B,IAAM,qBAAqB,EAAE,IAAI,MAAM,UAAU,WAAW,SAAS,eACnE,oBAAC,OAAO,MAAR;CAAiB;CAAU;CAAgB;CAAqB;CAAoB;CAAmB;CACpG,UAAA,KAAK,SAAS,YACb,oBAAC,eAAD,EAAe,OAAO,KAAK,MAAQ,CAAA,IAEnC,oBAAC,QAAQ,OAAT;EAAe,UAAU,KAAK;EAAU,WAAW,CAAC,KAAK;CAAO,CAAA;AAEvD,CAAA;AAkBf,IAAM,0BAA0B;AAChC,IAAM,yBAAyB;;AAG/B,IAAM,kBAAkB,EACtB,UACA,IACA,eAAe,IACf,WACA,gBAAgB,yBAChB,aAAa,MACb,eAAe,wBACf,iBACyB;CACzB,MAAM,EAAE,aAAa,eAAe,cAAc;CAClD,MAAM,CAAC,UAAU,eAAe,SAA6B,IAAI;CACjE,MAAM,QAAQ,cACN,cAAc,SAAS,OAAO,OAAO,GAAG;EAAE;EAAe;EAAY;EAAc;CAAS,CAAC,GACnG;EAAC;EAAU;EAAe;EAAY;EAAc;CAAQ,CAC9D;CAEA,MAAM,aAAa,MAAM;CACzB,MAAM,eAAe,eACZ;EAAE,IAAI,UAAU,MAAM,OAAO,GAAG;EAAc,eAAe;CAAM,IAC1E,CAAC,IAAI,UAAU,CACjB;CAEA,OACE,oBAAC,OAAO,WAAR;EACE,SAAA;EACA,aAAY;EACZ,YAAY;EACD;EACG;EAEd,UAAA,oBAAC,WAAW,MAAZ;GAAiB,YAAY,GAAG,6BAA6B,UAAU;GAAG,aAAY;GACpF,UAAA,oBAAC,WAAW,UAAZ;IAAqB,KAAK;IACxB,UAAA,oBAAC,OAAO,cAAR;KACE,MAAM;KACC;KACP,OAAO;KACP,WAAW;KACX,wBAAwB;KACxB,oBAAoB;IACrB,CAAA;GACkB,CAAA;EACN,CAAA;CACD,CAAA;AAEtB;AAEA,eAAe,cAAc;;AAmB7B,IAAM,iBAAiB,EAAE,aAAa,WAAW,UAAU,YAAY,QAAQ,GAAG,eAAmC;CACnH,MAAM,EAAE,MAAM,eAAe,cAAc;CAC3C,MAAM,EAAE,cAAc,gBAAgB;CACtC,MAAM,EAAE,0BAA0B,iBAAiB,gBAAgB;CACnE,MAAM,cAAc,OAAqC,IAAI;CAC7D,MAAM,aAAa,OAAO,EAAE;CAC5B,MAAM,CAAC,OAAO,YAAY,SAAS,CAAC;CAGpC,gBAAgB;EACd,4BAA4B,YAAY,SAAS,MAAM,CAAC;EACxD,aAAa,sBAAsB,KAAA,CAAS;CAC9C,GAAG,CAAC,qBAAqB,CAAC;CAE1B,MAAM,mBAAmB,cAErB;EACE,sBAAsB,EAAE,aAAa,eAAe,EAAE,qBAAqB,EAAE,CAAC;EAC9E,sBAAsB,EAAE,UAAU,CAAC;EACnC,SAAS,EAAE,WAAW,EAAE,WAAY,WAAW,UAAU,KAAM,CAAC;EAChE;EACA;CACF,CAAC,CAAC,OAAO,OAAO,GAClB;EAAC;EAAW;EAAa;EAAG;EAAY;CAAK,CAC/C;CAEA,MAAM,aAAa,kBAAkB;EACnC,MAAM,OAAO,WAAW;EACxB,IAAI,CAAC,MAAM,UAAU,CAAC,QACpB;EAEF,IAAI,OAAO,IAAI,GAAG;GAChB,WAAW,UAAU;GACrB,UAAU,UAAU,QAAQ,CAAC;EAC/B;CACF,GAAG,CAAC,CAAC;CAEL,OACE,oBAAC,QAAQ,SAAT;EACE,KAAK;EACL,GAAI;EACO;EACD;EACV,YAAY;EACZ,QAAQ;CACT,CAAA;AAEL;AAEA,cAAc,cAAc;AAU5B,IAAM,eAAe,YAClB,EAAE,UAAU,YAAY,UAAU,GAAG,SAAS,iBAAiB;CAC9D,MAAM,EAAE,MAAM,eAAe,cAAc;CAC3C,OACE,qBAAC,OAAD;EACE,GAAI;EACJ,WAAW,GACT,+FACA,UACF;EACA,KAAK;EANP,UAAA;GAQE,oBAAC,MAAD;IACE,MAAK;IACL,YAAW;IACX,gBAAc,WAAW,SAAS;GACnC,CAAA;GACD,oBAAC,QAAD;IAAM,WAAU;IAAmB,aAAU;IAC1C,UAAA,WAAW,WAAW;GACnB,CAAA;GACN,oBAAC,QAAD;IAAM,WAAW,GAAG,YAAY,oBAAoB;IAAI,UAAA,EAAE,uBAAuB;GAAQ,CAAA;EACtF;;AAET,CACF;AAEA,aAAa,cAAc;AAM3B,IAAa,SAAS;CACpB,MAAM;CACN,SAAS;CACT,QAAQ;CACR,UAAU;CACV,SAAS;CACT,QAAQ;AACV"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../../src/command.ts","../../src/context.ts","../../src/Message/Message.tsx","../../src/Thread/Thread.tsx"],"sourcesContent":["//\n// Copyright 2024 DXOS.org\n//\n\nimport { HighlightStyle, StreamLanguage, syntaxHighlighting } from '@codemirror/language';\nimport { type Extension } from '@codemirror/state';\nimport { EditorView } from '@codemirror/view';\n\nimport { tags } from '@dxos/ui-editor';\nimport { mx } from '@dxos/ui-theme';\n\n/**\n * Highlights leading slash-commands (e.g. `/resolve`) and `@mention` tags\n * (e.g. `@Kai`, anywhere) in the message composer.\n * https://github.com/codemirror/stream-parser/blob/main/test/test-stream-parser.ts\n */\nconst parser = StreamLanguage.define<{ count: number }>({\n startState: () => ({ count: 0 }),\n token: (stream, state) => {\n state.count++;\n if (stream.eatSpace()) {\n return null;\n }\n // Leading slash-command (first token only).\n if (state.count === 1 && stream.match(/^\\/\\w+/)) {\n return 'tagName';\n }\n // @mention (anywhere).\n if (stream.match(/^@\\w+/)) {\n return 'labelName';\n }\n stream.next();\n return null;\n },\n});\n\nconst styles = HighlightStyle.define([\n {\n tag: tags.tagName,\n class: mx('dx-tag dx-tag-inline dx-tag-indigo mx-0.5'),\n },\n {\n tag: tags.labelName,\n class: mx('dx-tag dx-tag-inline dx-tag-blue mx-0.5'),\n },\n]);\n\n// Center the inline `.dx-tag` pills on the text line. An inline tag is an inline box\n// with vertical padding, so on a CodeMirror line it baseline-aligns and rides\n// high; overriding `vertical-align` recentres it (cf. the `cm-reference-pill`\n// precedent in `@dxos/react-ui-chat`).\nconst theme = EditorView.theme({\n '.dx-tag': {\n verticalAlign: 'bottom',\n },\n});\n\nexport const command: Extension = [parser, syntaxHighlighting(styles), theme];\n","//\n// Copyright 2026 DXOS.org\n//\n\nimport { type FC, type ReactNode } from 'react';\n\nimport { createContext } from '@dxos/react-hooks';\n// Imported so TypeScript can name this type in the emitted .d.ts (ThreadContext → Message).\n// eslint-disable-next-line unused-imports/no-unused-imports\nimport { type Message } from '@dxos/types';\n\nimport { type ThreadContextValue } from './types.ts';\n\n/**\n * Context provided by `Thread.Root` and consumed by its parts. Extends the\n * consumer-facing {@link ThreadContextValue} with internal coordination (e.g.\n * focusing the composer from the header's select affordance).\n */\nexport type ThreadContext = ThreadContextValue & {\n /** Register the composer's focus handler (set by `Thread.Textbox`). */\n registerComposerFocus: (focus: (() => void) | undefined) => void;\n /** Focus the thread composer (used by `Thread.Header`'s select affordance). */\n focusComposer: () => void;\n};\n\nexport const [ThreadContextProvider, useThreadContext]: readonly [\n FC<ThreadContext & { children: ReactNode }>,\n (consumerName: string) => ThreadContext,\n] = createContext<ThreadContext>('Thread');\n","//\n// Copyright 2023 DXOS.org\n//\n\nimport { EditorView } from '@codemirror/view';\nimport { format } from 'date-fns/format';\nimport React, {\n type ComponentPropsWithoutRef,\n type ComponentPropsWithRef,\n type ReactNode,\n forwardRef,\n useCallback,\n useEffect,\n useImperativeHandle,\n useRef,\n useState,\n} from 'react';\n\nimport { Obj, Ref } from '@dxos/echo';\nimport { type UseTextEditorProps, useTextEditor } from '@dxos/react-ui-editor';\nimport * as Avatar from '@dxos/react-ui/Avatar';\nimport * as Button from '@dxos/react-ui/Button';\nimport * as Hooks from '@dxos/react-ui/Hooks';\nimport type * as Util from '@dxos/react-ui/Util';\nimport { type ContentBlock, type Message as MessageType } from '@dxos/types';\nimport { createBasicExtensions, createThemeExtensions, keymap, listener } from '@dxos/ui-editor';\nimport { hoverableControlItem, hoverableControls, hoverableFocusedWithinControls, mx } from '@dxos/ui-theme';\nimport { hexToEmoji, hexToHue, isTruthy } from '@dxos/util';\n\nimport { translationKey } from '#translations';\n\nimport { command } from '../command.ts';\nimport { useThreadContext } from '../context.ts';\nimport { type MessageMetadata } from '../types.ts';\n\nconst avatarSize = 'md';\n\nconst buttonGroupClassNames = 'flex flex-row items-center gap-0.5 pe-2';\nconst buttonClassNames = 'p-1! transition-opacity';\n\n//\n// Root\n//\n\nexport type MessageRootProps = Util.ThemedClassName<\n ComponentPropsWithRef<'div'> & MessageMetadata & Partial<{ continues: boolean; controls: ReactNode }>\n>;\n\n// TODO(burdon): Show authorName on tooltip.\nconst MessageRoot = forwardRef<HTMLDivElement, MessageRootProps>(\n (\n {\n authorImgSrc,\n authorId,\n authorName,\n authorAvatarProps,\n continues = true,\n controls,\n children,\n classNames,\n ...rootProps\n },\n forwardedRef,\n ) => {\n // Columns mirror Thread.Header (avatar/rail · content · controls) so trailing\n // controls align with the thread header's controls.\n return (\n <div\n data-testid='thread.message'\n {...rootProps}\n className={mx('grid grid-cols-[var(--dx-rail-size)_1fr_min-content] w-full', classNames)}\n ref={forwardedRef}\n >\n <div className='flex flex-col items-center gap-2 pt-1'>\n <Avatar.Root\n size={avatarSize}\n hue={authorAvatarProps?.hue || hexToHue(authorId ?? '0')}\n fallback={authorAvatarProps?.emoji || hexToEmoji(authorId ?? '0')}\n src={authorImgSrc}\n label={authorName}\n />\n {continues && <div className='w-px grow bg-separator' />}\n </div>\n <div className='py-1 min-w-0'>{children}</div>\n {controls && <div className='self-start'>{controls}</div>}\n </div>\n );\n },\n);\n\nMessageRoot.displayName = 'Message.Root';\n\n//\n// Heading\n//\n\nexport type MessageHeadingProps = Util.ThemedClassName<ComponentPropsWithoutRef<'div'>> &\n Pick<MessageMetadata, 'authorName' | 'timestamp'>;\n\nconst MessageHeading = ({ children, classNames, timestamp, authorName, ...props }: MessageHeadingProps) => {\n return (\n <div {...props} className={mx('flex gap-2 items-start', classNames)}>\n <p className='grow flex items-baseline gap-2 min-w-0'>\n <MessageAuthorName authorName={authorName} />\n {timestamp && <MessageTime timestamp={timestamp} />}\n </p>\n {children}\n </div>\n );\n};\n\nMessageHeading.displayName = 'Message.Heading';\n\nexport type MessageAuthorNameProps = Pick<MessageMetadata, 'authorName'>;\n\nconst MessageAuthorName = ({ authorName }: MessageAuthorNameProps) => {\n const { t } = Hooks.useTranslation(translationKey);\n return (\n <span className='block truncate min-w-0 shrink text-sm text-fg-subtle'>{authorName ?? t('anonymous.label')}</span>\n );\n};\n\nMessageAuthorName.displayName = 'Message.AuthorName';\n\nexport type MessageTimeProps = Pick<MessageMetadata, 'timestamp'>;\n\nconst MessageTime = ({ timestamp }: MessageTimeProps) => {\n const { dtLocale } = Hooks.useTranslation(translationKey);\n const dt = timestamp ? new Date(timestamp) : undefined;\n return (\n <time className='shrink-0 text-fg-subtle text-xs' dateTime={dt?.toISOString()}>\n {dt ? format(dt, 'p', { locale: dtLocale }) : ''}\n </time>\n );\n};\n\nMessageTime.displayName = 'Message.Time';\n\n//\n// Body\n//\n\nexport type MessageBodyProps = {\n /** The message whose content blocks are rendered. */\n message: MessageType.Message;\n /** When true, the local user authored this message. */\n isAuthor?: boolean;\n /** When true, the text block is editable. */\n editing?: boolean;\n /** Persist an edited text block. */\n onSave?: (text: string) => void;\n};\n\n/**\n * Renders a message's content blocks: text (via editor), proposal, and\n * object/reference tiles (delegated to the injected `Object` component).\n */\nconst MessageBody = ({ message, isAuthor, editing, onSave }: MessageBodyProps) => {\n const { components } = useThreadContext('Message.Body');\n const textBlockIndex = message.blocks.findIndex((block) => block._tag === 'text');\n const textBlock = textBlockIndex !== -1 ? (message.blocks[textBlockIndex] as ContentBlock.Text) : undefined;\n const proposalBlock = message.blocks.find((block) => block._tag === 'proposal') as ContentBlock.Proposal | undefined;\n const changeBlock = message.blocks.find((block) => block._tag === 'change') as ContentBlock.Change | undefined;\n const references = message.blocks\n .filter((block) => block._tag === 'reference')\n .map((block) => (block as ContentBlock.Reference).reference);\n const Object = components.Object;\n\n return (\n <>\n {textBlock && <TextBlock block={textBlock} isAuthor={isAuthor} editing={editing} onSave={onSave} />}\n {proposalBlock && <div className='me-4 italic'>{proposalBlock.text}</div>}\n {changeBlock && (\n <p className='me-4 text-sm break-words'>\n {changeBlock.before && <span className='line-through opacity-60'>{changeBlock.before}</span>}\n {changeBlock.before && changeBlock.after && ' '}\n {changeBlock.after && <span>{changeBlock.after}</span>}\n </p>\n )}\n {Object &&\n Ref.Array.targets(references).map((reference, index) => (\n <Object key={index} subject={reference as Obj.Unknown} />\n ))}\n </>\n );\n};\n\nMessageBody.displayName = 'Message.Body';\n\nconst TextBlock = ({\n block,\n isAuthor,\n editing,\n onSave,\n}: {\n block: ContentBlock.Text;\n isAuthor?: boolean;\n editing?: boolean;\n onSave?: (text: string) => void;\n}) => {\n const themeMode = Hooks.useThemeMode();\n const inMemoryContentRef = useRef(block.text);\n\n const handleDocumentChange = useCallback((next: string) => {\n inMemoryContentRef.current = next;\n }, []);\n\n const saveDocumentChange = useCallback(() => {\n onSave?.(inMemoryContentRef.current);\n }, [onSave]);\n\n Hooks.useOnTransition(editing, true, false, saveDocumentChange);\n\n const { parentRef, focusAttributes, view } = useTextEditor(\n () => ({\n initialValue: block.text,\n extensions: [\n // Edit mode is authorisation enough — the control that gets here is already gated on\n // `isAuthor` — and a mid-edit flip to read-only makes the editor drop input silently.\n createBasicExtensions({ readOnly: !editing }),\n createThemeExtensions({ themeMode }),\n command,\n EditorView.updateListener.of((update) => {\n if (update.docChanged) {\n handleDocumentChange(update.state.doc.toString());\n }\n }),\n ],\n }),\n // While editing, the editor owns its content and its authorisation: pinning both keeps an incoming\n // `block.text` update or member-list refresh from rebuilding the view being typed in. `editing` is\n // itself a dep, so the flip still rebuilds.\n [editing, editing ? undefined : block.text, editing ? undefined : isAuthor, themeMode, handleDocumentChange],\n );\n\n useEffect(() => {\n if (editing) {\n view?.focus();\n }\n }, [editing, view]);\n\n return <div ref={parentRef} className='me-4' {...focusAttributes} />;\n};\n\n//\n// Textbox\n//\n\nexport type MessageTextboxProps = {\n disabled?: boolean;\n onSend?: () => void;\n onClear?: () => void;\n onEditorFocus?: () => void;\n} & MessageMetadata &\n UseTextEditorProps;\n\nconst keyBindings = ({ onSend, onClear }: Pick<MessageTextboxProps, 'onSend' | 'onClear'>) => [\n {\n key: 'Enter',\n run: () => {\n if (onSend) {\n onSend();\n return true;\n }\n return false;\n },\n },\n {\n key: 'Meta+Backspace',\n run: () => {\n if (onClear) {\n onClear();\n return true;\n }\n return false;\n },\n },\n];\n\nexport type MessageTextboxHandle = { focus: () => void };\n\nconst MessageTextbox = forwardRef<MessageTextboxHandle, MessageTextboxProps>(\n (\n {\n id,\n authorId,\n authorName,\n authorImgSrc,\n authorAvatarProps,\n disabled,\n extensions,\n onSend,\n onClear,\n onEditorFocus,\n ...editorProps\n },\n forwardedRef,\n ) => {\n const { parentRef, focusAttributes, view } = useTextEditor(\n () => ({\n id,\n extensions: [\n keymap.of(keyBindings({ onSend, onClear })),\n listener({\n onFocus: ({ focusing }) => {\n if (focusing) {\n onEditorFocus?.();\n }\n },\n }),\n extensions,\n ].filter(isTruthy),\n ...editorProps,\n }),\n [id, extensions],\n );\n\n useImperativeHandle(forwardedRef, () => ({ focus: () => view?.focus() }), [view]);\n\n return (\n <MessageRoot {...{ id, authorId, authorName, authorImgSrc, authorAvatarProps }} continues={false}>\n {/*\n Addressable: the reply composer is the only textbox in a thread that is not a message body,\n so without a testid a test can only guess at it by editor order.\n */}\n <div\n ref={parentRef}\n data-testid='thread.reply'\n className={mx('py-0.5 me-1 rounded-xs dx-focus-ring', disabled && 'opacity-50')}\n {...focusAttributes}\n />\n </MessageRoot>\n );\n },\n);\n\nMessageTextbox.displayName = 'Message.Textbox';\n\n//\n// Tile\n//\n\nexport type MessageTileProps = {\n message: MessageType.Message;\n classNames?: MessageRootProps['classNames'];\n /** Whether the avatar-rail continuation line is drawn below the tile; false for the last tile. */\n continues?: boolean;\n};\n\n/**\n * Full message tile (frame + heading + controls + body). Reads `Thread.Root`\n * context for metadata resolution, injected renderers, and callbacks. This is\n * the unit rendered by `Thread.Messages`.\n */\nconst MessageTile = ({ message, classNames, continues = true }: MessageTileProps) => {\n const { t } = Hooks.useTranslation(translationKey);\n const {\n getMetadata,\n identityDid,\n editable,\n onMessageDelete,\n onAcceptProposal,\n onAcceptChange,\n onRejectChange,\n onMessageSelect,\n currentMessageId,\n } = useThreadContext('Message.Tile');\n const [editing, setEditing] = useState(false);\n\n const metadata = getMetadata(message);\n const isAuthor = !!identityDid && identityDid === metadata.authorId;\n const hasProposal = message.blocks.some((block) => block._tag === 'proposal');\n const hasChange = message.blocks.some((block) => block._tag === 'change');\n\n const handleEdit = useCallback(() => setEditing((value) => !value), []);\n const handleDelete = useCallback(() => onMessageDelete?.(message.id), [onMessageDelete, message.id]);\n const handleAcceptProposal = useCallback(() => onAcceptProposal?.(message.id), [onAcceptProposal, message.id]);\n const handleAcceptChange = useCallback(() => onAcceptChange?.(message.id), [onAcceptChange, message.id]);\n const handleRejectChange = useCallback(() => onRejectChange?.(message.id), [onRejectChange, message.id]);\n const handleSelect = useCallback(() => onMessageSelect?.(message.id), [onMessageSelect, message.id]);\n const handleSave = useCallback(\n (text: string) => {\n Obj.update(message, (message) => {\n const block = message.blocks.find((block) => block._tag === 'text');\n if (block && block._tag === 'text') {\n block.text = text;\n }\n });\n },\n [message],\n );\n\n const showEdit = isAuthor && editable;\n const showAccept = hasProposal && !!onAcceptProposal;\n const showAcceptChange = hasChange && !!onAcceptChange;\n const showRejectChange = hasChange && !!onRejectChange;\n const showDelete = !!onMessageDelete;\n const controls =\n showEdit || showAccept || showAcceptChange || showRejectChange || showDelete ? (\n <div className={buttonGroupClassNames}>\n {showEdit && (\n <Button.Root\n data-testid={editing ? 'thread.message.save' : 'thread.message.edit'}\n variant='ghost'\n icon={editing ? 'ph--check--regular' : 'ph--pencil-simple--regular'}\n iconOnly\n label={t(editing ? 'save-message.label' : 'edit-message.label')}\n classNames={[buttonClassNames, hoverableControlItem]}\n onClick={handleEdit}\n />\n )}\n {showAccept && (\n <Button.Root\n data-testid='thread.message.accept'\n variant='ghost'\n icon='ph--check--regular'\n iconOnly\n label={t('accept-proposal.label')}\n classNames={[buttonClassNames, hoverableControlItem]}\n onClick={handleAcceptProposal}\n />\n )}\n {showAcceptChange && (\n <Button.Root\n data-testid='thread.message.accept-change'\n variant='ghost'\n icon='ph--check--regular'\n iconOnly\n label={t('accept-change.label')}\n classNames={[buttonClassNames, hoverableControlItem]}\n onClick={handleAcceptChange}\n />\n )}\n {showRejectChange && (\n <Button.Root\n data-testid='thread.message.reject-change'\n variant='ghost'\n icon='ph--x--regular'\n iconOnly\n label={t('reject-change.label')}\n classNames={[buttonClassNames, hoverableControlItem]}\n onClick={handleRejectChange}\n />\n )}\n {showDelete && (\n <Button.Root\n data-testid='thread.message.delete'\n variant='ghost'\n icon='ph--x--regular'\n iconOnly\n label={t('delete-message.label')}\n classNames={[buttonClassNames, hoverableControlItem]}\n onClick={handleDelete}\n />\n )}\n </div>\n ) : undefined;\n\n return (\n <MessageRoot\n {...metadata}\n continues={continues}\n controls={controls}\n // Selecting a tile is how the host reveals what it refers to (a suggestion's range in the\n // document), so the whole tile is the target — the accent marks which one is showing.\n onClick={onMessageSelect ? handleSelect : undefined}\n aria-current={currentMessageId === message.id ? 'location' : undefined}\n classNames={[\n hoverableControls,\n hoverableFocusedWithinControls,\n onMessageSelect && 'cursor-pointer',\n currentMessageId === message.id && 'bg-current-surface',\n classNames,\n ]}\n >\n <MessageHeading authorName={metadata.authorName} timestamp={metadata.timestamp} />\n <MessageBody message={message} isAuthor={isAuthor} editing={editing} onSave={handleSave} />\n </MessageRoot>\n );\n};\n\nMessageTile.displayName = 'Message.Tile';\n\n//\n// Group\n//\n\nexport type MessageGroupProps = {\n /** Consecutive same-sender messages, in ascending time order, rendered as one tile. */\n messages: readonly MessageType.Message[];\n /** Whether the avatar-rail continuation line is drawn below the tile; false for the last tile. */\n continues?: boolean;\n classNames?: MessageRootProps['classNames'];\n};\n\n/**\n * Groups consecutive same-sender messages (see `Thread.Messages`'s grouping\n * window) into a single tile: one heading (author + first message's\n * timestamp) followed by one body per message, stacked in order. Per-message\n * edit/delete controls are not shown per-row in a group — v1 shows them for\n * the group's first message only, since the heading/controls layout is keyed\n * to a single message.\n */\nconst MessageGroup = ({ messages, continues = true, classNames }: MessageGroupProps) => {\n const { t } = Hooks.useTranslation(translationKey);\n const { getMetadata, identityDid, editable, onMessageDelete, onAcceptProposal } = useThreadContext('Message.Group');\n const [editing, setEditing] = useState(false);\n\n const first = messages[0];\n const metadata = getMetadata(first);\n const isAuthor = !!identityDid && identityDid === metadata.authorId;\n const hasProposal = first.blocks.some((block) => block._tag === 'proposal');\n\n const handleEdit = useCallback(() => setEditing((value) => !value), []);\n const handleDelete = useCallback(() => onMessageDelete?.(first.id), [onMessageDelete, first.id]);\n const handleAcceptProposal = useCallback(() => onAcceptProposal?.(first.id), [onAcceptProposal, first.id]);\n const handleSave = useCallback(\n (text: string) => {\n Obj.update(first, (first) => {\n const block = first.blocks.find((block) => block._tag === 'text');\n if (block && block._tag === 'text') {\n block.text = text;\n }\n });\n },\n [first],\n );\n\n const showEdit = isAuthor && editable;\n const showAccept = hasProposal && !!onAcceptProposal;\n const showDelete = !!onMessageDelete;\n const controls =\n showEdit || showAccept || showDelete ? (\n <div className={buttonGroupClassNames}>\n {showEdit && (\n <Button.Root\n data-testid={editing ? 'thread.message.save' : 'thread.message.edit'}\n variant='ghost'\n icon={editing ? 'ph--check--regular' : 'ph--pencil-simple--regular'}\n iconOnly\n label={t(editing ? 'save-message.label' : 'edit-message.label')}\n classNames={[buttonClassNames, hoverableControlItem]}\n onClick={handleEdit}\n />\n )}\n {showAccept && (\n <Button.Root\n data-testid='thread.message.accept'\n variant='ghost'\n icon='ph--check--regular'\n iconOnly\n label={t('accept-proposal.label')}\n classNames={[buttonClassNames, hoverableControlItem]}\n onClick={handleAcceptProposal}\n />\n )}\n {showDelete && (\n <Button.Root\n data-testid='thread.message.delete'\n variant='ghost'\n icon='ph--x--regular'\n iconOnly\n label={t('delete-message.label')}\n classNames={[buttonClassNames, hoverableControlItem]}\n onClick={handleDelete}\n />\n )}\n </div>\n ) : undefined;\n\n return (\n <MessageRoot\n {...metadata}\n continues={continues}\n controls={controls}\n classNames={[hoverableControls, hoverableFocusedWithinControls, classNames]}\n >\n <MessageHeading authorName={metadata.authorName} timestamp={metadata.timestamp} />\n {messages.map((message) => (\n <MessageBody\n key={message.id}\n message={message}\n isAuthor={isAuthor}\n editing={editing && message === first}\n onSave={handleSave}\n />\n ))}\n </MessageRoot>\n );\n};\n\nMessageGroup.displayName = 'Message.Group';\n\n//\n// Message\n//\n\nexport const Message = {\n Root: MessageRoot,\n Heading: MessageHeading,\n AuthorName: MessageAuthorName,\n Time: MessageTime,\n Body: MessageBody,\n Textbox: MessageTextbox,\n Tile: MessageTile,\n Group: MessageGroup,\n};\n","//\n// Copyright 2023 DXOS.org\n//\n\nimport { format } from 'date-fns/format';\nimport { type Locale } from 'date-fns/locale';\nimport { startOfDay } from 'date-fns/startOfDay';\nimport React, {\n type ComponentPropsWithRef,\n type PropsWithChildren,\n type ReactNode,\n forwardRef,\n useCallback,\n useEffect,\n useId,\n useMemo,\n useRef,\n useState,\n} from 'react';\n\nimport { Obj } from '@dxos/echo';\nimport { type DndContainerHandler } from '@dxos/react-ui-dnd';\nimport { Mosaic, type MosaicTileProps } from '@dxos/react-ui-mosaic';\nimport * as Button from '@dxos/react-ui/Button';\nimport * as Hooks from '@dxos/react-ui/Hooks';\nimport * as Icon from '@dxos/react-ui/Icon';\nimport * as ScrollArea from '@dxos/react-ui/ScrollArea';\nimport * as Util from '@dxos/react-ui/Util';\nimport { type Message as MessageType } from '@dxos/types';\nimport { type Extension, createBasicExtensions, createThemeExtensions, listener } from '@dxos/ui-editor';\nimport { hoverableControlItem, hoverableControls, hoverableFocusedWithinControls, mx } from '@dxos/ui-theme';\nimport { type ComposableProps } from '@dxos/ui-types';\n\nimport { translationKey } from '#translations';\n\nimport { command } from '../command.ts';\nimport { ThreadContextProvider, useThreadContext } from '../context.ts';\nimport { Message } from '../Message/index.ts';\nimport { type MessageMetadata, type ThreadContextValue } from '../types.ts';\n\n//\n// Root\n//\n\nexport type ThreadRootProps = PropsWithChildren<\n Omit<ThreadContextValue, 'components'> & Partial<Pick<ThreadContextValue, 'components'>>\n>;\n\n/**\n * Headless root of a thread. Provides message-tile context (metadata resolver,\n * injected renderers, callbacks). Requires an ambient `Dnd.Root` ancestor,\n * within which `Thread.Messages` renders its virtual stack. Renders no DOM of\n * its own — wrap the visible thread chrome in `Thread.Content`.\n */\nconst ThreadRoot = ({\n children,\n getMetadata,\n components,\n identityDid,\n editable,\n onMessageDelete,\n onAcceptProposal,\n onAcceptChange,\n onRejectChange,\n onMessageSelect,\n currentMessageId,\n}: ThreadRootProps) => {\n // Composer focus handler registered by Thread.Textbox; invoked by Thread.Header's caret.\n const composerFocus = useRef<(() => void) | undefined>(undefined);\n\n const registerComposerFocus = useCallback((focus: (() => void) | undefined) => {\n composerFocus.current = focus;\n }, []);\n\n const focusComposer = useCallback(() => composerFocus.current?.(), []);\n\n return (\n <ThreadContextProvider\n getMetadata={getMetadata}\n components={components ?? {}}\n identityDid={identityDid}\n editable={editable}\n registerComposerFocus={registerComposerFocus}\n focusComposer={focusComposer}\n onMessageDelete={onMessageDelete}\n onAcceptProposal={onAcceptProposal}\n onAcceptChange={onAcceptChange}\n onRejectChange={onRejectChange}\n onMessageSelect={onMessageSelect}\n currentMessageId={currentMessageId}\n >\n {children}\n </ThreadContextProvider>\n );\n};\n\nThreadRoot.displayName = 'Thread.Root';\n\n//\n// Content\n//\n\ntype ThreadContentExtra = {\n id?: string;\n current?: boolean | string;\n} & Pick<ComponentPropsWithRef<'div'>, 'onClickCapture' | 'onFocusCapture'>;\n\nexport type ThreadContentProps = ComposableProps<ThreadContentExtra>;\n\n/** Visible thread container (the attention surface that hosts header / messages / composer). */\nconst ThreadContent = Util.composable<HTMLDivElement, ThreadContentExtra>(\n ({ children, current, id, ...props }, forwardedRef) => {\n return (\n <div\n {...Util.composableProps(props, {\n role: 'group',\n classNames: [\n 'flex flex-col bg-[var(--surface-bg)] current-related dx-attention-surface [--controls-opacity:0]',\n hoverableFocusedWithinControls,\n ],\n })}\n data-testid='thread'\n id={id}\n aria-current={current ? 'location' : undefined}\n ref={forwardedRef}\n >\n {children}\n </div>\n );\n },\n);\n\nThreadContent.displayName = 'Thread.Content';\n\n//\n// Header\n//\n\nexport type ThreadHeaderProps = Omit<\n ComposableProps<{\n /** Snippet text rendered inside a `<p>` (a string, not a DOM element). */\n title?: string;\n detached?: boolean;\n /** Whether this is the current/selected thread; accents the title. */\n current?: boolean;\n /** Trailing controls rendered in the right column (aligns with message-tile controls). */\n controls?: ReactNode;\n /** Invoked when the caret affordance is activated to select/focus this thread. */\n onSelect?: () => void;\n }>,\n 'children'\n>;\n\n/**\n * Thread header row: caret (rail) · snippet (content) · controls. Owns its own\n * `[rail · 1fr · controls]` grid so the trailing controls align with message-tile\n * controls (which use the same template) — no grid is leaked in from the caller.\n */\nconst ThreadHeader = Util.composable<HTMLDivElement, ThreadHeaderProps>(\n ({ title, detached, current, controls, onSelect, ...props }, forwardedRef) => {\n const { t } = Hooks.useTranslation(translationKey);\n const { focusComposer } = useThreadContext('Thread.Header');\n const handleSelect = useCallback(() => {\n onSelect?.();\n focusComposer();\n }, [onSelect, focusComposer]);\n\n return (\n <div\n {...Util.composableProps(props, {\n classNames: [\n 'grid grid-cols-[var(--dx-rail-size)_1fr_min-content] items-center',\n hoverableControls,\n hoverableFocusedWithinControls,\n ],\n })}\n ref={forwardedRef}\n >\n <div className='flex items-center justify-center'>\n <Button.Root\n iconOnly\n variant='ghost'\n size='sm'\n icon='ph--caret-double-right--regular'\n label={t('select-thread.label')}\n classNames='text-fg-muted'\n onClick={handleSelect}\n />\n </div>\n <div className='flex items-center overflow-hidden'>\n <p\n role='heading'\n data-testid='thread.heading'\n className={mx(\n 'me-2 font-medium truncate italic',\n current ? 'text-accent-text' : 'text-fg-muted',\n detached && 'line-through decoration-1',\n )}\n >\n {title}\n </p>\n </div>\n {controls}\n </div>\n );\n },\n);\n\nThreadHeader.displayName = 'Thread.Header';\n\n//\n// Messages\n//\n\n/** A run of consecutive same-sender messages (within the grouping window) rendered as one tile. */\ntype MessageGroupItem = { kind: 'group'; id: string; messages: MessageType.Message[]; last?: boolean };\n\n/**\n * A divider row between groups: day-labeled (calendar-day boundary) or plain\n * (a same-day gap exceeding `gapDividerMs`). Day boundaries take priority\n * when both coincide on the same message — see `groupMessages`.\n */\ntype DividerItem = { kind: 'divider'; id: string; label?: string };\n\ntype ThreadItem = MessageGroupItem | DividerItem;\n\nconst getThreadItemId = (item: ThreadItem) => item.id;\n\n/** Stable per-sender key: prefers the DID, then the (deprecated) identity key, then name/email, else 'anon'. */\nconst senderKey = (message: MessageType.Message): string =>\n message.sender.identityDid ?? message.sender.identityKey ?? message.sender.name ?? message.sender.email ?? 'anon';\n\n/** Message send time in epoch ms; malformed/missing `created` sorts as if sent at the epoch. */\nconst messageTime = (message: MessageType.Message): number => {\n const time = Date.parse(message.created);\n return Number.isFinite(time) ? time : 0;\n};\n\n/**\n * Preprocesses ascending, time-ordered `messages` into a flat list of divider\n * and group items for `Thread.Messages`'s virtual stack:\n * - A day divider (labeled with the calendar date) precedes the first message\n * of each calendar day.\n * - A plain divider (no label) marks a same-day gap exceeding `gapDividerMs`.\n * - When both would fall on the same message, only the day divider is emitted.\n * - Consecutive messages from the same sender within `groupWindowMs` of the\n * previous message in the group are merged into one `MessageGroupItem`; a\n * divider always starts a new group.\n */\nconst groupMessages = (\n messages: readonly MessageType.Message[],\n {\n groupWindowMs,\n dayDivider,\n gapDividerMs,\n dtLocale,\n }: { groupWindowMs: number; dayDivider: boolean; gapDividerMs: number; dtLocale?: Locale },\n): ThreadItem[] => {\n const items: ThreadItem[] = [];\n let currentGroup: MessageGroupItem | undefined;\n // Tracks the last message appended to `currentGroup`, since `Array.prototype.at(-1)`\n // on `currentGroup.messages` would need a non-null assertion despite `currentGroup`\n // always being non-empty by construction.\n let lastGroupMessage: MessageType.Message | undefined;\n let prevTime: number | undefined;\n let prevDay: number | undefined;\n\n for (const message of messages) {\n const time = messageTime(message);\n const day = startOfDay(time).getTime();\n\n const dayBoundary = dayDivider && (prevDay === undefined || day !== prevDay);\n const gapBoundary = !dayBoundary && prevTime !== undefined && time - prevTime > gapDividerMs;\n\n if (dayBoundary) {\n items.push({\n kind: 'divider',\n id: `divider:day:${day}`,\n label: format(day, 'EEEE, MMMM d', { locale: dtLocale }),\n });\n currentGroup = undefined;\n } else if (gapBoundary) {\n items.push({ kind: 'divider', id: `divider:gap:${time}` });\n currentGroup = undefined;\n }\n\n const sameSenderAsGroup = currentGroup && lastGroupMessage && senderKey(lastGroupMessage) === senderKey(message);\n const withinGroupWindow = currentGroup && lastGroupMessage && time - messageTime(lastGroupMessage) <= groupWindowMs;\n\n if (currentGroup && sameSenderAsGroup && withinGroupWindow) {\n currentGroup.messages.push(message);\n } else {\n currentGroup = { kind: 'group', id: Obj.getURI(message), messages: [message] };\n items.push(currentGroup);\n }\n lastGroupMessage = message;\n\n prevTime = time;\n prevDay = day;\n }\n\n // The trailing group is the last message tile; suppress its avatar-rail continuation line.\n if (currentGroup) {\n currentGroup.last = true;\n }\n\n return items;\n};\n\n//\n// Divider\n//\n\nconst ThreadDivider = ({ label }: { label?: string }) =>\n label ? (\n <div className='flex items-center gap-2 px-2 py-2 text-xs text-fg-muted'>\n <div className='h-px grow bg-separator' />\n <span className='shrink-0'>{label}</span>\n <div className='h-px grow bg-separator' />\n </div>\n ) : (\n <div className='px-2 py-2'>\n <div className='h-px bg-separator' />\n </div>\n );\n\nThreadDivider.displayName = 'Thread.Divider';\n\nconst ThreadItemAdapter = ({ id, data, location, draggable, current, selected }: MosaicTileProps<ThreadItem>) => (\n <Mosaic.Tile id={id} data={data} location={location} draggable={draggable} current={current} selected={selected}>\n {data.kind === 'divider' ? (\n <ThreadDivider label={data.label} />\n ) : (\n <Message.Group messages={data.messages} continues={!data.last} />\n )}\n </Mosaic.Tile>\n);\n\nexport type ThreadMessagesProps = Util.ThemedClassName<{\n messages: readonly MessageType.Message[];\n /** Stable id of the owning thread; scopes the Mosaic container so multiple threads don't collide. */\n id?: string;\n /** Estimated tile height for the virtualizer. */\n estimateSize?: number;\n currentId?: string;\n /** Consecutive same-sender messages within this window (ms) are merged into one tile. */\n groupWindowMs?: number;\n /** Whether to insert a labeled divider before the first message of each calendar day. */\n dayDivider?: boolean;\n /** A same-day gap exceeding this (ms) inserts an unlabeled divider between groups. */\n gapDividerMs?: number;\n}>;\n\nconst DEFAULT_GROUP_WINDOW_MS = 60_000;\nconst DEFAULT_GAP_DIVIDER_MS = 3 * 60 * 60 * 1000;\n\n/** Virtualized stack of message tiles (via Mosaic), within an internal scroll area. */\nconst ThreadMessages = ({\n messages,\n id,\n estimateSize = 80,\n currentId,\n groupWindowMs = DEFAULT_GROUP_WINDOW_MS,\n dayDivider = true,\n gapDividerMs = DEFAULT_GAP_DIVIDER_MS,\n classNames,\n}: ThreadMessagesProps) => {\n const { dtLocale } = Hooks.useTranslation(translationKey);\n const [viewport, setViewport] = useState<HTMLElement | null>(null);\n const items = useMemo(\n () => groupMessages(messages.filter(Boolean), { groupWindowMs, dayDivider, gapDividerMs, dtLocale }),\n [messages, groupWindowMs, dayDivider, gapDividerMs, dtLocale],\n );\n // Per-instance id keeps concurrent threads (incl. the same thread mounted twice) distinct in the DnD registry.\n const instanceId = useId();\n const eventHandler = useMemo<DndContainerHandler>(\n () => ({ id: `thread:${id ?? 'anon'}:${instanceId}`, canDrop: () => false }),\n [id, instanceId],\n );\n\n return (\n <Mosaic.Container\n asChild\n orientation='vertical'\n autoScroll={viewport}\n currentId={currentId}\n eventHandler={eventHandler}\n >\n <ScrollArea.Root classNames={mx('col-span-2 dx-grow', classNames)} orientation='vertical'>\n <ScrollArea.Viewport ref={setViewport}>\n <Mosaic.VirtualStack\n Tile={ThreadItemAdapter}\n items={items}\n getId={getThreadItemId}\n draggable={false}\n getScrollElement={() => viewport}\n estimateSize={() => estimateSize}\n />\n </ScrollArea.Viewport>\n </ScrollArea.Root>\n </Mosaic.Container>\n );\n};\n\nThreadMessages.displayName = 'Thread.Messages';\n\n//\n// Textbox\n//\n\nexport type ThreadTextboxProps = MessageMetadata & {\n placeholder?: string;\n autoFocus?: boolean;\n disabled?: boolean;\n extensions?: Extension;\n /**\n * Called with the composer content on send. Return `true` to accept (the\n * editor is cleared and remounted), `false` to reject.\n */\n onSend?: (text: string) => boolean;\n};\n\n/** Message composer pinned at the foot of a thread. */\nconst ThreadTextbox = ({ placeholder, autoFocus, disabled, extensions, onSend, ...metadata }: ThreadTextboxProps) => {\n const { t } = Hooks.useTranslation(translationKey);\n const themeMode = Hooks.useThemeMode();\n const { registerComposerFocus } = useThreadContext('Thread.Textbox');\n const composerRef = useRef<{ focus: () => void } | null>(null);\n const messageRef = useRef('');\n const [count, setCount] = useState(0);\n\n // Expose the composer's focus to Thread.Root so the header caret can focus it.\n useEffect(() => {\n registerComposerFocus(() => composerRef.current?.focus());\n return () => registerComposerFocus(undefined);\n }, [registerComposerFocus]);\n\n const editorExtensions = useMemo(\n () =>\n [\n createBasicExtensions({ placeholder: placeholder ?? t('message.placeholder') }),\n createThemeExtensions({ themeMode }),\n listener({ onChange: ({ text }) => (messageRef.current = text) }),\n command,\n extensions,\n ].filter(Boolean) as Extension[],\n [themeMode, placeholder, t, extensions, count],\n );\n\n const handleSend = useCallback(() => {\n const text = messageRef.current;\n if (!text?.length || !onSend) {\n return;\n }\n if (onSend(text)) {\n messageRef.current = '';\n setCount((value) => value + 1);\n }\n }, []);\n\n return (\n <Message.Textbox\n ref={composerRef}\n {...metadata}\n autoFocus={autoFocus}\n disabled={disabled}\n extensions={editorExtensions}\n onSend={handleSend}\n />\n );\n};\n\nThreadTextbox.displayName = 'Thread.Textbox';\n\n//\n// Status\n//\n\nexport type ThreadStatusProps = Util.ThemedClassName<PropsWithChildren> & {\n activity?: boolean;\n};\n\nconst ThreadStatus = forwardRef<HTMLDivElement, ThreadStatusProps>(\n ({ activity, classNames, children, ...props }, forwardedRef) => {\n const { t } = Hooks.useTranslation(translationKey);\n return (\n <div\n {...props}\n className={mx(\n 'col-start-2 grid grid-cols-[min-content_1fr_max-content] pb-2 pe-2 text-xs text-fg-muted',\n classNames,\n )}\n ref={forwardedRef}\n >\n <Icon.Icon\n icon='ph--spinner--bold'\n classNames='w-6 h-4 invisible data-[visible=show]:visible animate-spin-slow'\n data-visible={activity ? 'show' : 'hide'}\n />\n <span className='truncate min-w-0' aria-live='polite'>\n {activity ? children : null}\n </span>\n <span className={mx('text-end', hoverableControlItem)}>{t('enter-to-send.message')}</span>\n </div>\n );\n },\n);\n\nThreadStatus.displayName = 'Thread.Status';\n\n//\n// Thread\n//\n\nexport const Thread = {\n Root: ThreadRoot,\n Content: ThreadContent,\n Header: ThreadHeader,\n Messages: ThreadMessages,\n Textbox: ThreadTextbox,\n Status: ThreadStatus,\n};\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"translations.mjs","names":[],"sources":["../../src/translations.ts"],"sourcesContent":["//\n// Copyright 2023 DXOS.org\n//\n\nimport { type Resource } from '@dxos/react-ui';\n\nexport const translationKey = '@dxos/react-ui-thread';\n\nexport const translations = [\n {\n 'en-US': {\n [translationKey]: {\n 'plugin.name': 'Threads',\n 'thread-title.placeholder': 'New thread',\n 'thread-title.label': 'Title',\n 'delete-thread.label': 'Delete',\n 'select-thread.label': 'Select comment',\n 'create-thread.label': 'Create thread',\n 'message.placeholder': 'Enter message...',\n 'comment.placeholder': 'Enter comment...',\n 'settings-standalone.label': 'Enable standalone thread creation',\n 'anonymous.label': 'Anonymous',\n 'enter-to-send.message': 'Enter to add ⏎',\n 'edit-message.label': 'Edit',\n 'save-message.label': 'Save',\n 'delete-message.label': 'Delete',\n 'accept-proposal.label': 'Accept proposal',\n 'accept-change.label': 'Accept change',\n 'reject-change.label': 'Reject change',\n },\n },\n },\n] as const satisfies Resource[];\n"],"mappings":";AAMA,IAAa,iBAAiB;AAE9B,IAAa,eAAe,CAC1B,EACE,SAAS,GACN,iBAAiB;CAChB,eAAe;CACf,4BAA4B;CAC5B,sBAAsB;CACtB,uBAAuB;CACvB,uBAAuB;CACvB,uBAAuB;CACvB,uBAAuB;CACvB,uBAAuB;CACvB,6BAA6B;CAC7B,mBAAmB;CACnB,yBAAyB;CACzB,sBAAsB;CACtB,sBAAsB;CACtB,wBAAwB;CACxB,yBAAyB;CACzB,uBAAuB;CACvB,uBAAuB;AACzB,EACF,EACF,CACF"}
1
+ {"version":3,"file":"translations.mjs","names":[],"sources":["../../src/translations.ts"],"sourcesContent":["//\n// Copyright 2023 DXOS.org\n//\n\nimport type * as Theme from '@dxos/react-ui/Theme';\n\nexport const translationKey = '@dxos/react-ui-thread';\n\nexport const translations = [\n {\n 'en-US': {\n [translationKey]: {\n 'plugin.name': 'Threads',\n 'thread-title.placeholder': 'New thread',\n 'thread-title.label': 'Title',\n 'delete-thread.label': 'Delete',\n 'select-thread.label': 'Select comment',\n 'create-thread.label': 'Create thread',\n 'message.placeholder': 'Enter message...',\n 'comment.placeholder': 'Enter comment...',\n 'settings-standalone.label': 'Enable standalone thread creation',\n 'anonymous.label': 'Anonymous',\n 'enter-to-send.message': 'Enter to add ⏎',\n 'edit-message.label': 'Edit',\n 'save-message.label': 'Save',\n 'delete-message.label': 'Delete',\n 'accept-proposal.label': 'Accept proposal',\n 'accept-change.label': 'Accept change',\n 'reject-change.label': 'Reject change',\n },\n },\n },\n] as const satisfies Theme.Resource[];\n"],"mappings":";AAMA,IAAa,iBAAiB;AAE9B,IAAa,eAAe,CAC1B,EACE,SAAS,GACN,iBAAiB;CAChB,eAAe;CACf,4BAA4B;CAC5B,sBAAsB;CACtB,uBAAuB;CACvB,uBAAuB;CACvB,uBAAuB;CACvB,uBAAuB;CACvB,uBAAuB;CACvB,6BAA6B;CAC7B,mBAAmB;CACnB,yBAAyB;CACzB,sBAAsB;CACtB,sBAAsB;CACtB,wBAAwB;CACxB,yBAAyB;CACzB,uBAAuB;CACvB,uBAAuB;AACzB,EACF,EACF,CACF"}