@dxos/plugin-thread 0.6.8-main.046e6cf
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 +8 -0
- package/README.md +15 -0
- package/dist/lib/browser/ThreadArticle-J4VCLPMS.mjs +18 -0
- package/dist/lib/browser/ThreadArticle-J4VCLPMS.mjs.map +7 -0
- package/dist/lib/browser/ThreadMain-VV7MGQZJ.mjs +26 -0
- package/dist/lib/browser/ThreadMain-VV7MGQZJ.mjs.map +7 -0
- package/dist/lib/browser/chunk-5IBJO53M.mjs +21 -0
- package/dist/lib/browser/chunk-5IBJO53M.mjs.map +7 -0
- package/dist/lib/browser/chunk-CQ2RYETH.mjs +410 -0
- package/dist/lib/browser/chunk-CQ2RYETH.mjs.map +7 -0
- package/dist/lib/browser/chunk-TL3JITN7.mjs +19 -0
- package/dist/lib/browser/chunk-TL3JITN7.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +1062 -0
- package/dist/lib/browser/index.mjs.map +7 -0
- package/dist/lib/browser/meta.json +1 -0
- package/dist/lib/browser/meta.mjs +11 -0
- package/dist/lib/browser/meta.mjs.map +7 -0
- package/dist/lib/browser/types.mjs +8 -0
- package/dist/lib/browser/types.mjs.map +7 -0
- package/dist/lib/node/ThreadArticle-CSENFNNT.cjs +44 -0
- package/dist/lib/node/ThreadArticle-CSENFNNT.cjs.map +7 -0
- package/dist/lib/node/ThreadMain-XP4YSF7U.cjs +52 -0
- package/dist/lib/node/ThreadMain-XP4YSF7U.cjs.map +7 -0
- package/dist/lib/node/chunk-3VAAUEU5.cjs +431 -0
- package/dist/lib/node/chunk-3VAAUEU5.cjs.map +7 -0
- package/dist/lib/node/chunk-BWBONZGU.cjs +55 -0
- package/dist/lib/node/chunk-BWBONZGU.cjs.map +7 -0
- package/dist/lib/node/chunk-WPPLQNMJ.cjs +38 -0
- package/dist/lib/node/chunk-WPPLQNMJ.cjs.map +7 -0
- package/dist/lib/node/index.cjs +1067 -0
- package/dist/lib/node/index.cjs.map +7 -0
- package/dist/lib/node/meta.cjs +32 -0
- package/dist/lib/node/meta.cjs.map +7 -0
- package/dist/lib/node/meta.json +1 -0
- package/dist/lib/node/types.cjs +30 -0
- package/dist/lib/node/types.cjs.map +7 -0
- package/dist/types/src/ThreadPlugin.d.ts +4 -0
- package/dist/types/src/ThreadPlugin.d.ts.map +1 -0
- package/dist/types/src/components/Chat.stories.d.ts +46 -0
- package/dist/types/src/components/Chat.stories.d.ts.map +1 -0
- package/dist/types/src/components/ChatContainer.d.ts +7 -0
- package/dist/types/src/components/ChatContainer.d.ts.map +1 -0
- package/dist/types/src/components/CommentContainer.d.ts +4 -0
- package/dist/types/src/components/CommentContainer.d.ts.map +1 -0
- package/dist/types/src/components/Comments.stories.d.ts +44 -0
- package/dist/types/src/components/Comments.stories.d.ts.map +1 -0
- package/dist/types/src/components/CommentsContainer.d.ts +26 -0
- package/dist/types/src/components/CommentsContainer.d.ts.map +1 -0
- package/dist/types/src/components/MessageContainer.d.ts +9 -0
- package/dist/types/src/components/MessageContainer.d.ts.map +1 -0
- package/dist/types/src/components/ThreadArticle.d.ts +5 -0
- package/dist/types/src/components/ThreadArticle.d.ts.map +1 -0
- package/dist/types/src/components/ThreadMain.d.ts +7 -0
- package/dist/types/src/components/ThreadMain.d.ts.map +1 -0
- package/dist/types/src/components/ThreadSettings.d.ts +6 -0
- package/dist/types/src/components/ThreadSettings.d.ts.map +1 -0
- package/dist/types/src/components/command-extension.d.ts +3 -0
- package/dist/types/src/components/command-extension.d.ts.map +1 -0
- package/dist/types/src/components/index.d.ts +22 -0
- package/dist/types/src/components/index.d.ts.map +1 -0
- package/dist/types/src/components/testing.d.ts +67 -0
- package/dist/types/src/components/testing.d.ts.map +1 -0
- package/dist/types/src/components/types.d.ts +28 -0
- package/dist/types/src/components/types.d.ts.map +1 -0
- package/dist/types/src/hooks/analytics.d.ts +4 -0
- package/dist/types/src/hooks/analytics.d.ts.map +1 -0
- package/dist/types/src/hooks/index.d.ts +3 -0
- package/dist/types/src/hooks/index.d.ts.map +1 -0
- package/dist/types/src/hooks/useStatus.d.ts +3 -0
- package/dist/types/src/hooks/useStatus.d.ts.map +1 -0
- package/dist/types/src/index.d.ts +6 -0
- package/dist/types/src/index.d.ts.map +1 -0
- package/dist/types/src/meta.d.ts +16 -0
- package/dist/types/src/meta.d.ts.map +1 -0
- package/dist/types/src/translations.d.ts +34 -0
- package/dist/types/src/translations.d.ts.map +1 -0
- package/dist/types/src/types.d.ts +19 -0
- package/dist/types/src/types.d.ts.map +1 -0
- package/dist/types/src/types.test.d.ts +2 -0
- package/dist/types/src/types.test.d.ts.map +1 -0
- package/dist/types/src/util.d.ts +6 -0
- package/dist/types/src/util.d.ts.map +1 -0
- package/package.json +99 -0
- package/src/ThreadPlugin.tsx +744 -0
- package/src/components/Chat.stories.tsx +76 -0
- package/src/components/ChatContainer.tsx +127 -0
- package/src/components/CommentContainer.tsx +206 -0
- package/src/components/Comments.stories.tsx +66 -0
- package/src/components/CommentsContainer.tsx +114 -0
- package/src/components/MessageContainer.tsx +200 -0
- package/src/components/ThreadArticle.tsx +18 -0
- package/src/components/ThreadMain.tsx +27 -0
- package/src/components/ThreadSettings.tsx +23 -0
- package/src/components/command-extension.ts +36 -0
- package/src/components/index.ts +16 -0
- package/src/components/testing.ts +46 -0
- package/src/components/types.ts +32 -0
- package/src/hooks/analytics.ts +39 -0
- package/src/hooks/index.ts +6 -0
- package/src/hooks/useStatus.ts +39 -0
- package/src/index.ts +12 -0
- package/src/meta.tsx +20 -0
- package/src/translations.ts +41 -0
- package/src/types.test.ts +13 -0
- package/src/types.ts +41 -0
- package/src/util.ts +26 -0
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
import { EditorView } from '@codemirror/view';
|
|
5
|
+
import { Check, PencilSimple, X } from '@phosphor-icons/react';
|
|
6
|
+
import React, { forwardRef, useCallback, useEffect, useRef, useState } from 'react';
|
|
7
|
+
|
|
8
|
+
import { Surface } from '@dxos/app-framework';
|
|
9
|
+
import { type Expando, type EchoReactiveObject } from '@dxos/echo-schema';
|
|
10
|
+
import { type MessageType } from '@dxos/plugin-space/types';
|
|
11
|
+
import { PublicKey } from '@dxos/react-client';
|
|
12
|
+
import { type SpaceMember } from '@dxos/react-client/echo';
|
|
13
|
+
import { useIdentity, type Identity } from '@dxos/react-client/halo';
|
|
14
|
+
import {
|
|
15
|
+
Button,
|
|
16
|
+
ButtonGroup,
|
|
17
|
+
DensityProvider,
|
|
18
|
+
Tooltip,
|
|
19
|
+
useOnTransition,
|
|
20
|
+
useThemeContext,
|
|
21
|
+
useTranslation,
|
|
22
|
+
} from '@dxos/react-ui';
|
|
23
|
+
import { createBasicExtensions, createThemeExtensions, useTextEditor } from '@dxos/react-ui-editor';
|
|
24
|
+
import { Mosaic, type MosaicTileComponent } from '@dxos/react-ui-mosaic';
|
|
25
|
+
import {
|
|
26
|
+
getSize,
|
|
27
|
+
hoverableControlItem,
|
|
28
|
+
hoverableControls,
|
|
29
|
+
hoverableFocusedWithinControls,
|
|
30
|
+
mx,
|
|
31
|
+
} from '@dxos/react-ui-theme';
|
|
32
|
+
import { MessageHeading, MessageRoot } from '@dxos/react-ui-thread';
|
|
33
|
+
import { nonNullable } from '@dxos/util';
|
|
34
|
+
|
|
35
|
+
import { command } from './command-extension';
|
|
36
|
+
import { useOnEditAnalytics } from '../hooks';
|
|
37
|
+
import { THREAD_ITEM, THREAD_PLUGIN } from '../meta';
|
|
38
|
+
import { getMessageMetadata } from '../util';
|
|
39
|
+
|
|
40
|
+
const messageControlClassNames = ['p-1 min-bs-0 transition-opacity items-start', hoverableControlItem];
|
|
41
|
+
|
|
42
|
+
export const MessageContainer = ({
|
|
43
|
+
message,
|
|
44
|
+
members,
|
|
45
|
+
onDelete,
|
|
46
|
+
}: {
|
|
47
|
+
message: MessageType;
|
|
48
|
+
members: SpaceMember[];
|
|
49
|
+
onDelete?: (id: string) => void;
|
|
50
|
+
}) => {
|
|
51
|
+
const senderIdentity = members.find(
|
|
52
|
+
(member) => message.sender.identityKey && PublicKey.equals(member.identity.identityKey, message.sender.identityKey),
|
|
53
|
+
)?.identity;
|
|
54
|
+
const messageMetadata = getMessageMetadata(message.id, senderIdentity);
|
|
55
|
+
const userIsAuthor = useIdentity()?.identityKey.toHex() === messageMetadata.authorId;
|
|
56
|
+
const [editing, setEditing] = useState(false);
|
|
57
|
+
const handleDelete = useCallback(() => onDelete?.(message.id), [message, onDelete]);
|
|
58
|
+
const { t } = useTranslation(THREAD_PLUGIN);
|
|
59
|
+
const editLabel = t(editing ? 'save message label' : 'edit message label');
|
|
60
|
+
const deleteLabel = t('delete message label');
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<MessageRoot {...messageMetadata} classNames={[hoverableControls, hoverableFocusedWithinControls]}>
|
|
64
|
+
<MessageHeading authorName={messageMetadata.authorName} timestamp={messageMetadata.timestamp}>
|
|
65
|
+
<ButtonGroup classNames='mie-1'>
|
|
66
|
+
{userIsAuthor && (
|
|
67
|
+
<Tooltip.Root>
|
|
68
|
+
<Tooltip.Trigger asChild>
|
|
69
|
+
<Button
|
|
70
|
+
variant='ghost'
|
|
71
|
+
data-testid={editing ? 'thread.message.save' : 'thread.message.edit'}
|
|
72
|
+
classNames={messageControlClassNames}
|
|
73
|
+
onClick={() => setEditing((editing) => !editing)}
|
|
74
|
+
>
|
|
75
|
+
<span className='sr-only'>{editLabel}</span>
|
|
76
|
+
{editing ? <Check className={getSize(4)} /> : <PencilSimple className={getSize(4)} />}
|
|
77
|
+
</Button>
|
|
78
|
+
</Tooltip.Trigger>
|
|
79
|
+
<Tooltip.Portal>
|
|
80
|
+
<Tooltip.Content classNames='z-[21]'>
|
|
81
|
+
{editLabel}
|
|
82
|
+
<Tooltip.Arrow />
|
|
83
|
+
</Tooltip.Content>
|
|
84
|
+
</Tooltip.Portal>
|
|
85
|
+
</Tooltip.Root>
|
|
86
|
+
)}
|
|
87
|
+
{onDelete && (
|
|
88
|
+
<Tooltip.Root>
|
|
89
|
+
<Tooltip.Trigger asChild>
|
|
90
|
+
<Button
|
|
91
|
+
variant='ghost'
|
|
92
|
+
data-testid='thread.message.delete'
|
|
93
|
+
classNames={messageControlClassNames}
|
|
94
|
+
onClick={handleDelete}
|
|
95
|
+
>
|
|
96
|
+
<span className='sr-only'>{deleteLabel}</span>
|
|
97
|
+
<X className={getSize(4)} />
|
|
98
|
+
</Button>
|
|
99
|
+
</Tooltip.Trigger>
|
|
100
|
+
<Tooltip.Portal>
|
|
101
|
+
<Tooltip.Content classNames='z-[21]'>
|
|
102
|
+
{deleteLabel}
|
|
103
|
+
<Tooltip.Arrow />
|
|
104
|
+
</Tooltip.Content>
|
|
105
|
+
</Tooltip.Portal>
|
|
106
|
+
</Tooltip.Root>
|
|
107
|
+
)}
|
|
108
|
+
</ButtonGroup>
|
|
109
|
+
</MessageHeading>
|
|
110
|
+
<TextboxBlock message={message} isAuthor={userIsAuthor} editing={editing} />
|
|
111
|
+
{message.parts?.filter(nonNullable).map((part, index) => <MessagePart key={index} part={part} />)}
|
|
112
|
+
</MessageRoot>
|
|
113
|
+
);
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const MessagePart = ({ part }: { part: Expando }) => {
|
|
117
|
+
return (
|
|
118
|
+
<Mosaic.Container id={part.id}>
|
|
119
|
+
<Mosaic.DraggableTile type={THREAD_ITEM} path={part.id} item={part} Component={MessageBlockObjectTile} />
|
|
120
|
+
</Mosaic.Container>
|
|
121
|
+
);
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const TextboxBlock = ({
|
|
125
|
+
message,
|
|
126
|
+
isAuthor,
|
|
127
|
+
editing,
|
|
128
|
+
}: {
|
|
129
|
+
message: MessageType;
|
|
130
|
+
editing?: boolean;
|
|
131
|
+
isAuthor?: boolean;
|
|
132
|
+
identity?: Identity;
|
|
133
|
+
}) => {
|
|
134
|
+
const { themeMode } = useThemeContext();
|
|
135
|
+
const inMemoryContentRef = useRef(message.text);
|
|
136
|
+
|
|
137
|
+
const handleDocumentChange = useCallback((newState: string) => {
|
|
138
|
+
inMemoryContentRef.current = newState;
|
|
139
|
+
}, []);
|
|
140
|
+
|
|
141
|
+
const saveDocumentChange = useCallback(() => {
|
|
142
|
+
message.text = inMemoryContentRef.current;
|
|
143
|
+
}, [message]);
|
|
144
|
+
|
|
145
|
+
useOnTransition(editing, true, false, saveDocumentChange);
|
|
146
|
+
|
|
147
|
+
const { parentRef, focusAttributes, view } = useTextEditor(
|
|
148
|
+
() => ({
|
|
149
|
+
initialValue: message.text,
|
|
150
|
+
extensions: [
|
|
151
|
+
createBasicExtensions({ readonly: !isAuthor || !editing }),
|
|
152
|
+
createThemeExtensions({ themeMode }),
|
|
153
|
+
command,
|
|
154
|
+
EditorView.updateListener.of((update) => {
|
|
155
|
+
if (update.docChanged) {
|
|
156
|
+
handleDocumentChange(update.state.doc.toString());
|
|
157
|
+
}
|
|
158
|
+
}),
|
|
159
|
+
],
|
|
160
|
+
}),
|
|
161
|
+
[message.text, editing, isAuthor, themeMode, handleDocumentChange],
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
useEffect(() => {
|
|
165
|
+
editing && view?.focus();
|
|
166
|
+
}, [editing, view]);
|
|
167
|
+
|
|
168
|
+
useOnEditAnalytics(message, !!editing);
|
|
169
|
+
|
|
170
|
+
return <div role='none' ref={parentRef} className='mie-4' {...focusAttributes} />;
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const MessageBlockObjectTile: MosaicTileComponent<EchoReactiveObject<any>> = forwardRef(
|
|
174
|
+
({ draggableStyle, draggableProps, item, active, ...props }, forwardedRef) => {
|
|
175
|
+
let title = item.name ?? item.title ?? item.__typename ?? 'Object';
|
|
176
|
+
if (typeof title !== 'string') {
|
|
177
|
+
title = title?.content ?? '';
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return (
|
|
181
|
+
<div
|
|
182
|
+
role='group'
|
|
183
|
+
className={mx('grid col-span-3 py-1 pr-4', hoverableControls, hoverableFocusedWithinControls)}
|
|
184
|
+
style={draggableStyle}
|
|
185
|
+
ref={forwardedRef}
|
|
186
|
+
>
|
|
187
|
+
<DensityProvider density='fine'>
|
|
188
|
+
<Surface
|
|
189
|
+
role='card'
|
|
190
|
+
limit={1}
|
|
191
|
+
data={{ content: item }}
|
|
192
|
+
draggableProps={draggableProps}
|
|
193
|
+
fallback={title}
|
|
194
|
+
{...props}
|
|
195
|
+
/>
|
|
196
|
+
</DensityProvider>
|
|
197
|
+
</div>
|
|
198
|
+
);
|
|
199
|
+
},
|
|
200
|
+
);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import React, { type ComponentProps, type FC } from 'react';
|
|
6
|
+
|
|
7
|
+
import { ChatContainer } from './ChatContainer';
|
|
8
|
+
|
|
9
|
+
const ThreadArticle: FC<Pick<ComponentProps<typeof ChatContainer>, 'thread' | 'context'>> = (props) => {
|
|
10
|
+
// TODO(burdon): Factor out Main container across plugins?
|
|
11
|
+
return (
|
|
12
|
+
<div role='none' className='row-span-2'>
|
|
13
|
+
<ChatContainer {...props} />
|
|
14
|
+
</div>
|
|
15
|
+
);
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export default ThreadArticle;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import React, { type FC } from 'react';
|
|
6
|
+
|
|
7
|
+
import { type ThreadType } from '@dxos/plugin-space/types';
|
|
8
|
+
import { Main } from '@dxos/react-ui';
|
|
9
|
+
import {
|
|
10
|
+
baseSurface,
|
|
11
|
+
topbarBlockPaddingStart,
|
|
12
|
+
fixedInsetFlexLayout,
|
|
13
|
+
bottombarBlockPaddingEnd,
|
|
14
|
+
} from '@dxos/react-ui-theme';
|
|
15
|
+
|
|
16
|
+
import { ChatContainer } from './ChatContainer';
|
|
17
|
+
|
|
18
|
+
const ThreadMain: FC<{ thread: ThreadType }> = ({ thread }) => {
|
|
19
|
+
// TODO(burdon): Factor out Main container across plugins?
|
|
20
|
+
return (
|
|
21
|
+
<Main.Content classNames={[baseSurface, fixedInsetFlexLayout, topbarBlockPaddingStart, bottombarBlockPaddingEnd]}>
|
|
22
|
+
<ChatContainer thread={thread} />
|
|
23
|
+
</Main.Content>
|
|
24
|
+
);
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export default ThreadMain;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import React from 'react';
|
|
6
|
+
|
|
7
|
+
import { SettingsValue } from '@dxos/plugin-settings';
|
|
8
|
+
import { Input, useTranslation } from '@dxos/react-ui';
|
|
9
|
+
|
|
10
|
+
import { THREAD_PLUGIN } from '../meta';
|
|
11
|
+
import type { ThreadSettingsProps } from '../types';
|
|
12
|
+
|
|
13
|
+
export const ThreadSettings = ({ settings }: { settings: ThreadSettingsProps }) => {
|
|
14
|
+
const { t } = useTranslation(THREAD_PLUGIN);
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<>
|
|
18
|
+
<SettingsValue label={t('settings standalone label')}>
|
|
19
|
+
<Input.Switch checked={settings.standalone} onCheckedChange={(checked) => (settings.standalone = !!checked)} />
|
|
20
|
+
</SettingsValue>
|
|
21
|
+
</>
|
|
22
|
+
);
|
|
23
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { StreamLanguage, HighlightStyle, syntaxHighlighting } from '@codemirror/language';
|
|
6
|
+
import { type Extension } from '@codemirror/state';
|
|
7
|
+
|
|
8
|
+
import { tags } from '@dxos/react-ui-editor';
|
|
9
|
+
import { mx, tagRoot } from '@dxos/react-ui-theme';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* https://github.com/codemirror/stream-parser/blob/main/test/test-stream-parser.ts
|
|
13
|
+
*/
|
|
14
|
+
const parser = StreamLanguage.define<{ count: number }>({
|
|
15
|
+
startState: () => ({ count: 0 }),
|
|
16
|
+
token: (stream, state) => {
|
|
17
|
+
state.count++;
|
|
18
|
+
if (stream.eatSpace()) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
if (state.count === 1 && stream.match(/^(\/\w+)/)) {
|
|
22
|
+
return 'tagName';
|
|
23
|
+
}
|
|
24
|
+
stream.next();
|
|
25
|
+
return null;
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const styles = HighlightStyle.define([
|
|
30
|
+
{
|
|
31
|
+
tag: tags.tagName,
|
|
32
|
+
class: mx(tagRoot({ palette: 'cyan' }), 'text-base font-medium'),
|
|
33
|
+
},
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
export const command: Extension = [parser, syntaxHighlighting(styles)];
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import React from 'react';
|
|
6
|
+
|
|
7
|
+
// Lazily load components for content surfaces.
|
|
8
|
+
export const ThreadMain = React.lazy(() => import('./ThreadMain'));
|
|
9
|
+
export const ThreadArticle = React.lazy(() => import('./ThreadArticle'));
|
|
10
|
+
|
|
11
|
+
// TODO(wittjosiah): Suspense boundary for sidebar?
|
|
12
|
+
export * from './ThreadSettings';
|
|
13
|
+
export * from './CommentContainer';
|
|
14
|
+
export * from './MessageContainer';
|
|
15
|
+
export * from './CommentsContainer';
|
|
16
|
+
export * from './ChatContainer';
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { create, Expando } from '@dxos/echo-schema';
|
|
6
|
+
import { PublicKey } from '@dxos/keys';
|
|
7
|
+
import { MessageType, ThreadType } from '@dxos/plugin-space/types';
|
|
8
|
+
import { faker } from '@dxos/random';
|
|
9
|
+
import { type Identity } from '@dxos/react-client/halo';
|
|
10
|
+
|
|
11
|
+
export const createChatThread = (identity: Identity) => {
|
|
12
|
+
return create(ThreadType, {
|
|
13
|
+
messages: Array.from({ length: 8 }).map(() =>
|
|
14
|
+
create(MessageType, {
|
|
15
|
+
sender: {
|
|
16
|
+
identityKey: faker.datatype.boolean() ? identity.identityKey.toHex() : PublicKey.random().toHex(),
|
|
17
|
+
},
|
|
18
|
+
timestamp: faker.date.recent().toISOString(),
|
|
19
|
+
text: faker.lorem.sentences(3),
|
|
20
|
+
parts: faker.helpers.multiple(
|
|
21
|
+
() =>
|
|
22
|
+
faker.datatype.boolean({ probability: 0.8 })
|
|
23
|
+
? undefined
|
|
24
|
+
: create(Expando, { name: faker.lorem.sentence() }),
|
|
25
|
+
{ count: { min: 1, max: 3 } },
|
|
26
|
+
),
|
|
27
|
+
}),
|
|
28
|
+
),
|
|
29
|
+
});
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const createCommentThread = (identity: Identity) => {
|
|
33
|
+
return create(ThreadType, {
|
|
34
|
+
messages: faker.helpers.multiple(
|
|
35
|
+
() =>
|
|
36
|
+
create(MessageType, {
|
|
37
|
+
sender: {
|
|
38
|
+
identityKey: faker.datatype.boolean() ? identity.identityKey.toHex() : PublicKey.random().toHex(),
|
|
39
|
+
},
|
|
40
|
+
timestamp: new Date().toISOString(),
|
|
41
|
+
text: faker.lorem.sentences(3),
|
|
42
|
+
}),
|
|
43
|
+
{ count: { min: 2, max: 3 } },
|
|
44
|
+
),
|
|
45
|
+
});
|
|
46
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type EchoReactiveObject } from '@dxos/echo-schema';
|
|
6
|
+
import { type ThreadType } from '@dxos/plugin-space/types';
|
|
7
|
+
import type { ThreadProps } from '@dxos/react-ui-thread';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Props for components which connect an ECHO Thread object to the UI component Thread.
|
|
11
|
+
* @param space - the containing Space entity
|
|
12
|
+
* @param thread - the Thread entity
|
|
13
|
+
* @param context - current application context
|
|
14
|
+
* @param current - whether this thread is current (wrt ARIA) in the app
|
|
15
|
+
* @param autoFocusTextbox - whether to set `autoFocus` on the thread’s textbox
|
|
16
|
+
* @param onTextboxFocus - a callback for when the textbox is focused
|
|
17
|
+
* @param detached - whether this thread is detached from the object
|
|
18
|
+
* @param onAttend - combined callback for `onClickCapture` and `onFocusCapture` within the thread
|
|
19
|
+
* @param onDelete - callback for deleting the thread
|
|
20
|
+
*/
|
|
21
|
+
export type ThreadContainerProps = {
|
|
22
|
+
thread: ThreadType;
|
|
23
|
+
context?: EchoReactiveObject<any>;
|
|
24
|
+
autoFocusTextbox?: boolean;
|
|
25
|
+
onTextboxFocus?: () => void;
|
|
26
|
+
detached?: boolean;
|
|
27
|
+
onAttend?: () => void;
|
|
28
|
+
onResolve?: () => void;
|
|
29
|
+
onThreadDelete?: () => void;
|
|
30
|
+
onMessageDelete?: (messageId: string) => void;
|
|
31
|
+
onComment?: (thread: ThreadType) => void;
|
|
32
|
+
} & Pick<ThreadProps, 'current'>;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { useCallback } from 'react';
|
|
6
|
+
|
|
7
|
+
import { useIntentDispatcher } from '@dxos/app-framework';
|
|
8
|
+
import { type ReactiveObject } from '@dxos/echo-schema';
|
|
9
|
+
import { ObservabilityAction } from '@dxos/plugin-observability/meta';
|
|
10
|
+
import { getSpace } from '@dxos/react-client/echo';
|
|
11
|
+
import { useOnTransition } from '@dxos/react-ui';
|
|
12
|
+
|
|
13
|
+
export const useAnalyticsCallback = (spaceId: string | undefined, name: string, meta?: any) => {
|
|
14
|
+
const dispatch = useIntentDispatcher();
|
|
15
|
+
|
|
16
|
+
return useCallback(
|
|
17
|
+
(dynamicMeta?: any) => {
|
|
18
|
+
void dispatch({
|
|
19
|
+
action: ObservabilityAction.SEND_EVENT,
|
|
20
|
+
data: {
|
|
21
|
+
name,
|
|
22
|
+
properties: { ...meta, ...dynamicMeta, spaceId },
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
},
|
|
26
|
+
[dispatch, name, meta, spaceId],
|
|
27
|
+
);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const useOnEditAnalytics = (message: ReactiveObject<any>, editing: boolean) => {
|
|
31
|
+
const space = getSpace(message);
|
|
32
|
+
|
|
33
|
+
const onEditAnalytics = useAnalyticsCallback(space?.id, 'threads.comment-edited', {
|
|
34
|
+
messageId: message.id,
|
|
35
|
+
messageLength: message.text.length,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
useOnTransition(editing, true, false, onEditAnalytics);
|
|
39
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { useEffect, useState } from 'react';
|
|
6
|
+
|
|
7
|
+
import { type Space } from '@dxos/react-client/echo';
|
|
8
|
+
|
|
9
|
+
// TODO(burdon): Generalize and factor out.
|
|
10
|
+
export const useStatus = (space?: Space, channel?: string) => {
|
|
11
|
+
const [processing, setProcessing] = useState(false);
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
if (!space || !channel) {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const unsubscribe = space.listen(`status/${channel}`, (status) => {
|
|
18
|
+
const { event } = status.payload ?? {};
|
|
19
|
+
setProcessing(event === 'processing');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const t = setInterval(() => {
|
|
23
|
+
setProcessing((prev: any) => {
|
|
24
|
+
if (typeof prev?.ts === 'number' && Date.now() - prev.ts > 30_000) {
|
|
25
|
+
return undefined;
|
|
26
|
+
} else {
|
|
27
|
+
return prev;
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
}, 1_000);
|
|
31
|
+
|
|
32
|
+
return () => {
|
|
33
|
+
unsubscribe();
|
|
34
|
+
clearInterval(t);
|
|
35
|
+
};
|
|
36
|
+
}, [space, channel]);
|
|
37
|
+
|
|
38
|
+
return processing;
|
|
39
|
+
};
|
package/src/index.ts
ADDED
package/src/meta.tsx
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { Chat, type IconProps } from '@phosphor-icons/react';
|
|
6
|
+
import React from 'react';
|
|
7
|
+
|
|
8
|
+
import { pluginMeta } from '@dxos/app-framework';
|
|
9
|
+
|
|
10
|
+
export const THREAD_PLUGIN = 'dxos.org/plugin/thread';
|
|
11
|
+
export const THREAD_ITEM = `${THREAD_PLUGIN}/item`;
|
|
12
|
+
|
|
13
|
+
export default pluginMeta({
|
|
14
|
+
id: THREAD_PLUGIN,
|
|
15
|
+
name: 'Threads',
|
|
16
|
+
// TODO(wittjosiah): Update once scope of threads expands.
|
|
17
|
+
description: 'Comment threads on documents.',
|
|
18
|
+
iconComponent: (props: IconProps) => <Chat {...props} />,
|
|
19
|
+
iconSymbol: 'ph--chat--regular',
|
|
20
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { THREAD_PLUGIN } from './meta';
|
|
6
|
+
|
|
7
|
+
export default [
|
|
8
|
+
{
|
|
9
|
+
'en-US': {
|
|
10
|
+
[THREAD_PLUGIN]: {
|
|
11
|
+
'plugin name': 'Threads',
|
|
12
|
+
'channel name placeholder': 'New channel',
|
|
13
|
+
'channel title label': 'Title',
|
|
14
|
+
'delete channel label': 'Delete channel',
|
|
15
|
+
'create channel label': 'Create channel',
|
|
16
|
+
'message placeholder': 'Reply…',
|
|
17
|
+
'settings standalone label': 'Enable channel creation (experimental)',
|
|
18
|
+
'activity message': 'Processing…',
|
|
19
|
+
'anonymous label': 'Anonymous',
|
|
20
|
+
'delete message block label': 'Delete message block',
|
|
21
|
+
'detached thread label': 'Referenced content was removed',
|
|
22
|
+
'delete thread label': 'Delete thread',
|
|
23
|
+
'thread deleted label': 'You deleted a thread.',
|
|
24
|
+
'message deleted label': 'You deleted a message.',
|
|
25
|
+
'comments heading': 'Comments',
|
|
26
|
+
'chat heading': 'Chat',
|
|
27
|
+
'draft button': 'DRAFT',
|
|
28
|
+
'no comments title': 'No comments (yet)',
|
|
29
|
+
'no comments message':
|
|
30
|
+
'To start a comment thread, select a range of text in a document and click the <commentIcon></commentIcon> Create comment button in the toolbar.',
|
|
31
|
+
'toggle show resolved': 'Show resolved',
|
|
32
|
+
'unnamed object threads label': 'Threads',
|
|
33
|
+
'mark as resolved label': 'Mark as resolved',
|
|
34
|
+
'mark as unresolved label': 'Mark as unresolved',
|
|
35
|
+
'save message label': 'Save',
|
|
36
|
+
'edit message label': 'Edit',
|
|
37
|
+
'delete message label': 'Delete message',
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
];
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import type {
|
|
6
|
+
GraphBuilderProvides,
|
|
7
|
+
IntentResolverProvides,
|
|
8
|
+
MetadataRecordsProvides,
|
|
9
|
+
SettingsProvides,
|
|
10
|
+
SurfaceProvides,
|
|
11
|
+
TranslationsProvides,
|
|
12
|
+
} from '@dxos/app-framework';
|
|
13
|
+
import { type SchemaProvides } from '@dxos/plugin-client';
|
|
14
|
+
import { type MarkdownExtensionProvides } from '@dxos/plugin-markdown';
|
|
15
|
+
import { type ThreadType } from '@dxos/plugin-space/types';
|
|
16
|
+
|
|
17
|
+
import { THREAD_PLUGIN } from './meta';
|
|
18
|
+
|
|
19
|
+
const THREAD_ACTION = `${THREAD_PLUGIN}/action`;
|
|
20
|
+
export enum ThreadAction {
|
|
21
|
+
CREATE = `${THREAD_ACTION}/create`,
|
|
22
|
+
SELECT = `${THREAD_ACTION}/select`,
|
|
23
|
+
DELETE = `${THREAD_ACTION}/delete`,
|
|
24
|
+
DELETE_MESSAGE = `${THREAD_ACTION}/delete-message`,
|
|
25
|
+
TOGGLE_RESOLVED = `${THREAD_ACTION}/toggle-resolved`,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type ThreadPluginProvides = SurfaceProvides &
|
|
29
|
+
IntentResolverProvides &
|
|
30
|
+
GraphBuilderProvides &
|
|
31
|
+
MetadataRecordsProvides &
|
|
32
|
+
SettingsProvides &
|
|
33
|
+
TranslationsProvides &
|
|
34
|
+
SchemaProvides &
|
|
35
|
+
MarkdownExtensionProvides;
|
|
36
|
+
|
|
37
|
+
export type ThreadSettingsProps = { standalone?: boolean };
|
|
38
|
+
|
|
39
|
+
export interface ThreadModel {
|
|
40
|
+
root: ThreadType;
|
|
41
|
+
}
|
package/src/util.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { generateName } from '@dxos/display-name';
|
|
6
|
+
import { type PublicKey } from '@dxos/react-client';
|
|
7
|
+
import { type Identity } from '@dxos/react-client/halo';
|
|
8
|
+
import { type MessageMetadata } from '@dxos/react-ui-thread';
|
|
9
|
+
import { hexToFallback } from '@dxos/util';
|
|
10
|
+
|
|
11
|
+
export type MessagePropertiesProvider = (identityKey: PublicKey | undefined) => MessageMetadata;
|
|
12
|
+
|
|
13
|
+
export const getMessageMetadata = (id: string, identity?: Identity): MessageMetadata => {
|
|
14
|
+
const fallback = hexToFallback(identity?.identityKey.toHex() ?? '0');
|
|
15
|
+
return {
|
|
16
|
+
id,
|
|
17
|
+
authorId: identity?.identityKey.toHex(),
|
|
18
|
+
authorName:
|
|
19
|
+
identity?.profile?.displayName ??
|
|
20
|
+
(identity?.identityKey ? generateName(identity.identityKey.toHex()) : undefined),
|
|
21
|
+
authorAvatarProps: {
|
|
22
|
+
hue: identity?.profile?.data?.hue ?? fallback.hue,
|
|
23
|
+
emoji: identity?.profile?.data?.emoji ?? fallback.emoji,
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
};
|