@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
|
@@ -400,7 +400,7 @@ describe("PostService - Timeline features", () => {
|
|
|
400
400
|
});
|
|
401
401
|
});
|
|
402
402
|
|
|
403
|
-
describe("
|
|
403
|
+
describe("getThreadTailIds", () => {
|
|
404
404
|
it("keeps independent root threads separate", async () => {
|
|
405
405
|
const root1 = await postService.create({
|
|
406
406
|
format: "note",
|
|
@@ -412,10 +412,7 @@ describe("PostService - Timeline features", () => {
|
|
|
412
412
|
quoteText: "quoted",
|
|
413
413
|
});
|
|
414
414
|
|
|
415
|
-
const result = await postService.
|
|
416
|
-
root1.id,
|
|
417
|
-
root2.id,
|
|
418
|
-
]);
|
|
415
|
+
const result = await postService.getThreadTailIds([root1.id, root2.id]);
|
|
419
416
|
|
|
420
417
|
expect(result.get(root1.id)).toBe(root1.id);
|
|
421
418
|
expect(result.get(root2.id)).toBe(root2.id);
|
|
@@ -436,14 +433,57 @@ describe("PostService - Timeline features", () => {
|
|
|
436
433
|
replyToId: root1.id,
|
|
437
434
|
});
|
|
438
435
|
|
|
439
|
-
const result = await postService.
|
|
440
|
-
root1.id,
|
|
441
|
-
root2.id,
|
|
442
|
-
]);
|
|
436
|
+
const result = await postService.getThreadTailIds([root1.id, root2.id]);
|
|
443
437
|
|
|
444
438
|
expect(result.get(root1.id)).toBe(reply1.id);
|
|
445
439
|
expect(result.get(root2.id)).toBe(root2.id);
|
|
446
440
|
});
|
|
441
|
+
|
|
442
|
+
it("skips a trailing draft by default", async () => {
|
|
443
|
+
const root = await postService.create({
|
|
444
|
+
format: "note",
|
|
445
|
+
bodyMarkdown: "root",
|
|
446
|
+
});
|
|
447
|
+
const reply = await postService.create({
|
|
448
|
+
format: "note",
|
|
449
|
+
bodyMarkdown: "published reply",
|
|
450
|
+
replyToId: root.id,
|
|
451
|
+
});
|
|
452
|
+
await postService.create({
|
|
453
|
+
format: "note",
|
|
454
|
+
bodyMarkdown: "unfinished",
|
|
455
|
+
replyToId: reply.id,
|
|
456
|
+
status: "draft",
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
const result = await postService.getThreadTailIds([root.id]);
|
|
460
|
+
|
|
461
|
+
expect(result.get(root.id)).toBe(reply.id);
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
it("returns a trailing draft when drafts are included", async () => {
|
|
465
|
+
const root = await postService.create({
|
|
466
|
+
format: "note",
|
|
467
|
+
bodyMarkdown: "root",
|
|
468
|
+
});
|
|
469
|
+
const reply = await postService.create({
|
|
470
|
+
format: "note",
|
|
471
|
+
bodyMarkdown: "published reply",
|
|
472
|
+
replyToId: root.id,
|
|
473
|
+
});
|
|
474
|
+
const draft = await postService.create({
|
|
475
|
+
format: "note",
|
|
476
|
+
bodyMarkdown: "unfinished",
|
|
477
|
+
replyToId: reply.id,
|
|
478
|
+
status: "draft",
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
const result = await postService.getThreadTailIds([root.id], {
|
|
482
|
+
includeDrafts: true,
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
expect(result.get(root.id)).toBe(draft.id);
|
|
486
|
+
});
|
|
447
487
|
});
|
|
448
488
|
|
|
449
489
|
describe("timeline assembly", () => {
|
|
@@ -586,7 +626,98 @@ describe("PostService - Timeline features", () => {
|
|
|
586
626
|
expect(rootIds).toEqual([newerRoot.id, olderRoot.id]);
|
|
587
627
|
});
|
|
588
628
|
|
|
589
|
-
|
|
629
|
+
// Quietness used to live only in control flow: the reply skipped the
|
|
630
|
+
// recalculation and nothing recorded that it had. Any later recalculation
|
|
631
|
+
// recomputed MAX(published_at) over the whole Thread, swallowed the quiet
|
|
632
|
+
// reply, and bumped the Thread anyway. It is a stored column now, so the
|
|
633
|
+
// decision survives whatever else happens to the Thread.
|
|
634
|
+
it("keeps a quiet reply quiet after an unrelated post is deleted", async () => {
|
|
635
|
+
const collection = await collectionService.create({
|
|
636
|
+
slug: "durable",
|
|
637
|
+
title: "Durable",
|
|
638
|
+
});
|
|
639
|
+
const olderRoot = await postService.create({
|
|
640
|
+
format: "note",
|
|
641
|
+
bodyMarkdown: "Older root",
|
|
642
|
+
publishedAt: 1000,
|
|
643
|
+
});
|
|
644
|
+
const newerRoot = await postService.create({
|
|
645
|
+
format: "note",
|
|
646
|
+
bodyMarkdown: "Newer root",
|
|
647
|
+
publishedAt: 2000,
|
|
648
|
+
});
|
|
649
|
+
await collectionService.addThread(collection.id, olderRoot.id);
|
|
650
|
+
await collectionService.addThread(collection.id, newerRoot.id);
|
|
651
|
+
|
|
652
|
+
const normalReply = await postService.create({
|
|
653
|
+
format: "note",
|
|
654
|
+
bodyMarkdown: "Normal reply",
|
|
655
|
+
replyToId: olderRoot.id,
|
|
656
|
+
publishedAt: 1500,
|
|
657
|
+
});
|
|
658
|
+
await postService.create({
|
|
659
|
+
format: "note",
|
|
660
|
+
bodyMarkdown: "Quiet reply",
|
|
661
|
+
replyToId: normalReply.id,
|
|
662
|
+
publishedAt: 3000,
|
|
663
|
+
quietReply: true,
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
// Triggers a full recalculation of the Thread.
|
|
667
|
+
await postService.delete(normalReply.id);
|
|
668
|
+
|
|
669
|
+
const root = await postService.getById(olderRoot.id);
|
|
670
|
+
expect(root?.lastActivityAt).toBe(1000);
|
|
671
|
+
expect(root?.threadUpdatedAt).toBe(3000);
|
|
672
|
+
|
|
673
|
+
const rootIds = await postService.listCollectionThreadRootIds(
|
|
674
|
+
collection.id,
|
|
675
|
+
{ status: "published", sortOrder: "newest" },
|
|
676
|
+
);
|
|
677
|
+
expect(rootIds).toEqual([newerRoot.id, olderRoot.id]);
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
it("records both activity timestamps for a quiet reply", async () => {
|
|
681
|
+
const root = await postService.create({
|
|
682
|
+
format: "note",
|
|
683
|
+
bodyMarkdown: "Root",
|
|
684
|
+
publishedAt: 1000,
|
|
685
|
+
});
|
|
686
|
+
await postService.create({
|
|
687
|
+
format: "note",
|
|
688
|
+
bodyMarkdown: "Quiet reply",
|
|
689
|
+
replyToId: root.id,
|
|
690
|
+
publishedAt: 3000,
|
|
691
|
+
quietReply: true,
|
|
692
|
+
});
|
|
693
|
+
|
|
694
|
+
const updated = await postService.getById(root.id);
|
|
695
|
+
expect(updated?.lastActivityAt).toBe(1000);
|
|
696
|
+
expect(updated?.threadUpdatedAt).toBe(3000);
|
|
697
|
+
});
|
|
698
|
+
|
|
699
|
+
it("moves both timestamps for a normal reply", async () => {
|
|
700
|
+
const root = await postService.create({
|
|
701
|
+
format: "note",
|
|
702
|
+
bodyMarkdown: "Root",
|
|
703
|
+
publishedAt: 1000,
|
|
704
|
+
});
|
|
705
|
+
await postService.create({
|
|
706
|
+
format: "note",
|
|
707
|
+
bodyMarkdown: "Normal reply",
|
|
708
|
+
replyToId: root.id,
|
|
709
|
+
publishedAt: 3000,
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
const updated = await postService.getById(root.id);
|
|
713
|
+
expect(updated?.lastActivityAt).toBe(3000);
|
|
714
|
+
expect(updated?.threadUpdatedAt).toBe(3000);
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
// Activity means the Thread gained a post. Editing an old post is not
|
|
718
|
+
// activity — otherwise fixing a typo would drag it back to the top of its
|
|
719
|
+
// collection, and "recently updated" would stop meaning anything.
|
|
720
|
+
it("does not reorder a Thread when its root is only edited", async () => {
|
|
590
721
|
const collection = await collectionService.create({
|
|
591
722
|
slug: "edited",
|
|
592
723
|
title: "Edited",
|
|
@@ -617,7 +748,7 @@ describe("PostService - Timeline features", () => {
|
|
|
617
748
|
},
|
|
618
749
|
);
|
|
619
750
|
|
|
620
|
-
expect(rootIds).toEqual([
|
|
751
|
+
expect(rootIds).toEqual([newerRoot.id, editedRoot.id]);
|
|
621
752
|
});
|
|
622
753
|
});
|
|
623
754
|
|
|
@@ -570,20 +570,53 @@ describe("PostService", () => {
|
|
|
570
570
|
const descending = await postService.list({
|
|
571
571
|
sortOrder: "rating_desc",
|
|
572
572
|
});
|
|
573
|
-
const ascending = await postService.list({
|
|
574
|
-
sortOrder: "rating_asc",
|
|
575
|
-
});
|
|
576
573
|
|
|
577
574
|
expect(descending.map((post) => post.bodyText)).toEqual([
|
|
578
575
|
"five stars",
|
|
579
576
|
"three stars",
|
|
580
577
|
"unrated newest",
|
|
581
578
|
]);
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
it("orders by time for an explicit newest, ratings and all", async () => {
|
|
582
|
+
// `newest` used to have no branch of its own in the ORDER BY and fell
|
|
583
|
+
// into the one written for the ascending rating order, so anything that
|
|
584
|
+
// named it — a smart collection page, most visibly — got its posts
|
|
585
|
+
// grouped by rating instead of dated. Rated rows here on purpose.
|
|
586
|
+
await postService.create({
|
|
587
|
+
format: "note",
|
|
588
|
+
bodyMarkdown: "oldest, best",
|
|
589
|
+
publishedAt: 1000,
|
|
590
|
+
rating: 5,
|
|
591
|
+
});
|
|
592
|
+
await postService.create({
|
|
593
|
+
format: "note",
|
|
594
|
+
bodyMarkdown: "middle, worst",
|
|
595
|
+
publishedAt: 2000,
|
|
596
|
+
rating: 1,
|
|
597
|
+
});
|
|
598
|
+
await postService.create({
|
|
599
|
+
format: "note",
|
|
600
|
+
bodyMarkdown: "newest, unrated",
|
|
601
|
+
publishedAt: 3000,
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
const ordered = await postService.list({ sortOrder: "newest" });
|
|
605
|
+
expect(ordered.map((post) => post.bodyText)).toEqual([
|
|
606
|
+
"newest, unrated",
|
|
607
|
+
"middle, worst",
|
|
608
|
+
"oldest, best",
|
|
586
609
|
]);
|
|
610
|
+
|
|
611
|
+
// The keyset cursor reads the same keys or pagination skips rows.
|
|
612
|
+
expect(
|
|
613
|
+
(
|
|
614
|
+
await postService.list({
|
|
615
|
+
sortOrder: "newest",
|
|
616
|
+
cursor: ordered[0]?.id,
|
|
617
|
+
})
|
|
618
|
+
).map((post) => post.bodyText),
|
|
619
|
+
).toEqual(["middle, worst", "oldest, best"]);
|
|
587
620
|
});
|
|
588
621
|
|
|
589
622
|
it("orders drafts by updatedAt descending", async () => {
|
|
@@ -1470,6 +1503,9 @@ describe("PostService", () => {
|
|
|
1470
1503
|
expect(published?.status).toBe("published");
|
|
1471
1504
|
expect(published?.publishedAt).toBeTypeOf("number");
|
|
1472
1505
|
expect((published?.publishedAt ?? 0) >= post.updatedAt).toBe(true);
|
|
1506
|
+
expect(
|
|
1507
|
+
(await postService.list({ status: "draft" })).map((draft) => draft.id),
|
|
1508
|
+
).not.toContain(post.id);
|
|
1473
1509
|
});
|
|
1474
1510
|
|
|
1475
1511
|
it("clears publishedAt when converting a published post back to draft", async () => {
|
|
@@ -1567,7 +1603,7 @@ describe("PostService", () => {
|
|
|
1567
1603
|
expect(updated?.pinnedAt).toBeTypeOf("number");
|
|
1568
1604
|
});
|
|
1569
1605
|
|
|
1570
|
-
it("updates slug and its page navigation
|
|
1606
|
+
it("updates slug and title on its page navigation entry", async () => {
|
|
1571
1607
|
const post = await postService.create({
|
|
1572
1608
|
format: "note",
|
|
1573
1609
|
title: "Test page",
|
|
@@ -1582,11 +1618,15 @@ describe("PostService", () => {
|
|
|
1582
1618
|
|
|
1583
1619
|
const updated = await postService.update(post.id, {
|
|
1584
1620
|
slug: "new-slug",
|
|
1621
|
+
title: "Renamed page",
|
|
1585
1622
|
});
|
|
1586
1623
|
|
|
1587
1624
|
expect(updated?.slug).toBe("new-slug");
|
|
1625
|
+
// The URL is synced on write; the title is read through, so renaming the
|
|
1626
|
+
// page carries its navigation entry along with no sync step at all.
|
|
1588
1627
|
expect(await navItemService.getById(navItem.id)).toMatchObject({
|
|
1589
|
-
label: "
|
|
1628
|
+
label: "",
|
|
1629
|
+
targetTitle: "Renamed page",
|
|
1590
1630
|
url: "/new-slug",
|
|
1591
1631
|
});
|
|
1592
1632
|
});
|
|
@@ -1856,6 +1896,92 @@ describe("PostService", () => {
|
|
|
1856
1896
|
});
|
|
1857
1897
|
});
|
|
1858
1898
|
|
|
1899
|
+
describe("createThreadWithAttachments", () => {
|
|
1900
|
+
// No attachments in these cases, so only the up-front id check is reached.
|
|
1901
|
+
const deps = {
|
|
1902
|
+
media: { validateIds: async () => [] },
|
|
1903
|
+
} as unknown as Parameters<
|
|
1904
|
+
typeof postService.createThreadWithAttachments
|
|
1905
|
+
>[1];
|
|
1906
|
+
|
|
1907
|
+
it("dates every reply from the root when the root is backdated", async () => {
|
|
1908
|
+
const backdated = 1710000000; // 2024-03-09
|
|
1909
|
+
|
|
1910
|
+
const created = await postService.createThreadWithAttachments(
|
|
1911
|
+
[
|
|
1912
|
+
{
|
|
1913
|
+
data: {
|
|
1914
|
+
format: "note",
|
|
1915
|
+
bodyMarkdown: "root",
|
|
1916
|
+
publishedAt: backdated,
|
|
1917
|
+
},
|
|
1918
|
+
attachments: [],
|
|
1919
|
+
},
|
|
1920
|
+
{ data: { format: "note", bodyMarkdown: "second" }, attachments: [] },
|
|
1921
|
+
{ data: { format: "note", bodyMarkdown: "third" }, attachments: [] },
|
|
1922
|
+
],
|
|
1923
|
+
deps,
|
|
1924
|
+
undefined,
|
|
1925
|
+
);
|
|
1926
|
+
|
|
1927
|
+
// Without inheritance the replies would land on today and the thread
|
|
1928
|
+
// would read as if it spanned two years.
|
|
1929
|
+
expect(created.map((p) => p.publishedAt)).toEqual([
|
|
1930
|
+
backdated,
|
|
1931
|
+
backdated,
|
|
1932
|
+
backdated,
|
|
1933
|
+
]);
|
|
1934
|
+
// Order still comes from createdAt/id, so the chain is intact.
|
|
1935
|
+
expect(created[1].replyToId).toBe(created[0].id);
|
|
1936
|
+
expect(created[2].replyToId).toBe(created[1].id);
|
|
1937
|
+
});
|
|
1938
|
+
|
|
1939
|
+
it("keeps a reply's own date when one is given", async () => {
|
|
1940
|
+
const rootAt = 1710000000;
|
|
1941
|
+
const replyAt = 1720000000;
|
|
1942
|
+
|
|
1943
|
+
const created = await postService.createThreadWithAttachments(
|
|
1944
|
+
[
|
|
1945
|
+
{
|
|
1946
|
+
data: { format: "note", bodyMarkdown: "root", publishedAt: rootAt },
|
|
1947
|
+
attachments: [],
|
|
1948
|
+
},
|
|
1949
|
+
{
|
|
1950
|
+
data: {
|
|
1951
|
+
format: "note",
|
|
1952
|
+
bodyMarkdown: "second",
|
|
1953
|
+
publishedAt: replyAt,
|
|
1954
|
+
},
|
|
1955
|
+
attachments: [],
|
|
1956
|
+
},
|
|
1957
|
+
],
|
|
1958
|
+
deps,
|
|
1959
|
+
undefined,
|
|
1960
|
+
);
|
|
1961
|
+
|
|
1962
|
+
expect(created.map((p) => p.publishedAt)).toEqual([rootAt, replyAt]);
|
|
1963
|
+
});
|
|
1964
|
+
|
|
1965
|
+
it("leaves draft threads undated", async () => {
|
|
1966
|
+
const created = await postService.createThreadWithAttachments(
|
|
1967
|
+
[
|
|
1968
|
+
{
|
|
1969
|
+
data: { format: "note", bodyMarkdown: "root", status: "draft" },
|
|
1970
|
+
attachments: [],
|
|
1971
|
+
},
|
|
1972
|
+
{
|
|
1973
|
+
data: { format: "note", bodyMarkdown: "second", status: "draft" },
|
|
1974
|
+
attachments: [],
|
|
1975
|
+
},
|
|
1976
|
+
],
|
|
1977
|
+
deps,
|
|
1978
|
+
undefined,
|
|
1979
|
+
);
|
|
1980
|
+
|
|
1981
|
+
expect(created.map((p) => p.publishedAt)).toEqual([null, null]);
|
|
1982
|
+
});
|
|
1983
|
+
});
|
|
1984
|
+
|
|
1859
1985
|
describe("threads", () => {
|
|
1860
1986
|
it("sets threadId on reply to a root post", async () => {
|
|
1861
1987
|
const root = await postService.create({
|
|
@@ -1915,6 +2041,143 @@ describe("PostService", () => {
|
|
|
1915
2041
|
);
|
|
1916
2042
|
});
|
|
1917
2043
|
|
|
2044
|
+
it("refuses to feature an unpublished post", async () => {
|
|
2045
|
+
await expect(
|
|
2046
|
+
postService.create({
|
|
2047
|
+
format: "note",
|
|
2048
|
+
bodyMarkdown: "not ready",
|
|
2049
|
+
status: "draft",
|
|
2050
|
+
featured: true,
|
|
2051
|
+
}),
|
|
2052
|
+
).rejects.toThrow("Publish this post before featuring it.");
|
|
2053
|
+
|
|
2054
|
+
const draft = await postService.create({
|
|
2055
|
+
format: "note",
|
|
2056
|
+
bodyMarkdown: "not ready",
|
|
2057
|
+
status: "draft",
|
|
2058
|
+
});
|
|
2059
|
+
await expect(
|
|
2060
|
+
postService.update(draft.id, { featured: true }),
|
|
2061
|
+
).rejects.toThrow("Publish this post before featuring it.");
|
|
2062
|
+
});
|
|
2063
|
+
|
|
2064
|
+
it("keeps a featured flag through a trip back to draft", async () => {
|
|
2065
|
+
const post = await postService.create({
|
|
2066
|
+
format: "note",
|
|
2067
|
+
bodyMarkdown: "published",
|
|
2068
|
+
featured: true,
|
|
2069
|
+
});
|
|
2070
|
+
|
|
2071
|
+
const unpublished = await postService.update(post.id, {
|
|
2072
|
+
status: "draft",
|
|
2073
|
+
});
|
|
2074
|
+
|
|
2075
|
+
expect(unpublished?.status).toBe("draft");
|
|
2076
|
+
expect(unpublished?.featuredAt).not.toBeNull();
|
|
2077
|
+
});
|
|
2078
|
+
|
|
2079
|
+
it("publishes a reply whose parent is still a draft", async () => {
|
|
2080
|
+
const root = await postService.create({
|
|
2081
|
+
format: "note",
|
|
2082
|
+
bodyMarkdown: "root",
|
|
2083
|
+
});
|
|
2084
|
+
const draft = await postService.create({
|
|
2085
|
+
format: "note",
|
|
2086
|
+
bodyMarkdown: "unfinished",
|
|
2087
|
+
replyToId: root.id,
|
|
2088
|
+
status: "draft",
|
|
2089
|
+
});
|
|
2090
|
+
|
|
2091
|
+
const next = await postService.create({
|
|
2092
|
+
format: "note",
|
|
2093
|
+
bodyMarkdown: "continues past the draft",
|
|
2094
|
+
replyToId: draft.id,
|
|
2095
|
+
});
|
|
2096
|
+
|
|
2097
|
+
// Readers never saw the draft, so publishing past it reads as
|
|
2098
|
+
// continuous. The draft stays parked until it is dealt with on its own.
|
|
2099
|
+
expect(next.status).toBe("published");
|
|
2100
|
+
expect(draft.status).toBe("draft");
|
|
2101
|
+
});
|
|
2102
|
+
|
|
2103
|
+
it("publishes only the post asked for, leaving other drafts alone", async () => {
|
|
2104
|
+
const root = await postService.create({
|
|
2105
|
+
format: "note",
|
|
2106
|
+
bodyMarkdown: "root",
|
|
2107
|
+
});
|
|
2108
|
+
const first = await postService.create({
|
|
2109
|
+
format: "note",
|
|
2110
|
+
bodyMarkdown: "first draft",
|
|
2111
|
+
replyToId: root.id,
|
|
2112
|
+
status: "draft",
|
|
2113
|
+
});
|
|
2114
|
+
const second = await postService.create({
|
|
2115
|
+
format: "note",
|
|
2116
|
+
bodyMarkdown: "second draft",
|
|
2117
|
+
replyToId: first.id,
|
|
2118
|
+
status: "draft",
|
|
2119
|
+
});
|
|
2120
|
+
|
|
2121
|
+
await postService.update(second.id, { status: "published" });
|
|
2122
|
+
|
|
2123
|
+
const statusById = new Map(
|
|
2124
|
+
(await postService.getThread(root.id)).map((p) => [p.id, p.status]),
|
|
2125
|
+
);
|
|
2126
|
+
expect(statusById.get(first.id)).toBe("draft");
|
|
2127
|
+
expect(statusById.get(second.id)).toBe("published");
|
|
2128
|
+
});
|
|
2129
|
+
|
|
2130
|
+
it("keeps a draft between two published replies", async () => {
|
|
2131
|
+
const root = await postService.create({
|
|
2132
|
+
format: "note",
|
|
2133
|
+
bodyMarkdown: "root",
|
|
2134
|
+
});
|
|
2135
|
+
const middle = await postService.create({
|
|
2136
|
+
format: "note",
|
|
2137
|
+
bodyMarkdown: "middle",
|
|
2138
|
+
replyToId: root.id,
|
|
2139
|
+
});
|
|
2140
|
+
const last = await postService.create({
|
|
2141
|
+
format: "note",
|
|
2142
|
+
bodyMarkdown: "last",
|
|
2143
|
+
replyToId: middle.id,
|
|
2144
|
+
});
|
|
2145
|
+
|
|
2146
|
+
await postService.update(middle.id, { status: "draft" });
|
|
2147
|
+
|
|
2148
|
+
const statusById = new Map(
|
|
2149
|
+
(await postService.getThread(root.id)).map((p) => [p.id, p.status]),
|
|
2150
|
+
);
|
|
2151
|
+
// Unpublishing one post touches only that post. The public thread simply
|
|
2152
|
+
// skips it; nothing else in the chain changes.
|
|
2153
|
+
expect(statusById.get(root.id)).toBe("published");
|
|
2154
|
+
expect(statusById.get(middle.id)).toBe("draft");
|
|
2155
|
+
expect(statusById.get(last.id)).toBe("published");
|
|
2156
|
+
});
|
|
2157
|
+
|
|
2158
|
+
it("names the draft when an unpublished reply holds the thread tail", async () => {
|
|
2159
|
+
const root = await postService.create({
|
|
2160
|
+
format: "note",
|
|
2161
|
+
bodyMarkdown: "root",
|
|
2162
|
+
});
|
|
2163
|
+
await postService.create({
|
|
2164
|
+
format: "note",
|
|
2165
|
+
bodyMarkdown: "unfinished",
|
|
2166
|
+
replyToId: root.id,
|
|
2167
|
+
status: "draft",
|
|
2168
|
+
});
|
|
2169
|
+
|
|
2170
|
+
await expect(
|
|
2171
|
+
postService.create({
|
|
2172
|
+
format: "note",
|
|
2173
|
+
bodyMarkdown: "reply",
|
|
2174
|
+
replyToId: root.id,
|
|
2175
|
+
}),
|
|
2176
|
+
).rejects.toThrow(
|
|
2177
|
+
"This thread ends with an unpublished draft. Finish that draft or discard it, then reply.",
|
|
2178
|
+
);
|
|
2179
|
+
});
|
|
2180
|
+
|
|
1918
2181
|
it("inherits status from root post", async () => {
|
|
1919
2182
|
const root = await postService.create({
|
|
1920
2183
|
format: "note",
|
|
@@ -2025,6 +2288,57 @@ describe("PostService", () => {
|
|
|
2025
2288
|
expect(thread[0]?.bodyText).toBe("root");
|
|
2026
2289
|
});
|
|
2027
2290
|
|
|
2291
|
+
it("getThreadPosition counts the chain from the root down to a post", async () => {
|
|
2292
|
+
const root = await postService.create({
|
|
2293
|
+
format: "note",
|
|
2294
|
+
bodyMarkdown: "root",
|
|
2295
|
+
});
|
|
2296
|
+
const reply1 = await postService.create({
|
|
2297
|
+
format: "note",
|
|
2298
|
+
bodyMarkdown: "reply1",
|
|
2299
|
+
replyToId: root.id,
|
|
2300
|
+
});
|
|
2301
|
+
const reply2 = await postService.create({
|
|
2302
|
+
format: "note",
|
|
2303
|
+
bodyMarkdown: "reply2",
|
|
2304
|
+
replyToId: reply1.id,
|
|
2305
|
+
});
|
|
2306
|
+
|
|
2307
|
+
expect(await postService.getThreadPosition(root.id)).toBe(1);
|
|
2308
|
+
expect(await postService.getThreadPosition(reply1.id)).toBe(2);
|
|
2309
|
+
expect(await postService.getThreadPosition(reply2.id)).toBe(3);
|
|
2310
|
+
});
|
|
2311
|
+
|
|
2312
|
+
it("getThreadPosition walks the reply chain rather than counting members", async () => {
|
|
2313
|
+
// `create` only allows replying to the tail, so today a thread is a
|
|
2314
|
+
// straight chain and depth happens to equal its size. The walk is what
|
|
2315
|
+
// makes the two agree — counting `threadId` members would give the same
|
|
2316
|
+
// answer here and the wrong one the moment a draft chain is incomplete.
|
|
2317
|
+
const root = await postService.create({
|
|
2318
|
+
format: "note",
|
|
2319
|
+
bodyMarkdown: "root",
|
|
2320
|
+
});
|
|
2321
|
+
const tail = await postService.create({
|
|
2322
|
+
format: "note",
|
|
2323
|
+
bodyMarkdown: "tail",
|
|
2324
|
+
replyToId: root.id,
|
|
2325
|
+
});
|
|
2326
|
+
|
|
2327
|
+
// A second post filed under the same thread without joining the chain:
|
|
2328
|
+
// it is not above the tail, so it must not push the tail's position.
|
|
2329
|
+
await postService.create({
|
|
2330
|
+
format: "note",
|
|
2331
|
+
bodyMarkdown: "unrelated",
|
|
2332
|
+
});
|
|
2333
|
+
|
|
2334
|
+
expect(await postService.getThreadPosition(tail.id)).toBe(2);
|
|
2335
|
+
expect(await postService.getThread(root.id)).toHaveLength(2);
|
|
2336
|
+
});
|
|
2337
|
+
|
|
2338
|
+
it("getThreadPosition returns 0 for a post that does not exist", async () => {
|
|
2339
|
+
expect(await postService.getThreadPosition("pst_missing")).toBe(0);
|
|
2340
|
+
});
|
|
2341
|
+
|
|
2028
2342
|
it("getThread excludes deleted posts", async () => {
|
|
2029
2343
|
const root = await postService.create({
|
|
2030
2344
|
format: "note",
|