@jant/core 0.6.14 → 0.6.15

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