@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.
Files changed (338) hide show
  1. package/README.md +0 -2
  2. package/commenting.css +1152 -350
  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/{ui/avatar.js → canvas/canvas-events.js} +14 -22
  6. package/dist-cjs/canvas/canvas-events.js.map +7 -0
  7. package/dist-cjs/canvas/cluster-badge.js +121 -0
  8. package/dist-cjs/canvas/cluster-badge.js.map +7 -0
  9. package/dist-cjs/canvas/cluster-fade.js +85 -0
  10. package/dist-cjs/canvas/cluster-fade.js.map +7 -0
  11. package/dist-cjs/canvas/cluster-input.js +53 -3
  12. package/dist-cjs/canvas/cluster-input.js.map +2 -2
  13. package/dist-cjs/canvas/cluster-model.js +252 -0
  14. package/dist-cjs/canvas/cluster-model.js.map +7 -0
  15. package/dist-cjs/canvas/comment-body.js +1 -1
  16. package/dist-cjs/canvas/comment-body.js.map +2 -2
  17. package/dist-cjs/canvas/comment-drafts.js +54 -0
  18. package/dist-cjs/canvas/comment-drafts.js.map +7 -0
  19. package/dist-cjs/canvas/comment-mutations.js +152 -0
  20. package/dist-cjs/canvas/comment-mutations.js.map +7 -0
  21. package/dist-cjs/canvas/comment-reactions.js +160 -0
  22. package/dist-cjs/canvas/comment-reactions.js.map +7 -0
  23. package/dist-cjs/canvas/comment-render.js +9 -6
  24. package/dist-cjs/canvas/comment-render.js.map +2 -2
  25. package/dist-cjs/canvas/comment-store.js +19 -9
  26. package/dist-cjs/canvas/comment-store.js.map +2 -2
  27. package/dist-cjs/canvas/comment-tool.js +63 -14
  28. package/dist-cjs/canvas/comment-tool.js.map +2 -2
  29. package/dist-cjs/canvas/comments-filter-menu.js +17 -37
  30. package/dist-cjs/canvas/comments-filter-menu.js.map +2 -2
  31. package/dist-cjs/canvas/comments-overflow-menu.js +25 -22
  32. package/dist-cjs/canvas/comments-overflow-menu.js.map +2 -2
  33. package/dist-cjs/canvas/comments-overlay.js +151 -989
  34. package/dist-cjs/canvas/comments-overlay.js.map +3 -3
  35. package/dist-cjs/canvas/comments-sidebar.js +49 -41
  36. package/dist-cjs/canvas/comments-sidebar.js.map +2 -2
  37. package/dist-cjs/canvas/comments-visibility-toggle.js +45 -0
  38. package/dist-cjs/canvas/comments-visibility-toggle.js.map +7 -0
  39. package/dist-cjs/canvas/context.js +17 -0
  40. package/dist-cjs/canvas/context.js.map +7 -0
  41. package/dist-cjs/canvas/hooks.js +28 -3
  42. package/dist-cjs/canvas/hooks.js.map +2 -2
  43. package/dist-cjs/canvas/license.js.map +1 -1
  44. package/dist-cjs/canvas/mobile-placement.js +91 -0
  45. package/dist-cjs/canvas/mobile-placement.js.map +7 -0
  46. package/dist-cjs/canvas/options.js +57 -2
  47. package/dist-cjs/canvas/options.js.map +2 -2
  48. package/dist-cjs/canvas/pending-composer.js +134 -0
  49. package/dist-cjs/canvas/pending-composer.js.map +7 -0
  50. package/dist-cjs/canvas/pin-stacking.js +81 -0
  51. package/dist-cjs/canvas/pin-stacking.js.map +7 -0
  52. package/dist-cjs/canvas/region-box.js +113 -0
  53. package/dist-cjs/canvas/region-box.js.map +7 -0
  54. package/dist-cjs/canvas/sidebar-filters.js +2 -2
  55. package/dist-cjs/canvas/sidebar-filters.js.map +2 -2
  56. package/dist-cjs/canvas/state.js +28 -14
  57. package/dist-cjs/canvas/state.js.map +2 -2
  58. package/dist-cjs/canvas/thread-pin.js +304 -0
  59. package/dist-cjs/canvas/thread-pin.js.map +7 -0
  60. package/dist-cjs/canvas/thread-preview.js +177 -0
  61. package/dist-cjs/canvas/thread-preview.js.map +7 -0
  62. package/dist-cjs/canvas/thread-stack.js +199 -0
  63. package/dist-cjs/canvas/thread-stack.js.map +7 -0
  64. package/dist-cjs/canvas/thread-state.js +82 -21
  65. package/dist-cjs/canvas/thread-state.js.map +2 -2
  66. package/dist-cjs/canvas/thread-view.js +450 -0
  67. package/dist-cjs/canvas/thread-view.js.map +7 -0
  68. package/dist-cjs/clustering/computeClusterTable.js +6 -6
  69. package/dist-cjs/clustering/computeClusterTable.js.map +2 -2
  70. package/dist-cjs/clustering/replay.js +89 -22
  71. package/dist-cjs/clustering/replay.js.map +3 -3
  72. package/dist-cjs/clustering/runtime.js +126 -7
  73. package/dist-cjs/clustering/runtime.js.map +2 -2
  74. package/dist-cjs/clustering/types.js.map +1 -1
  75. package/dist-cjs/index.d.ts +904 -353
  76. package/dist-cjs/index.js +52 -28
  77. package/dist-cjs/index.js.map +2 -2
  78. package/dist-cjs/ui/byline.js +10 -6
  79. package/dist-cjs/ui/byline.js.map +2 -2
  80. package/dist-cjs/ui/comment-card.js +15 -8
  81. package/dist-cjs/ui/comment-card.js.map +2 -2
  82. package/dist-cjs/ui/comment-composer.js +124 -39
  83. package/dist-cjs/ui/comment-composer.js.map +2 -2
  84. package/dist-cjs/ui/comment-pin.js +11 -2
  85. package/dist-cjs/ui/comment-pin.js.map +2 -2
  86. package/dist-cjs/ui/comment-thread.js +9 -7
  87. package/dist-cjs/ui/comment-thread.js.map +2 -2
  88. package/dist-cjs/ui/comments-list.js +39 -40
  89. package/dist-cjs/ui/comments-list.js.map +2 -2
  90. package/dist-cjs/ui/count-badge.js +3 -2
  91. package/dist-cjs/ui/count-badge.js.map +2 -2
  92. package/dist-cjs/ui/emoji-picker.js +54 -0
  93. package/dist-cjs/ui/emoji-picker.js.map +7 -0
  94. package/dist-cjs/ui/empty-state.js +3 -3
  95. package/dist-cjs/ui/empty-state.js.map +2 -2
  96. package/dist-cjs/ui/format-time.js +16 -1
  97. package/dist-cjs/ui/format-time.js.map +2 -2
  98. package/dist-cjs/ui/icons.js +115 -0
  99. package/dist-cjs/ui/icons.js.map +7 -0
  100. package/dist-cjs/ui/reaction-picker.js +71 -0
  101. package/dist-cjs/ui/reaction-picker.js.map +7 -0
  102. package/dist-cjs/ui/reaction.js +58 -6
  103. package/dist-cjs/ui/reaction.js.map +2 -2
  104. package/dist-cjs/ui/reactions.js +20 -7
  105. package/dist-cjs/ui/reactions.js.map +2 -2
  106. package/dist-cjs/ui/{mention.js → reply-count.js} +9 -11
  107. package/dist-cjs/ui/reply-count.js.map +7 -0
  108. package/dist-cjs/ui/send-button.js +13 -1
  109. package/dist-cjs/ui/send-button.js.map +2 -2
  110. package/dist-cjs/ui/{comment-text.js → visual-viewport.js} +12 -9
  111. package/dist-cjs/ui/visual-viewport.js.map +7 -0
  112. package/dist-esm/canvas/anchor-lifecycle.mjs +128 -0
  113. package/dist-esm/canvas/anchor-lifecycle.mjs.map +7 -0
  114. package/dist-esm/canvas/canvas-events.mjs +15 -0
  115. package/dist-esm/canvas/canvas-events.mjs.map +7 -0
  116. package/dist-esm/canvas/cluster-badge.mjs +106 -0
  117. package/dist-esm/canvas/cluster-badge.mjs.map +7 -0
  118. package/dist-esm/canvas/cluster-fade.mjs +65 -0
  119. package/dist-esm/canvas/cluster-fade.mjs.map +7 -0
  120. package/dist-esm/canvas/cluster-input.mjs +54 -4
  121. package/dist-esm/canvas/cluster-input.mjs.map +2 -2
  122. package/dist-esm/canvas/cluster-model.mjs +236 -0
  123. package/dist-esm/canvas/cluster-model.mjs.map +7 -0
  124. package/dist-esm/canvas/comment-body.mjs +1 -1
  125. package/dist-esm/canvas/comment-body.mjs.map +2 -2
  126. package/dist-esm/canvas/comment-drafts.mjs +34 -0
  127. package/dist-esm/canvas/comment-drafts.mjs.map +7 -0
  128. package/dist-esm/canvas/comment-mutations.mjs +132 -0
  129. package/dist-esm/canvas/comment-mutations.mjs.map +7 -0
  130. package/dist-esm/canvas/comment-reactions.mjs +145 -0
  131. package/dist-esm/canvas/comment-reactions.mjs.map +7 -0
  132. package/dist-esm/canvas/comment-render.mjs +10 -7
  133. package/dist-esm/canvas/comment-render.mjs.map +2 -2
  134. package/dist-esm/canvas/comment-store.mjs +19 -9
  135. package/dist-esm/canvas/comment-store.mjs.map +2 -2
  136. package/dist-esm/canvas/comment-tool.mjs +66 -17
  137. package/dist-esm/canvas/comment-tool.mjs.map +2 -2
  138. package/dist-esm/canvas/comments-filter-menu.mjs +17 -36
  139. package/dist-esm/canvas/comments-filter-menu.mjs.map +2 -2
  140. package/dist-esm/canvas/comments-overflow-menu.mjs +27 -22
  141. package/dist-esm/canvas/comments-overflow-menu.mjs.map +2 -2
  142. package/dist-esm/canvas/comments-overlay.mjs +157 -1010
  143. package/dist-esm/canvas/comments-overlay.mjs.map +3 -3
  144. package/dist-esm/canvas/comments-sidebar.mjs +53 -46
  145. package/dist-esm/canvas/comments-sidebar.mjs.map +2 -2
  146. package/dist-esm/canvas/comments-visibility-toggle.mjs +25 -0
  147. package/dist-esm/canvas/comments-visibility-toggle.mjs.map +7 -0
  148. package/dist-esm/canvas/context.mjs +1 -0
  149. package/dist-esm/canvas/context.mjs.map +7 -0
  150. package/dist-esm/canvas/hooks.mjs +35 -5
  151. package/dist-esm/canvas/hooks.mjs.map +2 -2
  152. package/dist-esm/canvas/license.mjs.map +1 -1
  153. package/dist-esm/canvas/mobile-placement.mjs +71 -0
  154. package/dist-esm/canvas/mobile-placement.mjs.map +7 -0
  155. package/dist-esm/canvas/options.mjs +59 -3
  156. package/dist-esm/canvas/options.mjs.map +2 -2
  157. package/dist-esm/canvas/pending-composer.mjs +126 -0
  158. package/dist-esm/canvas/pending-composer.mjs.map +7 -0
  159. package/dist-esm/canvas/pin-stacking.mjs +61 -0
  160. package/dist-esm/canvas/pin-stacking.mjs.map +7 -0
  161. package/dist-esm/canvas/region-box.mjs +93 -0
  162. package/dist-esm/canvas/region-box.mjs.map +7 -0
  163. package/dist-esm/canvas/sidebar-filters.mjs +2 -2
  164. package/dist-esm/canvas/sidebar-filters.mjs.map +2 -2
  165. package/dist-esm/canvas/state.mjs +29 -19
  166. package/dist-esm/canvas/state.mjs.map +2 -2
  167. package/dist-esm/canvas/thread-pin.mjs +310 -0
  168. package/dist-esm/canvas/thread-pin.mjs.map +7 -0
  169. package/dist-esm/canvas/thread-preview.mjs +163 -0
  170. package/dist-esm/canvas/thread-preview.mjs.map +7 -0
  171. package/dist-esm/canvas/thread-stack.mjs +189 -0
  172. package/dist-esm/canvas/thread-stack.mjs.map +7 -0
  173. package/dist-esm/canvas/thread-state.mjs +86 -22
  174. package/dist-esm/canvas/thread-state.mjs.map +2 -2
  175. package/dist-esm/canvas/thread-view.mjs +461 -0
  176. package/dist-esm/canvas/thread-view.mjs.map +7 -0
  177. package/dist-esm/clustering/computeClusterTable.mjs +6 -6
  178. package/dist-esm/clustering/computeClusterTable.mjs.map +2 -2
  179. package/dist-esm/clustering/replay.mjs +89 -22
  180. package/dist-esm/clustering/replay.mjs.map +3 -3
  181. package/dist-esm/clustering/runtime.mjs +126 -7
  182. package/dist-esm/clustering/runtime.mjs.map +2 -2
  183. package/dist-esm/index.d.mts +904 -353
  184. package/dist-esm/index.mjs +102 -43
  185. package/dist-esm/index.mjs.map +2 -2
  186. package/dist-esm/ui/byline.mjs +11 -7
  187. package/dist-esm/ui/byline.mjs.map +2 -2
  188. package/dist-esm/ui/comment-card.mjs +15 -8
  189. package/dist-esm/ui/comment-card.mjs.map +2 -2
  190. package/dist-esm/ui/comment-composer.mjs +135 -39
  191. package/dist-esm/ui/comment-composer.mjs.map +2 -2
  192. package/dist-esm/ui/comment-pin.mjs +11 -2
  193. package/dist-esm/ui/comment-pin.mjs.map +2 -2
  194. package/dist-esm/ui/comment-thread.mjs +9 -7
  195. package/dist-esm/ui/comment-thread.mjs.map +2 -2
  196. package/dist-esm/ui/comments-list.mjs +51 -50
  197. package/dist-esm/ui/comments-list.mjs.map +2 -2
  198. package/dist-esm/ui/count-badge.mjs +3 -2
  199. package/dist-esm/ui/count-badge.mjs.map +2 -2
  200. package/dist-esm/ui/emoji-picker.mjs +34 -0
  201. package/dist-esm/ui/emoji-picker.mjs.map +7 -0
  202. package/dist-esm/ui/empty-state.mjs +3 -3
  203. package/dist-esm/ui/empty-state.mjs.map +2 -2
  204. package/dist-esm/ui/format-time.mjs +16 -1
  205. package/dist-esm/ui/format-time.mjs.map +2 -2
  206. package/dist-esm/ui/icons.mjs +95 -0
  207. package/dist-esm/ui/icons.mjs.map +7 -0
  208. package/dist-esm/ui/reaction-picker.mjs +59 -0
  209. package/dist-esm/ui/reaction-picker.mjs.map +7 -0
  210. package/dist-esm/ui/reaction.mjs +59 -7
  211. package/dist-esm/ui/reaction.mjs.map +2 -2
  212. package/dist-esm/ui/reactions.mjs +21 -8
  213. package/dist-esm/ui/reactions.mjs.map +2 -2
  214. package/dist-esm/ui/reply-count.mjs +9 -0
  215. package/dist-esm/ui/reply-count.mjs.map +7 -0
  216. package/dist-esm/ui/send-button.mjs +13 -1
  217. package/dist-esm/ui/send-button.mjs.map +2 -2
  218. package/dist-esm/ui/visual-viewport.mjs +12 -0
  219. package/dist-esm/ui/visual-viewport.mjs.map +7 -0
  220. package/package.json +6 -6
  221. package/src/canvas/anchor-lifecycle.test.ts +290 -0
  222. package/src/canvas/anchor-lifecycle.ts +191 -0
  223. package/src/canvas/canvas-events.ts +18 -0
  224. package/src/canvas/canvas.css +464 -55
  225. package/src/canvas/cluster-badge.tsx +133 -0
  226. package/src/canvas/cluster-fade.ts +102 -0
  227. package/src/canvas/cluster-input.test.ts +178 -36
  228. package/src/canvas/cluster-input.ts +95 -9
  229. package/src/canvas/cluster-model.ts +378 -0
  230. package/src/canvas/comment-body.tsx +1 -1
  231. package/src/canvas/comment-drafts.test.ts +55 -0
  232. package/src/canvas/comment-drafts.ts +46 -0
  233. package/src/canvas/comment-mutations.test.ts +452 -0
  234. package/src/canvas/comment-mutations.ts +299 -0
  235. package/src/canvas/comment-reactions.test.ts +154 -0
  236. package/src/canvas/comment-reactions.tsx +246 -0
  237. package/src/canvas/comment-render.test.ts +1 -1
  238. package/src/canvas/comment-render.ts +17 -13
  239. package/src/canvas/comment-store.ts +68 -33
  240. package/src/canvas/comment-tool.test.ts +162 -0
  241. package/src/canvas/comment-tool.tsx +103 -25
  242. package/src/canvas/comments-filter-menu.tsx +20 -36
  243. package/src/canvas/comments-overflow-menu.tsx +17 -19
  244. package/src/canvas/comments-overlay.tsx +252 -1369
  245. package/src/canvas/comments-sidebar.test.ts +45 -0
  246. package/src/canvas/comments-sidebar.tsx +112 -73
  247. package/src/canvas/comments-visibility-toggle.tsx +25 -0
  248. package/src/canvas/context.ts +48 -0
  249. package/src/canvas/hooks.test.ts +94 -0
  250. package/src/canvas/hooks.ts +66 -11
  251. package/src/canvas/license.ts +1 -1
  252. package/src/canvas/mobile-placement.ts +115 -0
  253. package/src/canvas/options.test.ts +176 -4
  254. package/src/canvas/options.ts +276 -18
  255. package/src/canvas/pending-composer.tsx +157 -0
  256. package/src/canvas/pin-stacking.test.ts +224 -0
  257. package/src/canvas/pin-stacking.ts +103 -0
  258. package/src/canvas/region-box.tsx +124 -0
  259. package/src/canvas/sidebar-filters.ts +4 -3
  260. package/src/canvas/state.ts +88 -39
  261. package/src/canvas/thread-pin.tsx +418 -0
  262. package/src/canvas/thread-preview.test.ts +91 -0
  263. package/src/canvas/thread-preview.tsx +294 -0
  264. package/src/canvas/thread-stack.tsx +242 -0
  265. package/src/canvas/thread-state.test.ts +260 -0
  266. package/src/canvas/thread-state.ts +171 -37
  267. package/src/canvas/thread-view.test.ts +72 -0
  268. package/src/canvas/thread-view.tsx +614 -0
  269. package/src/clustering/computeClusterTable.test.ts +24 -23
  270. package/src/clustering/computeClusterTable.ts +17 -8
  271. package/src/clustering/replay.test.ts +0 -7
  272. package/src/clustering/replay.ts +131 -32
  273. package/src/clustering/runtime.test.ts +267 -13
  274. package/src/clustering/runtime.ts +176 -19
  275. package/src/clustering/schedule.test.ts +0 -6
  276. package/src/clustering/screen-offsets.test.ts +171 -0
  277. package/src/clustering/types.ts +19 -9
  278. package/src/index.ts +91 -42
  279. package/src/ui/byline.tsx +21 -9
  280. package/src/ui/comment-card.tsx +20 -10
  281. package/src/ui/comment-composer.tsx +178 -56
  282. package/src/ui/comment-pin.tsx +13 -2
  283. package/src/ui/comment-thread.tsx +13 -6
  284. package/src/ui/comments-list.test.ts +31 -0
  285. package/src/ui/comments-list.tsx +82 -47
  286. package/src/ui/comments.css +577 -294
  287. package/src/ui/count-badge.tsx +9 -2
  288. package/src/ui/emoji-picker.test.ts +25 -0
  289. package/src/ui/emoji-picker.tsx +76 -0
  290. package/src/ui/empty-state.tsx +3 -3
  291. package/src/ui/format-time.test.ts +69 -0
  292. package/src/ui/format-time.ts +27 -2
  293. package/src/ui/icons.tsx +116 -0
  294. package/src/ui/reaction-picker.tsx +102 -0
  295. package/src/ui/reaction.tsx +125 -6
  296. package/src/ui/reactions.tsx +69 -10
  297. package/src/ui/reply-count.ts +16 -0
  298. package/src/ui/send-button.tsx +13 -3
  299. package/src/ui/visual-viewport.test.ts +36 -0
  300. package/src/ui/visual-viewport.ts +27 -0
  301. package/dist-cjs/canvas/region-options.js +0 -49
  302. package/dist-cjs/canvas/region-options.js.map +0 -7
  303. package/dist-cjs/ui/avatar.js.map +0 -7
  304. package/dist-cjs/ui/comment-mention.js +0 -42
  305. package/dist-cjs/ui/comment-mention.js.map +0 -7
  306. package/dist-cjs/ui/comment-text.js.map +0 -7
  307. package/dist-cjs/ui/mention-list.js +0 -70
  308. package/dist-cjs/ui/mention-list.js.map +0 -7
  309. package/dist-cjs/ui/mention-suggestion.js +0 -206
  310. package/dist-cjs/ui/mention-suggestion.js.map +0 -7
  311. package/dist-cjs/ui/mention.js.map +0 -7
  312. package/dist-cjs/ui/render-markdown.js +0 -63
  313. package/dist-cjs/ui/render-markdown.js.map +0 -7
  314. package/dist-esm/canvas/region-options.mjs +0 -29
  315. package/dist-esm/canvas/region-options.mjs.map +0 -7
  316. package/dist-esm/ui/avatar.mjs +0 -23
  317. package/dist-esm/ui/avatar.mjs.map +0 -7
  318. package/dist-esm/ui/comment-mention.mjs +0 -22
  319. package/dist-esm/ui/comment-mention.mjs.map +0 -7
  320. package/dist-esm/ui/comment-text.mjs +0 -9
  321. package/dist-esm/ui/comment-text.mjs.map +0 -7
  322. package/dist-esm/ui/mention-list.mjs +0 -50
  323. package/dist-esm/ui/mention-list.mjs.map +0 -7
  324. package/dist-esm/ui/mention-suggestion.mjs +0 -186
  325. package/dist-esm/ui/mention-suggestion.mjs.map +0 -7
  326. package/dist-esm/ui/mention.mjs +0 -11
  327. package/dist-esm/ui/mention.mjs.map +0 -7
  328. package/dist-esm/ui/render-markdown.mjs +0 -45
  329. package/dist-esm/ui/render-markdown.mjs.map +0 -7
  330. package/src/canvas/region-options.ts +0 -57
  331. package/src/ui/avatar.tsx +0 -31
  332. package/src/ui/comment-mention.ts +0 -47
  333. package/src/ui/comment-text.tsx +0 -12
  334. package/src/ui/mention-list.tsx +0 -106
  335. package/src/ui/mention-suggestion.test.ts +0 -18
  336. package/src/ui/mention-suggestion.tsx +0 -285
  337. package/src/ui/mention.tsx +0 -9
  338. package/src/ui/render-markdown.tsx +0 -72
