@tldraw/commenting 0.0.0-bootstrap → 5.3.0-canary.04044ed9e96d
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/README.md +0 -2
- package/commenting.css +1152 -350
- package/dist-cjs/canvas/anchor-lifecycle.js +148 -0
- package/dist-cjs/canvas/anchor-lifecycle.js.map +7 -0
- package/dist-cjs/{ui/avatar.js → canvas/canvas-events.js} +14 -22
- package/dist-cjs/canvas/canvas-events.js.map +7 -0
- package/dist-cjs/canvas/cluster-badge.js +121 -0
- package/dist-cjs/canvas/cluster-badge.js.map +7 -0
- package/dist-cjs/canvas/cluster-fade.js +85 -0
- package/dist-cjs/canvas/cluster-fade.js.map +7 -0
- package/dist-cjs/canvas/cluster-input.js +53 -3
- package/dist-cjs/canvas/cluster-input.js.map +2 -2
- package/dist-cjs/canvas/cluster-model.js +252 -0
- package/dist-cjs/canvas/cluster-model.js.map +7 -0
- package/dist-cjs/canvas/comment-body.js +1 -1
- package/dist-cjs/canvas/comment-body.js.map +2 -2
- package/dist-cjs/canvas/comment-drafts.js +54 -0
- package/dist-cjs/canvas/comment-drafts.js.map +7 -0
- package/dist-cjs/canvas/comment-mutations.js +152 -0
- package/dist-cjs/canvas/comment-mutations.js.map +7 -0
- package/dist-cjs/canvas/comment-reactions.js +160 -0
- package/dist-cjs/canvas/comment-reactions.js.map +7 -0
- package/dist-cjs/canvas/comment-render.js +9 -6
- package/dist-cjs/canvas/comment-render.js.map +2 -2
- package/dist-cjs/canvas/comment-store.js +19 -9
- package/dist-cjs/canvas/comment-store.js.map +2 -2
- package/dist-cjs/canvas/comment-tool.js +63 -14
- package/dist-cjs/canvas/comment-tool.js.map +2 -2
- package/dist-cjs/canvas/comments-filter-menu.js +17 -37
- package/dist-cjs/canvas/comments-filter-menu.js.map +2 -2
- package/dist-cjs/canvas/comments-overflow-menu.js +25 -22
- package/dist-cjs/canvas/comments-overflow-menu.js.map +2 -2
- package/dist-cjs/canvas/comments-overlay.js +151 -989
- package/dist-cjs/canvas/comments-overlay.js.map +3 -3
- package/dist-cjs/canvas/comments-sidebar.js +49 -41
- package/dist-cjs/canvas/comments-sidebar.js.map +2 -2
- package/dist-cjs/canvas/comments-visibility-toggle.js +45 -0
- package/dist-cjs/canvas/comments-visibility-toggle.js.map +7 -0
- package/dist-cjs/canvas/context.js +17 -0
- package/dist-cjs/canvas/context.js.map +7 -0
- package/dist-cjs/canvas/hooks.js +28 -3
- package/dist-cjs/canvas/hooks.js.map +2 -2
- package/dist-cjs/canvas/license.js.map +1 -1
- package/dist-cjs/canvas/mobile-placement.js +91 -0
- package/dist-cjs/canvas/mobile-placement.js.map +7 -0
- package/dist-cjs/canvas/options.js +57 -2
- package/dist-cjs/canvas/options.js.map +2 -2
- package/dist-cjs/canvas/pending-composer.js +134 -0
- package/dist-cjs/canvas/pending-composer.js.map +7 -0
- package/dist-cjs/canvas/pin-stacking.js +81 -0
- package/dist-cjs/canvas/pin-stacking.js.map +7 -0
- package/dist-cjs/canvas/region-box.js +113 -0
- package/dist-cjs/canvas/region-box.js.map +7 -0
- package/dist-cjs/canvas/sidebar-filters.js +2 -2
- package/dist-cjs/canvas/sidebar-filters.js.map +2 -2
- package/dist-cjs/canvas/state.js +28 -14
- package/dist-cjs/canvas/state.js.map +2 -2
- package/dist-cjs/canvas/thread-pin.js +304 -0
- package/dist-cjs/canvas/thread-pin.js.map +7 -0
- package/dist-cjs/canvas/thread-preview.js +177 -0
- package/dist-cjs/canvas/thread-preview.js.map +7 -0
- package/dist-cjs/canvas/thread-stack.js +199 -0
- package/dist-cjs/canvas/thread-stack.js.map +7 -0
- package/dist-cjs/canvas/thread-state.js +82 -21
- package/dist-cjs/canvas/thread-state.js.map +2 -2
- package/dist-cjs/canvas/thread-view.js +450 -0
- package/dist-cjs/canvas/thread-view.js.map +7 -0
- package/dist-cjs/clustering/computeClusterTable.js +6 -6
- package/dist-cjs/clustering/computeClusterTable.js.map +2 -2
- package/dist-cjs/clustering/replay.js +89 -22
- package/dist-cjs/clustering/replay.js.map +3 -3
- package/dist-cjs/clustering/runtime.js +126 -7
- package/dist-cjs/clustering/runtime.js.map +2 -2
- package/dist-cjs/clustering/types.js.map +1 -1
- package/dist-cjs/index.d.ts +904 -353
- package/dist-cjs/index.js +52 -28
- package/dist-cjs/index.js.map +2 -2
- package/dist-cjs/ui/byline.js +10 -6
- package/dist-cjs/ui/byline.js.map +2 -2
- package/dist-cjs/ui/comment-card.js +15 -8
- package/dist-cjs/ui/comment-card.js.map +2 -2
- package/dist-cjs/ui/comment-composer.js +124 -39
- package/dist-cjs/ui/comment-composer.js.map +2 -2
- package/dist-cjs/ui/comment-pin.js +11 -2
- package/dist-cjs/ui/comment-pin.js.map +2 -2
- package/dist-cjs/ui/comment-thread.js +9 -7
- package/dist-cjs/ui/comment-thread.js.map +2 -2
- package/dist-cjs/ui/comments-list.js +39 -40
- package/dist-cjs/ui/comments-list.js.map +2 -2
- package/dist-cjs/ui/count-badge.js +3 -2
- package/dist-cjs/ui/count-badge.js.map +2 -2
- package/dist-cjs/ui/emoji-picker.js +54 -0
- package/dist-cjs/ui/emoji-picker.js.map +7 -0
- package/dist-cjs/ui/empty-state.js +3 -3
- package/dist-cjs/ui/empty-state.js.map +2 -2
- package/dist-cjs/ui/format-time.js +16 -1
- package/dist-cjs/ui/format-time.js.map +2 -2
- package/dist-cjs/ui/icons.js +115 -0
- package/dist-cjs/ui/icons.js.map +7 -0
- package/dist-cjs/ui/reaction-picker.js +71 -0
- package/dist-cjs/ui/reaction-picker.js.map +7 -0
- package/dist-cjs/ui/reaction.js +58 -6
- package/dist-cjs/ui/reaction.js.map +2 -2
- package/dist-cjs/ui/reactions.js +20 -7
- package/dist-cjs/ui/reactions.js.map +2 -2
- package/dist-cjs/ui/{mention.js → reply-count.js} +9 -11
- package/dist-cjs/ui/reply-count.js.map +7 -0
- package/dist-cjs/ui/send-button.js +13 -1
- package/dist-cjs/ui/send-button.js.map +2 -2
- package/dist-cjs/ui/{comment-text.js → visual-viewport.js} +12 -9
- package/dist-cjs/ui/visual-viewport.js.map +7 -0
- package/dist-esm/canvas/anchor-lifecycle.mjs +128 -0
- package/dist-esm/canvas/anchor-lifecycle.mjs.map +7 -0
- package/dist-esm/canvas/canvas-events.mjs +15 -0
- package/dist-esm/canvas/canvas-events.mjs.map +7 -0
- package/dist-esm/canvas/cluster-badge.mjs +106 -0
- package/dist-esm/canvas/cluster-badge.mjs.map +7 -0
- package/dist-esm/canvas/cluster-fade.mjs +65 -0
- package/dist-esm/canvas/cluster-fade.mjs.map +7 -0
- package/dist-esm/canvas/cluster-input.mjs +54 -4
- package/dist-esm/canvas/cluster-input.mjs.map +2 -2
- package/dist-esm/canvas/cluster-model.mjs +236 -0
- package/dist-esm/canvas/cluster-model.mjs.map +7 -0
- package/dist-esm/canvas/comment-body.mjs +1 -1
- package/dist-esm/canvas/comment-body.mjs.map +2 -2
- package/dist-esm/canvas/comment-drafts.mjs +34 -0
- package/dist-esm/canvas/comment-drafts.mjs.map +7 -0
- package/dist-esm/canvas/comment-mutations.mjs +132 -0
- package/dist-esm/canvas/comment-mutations.mjs.map +7 -0
- package/dist-esm/canvas/comment-reactions.mjs +145 -0
- package/dist-esm/canvas/comment-reactions.mjs.map +7 -0
- package/dist-esm/canvas/comment-render.mjs +10 -7
- package/dist-esm/canvas/comment-render.mjs.map +2 -2
- package/dist-esm/canvas/comment-store.mjs +19 -9
- package/dist-esm/canvas/comment-store.mjs.map +2 -2
- package/dist-esm/canvas/comment-tool.mjs +66 -17
- package/dist-esm/canvas/comment-tool.mjs.map +2 -2
- package/dist-esm/canvas/comments-filter-menu.mjs +17 -36
- package/dist-esm/canvas/comments-filter-menu.mjs.map +2 -2
- package/dist-esm/canvas/comments-overflow-menu.mjs +27 -22
- package/dist-esm/canvas/comments-overflow-menu.mjs.map +2 -2
- package/dist-esm/canvas/comments-overlay.mjs +157 -1010
- package/dist-esm/canvas/comments-overlay.mjs.map +3 -3
- package/dist-esm/canvas/comments-sidebar.mjs +53 -46
- package/dist-esm/canvas/comments-sidebar.mjs.map +2 -2
- package/dist-esm/canvas/comments-visibility-toggle.mjs +25 -0
- package/dist-esm/canvas/comments-visibility-toggle.mjs.map +7 -0
- package/dist-esm/canvas/context.mjs +1 -0
- package/dist-esm/canvas/context.mjs.map +7 -0
- package/dist-esm/canvas/hooks.mjs +35 -5
- package/dist-esm/canvas/hooks.mjs.map +2 -2
- package/dist-esm/canvas/license.mjs.map +1 -1
- package/dist-esm/canvas/mobile-placement.mjs +71 -0
- package/dist-esm/canvas/mobile-placement.mjs.map +7 -0
- package/dist-esm/canvas/options.mjs +59 -3
- package/dist-esm/canvas/options.mjs.map +2 -2
- package/dist-esm/canvas/pending-composer.mjs +126 -0
- package/dist-esm/canvas/pending-composer.mjs.map +7 -0
- package/dist-esm/canvas/pin-stacking.mjs +61 -0
- package/dist-esm/canvas/pin-stacking.mjs.map +7 -0
- package/dist-esm/canvas/region-box.mjs +93 -0
- package/dist-esm/canvas/region-box.mjs.map +7 -0
- package/dist-esm/canvas/sidebar-filters.mjs +2 -2
- package/dist-esm/canvas/sidebar-filters.mjs.map +2 -2
- package/dist-esm/canvas/state.mjs +29 -19
- package/dist-esm/canvas/state.mjs.map +2 -2
- package/dist-esm/canvas/thread-pin.mjs +310 -0
- package/dist-esm/canvas/thread-pin.mjs.map +7 -0
- package/dist-esm/canvas/thread-preview.mjs +163 -0
- package/dist-esm/canvas/thread-preview.mjs.map +7 -0
- package/dist-esm/canvas/thread-stack.mjs +189 -0
- package/dist-esm/canvas/thread-stack.mjs.map +7 -0
- package/dist-esm/canvas/thread-state.mjs +86 -22
- package/dist-esm/canvas/thread-state.mjs.map +2 -2
- package/dist-esm/canvas/thread-view.mjs +461 -0
- package/dist-esm/canvas/thread-view.mjs.map +7 -0
- package/dist-esm/clustering/computeClusterTable.mjs +6 -6
- package/dist-esm/clustering/computeClusterTable.mjs.map +2 -2
- package/dist-esm/clustering/replay.mjs +89 -22
- package/dist-esm/clustering/replay.mjs.map +3 -3
- package/dist-esm/clustering/runtime.mjs +126 -7
- package/dist-esm/clustering/runtime.mjs.map +2 -2
- package/dist-esm/index.d.mts +904 -353
- package/dist-esm/index.mjs +102 -43
- package/dist-esm/index.mjs.map +2 -2
- package/dist-esm/ui/byline.mjs +11 -7
- package/dist-esm/ui/byline.mjs.map +2 -2
- package/dist-esm/ui/comment-card.mjs +15 -8
- package/dist-esm/ui/comment-card.mjs.map +2 -2
- package/dist-esm/ui/comment-composer.mjs +135 -39
- package/dist-esm/ui/comment-composer.mjs.map +2 -2
- package/dist-esm/ui/comment-pin.mjs +11 -2
- package/dist-esm/ui/comment-pin.mjs.map +2 -2
- package/dist-esm/ui/comment-thread.mjs +9 -7
- package/dist-esm/ui/comment-thread.mjs.map +2 -2
- package/dist-esm/ui/comments-list.mjs +51 -50
- package/dist-esm/ui/comments-list.mjs.map +2 -2
- package/dist-esm/ui/count-badge.mjs +3 -2
- package/dist-esm/ui/count-badge.mjs.map +2 -2
- package/dist-esm/ui/emoji-picker.mjs +34 -0
- package/dist-esm/ui/emoji-picker.mjs.map +7 -0
- package/dist-esm/ui/empty-state.mjs +3 -3
- package/dist-esm/ui/empty-state.mjs.map +2 -2
- package/dist-esm/ui/format-time.mjs +16 -1
- package/dist-esm/ui/format-time.mjs.map +2 -2
- package/dist-esm/ui/icons.mjs +95 -0
- package/dist-esm/ui/icons.mjs.map +7 -0
- package/dist-esm/ui/reaction-picker.mjs +59 -0
- package/dist-esm/ui/reaction-picker.mjs.map +7 -0
- package/dist-esm/ui/reaction.mjs +59 -7
- package/dist-esm/ui/reaction.mjs.map +2 -2
- package/dist-esm/ui/reactions.mjs +21 -8
- package/dist-esm/ui/reactions.mjs.map +2 -2
- package/dist-esm/ui/reply-count.mjs +9 -0
- package/dist-esm/ui/reply-count.mjs.map +7 -0
- package/dist-esm/ui/send-button.mjs +13 -1
- package/dist-esm/ui/send-button.mjs.map +2 -2
- package/dist-esm/ui/visual-viewport.mjs +12 -0
- package/dist-esm/ui/visual-viewport.mjs.map +7 -0
- package/package.json +6 -6
- package/src/canvas/anchor-lifecycle.test.ts +290 -0
- package/src/canvas/anchor-lifecycle.ts +191 -0
- package/src/canvas/canvas-events.ts +18 -0
- package/src/canvas/canvas.css +464 -55
- package/src/canvas/cluster-badge.tsx +133 -0
- package/src/canvas/cluster-fade.ts +102 -0
- package/src/canvas/cluster-input.test.ts +178 -36
- package/src/canvas/cluster-input.ts +95 -9
- package/src/canvas/cluster-model.ts +378 -0
- package/src/canvas/comment-body.tsx +1 -1
- package/src/canvas/comment-drafts.test.ts +55 -0
- package/src/canvas/comment-drafts.ts +46 -0
- package/src/canvas/comment-mutations.test.ts +452 -0
- package/src/canvas/comment-mutations.ts +299 -0
- package/src/canvas/comment-reactions.test.ts +154 -0
- package/src/canvas/comment-reactions.tsx +246 -0
- package/src/canvas/comment-render.test.ts +1 -1
- package/src/canvas/comment-render.ts +17 -13
- package/src/canvas/comment-store.ts +68 -33
- package/src/canvas/comment-tool.test.ts +162 -0
- package/src/canvas/comment-tool.tsx +103 -25
- package/src/canvas/comments-filter-menu.tsx +20 -36
- package/src/canvas/comments-overflow-menu.tsx +17 -19
- package/src/canvas/comments-overlay.tsx +252 -1369
- package/src/canvas/comments-sidebar.test.ts +45 -0
- package/src/canvas/comments-sidebar.tsx +112 -73
- package/src/canvas/comments-visibility-toggle.tsx +25 -0
- package/src/canvas/context.ts +48 -0
- package/src/canvas/hooks.test.ts +94 -0
- package/src/canvas/hooks.ts +66 -11
- package/src/canvas/license.ts +1 -1
- package/src/canvas/mobile-placement.ts +115 -0
- package/src/canvas/options.test.ts +176 -4
- package/src/canvas/options.ts +276 -18
- package/src/canvas/pending-composer.tsx +157 -0
- package/src/canvas/pin-stacking.test.ts +224 -0
- package/src/canvas/pin-stacking.ts +103 -0
- package/src/canvas/region-box.tsx +124 -0
- package/src/canvas/sidebar-filters.ts +4 -3
- package/src/canvas/state.ts +88 -39
- package/src/canvas/thread-pin.tsx +418 -0
- package/src/canvas/thread-preview.test.ts +91 -0
- package/src/canvas/thread-preview.tsx +294 -0
- package/src/canvas/thread-stack.tsx +242 -0
- package/src/canvas/thread-state.test.ts +260 -0
- package/src/canvas/thread-state.ts +171 -37
- package/src/canvas/thread-view.test.ts +72 -0
- package/src/canvas/thread-view.tsx +614 -0
- package/src/clustering/computeClusterTable.test.ts +24 -23
- package/src/clustering/computeClusterTable.ts +17 -8
- package/src/clustering/replay.test.ts +0 -7
- package/src/clustering/replay.ts +131 -32
- package/src/clustering/runtime.test.ts +267 -13
- package/src/clustering/runtime.ts +176 -19
- package/src/clustering/schedule.test.ts +0 -6
- package/src/clustering/screen-offsets.test.ts +171 -0
- package/src/clustering/types.ts +19 -9
- package/src/index.ts +91 -42
- package/src/ui/byline.tsx +21 -9
- package/src/ui/comment-card.tsx +20 -10
- package/src/ui/comment-composer.tsx +178 -56
- package/src/ui/comment-pin.tsx +13 -2
- package/src/ui/comment-thread.tsx +13 -6
- package/src/ui/comments-list.test.ts +31 -0
- package/src/ui/comments-list.tsx +82 -47
- package/src/ui/comments.css +577 -294
- package/src/ui/count-badge.tsx +9 -2
- package/src/ui/emoji-picker.test.ts +25 -0
- package/src/ui/emoji-picker.tsx +76 -0
- package/src/ui/empty-state.tsx +3 -3
- package/src/ui/format-time.test.ts +69 -0
- package/src/ui/format-time.ts +27 -2
- package/src/ui/icons.tsx +116 -0
- package/src/ui/reaction-picker.tsx +102 -0
- package/src/ui/reaction.tsx +125 -6
- package/src/ui/reactions.tsx +69 -10
- package/src/ui/reply-count.ts +16 -0
- package/src/ui/send-button.tsx +13 -3
- package/src/ui/visual-viewport.test.ts +36 -0
- package/src/ui/visual-viewport.ts +27 -0
- package/dist-cjs/canvas/region-options.js +0 -49
- package/dist-cjs/canvas/region-options.js.map +0 -7
- package/dist-cjs/ui/avatar.js.map +0 -7
- package/dist-cjs/ui/comment-mention.js +0 -42
- package/dist-cjs/ui/comment-mention.js.map +0 -7
- package/dist-cjs/ui/comment-text.js.map +0 -7
- package/dist-cjs/ui/mention-list.js +0 -70
- package/dist-cjs/ui/mention-list.js.map +0 -7
- package/dist-cjs/ui/mention-suggestion.js +0 -206
- package/dist-cjs/ui/mention-suggestion.js.map +0 -7
- package/dist-cjs/ui/mention.js.map +0 -7
- package/dist-cjs/ui/render-markdown.js +0 -63
- package/dist-cjs/ui/render-markdown.js.map +0 -7
- package/dist-esm/canvas/region-options.mjs +0 -29
- package/dist-esm/canvas/region-options.mjs.map +0 -7
- package/dist-esm/ui/avatar.mjs +0 -23
- package/dist-esm/ui/avatar.mjs.map +0 -7
- package/dist-esm/ui/comment-mention.mjs +0 -22
- package/dist-esm/ui/comment-mention.mjs.map +0 -7
- package/dist-esm/ui/comment-text.mjs +0 -9
- package/dist-esm/ui/comment-text.mjs.map +0 -7
- package/dist-esm/ui/mention-list.mjs +0 -50
- package/dist-esm/ui/mention-list.mjs.map +0 -7
- package/dist-esm/ui/mention-suggestion.mjs +0 -186
- package/dist-esm/ui/mention-suggestion.mjs.map +0 -7
- package/dist-esm/ui/mention.mjs +0 -11
- package/dist-esm/ui/mention.mjs.map +0 -7
- package/dist-esm/ui/render-markdown.mjs +0 -45
- package/dist-esm/ui/render-markdown.mjs.map +0 -7
- package/src/canvas/region-options.ts +0 -57
- package/src/ui/avatar.tsx +0 -31
- package/src/ui/comment-mention.ts +0 -47
- package/src/ui/comment-text.tsx +0 -12
- package/src/ui/mention-list.tsx +0 -106
- package/src/ui/mention-suggestion.test.ts +0 -18
- package/src/ui/mention-suggestion.tsx +0 -285
- package/src/ui/mention.tsx +0 -9
- package/src/ui/render-markdown.tsx +0 -72
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { deleteFromLocalStorage, getFromLocalStorage, setInLocalStorage } from "@tldraw/utils";
|
|
2
|
+
import { isCommentEmpty } from "../ui/comment-extensions.mjs";
|
|
3
|
+
const DRAFT_KEY_PREFIX = "tldraw-comment-draft";
|
|
4
|
+
const NEW_COMMENT_DRAFT = "new";
|
|
5
|
+
function replyDraftSlot(threadId) {
|
|
6
|
+
return `reply:${threadId}`;
|
|
7
|
+
}
|
|
8
|
+
function getCommentDraft(slot) {
|
|
9
|
+
const raw = getFromLocalStorage(`${DRAFT_KEY_PREFIX}:${slot}`);
|
|
10
|
+
if (!raw) return void 0;
|
|
11
|
+
try {
|
|
12
|
+
return JSON.parse(raw);
|
|
13
|
+
} catch {
|
|
14
|
+
return void 0;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function saveCommentDraft(slot, value) {
|
|
18
|
+
if (isCommentEmpty(value)) {
|
|
19
|
+
clearCommentDraft(slot);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
setInLocalStorage(`${DRAFT_KEY_PREFIX}:${slot}`, JSON.stringify(value));
|
|
23
|
+
}
|
|
24
|
+
function clearCommentDraft(slot) {
|
|
25
|
+
deleteFromLocalStorage(`${DRAFT_KEY_PREFIX}:${slot}`);
|
|
26
|
+
}
|
|
27
|
+
export {
|
|
28
|
+
NEW_COMMENT_DRAFT,
|
|
29
|
+
clearCommentDraft,
|
|
30
|
+
getCommentDraft,
|
|
31
|
+
replyDraftSlot,
|
|
32
|
+
saveCommentDraft
|
|
33
|
+
};
|
|
34
|
+
//# sourceMappingURL=comment-drafts.mjs.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/canvas/comment-drafts.ts"],
|
|
4
|
+
"sourcesContent": ["import { deleteFromLocalStorage, getFromLocalStorage, setInLocalStorage } from '@tldraw/utils'\nimport type { TLRichText } from 'tldraw'\nimport { isCommentEmpty } from '../ui/comment-extensions'\n\nconst DRAFT_KEY_PREFIX = 'tldraw-comment-draft'\n\n/** The single slot for an unsent new-comment (placement composer) draft. The anchor is re-picked\n * by clicking when the composer reopens, so only the text is worth keeping \u2014 and without a\n * universal document id in the SDK, one slot is the honest scope. */\nexport const NEW_COMMENT_DRAFT = 'new'\n\n/** The draft slot for an unsent reply on a thread. Thread record ids are globally unique and\n * stable across reloads, so replies never need a document key. @public */\nexport function replyDraftSlot(threadId: string): string {\n\treturn `reply:${threadId}`\n}\n\n/** Read a saved unsent draft, or undefined when there is none (or it fails to parse). @public */\nexport function getCommentDraft(slot: string): TLRichText | undefined {\n\tconst raw = getFromLocalStorage(`${DRAFT_KEY_PREFIX}:${slot}`)\n\tif (!raw) return undefined\n\ttry {\n\t\treturn JSON.parse(raw) as TLRichText\n\t} catch {\n\t\treturn undefined\n\t}\n}\n\n/**\n * Save an unsent draft so click-away and reload don't lose it \u2014 the flip side of not warning\n * before discard. Saving empty content clears the slot instead, so deleting your text and\n * closing doesn't resurrect an empty draft later.\n * @public\n */\nexport function saveCommentDraft(slot: string, value: TLRichText) {\n\tif (isCommentEmpty(value)) {\n\t\tclearCommentDraft(slot)\n\t\treturn\n\t}\n\tsetInLocalStorage(`${DRAFT_KEY_PREFIX}:${slot}`, JSON.stringify(value))\n}\n\n/** Remove a saved draft (after posting, or when its content was emptied). @public */\nexport function clearCommentDraft(slot: string) {\n\tdeleteFromLocalStorage(`${DRAFT_KEY_PREFIX}:${slot}`)\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,wBAAwB,qBAAqB,yBAAyB;AAE/E,SAAS,sBAAsB;AAE/B,MAAM,mBAAmB;AAKlB,MAAM,oBAAoB;AAI1B,SAAS,eAAe,UAA0B;AACxD,SAAO,SAAS,QAAQ;AACzB;AAGO,SAAS,gBAAgB,MAAsC;AACrE,QAAM,MAAM,oBAAoB,GAAG,gBAAgB,IAAI,IAAI,EAAE;AAC7D,MAAI,CAAC,IAAK,QAAO;AACjB,MAAI;AACH,WAAO,KAAK,MAAM,GAAG;AAAA,EACtB,QAAQ;AACP,WAAO;AAAA,EACR;AACD;AAQO,SAAS,iBAAiB,MAAc,OAAmB;AACjE,MAAI,eAAe,KAAK,GAAG;AAC1B,sBAAkB,IAAI;AACtB;AAAA,EACD;AACA,oBAAkB,GAAG,gBAAgB,IAAI,IAAI,IAAI,KAAK,UAAU,KAAK,CAAC;AACvE;AAGO,SAAS,kBAAkB,MAAc;AAC/C,yBAAuB,GAAG,gBAAgB,IAAI,IAAI,EAAE;AACrD;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { getCommentRecord, getLiveComments } from "./comment-store.mjs";
|
|
2
|
+
import { getCommentingOptions } from "./options.mjs";
|
|
3
|
+
import { openThreadId } from "./state.mjs";
|
|
4
|
+
const activeCommentMutations = /* @__PURE__ */ new WeakMap();
|
|
5
|
+
function historyModeFor(options, kind) {
|
|
6
|
+
switch (kind) {
|
|
7
|
+
case "delete":
|
|
8
|
+
return "ignore";
|
|
9
|
+
case "drag":
|
|
10
|
+
return options.dragHistory ?? options.history;
|
|
11
|
+
case "mutation":
|
|
12
|
+
return options.history;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
function commitCommentMutation(editor, fn, kind = "mutation") {
|
|
16
|
+
const history = historyModeFor(getCommentingOptions(editor), kind);
|
|
17
|
+
const enclosing = activeCommentMutations.get(editor);
|
|
18
|
+
if (enclosing && enclosing.history !== history) {
|
|
19
|
+
throw new Error(
|
|
20
|
+
`A comment mutation that records history as '${history}' can't run inside one recording it as '${enclosing.history}': one of the two modes would be silently discarded. Use the provided writer for constituent records, or run this operation after the enclosing one has committed.`
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
activeCommentMutations.set(editor, { history });
|
|
24
|
+
try {
|
|
25
|
+
let result;
|
|
26
|
+
editor.run(
|
|
27
|
+
() => {
|
|
28
|
+
let isWriterActive = true;
|
|
29
|
+
const assertWriterActive = () => {
|
|
30
|
+
if (!isWriterActive) {
|
|
31
|
+
throw new Error(
|
|
32
|
+
"A comment mutation writer cannot be used after its commit has finished."
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
try {
|
|
37
|
+
result = fn({
|
|
38
|
+
put: (records) => {
|
|
39
|
+
assertWriterActive();
|
|
40
|
+
editor.store.put(records);
|
|
41
|
+
},
|
|
42
|
+
remove: (ids) => {
|
|
43
|
+
assertWriterActive();
|
|
44
|
+
editor.store.remove(ids);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
} finally {
|
|
48
|
+
isWriterActive = false;
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
{ history }
|
|
52
|
+
);
|
|
53
|
+
return result;
|
|
54
|
+
} finally {
|
|
55
|
+
if (enclosing) {
|
|
56
|
+
activeCommentMutations.set(editor, enclosing);
|
|
57
|
+
} else {
|
|
58
|
+
activeCommentMutations.delete(editor);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function putCommentRecords(editor, records) {
|
|
63
|
+
commitCommentMutation(editor, ({ put }) => put(records));
|
|
64
|
+
}
|
|
65
|
+
function removeCommentRecords(editor, ids) {
|
|
66
|
+
commitCommentMutation(editor, ({ remove }) => remove(ids));
|
|
67
|
+
}
|
|
68
|
+
function readLatest(editor, record) {
|
|
69
|
+
const current = getCommentRecord(editor, record.id);
|
|
70
|
+
return current?.typeName === record.typeName ? current : void 0;
|
|
71
|
+
}
|
|
72
|
+
function editComment(editor, comment, body) {
|
|
73
|
+
commitCommentMutation(editor, ({ put }) => {
|
|
74
|
+
const current = readLatest(editor, comment);
|
|
75
|
+
if (!current) return;
|
|
76
|
+
put([{ ...current, body, editedAt: Date.now() }]);
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
function resolveThread(editor, thread, userId) {
|
|
80
|
+
commitCommentMutation(editor, ({ put }) => {
|
|
81
|
+
const current = readLatest(editor, thread);
|
|
82
|
+
if (!current) return;
|
|
83
|
+
put([{ ...current, resolved: { at: Date.now(), by: userId } }]);
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
function reopenThread(editor, thread) {
|
|
87
|
+
commitCommentMutation(editor, ({ put }) => {
|
|
88
|
+
const current = readLatest(editor, thread);
|
|
89
|
+
if (!current) return;
|
|
90
|
+
put([{ ...current, resolved: null }]);
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
function deleteComment(editor, comment) {
|
|
94
|
+
commitCommentMutation(
|
|
95
|
+
editor,
|
|
96
|
+
({ put }) => {
|
|
97
|
+
const current = readLatest(editor, comment);
|
|
98
|
+
if (!current || current.isDeleted) return;
|
|
99
|
+
const isLastInThread = getLiveComments(editor).filter((c) => c.threadId === current.threadId).length <= 1;
|
|
100
|
+
if (isLastInThread && openThreadId.get(editor) === current.threadId) {
|
|
101
|
+
openThreadId.set(editor, null);
|
|
102
|
+
}
|
|
103
|
+
put([{ ...current, isDeleted: true }]);
|
|
104
|
+
},
|
|
105
|
+
"delete"
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
function deleteThread(editor, thread) {
|
|
109
|
+
commitCommentMutation(
|
|
110
|
+
editor,
|
|
111
|
+
({ put }) => {
|
|
112
|
+
const current = readLatest(editor, thread);
|
|
113
|
+
if (!current) return;
|
|
114
|
+
if (openThreadId.get(editor) === current.id) {
|
|
115
|
+
openThreadId.set(editor, null);
|
|
116
|
+
}
|
|
117
|
+
put([{ ...current, isDeleted: true }]);
|
|
118
|
+
},
|
|
119
|
+
"delete"
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
export {
|
|
123
|
+
commitCommentMutation,
|
|
124
|
+
deleteComment,
|
|
125
|
+
deleteThread,
|
|
126
|
+
editComment,
|
|
127
|
+
putCommentRecords,
|
|
128
|
+
removeCommentRecords,
|
|
129
|
+
reopenThread,
|
|
130
|
+
resolveThread
|
|
131
|
+
};
|
|
132
|
+
//# sourceMappingURL=comment-mutations.mjs.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/canvas/comment-mutations.ts"],
|
|
4
|
+
"sourcesContent": ["import {\n\tEditor,\n\tTLComment,\n\tTLCommentId,\n\tTLCommentReactionId,\n\tTLCommentThread,\n\tTLCommentThreadId,\n\tTLHistoryBatchOptions,\n\tTLRecord,\n\tTLRichText,\n} from 'tldraw'\nimport { getCommentRecord, getLiveComments, type TLCommentRecord } from './comment-store'\nimport { getCommentingOptions, type CommentingOptions } from './options'\nimport { openThreadId } from './state'\n\n/**\n * Every write to a comment record, and the undo/redo policy governing them.\n *\n * The file layers bottom-up: {@link commitCommentMutation} resolves the history mode and hands its\n * callback a writer, {@link putCommentRecords} and {@link removeCommentRecords} are the typed\n * writes that run under it, and the verbs below are those writes plus the one rule each carries.\n * Every verb takes the record it acts on as the identity of what to change, not the value to write\n * back \u2014 see {@link readLatest}.\n *\n * Posting carries no such rule, so it isn't a verb here: build the records with\n * `createCommentThread`/`createComment` and write them with {@link putCommentRecords}.\n */\n\n/**\n * Which history policy a comment write follows:\n *\n * - `mutation` \u2014 {@link CommentingOptions.history}: posts, replies, edits, resolves.\n * - `drag` \u2014 {@link CommentingOptions.dragHistory}, falling back to `history`: pin and region\n * re-anchors, which are spatial edits a host may reasonably want undoable alongside a shape move.\n * - `delete` \u2014 always `'ignore'`, whatever the options say. A soft-delete flag is write-once\n * server-side, so an undo clearing it would be vetoed and rebased rather than restore anything.\n *\n * @internal\n */\nexport type CommentMutationKind = 'delete' | 'drag' | 'mutation'\n\ninterface CommentMutationWriter {\n\tput(records: TLCommentRecord[]): void\n\tremove(ids: (TLCommentId | TLCommentReactionId | TLCommentThreadId)[]): void\n}\n\nconst activeCommentMutations = new WeakMap<Editor, { history: TLHistoryBatchOptions['history'] }>()\n\n/** The undo/redo mode a write of the given kind runs under. See {@link CommentMutationKind}. */\nfunction historyModeFor(\n\toptions: CommentingOptions,\n\tkind: CommentMutationKind\n): TLHistoryBatchOptions['history'] {\n\tswitch (kind) {\n\t\tcase 'delete':\n\t\t\treturn 'ignore'\n\t\tcase 'drag':\n\t\t\treturn options.dragHistory ?? options.history\n\t\tcase 'mutation':\n\t\t\treturn options.history\n\t}\n}\n\n/**\n * Commit a comment mutation with the configured undo/redo behavior, so the\n * {@link CommentingOptions.history} option governs whether it lands on the undo stack. Defaults to\n * `'ignore'`. See {@link CommentMutationKind} for what each kind resolves to.\n *\n * `editor.run`'s history option isn't additive \u2014 a nested run overwrites the enclosing mode \u2014 so\n * constituent records go through the callback's writer instead of opening a commit of their own,\n * which would quietly make a `drag` write non-undoable.\n *\n * Commits that nest are only a problem when they resolve to different modes, and then neither is\n * the right one to keep, so it throws. Matching modes have to nest: a store side effect runs inside\n * the write that triggered it, with no \"after the commit\" to defer to. (A `store.listen` handler\n * normally flushes on a later frame, so its writes open a commit of their own, but a synchronous\n * flush \u2014 as under test \u2014 lands it inside too.)\n * @internal\n */\nexport function commitCommentMutation<T>(\n\teditor: Editor,\n\tfn: (writer: CommentMutationWriter) => T,\n\tkind: CommentMutationKind = 'mutation'\n): T {\n\tconst history = historyModeFor(getCommentingOptions(editor), kind)\n\tconst enclosing = activeCommentMutations.get(editor)\n\tif (enclosing && enclosing.history !== history) {\n\t\tthrow new Error(\n\t\t\t`A comment mutation that records history as '${history}' can't run inside one recording it as '${enclosing.history}': one of the two modes would be silently discarded. Use the provided writer for constituent records, or run this operation after the enclosing one has committed.`\n\t\t)\n\t}\n\n\tactiveCommentMutations.set(editor, { history })\n\ttry {\n\t\tlet result: T\n\t\teditor.run(\n\t\t\t() => {\n\t\t\t\tlet isWriterActive = true\n\t\t\t\tconst assertWriterActive = () => {\n\t\t\t\t\tif (!isWriterActive) {\n\t\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t\t'A comment mutation writer cannot be used after its commit has finished.'\n\t\t\t\t\t\t)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\ttry {\n\t\t\t\t\tresult = fn({\n\t\t\t\t\t\tput: (records) => {\n\t\t\t\t\t\t\tassertWriterActive()\n\t\t\t\t\t\t\teditor.store.put(records as unknown as TLRecord[])\n\t\t\t\t\t\t},\n\t\t\t\t\t\tremove: (ids) => {\n\t\t\t\t\t\t\tassertWriterActive()\n\t\t\t\t\t\t\teditor.store.remove(ids as unknown as TLRecord['id'][])\n\t\t\t\t\t\t},\n\t\t\t\t\t})\n\t\t\t\t} finally {\n\t\t\t\t\tisWriterActive = false\n\t\t\t\t}\n\t\t\t},\n\t\t\t{ history }\n\t\t)\n\t\treturn result!\n\t} finally {\n\t\tif (enclosing) {\n\t\t\tactiveCommentMutations.set(editor, enclosing)\n\t\t} else {\n\t\t\tactiveCommentMutations.delete(editor)\n\t\t}\n\t}\n}\n\n/**\n * Write comment records to the store, under the configured {@link CommentingOptions.history}\n * behavior. Defaults to `'ignore'`.\n *\n * Use it to seed or import threads, and to save an edit. To delete, prefer {@link deleteComment}\n * and {@link deleteThread}: comments are soft-deleted, and a synced server rejects the hard delete.\n *\n * @public\n */\nexport function putCommentRecords(editor: Editor, records: TLCommentRecord[]): void {\n\tcommitCommentMutation(editor, ({ put }) => put(records))\n}\n\n/**\n * Remove comment records from the store by id, under the configured\n * {@link CommentingOptions.history} behavior.\n *\n * This is a hard delete, which is rarely what you want: the built-in UI soft-deletes\n * ({@link deleteComment}, {@link deleteThread}) so the server can prune the records, and a server\n * enforcing per-record permissions vetoes a hard delete outright. Reach for this on a local,\n * unsynced store, or to drop a reaction (see {@link toggleCommentReaction}).\n *\n * @public\n */\nexport function removeCommentRecords(\n\teditor: Editor,\n\tids: (TLCommentId | TLCommentReactionId | TLCommentThreadId)[]\n): void {\n\tcommitCommentMutation(editor, ({ remove }) => remove(ids))\n}\n\n/**\n * The record as the store currently holds it, or `undefined` if it isn't there any more.\n *\n * A verb is handed a record, but that record is a snapshot and comment records move underneath it:\n * deleting a pinned shape converts the anchor to a point, reparenting rehomes the thread, a drag\n * re-anchors it. Writing the caller's snapshot back would revert those fields for everyone. `put`\n * is also an upsert, so a record a remote delete already removed would come back.\n *\n * So a verb reads what it's changing rather than trusting what it was given, and a record that's\n * gone is a no-op.\n */\nfunction readLatest<T extends TLComment | TLCommentThread>(\n\teditor: Editor,\n\trecord: T\n): T | undefined {\n\tconst current = getCommentRecord(editor, record.id)\n\t// Record ids carry their type, so a matching `typeName` means a record of exactly T.\n\treturn current?.typeName === record.typeName ? (current as T) : undefined\n}\n\n/**\n * Replace a comment's body and stamp it as edited, which renders the \"(edited)\" marker on its\n * byline. Editing is the author's to do by default ({@link CommentingOptions.canModifyComment}),\n * and a server enforcing per-record permissions rejects anyone else's. Widening one end without the\n * other leaves an edit that's offered and then rejected, so widen both.\n *\n * The body lands on the version the store currently holds, so a stale copy can't revert a later\n * change or re-create a removed comment \u2014 editing one of those does nothing.\n *\n * @example\n * ```ts\n * editComment(editor, comment, toRichText('Actually, make it dashed'))\n * ```\n *\n * @public\n */\nexport function editComment(editor: Editor, comment: TLComment, body: TLRichText): void {\n\tcommitCommentMutation(editor, ({ put }) => {\n\t\tconst current = readLatest(editor, comment)\n\t\tif (!current) return\n\t\tput([{ ...current, body, editedAt: Date.now() }])\n\t})\n}\n\n/**\n * Mark a thread resolved, stamping who resolved it and when. Resolved threads keep their pin (a\n * checked one) and are hidden from the sidebar until its \"show resolved\" filter is on.\n *\n * Only the resolution is written \u2014 the rest of the thread is read fresh, so a stale copy can't drag\n * a pin back. A no-op on a thread that's gone.\n *\n * @public\n */\nexport function resolveThread(editor: Editor, thread: TLCommentThread, userId: string): void {\n\tcommitCommentMutation(editor, ({ put }) => {\n\t\tconst current = readLatest(editor, thread)\n\t\tif (!current) return\n\t\tput([{ ...current, resolved: { at: Date.now(), by: userId } }])\n\t})\n}\n\n/**\n * Reopen a resolved thread, clearing the resolution. A no-op on a thread that isn't resolved, and\n * on one that's gone. Like {@link resolveThread}, it touches only the resolution.\n *\n * @public\n */\nexport function reopenThread(editor: Editor, thread: TLCommentThread): void {\n\tcommitCommentMutation(editor, ({ put }) => {\n\t\tconst current = readLatest(editor, thread)\n\t\tif (!current) return\n\t\tput([{ ...current, resolved: null }])\n\t})\n}\n\n/**\n * Delete a comment.\n *\n * This is a soft delete: it sets `isDeleted` rather than removing the record, and the server prunes\n * the comment and its reactions once the flag is persisted \u2014 so no client removes records it\n * doesn't own, and a server enforcing per-record permissions has a write it can check.\n *\n * Deleting is the author's to do by default; {@link CommentingOptions.canModifyComment} widens\n * that, as does its counterpart on the server.\n *\n * Never undoable, whatever {@link CommentingOptions.history} says: the flag is write-once\n * server-side, so an undo clearing it would be vetoed rather than bring the comment back.\n *\n * Deleting a thread's last comment closes it and leaves the thread record for the server to prune,\n * since the deleter may not be its creator. An already-deleted comment is a no-op.\n *\n * @public\n */\nexport function deleteComment(editor: Editor, comment: TLComment): void {\n\tcommitCommentMutation(\n\t\teditor,\n\t\t({ put }) => {\n\t\t\tconst current = readLatest(editor, comment)\n\t\t\t// Deleting twice is nothing to do rather than something to redo. The check below counts this\n\t\t\t// comment among the live ones, so it only reads as \"the last one\" while this delete takes it away.\n\t\t\tif (!current || current.isDeleted) return\n\t\t\tconst isLastInThread =\n\t\t\t\tgetLiveComments(editor).filter((c) => c.threadId === current.threadId).length <= 1\n\t\t\tif (isLastInThread && openThreadId.get(editor) === current.threadId) {\n\t\t\t\topenThreadId.set(editor, null)\n\t\t\t}\n\t\t\tput([{ ...current, isDeleted: true }])\n\t\t},\n\t\t'delete'\n\t)\n}\n\n/**\n * Delete a thread and, with it, the whole conversation.\n *\n * A soft delete on the same model as {@link deleteComment}: the server prunes the thread, its\n * comments, and their reactions once the flag is persisted. Deleting a thread is its creator's to\n * do by default ({@link CommentingOptions.canModifyComment}), and the write is never undoable.\n * Closes the thread if it's the open one; a pruned thread is a no-op.\n *\n * @public\n */\nexport function deleteThread(editor: Editor, thread: TLCommentThread): void {\n\tcommitCommentMutation(\n\t\teditor,\n\t\t({ put }) => {\n\t\t\tconst current = readLatest(editor, thread)\n\t\t\tif (!current) return\n\t\t\tif (openThreadId.get(editor) === current.id) {\n\t\t\t\topenThreadId.set(editor, null)\n\t\t\t}\n\t\t\tput([{ ...current, isDeleted: true }])\n\t\t},\n\t\t'delete'\n\t)\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useMemo } from "react";
|
|
3
|
+
import {
|
|
4
|
+
createCommentReaction,
|
|
5
|
+
createCommentReactionId,
|
|
6
|
+
useEditor,
|
|
7
|
+
useValue
|
|
8
|
+
} from "tldraw";
|
|
9
|
+
import { ReactionPicker } from "../ui/reaction-picker.mjs";
|
|
10
|
+
import { Reactions } from "../ui/reactions.mjs";
|
|
11
|
+
import { commitCommentMutation } from "./comment-mutations.mjs";
|
|
12
|
+
import { UNKNOWN_AUTHOR } from "./comment-render.mjs";
|
|
13
|
+
import { getCommentReactions } from "./comment-store.mjs";
|
|
14
|
+
import { getCommentingOptions, useCommentingOptions } from "./options.mjs";
|
|
15
|
+
function useCommentReactions(editor, commentId) {
|
|
16
|
+
return useValue(
|
|
17
|
+
"comment reactions",
|
|
18
|
+
() => getCommentReactions(editor).filter((reaction) => reaction.commentId === commentId).sort((a, b) => a.createdAt - b.createdAt),
|
|
19
|
+
[editor, commentId]
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
function summarizeReactions(reactions, currentUserId, resolveName) {
|
|
23
|
+
const groups = /* @__PURE__ */ new Map();
|
|
24
|
+
for (const reaction of reactions) {
|
|
25
|
+
const mine = currentUserId != null && reaction.userId === currentUserId;
|
|
26
|
+
const reactor = { name: resolveName?.(reaction.userId) ?? UNKNOWN_AUTHOR, you: mine };
|
|
27
|
+
const group = groups.get(reaction.emoji);
|
|
28
|
+
if (group) {
|
|
29
|
+
group.count++;
|
|
30
|
+
group.active ||= mine;
|
|
31
|
+
group.firstAt = Math.min(group.firstAt, reaction.createdAt);
|
|
32
|
+
group.reactors.push(reactor);
|
|
33
|
+
} else {
|
|
34
|
+
groups.set(reaction.emoji, {
|
|
35
|
+
count: 1,
|
|
36
|
+
active: mine,
|
|
37
|
+
firstAt: reaction.createdAt,
|
|
38
|
+
reactors: [reactor]
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return [...groups].sort(([, a], [, b]) => a.firstAt - b.firstAt).map(([emoji, group]) => ({
|
|
43
|
+
emoji,
|
|
44
|
+
count: group.count,
|
|
45
|
+
active: group.active,
|
|
46
|
+
reactors: group.reactors
|
|
47
|
+
}));
|
|
48
|
+
}
|
|
49
|
+
function toggleCommentReaction(editor, comment, userId, emoji, now = Date.now()) {
|
|
50
|
+
const { allowMultipleReactions, isAllowedReaction } = getCommentingOptions(editor);
|
|
51
|
+
const targetId = createCommentReactionId(comment.id, userId, emoji);
|
|
52
|
+
const mine = getCommentReactions(editor).filter(
|
|
53
|
+
(reaction) => reaction.commentId === comment.id && reaction.userId === userId
|
|
54
|
+
);
|
|
55
|
+
const removing = mine.some((reaction) => reaction.id === targetId);
|
|
56
|
+
if (!removing && !isAllowedReaction(emoji)) return;
|
|
57
|
+
commitCommentMutation(editor, ({ put, remove }) => {
|
|
58
|
+
if (removing) {
|
|
59
|
+
remove([targetId]);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (!allowMultipleReactions && mine.length > 0) {
|
|
63
|
+
remove(mine.map((reaction) => reaction.id));
|
|
64
|
+
}
|
|
65
|
+
put([
|
|
66
|
+
createCommentReaction({
|
|
67
|
+
commentId: comment.id,
|
|
68
|
+
threadId: comment.threadId,
|
|
69
|
+
pageId: comment.pageId,
|
|
70
|
+
userId,
|
|
71
|
+
emoji,
|
|
72
|
+
now
|
|
73
|
+
})
|
|
74
|
+
]);
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
function useReactionRenderer() {
|
|
78
|
+
const { components } = useCommentingOptions();
|
|
79
|
+
const ReactionContent = components.ReactionContent;
|
|
80
|
+
return useMemo(
|
|
81
|
+
() => ReactionContent ? (token) => /* @__PURE__ */ jsx(ReactionContent, { token }) : void 0,
|
|
82
|
+
[ReactionContent]
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
function CommentReactions({ comment, currentUserId, resolveName }) {
|
|
86
|
+
const editor = useEditor();
|
|
87
|
+
const renderReaction = useReactionRenderer();
|
|
88
|
+
const { components } = useCommentingOptions();
|
|
89
|
+
const reactions = useCommentReactions(editor, comment.id);
|
|
90
|
+
const summaries = useMemo(
|
|
91
|
+
() => summarizeReactions(reactions, currentUserId, resolveName),
|
|
92
|
+
[reactions, currentUserId, resolveName]
|
|
93
|
+
);
|
|
94
|
+
const anyMenuOpen = useValue("any menu open", () => editor.menus.getOpenMenus().length > 0, [
|
|
95
|
+
editor
|
|
96
|
+
]);
|
|
97
|
+
return /* @__PURE__ */ jsx(
|
|
98
|
+
Reactions,
|
|
99
|
+
{
|
|
100
|
+
reactions: summaries,
|
|
101
|
+
canReact: currentUserId != null,
|
|
102
|
+
enableHoverList: !anyMenuOpen,
|
|
103
|
+
renderReaction,
|
|
104
|
+
ReactionTooltip: components.ReactionTooltip,
|
|
105
|
+
onToggle: (value) => {
|
|
106
|
+
if (currentUserId == null) return;
|
|
107
|
+
toggleCommentReaction(editor, comment, currentUserId, value);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
function CommentReactionPicker({
|
|
113
|
+
comment,
|
|
114
|
+
currentUserId,
|
|
115
|
+
emoji
|
|
116
|
+
}) {
|
|
117
|
+
const editor = useEditor();
|
|
118
|
+
const { components } = useCommentingOptions();
|
|
119
|
+
const renderReaction = useReactionRenderer();
|
|
120
|
+
const reactions = useCommentReactions(editor, comment.id);
|
|
121
|
+
const selected = useMemo(
|
|
122
|
+
() => reactions.filter((reaction) => currentUserId != null && reaction.userId === currentUserId).map((reaction) => reaction.emoji),
|
|
123
|
+
[reactions, currentUserId]
|
|
124
|
+
);
|
|
125
|
+
if (currentUserId == null) return null;
|
|
126
|
+
return /* @__PURE__ */ jsx(
|
|
127
|
+
ReactionPicker,
|
|
128
|
+
{
|
|
129
|
+
emoji,
|
|
130
|
+
selected,
|
|
131
|
+
renderReaction,
|
|
132
|
+
palette: components.ReactionPalette,
|
|
133
|
+
menuId: `comment-reactions-${comment.id}`,
|
|
134
|
+
onSelect: (value) => toggleCommentReaction(editor, comment, currentUserId, value)
|
|
135
|
+
}
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
export {
|
|
139
|
+
CommentReactionPicker,
|
|
140
|
+
CommentReactions,
|
|
141
|
+
summarizeReactions,
|
|
142
|
+
toggleCommentReaction,
|
|
143
|
+
useCommentReactions
|
|
144
|
+
};
|
|
145
|
+
//# sourceMappingURL=comment-reactions.mjs.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/canvas/comment-reactions.tsx"],
|
|
4
|
+
"sourcesContent": ["import { useMemo } from 'react'\nimport {\n\tEditor,\n\tTLComment,\n\tTLCommentReaction,\n\tcreateCommentReaction,\n\tcreateCommentReactionId,\n\tuseEditor,\n\tuseValue,\n} from 'tldraw'\nimport { RenderReaction } from '../ui/reaction'\nimport { ReactionPicker } from '../ui/reaction-picker'\nimport { Reactions, ReactionSummary } from '../ui/reactions'\nimport { commitCommentMutation } from './comment-mutations'\nimport { UNKNOWN_AUTHOR } from './comment-render'\nimport { getCommentReactions } from './comment-store'\nimport { getCommentingOptions, useCommentingOptions } from './options'\n\n/**\n * One comment's reactions, oldest first, reactively.\n *\n * @public\n */\nexport function useCommentReactions(\n\teditor: Editor,\n\tcommentId: TLComment['id']\n): TLCommentReaction[] {\n\treturn useValue(\n\t\t'comment reactions',\n\t\t() =>\n\t\t\tgetCommentReactions(editor)\n\t\t\t\t.filter((reaction) => reaction.commentId === commentId)\n\t\t\t\t.sort((a, b) => a.createdAt - b.createdAt),\n\t\t[editor, commentId]\n\t)\n}\n\n/**\n * The reaction fields {@link summarizeReactions} needs \u2014 a structural subset of\n * {@link tldraw#TLCommentReaction}, so tallies can also be built from rows synced outside the\n * editor store.\n *\n * @public\n */\nexport interface ReactionSummaryInput {\n\tuserId: string\n\temoji: string\n\tcreatedAt: number\n}\n\n/**\n * Tally a comment's reactions into an entry per emoji, ordered by when that emoji was first used so\n * the row stays stable as later reactions arrive. `active` marks the current user's emoji and\n * `reactors` lists who reacted, in reaction order. `resolveName` names each reactor; an id it can't\n * name falls back to a generic \"Someone\", never the raw user id.\n *\n * @public\n */\nexport function summarizeReactions(\n\treactions: readonly ReactionSummaryInput[],\n\tcurrentUserId?: string | null,\n\tresolveName?: (userId: string) => string | undefined\n): ReactionSummary[] {\n\tconst groups = new Map<\n\t\tstring,\n\t\t{ count: number; active: boolean; firstAt: number; reactors: ReactionSummary['reactors'] }\n\t>()\n\tfor (const reaction of reactions) {\n\t\tconst mine = currentUserId != null && reaction.userId === currentUserId\n\t\t// Fall back to a generic name, never the raw user id, when the id can't be resolved.\n\t\tconst reactor = { name: resolveName?.(reaction.userId) ?? UNKNOWN_AUTHOR, you: mine }\n\t\tconst group = groups.get(reaction.emoji)\n\t\tif (group) {\n\t\t\tgroup.count++\n\t\t\tgroup.active ||= mine\n\t\t\tgroup.firstAt = Math.min(group.firstAt, reaction.createdAt)\n\t\t\tgroup.reactors.push(reactor)\n\t\t} else {\n\t\t\tgroups.set(reaction.emoji, {\n\t\t\t\tcount: 1,\n\t\t\t\tactive: mine,\n\t\t\t\tfirstAt: reaction.createdAt,\n\t\t\t\treactors: [reactor],\n\t\t\t})\n\t\t}\n\t}\n\treturn [...groups]\n\t\t.sort(([, a], [, b]) => a.firstAt - b.firstAt)\n\t\t.map(([emoji, group]) => ({\n\t\t\temoji,\n\t\t\tcount: group.count,\n\t\t\tactive: group.active,\n\t\t\treactors: group.reactors,\n\t\t}))\n}\n\n/**\n * Toggle one user's reaction with a given emoji on a comment.\n *\n * Each reaction is its own record keyed by (comment, user, emoji), so this only touches that user's\n * own records and two people reacting at once never conflict. Behaviour depends on\n * `allowMultipleReactions`:\n *\n * - **multiple** (default): the emoji toggles independently, leaving other reactions alone.\n * - **single**: a new emoji replaces the user's existing reaction; the same one removes it.\n *\n * @public\n */\nexport function toggleCommentReaction(\n\teditor: Editor,\n\tcomment: TLComment,\n\tuserId: string,\n\temoji: string,\n\tnow = Date.now()\n): void {\n\tconst { allowMultipleReactions, isAllowedReaction } = getCommentingOptions(editor)\n\tconst targetId = createCommentReactionId(comment.id, userId, emoji)\n\tconst mine = getCommentReactions(editor).filter(\n\t\t(reaction) => reaction.commentId === comment.id && reaction.userId === userId\n\t)\n\tconst removing = mine.some((reaction) => reaction.id === targetId)\n\t// Only a token the configured palette allows may be added. Removals always go through \u2014 if a\n\t// reaction somehow carries an off-palette token, the user must still be able to clear it.\n\tif (!removing && !isAllowedReaction(emoji)) return\n\tcommitCommentMutation(editor, ({ put, remove }) => {\n\t\tif (removing) {\n\t\t\tremove([targetId])\n\t\t\treturn\n\t\t}\n\t\t// Single-select: a new emoji replaces the user's existing reaction(s) on this comment.\n\t\tif (!allowMultipleReactions && mine.length > 0) {\n\t\t\tremove(mine.map((reaction) => reaction.id))\n\t\t}\n\t\tput([\n\t\t\tcreateCommentReaction({\n\t\t\t\tcommentId: comment.id,\n\t\t\t\tthreadId: comment.threadId,\n\t\t\t\tpageId: comment.pageId,\n\t\t\t\tuserId,\n\t\t\t\temoji,\n\t\t\t\tnow,\n\t\t\t}),\n\t\t])\n\t})\n}\n\n/** @public */\nexport interface CommentReactionsProps {\n\tcomment: TLComment\n\t/** The reacting user. Null/omitted gives a read-only row (signed out): counts show, but the\n\t * pills don't toggle. */\n\tcurrentUserId?: string | null\n\t/** Names a reactor id for the hover list. Ids it can't name fall back to a generic \"Someone\". */\n\tresolveName?(userId: string): string | undefined\n}\n\n/**\n * Adapts the `ReactionContent` override into a `renderReaction` function for the presentational\n * components. Undefined when no override is set, so they fall back to drawing the token string.\n */\nfunction useReactionRenderer(): RenderReaction | undefined {\n\tconst { components } = useCommentingOptions()\n\tconst ReactionContent = components.ReactionContent\n\treturn useMemo(\n\t\t() => (ReactionContent ? (token: string) => <ReactionContent token={token} /> : undefined),\n\t\t[ReactionContent]\n\t)\n}\n\n/**\n * The tallied reaction row under one comment. Pair with `CommentReactionPicker`, which is what\n * adds a reaction.\n * @public @react\n */\nexport function CommentReactions({ comment, currentUserId, resolveName }: CommentReactionsProps) {\n\tconst editor = useEditor()\n\tconst renderReaction = useReactionRenderer()\n\tconst { components } = useCommentingOptions()\n\tconst reactions = useCommentReactions(editor, comment.id)\n\tconst summaries = useMemo(\n\t\t() => summarizeReactions(reactions, currentUserId, resolveName),\n\t\t[reactions, currentUserId, resolveName]\n\t)\n\t// Suppress the hover list while any menu is open (the reaction picker, an overflow menu\u2026) so it\n\t// doesn't compete with the menu the user is actually working in. Edit mode already hides the\n\t// pills entirely (the card becomes a composer), so it needs no special case here.\n\tconst anyMenuOpen = useValue('any menu open', () => editor.menus.getOpenMenus().length > 0, [\n\t\teditor,\n\t])\n\treturn (\n\t\t<Reactions\n\t\t\treactions={summaries}\n\t\t\tcanReact={currentUserId != null}\n\t\t\tenableHoverList={!anyMenuOpen}\n\t\t\trenderReaction={renderReaction}\n\t\t\tReactionTooltip={components.ReactionTooltip}\n\t\t\tonToggle={(value) => {\n\t\t\t\tif (currentUserId == null) return\n\t\t\t\ttoggleCommentReaction(editor, comment, currentUserId, value)\n\t\t\t}}\n\t\t/>\n\t)\n}\n\n/** @public */\nexport interface CommentReactionPickerProps {\n\tcomment: TLComment\n\t/** The reacting user. Null/omitted renders nothing \u2014 there's nobody to react as. */\n\tcurrentUserId?: string | null\n\t/** The emoji the picker offers. Defaults to `DEFAULT_REACTION_EMOJI`. */\n\temoji?: string[]\n}\n\n/**\n * The add-reaction button for one comment. Belongs with the comment card's hover actions rather\n * than in the reaction row, so opening it doesn't chase the row as reactions are added.\n * @public @react\n */\nexport function CommentReactionPicker({\n\tcomment,\n\tcurrentUserId,\n\temoji,\n}: CommentReactionPickerProps) {\n\tconst editor = useEditor()\n\tconst { components } = useCommentingOptions()\n\tconst renderReaction = useReactionRenderer()\n\tconst reactions = useCommentReactions(editor, comment.id)\n\tconst selected = useMemo(\n\t\t() =>\n\t\t\treactions\n\t\t\t\t.filter((reaction) => currentUserId != null && reaction.userId === currentUserId)\n\t\t\t\t.map((reaction) => reaction.emoji),\n\t\t[reactions, currentUserId]\n\t)\n\tif (currentUserId == null) return null\n\treturn (\n\t\t<ReactionPicker\n\t\t\temoji={emoji}\n\t\t\tselected={selected}\n\t\t\trenderReaction={renderReaction}\n\t\t\tpalette={components.ReactionPalette}\n\t\t\tmenuId={`comment-reactions-${comment.id}`}\n\t\t\tonSelect={(value) => toggleCommentReaction(editor, comment, currentUserId, value)}\n\t\t/>\n\t)\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { generateText } from "@tiptap/core";
|
|
2
|
+
import { createMentionExtension } from "@tldraw/mentions";
|
|
3
|
+
import { renderHtmlFromRichTextWithExtensions } from "tldraw";
|
|
2
4
|
import { commentTipTapExtensions, isCommentEmpty } from "../ui/comment-extensions.mjs";
|
|
3
|
-
import { commentMention } from "../ui/comment-mention.mjs";
|
|
4
5
|
const UNKNOWN_AUTHOR = "Someone";
|
|
6
|
+
const UNKNOWN_COMMENT_AUTHOR = Object.freeze({ name: UNKNOWN_AUTHOR });
|
|
5
7
|
function demoteHeadings(node) {
|
|
6
8
|
const content = Array.isArray(node.content) ? node.content.map(demoteHeadings) : node.content;
|
|
7
9
|
if (node.type === "heading") {
|
|
@@ -21,10 +23,10 @@ function renderCommentHtml(richText, resolveName) {
|
|
|
21
23
|
const cached = htmlCache.get(richText);
|
|
22
24
|
if (cached !== void 0) return cached;
|
|
23
25
|
}
|
|
24
|
-
const extensions = mentions ? [...commentTipTapExtensions,
|
|
25
|
-
const html =
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
const extensions = mentions ? [...commentTipTapExtensions, createMentionExtension({ resolveName })] : commentTipTapExtensions;
|
|
27
|
+
const html = renderHtmlFromRichTextWithExtensions(
|
|
28
|
+
demoteHeadings(richText),
|
|
29
|
+
extensions
|
|
28
30
|
);
|
|
29
31
|
if (!mentions) htmlCache.set(richText, html);
|
|
30
32
|
return html;
|
|
@@ -37,7 +39,7 @@ function renderCommentPlaintext(richText, resolveName) {
|
|
|
37
39
|
const cached = textCache.get(richText);
|
|
38
40
|
if (cached !== void 0) return cached;
|
|
39
41
|
}
|
|
40
|
-
const extensions = mentions ? [...commentTipTapExtensions,
|
|
42
|
+
const extensions = mentions ? [...commentTipTapExtensions, createMentionExtension({ resolveName })] : commentTipTapExtensions;
|
|
41
43
|
const text = generateText(demoteHeadings(richText), extensions, {
|
|
42
44
|
blockSeparator: "\n"
|
|
43
45
|
});
|
|
@@ -46,6 +48,7 @@ function renderCommentPlaintext(richText, resolveName) {
|
|
|
46
48
|
}
|
|
47
49
|
export {
|
|
48
50
|
UNKNOWN_AUTHOR,
|
|
51
|
+
UNKNOWN_COMMENT_AUTHOR,
|
|
49
52
|
renderCommentHtml,
|
|
50
53
|
renderCommentPlaintext
|
|
51
54
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/canvas/comment-render.ts"],
|
|
4
|
-
"sourcesContent": ["import {
|
|
5
|
-
"mappings": "AAAA,SAAS,
|
|
4
|
+
"sourcesContent": ["import { generateText, JSONContent } from '@tiptap/core'\nimport { type CommentAuthor, createMentionExtension } from '@tldraw/mentions'\nimport { renderHtmlFromRichTextWithExtensions, TLRichText } from 'tldraw'\nimport { commentTipTapExtensions, isCommentEmpty } from '../ui/comment-extensions'\n\n/**\n * The author name shown in a byline when no source can name an id (e.g. a deleted account, or a\n * member with no comment and no live presence). The toolkit's one generic default, applied where a\n * byline needs a name; hosts pre-empt it by resolving the id from `resolveAuthor`. Not a\n * translation key \u2014 a single English literal.\n */\nexport const UNKNOWN_AUTHOR = 'Someone'\n\n/** The `CommentAuthor` fallback for an unresolvable id \u2014 just the name, no color or image.\n * Frozen: the one shared instance is handed to host render slots. */\nexport const UNKNOWN_COMMENT_AUTHOR: CommentAuthor = Object.freeze({ name: UNKNOWN_AUTHOR })\n\n/**\n * The comment extension set has no heading node, so a body that nonetheless contains one (e.g. a\n * record created programmatically or synced from a client with a fuller set) would make TipTap\n * throw on an unknown node type. Demote any heading to a paragraph first, so headings can never\n * render as headings and rendering never crashes.\n */\nfunction demoteHeadings(node: JSONContent): JSONContent {\n\tconst content = Array.isArray(node.content) ? node.content.map(demoteHeadings) : node.content\n\tif (node.type === 'heading') {\n\t\tconst { attrs: _attrs, ...rest } = node\n\t\treturn { ...rest, type: 'paragraph', content }\n\t}\n\treturn content === node.content ? node : { ...node, content }\n}\n\n/**\n * Whether a body contains a mention node. Mention text is resolved from a member id at render time,\n * so a body with one can't be cached by identity alone (the same body renders differently as names\n * change) \u2014 those bodies skip the cache and re-render each call.\n */\nfunction hasMention(node: JSONContent): boolean {\n\tif (node.type === 'mention') return true\n\treturn Array.isArray(node.content) ? node.content.some(hasMention) : false\n}\n\nconst htmlCache = new WeakMap<TLRichText, string>()\n\n/**\n * Render a comment body to HTML through the limited comment extension set (no headings), so a body\n * always renders with comment formatting regardless of the host editor's rich-text config. Renders\n * through tldraw's shared helper, so it gets the same empty-paragraph fix that keeps blank lines\n * from collapsing. `resolveName` maps a member id to its current name for any @mentions.\n */\nexport function renderCommentHtml(\n\trichText: TLRichText,\n\tresolveName?: (id: string) => string | undefined\n): string {\n\tconst mentions = hasMention(richText as JSONContent)\n\tif (!mentions) {\n\t\tconst cached = htmlCache.get(richText)\n\t\tif (cached !== undefined) return cached\n\t}\n\tconst extensions = mentions\n\t\t? [...commentTipTapExtensions, createMentionExtension({ resolveName })]\n\t\t: commentTipTapExtensions\n\tconst html = renderHtmlFromRichTextWithExtensions(\n\t\tdemoteHeadings(richText as JSONContent) as TLRichText,\n\t\textensions\n\t)\n\tif (!mentions) htmlCache.set(richText, html)\n\treturn html\n}\n\nconst textCache = new WeakMap<TLRichText, string>()\n\n/**\n * Flatten a comment body to plaintext through the limited comment extension set \u2014 paragraphs and\n * list items separated by newlines. Used for previews (e.g. the sidebar) where formatting is dropped.\n * `resolveName` maps a member id to its current name for any @mentions.\n */\nexport function renderCommentPlaintext(\n\trichText: TLRichText,\n\tresolveName?: (id: string) => string | undefined\n): string {\n\tif (isCommentEmpty(richText)) return ''\n\tconst mentions = hasMention(richText as JSONContent)\n\tif (!mentions) {\n\t\tconst cached = textCache.get(richText)\n\t\tif (cached !== undefined) return cached\n\t}\n\tconst extensions = mentions\n\t\t? [...commentTipTapExtensions, createMentionExtension({ resolveName })]\n\t\t: commentTipTapExtensions\n\tconst text = generateText(demoteHeadings(richText as JSONContent), extensions, {\n\t\tblockSeparator: '\\n',\n\t})\n\tif (!mentions) textCache.set(richText, text)\n\treturn text\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,oBAAiC;AAC1C,SAA6B,8BAA8B;AAC3D,SAAS,4CAAwD;AACjE,SAAS,yBAAyB,sBAAsB;AAQjD,MAAM,iBAAiB;AAIvB,MAAM,yBAAwC,OAAO,OAAO,EAAE,MAAM,eAAe,CAAC;AAQ3F,SAAS,eAAe,MAAgC;AACvD,QAAM,UAAU,MAAM,QAAQ,KAAK,OAAO,IAAI,KAAK,QAAQ,IAAI,cAAc,IAAI,KAAK;AACtF,MAAI,KAAK,SAAS,WAAW;AAC5B,UAAM,EAAE,OAAO,QAAQ,GAAG,KAAK,IAAI;AACnC,WAAO,EAAE,GAAG,MAAM,MAAM,aAAa,QAAQ;AAAA,EAC9C;AACA,SAAO,YAAY,KAAK,UAAU,OAAO,EAAE,GAAG,MAAM,QAAQ;AAC7D;AAOA,SAAS,WAAW,MAA4B;AAC/C,MAAI,KAAK,SAAS,UAAW,QAAO;AACpC,SAAO,MAAM,QAAQ,KAAK,OAAO,IAAI,KAAK,QAAQ,KAAK,UAAU,IAAI;AACtE;AAEA,MAAM,YAAY,oBAAI,QAA4B;AAQ3C,SAAS,kBACf,UACA,aACS;AACT,QAAM,WAAW,WAAW,QAAuB;AACnD,MAAI,CAAC,UAAU;AACd,UAAM,SAAS,UAAU,IAAI,QAAQ;AACrC,QAAI,WAAW,OAAW,QAAO;AAAA,EAClC;AACA,QAAM,aAAa,WAChB,CAAC,GAAG,yBAAyB,uBAAuB,EAAE,YAAY,CAAC,CAAC,IACpE;AACH,QAAM,OAAO;AAAA,IACZ,eAAe,QAAuB;AAAA,IACtC;AAAA,EACD;AACA,MAAI,CAAC,SAAU,WAAU,IAAI,UAAU,IAAI;AAC3C,SAAO;AACR;AAEA,MAAM,YAAY,oBAAI,QAA4B;AAO3C,SAAS,uBACf,UACA,aACS;AACT,MAAI,eAAe,QAAQ,EAAG,QAAO;AACrC,QAAM,WAAW,WAAW,QAAuB;AACnD,MAAI,CAAC,UAAU;AACd,UAAM,SAAS,UAAU,IAAI,QAAQ;AACrC,QAAI,WAAW,OAAW,QAAO;AAAA,EAClC;AACA,QAAM,aAAa,WAChB,CAAC,GAAG,yBAAyB,uBAAuB,EAAE,YAAY,CAAC,CAAC,IACpE;AACH,QAAM,OAAO,aAAa,eAAe,QAAuB,GAAG,YAAY;AAAA,IAC9E,gBAAgB;AAAA,EACjB,CAAC;AACD,MAAI,CAAC,SAAU,WAAU,IAAI,UAAU,IAAI;AAC3C,SAAO;AACR;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,13 +1,9 @@
|
|
|
1
|
-
function putCommentRecords(editor, records) {
|
|
2
|
-
editor.store.put(records);
|
|
3
|
-
}
|
|
4
|
-
function removeCommentRecords(editor, ids) {
|
|
5
|
-
editor.store.remove(ids);
|
|
6
|
-
}
|
|
7
1
|
function getCommentRecord(editor, id) {
|
|
8
2
|
const record = editor.store.get(id);
|
|
9
3
|
if (!record) return void 0;
|
|
10
|
-
if (record.typeName === "comment" || record.typeName === "comment-thread"
|
|
4
|
+
if (record.typeName === "comment" || record.typeName === "comment-thread" || record.typeName === "comment-reaction") {
|
|
5
|
+
return record;
|
|
6
|
+
}
|
|
11
7
|
return void 0;
|
|
12
8
|
}
|
|
13
9
|
function getCommentThreads(editor) {
|
|
@@ -18,11 +14,25 @@ function getComments(editor) {
|
|
|
18
14
|
const typeName = "comment";
|
|
19
15
|
return editor.store.query.records(typeName).get();
|
|
20
16
|
}
|
|
17
|
+
function getLiveComments(editor) {
|
|
18
|
+
return getComments(editor).filter((comment) => !comment.isDeleted);
|
|
19
|
+
}
|
|
20
|
+
function getLiveCommentThreads(editor) {
|
|
21
|
+
const threadIdsWithComments = new Set(getLiveComments(editor).map((comment) => comment.threadId));
|
|
22
|
+
return getCommentThreads(editor).filter(
|
|
23
|
+
(thread) => !thread.isDeleted && threadIdsWithComments.has(thread.id)
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
function getCommentReactions(editor) {
|
|
27
|
+
const typeName = "comment-reaction";
|
|
28
|
+
return editor.store.query.records(typeName).get();
|
|
29
|
+
}
|
|
21
30
|
export {
|
|
31
|
+
getCommentReactions,
|
|
22
32
|
getCommentRecord,
|
|
23
33
|
getCommentThreads,
|
|
24
34
|
getComments,
|
|
25
|
-
|
|
26
|
-
|
|
35
|
+
getLiveCommentThreads,
|
|
36
|
+
getLiveComments
|
|
27
37
|
};
|
|
28
38
|
//# sourceMappingURL=comment-store.mjs.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/canvas/comment-store.ts"],
|
|
4
|
-
"sourcesContent": ["import {
|
|
5
|
-
"mappings": "
|
|
4
|
+
"sourcesContent": ["import { Editor, TLComment, TLCommentReaction, TLCommentThread, TLRecord } from 'tldraw'\n\n/**\n * Typed reads of comment records on the editor store.\n *\n * Comment records live on the editor's local store so the canvas can render them reactively, but\n * they're opt-in and aren't part of the `TLRecord` union \u2014 so `editor.store` is statically typed\n * `Store<TLRecord>` and every access has to reinterpret the type. These helpers own that\n * reinterpretation behind one boundary and keep call sites typed.\n *\n * Writes do the same, but also answer to the undo/redo policy, so they live in\n * `comment-mutations.ts`.\n */\n\n/**\n * A record that lives in a comment thread: the thread itself, one of its messages, or a reaction\n * to one of those messages.\n * @public\n */\nexport type TLCommentRecord = TLComment | TLCommentThread | TLCommentReaction\n\n/** Read one comment record by id, or `undefined` if the id isn't a present comment record. @public */\nexport function getCommentRecord(editor: Editor, id: string): TLCommentRecord | undefined {\n\tconst record = editor.store.get(id as TLRecord['id']) as unknown as TLCommentRecord | undefined\n\tif (!record) return undefined\n\tif (\n\t\trecord.typeName === 'comment' ||\n\t\trecord.typeName === 'comment-thread' ||\n\t\trecord.typeName === 'comment-reaction'\n\t) {\n\t\treturn record\n\t}\n\treturn undefined\n}\n\n/**\n * Every comment thread in the store, **including soft-deleted and emptied ones** awaiting the\n * server's prune, which nothing renders. For the set the UI shows, use {@link getLiveCommentThreads}.\n *\n * Non-reactive; wrap in `useValue`, or use `useCommentThreads`, to react.\n * @public\n */\nexport function getCommentThreads(editor: Editor): TLCommentThread[] {\n\tconst typeName = 'comment-thread' as TLRecord['typeName']\n\treturn editor.store.query.records(typeName).get() as unknown as TLCommentThread[]\n}\n\n/**\n * Every comment in the store, **including soft-deleted ones** awaiting the server's prune. For the\n * set the UI shows, use {@link getLiveComments}.\n *\n * Non-reactive; wrap in `useValue`, or use `useComments`, to react.\n * @public\n */\nexport function getComments(editor: Editor): TLComment[] {\n\tconst typeName = 'comment' as TLRecord['typeName']\n\treturn editor.store.query.records(typeName).get() as unknown as TLComment[]\n}\n\n/**\n * The comments that should render: not soft-deleted. A deleted record lingers in the store until\n * the server prunes it, so build counts and lists from this rather than {@link getComments}.\n *\n * Non-reactive; the reactive equivalent is `useComments` (which also sorts oldest first).\n * @public\n */\nexport function getLiveComments(editor: Editor): TLComment[] {\n\treturn getComments(editor).filter((comment) => !comment.isDeleted)\n}\n\n/**\n * The comment threads that should render (pins, sidebar): not soft-deleted, and still holding at\n * least one live comment. A thread emptied by its last comment's delete lingers with no surface\n * until the server's prune lands.\n *\n * Non-reactive; the reactive equivalent is `useCommentThreads`.\n * @public\n */\nexport function getLiveCommentThreads(editor: Editor): TLCommentThread[] {\n\tconst threadIdsWithComments = new Set(getLiveComments(editor).map((comment) => comment.threadId))\n\treturn getCommentThreads(editor).filter(\n\t\t(thread) => !thread.isDeleted && threadIdsWithComments.has(thread.id)\n\t)\n}\n\n/** All comment reactions currently in the store (non-reactive; wrap in `useValue` to react). @public */\nexport function getCommentReactions(editor: Editor): TLCommentReaction[] {\n\tconst typeName = 'comment-reaction' as TLRecord['typeName']\n\treturn editor.store.query.records(typeName).get() as unknown as TLCommentReaction[]\n}\n"],
|
|
5
|
+
"mappings": "AAsBO,SAAS,iBAAiB,QAAgB,IAAyC;AACzF,QAAM,SAAS,OAAO,MAAM,IAAI,EAAoB;AACpD,MAAI,CAAC,OAAQ,QAAO;AACpB,MACC,OAAO,aAAa,aACpB,OAAO,aAAa,oBACpB,OAAO,aAAa,oBACnB;AACD,WAAO;AAAA,EACR;AACA,SAAO;AACR;AASO,SAAS,kBAAkB,QAAmC;AACpE,QAAM,WAAW;AACjB,SAAO,OAAO,MAAM,MAAM,QAAQ,QAAQ,EAAE,IAAI;AACjD;AASO,SAAS,YAAY,QAA6B;AACxD,QAAM,WAAW;AACjB,SAAO,OAAO,MAAM,MAAM,QAAQ,QAAQ,EAAE,IAAI;AACjD;AASO,SAAS,gBAAgB,QAA6B;AAC5D,SAAO,YAAY,MAAM,EAAE,OAAO,CAAC,YAAY,CAAC,QAAQ,SAAS;AAClE;AAUO,SAAS,sBAAsB,QAAmC;AACxE,QAAM,wBAAwB,IAAI,IAAI,gBAAgB,MAAM,EAAE,IAAI,CAAC,YAAY,QAAQ,QAAQ,CAAC;AAChG,SAAO,kBAAkB,MAAM,EAAE;AAAA,IAChC,CAAC,WAAW,CAAC,OAAO,aAAa,sBAAsB,IAAI,OAAO,EAAE;AAAA,EACrE;AACD;AAGO,SAAS,oBAAoB,QAAqC;AACxE,QAAM,WAAW;AACjB,SAAO,OAAO,MAAM,MAAM,QAAQ,QAAQ,EAAE,IAAI;AACjD;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|