@tldraw/commenting 0.0.0-bootstrap → 5.3.0-canary.31036673d3f8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (279) hide show
  1. package/README.md +0 -2
  2. package/commenting.css +1139 -329
  3. package/dist-cjs/canvas/anchor-lifecycle.js +148 -0
  4. package/dist-cjs/canvas/anchor-lifecycle.js.map +7 -0
  5. package/dist-cjs/canvas/cluster-input.js +2 -2
  6. package/dist-cjs/canvas/cluster-input.js.map +2 -2
  7. package/dist-cjs/canvas/comment-body.js +1 -1
  8. package/dist-cjs/canvas/comment-body.js.map +2 -2
  9. package/dist-cjs/canvas/comment-drafts.js +54 -0
  10. package/dist-cjs/canvas/comment-drafts.js.map +7 -0
  11. package/dist-cjs/canvas/comment-mutations.js +123 -0
  12. package/dist-cjs/canvas/comment-mutations.js.map +7 -0
  13. package/dist-cjs/canvas/comment-reactions.js +163 -0
  14. package/dist-cjs/canvas/comment-reactions.js.map +7 -0
  15. package/dist-cjs/canvas/comment-render.js +5 -3
  16. package/dist-cjs/canvas/comment-render.js.map +2 -2
  17. package/dist-cjs/canvas/comment-store.js +19 -9
  18. package/dist-cjs/canvas/comment-store.js.map +2 -2
  19. package/dist-cjs/canvas/comment-tool.js +64 -14
  20. package/dist-cjs/canvas/comment-tool.js.map +2 -2
  21. package/dist-cjs/canvas/comments-filter-menu.js +20 -38
  22. package/dist-cjs/canvas/comments-filter-menu.js.map +2 -2
  23. package/dist-cjs/canvas/comments-overflow-menu.js +22 -27
  24. package/dist-cjs/canvas/comments-overflow-menu.js.map +2 -2
  25. package/dist-cjs/canvas/comments-overlay.js +429 -474
  26. package/dist-cjs/canvas/comments-overlay.js.map +2 -2
  27. package/dist-cjs/canvas/comments-sidebar.js +35 -32
  28. package/dist-cjs/canvas/comments-sidebar.js.map +2 -2
  29. package/dist-cjs/canvas/comments-visibility-toggle.js +65 -0
  30. package/dist-cjs/canvas/comments-visibility-toggle.js.map +7 -0
  31. package/dist-cjs/canvas/context.js +17 -0
  32. package/dist-cjs/canvas/context.js.map +7 -0
  33. package/dist-cjs/canvas/hooks.js +22 -3
  34. package/dist-cjs/canvas/hooks.js.map +2 -2
  35. package/dist-cjs/canvas/options.js +19 -2
  36. package/dist-cjs/canvas/options.js.map +2 -2
  37. package/dist-cjs/canvas/pin-stacking.js +56 -0
  38. package/dist-cjs/canvas/pin-stacking.js.map +7 -0
  39. package/dist-cjs/canvas/sidebar-filters.js +2 -2
  40. package/dist-cjs/canvas/sidebar-filters.js.map +2 -2
  41. package/dist-cjs/canvas/state.js +28 -14
  42. package/dist-cjs/canvas/state.js.map +2 -2
  43. package/dist-cjs/canvas/thread-preview.js +185 -0
  44. package/dist-cjs/canvas/thread-preview.js.map +7 -0
  45. package/dist-cjs/canvas/thread-stack.js +199 -0
  46. package/dist-cjs/canvas/thread-stack.js.map +7 -0
  47. package/dist-cjs/canvas/thread-state.js +67 -21
  48. package/dist-cjs/canvas/thread-state.js.map +2 -2
  49. package/dist-cjs/canvas/thread-view.js +397 -0
  50. package/dist-cjs/canvas/thread-view.js.map +7 -0
  51. package/dist-cjs/clustering/computeClusterTable.js +4 -4
  52. package/dist-cjs/clustering/computeClusterTable.js.map +2 -2
  53. package/dist-cjs/clustering/runtime.js +119 -7
  54. package/dist-cjs/clustering/runtime.js.map +2 -2
  55. package/dist-cjs/clustering/types.js.map +1 -1
  56. package/dist-cjs/index.d.ts +763 -333
  57. package/dist-cjs/index.js +46 -28
  58. package/dist-cjs/index.js.map +2 -2
  59. package/dist-cjs/ui/byline.js +6 -4
  60. package/dist-cjs/ui/byline.js.map +2 -2
  61. package/dist-cjs/ui/comment-card.js +15 -8
  62. package/dist-cjs/ui/comment-card.js.map +2 -2
  63. package/dist-cjs/ui/comment-composer.js +137 -36
  64. package/dist-cjs/ui/comment-composer.js.map +2 -2
  65. package/dist-cjs/ui/comment-pin.js +24 -2
  66. package/dist-cjs/ui/comment-pin.js.map +2 -2
  67. package/dist-cjs/ui/comment-thread.js +9 -7
  68. package/dist-cjs/ui/comment-thread.js.map +2 -2
  69. package/dist-cjs/ui/comments-list.js +30 -18
  70. package/dist-cjs/ui/comments-list.js.map +2 -2
  71. package/dist-cjs/ui/count-badge.js +3 -2
  72. package/dist-cjs/ui/count-badge.js.map +2 -2
  73. package/dist-cjs/ui/emoji-picker.js +54 -0
  74. package/dist-cjs/ui/emoji-picker.js.map +7 -0
  75. package/dist-cjs/ui/empty-state.js +3 -3
  76. package/dist-cjs/ui/empty-state.js.map +2 -2
  77. package/dist-cjs/ui/format-time.js +4 -1
  78. package/dist-cjs/ui/format-time.js.map +2 -2
  79. package/dist-cjs/ui/reaction-picker.js +73 -0
  80. package/dist-cjs/ui/reaction-picker.js.map +7 -0
  81. package/dist-cjs/ui/reaction.js +59 -6
  82. package/dist-cjs/ui/reaction.js.map +2 -2
  83. package/dist-cjs/ui/reactions.js +20 -7
  84. package/dist-cjs/ui/reactions.js.map +2 -2
  85. package/dist-cjs/ui/{mention.js → reply-count.js} +9 -11
  86. package/dist-cjs/ui/reply-count.js.map +7 -0
  87. package/dist-cjs/ui/send-button.js +20 -1
  88. package/dist-cjs/ui/send-button.js.map +2 -2
  89. package/dist-cjs/ui/{comment-text.js → tooltip-button.js} +12 -9
  90. package/dist-cjs/ui/tooltip-button.js.map +7 -0
  91. package/dist-esm/canvas/anchor-lifecycle.mjs +128 -0
  92. package/dist-esm/canvas/anchor-lifecycle.mjs.map +7 -0
  93. package/dist-esm/canvas/cluster-input.mjs +2 -2
  94. package/dist-esm/canvas/cluster-input.mjs.map +2 -2
  95. package/dist-esm/canvas/comment-body.mjs +1 -1
  96. package/dist-esm/canvas/comment-body.mjs.map +2 -2
  97. package/dist-esm/canvas/comment-drafts.mjs +34 -0
  98. package/dist-esm/canvas/comment-drafts.mjs.map +7 -0
  99. package/dist-esm/canvas/comment-mutations.mjs +103 -0
  100. package/dist-esm/canvas/comment-mutations.mjs.map +7 -0
  101. package/dist-esm/canvas/comment-reactions.mjs +152 -0
  102. package/dist-esm/canvas/comment-reactions.mjs.map +7 -0
  103. package/dist-esm/canvas/comment-render.mjs +5 -3
  104. package/dist-esm/canvas/comment-render.mjs.map +2 -2
  105. package/dist-esm/canvas/comment-store.mjs +19 -9
  106. package/dist-esm/canvas/comment-store.mjs.map +2 -2
  107. package/dist-esm/canvas/comment-tool.mjs +67 -17
  108. package/dist-esm/canvas/comment-tool.mjs.map +2 -2
  109. package/dist-esm/canvas/comments-filter-menu.mjs +20 -38
  110. package/dist-esm/canvas/comments-filter-menu.mjs.map +2 -2
  111. package/dist-esm/canvas/comments-overflow-menu.mjs +23 -27
  112. package/dist-esm/canvas/comments-overflow-menu.mjs.map +2 -2
  113. package/dist-esm/canvas/comments-overlay.mjs +450 -479
  114. package/dist-esm/canvas/comments-overlay.mjs.map +3 -3
  115. package/dist-esm/canvas/comments-sidebar.mjs +38 -35
  116. package/dist-esm/canvas/comments-sidebar.mjs.map +2 -2
  117. package/dist-esm/canvas/comments-visibility-toggle.mjs +45 -0
  118. package/dist-esm/canvas/comments-visibility-toggle.mjs.map +7 -0
  119. package/dist-esm/canvas/context.mjs +1 -0
  120. package/dist-esm/canvas/context.mjs.map +7 -0
  121. package/dist-esm/canvas/hooks.mjs +28 -5
  122. package/dist-esm/canvas/hooks.mjs.map +2 -2
  123. package/dist-esm/canvas/options.mjs +21 -3
  124. package/dist-esm/canvas/options.mjs.map +2 -2
  125. package/dist-esm/canvas/pin-stacking.mjs +36 -0
  126. package/dist-esm/canvas/pin-stacking.mjs.map +7 -0
  127. package/dist-esm/canvas/sidebar-filters.mjs +2 -2
  128. package/dist-esm/canvas/sidebar-filters.mjs.map +2 -2
  129. package/dist-esm/canvas/state.mjs +29 -19
  130. package/dist-esm/canvas/state.mjs.map +2 -2
  131. package/dist-esm/canvas/thread-preview.mjs +170 -0
  132. package/dist-esm/canvas/thread-preview.mjs.map +7 -0
  133. package/dist-esm/canvas/thread-stack.mjs +190 -0
  134. package/dist-esm/canvas/thread-stack.mjs.map +7 -0
  135. package/dist-esm/canvas/thread-state.mjs +71 -22
  136. package/dist-esm/canvas/thread-state.mjs.map +2 -2
  137. package/dist-esm/canvas/thread-view.mjs +402 -0
  138. package/dist-esm/canvas/thread-view.mjs.map +7 -0
  139. package/dist-esm/clustering/computeClusterTable.mjs +4 -4
  140. package/dist-esm/clustering/computeClusterTable.mjs.map +2 -2
  141. package/dist-esm/clustering/runtime.mjs +119 -7
  142. package/dist-esm/clustering/runtime.mjs.map +2 -2
  143. package/dist-esm/index.d.mts +763 -333
  144. package/dist-esm/index.mjs +87 -41
  145. package/dist-esm/index.mjs.map +2 -2
  146. package/dist-esm/ui/byline.mjs +6 -4
  147. package/dist-esm/ui/byline.mjs.map +2 -2
  148. package/dist-esm/ui/comment-card.mjs +15 -8
  149. package/dist-esm/ui/comment-card.mjs.map +2 -2
  150. package/dist-esm/ui/comment-composer.mjs +148 -36
  151. package/dist-esm/ui/comment-composer.mjs.map +2 -2
  152. package/dist-esm/ui/comment-pin.mjs +24 -2
  153. package/dist-esm/ui/comment-pin.mjs.map +2 -2
  154. package/dist-esm/ui/comment-thread.mjs +9 -7
  155. package/dist-esm/ui/comment-thread.mjs.map +2 -2
  156. package/dist-esm/ui/comments-list.mjs +43 -29
  157. package/dist-esm/ui/comments-list.mjs.map +2 -2
  158. package/dist-esm/ui/count-badge.mjs +3 -2
  159. package/dist-esm/ui/count-badge.mjs.map +2 -2
  160. package/dist-esm/ui/emoji-picker.mjs +34 -0
  161. package/dist-esm/ui/emoji-picker.mjs.map +7 -0
  162. package/dist-esm/ui/empty-state.mjs +3 -3
  163. package/dist-esm/ui/empty-state.mjs.map +2 -2
  164. package/dist-esm/ui/format-time.mjs +4 -1
  165. package/dist-esm/ui/format-time.mjs.map +2 -2
  166. package/dist-esm/ui/reaction-picker.mjs +60 -0
  167. package/dist-esm/ui/reaction-picker.mjs.map +7 -0
  168. package/dist-esm/ui/reaction.mjs +60 -7
  169. package/dist-esm/ui/reaction.mjs.map +2 -2
  170. package/dist-esm/ui/reactions.mjs +21 -8
  171. package/dist-esm/ui/reactions.mjs.map +2 -2
  172. package/dist-esm/ui/reply-count.mjs +9 -0
  173. package/dist-esm/ui/reply-count.mjs.map +7 -0
  174. package/dist-esm/ui/send-button.mjs +20 -1
  175. package/dist-esm/ui/send-button.mjs.map +2 -2
  176. package/dist-esm/ui/tooltip-button.mjs +12 -0
  177. package/dist-esm/ui/tooltip-button.mjs.map +7 -0
  178. package/package.json +6 -6
  179. package/src/canvas/anchor-lifecycle.test.ts +290 -0
  180. package/src/canvas/anchor-lifecycle.ts +205 -0
  181. package/src/canvas/canvas.css +461 -46
  182. package/src/canvas/cluster-input.test.ts +31 -23
  183. package/src/canvas/cluster-input.ts +3 -4
  184. package/src/canvas/comment-body.tsx +1 -1
  185. package/src/canvas/comment-drafts.test.ts +55 -0
  186. package/src/canvas/comment-drafts.ts +46 -0
  187. package/src/canvas/comment-mutations.test.ts +410 -0
  188. package/src/canvas/comment-mutations.ts +288 -0
  189. package/src/canvas/comment-reactions.test.ts +132 -0
  190. package/src/canvas/comment-reactions.tsx +245 -0
  191. package/src/canvas/comment-render.test.ts +1 -1
  192. package/src/canvas/comment-render.ts +9 -5
  193. package/src/canvas/comment-store.ts +67 -27
  194. package/src/canvas/comment-tool.test.ts +162 -0
  195. package/src/canvas/comment-tool.tsx +105 -23
  196. package/src/canvas/comments-filter-menu.tsx +23 -32
  197. package/src/canvas/comments-overflow-menu.tsx +14 -21
  198. package/src/canvas/comments-overlay.tsx +695 -622
  199. package/src/canvas/comments-sidebar.test.ts +45 -0
  200. package/src/canvas/comments-sidebar.tsx +76 -58
  201. package/src/canvas/comments-visibility-toggle.tsx +49 -0
  202. package/src/canvas/context.ts +47 -0
  203. package/src/canvas/hooks.ts +52 -11
  204. package/src/canvas/options.test.ts +52 -2
  205. package/src/canvas/options.ts +133 -9
  206. package/src/canvas/pin-stacking.test.ts +114 -0
  207. package/src/canvas/pin-stacking.ts +57 -0
  208. package/src/canvas/sidebar-filters.ts +4 -3
  209. package/src/canvas/state.ts +92 -35
  210. package/src/canvas/thread-preview.test.ts +91 -0
  211. package/src/canvas/thread-preview.tsx +304 -0
  212. package/src/canvas/thread-stack.tsx +252 -0
  213. package/src/canvas/thread-state.test.ts +209 -0
  214. package/src/canvas/thread-state.ts +137 -36
  215. package/src/canvas/thread-view.tsx +516 -0
  216. package/src/clustering/computeClusterTable.test.ts +24 -23
  217. package/src/clustering/computeClusterTable.ts +5 -5
  218. package/src/clustering/runtime.test.ts +217 -7
  219. package/src/clustering/runtime.ts +171 -17
  220. package/src/clustering/types.ts +9 -9
  221. package/src/index.ts +76 -40
  222. package/src/ui/byline.tsx +7 -5
  223. package/src/ui/comment-card.tsx +20 -10
  224. package/src/ui/comment-composer.tsx +206 -40
  225. package/src/ui/comment-pin.tsx +26 -2
  226. package/src/ui/comment-thread.tsx +13 -6
  227. package/src/ui/comments-list.test.ts +31 -0
  228. package/src/ui/comments-list.tsx +45 -21
  229. package/src/ui/comments.css +566 -281
  230. package/src/ui/count-badge.tsx +9 -2
  231. package/src/ui/emoji-picker.test.ts +25 -0
  232. package/src/ui/emoji-picker.tsx +76 -0
  233. package/src/ui/empty-state.tsx +3 -3
  234. package/src/ui/format-time.ts +7 -2
  235. package/src/ui/reaction-picker.tsx +109 -0
  236. package/src/ui/reaction.tsx +121 -6
  237. package/src/ui/reactions.tsx +69 -10
  238. package/src/ui/reply-count.ts +16 -0
  239. package/src/ui/send-button.tsx +18 -3
  240. package/src/ui/tooltip-button.tsx +20 -0
  241. package/dist-cjs/canvas/region-options.js +0 -49
  242. package/dist-cjs/canvas/region-options.js.map +0 -7
  243. package/dist-cjs/ui/avatar.js +0 -43
  244. package/dist-cjs/ui/avatar.js.map +0 -7
  245. package/dist-cjs/ui/comment-mention.js +0 -42
  246. package/dist-cjs/ui/comment-mention.js.map +0 -7
  247. package/dist-cjs/ui/comment-text.js.map +0 -7
  248. package/dist-cjs/ui/mention-list.js +0 -70
  249. package/dist-cjs/ui/mention-list.js.map +0 -7
  250. package/dist-cjs/ui/mention-suggestion.js +0 -206
  251. package/dist-cjs/ui/mention-suggestion.js.map +0 -7
  252. package/dist-cjs/ui/mention.js.map +0 -7
  253. package/dist-cjs/ui/render-markdown.js +0 -63
  254. package/dist-cjs/ui/render-markdown.js.map +0 -7
  255. package/dist-esm/canvas/region-options.mjs +0 -29
  256. package/dist-esm/canvas/region-options.mjs.map +0 -7
  257. package/dist-esm/ui/avatar.mjs +0 -23
  258. package/dist-esm/ui/avatar.mjs.map +0 -7
  259. package/dist-esm/ui/comment-mention.mjs +0 -22
  260. package/dist-esm/ui/comment-mention.mjs.map +0 -7
  261. package/dist-esm/ui/comment-text.mjs +0 -9
  262. package/dist-esm/ui/comment-text.mjs.map +0 -7
  263. package/dist-esm/ui/mention-list.mjs +0 -50
  264. package/dist-esm/ui/mention-list.mjs.map +0 -7
  265. package/dist-esm/ui/mention-suggestion.mjs +0 -186
  266. package/dist-esm/ui/mention-suggestion.mjs.map +0 -7
  267. package/dist-esm/ui/mention.mjs +0 -11
  268. package/dist-esm/ui/mention.mjs.map +0 -7
  269. package/dist-esm/ui/render-markdown.mjs +0 -45
  270. package/dist-esm/ui/render-markdown.mjs.map +0 -7
  271. package/src/canvas/region-options.ts +0 -57
  272. package/src/ui/avatar.tsx +0 -31
  273. package/src/ui/comment-mention.ts +0 -47
  274. package/src/ui/comment-text.tsx +0 -12
  275. package/src/ui/mention-list.tsx +0 -106
  276. package/src/ui/mention-suggestion.test.ts +0 -18
  277. package/src/ui/mention-suggestion.tsx +0 -285
  278. package/src/ui/mention.tsx +0 -9
  279. package/src/ui/render-markdown.tsx +0 -72