@@ -0,0 +1,45 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import type { CommentListItemProps } from '../ui/comments-list'
3
+ import { sortSidebarRows } from './comments-sidebar'
4
+
5
+ function row(id: string, lastActivity: number, resolved = false) {
6
+ return {
7
+ item: {
8
+ id,
9
+ resolved,
10
+ author: { id: 'user:1', name: 'A' },
11
+ preview: '',
12
+ date: '',
13
+ } as unknown as CommentListItemProps,
14
+ lastActivity,
15
+ }
16
+ }
17
+
18
+ describe('sortSidebarRows', () => {
19
+ it('orders threads by their most recent comment, not their first', () => {
20
+ // t1 was started first but was just replied to; t2 has been quiet since it was created.
21
+ const sorted = sortSidebarRows([row('t1', 300), row('t2', 200)])
22
+ expect(sorted.map((r) => r.item.id)).toEqual(['t1', 't2'])
23
+ })
24
+
25
+ it('sinks resolved threads below unresolved ones however recent they are', () => {
26
+ const sorted = sortSidebarRows([row('t1', 100), row('t2', 999, true), row('t3', 200)])
27
+ expect(sorted.map((r) => r.item.id)).toEqual(['t3', 't1', 't2'])
28
+ })
29
+
30
+ it('orders resolved threads among themselves by recency too', () => {
31
+ const sorted = sortSidebarRows([row('t1', 100, true), row('t2', 300, true)])
32
+ expect(sorted.map((r) => r.item.id)).toEqual(['t2', 't1'])
33
+ })
34
+
35
+ it('breaks activity ties by id so the list does not reshuffle', () => {
36
+ const sorted = sortSidebarRows([row('t3', 5), row('t1', 5), row('t2', 5)])
37
+ expect(sorted.map((r) => r.item.id)).toEqual(['t1', 't2', 't3'])
38
+ })
39
+
40
+ it('does not mutate the input', () => {
41
+ const input = [row('t1', 100), row('t2', 200)]
42
+ sortSidebarRows(input)
43
+ expect(input.map((r) => r.item.id)).toEqual(['t1', 't2'])
44
+ })
45
+ })
@@ -1,72 +1,61 @@
1
- import { ReactNode, useRef } from 'react'
2
- import { createPortal } from 'react-dom'
1
+ import { ReactNode, useCallback } from 'react'
3
2
  import {
3
+ EditorPortal,
4
4
  TLComment,
5
- TLCommentId,
6
- useContainer,
5
+ TLCommentThread,
7
6
  useEditor,
8
- usePassThroughMouseOverEvents,
9
7
  useTranslation,
10
8
  useValue,
11
9
  } from 'tldraw'
