@jant/core 0.6.14 → 0.6.16

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 (350) hide show
  1. package/bin/commands/import-site.js +88 -13
  2. package/bin/commands/migrate.js +55 -0
  3. package/bin/commands/posts/rebuild-html.js +184 -0
  4. package/bin/commands/site/snapshot/import.js +14 -10
  5. package/bin/lib/d1-query.js +23 -5
  6. package/bin/lib/migration-runner.js +29 -7
  7. package/bin/lib/node-database.js +32 -0
  8. package/bin/lib/site-snapshot.js +347 -46
  9. package/bin/lib/sql-export.js +1 -1
  10. package/bin/lib/thread-collection-migration.js +326 -0
  11. package/dist/app-C75Zc8dD.js +6 -0
  12. package/dist/{app-2i7-WQHg.js → app-C7_4oab9.js} +7774 -6425
  13. package/dist/client/.vite/manifest.json +8 -8
  14. package/dist/client/_assets/chunks/{url-pLre2DM_.js → url-CU9pEbk5.js} +1 -1
  15. package/dist/client/_assets/{client-BvID5ucz.js → client-Bq0vre8Y.js} +59 -38
  16. package/dist/client/_assets/client-D3VZyG4L.css +2 -0
  17. package/dist/client/_assets/client-auth-F2rb9WfF.js +5571 -0
  18. package/dist/{env-OHRKGcMj.js → env-BPYViDJJ.js} +1 -1
  19. package/dist/{export-DWn149b7.js → export-Dl5KCiEs.js} +1348 -128
  20. package/dist/{github-api-BgSiE71w.js → github-api-BNF35Lkx.js} +1 -1
  21. package/dist/{github-app-BbklkFmU.js → github-app-DdiD-bC4.js} +1 -1
  22. package/dist/{github-sync-C0zyF6XS.js → github-sync-Bqg62IvV.js} +3 -3
  23. package/dist/{github-sync-Ckk0sJxK.js → github-sync-HdK2EgvJ.js} +3 -3
  24. package/dist/index.js +5 -5
  25. package/dist/node.js +7 -7
  26. package/dist/{url-BMYO-Zlt.js → url-CbLAtlZe.js} +517 -12
  27. package/package.json +11 -10
  28. package/src/__tests__/bin/posts-rebuild-html.test.ts +137 -0
  29. package/src/__tests__/dev-navigation-defaults.test.ts +22 -0
  30. package/src/__tests__/export-hugo-build.test.ts +63 -4
  31. package/src/__tests__/export-import-roundtrip.test.ts +6 -4
  32. package/src/__tests__/export-service.test.ts +142 -9
  33. package/src/__tests__/helpers/app.ts +3 -0
  34. package/src/__tests__/helpers/export-fixtures.ts +2 -0
  35. package/src/__tests__/import-site-command.test.ts +65 -0
  36. package/src/__tests__/mise-config.test.ts +22 -0
  37. package/src/__tests__/vite-config.test.ts +45 -0
  38. package/src/app.tsx +16 -3
  39. package/src/client/__tests__/collection-created-notice.test.ts +36 -0
  40. package/src/client/__tests__/collection-form-bridge.test.ts +10 -1
  41. package/src/client/__tests__/collection-page-actions.test.ts +71 -2
  42. package/src/client/__tests__/compose-bridge.test.ts +316 -2
  43. package/src/client/__tests__/compose-shortcuts.test.ts +20 -0
  44. package/src/client/__tests__/feed-video-player.test.ts +16 -1
  45. package/src/client/__tests__/footnote-rail.test.ts +472 -0
  46. package/src/client/__tests__/hydrate-partial.test.ts +3 -0
  47. package/src/client/__tests__/toast.test.ts +82 -1
  48. package/src/client/collection-created-notice.ts +66 -0
  49. package/src/client/collection-form-bridge.ts +17 -0
  50. package/src/client/collection-navigation.ts +44 -0
  51. package/src/client/collection-page-actions.ts +57 -1
  52. package/src/client/components/__tests__/compose-format-convert.test.ts +0 -19
  53. package/src/client/components/__tests__/jant-collection-directory.test.ts +155 -6
  54. package/src/client/components/__tests__/jant-command-palette.test.ts +51 -0
  55. package/src/client/components/__tests__/jant-compose-dialog.test.ts +2268 -206
  56. package/src/client/components/__tests__/jant-compose-editor.test.ts +147 -59
  57. package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +114 -64
  58. package/src/client/components/__tests__/jant-config-editor.test.ts +405 -0
  59. package/src/client/components/__tests__/jant-media-lightbox.test.ts +121 -0
  60. package/src/client/components/__tests__/jant-nav-manager.test.ts +538 -2
  61. package/src/client/components/__tests__/jant-post-form.test.ts +2 -2
  62. package/src/client/components/__tests__/jant-post-menu.test.ts +47 -5
  63. package/src/client/components/__tests__/jant-settings-avatar.test.ts +2 -2
  64. package/src/client/components/__tests__/jant-settings-general.test.ts +3 -3
  65. package/src/client/components/collection-manager-types.ts +9 -3
  66. package/src/client/components/compose-format-convert.ts +5 -1
  67. package/src/client/components/compose-types.ts +55 -3
  68. package/src/client/components/jant-collection-directory.ts +165 -30
  69. package/src/client/components/jant-command-palette.ts +14 -7
  70. package/src/client/components/jant-compose-dialog.ts +1542 -694
  71. package/src/client/components/jant-compose-editor.ts +237 -99
  72. package/src/client/components/jant-compose-fullscreen.ts +27 -9
  73. package/src/client/components/jant-config-editor.ts +722 -0
  74. package/src/client/components/jant-media-lightbox.ts +100 -18
  75. package/src/client/components/jant-nav-manager.ts +1283 -54
  76. package/src/client/components/jant-post-form.ts +3 -3
  77. package/src/client/components/jant-post-menu.ts +177 -95
  78. package/src/client/components/nav-manager-types.ts +51 -2
  79. package/src/client/components/post-form-types.ts +1 -1
  80. package/src/client/compose-bridge.ts +147 -123
  81. package/src/client/compose-launch.ts +13 -0
  82. package/src/client/compose-shortcuts.ts +5 -1
  83. package/src/client/feed-video-player.ts +62 -2
  84. package/src/client/footnote-rail.ts +478 -0
  85. package/src/client/hydrate-partial.ts +2 -0
  86. package/src/client/media-lightbox-events.ts +63 -0
  87. package/src/client/post-refresh.ts +135 -0
  88. package/src/client/site-header-fragment.ts +38 -0
  89. package/src/client/tiptap/__tests__/blockquote-editing.test.ts +201 -0
  90. package/src/client/tiptap/__tests__/clear-formatting.test.ts +168 -0
  91. package/src/client/tiptap/__tests__/link-input-rules.test.ts +121 -0
  92. package/src/client/tiptap/__tests__/list-editing.test.ts +447 -0
  93. package/src/client/tiptap/__tests__/markdown-clipboard.test.ts +727 -4
  94. package/src/client/tiptap/__tests__/table-controls.test.ts +263 -0
  95. package/src/client/tiptap/__tests__/table-size-picker.test.ts +179 -0
  96. package/src/client/tiptap/bubble-menu.ts +88 -10
  97. package/src/client/tiptap/continuous-lists.ts +102 -0
  98. package/src/client/tiptap/create-editor.ts +3 -0
  99. package/src/client/tiptap/extensions.ts +14 -5
  100. package/src/client/tiptap/link-input-rules.ts +4 -4
  101. package/src/client/tiptap/markdown-clipboard.ts +608 -11
  102. package/src/client/tiptap/slash-commands.ts +8 -1
  103. package/src/client/tiptap/structural-keymap.ts +211 -0
  104. package/src/client/tiptap/tab-indent.ts +8 -22
  105. package/src/client/tiptap/table-control-labels.ts +30 -0
  106. package/src/client/tiptap/table-controls.ts +537 -0
  107. package/src/client/tiptap/table-size-picker.ts +254 -0
  108. package/src/client/toast.ts +133 -39
  109. package/src/client-auth.ts +1 -0
  110. package/src/client.ts +1 -0
  111. package/src/db/__tests__/backfill-thread-activity.test.ts +234 -0
  112. package/src/db/__tests__/d1-query.test.ts +43 -1
  113. package/src/db/__tests__/migration-runner.test.ts +63 -0
  114. package/src/db/__tests__/migrations.test.ts +469 -0
  115. package/src/db/__tests__/thread-activity.test.ts +113 -0
  116. package/src/db/__tests__/thread-collection-migration.test.ts +364 -0
  117. package/src/db/backfills/0005_split_thread_activity_from_quiet_replies.sql +68 -0
  118. package/src/db/migrations/0027_old_blue_blade.sql +39 -0
  119. package/src/db/migrations/0028_superb_post.sql +2 -0
  120. package/src/db/migrations/0029_boring_magma.sql +1 -0
  121. package/src/db/migrations/0030_typical_vivisector.sql +50 -0
  122. package/src/db/migrations/0031_many_ego.sql +3 -0
  123. package/src/db/migrations/meta/0027_snapshot.json +2511 -0
  124. package/src/db/migrations/meta/0028_snapshot.json +2511 -0
  125. package/src/db/migrations/meta/0029_snapshot.json +2519 -0
  126. package/src/db/migrations/meta/0030_snapshot.json +2541 -0
  127. package/src/db/migrations/meta/0031_snapshot.json +2562 -0
  128. package/src/db/migrations/meta/_journal.json +35 -0
  129. package/src/db/migrations/pg/0025_moaning_epoch.sql +39 -0
  130. package/src/db/migrations/pg/0026_talented_killer_shrike.sql +2 -0
  131. package/src/db/migrations/pg/0027_short_cannonball.sql +1 -0
  132. package/src/db/migrations/pg/0028_perfect_sharon_carter.sql +27 -0
  133. package/src/db/migrations/pg/0029_rare_hairball.sql +3 -0
  134. package/src/db/migrations/pg/meta/0025_snapshot.json +3204 -0
  135. package/src/db/migrations/pg/meta/0026_snapshot.json +3204 -0
  136. package/src/db/migrations/pg/meta/0027_snapshot.json +3211 -0
  137. package/src/db/migrations/pg/meta/0028_snapshot.json +3248 -0
  138. package/src/db/migrations/pg/meta/0029_snapshot.json +3289 -0
  139. package/src/db/migrations/pg/meta/_journal.json +35 -0
  140. package/src/db/pg/schema.ts +46 -20
  141. package/src/db/rehearsal-fixtures/demo-current.json +39 -4
  142. package/src/db/rehearsal-fixtures/demo-current.sql +121 -6
  143. package/src/db/schema.ts +48 -20
  144. package/src/db/thread-activity.ts +101 -0
  145. package/src/i18n/__tests__/detect.test.ts +8 -2
  146. package/src/i18n/detect.ts +14 -5
  147. package/src/i18n/locales/public/en.po +222 -51
  148. package/src/i18n/locales/public/en.ts +1 -1
  149. package/src/i18n/locales/public/zh-Hans.po +216 -45
  150. package/src/i18n/locales/public/zh-Hans.ts +1 -1
  151. package/src/i18n/locales/public/zh-Hant.po +216 -45
  152. package/src/i18n/locales/public/zh-Hant.ts +1 -1
  153. package/src/i18n/locales/settings/en.po +513 -10
  154. package/src/i18n/locales/settings/en.ts +1 -1
  155. package/src/i18n/locales/settings/zh-Hans.po +515 -12
  156. package/src/i18n/locales/settings/zh-Hans.ts +1 -1
  157. package/src/i18n/locales/settings/zh-Hant.po +515 -12
  158. package/src/i18n/locales/settings/zh-Hant.ts +1 -1
  159. package/src/lib/__tests__/api-settings.test.ts +299 -0
  160. package/src/lib/__tests__/constants.test.ts +5 -0
  161. package/src/lib/__tests__/feed-policy.test.ts +25 -0
  162. package/src/lib/__tests__/feed.test.ts +168 -0
  163. package/src/lib/__tests__/footnotes-upgrade.test.ts +181 -0
  164. package/src/lib/__tests__/markdown-roundtrip-embed.test.ts +61 -0
  165. package/src/lib/__tests__/markdown-to-tiptap.test.ts +32 -4
  166. package/src/lib/__tests__/markdown.test.ts +24 -6
  167. package/src/lib/__tests__/navigation.test.ts +59 -2
  168. package/src/lib/__tests__/post-body-html.test.ts +176 -0
  169. package/src/lib/__tests__/post-display.test.ts +64 -0
  170. package/src/lib/__tests__/resolve-config.test.ts +113 -1
  171. package/src/lib/__tests__/slug.test.ts +18 -0
  172. package/src/lib/__tests__/summary.test.ts +82 -0
  173. package/src/lib/__tests__/timeline.test.ts +109 -70
  174. package/src/lib/__tests__/timezones.test.ts +31 -3
  175. package/src/lib/__tests__/tiptap-render.test.ts +268 -9
  176. package/src/lib/__tests__/tiptap-to-markdown.test.ts +6 -0
  177. package/src/lib/__tests__/translit.test.ts +87 -0
  178. package/src/lib/__tests__/url.test.ts +53 -0
  179. package/src/lib/__tests__/view.test.ts +53 -0
  180. package/src/lib/__tests__/worker-response-cache.test.ts +18 -1
  181. package/src/lib/api-settings.ts +239 -12
  182. package/src/lib/collection-sort.ts +7 -5
  183. package/src/lib/constants.ts +2 -0
  184. package/src/lib/feed-path.ts +25 -0
  185. package/src/lib/feed-policy.ts +64 -0
  186. package/src/lib/feed.ts +113 -11
  187. package/src/lib/footnotes.ts +264 -0
  188. package/src/lib/github-sync-site-config.ts +2 -0
  189. package/src/lib/hugo-markdown.ts +15 -3
  190. package/src/lib/jant-branding.ts +1 -0
  191. package/src/lib/link-preview.ts +25 -0
  192. package/src/lib/markdown-manager.ts +193 -5
  193. package/src/lib/markdown.ts +10 -3
  194. package/src/lib/navigation.ts +16 -3
  195. package/src/lib/post-body-html.ts +131 -0
  196. package/src/lib/post-display.ts +47 -8
  197. package/src/lib/render.tsx +9 -2
  198. package/src/lib/resolve-config.ts +100 -16
  199. package/src/lib/schemas.ts +133 -1
  200. package/src/lib/settings-paths.ts +5 -0
  201. package/src/lib/site-skill.ts +78 -0
  202. package/src/lib/summary.ts +16 -5
  203. package/src/lib/timeline.ts +137 -131
  204. package/src/lib/timezones.ts +54 -14
  205. package/src/lib/tiptap-render.ts +359 -83
  206. package/src/lib/translit.ts +288 -0
  207. package/src/lib/url.ts +123 -12
  208. package/src/lib/view.ts +58 -20
  209. package/src/lib/worker-response-cache.ts +11 -18
  210. package/src/middleware/__tests__/public-content-access.test.ts +91 -0
  211. package/src/middleware/config.ts +2 -2
  212. package/src/middleware/public-content-access.ts +87 -0
  213. package/src/node/__tests__/cli-export.test.ts +14 -0
  214. package/src/node/__tests__/cli-migrate.test.ts +6 -0
  215. package/src/node/__tests__/cli-site-snapshot.test.ts +205 -2
  216. package/src/node/__tests__/cli-snapshot-meta.test.ts +100 -0
  217. package/src/node/index.ts +2 -1
  218. package/src/preset.css +16 -2
  219. package/src/routes/__tests__/compose.test.ts +51 -0
  220. package/src/routes/__tests__/site-skill.test.ts +112 -0
  221. package/src/routes/api/__tests__/collections.test.ts +91 -19
  222. package/src/routes/api/__tests__/mcp.test.ts +65 -1
  223. package/src/routes/api/__tests__/nav-items.test.ts +91 -0
  224. package/src/routes/api/__tests__/palette.test.ts +44 -0
  225. package/src/routes/api/__tests__/public-access.test.ts +81 -0
  226. package/src/routes/api/__tests__/settings.test.ts +217 -0
  227. package/src/routes/api/collections.ts +22 -26
  228. package/src/routes/api/export.ts +2 -0
  229. package/src/routes/api/internal/__tests__/post-body-html.test.ts +163 -0
  230. package/src/routes/api/internal/__tests__/sites.test.ts +35 -2
  231. package/src/routes/api/internal/post-body-html.ts +43 -0
  232. package/src/routes/api/internal/sites.ts +24 -0
  233. package/src/routes/api/nav-items.ts +24 -1
  234. package/src/routes/api/posts.ts +14 -10
  235. package/src/routes/api/public/__tests__/archive.test.ts +35 -0
  236. package/src/routes/api/public/__tests__/posts.test.ts +246 -0
  237. package/src/routes/api/public/archive.ts +4 -0
  238. package/src/routes/api/public/posts.ts +48 -24
  239. package/src/routes/api/search.ts +3 -0
  240. package/src/routes/api/settings.ts +33 -0
  241. package/src/routes/auth/__tests__/setup.test.ts +7 -4
  242. package/src/routes/compose.tsx +8 -3
  243. package/src/routes/dash/settings.tsx +45 -2
  244. package/src/routes/feed/__tests__/feed.test.ts +210 -6
  245. package/src/routes/feed/__tests__/sitemap.test.ts +68 -1
  246. package/src/routes/feed/feed.ts +23 -14
  247. package/src/routes/pages/__tests__/archive-params.test.ts +384 -3
  248. package/src/routes/pages/__tests__/collection-routing.test.ts +63 -5
  249. package/src/routes/pages/__tests__/collections.test.ts +9 -9
  250. package/src/routes/pages/__tests__/featured.test.ts +6 -2
  251. package/src/routes/pages/__tests__/preview.test.ts +176 -0
  252. package/src/routes/pages/__tests__/search.test.ts +69 -0
  253. package/src/routes/pages/archive.tsx +90 -22
  254. package/src/routes/pages/collection.tsx +32 -19
  255. package/src/routes/pages/collections.tsx +6 -1
  256. package/src/routes/pages/page.tsx +86 -11
  257. package/src/routes/site-skill.ts +18 -0
  258. package/src/services/__tests__/auth.test.ts +7 -5
  259. package/src/services/__tests__/collection.test.ts +190 -44
  260. package/src/services/__tests__/navigation.test.ts +229 -21
  261. package/src/services/__tests__/post-timeline.test.ts +249 -39
  262. package/src/services/__tests__/post.test.ts +844 -5
  263. package/src/services/__tests__/search.test.ts +48 -1
  264. package/src/services/__tests__/settings.test.ts +8 -0
  265. package/src/services/__tests__/site-admin.test.ts +121 -1
  266. package/src/services/auth.ts +2 -2
  267. package/src/services/bootstrap.ts +3 -2
  268. package/src/services/collection.ts +224 -167
  269. package/src/services/export-theme/assets/client-site.css +1 -1
  270. package/src/services/export-theme/assets/client-site.js +46 -25
  271. package/src/services/export-theme/layouts/_default/list.html +18 -4
  272. package/src/services/export-theme/layouts/_default/rss.xml +8 -12
  273. package/src/services/export-theme/layouts/featured/list.html +5 -4
  274. package/src/services/export-theme/layouts/partials/featured-thread.html +56 -0
  275. package/src/services/export-theme/layouts/partials/footer.html +1 -1
  276. package/src/services/export-theme/layouts/partials/media-gallery.html +1 -0
  277. package/src/services/export-theme/styles/main.css +30 -2
  278. package/src/services/export.ts +94 -85
  279. package/src/services/mcp.ts +37 -27
  280. package/src/services/media.ts +1 -1
  281. package/src/services/navigation.ts +185 -46
  282. package/src/services/path.ts +4 -1
  283. package/src/services/post.ts +1142 -342
  284. package/src/services/search.ts +17 -2
  285. package/src/services/settings.ts +2 -10
  286. package/src/services/site-admin.ts +63 -43
  287. package/src/style-cjk-jp.css +1 -1
  288. package/src/style-cjk-kr.css +1 -1
  289. package/src/style-cjk-tc.css +1 -1
  290. package/src/style-cjk.css +1 -1
  291. package/src/styles/components.css +540 -14
  292. package/src/styles/site-media.css +111 -19
  293. package/src/styles/tokens.css +76 -10
  294. package/src/styles/ui.css +2227 -1347
  295. package/src/types/bindings.ts +3 -0
  296. package/src/types/config.ts +243 -12
  297. package/src/types/constants.ts +22 -1
  298. package/src/types/entities.ts +14 -8
  299. package/src/types/operations.ts +18 -10
  300. package/src/types/props.ts +20 -2
  301. package/src/types/raw-assets.d.ts +5 -0
  302. package/src/types/views.ts +22 -4
  303. package/src/ui/__tests__/font-themes.test.ts +42 -30
  304. package/src/ui/compose/ComposeDialog.tsx +109 -19
  305. package/src/ui/dash/appearance/NavigationContent.tsx +332 -4
  306. package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +23 -0
  307. package/src/ui/dash/settings/ConfigEditorContent.tsx +578 -0
  308. package/src/ui/dash/settings/GeneralContent.tsx +15 -5
  309. package/src/ui/dash/settings/SettingsRootContent.tsx +21 -1
  310. package/src/ui/dash/settings/__tests__/ConfigEditorContent.test.tsx +98 -0
  311. package/src/ui/dash/settings/__tests__/SettingsRootContent.test.tsx +15 -0
  312. package/src/ui/feed/CuratedThreadPreview.tsx +16 -4
  313. package/src/ui/feed/LinkCard.tsx +2 -12
  314. package/src/ui/feed/LinkPreview.tsx +2 -7
  315. package/src/ui/feed/NoteCard.tsx +10 -20
  316. package/src/ui/feed/PostStatusBadges.tsx +42 -3
  317. package/src/ui/feed/QuoteCard.tsx +2 -12
  318. package/src/ui/feed/ThreadPreview.tsx +26 -34
  319. package/src/ui/feed/TimelineFeed.tsx +2 -2
  320. package/src/ui/feed/__tests__/thread-preview.test.ts +197 -56
  321. package/src/ui/feed/__tests__/timeline-cards.test.ts +178 -3
  322. package/src/ui/feed/thread-preview-state.ts +5 -7
  323. package/src/ui/font-themes.ts +74 -26
  324. package/src/ui/layouts/BaseLayout.tsx +22 -8
  325. package/src/ui/layouts/__tests__/BaseLayout.test.tsx +64 -1
  326. package/src/ui/pages/ArchivePage.tsx +131 -24
  327. package/src/ui/pages/CollectionPage.tsx +101 -22
  328. package/src/ui/pages/CollectionsPage.tsx +3 -1
  329. package/src/ui/pages/PostPage.tsx +39 -12
  330. package/src/ui/pages/SearchPage.tsx +6 -14
  331. package/src/ui/pages/__tests__/ArchivePage.test.tsx +70 -0
  332. package/src/ui/pages/__tests__/PostPage.test.tsx +88 -0
  333. package/src/ui/shared/CollectionDirectory.tsx +13 -9
  334. package/src/ui/shared/CollectionsManager.tsx +17 -7
  335. package/src/ui/shared/DraftPreviewBar.tsx +49 -0
  336. package/src/ui/shared/HomePageBranding.tsx +2 -2
  337. package/src/ui/shared/MediaGallery.tsx +1 -0
  338. package/src/ui/shared/PostFooter.tsx +38 -13
  339. package/src/ui/shared/__tests__/home-page-branding.test.tsx +1 -1
  340. package/src/ui/shared/__tests__/media-gallery.test.ts +1 -0
  341. package/src/ui/shared/__tests__/navigation-labels.test.ts +2 -2
  342. package/src/ui/shared/__tests__/post-footer.test.ts +15 -0
  343. package/src/ui/shared/collection-management-labels.ts +33 -2
  344. package/src/ui/shared/custom-icons.ts +5 -0
  345. package/src/ui/shared/navigation-labels.ts +2 -4
  346. package/src/ui/shared/post-article-attributes.ts +52 -0
  347. package/dist/app-DZDlmh7C.js +0 -6
  348. package/dist/client/_assets/client-Cevfpm8H.css +0 -2
  349. package/dist/client/_assets/client-auth-v3yvhbgZ.js +0 -4736
  350. package/src/client/tiptap/wrapping-input-rules.ts +0 -102
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * Collection Service (v2)
3
3
  *
