@dxos/react-ui-thread 0.8.4-main.fffef41 → 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 +507 -220
- 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 +22 -14
- 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 +16 -12
- 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 +31 -26
- package/src/Message/Message.tsx +325 -68
- package/src/Message/index.ts +10 -1
- package/src/Thread/Thread.stories.tsx +19 -67
- 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 -35
- package/dist/types/src/Thread/Comments.stories.d.ts.map +0 -1
- package/src/Thread/Comments.stories.tsx +0 -384
- package/src/testing.tsx +0 -58
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": "~19.2.
|
|
35
|
-
"@types/react-dom": "~19.2.
|
|
36
|
-
"react": "~19.2.
|
|
37
|
-
"react-dom": "~19.2.
|
|
38
|
-
"vite": "
|
|
39
|
-
"@dxos/
|
|
40
|
-
"@dxos/
|
|
41
|
-
"@dxos/
|
|
42
|
-
"@dxos/
|
|
43
|
-
"@dxos/
|
|
44
|
-
"@dxos/
|
|
45
|
-
"@dxos/
|
|
46
|
-
"@dxos/react-ui-theme": "0.8.4-main.fffef41",
|
|
47
|
-
"@dxos/storybook-utils": "0.8.4-main.fffef41"
|
|
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"
|
|
@@ -3,43 +3,38 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
|
6
|
-
import React, {
|
|
6
|
+
import React, { useMemo } from 'react';
|
|
7
7
|
|
|
8
|
-
import {
|
|
9
|
-
import { withTheme } from '@dxos/react-ui/testing';
|
|
10
|
-
import { hoverableControls, hoverableFocusedWithinControls } from '@dxos/react-ui-theme';
|
|
8
|
+
import { withLayout, withTheme } from '@dxos/react-ui/testing';
|
|
11
9
|
|
|
12
|
-
import {
|
|
13
|
-
import { Thread } from '../Thread';
|
|
14
|
-
import { translations } from '../translations';
|
|
10
|
+
import { translations } from '#translations';
|
|
15
11
|
|
|
16
|
-
import {
|
|
12
|
+
import { createMessages, getStoryMetadata } from '../testing';
|
|
13
|
+
import { Thread } from '../Thread';
|
|
14
|
+
import { Message } from './Message';
|
|
17
15
|
|
|
18
|
-
|
|
19
|
-
const [identityKey] = useState(PublicKey.random());
|
|
20
|
-
const [message] = useState<MessageEntity>({
|
|
21
|
-
id: 'm1',
|
|
22
|
-
timestamp: new Date().toISOString(),
|
|
23
|
-
authorId: identityKey.toHex(),
|
|
24
|
-
text: 'hello',
|
|
25
|
-
});
|
|
16
|
+
type StoryProps = { editable: boolean };
|
|
26
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), []);
|
|
27
22
|
return (
|
|
28
|
-
<
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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>
|
|
35
31
|
);
|
|
36
32
|
};
|
|
37
33
|
|
|
38
34
|
const meta = {
|
|
39
35
|
title: 'ui/react-ui-thread/Message',
|
|
40
|
-
component: MessageRoot as any,
|
|
41
36
|
render: DefaultStory,
|
|
42
|
-
decorators: [withTheme],
|
|
37
|
+
decorators: [withTheme(), withLayout({ layout: 'centered', classNames: 'border w-card-min-width' })],
|
|
43
38
|
parameters: {
|
|
44
39
|
layout: 'fullscreen',
|
|
45
40
|
translations,
|
|
@@ -50,4 +45,14 @@ export default meta;
|
|
|
50
45
|
|
|
51
46
|
type Story = StoryObj<typeof meta>;
|
|
52
47
|
|
|
53
|
-
export const
|
|
48
|
+
export const Editable: Story = {
|
|
49
|
+
args: {
|
|
50
|
+
editable: true,
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export const NonEditable: Story = {
|
|
55
|
+
args: {
|
|
56
|
+
editable: false,
|
|
57
|
+
},
|
|
58
|
+
};
|
package/src/Message/Message.tsx
CHANGED
|
@@ -2,64 +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
|
-
|
|
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';
|
|
12
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
|
-
|
|
41
|
+
const buttonGroupClassNames = 'flex flex-row items-center gap-0.5 pe-2';
|
|
42
|
+
const buttonClassNames = 'p-1! transition-opacity';
|
|
43
|
+
|
|
44
|
+
//
|
|
45
|
+
// Root
|
|
46
|
+
//
|
|
47
|
+
|
|
48
|
+
export type MessageRootProps = ThemedClassName<
|
|
49
|
+
ComponentPropsWithRef<'div'> & MessageMetadata & Partial<{ continues: boolean; controls: ReactNode }>
|
|
50
|
+
>;
|
|
22
51
|
|
|
23
52
|
// TODO(burdon): Show authorName on tooltip.
|
|
24
|
-
|
|
53
|
+
const MessageRoot = forwardRef<HTMLDivElement, MessageRootProps>(
|
|
25
54
|
(
|
|
26
|
-
{
|
|
55
|
+
{
|
|
56
|
+
authorImgSrc,
|
|
57
|
+
authorId,
|
|
58
|
+
authorName,
|
|
59
|
+
authorAvatarProps,
|
|
60
|
+
continues = true,
|
|
61
|
+
controls,
|
|
62
|
+
children,
|
|
63
|
+
classNames,
|
|
64
|
+
...rootProps
|
|
65
|
+
},
|
|
27
66
|
forwardedRef,
|
|
28
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.
|
|
29
71
|
return (
|
|
30
|
-
// Must wrap the message since Avatar.Label may be used in the content.
|
|
31
72
|
<Avatar.Root>
|
|
32
73
|
<div
|
|
33
|
-
role='none'
|
|
34
74
|
data-testid='thread.message'
|
|
35
75
|
{...rootProps}
|
|
36
|
-
className={mx('grid grid-cols-
|
|
76
|
+
className={mx('grid grid-cols-[var(--dx-rail-size)_1fr_min-content] w-full', classNames)}
|
|
37
77
|
ref={forwardedRef}
|
|
38
78
|
>
|
|
39
|
-
<div
|
|
79
|
+
<div className='flex flex-col items-center gap-2 pt-1'>
|
|
40
80
|
<Avatar.Content
|
|
41
81
|
size={avatarSize}
|
|
42
82
|
hue={authorAvatarProps?.hue || hexToHue(authorId ?? '0')}
|
|
43
83
|
fallback={authorAvatarProps?.emoji || hexToEmoji(authorId ?? '0')}
|
|
44
84
|
{...(authorImgSrc && { imgSrc: authorImgSrc })}
|
|
45
85
|
/>
|
|
46
|
-
{continues && <div
|
|
47
|
-
</div>
|
|
48
|
-
<div role='none' className='plb-1 min-is-0'>
|
|
49
|
-
{children}
|
|
86
|
+
{continues && <div className='w-px grow bg-separator' />}
|
|
50
87
|
</div>
|
|
88
|
+
<div className='py-1 min-w-0'>{children}</div>
|
|
89
|
+
{controls && <div className='self-start'>{controls}</div>}
|
|
51
90
|
</div>
|
|
52
91
|
</Avatar.Root>
|
|
53
92
|
);
|
|
54
93
|
},
|
|
55
94
|
);
|
|
56
95
|
|
|
96
|
+
MessageRoot.displayName = 'Message.Root';
|
|
97
|
+
|
|
98
|
+
//
|
|
99
|
+
// Heading
|
|
100
|
+
//
|
|
101
|
+
|
|
57
102
|
export type MessageHeadingProps = ThemedClassName<ComponentPropsWithoutRef<'div'>> &
|
|
58
103
|
Pick<MessageMetadata, 'authorName' | 'timestamp'>;
|
|
59
104
|
|
|
60
|
-
|
|
105
|
+
const MessageHeading = ({ children, classNames, timestamp, authorName, ...props }: MessageHeadingProps) => {
|
|
61
106
|
return (
|
|
62
|
-
<div
|
|
107
|
+
<div {...props} className={mx('flex gap-2 items-start', classNames)}>
|
|
63
108
|
<p className='grow'>
|
|
64
109
|
<MessageAuthorName authorName={authorName} />
|
|
65
110
|
{timestamp && <MessageTime timestamp={timestamp} />}
|
|
@@ -69,33 +114,130 @@ export const MessageHeading = ({ children, classNames, timestamp, authorName, ..
|
|
|
69
114
|
);
|
|
70
115
|
};
|
|
71
116
|
|
|
117
|
+
MessageHeading.displayName = 'Message.Heading';
|
|
118
|
+
|
|
72
119
|
export type MessageAuthorNameProps = Pick<MessageMetadata, 'authorName'>;
|
|
73
120
|
|
|
74
|
-
|
|
121
|
+
const MessageAuthorName = ({ authorName }: MessageAuthorNameProps) => {
|
|
75
122
|
const { t } = useTranslation(translationKey);
|
|
76
123
|
return (
|
|
77
|
-
<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>
|
|
78
125
|
);
|
|
79
126
|
};
|
|
80
127
|
|
|
128
|
+
MessageAuthorName.displayName = 'Message.AuthorName';
|
|
129
|
+
|
|
81
130
|
export type MessageTimeProps = Pick<MessageMetadata, 'timestamp'>;
|
|
82
131
|
|
|
83
|
-
|
|
132
|
+
const MessageTime = ({ timestamp }: MessageTimeProps) => {
|
|
84
133
|
const { dtLocale } = useTranslation(translationKey);
|
|
85
134
|
const dt = timestamp ? new Date(timestamp) : undefined;
|
|
86
135
|
return (
|
|
87
|
-
<time className='block text-subdued text-xs
|
|
136
|
+
<time className='block text-subdued text-xs pb-0.5' dateTime={dt?.toISOString()}>
|
|
88
137
|
{dt ? formatDistanceToNow(dt, { locale: dtLocale, addSuffix: true }) : ''}
|
|
89
138
|
</time>
|
|
90
139
|
);
|
|
91
140
|
};
|
|
92
141
|
|
|
93
|
-
|
|
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);
|
|
94
210
|
|
|
95
|
-
|
|
96
|
-
|
|
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} />;
|
|
97
235
|
};
|
|
98
236
|
|
|
237
|
+
//
|
|
238
|
+
// Textbox
|
|
239
|
+
//
|
|
240
|
+
|
|
99
241
|
export type MessageTextboxProps = {
|
|
100
242
|
disabled?: boolean;
|
|
101
243
|
onSend?: () => void;
|
|
@@ -111,65 +253,180 @@ const keyBindings = ({ onSend, onClear }: Pick<MessageTextboxProps, 'onSend' | '
|
|
|
111
253
|
if (onSend) {
|
|
112
254
|
onSend();
|
|
113
255
|
return true;
|
|
114
|
-
} else {
|
|
115
|
-
return false;
|
|
116
256
|
}
|
|
257
|
+
return false;
|
|
117
258
|
},
|
|
118
259
|
},
|
|
119
260
|
{
|
|
120
|
-
// TODO(burdon): Other key bindings.
|
|
121
261
|
key: 'Meta+Backspace',
|
|
122
262
|
run: () => {
|
|
123
263
|
if (onClear) {
|
|
124
264
|
onClear();
|
|
125
265
|
return true;
|
|
126
|
-
} else {
|
|
127
|
-
return false;
|
|
128
266
|
}
|
|
267
|
+
return false;
|
|
129
268
|
},
|
|
130
269
|
},
|
|
131
270
|
];
|
|
132
271
|
|
|
133
|
-
export
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
authorAvatarProps,
|
|
139
|
-
disabled,
|
|
140
|
-
extensions,
|
|
141
|
-
onSend,
|
|
142
|
-
onClear,
|
|
143
|
-
onEditorFocus,
|
|
144
|
-
...editorProps
|
|
145
|
-
}: MessageTextboxProps) => {
|
|
146
|
-
const { parentRef, focusAttributes } = useTextEditor(
|
|
147
|
-
() => ({
|
|
272
|
+
export type MessageTextboxHandle = { focus: () => void };
|
|
273
|
+
|
|
274
|
+
const MessageTextbox = forwardRef<MessageTextboxHandle, MessageTextboxProps>(
|
|
275
|
+
(
|
|
276
|
+
{
|
|
148
277
|
id,
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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],
|
|
163
362
|
);
|
|
164
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
|
+
|
|
165
406
|
return (
|
|
166
|
-
<MessageRoot
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
/>
|
|
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} />
|
|
173
414
|
</MessageRoot>
|
|
174
415
|
);
|
|
175
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
|
+
};
|
package/src/Message/index.ts
CHANGED
|
@@ -2,4 +2,13 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
export
|
|
5
|
+
export { Message } from './Message';
|
|
6
|
+
export type {
|
|
7
|
+
MessageRootProps,
|
|
8
|
+
MessageHeadingProps,
|
|
9
|
+
MessageAuthorNameProps,
|
|
10
|
+
MessageTimeProps,
|
|
11
|
+
MessageBodyProps,
|
|
12
|
+
MessageTextboxProps,
|
|
13
|
+
MessageTileProps,
|
|
14
|
+
} from './Message';
|