@lark-apaas/coding-template-nestjs-react-fullstack 0.1.60-alpha.20261105142153 → 0.1.60-beta.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 (33) hide show
  1. package/package.json +1 -1
  2. package/template/client/src/components/business-ui/README.md +0 -2
  3. package/template/package-lock.json +30 -546
  4. package/template/package.json +3 -14
  5. package/template/scripts/build.sh +39 -22
  6. package/template/scripts/prune-smart.js +20 -1
  7. package/template/client/src/components/business-ui/comments/README.md +0 -101
  8. package/template/client/src/components/business-ui/comments/comments.tsx +0 -670
  9. package/template/client/src/components/business-ui/comments/contracts.ts +0 -256
  10. package/template/client/src/components/business-ui/comments/index.ts +0 -20
  11. package/template/client/src/components/business-ui/comments/use-comments.ts +0 -492
  12. package/template/client/src/components/business-ui/record-comments/README.md +0 -141
  13. package/template/client/src/components/business-ui/record-comments/app-source.ts +0 -93
  14. package/template/client/src/components/business-ui/record-comments/assets/reaction-sprite.png +0 -0
  15. package/template/client/src/components/business-ui/record-comments/comment-panel.contract.ts +0 -240
  16. package/template/client/src/components/business-ui/record-comments/comment-panel.tsx +0 -231
  17. package/template/client/src/components/business-ui/record-comments/extensions.ts +0 -130
  18. package/template/client/src/components/business-ui/record-comments/index.ts +0 -9
  19. package/template/client/src/components/business-ui/record-comments/internal/composer.tsx +0 -531
  20. package/template/client/src/components/business-ui/record-comments/internal/content.tsx +0 -213
  21. package/template/client/src/components/business-ui/record-comments/internal/discussion.tsx +0 -523
  22. package/template/client/src/components/business-ui/record-comments/internal/editor.tsx +0 -681
  23. package/template/client/src/components/business-ui/record-comments/internal/icons.tsx +0 -148
  24. package/template/client/src/components/business-ui/record-comments/internal/images.tsx +0 -155
  25. package/template/client/src/components/business-ui/record-comments/internal/mentions.tsx +0 -163
  26. package/template/client/src/components/business-ui/record-comments/internal/model.ts +0 -581
  27. package/template/client/src/components/business-ui/record-comments/internal/mutations.ts +0 -198
  28. package/template/client/src/components/business-ui/record-comments/internal/reaction-catalog.ts +0 -196
  29. package/template/client/src/components/business-ui/record-comments/internal/reactions.tsx +0 -683
  30. package/template/client/src/components/business-ui/record-comments/internal/styles.css +0 -1109
  31. package/template/client/src/components/business-ui/record-comments/internal/suggestions.tsx +0 -456
  32. package/template/client/src/components/business-ui/record-comments/internal/view-types.ts +0 -91
  33. package/template/client/src/components/business-ui/record-comments/types.ts +0 -121
