@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
|
@@ -211,6 +211,27 @@
|
|
|
211
211
|
"when": 1786008671529,
|
|
212
212
|
"tag": "0029_rare_hairball",
|
|
213
213
|
"breakpoints": true
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
"idx": 30,
|
|
217
|
+
"version": "7",
|
|
218
|
+
"when": 1786069149825,
|
|
219
|
+
"tag": "0030_abandoned_nighthawk",
|
|
220
|
+
"breakpoints": true
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
"idx": 31,
|
|
224
|
+
"version": "7",
|
|
225
|
+
"when": 1787282384249,
|
|
226
|
+
"tag": "0031_strong_ultimatum",
|
|
227
|
+
"breakpoints": true
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
"idx": 32,
|
|
231
|
+
"version": "7",
|
|
232
|
+
"when": 1787425868269,
|
|
233
|
+
"tag": "0032_furry_multiple_man",
|
|
234
|
+
"breakpoints": true
|
|
214
235
|
}
|
|
215
236
|
]
|
|
216
237
|
}
|
package/src/db/pg/schema.ts
CHANGED
|
@@ -19,26 +19,37 @@ import {
|
|
|
19
19
|
customType,
|
|
20
20
|
} from "drizzle-orm/pg-core";
|
|
21
21
|
import { sql } from "drizzle-orm";
|
|
22
|
-
import {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
22
|
+
import {
|
|
23
|
+
ARCHIVE_LAYOUTS,
|
|
24
|
+
COLLECTION_DIRECTORY_ENTRY_TYPES,
|
|
25
|
+
COLLECTION_SORT_ORDERS,
|
|
26
|
+
CONTENT_DISPOSITIONS,
|
|
27
|
+
EARLIEST_FILTERABLE_YEAR,
|
|
28
|
+
FORMATS,
|
|
29
|
+
GITHUB_APP_ACCOUNT_TYPES,
|
|
30
|
+
LATEST_FILTERABLE_YEAR,
|
|
31
|
+
NAV_ITEM_PLACEMENTS,
|
|
32
|
+
NAV_ITEM_TYPES,
|
|
33
|
+
PATH_KINDS,
|
|
34
|
+
PUBLIC_ARCHIVE_VISIBILITIES,
|
|
35
|
+
SITE_DOMAIN_KINDS,
|
|
36
|
+
SITE_MEMBER_ROLES,
|
|
37
|
+
SITE_STATUSES,
|
|
38
|
+
SMART_COLLECTION_SORT_ORDERS,
|
|
39
|
+
STATUSES,
|
|
40
|
+
SYSTEM_NAV_KEY_VALUES,
|
|
41
|
+
UPLOAD_SESSION_STATES,
|
|
42
|
+
VISIBILITIES,
|
|
43
|
+
} from "../../types/constants.js";
|
|
41
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Render a value list as the `IN (...)` body of a CHECK constraint.
|
|
47
|
+
*
|
|
48
|
+
* Every list it is handed comes from `types/constants.ts`. Declaring one
|
|
49
|
+
* locally instead is how a CHECK once shipped missing an enum value: the
|
|
50
|
+
* shared list grew, the schema's private copy did not, and the generated
|
|
51
|
+
* constraint rejected rows the application treats as valid.
|
|
52
|
+
*/
|
|
42
53
|
function sqlTextEnum(values: readonly string[]) {
|
|
43
54
|
return sql.raw(values.map((value) => `'${value}'`).join(", "));
|
|
44
55
|
}
|
|
@@ -179,6 +190,26 @@ export const posts = pgTable(
|
|
|
179
190
|
previewProvider: text("preview_provider"),
|
|
180
191
|
replyToId: text("reply_to_id"),
|
|
181
192
|
threadId: text("thread_id").notNull(),
|
|
193
|
+
/**
|
|
194
|
+
* BCP 47 content language in canonical form (`en`, `zh-Hans`).
|
|
195
|
+
*
|
|
196
|
+
* Uniform across a Thread: replies inherit the Root's value on creation and
|
|
197
|
+
* a language change rewrites every row in the Thread. Language filters can
|
|
198
|
+
* therefore be plain column predicates even on member-grained queries.
|
|
199
|
+
*
|
|
200
|
+
* NULL only exists before multilingual content is first enabled; enabling
|
|
201
|
+
* stamps every NULL row with the primary language.
|
|
202
|
+
*/
|
|
203
|
+
language: text("language"),
|
|
204
|
+
/**
|
|
205
|
+
* Shared translation-group key (TypeID, `tgr_` prefix). Rows sharing a value
|
|
206
|
+
* are translations of one another — one row per language, no direction.
|
|
207
|
+
*
|
|
208
|
+
* Only ever set on Thread Roots (`thread_id = id`); enforced in the post
|
|
209
|
+
* service so both dialects share one rule (see the SQLite schema for why a
|
|
210
|
+
* table CHECK is not an option there).
|
|
211
|
+
*/
|
|
212
|
+
translationGroupId: text("translation_group_id"),
|
|
182
213
|
/**
|
|
183
214
|
* The author added this reply without announcing it. Persisted so both
|
|
184
215
|
* activity timestamps below stay recomputable from the rows alone.
|
|
@@ -259,6 +290,12 @@ export const posts = pgTable(
|
|
|
259
290
|
.where(
|
|
260
291
|
sql`${table.status} = 'published' AND ${table.featuredAt} IS NOT NULL`,
|
|
261
292
|
),
|
|
293
|
+
// One Post per language inside a translation group. The `site_id` prefix
|
|
294
|
+
// matches the indexing convention here and lets the same index serve
|
|
295
|
+
// "list the members of this group" without a second index.
|
|
296
|
+
uniqueIndex("uq_post_site_translation_group_language")
|
|
297
|
+
.on(table.siteId, table.translationGroupId, table.language)
|
|
298
|
+
.where(sql`${table.translationGroupId} IS NOT NULL`),
|
|
262
299
|
index("idx_post_search_document").using("gin", table.searchDocument),
|
|
263
300
|
index("idx_post_search_text_trgm").using(
|
|
264
301
|
"gin",
|
|
@@ -459,6 +496,82 @@ export const collections = pgTable(
|
|
|
459
496
|
],
|
|
460
497
|
);
|
|
461
498
|
|
|
499
|
+
// =============================================================================
|
|
500
|
+
// Smart Collections
|
|
501
|
+
// =============================================================================
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* A collection whose members are decided by conditions rather than by tagging.
|
|
505
|
+
*
|
|
506
|
+
* See the SQLite schema for why this is its own table and what `NULL` means in
|
|
507
|
+
* a condition column. The two dialects must stay identical: Drizzle ignores a
|
|
508
|
+
* column it does not know about, so a field added on one side and forgotten on
|
|
509
|
+
* the other loses data silently.
|
|
510
|
+
*/
|
|
511
|
+
export const smartCollections = pgTable(
|
|
512
|
+
"smart_collection",
|
|
513
|
+
{
|
|
514
|
+
id: text("id").primaryKey(),
|
|
515
|
+
siteId: text("site_id")
|
|
516
|
+
.notNull()
|
|
517
|
+
.references(() => sites.id, { onDelete: "cascade" }),
|
|
518
|
+
title: text("title").notNull(),
|
|
519
|
+
description: text("description"),
|
|
520
|
+
|
|
521
|
+
// --- Conditions (see lib/filter-dimensions.ts) --------------------------
|
|
522
|
+
format: text("format", { enum: FORMATS }),
|
|
523
|
+
year: integer("year"),
|
|
524
|
+
// No foreign key, alone among this schema's id columns. See the SQLite
|
|
525
|
+
// schema for why the refusal is held in the service instead.
|
|
526
|
+
collectionId: text("collection_id"),
|
|
527
|
+
/** `any` | `none` | comma-joined MEDIA_KINDS. One folded vocabulary. */
|
|
528
|
+
media: text("media"),
|
|
529
|
+
hasTitle: boolean("has_title"),
|
|
530
|
+
hasReplies: boolean("has_replies"),
|
|
531
|
+
visibility: text("visibility", { enum: PUBLIC_ARCHIVE_VISIBILITIES }),
|
|
532
|
+
|
|
533
|
+
// --- Presentation (not conditions) --------------------------------------
|
|
534
|
+
sort: text("sort", { enum: SMART_COLLECTION_SORT_ORDERS })
|
|
535
|
+
.notNull()
|
|
536
|
+
.default("newest"),
|
|
537
|
+
/** NULL follows the site's `ARCHIVE_DEFAULT_LAYOUT`. */
|
|
538
|
+
layout: text("layout", { enum: ARCHIVE_LAYOUTS }),
|
|
539
|
+
|
|
540
|
+
createdAt: integer("created_at").notNull(),
|
|
541
|
+
updatedAt: integer("updated_at").notNull(),
|
|
542
|
+
},
|
|
543
|
+
(table) => [
|
|
544
|
+
check(
|
|
545
|
+
"chk_smart_collection_format",
|
|
546
|
+
sql`${table.format} IS NULL OR ${table.format} IN (${sqlTextEnum(FORMATS)})`,
|
|
547
|
+
),
|
|
548
|
+
check(
|
|
549
|
+
"chk_smart_collection_visibility",
|
|
550
|
+
sql`${table.visibility} IS NULL OR ${table.visibility} IN (${sqlTextEnum(PUBLIC_ARCHIVE_VISIBILITIES)})`,
|
|
551
|
+
),
|
|
552
|
+
check(
|
|
553
|
+
"chk_smart_collection_sort",
|
|
554
|
+
sql`${table.sort} IN (${sqlTextEnum(SMART_COLLECTION_SORT_ORDERS)})`,
|
|
555
|
+
),
|
|
556
|
+
check(
|
|
557
|
+
"chk_smart_collection_layout",
|
|
558
|
+
sql`${table.layout} IS NULL OR ${table.layout} IN (${sqlTextEnum(ARCHIVE_LAYOUTS)})`,
|
|
559
|
+
),
|
|
560
|
+
check(
|
|
561
|
+
"chk_smart_collection_year",
|
|
562
|
+
sql`${table.year} IS NULL OR ${table.year} BETWEEN ${sql.raw(String(EARLIEST_FILTERABLE_YEAR))} AND ${sql.raw(String(LATEST_FILTERABLE_YEAR))}`,
|
|
563
|
+
),
|
|
564
|
+
index("idx_smart_collection_site_created_at").on(
|
|
565
|
+
table.siteId,
|
|
566
|
+
table.createdAt,
|
|
567
|
+
),
|
|
568
|
+
index("idx_smart_collection_site_collection_id").on(
|
|
569
|
+
table.siteId,
|
|
570
|
+
table.collectionId,
|
|
571
|
+
),
|
|
572
|
+
],
|
|
573
|
+
);
|
|
574
|
+
|
|
462
575
|
// =============================================================================
|
|
463
576
|
// Path Registry (slug + alias + redirect)
|
|
464
577
|
// =============================================================================
|
|
@@ -480,6 +593,10 @@ export const pathRegistry = pgTable(
|
|
|
480
593
|
collectionId: text("collection_id").references(() => collections.id, {
|
|
481
594
|
onDelete: "cascade",
|
|
482
595
|
}),
|
|
596
|
+
smartCollectionId: text("smart_collection_id").references(
|
|
597
|
+
() => smartCollections.id,
|
|
598
|
+
{ onDelete: "cascade" },
|
|
599
|
+
),
|
|
483
600
|
redirectToPath: text("redirect_to_path"),
|
|
484
601
|
redirectType: integer("redirect_type"),
|
|
485
602
|
archiveQuery: text("archive_query"),
|
|
@@ -489,7 +606,7 @@ export const pathRegistry = pgTable(
|
|
|
489
606
|
(table) => [
|
|
490
607
|
check(
|
|
491
608
|
"chk_path_registry_kind",
|
|
492
|
-
sql`${table.kind} IN (
|
|
609
|
+
sql`${table.kind} IN (${sqlTextEnum(PATH_KINDS)})`,
|
|
493
610
|
),
|
|
494
611
|
uniqueIndex("uq_path_registry_site_path").on(table.siteId, table.path),
|
|
495
612
|
uniqueIndex("uq_path_registry_site_post_slug")
|
|
@@ -498,18 +615,28 @@ export const pathRegistry = pgTable(
|
|
|
498
615
|
uniqueIndex("uq_path_registry_site_collection_slug")
|
|
499
616
|
.on(table.siteId, table.collectionId)
|
|
500
617
|
.where(sql`${table.kind} = 'slug' AND ${table.collectionId} IS NOT NULL`),
|
|
618
|
+
uniqueIndex("uq_path_registry_site_smart_collection_slug")
|
|
619
|
+
.on(table.siteId, table.smartCollectionId)
|
|
620
|
+
.where(
|
|
621
|
+
sql`${table.kind} = 'slug' AND ${table.smartCollectionId} IS NOT NULL`,
|
|
622
|
+
),
|
|
501
623
|
index("idx_path_registry_site_post_id").on(table.siteId, table.postId),
|
|
502
624
|
index("idx_path_registry_site_collection_id").on(
|
|
503
625
|
table.siteId,
|
|
504
626
|
table.collectionId,
|
|
505
627
|
),
|
|
628
|
+
index("idx_path_registry_site_smart_collection_id").on(
|
|
629
|
+
table.siteId,
|
|
630
|
+
table.smartCollectionId,
|
|
631
|
+
),
|
|
506
632
|
check(
|
|
507
633
|
"chk_path_registry_shape",
|
|
508
634
|
sql`(
|
|
509
635
|
${table.kind} IN ('slug', 'alias')
|
|
510
636
|
AND (
|
|
511
|
-
(${table.postId} IS NOT NULL AND ${table.collectionId} IS NULL)
|
|
512
|
-
OR (${table.postId} IS NULL AND ${table.collectionId} IS NOT NULL)
|
|
637
|
+
(${table.postId} IS NOT NULL AND ${table.collectionId} IS NULL AND ${table.smartCollectionId} IS NULL)
|
|
638
|
+
OR (${table.postId} IS NULL AND ${table.collectionId} IS NOT NULL AND ${table.smartCollectionId} IS NULL)
|
|
639
|
+
OR (${table.postId} IS NULL AND ${table.collectionId} IS NULL AND ${table.smartCollectionId} IS NOT NULL)
|
|
513
640
|
)
|
|
514
641
|
AND ${table.redirectToPath} IS NULL
|
|
515
642
|
AND ${table.redirectType} IS NULL
|
|
@@ -518,6 +645,7 @@ export const pathRegistry = pgTable(
|
|
|
518
645
|
${table.kind} = 'redirect'
|
|
519
646
|
AND ${table.postId} IS NULL
|
|
520
647
|
AND ${table.collectionId} IS NULL
|
|
648
|
+
AND ${table.smartCollectionId} IS NULL
|
|
521
649
|
AND ${table.redirectToPath} IS NOT NULL
|
|
522
650
|
AND ${table.redirectType} IN (301, 302)
|
|
523
651
|
AND ${table.archiveQuery} IS NULL
|
|
@@ -525,6 +653,7 @@ export const pathRegistry = pgTable(
|
|
|
525
653
|
${table.kind} = 'archive'
|
|
526
654
|
AND ${table.postId} IS NULL
|
|
527
655
|
AND ${table.collectionId} IS NULL
|
|
656
|
+
AND ${table.smartCollectionId} IS NULL
|
|
528
657
|
AND ${table.redirectToPath} IS NULL
|
|
529
658
|
AND ${table.redirectType} IS NULL
|
|
530
659
|
AND ${table.archiveQuery} IS NOT NULL
|
|
@@ -544,10 +673,16 @@ export const collectionDirectoryItems = pgTable(
|
|
|
544
673
|
siteId: text("site_id")
|
|
545
674
|
.notNull()
|
|
546
675
|
.references(() => sites.id, { onDelete: "cascade" }),
|
|
547
|
-
type: text("type", {
|
|
676
|
+
type: text("type", {
|
|
677
|
+
enum: COLLECTION_DIRECTORY_ENTRY_TYPES,
|
|
678
|
+
}).notNull(),
|
|
548
679
|
collectionId: text("collection_id").references(() => collections.id, {
|
|
549
680
|
onDelete: "cascade",
|
|
550
681
|
}),
|
|
682
|
+
smartCollectionId: text("smart_collection_id").references(
|
|
683
|
+
() => smartCollections.id,
|
|
684
|
+
{ onDelete: "cascade" },
|
|
685
|
+
),
|
|
551
686
|
label: text("label"),
|
|
552
687
|
url: text("url"),
|
|
553
688
|
description: text("description"),
|
|
@@ -558,12 +693,16 @@ export const collectionDirectoryItems = pgTable(
|
|
|
558
693
|
(table) => [
|
|
559
694
|
check(
|
|
560
695
|
"chk_collection_directory_item_type",
|
|
561
|
-
sql`${table.type} IN (
|
|
696
|
+
sql`${table.type} IN (${sqlTextEnum(COLLECTION_DIRECTORY_ENTRY_TYPES)})`,
|
|
562
697
|
),
|
|
563
698
|
index("idx_collection_directory_item_site_collection_id").on(
|
|
564
699
|
table.siteId,
|
|
565
700
|
table.collectionId,
|
|
566
701
|
),
|
|
702
|
+
index("idx_collection_directory_item_site_smart_collection_id").on(
|
|
703
|
+
table.siteId,
|
|
704
|
+
table.smartCollectionId,
|
|
705
|
+
),
|
|
567
706
|
uniqueIndex("uq_collection_directory_item_site_position").on(
|
|
568
707
|
table.siteId,
|
|
569
708
|
table.position,
|
|
@@ -573,27 +712,41 @@ export const collectionDirectoryItems = pgTable(
|
|
|
573
712
|
.where(
|
|
574
713
|
sql`${table.type} = 'collection' AND ${table.collectionId} IS NOT NULL`,
|
|
575
714
|
),
|
|
715
|
+
uniqueIndex("uq_collection_directory_item_site_smart_collection_once")
|
|
716
|
+
.on(table.siteId, table.smartCollectionId)
|
|
717
|
+
.where(
|
|
718
|
+
sql`${table.type} = 'smart_collection' AND ${table.smartCollectionId} IS NOT NULL`,
|
|
719
|
+
),
|
|
576
720
|
check(
|
|
577
721
|
"chk_collection_directory_item_shape",
|
|
578
722
|
sql`(
|
|
579
723
|
${table.type} = 'collection'
|
|
580
724
|
AND ${table.collectionId} IS NOT NULL
|
|
725
|
+
AND ${table.smartCollectionId} IS NULL
|
|
726
|
+
AND ${table.label} IS NULL
|
|
727
|
+
AND ${table.url} IS NULL
|
|
728
|
+
) OR (
|
|
729
|
+
${table.type} = 'smart_collection'
|
|
730
|
+
AND ${table.collectionId} IS NULL
|
|
731
|
+
AND ${table.smartCollectionId} IS NOT NULL
|
|
581
732
|
AND ${table.label} IS NULL
|
|
582
733
|
AND ${table.url} IS NULL
|
|
583
734
|
) OR (
|
|
584
735
|
${table.type} = 'divider'
|
|
585
736
|
AND ${table.collectionId} IS NULL
|
|
737
|
+
AND ${table.smartCollectionId} IS NULL
|
|
586
738
|
AND ${table.url} IS NULL
|
|
587
739
|
) OR (
|
|
588
740
|
${table.type} = 'link'
|
|
589
741
|
AND ${table.collectionId} IS NULL
|
|
742
|
+
AND ${table.smartCollectionId} IS NULL
|
|
590
743
|
AND ${table.label} IS NOT NULL
|
|
591
744
|
AND ${table.url} IS NOT NULL
|
|
592
745
|
)`,
|
|
593
746
|
),
|
|
594
747
|
check(
|
|
595
748
|
"chk_collection_directory_item_label",
|
|
596
|
-
sql`${table.type}
|
|
749
|
+
sql`${table.type} NOT IN ('collection', 'smart_collection') OR ${table.label} IS NULL`,
|
|
597
750
|
),
|
|
598
751
|
check(
|
|
599
752
|
"chk_collection_directory_item_description",
|
|
@@ -659,6 +812,10 @@ export const navItems = pgTable(
|
|
|
659
812
|
collectionId: text("collection_id").references(() => collections.id, {
|
|
660
813
|
onDelete: "cascade",
|
|
661
814
|
}),
|
|
815
|
+
smartCollectionId: text("smart_collection_id").references(
|
|
816
|
+
() => smartCollections.id,
|
|
817
|
+
{ onDelete: "cascade" },
|
|
818
|
+
),
|
|
662
819
|
postId: text("post_id").references(() => posts.id, {
|
|
663
820
|
onDelete: "cascade",
|
|
664
821
|
}),
|
|
@@ -688,21 +845,31 @@ export const navItems = pgTable(
|
|
|
688
845
|
${table.type} = 'link'
|
|
689
846
|
AND ${table.systemKey} IS NULL
|
|
690
847
|
AND ${table.collectionId} IS NULL
|
|
848
|
+
AND ${table.smartCollectionId} IS NULL
|
|
691
849
|
AND ${table.postId} IS NULL
|
|
692
850
|
) OR (
|
|
693
851
|
${table.type} = 'system'
|
|
694
852
|
AND ${table.systemKey} IS NOT NULL
|
|
695
853
|
AND ${table.collectionId} IS NULL
|
|
854
|
+
AND ${table.smartCollectionId} IS NULL
|
|
696
855
|
AND ${table.postId} IS NULL
|
|
697
856
|
) OR (
|
|
698
857
|
${table.type} = 'collection'
|
|
699
858
|
AND ${table.systemKey} IS NULL
|
|
700
859
|
AND ${table.collectionId} IS NOT NULL
|
|
860
|
+
AND ${table.smartCollectionId} IS NULL
|
|
861
|
+
AND ${table.postId} IS NULL
|
|
862
|
+
) OR (
|
|
863
|
+
${table.type} = 'smart_collection'
|
|
864
|
+
AND ${table.systemKey} IS NULL
|
|
865
|
+
AND ${table.collectionId} IS NULL
|
|
866
|
+
AND ${table.smartCollectionId} IS NOT NULL
|
|
701
867
|
AND ${table.postId} IS NULL
|
|
702
868
|
) OR (
|
|
703
869
|
${table.type} = 'page'
|
|
704
870
|
AND ${table.systemKey} IS NULL
|
|
705
871
|
AND ${table.collectionId} IS NULL
|
|
872
|
+
AND ${table.smartCollectionId} IS NULL
|
|
706
873
|
AND ${table.postId} IS NOT NULL
|
|
707
874
|
)`,
|
|
708
875
|
),
|
|
@@ -713,6 +880,9 @@ export const navItems = pgTable(
|
|
|
713
880
|
uniqueIndex("uq_nav_item_site_collection_id")
|
|
714
881
|
.on(table.siteId, table.collectionId)
|
|
715
882
|
.where(sql`${table.collectionId} IS NOT NULL`),
|
|
883
|
+
uniqueIndex("uq_nav_item_site_smart_collection_id")
|
|
884
|
+
.on(table.siteId, table.smartCollectionId)
|
|
885
|
+
.where(sql`${table.smartCollectionId} IS NOT NULL`),
|
|
716
886
|
uniqueIndex("uq_nav_item_site_post_id")
|
|
717
887
|
.on(table.siteId, table.postId)
|
|
718
888
|
.where(sql`${table.postId} IS NOT NULL`),
|
|
@@ -860,8 +1030,6 @@ export const verification = pgTable("verification", {
|
|
|
860
1030
|
// GitHub App Installations (junction table: installation ↔ site, many-to-many)
|
|
861
1031
|
// ---------------------------------------------------------------------------
|
|
862
1032
|
|
|
863
|
-
const GITHUB_APP_ACCOUNT_TYPES = ["User", "Organization"] as const;
|
|
864
|
-
|
|
865
1033
|
export const githubAppInstallation = pgTable(
|
|
866
1034
|
"github_app_installation",
|
|
867
1035
|
{
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Who may see a post — one definition, shared by every surface that counts or
|
|
3
|
+
* lists them.
|
|
4
|
+
*
|
|
5
|
+
* A reply carries no visibility of its own: it inherits the Thread root's, so
|
|
6
|
+
* hiding a root hides the whole thread. Every query that filters by visibility
|
|
7
|
+
* has to resolve that inheritance the same way, and a query that forgets is not
|
|
8
|
+
* loud about it — it returns *more* rows, which looks like working code.
|
|
9
|
+
*
|
|
10
|
+
* That is how the collections directory came to count drafts and private
|
|
11
|
+
* threads into the number it showed a signed-out reader while the collection
|
|
12
|
+
* page, one click away, counted them out. Subtracting one number from the other
|
|
13
|
+
* told a stranger how much unpublished material a collection held.
|
|
14
|
+
*
|
|
15
|
+
* The rule these helpers encode: **the same reader gets the same set on every
|
|
16
|
+
* surface.** A page and the directory entry that links to it must apply this
|
|
17
|
+
* from one place, not two.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { sql, type SQL, type SQLWrapper } from "drizzle-orm";
|
|
21
|
+
import type { Status, Visibility } from "../types/constants.js";
|
|
22
|
+
|
|
23
|
+
/** The `post` columns the visibility rules read, in either dialect's bundle. */
|
|
24
|
+
export interface PostVisibilityColumns {
|
|
25
|
+
status: SQLWrapper;
|
|
26
|
+
visibility: SQLWrapper;
|
|
27
|
+
threadId: SQLWrapper;
|
|
28
|
+
language: SQLWrapper;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* A post's visibility after Thread-root inheritance.
|
|
33
|
+
*
|
|
34
|
+
* Replies store `NULL` and answer with their root's value, so this is the
|
|
35
|
+
* expression to compare against — never the raw column.
|
|
36
|
+
*
|
|
37
|
+
* @param post - The post table's `visibility` and `threadId` columns
|
|
38
|
+
* @param siteId - Site the query is scoped to
|
|
39
|
+
* @returns COALESCE expression yielding a {@link Visibility} value
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* ```ts
|
|
43
|
+
* const visibility = buildEffectiveVisibilityExpr(posts, siteId);
|
|
44
|
+
* conditions.push(sql`${visibility} != 'private'`);
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
export function buildEffectiveVisibilityExpr(
|
|
48
|
+
post: Pick<PostVisibilityColumns, "visibility" | "threadId">,
|
|
49
|
+
siteId: string,
|
|
50
|
+
): SQL<string> {
|
|
51
|
+
return sql<string>`coalesce(
|
|
52
|
+
${post.visibility},
|
|
53
|
+
(SELECT root.visibility FROM post AS root WHERE root.id = ${post.threadId} AND root.site_id = ${siteId})
|
|
54
|
+
)`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** What a given reader is allowed to see, in the shape every caller passes. */
|
|
58
|
+
export interface ReaderVisibilityOptions {
|
|
59
|
+
/** Usually `"published"`. Omitted only where drafts are the subject. */
|
|
60
|
+
status?: Status;
|
|
61
|
+
/** Narrow to exactly one visibility, as the archive's filter bar does. */
|
|
62
|
+
visibility?: Visibility;
|
|
63
|
+
/** True for a signed-out reader. */
|
|
64
|
+
excludePrivate?: boolean;
|
|
65
|
+
/** True where `latest_hidden` posts are held back, as on Latest. */
|
|
66
|
+
excludeLatestHidden?: boolean;
|
|
67
|
+
/** Restrict to one content language, for a per-language view. */
|
|
68
|
+
lang?: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The WHERE clauses that narrow posts to what one reader may see.
|
|
73
|
+
*
|
|
74
|
+
* Returns clauses only for the options that are set, so it composes with
|
|
75
|
+
* whatever else a query filters on. Safe in a LEFT JOIN's `ON` clause, which is
|
|
76
|
+
* how a directory keeps listing a collection whose posts this reader cannot
|
|
77
|
+
* see, showing it as empty rather than dropping the row.
|
|
78
|
+
*
|
|
79
|
+
* @param post - The post table's columns
|
|
80
|
+
* @param siteId - Site the query is scoped to
|
|
81
|
+
* @param options - What this reader may see
|
|
82
|
+
* @returns Conditions to spread into `and(...)`
|
|
83
|
+
*
|
|
84
|
+
* @example
|
|
85
|
+
* ```ts
|
|
86
|
+
* const conditions = buildReaderVisibilityConditions(posts, siteId, {
|
|
87
|
+
* status: "published",
|
|
88
|
+
* excludePrivate: !isAuthenticated,
|
|
89
|
+
* });
|
|
90
|
+
* ```
|
|
91
|
+
*/
|
|
92
|
+
export function buildReaderVisibilityConditions(
|
|
93
|
+
post: PostVisibilityColumns,
|
|
94
|
+
siteId: string,
|
|
95
|
+
options: ReaderVisibilityOptions = {},
|
|
96
|
+
): SQL[] {
|
|
97
|
+
const effectiveVisibility = buildEffectiveVisibilityExpr(post, siteId);
|
|
98
|
+
const conditions: SQL[] = [];
|
|
99
|
+
|
|
100
|
+
if (options.status) {
|
|
101
|
+
conditions.push(sql`${post.status} = ${options.status}`);
|
|
102
|
+
}
|
|
103
|
+
if (options.visibility !== undefined) {
|
|
104
|
+
conditions.push(sql`${effectiveVisibility} = ${options.visibility}`);
|
|
105
|
+
}
|
|
106
|
+
if (options.excludePrivate) {
|
|
107
|
+
conditions.push(sql`${effectiveVisibility} != 'private'`);
|
|
108
|
+
}
|
|
109
|
+
if (options.excludeLatestHidden) {
|
|
110
|
+
conditions.push(sql`${effectiveVisibility} != 'latest_hidden'`);
|
|
111
|
+
}
|
|
112
|
+
if (options.lang) {
|
|
113
|
+
conditions.push(sql`${post.language} = ${options.lang}`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return conditions;
|
|
117
|
+
}
|