@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
package/src/services/search.ts
CHANGED
|
@@ -27,6 +27,13 @@ export interface SearchOptions {
|
|
|
27
27
|
status?: Status[];
|
|
28
28
|
/** Filter by format */
|
|
29
29
|
format?: Format;
|
|
30
|
+
/**
|
|
31
|
+
* Restrict to one BCP 47 content language (canonical form, matched exactly).
|
|
32
|
+
*
|
|
33
|
+
* The index itself is language-agnostic — one shared index serves every
|
|
34
|
+
* language — so this is a plain residual predicate on the matched rows.
|
|
35
|
+
*/
|
|
36
|
+
lang?: string;
|
|
30
37
|
}
|
|
31
38
|
|
|
32
39
|
export interface SearchService {
|
|
@@ -54,6 +61,8 @@ interface RawSearchRow {
|
|
|
54
61
|
rating: number | null;
|
|
55
62
|
reply_to_id: string | null;
|
|
56
63
|
thread_id: string;
|
|
64
|
+
language: string | null;
|
|
65
|
+
translation_group_id: string | null;
|
|
57
66
|
/** SQLite returns 0/1, Postgres a boolean. */
|
|
58
67
|
quiet_reply: number | boolean | null;
|
|
59
68
|
published_at: number | null;
|
|
@@ -95,6 +104,8 @@ function mapRow(row: RawSearchRow): SearchResult {
|
|
|
95
104
|
previewProvider: null,
|
|
96
105
|
replyToId: row.reply_to_id,
|
|
97
106
|
threadId: row.thread_id,
|
|
107
|
+
language: row.language,
|
|
108
|
+
translationGroupId: row.translation_group_id,
|
|
98
109
|
quietReply: Boolean(row.quiet_reply),
|
|
99
110
|
publishedAt: row.published_at,
|
|
100
111
|
lastActivityAt:
|
|
@@ -181,6 +192,8 @@ export function createSearchService(
|
|
|
181
192
|
const statusPlaceholders = status.map(() => "?").join(", ");
|
|
182
193
|
const formatFilter = options.format ? "AND post.format = ?" : "";
|
|
183
194
|
const formatParams = options.format ? [options.format] : [];
|
|
195
|
+
const langFilter = options.lang ? "AND post.language = ?" : "";
|
|
196
|
+
const langParams = options.lang ? [options.lang] : [];
|
|
184
197
|
|
|
185
198
|
if (databaseDialect === "sqlite") {
|
|
186
199
|
const ftsQuery = buildSqliteFtsQuery(query);
|
|
@@ -204,12 +217,21 @@ export function createSearchService(
|
|
|
204
217
|
AND post.site_id = ?
|
|
205
218
|
AND post.status IN (${statusPlaceholders})
|
|
206
219
|
${formatFilter}
|
|
220
|
+
${langFilter}
|
|
207
221
|
ORDER BY post_fts.rank
|
|
208
222
|
LIMIT ? OFFSET ?
|
|
209
223
|
`);
|
|
210
224
|
|
|
211
225
|
const { results } = await stmt
|
|
212
|
-
.bind(
|
|
226
|
+
.bind(
|
|
227
|
+
ftsQuery,
|
|
228
|
+
siteId,
|
|
229
|
+
...status,
|
|
230
|
+
...formatParams,
|
|
231
|
+
...langParams,
|
|
232
|
+
limit,
|
|
233
|
+
offset,
|
|
234
|
+
)
|
|
213
235
|
.all<RawSearchRow>();
|
|
214
236
|
|
|
215
237
|
return withSnippetFallback((results || []).map(mapRow), query);
|
|
@@ -278,12 +300,21 @@ export function createSearchService(
|
|
|
278
300
|
AND post.site_id = ?
|
|
279
301
|
AND post.status IN (${statusPlaceholders})
|
|
280
302
|
${formatFilter}
|
|
303
|
+
${langFilter}
|
|
281
304
|
ORDER BY rank DESC, post.published_at DESC NULLS LAST, post.id DESC
|
|
282
305
|
LIMIT ? OFFSET ?
|
|
283
306
|
`);
|
|
284
307
|
|
|
285
308
|
const { results } = await stmt
|
|
286
|
-
.bind(
|
|
309
|
+
.bind(
|
|
310
|
+
tsQuery,
|
|
311
|
+
siteId,
|
|
312
|
+
...status,
|
|
313
|
+
...formatParams,
|
|
314
|
+
...langParams,
|
|
315
|
+
limit,
|
|
316
|
+
offset,
|
|
317
|
+
)
|
|
287
318
|
.all<RawSearchRow>();
|
|
288
319
|
|
|
289
320
|
return withSnippetFallback((results || []).map(mapRow), query);
|
|
@@ -300,6 +331,8 @@ export function createSearchService(
|
|
|
300
331
|
const statusPlaceholders = status.map(() => "?").join(", ");
|
|
301
332
|
const formatFilter = options.format ? "AND post.format = ?" : "";
|
|
302
333
|
const formatParams = options.format ? [options.format] : [];
|
|
334
|
+
const langFilter = options.lang ? "AND post.language = ?" : "";
|
|
335
|
+
const langParams = options.lang ? [options.lang] : [];
|
|
303
336
|
|
|
304
337
|
if (databaseDialect === "pg") {
|
|
305
338
|
const stmt = rawQuery.prepare(`
|
|
@@ -324,6 +357,7 @@ export function createSearchService(
|
|
|
324
357
|
AND post.site_id = ?
|
|
325
358
|
AND post.status IN (${statusPlaceholders})
|
|
326
359
|
${formatFilter}
|
|
360
|
+
${langFilter}
|
|
327
361
|
ORDER BY rank DESC, post.published_at DESC NULLS LAST, post.id DESC
|
|
328
362
|
LIMIT ? OFFSET ?
|
|
329
363
|
`);
|
|
@@ -338,6 +372,7 @@ export function createSearchService(
|
|
|
338
372
|
siteId,
|
|
339
373
|
...status,
|
|
340
374
|
...formatParams,
|
|
375
|
+
...langParams,
|
|
341
376
|
limit,
|
|
342
377
|
offset,
|
|
343
378
|
)
|
|
@@ -371,6 +406,7 @@ export function createSearchService(
|
|
|
371
406
|
AND post.site_id = ?
|
|
372
407
|
AND post.status IN (${statusPlaceholders})
|
|
373
408
|
${formatFilter}
|
|
409
|
+
${langFilter}
|
|
374
410
|
${likeOrderBy}
|
|
375
411
|
LIMIT ? OFFSET ?
|
|
376
412
|
`);
|
|
@@ -384,6 +420,7 @@ export function createSearchService(
|
|
|
384
420
|
siteId,
|
|
385
421
|
...status,
|
|
386
422
|
...formatParams,
|
|
423
|
+
...langParams,
|
|
387
424
|
limit,
|
|
388
425
|
offset,
|
|
389
426
|
)
|
package/src/services/settings.ts
CHANGED
|
@@ -15,6 +15,7 @@ import { now } from "../lib/time.js";
|
|
|
15
15
|
import {
|
|
16
16
|
SETTINGS_KEYS,
|
|
17
17
|
ONBOARDING_STATUS,
|
|
18
|
+
type OnboardingStatus,
|
|
18
19
|
type SettingsKey,
|
|
19
20
|
} from "../lib/constants.js";
|
|
20
21
|
import {
|
|
@@ -22,8 +23,8 @@ import {
|
|
|
22
23
|
isLocale,
|
|
23
24
|
isValidContentLanguage,
|
|
24
25
|
normalizeContentLanguage,
|
|
26
|
+
resolveFirstRunDashboardLocale,
|
|
25
27
|
} from "../i18n/locales.js";
|
|
26
|
-
import { isCjkSerifFont } from "../i18n/detect.js";
|
|
27
28
|
import type { StorageDriver } from "../lib/storage.js";
|
|
28
29
|
import type { MediaService } from "./media.js";
|
|
29
30
|
import {
|
|
@@ -34,9 +35,49 @@ import {
|
|
|
34
35
|
import { arrayBufferToBase64 } from "../lib/favicon.js";
|
|
35
36
|
import { ValidationError } from "../lib/errors.js";
|
|
36
37
|
import { normalizeEditableSettingValue } from "../lib/schemas.js";
|
|
38
|
+
import { parseDiscoverSetting, type DiscoverSetting } from "../lib/discover.js";
|
|
39
|
+
import {
|
|
40
|
+
sendDiscoverPing,
|
|
41
|
+
type DiscoverAnnounceOutcome,
|
|
42
|
+
} from "../lib/discover-ping.js";
|
|
37
43
|
import { isSupportedTimeZone, normalizeTimeZone } from "../lib/timezones.js";
|
|
38
44
|
import type { FeedKind } from "../types/constants.js";
|
|
39
45
|
|
|
46
|
+
/**
|
|
47
|
+
* Read a stored announcement outcome back.
|
|
48
|
+
*
|
|
49
|
+
* Tolerant on purpose: this is a settings row a person can edit or an older
|
|
50
|
+
* version can have written differently, and the worst honest answer is "no
|
|
51
|
+
* attempt recorded" — never a crashed settings page.
|
|
52
|
+
*
|
|
53
|
+
* @param raw - The stored JSON, if any
|
|
54
|
+
* @returns The outcome, or null when absent or unreadable
|
|
55
|
+
*/
|
|
56
|
+
function parseAnnounceState(
|
|
57
|
+
raw: string | null,
|
|
58
|
+
): DiscoverAnnounceOutcome | null {
|
|
59
|
+
if (!raw) return null;
|
|
60
|
+
try {
|
|
61
|
+
const parsed: unknown = JSON.parse(raw);
|
|
62
|
+
if (typeof parsed !== "object" || parsed === null) return null;
|
|
63
|
+
const value = parsed as Record<string, unknown>;
|
|
64
|
+
if (typeof value["at"] !== "number") return null;
|
|
65
|
+
if (typeof value["ok"] !== "boolean") return null;
|
|
66
|
+
if (typeof value["feedUrl"] !== "string") return null;
|
|
67
|
+
return {
|
|
68
|
+
at: value["at"],
|
|
69
|
+
ok: value["ok"],
|
|
70
|
+
feedUrl: value["feedUrl"],
|
|
71
|
+
...(typeof value["status"] === "number"
|
|
72
|
+
? { status: value["status"] }
|
|
73
|
+
: {}),
|
|
74
|
+
...(typeof value["error"] === "string" ? { error: value["error"] } : {}),
|
|
75
|
+
};
|
|
76
|
+
} catch {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
40
81
|
export interface GeneralSettingsData {
|
|
41
82
|
siteName: string;
|
|
42
83
|
siteDescription: string;
|
|
@@ -44,7 +85,6 @@ export interface GeneralSettingsData {
|
|
|
44
85
|
siteLanguage: string;
|
|
45
86
|
/** Admin UI locale; empty string follows the content language. */
|
|
46
87
|
dashboardLanguage?: string;
|
|
47
|
-
cjkSerifFont: string;
|
|
48
88
|
showJantBrandingOnHome: boolean;
|
|
49
89
|
mainRssFeed?: FeedKind;
|
|
50
90
|
timeZone: string;
|
|
@@ -61,15 +101,23 @@ export interface SiteSettingsResult {
|
|
|
61
101
|
siteNameChanged: boolean;
|
|
62
102
|
}
|
|
63
103
|
|
|
104
|
+
/**
|
|
105
|
+
* Locale fields a settings page owns.
|
|
106
|
+
*
|
|
107
|
+
* Every field is "undefined = leave untouched" so the two pages that split
|
|
108
|
+
* these settings — Language and General — can each send only what they own,
|
|
109
|
+
* without either erasing the other's field.
|
|
110
|
+
*/
|
|
64
111
|
export interface LocaleSettingsData {
|
|
65
|
-
|
|
112
|
+
/** Site content language; also the primary language when multilingual is on. */
|
|
113
|
+
siteLanguage?: string;
|
|
66
114
|
/**
|
|
67
115
|
* Admin dashboard UI locale. Empty string clears the explicit setting so the
|
|
68
116
|
* dashboard follows the content language. When set, must be a catalog locale.
|
|
69
117
|
*/
|
|
70
118
|
dashboardLanguage?: string;
|
|
71
|
-
|
|
72
|
-
timeZone
|
|
119
|
+
/** IANA time zone; empty string resets to UTC. */
|
|
120
|
+
timeZone?: string;
|
|
73
121
|
}
|
|
74
122
|
|
|
75
123
|
export interface GeneralSettingsResult {
|
|
@@ -96,8 +144,27 @@ export interface SettingsService {
|
|
|
96
144
|
set(key: SettingsKey, value: string): Promise<void>;
|
|
97
145
|
setMany(entries: Partial<Record<SettingsKey, string>>): Promise<void>;
|
|
98
146
|
remove(key: SettingsKey): Promise<void>;
|
|
147
|
+
/** How far first-run setup has got: pending → provisioned → completed. */
|
|
148
|
+
getOnboardingStatus(): Promise<OnboardingStatus>;
|
|
99
149
|
isOnboardingComplete(): Promise<boolean>;
|
|
150
|
+
/**
|
|
151
|
+
* Mark a control-plane-created site as real but not yet confirmed by its
|
|
152
|
+
* owner. Public pages serve normally from here; setup still owes one answer.
|
|
153
|
+
*/
|
|
154
|
+
markSiteProvisioned(): Promise<void>;
|
|
100
155
|
completeOnboarding(): Promise<void>;
|
|
156
|
+
/**
|
|
157
|
+
* Close first-run setup on a site whose shell already exists, by recording
|
|
158
|
+
* the language its author says they write in.
|
|
159
|
+
*
|
|
160
|
+
* @param data - The confirmed content language, plus the browser's own
|
|
161
|
+
* language so the dashboard can be pinned to it
|
|
162
|
+
* @param opts - The language in effect before this answer
|
|
163
|
+
*/
|
|
164
|
+
confirmFirstRunLanguage(
|
|
165
|
+
data: { siteLanguage: string; browserLanguage?: string | null },
|
|
166
|
+
opts: { oldLanguage: string },
|
|
167
|
+
): Promise<void>;
|
|
101
168
|
updateSiteSettings(
|
|
102
169
|
data: SiteSettingsData,
|
|
103
170
|
opts: { fallbackSiteName: string; oldSiteName: string },
|
|
@@ -106,7 +173,6 @@ export interface SettingsService {
|
|
|
106
173
|
data: LocaleSettingsData,
|
|
107
174
|
opts: {
|
|
108
175
|
oldLanguage: string;
|
|
109
|
-
oldCjkSerifFont?: string;
|
|
110
176
|
oldDashboardLanguage?: string;
|
|
111
177
|
},
|
|
112
178
|
): Promise<{ languageChanged: boolean }>;
|
|
@@ -116,6 +182,46 @@ export interface SettingsService {
|
|
|
116
182
|
allowIndexing: boolean,
|
|
117
183
|
opts: { demoMode: boolean },
|
|
118
184
|
): Promise<void>;
|
|
185
|
+
/**
|
|
186
|
+
* Record the site's Jant Discover choice.
|
|
187
|
+
*
|
|
188
|
+
* Always stores an explicit value, including `off`. Once someone has used
|
|
189
|
+
* this control their answer is their own, and the rule that reads `noindex`
|
|
190
|
+
* as a refusal stops applying to them.
|
|
191
|
+
*
|
|
192
|
+
* @param mode - The choice the owner just made
|
|
193
|
+
* @param opts - Demo sites never announce themselves
|
|
194
|
+
* @returns Whether this save is the moment the site opted in, and so should
|
|
195
|
+
* announce itself to the configured directory
|
|
196
|
+
*/
|
|
197
|
+
updateDiscoverSetting(
|
|
198
|
+
mode: DiscoverSetting,
|
|
199
|
+
opts: { demoMode: boolean },
|
|
200
|
+
): Promise<{ shouldAnnounce: boolean }>;
|
|
201
|
+
/**
|
|
202
|
+
* Announce this site's feed to the configured directory, and remember how it
|
|
203
|
+
* went.
|
|
204
|
+
*
|
|
205
|
+
* The remembering is the point. An announcement that fails silently leaves
|
|
206
|
+
* the owner with a setting that reads "on" and a directory that has never
|
|
207
|
+
* heard of them, and no way to tell the two apart.
|
|
208
|
+
*
|
|
209
|
+
* Never throws: the caller runs this as background work behind a settings
|
|
210
|
+
* save, which must not fail because a directory is down.
|
|
211
|
+
*
|
|
212
|
+
* @param input - The directory endpoint and the feed address to announce
|
|
213
|
+
* @returns What the attempt came to
|
|
214
|
+
*/
|
|
215
|
+
announceToDiscover(input: {
|
|
216
|
+
endpoint: string;
|
|
217
|
+
feedUrl: string;
|
|
218
|
+
}): Promise<DiscoverAnnounceOutcome>;
|
|
219
|
+
/**
|
|
220
|
+
* The last announcement attempt, or `null` if the site has never made one.
|
|
221
|
+
*
|
|
222
|
+
* @returns The stored outcome, or null when absent or unreadable
|
|
223
|
+
*/
|
|
224
|
+
getDiscoverAnnounceState(): Promise<DiscoverAnnounceOutcome | null>;
|
|
119
225
|
/**
|
|
120
226
|
* Update general site settings with trim/set/remove logic.
|
|
121
227
|
* Empty strings are removed. Default values are removed to keep the DB clean.
|
|
@@ -128,7 +234,6 @@ export interface SettingsService {
|
|
|
128
234
|
data: GeneralSettingsData,
|
|
129
235
|
opts: {
|
|
130
236
|
oldLanguage: string;
|
|
131
|
-
oldCjkSerifFont?: string;
|
|
132
237
|
fallbackSiteName: string;
|
|
133
238
|
},
|
|
134
239
|
): Promise<GeneralSettingsResult>;
|
|
@@ -252,11 +357,29 @@ export function createSettingsService(
|
|
|
252
357
|
});
|
|
253
358
|
},
|
|
254
359
|
|
|
360
|
+
async getOnboardingStatus() {
|
|
361
|
+
const status = await this.get(SETTINGS_KEYS.ONBOARDING_STATUS);
|
|
362
|
+
if (status === ONBOARDING_STATUS.COMPLETED) {
|
|
363
|
+
return ONBOARDING_STATUS.COMPLETED;
|
|
364
|
+
}
|
|
365
|
+
if (status === ONBOARDING_STATUS.PROVISIONED) {
|
|
366
|
+
return ONBOARDING_STATUS.PROVISIONED;
|
|
367
|
+
}
|
|
368
|
+
return ONBOARDING_STATUS.PENDING;
|
|
369
|
+
},
|
|
370
|
+
|
|
255
371
|
async isOnboardingComplete() {
|
|
256
372
|
const status = await this.get(SETTINGS_KEYS.ONBOARDING_STATUS);
|
|
257
373
|
return status === ONBOARDING_STATUS.COMPLETED;
|
|
258
374
|
},
|
|
259
375
|
|
|
376
|
+
async markSiteProvisioned() {
|
|
377
|
+
await this.set(
|
|
378
|
+
SETTINGS_KEYS.ONBOARDING_STATUS,
|
|
379
|
+
ONBOARDING_STATUS.PROVISIONED,
|
|
380
|
+
);
|
|
381
|
+
},
|
|
382
|
+
|
|
260
383
|
async completeOnboarding() {
|
|
261
384
|
await this.set(
|
|
262
385
|
SETTINGS_KEYS.ONBOARDING_STATUS,
|
|
@@ -264,6 +387,23 @@ export function createSettingsService(
|
|
|
264
387
|
);
|
|
265
388
|
},
|
|
266
389
|
|
|
390
|
+
async confirmFirstRunLanguage(data, opts) {
|
|
391
|
+
await this.updateLocaleSettings(
|
|
392
|
+
{
|
|
393
|
+
siteLanguage: data.siteLanguage,
|
|
394
|
+
// Empty means "follow the content language", which is right unless
|
|
395
|
+
// the browser named a catalog following would not reach.
|
|
396
|
+
dashboardLanguage:
|
|
397
|
+
resolveFirstRunDashboardLocale(
|
|
398
|
+
data.siteLanguage,
|
|
399
|
+
data.browserLanguage,
|
|
400
|
+
) ?? "",
|
|
401
|
+
},
|
|
402
|
+
{ oldLanguage: opts.oldLanguage },
|
|
403
|
+
);
|
|
404
|
+
await this.completeOnboarding();
|
|
405
|
+
},
|
|
406
|
+
|
|
267
407
|
async updateSiteSettings(data, opts) {
|
|
268
408
|
const trimmedSiteName = data.siteName.trim();
|
|
269
409
|
const trimmedDescription = data.siteDescription.trim();
|
|
@@ -294,16 +434,18 @@ export function createSettingsService(
|
|
|
294
434
|
},
|
|
295
435
|
|
|
296
436
|
async updateLocaleSettings(data, opts) {
|
|
297
|
-
|
|
298
|
-
if (
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
437
|
+
let languageChanged = false;
|
|
438
|
+
if (data.siteLanguage !== undefined) {
|
|
439
|
+
const trimmedLanguage = data.siteLanguage.trim() || baseLocale;
|
|
440
|
+
if (!isValidContentLanguage(trimmedLanguage)) {
|
|
441
|
+
throw new ValidationError(
|
|
442
|
+
"Enter a valid BCP 47 language tag (e.g. en, zh-Hans, fi, ja, fr-CA).",
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
const normalized = normalizeContentLanguage(trimmedLanguage);
|
|
446
|
+
await this.set("SITE_LANGUAGE", normalized);
|
|
447
|
+
languageChanged = opts.oldLanguage !== normalized;
|
|
302
448
|
}
|
|
303
|
-
await this.set(
|
|
304
|
-
"SITE_LANGUAGE",
|
|
305
|
-
normalizeContentLanguage(trimmedLanguage),
|
|
306
|
-
);
|
|
307
449
|
|
|
308
450
|
// Dashboard UI locale. undefined = leave untouched; "" = clear so the
|
|
309
451
|
// dashboard follows the content language; otherwise it must be one of the
|
|
@@ -325,37 +467,24 @@ export function createSettingsService(
|
|
|
325
467
|
(opts.oldDashboardLanguage ?? "") !== dashboardLanguage;
|
|
326
468
|
}
|
|
327
469
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
const normalizedTimeZone = normalizeTimeZone(data.timeZone);
|
|
342
|
-
if (normalizedTimeZone !== "UTC") {
|
|
343
|
-
await this.set("TIME_ZONE", normalizedTimeZone);
|
|
470
|
+
if (data.timeZone !== undefined) {
|
|
471
|
+
if (data.timeZone) {
|
|
472
|
+
if (!isSupportedTimeZone(data.timeZone)) {
|
|
473
|
+
throw new ValidationError("Choose a valid time zone.");
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
const normalizedTimeZone = normalizeTimeZone(data.timeZone);
|
|
477
|
+
if (normalizedTimeZone !== "UTC") {
|
|
478
|
+
await this.set("TIME_ZONE", normalizedTimeZone);
|
|
479
|
+
} else {
|
|
480
|
+
await this.remove("TIME_ZONE");
|
|
481
|
+
}
|
|
344
482
|
} else {
|
|
345
483
|
await this.remove("TIME_ZONE");
|
|
346
484
|
}
|
|
347
|
-
} else {
|
|
348
|
-
await this.remove("TIME_ZONE");
|
|
349
485
|
}
|
|
350
486
|
|
|
351
|
-
|
|
352
|
-
cjkFont && isCjkSerifFont(cjkFont) ? cjkFont : "off";
|
|
353
|
-
return {
|
|
354
|
-
languageChanged:
|
|
355
|
-
opts.oldLanguage !== trimmedLanguage ||
|
|
356
|
-
(opts.oldCjkSerifFont ?? "off") !== effectiveCjkFont ||
|
|
357
|
-
dashboardChanged,
|
|
358
|
-
};
|
|
487
|
+
return { languageChanged: languageChanged || dashboardChanged };
|
|
359
488
|
},
|
|
360
489
|
|
|
361
490
|
async updateFeedSettings(data) {
|
|
@@ -384,6 +513,73 @@ export function createSettingsService(
|
|
|
384
513
|
}
|
|
385
514
|
},
|
|
386
515
|
|
|
516
|
+
async updateDiscoverSetting(mode, opts) {
|
|
517
|
+
const previous = parseDiscoverSetting(await this.get("DISCOVER"));
|
|
518
|
+
await this.set("DISCOVER", mode);
|
|
519
|
+
|
|
520
|
+
// A ping says "read me now", so both edges of this setting are worth
|
|
521
|
+
// sending one. Opting in is the obvious half — and coming from unset
|
|
522
|
+
// counts, because a site that has never touched this control has never
|
|
523
|
+
// told anyone it exists. Opting out is the half that used to be missed:
|
|
524
|
+
// a directory that is not told keeps the blog until its next scheduled
|
|
525
|
+
// read, and the owner watches a blog they just removed sit there for
|
|
526
|
+
// another hour. Switching between `latest` and `featured` while already
|
|
527
|
+
// listed changes nothing a directory needs to be told twice.
|
|
528
|
+
const wasListed = previous !== null && previous !== "off";
|
|
529
|
+
const isListed = mode !== "off";
|
|
530
|
+
const changed = wasListed !== isListed;
|
|
531
|
+
return { shouldAnnounce: changed && !opts.demoMode };
|
|
532
|
+
},
|
|
533
|
+
|
|
534
|
+
async announceToDiscover(input) {
|
|
535
|
+
const outcome = await sendDiscoverPing({
|
|
536
|
+
endpoint: input.endpoint,
|
|
537
|
+
feedUrl: input.feedUrl,
|
|
538
|
+
now,
|
|
539
|
+
});
|
|
540
|
+
// Recording is best-effort, and the try/catch is what makes the "never
|
|
541
|
+
// throws" above true. The caller runs this as background work with
|
|
542
|
+
// nothing awaiting it, so a rejection here has nobody to reject to — on
|
|
543
|
+
// a Node runtime that ends the process. A settings row that could not be
|
|
544
|
+
// written is worth a log and a status block the owner cannot read; it is
|
|
545
|
+
// not worth the site.
|
|
546
|
+
try {
|
|
547
|
+
await this.set("DISCOVER_ANNOUNCE_STATE", JSON.stringify(outcome));
|
|
548
|
+
} catch (error) {
|
|
549
|
+
// eslint-disable-next-line no-console -- The owner's status block is now stale; say why.
|
|
550
|
+
console.error(
|
|
551
|
+
`[Jant] Discover announcement could not be recorded: ${
|
|
552
|
+
error instanceof Error ? error.message : String(error)
|
|
553
|
+
}`,
|
|
554
|
+
);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// The stored outcome is for the owner, and it does not always reach
|
|
558
|
+
// them: the announcement runs behind the settings save so the response
|
|
559
|
+
// cannot carry it, and a hosted site's settings page hides the status
|
|
560
|
+
// block altogether. Log it as well, so an announcement that never got
|
|
561
|
+
// through is answerable by whoever runs the deployment — a directory
|
|
562
|
+
// pointed at the wrong host answers 404 and looks, from the dashboard,
|
|
563
|
+
// exactly like one that worked.
|
|
564
|
+
if (outcome.ok) {
|
|
565
|
+
// eslint-disable-next-line no-console -- One-line audit trail for a rare write.
|
|
566
|
+
console.log(
|
|
567
|
+
`[Jant] Discover announced: feed=${outcome.feedUrl} directory=${input.endpoint}`,
|
|
568
|
+
);
|
|
569
|
+
} else {
|
|
570
|
+
// eslint-disable-next-line no-console -- A lost announcement must be visible.
|
|
571
|
+
console.error(
|
|
572
|
+
`[Jant] Discover announcement failed: ${outcome.error ?? "Unknown error."} feed=${outcome.feedUrl} directory=${input.endpoint}`,
|
|
573
|
+
);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
return outcome;
|
|
577
|
+
},
|
|
578
|
+
|
|
579
|
+
async getDiscoverAnnounceState() {
|
|
580
|
+
return parseAnnounceState(await this.get("DISCOVER_ANNOUNCE_STATE"));
|
|
581
|
+
},
|
|
582
|
+
|
|
387
583
|
async updateGeneral(data, opts) {
|
|
388
584
|
const { displayName } = await this.updateSiteSettings(data, {
|
|
389
585
|
fallbackSiteName: opts.fallbackSiteName,
|
|
@@ -392,7 +588,6 @@ export function createSettingsService(
|
|
|
392
588
|
await this.updateHomeBranding(data.showJantBrandingOnHome);
|
|
393
589
|
const { languageChanged } = await this.updateLocaleSettings(data, {
|
|
394
590
|
oldLanguage: opts.oldLanguage,
|
|
395
|
-
oldCjkSerifFont: opts.oldCjkSerifFont,
|
|
396
591
|
});
|
|
397
592
|
|
|
398
593
|
await this.updateFeedSettings({ mainRssFeed: data.mainRssFeed });
|
|
@@ -218,6 +218,7 @@ export function createSiteAdminService(
|
|
|
218
218
|
media,
|
|
219
219
|
navItems,
|
|
220
220
|
pathRegistry,
|
|
221
|
+
smartCollections,
|
|
221
222
|
threadCollections,
|
|
222
223
|
posts,
|
|
223
224
|
settings,
|
|
@@ -313,17 +314,20 @@ export function createSiteAdminService(
|
|
|
313
314
|
databaseSchema,
|
|
314
315
|
databaseDialect,
|
|
315
316
|
);
|
|
316
|
-
if (await settingsService.
|
|
317
|
+
if ((await settingsService.getOnboardingStatus()) !== "pending") {
|
|
317
318
|
return;
|
|
318
319
|
}
|
|
319
320
|
|
|
320
321
|
await settingsService.set(SETTINGS_KEYS.SITE_NAME, input.siteName);
|
|
322
|
+
// The control plane can only pass a guess — today it forwards the locale
|
|
323
|
+
// the owner was browsing it in, which is their reading language, not
|
|
324
|
+
// necessarily the one they write in. It stands in until setup asks, so the
|
|
325
|
+
// site is never languageless, and setup offers it preselected.
|
|
321
326
|
await settingsService.updateLocaleSettings(
|
|
322
327
|
{
|
|
323
328
|
siteLanguage: input.siteLanguage?.trim()
|
|
324
329
|
? detectLocaleFromHeader(input.siteLanguage)
|
|
325
330
|
: baseLocale,
|
|
326
|
-
cjkSerifFont: "off",
|
|
327
331
|
timeZone: input.timeZone,
|
|
328
332
|
},
|
|
329
333
|
{
|
|
@@ -332,7 +336,9 @@ export function createSiteAdminService(
|
|
|
332
336
|
);
|
|
333
337
|
const navItems = createNavItemService(targetDb, siteId, databaseSchema);
|
|
334
338
|
await navItems.materializeDefaultNavigation();
|
|
335
|
-
|
|
339
|
+
// Not `completeOnboarding()`: the site is real and readable from here, but
|
|
340
|
+
// its owner still owes setup the one answer nothing can infer.
|
|
341
|
+
await settingsService.markSiteProvisioned();
|
|
336
342
|
}
|
|
337
343
|
|
|
338
344
|
async function createWithDatabase(
|
|
@@ -509,6 +515,14 @@ export function createSiteAdminService(
|
|
|
509
515
|
);
|
|
510
516
|
await targetDb.delete(posts).where(eq(posts.siteId, siteId));
|
|
511
517
|
|
|
518
|
+
// Both kinds of collection, and smart ones first — a smart collection holds
|
|
519
|
+
// a `collection_id`. There is no constraint forcing that order any more
|
|
520
|
+
// (see the note on the column in `db/schema.ts`), but this function names
|
|
521
|
+
// every table it clears, and leaving one to an implicit cascade from `site`
|
|
522
|
+
// is how the next table gets forgotten.
|
|
523
|
+
await targetDb
|
|
524
|
+
.delete(smartCollections)
|
|
525
|
+
.where(eq(smartCollections.siteId, siteId));
|
|
512
526
|
await targetDb.delete(collections).where(eq(collections.siteId, siteId));
|
|
513
527
|
await targetDb.delete(apiTokens).where(eq(apiTokens.siteId, siteId));
|
|
514
528
|
await targetDb.delete(settings).where(eq(settings.siteId, siteId));
|
|
@@ -782,11 +796,11 @@ export function createSiteAdminService(
|
|
|
782
796
|
(theme) => theme.id === appConfig.fontThemeId,
|
|
783
797
|
)
|
|
784
798
|
: undefined;
|
|
799
|
+
// The static export is one site-wide stylesheet, so it uses the site
|
|
800
|
+
// language for the CJK stack. Per-page language overrides are a runtime
|
|
801
|
+
// concern that a flat Hugo export has no equivalent for.
|
|
785
802
|
const fontOverrides = {
|
|
786
|
-
...getCjkFontCssVariables(
|
|
787
|
-
appConfig.siteLanguage,
|
|
788
|
-
appConfig.cjkSerifFont,
|
|
789
|
-
),
|
|
803
|
+
...getCjkFontCssVariables(appConfig.siteLanguage),
|
|
790
804
|
...(fontTheme ? getFontThemeCssVariables(fontTheme) : {}),
|
|
791
805
|
};
|
|
792
806
|
const themeCss = buildThemeStyle(
|
|
@@ -809,10 +823,13 @@ export function createSiteAdminService(
|
|
|
809
823
|
siteUrl: appConfig.siteUrl,
|
|
810
824
|
siteDescription: appConfig.siteDescription,
|
|
811
825
|
siteLanguage: appConfig.siteLanguage,
|
|
826
|
+
multilingualEnabled: appConfig.multilingualEnabled,
|
|
827
|
+
additionalLanguages: appConfig.additionalLanguages,
|
|
812
828
|
showJantBrandingOnHome: appConfig.showJantBrandingOnHome,
|
|
813
829
|
publicApiEnabled: appConfig.publicApiEnabled,
|
|
814
830
|
rssFeedsEnabled: appConfig.rssFeedsEnabled,
|
|
815
831
|
mainRssFeed: appConfig.mainRssFeed,
|
|
832
|
+
archiveDefaultLayout: appConfig.archiveDefaultLayout,
|
|
816
833
|
siteFooter: appConfig.siteFooter,
|
|
817
834
|
showHeaderAvatar: appConfig.showHeaderAvatar,
|
|
818
835
|
siteAvatarUrl: appConfig.siteAvatarUrl,
|