@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,9 +1,10 @@
1
+ import { eq } from "drizzle-orm";
1
2
  import { describe, it, expect, beforeEach } from "vitest";
2
3
  import {
3
4
  createTestDatabase,
4
5
  DEFAULT_TEST_SITE_ID,
5
6
  } from "../../__tests__/helpers/db.js";
6
- import { postCollections } from "../../db/schema.js";
7
+ import { posts, threadCollections } from "../../db/schema.js";
7
8
  import { createPostService } from "../post.js";
8
9
  import { createCollectionService } from "../collection.js";
9
10
  import type { Database } from "../../db/index.js";
@@ -236,7 +237,7 @@ describe("PostService - Timeline features", () => {
236
237
  expect(result.size).toBe(0);
237
238
  });
238
239
 
239
- it("returns second and latest as the same post for a 2-post thread", async () => {
240
+ it("returns the only reply as leading and latest context", async () => {
240
241
  const root = await postService.create({
241
242
  format: "note",
242
243
  bodyMarkdown: "root",
@@ -250,13 +251,13 @@ describe("PostService - Timeline features", () => {
250
251
  const result = await postService.getThreadTimelineContext([root.id]);
251
252
  const ctx = result.get(root.id);
252
253
  expect(ctx).toBeDefined();
253
- expect(ctx?.secondReply?.id).toBe(reply.id);
254
- expect(ctx?.penultimateReply).toBeNull();
254
+ expect(ctx?.leadingReplies.map((post) => post.id)).toEqual([reply.id]);
255
+ expect(ctx?.trailingReplies).toEqual([]);
255
256
  expect(ctx?.latestReply.id).toBe(reply.id);
256
257
  expect(ctx?.totalReplyCount).toBe(1);
257
258
  });
258
259
 
259
- it("returns second, penultimate, and latest slots for a 3-post thread", async () => {
260
+ it("returns overlapping leading and trailing context for short threads", async () => {
260
261
  const root = await postService.create({
261
262
  format: "note",
262
263
  bodyMarkdown: "root",
@@ -275,13 +276,16 @@ describe("PostService - Timeline features", () => {
275
276
  const result = await postService.getThreadTimelineContext([root.id]);
276
277
  const ctx = result.get(root.id);
277
278
  expect(ctx).toBeDefined();
278
- expect(ctx?.secondReply?.id).toBe(reply1.id);
279
- expect(ctx?.penultimateReply?.id).toBe(reply1.id);
279
+ expect(ctx?.leadingReplies.map((post) => post.id)).toEqual([
280
+ reply1.id,
281
+ reply2.id,
282
+ ]);
283
+ expect(ctx?.trailingReplies.map((post) => post.id)).toEqual([reply1.id]);
280
284
  expect(ctx?.latestReply.id).toBe(reply2.id);
281
285
  expect(ctx?.totalReplyCount).toBe(2);
282
286
  });
283
287
 
284
- it("returns second, penultimate, and latest slots for longer threads", async () => {
288
+ it("returns the first two and last three reply positions for longer threads", async () => {
285
289
  const root = await postService.create({
286
290
  format: "note",
287
291
  bodyMarkdown: "root",
@@ -298,8 +302,14 @@ describe("PostService - Timeline features", () => {
298
302
  const result = await postService.getThreadTimelineContext([root.id]);
299
303
  const ctx = result.get(root.id);
300
304
  expect(ctx).toBeDefined();
301
- expect(ctx?.secondReply?.bodyText).toBe("reply 0");
302
- expect(ctx?.penultimateReply?.bodyText).toBe("reply 3");
305
+ expect(ctx?.leadingReplies.map((post) => post.bodyText)).toEqual([
306
+ "reply 0",
307
+ "reply 1",
308
+ ]);
309
+ expect(ctx?.trailingReplies.map((post) => post.bodyText)).toEqual([
310
+ "reply 2",
311
+ "reply 3",
312
+ ]);
303
313
  expect(ctx?.latestReply.bodyText).toBe("reply 4");
304
314
  expect(ctx?.totalReplyCount).toBe(5);
305
315
  });
@@ -326,7 +336,7 @@ describe("PostService - Timeline features", () => {
326
336
  const result = await postService.getThreadTimelineContext([root.id]);
327
337
  const ctx = result.get(root.id);
328
338
  expect(ctx).toBeDefined();
329
- expect(ctx?.secondReply?.id).toBe(reply1.id);
339
+ expect(ctx?.leadingReplies.map((post) => post.id)).toEqual([reply1.id]);
330
340
  expect(ctx?.latestReply.id).toBe(reply1.id);
331
341
  expect(ctx?.totalReplyCount).toBe(1);
332
342
  });
@@ -351,7 +361,9 @@ describe("PostService - Timeline features", () => {
351
361
  const result = await postService.getThreadTimelineContext([root.id]);
352
362
  const ctx = result.get(root.id);
353
363
  expect(ctx).toBeDefined();
354
- expect(ctx?.secondReply?.id).toBe(publishedReply.id);
364
+ expect(ctx?.leadingReplies.map((post) => post.id)).toEqual([
365
+ publishedReply.id,
366
+ ]);
355
367
  expect(ctx?.latestReply.id).toBe(publishedReply.id);
356
368
  expect(ctx?.totalReplyCount).toBe(1);
357
369
  });
@@ -381,14 +393,14 @@ describe("PostService - Timeline features", () => {
381
393
  root2.id,
382
394
  ]);
383
395
  expect(result.size).toBe(2);
384
- expect(result.get(root1.id)?.secondReply?.id).toBe(r1Reply.id);
385
- expect(result.get(root2.id)?.secondReply?.id).toBe(r2Reply.id);
396
+ expect(result.get(root1.id)?.leadingReplies[0]?.id).toBe(r1Reply.id);
397
+ expect(result.get(root2.id)?.leadingReplies[0]?.id).toBe(r2Reply.id);
386
398
  expect(result.get(root1.id)?.latestReply.id).toBe(r1Reply.id);
387
399
  expect(result.get(root2.id)?.latestReply.id).toBe(r2Reply.id);
388
400
  });
389
401
  });
390
402
 
391
- describe("getLastPostIdsByThread", () => {
403
+ describe("getThreadTailIds", () => {
392
404
  it("keeps independent root threads separate", async () => {
393
405
  const root1 = await postService.create({
394
406
  format: "note",
@@ -400,10 +412,7 @@ describe("PostService - Timeline features", () => {
400
412
  quoteText: "quoted",
401
413
  });
402
414
 
403
- const result = await postService.getLastPostIdsByThread([
404
- root1.id,
405
- root2.id,
406
- ]);
415
+ const result = await postService.getThreadTailIds([root1.id, root2.id]);
407
416
 
408
417
  expect(result.get(root1.id)).toBe(root1.id);
409
418
  expect(result.get(root2.id)).toBe(root2.id);
@@ -424,14 +433,57 @@ describe("PostService - Timeline features", () => {
424
433
  replyToId: root1.id,
425
434
  });
426
435
 
427
- const result = await postService.getLastPostIdsByThread([
428
- root1.id,
429
- root2.id,
430
- ]);
436
+ const result = await postService.getThreadTailIds([root1.id, root2.id]);
431
437
 
432
438
  expect(result.get(root1.id)).toBe(reply1.id);
433
439
  expect(result.get(root2.id)).toBe(root2.id);
434
440
  });
441
+
442
+ it("skips a trailing draft by default", async () => {
443
+ const root = await postService.create({
444
+ format: "note",
445
+ bodyMarkdown: "root",
446
+ });
447
+ const reply = await postService.create({
448
+ format: "note",
449
+ bodyMarkdown: "published reply",
450
+ replyToId: root.id,
451
+ });
452
+ await postService.create({
453
+ format: "note",
454
+ bodyMarkdown: "unfinished",
455
+ replyToId: reply.id,
456
+ status: "draft",
457
+ });
458
+
459
+ const result = await postService.getThreadTailIds([root.id]);
460
+
461
+ expect(result.get(root.id)).toBe(reply.id);
462
+ });
463
+
464
+ it("returns a trailing draft when drafts are included", async () => {
465
+ const root = await postService.create({
466
+ format: "note",
467
+ bodyMarkdown: "root",
468
+ });
469
+ const reply = await postService.create({
470
+ format: "note",
471
+ bodyMarkdown: "published reply",
472
+ replyToId: root.id,
473
+ });
474
+ const draft = await postService.create({
475
+ format: "note",
476
+ bodyMarkdown: "unfinished",
477
+ replyToId: reply.id,
478
+ status: "draft",
479
+ });
480
+
481
+ const result = await postService.getThreadTailIds([root.id], {
482
+ includeDrafts: true,
483
+ });
484
+
485
+ expect(result.get(root.id)).toBe(draft.id);
486
+ });
435
487
  });
436
488
 
437
489
  describe("timeline assembly", () => {
@@ -464,6 +516,39 @@ describe("PostService - Timeline features", () => {
464
516
  });
465
517
  });
466
518
 
519
+ describe("countCollectionThreadRootsForCollections", () => {
520
+ it("counts rated Threads once even when multiple children are rated", async () => {
521
+ const collection = await collectionService.create({
522
+ slug: "rated-threads",
523
+ title: "Rated Threads",
524
+ });
525
+ const ratedRoot = await postService.create({
526
+ format: "note",
527
+ bodyMarkdown: "Rated root",
528
+ rating: 5,
529
+ });
530
+ await postService.create({
531
+ format: "note",
532
+ bodyMarkdown: "Rated child",
533
+ rating: 4,
534
+ replyToId: ratedRoot.id,
535
+ });
536
+ const unratedRoot = await postService.create({
537
+ format: "note",
538
+ bodyMarkdown: "Unrated root",
539
+ });
540
+ await collectionService.addThread(collection.id, ratedRoot.id);
541
+ await collectionService.addThread(collection.id, unratedRoot.id);
542
+
543
+ const count = await postService.countCollectionThreadRootsForCollections(
544
+ [collection.id],
545
+ { status: "published", hasRating: true },
546
+ );
547
+
548
+ expect(count).toBe(1);
549
+ });
550
+ });
551
+
467
552
  describe("listCollectionThreadRootIds", () => {
468
553
  it("bumps a hidden collection thread when a non-quiet reply is published", async () => {
469
554
  const collection = await collectionService.create({
@@ -482,8 +567,8 @@ describe("PostService - Timeline features", () => {
482
567
  publishedAt: 2000,
483
568
  });
484
569
 
485
- await collectionService.addPost(collection.id, hiddenRoot.id);
486
- await collectionService.addPost(collection.id, newerRoot.id);
570
+ await collectionService.addThread(collection.id, hiddenRoot.id);
571
+ await collectionService.addThread(collection.id, newerRoot.id);
487
572
 
488
573
  await postService.create({
489
574
  format: "note",
@@ -519,8 +604,8 @@ describe("PostService - Timeline features", () => {
519
604
  publishedAt: 2000,
520
605
  });
521
606
 
522
- await collectionService.addPost(collection.id, olderRoot.id);
523
- await collectionService.addPost(collection.id, newerRoot.id);
607
+ await collectionService.addThread(collection.id, olderRoot.id);
608
+ await collectionService.addThread(collection.id, newerRoot.id);
524
609
 
525
610
  await postService.create({
526
611
  format: "note",
@@ -540,6 +625,131 @@ describe("PostService - Timeline features", () => {
540
625
 
541
626
  expect(rootIds).toEqual([newerRoot.id, olderRoot.id]);
542
627
  });
628
+
629
+ // Quietness used to live only in control flow: the reply skipped the
630
+ // recalculation and nothing recorded that it had. Any later recalculation
631
+ // recomputed MAX(published_at) over the whole Thread, swallowed the quiet
632
+ // reply, and bumped the Thread anyway. It is a stored column now, so the
633
+ // decision survives whatever else happens to the Thread.
634
+ it("keeps a quiet reply quiet after an unrelated post is deleted", async () => {
635
+ const collection = await collectionService.create({
636
+ slug: "durable",
637
+ title: "Durable",
638
+ });
639
+ const olderRoot = await postService.create({
640
+ format: "note",
641
+ bodyMarkdown: "Older root",
642
+ publishedAt: 1000,
643
+ });
644
+ const newerRoot = await postService.create({
645
+ format: "note",
646
+ bodyMarkdown: "Newer root",
647
+ publishedAt: 2000,
648
+ });
649
+ await collectionService.addThread(collection.id, olderRoot.id);
650
+ await collectionService.addThread(collection.id, newerRoot.id);
651
+
652
+ const normalReply = await postService.create({
653
+ format: "note",
654
+ bodyMarkdown: "Normal reply",
655
+ replyToId: olderRoot.id,
656
+ publishedAt: 1500,
657
+ });
658
+ await postService.create({
659
+ format: "note",
660
+ bodyMarkdown: "Quiet reply",
661
+ replyToId: normalReply.id,
662
+ publishedAt: 3000,
663
+ quietReply: true,
664
+ });
665
+
666
+ // Triggers a full recalculation of the Thread.
667
+ await postService.delete(normalReply.id);
668
+
669
+ const root = await postService.getById(olderRoot.id);
670
+ expect(root?.lastActivityAt).toBe(1000);
671
+ expect(root?.threadUpdatedAt).toBe(3000);
672
+
673
+ const rootIds = await postService.listCollectionThreadRootIds(
674
+ collection.id,
675
+ { status: "published", sortOrder: "newest" },
676
+ );
677
+ expect(rootIds).toEqual([newerRoot.id, olderRoot.id]);
678
+ });
679
+
680
+ it("records both activity timestamps for a quiet reply", async () => {
681
+ const root = await postService.create({
682
+ format: "note",
683
+ bodyMarkdown: "Root",
684
+ publishedAt: 1000,
685
+ });
686
+ await postService.create({
687
+ format: "note",
688
+ bodyMarkdown: "Quiet reply",
689
+ replyToId: root.id,
690
+ publishedAt: 3000,
691
+ quietReply: true,
692
+ });
693
+
694
+ const updated = await postService.getById(root.id);
695
+ expect(updated?.lastActivityAt).toBe(1000);
696
+ expect(updated?.threadUpdatedAt).toBe(3000);
697
+ });
698
+
699
+ it("moves both timestamps for a normal reply", async () => {
700
+ const root = await postService.create({
701
+ format: "note",
702
+ bodyMarkdown: "Root",
703
+ publishedAt: 1000,
704
+ });
705
+ await postService.create({
706
+ format: "note",
707
+ bodyMarkdown: "Normal reply",
708
+ replyToId: root.id,
709
+ publishedAt: 3000,
710
+ });
711
+
712
+ const updated = await postService.getById(root.id);
713
+ expect(updated?.lastActivityAt).toBe(3000);
714
+ expect(updated?.threadUpdatedAt).toBe(3000);
715
+ });
716
+
717
+ // Activity means the Thread gained a post. Editing an old post is not
718
+ // activity — otherwise fixing a typo would drag it back to the top of its
719
+ // collection, and "recently updated" would stop meaning anything.
720
+ it("does not reorder a Thread when its root is only edited", async () => {
721
+ const collection = await collectionService.create({
722
+ slug: "edited",
723
+ title: "Edited",
724
+ });
725
+ const editedRoot = await postService.create({
726
+ format: "note",
727
+ bodyMarkdown: "Edited root",
728
+ publishedAt: 1000,
729
+ });
730
+ const newerRoot = await postService.create({
731
+ format: "note",
732
+ bodyMarkdown: "Newer root",
733
+ publishedAt: 2000,
734
+ });
735
+
736
+ await collectionService.addThread(collection.id, editedRoot.id);
737
+ await collectionService.addThread(collection.id, newerRoot.id);
738
+ await db
739
+ .update(posts)
740
+ .set({ createdAt: 500, updatedAt: 3000 })
741
+ .where(eq(posts.id, editedRoot.id));
742
+
743
+ const rootIds = await postService.listCollectionThreadRootIds(
744
+ collection.id,
745
+ {
746
+ status: "published",
747
+ sortOrder: "newest",
748
+ },
749
+ );
750
+
751
+ expect(rootIds).toEqual([newerRoot.id, editedRoot.id]);
752
+ });
543
753
  });
544
754
 
545
755
  describe("listCollectionFeedEntries", () => {
@@ -562,16 +772,16 @@ describe("PostService - Timeline features", () => {
562
772
  bodyMarkdown: "Second root",
563
773
  });
564
774
 
565
- await db.insert(postCollections).values([
775
+ await db.insert(threadCollections).values([
566
776
  {
567
777
  siteId: DEFAULT_TEST_SITE_ID,
568
- postId: collectedReply.id,
778
+ threadId: firstRoot.id,
569
779
  collectionId: collection.id,
570
780
  createdAt: 100,
571
781
  },
572
782
  {
573
783
  siteId: DEFAULT_TEST_SITE_ID,
574
- postId: secondRoot.id,
784
+ threadId: secondRoot.id,
575
785
  collectionId: collection.id,
576
786
  createdAt: 200,
577
787
  },
@@ -607,17 +817,17 @@ describe("PostService - Timeline features", () => {
607
817
  publishedAt: 2000,
608
818
  });
609
819
 
610
- await db.insert(postCollections).values([
820
+ await db.insert(threadCollections).values([
611
821
  {
612
822
  siteId: DEFAULT_TEST_SITE_ID,
613
- postId: olderRoot.id,
823
+ threadId: olderRoot.id,
614
824
  collectionId: collection.id,
615
825
  createdAt: 100,
616
826
  pinnedAt: 5000,
617
827
  },
618
828
  {
619
829
  siteId: DEFAULT_TEST_SITE_ID,
620
- postId: newerRoot.id,
830
+ threadId: newerRoot.id,
621
831
  collectionId: collection.id,
622
832
  createdAt: 200,
623
833
  },
@@ -670,22 +880,22 @@ describe("PostService - Timeline features", () => {
670
880
  bodyMarkdown: "Second root",
671
881
  });
672
882
 
673
- await db.insert(postCollections).values([
883
+ await db.insert(threadCollections).values([
674
884
  {
675
885
  siteId: DEFAULT_TEST_SITE_ID,
676
- postId: sharedRoot.id,
886
+ threadId: sharedRoot.id,
677
887
  collectionId: smart.id,
678
888
  createdAt: 100,
679
889
  },
680
890
  {
681
891
  siteId: DEFAULT_TEST_SITE_ID,
682
- postId: sharedReply.id,
892
+ threadId: sharedRoot.id,
683
893
  collectionId: movies.id,
684
894
  createdAt: 300,
685
895
  },
686
896
  {
687
897
  siteId: DEFAULT_TEST_SITE_ID,
688
- postId: secondRoot.id,
898
+ threadId: secondRoot.id,
689
899
  collectionId: movies.id,
690
900
  createdAt: 200,
691
901
  },
@@ -722,8 +932,8 @@ describe("PostService - Timeline features", () => {
722
932
  publishedAt: 2000,
723
933
  });
724
934
 
725
- await collectionService.addPost(collection.id, hiddenRoot.id);
726
- await collectionService.addPost(collection.id, newerRoot.id);
935
+ await collectionService.addThread(collection.id, hiddenRoot.id);
936
+ await collectionService.addThread(collection.id, newerRoot.id);
727
937
 
728
938
  await postService.create({
729
939
  format: "note",