@tldraw/commenting 5.3.0-next.2fa9c61a8de6 → 5.3.0-next.7654e7ac2a02
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/commenting.css +63 -71
- package/dist-cjs/canvas/anchor-lifecycle.js +2 -2
- package/dist-cjs/canvas/anchor-lifecycle.js.map +2 -2
- package/dist-cjs/{ui/tooltip-button.js → canvas/canvas-events.js} +16 -13
- 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 +51 -1
- 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-mutations.js +57 -28
- package/dist-cjs/canvas/comment-mutations.js.map +2 -2
- package/dist-cjs/canvas/comment-reactions.js +4 -7
- package/dist-cjs/canvas/comment-reactions.js.map +2 -2
- package/dist-cjs/canvas/comment-render.js +4 -3
- package/dist-cjs/canvas/comment-render.js.map +2 -2
- package/dist-cjs/canvas/comment-store.js.map +2 -2
- package/dist-cjs/canvas/comment-tool.js +0 -1
- package/dist-cjs/canvas/comment-tool.js.map +2 -2
- package/dist-cjs/canvas/comments-filter-menu.js +11 -13
- package/dist-cjs/canvas/comments-filter-menu.js.map +2 -2
- package/dist-cjs/canvas/comments-overflow-menu.js +10 -2
- package/dist-cjs/canvas/comments-overflow-menu.js.map +2 -2
- package/dist-cjs/canvas/comments-overlay.js +77 -870
- package/dist-cjs/canvas/comments-overlay.js.map +3 -3
- package/dist-cjs/canvas/comments-sidebar.js +16 -11
- package/dist-cjs/canvas/comments-sidebar.js.map +2 -2
- package/dist-cjs/canvas/comments-visibility-toggle.js +5 -25
- package/dist-cjs/canvas/comments-visibility-toggle.js.map +2 -2
- package/dist-cjs/canvas/context.js.map +1 -1
- package/dist-cjs/canvas/hooks.js +8 -2
- 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 +39 -1
- 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 +26 -1
- package/dist-cjs/canvas/pin-stacking.js.map +2 -2
- package/dist-cjs/canvas/region-box.js +113 -0
- package/dist-cjs/canvas/region-box.js.map +7 -0
- 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 +41 -49
- package/dist-cjs/canvas/thread-preview.js.map +2 -2
- package/dist-cjs/canvas/thread-stack.js +7 -7
- package/dist-cjs/canvas/thread-stack.js.map +2 -2
- package/dist-cjs/canvas/thread-state.js +15 -0
- package/dist-cjs/canvas/thread-state.js.map +2 -2
- package/dist-cjs/canvas/thread-view.js +120 -67
- package/dist-cjs/canvas/thread-view.js.map +3 -3
- package/dist-cjs/clustering/computeClusterTable.js +2 -2
- 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 +19 -12
- package/dist-cjs/clustering/runtime.js.map +2 -2
- package/dist-cjs/clustering/types.js.map +1 -1
- package/dist-cjs/index.d.ts +323 -202
- package/dist-cjs/index.js +7 -1
- package/dist-cjs/index.js.map +2 -2
- package/dist-cjs/ui/byline.js +5 -3
- package/dist-cjs/ui/byline.js.map +2 -2
- package/dist-cjs/ui/comment-composer.js +2 -18
- package/dist-cjs/ui/comment-composer.js.map +2 -2
- package/dist-cjs/ui/comment-pin.js +2 -15
- package/dist-cjs/ui/comment-pin.js.map +2 -2
- package/dist-cjs/ui/comments-list.js +11 -24
- package/dist-cjs/ui/comments-list.js.map +2 -2
- package/dist-cjs/ui/format-time.js +12 -0
- 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 +11 -13
- package/dist-cjs/ui/reaction-picker.js.map +2 -2
- package/dist-cjs/ui/reaction.js +3 -4
- package/dist-cjs/ui/reaction.js.map +2 -2
- package/dist-cjs/ui/send-button.js +2 -9
- package/dist-cjs/ui/send-button.js.map +2 -2
- package/dist-cjs/ui/visual-viewport.js +32 -0
- package/dist-cjs/ui/visual-viewport.js.map +7 -0
- package/dist-esm/canvas/anchor-lifecycle.mjs +3 -3
- package/dist-esm/canvas/anchor-lifecycle.mjs.map +2 -2
- 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 +52 -2
- 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-mutations.mjs +57 -28
- package/dist-esm/canvas/comment-mutations.mjs.map +2 -2
- package/dist-esm/canvas/comment-reactions.mjs +5 -12
- package/dist-esm/canvas/comment-reactions.mjs.map +2 -2
- package/dist-esm/canvas/comment-render.mjs +5 -4
- package/dist-esm/canvas/comment-render.mjs.map +2 -2
- package/dist-esm/canvas/comment-store.mjs.map +2 -2
- package/dist-esm/canvas/comment-tool.mjs +0 -1
- package/dist-esm/canvas/comment-tool.mjs.map +2 -2
- package/dist-esm/canvas/comments-filter-menu.mjs +12 -13
- package/dist-esm/canvas/comments-filter-menu.mjs.map +2 -2
- package/dist-esm/canvas/comments-overflow-menu.mjs +11 -2
- package/dist-esm/canvas/comments-overflow-menu.mjs.map +2 -2
- package/dist-esm/canvas/comments-overlay.mjs +54 -878
- package/dist-esm/canvas/comments-overlay.mjs.map +3 -3
- package/dist-esm/canvas/comments-sidebar.mjs +18 -14
- package/dist-esm/canvas/comments-sidebar.mjs.map +2 -2
- package/dist-esm/canvas/comments-visibility-toggle.mjs +5 -25
- package/dist-esm/canvas/comments-visibility-toggle.mjs.map +2 -2
- package/dist-esm/canvas/hooks.mjs +9 -2
- 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 +39 -1
- 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 +26 -1
- package/dist-esm/canvas/pin-stacking.mjs.map +2 -2
- package/dist-esm/canvas/region-box.mjs +93 -0
- package/dist-esm/canvas/region-box.mjs.map +7 -0
- 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 +42 -49
- package/dist-esm/canvas/thread-preview.mjs.map +2 -2
- package/dist-esm/canvas/thread-stack.mjs +8 -9
- package/dist-esm/canvas/thread-stack.mjs.map +2 -2
- package/dist-esm/canvas/thread-state.mjs +15 -0
- package/dist-esm/canvas/thread-state.mjs.map +2 -2
- package/dist-esm/canvas/thread-view.mjs +131 -72
- package/dist-esm/canvas/thread-view.mjs.map +3 -3
- package/dist-esm/clustering/computeClusterTable.mjs +2 -2
- 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 +19 -12
- package/dist-esm/clustering/runtime.mjs.map +2 -2
- package/dist-esm/index.d.mts +323 -202
- package/dist-esm/index.mjs +16 -3
- package/dist-esm/index.mjs.map +2 -2
- package/dist-esm/ui/byline.mjs +6 -4
- package/dist-esm/ui/byline.mjs.map +2 -2
- package/dist-esm/ui/comment-composer.mjs +2 -18
- package/dist-esm/ui/comment-composer.mjs.map +2 -2
- package/dist-esm/ui/comment-pin.mjs +2 -15
- package/dist-esm/ui/comment-pin.mjs.map +2 -2
- package/dist-esm/ui/comments-list.mjs +10 -23
- package/dist-esm/ui/comments-list.mjs.map +2 -2
- package/dist-esm/ui/format-time.mjs +12 -0
- 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 +12 -13
- package/dist-esm/ui/reaction-picker.mjs.map +2 -2
- package/dist-esm/ui/reaction.mjs +3 -4
- package/dist-esm/ui/reaction.mjs.map +2 -2
- package/dist-esm/ui/send-button.mjs +2 -9
- 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 +5 -5
- package/src/canvas/anchor-lifecycle.test.ts +2 -2
- package/src/canvas/anchor-lifecycle.ts +25 -39
- package/src/canvas/canvas-events.ts +18 -0
- package/src/canvas/canvas.css +5 -11
- package/src/canvas/cluster-badge.tsx +133 -0
- package/src/canvas/cluster-fade.ts +102 -0
- package/src/canvas/cluster-input.test.ts +148 -14
- package/src/canvas/cluster-input.ts +92 -5
- package/src/canvas/cluster-model.ts +378 -0
- package/src/canvas/comment-mutations.test.ts +46 -4
- package/src/canvas/comment-mutations.ts +115 -104
- package/src/canvas/comment-reactions.test.ts +22 -0
- package/src/canvas/comment-reactions.tsx +33 -32
- package/src/canvas/comment-render.ts +8 -8
- package/src/canvas/comment-store.ts +12 -17
- package/src/canvas/comment-tool.tsx +8 -12
- package/src/canvas/comments-filter-menu.tsx +9 -16
- package/src/canvas/comments-overflow-menu.tsx +8 -3
- package/src/canvas/comments-overlay.tsx +154 -1344
- package/src/canvas/comments-sidebar.tsx +40 -19
- package/src/canvas/comments-visibility-toggle.tsx +6 -30
- package/src/canvas/context.ts +4 -3
- package/src/canvas/hooks.test.ts +94 -0
- package/src/canvas/hooks.ts +17 -3
- package/src/canvas/license.ts +1 -1
- package/src/canvas/mobile-placement.ts +115 -0
- package/src/canvas/options.test.ts +124 -2
- package/src/canvas/options.ts +192 -58
- package/src/canvas/pending-composer.tsx +157 -0
- package/src/canvas/pin-stacking.test.ts +111 -1
- package/src/canvas/pin-stacking.ts +46 -0
- package/src/canvas/region-box.tsx +124 -0
- package/src/canvas/state.ts +20 -28
- package/src/canvas/thread-pin.tsx +418 -0
- package/src/canvas/thread-preview.tsx +77 -87
- package/src/canvas/thread-stack.tsx +19 -29
- package/src/canvas/thread-state.test.ts +51 -0
- package/src/canvas/thread-state.ts +56 -23
- package/src/canvas/thread-view.test.ts +72 -0
- package/src/canvas/thread-view.tsx +231 -133
- package/src/clustering/computeClusterTable.ts +12 -3
- package/src/clustering/replay.test.ts +0 -7
- package/src/clustering/replay.ts +131 -32
- package/src/clustering/runtime.test.ts +50 -6
- package/src/clustering/runtime.ts +42 -39
- package/src/clustering/schedule.test.ts +0 -6
- package/src/clustering/screen-offsets.test.ts +171 -0
- package/src/clustering/types.ts +10 -0
- package/src/index.ts +15 -2
- package/src/ui/byline.tsx +16 -6
- package/src/ui/comment-composer.tsx +25 -69
- package/src/ui/comment-pin.tsx +3 -16
- package/src/ui/comments-list.tsx +40 -29
- package/src/ui/comments.css +58 -60
- package/src/ui/format-time.test.ts +69 -0
- package/src/ui/format-time.ts +20 -0
- package/src/ui/icons.tsx +116 -0
- package/src/ui/reaction-picker.tsx +9 -16
- package/src/ui/reaction.tsx +10 -6
- package/src/ui/send-button.tsx +3 -8
- package/src/ui/visual-viewport.test.ts +36 -0
- package/src/ui/visual-viewport.ts +27 -0
- package/dist-cjs/ui/tooltip-button.js.map +0 -7
- package/dist-esm/ui/tooltip-button.mjs +0 -12
- package/dist-esm/ui/tooltip-button.mjs.map +0 -7
- package/src/ui/tooltip-button.tsx +0 -20
|
@@ -16,14 +16,11 @@ import { openThreadId } from './state'
|
|
|
16
16
|
/**
|
|
17
17
|
* Every write to a comment record, and the undo/redo policy governing them.
|
|
18
18
|
*
|
|
19
|
-
*
|
|
20
|
-
* {@link putCommentRecords} and {@link removeCommentRecords} are the typed
|
|
21
|
-
* it, and the verbs below are those writes plus the one rule each carries
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* Every verb takes the record it acts on, but treats it as the identity of what to change rather
|
|
26
|
-
* than as the value to write back — see {@link readLatest}.
|
|
19
|
+
* The file layers bottom-up: {@link commitCommentMutation} resolves the history mode and hands its
|
|
20
|
+
* callback a writer, {@link putCommentRecords} and {@link removeCommentRecords} are the typed
|
|
21
|
+
* writes that run under it, and the verbs below are those writes plus the one rule each carries.
|
|
22
|
+
* Every verb takes the record it acts on as the identity of what to change, not the value to write
|
|
23
|
+
* back — see {@link readLatest}.
|
|
27
24
|
*
|
|
28
25
|
* Posting carries no such rule, so it isn't a verb here: build the records with
|
|
29
26
|
* `createCommentThread`/`createComment` and write them with {@link putCommentRecords}.
|
|
@@ -42,6 +39,13 @@ import { openThreadId } from './state'
|
|
|
42
39
|
*/
|
|
43
40
|
export type CommentMutationKind = 'delete' | 'drag' | 'mutation'
|
|
44
41
|
|
|
42
|
+
interface CommentMutationWriter {
|
|
43
|
+
put(records: TLCommentRecord[]): void
|
|
44
|
+
remove(ids: (TLCommentId | TLCommentReactionId | TLCommentThreadId)[]): void
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const activeCommentMutations = new WeakMap<Editor, { history: TLHistoryBatchOptions['history'] }>()
|
|
48
|
+
|
|
45
49
|
/** The undo/redo mode a write of the given kind runs under. See {@link CommentMutationKind}. */
|
|
46
50
|
function historyModeFor(
|
|
47
51
|
options: CommentingOptions,
|
|
@@ -61,71 +65,92 @@ function historyModeFor(
|
|
|
61
65
|
* Commit a comment mutation with the configured undo/redo behavior, so the
|
|
62
66
|
* {@link CommentingOptions.history} option governs whether it lands on the undo stack. Defaults to
|
|
63
67
|
* `'ignore'`. See {@link CommentMutationKind} for what each kind resolves to.
|
|
68
|
+
*
|
|
69
|
+
* `editor.run`'s history option isn't additive — a nested run overwrites the enclosing mode — so
|
|
70
|
+
* constituent records go through the callback's writer instead of opening a commit of their own,
|
|
71
|
+
* which would quietly make a `drag` write non-undoable.
|
|
72
|
+
*
|
|
73
|
+
* Commits that nest are only a problem when they resolve to different modes, and then neither is
|
|
74
|
+
* the right one to keep, so it throws. Matching modes have to nest: a store side effect runs inside
|
|
75
|
+
* the write that triggered it, with no "after the commit" to defer to. (A `store.listen` handler
|
|
76
|
+
* normally flushes on a later frame, so its writes open a commit of their own, but a synchronous
|
|
77
|
+
* flush — as under test — lands it inside too.)
|
|
64
78
|
* @internal
|
|
65
79
|
*/
|
|
66
80
|
export function commitCommentMutation<T>(
|
|
67
81
|
editor: Editor,
|
|
68
|
-
fn: () => T,
|
|
82
|
+
fn: (writer: CommentMutationWriter) => T,
|
|
69
83
|
kind: CommentMutationKind = 'mutation'
|
|
70
84
|
): T {
|
|
71
85
|
const history = historyModeFor(getCommentingOptions(editor), kind)
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
)
|
|
79
|
-
return result!
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Write records without opening a commit, for call sites that already sit inside one.
|
|
84
|
-
*
|
|
85
|
-
* `editor.run`'s history option isn't additive — a nested run overwrites the enclosing mode for its
|
|
86
|
-
* own scope — so a {@link putCommentRecords} call inside a commit would discard the mode its caller
|
|
87
|
-
* chose. A pin drag committed as `drag` under `dragHistory: 'record'` would land back on
|
|
88
|
-
* `history: 'ignore'` and quietly stop being undoable. Inside a commit, write with this.
|
|
89
|
-
*
|
|
90
|
-
* @internal
|
|
91
|
-
*/
|
|
92
|
-
export function putRecordsInCommit(editor: Editor, records: TLCommentRecord[]): void {
|
|
93
|
-
editor.store.put(records as unknown as TLRecord[])
|
|
94
|
-
}
|
|
86
|
+
const enclosing = activeCommentMutations.get(editor)
|
|
87
|
+
if (enclosing && enclosing.history !== history) {
|
|
88
|
+
throw new Error(
|
|
89
|
+
`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.`
|
|
90
|
+
)
|
|
91
|
+
}
|
|
95
92
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
)
|
|
101
|
-
|
|
93
|
+
activeCommentMutations.set(editor, { history })
|
|
94
|
+
try {
|
|
95
|
+
let result: T
|
|
96
|
+
editor.run(
|
|
97
|
+
() => {
|
|
98
|
+
let isWriterActive = true
|
|
99
|
+
const assertWriterActive = () => {
|
|
100
|
+
if (!isWriterActive) {
|
|
101
|
+
throw new Error(
|
|
102
|
+
'A comment mutation writer cannot be used after its commit has finished.'
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
try {
|
|
107
|
+
result = fn({
|
|
108
|
+
put: (records) => {
|
|
109
|
+
assertWriterActive()
|
|
110
|
+
editor.store.put(records as unknown as TLRecord[])
|
|
111
|
+
},
|
|
112
|
+
remove: (ids) => {
|
|
113
|
+
assertWriterActive()
|
|
114
|
+
editor.store.remove(ids as unknown as TLRecord['id'][])
|
|
115
|
+
},
|
|
116
|
+
})
|
|
117
|
+
} finally {
|
|
118
|
+
isWriterActive = false
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
{ history }
|
|
122
|
+
)
|
|
123
|
+
return result!
|
|
124
|
+
} finally {
|
|
125
|
+
if (enclosing) {
|
|
126
|
+
activeCommentMutations.set(editor, enclosing)
|
|
127
|
+
} else {
|
|
128
|
+
activeCommentMutations.delete(editor)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
102
131
|
}
|
|
103
132
|
|
|
104
133
|
/**
|
|
105
|
-
* Write comment records to the store, under the configured
|
|
106
|
-
*
|
|
107
|
-
* doesn't) exactly like one the built-in UI writes. Defaults to `'ignore'`.
|
|
134
|
+
* Write comment records to the store, under the configured {@link CommentingOptions.history}
|
|
135
|
+
* behavior. Defaults to `'ignore'`.
|
|
108
136
|
*
|
|
109
|
-
* Use it to seed or import threads, and to save an edit. To delete, prefer
|
|
110
|
-
* {@link
|
|
111
|
-
* soft-deleted, and a synced server rejects the hard delete.
|
|
137
|
+
* Use it to seed or import threads, and to save an edit. To delete, prefer {@link deleteComment}
|
|
138
|
+
* and {@link deleteThread}: comments are soft-deleted, and a synced server rejects the hard delete.
|
|
112
139
|
*
|
|
113
140
|
* @public
|
|
114
141
|
*/
|
|
115
142
|
export function putCommentRecords(editor: Editor, records: TLCommentRecord[]): void {
|
|
116
|
-
commitCommentMutation(editor, () =>
|
|
143
|
+
commitCommentMutation(editor, ({ put }) => put(records))
|
|
117
144
|
}
|
|
118
145
|
|
|
119
146
|
/**
|
|
120
147
|
* Remove comment records from the store by id, under the configured
|
|
121
148
|
* {@link CommentingOptions.history} behavior.
|
|
122
149
|
*
|
|
123
|
-
* This is a hard delete, which is rarely what you want
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
* a local, unsynced comment store, or to drop a reaction (which is a hard delete — see
|
|
128
|
-
* {@link toggleCommentReaction}).
|
|
150
|
+
* This is a hard delete, which is rarely what you want: the built-in UI soft-deletes
|
|
151
|
+
* ({@link deleteComment}, {@link deleteThread}) so the server can prune the records, and a server
|
|
152
|
+
* enforcing per-record permissions vetoes a hard delete outright. Reach for this on a local,
|
|
153
|
+
* unsynced store, or to drop a reaction (see {@link toggleCommentReaction}).
|
|
129
154
|
*
|
|
130
155
|
* @public
|
|
131
156
|
*/
|
|
@@ -133,24 +158,19 @@ export function removeCommentRecords(
|
|
|
133
158
|
editor: Editor,
|
|
134
159
|
ids: (TLCommentId | TLCommentReactionId | TLCommentThreadId)[]
|
|
135
160
|
): void {
|
|
136
|
-
commitCommentMutation(editor, () =>
|
|
161
|
+
commitCommentMutation(editor, ({ remove }) => remove(ids))
|
|
137
162
|
}
|
|
138
163
|
|
|
139
164
|
/**
|
|
140
165
|
* The record as the store currently holds it, or `undefined` if it isn't there any more.
|
|
141
166
|
*
|
|
142
|
-
* A verb is handed a record, but that record is a snapshot
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
* back would put those fields as they were and sync the revert out to everyone.
|
|
147
|
-
*
|
|
148
|
-
* `put` is also an upsert, so a record a remote delete has already removed would come back — with
|
|
149
|
-
* `isDeleted: false` on an edit or a resolve. That's the multiplayer surprise the schema warns
|
|
150
|
-
* about (see `TLComment`), reached by a route the history option doesn't cover.
|
|
167
|
+
* A verb is handed a record, but that record is a snapshot and comment records move underneath it:
|
|
168
|
+
* deleting a pinned shape converts the anchor to a point, reparenting rehomes the thread, a drag
|
|
169
|
+
* re-anchors it. Writing the caller's snapshot back would revert those fields for everyone. `put`
|
|
170
|
+
* is also an upsert, so a record a remote delete already removed would come back.
|
|
151
171
|
*
|
|
152
172
|
* So a verb reads what it's changing rather than trusting what it was given, and a record that's
|
|
153
|
-
* gone is a no-op
|
|
173
|
+
* gone is a no-op.
|
|
154
174
|
*/
|
|
155
175
|
function readLatest<T extends TLComment | TLCommentThread>(
|
|
156
176
|
editor: Editor,
|
|
@@ -162,15 +182,13 @@ function readLatest<T extends TLComment | TLCommentThread>(
|
|
|
162
182
|
}
|
|
163
183
|
|
|
164
184
|
/**
|
|
165
|
-
* Replace a comment's body and stamp it as edited, which
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
*
|
|
169
|
-
* that enforces per-record permissions rejects anyone else's edit.
|
|
185
|
+
* Replace a comment's body and stamp it as edited, which renders the "(edited)" marker on its
|
|
186
|
+
* byline. Editing is the author's to do by default ({@link CommentingOptions.canModifyComment}),
|
|
187
|
+
* and a server enforcing per-record permissions rejects anyone else's. Widening one end without the
|
|
188
|
+
* other leaves an edit that's offered and then rejected, so widen both.
|
|
170
189
|
*
|
|
171
|
-
* The
|
|
172
|
-
*
|
|
173
|
-
* re-create a comment that's already been removed — editing one of those does nothing.
|
|
190
|
+
* The body lands on the version the store currently holds, so a stale copy can't revert a later
|
|
191
|
+
* change or re-create a removed comment — editing one of those does nothing.
|
|
174
192
|
*
|
|
175
193
|
* @example
|
|
176
194
|
* ```ts
|
|
@@ -180,10 +198,10 @@ function readLatest<T extends TLComment | TLCommentThread>(
|
|
|
180
198
|
* @public
|
|
181
199
|
*/
|
|
182
200
|
export function editComment(editor: Editor, comment: TLComment, body: TLRichText): void {
|
|
183
|
-
commitCommentMutation(editor, () => {
|
|
201
|
+
commitCommentMutation(editor, ({ put }) => {
|
|
184
202
|
const current = readLatest(editor, comment)
|
|
185
203
|
if (!current) return
|
|
186
|
-
|
|
204
|
+
put([{ ...current, body, editedAt: Date.now() }])
|
|
187
205
|
})
|
|
188
206
|
}
|
|
189
207
|
|
|
@@ -191,16 +209,16 @@ export function editComment(editor: Editor, comment: TLComment, body: TLRichText
|
|
|
191
209
|
* Mark a thread resolved, stamping who resolved it and when. Resolved threads keep their pin (a
|
|
192
210
|
* checked one) and are hidden from the sidebar until its "show resolved" filter is on.
|
|
193
211
|
*
|
|
194
|
-
* Only the resolution is written
|
|
195
|
-
*
|
|
212
|
+
* Only the resolution is written — the rest of the thread is read fresh, so a stale copy can't drag
|
|
213
|
+
* a pin back. A no-op on a thread that's gone.
|
|
196
214
|
*
|
|
197
215
|
* @public
|
|
198
216
|
*/
|
|
199
217
|
export function resolveThread(editor: Editor, thread: TLCommentThread, userId: string): void {
|
|
200
|
-
commitCommentMutation(editor, () => {
|
|
218
|
+
commitCommentMutation(editor, ({ put }) => {
|
|
201
219
|
const current = readLatest(editor, thread)
|
|
202
220
|
if (!current) return
|
|
203
|
-
|
|
221
|
+
put([{ ...current, resolved: { at: Date.now(), by: userId } }])
|
|
204
222
|
})
|
|
205
223
|
}
|
|
206
224
|
|
|
@@ -211,50 +229,45 @@ export function resolveThread(editor: Editor, thread: TLCommentThread, userId: s
|
|
|
211
229
|
* @public
|
|
212
230
|
*/
|
|
213
231
|
export function reopenThread(editor: Editor, thread: TLCommentThread): void {
|
|
214
|
-
commitCommentMutation(editor, () => {
|
|
232
|
+
commitCommentMutation(editor, ({ put }) => {
|
|
215
233
|
const current = readLatest(editor, thread)
|
|
216
234
|
if (!current) return
|
|
217
|
-
|
|
235
|
+
put([{ ...current, resolved: null }])
|
|
218
236
|
})
|
|
219
237
|
}
|
|
220
238
|
|
|
221
239
|
/**
|
|
222
240
|
* Delete a comment.
|
|
223
241
|
*
|
|
224
|
-
* This is a soft delete: it sets
|
|
225
|
-
*
|
|
226
|
-
*
|
|
227
|
-
* per-record permissions has a write it can check rather than a deletion it can only refuse.
|
|
228
|
-
* Deleting is the author's to do; the built-in UI only offers it on your own comments.
|
|
242
|
+
* This is a soft delete: it sets `isDeleted` rather than removing the record, and the server prunes
|
|
243
|
+
* the comment and its reactions once the flag is persisted — so no client removes records it
|
|
244
|
+
* doesn't own, and a server enforcing per-record permissions has a write it can check.
|
|
229
245
|
*
|
|
230
|
-
*
|
|
231
|
-
*
|
|
232
|
-
* comment back.
|
|
246
|
+
* Deleting is the author's to do by default; {@link CommentingOptions.canModifyComment} widens
|
|
247
|
+
* that, as does its counterpart on the server.
|
|
233
248
|
*
|
|
234
|
-
*
|
|
235
|
-
*
|
|
236
|
-
* the deleter may not be its creator.
|
|
249
|
+
* Never undoable, whatever {@link CommentingOptions.history} says: the flag is write-once
|
|
250
|
+
* server-side, so an undo clearing it would be vetoed rather than bring the comment back.
|
|
237
251
|
*
|
|
238
|
-
*
|
|
252
|
+
* Deleting a thread's last comment closes it and leaves the thread record for the server to prune,
|
|
253
|
+
* since the deleter may not be its creator. An already-deleted comment is a no-op.
|
|
239
254
|
*
|
|
240
255
|
* @public
|
|
241
256
|
*/
|
|
242
257
|
export function deleteComment(editor: Editor, comment: TLComment): void {
|
|
243
258
|
commitCommentMutation(
|
|
244
259
|
editor,
|
|
245
|
-
() => {
|
|
260
|
+
({ put }) => {
|
|
246
261
|
const current = readLatest(editor, comment)
|
|
247
|
-
// Deleting twice
|
|
248
|
-
//
|
|
249
|
-
// comment being deleted among the live ones, so it only reads as "the last one" while
|
|
250
|
-
// this delete is the one taking it away.
|
|
262
|
+
// Deleting twice is nothing to do rather than something to redo. The check below counts this
|
|
263
|
+
// comment among the live ones, so it only reads as "the last one" while this delete takes it away.
|
|
251
264
|
if (!current || current.isDeleted) return
|
|
252
265
|
const isLastInThread =
|
|
253
266
|
getLiveComments(editor).filter((c) => c.threadId === current.threadId).length <= 1
|
|
254
267
|
if (isLastInThread && openThreadId.get(editor) === current.threadId) {
|
|
255
268
|
openThreadId.set(editor, null)
|
|
256
269
|
}
|
|
257
|
-
|
|
270
|
+
put([{ ...current, isDeleted: true }])
|
|
258
271
|
},
|
|
259
272
|
'delete'
|
|
260
273
|
)
|
|
@@ -263,25 +276,23 @@ export function deleteComment(editor: Editor, comment: TLComment): void {
|
|
|
263
276
|
/**
|
|
264
277
|
* Delete a thread and, with it, the whole conversation.
|
|
265
278
|
*
|
|
266
|
-
* A soft delete on the same model as {@link deleteComment}: the
|
|
267
|
-
*
|
|
268
|
-
*
|
|
269
|
-
*
|
|
270
|
-
*
|
|
271
|
-
* Closes the thread if it's the open one. A thread that's already pruned is a no-op.
|
|
279
|
+
* A soft delete on the same model as {@link deleteComment}: the server prunes the thread, its
|
|
280
|
+
* comments, and their reactions once the flag is persisted. Deleting a thread is its creator's to
|
|
281
|
+
* do by default ({@link CommentingOptions.canModifyComment}), and the write is never undoable.
|
|
282
|
+
* Closes the thread if it's the open one; a pruned thread is a no-op.
|
|
272
283
|
*
|
|
273
284
|
* @public
|
|
274
285
|
*/
|
|
275
286
|
export function deleteThread(editor: Editor, thread: TLCommentThread): void {
|
|
276
287
|
commitCommentMutation(
|
|
277
288
|
editor,
|
|
278
|
-
() => {
|
|
289
|
+
({ put }) => {
|
|
279
290
|
const current = readLatest(editor, thread)
|
|
280
291
|
if (!current) return
|
|
281
292
|
if (openThreadId.get(editor) === current.id) {
|
|
282
293
|
openThreadId.set(editor, null)
|
|
283
294
|
}
|
|
284
|
-
|
|
295
|
+
put([{ ...current, isDeleted: true }])
|
|
285
296
|
},
|
|
286
297
|
'delete'
|
|
287
298
|
)
|
|
@@ -87,6 +87,28 @@ describe('summarizeReactions', () => {
|
|
|
87
87
|
})
|
|
88
88
|
})
|
|
89
89
|
|
|
90
|
+
describe('summarizeReactions input', () => {
|
|
91
|
+
// the notifications panel tallies rows synced from the app database, which carry only the
|
|
92
|
+
// fields the tally needs — full TLCommentReaction records must not be required
|
|
93
|
+
it('accepts minimal rows with only userId, emoji, and createdAt', () => {
|
|
94
|
+
const rows = [
|
|
95
|
+
{ userId: 'user1', emoji: '👍', createdAt: 100 },
|
|
96
|
+
{ userId: 'user2', emoji: '👍', createdAt: 200 },
|
|
97
|
+
]
|
|
98
|
+
expect(summarizeReactions(rows, 'user1')).toEqual([
|
|
99
|
+
{
|
|
100
|
+
emoji: '👍',
|
|
101
|
+
count: 2,
|
|
102
|
+
active: true,
|
|
103
|
+
reactors: [
|
|
104
|
+
{ name: 'Someone', you: true },
|
|
105
|
+
{ name: 'Someone', you: false },
|
|
106
|
+
],
|
|
107
|
+
},
|
|
108
|
+
])
|
|
109
|
+
})
|
|
110
|
+
})
|
|
111
|
+
|
|
90
112
|
describe('createCommentReactionId', () => {
|
|
91
113
|
// the id is what makes reaction identity structural: re-picking the same emoji addresses the
|
|
92
114
|
// same record (toggle off), while a different emoji is its own record (independent)
|
|
@@ -11,11 +11,7 @@ import {
|
|
|
11
11
|
import { RenderReaction } from '../ui/reaction'
|
|
12
12
|
import { ReactionPicker } from '../ui/reaction-picker'
|
|
13
13
|
import { Reactions, ReactionSummary } from '../ui/reactions'
|
|
14
|
-
import {
|
|
15
|
-
commitCommentMutation,
|
|
16
|
-
putRecordsInCommit,
|
|
17
|
-
removeRecordsInCommit,
|
|
18
|
-
} from './comment-mutations'
|
|
14
|
+
import { commitCommentMutation } from './comment-mutations'
|
|
19
15
|
import { UNKNOWN_AUTHOR } from './comment-render'
|
|
20
16
|
import { getCommentReactions } from './comment-store'
|
|
21
17
|
import { getCommentingOptions, useCommentingOptions } from './options'
|
|
@@ -40,16 +36,28 @@ export function useCommentReactions(
|
|
|
40
36
|
}
|
|
41
37
|
|
|
42
38
|
/**
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
39
|
+
* The reaction fields {@link summarizeReactions} needs — a structural subset of
|
|
40
|
+
* {@link tldraw#TLCommentReaction}, so tallies can also be built from rows synced outside the
|
|
41
|
+
* editor store.
|
|
42
|
+
*
|
|
43
|
+
* @public
|
|
44
|
+
*/
|
|
45
|
+
export interface ReactionSummaryInput {
|
|
46
|
+
userId: string
|
|
47
|
+
emoji: string
|
|
48
|
+
createdAt: number
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Tally a comment's reactions into an entry per emoji, ordered by when that emoji was first used so
|
|
53
|
+
* the row stays stable as later reactions arrive. `active` marks the current user's emoji and
|
|
54
|
+
* `reactors` lists who reacted, in reaction order. `resolveName` names each reactor; an id it can't
|
|
55
|
+
* name falls back to a generic "Someone", never the raw user id.
|
|
48
56
|
*
|
|
49
57
|
* @public
|
|
50
58
|
*/
|
|
51
59
|
export function summarizeReactions(
|
|
52
|
-
reactions:
|
|
60
|
+
reactions: readonly ReactionSummaryInput[],
|
|
53
61
|
currentUserId?: string | null,
|
|
54
62
|
resolveName?: (userId: string) => string | undefined
|
|
55
63
|
): ReactionSummary[] {
|
|
@@ -89,14 +97,12 @@ export function summarizeReactions(
|
|
|
89
97
|
/**
|
|
90
98
|
* Toggle one user's reaction with a given emoji on a comment.
|
|
91
99
|
*
|
|
92
|
-
* Each reaction is its own record keyed by (comment, user, emoji), so this only
|
|
93
|
-
*
|
|
94
|
-
* `allowMultipleReactions
|
|
100
|
+
* Each reaction is its own record keyed by (comment, user, emoji), so this only touches that user's
|
|
101
|
+
* own records and two people reacting at once never conflict. Behaviour depends on
|
|
102
|
+
* `allowMultipleReactions`:
|
|
95
103
|
*
|
|
96
|
-
* - **multiple** (default): the emoji toggles independently
|
|
97
|
-
*
|
|
98
|
-
* - **single**: picking a new emoji first removes the user's existing reaction(s) on the comment,
|
|
99
|
-
* then adds this one (a replace); picking the one they already have removes it.
|
|
104
|
+
* - **multiple** (default): the emoji toggles independently, leaving other reactions alone.
|
|
105
|
+
* - **single**: a new emoji replaces the user's existing reaction; the same one removes it.
|
|
100
106
|
*
|
|
101
107
|
* @public
|
|
102
108
|
*/
|
|
@@ -116,19 +122,16 @@ export function toggleCommentReaction(
|
|
|
116
122
|
// Only a token the configured palette allows may be added. Removals always go through — if a
|
|
117
123
|
// reaction somehow carries an off-palette token, the user must still be able to clear it.
|
|
118
124
|
if (!removing && !isAllowedReaction(emoji)) return
|
|
119
|
-
commitCommentMutation(editor, () => {
|
|
125
|
+
commitCommentMutation(editor, ({ put, remove }) => {
|
|
120
126
|
if (removing) {
|
|
121
|
-
|
|
127
|
+
remove([targetId])
|
|
122
128
|
return
|
|
123
129
|
}
|
|
124
130
|
// Single-select: a new emoji replaces the user's existing reaction(s) on this comment.
|
|
125
131
|
if (!allowMultipleReactions && mine.length > 0) {
|
|
126
|
-
|
|
127
|
-
editor,
|
|
128
|
-
mine.map((reaction) => reaction.id)
|
|
129
|
-
)
|
|
132
|
+
remove(mine.map((reaction) => reaction.id))
|
|
130
133
|
}
|
|
131
|
-
|
|
134
|
+
put([
|
|
132
135
|
createCommentReaction({
|
|
133
136
|
commentId: comment.id,
|
|
134
137
|
threadId: comment.threadId,
|
|
@@ -147,14 +150,13 @@ export interface CommentReactionsProps {
|
|
|
147
150
|
/** The reacting user. Null/omitted gives a read-only row (signed out): counts show, but the
|
|
148
151
|
* pills don't toggle. */
|
|
149
152
|
currentUserId?: string | null
|
|
150
|
-
/** Names a reactor id for the hover list. Ids it can't name fall back to
|
|
153
|
+
/** Names a reactor id for the hover list. Ids it can't name fall back to a generic "Someone". */
|
|
151
154
|
resolveName?(userId: string): string | undefined
|
|
152
155
|
}
|
|
153
156
|
|
|
154
157
|
/**
|
|
155
|
-
* Adapts the `ReactionContent`
|
|
156
|
-
*
|
|
157
|
-
* to the default (the token string, drawn by the OS emoji font).
|
|
158
|
+
* Adapts the `ReactionContent` override into a `renderReaction` function for the presentational
|
|
159
|
+
* components. Undefined when no override is set, so they fall back to drawing the token string.
|
|
158
160
|
*/
|
|
159
161
|
function useReactionRenderer(): RenderReaction | undefined {
|
|
160
162
|
const { components } = useCommentingOptions()
|
|
@@ -210,9 +212,8 @@ export interface CommentReactionPickerProps {
|
|
|
210
212
|
}
|
|
211
213
|
|
|
212
214
|
/**
|
|
213
|
-
* The add-reaction button for one comment. Belongs with the comment card's hover actions
|
|
214
|
-
*
|
|
215
|
-
* are added.
|
|
215
|
+
* The add-reaction button for one comment. Belongs with the comment card's hover actions rather
|
|
216
|
+
* than in the reaction row, so opening it doesn't chase the row as reactions are added.
|
|
216
217
|
* @public @react
|
|
217
218
|
*/
|
|
218
219
|
export function CommentReactionPicker({
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { generateText, JSONContent } from '@tiptap/core'
|
|
2
2
|
import { type CommentAuthor, createMentionExtension } from '@tldraw/mentions'
|
|
3
|
-
import { TLRichText } from 'tldraw'
|
|
3
|
+
import { renderHtmlFromRichTextWithExtensions, TLRichText } from 'tldraw'
|
|
4
4
|
import { commentTipTapExtensions, isCommentEmpty } from '../ui/comment-extensions'
|
|
5
5
|
|
|
6
6
|
/**
|
|
@@ -44,9 +44,9 @@ const htmlCache = new WeakMap<TLRichText, string>()
|
|
|
44
44
|
|
|
45
45
|
/**
|
|
46
46
|
* Render a comment body to HTML through the limited comment extension set (no headings), so a body
|
|
47
|
-
* always renders with comment formatting regardless of the host editor's rich-text config.
|
|
48
|
-
* tldraw's
|
|
49
|
-
* collapsing. `resolveName` maps a member id to its current name for any @mentions.
|
|
47
|
+
* always renders with comment formatting regardless of the host editor's rich-text config. Renders
|
|
48
|
+
* through tldraw's shared helper, so it gets the same empty-paragraph fix that keeps blank lines
|
|
49
|
+
* from collapsing. `resolveName` maps a member id to its current name for any @mentions.
|
|
50
50
|
*/
|
|
51
51
|
export function renderCommentHtml(
|
|
52
52
|
richText: TLRichText,
|
|
@@ -60,9 +60,9 @@ export function renderCommentHtml(
|
|
|
60
60
|
const extensions = mentions
|
|
61
61
|
? [...commentTipTapExtensions, createMentionExtension({ resolveName })]
|
|
62
62
|
: commentTipTapExtensions
|
|
63
|
-
const html =
|
|
64
|
-
|
|
65
|
-
|
|
63
|
+
const html = renderHtmlFromRichTextWithExtensions(
|
|
64
|
+
demoteHeadings(richText as JSONContent) as TLRichText,
|
|
65
|
+
extensions
|
|
66
66
|
)
|
|
67
67
|
if (!mentions) htmlCache.set(richText, html)
|
|
68
68
|
return html
|
|
@@ -3,15 +3,13 @@ import { Editor, TLComment, TLCommentReaction, TLCommentThread, TLRecord } from
|
|
|
3
3
|
/**
|
|
4
4
|
* Typed reads of comment records on the editor store.
|
|
5
5
|
*
|
|
6
|
-
* Comment
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* the type. These helpers own that reinterpretation — an `unknown` hop to exactly the type the store
|
|
11
|
-
* expects, so the rest of each call stays checked — behind one boundary, and keep call sites typed.
|
|
6
|
+
* Comment records live on the editor's local store so the canvas can render them reactively, but
|
|
7
|
+
* they're opt-in and aren't part of the `TLRecord` union — so `editor.store` is statically typed
|
|
8
|
+
* `Store<TLRecord>` and every access has to reinterpret the type. These helpers own that
|
|
9
|
+
* reinterpretation behind one boundary and keep call sites typed.
|
|
12
10
|
*
|
|
13
|
-
* Writes do the same
|
|
14
|
-
*
|
|
11
|
+
* Writes do the same, but also answer to the undo/redo policy, so they live in
|
|
12
|
+
* `comment-mutations.ts`.
|
|
15
13
|
*/
|
|
16
14
|
|
|
17
15
|
/**
|
|
@@ -36,9 +34,8 @@ export function getCommentRecord(editor: Editor, id: string): TLCommentRecord |
|
|
|
36
34
|
}
|
|
37
35
|
|
|
38
36
|
/**
|
|
39
|
-
* Every comment thread in the store, **including soft-deleted
|
|
40
|
-
*
|
|
41
|
-
* nothing renders. For the set the UI shows, use {@link getLiveCommentThreads}.
|
|
37
|
+
* Every comment thread in the store, **including soft-deleted and emptied ones** awaiting the
|
|
38
|
+
* server's prune, which nothing renders. For the set the UI shows, use {@link getLiveCommentThreads}.
|
|
42
39
|
*
|
|
43
40
|
* Non-reactive; wrap in `useValue`, or use `useCommentThreads`, to react.
|
|
44
41
|
* @public
|
|
@@ -62,8 +59,7 @@ export function getComments(editor: Editor): TLComment[] {
|
|
|
62
59
|
|
|
63
60
|
/**
|
|
64
61
|
* The comments that should render: not soft-deleted. A deleted record lingers in the store until
|
|
65
|
-
* the server prunes it, so this
|
|
66
|
-
* should be built from.
|
|
62
|
+
* the server prunes it, so build counts and lists from this rather than {@link getComments}.
|
|
67
63
|
*
|
|
68
64
|
* Non-reactive; the reactive equivalent is `useComments` (which also sorts oldest first).
|
|
69
65
|
* @public
|
|
@@ -73,10 +69,9 @@ export function getLiveComments(editor: Editor): TLComment[] {
|
|
|
73
69
|
}
|
|
74
70
|
|
|
75
71
|
/**
|
|
76
|
-
* The comment threads that should render (pins, sidebar):
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
* thread record lingers with no surface.
|
|
72
|
+
* The comment threads that should render (pins, sidebar): not soft-deleted, and still holding at
|
|
73
|
+
* least one live comment. A thread emptied by its last comment's delete lingers with no surface
|
|
74
|
+
* until the server's prune lands.
|
|
80
75
|
*
|
|
81
76
|
* Non-reactive; the reactive equivalent is `useCommentThreads`.
|
|
82
77
|
* @public
|