@tldraw/commenting 5.6.0-canary.ff4411b6d84d → 5.6.0-next.01b6e97e0728
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 +8 -2
- 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 +6 -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 +12 -6
- package/dist-cjs/index.js +1 -1
- 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 +10 -3
- 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 +7 -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 +12 -6
- package/dist-esm/index.mjs +1 -1
- 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 +45 -1
- package/src/canvas/comment-tool.tsx +14 -3
- 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 +8 -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/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,5 +1,6 @@
|
|
|
1
1
|
import { memo, useMemo, useRef } from 'react'
|
|
2
2
|
import {
|
|
3
|
+
defineMessages,
|
|
3
4
|
Editor,
|
|
4
5
|
TLCommentThread,
|
|
5
6
|
useContainer,
|
|
@@ -14,6 +15,13 @@ import { type CommentingContext } from './context'
|
|
|
14
15
|
import { sortThreadsForPreview, ThreadPreview, useMarkerPreview } from './thread-preview'
|
|
15
16
|
import { impreciseShapePinInset, isInInflatedViewport } from './thread-state'
|
|
16
17
|
|
|
18
|
+
const messages = defineMessages({
|
|
19
|
+
commentsClusterLabel: {
|
|
20
|
+
id: 'comments.cluster-label',
|
|
21
|
+
defaultMessage: '{count, plural, one {# comment here} other {# comments here}}',
|
|
22
|
+
},
|
|
23
|
+
})
|
|
24
|
+
|
|
17
25
|
/**
|
|
18
26
|
* The count badge standing in for several threads folded together at the current zoom. Hovering it
|
|
19
27
|
* previews its threads as a list; clicking zooms to the zoom level at which the cluster splits.
|
|
@@ -99,7 +107,7 @@ export const ClusterBadge = memo(function ClusterBadge({
|
|
|
99
107
|
type="button"
|
|
100
108
|
className="tlui-cmt-button tlui-cmt-canvas-cluster"
|
|
101
109
|
style={{ left: point.x, top: point.y }}
|
|
102
|
-
aria-label={msg(
|
|
110
|
+
aria-label={msg(messages.commentsClusterLabel, { count: node.count })}
|
|
103
111
|
onPointerDown={(e) => {
|
|
104
112
|
if (isCanvasPanGesture(editor, e)) {
|
|
105
113
|
forwardPointerEventToCanvas(container, e)
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { useMemo } from 'react'
|
|
2
2
|
import {
|
|
3
|
+
createCommentReaction,
|
|
4
|
+
createCommentReactionId,
|
|
5
|
+
defineMessages,
|
|
3
6
|
Editor,
|
|
4
7
|
TLComment,
|
|
5
8
|
TLCommentReaction,
|
|
6
|
-
createCommentReaction,
|
|
7
|
-
createCommentReactionId,
|
|
8
9
|
useEditor,
|
|
10
|
+
useTranslation,
|
|
9
11
|
useValue,
|
|
10
12
|
} from 'tldraw'
|
|
11
13
|
import { RenderReaction } from '../ui/reaction'
|
|
@@ -16,6 +18,10 @@ import { UNKNOWN_AUTHOR } from './comment-render'
|
|
|
16
18
|
import { getCommentReactions } from './comment-store'
|
|
17
19
|
import { getCommentingOptions, useCommentingOptions } from './options'
|
|
18
20
|
|
|
21
|
+
const messages = defineMessages({
|
|
22
|
+
commentsUnknownAuthor: { id: 'comments.unknown-author', defaultMessage: 'Someone' },
|
|
23
|
+
})
|
|
24
|
+
|
|
19
25
|
/**
|
|
20
26
|
* One comment's reactions, oldest first, reactively.
|
|
21
27
|
*
|
|
@@ -52,15 +58,18 @@ export interface ReactionSummaryInput {
|
|
|
52
58
|
* Tally a comment's reactions into an entry per emoji, ordered by when that emoji was first used so
|
|
53
59
|
* the row stays stable as later reactions arrive. `active` marks the current user's emoji and
|
|
54
60
|
* `reactors` lists who reacted, in reaction order. `resolveName` names each reactor; an id it can't
|
|
55
|
-
* name falls back to
|
|
61
|
+
* name falls back to `unknownAuthorName`, never the raw user id. Pass a translated name for that:
|
|
62
|
+
* this is a plain function, so it can't reach the translations itself.
|
|
56
63
|
*
|
|
57
64
|
* @public
|
|
58
65
|
*/
|
|
59
66
|
export function summarizeReactions(
|
|
60
67
|
reactions: readonly ReactionSummaryInput[],
|
|
61
68
|
currentUserId?: string | null,
|
|
62
|
-
resolveName?: (userId: string) => string | undefined
|
|
69
|
+
resolveName?: (userId: string) => string | undefined,
|
|
70
|
+
opts?: { unknownAuthorName?: string }
|
|
63
71
|
): ReactionSummary[] {
|
|
72
|
+
const unknownAuthorName = opts?.unknownAuthorName ?? UNKNOWN_AUTHOR
|
|
64
73
|
const groups = new Map<
|
|
65
74
|
string,
|
|
66
75
|
{ count: number; active: boolean; firstAt: number; reactors: ReactionSummary['reactors'] }
|
|
@@ -68,7 +77,7 @@ export function summarizeReactions(
|
|
|
68
77
|
for (const reaction of reactions) {
|
|
69
78
|
const mine = currentUserId != null && reaction.userId === currentUserId
|
|
70
79
|
// Fall back to a generic name, never the raw user id, when the id can't be resolved.
|
|
71
|
-
const reactor = { name: resolveName?.(reaction.userId) ??
|
|
80
|
+
const reactor = { name: resolveName?.(reaction.userId) ?? unknownAuthorName, you: mine }
|
|
72
81
|
const group = groups.get(reaction.emoji)
|
|
73
82
|
if (group) {
|
|
74
83
|
group.count++
|
|
@@ -177,9 +186,11 @@ export function CommentReactions({ comment, currentUserId, resolveName }: Commen
|
|
|
177
186
|
const renderReaction = useReactionRenderer()
|
|
178
187
|
const { components } = useCommentingOptions()
|
|
179
188
|
const reactions = useCommentReactions(editor, comment.id)
|
|
189
|
+
const msg = useTranslation()
|
|
190
|
+
const unknownAuthorName = msg(messages.commentsUnknownAuthor)
|
|
180
191
|
const summaries = useMemo(
|
|
181
|
-
() => summarizeReactions(reactions, currentUserId, resolveName),
|
|
182
|
-
[reactions, currentUserId, resolveName]
|
|
192
|
+
() => summarizeReactions(reactions, currentUserId, resolveName, { unknownAuthorName }),
|
|
193
|
+
[reactions, currentUserId, resolveName, unknownAuthorName]
|
|
183
194
|
)
|
|
184
195
|
// Suppress the hover list while any menu is open (the reaction picker, an overflow menu…) so it
|
|
185
196
|
// doesn't compete with the menu the user is actually working in. Edit mode already hides the
|
|
@@ -4,10 +4,12 @@ import { renderHtmlFromRichTextWithExtensions, TLRichText } from 'tldraw'
|
|
|
4
4
|
import { commentTipTapExtensions, isCommentEmpty } from '../ui/comment-extensions'
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
* The author name
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
7
|
+
* The author name used when no source can name an id (e.g. a deleted account, or a member with no
|
|
8
|
+
* comment and no live presence). Hosts pre-empt it by resolving the id from `resolveAuthor`.
|
|
9
|
+
*
|
|
10
|
+
* The English fallback, for the places that can't translate: it's a module constant, so the UI
|
|
11
|
+
* reads `comments.unknown-author` instead and only lands here when there are no translations at
|
|
12
|
+
* all. Pass a translated name to {@link summarizeReactions} for the same reason.
|
|
11
13
|
*/
|
|
12
14
|
export const UNKNOWN_AUTHOR = 'Someone'
|
|
13
15
|
|
|
@@ -10,9 +10,11 @@ import {
|
|
|
10
10
|
Editor,
|
|
11
11
|
TLShapeId,
|
|
12
12
|
TLStateNodeConstructor,
|
|
13
|
+
TLUiOverrideHelpers,
|
|
14
|
+
TLUiToolsContextType,
|
|
13
15
|
} from 'tldraw'
|
|
14
16
|
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
|
15
|
-
import { CommentTool, startCommentAt } from './comment-tool'
|
|
17
|
+
import { CommentTool, commentToolOverrides, startCommentAt } from './comment-tool'
|
|
16
18
|
import { commentsSidebarOpen, pendingComment, regionDraft } from './state'
|
|
17
19
|
|
|
18
20
|
/**
|
|
@@ -197,6 +199,37 @@ describe('startCommentAt', () => {
|
|
|
197
199
|
})
|
|
198
200
|
})
|
|
199
201
|
|
|
202
|
+
describe('commentToolOverrides', () => {
|
|
203
|
+
let driver: Driver
|
|
204
|
+
|
|
205
|
+
function getCommentTool() {
|
|
206
|
+
const tools = commentToolOverrides.tools!(
|
|
207
|
+
editor,
|
|
208
|
+
{} as TLUiToolsContextType,
|
|
209
|
+
{} as TLUiOverrideHelpers
|
|
210
|
+
)
|
|
211
|
+
return tools.comment
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
beforeEach(() => {
|
|
215
|
+
driver = makeEditor()
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
it('opens the composer where the context menu was opened, not at the live pointer', () => {
|
|
219
|
+
driver.pointerDown(400, 300).pointerUp(400, 300)
|
|
220
|
+
driver.pointerMove(600, 500)
|
|
221
|
+
getCommentTool().onSelect('context-menu')
|
|
222
|
+
expect(editor.isIn('comment.idle')).toBe(true)
|
|
223
|
+
expect(pendingComment.get(editor)?.point).toEqual({ x: 400, y: 300 })
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
it('only enters the tool from other sources', () => {
|
|
227
|
+
getCommentTool().onSelect('quick-actions')
|
|
228
|
+
expect(editor.isIn('comment.idle')).toBe(true)
|
|
229
|
+
expect(pendingComment.get(editor)).toBeNull()
|
|
230
|
+
})
|
|
231
|
+
})
|
|
232
|
+
|
|
200
233
|
describe('comment tool placement hints with regions enabled', () => {
|
|
201
234
|
it('clears the hint when a drag becomes a region', () => {
|
|
202
235
|
const driver = makeEditor(CommentTool.configure({ enableRegions: true }))
|
|
@@ -297,3 +330,14 @@ describe('comment tool anchoring to frames', () => {
|
|
|
297
330
|
expect(editor.getHintingShapeIds()).toEqual([])
|
|
298
331
|
})
|
|
299
332
|
})
|
|
333
|
+
|
|
334
|
+
describe('commentToolOverrides', () => {
|
|
335
|
+
// The label is data the toolbar puts through `msg()`, so free text here renders as itself in
|
|
336
|
+
// every locale and the extractor never sees it — which is how this one shipped as 'Comment'
|
|
337
|
+
// and stayed English everywhere. It has to be a declared id.
|
|
338
|
+
it('labels the tool with a translatable id', () => {
|
|
339
|
+
makeEditor()
|
|
340
|
+
const tools = commentToolOverrides.tools!(editor, {}, {} as never)
|
|
341
|
+
expect(tools.comment.label).toBe('tool.comment')
|
|
342
|
+
})
|
|
343
|
+
})
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
TLStateNodeConstructor,
|
|
7
7
|
TLUiOverrides,
|
|
8
8
|
VecLike,
|
|
9
|
+
defineMessages,
|
|
9
10
|
} from 'tldraw'
|
|
10
11
|
import { type CommentingOptions, defaultCommentingOptions, getCommentingOptions } from './options'
|
|
11
12
|
import { commentsSidebarOpen, pendingComment, regionDraft } from './state'
|
|
@@ -308,17 +309,27 @@ class CommentDragging extends StateNode {
|
|
|
308
309
|
/** @public */
|
|
309
310
|
export const commentTools = [CommentTool]
|
|
310
311
|
|
|
312
|
+
const messages = defineMessages({
|
|
313
|
+
toolComment: { id: 'tool.comment', defaultMessage: 'Comment' },
|
|
314
|
+
})
|
|
315
|
+
|
|
311
316
|
/** Registers the comment tool in the UI (icon, label, shortcut). Compose into your overrides.
|
|
312
|
-
* Once registered, tldraw's `DefaultQuickActionsContent` shows the comment button
|
|
317
|
+
* Once registered, tldraw's `DefaultQuickActionsContent` shows the comment button, and its
|
|
318
|
+
* `DefaultContextMenuContent` shows a comment item that opens the composer where the menu was
|
|
319
|
+
* opened.
|
|
313
320
|
* @public */
|
|
314
321
|
export const commentToolOverrides: TLUiOverrides = {
|
|
315
322
|
tools(editor, tools) {
|
|
316
323
|
tools.comment = {
|
|
317
324
|
id: 'comment',
|
|
318
325
|
icon: 'comment',
|
|
319
|
-
label:
|
|
326
|
+
label: messages.toolComment.id,
|
|
320
327
|
kbd: 'c',
|
|
321
|
-
onSelect: () =>
|
|
328
|
+
onSelect: (source) => {
|
|
329
|
+
// The menu was opened by the last pointer down; the live pointer is over the menu now.
|
|
330
|
+
if (source === 'context-menu') startCommentAt(editor, editor.inputs.getOriginPagePoint())
|
|
331
|
+
else editor.setCurrentTool('comment')
|
|
332
|
+
},
|
|
322
333
|
}
|
|
323
334
|
return tools
|
|
324
335
|
},
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
defineMessages,
|
|
2
3
|
TldrawUiButton,
|
|
3
4
|
TldrawUiDropdownMenuContent,
|
|
4
5
|
TldrawUiDropdownMenuRoot,
|
|
@@ -14,6 +15,14 @@ import { MoreMenuIcon } from '../ui/icons'
|
|
|
14
15
|
import { SidebarFilters } from './sidebar-filters'
|
|
15
16
|
import { sidebarFilters } from './state'
|
|
16
17
|
|
|
18
|
+
const messages = defineMessages({
|
|
19
|
+
commentsFilter: { id: 'comments.filter', defaultMessage: 'Filter comments' },
|
|
20
|
+
commentsOnlyMyComments: { id: 'comments.only-my-comments', defaultMessage: 'Only my comments' },
|
|
21
|
+
commentsOnlyUnread: { id: 'comments.only-unread', defaultMessage: 'Only unread' },
|
|
22
|
+
commentsShowAllPages: { id: 'comments.show-all-pages', defaultMessage: 'Show all pages' },
|
|
23
|
+
commentsShowResolved: { id: 'comments.show-resolved', defaultMessage: 'Show resolved comments' },
|
|
24
|
+
})
|
|
25
|
+
|
|
17
26
|
/** @public */
|
|
18
27
|
export interface CommentsFilterMenuProps {
|
|
19
28
|
/** Whether to offer the "only your threads" toggle (needs a known current user). */
|
|
@@ -40,8 +49,8 @@ export function CommentsFilterMenu({
|
|
|
40
49
|
<TldrawUiDropdownMenuTrigger>
|
|
41
50
|
<TldrawUiButton
|
|
42
51
|
type="icon"
|
|
43
|
-
tooltip={msg(
|
|
44
|
-
title={msg(
|
|
52
|
+
tooltip={msg(messages.commentsFilter)}
|
|
53
|
+
title={msg(messages.commentsFilter)}
|
|
45
54
|
className="tlui-cmt-header-btn"
|
|
46
55
|
>
|
|
47
56
|
<MoreMenuIcon />
|
|
@@ -54,14 +63,14 @@ export function CommentsFilterMenu({
|
|
|
54
63
|
<TldrawUiMenuGroup id="comments-filter">
|
|
55
64
|
<TldrawUiMenuCheckboxItem
|
|
56
65
|
id="show-all-pages"
|
|
57
|
-
label=
|
|
66
|
+
label={messages.commentsShowAllPages.id}
|
|
58
67
|
checked={!filters.onlyCurrentPage}
|
|
59
68
|
onSelect={() => toggle('onlyCurrentPage')}
|
|
60
69
|
/>
|
|
61
70
|
{canFilterByAuthor && (
|
|
62
71
|
<TldrawUiMenuCheckboxItem
|
|
63
72
|
id="only-my-comments"
|
|
64
|
-
label=
|
|
73
|
+
label={messages.commentsOnlyMyComments.id}
|
|
65
74
|
checked={filters.onlyMine}
|
|
66
75
|
onSelect={() => toggle('onlyMine')}
|
|
67
76
|
/>
|
|
@@ -69,14 +78,14 @@ export function CommentsFilterMenu({
|
|
|
69
78
|
{canFilterByUnread && (
|
|
70
79
|
<TldrawUiMenuCheckboxItem
|
|
71
80
|
id="only-unread"
|
|
72
|
-
label=
|
|
81
|
+
label={messages.commentsOnlyUnread.id}
|
|
73
82
|
checked={filters.onlyUnread}
|
|
74
83
|
onSelect={() => toggle('onlyUnread')}
|
|
75
84
|
/>
|
|
76
85
|
)}
|
|
77
86
|
<TldrawUiMenuCheckboxItem
|
|
78
87
|
id="show-resolved"
|
|
79
|
-
label=
|
|
88
|
+
label={messages.commentsShowResolved.id}
|
|
80
89
|
checked={filters.showResolved}
|
|
81
90
|
onSelect={() => toggle('showResolved')}
|
|
82
91
|
/>
|
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
import { TldrawUiMenuCheckboxItem, useEditor, useValue } from 'tldraw'
|
|
1
|
+
import { defineMessages, TldrawUiMenuCheckboxItem, useEditor, useValue } from 'tldraw'
|
|
2
2
|
import { commentsHidden, toggleCommentsHidden } from './state'
|
|
3
3
|
|
|
4
|
+
const messages = defineMessages({
|
|
5
|
+
commentsTitle: { id: 'comments.title', defaultMessage: 'Comments' },
|
|
6
|
+
})
|
|
7
|
+
|
|
4
8
|
/**
|
|
5
9
|
* A checkbox menu item for a "View" menu that shows/hides comment pins on the canvas — checked
|
|
6
10
|
* when comments are visible, like tldraw's own grid/snap toggles. Reads and toggles the shared
|
|
@@ -14,7 +18,7 @@ export function CommentsMenuItem() {
|
|
|
14
18
|
return (
|
|
15
19
|
<TldrawUiMenuCheckboxItem
|
|
16
20
|
id="comments-visible"
|
|
17
|
-
label=
|
|
21
|
+
label={messages.commentsTitle.id}
|
|
18
22
|
kbd="!c"
|
|
19
23
|
readonlyOk
|
|
20
24
|
checked={!hidden}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
defineMessages,
|
|
2
3
|
TldrawUiButton,
|
|
3
4
|
TldrawUiDropdownMenuContent,
|
|
4
5
|
TldrawUiDropdownMenuGroup,
|
|
@@ -12,6 +13,12 @@ import {
|
|
|
12
13
|
} from 'tldraw'
|
|
13
14
|
import { commentsHidden, toggleCommentsHidden } from './state'
|
|
14
15
|
|
|
16
|
+
const messages = defineMessages({
|
|
17
|
+
commentsMoreOptions: { id: 'comments.more-options', defaultMessage: 'More options' },
|
|
18
|
+
commentsHide: { id: 'comments.hide', defaultMessage: 'Hide comments' },
|
|
19
|
+
commentsShow: { id: 'comments.show', defaultMessage: 'Show comments' },
|
|
20
|
+
})
|
|
21
|
+
|
|
15
22
|
// A keyboard-shortcut glyph, not translatable copy — kept out of JSX as a constant.
|
|
16
23
|
const HIDE_SHORTCUT = '⇧C'
|
|
17
24
|
|
|
@@ -28,11 +35,11 @@ export function CommentsOverflowMenu() {
|
|
|
28
35
|
<TldrawUiDropdownMenuTrigger>
|
|
29
36
|
<TldrawUiButton
|
|
30
37
|
type="icon"
|
|
31
|
-
tooltip={msg(
|
|
32
|
-
title={msg(
|
|
38
|
+
tooltip={msg(messages.commentsMoreOptions)}
|
|
39
|
+
title={msg(messages.commentsMoreOptions)}
|
|
33
40
|
className="tlui-cmt-header-btn"
|
|
34
41
|
>
|
|
35
|
-
<TldrawUiIcon icon="dots-vertical" label={msg(
|
|
42
|
+
<TldrawUiIcon icon="dots-vertical" label={msg(messages.commentsMoreOptions)} small />
|
|
36
43
|
</TldrawUiButton>
|
|
37
44
|
</TldrawUiDropdownMenuTrigger>
|
|
38
45
|
<TldrawUiDropdownMenuContent
|
|
@@ -49,7 +56,7 @@ export function CommentsOverflowMenu() {
|
|
|
49
56
|
className="tlui-cmt-menu-item"
|
|
50
57
|
onClick={() => toggleCommentsHidden(editor)}
|
|
51
58
|
>
|
|
52
|
-
<span>{hidden ? msg(
|
|
59
|
+
<span>{hidden ? msg(messages.commentsShow) : msg(messages.commentsHide)}</span>
|
|
53
60
|
<span className="tlui-cmt-menu-item__shortcut">{HIDE_SHORTCUT}</span>
|
|
54
61
|
</button>
|
|
55
62
|
</TldrawUiDropdownMenuItem>
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import { ReactNode, useCallback } from 'react'
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
defineMessages,
|
|
4
|
+
EditorPortal,
|
|
5
|
+
TLCommentThread,
|
|
6
|
+
useEditor,
|
|
7
|
+
useTranslation,
|
|
8
|
+
useValue,
|
|
9
|
+
} from 'tldraw'
|
|
3
10
|
import { CommentListItemProps, CommentListItemRenderProps, CommentsList } from '../ui/comments-list'
|
|
4
11
|
import { UNKNOWN_COMMENT_AUTHOR } from './comment-render'
|
|
5
12
|
import { CommentsFilterMenu } from './comments-filter-menu'
|
|
@@ -12,6 +19,16 @@ import { richTextToPlaintext } from './rich-text'
|
|
|
12
19
|
import { commentsSidebarOpen, openThreadId, sidebarFilters } from './state'
|
|
13
20
|
import { focusThread } from './thread-state'
|
|
14
21
|
|
|
22
|
+
const messages = defineMessages({
|
|
23
|
+
commentsEmpty: { id: 'comments.empty', defaultMessage: 'No comments on this page yet.' },
|
|
24
|
+
commentsEmptyFiltered: {
|
|
25
|
+
id: 'comments.empty-filtered',
|
|
26
|
+
defaultMessage: 'No comments match your filters.',
|
|
27
|
+
},
|
|
28
|
+
commentsResolved: { id: 'comments.resolved', defaultMessage: 'Resolved' },
|
|
29
|
+
commentsTitle: { id: 'comments.title', defaultMessage: 'Comments' },
|
|
30
|
+
})
|
|
31
|
+
|
|
15
32
|
/**
|
|
16
33
|
* The host wiring for {@link CanvasCommentsSidebar}: the {@link CommentingContext} fields it reads,
|
|
17
34
|
* plus the panel's own slots. A non-null `currentUserId` enables the "only your threads" filter, and
|
|
@@ -146,22 +163,22 @@ export function CanvasCommentsSidebar(props: CanvasCommentsSidebarProps) {
|
|
|
146
163
|
<SidebarPanel>
|
|
147
164
|
<CommentsList
|
|
148
165
|
items={items}
|
|
149
|
-
header={header ?? msg(
|
|
166
|
+
header={header ?? msg(messages.commentsTitle)}
|
|
150
167
|
headerAction={
|
|
151
168
|
<div className="tlui-cmt-list__header-actions">
|
|
169
|
+
<CommentsVisibilityToggle />
|
|
152
170
|
<CommentsFilterMenu
|
|
153
171
|
canFilterByAuthor={currentUserId !== null}
|
|
154
172
|
canFilterByUnread={isCommentUnread !== undefined}
|
|
155
173
|
/>
|
|
156
|
-
<CommentsVisibilityToggle />
|
|
157
174
|
</div>
|
|
158
175
|
}
|
|
159
176
|
empty={
|
|
160
177
|
items.length === 0 && pageThreads.length > 0
|
|
161
|
-
? msg(
|
|
162
|
-
: (empty ?? msg(
|
|
178
|
+
? msg(messages.commentsEmptyFiltered)
|
|
179
|
+
: (empty ?? msg(messages.commentsEmpty))
|
|
163
180
|
}
|
|
164
|
-
resolvedLabel={msg(
|
|
181
|
+
resolvedLabel={msg(messages.commentsResolved)}
|
|
165
182
|
onSelect={focus}
|
|
166
183
|
renderItem={renderItem}
|
|
167
184
|
/>
|
|
@@ -1,7 +1,12 @@
|
|
|
1
|
-
import { TldrawUiButton, useEditor, useTranslation, useValue } from 'tldraw'
|
|
1
|
+
import { defineMessages, TldrawUiButton, useEditor, useTranslation, useValue } from 'tldraw'
|
|
2
2
|
import { EyeClosedIcon, EyeOpenIcon } from '../ui/icons'
|
|
3
3
|
import { commentsHidden, toggleCommentsHidden } from './state'
|
|
4
4
|
|
|
5
|
+
const messages = defineMessages({
|
|
6
|
+
commentsHide: { id: 'comments.hide', defaultMessage: 'Hide comments' },
|
|
7
|
+
commentsShow: { id: 'comments.show', defaultMessage: 'Show comments' },
|
|
8
|
+
})
|
|
9
|
+
|
|
5
10
|
/** The sidebar header's show/hide toggle for comment pins — an eye that closes while comments
|
|
6
11
|
* are hidden. The same state as the Shift+C shortcut.
|
|
7
12
|
* @public @react */
|
|
@@ -9,7 +14,7 @@ export function CommentsVisibilityToggle() {
|
|
|
9
14
|
const editor = useEditor()
|
|
10
15
|
const msg = useTranslation()
|
|
11
16
|
const hidden = useValue('comments hidden', () => commentsHidden.get(editor), [editor])
|
|
12
|
-
const label = hidden ? msg(
|
|
17
|
+
const label = hidden ? msg(messages.commentsShow) : msg(messages.commentsHide)
|
|
13
18
|
|
|
14
19
|
return (
|
|
15
20
|
<TldrawUiButton
|
package/src/canvas/options.ts
CHANGED
|
@@ -91,7 +91,7 @@ export interface CommentingComponents {
|
|
|
91
91
|
/**
|
|
92
92
|
* The hover affordance naming who reacted with an emoji. Receives the reactors and the pill (as
|
|
93
93
|
* `children`) and owns the whole thing — box, size, shape, position. For a wording change,
|
|
94
|
-
* translate the `comments.reacted
|
|
94
|
+
* translate the `comments.reacted` string instead.
|
|
95
95
|
*/
|
|
96
96
|
ReactionTooltip?: ComponentType<ReactionTooltipProps>
|
|
97
97
|
/**
|
|
@@ -3,6 +3,7 @@ import { useEffect, useRef, useState } from 'react'
|
|
|
3
3
|
import {
|
|
4
4
|
createComment,
|
|
5
5
|
createCommentThread,
|
|
6
|
+
defineMessages,
|
|
6
7
|
Editor,
|
|
7
8
|
EditorPortal,
|
|
8
9
|
TLRichText,
|
|
@@ -27,6 +28,11 @@ import { useIsMobileCommenting, useMobilePlacement } from './mobile-placement'
|
|
|
27
28
|
import { useCanComment, useCommentingOptions } from './options'
|
|
28
29
|
import { pendingComment } from './state'
|
|
29
30
|
|
|
31
|
+
const messages = defineMessages({
|
|
32
|
+
commentsSend: { id: 'comments.send', defaultMessage: 'Send' },
|
|
33
|
+
commentsAddPlaceholder: { id: 'comments.add-placeholder', defaultMessage: 'Add a comment…' },
|
|
34
|
+
})
|
|
35
|
+
|
|
30
36
|
const stop = (e: { stopPropagation(): void }) => e.stopPropagation()
|
|
31
37
|
|
|
32
38
|
/**
|
|
@@ -140,8 +146,8 @@ export function PendingComposer({
|
|
|
140
146
|
{canComment ? (
|
|
141
147
|
<CommentComposer
|
|
142
148
|
author={me ?? UNKNOWN_COMMENT_AUTHOR}
|
|
143
|
-
placeholder={msg(
|
|
144
|
-
sendLabel={msg(
|
|
149
|
+
placeholder={msg(messages.commentsAddPlaceholder)}
|
|
150
|
+
sendLabel={msg(messages.commentsSend)}
|
|
145
151
|
value={text}
|
|
146
152
|
onChange={(value) => {
|
|
147
153
|
setText(value)
|
|
@@ -8,9 +8,10 @@ import {
|
|
|
8
8
|
useState,
|
|
9
9
|
} from 'react'
|
|
10
10
|
import {
|
|
11
|
-
|
|
11
|
+
defineMessages,
|
|
12
12
|
Editor,
|
|
13
13
|
TLCommentThread,
|
|
14
|
+
type BoxModel,
|
|
14
15
|
useContainer,
|
|
15
16
|
usePassThroughWheelEvents,
|
|
16
17
|
useTranslation,
|
|
@@ -19,7 +20,7 @@ import {
|
|
|
19
20
|
import { CommentPin } from '../ui/comment-pin'
|
|
20
21
|
import { forwardPointerEventToCanvas, isCanvasPanGesture } from './canvas-events'
|
|
21
22
|
import { commitCommentMutation } from './comment-mutations'
|
|
22
|
-
import {
|
|
23
|
+
import { UNKNOWN_COMMENT_AUTHOR } from './comment-render'
|
|
23
24
|
import { type CommentingContext } from './context'
|
|
24
25
|
import { useThreadComments } from './hooks'
|
|
25
26
|
import { getCommentingOptions, useCanComment, useCommentingOptions } from './options'
|
|
@@ -46,6 +47,15 @@ import {
|
|
|
46
47
|
} from './thread-state'
|
|
47
48
|
import { POPOVER_OFFSET, ThreadPopover, ThreadView } from './thread-view'
|
|
48
49
|
|
|
50
|
+
const messages = defineMessages({
|
|
51
|
+
commentsUnknownAuthor: { id: 'comments.unknown-author', defaultMessage: 'Someone' },
|
|
52
|
+
commentsPinLabel: { id: 'comments.pin-label', defaultMessage: 'Comment by {name}' },
|
|
53
|
+
commentsPinLabelResolved: {
|
|
54
|
+
id: 'comments.pin-label-resolved',
|
|
55
|
+
defaultMessage: 'Resolved comment by {name}',
|
|
56
|
+
},
|
|
57
|
+
})
|
|
58
|
+
|
|
49
59
|
/** The opened popover has a header row the hover preview lacks, so it opens this much higher — the
|
|
50
60
|
* first comment then lands where the preview's sat. Re-measure if the header height or the preview
|
|
51
61
|
* panel padding changes. */
|
|
@@ -195,8 +205,11 @@ export const ThreadPin = memo(function ThreadPin({
|
|
|
195
205
|
<Avatar author={threadAuthor ?? UNKNOWN_COMMENT_AUTHOR} />
|
|
196
206
|
)
|
|
197
207
|
const pinLabel = msg(
|
|
198
|
-
thread.resolved ?
|
|
199
|
-
|
|
208
|
+
thread.resolved ? messages.commentsPinLabelResolved : messages.commentsPinLabel,
|
|
209
|
+
{
|
|
210
|
+
name: threadAuthor?.name ?? msg(messages.commentsUnknownAuthor),
|
|
211
|
+
}
|
|
212
|
+
)
|
|
200
213
|
|
|
201
214
|
// Drag the marker to move the thread: position is overridden locally while dragging, then re-anchored
|
|
202
215
|
// on drop. A region translates keeping its size; a barely-moved pointer is a click.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useEffect, useMemo, useRef, type CSSProperties } from 'react'
|
|
2
2
|
import {
|
|
3
|
+
defineMessages,
|
|
3
4
|
Editor,
|
|
4
5
|
EditorAtom,
|
|
5
6
|
EditorPortal,
|
|
@@ -17,6 +18,10 @@ import { useCommentingOptions } from './options'
|
|
|
17
18
|
import { openStackId, openThreadId } from './state'
|
|
18
19
|
import { POPOVER_OFFSET, toCardProps, useResolveName } from './thread-view'
|
|
19
20
|
|
|
21
|
+
const messages = defineMessages({
|
|
22
|
+
commentsPreviewMore: { id: 'comments.preview-more', defaultMessage: '{count} more' },
|
|
23
|
+
})
|
|
24
|
+
|
|
20
25
|
/**
|
|
21
26
|
* Hover previews for every canvas marker — a single pin, a coincident stack, or a cluster badge.
|
|
22
27
|
* Hovering shows the thread(s) behind the marker as cards; the marker's own click is unchanged.
|
|
@@ -277,7 +282,7 @@ export function ThreadPreview({
|
|
|
277
282
|
})}
|
|
278
283
|
{overflow > 0 && (
|
|
279
284
|
<div className="tlui-cmt-preview-more">
|
|
280
|
-
{msg(
|
|
285
|
+
{msg(messages.commentsPreviewMore, { count: overflow })}
|
|
281
286
|
</div>
|
|
282
287
|
)}
|
|
283
288
|
</div>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type MouseEvent as ReactMouseEvent, memo, useEffect, useRef } from 'react'
|
|
2
2
|
import {
|
|
3
|
+
defineMessages,
|
|
3
4
|
Editor,
|
|
4
5
|
TLCommentThread,
|
|
5
6
|
usePassThroughWheelEvents,
|
|
@@ -8,7 +9,6 @@ import {
|
|
|
8
9
|
} from 'tldraw'
|
|
9
10
|
import { CommentCard } from '../ui/comment-card'
|
|
10
11
|
import { CountBadge } from '../ui/count-badge'
|
|
11
|
-
import { UNKNOWN_AUTHOR } from './comment-render'
|
|
12
12
|
import { type CommentingContext } from './context'
|
|
13
13
|
import { useThreadComments } from './hooks'
|
|
14
14
|
import { useCommentingOptions } from './options'
|
|
@@ -24,6 +24,19 @@ import {
|
|
|
24
24
|
useResolveName,
|
|
25
25
|
} from './thread-view'
|
|
26
26
|
|
|
27
|
+
const messages = defineMessages({
|
|
28
|
+
commentsUnknownAuthor: { id: 'comments.unknown-author', defaultMessage: 'Someone' },
|
|
29
|
+
commentsPinLabel: { id: 'comments.pin-label', defaultMessage: 'Comment by {name}' },
|
|
30
|
+
commentsPinLabelResolved: {
|
|
31
|
+
id: 'comments.pin-label-resolved',
|
|
32
|
+
defaultMessage: 'Resolved comment by {name}',
|
|
33
|
+
},
|
|
34
|
+
commentsStackLabel: {
|
|
35
|
+
id: 'comments.stack-label',
|
|
36
|
+
defaultMessage: '{count, plural, one {# comment at this spot} other {# comments at this spot}}',
|
|
37
|
+
},
|
|
38
|
+
})
|
|
39
|
+
|
|
27
40
|
/**
|
|
28
41
|
* The pin for threads whose anchors resolve to the same page point — pins zooming can never
|
|
29
42
|
* separate, so they share one count badge. Clicking it lists each thread as a card; clicking a card
|
|
@@ -137,7 +150,7 @@ export const ThreadStackPin = memo(function ThreadStackPin({
|
|
|
137
150
|
ref={badgeRef}
|
|
138
151
|
type="button"
|
|
139
152
|
className="tlui-cmt-button tlui-cmt-canvas-stack-badge"
|
|
140
|
-
aria-label={msg(
|
|
153
|
+
aria-label={msg(messages.commentsStackLabel, { count: threads.length })}
|
|
141
154
|
aria-expanded={open}
|
|
142
155
|
onPointerDown={(e) => e.stopPropagation()}
|
|
143
156
|
onClick={(e) => {
|
|
@@ -228,8 +241,12 @@ function StackThreadCard({
|
|
|
228
241
|
type="button"
|
|
229
242
|
className="tlui-cmt-button tlui-cmt-stack-list__card-action"
|
|
230
243
|
aria-label={msg(
|
|
231
|
-
thread.resolved ?
|
|
232
|
-
|
|
244
|
+
thread.resolved ? messages.commentsPinLabelResolved : messages.commentsPinLabel,
|
|
245
|
+
{
|
|
246
|
+
name:
|
|
247
|
+
props.resolveAuthor(thread.createdBy)?.name ?? msg(messages.commentsUnknownAuthor),
|
|
248
|
+
}
|
|
249
|
+
)}
|
|
233
250
|
onClick={open}
|
|
234
251
|
/>
|
|
235
252
|
<CommentCard {...toCardProps(first, props, options.components, resolveName)} />
|