@jant/core 0.6.16 → 0.7.0

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 (508) hide show
  1. package/README.md +11 -11
  2. package/bin/commands/assets/upload.js +192 -15
  3. package/bin/commands/import-site.js +60 -4
  4. package/bin/commands/reset-password.js +21 -8
  5. package/bin/commands/site/snapshot/export.js +1 -1
  6. package/bin/commands/site/snapshot/import.js +1 -1
  7. package/bin/lib/d1-query.js +5 -16
  8. package/bin/lib/node-env.js +40 -4
  9. package/bin/lib/site-selection.js +85 -29
  10. package/bin/lib/wrangler-cli.js +28 -2
  11. package/bin/lib/wrangler-json.js +35 -0
  12. package/dist/{app-C7_4oab9.js → app-K_Aa1MMn.js} +52476 -13330
  13. package/dist/client/.vite/manifest.json +255 -90
  14. package/dist/client/_assets/chunks/confirm-D7GrwsCN.js +45 -0
  15. package/dist/client/_assets/chunks/create-editor-CD3FhrOB.js +257 -0
  16. package/dist/client/_assets/chunks/{native-DpcrFAPh.js → emoji-mart-C4RpRKcU.js} +716 -1
  17. package/dist/client/_assets/chunks/footnote-rail-CmeoVtnU.js +1 -0
  18. package/dist/client/_assets/chunks/heic-to-CuP2Qlsw.js +1 -0
  19. package/dist/client/_assets/chunks/jant-locale-picker-DL6mIOT5.js +69 -0
  20. package/dist/client/_assets/chunks/lit-DW8VmJAT.js +2 -0
  21. package/dist/client/_assets/chunks/mediabunny-DRaCQ1r-.js +31 -0
  22. package/dist/client/_assets/chunks/rolldown-runtime-hePW80VL.js +1 -0
  23. package/dist/client/_assets/chunks/settings-paths-BcdySNx6.js +84 -0
  24. package/dist/client/_assets/chunks/site-header-nav-DzXsFk05.js +1 -0
  25. package/dist/client/_assets/chunks/slug-format-CRXO1AL5.js +1 -0
  26. package/dist/client/_assets/chunks/slugify-373citbs.js +1 -0
  27. package/dist/client/_assets/chunks/sortable-list-CgaL2jCs.js +2 -0
  28. package/dist/client/_assets/chunks/types-D5iF3H1a.js +1 -0
  29. package/dist/client/_assets/chunks/unsafe-html-B5ZEXaxg.js +1 -0
  30. package/dist/client/_assets/chunks/unsafe-svg-0QCkP0vZ.js +2 -0
  31. package/dist/client/_assets/client-GiYENVw8.css +2 -0
  32. package/dist/client/_assets/client-VnFxJN7G.js +295 -0
  33. package/dist/client/_assets/client-auth-k8gJ6QJj.js +828 -0
  34. package/dist/client/_assets/client-author-DQ8R-8KR.css +2 -0
  35. package/dist/client/_assets/client-compose-B_kDtWkW.js +2004 -0
  36. package/dist/client/_assets/client-manage-M90aSTOk.js +2170 -0
  37. package/dist/client/_assets/client-settings-DfYs9n1F.js +1112 -0
  38. package/dist/{export-Dl5KCiEs.js → github-sync-BPAvT999.js} +2254 -107
  39. package/dist/index.js +2 -5
  40. package/dist/node.js +5 -7
  41. package/package.json +3 -3
  42. package/src/__tests__/auth-session-freshness.test.ts +57 -0
  43. package/src/__tests__/basecoat-variants.test.ts +121 -0
  44. package/src/__tests__/bin/assets-upload.test.ts +68 -0
  45. package/src/__tests__/export-hugo-build.test.ts +260 -0
  46. package/src/__tests__/export-service.test.ts +231 -0
  47. package/src/__tests__/helpers/component-css.ts +91 -0
  48. package/src/__tests__/helpers/db.ts +33 -0
  49. package/src/__tests__/helpers/export-fixtures.ts +3 -0
  50. package/src/__tests__/stylesheet-audience.test.ts +159 -0
  51. package/src/__tests__/stylesheet-breakpoints.test.ts +183 -0
  52. package/src/app.tsx +71 -16
  53. package/src/auth.ts +18 -1
  54. package/src/client/__tests__/collection-page-actions.test.ts +51 -2
  55. package/src/client/__tests__/compose-bridge-image-upload.test.ts +196 -0
  56. package/src/client/__tests__/compose-bridge-video-poster.test.ts +159 -0
  57. package/src/client/__tests__/compose-bridge.test.ts +272 -0
  58. package/src/client/__tests__/compose-launch.test.ts +62 -1
  59. package/src/client/__tests__/compose-shortcuts.test.ts +38 -9
  60. package/src/client/__tests__/compose-triggers.test.ts +90 -0
  61. package/src/client/__tests__/copy-field.test.ts +162 -0
  62. package/src/client/__tests__/custom-url-menu.test.ts +127 -6
  63. package/src/client/__tests__/image-processor.test.ts +324 -2
  64. package/src/client/__tests__/lazy-entries.test.ts +92 -0
  65. package/src/client/__tests__/media-scroll-hint.test.ts +155 -0
  66. package/src/client/__tests__/runtime-paths.test.ts +84 -0
  67. package/src/client/__tests__/smart-collection-page-actions.test.ts +158 -0
  68. package/src/client/__tests__/toast.test.ts +23 -0
  69. package/src/client/__tests__/video-processor-probe.test.ts +175 -0
  70. package/src/client/__tests__/video-processor.test.ts +181 -0
  71. package/src/client/audio-processor.ts +24 -15
  72. package/src/client/audio-waveform.ts +49 -0
  73. package/src/client/collection-dialog-host.ts +72 -0
  74. package/src/client/collection-navigation.ts +51 -16
  75. package/src/client/collection-page-actions.ts +26 -0
  76. package/src/client/components/__tests__/jant-collection-dialog.test.ts +329 -0
  77. package/src/client/components/__tests__/jant-collection-directory.test.ts +274 -10
  78. package/src/client/components/__tests__/jant-collection-form.test.ts +59 -178
  79. package/src/client/components/__tests__/jant-command-palette.test.ts +28 -0
  80. package/src/client/components/__tests__/jant-compose-dialog-draft-storage.test.ts +340 -0
  81. package/src/client/components/__tests__/jant-compose-dialog.test.ts +1250 -53
  82. package/src/client/components/__tests__/jant-compose-editor.test.ts +544 -40
  83. package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +0 -1
  84. package/src/client/components/__tests__/jant-locale-picker.test.ts +207 -0
  85. package/src/client/components/__tests__/jant-nav-manager.test.ts +234 -15
  86. package/src/client/components/__tests__/jant-post-menu.test.ts +413 -2
  87. package/src/client/components/__tests__/jant-post-picker.test.ts +197 -0
  88. package/src/client/components/__tests__/jant-settings-avatar.test.ts +15 -12
  89. package/src/client/components/__tests__/jant-settings-general.test.ts +541 -172
  90. package/src/client/components/__tests__/jant-settings-language.test.ts +851 -0
  91. package/src/client/components/__tests__/jant-smart-collection-dialog.test.ts +665 -0
  92. package/src/client/components/collection-dialog-types.ts +49 -0
  93. package/src/client/components/collection-manager-types.ts +32 -1
  94. package/src/client/components/collection-types.ts +1 -15
  95. package/src/client/components/compose-types.ts +75 -1
  96. package/src/client/components/jant-collection-dialog.ts +722 -0
  97. package/src/client/components/jant-collection-directory.ts +567 -182
  98. package/src/client/components/jant-collection-form.ts +53 -232
  99. package/src/client/components/jant-command-palette.ts +100 -49
  100. package/src/client/components/jant-compose-dialog.ts +1486 -707
  101. package/src/client/components/jant-compose-editor.ts +673 -336
  102. package/src/client/components/jant-compose-fullscreen.ts +51 -43
  103. package/src/client/components/jant-config-editor.ts +65 -53
  104. package/src/client/components/jant-confirm-dialog.ts +13 -11
  105. package/src/client/components/jant-locale-picker.ts +418 -0
  106. package/src/client/components/jant-media-lightbox.ts +146 -136
  107. package/src/client/components/jant-nav-manager.ts +690 -366
  108. package/src/client/components/jant-post-menu.ts +1086 -282
  109. package/src/client/components/jant-post-picker.ts +324 -0
  110. package/src/client/components/jant-repo-picker.ts +138 -106
  111. package/src/client/components/jant-settings-avatar.ts +31 -27
  112. package/src/client/components/jant-settings-general.ts +617 -543
  113. package/src/client/components/jant-settings-language.ts +1087 -0
  114. package/src/client/components/jant-smart-collection-dialog.ts +1021 -0
  115. package/src/client/components/jant-text-preview.ts +94 -86
  116. package/src/client/components/nav-manager-types.ts +62 -1
  117. package/src/client/components/settings-types.ts +14 -29
  118. package/src/client/components/smart-collection-conditions.ts +285 -0
  119. package/src/client/components/smart-collection-dialog-types.ts +68 -0
  120. package/src/client/compose-bridge.ts +214 -141
  121. package/src/client/compose-launch.ts +76 -2
  122. package/src/client/compose-shortcuts.ts +6 -8
  123. package/src/client/compose-triggers.ts +50 -0
  124. package/src/client/copy-field.ts +108 -0
  125. package/src/client/custom-url-menu.ts +60 -4
  126. package/src/client/icons.ts +14 -0
  127. package/src/client/image-plan.ts +165 -0
  128. package/src/client/image-processor.ts +195 -117
  129. package/src/client/image-worker-client.ts +45 -0
  130. package/src/client/image-worker.ts +135 -0
  131. package/src/client/lazy-entries.ts +95 -0
  132. package/src/client/lazy-slugify.ts +2 -2
  133. package/src/client/media-scroll-hint.ts +43 -2
  134. package/src/client/mediabunny.ts +29 -0
  135. package/src/client/post-picker.ts +41 -0
  136. package/src/client/runtime-paths.ts +74 -1
  137. package/src/client/settings-bridge.ts +4 -10
  138. package/src/client/site-header-nav.js +18 -6
  139. package/src/client/smart-collection-dialog-host.ts +116 -0
  140. package/src/client/smart-collection-page-actions.ts +208 -0
  141. package/src/client/tiptap/__tests__/bubble-menu.test.ts +110 -0
  142. package/src/client/tiptap/__tests__/floating-position.test.ts +136 -1
  143. package/src/client/tiptap/__tests__/inline-image-upload.test.ts +37 -0
  144. package/src/client/tiptap/bubble-menu.ts +47 -16
  145. package/src/client/tiptap/floating-position.ts +216 -2
  146. package/src/client/tiptap/inline-image-upload.ts +7 -3
  147. package/src/client/tiptap/link-toolbar.ts +22 -9
  148. package/src/client/toast.ts +13 -0
  149. package/src/client/types/vite-worker.d.ts +12 -0
  150. package/src/client/upload-with-metadata.ts +3 -13
  151. package/src/client/video-processor.ts +351 -107
  152. package/src/client-auth.ts +14 -31
  153. package/src/client-compose.ts +19 -0
  154. package/src/client-manage.ts +13 -0
  155. package/src/client-settings.ts +14 -0
  156. package/src/client-site.ts +10 -3
  157. package/src/client.ts +4 -0
  158. package/src/db/__tests__/backfill-nav-label-mirrors.test.ts +149 -0
  159. package/src/db/__tests__/schema-enum-sources.test.ts +33 -0
  160. package/src/db/__tests__/smart-collection-placement.test.ts +402 -0
  161. package/src/db/backfills/0006_clear_mirrored_page_nav_labels.sql +31 -0
  162. package/src/db/migrations/0032_married_tomas.sql +3 -0
  163. package/src/db/migrations/0033_busy_johnny_storm.sql +198 -0
  164. package/src/db/migrations/0034_cheerful_rumiko_fujikawa.sql +35 -0
  165. package/src/db/migrations/meta/0032_snapshot.json +2582 -0
  166. package/src/db/migrations/meta/0033_snapshot.json +2816 -0
  167. package/src/db/migrations/meta/0034_snapshot.json +2816 -0
  168. package/src/db/migrations/meta/_journal.json +21 -0
  169. package/src/db/migrations/pg/0030_abandoned_nighthawk.sql +3 -0
  170. package/src/db/migrations/pg/0031_strong_ultimatum.sql +128 -0
  171. package/src/db/migrations/pg/0032_furry_multiple_man.sql +6 -0
  172. package/src/db/migrations/pg/meta/0030_snapshot.json +3329 -0
  173. package/src/db/migrations/pg/meta/0031_snapshot.json +3660 -0
  174. package/src/db/migrations/pg/meta/0032_snapshot.json +3660 -0
  175. package/src/db/migrations/pg/meta/_journal.json +21 -0
  176. package/src/db/pg/schema.ts +195 -27
  177. package/src/db/post-visibility.ts +117 -0
  178. package/src/db/schema.ts +222 -27
  179. package/src/i18n/__tests__/detect.test.ts +26 -81
  180. package/src/i18n/__tests__/supported-locales.test.ts +83 -0
  181. package/src/i18n/context.tsx +12 -2
  182. package/src/i18n/detect.ts +7 -66
  183. package/src/i18n/locales/glossary.zh-Hans.yml +44 -4
  184. package/src/i18n/locales/glossary.zh-Hant.yml +44 -4
  185. package/src/i18n/locales/public/en.po +490 -158
  186. package/src/i18n/locales/public/en.ts +1 -1
  187. package/src/i18n/locales/public/zh-Hans.po +487 -155
  188. package/src/i18n/locales/public/zh-Hans.ts +1 -1
  189. package/src/i18n/locales/public/zh-Hant.po +487 -155
  190. package/src/i18n/locales/public/zh-Hant.ts +1 -1
  191. package/src/i18n/locales/settings/en.po +551 -69
  192. package/src/i18n/locales/settings/en.ts +1 -1
  193. package/src/i18n/locales/settings/zh-Hans.po +597 -115
  194. package/src/i18n/locales/settings/zh-Hans.ts +1 -1
  195. package/src/i18n/locales/settings/zh-Hant.po +610 -128
  196. package/src/i18n/locales/settings/zh-Hant.ts +1 -1
  197. package/src/i18n/locales.ts +94 -0
  198. package/src/i18n/middleware.ts +12 -2
  199. package/src/i18n/supported-locales.ts +80 -0
  200. package/src/lib/__tests__/api-settings.test.ts +11 -2
  201. package/src/lib/__tests__/asset-path.test.ts +92 -0
  202. package/src/lib/__tests__/deferred.test.ts +90 -0
  203. package/src/lib/__tests__/discover-ping.test.ts +89 -0
  204. package/src/lib/__tests__/discover.test.ts +410 -0
  205. package/src/lib/__tests__/feed-policy.test.ts +26 -0
  206. package/src/lib/__tests__/feed.test.ts +1526 -15
  207. package/src/lib/__tests__/filter-dimensions.test.ts +341 -0
  208. package/src/lib/__tests__/hosted-signin.test.ts +1 -1
  209. package/src/lib/__tests__/http-cache.test.ts +162 -0
  210. package/src/lib/__tests__/jant-docs-dev.test.ts +21 -0
  211. package/src/lib/__tests__/jant-docs.test.ts +23 -0
  212. package/src/lib/__tests__/lang-detect.test.ts +231 -0
  213. package/src/lib/__tests__/media-helpers.test.ts +81 -0
  214. package/src/lib/__tests__/navigation.test.ts +135 -1
  215. package/src/lib/__tests__/resolve-config.test.ts +64 -3
  216. package/src/lib/__tests__/schemas.test.ts +42 -4
  217. package/src/lib/__tests__/site-unavailable-page.test.ts +25 -0
  218. package/src/lib/__tests__/slug.test.ts +70 -0
  219. package/src/lib/__tests__/slugify.test.ts +85 -0
  220. package/src/lib/__tests__/storage.test.ts +32 -0
  221. package/src/lib/__tests__/thread-fold.test.ts +113 -0
  222. package/src/lib/__tests__/upload-sideload.test.ts +14 -0
  223. package/src/lib/__tests__/url-fetch.test.ts +1 -2
  224. package/src/lib/__tests__/url.test.ts +78 -72
  225. package/src/lib/__tests__/view-language.test.ts +413 -0
  226. package/src/lib/__tests__/worker-response-cache.test.ts +90 -0
  227. package/src/lib/address.ts +57 -0
  228. package/src/lib/api-posts.ts +12 -0
  229. package/src/lib/asset-path.ts +98 -1
  230. package/src/lib/build-env.ts +16 -0
  231. package/src/lib/collection-groups.ts +5 -1
  232. package/src/lib/collection-paths.ts +3 -10
  233. package/src/lib/constants.ts +28 -3
  234. package/src/lib/copy-field.ts +36 -0
  235. package/src/lib/crypto.ts +40 -0
  236. package/src/lib/csp-builder.ts +2 -1
  237. package/src/lib/deferred.ts +79 -0
  238. package/src/lib/discover-ping.ts +141 -0
  239. package/src/lib/discover.ts +437 -0
  240. package/src/lib/env.ts +134 -0
  241. package/src/lib/errors.ts +33 -0
  242. package/src/lib/feed-policy.ts +236 -2
  243. package/src/lib/feed.ts +926 -73
  244. package/src/lib/filter-dimensions.ts +1349 -0
  245. package/src/lib/github-sync-site-config.ts +3 -0
  246. package/src/lib/hosted-signin.ts +1 -1
  247. package/src/lib/http-cache.ts +148 -0
  248. package/src/lib/hugo-markdown.ts +16 -0
  249. package/src/lib/icons.ts +9 -2
  250. package/src/lib/ids.ts +3 -0
  251. package/src/lib/jant-docs.ts +42 -0
  252. package/src/lib/lang-detect.ts +308 -0
  253. package/src/lib/media-helpers.ts +13 -1
  254. package/src/lib/navigation.ts +78 -11
  255. package/src/lib/pagination.ts +28 -24
  256. package/src/lib/per-language-surfaces.ts +67 -0
  257. package/src/lib/post-meta.ts +19 -0
  258. package/src/lib/render.tsx +55 -7
  259. package/src/lib/resolve-config.ts +53 -5
  260. package/src/lib/schemas.ts +201 -10
  261. package/src/lib/site-header-fragment.tsx +2 -1
  262. package/src/lib/site-unavailable-page.ts +47 -0
  263. package/src/lib/slug.ts +51 -7
  264. package/src/lib/slugify.ts +113 -0
  265. package/src/lib/startup-config.ts +4 -6
  266. package/src/lib/storage.ts +7 -0
  267. package/src/lib/summary.ts +50 -0
  268. package/src/lib/thread-fold.ts +161 -0
  269. package/src/lib/time.ts +32 -0
  270. package/src/lib/timeline.ts +42 -1
  271. package/src/lib/tiptap-render.ts +1 -2
  272. package/src/lib/upload.ts +16 -13
  273. package/src/lib/url.ts +139 -86
  274. package/src/lib/version.ts +10 -8
  275. package/src/lib/view-language.ts +436 -0
  276. package/src/lib/view.ts +51 -23
  277. package/src/lib/viewer-context.ts +62 -0
  278. package/src/middleware/__tests__/auth.test.ts +70 -2
  279. package/src/middleware/__tests__/cache-control.test.ts +71 -3
  280. package/src/middleware/__tests__/onboarding.test.ts +76 -5
  281. package/src/middleware/__tests__/secure-headers.test.ts +1 -1
  282. package/src/middleware/__tests__/session.test.ts +181 -33
  283. package/src/middleware/auth.ts +50 -31
  284. package/src/middleware/cache-control.ts +75 -10
  285. package/src/middleware/config.ts +6 -6
  286. package/src/middleware/error-handler.ts +12 -0
  287. package/src/middleware/onboarding.ts +32 -10
  288. package/src/middleware/secure-headers.ts +1 -1
  289. package/src/middleware/session.ts +96 -8
  290. package/src/node/__tests__/cli-migrate.test.ts +21 -19
  291. package/src/node/__tests__/cli-reset-password.test.ts +81 -8
  292. package/src/node/__tests__/cli-runtime-target.test.ts +48 -1
  293. package/src/node/__tests__/cli-site-selection.test.ts +128 -38
  294. package/src/node/__tests__/runtime.test.ts +37 -0
  295. package/src/node/index.ts +1 -0
  296. package/src/preset.css +76 -22
  297. package/src/routes/__tests__/feed-validator.test.ts +156 -0
  298. package/src/routes/api/__tests__/collections.test.ts +114 -0
  299. package/src/routes/api/__tests__/discover.test.ts +222 -0
  300. package/src/routes/api/__tests__/nav-items.test.ts +212 -2
  301. package/src/routes/api/__tests__/post-translations.test.ts +829 -0
  302. package/src/routes/api/__tests__/settings.test.ts +24 -3
  303. package/src/routes/api/__tests__/telegram.test.ts +6 -5
  304. package/src/routes/api/collections.ts +49 -6
  305. package/src/routes/api/custom-urls.ts +0 -1
  306. package/src/routes/api/discover.ts +95 -0
  307. package/src/routes/api/export.ts +3 -0
  308. package/src/routes/api/internal/__tests__/sites.test.ts +42 -2
  309. package/src/routes/api/nav-items.ts +55 -8
  310. package/src/routes/api/posts.ts +159 -0
  311. package/src/routes/api/public/__tests__/archive.test.ts +111 -0
  312. package/src/routes/api/public/archive.ts +77 -81
  313. package/src/routes/api/public/posts.ts +23 -6
  314. package/src/routes/api/smart-collections.ts +132 -0
  315. package/src/routes/api/telegram.ts +2 -40
  316. package/src/routes/api/uploads.ts +1 -2
  317. package/src/routes/auth/__tests__/setup-auth.test.ts +137 -0
  318. package/src/routes/auth/__tests__/setup-discover.test.ts +204 -0
  319. package/src/routes/auth/__tests__/setup-flow.test.ts +270 -0
  320. package/src/routes/auth/__tests__/setup-page.test.tsx +329 -0
  321. package/src/routes/auth/__tests__/setup-steps.test.ts +310 -0
  322. package/src/routes/auth/__tests__/setup.test.ts +278 -26
  323. package/src/routes/auth/__tests__/signin.test.ts +135 -0
  324. package/src/routes/auth/setup.tsx +934 -186
  325. package/src/routes/auth/signin.tsx +10 -8
  326. package/src/routes/compose.tsx +5 -0
  327. package/src/routes/dash/__tests__/custom-urls-archive.test.ts +44 -0
  328. package/src/routes/dash/__tests__/custom-urls-page.test.ts +49 -0
  329. package/src/routes/dash/__tests__/delete-account.test.ts +106 -0
  330. package/src/routes/dash/__tests__/settings-discover.test.ts +213 -0
  331. package/src/routes/dash/custom-urls.tsx +1 -37
  332. package/src/routes/dash/settings.tsx +502 -41
  333. package/src/routes/discover-announce.ts +96 -0
  334. package/src/routes/feed/__tests__/feed.test.ts +165 -2
  335. package/src/routes/feed/feed.ts +69 -40
  336. package/src/routes/feed/sitemap.ts +95 -11
  337. package/src/routes/pages/__tests__/archive-indexing.test.ts +200 -0
  338. package/src/routes/pages/__tests__/archive-params.test.ts +370 -6
  339. package/src/routes/pages/__tests__/collection-routing.test.ts +16 -0
  340. package/src/routes/pages/__tests__/collections-feed-links.test.ts +72 -0
  341. package/src/routes/pages/__tests__/collections.test.ts +9 -3
  342. package/src/routes/pages/__tests__/custom-archive-url.test.ts +119 -0
  343. package/src/routes/pages/__tests__/feed-autodiscovery.test.ts +148 -0
  344. package/src/routes/pages/__tests__/language-routing.test.ts +679 -0
  345. package/src/routes/pages/__tests__/post-preview-partial.test.ts +96 -0
  346. package/src/routes/pages/__tests__/smart-collection.test.ts +298 -0
  347. package/src/routes/pages/__tests__/subscribe.test.ts +248 -0
  348. package/src/routes/pages/archive.tsx +471 -369
  349. package/src/routes/pages/collection.tsx +94 -96
  350. package/src/routes/pages/collections.tsx +38 -50
  351. package/src/routes/pages/featured.tsx +32 -13
  352. package/src/routes/pages/home.tsx +25 -6
  353. package/src/routes/pages/language.tsx +113 -0
  354. package/src/routes/pages/latest.tsx +26 -16
  355. package/src/routes/pages/new.tsx +3 -0
  356. package/src/routes/pages/page.tsx +178 -17
  357. package/src/routes/pages/partials.tsx +41 -0
  358. package/src/routes/pages/search.tsx +18 -3
  359. package/src/routes/pages/smart-collection.tsx +340 -0
  360. package/src/routes/pages/subscribe.tsx +182 -0
  361. package/src/runtime/__tests__/index.test.ts +19 -0
  362. package/src/runtime/__tests__/node.test.ts +129 -4
  363. package/src/runtime/cloudflare.ts +21 -2
  364. package/src/runtime/index.ts +27 -1
  365. package/src/runtime/node.ts +18 -1
  366. package/src/runtime/site.ts +95 -24
  367. package/src/services/__tests__/auth.test.ts +116 -7
  368. package/src/services/__tests__/collection.test.ts +108 -4
  369. package/src/services/__tests__/language.test.ts +495 -0
  370. package/src/services/__tests__/navigation.test.ts +213 -10
  371. package/src/services/__tests__/path.test.ts +180 -0
  372. package/src/services/__tests__/post-language.test.ts +830 -0
  373. package/src/services/__tests__/post.test.ts +46 -9
  374. package/src/services/__tests__/settings.test.ts +228 -17
  375. package/src/services/__tests__/site-admin.test.ts +5 -5
  376. package/src/services/__tests__/smart-collection.test.ts +651 -0
  377. package/src/services/about-page.ts +22 -1
  378. package/src/services/auth.ts +51 -49
  379. package/src/services/bootstrap.ts +188 -38
  380. package/src/services/collection-directory-position.ts +76 -0
  381. package/src/services/collection.ts +331 -46
  382. package/src/services/custom-url.ts +27 -22
  383. package/src/services/export-theme/assets/client-site.css +1 -1
  384. package/src/services/export-theme/assets/client-site.js +123 -123
  385. package/src/services/export-theme/layouts/_default/rss.xml +77 -1
  386. package/src/services/export-theme/layouts/partials/feed-post-content.xml +105 -2
  387. package/src/services/export-theme/layouts/partials/media-gallery.html +35 -21
  388. package/src/services/export-theme/layouts/partials/pagination.html +14 -13
  389. package/src/services/export-theme/layouts/partials/post-card.html +8 -4
  390. package/src/services/export-theme/layouts/partials/reply.html +2 -1
  391. package/src/services/export-theme/layouts/partials/thread-preview.html +40 -30
  392. package/src/services/export-theme/styles/main.css +13 -1
  393. package/src/services/export.ts +278 -19
  394. package/src/services/index.ts +34 -3
  395. package/src/services/language.ts +412 -0
  396. package/src/services/mcp.ts +3 -1
  397. package/src/services/media.ts +14 -6
  398. package/src/services/navigation.ts +429 -67
  399. package/src/services/path.ts +221 -6
  400. package/src/services/post.ts +1021 -152
  401. package/src/services/search.ts +39 -2
  402. package/src/services/settings.ts +238 -43
  403. package/src/services/site-admin.ts +24 -7
  404. package/src/services/site.ts +77 -1
  405. package/src/services/smart-collection.ts +636 -0
  406. package/src/services/telegram.ts +20 -22
  407. package/src/services/upload-session.ts +1 -3
  408. package/src/style-author.css +27 -0
  409. package/src/styles/components-author.css +1622 -0
  410. package/src/styles/components.css +24 -1571
  411. package/src/styles/site-media.css +16 -2
  412. package/src/styles/tokens.css +82 -12
  413. package/src/styles/ui-author.css +7602 -0
  414. package/src/styles/ui.css +1087 -7219
  415. package/src/types/app-context.ts +7 -0
  416. package/src/types/bindings.ts +3 -0
  417. package/src/types/config.ts +94 -15
  418. package/src/types/constants.ts +188 -16
  419. package/src/types/entities.ts +87 -1
  420. package/src/types/operations.ts +64 -2
  421. package/src/types/props.ts +157 -31
  422. package/src/types/views.ts +55 -1
  423. package/src/ui/__tests__/font-themes.test.ts +52 -12
  424. package/src/ui/compose/ComposeDialog.tsx +201 -7
  425. package/src/ui/compose/ComposePrompt.tsx +1 -5
  426. package/src/ui/compose/__tests__/ComposeDialog.test.tsx +125 -0
  427. package/src/ui/dash/appearance/AdvancedContent.tsx +2 -2
  428. package/src/ui/dash/appearance/CodeInjectionContent.tsx +12 -16
  429. package/src/ui/dash/appearance/NavigationContent.tsx +117 -16
  430. package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +33 -11
  431. package/src/ui/dash/appearance/__tests__/system-nav-descriptions.test.ts +44 -0
  432. package/src/ui/dash/appearance/system-nav-descriptions.ts +75 -0
  433. package/src/ui/dash/settings/AccountMenuContent.tsx +2 -2
  434. package/src/ui/dash/settings/ApiTokensContent.tsx +2 -1
  435. package/src/ui/dash/settings/ConfigEditorContent.tsx +35 -16
  436. package/src/ui/dash/settings/GeneralContent.tsx +275 -113
  437. package/src/ui/dash/settings/LanguageContent.tsx +409 -0
  438. package/src/ui/dash/settings/SettingsRootContent.tsx +19 -1
  439. package/src/ui/dash/settings/__tests__/GeneralContent.test.tsx +324 -4
  440. package/src/ui/dash/settings/__tests__/LanguageContent.test.tsx +144 -0
  441. package/src/ui/dash/settings/__tests__/discover-copy.test.ts +66 -0
  442. package/src/ui/dash/settings/discover-copy.ts +116 -0
  443. package/src/ui/feed/LinkCard.tsx +55 -45
  444. package/src/ui/feed/NoteCard.tsx +9 -4
  445. package/src/ui/feed/PostStatusBadges.tsx +90 -43
  446. package/src/ui/feed/QuoteCard.tsx +55 -30
  447. package/src/ui/feed/ThreadPreview.tsx +6 -2
  448. package/src/ui/feed/__tests__/thread-preview.test.ts +13 -16
  449. package/src/ui/feed/__tests__/timeline-cards.test.ts +314 -49
  450. package/src/ui/feed/thread-preview-state.ts +20 -6
  451. package/src/ui/font-themes.ts +39 -29
  452. package/src/ui/layouts/BaseLayout.tsx +155 -10
  453. package/src/ui/layouts/SiteLayout.tsx +192 -34
  454. package/src/ui/layouts/__tests__/BaseLayout.test.tsx +196 -17
  455. package/src/ui/layouts/__tests__/SiteLayout.test.tsx +109 -85
  456. package/src/ui/pages/ArchivePage.tsx +282 -289
  457. package/src/ui/pages/CollectionPage.tsx +86 -76
  458. package/src/ui/pages/CollectionsPage.tsx +5 -0
  459. package/src/ui/pages/ComposePage.tsx +8 -0
  460. package/src/ui/pages/PostPage.tsx +47 -0
  461. package/src/ui/pages/SearchPage.tsx +3 -3
  462. package/src/ui/pages/SmartCollectionPage.tsx +434 -0
  463. package/src/ui/pages/SubscribePage.tsx +236 -0
  464. package/src/ui/pages/__tests__/ArchivePage.test.tsx +65 -9
  465. package/src/ui/pages/__tests__/collections-directory-rows.test.tsx +92 -0
  466. package/src/ui/pages/__tests__/collections-view-links.test.tsx +81 -0
  467. package/src/ui/shared/CollectionDirectory.tsx +178 -3
  468. package/src/ui/shared/CollectionsManager.tsx +70 -17
  469. package/src/ui/shared/CopyField.tsx +127 -0
  470. package/src/ui/shared/EmptyPostContent.tsx +23 -0
  471. package/src/ui/shared/MediaGallery.tsx +125 -46
  472. package/src/ui/shared/Pagination.tsx +20 -5
  473. package/src/ui/shared/PostFooter.tsx +20 -11
  474. package/src/ui/shared/__tests__/archive-labels.test.ts +100 -0
  475. package/src/ui/shared/__tests__/media-gallery.test.ts +210 -2
  476. package/src/ui/shared/__tests__/navigation-labels.test.ts +16 -22
  477. package/src/ui/shared/__tests__/pagination.test.ts +38 -24
  478. package/src/ui/shared/__tests__/post-footer.test.ts +32 -0
  479. package/src/ui/shared/archive-labels.ts +98 -0
  480. package/src/ui/shared/collection-management-labels.ts +94 -16
  481. package/src/ui/shared/navigation-labels.ts +45 -71
  482. package/src/ui/shared/post-article-attributes.ts +2 -0
  483. package/src/ui/shared/smart-collection-labels.ts +343 -0
  484. package/dist/app-C75Zc8dD.js +0 -6
  485. package/dist/client/_assets/chunks/heic-to-DUUaO23q.js +0 -1
  486. package/dist/client/_assets/chunks/module-DcsAZQZ_.js +0 -716
  487. package/dist/client/_assets/chunks/url-CU9pEbk5.js +0 -1
  488. package/dist/client/_assets/client-Bq0vre8Y.js +0 -296
  489. package/dist/client/_assets/client-D3VZyG4L.css +0 -2
  490. package/dist/client/_assets/client-auth-F2rb9WfF.js +0 -5571
  491. package/dist/env-BPYViDJJ.js +0 -281
  492. package/dist/github-api-BNF35Lkx.js +0 -176
  493. package/dist/github-app-DdiD-bC4.js +0 -275
  494. package/dist/github-sync-Bqg62IvV.js +0 -4
  495. package/dist/github-sync-HdK2EgvJ.js +0 -431
  496. package/dist/url-CbLAtlZe.js +0 -900
  497. package/src/client/__tests__/collection-created-notice.test.ts +0 -36
  498. package/src/client/__tests__/collection-form-bridge.test.ts +0 -216
  499. package/src/client/__tests__/post-form-bridge.test.ts +0 -82
  500. package/src/client/collection-created-notice.ts +0 -66
  501. package/src/client/collection-form-bridge.ts +0 -191
  502. package/src/client/components/__tests__/jant-post-form.test.ts +0 -193
  503. package/src/client/components/jant-post-form.ts +0 -422
  504. package/src/client/components/post-form-template.ts +0 -352
  505. package/src/client/components/post-form-types.ts +0 -96
  506. package/src/client/media-metadata.ts +0 -247
  507. package/src/client/post-form-bridge.ts +0 -193
  508. package/src/ui/pages/CollectionEditorPage.tsx +0 -175
