@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/src/testing.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { Obj } from '@dxos/echo';
|
|
6
|
+
import { random } from '@dxos/random';
|
|
7
|
+
import { type ValueGenerator, createGenerator } from '@dxos/schema/testing';
|
|
8
|
+
import { Message } from '@dxos/types';
|
|
9
|
+
import { hexToFallback } from '@dxos/util';
|
|
10
|
+
|
|
11
|
+
import { type MessageMetadata } from './types';
|
|
12
|
+
|
|
13
|
+
const generator: ValueGenerator = random as any;
|
|
14
|
+
|
|
15
|
+
const authors = [
|
|
16
|
+
{ did: 'did:key:alice', name: 'Alice' },
|
|
17
|
+
{ did: 'did:key:bob', name: 'Bob' },
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Generate sample `Message` objects (real `@dxos/types` schema) with text
|
|
22
|
+
* blocks for stories, using the `@dxos/schema/testing` generator for base
|
|
23
|
+
* fields and `@dxos/random` for content.
|
|
24
|
+
*/
|
|
25
|
+
export const createMessages = (count = 8): Message.Message[] => {
|
|
26
|
+
const objectGenerator = createGenerator(generator, Message.Message);
|
|
27
|
+
return objectGenerator.createObjects(count).map((message, index) => {
|
|
28
|
+
const author = authors[index % authors.length];
|
|
29
|
+
Obj.update(message, (message) => {
|
|
30
|
+
message.sender = { role: 'user', identityDid: author.did, name: author.name };
|
|
31
|
+
message.blocks = [{ _tag: 'text', text: random.lorem.paragraph() }];
|
|
32
|
+
});
|
|
33
|
+
return message;
|
|
34
|
+
});
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/** Story metadata resolver mapping a message's sender to presentational fields. */
|
|
38
|
+
export const getStoryMetadata = (message: Message.Message): MessageMetadata => {
|
|
39
|
+
const did = message.sender.identityDid ?? '0';
|
|
40
|
+
const fallback = hexToFallback(did);
|
|
41
|
+
return {
|
|
42
|
+
id: Obj.getURI(message),
|
|
43
|
+
timestamp: message.created,
|
|
44
|
+
authorId: did,
|
|
45
|
+
authorName: message.sender.name,
|
|
46
|
+
authorAvatarProps: { hue: fallback.hue, emoji: fallback.emoji },
|
|
47
|
+
};
|
|
48
|
+
};
|
package/src/translations.ts
CHANGED
|
@@ -4,22 +4,27 @@
|
|
|
4
4
|
|
|
5
5
|
import { type Resource } from '@dxos/react-ui';
|
|
6
6
|
|
|
7
|
-
export const translationKey = 'react-ui-thread';
|
|
7
|
+
export const translationKey = '@dxos/react-ui-thread';
|
|
8
8
|
|
|
9
9
|
export const translations = [
|
|
10
10
|
{
|
|
11
11
|
'en-US': {
|
|
12
12
|
[translationKey]: {
|
|
13
|
-
'plugin
|
|
14
|
-
'thread
|
|
15
|
-
'thread
|
|
16
|
-
'delete
|
|
17
|
-
'
|
|
18
|
-
'
|
|
19
|
-
'
|
|
20
|
-
'
|
|
21
|
-
'
|
|
22
|
-
'
|
|
13
|
+
'plugin.name': 'Threads',
|
|
14
|
+
'thread-title.placeholder': 'New thread',
|
|
15
|
+
'thread-title.label': 'Title',
|
|
16
|
+
'delete-thread.label': 'Delete',
|
|
17
|
+
'select-thread.label': 'Select comment',
|
|
18
|
+
'create-thread.label': 'Create thread',
|
|
19
|
+
'message.placeholder': 'Enter message...',
|
|
20
|
+
'comment.placeholder': 'Enter comment...',
|
|
21
|
+
'settings-standalone.label': 'Enable standalone thread creation',
|
|
22
|
+
'anonymous.label': 'Anonymous',
|
|
23
|
+
'enter-to-send.message': 'Enter to add ⏎',
|
|
24
|
+
'edit-message.label': 'Edit',
|
|
25
|
+
'save-message.label': 'Save',
|
|
26
|
+
'delete-message.label': 'Delete',
|
|
27
|
+
'accept-proposal.label': 'Accept proposal',
|
|
23
28
|
},
|
|
24
29
|
},
|
|
25
30
|
},
|
package/src/types.ts
CHANGED
|
@@ -2,10 +2,17 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import { type
|
|
5
|
+
import { type ComponentType } from 'react';
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
import { type Obj } from '@dxos/echo';
|
|
8
|
+
import { type Message } from '@dxos/types';
|
|
9
|
+
import { type FallbackValue } from '@dxos/util';
|
|
8
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Presentational metadata for a message author, resolved by the host (e.g. from
|
|
13
|
+
* space members / identity) and supplied to the UI layer — keeps this package
|
|
14
|
+
* free of `@dxos/react-client`.
|
|
15
|
+
*/
|
|
9
16
|
export type MessageMetadata = {
|
|
10
17
|
id: string;
|
|
11
18
|
timestamp?: string;
|
|
@@ -15,7 +22,33 @@ export type MessageMetadata = {
|
|
|
15
22
|
authorAvatarProps?: FallbackValue;
|
|
16
23
|
};
|
|
17
24
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
25
|
+
/**
|
|
26
|
+
* Renders an object/reference message block. Injected by the host plugin
|
|
27
|
+
* (typically backed by an app-framework `Surface`) so this package stays free
|
|
28
|
+
* of `@dxos/app-framework`.
|
|
29
|
+
*/
|
|
30
|
+
export type ObjectTileComponent = ComponentType<{ subject: Obj.Unknown }>;
|
|
31
|
+
|
|
32
|
+
export type ThreadComponents = {
|
|
33
|
+
Object?: ObjectTileComponent;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/** Callbacks raised by message tiles, handled by the host. */
|
|
37
|
+
export type MessageCallbacks = {
|
|
38
|
+
/** Delete a message by id (omit to hide the affordance). */
|
|
39
|
+
onMessageDelete?: (messageId: string) => void;
|
|
40
|
+
/** Accept an assistant proposal block on a message (omit to hide the affordance). */
|
|
41
|
+
onAcceptProposal?: (messageId: string) => void;
|
|
21
42
|
};
|
|
43
|
+
|
|
44
|
+
/** Shared context provided by `Thread.Root` to its message tiles. */
|
|
45
|
+
export type ThreadContextValue = {
|
|
46
|
+
/** Resolve presentational metadata for a message. */
|
|
47
|
+
getMetadata: (message: Message.Message) => MessageMetadata;
|
|
48
|
+
/** Injected renderers (e.g. object/reference tiles). */
|
|
49
|
+
components: ThreadComponents;
|
|
50
|
+
/** DID of the local identity; used to decide message editability. */
|
|
51
|
+
identityDid?: string;
|
|
52
|
+
/** When true, the author may edit their own text messages in place. */
|
|
53
|
+
editable?: boolean;
|
|
54
|
+
} & MessageCallbacks;
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import '@dxos-theme';
|
|
2
|
-
import React from 'react';
|
|
3
|
-
import { type Range } from '@dxos/react-ui-editor';
|
|
4
|
-
import { type MessageEntity } from '../testing';
|
|
5
|
-
type StoryCommentThread = {
|
|
6
|
-
id: string;
|
|
7
|
-
cursor?: string;
|
|
8
|
-
range?: Range;
|
|
9
|
-
yPos?: number;
|
|
10
|
-
messages: MessageEntity[];
|
|
11
|
-
};
|
|
12
|
-
type StoryProps = {
|
|
13
|
-
text?: string;
|
|
14
|
-
autoCreate?: boolean;
|
|
15
|
-
};
|
|
16
|
-
declare const _default: {
|
|
17
|
-
title: string;
|
|
18
|
-
component: React.FC<{
|
|
19
|
-
thread: StoryCommentThread;
|
|
20
|
-
selected: boolean;
|
|
21
|
-
onSelect: () => void;
|
|
22
|
-
onResolve: () => void;
|
|
23
|
-
}>;
|
|
24
|
-
decorators: import("@storybook/react").Decorator[];
|
|
25
|
-
render: (args: StoryProps) => React.JSX.Element;
|
|
26
|
-
parameters: {
|
|
27
|
-
translations: [{
|
|
28
|
-
readonly 'en-US': {
|
|
29
|
-
readonly "react-ui-thread": {
|
|
30
|
-
readonly 'plugin name': "Threads";
|
|
31
|
-
readonly 'thread title placeholder': "New thread";
|
|
32
|
-
readonly 'thread title label': "Title";
|
|
33
|
-
readonly 'delete thread label': "Delete";
|
|
34
|
-
readonly 'create thread label': "Create thread";
|
|
35
|
-
readonly 'message placeholder': "Enter message...";
|
|
36
|
-
readonly 'comment placeholder': "Enter comment...";
|
|
37
|
-
readonly 'settings standalone label': "Enable standalone thread creation";
|
|
38
|
-
readonly 'anonymous label': "Anonymous";
|
|
39
|
-
readonly 'enter to send message': "Enter to add ⏎";
|
|
40
|
-
};
|
|
41
|
-
};
|
|
42
|
-
}];
|
|
43
|
-
layout: string;
|
|
44
|
-
};
|
|
45
|
-
};
|
|
46
|
-
export default _default;
|
|
47
|
-
export declare const Default: {
|
|
48
|
-
args: {
|
|
49
|
-
text: string;
|
|
50
|
-
};
|
|
51
|
-
};
|
|
52
|
-
//# sourceMappingURL=Comments.stories.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"Comments.stories.d.ts","sourceRoot":"","sources":["../../../../src/Thread/Comments.stories.tsx"],"names":[],"mappings":"AAIA,OAAO,aAAa,CAAC;AAErB,OAAO,KAAwD,MAAM,OAAO,CAAC;AAS7E,OAAO,EAGL,KAAK,KAAK,EASX,MAAM,uBAAuB,CAAC;AAK/B,OAAO,EAAE,KAAK,aAAa,EAAE,MAAM,YAAY,CAAC;AAuEhD,KAAK,kBAAkB,GAAG;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,KAAK,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,aAAa,EAAE,CAAC;CAC3B,CAAC;AAwHF,KAAK,UAAU,GAAG;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB,CAAC;;;;gBAxHQ,kBAAkB;kBAChB,OAAO;kBACP,MAAM,IAAI;mBACT,MAAM,IAAI;;;mBAgPN,UAAU;;;;;;;;;;;;;;;;;;;;;AAJ3B,wBAME;AAIF,eAAO,MAAM,OAAO;;;;CASnB,CAAC"}
|
|
@@ -1,371 +0,0 @@
|
|
|
1
|
-
//
|
|
2
|
-
// Copyright 2023 DXOS.org
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
import '@dxos-theme';
|
|
6
|
-
|
|
7
|
-
import React, { type FC, useEffect, useMemo, useRef, useState } from 'react';
|
|
8
|
-
|
|
9
|
-
import { createDocAccessor, createObject } from '@dxos/echo-db';
|
|
10
|
-
import { Expando } from '@dxos/echo-schema';
|
|
11
|
-
import { PublicKey } from '@dxos/keys';
|
|
12
|
-
import { live } from '@dxos/live-object';
|
|
13
|
-
import { log } from '@dxos/log';
|
|
14
|
-
import { faker } from '@dxos/random';
|
|
15
|
-
import { Button, Icon, useThemeContext } from '@dxos/react-ui';
|
|
16
|
-
import {
|
|
17
|
-
type Comment,
|
|
18
|
-
type CommentsOptions,
|
|
19
|
-
type Range,
|
|
20
|
-
automerge,
|
|
21
|
-
comments,
|
|
22
|
-
createBasicExtensions,
|
|
23
|
-
createThemeExtensions,
|
|
24
|
-
listener,
|
|
25
|
-
scrollThreadIntoView,
|
|
26
|
-
useComments,
|
|
27
|
-
useTextEditor,
|
|
28
|
-
} from '@dxos/react-ui-editor';
|
|
29
|
-
import { hoverableControls, hoverableFocusedWithinControls } from '@dxos/react-ui-theme';
|
|
30
|
-
import { withTheme } from '@dxos/storybook-utils';
|
|
31
|
-
|
|
32
|
-
import { MessageBody, MessageHeading, MessageRoot, MessageTextbox } from '../Message';
|
|
33
|
-
import { type MessageEntity } from '../testing';
|
|
34
|
-
import { translations } from '../translations';
|
|
35
|
-
|
|
36
|
-
import { Thread } from './Thread';
|
|
37
|
-
|
|
38
|
-
faker.seed(101);
|
|
39
|
-
|
|
40
|
-
const authorId = PublicKey.random().toHex();
|
|
41
|
-
|
|
42
|
-
//
|
|
43
|
-
// Editor
|
|
44
|
-
//
|
|
45
|
-
|
|
46
|
-
const Editor: FC<{
|
|
47
|
-
id?: string;
|
|
48
|
-
item: Expando;
|
|
49
|
-
comments: Comment[];
|
|
50
|
-
selected?: string;
|
|
51
|
-
onCreateComment: CommentsOptions['onCreate'];
|
|
52
|
-
onDeleteComment: CommentsOptions['onDelete'];
|
|
53
|
-
onUpdateComment: CommentsOptions['onUpdate'];
|
|
54
|
-
onSelectComment: CommentsOptions['onSelect'];
|
|
55
|
-
}> = ({
|
|
56
|
-
id = 'test',
|
|
57
|
-
item,
|
|
58
|
-
selected: selectedValue,
|
|
59
|
-
comments: commentRanges,
|
|
60
|
-
onCreateComment,
|
|
61
|
-
onDeleteComment,
|
|
62
|
-
onUpdateComment,
|
|
63
|
-
onSelectComment,
|
|
64
|
-
}) => {
|
|
65
|
-
const [selected, setSelected] = useState<string>();
|
|
66
|
-
|
|
67
|
-
const { themeMode } = useThemeContext();
|
|
68
|
-
const { parentRef, view } = useTextEditor(
|
|
69
|
-
() => ({
|
|
70
|
-
id,
|
|
71
|
-
initialValue: item.content,
|
|
72
|
-
extensions: [
|
|
73
|
-
createBasicExtensions(),
|
|
74
|
-
createThemeExtensions({ themeMode }),
|
|
75
|
-
automerge(createDocAccessor(item, ['content'])),
|
|
76
|
-
comments({
|
|
77
|
-
id,
|
|
78
|
-
onCreate: onCreateComment,
|
|
79
|
-
onDelete: onDeleteComment,
|
|
80
|
-
onUpdate: onUpdateComment,
|
|
81
|
-
onSelect: onSelectComment,
|
|
82
|
-
}),
|
|
83
|
-
],
|
|
84
|
-
}),
|
|
85
|
-
[id, item, themeMode],
|
|
86
|
-
);
|
|
87
|
-
useComments(view, id, commentRanges);
|
|
88
|
-
useEffect(() => {
|
|
89
|
-
if (view && !view.hasFocus && selectedValue !== selected) {
|
|
90
|
-
setSelected(selectedValue);
|
|
91
|
-
if (selectedValue) {
|
|
92
|
-
scrollThreadIntoView(view, selectedValue);
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
}, [view, selected, commentRanges, selectedValue]);
|
|
96
|
-
|
|
97
|
-
return <div ref={parentRef} />;
|
|
98
|
-
};
|
|
99
|
-
|
|
100
|
-
//
|
|
101
|
-
// Comments thread
|
|
102
|
-
//
|
|
103
|
-
|
|
104
|
-
type StoryCommentThread = {
|
|
105
|
-
id: string;
|
|
106
|
-
cursor?: string;
|
|
107
|
-
range?: Range;
|
|
108
|
-
yPos?: number;
|
|
109
|
-
messages: MessageEntity[];
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
const StoryThread: FC<{
|
|
113
|
-
thread: StoryCommentThread;
|
|
114
|
-
selected: boolean;
|
|
115
|
-
onSelect: () => void;
|
|
116
|
-
onResolve: () => void;
|
|
117
|
-
}> = ({ thread, selected, onSelect, onResolve }) => {
|
|
118
|
-
const containerRef = useRef<HTMLDivElement>(null);
|
|
119
|
-
const { themeMode } = useThemeContext();
|
|
120
|
-
// TODO(wittjosiah): This is a hack to reset the editor after a message is sent.
|
|
121
|
-
const [_count, _setCount] = useState(0);
|
|
122
|
-
const rerenderEditor = () => _setCount((count) => count + 1);
|
|
123
|
-
const messageRef = useRef('');
|
|
124
|
-
const extensions = useMemo(
|
|
125
|
-
() => [
|
|
126
|
-
createBasicExtensions({ placeholder: 'Enter comment' }),
|
|
127
|
-
createThemeExtensions({ themeMode }),
|
|
128
|
-
listener({ onChange: (text) => (messageRef.current = text) }),
|
|
129
|
-
],
|
|
130
|
-
[themeMode, _count],
|
|
131
|
-
);
|
|
132
|
-
|
|
133
|
-
const [autoFocus, setAutoFocus] = useState(false);
|
|
134
|
-
useEffect(() => {
|
|
135
|
-
if (selected) {
|
|
136
|
-
containerRef.current?.scrollIntoView({ block: 'center', behavior: 'smooth' });
|
|
137
|
-
if (thread.messages.length === 0) {
|
|
138
|
-
// TODO(burdon): Causes thread to be reselected when loses focus.
|
|
139
|
-
setAutoFocus(true);
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
}, [selected]);
|
|
143
|
-
|
|
144
|
-
const handleCreateMessage = () => {
|
|
145
|
-
if (messageRef.current?.length) {
|
|
146
|
-
const message = live(Expando, {
|
|
147
|
-
timestamp: new Date().toISOString(),
|
|
148
|
-
sender: { identityKey: authorId },
|
|
149
|
-
text: messageRef.current,
|
|
150
|
-
});
|
|
151
|
-
thread.messages.push(message as any);
|
|
152
|
-
|
|
153
|
-
messageRef.current = '';
|
|
154
|
-
setAutoFocus(true);
|
|
155
|
-
rerenderEditor();
|
|
156
|
-
}
|
|
157
|
-
};
|
|
158
|
-
|
|
159
|
-
return (
|
|
160
|
-
<Thread.Root current={selected} onClickCapture={onSelect}>
|
|
161
|
-
<div className='col-start-2 flex gap-1 text-xs text-description'>
|
|
162
|
-
<span>id:{thread.id.slice(0, 4)}</span>
|
|
163
|
-
<span>y:{thread.yPos}</span>
|
|
164
|
-
<span className='grow' />
|
|
165
|
-
{!thread.cursor && <Icon icon='ph--trash--regular' />}
|
|
166
|
-
</div>
|
|
167
|
-
|
|
168
|
-
<Thread.Header>
|
|
169
|
-
{thread.range?.from} – {thread.range?.to}
|
|
170
|
-
</Thread.Header>
|
|
171
|
-
|
|
172
|
-
{thread.messages.map((message) => (
|
|
173
|
-
<MessageRoot key={message.id} classNames={[hoverableControls, hoverableFocusedWithinControls]} {...message}>
|
|
174
|
-
<MessageHeading authorName={message.authorName} timestamp={message.timestamp} />
|
|
175
|
-
<MessageBody>{message.text}</MessageBody>
|
|
176
|
-
</MessageRoot>
|
|
177
|
-
))}
|
|
178
|
-
|
|
179
|
-
<div ref={containerRef} className='contents'>
|
|
180
|
-
<MessageTextbox
|
|
181
|
-
id={thread.id}
|
|
182
|
-
authorId={authorId}
|
|
183
|
-
autoFocus={autoFocus}
|
|
184
|
-
extensions={extensions}
|
|
185
|
-
onEditorFocus={onSelect}
|
|
186
|
-
onSend={handleCreateMessage}
|
|
187
|
-
/>
|
|
188
|
-
<Thread.Status />
|
|
189
|
-
<Button variant='ghost' classNames='px-1' title='Resolve' onClick={onResolve}>
|
|
190
|
-
<Icon icon='ph--check--regular' />
|
|
191
|
-
</Button>
|
|
192
|
-
</div>
|
|
193
|
-
</Thread.Root>
|
|
194
|
-
);
|
|
195
|
-
};
|
|
196
|
-
|
|
197
|
-
const Sidebar: FC<{
|
|
198
|
-
threads: StoryCommentThread[];
|
|
199
|
-
selected?: string;
|
|
200
|
-
onSelect: (thread: string) => void;
|
|
201
|
-
onResolve: (thread: string) => void;
|
|
202
|
-
}> = ({ threads, selected, onSelect, onResolve }) => {
|
|
203
|
-
// Sort by y-position.
|
|
204
|
-
const sortedThreads = useMemo(() => {
|
|
205
|
-
const sorted = [...threads];
|
|
206
|
-
return sorted.sort(({ yPos: a = Infinity }, { yPos: b = Infinity }) => {
|
|
207
|
-
return a < b ? -1 : a > b ? 1 : 0;
|
|
208
|
-
});
|
|
209
|
-
}, [threads]);
|
|
210
|
-
|
|
211
|
-
return (
|
|
212
|
-
<>
|
|
213
|
-
{sortedThreads.map((thread) => (
|
|
214
|
-
<StoryThread
|
|
215
|
-
key={thread.id}
|
|
216
|
-
thread={thread}
|
|
217
|
-
selected={thread.id === selected}
|
|
218
|
-
onSelect={() => onSelect(thread.id)}
|
|
219
|
-
onResolve={() => onResolve(thread.id)}
|
|
220
|
-
/>
|
|
221
|
-
))}
|
|
222
|
-
</>
|
|
223
|
-
);
|
|
224
|
-
};
|
|
225
|
-
|
|
226
|
-
//
|
|
227
|
-
// Story container.
|
|
228
|
-
//
|
|
229
|
-
|
|
230
|
-
type StoryProps = {
|
|
231
|
-
text?: string;
|
|
232
|
-
autoCreate?: boolean;
|
|
233
|
-
};
|
|
234
|
-
|
|
235
|
-
const Story = ({ text, autoCreate }: StoryProps) => {
|
|
236
|
-
const [item] = useState(createObject(live(Expando, { content: text ?? '' })));
|
|
237
|
-
const [threads, setThreads] = useState<StoryCommentThread[]>([]);
|
|
238
|
-
const [selected, setSelected] = useState<string>();
|
|
239
|
-
|
|
240
|
-
const comments = useMemo<Comment[]>(() => threads.map(({ id, cursor }) => ({ id, cursor })), [threads]);
|
|
241
|
-
|
|
242
|
-
// Filter by visibility.
|
|
243
|
-
const visibleThreads = useMemo(() => threads.filter((thread) => thread.yPos !== undefined), [threads]);
|
|
244
|
-
|
|
245
|
-
const handleCreateComment: CommentsOptions['onCreate'] = ({ cursor, location }) => {
|
|
246
|
-
const id = PublicKey.random().toHex();
|
|
247
|
-
log.info('create', { id: id.slice(0, 4), cursor });
|
|
248
|
-
setThreads((threads) => [
|
|
249
|
-
...threads,
|
|
250
|
-
{
|
|
251
|
-
id,
|
|
252
|
-
cursor,
|
|
253
|
-
yPos: location ? Math.floor(location.top) : undefined,
|
|
254
|
-
messages: autoCreate
|
|
255
|
-
? faker.helpers.multiple(
|
|
256
|
-
() => ({
|
|
257
|
-
id: PublicKey.random().toHex(),
|
|
258
|
-
timestamp: new Date().toISOString(),
|
|
259
|
-
authorId: PublicKey.random().toHex(),
|
|
260
|
-
text: faker.lorem.sentence(),
|
|
261
|
-
}),
|
|
262
|
-
{ count: { min: 1, max: 3 } },
|
|
263
|
-
)
|
|
264
|
-
: [],
|
|
265
|
-
},
|
|
266
|
-
]);
|
|
267
|
-
|
|
268
|
-
return id;
|
|
269
|
-
};
|
|
270
|
-
|
|
271
|
-
const handleDeleteComment: CommentsOptions['onDelete'] = ({ id }) => {
|
|
272
|
-
log.info('delete', { id: id.slice(0, 4) });
|
|
273
|
-
setThreads((threads) =>
|
|
274
|
-
threads.map((thread) => {
|
|
275
|
-
if (thread.id === id) {
|
|
276
|
-
thread.cursor = undefined;
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
return thread;
|
|
280
|
-
}),
|
|
281
|
-
);
|
|
282
|
-
|
|
283
|
-
setSelected((selected) => (selected === id ? undefined : selected));
|
|
284
|
-
};
|
|
285
|
-
|
|
286
|
-
const handleUpdateComment: CommentsOptions['onUpdate'] = ({ id, cursor }) => {
|
|
287
|
-
log.info('update', { id: id.slice(0, 4), cursor });
|
|
288
|
-
setThreads((threads) =>
|
|
289
|
-
threads.map((thread) => {
|
|
290
|
-
if (thread.id === id) {
|
|
291
|
-
thread.cursor = cursor;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
return thread;
|
|
295
|
-
}),
|
|
296
|
-
);
|
|
297
|
-
};
|
|
298
|
-
|
|
299
|
-
const handleSelectComment: CommentsOptions['onSelect'] = ({ comments, selection: { current, closest } }) => {
|
|
300
|
-
log.info('select', { active: current?.slice(0, 4), closest: closest?.slice(0, 4) });
|
|
301
|
-
setThreads((threads) =>
|
|
302
|
-
threads.map((thread) => {
|
|
303
|
-
const comment = comments.find(({ comment }) => comment.id === thread.id);
|
|
304
|
-
if (comment) {
|
|
305
|
-
thread.yPos = comment.location ? Math.floor(comment.location.top) : undefined;
|
|
306
|
-
thread.range = { from: comment.range.from, to: comment.range.to };
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
return thread;
|
|
310
|
-
}),
|
|
311
|
-
);
|
|
312
|
-
|
|
313
|
-
setSelected(current);
|
|
314
|
-
};
|
|
315
|
-
|
|
316
|
-
// TODO(burdon): Focus switches to other thread.
|
|
317
|
-
const handleSelectThread = (id: string) => {
|
|
318
|
-
setSelected(id);
|
|
319
|
-
};
|
|
320
|
-
|
|
321
|
-
const handleResolveThread = (id: string) => {
|
|
322
|
-
setThreads((threads) => [...threads.filter((thread) => thread.id !== id)]);
|
|
323
|
-
setSelected(undefined);
|
|
324
|
-
};
|
|
325
|
-
|
|
326
|
-
return (
|
|
327
|
-
<main className='fixed inset-0 grid grid-cols-[1fr_24rem]'>
|
|
328
|
-
<div role='none' className='max-bs-full overflow-y-auto p-4'>
|
|
329
|
-
<Editor
|
|
330
|
-
item={item}
|
|
331
|
-
selected={selected}
|
|
332
|
-
comments={comments}
|
|
333
|
-
onCreateComment={handleCreateComment}
|
|
334
|
-
onDeleteComment={handleDeleteComment}
|
|
335
|
-
onUpdateComment={handleUpdateComment}
|
|
336
|
-
onSelectComment={handleSelectComment}
|
|
337
|
-
/>
|
|
338
|
-
</div>
|
|
339
|
-
|
|
340
|
-
<div role='none' className='max-bs-full overflow-y-auto p-4'>
|
|
341
|
-
<Sidebar
|
|
342
|
-
threads={visibleThreads}
|
|
343
|
-
selected={selected}
|
|
344
|
-
onSelect={handleSelectThread}
|
|
345
|
-
onResolve={handleResolveThread}
|
|
346
|
-
/>
|
|
347
|
-
</div>
|
|
348
|
-
</main>
|
|
349
|
-
);
|
|
350
|
-
};
|
|
351
|
-
|
|
352
|
-
export default {
|
|
353
|
-
title: 'ui/react-ui-thread/Comments',
|
|
354
|
-
component: StoryThread,
|
|
355
|
-
decorators: [withTheme],
|
|
356
|
-
render: (args: StoryProps) => <Story {...args} />,
|
|
357
|
-
parameters: { translations, layout: 'fullscreen' },
|
|
358
|
-
};
|
|
359
|
-
|
|
360
|
-
const str = (...lines: string[]) => lines.join('\n');
|
|
361
|
-
|
|
362
|
-
export const Default = {
|
|
363
|
-
args: {
|
|
364
|
-
text: str(
|
|
365
|
-
'# Comments',
|
|
366
|
-
'',
|
|
367
|
-
str(...faker.helpers.multiple(() => [faker.lorem.paragraph({ min: 5, max: 10 }), ''], { count: 20 }).flat()),
|
|
368
|
-
'',
|
|
369
|
-
),
|
|
370
|
-
},
|
|
371
|
-
};
|
package/src/testing.tsx
DELETED
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
//
|
|
2
|
-
// Copyright 2024 DXOS.org
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
import React from 'react';
|
|
6
|
-
|
|
7
|
-
import { Button, Icon } from '@dxos/react-ui';
|
|
8
|
-
import { hoverableControlItem, hoverableControls, hoverableFocusedWithinControls, mx } from '@dxos/react-ui-theme';
|
|
9
|
-
|
|
10
|
-
import { MessageBody, MessageHeading } from './Message';
|
|
11
|
-
import { type MessageMetadata } from './types';
|
|
12
|
-
|
|
13
|
-
export type MessageEntity<PartValue = any> = MessageMetadata & {
|
|
14
|
-
text: string;
|
|
15
|
-
parts?: PartValue[];
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
export type MessageTextProps = MessageMetadata & {
|
|
19
|
-
text: string;
|
|
20
|
-
onDelete?: () => void;
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
export const MessageStoryText = ({ text, onDelete, authorName, timestamp }: MessageTextProps) => {
|
|
24
|
-
return (
|
|
25
|
-
<>
|
|
26
|
-
<MessageHeading authorName={authorName} timestamp={timestamp}>
|
|
27
|
-
{onDelete && (
|
|
28
|
-
<Button
|
|
29
|
-
variant='ghost'
|
|
30
|
-
classNames={['p-1.5 min-bs-0 mie-1 items-start transition-opacity', hoverableControlItem]}
|
|
31
|
-
onClick={() => onDelete()}
|
|
32
|
-
>
|
|
33
|
-
<Icon icon='ph--x--regular' />
|
|
34
|
-
</Button>
|
|
35
|
-
)}
|
|
36
|
-
</MessageHeading>
|
|
37
|
-
<MessageBody>{text}</MessageBody>
|
|
38
|
-
</>
|
|
39
|
-
);
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
export type MessagePartProps<PartValue> = {
|
|
43
|
-
part: PartValue;
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
export const MessageStoryBlock = <PartValue,>({ part }: MessagePartProps<PartValue>) => {
|
|
47
|
-
return (
|
|
48
|
-
<div
|
|
49
|
-
role='none'
|
|
50
|
-
className={mx('grid grid-cols-subgrid col-span-3', hoverableControls, hoverableFocusedWithinControls)}
|
|
51
|
-
>
|
|
52
|
-
<pre className='font-mono max-is-full overflow-x-auto col-span-3'>
|
|
53
|
-
<code>{JSON.stringify(part, undefined, 2)}</code>
|
|
54
|
-
</pre>
|
|
55
|
-
</div>
|
|
56
|
-
);
|
|
57
|
-
};
|