12
- import { CommentListItemProps, CommentsList } from '../ui/comments-list'
13
- import { UNKNOWN_AUTHOR } from './comment-render'
10
+ import { CommentListItemProps, CommentListItemRenderProps, CommentsList } from '../ui/comments-list'
11
+ import { UNKNOWN_COMMENT_AUTHOR } from './comment-render'
14
12
  import { CommentsFilterMenu } from './comments-filter-menu'
15
- import { CommentsOverflowMenu } from './comments-overflow-menu'
13
+ import { CommentsVisibilityToggle } from './comments-visibility-toggle'
14
+ import { type CommentingContext } from './context'
16
15
  import { useComments, useCommentThreads } from './hooks'
17
16
  import { useCommentingEnabled } from './license'
18
17
  import { useCommentingOptions } from './options'
19
18
  import { richTextToPlaintext } from './rich-text'
20
- import { openThreadId, sidebarFilters } from './state'
19
+ import { commentsSidebarOpen, openThreadId, sidebarFilters } from './state'
21
20
  import { focusThread } from './thread-state'
22
21
 
23
- /** @public */
24
- export interface CanvasCommentsSidebarProps {
25
- /** Map an author id to a display name, or `undefined` when the id can't be named. */
26
- resolveName(id: string): string | undefined
27
- /** The signed-in user's id. Enables the "only your threads" filter when present. */
28
- currentUserId?: string
29
- /**
30
- * Whether a comment is unread for the current user (return true for unread). Enables the
31
- * "only unread" filter when present.
32
- */
33
- isCommentUnread?(commentId: TLCommentId): boolean
34
- /** Tool ids that show the sidebar. Defaults to the comment tool. */
35
- tools?: string[]
22
+ /**
23
+ * The host wiring for {@link CanvasCommentsSidebar}: the {@link CommentingContext} fields it reads,
24
+ * plus the panel's own slots. A non-null `currentUserId` enables the "only your threads" filter, and
25
+ * an `isCommentUnread` the "only unread" one. `CanvasComments` takes the same fields, so a host
26
+ * mounting both can spread one object into each.
27
+ *
28
+ * @public
29
+ */
30
+ export interface CanvasCommentsSidebarProps extends Pick<
31
+ CommentingContext,
32
+ 'currentUserId' | 'resolveAuthor' | 'isCommentUnread' | 'getThreadHref'
33
+ > {
36
34
  /** Header above the list. */
37
35
  header?: ReactNode
38
36
  /** Shown when the page has no threads. */
39
37
  empty?: ReactNode
40
- /** Where imprecise shape pins sit, so navigation centres on the same spot. Default top-right. */
41
- impreciseShapeAnchor?: { x: number; y: number }
42
38
  }
43
39
 
44
40
  /**
45
- * A comments list panel for the current page, shown while the comment tool is active. Clicking a
46
- * thread brings its pin into view and opens it. Batteries-included over the store (a sibling to
47
- * `CanvasComments`); `CommentsList` is exported for a differently-placed or always-on list.
41
+ * A comments list panel for the current page, shown while {@link commentsSidebarOpen} is set (e.g.
42
+ * toggled by a button). Clicking a thread brings its pin into view and opens it. Batteries-included
43
+ * over the store (a sibling to `CanvasComments`); `CommentsList` is exported for a differently-placed
44
+ * or always-on list.
48
45
  * @public @react
49
46
  */
50
47
  export function CanvasCommentsSidebar(props: CanvasCommentsSidebarProps) {
51
- const {
52
- resolveName,
53
- currentUserId,
54
- isCommentUnread,
55
- tools,
56
- header,
57
- empty,
58
- impreciseShapeAnchor,
59
- } = props
48
+ const { resolveAuthor, currentUserId, isCommentUnread, header, empty, getThreadHref } = props
49
+ // Name-only view of the resolver, for the plaintext previews (which resolve @-mentions).
50
+ const resolveName = useCallback((id: string) => resolveAuthor(id)?.name, [resolveAuthor])
60
51
  const editor = useEditor()
61
52
  const options = useCommentingOptions()
62
- const sidebarTools = tools ?? ['comment']
63
- const container = useContainer()
64
53
  const commentingEnabled = useCommentingEnabled()
65
54
  const msg = useTranslation()
66
55
  const threads = useCommentThreads(editor)
67
56
  const comments = useComments(editor)
68
57
  const currentPageId = useValue('page id', () => editor.getCurrentPageId(), [editor])
69
- const activeTool = useValue('tool id', () => editor.getCurrentToolId(), [editor])
58
+ const open = useValue('sidebar open', () => commentsSidebarOpen.get(editor), [editor])
70
59
  const openId = useValue('open thread', () => openThreadId.get(editor), [editor])
71
60
  const filters = useValue('sidebar filters', () => sidebarFilters.get(editor), [editor])
72
61
  const pageNames = useValue(
@@ -75,7 +64,7 @@ export function CanvasCommentsSidebar(props: CanvasCommentsSidebarProps) {
75
64
  [editor]
76
65
  )
77
66
 
78
- if (!commentingEnabled || !sidebarTools.includes(activeTool)) return null
67
+ if (!commentingEnabled || !open) return null
79
68
 
80
69
  // Group comments by thread (they arrive oldest-first, so [0] is each thread's first comment).
81
70
  const byThread = new Map<string, TLComment[]>()
@@ -91,13 +80,12 @@ export function CanvasCommentsSidebar(props: CanvasCommentsSidebarProps) {
91
80
  (thread) => !filters.onlyCurrentPage || thread.pageId === currentPageId
92
81
  )
93
82
 
94
- const items: CommentListItemProps[] = pageThreads
83
+ const rows: SidebarRow[] = pageThreads
95
84
  .filter((thread) => filters.showResolved || thread.resolved == null)
96
85
  // "Only mine" is ignored without a known user — otherwise a persisted onlyMine=true would
97
86
  // empty the list for a signed-out viewer, with the (hidden) toggle giving no way to clear it.
98
87
  .filter(
99
- (thread) =>
100
- !filters.onlyMine || currentUserId === undefined || thread.createdBy === currentUserId
88
+ (thread) => !filters.onlyMine || currentUserId === null || thread.createdBy === currentUserId
101
89
  )
102
90
  // "Only unread" is likewise ignored without a read-status source.
103
91
  .filter(
@@ -109,6 +97,8 @@ export function CanvasCommentsSidebar(props: CanvasCommentsSidebarProps) {
109
97
  .map((thread) => {
110
98
  const threadComments = byThread.get(thread.id) ?? []
111
99
  const first = threadComments[0]
100
+ // Comments arrive oldest-first, so the last one is the thread's most recent activity.
101
+ const last = threadComments[threadComments.length - 1]
112
102
  let preview: ReactNode = ''
113
103
  // The `ThreadPreview` component slot overrides the built-in plaintext default.
114
104
  const ThreadPreview = options.components.ThreadPreview
@@ -120,41 +110,65 @@ export function CanvasCommentsSidebar(props: CanvasCommentsSidebarProps) {
120
110
  )
121
111
  }
122
112
  return {
123
- id: thread.id,
124
- author: resolveName(thread.createdBy) ?? UNKNOWN_AUTHOR,
125
- preview,
126
- date: new Date((first ?? thread).createdAt).toISOString(),
127
- resolved: thread.resolved != null,
128
- page: pageNames.get(thread.pageId),
129
- count: threadComments.length,
130
- selected: openId === thread.id,
113
+ item: {
114
+ id: thread.id,
115
+ author: resolveAuthor(thread.createdBy) ?? UNKNOWN_COMMENT_AUTHOR,
116
+ preview,
117
+ date: new Date((first ?? thread).createdAt).toISOString(),
118
+ resolved: thread.resolved != null,
119
+ // The page label only earns its place when it adds information: multiple pages
120
+ // exist, and the thread is somewhere other than where you already are.
121
+ page:
122
+ pageNames.size > 1 && thread.pageId !== currentPageId
123
+ ? pageNames.get(thread.pageId)
124
+ : undefined,
125
+ count: threadComments.length,
126
+ selected: openId === thread.id,
127
+ href: getThreadHref?.(thread.id),
128
+ },
129
+ lastActivity: (last ?? thread).createdAt,
131
130
  }
132
131
  })
133
- // unresolved first, then most-recent first
134
- .sort((a, b) => {
135
- if (!!a.resolved !== !!b.resolved) return a.resolved ? 1 : -1
136
- return b.date.localeCompare(a.date)
137
- })
132
+
133
+ const items = sortSidebarRows(rows).map((row) => row.item)
138
134
 
139
135
  const focus = (id: string) => {
140
136
  const thread = threads.find((t) => t.id === id)
141
- // Resolve prop-or-option like the overlay pins do, so sidebar navigation centers on the same
142
- // spot the pin renders at when the anchor is configured via `CommentTool.configure`.
143
- if (thread) focusThread(editor, thread, impreciseShapeAnchor ?? options.impreciseShapeAnchor)
137
+ if (thread) focusThread(editor, thread)
144
138
  }
145
139
 
140
+ // The `ThreadRow` component slot overrides the built-in row. It gets the thread record alongside
141
+ // the summarized row, so host state on `thread.meta` is in reach without a second lookup. Indexed
142
+ // once rather than scanned per row — this runs for every row on every render of the panel.
143
+ const ThreadRow = options.components.ThreadRow
144
+ const renderItem = ThreadRow
145
+ ? (() => {
146
+ // Keyed by plain string: a row's id comes back as one, not as a branded thread id.
147
+ const threadsById = new Map<string, TLCommentThread>(
148
+ threads.map((thread) => [thread.id, thread])
149
+ )
150
+ return (rowProps: CommentListItemRenderProps) => {
151
+ const thread = threadsById.get(rowProps.id)
152
+ // Can't happen — the items are built from these threads — but a row with no thread has
153
+ // nothing to render, and the slot's contract says the record is there.
154
+ if (!thread) return null
155
+ return <ThreadRow {...rowProps} thread={thread} />
156
+ }
157
+ })()
158
+ : undefined
159
+
146
160
  return (
147
- <SidebarPanel container={container}>
161
+ <SidebarPanel>
148
162
  <CommentsList
149
163
  items={items}
150
164
  header={header ?? msg('comments.title')}
151
165
  headerAction={
152
- <div className="cmt-list__header-actions">
166
+ <div className="tlui-cmt-list__header-actions">
153
167
  <CommentsFilterMenu
154
- canFilterByAuthor={currentUserId !== undefined}
168
+ canFilterByAuthor={currentUserId !== null}
155
169
  canFilterByUnread={isCommentUnread !== undefined}
156
170
  />
157
- <CommentsOverflowMenu />
171
+ <CommentsVisibilityToggle />
158
172
  </div>
159
173
  }
160
174
  empty={
@@ -164,21 +178,46 @@ export function CanvasCommentsSidebar(props: CanvasCommentsSidebarProps) {
164
178
  }
165
179
  resolvedLabel={msg('comments.resolved')}
166
180
  onSelect={focus}
181
+ renderItem={renderItem}
167
182
  />
168
183
  </SidebarPanel>
169
184
  )
170
185
  }
171
186
 
187
+ /** A list row paired with the sort key that isn't part of what the row displays. @public */
188
+ export interface SidebarRow {
189
+ item: CommentListItemProps
190
+ /** When the thread's most recent comment was posted — what the list orders by. */
191
+ lastActivity: number
192
+ }
193
+
194
+ /**
195
+ * Order the list: unresolved threads first, then by most recent activity, id as a stable tiebreak.
196
+ * Recency is the thread's *latest* comment, not its first, so a thread someone just replied to rises
197
+ * to the top instead of staying wherever it was started. (The row still shows the thread's opening
198
+ * comment and its date — that's what identifies the thread; only the ordering follows the replies.)
199
+ *
200
+ * Exported so a hand-built list can match the sidebar's ordering instead of re-deriving it.
201
+ *
202
+ * @public
203
+ */
204
+ export function sortSidebarRows(rows: readonly SidebarRow[]): readonly SidebarRow[] {
205
+ return [...rows].sort(
206
+ (a, b) =>
207
+ Number(!!a.item.resolved) - Number(!!b.item.resolved) ||
208
+ b.lastActivity - a.lastActivity ||
209
+ (a.item.id < b.item.id ? -1 : 1)
210
+ )
211
+ }
212
+
172
213
  /** The sidebar surface, portaled into the container. It scrolls its own list, so — unlike tldraw's
173
- * wheel-transparent panels — a wheel over it doesn't pan the canvas. Hover still passes through so
174
- * shapes beneath it stay interactive. */
175
- function SidebarPanel({ container, children }: { container: HTMLElement; children: ReactNode }) {
176
- const ref = useRef<HTMLDivElement>(null)
177
- usePassThroughMouseOverEvents(ref)
178
- return createPortal(
179
- <div ref={ref} className="cmt-canvas-sidebar">
180
- {children}
181
- </div>,
182
- container
214
+ * wheel-transparent panels — a wheel over it doesn't pan the canvas. */
215
+ function SidebarPanel({ children }: { children: ReactNode }) {
216
+ return (
217
+ <EditorPortal>
218
+ <div className="tlui-cmt-canvas-sidebar" onContextMenu={(e) => e.stopPropagation()}>
219
+ {children}
220
+ </div>
221
+ </EditorPortal>
183
222
  )
184
223
  }
@@ -0,0 +1,25 @@
1
+ import { TldrawUiButton, useEditor, useTranslation, useValue } from 'tldraw'
2
+ import { EyeClosedIcon, EyeOpenIcon } from '../ui/icons'
3
+ import { commentsHidden, toggleCommentsHidden } from './state'
4
+
5
+ /** The sidebar header's show/hide toggle for comment pins — an eye that closes while comments
6
+ * are hidden. The same state as the Shift+C shortcut.
7
+ * @public @react */
8
+ export function CommentsVisibilityToggle() {
9
+ const editor = useEditor()
10
+ const msg = useTranslation()
11
+ const hidden = useValue('comments hidden', () => commentsHidden.get(editor), [editor])
12
+ const label = hidden ? msg('comments.show') : msg('comments.hide')
13
+
14
+ return (
15
+ <TldrawUiButton
16
+ type="icon"
17
+ tooltip={label}
18
+ title={label}
19
+ className="tlui-cmt-header-btn"
20
+ onClick={() => toggleCommentsHidden(editor)}
21
+ >
22
+ {hidden ? <EyeClosedIcon /> : <EyeOpenIcon />}
23
+ </TldrawUiButton>
24
+ )
25
+ }
@@ -0,0 +1,48 @@
1
+ import { type CommentAuthor, type MentionMember } from '@tldraw/mentions'
2
+ import { type ReactNode } from 'react'
3
+ import { type TLComment, type TLCommentId, type TLCommentThreadId } from 'tldraw'
4
+
5
+ /**
6
+ * The live, host-supplied half of commenting: who the viewer is, how author ids become names, read
7
+ * status, and the mention roster. (The static half is {@link CommentingOptions}, configured once on
8
+ * the tool.)
9
+ *
10
+ * Every commenting surface reads the same set, so a host mounting more than one — `CanvasComments`
11
+ * and `CanvasCommentsSidebar`, say — can build it once and spread it into each rather than
12
+ * repeating the props:
13
+ *
14
+ * ```tsx
15
+ * const commenting: CommentingContext = { currentUserId, resolveAuthor, isCommentUnread }
16
+ *
17
+ * <CanvasComments {...commenting} />
18
+ * <CanvasCommentsSidebar {...commenting} />
19
+ * ```
20
+ *
21
+ * @public
22
+ */
23
+ export interface CommentingContext {
24
+ /** The signed-in user's id, or null for a read-only viewer. Only a signed-in user composes. */
25
+ currentUserId: string | null
26
+ /** Map an author id to their display info, or `undefined` when the id can't be resolved. */
27
+ resolveAuthor(id: string): CommentAuthor | undefined
28
+ /** Called after any comment (a new thread's first comment, or a reply) is posted. */
29
+ onPostComment?(comment: TLComment): void
30
+ /** Whether a comment is unread for the current user (return true for unread). */
31
+ isCommentUnread?(commentId: TLCommentId): boolean
32
+ /**
33
+ * Called with every unread comment shown to the user in an open thread popover, batched per
34
+ * report, so hosts can record read receipts without a write per comment. Needs
35
+ * `isCommentUnread` to know what's unread.
36
+ */
37
+ onCommentsRead?(commentIds: TLCommentId[]): void
38
+ /** Resolve the members matching an `@`-query in the composers (sync or async). */
39
+ getMentionSuggestions?(query: string): MentionMember[] | Promise<MentionMember[]>
40
+ /** Override a mention-picker row's content. */
41
+ renderMentionSuggestion?(member: MentionMember): ReactNode
42
+ /**
43
+ * The host's URL for a thread, so surfaces can link to it — e.g. the sidebar's rows render as
44
+ * anchors when this is present, letting ctrl/cmd-click and middle-click open the thread in a
45
+ * new tab (a plain click still selects the thread in place; the href isn't followed).
46
+ */
47
+ getThreadHref?(threadId: TLCommentThreadId): string | undefined
48
+ }
@@ -0,0 +1,94 @@
1
+ import {
2
+ commentSchemaRecords,
3
+ createComment,
4
+ createCommentThread,
5
+ createTLSchema,
6
+ createTLStore,
7
+ defaultBindingUtils,
8
+ defaultShapeUtils,
9
+ defaultTools,
10
+ Editor,
11
+ toRichText,
12
+ } from 'tldraw'
13
+ import { afterEach, describe, expect, it } from 'vitest'
14
+ import { putCommentRecords } from './comment-mutations'
15
+ import { getCommentsByThread } from './hooks'
16
+
17
+ let editors: Editor[] = []
18
+
19
+ afterEach(() => {
20
+ for (const editor of editors) editor.dispose()
21
+ editors = []
22
+ })
23
+
24
+ function makeEditor(): Editor {
25
+ const editor = new Editor({
26
+ store: createTLStore({ schema: createTLSchema({ records: commentSchemaRecords }) }),
27
+ shapeUtils: defaultShapeUtils,
28
+ bindingUtils: defaultBindingUtils,
29
+ tools: defaultTools,
30
+ getContainer: () => document.body,
31
+ })
32
+ editors.push(editor)
33
+ return editor
34
+ }
35
+
36
+ function makeThreadWithComment(editor: Editor, text: string) {
37
+ const pageId = editor.getCurrentPageId()
38
+ const thread = createCommentThread({
39
+ pageId,
40
+ anchor: { type: 'point', x: 0, y: 0 },
41
+ createdBy: 'user1',
42
+ })
43
+ const comment = createComment({
44
+ threadId: thread.id,
45
+ pageId,
46
+ authorId: 'user1',
47
+ body: toRichText(text),
48
+ })
49
+ return { thread, comment }
50
+ }
51
+
52
+ describe('getCommentsByThread', () => {
53
+ it('groups live comments by thread, oldest first', () => {
54
+ const editor = makeEditor()
55
+ const { thread, comment } = makeThreadWithComment(editor, 'first')
56
+ const reply = createComment({
57
+ threadId: thread.id,
58
+ pageId: editor.getCurrentPageId(),
59
+ authorId: 'user2',
60
+ body: toRichText('second'),
61
+ now: comment.createdAt + 1,
62
+ })
63
+ // Inserted newest-first to prove the grouping sorts by createdAt rather than store order.
64
+ putCommentRecords(editor, [thread, reply, comment])
65
+ expect(getCommentsByThread(editor).get().get(thread.id)).toEqual([comment, reply])
66
+ })
67
+
68
+ it("reuses a thread's array when another thread's comments change", () => {
69
+ const editor = makeEditor()
70
+ const a = makeThreadWithComment(editor, 'thread a')
71
+ const b = makeThreadWithComment(editor, 'thread b')
72
+ putCommentRecords(editor, [a.thread, a.comment, b.thread, b.comment])
73
+
74
+ const byThread = getCommentsByThread(editor)
75
+ const before = byThread.get()
76
+
77
+ const replyToB = createComment({
78
+ threadId: b.thread.id,
79
+ pageId: editor.getCurrentPageId(),
80
+ authorId: 'user2',
81
+ body: toRichText('reply'),
82
+ now: b.comment.createdAt + 1,
83
+ })
84
+ putCommentRecords(editor, [replyToB])
85
+ const after = byThread.get()
86
+
87
+ expect(after).not.toBe(before)
88
+ // The unchanged thread keeps its exact array, so its subscribers don't re-render...
89
+ expect(after.get(a.thread.id)).toBe(before.get(a.thread.id))
90
+ // ...while the changed thread gets a fresh one with the reply in place.
91
+ expect(after.get(b.thread.id)).not.toBe(before.get(b.thread.id))
92
+ expect(after.get(b.thread.id)).toEqual([b.comment, replyToB])
93
+ })
94
+ })
@@ -1,28 +1,83 @@
1
- import { Editor, TLComment, TLCommentThread, TLCommentThreadId, useValue } from 'tldraw'
2
- import { getComments, getCommentThreads } from './comment-store'
1
+ import {
2
+ computed,
3
+ Computed,
4
+ Editor,
5
+ isUninitialized,
6
+ TLComment,
7
+ TLCommentThread,
8
+ TLCommentThreadId,
9
+ useValue,
10
+ WeakCache,
11
+ } from 'tldraw'
12
+ import { getLiveComments, getLiveCommentThreads } from './comment-store'
3
13
 
4
- /** All comment threads in the store, reactively. @public */
14
+ /**
15
+ * The comment threads that should render (pins, sidebar), reactively — the live set described by
16
+ * {@link getLiveCommentThreads}. Use `getCommentThreads` for the unfiltered set, including
17
+ * soft-deleted threads awaiting the server's prune.
18
+ *
19
+ * @public
20
+ */
5
21
  export function useCommentThreads(editor: Editor): TLCommentThread[] {
6
- return useValue('comment threads', () => getCommentThreads(editor), [editor])
22
+ return useValue('comment threads', () => getLiveCommentThreads(editor), [editor])
7
23
  }
8
24
 
9
- /** A thread's comments, oldest first, reactively. @public */
25
+ /**
26
+ * Every live comment grouped by thread, oldest first within each thread. One computed shared by
27
+ * every mounted thread: each `useThreadComments` would otherwise scan and sort the whole comment
28
+ * set, so N open threads did N full passes on every comment mutation. Cached per editor so the
29
+ * grouping survives re-renders and is rebuilt only when the comment set actually changes.
30
+ */
31
+ const commentsByThread = new WeakCache<Editor, Computed<Map<TLCommentThreadId, TLComment[]>>>()
32
+
33
+ /** Exported for tests; use {@link useThreadComments} to consume. @internal */
34
+ export function getCommentsByThread(editor: Editor) {
35
+ return commentsByThread.get(editor, () =>
36
+ computed('comments by thread', (prev) => {
37
+ const byThread = new Map<TLCommentThreadId, TLComment[]>()
38
+ for (const comment of getLiveComments(editor)) {
39
+ const existing = byThread.get(comment.threadId)
40
+ if (existing) existing.push(comment)
41
+ else byThread.set(comment.threadId, [comment])
42
+ }
43
+ for (const [threadId, comments] of byThread) {
44
+ comments.sort((a, b) => a.createdAt - b.createdAt)
45
+ // Reuse the previous per-thread array when that thread's comment list is unchanged
46
+ // (record identities are stable while unchanged), so `useThreadComments` subscribers
47
+ // on other threads don't re-render when one thread gains a reply.
48
+ if (isUninitialized(prev)) continue
49
+ const prevComments = prev.get(threadId)
50
+ if (
51
+ prevComments &&
52
+ prevComments.length === comments.length &&
53
+ comments.every((comment, i) => prevComments[i] === comment)
54
+ ) {
55
+ byThread.set(threadId, prevComments)
56
+ }
57
+ }
58
+ return byThread
59
+ })
60
+ )
61
+ }
62
+
63
+ /** A thread's live comments, oldest first, reactively. @public */
10
64
  export function useThreadComments(editor: Editor, threadId: TLCommentThreadId): TLComment[] {
11
65
  return useValue(
12
66
  'thread comments',
13
- () =>
14
- getComments(editor)
15
- .filter((c) => c.threadId === threadId)
16
- .sort((a, b) => a.createdAt - b.createdAt),
67
+ () => getCommentsByThread(editor).get().get(threadId) ?? EMPTY_COMMENTS,
17
68
  [editor, threadId]
18
69
  )
19
70
  }
20
71
 
21
- /** Every comment in the store, oldest first, reactively. Group by `threadId` for per-thread lists. @public */
72
+ /** Shared empty result so a thread with no comments doesn't churn referential equality. */
73
+ const EMPTY_COMMENTS: TLComment[] = []
74
+
75
+ /** Every live comment in the store ({@link getLiveComments}), oldest first, reactively. Group by
76
+ * `threadId` for per-thread lists. @public */
22
77
  export function useComments(editor: Editor): TLComment[] {
23
78
  return useValue(
24
79
  'all comments',
25
- () => getComments(editor).sort((a, b) => a.createdAt - b.createdAt),
80
+ () => getLiveComments(editor).sort((a, b) => a.createdAt - b.createdAt),
26
81
  [editor]
27
82
  )
28
83
  }
@@ -7,7 +7,7 @@ import { useLicenseContext, useLicenseFeatureFlag } from 'tldraw'
7
7
  * pending, so gated UI stays hidden until the license is confirmed.
8
8
  *
9
9
  * The built-in commenting components (`CanvasComments`, `CanvasCommentsSidebar`, and the comment
10
- * tool's toolbar button) gate on this. Use it to gate any custom commenting UI the same way.
10
+ * tool's Quick Action) gate on this. Use it to gate any custom commenting UI the same way.
11
11
  * @public
12
12
  */
13
13
  export function useCommentingEnabled(): boolean {