@@ -0,0 +1,205 @@
1
+ import { WeakCache } from '@tldraw/utils'
2
+ import { Editor, TLCommentAnchor, TLCommentThread, TLPageId, TLShapeId, VecLike } from 'tldraw'
3
+ import { commitCommentMutation, putRecordsInCommit } from './comment-mutations'
4
+ import { getCommentRecord, getComments, getCommentThreads, TLCommentRecord } from './comment-store'
5
+ import { anchorPagePoint, impreciseShapePinInset } from './thread-state'
6
+
7
+ type ShapeAnchor = Extract<TLCommentAnchor, { type: 'shape' }>
8
+
9
+ /**
10
+ * Threads converted to point anchors because their shape was deleted, kept so the anchor can be
11
+ * restored if the shape comes back (page move re-create, undo of the delete). Owned by the
12
+ * editor, not the registration closure: the registering effect can re-run (a prop identity
13
+ * change, a remount), and an undo can arrive arbitrarily long after the delete — the memory has
14
+ * to outlive any one registration.
15
+ */
16
+ const convertedByShapeCache = new WeakCache<
17
+ Editor,
18
+ Map<TLShapeId, { threadId: string; anchor: ShapeAnchor; point: VecLike }[]>
19
+ >()
20
+
21
+ function isAnchoredToShape(
22
+ thread: TLCommentThread,
23
+ shapeId: TLShapeId
24
+ ): thread is TLCommentThread & { anchor: ShapeAnchor } {
25
+ const anchor = thread.anchor
26
+ return anchor.type === 'shape' && anchor.shapeId === shapeId
27
+ }
28
+
29
+ /**
30
+ * Keep shape-anchored threads alive across their shape's lifecycle:
31
+ *
32
+ * - When the shape is deleted, the thread converts to a `point` anchor at the spot its pin last
33
+ * occupied, so the conversation outlives the shape instead of becoming invisible (a missing
34
+ * shape has no bounds, which hides the pin).
35
+ * - When the shape moves to another page, the thread follows: its `pageId` and each comment's
36
+ * denormalized `pageId` update to the shape's new page, and the anchor keeps riding the shape.
37
+ * - When a deleted shape comes back (a page move re-creates it with its id preserved; undoing a
38
+ * delete restores it), the thread re-attaches — unless its pin was manually moved in the
39
+ * meantime, in which case the manual placement wins.
40
+ *
41
+ * A page move is `deleteShapes` + re-create with preserved ids inside one `editor.run`, but each
42
+ * store write is its own operation — so at no single moment does "this shape is being moved"
43
+ * exist as an observable event. The handlers therefore cooperate across operations:
44
+ * `beforeDelete` snapshots where each affected pin sits (every record is still in the store at
45
+ * that point, so page bounds resolve even for children of a deleted frame); the
46
+ * operation-complete pass converts threads whose shape is really gone, remembering the original
47
+ * anchor; `afterCreate` notes a reappeared shape id, and the operation-complete pass restores
48
+ * the anchor once the store has settled (creation order within an operation is arbitrary, so a
49
+ * fresh shape may not resolve an ancestor page mid-operation). Undo/redo of a move
50
+ * replays as a `parentId` update (remove + add of one id squash to an update in the history
51
+ * diff), so an `afterChange` handler re-homes threads on cross-page reparents — including
52
+ * threads anchored to descendants, which move with their parent without change events of their
53
+ * own.
54
+ *
55
+ * Remote changes are ignored: the client that performed the operation runs this same
56
+ * maintenance and syncs the resulting thread updates. Writes honour the
57
+ * {@link CommentingOptions.history} option, so the consumer decides whether they're undoable.
58
+ *
59
+ * Registered by `CanvasComments` on mount; parts-built consumers can call this directly.
60
+ * Returns a cleanup function that unregisters all handlers.
61
+ *
62
+ * @public
63
+ */
64
+ export function registerCommentAnchorLifecycle(editor: Editor): () => void {
65
+ // Pin positions snapshotted during the current operation: shape id -> (thread id -> pin page
66
+ // point).
67
+ const pendingByShape = new Map<TLShapeId, Map<string, VecLike | null>>()
68
+ const convertedByShape = convertedByShapeCache.get(editor, () => new Map())
69
+
70
+ function rehomeThread(thread: TLCommentThread, pageId: TLPageId, updates: TLCommentRecord[]) {
71
+ updates.push({ ...thread, pageId })
72
+ for (const comment of getComments(editor)) {
73
+ if (comment.threadId === thread.id) updates.push({ ...comment, pageId })
74
+ }
75
+ }
76
+
77
+ const disposeBeforeDelete = editor.sideEffects.registerBeforeDeleteHandler(
78
+ 'shape',
79
+ (shape, source) => {
80
+ if (source === 'remote') return
81
+ for (const thread of getCommentThreads(editor)) {
82
+ if (!isAnchoredToShape(thread, shape.id)) continue
83
+ let snapshot = pendingByShape.get(shape.id)
84
+ if (!snapshot) {
85
+ snapshot = new Map()
86
+ pendingByShape.set(shape.id, snapshot)
87
+ }
88
+ // Snapshot where the pin is drawn, not just the anchor point: imprecise shape pins
89
+ // render inset toward the shape's centre, and the converted point pin renders at
90
+ // its point exactly — without the inset the pin would jump on deletion. The inset
91
+ // is screen-fixed, so bake it in at the current zoom.
92
+ let point = anchorPagePoint(editor, thread.anchor)
93
+ const inset = impreciseShapePinInset(editor, thread.anchor)
94
+ if (point && inset) {
95
+ const zoom = editor.getZoomLevel()
96
+ point = { x: point.x + inset.x / zoom, y: point.y + inset.y / zoom }
97
+ }
98
+ snapshot.set(thread.id, point)
99
+ }
100
+ }
101
+ )
102
+
103
+ // Shape ids from `convertedByShape` re-created during the current operation. Restores are
104
+ // settled at operation complete rather than in `afterCreate` itself: creation order within an
105
+ // operation is arbitrary, so a shape can appear before its parent and not resolve an ancestor
106
+ // page yet — and the conversion memory must outlive any such partial state.
107
+ const returnedShapeIds = new Set<TLShapeId>()
108
+
109
+ const disposeOperationComplete = editor.sideEffects.registerOperationCompleteHandler((source) => {
110
+ // Only user-sourced handlers populate the maps and only local operations should settle
111
+ // them; a remote operation completing leaves any (impossible-in-practice) leftovers for
112
+ // the next local settle rather than judging them against remote state.
113
+ if (source === 'remote') return
114
+ if (pendingByShape.size === 0 && returnedShapeIds.size === 0) return
115
+ const settled = [...pendingByShape.entries()]
116
+ pendingByShape.clear()
117
+ const returned = [...returnedShapeIds]
118
+ returnedShapeIds.clear()
119
+
120
+ const updates: TLCommentRecord[] = []
121
+ for (const [shapeId, threadPoints] of settled) {
122
+ if (editor.getShape(shapeId)) continue
123
+ for (const [threadId, point] of threadPoints) {
124
+ if (!point) continue
125
+ const thread = getCommentRecord(editor, threadId)
126
+ if (!thread || thread.typeName !== 'comment-thread') continue
127
+ if (!isAnchoredToShape(thread, shapeId)) continue
128
+ let converted = convertedByShape.get(shapeId)
129
+ if (!converted) {
130
+ converted = []
131
+ convertedByShape.set(shapeId, converted)
132
+ }
133
+ converted.push({ threadId, anchor: thread.anchor, point })
134
+ updates.push({ ...thread, anchor: { type: 'point', x: point.x, y: point.y } })
135
+ }
136
+ }
137
+
138
+ for (const shapeId of returned) {
139
+ const shape = editor.getShape(shapeId)
140
+ if (!shape) continue
141
+ const converted = convertedByShape.get(shapeId)
142
+ if (!converted) continue
143
+ convertedByShape.delete(shapeId)
144
+
145
+ const pageId = editor.getAncestorPageId(shape)
146
+ for (const { threadId, anchor, point } of converted) {
147
+ const thread = getCommentRecord(editor, threadId)
148
+ if (!thread || thread.typeName !== 'comment-thread') continue
149
+ // Re-attach only threads still sitting exactly where the conversion left them — a
150
+ // pin moved since then was placed deliberately, and that placement wins.
151
+ const current = thread.anchor
152
+ if (current.type !== 'point' || current.x !== point.x || current.y !== point.y) {
153
+ continue
154
+ }
155
+ if (!pageId || thread.pageId === pageId) {
156
+ updates.push({ ...thread, anchor })
157
+ } else {
158
+ rehomeThread({ ...thread, anchor }, pageId, updates)
159
+ }
160
+ }
161
+ }
162
+
163
+ if (updates.length > 0) {
164
+ commitCommentMutation(editor, () => putRecordsInCommit(editor, updates))
165
+ }
166
+ })
167
+
168
+ const disposeAfterCreate = editor.sideEffects.registerAfterCreateHandler(
169
+ 'shape',
170
+ (shape, source) => {
171
+ if (source === 'remote') return
172
+ if (convertedByShape.has(shape.id)) returnedShapeIds.add(shape.id)
173
+ }
174
+ )
175
+
176
+ const disposeAfterChange = editor.sideEffects.registerAfterChangeHandler(
177
+ 'shape',
178
+ (prev, next, source) => {
179
+ if (source === 'remote') return
180
+ if (prev.parentId === next.parentId) return
181
+ const pageId = editor.getAncestorPageId(next)
182
+ if (!pageId) return
183
+ // Descendants move with their parent without change events of their own.
184
+ const movedIds = editor.getShapeAndDescendantIds([next.id])
185
+ const updates: TLCommentRecord[] = []
186
+ for (const thread of getCommentThreads(editor)) {
187
+ const anchor = thread.anchor
188
+ if (anchor.type !== 'shape') continue
189
+ if (!movedIds.has(anchor.shapeId)) continue
190
+ if (thread.pageId === pageId) continue
191
+ rehomeThread(thread, pageId, updates)
192
+ }
193
+ if (updates.length > 0) {
194
+ commitCommentMutation(editor, () => putRecordsInCommit(editor, updates))
195
+ }
196
+ }
197
+ )
198
+
199
+ return () => {
200
+ disposeBeforeDelete()
201
+ disposeOperationComplete()
202
+ disposeAfterCreate()
203
+ disposeAfterChange()
204
+ }
205
+ }