@dxos/react-ui-thread 0.8.4-main.fd6878d → 0.8.4-staging.60fe92afc8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +102 -5
- package/dist/lib/browser/chunk-QFRRLZVL.mjs +31 -0
- package/dist/lib/browser/chunk-QFRRLZVL.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +508 -221
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/translations.mjs +9 -0
- package/dist/lib/browser/translations.mjs.map +7 -0
- package/dist/types/src/Message/Message.d.ts +81 -10
- package/dist/types/src/Message/Message.d.ts.map +1 -1
- package/dist/types/src/Message/Message.stories.d.ts +27 -17
- package/dist/types/src/Message/Message.stories.d.ts.map +1 -1
- package/dist/types/src/Message/index.d.ts +2 -1
- package/dist/types/src/Message/index.d.ts.map +1 -1
- package/dist/types/src/Thread/Thread.d.ts +76 -13
- package/dist/types/src/Thread/Thread.d.ts.map +1 -1
- package/dist/types/src/Thread/Thread.stories.d.ts +33 -5
- package/dist/types/src/Thread/Thread.stories.d.ts.map +1 -1
- package/dist/types/src/Thread/index.d.ts +2 -1
- package/dist/types/src/Thread/index.d.ts.map +1 -1
- package/dist/types/src/command.d.ts +3 -0
- package/dist/types/src/command.d.ts.map +1 -0
- package/dist/types/src/context.d.ts +227 -0
- package/dist/types/src/context.d.ts.map +1 -0
- package/dist/types/src/index.d.ts +7 -5
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/testing.d.ts +9 -14
- package/dist/types/src/testing.d.ts.map +1 -1
- package/dist/types/src/translations.d.ts +17 -12
- package/dist/types/src/translations.d.ts.map +1 -1
- package/dist/types/src/types.d.ts +36 -3
- package/dist/types/src/types.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +44 -28
- package/src/Message/Message.stories.tsx +40 -29
- package/src/Message/Message.tsx +327 -69
- package/src/Message/index.ts +10 -1
- package/src/Thread/Thread.stories.tsx +28 -72
- package/src/Thread/Thread.tsx +296 -38
- package/src/Thread/index.ts +9 -1
- package/src/command.ts +58 -0
- package/src/context.ts +21 -0
- package/src/index.ts +28 -6
- package/src/testing.ts +48 -0
- package/src/translations.ts +16 -11
- package/src/types.ts +38 -5
- package/dist/types/src/Thread/Comments.stories.d.ts +0 -52
- package/dist/types/src/Thread/Comments.stories.d.ts.map +0 -1
- package/src/Thread/Comments.stories.tsx +0 -371
- package/src/testing.tsx +0 -57
package/package.json
CHANGED
|
@@ -1,56 +1,72 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dxos/react-ui-thread",
|
|
3
|
-
"version": "0.8.4-
|
|
3
|
+
"version": "0.8.4-staging.60fe92afc8",
|
|
4
4
|
"description": "A set of components to use with threads, including comments and messages.",
|
|
5
5
|
"homepage": "https://dxos.org",
|
|
6
6
|
"bugs": "https://github.com/dxos/dxos/issues",
|
|
7
|
-
"
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/dxos/dxos"
|
|
10
|
+
},
|
|
11
|
+
"license": "FSL-1.1-Apache-2.0",
|
|
8
12
|
"author": "DXOS.org",
|
|
9
|
-
"sideEffects":
|
|
13
|
+
"sideEffects": false,
|
|
10
14
|
"type": "module",
|
|
15
|
+
"imports": {
|
|
16
|
+
"#translations": "./src/translations.ts"
|
|
17
|
+
},
|
|
11
18
|
"exports": {
|
|
12
19
|
".": {
|
|
13
20
|
"source": "./src/index.ts",
|
|
14
21
|
"types": "./dist/types/src/index.d.ts",
|
|
15
22
|
"browser": "./dist/lib/browser/index.mjs"
|
|
23
|
+
},
|
|
24
|
+
"./translations": {
|
|
25
|
+
"source": "./src/translations.ts",
|
|
26
|
+
"types": "./dist/types/src/translations.d.ts",
|
|
27
|
+
"browser": "./dist/lib/browser/translations.mjs",
|
|
28
|
+
"node": "./dist/lib/node-esm/translations.mjs"
|
|
16
29
|
}
|
|
17
30
|
},
|
|
18
31
|
"types": "dist/types/src/index.d.ts",
|
|
19
|
-
"typesVersions": {
|
|
20
|
-
"*": {}
|
|
21
|
-
},
|
|
22
32
|
"files": [
|
|
23
33
|
"dist",
|
|
24
34
|
"src"
|
|
25
35
|
],
|
|
26
36
|
"dependencies": {
|
|
27
|
-
"@
|
|
37
|
+
"@codemirror/language": "^6.12.3",
|
|
38
|
+
"@codemirror/state": "^6.6.0",
|
|
39
|
+
"@codemirror/view": "^6.43.0",
|
|
40
|
+
"@radix-ui/react-context": "1.1.1",
|
|
28
41
|
"@radix-ui/react-primitive": "2.0.2",
|
|
29
|
-
"date-fns": "^3.
|
|
30
|
-
"@dxos/
|
|
31
|
-
"@dxos/
|
|
42
|
+
"date-fns": "^3.6.0",
|
|
43
|
+
"@dxos/echo": "0.8.4-staging.60fe92afc8",
|
|
44
|
+
"@dxos/echo-react": "0.8.4-staging.60fe92afc8",
|
|
45
|
+
"@dxos/react-ui-mosaic": "0.8.4-staging.60fe92afc8",
|
|
46
|
+
"@dxos/types": "0.8.4-staging.60fe92afc8",
|
|
47
|
+
"@dxos/ui-editor": "0.8.4-staging.60fe92afc8",
|
|
48
|
+
"@dxos/util": "0.8.4-staging.60fe92afc8",
|
|
49
|
+
"@dxos/react-ui-editor": "0.8.4-staging.60fe92afc8"
|
|
32
50
|
},
|
|
33
51
|
"devDependencies": {
|
|
34
|
-
"@types/react": "~
|
|
35
|
-
"@types/react-dom": "~
|
|
36
|
-
"react": "~
|
|
37
|
-
"react-dom": "~
|
|
38
|
-
"vite": "
|
|
39
|
-
"@dxos/
|
|
40
|
-
"@dxos/
|
|
41
|
-
"@dxos/
|
|
42
|
-
"@dxos/
|
|
43
|
-
"@dxos/
|
|
44
|
-
"@dxos/
|
|
45
|
-
"@dxos/
|
|
46
|
-
"@dxos/react-ui": "0.8.4-main.fd6878d",
|
|
47
|
-
"@dxos/storybook-utils": "0.8.4-main.fd6878d"
|
|
52
|
+
"@types/react": "~19.2.7",
|
|
53
|
+
"@types/react-dom": "~19.2.3",
|
|
54
|
+
"react": "~19.2.3",
|
|
55
|
+
"react-dom": "~19.2.3",
|
|
56
|
+
"vite": "^8.0.16",
|
|
57
|
+
"@dxos/keys": "0.8.4-staging.60fe92afc8",
|
|
58
|
+
"@dxos/log": "0.8.4-staging.60fe92afc8",
|
|
59
|
+
"@dxos/react-ui": "0.8.4-staging.60fe92afc8",
|
|
60
|
+
"@dxos/random": "0.8.4-staging.60fe92afc8",
|
|
61
|
+
"@dxos/schema": "0.8.4-staging.60fe92afc8",
|
|
62
|
+
"@dxos/storybook-utils": "0.8.4-staging.60fe92afc8",
|
|
63
|
+
"@dxos/ui-theme": "0.8.4-staging.60fe92afc8"
|
|
48
64
|
},
|
|
49
65
|
"peerDependencies": {
|
|
50
|
-
"react": "~
|
|
51
|
-
"react-dom": "~
|
|
52
|
-
"@dxos/react-ui": "0.8.4-
|
|
53
|
-
"@dxos/
|
|
66
|
+
"react": "~19.2.3",
|
|
67
|
+
"react-dom": "~19.2.3",
|
|
68
|
+
"@dxos/react-ui": "0.8.4-staging.60fe92afc8",
|
|
69
|
+
"@dxos/ui-theme": "0.8.4-staging.60fe92afc8"
|
|
54
70
|
},
|
|
55
71
|
"publishConfig": {
|
|
56
72
|
"access": "public"
|
|
@@ -2,46 +2,57 @@
|
|
|
2
2
|
// Copyright 2023 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import '@
|
|
5
|
+
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
|
6
|
+
import React, { useMemo } from 'react';
|
|
6
7
|
|
|
7
|
-
import
|
|
8
|
+
import { withLayout, withTheme } from '@dxos/react-ui/testing';
|
|
8
9
|
|
|
9
|
-
import {
|
|
10
|
-
import { hoverableControls, hoverableFocusedWithinControls } from '@dxos/react-ui-theme';
|
|
11
|
-
import { withLayout, withTheme } from '@dxos/storybook-utils';
|
|
10
|
+
import { translations } from '#translations';
|
|
12
11
|
|
|
13
|
-
import {
|
|
12
|
+
import { createMessages, getStoryMetadata } from '../testing';
|
|
14
13
|
import { Thread } from '../Thread';
|
|
15
|
-
import {
|
|
14
|
+
import { Message } from './Message';
|
|
16
15
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const DefaultStory = () => {
|
|
20
|
-
const [identityKey] = useState(PublicKey.random());
|
|
21
|
-
const [message] = useState<MessageEntity>({
|
|
22
|
-
id: 'm1',
|
|
23
|
-
timestamp: new Date().toISOString(),
|
|
24
|
-
authorId: identityKey.toHex(),
|
|
25
|
-
text: 'hello',
|
|
26
|
-
});
|
|
16
|
+
type StoryProps = { editable: boolean };
|
|
27
17
|
|
|
18
|
+
// Sample messages are authored by 'did:key:alice'; the local identity matches,
|
|
19
|
+
// so `editable` toggles whether the author's own message shows the edit affordance.
|
|
20
|
+
const DefaultStory = ({ editable }: StoryProps) => {
|
|
21
|
+
const [message] = useMemo(() => createMessages(1), []);
|
|
28
22
|
return (
|
|
29
|
-
<
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
23
|
+
<Thread.Root
|
|
24
|
+
getMetadata={getStoryMetadata}
|
|
25
|
+
identityDid='did:key:alice'
|
|
26
|
+
editable={editable}
|
|
27
|
+
onMessageDelete={() => {}}
|
|
28
|
+
>
|
|
29
|
+
<Message.Tile message={message} />
|
|
30
|
+
</Thread.Root>
|
|
36
31
|
);
|
|
37
32
|
};
|
|
38
33
|
|
|
39
|
-
|
|
34
|
+
const meta = {
|
|
40
35
|
title: 'ui/react-ui-thread/Message',
|
|
41
|
-
component: MessageRoot,
|
|
42
36
|
render: DefaultStory,
|
|
43
|
-
decorators: [withTheme, withLayout({
|
|
44
|
-
parameters: {
|
|
37
|
+
decorators: [withTheme(), withLayout({ layout: 'centered', classNames: 'border w-card-min-width' })],
|
|
38
|
+
parameters: {
|
|
39
|
+
layout: 'fullscreen',
|
|
40
|
+
translations,
|
|
41
|
+
},
|
|
42
|
+
} satisfies Meta<typeof DefaultStory>;
|
|
43
|
+
|
|
44
|
+
export default meta;
|
|
45
|
+
|
|
46
|
+
type Story = StoryObj<typeof meta>;
|
|
47
|
+
|
|
48
|
+
export const Editable: Story = {
|
|
49
|
+
args: {
|
|
50
|
+
editable: true,
|
|
51
|
+
},
|
|
45
52
|
};
|
|
46
53
|
|
|
47
|
-
export const
|
|
54
|
+
export const NonEditable: Story = {
|
|
55
|
+
args: {
|
|
56
|
+
editable: false,
|
|
57
|
+
},
|
|
58
|
+
};
|
package/src/Message/Message.tsx
CHANGED
|
@@ -2,63 +2,109 @@
|
|
|
2
2
|
// Copyright 2023 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
+
import { EditorView } from '@codemirror/view';
|
|
5
6
|
import { formatDistanceToNow } from 'date-fns/formatDistanceToNow';
|
|
6
|
-
import React, {
|
|
7
|
+
import React, {
|
|
8
|
+
type ComponentPropsWithRef,
|
|
9
|
+
type ComponentPropsWithoutRef,
|
|
10
|
+
type ReactNode,
|
|
11
|
+
forwardRef,
|
|
12
|
+
useCallback,
|
|
13
|
+
useEffect,
|
|
14
|
+
useImperativeHandle,
|
|
15
|
+
useRef,
|
|
16
|
+
useState,
|
|
17
|
+
} from 'react';
|
|
7
18
|
|
|
8
|
-
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
19
|
+
import { Obj, Ref } from '@dxos/echo';
|
|
20
|
+
import {
|
|
21
|
+
Avatar,
|
|
22
|
+
IconButton,
|
|
23
|
+
type ThemedClassName,
|
|
24
|
+
useOnTransition,
|
|
25
|
+
useThemeContext,
|
|
26
|
+
useTranslation,
|
|
27
|
+
} from '@dxos/react-ui';
|
|
28
|
+
import { type UseTextEditorProps, useTextEditor } from '@dxos/react-ui-editor';
|
|
29
|
+
import { type ContentBlock, type Message as MessageType } from '@dxos/types';
|
|
30
|
+
import { createBasicExtensions, createThemeExtensions, keymap, listener } from '@dxos/ui-editor';
|
|
31
|
+
import { hoverableControlItem, hoverableControls, hoverableFocusedWithinControls, mx } from '@dxos/ui-theme';
|
|
32
|
+
import { hexToEmoji, hexToHue, isTruthy } from '@dxos/util';
|
|
13
33
|
|
|
34
|
+
import { command } from '../command';
|
|
35
|
+
import { useThreadContext } from '../context';
|
|
14
36
|
import { translationKey } from '../translations';
|
|
15
37
|
import { type MessageMetadata } from '../types';
|
|
16
38
|
|
|
17
39
|
const avatarSize = 7;
|
|
18
40
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
Partial<{ continues: boolean }>;
|
|
41
|
+
const buttonGroupClassNames = 'flex flex-row items-center gap-0.5 pe-2';
|
|
42
|
+
const buttonClassNames = 'p-1! transition-opacity';
|
|
22
43
|
|
|
23
|
-
|
|
44
|
+
//
|
|
45
|
+
// Root
|
|
46
|
+
//
|
|
47
|
+
|
|
48
|
+
export type MessageRootProps = ThemedClassName<
|
|
49
|
+
ComponentPropsWithRef<'div'> & MessageMetadata & Partial<{ continues: boolean; controls: ReactNode }>
|
|
50
|
+
>;
|
|
51
|
+
|
|
52
|
+
// TODO(burdon): Show authorName on tooltip.
|
|
53
|
+
const MessageRoot = forwardRef<HTMLDivElement, MessageRootProps>(
|
|
24
54
|
(
|
|
25
|
-
{
|
|
55
|
+
{
|
|
56
|
+
authorImgSrc,
|
|
57
|
+
authorId,
|
|
58
|
+
authorName,
|
|
59
|
+
authorAvatarProps,
|
|
60
|
+
continues = true,
|
|
61
|
+
controls,
|
|
62
|
+
children,
|
|
63
|
+
classNames,
|
|
64
|
+
...rootProps
|
|
65
|
+
},
|
|
26
66
|
forwardedRef,
|
|
27
67
|
) => {
|
|
68
|
+
// Must wrap the message since Avatar.Label may be used in the content.
|
|
69
|
+
// Columns mirror Thread.Header (avatar/rail · content · controls) so trailing
|
|
70
|
+
// controls align with the thread header's controls.
|
|
28
71
|
return (
|
|
29
|
-
// Must wrap the message since Avatar.Label may be used in the content.
|
|
30
72
|
<Avatar.Root>
|
|
31
73
|
<div
|
|
32
|
-
role='none'
|
|
33
74
|
data-testid='thread.message'
|
|
34
75
|
{...rootProps}
|
|
35
|
-
className={mx('grid grid-cols-
|
|
76
|
+
className={mx('grid grid-cols-[var(--dx-rail-size)_1fr_min-content] w-full', classNames)}
|
|
36
77
|
ref={forwardedRef}
|
|
37
78
|
>
|
|
38
|
-
<div
|
|
79
|
+
<div className='flex flex-col items-center gap-2 pt-1'>
|
|
39
80
|
<Avatar.Content
|
|
40
81
|
size={avatarSize}
|
|
41
82
|
hue={authorAvatarProps?.hue || hexToHue(authorId ?? '0')}
|
|
42
83
|
fallback={authorAvatarProps?.emoji || hexToEmoji(authorId ?? '0')}
|
|
43
84
|
{...(authorImgSrc && { imgSrc: authorImgSrc })}
|
|
44
85
|
/>
|
|
45
|
-
{continues && <div
|
|
46
|
-
</div>
|
|
47
|
-
<div role='none' className='plb-1 min-is-0'>
|
|
48
|
-
{children}
|
|
86
|
+
{continues && <div className='w-px grow bg-separator' />}
|
|
49
87
|
</div>
|
|
88
|
+
<div className='py-1 min-w-0'>{children}</div>
|
|
89
|
+
{controls && <div className='self-start'>{controls}</div>}
|
|
50
90
|
</div>
|
|
51
91
|
</Avatar.Root>
|
|
52
92
|
);
|
|
53
93
|
},
|
|
54
94
|
);
|
|
55
95
|
|
|
96
|
+
MessageRoot.displayName = 'Message.Root';
|
|
97
|
+
|
|
98
|
+
//
|
|
99
|
+
// Heading
|
|
100
|
+
//
|
|
101
|
+
|
|
56
102
|
export type MessageHeadingProps = ThemedClassName<ComponentPropsWithoutRef<'div'>> &
|
|
57
103
|
Pick<MessageMetadata, 'authorName' | 'timestamp'>;
|
|
58
104
|
|
|
59
|
-
|
|
105
|
+
const MessageHeading = ({ children, classNames, timestamp, authorName, ...props }: MessageHeadingProps) => {
|
|
60
106
|
return (
|
|
61
|
-
<div
|
|
107
|
+
<div {...props} className={mx('flex gap-2 items-start', classNames)}>
|
|
62
108
|
<p className='grow'>
|
|
63
109
|
<MessageAuthorName authorName={authorName} />
|
|
64
110
|
{timestamp && <MessageTime timestamp={timestamp} />}
|
|
@@ -68,33 +114,130 @@ export const MessageHeading = ({ children, classNames, timestamp, authorName, ..
|
|
|
68
114
|
);
|
|
69
115
|
};
|
|
70
116
|
|
|
117
|
+
MessageHeading.displayName = 'Message.Heading';
|
|
118
|
+
|
|
71
119
|
export type MessageAuthorNameProps = Pick<MessageMetadata, 'authorName'>;
|
|
72
120
|
|
|
73
|
-
|
|
121
|
+
const MessageAuthorName = ({ authorName }: MessageAuthorNameProps) => {
|
|
74
122
|
const { t } = useTranslation(translationKey);
|
|
75
123
|
return (
|
|
76
|
-
<Avatar.Label classNames='block truncate text-sm text-subdued'>{authorName ?? t('anonymous
|
|
124
|
+
<Avatar.Label classNames='block truncate text-sm text-subdued'>{authorName ?? t('anonymous.label')}</Avatar.Label>
|
|
77
125
|
);
|
|
78
126
|
};
|
|
79
127
|
|
|
128
|
+
MessageAuthorName.displayName = 'Message.AuthorName';
|
|
129
|
+
|
|
80
130
|
export type MessageTimeProps = Pick<MessageMetadata, 'timestamp'>;
|
|
81
131
|
|
|
82
|
-
|
|
132
|
+
const MessageTime = ({ timestamp }: MessageTimeProps) => {
|
|
83
133
|
const { dtLocale } = useTranslation(translationKey);
|
|
84
134
|
const dt = timestamp ? new Date(timestamp) : undefined;
|
|
85
135
|
return (
|
|
86
|
-
<time className='block text-subdued text-xs
|
|
136
|
+
<time className='block text-subdued text-xs pb-0.5' dateTime={dt?.toISOString()}>
|
|
87
137
|
{dt ? formatDistanceToNow(dt, { locale: dtLocale, addSuffix: true }) : ''}
|
|
88
138
|
</time>
|
|
89
139
|
);
|
|
90
140
|
};
|
|
91
141
|
|
|
92
|
-
|
|
142
|
+
MessageTime.displayName = 'Message.Time';
|
|
143
|
+
|
|
144
|
+
//
|
|
145
|
+
// Body
|
|
146
|
+
//
|
|
147
|
+
|
|
148
|
+
export type MessageBodyProps = {
|
|
149
|
+
/** The message whose content blocks are rendered. */
|
|
150
|
+
message: MessageType.Message;
|
|
151
|
+
/** When true, the local user authored this message. */
|
|
152
|
+
isAuthor?: boolean;
|
|
153
|
+
/** When true, the text block is editable. */
|
|
154
|
+
editing?: boolean;
|
|
155
|
+
/** Persist an edited text block. */
|
|
156
|
+
onSave?: (text: string) => void;
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Renders a message's content blocks: text (via editor), proposal, and
|
|
161
|
+
* object/reference tiles (delegated to the injected `Object` component).
|
|
162
|
+
*/
|
|
163
|
+
const MessageBody = ({ message, isAuthor, editing, onSave }: MessageBodyProps) => {
|
|
164
|
+
const { components } = useThreadContext('Message.Body');
|
|
165
|
+
const textBlockIndex = message.blocks.findIndex((block) => block._tag === 'text');
|
|
166
|
+
const textBlock = textBlockIndex !== -1 ? (message.blocks[textBlockIndex] as ContentBlock.Text) : undefined;
|
|
167
|
+
const proposalBlock = message.blocks.find((block) => block._tag === 'proposal') as ContentBlock.Proposal | undefined;
|
|
168
|
+
const references = message.blocks
|
|
169
|
+
.filter((block) => block._tag === 'reference')
|
|
170
|
+
.map((block) => (block as ContentBlock.Reference).reference);
|
|
171
|
+
const Object = components.Object;
|
|
172
|
+
|
|
173
|
+
return (
|
|
174
|
+
<>
|
|
175
|
+
{textBlock && <TextBlock block={textBlock} isAuthor={isAuthor} editing={editing} onSave={onSave} />}
|
|
176
|
+
{proposalBlock && <div className='me-4 italic'>{proposalBlock.text}</div>}
|
|
177
|
+
{Object &&
|
|
178
|
+
Ref.Array.targets(references).map((reference, index) => (
|
|
179
|
+
<Object key={index} subject={reference as Obj.Unknown} />
|
|
180
|
+
))}
|
|
181
|
+
</>
|
|
182
|
+
);
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
MessageBody.displayName = 'Message.Body';
|
|
186
|
+
|
|
187
|
+
const TextBlock = ({
|
|
188
|
+
block,
|
|
189
|
+
isAuthor,
|
|
190
|
+
editing,
|
|
191
|
+
onSave,
|
|
192
|
+
}: {
|
|
193
|
+
block: ContentBlock.Text;
|
|
194
|
+
isAuthor?: boolean;
|
|
195
|
+
editing?: boolean;
|
|
196
|
+
onSave?: (text: string) => void;
|
|
197
|
+
}) => {
|
|
198
|
+
const { themeMode } = useThemeContext();
|
|
199
|
+
const inMemoryContentRef = useRef(block.text);
|
|
200
|
+
|
|
201
|
+
const handleDocumentChange = useCallback((next: string) => {
|
|
202
|
+
inMemoryContentRef.current = next;
|
|
203
|
+
}, []);
|
|
204
|
+
|
|
205
|
+
const saveDocumentChange = useCallback(() => {
|
|
206
|
+
onSave?.(inMemoryContentRef.current);
|
|
207
|
+
}, [onSave]);
|
|
208
|
+
|
|
209
|
+
useOnTransition(editing, true, false, saveDocumentChange);
|
|
93
210
|
|
|
94
|
-
|
|
95
|
-
|
|
211
|
+
const { parentRef, focusAttributes, view } = useTextEditor(
|
|
212
|
+
() => ({
|
|
213
|
+
initialValue: block.text,
|
|
214
|
+
extensions: [
|
|
215
|
+
createBasicExtensions({ readOnly: !isAuthor || !editing }),
|
|
216
|
+
createThemeExtensions({ themeMode }),
|
|
217
|
+
command,
|
|
218
|
+
EditorView.updateListener.of((update) => {
|
|
219
|
+
if (update.docChanged) {
|
|
220
|
+
handleDocumentChange(update.state.doc.toString());
|
|
221
|
+
}
|
|
222
|
+
}),
|
|
223
|
+
],
|
|
224
|
+
}),
|
|
225
|
+
[block.text, editing, isAuthor, themeMode, handleDocumentChange],
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
useEffect(() => {
|
|
229
|
+
if (editing) {
|
|
230
|
+
view?.focus();
|
|
231
|
+
}
|
|
232
|
+
}, [editing, view]);
|
|
233
|
+
|
|
234
|
+
return <div ref={parentRef} className='me-4' {...focusAttributes} />;
|
|
96
235
|
};
|
|
97
236
|
|
|
237
|
+
//
|
|
238
|
+
// Textbox
|
|
239
|
+
//
|
|
240
|
+
|
|
98
241
|
export type MessageTextboxProps = {
|
|
99
242
|
disabled?: boolean;
|
|
100
243
|
onSend?: () => void;
|
|
@@ -110,65 +253,180 @@ const keyBindings = ({ onSend, onClear }: Pick<MessageTextboxProps, 'onSend' | '
|
|
|
110
253
|
if (onSend) {
|
|
111
254
|
onSend();
|
|
112
255
|
return true;
|
|
113
|
-
} else {
|
|
114
|
-
return false;
|
|
115
256
|
}
|
|
257
|
+
return false;
|
|
116
258
|
},
|
|
117
259
|
},
|
|
118
260
|
{
|
|
119
|
-
// TODO(burdon): Other key bindings.
|
|
120
261
|
key: 'Meta+Backspace',
|
|
121
262
|
run: () => {
|
|
122
263
|
if (onClear) {
|
|
123
264
|
onClear();
|
|
124
265
|
return true;
|
|
125
|
-
} else {
|
|
126
|
-
return false;
|
|
127
266
|
}
|
|
267
|
+
return false;
|
|
128
268
|
},
|
|
129
269
|
},
|
|
130
270
|
];
|
|
131
271
|
|
|
132
|
-
export
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
authorAvatarProps,
|
|
138
|
-
disabled,
|
|
139
|
-
extensions,
|
|
140
|
-
onSend,
|
|
141
|
-
onClear,
|
|
142
|
-
onEditorFocus,
|
|
143
|
-
...editorProps
|
|
144
|
-
}: MessageTextboxProps) => {
|
|
145
|
-
const { parentRef, focusAttributes } = useTextEditor(
|
|
146
|
-
() => ({
|
|
272
|
+
export type MessageTextboxHandle = { focus: () => void };
|
|
273
|
+
|
|
274
|
+
const MessageTextbox = forwardRef<MessageTextboxHandle, MessageTextboxProps>(
|
|
275
|
+
(
|
|
276
|
+
{
|
|
147
277
|
id,
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
278
|
+
authorId,
|
|
279
|
+
authorName,
|
|
280
|
+
authorImgSrc,
|
|
281
|
+
authorAvatarProps,
|
|
282
|
+
disabled,
|
|
283
|
+
extensions,
|
|
284
|
+
onSend,
|
|
285
|
+
onClear,
|
|
286
|
+
onEditorFocus,
|
|
287
|
+
...editorProps
|
|
288
|
+
},
|
|
289
|
+
forwardedRef,
|
|
290
|
+
) => {
|
|
291
|
+
const { parentRef, focusAttributes, view } = useTextEditor(
|
|
292
|
+
() => ({
|
|
293
|
+
id,
|
|
294
|
+
extensions: [
|
|
295
|
+
keymap.of(keyBindings({ onSend, onClear })),
|
|
296
|
+
listener({
|
|
297
|
+
onFocus: ({ focusing }) => {
|
|
298
|
+
if (focusing) {
|
|
299
|
+
onEditorFocus?.();
|
|
300
|
+
}
|
|
301
|
+
},
|
|
302
|
+
}),
|
|
303
|
+
extensions,
|
|
304
|
+
].filter(isTruthy),
|
|
305
|
+
...editorProps,
|
|
306
|
+
}),
|
|
307
|
+
[id, extensions],
|
|
308
|
+
);
|
|
309
|
+
|
|
310
|
+
useImperativeHandle(forwardedRef, () => ({ focus: () => view?.focus() }), [view]);
|
|
311
|
+
|
|
312
|
+
return (
|
|
313
|
+
<MessageRoot {...{ id, authorId, authorName, authorImgSrc, authorAvatarProps }} continues={false}>
|
|
314
|
+
<div
|
|
315
|
+
ref={parentRef}
|
|
316
|
+
className={mx('py-0.5 me-1 rounded-xs dx-focus-ring', disabled && 'opacity-50')}
|
|
317
|
+
{...focusAttributes}
|
|
318
|
+
/>
|
|
319
|
+
</MessageRoot>
|
|
320
|
+
);
|
|
321
|
+
},
|
|
322
|
+
);
|
|
323
|
+
|
|
324
|
+
MessageTextbox.displayName = 'Message.Textbox';
|
|
325
|
+
|
|
326
|
+
//
|
|
327
|
+
// Tile
|
|
328
|
+
//
|
|
329
|
+
|
|
330
|
+
export type MessageTileProps = {
|
|
331
|
+
message: MessageType.Message;
|
|
332
|
+
classNames?: MessageRootProps['classNames'];
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Full message tile (frame + heading + controls + body). Reads `Thread.Root`
|
|
337
|
+
* context for metadata resolution, injected renderers, and callbacks. This is
|
|
338
|
+
* the unit rendered by `Thread.Messages`.
|
|
339
|
+
*/
|
|
340
|
+
const MessageTile = ({ message, classNames }: MessageTileProps) => {
|
|
341
|
+
const { t } = useTranslation(translationKey);
|
|
342
|
+
const { getMetadata, identityDid, editable, onMessageDelete, onAcceptProposal } = useThreadContext('Message.Tile');
|
|
343
|
+
const [editing, setEditing] = useState(false);
|
|
344
|
+
|
|
345
|
+
const metadata = getMetadata(message);
|
|
346
|
+
const isAuthor = !!identityDid && identityDid === metadata.authorId;
|
|
347
|
+
const hasProposal = message.blocks.some((block) => block._tag === 'proposal');
|
|
348
|
+
|
|
349
|
+
const handleEdit = useCallback(() => setEditing((value) => !value), []);
|
|
350
|
+
const handleDelete = useCallback(() => onMessageDelete?.(message.id), [onMessageDelete, message.id]);
|
|
351
|
+
const handleAcceptProposal = useCallback(() => onAcceptProposal?.(message.id), [onAcceptProposal, message.id]);
|
|
352
|
+
const handleSave = useCallback(
|
|
353
|
+
(text: string) => {
|
|
354
|
+
Obj.update(message, (message) => {
|
|
355
|
+
const block = message.blocks.find((block) => block._tag === 'text');
|
|
356
|
+
if (block && block._tag === 'text') {
|
|
357
|
+
block.text = text;
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
},
|
|
361
|
+
[message],
|
|
162
362
|
);
|
|
163
363
|
|
|
364
|
+
const showEdit = isAuthor && editable;
|
|
365
|
+
const showAccept = hasProposal && !!onAcceptProposal;
|
|
366
|
+
const showDelete = !!onMessageDelete;
|
|
367
|
+
const controls =
|
|
368
|
+
showEdit || showAccept || showDelete ? (
|
|
369
|
+
<div className={buttonGroupClassNames}>
|
|
370
|
+
{showEdit && (
|
|
371
|
+
<IconButton
|
|
372
|
+
data-testid={editing ? 'thread.message.save' : 'thread.message.edit'}
|
|
373
|
+
variant='ghost'
|
|
374
|
+
icon={editing ? 'ph--check--regular' : 'ph--pencil-simple--regular'}
|
|
375
|
+
iconOnly
|
|
376
|
+
label={t(editing ? 'save-message.label' : 'edit-message.label')}
|
|
377
|
+
classNames={[buttonClassNames, hoverableControlItem]}
|
|
378
|
+
onClick={handleEdit}
|
|
379
|
+
/>
|
|
380
|
+
)}
|
|
381
|
+
{showAccept && (
|
|
382
|
+
<IconButton
|
|
383
|
+
data-testid='thread.message.accept'
|
|
384
|
+
variant='ghost'
|
|
385
|
+
icon='ph--check--regular'
|
|
386
|
+
iconOnly
|
|
387
|
+
label={t('accept-proposal.label')}
|
|
388
|
+
classNames={[buttonClassNames, hoverableControlItem]}
|
|
389
|
+
onClick={handleAcceptProposal}
|
|
390
|
+
/>
|
|
391
|
+
)}
|
|
392
|
+
{showDelete && (
|
|
393
|
+
<IconButton
|
|
394
|
+
data-testid='thread.message.delete'
|
|
395
|
+
variant='ghost'
|
|
396
|
+
icon='ph--x--regular'
|
|
397
|
+
iconOnly
|
|
398
|
+
label={t('delete-message.label')}
|
|
399
|
+
classNames={[buttonClassNames, hoverableControlItem]}
|
|
400
|
+
onClick={handleDelete}
|
|
401
|
+
/>
|
|
402
|
+
)}
|
|
403
|
+
</div>
|
|
404
|
+
) : undefined;
|
|
405
|
+
|
|
164
406
|
return (
|
|
165
|
-
<MessageRoot
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
/>
|
|
407
|
+
<MessageRoot
|
|
408
|
+
{...metadata}
|
|
409
|
+
controls={controls}
|
|
410
|
+
classNames={[hoverableControls, hoverableFocusedWithinControls, classNames]}
|
|
411
|
+
>
|
|
412
|
+
<MessageHeading authorName={metadata.authorName} timestamp={metadata.timestamp} />
|
|
413
|
+
<MessageBody message={message} isAuthor={isAuthor} editing={editing} onSave={handleSave} />
|
|
172
414
|
</MessageRoot>
|
|
173
415
|
);
|
|
174
416
|
};
|
|
417
|
+
|
|
418
|
+
MessageTile.displayName = 'Message.Tile';
|
|
419
|
+
|
|
420
|
+
//
|
|
421
|
+
// Message
|
|
422
|
+
//
|
|
423
|
+
|
|
424
|
+
export const Message = {
|
|
425
|
+
Root: MessageRoot,
|
|
426
|
+
Heading: MessageHeading,
|
|
427
|
+
AuthorName: MessageAuthorName,
|
|
428
|
+
Time: MessageTime,
|
|
429
|
+
Body: MessageBody,
|
|
430
|
+
Textbox: MessageTextbox,
|
|
431
|
+
Tile: MessageTile,
|
|
432
|
+
};
|