@@ -1,670 +0,0 @@
1
- import type {
2
- CommentsProviderProps,
3
- CommentsProviderInput,
4
- CommentPanelInput,
5
- CommentThread,
6
- CommentContent,
7
- CommentDraft,
8
- EntityRef,
9
- UserRef,
10
- CommentEntitySource,
11
- CommentReaction,
12
- CommentTarget,
13
- CommentWriteResult,
14
- CommentMessage,
15
- } from './contracts';
16
- import type {
17
- CommentDataSource,
18
- CommentValue,
19
- MentionRef,
20
- MentionOption,
21
- } from '../record-comments/types';
22
- import type {
23
- DiscussionThread,
24
- DiscussionDraft,
25
- DiscussionReply,
26
- } from '../record-comments/internal/view-types';
27
- import {
28
- useEffect,
29
- useMemo,
30
- useRef,
31
- useState,
32
- useSyncExternalStore,
33
- } from 'react';
34
- import { CommentMutations } from '../record-comments/internal/mutations';
35
- import {
36
- DiscussionProvider,
37
- DiscussionLayout,
38
- } from '../record-comments/internal/discussion';
39
- import { ReadStatus, PaginationFooter } from '../record-comments/comment-panel';
40
- import { withDocumentSource } from '../record-comments/extensions';
41
- import { DEFAULT_REACTION_EMOJIS } from '../record-comments/internal/reactions';
42
- import { normalizeValue } from '../record-comments/internal/model';
43
-
44
- // Encoding is private UI identity, not a backend ID conversion. Namespace stays lossless.
45
- const toRef = (ref: EntityRef): MentionRef => {
46
- if (!ref.id || !ref.namespace) throw new Error('引用标识不能为空');
47
- return ref.kind === 'user'
48
- ? {
49
- kind: 'user',
50
- idType: 'generic',
51
- id: JSON.stringify([ref.namespace, ref.id]),
52
- }
53
- : { kind: 'document', resourceType: ref.namespace, id: ref.id };
54
- };
55
- const fromRef = (ref: MentionRef): EntityRef => {
56
- if (ref.kind === 'document')
57
- return { kind: 'document', namespace: ref.resourceType, id: ref.id };
58
- if (ref.idType !== 'generic')
59
- return { kind: 'user', namespace: ref.idType, id: ref.id };
60
- const [namespace, id] = JSON.parse(ref.id);
61
- return { kind: 'user', namespace, id };
62
- };
63
- const value = (content: CommentContent): CommentValue =>
64
- normalizeValue({
65
- version: 1,
66
- content: content.nodes.map((n) =>
67
- n.type === 'mention'
68
- ? { ...n, ref: toRef(n.ref) }
69
- : n.type === 'unsupported'
70
- ? { type: 'text', text: n.label }
71
- : n,
72
- ),
73
- });
74
- const content = (v: CommentValue): CommentContent => ({
75
- version: 1,
76
- nodes: normalizeValue(v).content.map((n) =>
77
- n.type === 'mention' ? { ...n, ref: fromRef(n.ref) } : n,
78
- ),
79
- });
80
- const fail = (message: string) =>
81
- Object.assign(new Error(message), { outcome: 'not-applied' });
82
- const reactionState = (items: CommentReaction[]) =>
83
- JSON.stringify(
84
- items
85
- .map((r) => [r.key, r.count, r.mine])
86
- .sort((a, b) => String(a[0]).localeCompare(String(b[0]))),
87
- );
88
- const key = (ref: EntityRef) =>
89
- JSON.stringify([ref.kind, ref.namespace, ref.id]);
90
- export function CommentsProvider(input: CommentsProviderInput) {
91
- const props: CommentsProviderProps =
92
- input.value !== undefined
93
- ? { ...input.value.props, children: input.children }
94
- : (input as CommentsProviderProps);
95
- return (
96
- <Session
97
- key={JSON.stringify([props.scopeKey, props.subjectKey])}
98
- {...props}
99
- />
100
- );
101
- }
102
- function Session(props: CommentsProviderProps) {
103
- const latest = useRef(props);
104
- latest.current = props;
105
- const [mutations] = useState(
106
- () =>
107
- new CommentMutations({
108
- opaqueIDs: true,
109
- unknownDeletion: true,
110
- unknownErrors: true,
111
- removePending: true,
112
- }),
113
- );
114
- useSyncExternalStore(
115
- mutations.subscribe,
116
- mutations.getVersion,
117
- mutations.getVersion,
118
- );
119
- useEffect(() => {
120
- mutations.activate();
121
- return () => mutations.dispose();
122
- }, [mutations]);
123
- const abort = useRef(new AbortController());
124
- useEffect(() => {
125
- const controller = new AbortController();
126
- abort.current = controller;
127
- return () => controller.abort();
128
- }, []);
129
- const context = () => ({
130
- operationId: crypto.randomUUID(),
131
- signal: abort.current.signal,
132
- });
133
- const extensions = new Map(props.extensions?.map((x) => [x.type, x]));
134
- if (extensions.size !== (props.extensions?.length ?? 0))
135
- throw new Error('同种评论扩展只能注册一次');
136
- const documents = props.extensions?.find((x) => x.type === 'documents');
137
- const reactions = props.extensions?.find((x) => x.type === 'reactions');
138
- const images = extensions.has('images');
139
- const names = useRef(new Map<string, MentionOption>());
140
- names.current = new Map();
141
- for (const author of [
142
- props.currentUser,
143
- ...props.threads.flatMap((t) =>
144
- t.messages.flatMap((m) => [
145
- m.author,
146
- ...(m.reactions ?? []).flatMap((r) => r.previewUsers ?? []),
147
- ]),
148
- ),
149
- ])
150
- if (author?.name)
151
- names.current.set(key(author.ref), {
152
- ref: toRef(author.ref),
153
- label: author.name || '用户',
154
- avatarUrl: author.avatarUrl,
155
- });
156
- const source = useMemo(() => {
157
- function wrap(
158
- external: CommentEntitySource | undefined,
159
- user: boolean,
160
- ): CommentDataSource {
161
- const option = (o: import('./contracts').CommentEntityOption) => ({
162
- ...o,
163
- ref: toRef(o.ref),
164
- });
165
- const allowed = (o: import('./contracts').CommentEntityOption) =>
166
- o.ref.kind === 'user'
167
- ? Boolean(latest.current.input?.userMentions?.(o.ref as UserRef))
168
- : true;
169
- return {
170
- scopeKey: props.scopeKey + (user ? ':actors' : ':documents'),
171
- allowLinks: props.input?.links ?? false,
172
- search: async (args) => {
173
- if (user && !latest.current.input?.userMentions) return { items: [] };
174
- const result = await external?.search?.(args);
175
- return {
176
- items: (result?.items ?? []).filter(allowed).map(option),
177
- nextCursor: result?.nextCursor,
178
- };
179
- },
180
- ...(external?.recommend
181
- ? {
182
- recommend: async (args) => {
183
- if (user && !latest.current.input?.userMentions)
184
- return { items: [] };
185
- const r = await external.recommend!(args);
186
- return {
187
- items: r.items.filter(allowed).map(option),
188
- nextCursor: r.nextCursor,
189
- };
190
- },
191
- }
192
- : {}),
193
- parseLink: (url) => {
194
- const ref = external?.parseUrl?.(url);
195
- return ref
196
- ? { type: 'mention', ref: toRef(ref), href: url }
197
- : undefined;
198
- },
199
- resolve: async (refs, signal) => {
200
- const found = new Map(
201
- refs.flatMap((ref) => {
202
- const r = names.current.get(key(fromRef(ref)));
203
- return r
204
- ? [
205
- [
206
- key(fromRef(ref)),
207
- { ...r, status: 'resolved' as const },
208
- ] as const,
209
- ]
210
- : [];
211
- }),
212
- );
213
- const missing = refs.filter((r) => !found.has(key(fromRef(r))));
214
- if (missing.length && external)
215
- for (const r of await external.resolve(
216
- missing.map(fromRef),
217
- signal,
218
- ))
219
- found.set(key(r.ref), { ...r, ref: toRef(r.ref) } as never);
220
- signal.throwIfAborted();
221
- return refs.map(
222
- (ref) =>
223
- found.get(key(fromRef(ref))) ?? { ref, status: 'error' as const },
224
- );
225
- },
226
- };
227
- }
228
- const people = wrap(props.actors, true);
229
- return documents
230
- ? {
231
- ...withDocumentSource(people, wrap(documents.source, false)),
232
- allowLinks: props.input?.links ?? false,
233
- }
234
- : people;
235
- }, [props.actors, props.scopeKey, props.input?.links, documents?.source]);
236
- const [submitted, setSubmitted] = useState<string>();
237
- const [reactionVersion, setReactionVersion] = useState(0);
238
- const reactionOverlays = useRef(
239
- new Map<
240
- string,
241
- {
242
- data: CommentReaction[];
243
- before: CommentReaction[];
244
- confirmed: boolean;
245
- uncertain?: boolean;
246
- key: string;
247
- action: 'add' | 'remove';
248
- }
249
- >(),
250
- );
251
- const reactionBusy = useRef(new Set<string>());
252
- const reactKey = (target: CommentTarget) =>
253
- JSON.stringify([target.threadId, target.messageId]);
254
- const dataToReaction = (data: CommentReaction[]) =>
255
- data
256
- .filter((r) => r.count > 0)
257
- .map((r) => ({
258
- key: r.key,
259
- count: r.count,
260
- mine: r.mine === true,
261
- users: (r.previewUsers ?? []).map((u) => toRef(u.ref).id),
262
- }));
263
- const toMessage = (m: CommentMessage, threadId: string): DiscussionReply => {
264
- const target = { threadId, messageId: m.id };
265
- const overlay = reactionOverlays.current.get(reactKey(target));
266
- const canEdit = Boolean(
267
- m.permissions?.edit &&
268
- props.actions?.update &&
269
- !m.content.nodes.some((n) => n.type === 'unsupported') &&
270
- (!m.attachments?.length ||
271
- (images &&
272
- m.attachments.every((a) => a.kind === 'image' && Boolean(a.url)))),
273
- );
274
- return {
275
- replyID: m.id,
276
- clientOperationId: m.clientOperationId,
277
- author: m.author ? toRef(m.author.ref) : null,
278
- createTime: m.createdAt / 1000,
279
- updateTime: m.updatedAt === undefined ? undefined : m.updatedAt / 1000,
280
- edited: m.edited || Boolean(m.updatedAt && m.updatedAt > m.createdAt),
281
- value: value({
282
- version: 1,
283
- nodes: [
284
- ...m.content.nodes,
285
- ...(m.attachments ?? [])
286
- .filter((a) => a.kind !== 'image' || !a.url)
287
- .map((a) => ({
288
- type: 'unsupported' as const,
289
- label: `[附件暂未接入:${a.name}]`,
290
- })),
291
- ],
292
- }),
293
- images: (m.attachments ?? [])
294
- .filter((a) => a.kind === 'image' && a.url)
295
- .map((a) => ({ ...a, url: a.url! })),
296
- reactions: dataToReaction(overlay?.data ?? m.reactions ?? []),
297
- permissions: {
298
- canEdit,
299
- canDelete: Boolean(m.permissions?.delete && props.actions?.remove),
300
- canCopyLink: Boolean(props.getMessageLink?.(target)),
301
- canReact: Boolean(
302
- reactions?.options.onChange &&
303
- m.permissions?.react &&
304
- !overlay?.uncertain &&
305
- m.reactions !== undefined &&
306
- !m.reactions.some((r) => r.mine === undefined),
307
- ),
308
- },
309
- };
310
- };
311
- const base: DiscussionThread[] = props.threads.map((t) => ({
312
- commentID: t.id,
313
- canReply: t.canReply,
314
- creator: t.messages[0]?.author ? toRef(t.messages[0].author.ref) : null,
315
- createTime: (t.messages[0]?.createdAt ?? 0) / 1000,
316
- isSolved: !!t.resolved,
317
- listFooter: t.repliesPagination?.hasMore ? (
318
- <PaginationFooter pagination={t.repliesPagination} />
319
- ) : undefined,
320
- replies: t.messages.map((m) => toMessage(m, t.id)),
321
- }));
322
- useEffect(() => {
323
- mutations.reconcile(base);
324
- });
325
- useEffect(() => {
326
- for (const [k, o] of reactionOverlays.current)
327
- if (o.confirmed) {
328
- const [tid, mid] = JSON.parse(k);
329
- const data = props.threads
330
- .find((t) => t.id === tid)
331
- ?.messages.find((m) => m.id === mid)?.reactions;
332
- if (
333
- data &&
334
- (reactionState(data) === reactionState(o.data) ||
335
- (o.action === 'add'
336
- ? data.find((r) => r.key === o.key)?.mine === true
337
- : !data.some((r) => r.key === o.key) ||
338
- data.find((r) => r.key === o.key)?.mine === false))
339
- ) {
340
- reactionOverlays.current.delete(k);
341
- reactionBusy.current.delete(k);
342
- setReactionVersion((v) => v + 1);
343
- }
344
- }
345
- }, [props.threads, reactionVersion]);
346
- const readDraft = (
347
- draft: DiscussionDraft,
348
- mode: 'create' | 'update',
349
- target: Partial<CommentTarget>,
350
- ): CommentDraft => {
351
- const result: CommentDraft = {
352
- content: content(draft.value),
353
- attachments: draft.images.map((i) =>
354
- i.file
355
- ? { kind: 'local', localId: i.id, file: i.file }
356
- : {
357
- kind: 'existing',
358
- attachment: {
359
- id: i.id,
360
- kind: 'image',
361
- name: i.name,
362
- url: i.url,
363
- width: i.width,
364
- height: i.height,
365
- },
366
- },
367
- ),
368
- };
369
- const signature = (n: import('./contracts').CommentNode) =>
370
- n.type === 'link'
371
- ? JSON.stringify(['link', n.url])
372
- : n.type === 'mention'
373
- ? JSON.stringify(['mention', key(n.ref), n.href ?? ''])
374
- : '';
375
- const retained = new Map<string, number>();
376
- if (mode === 'update')
377
- for (const n of latest.current.threads
378
- .find((t) => t.id === target.threadId)
379
- ?.messages.find((m) => m.id === target.messageId)?.content.nodes ??
380
- []) {
381
- const k = signature(n);
382
- if (k) retained.set(k, (retained.get(k) ?? 0) + 1);
383
- }
384
- for (const n of result.content.nodes) {
385
- const k = signature(n),
386
- remaining = retained.get(k) ?? 0;
387
- const existing = remaining > 0;
388
- if (existing) retained.set(k, remaining - 1);
389
- if (n.type === 'link' && !latest.current.input?.links && !existing)
390
- throw fail('当前接口不支持结构化链接');
391
- if (
392
- n.type === 'mention' &&
393
- n.ref.kind === 'document' &&
394
- !documents?.canInsert &&
395
- !existing
396
- )
397
- throw fail('当前接口不支持插入文档引用');
398
- if (n.type === 'unresolved-mention' || n.type === 'unsupported')
399
- throw fail('请重新选择无法识别的引用');
400
- if (
401
- n.type === 'mention' &&
402
- n.ref.kind === 'user' &&
403
- !latest.current.input?.userMentions?.(n.ref as UserRef) &&
404
- !existing
405
- )
406
- throw fail('当前接口不支持此人员引用');
407
- }
408
- const error = latest.current.validateDraft?.(result, { mode, ...target });
409
- if (error) throw fail(error);
410
- return result;
411
- };
412
- const accept = async (result: CommentWriteResult, target?: CommentTarget) => {
413
- if (
414
- target &&
415
- (result.threadId !== target.threadId ||
416
- result.messageId !== target.messageId)
417
- )
418
- throw Object.assign(new Error('写入目标不匹配'), { outcome: 'unknown' });
419
- if (result.message && result.message.id !== result.messageId)
420
- throw Object.assign(new Error('回读消息 ID 不匹配'), {
421
- outcome: 'unknown',
422
- });
423
- return {
424
- commentID: result.threadId,
425
- replyID: result.messageId,
426
- ...(result.message
427
- ? { authoritativeReply: toMessage(result.message, result.threadId) }
428
- : {}),
429
- };
430
- };
431
- return (
432
- <DiscussionProvider
433
- panel={{
434
- source,
435
- currentUser: props.currentUser
436
- ? toRef(props.currentUser.ref)
437
- : undefined,
438
- comments: mutations.view(base),
439
- allowCompose: !!props.actions?.create,
440
- optimisticMutations: true,
441
- canMentionUser: (r) =>
442
- r.kind === 'user' &&
443
- Boolean(latest.current.input?.userMentions?.(fromRef(r) as UserRef)),
444
- reactionEmojis: reactions
445
- ? (reactions.options.catalog ?? DEFAULT_REACTION_EMOJIS).filter(
446
- (e) =>
447
- !reactions.options.allowedKeys ||
448
- reactions.options.allowedKeys.includes(e.key),
449
- )
450
- : undefined,
451
- showReactions: !!reactions,
452
- allowReact: !!reactions?.options.onChange,
453
- enablePrivateComments: false,
454
- enableImages: images && !!props.actions?.create,
455
- enableEditImages: images && !!props.actions?.update,
456
- enableDocuments: documents?.canInsert ?? false,
457
- submittedReplyID: submitted ? mutations.replyID(submitted) : undefined,
458
- onScrolledToReply: () => setSubmitted(undefined),
459
- onRetryMutation: (id) => mutations.retry(id),
460
- autoScrollNewReplies: false,
461
- listHeader: (
462
- <ReadStatus
463
- loading={props.loading}
464
- error={props.error}
465
- onRetry={props.onRetry}
466
- empty={
467
- !base.some((t) => t.replies.length) &&
468
- !mutations.view(base).length
469
- }
470
- hasMore={props.pagination?.hasMore}
471
- />
472
- ),
473
- listFooter: props.pagination ? (
474
- <PaginationFooter
475
- pagination={props.pagination}
476
- loading={props.loading || !!props.error}
477
- />
478
- ) : undefined,
479
- emptyState: null,
480
- onMentionClick: (ref, anchor) => {
481
- if (ref.kind === 'user')
482
- props.onActorClick?.(fromRef(ref) as UserRef, anchor);
483
- },
484
- getCommentLink: (threadId, m) =>
485
- props.getMessageLink?.({ threadId, messageId: m.replyID }) ?? '',
486
- resolveUserRef: (id) => ({ kind: 'user', idType: 'generic', id }),
487
- onSubmit: (draft) => {
488
- if (
489
- draft.commentID &&
490
- !latest.current.threads.some(
491
- (t) =>
492
- t.id === draft.commentID && t.canReply !== false && !t.resolved,
493
- )
494
- )
495
- throw fail('该讨论当前不可回复');
496
- const input = readDraft(
497
- draft,
498
- 'create',
499
- draft.commentID ? { threadId: draft.commentID } : {},
500
- );
501
- const ctx = context();
502
- const id = mutations.add(
503
- base.find((t) => t.commentID === draft.commentID),
504
- draft,
505
- props.currentUser ? toRef(props.currentUser.ref) : undefined,
506
- async () =>
507
- accept(
508
- await latest.current.actions!.create!(
509
- {
510
- ...input,
511
- ...(draft.commentID
512
- ? {
513
- threadId: draft.commentID,
514
- ...(draft.replyToMessageId
515
- ? { replyToMessageId: draft.replyToMessageId }
516
- : {}),
517
- }
518
- : {}),
519
- },
520
- ctx,
521
- ),
522
- ),
523
- ctx.operationId,
524
- );
525
- setSubmitted(id);
526
- },
527
- onEdit: (threadId, messageId, v, attachments) => {
528
- const input = readDraft(
529
- { value: v, images: attachments, private: false },
530
- 'update',
531
- { threadId, messageId },
532
- );
533
- const ctx = context();
534
- mutations.edit(
535
- base.find((t) => t.commentID === threadId)!,
536
- messageId,
537
- { value: v, images: attachments, private: false },
538
- async () =>
539
- accept(
540
- await latest.current.actions!.update!(
541
- { ...input, threadId, messageId },
542
- ctx,
543
- ),
544
- { threadId, messageId },
545
- ),
546
- );
547
- },
548
- onDelete: (threadId, messageId) => {
549
- if (!messageId) throw fail('未选择消息');
550
- const ctx = context();
551
- mutations.delete(
552
- base.find((t) => t.commentID === threadId)!,
553
- messageId,
554
- async () => {
555
- const r = await latest.current.actions!.remove!(
556
- { threadId, messageId },
557
- ctx,
558
- );
559
- return {
560
- commentID: r.threadId,
561
- ...(r.threadDeleted === undefined
562
- ? {}
563
- : { commentDeleted: r.threadDeleted }),
564
- } as never;
565
- },
566
- );
567
- },
568
- onToggleReaction: async (threadId, messageId, emoji) => {
569
- const target = { threadId, messageId };
570
- const k = reactKey(target);
571
- if (reactionBusy.current.has(k)) return;
572
- const m = latest.current.threads
573
- .find((t) => t.id === threadId)
574
- ?.messages.find((m) => m.id === messageId);
575
- if (
576
- !m?.permissions?.react ||
577
- !m.reactions ||
578
- m.reactions.some((r) => r.mine === undefined) ||
579
- !reactions?.options.onChange
580
- )
581
- throw fail('回应状态或权限未知');
582
- if (
583
- reactions.options.allowedKeys &&
584
- !reactions.options.allowedKeys.includes(emoji)
585
- )
586
- throw fail('该表情未启用');
587
- const before = reactionOverlays.current.get(k)?.data ?? m.reactions;
588
- const old = before.find((r) => r.key === emoji);
589
- const action = old?.mine ? 'remove' : 'add';
590
- const next = before.filter((r) => r.key !== emoji);
591
- const count = (old?.count ?? 0) + (action === 'add' ? 1 : -1);
592
- if (count > 0)
593
- next.push({
594
- key: emoji,
595
- count,
596
- mine: action === 'add',
597
- previewUsers:
598
- action === 'add' && props.currentUser
599
- ? [
600
- props.currentUser,
601
- ...(old?.previewUsers ?? []).filter(
602
- (u) => key(u.ref) !== key(props.currentUser!.ref),
603
- ),
604
- ]
605
- : (old?.previewUsers ?? []).filter(
606
- (u) =>
607
- !props.currentUser ||
608
- key(u.ref) !== key(props.currentUser.ref),
609
- ),
610
- });
611
- reactionBusy.current.add(k);
612
- reactionOverlays.current.set(k, {
613
- data: next,
614
- before,
615
- confirmed: false,
616
- key: emoji,
617
- action,
618
- });
619
- setReactionVersion((v) => v + 1);
620
- try {
621
- const r = await reactions.options.onChange(
622
- { ...target, key: emoji, action },
623
- context(),
624
- );
625
- if (!abort.current.signal.aborted) {
626
- reactionOverlays.current.set(k, {
627
- data: r.reactions ?? next,
628
- before,
629
- confirmed: true,
630
- key: emoji,
631
- action,
632
- });
633
- setReactionVersion((v) => v + 1);
634
- }
635
- } catch (e) {
636
- if (!abort.current.signal.aborted) {
637
- if ((e as { outcome?: string })?.outcome === 'not-applied')
638
- reactionOverlays.current.delete(k);
639
- else
640
- reactionOverlays.current.set(k, {
641
- data: next,
642
- before,
643
- confirmed: true,
644
- uncertain: true,
645
- key: emoji,
646
- action,
647
- });
648
- setReactionVersion((v) => v + 1);
649
- }
650
- throw e;
651
- } finally {
652
- if (!reactionOverlays.current.get(k)?.uncertain)
653
- reactionBusy.current.delete(k);
654
- }
655
- },
656
- }}
657
- >
658
- {props.children}
659
- </DiscussionProvider>
660
- );
661
- }
662
- export function CommentPanel(props: CommentPanelInput) {
663
- return (
664
- <CommentsProvider {...props}>
665
- <div className="replica-comment-panel">
666
- <DiscussionLayout />
667
- </div>
668
- </CommentsProvider>
669
- );
670
- }