@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.
- package/README.md +11 -11
- package/bin/commands/assets/upload.js +192 -15
- package/bin/commands/import-site.js +60 -4
- package/bin/commands/reset-password.js +21 -8
- package/bin/commands/site/snapshot/export.js +1 -1
- package/bin/commands/site/snapshot/import.js +1 -1
- package/bin/lib/d1-query.js +5 -16
- package/bin/lib/node-env.js +40 -4
- package/bin/lib/site-selection.js +85 -29
- package/bin/lib/wrangler-cli.js +28 -2
- package/bin/lib/wrangler-json.js +35 -0
- package/dist/{app-C7_4oab9.js → app-K_Aa1MMn.js} +52476 -13330
- package/dist/client/.vite/manifest.json +255 -90
- package/dist/client/_assets/chunks/confirm-D7GrwsCN.js +45 -0
- package/dist/client/_assets/chunks/create-editor-CD3FhrOB.js +257 -0
- package/dist/client/_assets/chunks/{native-DpcrFAPh.js → emoji-mart-C4RpRKcU.js} +716 -1
- package/dist/client/_assets/chunks/footnote-rail-CmeoVtnU.js +1 -0
- package/dist/client/_assets/chunks/heic-to-CuP2Qlsw.js +1 -0
- package/dist/client/_assets/chunks/jant-locale-picker-DL6mIOT5.js +69 -0
- package/dist/client/_assets/chunks/lit-DW8VmJAT.js +2 -0
- package/dist/client/_assets/chunks/mediabunny-DRaCQ1r-.js +31 -0
- package/dist/client/_assets/chunks/rolldown-runtime-hePW80VL.js +1 -0
- package/dist/client/_assets/chunks/settings-paths-BcdySNx6.js +84 -0
- package/dist/client/_assets/chunks/site-header-nav-DzXsFk05.js +1 -0
- package/dist/client/_assets/chunks/slug-format-CRXO1AL5.js +1 -0
- package/dist/client/_assets/chunks/slugify-373citbs.js +1 -0
- package/dist/client/_assets/chunks/sortable-list-CgaL2jCs.js +2 -0
- package/dist/client/_assets/chunks/types-D5iF3H1a.js +1 -0
- package/dist/client/_assets/chunks/unsafe-html-B5ZEXaxg.js +1 -0
- package/dist/client/_assets/chunks/unsafe-svg-0QCkP0vZ.js +2 -0
- package/dist/client/_assets/client-GiYENVw8.css +2 -0
- package/dist/client/_assets/client-VnFxJN7G.js +295 -0
- package/dist/client/_assets/client-auth-k8gJ6QJj.js +828 -0
- package/dist/client/_assets/client-author-DQ8R-8KR.css +2 -0
- package/dist/client/_assets/client-compose-B_kDtWkW.js +2004 -0
- package/dist/client/_assets/client-manage-M90aSTOk.js +2170 -0
- package/dist/client/_assets/client-settings-DfYs9n1F.js +1112 -0
- package/dist/{export-Dl5KCiEs.js → github-sync-BPAvT999.js} +2254 -107
- package/dist/index.js +2 -5
- package/dist/node.js +5 -7
- package/package.json +3 -3
- package/src/__tests__/auth-session-freshness.test.ts +57 -0
- package/src/__tests__/basecoat-variants.test.ts +121 -0
- package/src/__tests__/bin/assets-upload.test.ts +68 -0
- package/src/__tests__/export-hugo-build.test.ts +260 -0
- package/src/__tests__/export-service.test.ts +231 -0
- package/src/__tests__/helpers/component-css.ts +91 -0
- package/src/__tests__/helpers/db.ts +33 -0
- package/src/__tests__/helpers/export-fixtures.ts +3 -0
- package/src/__tests__/stylesheet-audience.test.ts +159 -0
- package/src/__tests__/stylesheet-breakpoints.test.ts +183 -0
- package/src/app.tsx +71 -16
- package/src/auth.ts +18 -1
- package/src/client/__tests__/collection-page-actions.test.ts +51 -2
- package/src/client/__tests__/compose-bridge-image-upload.test.ts +196 -0
- package/src/client/__tests__/compose-bridge-video-poster.test.ts +159 -0
- package/src/client/__tests__/compose-bridge.test.ts +272 -0
- package/src/client/__tests__/compose-launch.test.ts +62 -1
- package/src/client/__tests__/compose-shortcuts.test.ts +38 -9
- package/src/client/__tests__/compose-triggers.test.ts +90 -0
- package/src/client/__tests__/copy-field.test.ts +162 -0
- package/src/client/__tests__/custom-url-menu.test.ts +127 -6
- package/src/client/__tests__/image-processor.test.ts +324 -2
- package/src/client/__tests__/lazy-entries.test.ts +92 -0
- package/src/client/__tests__/media-scroll-hint.test.ts +155 -0
- package/src/client/__tests__/runtime-paths.test.ts +84 -0
- package/src/client/__tests__/smart-collection-page-actions.test.ts +158 -0
- package/src/client/__tests__/toast.test.ts +23 -0
- package/src/client/__tests__/video-processor-probe.test.ts +175 -0
- package/src/client/__tests__/video-processor.test.ts +181 -0
- package/src/client/audio-processor.ts +24 -15
- package/src/client/audio-waveform.ts +49 -0
- package/src/client/collection-dialog-host.ts +72 -0
- package/src/client/collection-navigation.ts +51 -16
- package/src/client/collection-page-actions.ts +26 -0
- package/src/client/components/__tests__/jant-collection-dialog.test.ts +329 -0
- package/src/client/components/__tests__/jant-collection-directory.test.ts +274 -10
- package/src/client/components/__tests__/jant-collection-form.test.ts +59 -178
- package/src/client/components/__tests__/jant-command-palette.test.ts +28 -0
- package/src/client/components/__tests__/jant-compose-dialog-draft-storage.test.ts +340 -0
- package/src/client/components/__tests__/jant-compose-dialog.test.ts +1250 -53
- package/src/client/components/__tests__/jant-compose-editor.test.ts +544 -40
- package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +0 -1
- package/src/client/components/__tests__/jant-locale-picker.test.ts +207 -0
- package/src/client/components/__tests__/jant-nav-manager.test.ts +234 -15
- package/src/client/components/__tests__/jant-post-menu.test.ts +413 -2
- package/src/client/components/__tests__/jant-post-picker.test.ts +197 -0
- package/src/client/components/__tests__/jant-settings-avatar.test.ts +15 -12
- package/src/client/components/__tests__/jant-settings-general.test.ts +541 -172
- package/src/client/components/__tests__/jant-settings-language.test.ts +851 -0
- package/src/client/components/__tests__/jant-smart-collection-dialog.test.ts +665 -0
- package/src/client/components/collection-dialog-types.ts +49 -0
- package/src/client/components/collection-manager-types.ts +32 -1
- package/src/client/components/collection-types.ts +1 -15
- package/src/client/components/compose-types.ts +75 -1
- package/src/client/components/jant-collection-dialog.ts +722 -0
- package/src/client/components/jant-collection-directory.ts +567 -182
- package/src/client/components/jant-collection-form.ts +53 -232
- package/src/client/components/jant-command-palette.ts +100 -49
- package/src/client/components/jant-compose-dialog.ts +1486 -707
- package/src/client/components/jant-compose-editor.ts +673 -336
- package/src/client/components/jant-compose-fullscreen.ts +51 -43
- package/src/client/components/jant-config-editor.ts +65 -53
- package/src/client/components/jant-confirm-dialog.ts +13 -11
- package/src/client/components/jant-locale-picker.ts +418 -0
- package/src/client/components/jant-media-lightbox.ts +146 -136
- package/src/client/components/jant-nav-manager.ts +690 -366
- package/src/client/components/jant-post-menu.ts +1086 -282
- package/src/client/components/jant-post-picker.ts +324 -0
- package/src/client/components/jant-repo-picker.ts +138 -106
- package/src/client/components/jant-settings-avatar.ts +31 -27
- package/src/client/components/jant-settings-general.ts +617 -543
- package/src/client/components/jant-settings-language.ts +1087 -0
- package/src/client/components/jant-smart-collection-dialog.ts +1021 -0
- package/src/client/components/jant-text-preview.ts +94 -86
- package/src/client/components/nav-manager-types.ts +62 -1
- package/src/client/components/settings-types.ts +14 -29
- package/src/client/components/smart-collection-conditions.ts +285 -0
- package/src/client/components/smart-collection-dialog-types.ts +68 -0
- package/src/client/compose-bridge.ts +214 -141
- package/src/client/compose-launch.ts +76 -2
- package/src/client/compose-shortcuts.ts +6 -8
- package/src/client/compose-triggers.ts +50 -0
- package/src/client/copy-field.ts +108 -0
- package/src/client/custom-url-menu.ts +60 -4
- package/src/client/icons.ts +14 -0
- package/src/client/image-plan.ts +165 -0
- package/src/client/image-processor.ts +195 -117
- package/src/client/image-worker-client.ts +45 -0
- package/src/client/image-worker.ts +135 -0
- package/src/client/lazy-entries.ts +95 -0
- package/src/client/lazy-slugify.ts +2 -2
- package/src/client/media-scroll-hint.ts +43 -2
- package/src/client/mediabunny.ts +29 -0
- package/src/client/post-picker.ts +41 -0
- package/src/client/runtime-paths.ts +74 -1
- package/src/client/settings-bridge.ts +4 -10
- package/src/client/site-header-nav.js +18 -6
- package/src/client/smart-collection-dialog-host.ts +116 -0
- package/src/client/smart-collection-page-actions.ts +208 -0
- package/src/client/tiptap/__tests__/bubble-menu.test.ts +110 -0
- package/src/client/tiptap/__tests__/floating-position.test.ts +136 -1
- package/src/client/tiptap/__tests__/inline-image-upload.test.ts +37 -0
- package/src/client/tiptap/bubble-menu.ts +47 -16
- package/src/client/tiptap/floating-position.ts +216 -2
- package/src/client/tiptap/inline-image-upload.ts +7 -3
- package/src/client/tiptap/link-toolbar.ts +22 -9
- package/src/client/toast.ts +13 -0
- package/src/client/types/vite-worker.d.ts +12 -0
- package/src/client/upload-with-metadata.ts +3 -13
- package/src/client/video-processor.ts +351 -107
- package/src/client-auth.ts +14 -31
- package/src/client-compose.ts +19 -0
- package/src/client-manage.ts +13 -0
- package/src/client-settings.ts +14 -0
- package/src/client-site.ts +10 -3
- package/src/client.ts +4 -0
- package/src/db/__tests__/backfill-nav-label-mirrors.test.ts +149 -0
- package/src/db/__tests__/schema-enum-sources.test.ts +33 -0
- package/src/db/__tests__/smart-collection-placement.test.ts +402 -0
- package/src/db/backfills/0006_clear_mirrored_page_nav_labels.sql +31 -0
- package/src/db/migrations/0032_married_tomas.sql +3 -0
- package/src/db/migrations/0033_busy_johnny_storm.sql +198 -0
- package/src/db/migrations/0034_cheerful_rumiko_fujikawa.sql +35 -0
- package/src/db/migrations/meta/0032_snapshot.json +2582 -0
- package/src/db/migrations/meta/0033_snapshot.json +2816 -0
- package/src/db/migrations/meta/0034_snapshot.json +2816 -0
- package/src/db/migrations/meta/_journal.json +21 -0
- package/src/db/migrations/pg/0030_abandoned_nighthawk.sql +3 -0
- package/src/db/migrations/pg/0031_strong_ultimatum.sql +128 -0
- package/src/db/migrations/pg/0032_furry_multiple_man.sql +6 -0
- package/src/db/migrations/pg/meta/0030_snapshot.json +3329 -0
- package/src/db/migrations/pg/meta/0031_snapshot.json +3660 -0
- package/src/db/migrations/pg/meta/0032_snapshot.json +3660 -0
- package/src/db/migrations/pg/meta/_journal.json +21 -0
- package/src/db/pg/schema.ts +195 -27
- package/src/db/post-visibility.ts +117 -0
- package/src/db/schema.ts +222 -27
- package/src/i18n/__tests__/detect.test.ts +26 -81
- package/src/i18n/__tests__/supported-locales.test.ts +83 -0
- package/src/i18n/context.tsx +12 -2
- package/src/i18n/detect.ts +7 -66
- package/src/i18n/locales/glossary.zh-Hans.yml +44 -4
- package/src/i18n/locales/glossary.zh-Hant.yml +44 -4
- package/src/i18n/locales/public/en.po +490 -158
- package/src/i18n/locales/public/en.ts +1 -1
- package/src/i18n/locales/public/zh-Hans.po +487 -155
- package/src/i18n/locales/public/zh-Hans.ts +1 -1
- package/src/i18n/locales/public/zh-Hant.po +487 -155
- package/src/i18n/locales/public/zh-Hant.ts +1 -1
- package/src/i18n/locales/settings/en.po +551 -69
- package/src/i18n/locales/settings/en.ts +1 -1
- package/src/i18n/locales/settings/zh-Hans.po +597 -115
- package/src/i18n/locales/settings/zh-Hans.ts +1 -1
- package/src/i18n/locales/settings/zh-Hant.po +610 -128
- package/src/i18n/locales/settings/zh-Hant.ts +1 -1
- package/src/i18n/locales.ts +94 -0
- package/src/i18n/middleware.ts +12 -2
- package/src/i18n/supported-locales.ts +80 -0
- package/src/lib/__tests__/api-settings.test.ts +11 -2
- package/src/lib/__tests__/asset-path.test.ts +92 -0
- package/src/lib/__tests__/deferred.test.ts +90 -0
- package/src/lib/__tests__/discover-ping.test.ts +89 -0
- package/src/lib/__tests__/discover.test.ts +410 -0
- package/src/lib/__tests__/feed-policy.test.ts +26 -0
- package/src/lib/__tests__/feed.test.ts +1526 -15
- package/src/lib/__tests__/filter-dimensions.test.ts +341 -0
- package/src/lib/__tests__/hosted-signin.test.ts +1 -1
- package/src/lib/__tests__/http-cache.test.ts +162 -0
- package/src/lib/__tests__/jant-docs-dev.test.ts +21 -0
- package/src/lib/__tests__/jant-docs.test.ts +23 -0
- package/src/lib/__tests__/lang-detect.test.ts +231 -0
- package/src/lib/__tests__/media-helpers.test.ts +81 -0
- package/src/lib/__tests__/navigation.test.ts +135 -1
- package/src/lib/__tests__/resolve-config.test.ts +64 -3
- package/src/lib/__tests__/schemas.test.ts +42 -4
- package/src/lib/__tests__/site-unavailable-page.test.ts +25 -0
- package/src/lib/__tests__/slug.test.ts +70 -0
- package/src/lib/__tests__/slugify.test.ts +85 -0
- package/src/lib/__tests__/storage.test.ts +32 -0
- package/src/lib/__tests__/thread-fold.test.ts +113 -0
- package/src/lib/__tests__/upload-sideload.test.ts +14 -0
- package/src/lib/__tests__/url-fetch.test.ts +1 -2
- package/src/lib/__tests__/url.test.ts +78 -72
- package/src/lib/__tests__/view-language.test.ts +413 -0
- package/src/lib/__tests__/worker-response-cache.test.ts +90 -0
- package/src/lib/address.ts +57 -0
- package/src/lib/api-posts.ts +12 -0
- package/src/lib/asset-path.ts +98 -1
- package/src/lib/build-env.ts +16 -0
- package/src/lib/collection-groups.ts +5 -1
- package/src/lib/collection-paths.ts +3 -10
- package/src/lib/constants.ts +28 -3
- package/src/lib/copy-field.ts +36 -0
- package/src/lib/crypto.ts +40 -0
- package/src/lib/csp-builder.ts +2 -1
- package/src/lib/deferred.ts +79 -0
- package/src/lib/discover-ping.ts +141 -0
- package/src/lib/discover.ts +437 -0
- package/src/lib/env.ts +134 -0
- package/src/lib/errors.ts +33 -0
- package/src/lib/feed-policy.ts +236 -2
- package/src/lib/feed.ts +926 -73
- package/src/lib/filter-dimensions.ts +1349 -0
- package/src/lib/github-sync-site-config.ts +3 -0
- package/src/lib/hosted-signin.ts +1 -1
- package/src/lib/http-cache.ts +148 -0
- package/src/lib/hugo-markdown.ts +16 -0
- package/src/lib/icons.ts +9 -2
- package/src/lib/ids.ts +3 -0
- package/src/lib/jant-docs.ts +42 -0
- package/src/lib/lang-detect.ts +308 -0
- package/src/lib/media-helpers.ts +13 -1
- package/src/lib/navigation.ts +78 -11
- package/src/lib/pagination.ts +28 -24
- package/src/lib/per-language-surfaces.ts +67 -0
- package/src/lib/post-meta.ts +19 -0
- package/src/lib/render.tsx +55 -7
- package/src/lib/resolve-config.ts +53 -5
- package/src/lib/schemas.ts +201 -10
- package/src/lib/site-header-fragment.tsx +2 -1
- package/src/lib/site-unavailable-page.ts +47 -0
- package/src/lib/slug.ts +51 -7
- package/src/lib/slugify.ts +113 -0
- package/src/lib/startup-config.ts +4 -6
- package/src/lib/storage.ts +7 -0
- package/src/lib/summary.ts +50 -0
- package/src/lib/thread-fold.ts +161 -0
- package/src/lib/time.ts +32 -0
- package/src/lib/timeline.ts +42 -1
- package/src/lib/tiptap-render.ts +1 -2
- package/src/lib/upload.ts +16 -13
- package/src/lib/url.ts +139 -86
- package/src/lib/version.ts +10 -8
- package/src/lib/view-language.ts +436 -0
- package/src/lib/view.ts +51 -23
- package/src/lib/viewer-context.ts +62 -0
- package/src/middleware/__tests__/auth.test.ts +70 -2
- package/src/middleware/__tests__/cache-control.test.ts +71 -3
- package/src/middleware/__tests__/onboarding.test.ts +76 -5
- package/src/middleware/__tests__/secure-headers.test.ts +1 -1
- package/src/middleware/__tests__/session.test.ts +181 -33
- package/src/middleware/auth.ts +50 -31
- package/src/middleware/cache-control.ts +75 -10
- package/src/middleware/config.ts +6 -6
- package/src/middleware/error-handler.ts +12 -0
- package/src/middleware/onboarding.ts +32 -10
- package/src/middleware/secure-headers.ts +1 -1
- package/src/middleware/session.ts +96 -8
- package/src/node/__tests__/cli-migrate.test.ts +21 -19
- package/src/node/__tests__/cli-reset-password.test.ts +81 -8
- package/src/node/__tests__/cli-runtime-target.test.ts +48 -1
- package/src/node/__tests__/cli-site-selection.test.ts +128 -38
- package/src/node/__tests__/runtime.test.ts +37 -0
- package/src/node/index.ts +1 -0
- package/src/preset.css +76 -22
- package/src/routes/__tests__/feed-validator.test.ts +156 -0
- package/src/routes/api/__tests__/collections.test.ts +114 -0
- package/src/routes/api/__tests__/discover.test.ts +222 -0
- package/src/routes/api/__tests__/nav-items.test.ts +212 -2
- package/src/routes/api/__tests__/post-translations.test.ts +829 -0
- package/src/routes/api/__tests__/settings.test.ts +24 -3
- package/src/routes/api/__tests__/telegram.test.ts +6 -5
- package/src/routes/api/collections.ts +49 -6
- package/src/routes/api/custom-urls.ts +0 -1
- package/src/routes/api/discover.ts +95 -0
- package/src/routes/api/export.ts +3 -0
- package/src/routes/api/internal/__tests__/sites.test.ts +42 -2
- package/src/routes/api/nav-items.ts +55 -8
- package/src/routes/api/posts.ts +159 -0
- package/src/routes/api/public/__tests__/archive.test.ts +111 -0
- package/src/routes/api/public/archive.ts +77 -81
- package/src/routes/api/public/posts.ts +23 -6
- package/src/routes/api/smart-collections.ts +132 -0
- package/src/routes/api/telegram.ts +2 -40
- package/src/routes/api/uploads.ts +1 -2
- package/src/routes/auth/__tests__/setup-auth.test.ts +137 -0
- package/src/routes/auth/__tests__/setup-discover.test.ts +204 -0
- package/src/routes/auth/__tests__/setup-flow.test.ts +270 -0
- package/src/routes/auth/__tests__/setup-page.test.tsx +329 -0
- package/src/routes/auth/__tests__/setup-steps.test.ts +310 -0
- package/src/routes/auth/__tests__/setup.test.ts +278 -26
- package/src/routes/auth/__tests__/signin.test.ts +135 -0
- package/src/routes/auth/setup.tsx +934 -186
- package/src/routes/auth/signin.tsx +10 -8
- package/src/routes/compose.tsx +5 -0
- package/src/routes/dash/__tests__/custom-urls-archive.test.ts +44 -0
- package/src/routes/dash/__tests__/custom-urls-page.test.ts +49 -0
- package/src/routes/dash/__tests__/delete-account.test.ts +106 -0
- package/src/routes/dash/__tests__/settings-discover.test.ts +213 -0
- package/src/routes/dash/custom-urls.tsx +1 -37
- package/src/routes/dash/settings.tsx +502 -41
- package/src/routes/discover-announce.ts +96 -0
- package/src/routes/feed/__tests__/feed.test.ts +165 -2
- package/src/routes/feed/feed.ts +69 -40
- package/src/routes/feed/sitemap.ts +95 -11
- package/src/routes/pages/__tests__/archive-indexing.test.ts +200 -0
- package/src/routes/pages/__tests__/archive-params.test.ts +370 -6
- package/src/routes/pages/__tests__/collection-routing.test.ts +16 -0
- package/src/routes/pages/__tests__/collections-feed-links.test.ts +72 -0
- package/src/routes/pages/__tests__/collections.test.ts +9 -3
- package/src/routes/pages/__tests__/custom-archive-url.test.ts +119 -0
- package/src/routes/pages/__tests__/feed-autodiscovery.test.ts +148 -0
- package/src/routes/pages/__tests__/language-routing.test.ts +679 -0
- package/src/routes/pages/__tests__/post-preview-partial.test.ts +96 -0
- package/src/routes/pages/__tests__/smart-collection.test.ts +298 -0
- package/src/routes/pages/__tests__/subscribe.test.ts +248 -0
- package/src/routes/pages/archive.tsx +471 -369
- package/src/routes/pages/collection.tsx +94 -96
- package/src/routes/pages/collections.tsx +38 -50
- package/src/routes/pages/featured.tsx +32 -13
- package/src/routes/pages/home.tsx +25 -6
- package/src/routes/pages/language.tsx +113 -0
- package/src/routes/pages/latest.tsx +26 -16
- package/src/routes/pages/new.tsx +3 -0
- package/src/routes/pages/page.tsx +178 -17
- package/src/routes/pages/partials.tsx +41 -0
- package/src/routes/pages/search.tsx +18 -3
- package/src/routes/pages/smart-collection.tsx +340 -0
- package/src/routes/pages/subscribe.tsx +182 -0
- package/src/runtime/__tests__/index.test.ts +19 -0
- package/src/runtime/__tests__/node.test.ts +129 -4
- package/src/runtime/cloudflare.ts +21 -2
- package/src/runtime/index.ts +27 -1
- package/src/runtime/node.ts +18 -1
- package/src/runtime/site.ts +95 -24
- package/src/services/__tests__/auth.test.ts +116 -7
- package/src/services/__tests__/collection.test.ts +108 -4
- package/src/services/__tests__/language.test.ts +495 -0
- package/src/services/__tests__/navigation.test.ts +213 -10
- package/src/services/__tests__/path.test.ts +180 -0
- package/src/services/__tests__/post-language.test.ts +830 -0
- package/src/services/__tests__/post.test.ts +46 -9
- package/src/services/__tests__/settings.test.ts +228 -17
- package/src/services/__tests__/site-admin.test.ts +5 -5
- package/src/services/__tests__/smart-collection.test.ts +651 -0
- package/src/services/about-page.ts +22 -1
- package/src/services/auth.ts +51 -49
- package/src/services/bootstrap.ts +188 -38
- package/src/services/collection-directory-position.ts +76 -0
- package/src/services/collection.ts +331 -46
- package/src/services/custom-url.ts +27 -22
- package/src/services/export-theme/assets/client-site.css +1 -1
- package/src/services/export-theme/assets/client-site.js +123 -123
- package/src/services/export-theme/layouts/_default/rss.xml +77 -1
- package/src/services/export-theme/layouts/partials/feed-post-content.xml +105 -2
- package/src/services/export-theme/layouts/partials/media-gallery.html +35 -21
- package/src/services/export-theme/layouts/partials/pagination.html +14 -13
- package/src/services/export-theme/layouts/partials/post-card.html +8 -4
- package/src/services/export-theme/layouts/partials/reply.html +2 -1
- package/src/services/export-theme/layouts/partials/thread-preview.html +40 -30
- package/src/services/export-theme/styles/main.css +13 -1
- package/src/services/export.ts +278 -19
- package/src/services/index.ts +34 -3
- package/src/services/language.ts +412 -0
- package/src/services/mcp.ts +3 -1
- package/src/services/media.ts +14 -6
- package/src/services/navigation.ts +429 -67
- package/src/services/path.ts +221 -6
- package/src/services/post.ts +1021 -152
- package/src/services/search.ts +39 -2
- package/src/services/settings.ts +238 -43
- package/src/services/site-admin.ts +24 -7
- package/src/services/site.ts +77 -1
- package/src/services/smart-collection.ts +636 -0
- package/src/services/telegram.ts +20 -22
- package/src/services/upload-session.ts +1 -3
- package/src/style-author.css +27 -0
- package/src/styles/components-author.css +1622 -0
- package/src/styles/components.css +24 -1571
- package/src/styles/site-media.css +16 -2
- package/src/styles/tokens.css +82 -12
- package/src/styles/ui-author.css +7602 -0
- package/src/styles/ui.css +1087 -7219
- package/src/types/app-context.ts +7 -0
- package/src/types/bindings.ts +3 -0
- package/src/types/config.ts +94 -15
- package/src/types/constants.ts +188 -16
- package/src/types/entities.ts +87 -1
- package/src/types/operations.ts +64 -2
- package/src/types/props.ts +157 -31
- package/src/types/views.ts +55 -1
- package/src/ui/__tests__/font-themes.test.ts +52 -12
- package/src/ui/compose/ComposeDialog.tsx +201 -7
- package/src/ui/compose/ComposePrompt.tsx +1 -5
- package/src/ui/compose/__tests__/ComposeDialog.test.tsx +125 -0
- package/src/ui/dash/appearance/AdvancedContent.tsx +2 -2
- package/src/ui/dash/appearance/CodeInjectionContent.tsx +12 -16
- package/src/ui/dash/appearance/NavigationContent.tsx +117 -16
- package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +33 -11
- package/src/ui/dash/appearance/__tests__/system-nav-descriptions.test.ts +44 -0
- package/src/ui/dash/appearance/system-nav-descriptions.ts +75 -0
- package/src/ui/dash/settings/AccountMenuContent.tsx +2 -2
- package/src/ui/dash/settings/ApiTokensContent.tsx +2 -1
- package/src/ui/dash/settings/ConfigEditorContent.tsx +35 -16
- package/src/ui/dash/settings/GeneralContent.tsx +275 -113
- package/src/ui/dash/settings/LanguageContent.tsx +409 -0
- package/src/ui/dash/settings/SettingsRootContent.tsx +19 -1
- package/src/ui/dash/settings/__tests__/GeneralContent.test.tsx +324 -4
- package/src/ui/dash/settings/__tests__/LanguageContent.test.tsx +144 -0
- package/src/ui/dash/settings/__tests__/discover-copy.test.ts +66 -0
- package/src/ui/dash/settings/discover-copy.ts +116 -0
- package/src/ui/feed/LinkCard.tsx +55 -45
- package/src/ui/feed/NoteCard.tsx +9 -4
- package/src/ui/feed/PostStatusBadges.tsx +90 -43
- package/src/ui/feed/QuoteCard.tsx +55 -30
- package/src/ui/feed/ThreadPreview.tsx +6 -2
- package/src/ui/feed/__tests__/thread-preview.test.ts +13 -16
- package/src/ui/feed/__tests__/timeline-cards.test.ts +314 -49
- package/src/ui/feed/thread-preview-state.ts +20 -6
- package/src/ui/font-themes.ts +39 -29
- package/src/ui/layouts/BaseLayout.tsx +155 -10
- package/src/ui/layouts/SiteLayout.tsx +192 -34
- package/src/ui/layouts/__tests__/BaseLayout.test.tsx +196 -17
- package/src/ui/layouts/__tests__/SiteLayout.test.tsx +109 -85
- package/src/ui/pages/ArchivePage.tsx +282 -289
- package/src/ui/pages/CollectionPage.tsx +86 -76
- package/src/ui/pages/CollectionsPage.tsx +5 -0
- package/src/ui/pages/ComposePage.tsx +8 -0
- package/src/ui/pages/PostPage.tsx +47 -0
- package/src/ui/pages/SearchPage.tsx +3 -3
- package/src/ui/pages/SmartCollectionPage.tsx +434 -0
- package/src/ui/pages/SubscribePage.tsx +236 -0
- package/src/ui/pages/__tests__/ArchivePage.test.tsx +65 -9
- package/src/ui/pages/__tests__/collections-directory-rows.test.tsx +92 -0
- package/src/ui/pages/__tests__/collections-view-links.test.tsx +81 -0
- package/src/ui/shared/CollectionDirectory.tsx +178 -3
- package/src/ui/shared/CollectionsManager.tsx +70 -17
- package/src/ui/shared/CopyField.tsx +127 -0
- package/src/ui/shared/EmptyPostContent.tsx +23 -0
- package/src/ui/shared/MediaGallery.tsx +125 -46
- package/src/ui/shared/Pagination.tsx +20 -5
- package/src/ui/shared/PostFooter.tsx +20 -11
- package/src/ui/shared/__tests__/archive-labels.test.ts +100 -0
- package/src/ui/shared/__tests__/media-gallery.test.ts +210 -2
- package/src/ui/shared/__tests__/navigation-labels.test.ts +16 -22
- package/src/ui/shared/__tests__/pagination.test.ts +38 -24
- package/src/ui/shared/__tests__/post-footer.test.ts +32 -0
- package/src/ui/shared/archive-labels.ts +98 -0
- package/src/ui/shared/collection-management-labels.ts +94 -16
- package/src/ui/shared/navigation-labels.ts +45 -71
- package/src/ui/shared/post-article-attributes.ts +2 -0
- package/src/ui/shared/smart-collection-labels.ts +343 -0
- package/dist/app-C75Zc8dD.js +0 -6
- package/dist/client/_assets/chunks/heic-to-DUUaO23q.js +0 -1
- package/dist/client/_assets/chunks/module-DcsAZQZ_.js +0 -716
- package/dist/client/_assets/chunks/url-CU9pEbk5.js +0 -1
- package/dist/client/_assets/client-Bq0vre8Y.js +0 -296
- package/dist/client/_assets/client-D3VZyG4L.css +0 -2
- package/dist/client/_assets/client-auth-F2rb9WfF.js +0 -5571
- package/dist/env-BPYViDJJ.js +0 -281
- package/dist/github-api-BNF35Lkx.js +0 -176
- package/dist/github-app-DdiD-bC4.js +0 -275
- package/dist/github-sync-Bqg62IvV.js +0 -4
- package/dist/github-sync-HdK2EgvJ.js +0 -431
- package/dist/url-CbLAtlZe.js +0 -900
- package/src/client/__tests__/collection-created-notice.test.ts +0 -36
- package/src/client/__tests__/collection-form-bridge.test.ts +0 -216
- package/src/client/__tests__/post-form-bridge.test.ts +0 -82
- package/src/client/collection-created-notice.ts +0 -66
- package/src/client/collection-form-bridge.ts +0 -191
- package/src/client/components/__tests__/jant-post-form.test.ts +0 -193
- package/src/client/components/jant-post-form.ts +0 -422
- package/src/client/components/post-form-template.ts +0 -352
- package/src/client/components/post-form-types.ts +0 -96
- package/src/client/media-metadata.ts +0 -247
- package/src/client/post-form-bridge.ts +0 -193
- package/src/ui/pages/CollectionEditorPage.tsx +0 -175
|
@@ -52,6 +52,117 @@ describe("Public Archive API Routes", () => {
|
|
|
52
52
|
expect(hidden.visibility).toBe("latest_hidden");
|
|
53
53
|
});
|
|
54
54
|
|
|
55
|
+
it("answers the visibility question it was asked", async () => {
|
|
56
|
+
const { app, services } = createTestApp({ authenticated: false });
|
|
57
|
+
app.route("/api/public/archive", publicArchiveApiRoutes);
|
|
58
|
+
|
|
59
|
+
const plain = await services.posts.create({
|
|
60
|
+
format: "note",
|
|
61
|
+
title: "Plain root",
|
|
62
|
+
bodyMarkdown: "plain",
|
|
63
|
+
});
|
|
64
|
+
const hidden = await services.posts.create({
|
|
65
|
+
format: "note",
|
|
66
|
+
title: "Hidden root",
|
|
67
|
+
bodyMarkdown: "hidden",
|
|
68
|
+
visibility: "latest_hidden",
|
|
69
|
+
});
|
|
70
|
+
const featured = await services.posts.create({
|
|
71
|
+
format: "note",
|
|
72
|
+
title: "Featured root",
|
|
73
|
+
bodyMarkdown: "featured",
|
|
74
|
+
featured: true,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const hiddenOnly = await app.request(
|
|
78
|
+
"/api/public/archive?visibility=hidden",
|
|
79
|
+
);
|
|
80
|
+
expect(hiddenOnly.status).toBe(200);
|
|
81
|
+
const hiddenBody = await hiddenOnly.json();
|
|
82
|
+
expect(hiddenBody.posts.map((p: { id: string }) => p.id)).toEqual([
|
|
83
|
+
hidden.id,
|
|
84
|
+
]);
|
|
85
|
+
|
|
86
|
+
const featuredOnly = await app.request(
|
|
87
|
+
"/api/public/archive?visibility=featured",
|
|
88
|
+
);
|
|
89
|
+
const featuredBody = await featuredOnly.json();
|
|
90
|
+
expect(featuredBody.posts.map((p: { id: string }) => p.id)).toEqual([
|
|
91
|
+
featured.id,
|
|
92
|
+
]);
|
|
93
|
+
|
|
94
|
+
const publicOnly = await app.request(
|
|
95
|
+
"/api/public/archive?visibility=public",
|
|
96
|
+
);
|
|
97
|
+
const publicBody = await publicOnly.json();
|
|
98
|
+
const publicIds = publicBody.posts.map((p: { id: string }) => p.id);
|
|
99
|
+
expect(publicIds).toContain(plain.id);
|
|
100
|
+
expect(publicIds).not.toContain(hidden.id);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// The failure this guards: an unknown key is stripped by the schema, so a
|
|
104
|
+
// caller asking for something this endpoint cannot serve used to get the
|
|
105
|
+
// whole archive back and no way to tell.
|
|
106
|
+
it("rejects a visibility it cannot serve instead of widening", async () => {
|
|
107
|
+
const { app, services } = createTestApp({ authenticated: false });
|
|
108
|
+
app.route("/api/public/archive", publicArchiveApiRoutes);
|
|
109
|
+
|
|
110
|
+
await services.posts.create({
|
|
111
|
+
format: "note",
|
|
112
|
+
title: "Plain root",
|
|
113
|
+
bodyMarkdown: "plain",
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
for (const value of ["private", "nonsense"]) {
|
|
117
|
+
const res = await app.request(
|
|
118
|
+
`/api/public/archive?visibility=${value}`,
|
|
119
|
+
);
|
|
120
|
+
expect(res.status).toBe(400);
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// `latest_hidden` is the stored spelling of `hidden`, which the page and
|
|
125
|
+
// the feed both read. This endpoint used to be the one surface that called
|
|
126
|
+
// it nonsense — a value it can serve perfectly, rejected only because it
|
|
127
|
+
// kept a private copy of the vocabulary.
|
|
128
|
+
it("reads the stored spelling of hidden, like every other surface", async () => {
|
|
129
|
+
const { app, services } = createTestApp({ authenticated: false });
|
|
130
|
+
app.route("/api/public/archive", publicArchiveApiRoutes);
|
|
131
|
+
|
|
132
|
+
await services.posts.create({
|
|
133
|
+
format: "note",
|
|
134
|
+
title: "Plain root",
|
|
135
|
+
bodyMarkdown: "plain",
|
|
136
|
+
});
|
|
137
|
+
const hidden = await services.posts.create({
|
|
138
|
+
format: "note",
|
|
139
|
+
title: "Hidden root",
|
|
140
|
+
bodyMarkdown: "hidden",
|
|
141
|
+
visibility: "latest_hidden",
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const res = await app.request(
|
|
145
|
+
"/api/public/archive?visibility=latest_hidden",
|
|
146
|
+
);
|
|
147
|
+
expect(res.status).toBe(200);
|
|
148
|
+
const body = await res.json();
|
|
149
|
+
expect(body.posts.map((p: { id: string }) => p.id)).toEqual([hidden.id]);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("rejects a parameter it does not know", async () => {
|
|
153
|
+
const { app, services } = createTestApp({ authenticated: false });
|
|
154
|
+
app.route("/api/public/archive", publicArchiveApiRoutes);
|
|
155
|
+
|
|
156
|
+
await services.posts.create({
|
|
157
|
+
format: "note",
|
|
158
|
+
title: "Plain root",
|
|
159
|
+
bodyMarkdown: "plain",
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
const res = await app.request("/api/public/archive?formta=note");
|
|
163
|
+
expect(res.status).toBe(400);
|
|
164
|
+
});
|
|
165
|
+
|
|
55
166
|
it("supports format and limit filters with cursor", async () => {
|
|
56
167
|
const { app, services } = createTestApp({ authenticated: false });
|
|
57
168
|
app.route("/api/public/archive", publicArchiveApiRoutes);
|
|
@@ -2,8 +2,17 @@ import { Hono } from "hono";
|
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import type { Bindings } from "../../../types.js";
|
|
4
4
|
import type { AppVariables } from "../../../types/app-context.js";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
5
|
+
import { PUBLIC_ARCHIVE_VISIBILITIES } from "../../../types.js";
|
|
6
|
+
import { ContentLanguageSchema, parseValidated } from "../../../lib/schemas.js";
|
|
7
|
+
import type { DimensionContext } from "../../../lib/filter-dimensions.js";
|
|
8
|
+
import {
|
|
9
|
+
buildCollectionVocabulary,
|
|
10
|
+
EMPTY_COLLECTION_VOCABULARY,
|
|
11
|
+
parsePostFilterSelectionStrict,
|
|
12
|
+
readCollectionSlugs,
|
|
13
|
+
toPostFilters,
|
|
14
|
+
} from "../../../lib/filter-dimensions.js";
|
|
15
|
+
import { ValidationError } from "../../../lib/errors.js";
|
|
7
16
|
import { toPublicPost } from "./posts.js";
|
|
8
17
|
import { requirePublicApiEnabled } from "../../../middleware/public-content-access.js";
|
|
9
18
|
|
|
@@ -13,104 +22,91 @@ export const publicArchiveApiRoutes = new Hono<Env>();
|
|
|
13
22
|
|
|
14
23
|
publicArchiveApiRoutes.use("*", requirePublicApiEnabled());
|
|
15
24
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
.enum(["any", "none"])
|
|
24
|
-
.transform((value) => value === "any");
|
|
25
|
-
|
|
25
|
+
/**
|
|
26
|
+
* The endpoint's own parameters — everything that is not a filter dimension.
|
|
27
|
+
*
|
|
28
|
+
* The dimensions themselves are read by the shared registry, so this endpoint
|
|
29
|
+
* cannot drift from the page it mirrors. They are listed here only so the strict
|
|
30
|
+
* parser can tell them apart from a parameter nobody understands.
|
|
31
|
+
*/
|
|
26
32
|
const ListPublicArchiveQuerySchema = z.object({
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
year: z.coerce.number().int().min(1971).optional(),
|
|
30
|
-
// Kinds list (image,video,...) or presence words: any = posts with any
|
|
31
|
-
// attachment, none = posts without attachments.
|
|
32
|
-
media: z
|
|
33
|
-
.string()
|
|
34
|
-
.optional()
|
|
35
|
-
.transform((value, ctx) => {
|
|
36
|
-
if (!value) return undefined;
|
|
37
|
-
if (value === "any" || value === "none") return value;
|
|
38
|
-
const parts = value
|
|
39
|
-
.split(",")
|
|
40
|
-
.map((part) => part.trim())
|
|
41
|
-
.filter((part) => part.length > 0);
|
|
42
|
-
if (parts.length === 0) return undefined;
|
|
43
|
-
const result = z.array(MediaKindSchema).safeParse(parts);
|
|
44
|
-
if (!result.success) {
|
|
45
|
-
ctx.addIssue({
|
|
46
|
-
code: z.ZodIssueCode.custom,
|
|
47
|
-
message: INVALID_MEDIA_MESSAGE,
|
|
48
|
-
});
|
|
49
|
-
return z.NEVER;
|
|
50
|
-
}
|
|
51
|
-
return result.data;
|
|
52
|
-
}),
|
|
53
|
-
title: PresenceSchema.optional(),
|
|
54
|
-
replies: PresenceSchema.optional(),
|
|
55
|
-
// Deprecated: use media=any|none and title=any|none instead.
|
|
56
|
-
hasMedia: BoolFlagSchema.optional(),
|
|
57
|
-
hasTitle: BoolFlagSchema.optional(),
|
|
33
|
+
/** Restrict to one content language. See `/api/public/posts`. */
|
|
34
|
+
lang: ContentLanguageSchema.optional(),
|
|
58
35
|
cursor: z.string().optional(),
|
|
59
36
|
limit: z.coerce.number().int().min(1).max(100).optional().default(20),
|
|
60
37
|
content: z.enum(["markdown"]).optional(),
|
|
61
38
|
});
|
|
62
39
|
|
|
40
|
+
const ENDPOINT_PARAMS = ["lang", "cursor", "limit", "content"] as const;
|
|
41
|
+
|
|
42
|
+
const INVALID_VISIBILITY_MESSAGE =
|
|
43
|
+
"Invalid visibility value. Allowed: " +
|
|
44
|
+
PUBLIC_ARCHIVE_VISIBILITIES.map((value) =>
|
|
45
|
+
value === "latest_hidden" ? "hidden" : value,
|
|
46
|
+
).join(", ");
|
|
47
|
+
|
|
63
48
|
publicArchiveApiRoutes.get("/", async (c) => {
|
|
64
|
-
const {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
media,
|
|
69
|
-
title,
|
|
70
|
-
replies,
|
|
71
|
-
hasMedia,
|
|
72
|
-
hasTitle,
|
|
73
|
-
cursor,
|
|
74
|
-
limit,
|
|
75
|
-
content,
|
|
76
|
-
} = parseValidated(ListPublicArchiveQuerySchema, c.req.query());
|
|
49
|
+
const { lang, cursor, limit, content } = parseValidated(
|
|
50
|
+
ListPublicArchiveQuerySchema,
|
|
51
|
+
c.req.query(),
|
|
52
|
+
);
|
|
77
53
|
|
|
78
|
-
const
|
|
79
|
-
const mediaPresence =
|
|
80
|
-
media === "any" ? true : media === "none" ? false : undefined;
|
|
54
|
+
const read = (key: string): string | undefined => c.req.query(key);
|
|
81
55
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
56
|
+
// Resolving a collection slug costs a round trip, so it is paid for only when
|
|
57
|
+
// the caller named one.
|
|
58
|
+
const slugs = readCollectionSlugs(read);
|
|
59
|
+
const ctx: DimensionContext = {
|
|
60
|
+
collections:
|
|
61
|
+
slugs.length > 0
|
|
62
|
+
? buildCollectionVocabulary(await c.var.services.collections.list())
|
|
63
|
+
: EMPTY_COLLECTION_VOCABULARY,
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const parsed = parsePostFilterSelectionStrict(
|
|
67
|
+
read,
|
|
68
|
+
Object.keys(c.req.query()),
|
|
69
|
+
ctx,
|
|
70
|
+
{ allow: ENDPOINT_PARAMS },
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
// A collection slug that names nothing is answered as an empty result rather
|
|
74
|
+
// than a rejection: the caller's words were understood, and this site simply
|
|
75
|
+
// holds no such collection.
|
|
76
|
+
if (
|
|
77
|
+
!parsed.ok &&
|
|
78
|
+
parsed.issues.every((issue) => issue.param === "collection")
|
|
79
|
+
) {
|
|
80
|
+
return c.json({ posts: [], nextCursor: null });
|
|
81
|
+
}
|
|
82
|
+
if (!parsed.ok) {
|
|
83
|
+
const first = parsed.issues[0];
|
|
84
|
+
throw new ValidationError(first?.message ?? "Validation failed");
|
|
92
85
|
}
|
|
93
86
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
87
|
+
// `private` names a set no anonymous caller may see. Rejected rather than
|
|
88
|
+
// dropped: silently widening the result to the whole archive would answer a
|
|
89
|
+
// different question under the caller's own words.
|
|
90
|
+
const visibility = parsed.selection.visibility;
|
|
91
|
+
if (
|
|
92
|
+
visibility !== undefined &&
|
|
93
|
+
!(PUBLIC_ARCHIVE_VISIBILITIES as readonly string[]).includes(visibility)
|
|
94
|
+
) {
|
|
95
|
+
throw new ValidationError(INVALID_VISIBILITY_MESSAGE);
|
|
96
|
+
}
|
|
98
97
|
|
|
99
98
|
const posts = await c.var.services.posts.list({
|
|
100
|
-
|
|
101
|
-
collectionIds,
|
|
99
|
+
lang,
|
|
102
100
|
status: "published",
|
|
103
101
|
cursor: cursor ?? undefined,
|
|
104
102
|
limit,
|
|
105
103
|
excludePrivate: true,
|
|
106
104
|
excludeLatestHidden: false,
|
|
107
105
|
excludeReplies: true,
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
hasTitle: title ?? hasTitle,
|
|
113
|
-
hasReplies: replies,
|
|
106
|
+
// Ordering is fixed at newest-published-first here, so the year filter
|
|
107
|
+
// stays on the publication axis — the page's `?sort=` switch has no
|
|
108
|
+
// counterpart on a cursor keyed off the post id.
|
|
109
|
+
...toPostFilters(parsed.selection, { yearAxis: "published" }),
|
|
114
110
|
ignorePinnedSort: true,
|
|
115
111
|
});
|
|
116
112
|
|
|
@@ -5,6 +5,7 @@ import type { AppVariables } from "../../../types/app-context.js";
|
|
|
5
5
|
import { getCollectionPagePath } from "../../../lib/collection-paths.js";
|
|
6
6
|
import {
|
|
7
7
|
CollectionSortOrderSchema,
|
|
8
|
+
ContentLanguageSchema,
|
|
8
9
|
FormatSchema,
|
|
9
10
|
parseValidated,
|
|
10
11
|
} from "../../../lib/schemas.js";
|
|
@@ -31,6 +32,11 @@ publicPostsApiRoutes.use("*", requirePublicApiEnabled());
|
|
|
31
32
|
|
|
32
33
|
const ListPublicPostsQuerySchema = z.object({
|
|
33
34
|
format: FormatSchema.optional(),
|
|
35
|
+
/**
|
|
36
|
+
* Restrict to one content language. Machine surfaces name the language
|
|
37
|
+
* explicitly; browsing surfaces express it as a URL prefix instead.
|
|
38
|
+
*/
|
|
39
|
+
lang: ContentLanguageSchema.optional(),
|
|
34
40
|
collection: z.string().optional(),
|
|
35
41
|
sort: CollectionSortOrderSchema.optional(),
|
|
36
42
|
cursor: z.string().optional(),
|
|
@@ -61,6 +67,16 @@ export type PublicPostBaseResponse = {
|
|
|
61
67
|
previewImageUrl: string | null;
|
|
62
68
|
replyToId: string | null;
|
|
63
69
|
threadId: string;
|
|
70
|
+
/**
|
|
71
|
+
* BCP 47 content language, or null on a post written before the site
|
|
72
|
+
* enabled multilingual content.
|
|
73
|
+
*/
|
|
74
|
+
language: string | null;
|
|
75
|
+
/**
|
|
76
|
+
* Shared by every translation of this Thread. Null when the Thread has no
|
|
77
|
+
* translations.
|
|
78
|
+
*/
|
|
79
|
+
translationGroupId: string | null;
|
|
64
80
|
/** Reply published without announcing its Thread. Always false on roots. */
|
|
65
81
|
quietReply: boolean;
|
|
66
82
|
pinnedAt: number | null;
|
|
@@ -91,8 +107,7 @@ export type PublicPostMarkdownResponse = PublicPostBaseResponse & {
|
|
|
91
107
|
};
|
|
92
108
|
|
|
93
109
|
export type PublicPostResponse =
|
|
94
|
-
|
|
|
95
|
-
| PublicPostMarkdownResponse;
|
|
110
|
+
PublicPostRenderedResponse | PublicPostMarkdownResponse;
|
|
96
111
|
|
|
97
112
|
function isPublicDetailVisible(post: Post | null): post is Post {
|
|
98
113
|
return (
|
|
@@ -151,6 +166,8 @@ export function toPublicPost(
|
|
|
151
166
|
previewImageUrl,
|
|
152
167
|
replyToId: post.replyToId,
|
|
153
168
|
threadId: post.threadId,
|
|
169
|
+
language: post.language,
|
|
170
|
+
translationGroupId: post.translationGroupId,
|
|
154
171
|
quietReply: post.quietReply,
|
|
155
172
|
pinnedAt: post.pinnedAt,
|
|
156
173
|
featuredAt: post.featuredAt,
|
|
@@ -204,10 +221,8 @@ export function toPublicPost(
|
|
|
204
221
|
}
|
|
205
222
|
|
|
206
223
|
publicPostsApiRoutes.get("/", async (c) => {
|
|
207
|
-
const { format, collection, sort, cursor, limit, content } =
|
|
208
|
-
ListPublicPostsQuerySchema,
|
|
209
|
-
c.req.query(),
|
|
210
|
-
);
|
|
224
|
+
const { format, lang, collection, sort, cursor, limit, content } =
|
|
225
|
+
parseValidated(ListPublicPostsQuerySchema, c.req.query());
|
|
211
226
|
|
|
212
227
|
// Resolve collection slug(s) — accepts comma-separated (e.g. "tech,art")
|
|
213
228
|
// or "+" separated (e.g. "tech+art"), matching the page URL convention.
|
|
@@ -265,6 +280,7 @@ publicPostsApiRoutes.get("/", async (c) => {
|
|
|
265
280
|
excludePrivate: true,
|
|
266
281
|
excludeLatestHidden: true,
|
|
267
282
|
rootFormat: format,
|
|
283
|
+
lang,
|
|
268
284
|
sortOrder,
|
|
269
285
|
cursor: cursor ?? undefined,
|
|
270
286
|
limit,
|
|
@@ -272,6 +288,7 @@ publicPostsApiRoutes.get("/", async (c) => {
|
|
|
272
288
|
)
|
|
273
289
|
: await c.var.services.posts.list({
|
|
274
290
|
format,
|
|
291
|
+
lang,
|
|
275
292
|
status: "published",
|
|
276
293
|
cursor: cursor ?? undefined,
|
|
277
294
|
limit,
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Smart Collections API Routes
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Hono } from "hono";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import type { Bindings } from "../../types.js";
|
|
8
|
+
import type { AppVariables } from "../../types/app-context.js";
|
|
9
|
+
import { requireAuthApi } from "../../middleware/auth.js";
|
|
10
|
+
import {
|
|
11
|
+
ContentLanguageSchema,
|
|
12
|
+
CreateSmartCollectionSchema,
|
|
13
|
+
SmartCollectionIdSchema,
|
|
14
|
+
SmartCollectionPreviewSchema,
|
|
15
|
+
UpdateSmartCollectionSchema,
|
|
16
|
+
parseValidated,
|
|
17
|
+
} from "../../lib/schemas.js";
|
|
18
|
+
import { assertFound, parseIdParam } from "../../lib/errors.js";
|
|
19
|
+
import { ID_PREFIX } from "../../lib/ids.js";
|
|
20
|
+
|
|
21
|
+
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
22
|
+
|
|
23
|
+
export const smartCollectionsApiRoutes = new Hono<Env>();
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Address availability, in the shape posts already use.
|
|
27
|
+
*
|
|
28
|
+
* `suggest` derives one from a title; `check` answers whether a typed one is
|
|
29
|
+
* free. Collections answer the same question at `/api/collections/slug`.
|
|
30
|
+
*/
|
|
31
|
+
const SlugQuerySchema = z.discriminatedUnion("mode", [
|
|
32
|
+
z.object({
|
|
33
|
+
mode: z.literal("suggest"),
|
|
34
|
+
title: z.string().trim().max(300).optional(),
|
|
35
|
+
smartCollectionId: SmartCollectionIdSchema.optional(),
|
|
36
|
+
}),
|
|
37
|
+
z.object({
|
|
38
|
+
mode: z.literal("check"),
|
|
39
|
+
slug: z.string().trim().toLowerCase().min(1).max(200),
|
|
40
|
+
smartCollectionId: SmartCollectionIdSchema.optional(),
|
|
41
|
+
}),
|
|
42
|
+
]);
|
|
43
|
+
|
|
44
|
+
const PreviewQuerySchema = z.object({
|
|
45
|
+
lang: ContentLanguageSchema.optional(),
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// --- Address ---------------------------------------------------------------
|
|
49
|
+
// Declared before `/:id` so the literal path wins.
|
|
50
|
+
|
|
51
|
+
smartCollectionsApiRoutes.get("/slug", requireAuthApi(), async (c) => {
|
|
52
|
+
const query = parseValidated(SlugQuerySchema, c.req.query());
|
|
53
|
+
|
|
54
|
+
if (query.mode === "suggest") {
|
|
55
|
+
const slug = await c.var.services.smartCollections.suggestSlug({
|
|
56
|
+
title: query.title,
|
|
57
|
+
excludeId: query.smartCollectionId,
|
|
58
|
+
});
|
|
59
|
+
return c.json({ slug });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const available = await c.var.services.smartCollections.checkSlugAvailability(
|
|
63
|
+
query.slug,
|
|
64
|
+
query.smartCollectionId,
|
|
65
|
+
);
|
|
66
|
+
return c.json({ slug: query.slug, available });
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// --- Preview ---------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* How many threads a set of conditions would gather, against the site total.
|
|
73
|
+
*
|
|
74
|
+
* POST rather than GET: the conditions are a typed body in the same shape the
|
|
75
|
+
* create endpoint validates, and putting them in a URL would mean inventing a
|
|
76
|
+
* second spelling for them.
|
|
77
|
+
*/
|
|
78
|
+
smartCollectionsApiRoutes.post("/preview", requireAuthApi(), async (c) => {
|
|
79
|
+
const body = parseValidated(SmartCollectionPreviewSchema, await c.req.json());
|
|
80
|
+
const query = parseValidated(PreviewQuerySchema, c.req.query());
|
|
81
|
+
|
|
82
|
+
const { count, baseline } = await c.var.services.smartCollections.preview(
|
|
83
|
+
body.selection ?? {},
|
|
84
|
+
{ isAuthenticated: c.var.isAuthenticated, lang: query.lang },
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
c.header("Cache-Control", "no-store");
|
|
88
|
+
return c.json({ count, baseline });
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// --- CRUD ------------------------------------------------------------------
|
|
92
|
+
|
|
93
|
+
smartCollectionsApiRoutes.get("/", requireAuthApi(), async (c) => {
|
|
94
|
+
const query = parseValidated(PreviewQuerySchema, c.req.query());
|
|
95
|
+
const smartCollections =
|
|
96
|
+
await c.var.services.smartCollections.listDirectoryEntries({
|
|
97
|
+
isAuthenticated: c.var.isAuthenticated,
|
|
98
|
+
lang: query.lang,
|
|
99
|
+
});
|
|
100
|
+
return c.json({ smartCollections });
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
smartCollectionsApiRoutes.post("/", requireAuthApi(), async (c) => {
|
|
104
|
+
const body = parseValidated(CreateSmartCollectionSchema, await c.req.json());
|
|
105
|
+
const smartCollection = await c.var.services.smartCollections.create(body);
|
|
106
|
+
return c.json({ smartCollection }, 201);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
smartCollectionsApiRoutes.get("/:id", requireAuthApi(), async (c) => {
|
|
110
|
+
const id = parseIdParam(c.req.param("id"), ID_PREFIX.smartCollection);
|
|
111
|
+
const smartCollection = await c.var.services.smartCollections.getById(id);
|
|
112
|
+
assertFound(smartCollection, "Smart collection");
|
|
113
|
+
return c.json({ smartCollection });
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
smartCollectionsApiRoutes.put("/:id", requireAuthApi(), async (c) => {
|
|
117
|
+
const id = parseIdParam(c.req.param("id"), ID_PREFIX.smartCollection);
|
|
118
|
+
const body = parseValidated(UpdateSmartCollectionSchema, await c.req.json());
|
|
119
|
+
const smartCollection = await c.var.services.smartCollections.update(
|
|
120
|
+
id,
|
|
121
|
+
body,
|
|
122
|
+
);
|
|
123
|
+
assertFound(smartCollection, "Smart collection");
|
|
124
|
+
return c.json({ smartCollection });
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
smartCollectionsApiRoutes.delete("/:id", requireAuthApi(), async (c) => {
|
|
128
|
+
const id = parseIdParam(c.req.param("id"), ID_PREFIX.smartCollection);
|
|
129
|
+
const deleted = await c.var.services.smartCollections.delete(id);
|
|
130
|
+
assertFound(deleted || null, "Smart collection");
|
|
131
|
+
return c.json({ success: true });
|
|
132
|
+
});
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
getTelegramWebhookSecret,
|
|
26
26
|
} from "../../lib/env.js";
|
|
27
27
|
import { timingSafeEqualText } from "../../lib/crypto.js";
|
|
28
|
+
import { runDeferred } from "../../lib/deferred.js";
|
|
28
29
|
import {
|
|
29
30
|
answerCallbackQuery,
|
|
30
31
|
buildDeepLink,
|
|
@@ -316,7 +317,7 @@ async function processUpdate(
|
|
|
316
317
|
posterFileId: media.posterFileId ?? null,
|
|
317
318
|
});
|
|
318
319
|
|
|
319
|
-
runDeferred(c, async () => {
|
|
320
|
+
runDeferred(c, "Telegram", async () => {
|
|
320
321
|
await sleep(ALBUM_BUFFER_DELAY_MS);
|
|
321
322
|
const claimed = await telegram.claimAlbumGroup(botId, mediaGroupId);
|
|
322
323
|
if (claimed.length === 0) return;
|
|
@@ -371,45 +372,6 @@ function sleep(ms: number): Promise<void> {
|
|
|
371
372
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
372
373
|
}
|
|
373
374
|
|
|
374
|
-
/**
|
|
375
|
-
* Schedules background work that must outlive the current HTTP response so
|
|
376
|
-
* Telegram can deliver the next webhook in this chat without waiting.
|
|
377
|
-
*
|
|
378
|
-
* On Cloudflare Workers `c.executionCtx.waitUntil` keeps the worker alive
|
|
379
|
-
* until the promise settles. In Node / tests there's no such API, so we just
|
|
380
|
-
* let the promise float — Node's event loop keeps running it. Either way the
|
|
381
|
-
* promise has its own try/catch so unhandled rejections never bubble up.
|
|
382
|
-
*
|
|
383
|
-
* Background work also tracks the pending-promises array we register on the
|
|
384
|
-
* env binding for tests so a vitest can `await` them after advancing timers.
|
|
385
|
-
*/
|
|
386
|
-
function runDeferred(
|
|
387
|
-
c: {
|
|
388
|
-
env: Bindings;
|
|
389
|
-
executionCtx?: { waitUntil: (promise: Promise<unknown>) => void };
|
|
390
|
-
},
|
|
391
|
-
work: () => Promise<void>,
|
|
392
|
-
): void {
|
|
393
|
-
const promise = work().catch((err) => {
|
|
394
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
395
|
-
// eslint-disable-next-line no-console -- Background failures must be visible.
|
|
396
|
-
console.error(`[Jant] Telegram background error: ${message}`);
|
|
397
|
-
});
|
|
398
|
-
try {
|
|
399
|
-
c.executionCtx?.waitUntil(promise);
|
|
400
|
-
} catch {
|
|
401
|
-
// executionCtx not available (e.g. Node, tests) — promise still runs.
|
|
402
|
-
}
|
|
403
|
-
const pending = (
|
|
404
|
-
c.env as Bindings & {
|
|
405
|
-
__telegramPending?: Promise<unknown>[];
|
|
406
|
-
}
|
|
407
|
-
).__telegramPending;
|
|
408
|
-
if (Array.isArray(pending)) {
|
|
409
|
-
pending.push(promise);
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
|
|
413
375
|
/**
|
|
414
376
|
* Convert a message's caption + caption_entities into markdown, returning
|
|
415
377
|
* `null` when there's nothing to record. Centralized so single-media and album
|
|
@@ -116,8 +116,7 @@ function scheduleExpiredUploadCleanup(
|
|
|
116
116
|
.catch(() => {});
|
|
117
117
|
|
|
118
118
|
let executionCtx:
|
|
119
|
-
|
|
120
|
-
| undefined;
|
|
119
|
+
{ waitUntil: (promise: Promise<unknown>) => void } | undefined;
|
|
121
120
|
try {
|
|
122
121
|
executionCtx = c.executionCtx;
|
|
123
122
|
} catch {
|