@jant/core 0.6.15 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -11
- package/bin/commands/assets/upload.js +192 -15
- package/bin/commands/import-site.js +67 -4
- package/bin/commands/reset-password.js +21 -8
- package/bin/commands/site/snapshot/export.js +1 -1
- package/bin/commands/site/snapshot/import.js +1 -1
- package/bin/lib/d1-query.js +5 -16
- package/bin/lib/node-env.js +40 -4
- package/bin/lib/site-selection.js +85 -29
- package/bin/lib/wrangler-cli.js +28 -2
- package/bin/lib/wrangler-json.js +35 -0
- package/dist/{app-C-oi_t8W.js → app-K_Aa1MMn.js} +52623 -13193
- package/dist/client/.vite/manifest.json +255 -90
- package/dist/client/_assets/chunks/confirm-D7GrwsCN.js +45 -0
- package/dist/client/_assets/chunks/create-editor-CD3FhrOB.js +257 -0
- package/dist/client/_assets/chunks/{native-DpcrFAPh.js → emoji-mart-C4RpRKcU.js} +716 -1
- package/dist/client/_assets/chunks/footnote-rail-CmeoVtnU.js +1 -0
- package/dist/client/_assets/chunks/heic-to-CuP2Qlsw.js +1 -0
- package/dist/client/_assets/chunks/jant-locale-picker-DL6mIOT5.js +69 -0
- package/dist/client/_assets/chunks/lit-DW8VmJAT.js +2 -0
- package/dist/client/_assets/chunks/mediabunny-DRaCQ1r-.js +31 -0
- package/dist/client/_assets/chunks/rolldown-runtime-hePW80VL.js +1 -0
- package/dist/client/_assets/chunks/settings-paths-BcdySNx6.js +84 -0
- package/dist/client/_assets/chunks/site-header-nav-DzXsFk05.js +1 -0
- package/dist/client/_assets/chunks/slug-format-CRXO1AL5.js +1 -0
- package/dist/client/_assets/chunks/slugify-373citbs.js +1 -0
- package/dist/client/_assets/chunks/sortable-list-CgaL2jCs.js +2 -0
- package/dist/client/_assets/chunks/types-D5iF3H1a.js +1 -0
- package/dist/client/_assets/chunks/unsafe-html-B5ZEXaxg.js +1 -0
- package/dist/client/_assets/chunks/unsafe-svg-0QCkP0vZ.js +2 -0
- package/dist/client/_assets/client-GiYENVw8.css +2 -0
- package/dist/client/_assets/client-VnFxJN7G.js +295 -0
- package/dist/client/_assets/client-auth-k8gJ6QJj.js +828 -0
- package/dist/client/_assets/client-author-DQ8R-8KR.css +2 -0
- package/dist/client/_assets/client-compose-B_kDtWkW.js +2004 -0
- package/dist/client/_assets/client-manage-M90aSTOk.js +2170 -0
- package/dist/client/_assets/client-settings-DfYs9n1F.js +1112 -0
- package/dist/{export-CsFx6F_M.js → github-sync-BPAvT999.js} +2343 -110
- package/dist/index.js +2 -5
- package/dist/node.js +5 -7
- package/package.json +3 -3
- package/src/__tests__/auth-session-freshness.test.ts +57 -0
- package/src/__tests__/basecoat-variants.test.ts +121 -0
- package/src/__tests__/bin/assets-upload.test.ts +68 -0
- package/src/__tests__/export-hugo-build.test.ts +260 -0
- package/src/__tests__/export-service.test.ts +231 -0
- package/src/__tests__/helpers/component-css.ts +91 -0
- package/src/__tests__/helpers/db.ts +33 -0
- package/src/__tests__/helpers/export-fixtures.ts +3 -0
- package/src/__tests__/mise-config.test.ts +22 -0
- package/src/__tests__/stylesheet-audience.test.ts +159 -0
- package/src/__tests__/stylesheet-breakpoints.test.ts +183 -0
- package/src/app.tsx +71 -16
- package/src/auth.ts +18 -1
- package/src/client/__tests__/collection-page-actions.test.ts +51 -2
- package/src/client/__tests__/compose-bridge-image-upload.test.ts +196 -0
- package/src/client/__tests__/compose-bridge-video-poster.test.ts +159 -0
- package/src/client/__tests__/compose-bridge.test.ts +524 -2
- package/src/client/__tests__/compose-launch.test.ts +62 -1
- package/src/client/__tests__/compose-shortcuts.test.ts +57 -8
- package/src/client/__tests__/compose-triggers.test.ts +90 -0
- package/src/client/__tests__/copy-field.test.ts +162 -0
- package/src/client/__tests__/custom-url-menu.test.ts +127 -6
- package/src/client/__tests__/image-processor.test.ts +324 -2
- package/src/client/__tests__/lazy-entries.test.ts +92 -0
- package/src/client/__tests__/media-scroll-hint.test.ts +155 -0
- package/src/client/__tests__/runtime-paths.test.ts +84 -0
- package/src/client/__tests__/smart-collection-page-actions.test.ts +158 -0
- package/src/client/__tests__/toast.test.ts +88 -1
- package/src/client/__tests__/video-processor-probe.test.ts +175 -0
- package/src/client/__tests__/video-processor.test.ts +181 -0
- package/src/client/audio-processor.ts +24 -15
- package/src/client/audio-waveform.ts +49 -0
- package/src/client/collection-dialog-host.ts +72 -0
- package/src/client/collection-navigation.ts +51 -16
- package/src/client/collection-page-actions.ts +26 -0
- package/src/client/components/__tests__/compose-format-convert.test.ts +0 -19
- package/src/client/components/__tests__/jant-collection-dialog.test.ts +329 -0
- package/src/client/components/__tests__/jant-collection-directory.test.ts +274 -10
- package/src/client/components/__tests__/jant-collection-form.test.ts +59 -178
- package/src/client/components/__tests__/jant-command-palette.test.ts +79 -0
- package/src/client/components/__tests__/jant-compose-dialog-draft-storage.test.ts +340 -0
- package/src/client/components/__tests__/jant-compose-dialog.test.ts +3283 -389
- package/src/client/components/__tests__/jant-compose-editor.test.ts +581 -86
- package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +34 -89
- package/src/client/components/__tests__/jant-locale-picker.test.ts +207 -0
- package/src/client/components/__tests__/jant-nav-manager.test.ts +234 -15
- package/src/client/components/__tests__/jant-post-menu.test.ts +413 -2
- package/src/client/components/__tests__/jant-post-picker.test.ts +197 -0
- package/src/client/components/__tests__/jant-settings-avatar.test.ts +15 -12
- package/src/client/components/__tests__/jant-settings-general.test.ts +541 -172
- package/src/client/components/__tests__/jant-settings-language.test.ts +851 -0
- package/src/client/components/__tests__/jant-smart-collection-dialog.test.ts +665 -0
- package/src/client/components/collection-dialog-types.ts +49 -0
- package/src/client/components/collection-manager-types.ts +32 -1
- package/src/client/components/collection-types.ts +1 -15
- package/src/client/components/compose-format-convert.ts +5 -1
- package/src/client/components/compose-types.ts +121 -3
- package/src/client/components/jant-collection-dialog.ts +722 -0
- package/src/client/components/jant-collection-directory.ts +567 -182
- package/src/client/components/jant-collection-form.ts +53 -232
- package/src/client/components/jant-command-palette.ts +106 -49
- package/src/client/components/jant-compose-dialog.ts +2757 -1194
- package/src/client/components/jant-compose-editor.ts +866 -397
- package/src/client/components/jant-compose-fullscreen.ts +53 -47
- package/src/client/components/jant-config-editor.ts +65 -53
- package/src/client/components/jant-confirm-dialog.ts +13 -11
- package/src/client/components/jant-locale-picker.ts +418 -0
- package/src/client/components/jant-media-lightbox.ts +146 -136
- package/src/client/components/jant-nav-manager.ts +690 -366
- package/src/client/components/jant-post-menu.ts +1142 -268
- package/src/client/components/jant-post-picker.ts +324 -0
- package/src/client/components/jant-repo-picker.ts +138 -106
- package/src/client/components/jant-settings-avatar.ts +31 -27
- package/src/client/components/jant-settings-general.ts +617 -543
- package/src/client/components/jant-settings-language.ts +1087 -0
- package/src/client/components/jant-smart-collection-dialog.ts +1021 -0
- package/src/client/components/jant-text-preview.ts +94 -86
- package/src/client/components/nav-manager-types.ts +62 -1
- package/src/client/components/settings-types.ts +14 -29
- package/src/client/components/smart-collection-conditions.ts +285 -0
- package/src/client/components/smart-collection-dialog-types.ts +68 -0
- package/src/client/compose-bridge.ts +336 -242
- package/src/client/compose-launch.ts +89 -2
- package/src/client/compose-shortcuts.ts +9 -7
- package/src/client/compose-triggers.ts +50 -0
- package/src/client/copy-field.ts +108 -0
- package/src/client/custom-url-menu.ts +60 -4
- package/src/client/icons.ts +14 -0
- package/src/client/image-plan.ts +165 -0
- package/src/client/image-processor.ts +195 -117
- package/src/client/image-worker-client.ts +45 -0
- package/src/client/image-worker.ts +135 -0
- package/src/client/lazy-entries.ts +95 -0
- package/src/client/lazy-slugify.ts +2 -2
- package/src/client/media-scroll-hint.ts +43 -2
- package/src/client/mediabunny.ts +29 -0
- package/src/client/post-picker.ts +41 -0
- package/src/client/post-refresh.ts +135 -0
- package/src/client/runtime-paths.ts +74 -1
- package/src/client/settings-bridge.ts +4 -10
- package/src/client/site-header-nav.js +18 -6
- package/src/client/smart-collection-dialog-host.ts +116 -0
- package/src/client/smart-collection-page-actions.ts +208 -0
- package/src/client/tiptap/__tests__/bubble-menu.test.ts +110 -0
- package/src/client/tiptap/__tests__/floating-position.test.ts +136 -1
- package/src/client/tiptap/__tests__/inline-image-upload.test.ts +37 -0
- package/src/client/tiptap/__tests__/link-input-rules.test.ts +121 -0
- package/src/client/tiptap/bubble-menu.ts +47 -16
- package/src/client/tiptap/floating-position.ts +216 -2
- package/src/client/tiptap/inline-image-upload.ts +7 -3
- package/src/client/tiptap/link-input-rules.ts +4 -4
- package/src/client/tiptap/link-toolbar.ts +22 -9
- package/src/client/toast.ts +143 -38
- package/src/client/types/vite-worker.d.ts +12 -0
- package/src/client/upload-with-metadata.ts +3 -13
- package/src/client/video-processor.ts +351 -107
- package/src/client-auth.ts +14 -31
- package/src/client-compose.ts +19 -0
- package/src/client-manage.ts +13 -0
- package/src/client-settings.ts +14 -0
- package/src/client-site.ts +10 -3
- package/src/client.ts +4 -0
- package/src/db/__tests__/backfill-nav-label-mirrors.test.ts +149 -0
- package/src/db/__tests__/backfill-thread-activity.test.ts +234 -0
- package/src/db/__tests__/schema-enum-sources.test.ts +33 -0
- package/src/db/__tests__/smart-collection-placement.test.ts +402 -0
- package/src/db/__tests__/thread-activity.test.ts +113 -0
- package/src/db/backfills/0005_split_thread_activity_from_quiet_replies.sql +68 -0
- package/src/db/backfills/0006_clear_mirrored_page_nav_labels.sql +31 -0
- package/src/db/migrations/0031_many_ego.sql +3 -0
- package/src/db/migrations/0032_married_tomas.sql +3 -0
- package/src/db/migrations/0033_busy_johnny_storm.sql +198 -0
- package/src/db/migrations/0034_cheerful_rumiko_fujikawa.sql +35 -0
- package/src/db/migrations/meta/0031_snapshot.json +2562 -0
- package/src/db/migrations/meta/0032_snapshot.json +2582 -0
- package/src/db/migrations/meta/0033_snapshot.json +2816 -0
- package/src/db/migrations/meta/0034_snapshot.json +2816 -0
- package/src/db/migrations/meta/_journal.json +28 -0
- package/src/db/migrations/pg/0029_rare_hairball.sql +3 -0
- package/src/db/migrations/pg/0030_abandoned_nighthawk.sql +3 -0
- package/src/db/migrations/pg/0031_strong_ultimatum.sql +128 -0
- package/src/db/migrations/pg/0032_furry_multiple_man.sql +6 -0
- package/src/db/migrations/pg/meta/0029_snapshot.json +3289 -0
- package/src/db/migrations/pg/meta/0030_snapshot.json +3329 -0
- package/src/db/migrations/pg/meta/0031_snapshot.json +3660 -0
- package/src/db/migrations/pg/meta/0032_snapshot.json +3660 -0
- package/src/db/migrations/pg/meta/_journal.json +28 -0
- package/src/db/pg/schema.ts +213 -27
- package/src/db/post-visibility.ts +117 -0
- package/src/db/schema.ts +242 -27
- package/src/db/thread-activity.ts +101 -0
- package/src/i18n/__tests__/detect.test.ts +26 -81
- package/src/i18n/__tests__/supported-locales.test.ts +83 -0
- package/src/i18n/context.tsx +12 -2
- package/src/i18n/detect.ts +7 -66
- package/src/i18n/locales/glossary.zh-Hans.yml +44 -4
- package/src/i18n/locales/glossary.zh-Hant.yml +44 -4
- package/src/i18n/locales/public/en.po +568 -168
- package/src/i18n/locales/public/en.ts +1 -1
- package/src/i18n/locales/public/zh-Hans.po +562 -162
- package/src/i18n/locales/public/zh-Hans.ts +1 -1
- package/src/i18n/locales/public/zh-Hant.po +562 -162
- package/src/i18n/locales/public/zh-Hant.ts +1 -1
- package/src/i18n/locales/settings/en.po +551 -69
- package/src/i18n/locales/settings/en.ts +1 -1
- package/src/i18n/locales/settings/zh-Hans.po +597 -115
- package/src/i18n/locales/settings/zh-Hans.ts +1 -1
- package/src/i18n/locales/settings/zh-Hant.po +610 -128
- package/src/i18n/locales/settings/zh-Hant.ts +1 -1
- package/src/i18n/locales.ts +94 -0
- package/src/i18n/middleware.ts +12 -2
- package/src/i18n/supported-locales.ts +80 -0
- package/src/lib/__tests__/api-settings.test.ts +11 -2
- package/src/lib/__tests__/asset-path.test.ts +92 -0
- package/src/lib/__tests__/deferred.test.ts +90 -0
- package/src/lib/__tests__/discover-ping.test.ts +89 -0
- package/src/lib/__tests__/discover.test.ts +410 -0
- package/src/lib/__tests__/feed-policy.test.ts +26 -0
- package/src/lib/__tests__/feed.test.ts +1691 -12
- package/src/lib/__tests__/filter-dimensions.test.ts +341 -0
- package/src/lib/__tests__/hosted-signin.test.ts +1 -1
- package/src/lib/__tests__/http-cache.test.ts +162 -0
- package/src/lib/__tests__/jant-docs-dev.test.ts +21 -0
- package/src/lib/__tests__/jant-docs.test.ts +23 -0
- package/src/lib/__tests__/lang-detect.test.ts +231 -0
- package/src/lib/__tests__/markdown.test.ts +16 -0
- package/src/lib/__tests__/media-helpers.test.ts +81 -0
- package/src/lib/__tests__/navigation.test.ts +135 -1
- package/src/lib/__tests__/resolve-config.test.ts +64 -3
- package/src/lib/__tests__/schemas.test.ts +42 -4
- package/src/lib/__tests__/site-unavailable-page.test.ts +25 -0
- package/src/lib/__tests__/slug.test.ts +88 -0
- package/src/lib/__tests__/slugify.test.ts +85 -0
- package/src/lib/__tests__/storage.test.ts +32 -0
- package/src/lib/__tests__/thread-fold.test.ts +113 -0
- package/src/lib/__tests__/translit.test.ts +87 -0
- package/src/lib/__tests__/upload-sideload.test.ts +14 -0
- package/src/lib/__tests__/url-fetch.test.ts +1 -2
- package/src/lib/__tests__/url.test.ts +93 -34
- package/src/lib/__tests__/view-language.test.ts +413 -0
- package/src/lib/__tests__/worker-response-cache.test.ts +90 -0
- package/src/lib/address.ts +57 -0
- package/src/lib/api-posts.ts +12 -0
- package/src/lib/asset-path.ts +98 -1
- package/src/lib/build-env.ts +16 -0
- package/src/lib/collection-groups.ts +5 -1
- package/src/lib/collection-paths.ts +3 -10
- package/src/lib/constants.ts +28 -3
- package/src/lib/copy-field.ts +36 -0
- package/src/lib/crypto.ts +40 -0
- package/src/lib/csp-builder.ts +2 -1
- package/src/lib/deferred.ts +79 -0
- package/src/lib/discover-ping.ts +141 -0
- package/src/lib/discover.ts +437 -0
- package/src/lib/env.ts +134 -0
- package/src/lib/errors.ts +33 -0
- package/src/lib/feed-policy.ts +236 -2
- package/src/lib/feed.ts +1021 -66
- package/src/lib/filter-dimensions.ts +1349 -0
- package/src/lib/github-sync-site-config.ts +3 -0
- package/src/lib/hosted-signin.ts +1 -1
- package/src/lib/http-cache.ts +148 -0
- package/src/lib/hugo-markdown.ts +21 -0
- package/src/lib/icons.ts +9 -2
- package/src/lib/ids.ts +3 -0
- package/src/lib/jant-docs.ts +42 -0
- package/src/lib/lang-detect.ts +308 -0
- package/src/lib/link-preview.ts +25 -0
- package/src/lib/markdown-manager.ts +6 -1
- package/src/lib/media-helpers.ts +13 -1
- package/src/lib/navigation.ts +78 -11
- package/src/lib/pagination.ts +28 -24
- package/src/lib/per-language-surfaces.ts +67 -0
- package/src/lib/post-display.ts +42 -9
- package/src/lib/post-meta.ts +19 -0
- package/src/lib/render.tsx +55 -7
- package/src/lib/resolve-config.ts +53 -5
- package/src/lib/schemas.ts +201 -10
- package/src/lib/site-header-fragment.tsx +2 -1
- package/src/lib/site-unavailable-page.ts +47 -0
- package/src/lib/slug.ts +51 -7
- package/src/lib/slugify.ts +113 -0
- package/src/lib/startup-config.ts +4 -6
- package/src/lib/storage.ts +7 -0
- package/src/lib/summary.ts +50 -0
- package/src/lib/thread-fold.ts +161 -0
- package/src/lib/time.ts +32 -0
- package/src/lib/timeline.ts +81 -10
- package/src/lib/tiptap-render.ts +5 -4
- package/src/lib/translit.ts +288 -0
- package/src/lib/upload.ts +16 -13
- package/src/lib/url.ts +188 -24
- package/src/lib/version.ts +10 -8
- package/src/lib/view-language.ts +436 -0
- package/src/lib/view.ts +79 -23
- package/src/lib/viewer-context.ts +62 -0
- package/src/middleware/__tests__/auth.test.ts +70 -2
- package/src/middleware/__tests__/cache-control.test.ts +71 -3
- package/src/middleware/__tests__/onboarding.test.ts +76 -5
- package/src/middleware/__tests__/secure-headers.test.ts +1 -1
- package/src/middleware/__tests__/session.test.ts +181 -33
- package/src/middleware/auth.ts +50 -31
- package/src/middleware/cache-control.ts +75 -10
- package/src/middleware/config.ts +6 -6
- package/src/middleware/error-handler.ts +12 -0
- package/src/middleware/onboarding.ts +32 -10
- package/src/middleware/secure-headers.ts +1 -1
- package/src/middleware/session.ts +96 -8
- package/src/node/__tests__/cli-migrate.test.ts +21 -19
- package/src/node/__tests__/cli-reset-password.test.ts +81 -8
- package/src/node/__tests__/cli-runtime-target.test.ts +48 -1
- package/src/node/__tests__/cli-site-selection.test.ts +128 -38
- package/src/node/__tests__/runtime.test.ts +37 -0
- package/src/node/index.ts +1 -0
- package/src/preset.css +76 -22
- package/src/routes/__tests__/feed-validator.test.ts +156 -0
- package/src/routes/api/__tests__/collections.test.ts +114 -0
- package/src/routes/api/__tests__/discover.test.ts +222 -0
- package/src/routes/api/__tests__/nav-items.test.ts +212 -2
- package/src/routes/api/__tests__/palette.test.ts +44 -0
- package/src/routes/api/__tests__/post-translations.test.ts +829 -0
- package/src/routes/api/__tests__/settings.test.ts +24 -3
- package/src/routes/api/__tests__/telegram.test.ts +6 -5
- package/src/routes/api/collections.ts +49 -6
- package/src/routes/api/custom-urls.ts +0 -1
- package/src/routes/api/discover.ts +95 -0
- package/src/routes/api/export.ts +3 -0
- package/src/routes/api/internal/__tests__/sites.test.ts +42 -2
- package/src/routes/api/nav-items.ts +55 -8
- package/src/routes/api/posts.ts +172 -9
- package/src/routes/api/public/__tests__/archive.test.ts +111 -0
- package/src/routes/api/public/__tests__/posts.test.ts +38 -0
- package/src/routes/api/public/archive.ts +77 -81
- package/src/routes/api/public/posts.ts +30 -6
- package/src/routes/api/smart-collections.ts +132 -0
- package/src/routes/api/telegram.ts +2 -40
- package/src/routes/api/uploads.ts +1 -2
- package/src/routes/auth/__tests__/setup-auth.test.ts +137 -0
- package/src/routes/auth/__tests__/setup-discover.test.ts +204 -0
- package/src/routes/auth/__tests__/setup-flow.test.ts +270 -0
- package/src/routes/auth/__tests__/setup-page.test.tsx +329 -0
- package/src/routes/auth/__tests__/setup-steps.test.ts +310 -0
- package/src/routes/auth/__tests__/setup.test.ts +278 -26
- package/src/routes/auth/__tests__/signin.test.ts +135 -0
- package/src/routes/auth/setup.tsx +934 -186
- package/src/routes/auth/signin.tsx +10 -8
- package/src/routes/compose.tsx +11 -2
- package/src/routes/dash/__tests__/custom-urls-archive.test.ts +44 -0
- package/src/routes/dash/__tests__/custom-urls-page.test.ts +49 -0
- package/src/routes/dash/__tests__/delete-account.test.ts +106 -0
- package/src/routes/dash/__tests__/settings-discover.test.ts +213 -0
- package/src/routes/dash/custom-urls.tsx +1 -37
- package/src/routes/dash/settings.tsx +502 -41
- package/src/routes/discover-announce.ts +96 -0
- package/src/routes/feed/__tests__/feed.test.ts +165 -2
- package/src/routes/feed/__tests__/sitemap.test.ts +68 -1
- package/src/routes/feed/feed.ts +69 -40
- package/src/routes/feed/sitemap.ts +95 -11
- package/src/routes/pages/__tests__/archive-indexing.test.ts +200 -0
- package/src/routes/pages/__tests__/archive-params.test.ts +657 -1
- package/src/routes/pages/__tests__/collection-routing.test.ts +16 -0
- package/src/routes/pages/__tests__/collections-feed-links.test.ts +72 -0
- package/src/routes/pages/__tests__/collections.test.ts +9 -3
- package/src/routes/pages/__tests__/custom-archive-url.test.ts +119 -0
- package/src/routes/pages/__tests__/featured.test.ts +6 -2
- package/src/routes/pages/__tests__/feed-autodiscovery.test.ts +148 -0
- package/src/routes/pages/__tests__/language-routing.test.ts +679 -0
- package/src/routes/pages/__tests__/post-preview-partial.test.ts +96 -0
- package/src/routes/pages/__tests__/preview.test.ts +3 -1
- package/src/routes/pages/__tests__/smart-collection.test.ts +298 -0
- package/src/routes/pages/__tests__/subscribe.test.ts +248 -0
- package/src/routes/pages/archive.tsx +507 -357
- package/src/routes/pages/collection.tsx +94 -96
- package/src/routes/pages/collections.tsx +38 -50
- package/src/routes/pages/featured.tsx +32 -13
- package/src/routes/pages/home.tsx +25 -6
- package/src/routes/pages/language.tsx +113 -0
- package/src/routes/pages/latest.tsx +26 -16
- package/src/routes/pages/new.tsx +3 -0
- package/src/routes/pages/page.tsx +183 -19
- package/src/routes/pages/partials.tsx +41 -0
- package/src/routes/pages/search.tsx +18 -3
- package/src/routes/pages/smart-collection.tsx +340 -0
- package/src/routes/pages/subscribe.tsx +182 -0
- package/src/runtime/__tests__/index.test.ts +19 -0
- package/src/runtime/__tests__/node.test.ts +129 -4
- package/src/runtime/cloudflare.ts +21 -2
- package/src/runtime/index.ts +27 -1
- package/src/runtime/node.ts +18 -1
- package/src/runtime/site.ts +95 -24
- package/src/services/__tests__/auth.test.ts +116 -7
- package/src/services/__tests__/collection.test.ts +139 -4
- package/src/services/__tests__/language.test.ts +495 -0
- package/src/services/__tests__/navigation.test.ts +213 -10
- package/src/services/__tests__/path.test.ts +180 -0
- package/src/services/__tests__/post-language.test.ts +830 -0
- package/src/services/__tests__/post-timeline.test.ts +142 -11
- package/src/services/__tests__/post.test.ts +323 -9
- package/src/services/__tests__/settings.test.ts +228 -17
- package/src/services/__tests__/site-admin.test.ts +5 -5
- package/src/services/__tests__/smart-collection.test.ts +651 -0
- package/src/services/about-page.ts +22 -1
- package/src/services/auth.ts +51 -49
- package/src/services/bootstrap.ts +188 -38
- package/src/services/collection-directory-position.ts +76 -0
- package/src/services/collection.ts +330 -47
- package/src/services/custom-url.ts +27 -22
- package/src/services/export-theme/assets/client-site.css +1 -1
- package/src/services/export-theme/assets/client-site.js +123 -123
- package/src/services/export-theme/layouts/_default/rss.xml +77 -1
- package/src/services/export-theme/layouts/partials/feed-post-content.xml +105 -2
- package/src/services/export-theme/layouts/partials/media-gallery.html +35 -21
- package/src/services/export-theme/layouts/partials/pagination.html +14 -13
- package/src/services/export-theme/layouts/partials/post-card.html +8 -4
- package/src/services/export-theme/layouts/partials/reply.html +2 -1
- package/src/services/export-theme/layouts/partials/thread-preview.html +40 -30
- package/src/services/export-theme/styles/main.css +13 -1
- package/src/services/export.ts +281 -24
- package/src/services/index.ts +34 -3
- package/src/services/language.ts +412 -0
- package/src/services/mcp.ts +3 -1
- package/src/services/media.ts +14 -6
- package/src/services/navigation.ts +429 -67
- package/src/services/path.ts +225 -7
- package/src/services/post.ts +1318 -252
- package/src/services/search.ts +48 -2
- package/src/services/settings.ts +238 -43
- package/src/services/site-admin.ts +24 -7
- package/src/services/site.ts +77 -1
- package/src/services/smart-collection.ts +636 -0
- package/src/services/telegram.ts +20 -22
- package/src/services/upload-session.ts +1 -3
- package/src/style-author.css +27 -0
- package/src/styles/components-author.css +1622 -0
- package/src/styles/components.css +24 -1585
- package/src/styles/site-media.css +20 -6
- package/src/styles/tokens.css +144 -13
- package/src/styles/ui-author.css +7602 -0
- package/src/styles/ui.css +1224 -7276
- package/src/types/app-context.ts +7 -0
- package/src/types/bindings.ts +3 -0
- package/src/types/config.ts +94 -15
- package/src/types/constants.ts +188 -16
- package/src/types/entities.ts +92 -1
- package/src/types/operations.ts +64 -2
- package/src/types/props.ts +168 -30
- package/src/types/views.ts +61 -1
- package/src/ui/__tests__/font-themes.test.ts +52 -12
- package/src/ui/compose/ComposeDialog.tsx +215 -21
- package/src/ui/compose/ComposePrompt.tsx +1 -5
- package/src/ui/compose/__tests__/ComposeDialog.test.tsx +125 -0
- package/src/ui/dash/appearance/AdvancedContent.tsx +2 -2
- package/src/ui/dash/appearance/CodeInjectionContent.tsx +12 -16
- package/src/ui/dash/appearance/NavigationContent.tsx +117 -16
- package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +33 -11
- package/src/ui/dash/appearance/__tests__/system-nav-descriptions.test.ts +44 -0
- package/src/ui/dash/appearance/system-nav-descriptions.ts +75 -0
- package/src/ui/dash/settings/AccountMenuContent.tsx +2 -2
- package/src/ui/dash/settings/ApiTokensContent.tsx +2 -1
- package/src/ui/dash/settings/ConfigEditorContent.tsx +35 -16
- package/src/ui/dash/settings/GeneralContent.tsx +275 -113
- package/src/ui/dash/settings/LanguageContent.tsx +409 -0
- package/src/ui/dash/settings/SettingsRootContent.tsx +19 -1
- package/src/ui/dash/settings/__tests__/GeneralContent.test.tsx +324 -4
- package/src/ui/dash/settings/__tests__/LanguageContent.test.tsx +144 -0
- package/src/ui/dash/settings/__tests__/discover-copy.test.ts +66 -0
- package/src/ui/dash/settings/discover-copy.ts +116 -0
- package/src/ui/feed/LinkCard.tsx +55 -45
- package/src/ui/feed/LinkPreview.tsx +2 -7
- package/src/ui/feed/NoteCard.tsx +9 -4
- package/src/ui/feed/PostStatusBadges.tsx +103 -17
- package/src/ui/feed/QuoteCard.tsx +55 -30
- package/src/ui/feed/ThreadPreview.tsx +6 -2
- package/src/ui/feed/__tests__/thread-preview.test.ts +13 -16
- package/src/ui/feed/__tests__/timeline-cards.test.ts +314 -49
- package/src/ui/feed/thread-preview-state.ts +20 -6
- package/src/ui/font-themes.ts +39 -29
- package/src/ui/layouts/BaseLayout.tsx +155 -10
- package/src/ui/layouts/SiteLayout.tsx +192 -34
- package/src/ui/layouts/__tests__/BaseLayout.test.tsx +196 -17
- package/src/ui/layouts/__tests__/SiteLayout.test.tsx +109 -85
- package/src/ui/pages/ArchivePage.tsx +401 -301
- package/src/ui/pages/CollectionPage.tsx +86 -76
- package/src/ui/pages/CollectionsPage.tsx +5 -0
- package/src/ui/pages/ComposePage.tsx +8 -0
- package/src/ui/pages/PostPage.tsx +47 -0
- package/src/ui/pages/SearchPage.tsx +3 -3
- package/src/ui/pages/SmartCollectionPage.tsx +434 -0
- package/src/ui/pages/SubscribePage.tsx +236 -0
- package/src/ui/pages/__tests__/ArchivePage.test.tsx +122 -3
- package/src/ui/pages/__tests__/collections-directory-rows.test.tsx +92 -0
- package/src/ui/pages/__tests__/collections-view-links.test.tsx +81 -0
- package/src/ui/shared/CollectionDirectory.tsx +178 -3
- package/src/ui/shared/CollectionsManager.tsx +70 -17
- package/src/ui/shared/CopyField.tsx +127 -0
- package/src/ui/shared/DraftPreviewBar.tsx +25 -9
- package/src/ui/shared/EmptyPostContent.tsx +23 -0
- package/src/ui/shared/MediaGallery.tsx +125 -46
- package/src/ui/shared/Pagination.tsx +20 -5
- package/src/ui/shared/PostFooter.tsx +55 -23
- package/src/ui/shared/__tests__/archive-labels.test.ts +100 -0
- package/src/ui/shared/__tests__/media-gallery.test.ts +210 -2
- package/src/ui/shared/__tests__/navigation-labels.test.ts +16 -22
- package/src/ui/shared/__tests__/pagination.test.ts +38 -24
- package/src/ui/shared/__tests__/post-footer.test.ts +32 -0
- package/src/ui/shared/archive-labels.ts +98 -0
- package/src/ui/shared/collection-management-labels.ts +94 -16
- package/src/ui/shared/custom-icons.ts +5 -0
- package/src/ui/shared/navigation-labels.ts +45 -71
- package/src/ui/shared/post-article-attributes.ts +8 -0
- package/src/ui/shared/smart-collection-labels.ts +343 -0
- package/dist/app-BxCOR3Uc.js +0 -6
- package/dist/client/_assets/chunks/heic-to-DUUaO23q.js +0 -1
- package/dist/client/_assets/chunks/module-DcsAZQZ_.js +0 -716
- package/dist/client/_assets/chunks/url-pLre2DM_.js +0 -1
- package/dist/client/_assets/client-B_eGKN5p.css +0 -2
- package/dist/client/_assets/client-DxY5BFe8.js +0 -296
- package/dist/client/_assets/client-auth-CakPESv9.js +0 -5533
- package/dist/env-OHRKGcMj.js +0 -281
- package/dist/github-api-BgSiE71w.js +0 -176
- package/dist/github-app-BbklkFmU.js +0 -275
- package/dist/github-sync-Cq1pzzOI.js +0 -431
- package/dist/github-sync-ppWXN3jb.js +0 -4
- package/dist/url-BMYO-Zlt.js +0 -395
- package/src/client/__tests__/collection-created-notice.test.ts +0 -36
- package/src/client/__tests__/collection-form-bridge.test.ts +0 -216
- package/src/client/__tests__/post-form-bridge.test.ts +0 -82
- package/src/client/collection-created-notice.ts +0 -66
- package/src/client/collection-form-bridge.ts +0 -191
- package/src/client/components/__tests__/jant-post-form.test.ts +0 -193
- package/src/client/components/jant-post-form.ts +0 -422
- package/src/client/components/post-form-template.ts +0 -352
- package/src/client/components/post-form-types.ts +0 -96
- package/src/client/media-metadata.ts +0 -247
- package/src/client/post-form-bridge.ts +0 -193
- package/src/ui/pages/CollectionEditorPage.tsx +0 -175
|
@@ -204,6 +204,34 @@
|
|
|
204
204
|
"when": 1784531392184,
|
|
205
205
|
"tag": "0028_perfect_sharon_carter",
|
|
206
206
|
"breakpoints": true
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
"idx": 29,
|
|
210
|
+
"version": "7",
|
|
211
|
+
"when": 1786008671529,
|
|
212
|
+
"tag": "0029_rare_hairball",
|
|
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
|
|
207
235
|
}
|
|
208
236
|
]
|
|
209
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,8 +190,43 @@ 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"),
|
|
213
|
+
/**
|
|
214
|
+
* The author added this reply without announcing it. Persisted so both
|
|
215
|
+
* activity timestamps below stay recomputable from the rows alone.
|
|
216
|
+
* Always false on Thread roots.
|
|
217
|
+
*/
|
|
218
|
+
quietReply: boolean("quiet_reply").notNull().default(false),
|
|
182
219
|
publishedAt: integer("published_at"),
|
|
220
|
+
/**
|
|
221
|
+
* Root only: newest published_at in the Thread, excluding quiet replies —
|
|
222
|
+
* "last announced". Drives Latest and the feeds.
|
|
223
|
+
*/
|
|
183
224
|
lastActivityAt: integer("last_activity_at"),
|
|
225
|
+
/**
|
|
226
|
+
* Root only: newest published_at in the Thread, quiet replies included —
|
|
227
|
+
* "last changed". Drives the archive's updated sort.
|
|
228
|
+
*/
|
|
229
|
+
threadUpdatedAt: integer("thread_updated_at"),
|
|
184
230
|
createdAt: integer("created_at").notNull(),
|
|
185
231
|
updatedAt: integer("updated_at").notNull(),
|
|
186
232
|
},
|
|
@@ -228,6 +274,9 @@ export const posts = pgTable(
|
|
|
228
274
|
index("idx_post_site_root_published_activity")
|
|
229
275
|
.on(table.siteId, table.lastActivityAt, table.id)
|
|
230
276
|
.where(sql`${table.replyToId} IS NULL AND ${table.status} = 'published'`),
|
|
277
|
+
index("idx_post_site_root_thread_updated")
|
|
278
|
+
.on(table.siteId, table.threadUpdatedAt, table.id)
|
|
279
|
+
.where(sql`${table.replyToId} IS NULL AND ${table.status} = 'published'`),
|
|
231
280
|
index("idx_post_site_root_draft_updated")
|
|
232
281
|
.on(table.siteId, table.updatedAt, table.id)
|
|
233
282
|
.where(sql`${table.replyToId} IS NULL AND ${table.status} = 'draft'`),
|
|
@@ -241,6 +290,12 @@ export const posts = pgTable(
|
|
|
241
290
|
.where(
|
|
242
291
|
sql`${table.status} = 'published' AND ${table.featuredAt} IS NOT NULL`,
|
|
243
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`),
|
|
244
299
|
index("idx_post_search_document").using("gin", table.searchDocument),
|
|
245
300
|
index("idx_post_search_text_trgm").using(
|
|
246
301
|
"gin",
|
|
@@ -441,6 +496,82 @@ export const collections = pgTable(
|
|
|
441
496
|
],
|
|
442
497
|
);
|
|
443
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
|
+
|
|
444
575
|
// =============================================================================
|
|
445
576
|
// Path Registry (slug + alias + redirect)
|
|
446
577
|
// =============================================================================
|
|
@@ -462,6 +593,10 @@ export const pathRegistry = pgTable(
|
|
|
462
593
|
collectionId: text("collection_id").references(() => collections.id, {
|
|
463
594
|
onDelete: "cascade",
|
|
464
595
|
}),
|
|
596
|
+
smartCollectionId: text("smart_collection_id").references(
|
|
597
|
+
() => smartCollections.id,
|
|
598
|
+
{ onDelete: "cascade" },
|
|
599
|
+
),
|
|
465
600
|
redirectToPath: text("redirect_to_path"),
|
|
466
601
|
redirectType: integer("redirect_type"),
|
|
467
602
|
archiveQuery: text("archive_query"),
|
|
@@ -471,7 +606,7 @@ export const pathRegistry = pgTable(
|
|
|
471
606
|
(table) => [
|
|
472
607
|
check(
|
|
473
608
|
"chk_path_registry_kind",
|
|
474
|
-
sql`${table.kind} IN (
|
|
609
|
+
sql`${table.kind} IN (${sqlTextEnum(PATH_KINDS)})`,
|
|
475
610
|
),
|
|
476
611
|
uniqueIndex("uq_path_registry_site_path").on(table.siteId, table.path),
|
|
477
612
|
uniqueIndex("uq_path_registry_site_post_slug")
|
|
@@ -480,18 +615,28 @@ export const pathRegistry = pgTable(
|
|
|
480
615
|
uniqueIndex("uq_path_registry_site_collection_slug")
|
|
481
616
|
.on(table.siteId, table.collectionId)
|
|
482
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
|
+
),
|
|
483
623
|
index("idx_path_registry_site_post_id").on(table.siteId, table.postId),
|
|
484
624
|
index("idx_path_registry_site_collection_id").on(
|
|
485
625
|
table.siteId,
|
|
486
626
|
table.collectionId,
|
|
487
627
|
),
|
|
628
|
+
index("idx_path_registry_site_smart_collection_id").on(
|
|
629
|
+
table.siteId,
|
|
630
|
+
table.smartCollectionId,
|
|
631
|
+
),
|
|
488
632
|
check(
|
|
489
633
|
"chk_path_registry_shape",
|
|
490
634
|
sql`(
|
|
491
635
|
${table.kind} IN ('slug', 'alias')
|
|
492
636
|
AND (
|
|
493
|
-
(${table.postId} IS NOT NULL AND ${table.collectionId} IS NULL)
|
|
494
|
-
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)
|
|
495
640
|
)
|
|
496
641
|
AND ${table.redirectToPath} IS NULL
|
|
497
642
|
AND ${table.redirectType} IS NULL
|
|
@@ -500,6 +645,7 @@ export const pathRegistry = pgTable(
|
|
|
500
645
|
${table.kind} = 'redirect'
|
|
501
646
|
AND ${table.postId} IS NULL
|
|
502
647
|
AND ${table.collectionId} IS NULL
|
|
648
|
+
AND ${table.smartCollectionId} IS NULL
|
|
503
649
|
AND ${table.redirectToPath} IS NOT NULL
|
|
504
650
|
AND ${table.redirectType} IN (301, 302)
|
|
505
651
|
AND ${table.archiveQuery} IS NULL
|
|
@@ -507,6 +653,7 @@ export const pathRegistry = pgTable(
|
|
|
507
653
|
${table.kind} = 'archive'
|
|
508
654
|
AND ${table.postId} IS NULL
|
|
509
655
|
AND ${table.collectionId} IS NULL
|
|
656
|
+
AND ${table.smartCollectionId} IS NULL
|
|
510
657
|
AND ${table.redirectToPath} IS NULL
|
|
511
658
|
AND ${table.redirectType} IS NULL
|
|
512
659
|
AND ${table.archiveQuery} IS NOT NULL
|
|
@@ -526,10 +673,16 @@ export const collectionDirectoryItems = pgTable(
|
|
|
526
673
|
siteId: text("site_id")
|
|
527
674
|
.notNull()
|
|
528
675
|
.references(() => sites.id, { onDelete: "cascade" }),
|
|
529
|
-
type: text("type", {
|
|
676
|
+
type: text("type", {
|
|
677
|
+
enum: COLLECTION_DIRECTORY_ENTRY_TYPES,
|
|
678
|
+
}).notNull(),
|
|
530
679
|
collectionId: text("collection_id").references(() => collections.id, {
|
|
531
680
|
onDelete: "cascade",
|
|
532
681
|
}),
|
|
682
|
+
smartCollectionId: text("smart_collection_id").references(
|
|
683
|
+
() => smartCollections.id,
|
|
684
|
+
{ onDelete: "cascade" },
|
|
685
|
+
),
|
|
533
686
|
label: text("label"),
|
|
534
687
|
url: text("url"),
|
|
535
688
|
description: text("description"),
|
|
@@ -540,12 +693,16 @@ export const collectionDirectoryItems = pgTable(
|
|
|
540
693
|
(table) => [
|
|
541
694
|
check(
|
|
542
695
|
"chk_collection_directory_item_type",
|
|
543
|
-
sql`${table.type} IN (
|
|
696
|
+
sql`${table.type} IN (${sqlTextEnum(COLLECTION_DIRECTORY_ENTRY_TYPES)})`,
|
|
544
697
|
),
|
|
545
698
|
index("idx_collection_directory_item_site_collection_id").on(
|
|
546
699
|
table.siteId,
|
|
547
700
|
table.collectionId,
|
|
548
701
|
),
|
|
702
|
+
index("idx_collection_directory_item_site_smart_collection_id").on(
|
|
703
|
+
table.siteId,
|
|
704
|
+
table.smartCollectionId,
|
|
705
|
+
),
|
|
549
706
|
uniqueIndex("uq_collection_directory_item_site_position").on(
|
|
550
707
|
table.siteId,
|
|
551
708
|
table.position,
|
|
@@ -555,27 +712,41 @@ export const collectionDirectoryItems = pgTable(
|
|
|
555
712
|
.where(
|
|
556
713
|
sql`${table.type} = 'collection' AND ${table.collectionId} IS NOT NULL`,
|
|
557
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
|
+
),
|
|
558
720
|
check(
|
|
559
721
|
"chk_collection_directory_item_shape",
|
|
560
722
|
sql`(
|
|
561
723
|
${table.type} = 'collection'
|
|
562
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
|
|
563
732
|
AND ${table.label} IS NULL
|
|
564
733
|
AND ${table.url} IS NULL
|
|
565
734
|
) OR (
|
|
566
735
|
${table.type} = 'divider'
|
|
567
736
|
AND ${table.collectionId} IS NULL
|
|
737
|
+
AND ${table.smartCollectionId} IS NULL
|
|
568
738
|
AND ${table.url} IS NULL
|
|
569
739
|
) OR (
|
|
570
740
|
${table.type} = 'link'
|
|
571
741
|
AND ${table.collectionId} IS NULL
|
|
742
|
+
AND ${table.smartCollectionId} IS NULL
|
|
572
743
|
AND ${table.label} IS NOT NULL
|
|
573
744
|
AND ${table.url} IS NOT NULL
|
|
574
745
|
)`,
|
|
575
746
|
),
|
|
576
747
|
check(
|
|
577
748
|
"chk_collection_directory_item_label",
|
|
578
|
-
sql`${table.type}
|
|
749
|
+
sql`${table.type} NOT IN ('collection', 'smart_collection') OR ${table.label} IS NULL`,
|
|
579
750
|
),
|
|
580
751
|
check(
|
|
581
752
|
"chk_collection_directory_item_description",
|
|
@@ -641,6 +812,10 @@ export const navItems = pgTable(
|
|
|
641
812
|
collectionId: text("collection_id").references(() => collections.id, {
|
|
642
813
|
onDelete: "cascade",
|
|
643
814
|
}),
|
|
815
|
+
smartCollectionId: text("smart_collection_id").references(
|
|
816
|
+
() => smartCollections.id,
|
|
817
|
+
{ onDelete: "cascade" },
|
|
818
|
+
),
|
|
644
819
|
postId: text("post_id").references(() => posts.id, {
|
|
645
820
|
onDelete: "cascade",
|
|
646
821
|
}),
|
|
@@ -670,21 +845,31 @@ export const navItems = pgTable(
|
|
|
670
845
|
${table.type} = 'link'
|
|
671
846
|
AND ${table.systemKey} IS NULL
|
|
672
847
|
AND ${table.collectionId} IS NULL
|
|
848
|
+
AND ${table.smartCollectionId} IS NULL
|
|
673
849
|
AND ${table.postId} IS NULL
|
|
674
850
|
) OR (
|
|
675
851
|
${table.type} = 'system'
|
|
676
852
|
AND ${table.systemKey} IS NOT NULL
|
|
677
853
|
AND ${table.collectionId} IS NULL
|
|
854
|
+
AND ${table.smartCollectionId} IS NULL
|
|
678
855
|
AND ${table.postId} IS NULL
|
|
679
856
|
) OR (
|
|
680
857
|
${table.type} = 'collection'
|
|
681
858
|
AND ${table.systemKey} IS NULL
|
|
682
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
|
|
683
867
|
AND ${table.postId} IS NULL
|
|
684
868
|
) OR (
|
|
685
869
|
${table.type} = 'page'
|
|
686
870
|
AND ${table.systemKey} IS NULL
|
|
687
871
|
AND ${table.collectionId} IS NULL
|
|
872
|
+
AND ${table.smartCollectionId} IS NULL
|
|
688
873
|
AND ${table.postId} IS NOT NULL
|
|
689
874
|
)`,
|
|
690
875
|
),
|
|
@@ -695,6 +880,9 @@ export const navItems = pgTable(
|
|
|
695
880
|
uniqueIndex("uq_nav_item_site_collection_id")
|
|
696
881
|
.on(table.siteId, table.collectionId)
|
|
697
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`),
|
|
698
886
|
uniqueIndex("uq_nav_item_site_post_id")
|
|
699
887
|
.on(table.siteId, table.postId)
|
|
700
888
|
.where(sql`${table.postId} IS NOT NULL`),
|
|
@@ -842,8 +1030,6 @@ export const verification = pgTable("verification", {
|
|
|
842
1030
|
// GitHub App Installations (junction table: installation ↔ site, many-to-many)
|
|
843
1031
|
// ---------------------------------------------------------------------------
|
|
844
1032
|
|
|
845
|
-
const GITHUB_APP_ACCOUNT_TYPES = ["User", "Organization"] as const;
|
|
846
|
-
|
|
847
1033
|
export const githubAppInstallation = pgTable(
|
|
848
1034
|
"github_app_installation",
|
|
849
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
|
+
}
|