@tldraw/commenting 5.6.0-next.c4e9c54a816e → 5.6.0-next.f151f7f3ebd6
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/dist-cjs/canvas/cluster-badge.js +7 -1
- package/dist-cjs/canvas/cluster-badge.js.map +2 -2
- package/dist-cjs/canvas/comment-reactions.js +10 -4
- package/dist-cjs/canvas/comment-reactions.js.map +2 -2
- package/dist-cjs/canvas/comment-render.js.map +2 -2
- package/dist-cjs/canvas/comment-tool.js +35 -15
- package/dist-cjs/canvas/comment-tool.js.map +2 -2
- package/dist-cjs/canvas/comments-filter-menu.js +13 -6
- package/dist-cjs/canvas/comments-filter-menu.js.map +2 -2
- package/dist-cjs/canvas/comments-menu-item.js +4 -1
- package/dist-cjs/canvas/comments-menu-item.js.map +2 -2
- package/dist-cjs/canvas/comments-overflow-menu.js +9 -4
- package/dist-cjs/canvas/comments-overflow-menu.js.map +2 -2
- package/dist-cjs/canvas/comments-sidebar.js +14 -5
- package/dist-cjs/canvas/comments-sidebar.js.map +2 -2
- package/dist-cjs/canvas/comments-visibility-toggle.js +5 -1
- package/dist-cjs/canvas/comments-visibility-toggle.js.map +2 -2
- package/dist-cjs/canvas/options.js.map +1 -1
- package/dist-cjs/canvas/pending-composer.js +7 -2
- package/dist-cjs/canvas/pending-composer.js.map +2 -2
- package/dist-cjs/canvas/thread-pin.js +13 -2
- package/dist-cjs/canvas/thread-pin.js.map +2 -2
- package/dist-cjs/canvas/thread-preview.js +4 -1
- package/dist-cjs/canvas/thread-preview.js.map +2 -2
- package/dist-cjs/canvas/thread-stack.js +18 -4
- package/dist-cjs/canvas/thread-stack.js.map +2 -2
- package/dist-cjs/canvas/thread-view.js +40 -20
- package/dist-cjs/canvas/thread-view.js.map +2 -2
- package/dist-cjs/index.d.ts +28 -8
- package/dist-cjs/index.js +2 -1
- package/dist-cjs/index.js.map +2 -2
- package/dist-cjs/ui/comment-composer.js +5 -0
- package/dist-cjs/ui/comment-composer.js.map +2 -2
- package/dist-cjs/ui/gesture-focus.js +37 -0
- package/dist-cjs/ui/gesture-focus.js.map +7 -0
- package/dist-cjs/ui/reaction-picker.js +5 -2
- package/dist-cjs/ui/reaction-picker.js.map +2 -2
- package/dist-cjs/ui/reaction.js +15 -5
- package/dist-cjs/ui/reaction.js.map +2 -2
- package/dist-cjs/ui/reply-count.js +8 -2
- package/dist-cjs/ui/reply-count.js.map +2 -2
- package/dist-esm/canvas/cluster-badge.mjs +8 -1
- package/dist-esm/canvas/cluster-badge.mjs.map +2 -2
- package/dist-esm/canvas/comment-reactions.mjs +12 -4
- package/dist-esm/canvas/comment-reactions.mjs.map +2 -2
- package/dist-esm/canvas/comment-render.mjs.map +2 -2
- package/dist-esm/canvas/comment-tool.mjs +37 -16
- package/dist-esm/canvas/comment-tool.mjs.map +2 -2
- package/dist-esm/canvas/comments-filter-menu.mjs +14 -6
- package/dist-esm/canvas/comments-filter-menu.mjs.map +2 -2
- package/dist-esm/canvas/comments-menu-item.mjs +5 -2
- package/dist-esm/canvas/comments-menu-item.mjs.map +2 -2
- package/dist-esm/canvas/comments-overflow-menu.mjs +10 -4
- package/dist-esm/canvas/comments-overflow-menu.mjs.map +2 -2
- package/dist-esm/canvas/comments-sidebar.mjs +21 -6
- package/dist-esm/canvas/comments-sidebar.mjs.map +2 -2
- package/dist-esm/canvas/comments-visibility-toggle.mjs +6 -2
- package/dist-esm/canvas/comments-visibility-toggle.mjs.map +2 -2
- package/dist-esm/canvas/options.mjs.map +1 -1
- package/dist-esm/canvas/pending-composer.mjs +8 -2
- package/dist-esm/canvas/pending-composer.mjs.map +2 -2
- package/dist-esm/canvas/thread-pin.mjs +15 -3
- package/dist-esm/canvas/thread-pin.mjs.map +2 -2
- package/dist-esm/canvas/thread-preview.mjs +5 -1
- package/dist-esm/canvas/thread-preview.mjs.map +2 -2
- package/dist-esm/canvas/thread-stack.mjs +19 -4
- package/dist-esm/canvas/thread-stack.mjs.map +2 -2
- package/dist-esm/canvas/thread-view.mjs +42 -21
- package/dist-esm/canvas/thread-view.mjs.map +2 -2
- package/dist-esm/index.d.mts +28 -8
- package/dist-esm/index.mjs +8 -2
- package/dist-esm/index.mjs.map +2 -2
- package/dist-esm/ui/comment-composer.mjs +5 -0
- package/dist-esm/ui/comment-composer.mjs.map +2 -2
- package/dist-esm/ui/gesture-focus.mjs +17 -0
- package/dist-esm/ui/gesture-focus.mjs.map +7 -0
- package/dist-esm/ui/reaction-picker.mjs +6 -2
- package/dist-esm/ui/reaction-picker.mjs.map +2 -2
- package/dist-esm/ui/reaction.mjs +16 -8
- package/dist-esm/ui/reaction.mjs.map +2 -2
- package/dist-esm/ui/reply-count.mjs +8 -2
- package/dist-esm/ui/reply-count.mjs.map +2 -2
- package/package.json +5 -5
- package/src/canvas/cluster-badge.tsx +9 -1
- package/src/canvas/comment-reactions.tsx +18 -7
- package/src/canvas/comment-render.ts +6 -4
- package/src/canvas/comment-tool.test.ts +83 -7
- package/src/canvas/comment-tool.tsx +61 -19
- package/src/canvas/comments-filter-menu.tsx +15 -6
- package/src/canvas/comments-menu-item.tsx +6 -2
- package/src/canvas/comments-overflow-menu.tsx +11 -4
- package/src/canvas/comments-sidebar.tsx +23 -6
- package/src/canvas/comments-visibility-toggle.tsx +7 -2
- package/src/canvas/options.ts +1 -1
- package/src/canvas/pending-composer.tsx +11 -2
- package/src/canvas/thread-pin.tsx +17 -4
- package/src/canvas/thread-preview.tsx +6 -1
- package/src/canvas/thread-stack.tsx +21 -4
- package/src/canvas/thread-view.tsx +44 -22
- package/src/index.ts +6 -1
- package/src/ui/comment-composer.tsx +9 -0
- package/src/ui/gesture-focus.test.ts +45 -0
- package/src/ui/gesture-focus.ts +29 -0
- package/src/ui/reaction-picker.tsx +7 -2
- package/src/ui/reaction.test.tsx +22 -0
- package/src/ui/reaction.tsx +25 -14
- package/src/ui/reply-count.ts +9 -3
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { memo, ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
2
2
|
import {
|
|
3
3
|
createComment,
|
|
4
|
+
defineMessages,
|
|
4
5
|
Editor,
|
|
5
6
|
EditorPortal,
|
|
6
7
|
TLComment,
|
|
7
8
|
TLCommentThread,
|
|
8
|
-
TLRichText,
|
|
9
9
|
TldrawUiButton,
|
|
10
10
|
TldrawUiDropdownMenuContent,
|
|
11
11
|
TldrawUiDropdownMenuGroup,
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
TldrawUiDropdownMenuRoot,
|
|
14
14
|
TldrawUiDropdownMenuTrigger,
|
|
15
15
|
TldrawUiIcon,
|
|
16
|
+
TLRichText,
|
|
16
17
|
useContainer,
|
|
17
18
|
usePassThroughWheelEvents,
|
|
18
19
|
useTranslation,
|
|
@@ -38,7 +39,7 @@ import {
|
|
|
38
39
|
resolveThread,
|
|
39
40
|
} from './comment-mutations'
|
|
40
41
|
import { CommentReactionPicker, CommentReactions } from './comment-reactions'
|
|
41
|
-
import {
|
|
42
|
+
import { UNKNOWN_COMMENT_AUTHOR } from './comment-render'
|
|
42
43
|
import { type CommentingContext } from './context'
|
|
43
44
|
import { useThreadComments } from './hooks'
|
|
44
45
|
import { useIsMobileCommenting, useMobilePlacement } from './mobile-placement'
|
|
@@ -51,6 +52,28 @@ import {
|
|
|
51
52
|
} from './options'
|
|
52
53
|
import { openThreadId } from './state'
|
|
53
54
|
|
|
55
|
+
const messages = defineMessages({
|
|
56
|
+
actionDelete: { id: 'action.delete', defaultMessage: 'Delete' },
|
|
57
|
+
commentsCopyLink: { id: 'comments.copy-link', defaultMessage: 'Copy link' },
|
|
58
|
+
commentsDelete: { id: 'comments.delete', defaultMessage: 'Delete thread' },
|
|
59
|
+
commentsDismiss: { id: 'comments.dismiss', defaultMessage: 'Dismiss' },
|
|
60
|
+
commentsEdit: { id: 'comments.edit', defaultMessage: 'Edit' },
|
|
61
|
+
commentsEditPlaceholder: {
|
|
62
|
+
id: 'comments.edit-placeholder',
|
|
63
|
+
defaultMessage: 'Edit comment…',
|
|
64
|
+
},
|
|
65
|
+
commentsLinkCopied: { id: 'comments.link-copied', defaultMessage: 'Link copied' },
|
|
66
|
+
commentsMoreOptions: { id: 'comments.more-options', defaultMessage: 'More options' },
|
|
67
|
+
commentsReopen: { id: 'comments.reopen', defaultMessage: 'Reopen' },
|
|
68
|
+
commentsReplyPlaceholder: { id: 'comments.reply-placeholder', defaultMessage: 'Reply…' },
|
|
69
|
+
commentsResolve: { id: 'comments.resolve', defaultMessage: 'Resolve' },
|
|
70
|
+
commentsResolvedBy: { id: 'comments.resolved-by', defaultMessage: 'Resolved by {name}' },
|
|
71
|
+
commentsSave: { id: 'comments.save', defaultMessage: 'Save' },
|
|
72
|
+
commentsSend: { id: 'comments.send', defaultMessage: 'Send' },
|
|
73
|
+
commentsThreadTitle: { id: 'comments.thread-title', defaultMessage: 'Comment' },
|
|
74
|
+
commentsUnknownAuthor: { id: 'comments.unknown-author', defaultMessage: 'Someone' },
|
|
75
|
+
})
|
|
76
|
+
|
|
54
77
|
const stop = (e: { stopPropagation(): void }) => e.stopPropagation()
|
|
55
78
|
|
|
56
79
|
/** The "⋯" dropdown used for both a comment's and the thread's actions. */
|
|
@@ -70,12 +93,12 @@ function MoreOptionsMenu({
|
|
|
70
93
|
<TldrawUiDropdownMenuTrigger>
|
|
71
94
|
<TldrawUiButton
|
|
72
95
|
type="icon"
|
|
73
|
-
tooltip={msg(
|
|
74
|
-
title={msg(
|
|
96
|
+
tooltip={msg(messages.commentsMoreOptions)}
|
|
97
|
+
title={msg(messages.commentsMoreOptions)}
|
|
75
98
|
className="tlui-cmt-thread__action"
|
|
76
99
|
data-cmt-more-for={moreFor}
|
|
77
100
|
>
|
|
78
|
-
<TldrawUiIcon icon="dots-vertical" label={msg(
|
|
101
|
+
<TldrawUiIcon icon="dots-vertical" label={msg(messages.commentsMoreOptions)} small />
|
|
79
102
|
</TldrawUiButton>
|
|
80
103
|
</TldrawUiDropdownMenuTrigger>
|
|
81
104
|
<TldrawUiDropdownMenuContent
|
|
@@ -459,11 +482,11 @@ export const ThreadView = memo(function ThreadView({
|
|
|
459
482
|
>
|
|
460
483
|
<CommentComposer
|
|
461
484
|
author={card.author}
|
|
462
|
-
placeholder={msg(
|
|
485
|
+
placeholder={msg(messages.commentsEditPlaceholder)}
|
|
463
486
|
value={editText}
|
|
464
487
|
onChange={setEditText}
|
|
465
488
|
onSubmit={saveEdit}
|
|
466
|
-
sendLabel={msg(
|
|
489
|
+
sendLabel={msg(messages.commentsSave)}
|
|
467
490
|
disabled={isCommentEmpty(editText)}
|
|
468
491
|
getMentionSuggestions={getMentionSuggestions}
|
|
469
492
|
renderMentionSuggestion={renderMentionSuggestion}
|
|
@@ -491,13 +514,13 @@ export const ThreadView = memo(function ThreadView({
|
|
|
491
514
|
<MoreOptionsMenu id={`comment-actions-${comment.id}`} moreFor={comment.id}>
|
|
492
515
|
{permissions?.edit && (
|
|
493
516
|
<MenuItem
|
|
494
|
-
label={msg(
|
|
517
|
+
label={msg(messages.commentsEdit)}
|
|
495
518
|
onClick={() => startEdit(comment, { fromMoreMenu: true })}
|
|
496
519
|
/>
|
|
497
520
|
)}
|
|
498
521
|
{permissions?.delete && (
|
|
499
522
|
<MenuItem
|
|
500
|
-
label={msg(
|
|
523
|
+
label={msg(messages.actionDelete)}
|
|
501
524
|
danger
|
|
502
525
|
onClick={() => deleteComment(editor, comment)}
|
|
503
526
|
/>
|
|
@@ -515,7 +538,7 @@ export const ThreadView = memo(function ThreadView({
|
|
|
515
538
|
|
|
516
539
|
// Resolve and delete are commenting writes: behind `canComment`, plus the `currentUserId` a
|
|
517
540
|
// resolve stamps into `resolved.by`.
|
|
518
|
-
const resolveLabel = msg(thread.resolved ?
|
|
541
|
+
const resolveLabel = msg(thread.resolved ? messages.commentsReopen : messages.commentsResolve)
|
|
519
542
|
const headerActions = (
|
|
520
543
|
<>
|
|
521
544
|
{/* Host verbs — assign, link a ticket — sit ahead of the built-in actions. */}
|
|
@@ -527,13 +550,13 @@ export const ThreadView = memo(function ThreadView({
|
|
|
527
550
|
item can confirm the copy in place. */}
|
|
528
551
|
{threadHref !== undefined && (
|
|
529
552
|
<MenuItem
|
|
530
|
-
label={msg(linkCopied ?
|
|
553
|
+
label={msg(linkCopied ? messages.commentsLinkCopied : messages.commentsCopyLink)}
|
|
531
554
|
noClose
|
|
532
555
|
onClick={copyThreadLink}
|
|
533
556
|
/>
|
|
534
557
|
)}
|
|
535
558
|
{canDeleteThread && (
|
|
536
|
-
<MenuItem label={msg(
|
|
559
|
+
<MenuItem label={msg(messages.commentsDelete)} danger onClick={removeThread} />
|
|
537
560
|
)}
|
|
538
561
|
</MoreOptionsMenu>
|
|
539
562
|
)}
|
|
@@ -550,36 +573,35 @@ export const ThreadView = memo(function ThreadView({
|
|
|
550
573
|
)}
|
|
551
574
|
<TldrawUiButton
|
|
552
575
|
type="icon"
|
|
553
|
-
tooltip={msg(
|
|
554
|
-
title={msg(
|
|
576
|
+
tooltip={msg(messages.commentsDismiss)}
|
|
577
|
+
title={msg(messages.commentsDismiss)}
|
|
555
578
|
className="tlui-cmt-thread__action"
|
|
556
579
|
onClick={() => openThreadId.set(editor, null)}
|
|
557
580
|
>
|
|
558
|
-
<TldrawUiIcon icon="cross-2" label={msg(
|
|
581
|
+
<TldrawUiIcon icon="cross-2" label={msg(messages.commentsDismiss)} small />
|
|
559
582
|
</TldrawUiButton>
|
|
560
583
|
</>
|
|
561
584
|
)
|
|
562
585
|
|
|
563
586
|
return (
|
|
564
587
|
<CommentThread
|
|
565
|
-
header={msg(
|
|
588
|
+
header={msg(messages.commentsThreadTitle)}
|
|
566
589
|
headerActions={headerActions}
|
|
567
590
|
renderComment={renderComment}
|
|
568
591
|
comments={cards}
|
|
569
592
|
resolvedBanner={
|
|
570
593
|
thread.resolved
|
|
571
|
-
? msg(
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
)
|
|
594
|
+
? msg(messages.commentsResolvedBy, {
|
|
595
|
+
name: resolveAuthor(thread.resolved.by)?.name ?? msg(messages.commentsUnknownAuthor),
|
|
596
|
+
})
|
|
575
597
|
: undefined
|
|
576
598
|
}
|
|
577
599
|
composer={
|
|
578
600
|
canReply
|
|
579
601
|
? {
|
|
580
602
|
author: me ?? UNKNOWN_COMMENT_AUTHOR,
|
|
581
|
-
placeholder: msg(
|
|
582
|
-
sendLabel: msg(
|
|
603
|
+
placeholder: msg(messages.commentsReplyPlaceholder),
|
|
604
|
+
sendLabel: msg(messages.commentsSend),
|
|
583
605
|
value: reply,
|
|
584
606
|
onChange: (value: TLRichText) => {
|
|
585
607
|
setReply(value)
|
package/src/index.ts
CHANGED
|
@@ -80,7 +80,12 @@ export {
|
|
|
80
80
|
toggleCommentReaction,
|
|
81
81
|
useCommentReactions,
|
|
82
82
|
} from './canvas/comment-reactions'
|
|
83
|
-
export {
|
|
83
|
+
export {
|
|
84
|
+
CommentTool,
|
|
85
|
+
commentToolOverrides,
|
|
86
|
+
commentTools,
|
|
87
|
+
startCommentAt,
|
|
88
|
+
} from './canvas/comment-tool'
|
|
84
89
|
export {
|
|
85
90
|
getCommentReactions,
|
|
86
91
|
getCommentRecord,
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
} from 'react'
|
|
20
20
|
import { isEqual, TLRichText, useMaybeEditor } from 'tldraw'
|
|
21
21
|
import { commentTipTapExtensions, EMPTY_COMMENT, isCommentEmpty } from './comment-extensions'
|
|
22
|
+
import { focusOnGestureEnd } from './gesture-focus'
|
|
22
23
|
import { SendButton } from './send-button'
|
|
23
24
|
|
|
24
25
|
/** @public */
|
|
@@ -258,6 +259,14 @@ export function CommentComposer({
|
|
|
258
259
|
return () => cancelAnimationFrame(raf)
|
|
259
260
|
}, [autoFocus, editor])
|
|
260
261
|
|
|
262
|
+
// The frame-later focus above places the caret but can't raise iOS's software keyboard, leaving
|
|
263
|
+
// the composer looking ready while it swallows every keystroke. Focus once more from the release
|
|
264
|
+
// that ends the placing gesture, which can.
|
|
265
|
+
useEffect(() => {
|
|
266
|
+
if (!autoFocus || !editor) return
|
|
267
|
+
return focusOnGestureEnd(editor.view.dom.ownerDocument, () => editor.commands.focus('end'))
|
|
268
|
+
}, [autoFocus, editor])
|
|
269
|
+
|
|
261
270
|
// The whole field behaves like the text input: clicking its empty area (the padding, or the
|
|
262
271
|
// space beside/below a short line) focuses the editor rather than only the text glyphs being
|
|
263
272
|
// clickable. The input (caret placement) and the send button keep their own click handling.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import { focusOnGestureEnd } from './gesture-focus'
|
|
3
|
+
|
|
4
|
+
const pointer = (type: 'pointerdown' | 'pointerup') =>
|
|
5
|
+
document.dispatchEvent(new Event(type, { bubbles: true }))
|
|
6
|
+
|
|
7
|
+
describe('focusOnGestureEnd', () => {
|
|
8
|
+
it('focuses on the release that ends the gesture the field was mounted during', () => {
|
|
9
|
+
const focus = vi.fn()
|
|
10
|
+
focusOnGestureEnd(document, focus)
|
|
11
|
+
pointer('pointerup')
|
|
12
|
+
expect(focus).toHaveBeenCalledTimes(1)
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it('only focuses once, so later gestures leave the caret alone', () => {
|
|
16
|
+
const focus = vi.fn()
|
|
17
|
+
focusOnGestureEnd(document, focus)
|
|
18
|
+
pointer('pointerup')
|
|
19
|
+
pointer('pointerdown')
|
|
20
|
+
pointer('pointerup')
|
|
21
|
+
expect(focus).toHaveBeenCalledTimes(1)
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('gives up when a press comes first — that release belongs to another gesture', () => {
|
|
25
|
+
const focus = vi.fn()
|
|
26
|
+
focusOnGestureEnd(document, focus)
|
|
27
|
+
pointer('pointerdown')
|
|
28
|
+
pointer('pointerup')
|
|
29
|
+
expect(focus).not.toHaveBeenCalled()
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('stops listening once detached', () => {
|
|
33
|
+
const focus = vi.fn()
|
|
34
|
+
const stop = focusOnGestureEnd(document, focus)
|
|
35
|
+
stop()
|
|
36
|
+
pointer('pointerup')
|
|
37
|
+
expect(focus).not.toHaveBeenCalled()
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('can be detached more than once', () => {
|
|
41
|
+
const stop = focusOnGestureEnd(document, vi.fn())
|
|
42
|
+
stop()
|
|
43
|
+
expect(() => stop()).not.toThrow()
|
|
44
|
+
})
|
|
45
|
+
})
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* iOS opens the software keyboard only for a focus that runs inside a user gesture's own task. A
|
|
3
|
+
* focus scheduled for a later frame still moves the caret, so a field can look ready while
|
|
4
|
+
* swallowing every keystroke until the reader taps it a second time.
|
|
5
|
+
*
|
|
6
|
+
* This covers the case where a field is mounted *during* a press — the comment composer opens on
|
|
7
|
+
* pointer down and settles its anchor on pointer up — by running `focus` from the native pointerup
|
|
8
|
+
* that ends the same gesture. That is late enough to survive the canvas taking focus for itself on
|
|
9
|
+
* pointer down, and still inside the gesture, so iOS raises the keyboard.
|
|
10
|
+
*
|
|
11
|
+
* A pointerdown arriving first means the mounting gesture had already ended (a field opened from a
|
|
12
|
+
* button, say), so the next release belongs to an unrelated gesture: give up rather than pull the
|
|
13
|
+
* caret somewhere the reader didn't ask for.
|
|
14
|
+
*
|
|
15
|
+
* Returns a function that detaches the listeners; safe to call more than once.
|
|
16
|
+
*/
|
|
17
|
+
export function focusOnGestureEnd(doc: Document, focus: () => void): () => void {
|
|
18
|
+
const stop = () => {
|
|
19
|
+
doc.removeEventListener('pointerup', onPointerUp, true)
|
|
20
|
+
doc.removeEventListener('pointerdown', stop, true)
|
|
21
|
+
}
|
|
22
|
+
function onPointerUp() {
|
|
23
|
+
stop()
|
|
24
|
+
focus()
|
|
25
|
+
}
|
|
26
|
+
doc.addEventListener('pointerup', onPointerUp, true)
|
|
27
|
+
doc.addEventListener('pointerdown', stop, true)
|
|
28
|
+
return stop
|
|
29
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useCallback, useId, type ComponentType, type KeyboardEvent, type MouseEvent } from 'react'
|
|
2
2
|
import {
|
|
3
|
+
defineMessages,
|
|
3
4
|
TldrawUiButton,
|
|
4
5
|
TldrawUiDropdownMenuContent,
|
|
5
6
|
TldrawUiDropdownMenuRoot,
|
|
@@ -12,6 +13,10 @@ import { EmojiPicker, type EmojiPickerProps } from './emoji-picker'
|
|
|
12
13
|
import { SmileyIcon } from './icons'
|
|
13
14
|
import { RenderReaction } from './reaction'
|
|
14
15
|
|
|
16
|
+
const messages = defineMessages({
|
|
17
|
+
commentsAddReaction: { id: 'comments.add-reaction', defaultMessage: 'Add reaction' },
|
|
18
|
+
})
|
|
19
|
+
|
|
15
20
|
/** @public */
|
|
16
21
|
export interface ReactionPickerProps {
|
|
17
22
|
/** The emoji to offer. Defaults to `DEFAULT_REACTION_EMOJI`. */
|
|
@@ -92,8 +97,8 @@ export function ReactionPicker({
|
|
|
92
97
|
<TldrawUiDropdownMenuTrigger>
|
|
93
98
|
<TldrawUiButton
|
|
94
99
|
type="icon"
|
|
95
|
-
tooltip={msg(
|
|
96
|
-
title={msg(
|
|
100
|
+
tooltip={msg(messages.commentsAddReaction)}
|
|
101
|
+
title={msg(messages.commentsAddReaction)}
|
|
97
102
|
className={className}
|
|
98
103
|
>
|
|
99
104
|
<SmileyIcon />
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { renderToStaticMarkup } from 'react-dom/server'
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
import { DefaultReactionTooltipContent } from './reaction'
|
|
4
|
+
|
|
5
|
+
function sentence(...names: string[]) {
|
|
6
|
+
return renderToStaticMarkup(
|
|
7
|
+
<DefaultReactionTooltipContent
|
|
8
|
+
reactors={names.map((name) => ({ name, you: name === 'You' }))}
|
|
9
|
+
/>
|
|
10
|
+
)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
describe('DefaultReactionTooltipContent', () => {
|
|
14
|
+
// Rendered outside a translation provider, so this is the shipped English of `comments.reacted`.
|
|
15
|
+
it('picks the phrasing by number of reactors', () => {
|
|
16
|
+
expect(sentence('You')).toBe('You reacted')
|
|
17
|
+
expect(sentence('You', 'Bo')).toBe('You and Bo reacted')
|
|
18
|
+
expect(sentence('Ada', 'Bo', 'Cy')).toBe('Ada, Bo and Cy reacted')
|
|
19
|
+
expect(sentence('Ada', 'Bo', 'Cy', 'Di')).toBe('Ada, Bo, Cy and 1 other reacted')
|
|
20
|
+
expect(sentence('Ada', 'Bo', 'Cy', 'Di', 'Ed', 'Fi')).toBe('Ada, Bo, Cy and 3 others reacted')
|
|
21
|
+
})
|
|
22
|
+
})
|
package/src/ui/reaction.tsx
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
import { ComponentType, ReactNode } from 'react'
|
|
2
|
-
import { TldrawUiTooltip, useTranslation } from 'tldraw'
|
|
2
|
+
import { defineMessages, TldrawUiTooltip, useTranslation } from 'tldraw'
|
|
3
3
|
import { ReactionReactor } from './reactions'
|
|
4
4
|
|
|
5
|
+
const messages = defineMessages({
|
|
6
|
+
commentsMentionYou: { id: 'comments.mention-you', defaultMessage: 'You' },
|
|
7
|
+
commentsReacted: {
|
|
8
|
+
id: 'comments.reacted',
|
|
9
|
+
defaultMessage:
|
|
10
|
+
'{count, plural, =1 {{a} reacted} =2 {{a} and {b} reacted} =3 {{a}, {b} and {c} reacted} other {{a}, {b}, {c} and {others, plural, one {# other} other {# others}} reacted}}',
|
|
11
|
+
},
|
|
12
|
+
})
|
|
13
|
+
|
|
5
14
|
/** Render a reaction token to its visual — the emoji glyph by default. @public */
|
|
6
15
|
export type RenderReaction = (token: string) => ReactNode
|
|
7
16
|
|
|
@@ -99,25 +108,27 @@ export function DefaultReactionTooltip({ reactors, children }: ReactionTooltipPr
|
|
|
99
108
|
/**
|
|
100
109
|
* The default reactor sentence naming who reacted — up to three names spelled out, then "and N
|
|
101
110
|
* others" (e.g. "You reacted", "You and Bo reacted", "You, Bo and Ada reacted", "You, Bo, Ada and 2
|
|
102
|
-
* others reacted"). The wording lives in the `comments.reacted
|
|
103
|
-
*
|
|
111
|
+
* others reacted"). The wording lives in the `comments.reacted` translation string, whose ICU
|
|
112
|
+
* plural picks the phrasing, so each locale controls both the grammar and how it counts. Exported
|
|
113
|
+
* so a custom `ReactionTooltip` can reuse it inside its own box.
|
|
104
114
|
* @public @react
|
|
105
115
|
*/
|
|
106
116
|
export function DefaultReactionTooltipContent({ reactors }: { reactors: ReactionReactor[] }) {
|
|
107
117
|
const msg = useTranslation()
|
|
108
118
|
const names = reactors.map((reactor) =>
|
|
109
|
-
reactor.you ? msg(
|
|
119
|
+
reactor.you ? msg(messages.commentsMentionYou) : reactor.name
|
|
110
120
|
)
|
|
111
121
|
if (names.length === 0) return null
|
|
112
122
|
const [a, b, c] = names
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
+
return (
|
|
124
|
+
<>
|
|
125
|
+
{msg(messages.commentsReacted, {
|
|
126
|
+
count: names.length,
|
|
127
|
+
others: names.length - 3,
|
|
128
|
+
a,
|
|
129
|
+
b,
|
|
130
|
+
c,
|
|
131
|
+
})}
|
|
132
|
+
</>
|
|
133
|
+
)
|
|
123
134
|
}
|
package/src/ui/reply-count.ts
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
import { useTranslation } from 'tldraw'
|
|
1
|
+
import { defineMessages, useTranslation } from 'tldraw'
|
|
2
|
+
|
|
3
|
+
const messages = defineMessages({
|
|
4
|
+
commentsReplies: {
|
|
5
|
+
id: 'comments.replies',
|
|
6
|
+
defaultMessage: '{count, plural, one {# reply} other {# replies}}',
|
|
7
|
+
},
|
|
8
|
+
})
|
|
2
9
|
|
|
3
10
|
/**
|
|
4
11
|
* The localized "N replies" label (e.g. "1 reply", "3 replies") for a thread with `replyCount`
|
|
@@ -11,6 +18,5 @@ export function replyCountLabel(
|
|
|
11
18
|
replyCount: number
|
|
12
19
|
): string | null {
|
|
13
20
|
if (replyCount <= 0) return null
|
|
14
|
-
|
|
15
|
-
return msg(key).replace('{count}', String(replyCount))
|
|
21
|
+
return msg(messages.commentsReplies, { count: replyCount })
|
|
16
22
|
}
|