@@ -0,0 +1,156 @@
1
+ import { beforeEach, describe, expect, it } from "vitest";
2
+ import { createTestApp } from "../../__tests__/helpers/app.js";
3
+ import { archiveRoutes } from "../pages/archive.js";
4
+ import { collectionRoutes } from "../pages/collection.js";
5
+ import { collectionsPageRoutes } from "../pages/collections.js";
6
+ import { featuredRoutes } from "../pages/featured.js";
7
+ import { latestRoutes } from "../pages/latest.js";
8
+ import { pageRoutes } from "../pages/page.js";
9
+ import { feedRoutes } from "../feed/feed.js";
10
+
11
+ /**
12
+ * The validator every feed surface carries.
13
+ *
14
+ * Six route modules assemble a feed, and each used to build its own Response.
15
+ * They now share `renderFeed`, so what these tests hold in place is that the
16
+ * sharing is real — a tag on every feed, not just on `/feed`, whose tests are
17
+ * the ones anyone remembers to run.
18
+ *
19
+ * Turning that tag into a `304` belongs to `withConditionalResponse` at the
20
+ * edge of the app, and is covered where it lives.
21
+ */
22
+
23
+ const ETAG = /^"[0-9a-f]{32}"$/;
24
+
25
+ function setup() {
26
+ const testApp = createTestApp({ rssPublishDelaySeconds: 0 });
27
+ const { app } = testApp;
28
+
29
+ app.use("*", async (c, next) => {
30
+ c.set("publicPath", c.req.path);
31
+ c.set("publicRequestUrl", c.req.url);
32
+ await next();
33
+ });
34
+
35
+ app.route("/feed", feedRoutes);
36
+ app.route("/latest", latestRoutes);
37
+ app.route("/featured", featuredRoutes);
38
+ app.route("/archive", archiveRoutes);
39
+ app.route("/collections", collectionsPageRoutes);
40
+ app.route("/collections", collectionRoutes);
41
+ // The root namespace matches any slug, so it is mounted last.
42
+ app.route("/", pageRoutes);
43
+
44
+ return testApp;
45
+ }
46
+
47
+ /**
48
+ * Seed every surface with an entry.
49
+ *
50
+ * An empty feed has no content timestamp to date itself by and falls back to
51
+ * the current time, so its bytes — and its tag — change on every poll. Feeds
52
+ * under test therefore all carry content.
53
+ */
54
+ async function seed(services: ReturnType<typeof createTestApp>["services"]) {
55
+ await services.posts.create({
56
+ format: "note",
57
+ title: "Featured note",
58
+ bodyMarkdown: "Featured body",
59
+ status: "published",
60
+ featured: true,
61
+ });
62
+ await services.posts.create({
63
+ format: "quote",
64
+ title: "A quote",
65
+ quoteText: "Something worth keeping",
66
+ bodyMarkdown: "Quote body",
67
+ status: "published",
68
+ });
69
+
70
+ for (const [slug, title] of [
71
+ ["reading", "Reading"],
72
+ ["movies", "Movies"],
73
+ ] as const) {
74
+ const collection = await services.collections.create({ slug, title });
75
+ const post = await services.posts.create({
76
+ format: "note",
77
+ title: `${title} log`,
78
+ bodyMarkdown: `${title} body`,
79
+ status: "published",
80
+ });
81
+ await services.collections.addThread(collection.id, post.id);
82
+ }
83
+
84
+ await services.smartCollections.create({
85
+ slug: "quotes",
86
+ title: "Quotes",
87
+ selection: { format: "quote" },
88
+ });
89
+ }
90
+
91
+ const FEED_PATHS = [
92
+ ["site main feed", "/feed"],
93
+ ["latest", "/latest/feed"],
94
+ ["featured", "/featured/feed"],
95
+ ["archive", "/archive/feed"],
96
+ ["single collection", "/reading/feed"],
97
+ ["collection selection", "/collections/reading+movies/feed"],
98
+ ["smart collection", "/quotes/feed"],
99
+ ] as const;
100
+
101
+ describe("feed conditional GET", () => {
102
+ let app: ReturnType<typeof createTestApp>["app"];
103
+ let services: ReturnType<typeof createTestApp>["services"];
104
+
105
+ beforeEach(async () => {
106
+ const testApp = setup();
107
+ app = testApp.app;
108
+ services = testApp.services;
109
+ await seed(services);
110
+ });
111
+
112
+ describe.each(FEED_PATHS)("%s (%s)", (_name, path) => {
113
+ it("serves the feed with a strong validator", async () => {
114
+ const res = await app.request(path);
115
+
116
+ expect(res.status).toBe(200);
117
+ expect(res.headers.get("ETag")).toMatch(ETAG);
118
+ expect(res.headers.get("Cache-Control")).toBe("public, max-age=60");
119
+ expect(res.headers.get("Content-Type")).toBe(
120
+ "application/atom+xml; charset=utf-8",
121
+ );
122
+ expect(await res.text()).toContain("<feed");
123
+ });
124
+
125
+ it("re-derives the same tag for an unchanged feed", async () => {
126
+ const first = await app.request(path);
127
+ const second = await app.request(path);
128
+
129
+ expect(second.headers.get("ETag")).toBe(first.headers.get("ETag"));
130
+ });
131
+ });
132
+
133
+ it("issues a new tag once the feed changes", async () => {
134
+ const before = await app.request("/latest/feed");
135
+ const staleTag = before.headers.get("ETag") as string;
136
+
137
+ await services.posts.create({
138
+ format: "note",
139
+ title: "Published after the poll",
140
+ bodyMarkdown: "New body",
141
+ status: "published",
142
+ });
143
+
144
+ const after = await app.request("/latest/feed");
145
+
146
+ expect(after.headers.get("ETag")).not.toBe(staleTag);
147
+ expect(await after.text()).toContain("Published after the poll");
148
+ });
149
+
150
+ it("gives two different feeds two different tags", async () => {
151
+ const latest = await app.request("/latest/feed");
152
+ const featured = await app.request("/featured/feed");
153
+
154
+ expect(featured.headers.get("ETag")).not.toBe(latest.headers.get("ETag"));
155
+ });
156
+ });
@@ -45,6 +45,30 @@ describe("Collections API Routes", () => {
45
45
  expect(body.directoryItems[0].collectionId).toBe(col.id);
46
46
  });
