@opensumi/ide-comments 2.21.13 → 2.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/lib/browser/comments-feature.registry.js.map +1 -1
  2. package/lib/browser/comments-panel.view.d.ts +2 -3
  3. package/lib/browser/comments-panel.view.d.ts.map +1 -1
  4. package/lib/browser/comments-panel.view.js +32 -70
  5. package/lib/browser/comments-panel.view.js.map +1 -1
  6. package/lib/browser/comments-thread.d.ts +4 -2
  7. package/lib/browser/comments-thread.d.ts.map +1 -1
  8. package/lib/browser/comments-thread.js +14 -7
  9. package/lib/browser/comments-thread.js.map +1 -1
  10. package/lib/browser/comments-zone.service.js.map +1 -1
  11. package/lib/browser/comments-zone.view.d.ts +1 -1
  12. package/lib/browser/comments-zone.view.d.ts.map +1 -1
  13. package/lib/browser/comments-zone.view.js +1 -1
  14. package/lib/browser/comments-zone.view.js.map +1 -1
  15. package/lib/browser/comments.contribution.d.ts +1 -0
  16. package/lib/browser/comments.contribution.d.ts.map +1 -1
  17. package/lib/browser/comments.contribution.js +6 -1
  18. package/lib/browser/comments.contribution.js.map +1 -1
  19. package/lib/browser/comments.service.d.ts +7 -4
  20. package/lib/browser/comments.service.d.ts.map +1 -1
  21. package/lib/browser/comments.service.js +60 -87
  22. package/lib/browser/comments.service.js.map +1 -1
  23. package/lib/browser/index.d.ts +0 -1
  24. package/lib/browser/index.d.ts.map +1 -1
  25. package/lib/browser/index.js +5 -1
  26. package/lib/browser/index.js.map +1 -1
  27. package/lib/browser/tree/comment-node.d.ts +14 -0
  28. package/lib/browser/tree/comment-node.d.ts.map +1 -0
  29. package/lib/browser/tree/comment-node.js +64 -0
  30. package/lib/browser/tree/comment-node.js.map +1 -0
  31. package/lib/browser/tree/tree-model.service.d.ts +43 -0
  32. package/lib/browser/tree/tree-model.service.d.ts.map +1 -0
  33. package/lib/browser/tree/tree-model.service.js +150 -0
  34. package/lib/browser/tree/tree-model.service.js.map +1 -0
  35. package/lib/browser/tree/tree-node.defined.d.ts +61 -0
  36. package/lib/browser/tree/tree-node.defined.d.ts.map +1 -0
  37. package/lib/browser/tree/tree-node.defined.js +116 -0
  38. package/lib/browser/tree/tree-node.defined.js.map +1 -0
  39. package/lib/browser/tree/tree-node.module.less +154 -0
  40. package/lib/common/index.d.ts +38 -36
  41. package/lib/common/index.d.ts.map +1 -1
  42. package/lib/common/index.js.map +1 -1
  43. package/package.json +13 -12
  44. package/src/browser/comment-reactions.view.tsx +109 -0
  45. package/src/browser/comments-body.tsx +57 -0
  46. package/src/browser/comments-feature.registry.ts +91 -0
  47. package/src/browser/comments-item.view.tsx +362 -0
  48. package/src/browser/comments-panel.view.tsx +90 -0
  49. package/src/browser/comments-textarea.view.tsx +194 -0
  50. package/src/browser/comments-thread.ts +309 -0
  51. package/src/browser/comments-zone.service.ts +29 -0
  52. package/src/browser/comments-zone.view.tsx +206 -0
  53. package/src/browser/comments.contribution.ts +201 -0
  54. package/src/browser/comments.module.less +210 -0
  55. package/src/browser/comments.service.ts +546 -0
  56. package/src/browser/index.ts +29 -0
  57. package/src/browser/markdown.style.ts +25 -0
  58. package/src/browser/mentions.style.ts +55 -0
  59. package/src/browser/tree/comment-node.tsx +130 -0
  60. package/src/browser/tree/tree-model.service.ts +173 -0
  61. package/src/browser/tree/tree-node.defined.ts +167 -0
  62. package/src/browser/tree/tree-node.module.less +154 -0
  63. package/src/common/index.ts +710 -0
  64. package/src/index.ts +1 -0
