@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
|
@@ -15,12 +15,16 @@ import {
|
|
|
15
15
|
supportsDrizzleTransaction,
|
|
16
16
|
} from "../db/index.js";
|
|
17
17
|
import type { DatabaseDialect } from "../db/dialect.js";
|
|
18
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
buildRootActivityExpr,
|
|
20
|
+
rootActivityColumns,
|
|
21
|
+
} from "../db/thread-activity.js";
|
|
22
|
+
import { buildReaderVisibilityConditions } from "../db/post-visibility.js";
|
|
19
23
|
import {
|
|
20
24
|
sqliteSchemaBundle,
|
|
21
25
|
type DatabaseSchema,
|
|
22
26
|
} from "../db/schema-bundle.js";
|
|
23
|
-
import { createEntityId } from "../lib/ids.js";
|
|
27
|
+
import { createEntityId, isTypeId, ID_PREFIX } from "../lib/ids.js";
|
|
24
28
|
import { now } from "../lib/time.js";
|
|
25
29
|
import type {
|
|
26
30
|
Collection,
|
|
@@ -31,6 +35,7 @@ import type {
|
|
|
31
35
|
CollectionsDirectoryData,
|
|
32
36
|
CreateCollection,
|
|
33
37
|
CreateCollectionDirectoryEntry,
|
|
38
|
+
SmartCollectionDirectoryEntry,
|
|
34
39
|
UpdateCollection,
|
|
35
40
|
UpdateCollectionDirectoryEntry,
|
|
36
41
|
CollectionSortOrder,
|
|
@@ -46,6 +51,13 @@ import {
|
|
|
46
51
|
type PathService,
|
|
47
52
|
} from "./path.js";
|
|
48
53
|
import { getCollectionPagePath } from "../lib/collection-paths.js";
|
|
54
|
+
import { getSlugValidationIssue } from "../lib/slug-format.js";
|
|
55
|
+
import type { SmartCollectionService } from "./smart-collection.js";
|
|
56
|
+
import {
|
|
57
|
+
DIRECTORY_POSITION_RETRY_ATTEMPTS as POSITION_RETRY_ATTEMPTS,
|
|
58
|
+
getAppendDirectoryPosition as appendDirectoryPosition,
|
|
59
|
+
getLastDirectoryPosition as lastDirectoryPosition,
|
|
60
|
+
} from "./collection-directory-position.js";
|
|
49
61
|
import {
|
|
50
62
|
CreateCollectionDirectoryItemSchema,
|
|
51
63
|
CollectionDirectoryLabelSchema,
|
|
@@ -57,8 +69,6 @@ import {
|
|
|
57
69
|
parseValidated,
|
|
58
70
|
} from "../lib/schemas.js";
|
|
59
71
|
|
|
60
|
-
const POSITION_RETRY_ATTEMPTS = 5;
|
|
61
|
-
|
|
62
72
|
// Re-export shared constraint detection — see db/dialect.ts
|
|
63
73
|
import { isUniqueConstraintError } from "../db/dialect.js";
|
|
64
74
|
|
|
@@ -88,6 +98,21 @@ function parseCollectionSelectionSlugs(
|
|
|
88
98
|
return slugs.length > 0 ? slugs : null;
|
|
89
99
|
}
|
|
90
100
|
|
|
101
|
+
/**
|
|
102
|
+
* Who is looking at the collections directory.
|
|
103
|
+
*
|
|
104
|
+
* The directory lists every collection in every language view — that is site
|
|
105
|
+
* skeleton, and it does not depend on the reader. Its *numbers* do: they answer
|
|
106
|
+
* "how many threads will I see if I click this", so they carry the same
|
|
107
|
+
* visibility and language narrowing the collection page applies.
|
|
108
|
+
*/
|
|
109
|
+
export interface CollectionDirectoryViewer {
|
|
110
|
+
/** Signed-in authors see their private threads counted; readers do not. */
|
|
111
|
+
isAuthenticated: boolean;
|
|
112
|
+
/** Content language of the current view, when the site has more than one. */
|
|
113
|
+
lang?: string;
|
|
114
|
+
}
|
|
115
|
+
|
|
91
116
|
export interface CollectionService {
|
|
92
117
|
getById(id: string): Promise<Collection | null>;
|
|
93
118
|
getBySlug(slug: string): Promise<Collection | null>;
|
|
@@ -96,12 +121,25 @@ export interface CollectionService {
|
|
|
96
121
|
slugExpression: string,
|
|
97
122
|
): Promise<ResolvedCollectionSelection | null>;
|
|
98
123
|
list(): Promise<Collection[]>;
|
|
99
|
-
listDirectoryData(
|
|
124
|
+
listDirectoryData(
|
|
125
|
+
viewer: CollectionDirectoryViewer,
|
|
126
|
+
): Promise<CollectionsDirectoryData>;
|
|
100
127
|
/** List collections sorted by most recent Thread addition (for compose dialog) */
|
|
101
128
|
listByRecentActivity(): Promise<Collection[]>;
|
|
102
129
|
create(data: CreateCollection): Promise<Collection>;
|
|
103
130
|
update(id: string, data: UpdateCollection): Promise<Collection | null>;
|
|
104
131
|
delete(id: string): Promise<boolean>;
|
|
132
|
+
/**
|
|
133
|
+
* Whether a collection can take this address.
|
|
134
|
+
*
|
|
135
|
+
* A collection shares the root namespace with posts and smart collections,
|
|
136
|
+
* so the whole path registry answers this, not the collection table.
|
|
137
|
+
*
|
|
138
|
+
* @param slug - Address to test, already lowercased
|
|
139
|
+
* @param excludeId - Ignore the address this collection already holds
|
|
140
|
+
* @returns True when nothing else holds the address
|
|
141
|
+
*/
|
|
142
|
+
checkSlugAvailability(slug: string, excludeId?: string): Promise<boolean>;
|
|
105
143
|
/** List all collection directory items ordered by position */
|
|
106
144
|
listDirectoryItems(): Promise<CollectionDirectoryEntry[]>;
|
|
107
145
|
/** Create a collection directory item (collection, divider, or link) */
|
|
@@ -115,7 +153,14 @@ export interface CollectionService {
|
|
|
115
153
|
id: string,
|
|
116
154
|
data: UpdateCollectionDirectoryEntry,
|
|
117
155
|
): Promise<CollectionDirectoryEntry | null>;
|
|
118
|
-
/**
|
|
156
|
+
/**
|
|
157
|
+
* Move a collection directory row between two neighbors.
|
|
158
|
+
*
|
|
159
|
+
* Each id is a directory row id, or — for a collection or smart collection
|
|
160
|
+
* the directory renders without one — that entry's own id. Naming one of
|
|
161
|
+
* those writes rows for everything unplaced, in the order the directory
|
|
162
|
+
* already shows, and then moves within them.
|
|
163
|
+
*/
|
|
119
164
|
moveDirectoryItem(
|
|
120
165
|
id: string,
|
|
121
166
|
after: string | null,
|
|
@@ -210,12 +255,22 @@ export function createCollectionService(
|
|
|
210
255
|
paths: PathService | undefined,
|
|
211
256
|
databaseSchema: DatabaseSchema = sqliteSchemaBundle,
|
|
212
257
|
databaseDialect: DatabaseDialect = "sqlite",
|
|
258
|
+
/**
|
|
259
|
+
* Consulted before a collection is deleted, so a smart collection that
|
|
260
|
+
* filters by it is named rather than silently widened.
|
|
261
|
+
*
|
|
262
|
+
* Passed as a dependency rather than reached for, following the same shape
|
|
263
|
+
* `storage?: StorageDriver` uses for media cleanup — the route stays out of
|
|
264
|
+
* cross-service orchestration.
|
|
265
|
+
*/
|
|
266
|
+
smartCollections?: SmartCollectionService,
|
|
213
267
|
): CollectionService {
|
|
214
268
|
const resolvedPaths = paths ?? createPathService(db, siteId, databaseSchema);
|
|
215
269
|
const {
|
|
216
270
|
collections,
|
|
217
271
|
pathRegistry,
|
|
218
272
|
collectionDirectoryItems: directoryItemsTable,
|
|
273
|
+
smartCollections: smartCollectionsTable,
|
|
219
274
|
threadCollections,
|
|
220
275
|
posts,
|
|
221
276
|
navItems,
|
|
@@ -284,6 +339,7 @@ export function createCollectionService(
|
|
|
284
339
|
siteId: row.siteId,
|
|
285
340
|
type: row.type as CollectionDirectoryEntryType,
|
|
286
341
|
collectionId: row.collectionId,
|
|
342
|
+
smartCollectionId: row.smartCollectionId,
|
|
287
343
|
label: row.label,
|
|
288
344
|
url: row.url,
|
|
289
345
|
description: row.description,
|
|
@@ -347,13 +403,7 @@ export function createCollectionService(
|
|
|
347
403
|
}
|
|
348
404
|
|
|
349
405
|
async function getLastDirectoryPosition(): Promise<string | null> {
|
|
350
|
-
|
|
351
|
-
.select({ position: directoryItemsTable.position })
|
|
352
|
-
.from(directoryItemsTable)
|
|
353
|
-
.where(eq(directoryItemsTable.siteId, siteId))
|
|
354
|
-
.orderBy(sql`${directoryItemsTable.position} DESC`)
|
|
355
|
-
.limit(1);
|
|
356
|
-
return rows[0]?.position ?? null;
|
|
406
|
+
return lastDirectoryPosition(db, directoryItemsTable, siteId);
|
|
357
407
|
}
|
|
358
408
|
|
|
359
409
|
async function listOrderedDirectoryPositions(excludeId?: string) {
|
|
@@ -369,8 +419,7 @@ export function createCollectionService(
|
|
|
369
419
|
}
|
|
370
420
|
|
|
371
421
|
async function getAppendDirectoryPosition(): Promise<string> {
|
|
372
|
-
|
|
373
|
-
return generateKeyBetween(lastPos, null);
|
|
422
|
+
return appendDirectoryPosition(db, directoryItemsTable, siteId);
|
|
374
423
|
}
|
|
375
424
|
|
|
376
425
|
async function pathExists(path: string): Promise<boolean> {
|
|
@@ -422,6 +471,140 @@ export function createCollectionService(
|
|
|
422
471
|
return generateKeyBetween(rows.at(-1)?.position ?? null, null);
|
|
423
472
|
}
|
|
424
473
|
|
|
474
|
+
/**
|
|
475
|
+
* Write rows for the entries the directory only shows, in the order it shows
|
|
476
|
+
* them.
|
|
477
|
+
*
|
|
478
|
+
* A directory row is a position, not a membership: `buildDirectoryItems`
|
|
479
|
+
* appends every collection and smart collection that has none, so an unplaced
|
|
480
|
+
* entry is visible but has nothing to move and cannot be named as a
|
|
481
|
+
* neighbour. Materializing in exactly the render order is invisible — nothing
|
|
482
|
+
* on screen changes — and afterwards every row the author can see is a row
|
|
483
|
+
* the author can drag.
|
|
484
|
+
*
|
|
485
|
+
* @returns Directory row id, keyed by the collection or smart collection it places
|
|
486
|
+
*/
|
|
487
|
+
async function materializeDirectoryRows(): Promise<Map<string, string>> {
|
|
488
|
+
const [directoryRows, collectionRows, smartCollectionRows] =
|
|
489
|
+
await Promise.all([
|
|
490
|
+
db
|
|
491
|
+
.select({
|
|
492
|
+
id: directoryItemsTable.id,
|
|
493
|
+
collectionId: directoryItemsTable.collectionId,
|
|
494
|
+
smartCollectionId: directoryItemsTable.smartCollectionId,
|
|
495
|
+
position: directoryItemsTable.position,
|
|
496
|
+
})
|
|
497
|
+
.from(directoryItemsTable)
|
|
498
|
+
.where(eq(directoryItemsTable.siteId, siteId))
|
|
499
|
+
.orderBy(asc(directoryItemsTable.position)),
|
|
500
|
+
db
|
|
501
|
+
.select({ id: collections.id })
|
|
502
|
+
.from(collections)
|
|
503
|
+
.where(eq(collections.siteId, siteId))
|
|
504
|
+
.orderBy(asc(collections.createdAt)),
|
|
505
|
+
db
|
|
506
|
+
.select({ id: smartCollectionsTable.id })
|
|
507
|
+
.from(smartCollectionsTable)
|
|
508
|
+
.where(eq(smartCollectionsTable.siteId, siteId))
|
|
509
|
+
.orderBy(asc(smartCollectionsTable.createdAt)),
|
|
510
|
+
]);
|
|
511
|
+
|
|
512
|
+
const placed = new Map<string, string>();
|
|
513
|
+
let lastPosition: string | null = null;
|
|
514
|
+
for (const row of directoryRows) {
|
|
515
|
+
const entityId = row.collectionId ?? row.smartCollectionId;
|
|
516
|
+
if (entityId) placed.set(entityId, row.id);
|
|
517
|
+
lastPosition = row.position;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// Manual collections before smart ones, matching the two fallback loops in
|
|
521
|
+
// `buildDirectoryItems`. Read them in the other order and the first drag
|
|
522
|
+
// would reshuffle rows the author never touched.
|
|
523
|
+
const unplaced: { type: CollectionDirectoryEntryType; entityId: string }[] =
|
|
524
|
+
[
|
|
525
|
+
...collectionRows
|
|
526
|
+
.filter((row) => !placed.has(row.id))
|
|
527
|
+
.map((row) => ({ type: "collection" as const, entityId: row.id })),
|
|
528
|
+
...smartCollectionRows
|
|
529
|
+
.filter((row) => !placed.has(row.id))
|
|
530
|
+
.map((row) => ({
|
|
531
|
+
type: "smart_collection" as const,
|
|
532
|
+
entityId: row.id,
|
|
533
|
+
})),
|
|
534
|
+
];
|
|
535
|
+
|
|
536
|
+
const timestamp = now();
|
|
537
|
+
for (const entry of unplaced) {
|
|
538
|
+
for (let attempt = 0; attempt < POSITION_RETRY_ATTEMPTS; attempt += 1) {
|
|
539
|
+
const id = createEntityId("collectionDirectoryItem");
|
|
540
|
+
const position = generateKeyBetween(lastPosition, null);
|
|
541
|
+
try {
|
|
542
|
+
await db.insert(directoryItemsTable).values({
|
|
543
|
+
id,
|
|
544
|
+
siteId,
|
|
545
|
+
type: entry.type,
|
|
546
|
+
collectionId: entry.type === "collection" ? entry.entityId : null,
|
|
547
|
+
smartCollectionId:
|
|
548
|
+
entry.type === "smart_collection" ? entry.entityId : null,
|
|
549
|
+
label: null,
|
|
550
|
+
url: null,
|
|
551
|
+
position,
|
|
552
|
+
createdAt: timestamp,
|
|
553
|
+
updatedAt: timestamp,
|
|
554
|
+
});
|
|
555
|
+
placed.set(entry.entityId, id);
|
|
556
|
+
lastPosition = position;
|
|
557
|
+
break;
|
|
558
|
+
} catch (err) {
|
|
559
|
+
if (
|
|
560
|
+
!isUniqueConstraintError(err) ||
|
|
561
|
+
attempt === POSITION_RETRY_ATTEMPTS - 1
|
|
562
|
+
) {
|
|
563
|
+
throw err;
|
|
564
|
+
}
|
|
565
|
+
lastPosition = await getLastDirectoryPosition();
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
return placed;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
* Name the three rows a move talks about by their directory row id.
|
|
575
|
+
*
|
|
576
|
+
* The drag surface names an unplaced collection or smart collection by its
|
|
577
|
+
* own id, because that is all it has to render. Any such id here means the
|
|
578
|
+
* directory has to become explicit before a position can be computed.
|
|
579
|
+
*
|
|
580
|
+
* @returns Row ids, or `null` when the moved row no longer exists
|
|
581
|
+
*/
|
|
582
|
+
async function resolveDirectoryMoveRows(
|
|
583
|
+
id: string,
|
|
584
|
+
afterId: string | null,
|
|
585
|
+
beforeId: string | null,
|
|
586
|
+
): Promise<{
|
|
587
|
+
id: string;
|
|
588
|
+
after: string | null;
|
|
589
|
+
before: string | null;
|
|
590
|
+
} | null> {
|
|
591
|
+
const isRowId = (value: string | null) =>
|
|
592
|
+
value === null || isTypeId(value, ID_PREFIX.collectionDirectoryItem);
|
|
593
|
+
|
|
594
|
+
if (isRowId(id) && isRowId(afterId) && isRowId(beforeId)) {
|
|
595
|
+
return { id, after: afterId, before: beforeId };
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
const placed = await materializeDirectoryRows();
|
|
599
|
+
const toRowId = (value: string | null) =>
|
|
600
|
+
value === null || isRowId(value) ? value : (placed.get(value) ?? null);
|
|
601
|
+
|
|
602
|
+
const rowId = toRowId(id);
|
|
603
|
+
if (!rowId) return null;
|
|
604
|
+
|
|
605
|
+
return { id: rowId, after: toRowId(afterId), before: toRowId(beforeId) };
|
|
606
|
+
}
|
|
607
|
+
|
|
425
608
|
async function hydrateCollection(
|
|
426
609
|
row: typeof collections.$inferSelect | undefined,
|
|
427
610
|
): Promise<Collection | null> {
|
|
@@ -446,31 +629,38 @@ export function createCollectionService(
|
|
|
446
629
|
.filter((row): row is Collection => row !== null);
|
|
447
630
|
}
|
|
448
631
|
|
|
449
|
-
async function listDirectoryCollections(
|
|
450
|
-
|
|
451
|
-
> {
|
|
452
|
-
//
|
|
453
|
-
//
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
632
|
+
async function listDirectoryCollections(
|
|
633
|
+
viewer: CollectionDirectoryViewer,
|
|
634
|
+
): Promise<CollectionDirectoryCollection[]> {
|
|
635
|
+
// Joined on thread_id rather than id, so a Thread counts once no matter
|
|
636
|
+
// which of its posts matched — the same shape
|
|
637
|
+
// `posts.countCollectionThreadRootsForCollections` uses for the collection
|
|
638
|
+
// page. The two numbers are shown one click apart and have to agree.
|
|
639
|
+
const readerConditions = buildReaderVisibilityConditions(posts, siteId, {
|
|
640
|
+
status: "published",
|
|
641
|
+
excludePrivate: !viewer.isAuthenticated,
|
|
642
|
+
lang: viewer.lang,
|
|
458
643
|
});
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
644
|
+
|
|
645
|
+
const threadCount =
|
|
646
|
+
sql<number>`CAST(COUNT(DISTINCT ${posts.threadId}) AS INTEGER)`.as(
|
|
647
|
+
"thread_count",
|
|
648
|
+
);
|
|
649
|
+
// Rows here are Thread *members*, so the root's activity is reached
|
|
650
|
+
// through a correlated subquery; the outer MAX only collapses the group.
|
|
651
|
+
// A member whose root row is missing falls back to its own timestamps.
|
|
652
|
+
const recentActivityAt = sql<number | null>`MAX(
|
|
653
|
+
COALESCE(
|
|
654
|
+
(
|
|
655
|
+
SELECT ${buildRootActivityExpr(rootActivityColumns("root"))}
|
|
656
|
+
FROM post AS root
|
|
657
|
+
WHERE root.site_id = ${siteId}
|
|
658
|
+
AND root.id = ${posts.threadId}
|
|
659
|
+
),
|
|
660
|
+
${posts.publishedAt},
|
|
661
|
+
${posts.updatedAt}
|
|
472
662
|
)
|
|
473
|
-
`.as("recent_activity_at");
|
|
663
|
+
)`.as("recent_activity_at");
|
|
474
664
|
|
|
475
665
|
const rows = await db
|
|
476
666
|
.select({
|
|
@@ -486,11 +676,15 @@ export function createCollectionService(
|
|
|
486
676
|
eq(collections.id, threadCollections.collectionId),
|
|
487
677
|
),
|
|
488
678
|
)
|
|
679
|
+
// The reader's conditions belong in ON, not WHERE: a collection this
|
|
680
|
+
// reader can see nothing in stays listed and reads `0 threads`, which is
|
|
681
|
+
// what the directory promises — every collection in every view.
|
|
489
682
|
.leftJoin(
|
|
490
683
|
posts,
|
|
491
684
|
and(
|
|
492
685
|
eq(posts.siteId, threadCollections.siteId),
|
|
493
|
-
eq(
|
|
686
|
+
eq(posts.threadId, threadCollections.threadId),
|
|
687
|
+
...readerConditions,
|
|
494
688
|
),
|
|
495
689
|
)
|
|
496
690
|
.where(eq(collections.siteId, siteId))
|
|
@@ -517,14 +711,33 @@ export function createCollectionService(
|
|
|
517
711
|
.filter((row): row is CollectionDirectoryCollection => row !== null);
|
|
518
712
|
}
|
|
519
713
|
|
|
714
|
+
/**
|
|
715
|
+
* Order the directory: explicitly placed rows first, then everything else.
|
|
716
|
+
*
|
|
717
|
+
* A directory row is a *position*, not a membership. Both kinds of collection
|
|
718
|
+
* appear whether or not one exists — anything unplaced is appended here — so
|
|
719
|
+
* there is no such thing as a collection missing from `/collections`, and no
|
|
720
|
+
* "add to directory" picker to build. The rows exist only so a collection can
|
|
721
|
+
* be interleaved with dividers and links and dragged around.
|
|
722
|
+
*
|
|
723
|
+
* Creating either kind writes its row, so the appended tail is for rows from
|
|
724
|
+
* before that was true and for imports. An entry in it is rendered under its
|
|
725
|
+
* own id, because there is no row id to render; moving one places it first —
|
|
726
|
+
* see `materializeDirectoryRows`.
|
|
727
|
+
*/
|
|
520
728
|
function buildDirectoryItems(
|
|
521
729
|
directoryCollections: CollectionDirectoryCollection[],
|
|
730
|
+
directorySmartCollections: SmartCollectionDirectoryEntry[],
|
|
522
731
|
orderedDirectoryItems: CollectionDirectoryEntry[],
|
|
523
732
|
): CollectionDirectoryItem[] {
|
|
524
733
|
const collectionMap = new Map(
|
|
525
734
|
directoryCollections.map((collection) => [collection.id, collection]),
|
|
526
735
|
);
|
|
736
|
+
const smartCollectionMap = new Map(
|
|
737
|
+
directorySmartCollections.map((entry) => [entry.id, entry]),
|
|
738
|
+
);
|
|
527
739
|
const seenCollections = new Set<string>();
|
|
740
|
+
const seenSmartCollections = new Set<string>();
|
|
528
741
|
const items: CollectionDirectoryItem[] = [];
|
|
529
742
|
|
|
530
743
|
for (const item of orderedDirectoryItems) {
|
|
@@ -549,6 +762,21 @@ export function createCollectionService(
|
|
|
549
762
|
continue;
|
|
550
763
|
}
|
|
551
764
|
|
|
765
|
+
if (item.type === "smart_collection") {
|
|
766
|
+
const smartCollection = item.smartCollectionId
|
|
767
|
+
? smartCollectionMap.get(item.smartCollectionId)
|
|
768
|
+
: undefined;
|
|
769
|
+
if (!smartCollection) continue;
|
|
770
|
+
|
|
771
|
+
seenSmartCollections.add(smartCollection.id);
|
|
772
|
+
items.push({
|
|
773
|
+
id: item.id,
|
|
774
|
+
type: "smart_collection",
|
|
775
|
+
smartCollection,
|
|
776
|
+
});
|
|
777
|
+
continue;
|
|
778
|
+
}
|
|
779
|
+
|
|
552
780
|
const collection = item.collectionId
|
|
553
781
|
? collectionMap.get(item.collectionId)
|
|
554
782
|
: undefined;
|
|
@@ -571,6 +799,15 @@ export function createCollectionService(
|
|
|
571
799
|
});
|
|
572
800
|
}
|
|
573
801
|
|
|
802
|
+
for (const smartCollection of directorySmartCollections) {
|
|
803
|
+
if (seenSmartCollections.has(smartCollection.id)) continue;
|
|
804
|
+
items.push({
|
|
805
|
+
id: smartCollection.id,
|
|
806
|
+
type: "smart_collection",
|
|
807
|
+
smartCollection,
|
|
808
|
+
});
|
|
809
|
+
}
|
|
810
|
+
|
|
574
811
|
return items;
|
|
575
812
|
}
|
|
576
813
|
|
|
@@ -626,15 +863,31 @@ export function createCollectionService(
|
|
|
626
863
|
return hydrateCollections(rows);
|
|
627
864
|
},
|
|
628
865
|
|
|
629
|
-
async listDirectoryData() {
|
|
630
|
-
const [
|
|
631
|
-
|
|
866
|
+
async listDirectoryData(viewer) {
|
|
867
|
+
const [
|
|
868
|
+
directoryCollections,
|
|
869
|
+
directorySmartCollections,
|
|
870
|
+
orderedDirectoryItems,
|
|
871
|
+
] = await Promise.all([
|
|
872
|
+
listDirectoryCollections(viewer),
|
|
873
|
+
// One pass for every smart collection, not one query each — see
|
|
874
|
+
// `posts.aggregateMany`. Both kinds of row are measured through the
|
|
875
|
+
// same reader predicate, so a signed-out visitor sees a directory
|
|
876
|
+
// whose counts and dates agree with the pages behind them.
|
|
877
|
+
smartCollections
|
|
878
|
+
? smartCollections.listDirectoryEntries(viewer)
|
|
879
|
+
: Promise.resolve([] as SmartCollectionDirectoryEntry[]),
|
|
632
880
|
this.listDirectoryItems(),
|
|
633
881
|
]);
|
|
634
882
|
|
|
635
883
|
return {
|
|
636
884
|
collections: directoryCollections,
|
|
637
|
-
|
|
885
|
+
smartCollections: directorySmartCollections,
|
|
886
|
+
items: buildDirectoryItems(
|
|
887
|
+
directoryCollections,
|
|
888
|
+
directorySmartCollections,
|
|
889
|
+
orderedDirectoryItems,
|
|
890
|
+
),
|
|
638
891
|
directoryItems: orderedDirectoryItems,
|
|
639
892
|
};
|
|
640
893
|
},
|
|
@@ -839,6 +1092,13 @@ export function createCollectionService(
|
|
|
839
1092
|
},
|
|
840
1093
|
|
|
841
1094
|
async delete(id) {
|
|
1095
|
+
// Refused, not repaired: clearing the condition would quietly widen a
|
|
1096
|
+
// smart collection the author never edited. This is the only place that
|
|
1097
|
+
// refusal lives — `smart_collection.collection_id` carries no foreign key,
|
|
1098
|
+
// precisely so the answer can name the smart collections in the way
|
|
1099
|
+
// instead of failing a constraint.
|
|
1100
|
+
await smartCollections?.assertCollectionUnused(id);
|
|
1101
|
+
|
|
842
1102
|
await db
|
|
843
1103
|
.delete(threadCollections)
|
|
844
1104
|
.where(
|
|
@@ -865,6 +1125,24 @@ export function createCollectionService(
|
|
|
865
1125
|
return result.length > 0;
|
|
866
1126
|
},
|
|
867
1127
|
|
|
1128
|
+
async checkSlugAvailability(slug, excludeId) {
|
|
1129
|
+
const issue = getSlugValidationIssue(slug);
|
|
1130
|
+
if (issue === "invalid") {
|
|
1131
|
+
throw new ValidationError("Slug contains invalid characters");
|
|
1132
|
+
}
|
|
1133
|
+
if (issue === "reserved") {
|
|
1134
|
+
throw new ValidationError("Slug is reserved");
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
const resolved = await resolvedPaths.resolve(toCollectionPath(slug));
|
|
1138
|
+
if (!resolved) return true;
|
|
1139
|
+
return Boolean(
|
|
1140
|
+
excludeId &&
|
|
1141
|
+
resolved.kind === "slug" &&
|
|
1142
|
+
resolved.collectionId === excludeId,
|
|
1143
|
+
);
|
|
1144
|
+
},
|
|
1145
|
+
|
|
868
1146
|
async listDirectoryItems() {
|
|
869
1147
|
const rows = await db
|
|
870
1148
|
.select()
|
|
@@ -1024,13 +1302,16 @@ export function createCollectionService(
|
|
|
1024
1302
|
},
|
|
1025
1303
|
|
|
1026
1304
|
async moveDirectoryItem(id, afterId, beforeId) {
|
|
1305
|
+
const rows = await resolveDirectoryMoveRows(id, afterId, beforeId);
|
|
1306
|
+
if (!rows) return null;
|
|
1307
|
+
|
|
1027
1308
|
const items = await db
|
|
1028
1309
|
.select()
|
|
1029
1310
|
.from(directoryItemsTable)
|
|
1030
1311
|
.where(
|
|
1031
1312
|
and(
|
|
1032
1313
|
eq(directoryItemsTable.siteId, siteId),
|
|
1033
|
-
eq(directoryItemsTable.id, id),
|
|
1314
|
+
eq(directoryItemsTable.id, rows.id),
|
|
1034
1315
|
),
|
|
1035
1316
|
)
|
|
1036
1317
|
.limit(1);
|
|
@@ -1042,13 +1323,17 @@ export function createCollectionService(
|
|
|
1042
1323
|
const result = await db
|
|
1043
1324
|
.update(directoryItemsTable)
|
|
1044
1325
|
.set({
|
|
1045
|
-
position: await getDirectoryMovePosition(
|
|
1326
|
+
position: await getDirectoryMovePosition(
|
|
1327
|
+
rows.id,
|
|
1328
|
+
rows.after,
|
|
1329
|
+
rows.before,
|
|
1330
|
+
),
|
|
1046
1331
|
updatedAt: timestamp,
|
|
1047
1332
|
})
|
|
1048
1333
|
.where(
|
|
1049
1334
|
and(
|
|
1050
1335
|
eq(directoryItemsTable.siteId, siteId),
|
|
1051
|
-
eq(directoryItemsTable.id, id),
|
|
1336
|
+
eq(directoryItemsTable.id, rows.id),
|
|
1052
1337
|
),
|
|
1053
1338
|
)
|
|
1054
1339
|
.returning();
|
|
@@ -15,10 +15,15 @@ import { isReservedPath } from "../lib/constants.js";
|
|
|
15
15
|
import { ConflictError, ValidationError } from "../lib/errors.js";
|
|
16
16
|
import { normalizePath } from "../lib/url.js";
|
|
17
17
|
import type { CustomUrl } from "../types.js";
|
|
18
|
+
import { readLanguageSettings } from "./language.js";
|
|
18
19
|
import { createPathService, type PathService } from "./path.js";
|
|
19
20
|
|
|
20
21
|
export interface CreateCustomUrl {
|
|
21
22
|
path: string;
|
|
23
|
+
/**
|
|
24
|
+
* `archive` is readable but no longer creatable — {@link CustomUrlService.create}
|
|
25
|
+
* refuses it. Stored ones predate smart collections and keep working.
|
|
26
|
+
*/
|
|
22
27
|
targetType: "post" | "collection" | "redirect" | "archive";
|
|
23
28
|
targetId?: string;
|
|
24
29
|
toPath?: string;
|
|
@@ -111,7 +116,12 @@ export function createCustomUrlService(
|
|
|
111
116
|
async create(data) {
|
|
112
117
|
const normalized = normalizeInputPath(data.path);
|
|
113
118
|
|
|
114
|
-
|
|
119
|
+
const { reservedPrefixes } = await readLanguageSettings(
|
|
120
|
+
db,
|
|
121
|
+
siteId,
|
|
122
|
+
databaseSchema,
|
|
123
|
+
);
|
|
124
|
+
if (isReservedPath(normalized, reservedPrefixes)) {
|
|
115
125
|
throw new ValidationError(
|
|
116
126
|
`Path "${normalized}" is reserved and cannot be used`,
|
|
117
127
|
);
|
|
@@ -128,26 +138,16 @@ export function createCustomUrlService(
|
|
|
128
138
|
}
|
|
129
139
|
|
|
130
140
|
if (data.targetType === "archive") {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
.from(pathRegistry)
|
|
142
|
-
.where(
|
|
143
|
-
and(
|
|
144
|
-
eq(pathRegistry.siteId, siteId),
|
|
145
|
-
eq(pathRegistry.id, record.id),
|
|
146
|
-
),
|
|
147
|
-
)
|
|
148
|
-
.limit(1);
|
|
149
|
-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- freshly inserted row exists
|
|
150
|
-
return toCustomUrl(row[0]!);
|
|
141
|
+
// Refused here, not only hidden in the settings form. Typing
|
|
142
|
+
// `format=note&title=none` into a text field was the problem smart
|
|
143
|
+
// collections were built to replace, and a UI that stops offering it
|
|
144
|
+
// while the API still accepts it has not stopped offering it.
|
|
145
|
+
//
|
|
146
|
+
// Reading, listing, and deleting these paths all stay: existing ones
|
|
147
|
+
// keep working indefinitely, and each carries an upgrade button.
|
|
148
|
+
throw new ValidationError(
|
|
149
|
+
"Archive addresses are no longer created here. Make a smart collection instead.",
|
|
150
|
+
);
|
|
151
151
|
}
|
|
152
152
|
|
|
153
153
|
if (data.targetType === "redirect") {
|
|
@@ -238,7 +238,12 @@ export function createCustomUrlService(
|
|
|
238
238
|
|
|
239
239
|
async isPathAvailable(path) {
|
|
240
240
|
const normalized = normalizeInputPath(path);
|
|
241
|
-
|
|
241
|
+
const { reservedPrefixes } = await readLanguageSettings(
|
|
242
|
+
db,
|
|
243
|
+
siteId,
|
|
244
|
+
databaseSchema,
|
|
245
|
+
);
|
|
246
|
+
if (isReservedPath(normalized, reservedPrefixes)) return false;
|
|
242
247
|
return resolvedPaths.isPathAvailable(normalized);
|
|
243
248
|
},
|
|
244
249
|
};
|