4
- * Manages collections. Posts belong to collections via post_collections junction table (M:N).
4
+ * Manages collections. Threads belong to collections through the
5
+ * `thread_collection` junction table (M:N).
5
6
  * Collection directory ordering is managed through the collection_directory_item
6
7
  * table with fractional indexing.
7
8
  */
@@ -14,6 +15,7 @@ import {
14
15
  supportsDrizzleTransaction,
15
16
  } from "../db/index.js";
16
17
  import type { DatabaseDialect } from "../db/dialect.js";
18
+ import { buildRootActivityExpr } from "../db/thread-activity.js";
17
19
  import {
18
20
  sqliteSchemaBundle,
19
21
  type DatabaseSchema,
@@ -33,7 +35,11 @@ import type {
33
35
  UpdateCollectionDirectoryEntry,
34
36
  CollectionSortOrder,
35
37
  } from "../types.js";
36
- import { ConflictError, ValidationError } from "../lib/errors.js";
38
+ import {
39
+ ConflictError,
40
+ NotFoundError,
41
+ ValidationError,
42
+ } from "../lib/errors.js";
37
43
  import {
38
44
  createPathService,
39
45
  toCollectionPath,
@@ -91,7 +97,7 @@ export interface CollectionService {
91
97
  ): Promise<ResolvedCollectionSelection | null>;
92
98
  list(): Promise<Collection[]>;
93
99
  listDirectoryData(): Promise<CollectionsDirectoryData>;
94
- /** List collections sorted by most recent post addition (for compose dialog) */
100
+ /** List collections sorted by most recent Thread addition (for compose dialog) */
95
101
  listByRecentActivity(): Promise<Collection[]>;
96
102
  create(data: CreateCollection): Promise<Collection>;
97
103
  update(id: string, data: UpdateCollection): Promise<Collection | null>;
@@ -115,16 +121,16 @@ export interface CollectionService {
115
121
  after: string | null,
116
122
  before: string | null,
117
123
  ): Promise<CollectionDirectoryEntry | null>;
118
- /** Get post count per collection */
119
- getPostCounts(): Promise<Map<string, number>>;
124
+ /** Get Thread count per collection */
125
+ getThreadCounts(): Promise<Map<string, number>>;
120
126
  /**
121
- * Add a post to a collection. Optional metadata lets the caller set the
127
+ * Add a Thread to a collection. Optional metadata lets the caller set the
122
128
  * per-row `createdAt` / `position` / `pinnedAt` explicitly; omitted
123
129
  * fields default to `now()` / append-at-end / `null`.
124
130
  */
125
- addPost(
131
+ addThread(
126
132
  collectionId: string,
127
- postId: string,
133
+ threadId: string,
128
134
  opts?: {
129
135
  createdAt?: number;
130
136
  position?: number;
@@ -132,13 +138,13 @@ export interface CollectionService {
132
138
  },
133
139
  ): Promise<void>;
134
140
  /**
135
- * Replace all collection memberships of a post in one transaction,
141
+ * Replace all collection memberships of a Thread in one transaction,
136
142
  * preserving each entry's `createdAt` / `position` / `pinnedAt`.
137
143
  * Used by the Hugo import path so round-tripping through
138
144
  * `jant site export | jant site import` is lossless.
139
145
  */
140
- syncPostCollectionsWithMeta(
141
- postId: string,
146
+ syncThreadCollectionsWithMeta(
147
+ threadId: string,
142
148
  entries: {
143
149
  collectionId: string;
144
150
  createdAt?: number;
@@ -146,39 +152,39 @@ export interface CollectionService {
146
152
  pinnedAt?: number | null;
147
153
  }[],
148
154
  ): Promise<void>;
149
- /** Remove a post from a collection */
150
- removePost(collectionId: string, postId: string): Promise<void>;
151
- /** Pin a post within a collection */
152
- pinPost(collectionId: string, postId: string): Promise<void>;
153
- /** Unpin a post within a collection */
154
- unpinPost(collectionId: string, postId: string): Promise<void>;
155
- /** Get pinned post IDs for given collections */
156
- getPinnedPostIds(collectionIds: string[]): Promise<Set<string>>;
157
- /** Get all collections a post belongs to */
155
+ /** Remove a Thread from a collection */
156
+ removeThread(collectionId: string, threadId: string): Promise<void>;
157
+ /** Pin a Thread within a collection */
158
+ pinThread(collectionId: string, threadId: string): Promise<void>;
159
+ /** Unpin a Thread within a collection */
160
+ unpinThread(collectionId: string, threadId: string): Promise<void>;
161
+ /** Get pinned Thread IDs for given collections */
162
+ getPinnedThreadIds(collectionIds: string[]): Promise<Set<string>>;
163
+ /** Get the Thread-level collections shared by a post and its siblings */
158
164
  getCollectionsByPostId(postId: string): Promise<Collection[]>;
159
- /** Batch get collections for multiple posts */
165
+ /** Batch project Thread-level collections onto multiple posts */
160
166
  getCollectionsByPostIds(
161
167
  postIds: string[],
162
168
  ): Promise<Map<string, Collection[]>>;
163
169
  /**
164
- * Batch get the set of collection IDs each post is pinned in.
170
+ * Batch get the set of collection IDs each Thread is pinned in.
165
171
  *
166
172
  * Used by the Hugo export to surface per-collection pins so the static
167
- * collection page can sort pinned posts to the top, mirroring the live
173
+ * collection page can sort pinned Threads to the top, mirroring the live
168
174
  * site behavior.
169
175
  */
170
- getCollectionPinsByPostIds(
171
- postIds: string[],
176
+ getCollectionPinsByThreadIds(
177
+ threadIds: string[],
172
178
  ): Promise<Map<string, Set<string>>>;
173
179
  /**
174
- * Batch fetch the full per-entry collection metadata for each post: the
180
+ * Batch fetch the full per-entry collection metadata for each Thread: the
175
181
  * `createdAt` timestamp, `position`, and per-collection `pinnedAt`.
176
182
  *
177
183
  * Used by the Hugo export to emit lossless `collections` front-matter
178
184
  * entries so round-tripping through `jant site export | jant site import`
179
185
  * preserves per-entry state.
180
186
  */
181
- getCollectionEntriesByPostIds(postIds: string[]): Promise<
187
+ getCollectionEntriesByThreadIds(threadIds: string[]): Promise<
182
188
  Map<
183
189
  string,
184
190
  {
@@ -189,10 +195,13 @@ export interface CollectionService {
189
195
  }[]
190
196
  >
191
197
  >;
192
- /** Get all post IDs in a collection */
193
- getPostIds(collectionId: string): Promise<string[]>;
194
- /** Sync a post's collection memberships (replace all with given IDs) */
195
- syncPostCollections(postId: string, collectionIds: string[]): Promise<void>;
198
+ /** Get all Thread IDs in a collection */
199
+ getThreadIds(collectionId: string): Promise<string[]>;
200
+ /** Sync a Thread's collection memberships (replace all with given IDs) */
201
+ syncThreadCollections(
202
+ threadId: string,
203
+ collectionIds: string[],
204
+ ): Promise<void>;
196
205
  }
197
206
 
198
207
  export function createCollectionService(
@@ -207,7 +216,7 @@ export function createCollectionService(
207
216
  collections,
208
217
  pathRegistry,
209
218
  collectionDirectoryItems: directoryItemsTable,
210
- postCollections,
219
+ threadCollections,
211
220
  posts,
212
221
  navItems,
213
222
  } = databaseSchema;
@@ -373,6 +382,17 @@ export function createCollectionService(
373
382
  return rows.length > 0;
374
383
  }
375
384
 
385
+ async function resolveThreadRootId(postId: string): Promise<string> {
386
+ const rows = await db
387
+ .select({ threadId: posts.threadId })
388
+ .from(posts)
389
+ .where(and(eq(posts.siteId, siteId), eq(posts.id, postId)))
390
+ .limit(1);
391
+ const threadId = rows[0]?.threadId;
392
+ if (!threadId) throw new NotFoundError("Thread");
393
+ return threadId;
394
+ }
395
+
376
396
  async function getDirectoryMovePosition(
377
397
  id: string,
378
398
  afterId: string | null,
@@ -429,22 +449,25 @@ export function createCollectionService(
429
449
  async function listDirectoryCollections(): Promise<
430
450
  CollectionDirectoryCollection[]
431
451
  > {
432
- const postCount = sql<number>`
452
+ // `posts` is joined on thread_collections.thread_id, so each row is
453
+ // already the Thread root — the definition applies to it directly.
454
+ const threadActivityAt = buildRootActivityExpr({
455
+ lastActivityAt: posts.lastActivityAt,
456
+ publishedAt: posts.publishedAt,
457
+ updatedAt: posts.updatedAt,
458
+ });
459
+ const threadCount = sql<number>`
433
460
  CAST(COUNT(
434
461
  CASE
435
462
  WHEN ${posts.id} IS NOT NULL THEN 1
436
463
  END
437
464
  ) AS INTEGER)
438
- `.as("post_count");
465
+ `.as("thread_count");
439
466
  const recentActivityAt = sql<number | null>`
440
467
  MAX(
441
468
  CASE
442
469
  WHEN ${posts.id} IS NOT NULL
443
- THEN COALESCE(
444
- ${posts.lastActivityAt},
445
- ${posts.publishedAt},
446
- ${posts.updatedAt}
447
- )
470
+ THEN ${threadActivityAt}
448
471
  END
449
472
  )
450
473
  `.as("recent_activity_at");
@@ -452,20 +475,23 @@ export function createCollectionService(
452
475
  const rows = await db
453
476
  .select({
454
477
  collection: collections,
455
- postCount,
478
+ threadCount,
456
479
  recentActivityAt,
457
480
  })
458
481
  .from(collections)
459
482
  .leftJoin(
460
- postCollections,
483
+ threadCollections,
461
484
  and(
462
- eq(postCollections.siteId, siteId),
463
- eq(collections.id, postCollections.collectionId),
485
+ eq(threadCollections.siteId, siteId),
486
+ eq(collections.id, threadCollections.collectionId),
464
487
  ),
465
488
  )
466
489
  .leftJoin(
467
490
  posts,
468
- and(eq(posts.siteId, siteId), eq(postCollections.postId, posts.id)),
491
+ and(
492
+ eq(posts.siteId, threadCollections.siteId),
493
+ eq(threadCollections.threadId, posts.id),
494
+ ),
469
495
  )
470
496
  .where(eq(collections.siteId, siteId))
471
497
  .groupBy(collections.id)
@@ -484,7 +510,7 @@ export function createCollectionService(
484
510
 
485
511
  return {
486
512
  ...toCollection(row.collection, slug),
487
- postCount: row.postCount,
513
+ threadCount: row.threadCount,
488
514
  recentActivityAt: row.recentActivityAt ?? row.collection.updatedAt,
489
515
  };
490
516
  })
@@ -616,21 +642,21 @@ export function createCollectionService(
616
642
  async listByRecentActivity() {
617
643
  const lastAddedAt = sql<
618
644
  number | null
619
- >`MAX(${postCollections.createdAt})`.as("last_added_at");
645
+ >`MAX(${threadCollections.createdAt})`.as("last_added_at");
620
646
  const rows = await db
621
647
  .select({ collection: collections, lastAddedAt })
622
648
  .from(collections)
623
649
  .leftJoin(
624
- postCollections,
650
+ threadCollections,
625
651
  and(
626
- eq(postCollections.siteId, siteId),
627
- eq(collections.id, postCollections.collectionId),
652
+ eq(threadCollections.siteId, siteId),
653
+ eq(collections.id, threadCollections.collectionId),
628
654
  ),
629
655
  )
630
656
  .where(eq(collections.siteId, siteId))
631
657
  .groupBy(collections.id)
632
658
  .orderBy(
633
- desc(sql`COALESCE(MAX(${postCollections.createdAt}), 0)`),
659
+ desc(sql`COALESCE(MAX(${threadCollections.createdAt}), 0)`),
634
660
  desc(collections.createdAt),
635
661
  );
636
662
  return hydrateCollections(rows.map((row) => row.collection));
@@ -814,11 +840,11 @@ export function createCollectionService(
814
840
 
815
841
  async delete(id) {
816
842
  await db
817
- .delete(postCollections)
843
+ .delete(threadCollections)
818
844
  .where(
819
845
  and(
820
- eq(postCollections.siteId, siteId),
821
- eq(postCollections.collectionId, id),
846
+ eq(threadCollections.siteId, siteId),
847
+ eq(threadCollections.collectionId, id),
822
848
  ),
823
849
  );
824
850
  await db
@@ -1041,19 +1067,15 @@ export function createCollectionService(
1041
1067
  throw new Error("Failed to assign a unique directory item position");
1042
1068
  },
1043
1069
 
1044
- async getPostCounts() {
1070
+ async getThreadCounts() {
1045
1071
  const rows = await db
1046
1072
  .select({
1047
- collectionId: postCollections.collectionId,
1073
+ collectionId: threadCollections.collectionId,
1048
1074
  count: sql<number>`CAST(count(*) AS INTEGER)`.as("count"),
1049
1075
  })
1050
- .from(postCollections)
1051
- .innerJoin(
1052
- sql`post`,
1053
- sql`post.id = ${postCollections.postId} AND post.site_id = ${siteId}`,
1054
- )
1055
- .where(eq(postCollections.siteId, siteId))
1056
- .groupBy(postCollections.collectionId);
1076
+ .from(threadCollections)
1077
+ .where(eq(threadCollections.siteId, siteId))
1078
+ .groupBy(threadCollections.collectionId);
1057
1079
 
1058
1080
  const counts = new Map<string, number>();
1059
1081
  for (const row of rows) {
@@ -1062,24 +1084,25 @@ export function createCollectionService(
1062
1084
  return counts;
1063
1085
  },
1064
1086
 
1065
- async addPost(collectionId, postId, opts) {
1087
+ async addThread(collectionId, postId, opts) {
1088
+ const threadId = await resolveThreadRootId(postId);
1066
1089
  const [maxRow] = await db
1067
1090
  .select({
1068
- maxPos: sql<number>`COALESCE(MAX(${postCollections.position}), -1)`,
1091
+ maxPos: sql<number>`COALESCE(MAX(${threadCollections.position}), -1)`,
1069
1092
  })
1070
- .from(postCollections)
1093
+ .from(threadCollections)
1071
1094
  .where(
1072
1095
  and(
1073
- eq(postCollections.siteId, siteId),
1074
- eq(postCollections.postId, postId),
1096
+ eq(threadCollections.siteId, siteId),
1097
+ eq(threadCollections.threadId, threadId),
1075
1098
  ),
1076
1099
  );
1077
1100
  const nextPosition = (maxRow?.maxPos ?? -1) + 1;
1078
1101
  await db
1079
- .insert(postCollections)
1102
+ .insert(threadCollections)
1080
1103
  .values({
1081
1104
  siteId,
1082
- postId,
1105
+ threadId,
1083
1106
  collectionId,
1084
1107
  createdAt: opts?.createdAt ?? now(),
1085
1108
  position: opts?.position ?? nextPosition,
@@ -1088,12 +1111,12 @@ export function createCollectionService(
1088
1111
  .onConflictDoNothing();
1089
1112
  },
1090
1113
 
1091
- async syncPostCollectionsWithMeta(postId, entries) {
1114
+ async syncThreadCollectionsWithMeta(threadId, entries) {
1092
1115
  const seen = new Set<string>();
1093
1116
  const timestamp = now();
1094
1117
  const insertValues: {
1095
1118
  siteId: string;
1096
- postId: string;
1119
+ threadId: string;
1097
1120
  collectionId: string;
1098
1121
  createdAt: number;
1099
1122
  position: number;
@@ -1105,7 +1128,7 @@ export function createCollectionService(
1105
1128
  seen.add(entry.collectionId);
1106
1129
  insertValues.push({
1107
1130
  siteId,
1108
- postId,
1131
+ threadId,
1109
1132
  collectionId: entry.collectionId,
1110
1133
  createdAt: entry.createdAt ?? timestamp,
1111
1134
  position: entry.position ?? fallbackPosition,
@@ -1115,11 +1138,11 @@ export function createCollectionService(
1115
1138
  }
1116
1139
 
1117
1140
  const deleteQuery = db
1118
- .delete(postCollections)
1141
+ .delete(threadCollections)
1119
1142
  .where(
1120
1143
  and(
1121
- eq(postCollections.siteId, siteId),
1122
- eq(postCollections.postId, postId),
1144
+ eq(threadCollections.siteId, siteId),
1145
+ eq(threadCollections.threadId, threadId),
1123
1146
  ),
1124
1147
  );
1125
1148
 
@@ -1127,7 +1150,7 @@ export function createCollectionService(
1127
1150
  const writeQueries = [];
1128
1151
  writeQueries.push(deleteQuery);
1129
1152
  if (insertValues.length > 0) {
1130
- writeQueries.push(db.insert(postCollections).values(insertValues));
1153
+ writeQueries.push(db.insert(threadCollections).values(insertValues));
1131
1154
  }
1132
1155
  await db.batch(
1133
1156
  writeQueries as [
@@ -1140,88 +1163,101 @@ export function createCollectionService(
1140
1163
 
1141
1164
  await db.transaction(async (tx) => {
1142
1165
  await tx
1143
- .delete(postCollections)
1166
+ .delete(threadCollections)
1144
1167
  .where(
1145
1168
  and(
1146
- eq(postCollections.siteId, siteId),
1147
- eq(postCollections.postId, postId),
1169
+ eq(threadCollections.siteId, siteId),
1170
+ eq(threadCollections.threadId, threadId),
1148
1171
  ),
1149
1172
  );
1150
1173
  if (insertValues.length > 0) {
1151
- await tx.insert(postCollections).values(insertValues);
1174
+ await tx.insert(threadCollections).values(insertValues);
1152
1175
  }
1153
1176
  });
1154
1177
  },
1155
1178
 
1156
- async removePost(collectionId, postId) {
1179
+ async removeThread(collectionId, postId) {
1180
+ const threadId = await resolveThreadRootId(postId);
1157
1181
  await db
1158
- .delete(postCollections)
1182
+ .delete(threadCollections)
1159
1183
  .where(
1160
1184
  and(
1161
- eq(postCollections.siteId, siteId),
1162
- eq(postCollections.postId, postId),
1163
- eq(postCollections.collectionId, collectionId),
1185
+ eq(threadCollections.siteId, siteId),
1186
+ eq(threadCollections.threadId, threadId),
1187
+ eq(threadCollections.collectionId, collectionId),
1164
1188
  ),
1165
1189
  );
1166
1190
  },
1167
1191
 
1168
- async pinPost(collectionId, postId) {
1192
+ async pinThread(collectionId, postId) {
1193
+ const threadId = await resolveThreadRootId(postId);
1169
1194
  await db
1170
- .update(postCollections)
1195
+ .update(threadCollections)
1171
1196
  .set({ pinnedAt: now() })
1172
1197
  .where(
1173
1198
  and(
1174
- eq(postCollections.siteId, siteId),
1175
- eq(postCollections.postId, postId),
1176
- eq(postCollections.collectionId, collectionId),
1199
+ eq(threadCollections.siteId, siteId),
1200
+ eq(threadCollections.threadId, threadId),
1201
+ eq(threadCollections.collectionId, collectionId),
1177
1202
  ),
1178
1203
  );
1179
1204
  },
1180
1205
 
1181
- async unpinPost(collectionId, postId) {
1206
+ async unpinThread(collectionId, postId) {
1207
+ const threadId = await resolveThreadRootId(postId);
1182
1208
  await db
1183
- .update(postCollections)
1209
+ .update(threadCollections)
1184
1210
  .set({ pinnedAt: null })
1185
1211
  .where(
1186
1212
  and(
1187
- eq(postCollections.siteId, siteId),
1188
- eq(postCollections.postId, postId),
1189
- eq(postCollections.collectionId, collectionId),
1213
+ eq(threadCollections.siteId, siteId),
1214
+ eq(threadCollections.threadId, threadId),
1215
+ eq(threadCollections.collectionId, collectionId),
1190
1216
  ),
1191
1217
  );
1192
1218
  },
1193
1219
 
1194
- async getPinnedPostIds(collectionIds) {
1220
+ async getPinnedThreadIds(collectionIds) {
1195
1221
  if (collectionIds.length === 0) return new Set<string>();
1196
1222
  const rows = await db
1197
- .select({ postId: postCollections.postId })
1198
- .from(postCollections)
1223
+ .select({ threadId: threadCollections.threadId })
1224
+ .from(threadCollections)
1199
1225
  .where(
1200
1226
  and(
1201
- eq(postCollections.siteId, siteId),
1202
- inArray(postCollections.collectionId, collectionIds),
1203
- sql`${postCollections.pinnedAt} IS NOT NULL`,
1227
+ eq(threadCollections.siteId, siteId),
1228
+ inArray(threadCollections.collectionId, collectionIds),
1229
+ sql`${threadCollections.pinnedAt} IS NOT NULL`,
1204
1230
  ),
1205
1231
  );
1206
- return new Set(rows.map((r) => r.postId));
1232
+ return new Set(rows.map((row) => row.threadId));
1207
1233
  },
1208
1234
 
1209
1235
  async getCollectionsByPostId(postId) {
1210
1236
  const rows = await db
1211
1237
  .select({ collection: collections })
1212
- .from(postCollections)
1238
+ .from(posts)
1239
+ .innerJoin(
1240
+ threadCollections,
1241
+ and(
1242
+ eq(threadCollections.siteId, posts.siteId),
1243
+ eq(threadCollections.threadId, posts.threadId),
1244
+ ),
1245
+ )
1213
1246
  .innerJoin(
1214
1247
  collections,
1215
- eq(postCollections.collectionId, collections.id),
1248
+ eq(threadCollections.collectionId, collections.id),
1216
1249
  )
1217
1250
  .where(
1218
1251
  and(
1219
- eq(postCollections.siteId, siteId),
1252
+ eq(posts.siteId, siteId),
1220
1253
  eq(collections.siteId, siteId),
1221
- eq(postCollections.postId, postId),
1254
+ eq(posts.id, postId),
1222
1255
  ),
1223
1256
  )
1224
- .orderBy(asc(postCollections.position), asc(postCollections.createdAt));
1257
+ .orderBy(
1258
+ asc(threadCollections.position),
1259
+ asc(threadCollections.createdAt),
1260
+ );
1225
1261
 
1226
1262
  return hydrateCollections(rows.map((row) => row.collection));
1227
1263
  },
@@ -1230,27 +1266,38 @@ export function createCollectionService(
1230
1266
  const result = new Map<string, Collection[]>();
1231
1267
  if (postIds.length === 0) return result;
1232
1268
 
1233
- const rows = await batchQueryRows(postIds, (chunk) =>
1269
+ const postRows = await batchQueryRows(postIds, (chunk) =>
1234
1270
  db
1235
1271
  .select({
1236
- postId: postCollections.postId,
1272
+ postId: posts.id,
1273
+ threadId: posts.threadId,
1274
+ })
1275
+ .from(posts)
1276
+ .where(and(eq(posts.siteId, siteId), inArray(posts.id, chunk))),
1277
+ );
1278
+ const threadIds = [...new Set(postRows.map((row) => row.threadId))];
1279
+ if (threadIds.length === 0) return result;
1280
+ const rows = await batchQueryRows(threadIds, (chunk) =>
1281
+ db
1282
+ .select({
1283
+ threadId: threadCollections.threadId,
1237
1284
  collection: collections,
1238
1285
  })
1239
- .from(postCollections)
1286
+ .from(threadCollections)
1240
1287
  .innerJoin(
1241
1288
  collections,
1242
- eq(postCollections.collectionId, collections.id),
1289
+ eq(threadCollections.collectionId, collections.id),
1243
1290
  )
1244
1291
  .where(
1245
1292
  and(
1246
- eq(postCollections.siteId, siteId),
1293
+ eq(threadCollections.siteId, siteId),
1247
1294
  eq(collections.siteId, siteId),
1248
- inArray(postCollections.postId, chunk),
1295
+ inArray(threadCollections.threadId, chunk),
1249
1296
  ),
1250
1297
  )
1251
1298
  .orderBy(
1252
- asc(postCollections.position),
1253
- asc(postCollections.createdAt),
1299
+ asc(threadCollections.position),
1300
+ asc(threadCollections.createdAt),
1254
1301
  ),
1255
1302
  );
1256
1303
 
@@ -1259,61 +1306,69 @@ export function createCollectionService(
1259
1306
  collectionRows.map((row) => row.id),
1260
1307
  );
1261
1308
 
1309
+ const collectionsByThread = new Map<string, Collection[]>();
1262
1310
  for (const row of rows) {
1263
1311
  const slug = slugMap.get(row.collection.id);
1264
1312
  if (!slug) continue;
1265
- const existing = result.get(row.postId) ?? [];
1313
+ const existing = collectionsByThread.get(row.threadId) ?? [];
1266
1314
  existing.push(toCollection(row.collection, slug));
1267
- result.set(row.postId, existing);
1315
+ collectionsByThread.set(row.threadId, existing);
1316
+ }
1317
+
1318
+ for (const row of postRows) {
1319
+ const threadMemberships = collectionsByThread.get(row.threadId);
1320
+ if (threadMemberships) {
1321
+ result.set(row.postId, threadMemberships);
1322
+ }
1268
1323
  }
1269
1324
 
1270
1325
  return result;
1271
1326
  },
1272
1327
 
1273
- async getCollectionPinsByPostIds(postIds) {
1328
+ async getCollectionPinsByThreadIds(threadIds) {
1274
1329
  const result = new Map<string, Set<string>>();
1275
- if (postIds.length === 0) return result;
1330
+ if (threadIds.length === 0) return result;
1276
1331
 
1277
- const rows = await batchQueryRows(postIds, (chunk) =>
1332
+ const rows = await batchQueryRows(threadIds, (chunk) =>
1278
1333
  db
1279
1334
  .select({
1280
- postId: postCollections.postId,
1281
- collectionId: postCollections.collectionId,
1335
+ threadId: threadCollections.threadId,
1336
+ collectionId: threadCollections.collectionId,
1282
1337
  })
1283
- .from(postCollections)
1338
+ .from(threadCollections)
1284
1339
  .where(
1285
1340
  and(
1286
- eq(postCollections.siteId, siteId),
1287
- inArray(postCollections.postId, chunk),
1288
- sql`${postCollections.pinnedAt} IS NOT NULL`,
1341
+ eq(threadCollections.siteId, siteId),
1342
+ inArray(threadCollections.threadId, chunk),
1343
+ sql`${threadCollections.pinnedAt} IS NOT NULL`,
1289
1344
  ),
1290
1345
  ),
1291
1346
  );
1292
1347
 
1293
1348
  for (const row of rows) {
1294
- const existing = result.get(row.postId) ?? new Set<string>();
1349
+ const existing = result.get(row.threadId) ?? new Set<string>();
1295
1350
  existing.add(row.collectionId);
1296
- result.set(row.postId, existing);
1351
+ result.set(row.threadId, existing);
1297
1352
  }
1298
1353
 
1299
1354
  return result;
1300
1355
  },
1301
1356
 
1302
- async getPostIds(collectionId) {
1357
+ async getThreadIds(collectionId) {
1303
1358
  const rows = await db
1304
- .select({ postId: postCollections.postId })
1305
- .from(postCollections)
1359
+ .select({ threadId: threadCollections.threadId })
1360
+ .from(threadCollections)
1306
1361
  .where(
1307
1362
  and(
1308
- eq(postCollections.siteId, siteId),
1309
- eq(postCollections.collectionId, collectionId),
1363
+ eq(threadCollections.siteId, siteId),
1364
+ eq(threadCollections.collectionId, collectionId),
1310
1365
  ),
1311
1366
  );
1312
1367
 
1313
- return rows.map((row) => row.postId);
1368
+ return rows.map((row) => row.threadId);
1314
1369
  },
1315
1370
 
1316
- async getCollectionEntriesByPostIds(postIds) {
1371
+ async getCollectionEntriesByThreadIds(threadIds) {
1317
1372
  const result = new Map<
1318
1373
  string,
1319
1374
  {
@@ -1323,58 +1378,59 @@ export function createCollectionService(
1323
1378
  pinnedAt: number | null;
1324
1379
  }[]
1325
1380
  >();
1326
- if (postIds.length === 0) return result;
1381
+ if (threadIds.length === 0) return result;
1327
1382
 
1328
- const rows = await batchQueryRows(postIds, (chunk) =>
1383
+ const rows = await batchQueryRows(threadIds, (chunk) =>
1329
1384
  db
1330
1385
  .select({
1331
- postId: postCollections.postId,
1332
- collectionId: postCollections.collectionId,
1333
- createdAt: postCollections.createdAt,
1334
- position: postCollections.position,
1335
- pinnedAt: postCollections.pinnedAt,
1386
+ threadId: threadCollections.threadId,
1387
+ collectionId: threadCollections.collectionId,
1388
+ createdAt: threadCollections.createdAt,
1389
+ position: threadCollections.position,
1390
+ pinnedAt: threadCollections.pinnedAt,
1336
1391
  })
1337
- .from(postCollections)
1392
+ .from(threadCollections)
1338
1393
  .where(
1339
1394
  and(
1340
- eq(postCollections.siteId, siteId),
1341
- inArray(postCollections.postId, chunk),
1395
+ eq(threadCollections.siteId, siteId),
1396
+ inArray(threadCollections.threadId, chunk),
1342
1397
  ),
1343
1398
  )
1344
1399
  .orderBy(
1345
- asc(postCollections.position),
1346
- asc(postCollections.createdAt),
1400
+ asc(threadCollections.position),
1401
+ asc(threadCollections.createdAt),
1347
1402
  ),
1348
1403
  );
1349
1404
 
1350
1405
  for (const row of rows) {
1351
- const existing = result.get(row.postId) ?? [];
1406
+ const existing = result.get(row.threadId) ?? [];
1352
1407
  existing.push({
1353
1408
  collectionId: row.collectionId,
1354
1409
  createdAt: row.createdAt,
1355
1410
  position: row.position,
1356
1411
  pinnedAt: row.pinnedAt,
1357
1412
  });
1358
- result.set(row.postId, existing);
1413
+ result.set(row.threadId, existing);
1359
1414
  }
1360
1415
 
1361
1416
  return result;
1362
1417
  },
1363
1418
 
1364
- async syncPostCollections(postId, collectionIds) {
1419
+ async syncThreadCollections(threadId, collectionIds) {
1365
1420
  const nextCollectionIds = [...new Set(collectionIds)];
1366
1421
 
1367
1422
  // Fetch existing rows to preserve createdAt for retained collections
1368
1423
  const existingRows = await db
1369
1424
  .select({
1370
- collectionId: postCollections.collectionId,
1371
- createdAt: postCollections.createdAt,
1425
+ collectionId: threadCollections.collectionId,
1426
+ createdAt: threadCollections.createdAt,
1427
+ pinnedAt: threadCollections.pinnedAt,
1372
1428
  })
1373
- .from(postCollections)
1429
+ .from(threadCollections)
1374
1430
  .where(
1375
1431
  and(
1376
- eq(postCollections.siteId, siteId),
1377
- eq(postCollections.postId, postId),
1432
+ eq(threadCollections.siteId, siteId),
1433
+ eq(threadCollections.threadId, threadId),
1378
1434
  ),
1379
1435
  );
1380
1436
 
@@ -1382,25 +1438,26 @@ export function createCollectionService(
1382
1438
  return;
1383
1439
  }
1384
1440
 
1385
- const existingTimestamps = new Map(
1386
- existingRows.map((r) => [r.collectionId, r.createdAt]),
1441
+ const existingEntries = new Map(
1442
+ existingRows.map((row) => [row.collectionId, row]),
1387
1443
  );
1388
1444
  const timestamp = now();
1389
1445
  const insertValues = nextCollectionIds.map((collectionId, index) => ({
1390
1446
  siteId,
1391
- postId,
1447
+ threadId,
1392
1448
  collectionId,
1393
- createdAt: existingTimestamps.get(collectionId) ?? timestamp,
1449
+ createdAt: existingEntries.get(collectionId)?.createdAt ?? timestamp,
1394
1450
  position: index,
1451
+ pinnedAt: existingEntries.get(collectionId)?.pinnedAt ?? null,
1395
1452
  }));
1396
1453
 
1397
1454
  // Delete all and re-insert to preserve user-specified ordering
1398
1455
  const deleteQuery = db
1399
- .delete(postCollections)
1456
+ .delete(threadCollections)
1400
1457
  .where(
1401
1458
  and(
1402
- eq(postCollections.siteId, siteId),
1403
- eq(postCollections.postId, postId),
1459
+ eq(threadCollections.siteId, siteId),
1460
+ eq(threadCollections.threadId, threadId),
1404
1461
  ),
1405
1462
  );
1406
1463
 
@@ -1408,7 +1465,7 @@ export function createCollectionService(
1408
1465
  const writeQueries = [];
1409
1466
  writeQueries.push(deleteQuery);
1410
1467
  if (insertValues.length > 0) {
1411
- writeQueries.push(db.insert(postCollections).values(insertValues));
1468
+ writeQueries.push(db.insert(threadCollections).values(insertValues));
1412
1469
  }
1413
1470
  await db.batch(
1414
1471
  writeQueries as [
@@ -1421,15 +1478,15 @@ export function createCollectionService(
1421
1478
 
1422
1479
  await db.transaction(async (tx) => {
1423
1480
  await tx
1424
- .delete(postCollections)
1481
+ .delete(threadCollections)
1425
1482
  .where(
1426
1483
  and(
1427
- eq(postCollections.siteId, siteId),
1428
- eq(postCollections.postId, postId),
1484
+ eq(threadCollections.siteId, siteId),
1485
+ eq(threadCollections.threadId, threadId),
1429
1486
  ),
1430
1487
  );
1431
1488
  if (insertValues.length > 0) {
1432
- await tx.insert(postCollections).values(insertValues);
1489
+ await tx.insert(threadCollections).values(insertValues);
1433
1490
  }
1434
1491
  });
1435
1492
  },