@@ -0,0 +1,109 @@
1
+ import { observer } from 'mobx-react-lite';
2
+ import React from 'react';
3
+
4
+
5
+ import { useInjectable, IEventBus, getExternalIcon, Disposable } from '@opensumi/ide-core-browser';
6
+ import { Button } from '@opensumi/ide-core-browser/lib/components';
7
+ import { InlineActionBar } from '@opensumi/ide-core-browser/lib/components/actions';
8
+ import { AbstractMenuService, IMenuRegistry, MenuId } from '@opensumi/ide-core-browser/lib/menu/next';
9
+ import { IIconService, IconType } from '@opensumi/ide-theme';
10
+
11
+ import {
12
+ IThreadComment,
13
+ ICommentsThread,
14
+ CommentReaction,
15
+ CommentReactionClick,
16
+ SwitchCommandReaction,
17
+ } from '../common';
18
+
19
+ import styles from './comments.module.less';
20
+
21
+ export const CommentReactionSwitcher: React.FC<{
22
+ thread: ICommentsThread;
23
+ comment: IThreadComment;
24
+ className?: string;
25
+ }> = observer(({ thread, comment, className }) => {
26
+ const key = `${thread.providerId}_${thread.id}_${comment.id}`;
27
+ const menuId = `${MenuId.CommentReactionSwitcherMenu}_${key}`;
28
+ const menuRegistry = useInjectable<IMenuRegistry>(IMenuRegistry);
29
+ const menuService = useInjectable<AbstractMenuService>(AbstractMenuService);
30
+
31
+ React.useEffect(() => {
32
+ const disposer = new Disposable();
33
+ const subMenuId = `${MenuId.CommentReactionSwitcherSubmenu}_${key}`;
34
+
35
+ disposer.addDispose(
36
+ menuRegistry.registerMenuItem(menuId, {
37
+ submenu: subMenuId,
38
+ // 目前 label 必须要填
39
+ label: subMenuId,
40
+ iconClass: getExternalIcon('reactions'),
41
+ group: 'navigation',
42
+ }),
43
+ );
44
+
45
+ disposer.addDispose(
46
+ menuRegistry.registerMenuItems(
47
+ subMenuId,
48
+ comment.reactions!.map((reaction) => ({
49
+ command: {
50
+ id: SwitchCommandReaction,
51
+ label: reaction.label!,
52
+ },
53
+ extraTailArgs: [
54
+ {
55
+ thread,
56
+ comment,
57
+ reaction,
58
+ },
59
+ ],
60
+ })),
61
+ ),
62
+ );
63
+ return () => disposer.dispose();
64
+ }, []);
65
+
66
+ const reactionsContext = React.useMemo(() => {
67
+ const menu = menuService.createMenu(menuId);
68
+ return menu;
69
+ }, []);
70
+
71
+ return <InlineActionBar className={className} menus={reactionsContext} regroup={(nav) => [nav, []]} type='icon' />;
72
+ });
73
+
74
+ export const CommentReactions: React.FC<{
75
+ thread: ICommentsThread;
76
+ comment: IThreadComment;
77
+ }> = observer(({ thread, comment }) => {
78
+ const eventBus = useInjectable<IEventBus>(IEventBus);
79
+ const iconService = useInjectable<IIconService>(IIconService);
80
+ const handleClickReaction = React.useCallback((reaction: CommentReaction) => {
81
+ eventBus.fire(
82
+ new CommentReactionClick({
83
+ thread,
84
+ comment,
85
+ reaction,
86
+ }),
87
+ );
88
+ }, []);
89
+
90
+ return (
91
+ <div className={styles.comment_reactions}>
92
+ {comment.reactions
93
+ ?.filter((reaction) => reaction.count !== 0)
94
+ .map((reaction) => (
95
+ <Button
96
+ key={reaction.label}
97
+ onClick={() => handleClickReaction(reaction)}
98
+ type='secondary'
99
+ size='small'
100
+ title={reaction.label}
101
+ className={styles.comment_reaction}
102
+ iconClass={iconService.fromIcon('', reaction.iconPath.toString(), IconType.Background)}
103
+ >
104
+ &nbsp;{reaction.count}
105
+ </Button>
106
+ ))}
107
+ </div>
108
+ );
109
+ });
@@ -0,0 +1,57 @@
1
+ import React from 'react';
2
+ import ReactDOM from 'react-dom';
3
+
4
+ import { createMarkedRenderer, toMarkdownHtml } from '@opensumi/ide-components/lib/utils';
5
+ import { IMarkdownString } from '@opensumi/ide-core-browser';
6
+
7
+ import styles from './comments.module.less';
8
+ import { markdownCss } from './markdown.style';
9
+
10
+ const ShadowContent = ({ root, children }) => ReactDOM.createPortal(children, root);
11
+
12
+ export const CommentsBody: React.FC<{
13
+ body: string | IMarkdownString;
14
+ }> = React.memo(({ body }) => {
15
+ const shadowRootRef = React.useRef<HTMLDivElement | null>(null);
16
+ const [shadowRoot, setShadowRoot] = React.useState<ShadowRoot | null>(null);
17
+
18
+ const renderer = React.useMemo(() => {
19
+ const renderer = createMarkedRenderer();
20
+
21
+ renderer.link = (href, title, text) =>
22
+ `<a target="_blank" rel="noopener" href="${href}" title="${title}">${text}</a>`;
23
+ return renderer;
24
+ }, []);
25
+
26
+ React.useEffect(() => {
27
+ if (shadowRootRef.current) {
28
+ const shadowRootElement = shadowRootRef.current.attachShadow({ mode: 'open' });
29
+ if (!shadowRoot) {
30
+ setShadowRoot(shadowRootElement);
31
+ }
32
+ }
33
+ }, []);
34
+
35
+ return (
36
+ <div ref={shadowRootRef} className={styles.comment_shadow_box}>
37
+ {shadowRoot && (
38
+ <ShadowContent root={shadowRoot}>
39
+ <style>{markdownCss}</style>
40
+ <div
41
+ dangerouslySetInnerHTML={{
42
+ __html: toMarkdownHtml(typeof body === 'string' ? body : body.value, {
43
+ gfm: true,
44
+ breaks: false,
45
+ pedantic: false,
46
+ sanitize: true,
47
+ smartLists: true,
48
+ smartypants: false,
49
+ renderer,
50
+ }),
51
+ }}
52
+ ></div>
53
+ </ShadowContent>
54
+ )}
55
+ </div>
56
+ );
57
+ });
@@ -0,0 +1,91 @@
1
+ import { Injectable } from '@opensumi/di';
2
+
3
+ import {
4
+ CommentsPanelOptions,
5
+ ICommentsFeatureRegistry,
6
+ PanelTreeNodeHandler,
7
+ FileUploadHandler,
8
+ MentionsOptions,
9
+ ZoneWidgerRender,
10
+ ICommentsConfig,
11
+ ICommentProviderFeature,
12
+ } from '../common';
13
+
14
+ @Injectable()
15
+ export class CommentsFeatureRegistry implements ICommentsFeatureRegistry {
16
+ private config: ICommentsConfig = {};
17
+
18
+ private options: CommentsPanelOptions = {};
19
+
20
+ private panelTreeNodeHandlers: PanelTreeNodeHandler[] = [];
21
+
22
+ private fileUploadHandler: FileUploadHandler;
23
+
24
+ private mentionsOptions: MentionsOptions = {};
25
+
26
+ private zoneWidgetRender: ZoneWidgerRender;
27
+
28
+ private providerFeature = new Map<string, ICommentProviderFeature>();
29
+
30
+ registerConfig(config: ICommentsConfig): void {
31
+ this.config = {
32
+ ...this.config,
33
+ ...config,
34
+ };
35
+ }
36
+
37
+ registerPanelTreeNodeHandler(handler: PanelTreeNodeHandler): void {
38
+ this.panelTreeNodeHandlers.push(handler);
39
+ }
40
+
41
+ registerPanelOptions(options: CommentsPanelOptions): void {
42
+ this.options = {
43
+ ...this.options,
44
+ ...options,
45
+ };
46
+ }
47
+
48
+ registerFileUploadHandler(handler: FileUploadHandler): void {
49
+ this.fileUploadHandler = handler;
50
+ }
51
+
52
+ registerMentionsOptions(options: MentionsOptions): void {
53
+ this.mentionsOptions = options;
54
+ }
55
+
56
+ registerZoneWidgetRender(render: ZoneWidgerRender): void {
57
+ this.zoneWidgetRender = render;
58
+ }
59
+
60
+ getConfig(): ICommentsConfig {
61
+ return this.config;
62
+ }
63
+
64
+ getCommentsPanelOptions(): CommentsPanelOptions {
65
+ return this.options;
66
+ }
67
+
68
+ getCommentsPanelTreeNodeHandlers(): PanelTreeNodeHandler[] {
69
+ return this.panelTreeNodeHandlers;
70
+ }
71
+
72
+ getFileUploadHandler() {
73
+ return this.fileUploadHandler;
74
+ }
75
+
76
+ getMentionsOptions() {
77
+ return this.mentionsOptions;
78
+ }
79
+
80
+ getZoneWidgetRender(): ZoneWidgerRender | undefined {
81
+ return this.zoneWidgetRender;
82
+ }
83
+
84
+ registerProviderFeature(providerId: string, feature: ICommentProviderFeature): void {
85
+ this.providerFeature.set(providerId, feature);
86
+ }
87
+
88
+ getProviderFeature(providerId: string): ICommentProviderFeature | undefined {
89
+ return this.providerFeature.get(providerId);
90
+ }
91
+ }
@@ -0,0 +1,362 @@
1
+ import { observer } from 'mobx-react-lite';
2
+ import React from 'react';
3
+
4
+ import { Button } from '@opensumi/ide-components';
5
+ import {
6
+ useInjectable,
7
+ localize,
8
+ IContextKeyService,
9
+ isUndefined,
10
+ IMarkdownString,
11
+ toLocalString,
12
+ toMarkdownHtml,
13
+ } from '@opensumi/ide-core-browser';
14
+ import { InlineActionBar } from '@opensumi/ide-core-browser/lib/components/actions';
15
+ import { AbstractMenuService, MenuId, IMenu } from '@opensumi/ide-core-browser/lib/menu/next';
16
+
17
+ import {
18
+ IThreadComment,
19
+ ICommentsCommentTitle,
20
+ CommentMode,
21
+ ICommentReply,
22
+ ICommentsCommentContext,
23
+ ICommentsZoneWidget,
24
+ ICommentsFeatureRegistry,
25
+ ICommentsThread,
26
+ } from '../common';
27
+
28
+ import { CommentReactions, CommentReactionSwitcher } from './comment-reactions.view';
29
+ import { CommentsBody } from './comments-body';
30
+ import { CommentsTextArea } from './comments-textarea.view';
31
+ import styles from './comments.module.less';
32
+
33
+ // TODO: 更好的时间格式化组件
34
+ const Timestamp: React.FC<{ timestamp: string }> = ({ timestamp }) => {
35
+ const formatTimestamp = React.useMemo(() => {
36
+ const date = new Date(timestamp);
37
+ return toLocalString(date);
38
+ }, [timestamp]);
39
+
40
+ return <span className={styles.comment_item_timestamp}>{formatTimestamp}</span>;
41
+ };
42
+
43
+ const useCommentContext = (
44
+ contextKeyService: IContextKeyService,
45
+ comment: IThreadComment,
46
+ ): [
47
+ string | IMarkdownString,
48
+ React.Dispatch<React.SetStateAction<string | IMarkdownString>>,
49
+ (event: React.ChangeEvent<HTMLTextAreaElement>) => void,
50
+ IMenu,
51
+ IMenu,
52
+ (files: FileList) => Promise<void>,
53
+ ] => {
54
+ const menuService = useInjectable<AbstractMenuService>(AbstractMenuService);
55
+ const { body, contextValue } = comment;
56
+ const [textValue, setTextValue] = React.useState<string | IMarkdownString>('');
57
+ const commentsFeatureRegistry = useInjectable<ICommentsFeatureRegistry>(ICommentsFeatureRegistry);
58
+ const fileUploadHandler = React.useMemo(() => commentsFeatureRegistry.getFileUploadHandler(), []);
59
+ // set textValue when body changed
60
+ React.useEffect(() => {
61
+ setTextValue(body);
62
+ }, [body]);
63
+
64
+ // Each comment has its own commentContext and commentTitleContext.
65
+ const commentContextService = React.useMemo(() => contextKeyService.createScoped(), []);
66
+ // it's value will true when textarea is empty
67
+ const commentIsEmptyContext = React.useMemo(
68
+ () => commentContextService.createKey<boolean>('commentIsEmpty', !comment.body),
69
+ [],
70
+ );
71
+ // below the comment textarea
72
+ const commentContext = React.useMemo(
73
+ () => menuService.createMenu(MenuId.CommentsCommentContext, commentContextService),
74
+ [],
75
+ );
76
+ // after the comment body
77
+ const commentTitleContext = React.useMemo(
78
+ () => menuService.createMenu(MenuId.CommentsCommentTitle, commentContextService),
79
+ [],
80
+ );
81
+
82
+ const itemCommentContext = React.useRef(commentContextService.createKey('comment', contextValue));
83
+
84
+ React.useEffect(() => {
85
+ itemCommentContext.current.set(contextValue);
86
+ }, [contextValue]);
87
+
88
+ const onChangeTextArea = React.useCallback((event: React.ChangeEvent<HTMLTextAreaElement>) => {
89
+ commentIsEmptyContext.set(!event.target.value);
90
+ setTextValue(event.target.value);
91
+ }, []);
92
+
93
+ const handleDragFiles = React.useCallback(
94
+ async (files: FileList) => {
95
+ if (fileUploadHandler) {
96
+ const appendText = await fileUploadHandler(textValue, files);
97
+ setTextValue((text) => {
98
+ const value = text + appendText;
99
+ commentIsEmptyContext.set(!value);
100
+ return value;
101
+ });
102
+ }
103
+ },
104
+ [textValue],
105
+ );
106
+
107
+ return [textValue, setTextValue, onChangeTextArea, commentContext, commentTitleContext, handleDragFiles];
108
+ };
109
+
110
+ const ReplyItem: React.FC<{
111
+ reply: IThreadComment;
112
+ thread: ICommentsThread;
113
+ }> = observer(({ reply, thread }) => {
114
+ const { contextKeyService } = thread;
115
+ const { author, label, body, mode, timestamp } = reply;
116
+ const iconUrl = author.iconPath?.toString();
117
+ const [textValue, setTextValue, onChangeTextArea, commentContext, commentTitleContext, handleDragFiles] =
118
+ useCommentContext(contextKeyService, reply);
119
+
120
+ // 判断是正常 Inline Text 还是 Markdown Text
121
+ const isInlineText = React.useMemo(() => {
122
+ const parsedStr = toMarkdownHtml(typeof body === 'string' ? body : body.value);
123
+ // 解析出来非纯p标签的则为Markdown Text
124
+ const isInline = /^\<p\>[^<>]+\<\/p\>\n$/.test(parsedStr);
125
+ return isInline;
126
+ }, [body]);
127
+
128
+ return (
129
+ <div className={styles.reply_item}>
130
+ {isUndefined(mode) || mode === CommentMode.Preview ? (
131
+ <div>
132
+ {isInlineText ? (
133
+ <>
134
+ {iconUrl && <img className={styles.reply_item_icon} src={iconUrl} alt={author.name} />}
135
+ <span className={styles.comment_item_author_name}>{author.name}</span>
136
+ {timestamp && <Timestamp timestamp={timestamp} />}
137
+ {typeof label === 'string' ? <span className={styles.comment_item_label}>{label}</span> : label}
138
+ {' : '}
139
+ <span className={styles.comment_item_body}>{body}</span>
140
+ {reply.reactions && reply.reactions.length > 0 && (
141
+ <CommentReactionSwitcher className={styles.reply_item_title} thread={thread} comment={reply} />
142
+ )}
143
+ <InlineActionBar<ICommentsCommentTitle>
144
+ separator='inline'
145
+ className={styles.reply_item_title}
146
+ menus={commentTitleContext}
147
+ context={[
148
+ {
149
+ thread,
150
+ comment: reply,
151
+ menuId: MenuId.CommentsCommentTitle,
152
+ },
153
+ ]}
154
+ type='icon'
155
+ />
156
+ </>
157
+ ) : (
158
+ <>
159
+ <div className={styles.comment_item_markdown_header}>
160
+ <div>
161
+ {iconUrl && <img className={styles.reply_item_icon} src={iconUrl} alt={author.name} />}
162
+ <span className={styles.comment_item_author_name}>{author.name}</span>
163
+ {timestamp && <Timestamp timestamp={timestamp} />}
164
+ {typeof label === 'string' ? <span className={styles.comment_item_label}>{label}</span> : label}
165
+ {' : '}
166
+ </div>
167
+ <InlineActionBar<ICommentsCommentTitle>
168
+ separator='inline'
169
+ className={styles.reply_item_title}
170
+ menus={commentTitleContext}
171
+ context={[
172
+ {
173
+ thread,
174
+ comment: reply,
175
+ menuId: MenuId.CommentsCommentTitle,
176
+ },
177
+ ]}
178
+ type='icon'
179
+ />
180
+ </div>
181
+ <CommentsBody body={body} />
182
+ </>
183
+ )}
184
+ </div>
185
+ ) : (
186
+ <div>
187
+ <CommentsTextArea
188
+ value={typeof textValue === 'string' ? textValue : textValue.value}
189
+ autoFocus={true}
190
+ onChange={onChangeTextArea}
191
+ dragFiles={handleDragFiles}
192
+ />
193
+ <InlineActionBar<ICommentsCommentContext>
194
+ className={styles.comment_item_reply}
195
+ menus={commentContext}
196
+ context={[
197
+ {
198
+ thread,
199
+ comment: reply,
200
+ body: textValue,
201
+ menuId: MenuId.CommentsCommentContext,
202
+ },
203
+ ]}
204
+ type='button'
205
+ separator='inline'
206
+ afterClick={() => {
207
+ // restore textarea value
208
+ setTextValue(body);
209
+ }}
210
+ />
211
+ </div>
212
+ )}
213
+ {reply.reactions && reply.reactions.length > 0 && <CommentReactions thread={thread} comment={reply} />}
214
+ </div>
215
+ );
216
+ });
217
+
218
+ export const CommentItem: React.FC<{
219
+ thread: ICommentsThread;
220
+ commentThreadContext: IMenu;
221
+ widget: ICommentsZoneWidget;
222
+ }> = observer(({ thread, commentThreadContext, widget }) => {
223
+ const { readOnly, contextKeyService } = thread;
224
+ const [showReply, setShowReply] = React.useState(false);
225
+ const [replyText, setReplyText] = React.useState('');
226
+ const [comment, ...replies] = thread.comments;
227
+ const { author, label, body, mode, timestamp } = comment;
228
+ const iconUrl = author.iconPath?.toString();
229
+ const [textValue, setTextValue, onChangeTextArea, commentContext, commentTitleContext, handleDragFiles] =
230
+ useCommentContext(contextKeyService, comment);
231
+ const commentsFeatureRegistry = useInjectable<ICommentsFeatureRegistry>(ICommentsFeatureRegistry);
232
+ const fileUploadHandler = React.useMemo(() => commentsFeatureRegistry.getFileUploadHandler(), []);
233
+ const replyIsEmptyContext = React.useMemo(() => contextKeyService.createKey<boolean>('commentIsEmpty', true), []);
234
+
235
+ // modify reply
236
+ function onChangeReply(event: React.ChangeEvent<HTMLTextAreaElement>) {
237
+ replyIsEmptyContext.set(!event.target.value);
238
+ setReplyText(event.target.value);
239
+ }
240
+
241
+ const handleDragFilesToReply = React.useCallback(
242
+ async (files: FileList) => {
243
+ if (fileUploadHandler) {
244
+ const appendText = await fileUploadHandler(textValue, files);
245
+ setReplyText((text) => {
246
+ const value = text + appendText;
247
+ replyIsEmptyContext.set(!value);
248
+ return value;
249
+ });
250
+ }
251
+ },
252
+ [replyText],
253
+ );
254
+
255
+ return (
256
+ <div className={styles.comment_item}>
257
+ {iconUrl && <img className={styles.comment_item_icon} src={iconUrl} alt={author.name} />}
258
+ <div className={styles.comment_item_content}>
259
+ <div className={styles.comment_item_head}>
260
+ <div className={styles.comment_item_name}>
261
+ <span className={styles.comment_item_author_name}>{author.name}</span>
262
+ {timestamp && <Timestamp timestamp={timestamp} />}
263
+ {typeof label === 'string' ? <span className={styles.comment_item_label}>{label}</span> : label}
264
+ </div>
265
+ <div className={styles.comment_item_actions}>
266
+ {comment.reactions && comment.reactions.length > 0 && (
267
+ <CommentReactionSwitcher thread={thread} comment={comment} />
268
+ )}
269
+ {!readOnly && (
270
+ <Button
271
+ className={styles.comment_item_reply_button}
272
+ size='small'
273
+ type='secondary'
274
+ onClick={() => setShowReply(true)}
275
+ >
276
+ {localize('comments.thread.action.reply')}
277
+ </Button>
278
+ )}
279
+ <InlineActionBar<ICommentsCommentTitle>
280
+ menus={commentTitleContext}
281
+ context={[
282
+ {
283
+ thread,
284
+ comment,
285
+ menuId: MenuId.CommentsCommentTitle,
286
+ },
287
+ ]}
288
+ type='button'
289
+ />
290
+ </div>
291
+ </div>
292
+ {isUndefined(mode) || mode === CommentMode.Preview ? (
293
+ <CommentsBody body={body} />
294
+ ) : (
295
+ <div>
296
+ <CommentsTextArea
297
+ value={typeof textValue === 'string' ? textValue : textValue.value}
298
+ autoFocus={true}
299
+ onChange={onChangeTextArea}
300
+ dragFiles={handleDragFiles}
301
+ />
302
+ <InlineActionBar<ICommentsCommentContext>
303
+ className={styles.comment_item_context}
304
+ menus={commentContext}
305
+ context={[
306
+ {
307
+ thread,
308
+ comment,
309
+ body: textValue,
310
+ menuId: MenuId.CommentsCommentContext,
311
+ },
312
+ ]}
313
+ separator='inline'
314
+ type='button'
315
+ afterClick={() => {
316
+ // restore textarea value
317
+ setTextValue(body);
318
+ }}
319
+ />
320
+ </div>
321
+ )}
322
+ {comment.reactions && comment.reactions.length > 0 && <CommentReactions thread={thread} comment={comment} />}
323
+ {(replies.length > 0 || showReply) && (
324
+ <div className={styles.comment_item_reply_wrap}>
325
+ {replies.map((reply) => (
326
+ <ReplyItem key={reply.id} thread={thread} reply={reply} />
327
+ ))}
328
+ {showReply && (
329
+ <div>
330
+ <CommentsTextArea
331
+ autoFocus={true}
332
+ value={replyText}
333
+ onChange={onChangeReply}
334
+ placeholder={`${localize('comments.reply.placeholder')}...`}
335
+ dragFiles={handleDragFilesToReply}
336
+ />
337
+ <InlineActionBar<ICommentReply>
338
+ className={styles.comment_item_reply}
339
+ menus={commentThreadContext}
340
+ context={[
341
+ {
342
+ thread,
343
+ text: replyText,
344
+ widget,
345
+ menuId: MenuId.CommentsCommentThreadContext,
346
+ },
347
+ ]}
348
+ separator='inline'
349
+ type='button'
350
+ afterClick={() => {
351
+ setReplyText('');
352
+ setShowReply(false);
353
+ }}
354
+ />
355
+ </div>
356
+ )}
357
+ </div>
358
+ )}
359
+ </div>
360
+ </div>
361
+ );
362
+ });