47
47
 
48
+ it("returns smart collections with the same two measures", async () => {
49
+ const { app, services } = createTestApp();
50
+ app.route("/api/collections", collectionsApiRoutes);
51
+
52
+ await services.smartCollections.create({
53
+ slug: "quotes",
54
+ title: "Quotes",
55
+ selection: { format: "quote" },
56
+ });
57
+ const quote = await services.posts.create({
58
+ format: "quote",
59
+ quoteText: "Worth keeping",
60
+ });
61
+
62
+ const res = await app.request("/api/collections");
63
+ const body = await res.json();
64
+
65
+ expect(body.smartCollections).toHaveLength(1);
66
+ expect(body.smartCollections[0].threadCount).toBe(1);
67
+ expect(body.smartCollections[0].recentActivityAt).toBe(
68
+ quote.lastActivityAt,
69
+ );
70
+ });
71
+
48
72
  it("returns divider labels", async () => {
49
73
  const { app, services } = createTestApp({ authenticated: true });
50
74
  app.route("/api/collections", collectionsApiRoutes);
@@ -143,6 +167,61 @@ describe("Collections API Routes", () => {
143
167
  });
144
168
  });
145
169
 
170
+ describe("GET /api/collections/slug", () => {
171
+ it("answers whether an address is free", async () => {
172
+ const { app, services } = createTestApp({ authenticated: true });
173
+ app.route("/api/collections", collectionsApiRoutes);
174
+
175
+ await services.collections.create({ slug: "tech", title: "Tech" });
176
+
177
+ const taken = await app.request(
178
+ "/api/collections/slug?mode=check&slug=tech",
179
+ );
180
+ expect(taken.status).toBe(200);
181
+ expect(await taken.json()).toEqual({ slug: "tech", available: false });
182
+
183
+ const free = await app.request(
184
+ "/api/collections/slug?mode=check&slug=reading",
185
+ );
186
+ expect(await free.json()).toEqual({ slug: "reading", available: true });
187
+ });
188
+
189
+ it("ignores the address the named collection already holds", async () => {
190
+ const { app, services } = createTestApp({ authenticated: true });
191
+ app.route("/api/collections", collectionsApiRoutes);
192
+
193
+ const collection = await services.collections.create({
194
+ slug: "tech",
195
+ title: "Tech",
196
+ });
197
+
198
+ const res = await app.request(
199
+ `/api/collections/slug?mode=check&slug=tech&collectionId=${collection.id}`,
200
+ );
201
+ expect(await res.json()).toEqual({ slug: "tech", available: true });
202
+ });
203
+
204
+ it("refuses a reserved address rather than calling it taken", async () => {
205
+ const { app } = createTestApp({ authenticated: true });
206
+ app.route("/api/collections", collectionsApiRoutes);
207
+
208
+ const res = await app.request(
209
+ "/api/collections/slug?mode=check&slug=settings",
210
+ );
211
+ expect(res.status).toBe(400);
212
+ });
213
+
214
+ it("returns 401 when not authenticated", async () => {
215
+ const { app } = createTestApp({ authenticated: false });
216
+ app.route("/api/collections", collectionsApiRoutes);
217
+
218
+ const res = await app.request(
219
+ "/api/collections/slug?mode=check&slug=tech",
220
+ );
221
+ expect(res.status).toBe(401);
222
+ });
223
+ });
224
+
146
225
  describe("POST /api/collections", () => {
147
226
  it("returns 401 when not authenticated", async () => {
148
227
  const { app } = createTestApp({ authenticated: false });
@@ -458,6 +537,41 @@ describe("Collections API Routes", () => {
458
537
  expect(reordered[1]?.id).toBe(itemC?.id);
459
538
  expect(reordered[2]?.id).toBe(itemB?.id);
460
539
  });
540
+
541
+ it("moves a smart collection, which the directory names by its own id", async () => {
542
+ const { app, services } = createTestApp({ authenticated: true });
543
+ app.route("/api/collections", collectionsApiRoutes);
544
+
545
+ const a = await services.collections.create({ slug: "a", title: "A" });
546
+ const b = await services.collections.create({ slug: "b", title: "B" });
547
+ const quotes = await services.smartCollections.create({
548
+ slug: "quotes",
549
+ title: "Quotes",
550
+ });
551
+
552
+ const items = await services.collections.listDirectoryItems();
553
+
554
+ // The shape the drag surface sends: a smart collection id beside a
555
+ // directory row id.
556
+ const res = await app.request(
557
+ `/api/collections/directory-items/${quotes.id}/move`,
558
+ {
559
+ method: "PUT",
560
+ headers: { "Content-Type": "application/json" },
561
+ body: JSON.stringify({
562
+ after: items[0]?.id ?? "",
563
+ before: items[1]?.id ?? "",
564
+ }),
565
+ },
566
+ );
567
+
568
+ expect(res.status).toBe(200);
569
+
570
+ const reordered = await services.collections.listDirectoryItems();
571
+ expect(
572
+ reordered.map((item) => item.collectionId ?? item.smartCollectionId),
573
+ ).toEqual([a.id, quotes.id, b.id]);
574
+ });
461
575
  });
462
576
 
463
577
  describe("POST /api/collections/:id/threads", () => {
@@ -0,0 +1,222 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { createTestApp } from "../../../__tests__/helpers/app.js";
3
+ import { enforceD1BoundParameterLimit } from "../../../__tests__/helpers/db.js";
4
+ import { DISCOVER_STATUS_MAX_IDS } from "../../../lib/discover.js";
5
+ import { createEntityId } from "../../../lib/ids.js";
6
+ import { discoverApiRoutes } from "../discover.js";
7
+
8
+ type Status = { id: string; latest: boolean; featured: boolean };
9
+
10
+ async function setupApp(
11
+ options: Parameters<typeof createTestApp>[0] = {},
12
+ { discover = "latest" }: { discover?: string | null } = {},
13
+ ) {
14
+ const testApp = createTestApp({ authenticated: false, ...options });
15
+ testApp.app.route("/api/discover", discoverApiRoutes);
16
+ if (discover) await testApp.services.settings.set("DISCOVER", discover);
17
+ return testApp;
18
+ }
19
+
20
+ async function ask(
21
+ app: { request: (path: string) => Promise<Response> },
22
+ ids: string[],
23
+ extra = "",
24
+ ): Promise<Status[]> {
25
+ const query = ids.map((id) => `id=${id}`).join("&");
26
+ const res = await app.request(`/api/discover/posts?${query}${extra}`);
27
+ expect(res.status).toBe(200);
28
+ const body = (await res.json()) as { posts: Status[] };
29
+ return body.posts;
30
+ }
31
+
32
+ describe("GET /api/discover/posts", () => {
33
+ it("says whether each post is in the Latest and featured feeds", async () => {
34
+ const { app, services } = await setupApp();
35
+ const plain = await services.posts.create({
36
+ format: "note",
37
+ bodyMarkdown: "plain",
38
+ });
39
+ const featured = await services.posts.create({
40
+ format: "note",
41
+ bodyMarkdown: "featured",
42
+ featured: true,
43
+ });
44
+ const hiddenFeatured = await services.posts.create({
45
+ format: "note",
46
+ bodyMarkdown: "hidden from Latest, still featured",
47
+ visibility: "latest_hidden",
48
+ featured: true,
49
+ });
50
+
51
+ expect(await ask(app, [plain.id, featured.id, hiddenFeatured.id])).toEqual([
52
+ { id: plain.id, latest: true, featured: false },
53
+ { id: featured.id, latest: true, featured: true },
54
+ { id: hiddenFeatured.id, latest: false, featured: true },
55
+ ]);
56
+ });
57
+
58
+ it("answers false for every way a post can be gone", async () => {
59
+ const { app, services } = await setupApp();
60
+ const privatePost = await services.posts.create({
61
+ format: "note",
62
+ bodyMarkdown: "private",
63
+ visibility: "private",
64
+ featured: true,
65
+ });
66
+ const draft = await services.posts.create({
67
+ format: "note",
68
+ bodyMarkdown: "draft",
69
+ status: "draft",
70
+ });
71
+ const deleted = await services.posts.create({
72
+ format: "note",
73
+ bodyMarkdown: "deleted",
74
+ featured: true,
75
+ });
76
+ await services.posts.delete(deleted.id);
77
+ const unknown = createEntityId("post");
78
+
79
+ const ids = [privatePost.id, draft.id, deleted.id, unknown];
80
+ expect(await ask(app, ids)).toEqual(
81
+ ids.map((id) => ({ id, latest: false, featured: false })),
82
+ );
83
+ });
84
+
85
+ it("counts a Thread featured when a reply in it is", async () => {
86
+ const { app, services } = await setupApp();
87
+ const root = await services.posts.create({
88
+ format: "note",
89
+ bodyMarkdown: "root",
90
+ });
91
+ const reply = await services.posts.create({
92
+ format: "note",
93
+ bodyMarkdown: "featured reply",
94
+ replyToId: root.id,
95
+ featured: true,
96
+ });
97
+
98
+ // A reply is never an entry of its own, so it is in neither feed; its
99
+ // Thread is in both.
100
+ expect(await ask(app, [root.id, reply.id])).toEqual([
101
+ { id: root.id, latest: true, featured: true },
102
+ { id: reply.id, latest: false, featured: false },
103
+ ]);
104
+ });
105
+
106
+ it("holds a new post back for the RSS delay, as the feeds do", async () => {
107
+ const { app, services } = await setupApp({ rssPublishDelaySeconds: 300 });
108
+ const currentTime = Math.floor(Date.now() / 1000);
109
+ const settled = await services.posts.create({
110
+ format: "note",
111
+ bodyMarkdown: "settled",
112
+ publishedAt: currentTime - 400,
113
+ });
114
+ const fresh = await services.posts.create({
115
+ format: "note",
116
+ bodyMarkdown: "fresh",
117
+ publishedAt: currentTime,
118
+ });
119
+
120
+ expect(await ask(app, [settled.id, fresh.id])).toEqual([
121
+ { id: settled.id, latest: true, featured: false },
122
+ { id: fresh.id, latest: false, featured: false },
123
+ ]);
124
+ });
125
+
126
+ it("scopes the answer to one language view with ?lang=", async () => {
127
+ const { app, services } = await setupApp();
128
+ await services.settings.set("SITE_LANGUAGE", "zh-Hans");
129
+ await services.settings.set("ADDITIONAL_LANGUAGES", "en");
130
+ await services.settings.set("MULTILINGUAL_ENABLED", "true");
131
+ const english = await services.posts.create({
132
+ format: "note",
133
+ bodyMarkdown: "hello",
134
+ language: "en",
135
+ });
136
+
137
+ expect(await ask(app, [english.id], "&lang=en")).toEqual([
138
+ { id: english.id, latest: true, featured: false },
139
+ ]);
140
+ expect(await ask(app, [english.id], "&lang=zh-Hans")).toEqual([
141
+ { id: english.id, latest: false, featured: false },
142
+ ]);
143
+ });
144
+
145
+ it("answers each post once, in the order asked", async () => {
146
+ const { app, services } = await setupApp();
147
+ const first = await services.posts.create({
148
+ format: "note",
149
+ bodyMarkdown: "first",
150
+ });
151
+ const second = await services.posts.create({
152
+ format: "note",
153
+ bodyMarkdown: "second",
154
+ });
155
+
156
+ const answer = await ask(app, [second.id, first.id, second.id]);
157
+
158
+ expect(answer.map((status) => status.id)).toEqual([second.id, first.id]);
159
+ });
160
+
161
+ it("fits a full request within D1's bound-parameter limit", async () => {
162
+ const { app, services, sqlite } = await setupApp();
163
+ const real = await services.posts.create({
164
+ format: "note",
165
+ bodyMarkdown: "real",
166
+ featured: true,
167
+ });
168
+ const ids = [
169
+ real.id,
170
+ ...Array.from({ length: DISCOVER_STATUS_MAX_IDS - 1 }, () =>
171
+ createEntityId("post"),
172
+ ),
173
+ ];
174
+ enforceD1BoundParameterLimit(sqlite);
175
+
176
+ const answer = await ask(app, ids);
177
+
178
+ expect(answer).toHaveLength(DISCOVER_STATUS_MAX_IDS);
179
+ expect(answer[0]).toEqual({ id: real.id, latest: true, featured: true });
180
+ });
181
+
182
+ it("rejects a request with no ids, too many, or one that is not a post ID", async () => {
183
+ const { app } = await setupApp();
184
+ const tooMany = Array.from(
185
+ { length: DISCOVER_STATUS_MAX_IDS + 1 },
186
+ () => `id=${createEntityId("post")}`,
187
+ ).join("&");
188
+
189
+ for (const query of [
190
+ "",
191
+ tooMany,
192
+ "id=not-an-id",
193
+ `id=${createEntityId("collection")}`,
194
+ ]) {
195
+ const res = await app.request(`/api/discover/posts?${query}`);
196
+ expect(res.status, query.slice(0, 40)).toBe(400);
197
+ }
198
+ });
199
+
200
+ it("is not there for a site that is not in Discover", async () => {
201
+ const { app } = await setupApp({}, { discover: "off" });
202
+
203
+ const res = await app.request(
204
+ `/api/discover/posts?id=${createEntityId("post")}`,
205
+ );
206
+
207
+ expect(res.status).toBe(404);
208
+ });
209
+
210
+ it("still answers when the public JSON API is turned off", async () => {
211
+ const { app, services } = await setupApp();
212
+ await services.settings.set("PUBLIC_API_ENABLED", "false");
213
+ const post = await services.posts.create({
214
+ format: "note",
215
+ bodyMarkdown: "listed",
216
+ });
217
+
218
+ expect(await ask(app, [post.id])).toEqual([
219
+ { id: post.id, latest: true, featured: false },
220
+ ]);
221
+ });
222
+ });