@jant/core 0.6.15 → 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 +67 -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-C-oi_t8W.js → app-K_Aa1MMn.js} +52623 -13193
- 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-CsFx6F_M.js → github-sync-BPAvT999.js} +2343 -110
- 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__/mise-config.test.ts +22 -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 +524 -2
- package/src/client/__tests__/compose-launch.test.ts +62 -1
- package/src/client/__tests__/compose-shortcuts.test.ts +57 -8
- 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 +88 -1
- 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__/compose-format-convert.test.ts +0 -19
- 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 +79 -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 +3283 -389
- package/src/client/components/__tests__/jant-compose-editor.test.ts +581 -86
- package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +34 -89
- 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-format-convert.ts +5 -1
- package/src/client/components/compose-types.ts +121 -3
- 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 +106 -49
- package/src/client/components/jant-compose-dialog.ts +2757 -1194
- package/src/client/components/jant-compose-editor.ts +866 -397
- package/src/client/components/jant-compose-fullscreen.ts +53 -47
- 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 +1142 -268
- 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 +336 -242
- package/src/client/compose-launch.ts +89 -2
- package/src/client/compose-shortcuts.ts +9 -7
- 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/post-refresh.ts +135 -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/__tests__/link-input-rules.test.ts +121 -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-input-rules.ts +4 -4
- package/src/client/tiptap/link-toolbar.ts +22 -9
- package/src/client/toast.ts +143 -38
- 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__/backfill-thread-activity.test.ts +234 -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/__tests__/thread-activity.test.ts +113 -0
- package/src/db/backfills/0005_split_thread_activity_from_quiet_replies.sql +68 -0
- package/src/db/backfills/0006_clear_mirrored_page_nav_labels.sql +31 -0
- package/src/db/migrations/0031_many_ego.sql +3 -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/0031_snapshot.json +2562 -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 +28 -0
- package/src/db/migrations/pg/0029_rare_hairball.sql +3 -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/0029_snapshot.json +3289 -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 +28 -0
- package/src/db/pg/schema.ts +213 -27
- package/src/db/post-visibility.ts +117 -0
- package/src/db/schema.ts +242 -27
- package/src/db/thread-activity.ts +101 -0
- 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 +568 -168
- package/src/i18n/locales/public/en.ts +1 -1
- package/src/i18n/locales/public/zh-Hans.po +562 -162
- package/src/i18n/locales/public/zh-Hans.ts +1 -1
- package/src/i18n/locales/public/zh-Hant.po +562 -162
- 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 +1691 -12
- 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__/markdown.test.ts +16 -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 +88 -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__/translit.test.ts +87 -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 +93 -34
- 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 +1021 -66
- 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 +21 -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/link-preview.ts +25 -0
- package/src/lib/markdown-manager.ts +6 -1
- 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-display.ts +42 -9
- 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 +81 -10
- package/src/lib/tiptap-render.ts +5 -4
- package/src/lib/translit.ts +288 -0
- package/src/lib/upload.ts +16 -13
- package/src/lib/url.ts +188 -24
- package/src/lib/version.ts +10 -8
- package/src/lib/view-language.ts +436 -0
- package/src/lib/view.ts +79 -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__/palette.test.ts +44 -0
- 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 +172 -9
- package/src/routes/api/public/__tests__/archive.test.ts +111 -0
- package/src/routes/api/public/__tests__/posts.test.ts +38 -0
- package/src/routes/api/public/archive.ts +77 -81
- package/src/routes/api/public/posts.ts +30 -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 +11 -2
- 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/__tests__/sitemap.test.ts +68 -1
- 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 +657 -1
- 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__/featured.test.ts +6 -2
- 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__/preview.test.ts +3 -1
- 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 +507 -357
- 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 +183 -19
- 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 +139 -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-timeline.test.ts +142 -11
- package/src/services/__tests__/post.test.ts +323 -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 +330 -47
- 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 +281 -24
- 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 +225 -7
- package/src/services/post.ts +1318 -252
- package/src/services/search.ts +48 -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 -1585
- package/src/styles/site-media.css +20 -6
- package/src/styles/tokens.css +144 -13
- package/src/styles/ui-author.css +7602 -0
- package/src/styles/ui.css +1224 -7276
- 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 +92 -1
- package/src/types/operations.ts +64 -2
- package/src/types/props.ts +168 -30
- package/src/types/views.ts +61 -1
- package/src/ui/__tests__/font-themes.test.ts +52 -12
- package/src/ui/compose/ComposeDialog.tsx +215 -21
- 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/LinkPreview.tsx +2 -7
- package/src/ui/feed/NoteCard.tsx +9 -4
- package/src/ui/feed/PostStatusBadges.tsx +103 -17
- 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 +401 -301
- 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 +122 -3
- 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/DraftPreviewBar.tsx +25 -9
- 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 +55 -23
- 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/custom-icons.ts +5 -0
- package/src/ui/shared/navigation-labels.ts +45 -71
- package/src/ui/shared/post-article-attributes.ts +8 -0
- package/src/ui/shared/smart-collection-labels.ts +343 -0
- package/dist/app-BxCOR3Uc.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-pLre2DM_.js +0 -1
- package/dist/client/_assets/client-B_eGKN5p.css +0 -2
- package/dist/client/_assets/client-DxY5BFe8.js +0 -296
- package/dist/client/_assets/client-auth-CakPESv9.js +0 -5533
- package/dist/env-OHRKGcMj.js +0 -281
- package/dist/github-api-BgSiE71w.js +0 -176
- package/dist/github-app-BbklkFmU.js +0 -275
- package/dist/github-sync-Cq1pzzOI.js +0 -431
- package/dist/github-sync-ppWXN3jb.js +0 -4
- package/dist/url-BMYO-Zlt.js +0 -395
- 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
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Language Service
|
|
3
|
+
*
|
|
4
|
+
* Owns the multilingual content lifecycle: which languages a site serves, which
|
|
5
|
+
* one the unprefixed root URLs belong to, and the ordering rules that keep
|
|
6
|
+
* those two settings consistent with the `post.language` column.
|
|
7
|
+
*
|
|
8
|
+
* These operations are multi-step by nature — enabling has to check URL-prefix
|
|
9
|
+
* conflicts, write two settings, and stamp every unmarked post, in that order —
|
|
10
|
+
* so they live here rather than in routes, and this service is the only writer
|
|
11
|
+
* of `MULTILINGUAL_ENABLED` and `ADDITIONAL_LANGUAGES`.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { and, eq, inArray } from "drizzle-orm";
|
|
15
|
+
import type { Database } from "../db/index.js";
|
|
16
|
+
import {
|
|
17
|
+
sqliteSchemaBundle,
|
|
18
|
+
type DatabaseSchema,
|
|
19
|
+
} from "../db/schema-bundle.js";
|
|
20
|
+
import {
|
|
21
|
+
formatLanguageList,
|
|
22
|
+
isValidContentLanguage,
|
|
23
|
+
normalizeContentLanguage,
|
|
24
|
+
parseLanguageList,
|
|
25
|
+
toLanguagePrefix,
|
|
26
|
+
} from "../i18n/locales.js";
|
|
27
|
+
import { RESERVED_PATHS } from "../lib/constants.js";
|
|
28
|
+
import {
|
|
29
|
+
ConflictError,
|
|
30
|
+
LanguageInUseError,
|
|
31
|
+
ValidationError,
|
|
32
|
+
} from "../lib/errors.js";
|
|
33
|
+
import type { PathService } from "./path.js";
|
|
34
|
+
import type { PostService } from "./post.js";
|
|
35
|
+
import type { SettingsService } from "./settings.js";
|
|
36
|
+
|
|
37
|
+
/** The site's language configuration, as the rest of the app reads it. */
|
|
38
|
+
export interface LanguageState {
|
|
39
|
+
/** Whether per-language browsing views are served. */
|
|
40
|
+
enabled: boolean;
|
|
41
|
+
/** Canonical tag served from the unprefixed root URLs. */
|
|
42
|
+
primary: string;
|
|
43
|
+
/** Canonical tags served under a URL prefix, in switcher order. */
|
|
44
|
+
additional: string[];
|
|
45
|
+
/** Every configured language, primary first. */
|
|
46
|
+
all: string[];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** What the enable dialog needs to describe what it is about to do. */
|
|
50
|
+
export interface LanguageEnablePreview {
|
|
51
|
+
/** Posts that would be stamped with the primary language. */
|
|
52
|
+
pendingCount: number;
|
|
53
|
+
/** Current primary language, prefilled into the dialog. */
|
|
54
|
+
primary: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface EnableMultilingualInput {
|
|
58
|
+
/** Primary language; may differ from the stored one when the author edits it in the dialog. */
|
|
59
|
+
primary: string;
|
|
60
|
+
/** At least one language to serve under a prefix. */
|
|
61
|
+
additional: string[];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface LanguageService {
|
|
65
|
+
/** Read the site's language configuration. */
|
|
66
|
+
getState(): Promise<LanguageState>;
|
|
67
|
+
/** Numbers and defaults for the enable confirmation dialog. */
|
|
68
|
+
getEnablePreview(): Promise<LanguageEnablePreview>;
|
|
69
|
+
/**
|
|
70
|
+
* Turn on multilingual content.
|
|
71
|
+
*
|
|
72
|
+
* Validates the language set, rejects prefixes that would shadow an existing
|
|
73
|
+
* URL, writes both settings, then stamps every post that has no language yet
|
|
74
|
+
* with the primary language — so the root view keeps showing exactly what it
|
|
75
|
+
* showed a moment earlier.
|
|
76
|
+
*
|
|
77
|
+
* @returns How many posts were stamped
|
|
78
|
+
* @throws {ValidationError} When the language set is empty or malformed
|
|
79
|
+
* @throws {ConflictError} When a language prefix collides with a stored path
|
|
80
|
+
*/
|
|
81
|
+
enable(input: EnableMultilingualInput): Promise<{ markedCount: number }>;
|
|
82
|
+
/**
|
|
83
|
+
* Turn off multilingual content, keeping the configuration and every stored
|
|
84
|
+
* `post.language` value so re-enabling restores the same setup and the old
|
|
85
|
+
* prefixes can still redirect.
|
|
86
|
+
*/
|
|
87
|
+
disable(): Promise<void>;
|
|
88
|
+
/**
|
|
89
|
+
* Move the root URLs to another configured language.
|
|
90
|
+
*
|
|
91
|
+
* Swaps the two lists in one step: the new primary leaves the additional
|
|
92
|
+
* list and the old primary joins it. Doing only half would leave the old
|
|
93
|
+
* primary's posts with no view at all — filtered out of the root, and with no
|
|
94
|
+
* prefix of their own.
|
|
95
|
+
*/
|
|
96
|
+
setPrimary(language: string): Promise<void>;
|
|
97
|
+
/** Add a language to serve under a URL prefix. */
|
|
98
|
+
addLanguage(language: string): Promise<void>;
|
|
99
|
+
/**
|
|
100
|
+
* Stop serving a language.
|
|
101
|
+
*
|
|
102
|
+
* Only allowed while no post is written in it. With posts present there is no
|
|
103
|
+
* good answer — hiding, migrating, or redirecting them all guess at intent —
|
|
104
|
+
* so this reports the count and leaves the choice to the author.
|
|
105
|
+
*
|
|
106
|
+
* Removing the last additional language turns multilingual content off with
|
|
107
|
+
* it: per-language views need a second language to mean anything, and a site
|
|
108
|
+
* left with the flag on and nothing to serve under it would report the
|
|
109
|
+
* feature as on everywhere the setting is read while behaving as
|
|
110
|
+
* single-language everywhere it is used.
|
|
111
|
+
*
|
|
112
|
+
* @returns Whether multilingual content was turned off along with it
|
|
113
|
+
* @throws {ConflictError} When posts still use the language
|
|
114
|
+
*/
|
|
115
|
+
removeLanguage(language: string): Promise<{ multilingualDisabled: boolean }>;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export interface LanguageServiceDeps {
|
|
119
|
+
settings: SettingsService;
|
|
120
|
+
posts: PostService;
|
|
121
|
+
paths: PathService;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Language tags whose URL prefix can never work, whatever the site contains. */
|
|
125
|
+
const RESERVED_PREFIXES = new Set<string>(RESERVED_PATHS);
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Read the site's language configuration directly from its settings rows.
|
|
129
|
+
*
|
|
130
|
+
* A standalone reader rather than a method on the service because slug and
|
|
131
|
+
* custom-URL validation need it from inside other services, where threading a
|
|
132
|
+
* whole `LanguageService` through would be more wiring than the two values are
|
|
133
|
+
* worth. The service remains the only *writer*.
|
|
134
|
+
*
|
|
135
|
+
* @param db - Site database handle
|
|
136
|
+
* @param siteId - Site the settings belong to
|
|
137
|
+
* @param databaseSchema - Dialect-specific schema bundle
|
|
138
|
+
* @returns The primary language tag; every language the site publishes, primary
|
|
139
|
+
* first (empty while multilingual content is off); and the URL prefixes
|
|
140
|
+
* language views currently serve
|
|
141
|
+
* @example
|
|
142
|
+
* await readLanguageSettings(db, siteId);
|
|
143
|
+
* // => { primary: "zh-Hans", languages: ["zh-Hans", "en"], reservedPrefixes: ["en"] }
|
|
144
|
+
*/
|
|
145
|
+
export async function readLanguageSettings(
|
|
146
|
+
db: Database,
|
|
147
|
+
siteId: string,
|
|
148
|
+
databaseSchema: DatabaseSchema = sqliteSchemaBundle,
|
|
149
|
+
): Promise<{
|
|
150
|
+
primary: string;
|
|
151
|
+
languages: string[];
|
|
152
|
+
reservedPrefixes: string[];
|
|
153
|
+
}> {
|
|
154
|
+
const { settings } = databaseSchema;
|
|
155
|
+
const rows = await db
|
|
156
|
+
.select({ key: settings.key, value: settings.value })
|
|
157
|
+
.from(settings)
|
|
158
|
+
.where(
|
|
159
|
+
and(
|
|
160
|
+
eq(settings.siteId, siteId),
|
|
161
|
+
inArray(settings.key, [
|
|
162
|
+
"SITE_LANGUAGE",
|
|
163
|
+
"MULTILINGUAL_ENABLED",
|
|
164
|
+
"ADDITIONAL_LANGUAGES",
|
|
165
|
+
]),
|
|
166
|
+
),
|
|
167
|
+
);
|
|
168
|
+
const values = new Map(rows.map((row) => [row.key, row.value]));
|
|
169
|
+
const primary = normalizeContentLanguage(values.get("SITE_LANGUAGE") ?? "");
|
|
170
|
+
const additional =
|
|
171
|
+
values.get("MULTILINGUAL_ENABLED") === "true"
|
|
172
|
+
? parseLanguageList(values.get("ADDITIONAL_LANGUAGES")).filter(
|
|
173
|
+
(tag) => tag !== primary,
|
|
174
|
+
)
|
|
175
|
+
: [];
|
|
176
|
+
// Empty — not `[primary]` — when the site publishes one language, so callers
|
|
177
|
+
// can treat "no languages" as "this site is not multilingual".
|
|
178
|
+
const languages = additional.length > 0 ? [primary, ...additional] : [];
|
|
179
|
+
return {
|
|
180
|
+
primary,
|
|
181
|
+
languages,
|
|
182
|
+
reservedPrefixes: additional.map(toLanguagePrefix),
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function requireValidLanguage(value: string): string {
|
|
187
|
+
const trimmed = value.trim();
|
|
188
|
+
if (!trimmed || !isValidContentLanguage(trimmed)) {
|
|
189
|
+
throw new ValidationError(
|
|
190
|
+
"Enter a valid BCP 47 language tag (e.g. en, zh-Hans, ja).",
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
return normalizeContentLanguage(trimmed);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export function createLanguageService(
|
|
197
|
+
deps: LanguageServiceDeps,
|
|
198
|
+
): LanguageService {
|
|
199
|
+
const { settings, posts, paths } = deps;
|
|
200
|
+
|
|
201
|
+
async function readState(): Promise<LanguageState> {
|
|
202
|
+
const [enabledValue, primaryValue, additionalValue] = await Promise.all([
|
|
203
|
+
settings.get("MULTILINGUAL_ENABLED"),
|
|
204
|
+
settings.get("SITE_LANGUAGE"),
|
|
205
|
+
settings.get("ADDITIONAL_LANGUAGES"),
|
|
206
|
+
]);
|
|
207
|
+
|
|
208
|
+
const primary = primaryValue?.trim()
|
|
209
|
+
? normalizeContentLanguage(primaryValue)
|
|
210
|
+
: "en";
|
|
211
|
+
// Defensive: a hand-edited settings row could list the primary language
|
|
212
|
+
// twice. The prefix for it would never route, so drop it here rather than
|
|
213
|
+
// letting a dead entry reach the switcher.
|
|
214
|
+
const additional = parseLanguageList(additionalValue).filter(
|
|
215
|
+
(tag) => tag !== primary,
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
enabled: enabledValue === "true" && additional.length > 0,
|
|
220
|
+
primary,
|
|
221
|
+
additional,
|
|
222
|
+
all: [primary, ...additional],
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Reject a language whose URL prefix would shadow something already served.
|
|
228
|
+
*
|
|
229
|
+
* Language prefixes are checked here, when a language is added, rather than
|
|
230
|
+
* being reserved globally: the two-letter ISO codes are ordinary English
|
|
231
|
+
* words (`it`, `is`, `no`, `go`, `my`), and permanently confiscating those
|
|
232
|
+
* slugs from every site for an off-by-default feature is out of proportion.
|
|
233
|
+
*/
|
|
234
|
+
async function assertPrefixAvailable(language: string): Promise<void> {
|
|
235
|
+
const prefix = toLanguagePrefix(language);
|
|
236
|
+
|
|
237
|
+
if (RESERVED_PREFIXES.has(prefix)) {
|
|
238
|
+
throw new ConflictError(
|
|
239
|
+
`Jant already uses /${prefix} for its own pages, so it cannot serve a language there.`,
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const clashes = await paths.findPathsUnderSegment(prefix, 3);
|
|
244
|
+
const first = clashes[0];
|
|
245
|
+
if (first) {
|
|
246
|
+
throw new ConflictError(
|
|
247
|
+
`/${first.path} is already taken. Change that URL, then add this language.`,
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function normalizeAdditional(
|
|
253
|
+
tags: readonly string[],
|
|
254
|
+
primary: string,
|
|
255
|
+
): string[] {
|
|
256
|
+
const seen = new Set<string>();
|
|
257
|
+
const result: string[] = [];
|
|
258
|
+
for (const raw of tags) {
|
|
259
|
+
const tag = requireValidLanguage(raw);
|
|
260
|
+
if (tag === primary || seen.has(tag)) continue;
|
|
261
|
+
seen.add(tag);
|
|
262
|
+
result.push(tag);
|
|
263
|
+
}
|
|
264
|
+
return result;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
async function writeLanguages(
|
|
268
|
+
primary: string,
|
|
269
|
+
additional: readonly string[],
|
|
270
|
+
): Promise<void> {
|
|
271
|
+
await settings.set("SITE_LANGUAGE", primary);
|
|
272
|
+
if (additional.length > 0) {
|
|
273
|
+
await settings.set(
|
|
274
|
+
"ADDITIONAL_LANGUAGES",
|
|
275
|
+
formatLanguageList(additional),
|
|
276
|
+
);
|
|
277
|
+
} else {
|
|
278
|
+
await settings.remove("ADDITIONAL_LANGUAGES");
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return {
|
|
283
|
+
getState: readState,
|
|
284
|
+
|
|
285
|
+
async getEnablePreview() {
|
|
286
|
+
const [state, pendingCount] = await Promise.all([
|
|
287
|
+
readState(),
|
|
288
|
+
posts.countMissingLanguage(),
|
|
289
|
+
]);
|
|
290
|
+
return { pendingCount, primary: state.primary };
|
|
291
|
+
},
|
|
292
|
+
|
|
293
|
+
async enable(input) {
|
|
294
|
+
const primary = requireValidLanguage(input.primary);
|
|
295
|
+
const additional = normalizeAdditional(input.additional, primary);
|
|
296
|
+
if (additional.length === 0) {
|
|
297
|
+
throw new ValidationError(
|
|
298
|
+
"Add at least one more language before turning this on.",
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// The list must keep a place for every language posts are already
|
|
303
|
+
// written in. Turning multilingual off keeps both the languages and
|
|
304
|
+
// the stamps, so a re-enable that dropped one from the dialog would
|
|
305
|
+
// otherwise strand its posts outside every view — the exact orphaning
|
|
306
|
+
// the remove guard exists to prevent, through a side door.
|
|
307
|
+
const allowed = new Set([primary, ...additional]);
|
|
308
|
+
const stranded = (await posts.listLanguagesInUse()).find(
|
|
309
|
+
(entry) => !allowed.has(entry.language),
|
|
310
|
+
);
|
|
311
|
+
if (stranded) {
|
|
312
|
+
throw new LanguageInUseError(
|
|
313
|
+
stranded.count === 1
|
|
314
|
+
? "One post is still written in a language missing from this list. Keep the language, or change the post's language first."
|
|
315
|
+
: `${stranded.count} posts are still written in a language missing from this list. Keep the language, or change their language first.`,
|
|
316
|
+
stranded.language,
|
|
317
|
+
stranded.count,
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Every prefix is checked before anything is written, so a conflict
|
|
322
|
+
// leaves the site exactly as it was.
|
|
323
|
+
for (const language of additional) {
|
|
324
|
+
await assertPrefixAvailable(language);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
await writeLanguages(primary, additional);
|
|
328
|
+
await settings.set("MULTILINGUAL_ENABLED", "true");
|
|
329
|
+
|
|
330
|
+
const markedCount = await posts.materializeMissingLanguage(primary);
|
|
331
|
+
return { markedCount };
|
|
332
|
+
},
|
|
333
|
+
|
|
334
|
+
async disable() {
|
|
335
|
+
await settings.remove("MULTILINGUAL_ENABLED");
|
|
336
|
+
},
|
|
337
|
+
|
|
338
|
+
async setPrimary(language) {
|
|
339
|
+
const next = requireValidLanguage(language);
|
|
340
|
+
const state = await readState();
|
|
341
|
+
if (next === state.primary) return;
|
|
342
|
+
|
|
343
|
+
if (!state.additional.includes(next)) {
|
|
344
|
+
throw new ValidationError(
|
|
345
|
+
"Add that language to the site before making it the primary language.",
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// The old primary needs a prefix from now on, and the new one no longer
|
|
350
|
+
// does. Both halves in one write: doing either alone would leave a
|
|
351
|
+
// language's posts with nowhere to appear.
|
|
352
|
+
const additional = [
|
|
353
|
+
...state.additional.filter((tag) => tag !== next),
|
|
354
|
+
state.primary,
|
|
355
|
+
];
|
|
356
|
+
await writeLanguages(next, additional);
|
|
357
|
+
},
|
|
358
|
+
|
|
359
|
+
async addLanguage(language) {
|
|
360
|
+
const tag = requireValidLanguage(language);
|
|
361
|
+
const state = await readState();
|
|
362
|
+
if (tag === state.primary) {
|
|
363
|
+
throw new ConflictError(
|
|
364
|
+
"That is already this site's primary language.",
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
if (state.additional.includes(tag)) return;
|
|
368
|
+
|
|
369
|
+
await assertPrefixAvailable(tag);
|
|
370
|
+
await writeLanguages(state.primary, [...state.additional, tag]);
|
|
371
|
+
},
|
|
372
|
+
|
|
373
|
+
async removeLanguage(language) {
|
|
374
|
+
const tag = requireValidLanguage(language);
|
|
375
|
+
const state = await readState();
|
|
376
|
+
if (tag === state.primary) {
|
|
377
|
+
throw new ConflictError(
|
|
378
|
+
"This is the primary language. Make another language primary first.",
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
if (!state.additional.includes(tag)) {
|
|
382
|
+
return { multilingualDisabled: false };
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Counted whether or not multilingual is currently on: the settings page
|
|
386
|
+
// stays reachable after it is switched off, and the posts are still there.
|
|
387
|
+
const postCount = await posts.countByLanguage(tag);
|
|
388
|
+
if (postCount > 0) {
|
|
389
|
+
throw new LanguageInUseError(
|
|
390
|
+
postCount === 1
|
|
391
|
+
? "One post is still written in this language. Change its language, or keep the language."
|
|
392
|
+
: `${postCount} posts are still written in this language. Change their language, or keep the language.`,
|
|
393
|
+
tag,
|
|
394
|
+
postCount,
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const remaining = state.additional.filter((entry) => entry !== tag);
|
|
399
|
+
await writeLanguages(state.primary, remaining);
|
|
400
|
+
|
|
401
|
+
// Nothing left to serve under a prefix, so the feature goes with it. The
|
|
402
|
+
// flag is only cleared when it was actually on — a removal made while
|
|
403
|
+
// multilingual is off has nothing to turn off, and saying so would be a
|
|
404
|
+
// lie the settings page then has to explain.
|
|
405
|
+
const multilingualDisabled = remaining.length === 0 && state.enabled;
|
|
406
|
+
if (multilingualDisabled) {
|
|
407
|
+
await settings.remove("MULTILINGUAL_ENABLED");
|
|
408
|
+
}
|
|
409
|
+
return { multilingualDisabled };
|
|
410
|
+
},
|
|
411
|
+
};
|
|
412
|
+
}
|
package/src/services/mcp.ts
CHANGED
package/src/services/media.ts
CHANGED
|
@@ -17,7 +17,11 @@ import {
|
|
|
17
17
|
lte,
|
|
18
18
|
} from "drizzle-orm";
|
|
19
19
|
import { generateKeyBetween } from "fractional-indexing";
|
|
20
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
batchQueryRows,
|
|
22
|
+
type Database,
|
|
23
|
+
supportsDrizzleTransaction,
|
|
24
|
+
} from "../db/index.js";
|
|
21
25
|
import type { DatabaseDialect } from "../db/dialect.js";
|
|
22
26
|
import {
|
|
23
27
|
sqliteSchemaBundle,
|
|
@@ -723,11 +727,15 @@ export function createMediaService(
|
|
|
723
727
|
const result = new Map<string, Media[]>();
|
|
724
728
|
if (postIds.length === 0) return result;
|
|
725
729
|
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
730
|
+
// A feed asks for every post it renders at once, which can be hundreds.
|
|
731
|
+
// Each post's attachments land in one chunk, so their order holds.
|
|
732
|
+
const rows = await batchQueryRows([...new Set(postIds)], (chunk) =>
|
|
733
|
+
db
|
|
734
|
+
.select()
|
|
735
|
+
.from(media)
|
|
736
|
+
.where(and(eq(media.siteId, siteId), inArray(media.postId, chunk)))
|
|
737
|
+
.orderBy(asc(media.position)),
|
|
738
|
+
);
|
|
731
739
|
|
|
732
740
|
for (const row of rows) {
|
|
733
741
|
const m = toMedia(row);
|