@jant/core 0.6.16 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -11
- package/bin/commands/assets/upload.js +192 -15
- package/bin/commands/import-site.js +60 -4
- package/bin/commands/reset-password.js +21 -8
- package/bin/commands/site/snapshot/export.js +1 -1
- package/bin/commands/site/snapshot/import.js +1 -1
- package/bin/lib/d1-query.js +5 -16
- package/bin/lib/node-env.js +40 -4
- package/bin/lib/site-selection.js +85 -29
- package/bin/lib/wrangler-cli.js +28 -2
- package/bin/lib/wrangler-json.js +35 -0
- package/dist/{app-C7_4oab9.js → app-K_Aa1MMn.js} +52476 -13330
- package/dist/client/.vite/manifest.json +255 -90
- package/dist/client/_assets/chunks/confirm-D7GrwsCN.js +45 -0
- package/dist/client/_assets/chunks/create-editor-CD3FhrOB.js +257 -0
- package/dist/client/_assets/chunks/{native-DpcrFAPh.js → emoji-mart-C4RpRKcU.js} +716 -1
- package/dist/client/_assets/chunks/footnote-rail-CmeoVtnU.js +1 -0
- package/dist/client/_assets/chunks/heic-to-CuP2Qlsw.js +1 -0
- package/dist/client/_assets/chunks/jant-locale-picker-DL6mIOT5.js +69 -0
- package/dist/client/_assets/chunks/lit-DW8VmJAT.js +2 -0
- package/dist/client/_assets/chunks/mediabunny-DRaCQ1r-.js +31 -0
- package/dist/client/_assets/chunks/rolldown-runtime-hePW80VL.js +1 -0
- package/dist/client/_assets/chunks/settings-paths-BcdySNx6.js +84 -0
- package/dist/client/_assets/chunks/site-header-nav-DzXsFk05.js +1 -0
- package/dist/client/_assets/chunks/slug-format-CRXO1AL5.js +1 -0
- package/dist/client/_assets/chunks/slugify-373citbs.js +1 -0
- package/dist/client/_assets/chunks/sortable-list-CgaL2jCs.js +2 -0
- package/dist/client/_assets/chunks/types-D5iF3H1a.js +1 -0
- package/dist/client/_assets/chunks/unsafe-html-B5ZEXaxg.js +1 -0
- package/dist/client/_assets/chunks/unsafe-svg-0QCkP0vZ.js +2 -0
- package/dist/client/_assets/client-GiYENVw8.css +2 -0
- package/dist/client/_assets/client-VnFxJN7G.js +295 -0
- package/dist/client/_assets/client-auth-k8gJ6QJj.js +828 -0
- package/dist/client/_assets/client-author-DQ8R-8KR.css +2 -0
- package/dist/client/_assets/client-compose-B_kDtWkW.js +2004 -0
- package/dist/client/_assets/client-manage-M90aSTOk.js +2170 -0
- package/dist/client/_assets/client-settings-DfYs9n1F.js +1112 -0
- package/dist/{export-Dl5KCiEs.js → github-sync-BPAvT999.js} +2254 -107
- package/dist/index.js +2 -5
- package/dist/node.js +5 -7
- package/package.json +3 -3
- package/src/__tests__/auth-session-freshness.test.ts +57 -0
- package/src/__tests__/basecoat-variants.test.ts +121 -0
- package/src/__tests__/bin/assets-upload.test.ts +68 -0
- package/src/__tests__/export-hugo-build.test.ts +260 -0
- package/src/__tests__/export-service.test.ts +231 -0
- package/src/__tests__/helpers/component-css.ts +91 -0
- package/src/__tests__/helpers/db.ts +33 -0
- package/src/__tests__/helpers/export-fixtures.ts +3 -0
- package/src/__tests__/stylesheet-audience.test.ts +159 -0
- package/src/__tests__/stylesheet-breakpoints.test.ts +183 -0
- package/src/app.tsx +71 -16
- package/src/auth.ts +18 -1
- package/src/client/__tests__/collection-page-actions.test.ts +51 -2
- package/src/client/__tests__/compose-bridge-image-upload.test.ts +196 -0
- package/src/client/__tests__/compose-bridge-video-poster.test.ts +159 -0
- package/src/client/__tests__/compose-bridge.test.ts +272 -0
- package/src/client/__tests__/compose-launch.test.ts +62 -1
- package/src/client/__tests__/compose-shortcuts.test.ts +38 -9
- package/src/client/__tests__/compose-triggers.test.ts +90 -0
- package/src/client/__tests__/copy-field.test.ts +162 -0
- package/src/client/__tests__/custom-url-menu.test.ts +127 -6
- package/src/client/__tests__/image-processor.test.ts +324 -2
- package/src/client/__tests__/lazy-entries.test.ts +92 -0
- package/src/client/__tests__/media-scroll-hint.test.ts +155 -0
- package/src/client/__tests__/runtime-paths.test.ts +84 -0
- package/src/client/__tests__/smart-collection-page-actions.test.ts +158 -0
- package/src/client/__tests__/toast.test.ts +23 -0
- package/src/client/__tests__/video-processor-probe.test.ts +175 -0
- package/src/client/__tests__/video-processor.test.ts +181 -0
- package/src/client/audio-processor.ts +24 -15
- package/src/client/audio-waveform.ts +49 -0
- package/src/client/collection-dialog-host.ts +72 -0
- package/src/client/collection-navigation.ts +51 -16
- package/src/client/collection-page-actions.ts +26 -0
- package/src/client/components/__tests__/jant-collection-dialog.test.ts +329 -0
- package/src/client/components/__tests__/jant-collection-directory.test.ts +274 -10
- package/src/client/components/__tests__/jant-collection-form.test.ts +59 -178
- package/src/client/components/__tests__/jant-command-palette.test.ts +28 -0
- package/src/client/components/__tests__/jant-compose-dialog-draft-storage.test.ts +340 -0
- package/src/client/components/__tests__/jant-compose-dialog.test.ts +1250 -53
- package/src/client/components/__tests__/jant-compose-editor.test.ts +544 -40
- package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +0 -1
- package/src/client/components/__tests__/jant-locale-picker.test.ts +207 -0
- package/src/client/components/__tests__/jant-nav-manager.test.ts +234 -15
- package/src/client/components/__tests__/jant-post-menu.test.ts +413 -2
- package/src/client/components/__tests__/jant-post-picker.test.ts +197 -0
- package/src/client/components/__tests__/jant-settings-avatar.test.ts +15 -12
- package/src/client/components/__tests__/jant-settings-general.test.ts +541 -172
- package/src/client/components/__tests__/jant-settings-language.test.ts +851 -0
- package/src/client/components/__tests__/jant-smart-collection-dialog.test.ts +665 -0
- package/src/client/components/collection-dialog-types.ts +49 -0
- package/src/client/components/collection-manager-types.ts +32 -1
- package/src/client/components/collection-types.ts +1 -15
- package/src/client/components/compose-types.ts +75 -1
- package/src/client/components/jant-collection-dialog.ts +722 -0
- package/src/client/components/jant-collection-directory.ts +567 -182
- package/src/client/components/jant-collection-form.ts +53 -232
- package/src/client/components/jant-command-palette.ts +100 -49
- package/src/client/components/jant-compose-dialog.ts +1486 -707
- package/src/client/components/jant-compose-editor.ts +673 -336
- package/src/client/components/jant-compose-fullscreen.ts +51 -43
- package/src/client/components/jant-config-editor.ts +65 -53
- package/src/client/components/jant-confirm-dialog.ts +13 -11
- package/src/client/components/jant-locale-picker.ts +418 -0
- package/src/client/components/jant-media-lightbox.ts +146 -136
- package/src/client/components/jant-nav-manager.ts +690 -366
- package/src/client/components/jant-post-menu.ts +1086 -282
- package/src/client/components/jant-post-picker.ts +324 -0
- package/src/client/components/jant-repo-picker.ts +138 -106
- package/src/client/components/jant-settings-avatar.ts +31 -27
- package/src/client/components/jant-settings-general.ts +617 -543
- package/src/client/components/jant-settings-language.ts +1087 -0
- package/src/client/components/jant-smart-collection-dialog.ts +1021 -0
- package/src/client/components/jant-text-preview.ts +94 -86
- package/src/client/components/nav-manager-types.ts +62 -1
- package/src/client/components/settings-types.ts +14 -29
- package/src/client/components/smart-collection-conditions.ts +285 -0
- package/src/client/components/smart-collection-dialog-types.ts +68 -0
- package/src/client/compose-bridge.ts +214 -141
- package/src/client/compose-launch.ts +76 -2
- package/src/client/compose-shortcuts.ts +6 -8
- package/src/client/compose-triggers.ts +50 -0
- package/src/client/copy-field.ts +108 -0
- package/src/client/custom-url-menu.ts +60 -4
- package/src/client/icons.ts +14 -0
- package/src/client/image-plan.ts +165 -0
- package/src/client/image-processor.ts +195 -117
- package/src/client/image-worker-client.ts +45 -0
- package/src/client/image-worker.ts +135 -0
- package/src/client/lazy-entries.ts +95 -0
- package/src/client/lazy-slugify.ts +2 -2
- package/src/client/media-scroll-hint.ts +43 -2
- package/src/client/mediabunny.ts +29 -0
- package/src/client/post-picker.ts +41 -0
- package/src/client/runtime-paths.ts +74 -1
- package/src/client/settings-bridge.ts +4 -10
- package/src/client/site-header-nav.js +18 -6
- package/src/client/smart-collection-dialog-host.ts +116 -0
- package/src/client/smart-collection-page-actions.ts +208 -0
- package/src/client/tiptap/__tests__/bubble-menu.test.ts +110 -0
- package/src/client/tiptap/__tests__/floating-position.test.ts +136 -1
- package/src/client/tiptap/__tests__/inline-image-upload.test.ts +37 -0
- package/src/client/tiptap/bubble-menu.ts +47 -16
- package/src/client/tiptap/floating-position.ts +216 -2
- package/src/client/tiptap/inline-image-upload.ts +7 -3
- package/src/client/tiptap/link-toolbar.ts +22 -9
- package/src/client/toast.ts +13 -0
- package/src/client/types/vite-worker.d.ts +12 -0
- package/src/client/upload-with-metadata.ts +3 -13
- package/src/client/video-processor.ts +351 -107
- package/src/client-auth.ts +14 -31
- package/src/client-compose.ts +19 -0
- package/src/client-manage.ts +13 -0
- package/src/client-settings.ts +14 -0
- package/src/client-site.ts +10 -3
- package/src/client.ts +4 -0
- package/src/db/__tests__/backfill-nav-label-mirrors.test.ts +149 -0
- package/src/db/__tests__/schema-enum-sources.test.ts +33 -0
- package/src/db/__tests__/smart-collection-placement.test.ts +402 -0
- package/src/db/backfills/0006_clear_mirrored_page_nav_labels.sql +31 -0
- package/src/db/migrations/0032_married_tomas.sql +3 -0
- package/src/db/migrations/0033_busy_johnny_storm.sql +198 -0
- package/src/db/migrations/0034_cheerful_rumiko_fujikawa.sql +35 -0
- package/src/db/migrations/meta/0032_snapshot.json +2582 -0
- package/src/db/migrations/meta/0033_snapshot.json +2816 -0
- package/src/db/migrations/meta/0034_snapshot.json +2816 -0
- package/src/db/migrations/meta/_journal.json +21 -0
- package/src/db/migrations/pg/0030_abandoned_nighthawk.sql +3 -0
- package/src/db/migrations/pg/0031_strong_ultimatum.sql +128 -0
- package/src/db/migrations/pg/0032_furry_multiple_man.sql +6 -0
- package/src/db/migrations/pg/meta/0030_snapshot.json +3329 -0
- package/src/db/migrations/pg/meta/0031_snapshot.json +3660 -0
- package/src/db/migrations/pg/meta/0032_snapshot.json +3660 -0
- package/src/db/migrations/pg/meta/_journal.json +21 -0
- package/src/db/pg/schema.ts +195 -27
- package/src/db/post-visibility.ts +117 -0
- package/src/db/schema.ts +222 -27
- package/src/i18n/__tests__/detect.test.ts +26 -81
- package/src/i18n/__tests__/supported-locales.test.ts +83 -0
- package/src/i18n/context.tsx +12 -2
- package/src/i18n/detect.ts +7 -66
- package/src/i18n/locales/glossary.zh-Hans.yml +44 -4
- package/src/i18n/locales/glossary.zh-Hant.yml +44 -4
- package/src/i18n/locales/public/en.po +490 -158
- package/src/i18n/locales/public/en.ts +1 -1
- package/src/i18n/locales/public/zh-Hans.po +487 -155
- package/src/i18n/locales/public/zh-Hans.ts +1 -1
- package/src/i18n/locales/public/zh-Hant.po +487 -155
- package/src/i18n/locales/public/zh-Hant.ts +1 -1
- package/src/i18n/locales/settings/en.po +551 -69
- package/src/i18n/locales/settings/en.ts +1 -1
- package/src/i18n/locales/settings/zh-Hans.po +597 -115
- package/src/i18n/locales/settings/zh-Hans.ts +1 -1
- package/src/i18n/locales/settings/zh-Hant.po +610 -128
- package/src/i18n/locales/settings/zh-Hant.ts +1 -1
- package/src/i18n/locales.ts +94 -0
- package/src/i18n/middleware.ts +12 -2
- package/src/i18n/supported-locales.ts +80 -0
- package/src/lib/__tests__/api-settings.test.ts +11 -2
- package/src/lib/__tests__/asset-path.test.ts +92 -0
- package/src/lib/__tests__/deferred.test.ts +90 -0
- package/src/lib/__tests__/discover-ping.test.ts +89 -0
- package/src/lib/__tests__/discover.test.ts +410 -0
- package/src/lib/__tests__/feed-policy.test.ts +26 -0
- package/src/lib/__tests__/feed.test.ts +1526 -15
- package/src/lib/__tests__/filter-dimensions.test.ts +341 -0
- package/src/lib/__tests__/hosted-signin.test.ts +1 -1
- package/src/lib/__tests__/http-cache.test.ts +162 -0
- package/src/lib/__tests__/jant-docs-dev.test.ts +21 -0
- package/src/lib/__tests__/jant-docs.test.ts +23 -0
- package/src/lib/__tests__/lang-detect.test.ts +231 -0
- package/src/lib/__tests__/media-helpers.test.ts +81 -0
- package/src/lib/__tests__/navigation.test.ts +135 -1
- package/src/lib/__tests__/resolve-config.test.ts +64 -3
- package/src/lib/__tests__/schemas.test.ts +42 -4
- package/src/lib/__tests__/site-unavailable-page.test.ts +25 -0
- package/src/lib/__tests__/slug.test.ts +70 -0
- package/src/lib/__tests__/slugify.test.ts +85 -0
- package/src/lib/__tests__/storage.test.ts +32 -0
- package/src/lib/__tests__/thread-fold.test.ts +113 -0
- package/src/lib/__tests__/upload-sideload.test.ts +14 -0
- package/src/lib/__tests__/url-fetch.test.ts +1 -2
- package/src/lib/__tests__/url.test.ts +78 -72
- package/src/lib/__tests__/view-language.test.ts +413 -0
- package/src/lib/__tests__/worker-response-cache.test.ts +90 -0
- package/src/lib/address.ts +57 -0
- package/src/lib/api-posts.ts +12 -0
- package/src/lib/asset-path.ts +98 -1
- package/src/lib/build-env.ts +16 -0
- package/src/lib/collection-groups.ts +5 -1
- package/src/lib/collection-paths.ts +3 -10
- package/src/lib/constants.ts +28 -3
- package/src/lib/copy-field.ts +36 -0
- package/src/lib/crypto.ts +40 -0
- package/src/lib/csp-builder.ts +2 -1
- package/src/lib/deferred.ts +79 -0
- package/src/lib/discover-ping.ts +141 -0
- package/src/lib/discover.ts +437 -0
- package/src/lib/env.ts +134 -0
- package/src/lib/errors.ts +33 -0
- package/src/lib/feed-policy.ts +236 -2
- package/src/lib/feed.ts +926 -73
- package/src/lib/filter-dimensions.ts +1349 -0
- package/src/lib/github-sync-site-config.ts +3 -0
- package/src/lib/hosted-signin.ts +1 -1
- package/src/lib/http-cache.ts +148 -0
- package/src/lib/hugo-markdown.ts +16 -0
- package/src/lib/icons.ts +9 -2
- package/src/lib/ids.ts +3 -0
- package/src/lib/jant-docs.ts +42 -0
- package/src/lib/lang-detect.ts +308 -0
- package/src/lib/media-helpers.ts +13 -1
- package/src/lib/navigation.ts +78 -11
- package/src/lib/pagination.ts +28 -24
- package/src/lib/per-language-surfaces.ts +67 -0
- package/src/lib/post-meta.ts +19 -0
- package/src/lib/render.tsx +55 -7
- package/src/lib/resolve-config.ts +53 -5
- package/src/lib/schemas.ts +201 -10
- package/src/lib/site-header-fragment.tsx +2 -1
- package/src/lib/site-unavailable-page.ts +47 -0
- package/src/lib/slug.ts +51 -7
- package/src/lib/slugify.ts +113 -0
- package/src/lib/startup-config.ts +4 -6
- package/src/lib/storage.ts +7 -0
- package/src/lib/summary.ts +50 -0
- package/src/lib/thread-fold.ts +161 -0
- package/src/lib/time.ts +32 -0
- package/src/lib/timeline.ts +42 -1
- package/src/lib/tiptap-render.ts +1 -2
- package/src/lib/upload.ts +16 -13
- package/src/lib/url.ts +139 -86
- package/src/lib/version.ts +10 -8
- package/src/lib/view-language.ts +436 -0
- package/src/lib/view.ts +51 -23
- package/src/lib/viewer-context.ts +62 -0
- package/src/middleware/__tests__/auth.test.ts +70 -2
- package/src/middleware/__tests__/cache-control.test.ts +71 -3
- package/src/middleware/__tests__/onboarding.test.ts +76 -5
- package/src/middleware/__tests__/secure-headers.test.ts +1 -1
- package/src/middleware/__tests__/session.test.ts +181 -33
- package/src/middleware/auth.ts +50 -31
- package/src/middleware/cache-control.ts +75 -10
- package/src/middleware/config.ts +6 -6
- package/src/middleware/error-handler.ts +12 -0
- package/src/middleware/onboarding.ts +32 -10
- package/src/middleware/secure-headers.ts +1 -1
- package/src/middleware/session.ts +96 -8
- package/src/node/__tests__/cli-migrate.test.ts +21 -19
- package/src/node/__tests__/cli-reset-password.test.ts +81 -8
- package/src/node/__tests__/cli-runtime-target.test.ts +48 -1
- package/src/node/__tests__/cli-site-selection.test.ts +128 -38
- package/src/node/__tests__/runtime.test.ts +37 -0
- package/src/node/index.ts +1 -0
- package/src/preset.css +76 -22
- package/src/routes/__tests__/feed-validator.test.ts +156 -0
- package/src/routes/api/__tests__/collections.test.ts +114 -0
- package/src/routes/api/__tests__/discover.test.ts +222 -0
- package/src/routes/api/__tests__/nav-items.test.ts +212 -2
- package/src/routes/api/__tests__/post-translations.test.ts +829 -0
- package/src/routes/api/__tests__/settings.test.ts +24 -3
- package/src/routes/api/__tests__/telegram.test.ts +6 -5
- package/src/routes/api/collections.ts +49 -6
- package/src/routes/api/custom-urls.ts +0 -1
- package/src/routes/api/discover.ts +95 -0
- package/src/routes/api/export.ts +3 -0
- package/src/routes/api/internal/__tests__/sites.test.ts +42 -2
- package/src/routes/api/nav-items.ts +55 -8
- package/src/routes/api/posts.ts +159 -0
- package/src/routes/api/public/__tests__/archive.test.ts +111 -0
- package/src/routes/api/public/archive.ts +77 -81
- package/src/routes/api/public/posts.ts +23 -6
- package/src/routes/api/smart-collections.ts +132 -0
- package/src/routes/api/telegram.ts +2 -40
- package/src/routes/api/uploads.ts +1 -2
- package/src/routes/auth/__tests__/setup-auth.test.ts +137 -0
- package/src/routes/auth/__tests__/setup-discover.test.ts +204 -0
- package/src/routes/auth/__tests__/setup-flow.test.ts +270 -0
- package/src/routes/auth/__tests__/setup-page.test.tsx +329 -0
- package/src/routes/auth/__tests__/setup-steps.test.ts +310 -0
- package/src/routes/auth/__tests__/setup.test.ts +278 -26
- package/src/routes/auth/__tests__/signin.test.ts +135 -0
- package/src/routes/auth/setup.tsx +934 -186
- package/src/routes/auth/signin.tsx +10 -8
- package/src/routes/compose.tsx +5 -0
- package/src/routes/dash/__tests__/custom-urls-archive.test.ts +44 -0
- package/src/routes/dash/__tests__/custom-urls-page.test.ts +49 -0
- package/src/routes/dash/__tests__/delete-account.test.ts +106 -0
- package/src/routes/dash/__tests__/settings-discover.test.ts +213 -0
- package/src/routes/dash/custom-urls.tsx +1 -37
- package/src/routes/dash/settings.tsx +502 -41
- package/src/routes/discover-announce.ts +96 -0
- package/src/routes/feed/__tests__/feed.test.ts +165 -2
- package/src/routes/feed/feed.ts +69 -40
- package/src/routes/feed/sitemap.ts +95 -11
- package/src/routes/pages/__tests__/archive-indexing.test.ts +200 -0
- package/src/routes/pages/__tests__/archive-params.test.ts +370 -6
- package/src/routes/pages/__tests__/collection-routing.test.ts +16 -0
- package/src/routes/pages/__tests__/collections-feed-links.test.ts +72 -0
- package/src/routes/pages/__tests__/collections.test.ts +9 -3
- package/src/routes/pages/__tests__/custom-archive-url.test.ts +119 -0
- package/src/routes/pages/__tests__/feed-autodiscovery.test.ts +148 -0
- package/src/routes/pages/__tests__/language-routing.test.ts +679 -0
- package/src/routes/pages/__tests__/post-preview-partial.test.ts +96 -0
- package/src/routes/pages/__tests__/smart-collection.test.ts +298 -0
- package/src/routes/pages/__tests__/subscribe.test.ts +248 -0
- package/src/routes/pages/archive.tsx +471 -369
- package/src/routes/pages/collection.tsx +94 -96
- package/src/routes/pages/collections.tsx +38 -50
- package/src/routes/pages/featured.tsx +32 -13
- package/src/routes/pages/home.tsx +25 -6
- package/src/routes/pages/language.tsx +113 -0
- package/src/routes/pages/latest.tsx +26 -16
- package/src/routes/pages/new.tsx +3 -0
- package/src/routes/pages/page.tsx +178 -17
- package/src/routes/pages/partials.tsx +41 -0
- package/src/routes/pages/search.tsx +18 -3
- package/src/routes/pages/smart-collection.tsx +340 -0
- package/src/routes/pages/subscribe.tsx +182 -0
- package/src/runtime/__tests__/index.test.ts +19 -0
- package/src/runtime/__tests__/node.test.ts +129 -4
- package/src/runtime/cloudflare.ts +21 -2
- package/src/runtime/index.ts +27 -1
- package/src/runtime/node.ts +18 -1
- package/src/runtime/site.ts +95 -24
- package/src/services/__tests__/auth.test.ts +116 -7
- package/src/services/__tests__/collection.test.ts +108 -4
- package/src/services/__tests__/language.test.ts +495 -0
- package/src/services/__tests__/navigation.test.ts +213 -10
- package/src/services/__tests__/path.test.ts +180 -0
- package/src/services/__tests__/post-language.test.ts +830 -0
- package/src/services/__tests__/post.test.ts +46 -9
- package/src/services/__tests__/settings.test.ts +228 -17
- package/src/services/__tests__/site-admin.test.ts +5 -5
- package/src/services/__tests__/smart-collection.test.ts +651 -0
- package/src/services/about-page.ts +22 -1
- package/src/services/auth.ts +51 -49
- package/src/services/bootstrap.ts +188 -38
- package/src/services/collection-directory-position.ts +76 -0
- package/src/services/collection.ts +331 -46
- package/src/services/custom-url.ts +27 -22
- package/src/services/export-theme/assets/client-site.css +1 -1
- package/src/services/export-theme/assets/client-site.js +123 -123
- package/src/services/export-theme/layouts/_default/rss.xml +77 -1
- package/src/services/export-theme/layouts/partials/feed-post-content.xml +105 -2
- package/src/services/export-theme/layouts/partials/media-gallery.html +35 -21
- package/src/services/export-theme/layouts/partials/pagination.html +14 -13
- package/src/services/export-theme/layouts/partials/post-card.html +8 -4
- package/src/services/export-theme/layouts/partials/reply.html +2 -1
- package/src/services/export-theme/layouts/partials/thread-preview.html +40 -30
- package/src/services/export-theme/styles/main.css +13 -1
- package/src/services/export.ts +278 -19
- package/src/services/index.ts +34 -3
- package/src/services/language.ts +412 -0
- package/src/services/mcp.ts +3 -1
- package/src/services/media.ts +14 -6
- package/src/services/navigation.ts +429 -67
- package/src/services/path.ts +221 -6
- package/src/services/post.ts +1021 -152
- package/src/services/search.ts +39 -2
- package/src/services/settings.ts +238 -43
- package/src/services/site-admin.ts +24 -7
- package/src/services/site.ts +77 -1
- package/src/services/smart-collection.ts +636 -0
- package/src/services/telegram.ts +20 -22
- package/src/services/upload-session.ts +1 -3
- package/src/style-author.css +27 -0
- package/src/styles/components-author.css +1622 -0
- package/src/styles/components.css +24 -1571
- package/src/styles/site-media.css +16 -2
- package/src/styles/tokens.css +82 -12
- package/src/styles/ui-author.css +7602 -0
- package/src/styles/ui.css +1087 -7219
- package/src/types/app-context.ts +7 -0
- package/src/types/bindings.ts +3 -0
- package/src/types/config.ts +94 -15
- package/src/types/constants.ts +188 -16
- package/src/types/entities.ts +87 -1
- package/src/types/operations.ts +64 -2
- package/src/types/props.ts +157 -31
- package/src/types/views.ts +55 -1
- package/src/ui/__tests__/font-themes.test.ts +52 -12
- package/src/ui/compose/ComposeDialog.tsx +201 -7
- package/src/ui/compose/ComposePrompt.tsx +1 -5
- package/src/ui/compose/__tests__/ComposeDialog.test.tsx +125 -0
- package/src/ui/dash/appearance/AdvancedContent.tsx +2 -2
- package/src/ui/dash/appearance/CodeInjectionContent.tsx +12 -16
- package/src/ui/dash/appearance/NavigationContent.tsx +117 -16
- package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +33 -11
- package/src/ui/dash/appearance/__tests__/system-nav-descriptions.test.ts +44 -0
- package/src/ui/dash/appearance/system-nav-descriptions.ts +75 -0
- package/src/ui/dash/settings/AccountMenuContent.tsx +2 -2
- package/src/ui/dash/settings/ApiTokensContent.tsx +2 -1
- package/src/ui/dash/settings/ConfigEditorContent.tsx +35 -16
- package/src/ui/dash/settings/GeneralContent.tsx +275 -113
- package/src/ui/dash/settings/LanguageContent.tsx +409 -0
- package/src/ui/dash/settings/SettingsRootContent.tsx +19 -1
- package/src/ui/dash/settings/__tests__/GeneralContent.test.tsx +324 -4
- package/src/ui/dash/settings/__tests__/LanguageContent.test.tsx +144 -0
- package/src/ui/dash/settings/__tests__/discover-copy.test.ts +66 -0
- package/src/ui/dash/settings/discover-copy.ts +116 -0
- package/src/ui/feed/LinkCard.tsx +55 -45
- package/src/ui/feed/NoteCard.tsx +9 -4
- package/src/ui/feed/PostStatusBadges.tsx +90 -43
- package/src/ui/feed/QuoteCard.tsx +55 -30
- package/src/ui/feed/ThreadPreview.tsx +6 -2
- package/src/ui/feed/__tests__/thread-preview.test.ts +13 -16
- package/src/ui/feed/__tests__/timeline-cards.test.ts +314 -49
- package/src/ui/feed/thread-preview-state.ts +20 -6
- package/src/ui/font-themes.ts +39 -29
- package/src/ui/layouts/BaseLayout.tsx +155 -10
- package/src/ui/layouts/SiteLayout.tsx +192 -34
- package/src/ui/layouts/__tests__/BaseLayout.test.tsx +196 -17
- package/src/ui/layouts/__tests__/SiteLayout.test.tsx +109 -85
- package/src/ui/pages/ArchivePage.tsx +282 -289
- package/src/ui/pages/CollectionPage.tsx +86 -76
- package/src/ui/pages/CollectionsPage.tsx +5 -0
- package/src/ui/pages/ComposePage.tsx +8 -0
- package/src/ui/pages/PostPage.tsx +47 -0
- package/src/ui/pages/SearchPage.tsx +3 -3
- package/src/ui/pages/SmartCollectionPage.tsx +434 -0
- package/src/ui/pages/SubscribePage.tsx +236 -0
- package/src/ui/pages/__tests__/ArchivePage.test.tsx +65 -9
- package/src/ui/pages/__tests__/collections-directory-rows.test.tsx +92 -0
- package/src/ui/pages/__tests__/collections-view-links.test.tsx +81 -0
- package/src/ui/shared/CollectionDirectory.tsx +178 -3
- package/src/ui/shared/CollectionsManager.tsx +70 -17
- package/src/ui/shared/CopyField.tsx +127 -0
- package/src/ui/shared/EmptyPostContent.tsx +23 -0
- package/src/ui/shared/MediaGallery.tsx +125 -46
- package/src/ui/shared/Pagination.tsx +20 -5
- package/src/ui/shared/PostFooter.tsx +20 -11
- package/src/ui/shared/__tests__/archive-labels.test.ts +100 -0
- package/src/ui/shared/__tests__/media-gallery.test.ts +210 -2
- package/src/ui/shared/__tests__/navigation-labels.test.ts +16 -22
- package/src/ui/shared/__tests__/pagination.test.ts +38 -24
- package/src/ui/shared/__tests__/post-footer.test.ts +32 -0
- package/src/ui/shared/archive-labels.ts +98 -0
- package/src/ui/shared/collection-management-labels.ts +94 -16
- package/src/ui/shared/navigation-labels.ts +45 -71
- package/src/ui/shared/post-article-attributes.ts +2 -0
- package/src/ui/shared/smart-collection-labels.ts +343 -0
- package/dist/app-C75Zc8dD.js +0 -6
- package/dist/client/_assets/chunks/heic-to-DUUaO23q.js +0 -1
- package/dist/client/_assets/chunks/module-DcsAZQZ_.js +0 -716
- package/dist/client/_assets/chunks/url-CU9pEbk5.js +0 -1
- package/dist/client/_assets/client-Bq0vre8Y.js +0 -296
- package/dist/client/_assets/client-D3VZyG4L.css +0 -2
- package/dist/client/_assets/client-auth-F2rb9WfF.js +0 -5571
- package/dist/env-BPYViDJJ.js +0 -281
- package/dist/github-api-BNF35Lkx.js +0 -176
- package/dist/github-app-DdiD-bC4.js +0 -275
- package/dist/github-sync-Bqg62IvV.js +0 -4
- package/dist/github-sync-HdK2EgvJ.js +0 -431
- package/dist/url-CbLAtlZe.js +0 -900
- package/src/client/__tests__/collection-created-notice.test.ts +0 -36
- package/src/client/__tests__/collection-form-bridge.test.ts +0 -216
- package/src/client/__tests__/post-form-bridge.test.ts +0 -82
- package/src/client/collection-created-notice.ts +0 -66
- package/src/client/collection-form-bridge.ts +0 -191
- package/src/client/components/__tests__/jant-post-form.test.ts +0 -193
- package/src/client/components/jant-post-form.ts +0 -422
- package/src/client/components/post-form-template.ts +0 -352
- package/src/client/components/post-form-types.ts +0 -96
- package/src/client/media-metadata.ts +0 -247
- package/src/client/post-form-bridge.ts +0 -193
- package/src/ui/pages/CollectionEditorPage.tsx +0 -175
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The compose card and the uploaded media used to draw their poster from two
|
|
5
|
+
* different places: the card from a quick `<video>` frame grab, the upload
|
|
6
|
+
* from the mediabunny probe. When the probe came back without one, the card
|
|
7
|
+
* showed a thumbnail the published post did not have. The quick frame is now
|
|
8
|
+
* the upload's fallback, so the two cannot disagree.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
12
|
+
|
|
13
|
+
const processToFile = vi.fn();
|
|
14
|
+
const uploadViaSession = vi.fn();
|
|
15
|
+
|
|
16
|
+
vi.mock("../video-processor.js", () => ({
|
|
17
|
+
VideoProcessor: {
|
|
18
|
+
isSupported: () => true,
|
|
19
|
+
processToFile: (...args: unknown[]) => processToFile(...args),
|
|
20
|
+
},
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
vi.mock("../upload-session.js", () => ({
|
|
24
|
+
uploadViaSession: (...args: unknown[]) => uploadViaSession(...args),
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
await import("../compose-bridge.js");
|
|
28
|
+
|
|
29
|
+
const QUICK_POSTER = new Blob(["quick"], { type: "image/webp" });
|
|
30
|
+
const PROBE_POSTER = new Blob(["probe"], { type: "image/webp" });
|
|
31
|
+
|
|
32
|
+
/** Stand in for the `<video>` element `captureQuickPoster` grabs a frame from. */
|
|
33
|
+
function stubFrameCapture(options: { decodes: boolean }) {
|
|
34
|
+
vi.spyOn(document, "createElement").mockImplementation(((tag: string) => {
|
|
35
|
+
if (tag === "canvas") {
|
|
36
|
+
return {
|
|
37
|
+
width: 0,
|
|
38
|
+
height: 0,
|
|
39
|
+
getContext: () => ({ drawImage: () => {} }),
|
|
40
|
+
toBlob: (cb: (blob: Blob | null) => void) => cb(QUICK_POSTER),
|
|
41
|
+
} as unknown as HTMLElement;
|
|
42
|
+
}
|
|
43
|
+
const video = {
|
|
44
|
+
muted: false,
|
|
45
|
+
playsInline: false,
|
|
46
|
+
preload: "",
|
|
47
|
+
videoWidth: 1280,
|
|
48
|
+
videoHeight: 720,
|
|
49
|
+
onloadeddata: null as null | (() => void),
|
|
50
|
+
onerror: null as null | (() => void),
|
|
51
|
+
removeAttribute: () => {},
|
|
52
|
+
load: () => {},
|
|
53
|
+
set src(_value: string) {
|
|
54
|
+
queueMicrotask(() =>
|
|
55
|
+
options.decodes ? this.onloadeddata?.() : this.onerror?.(),
|
|
56
|
+
);
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
return video as unknown as HTMLElement;
|
|
60
|
+
}) as typeof document.createElement);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function selectVideoFile() {
|
|
64
|
+
document.dispatchEvent(
|
|
65
|
+
new CustomEvent("jant:files-selected", {
|
|
66
|
+
detail: {
|
|
67
|
+
files: [
|
|
68
|
+
{
|
|
69
|
+
file: new File(["source"], "clip.mov", {
|
|
70
|
+
type: "video/quicktime",
|
|
71
|
+
}),
|
|
72
|
+
clientId: "attachment-1",
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
},
|
|
76
|
+
}),
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function settle() {
|
|
81
|
+
for (let i = 0; i < 6; i++) {
|
|
82
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function uploadedPoster(): Blob | undefined {
|
|
87
|
+
const metadata = uploadViaSession.mock.calls[0]?.[1] as
|
|
88
|
+
{ poster?: Blob } | undefined;
|
|
89
|
+
return metadata?.poster;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
describe("compose bridge video poster", () => {
|
|
93
|
+
beforeEach(() => {
|
|
94
|
+
document.body.innerHTML = "";
|
|
95
|
+
vi.restoreAllMocks();
|
|
96
|
+
processToFile.mockReset();
|
|
97
|
+
uploadViaSession.mockReset();
|
|
98
|
+
uploadViaSession.mockResolvedValue({
|
|
99
|
+
id: "med_1",
|
|
100
|
+
filename: "clip.mp4",
|
|
101
|
+
url: "/media/clip.mp4",
|
|
102
|
+
mimeType: "video/mp4",
|
|
103
|
+
size: 10,
|
|
104
|
+
});
|
|
105
|
+
vi.stubGlobal("URL", {
|
|
106
|
+
createObjectURL: () => "blob:test",
|
|
107
|
+
revokeObjectURL: () => {},
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("uploads the probe's poster when it has one", async () => {
|
|
112
|
+
stubFrameCapture({ decodes: true });
|
|
113
|
+
processToFile.mockResolvedValue({
|
|
114
|
+
file: new File(["out"], "clip.mp4", { type: "video/mp4" }),
|
|
115
|
+
width: 1280,
|
|
116
|
+
height: 720,
|
|
117
|
+
poster: PROBE_POSTER,
|
|
118
|
+
blurhash: "LGK",
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
selectVideoFile();
|
|
122
|
+
await settle();
|
|
123
|
+
|
|
124
|
+
expect(uploadedPoster()).toBe(PROBE_POSTER);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("falls back to the quick frame when the probe found no poster", async () => {
|
|
128
|
+
stubFrameCapture({ decodes: true });
|
|
129
|
+
processToFile.mockResolvedValue({
|
|
130
|
+
file: new File(["out"], "clip.mp4", { type: "video/mp4" }),
|
|
131
|
+
width: 1280,
|
|
132
|
+
height: 720,
|
|
133
|
+
poster: undefined,
|
|
134
|
+
blurhash: undefined,
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
selectVideoFile();
|
|
138
|
+
await settle();
|
|
139
|
+
|
|
140
|
+
expect(uploadViaSession).toHaveBeenCalledTimes(1);
|
|
141
|
+
expect(uploadedPoster()).toBe(QUICK_POSTER);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("uploads without a poster when neither path produced one", async () => {
|
|
145
|
+
stubFrameCapture({ decodes: false });
|
|
146
|
+
processToFile.mockResolvedValue({
|
|
147
|
+
file: new File(["out"], "clip.mp4", { type: "video/mp4" }),
|
|
148
|
+
width: 1280,
|
|
149
|
+
height: 720,
|
|
150
|
+
poster: undefined,
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
selectVideoFile();
|
|
154
|
+
await settle();
|
|
155
|
+
|
|
156
|
+
expect(uploadViaSession).toHaveBeenCalledTimes(1);
|
|
157
|
+
expect(uploadedPoster()).toBeUndefined();
|
|
158
|
+
});
|
|
159
|
+
});
|
|
@@ -43,6 +43,46 @@ async function flushBridgeWork(times = 4) {
|
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
/** A minimal published-post detail, as the composer dispatches it. */
|
|
47
|
+
function singlePostDetail(overrides: Record<string, unknown> = {}) {
|
|
48
|
+
return {
|
|
49
|
+
format: "quote",
|
|
50
|
+
title: "",
|
|
51
|
+
body: '{"type":"doc","content":[]}',
|
|
52
|
+
url: "",
|
|
53
|
+
quoteText: "A borrowed sentence.",
|
|
54
|
+
quoteAuthor: "",
|
|
55
|
+
slug: "",
|
|
56
|
+
status: "published",
|
|
57
|
+
visibility: "public",
|
|
58
|
+
rating: 0,
|
|
59
|
+
collectionIds: [],
|
|
60
|
+
attachments: [],
|
|
61
|
+
...overrides,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** The same, as a two-quote thread. */
|
|
66
|
+
function threadSubmitDetail(overrides: Record<string, unknown> = {}) {
|
|
67
|
+
const first = singlePostDetail();
|
|
68
|
+
return {
|
|
69
|
+
...first,
|
|
70
|
+
pendingAttachments: [],
|
|
71
|
+
threadPosts: [first, singlePostDetail({ quoteText: "And its answer." })],
|
|
72
|
+
...overrides,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Text of the compose progress toast, which the result message takes over. */
|
|
77
|
+
function progressToastText(): string {
|
|
78
|
+
return (
|
|
79
|
+
document
|
|
80
|
+
.getElementById("toast-compose-deferred")
|
|
81
|
+
?.querySelector("span")
|
|
82
|
+
?.textContent?.trim() ?? ""
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
46
86
|
function renderPostView(postId: string) {
|
|
47
87
|
const postView = document.createElement("div");
|
|
48
88
|
postView.dataset.postView = "";
|
|
@@ -968,4 +1008,236 @@ describe("compose bridge", () => {
|
|
|
968
1008
|
|
|
969
1009
|
expect(assignSpy).toHaveBeenCalledWith("/blog/published-post");
|
|
970
1010
|
});
|
|
1011
|
+
|
|
1012
|
+
it("saves a thread as a draft when the connection drops, not just when the server refuses", async () => {
|
|
1013
|
+
// A publish that never reaches the server and one the server rejects are
|
|
1014
|
+
// the same event to the author: the post did not go up. Both earn the
|
|
1015
|
+
// save-as-draft attempt.
|
|
1016
|
+
const composeEl = document.createElement(
|
|
1017
|
+
"jant-compose-dialog",
|
|
1018
|
+
) as ComposeHarness;
|
|
1019
|
+
composeEl.refreshCollections = vi.fn(async () => true);
|
|
1020
|
+
composeEl.pageMode = false;
|
|
1021
|
+
composeEl.openNew = vi.fn(async () => {});
|
|
1022
|
+
composeEl.clearLocalDraftFromStorage = vi.fn();
|
|
1023
|
+
composeEl.labels = {
|
|
1024
|
+
publishFailedDraft: "Couldn't publish. Saved as draft.",
|
|
1025
|
+
};
|
|
1026
|
+
document.body.appendChild(composeEl);
|
|
1027
|
+
|
|
1028
|
+
const bodies: string[] = [];
|
|
1029
|
+
vi.spyOn(globalThis, "fetch").mockImplementation(async (input, init) => {
|
|
1030
|
+
const raw =
|
|
1031
|
+
typeof input === "string"
|
|
1032
|
+
? input
|
|
1033
|
+
: input instanceof URL
|
|
1034
|
+
? input.toString()
|
|
1035
|
+
: input.url;
|
|
1036
|
+
if (new URL(raw, "http://localhost").pathname !== "/compose/thread") {
|
|
1037
|
+
throw new Error(`Unexpected fetch: ${raw}`);
|
|
1038
|
+
}
|
|
1039
|
+
const body = String(init?.body ?? "");
|
|
1040
|
+
bodies.push(body);
|
|
1041
|
+
// The publish attempt never completes; the draft attempt gets through.
|
|
1042
|
+
if (bodies.length === 1) throw new TypeError("Failed to fetch");
|
|
1043
|
+
return new Response(JSON.stringify({ status: "draft" }), {
|
|
1044
|
+
status: 200,
|
|
1045
|
+
headers: { "Content-Type": "application/json" },
|
|
1046
|
+
});
|
|
1047
|
+
});
|
|
1048
|
+
|
|
1049
|
+
composeEl.dispatchEvent(
|
|
1050
|
+
new CustomEvent("jant:compose-submit-deferred", {
|
|
1051
|
+
bubbles: true,
|
|
1052
|
+
detail: threadSubmitDetail(),
|
|
1053
|
+
}),
|
|
1054
|
+
);
|
|
1055
|
+
await flushBridgeWork();
|
|
1056
|
+
|
|
1057
|
+
expect(bodies).toHaveLength(2);
|
|
1058
|
+
expect(JSON.parse(bodies[1]).posts[0].status).toBe("draft");
|
|
1059
|
+
// The work reached the server, so the local copy is no longer the only one.
|
|
1060
|
+
expect(composeEl.clearLocalDraftFromStorage).toHaveBeenCalled();
|
|
1061
|
+
expect(composeEl.openNew).not.toHaveBeenCalled();
|
|
1062
|
+
});
|
|
1063
|
+
|
|
1064
|
+
it("reopens the composer with its local draft when the connection stays down", async () => {
|
|
1065
|
+
const composeEl = document.createElement(
|
|
1066
|
+
"jant-compose-dialog",
|
|
1067
|
+
) as ComposeHarness;
|
|
1068
|
+
composeEl.refreshCollections = vi.fn(async () => true);
|
|
1069
|
+
composeEl.pageMode = false;
|
|
1070
|
+
composeEl.openNew = vi.fn(async () => {});
|
|
1071
|
+
composeEl.clearLocalDraftFromStorage = vi.fn();
|
|
1072
|
+
document.body.appendChild(composeEl);
|
|
1073
|
+
|
|
1074
|
+
let calls = 0;
|
|
1075
|
+
vi.spyOn(globalThis, "fetch").mockImplementation(async () => {
|
|
1076
|
+
calls += 1;
|
|
1077
|
+
throw new TypeError("Failed to fetch");
|
|
1078
|
+
});
|
|
1079
|
+
|
|
1080
|
+
composeEl.dispatchEvent(
|
|
1081
|
+
new CustomEvent("jant:compose-submit-deferred", {
|
|
1082
|
+
bubbles: true,
|
|
1083
|
+
detail: threadSubmitDetail(),
|
|
1084
|
+
}),
|
|
1085
|
+
);
|
|
1086
|
+
await flushBridgeWork();
|
|
1087
|
+
|
|
1088
|
+
// Publish, then the draft attempt — both dropped.
|
|
1089
|
+
expect(calls).toBe(2);
|
|
1090
|
+
expect(composeEl.openNew).toHaveBeenCalledWith({
|
|
1091
|
+
restoreDraft: true,
|
|
1092
|
+
restoreToast: false,
|
|
1093
|
+
restoreMedia: [],
|
|
1094
|
+
});
|
|
1095
|
+
// Nothing reached the server, so the local copy must stay.
|
|
1096
|
+
expect(composeEl.clearLocalDraftFromStorage).not.toHaveBeenCalled();
|
|
1097
|
+
});
|
|
1098
|
+
|
|
1099
|
+
it("saves a single post as a draft when the connection drops", async () => {
|
|
1100
|
+
const composeEl = document.createElement(
|
|
1101
|
+
"jant-compose-dialog",
|
|
1102
|
+
) as ComposeHarness;
|
|
1103
|
+
composeEl.refreshCollections = vi.fn(async () => true);
|
|
1104
|
+
composeEl.pageMode = false;
|
|
1105
|
+
composeEl.openNew = vi.fn(async () => {});
|
|
1106
|
+
composeEl.clearLocalDraftFromStorage = vi.fn();
|
|
1107
|
+
document.body.appendChild(composeEl);
|
|
1108
|
+
|
|
1109
|
+
const bodies: string[] = [];
|
|
1110
|
+
vi.spyOn(globalThis, "fetch").mockImplementation(async (input, init) => {
|
|
1111
|
+
const raw =
|
|
1112
|
+
typeof input === "string"
|
|
1113
|
+
? input
|
|
1114
|
+
: input instanceof URL
|
|
1115
|
+
? input.toString()
|
|
1116
|
+
: input.url;
|
|
1117
|
+
if (new URL(raw, "http://localhost").pathname !== "/compose") {
|
|
1118
|
+
throw new Error(`Unexpected fetch: ${raw}`);
|
|
1119
|
+
}
|
|
1120
|
+
bodies.push(String(init?.body ?? ""));
|
|
1121
|
+
if (bodies.length === 1) throw new TypeError("Failed to fetch");
|
|
1122
|
+
return new Response(JSON.stringify({ status: "draft" }), {
|
|
1123
|
+
status: 200,
|
|
1124
|
+
headers: { "Content-Type": "application/json" },
|
|
1125
|
+
});
|
|
1126
|
+
});
|
|
1127
|
+
|
|
1128
|
+
composeEl.dispatchEvent(
|
|
1129
|
+
new CustomEvent("jant:compose-submit-deferred", {
|
|
1130
|
+
bubbles: true,
|
|
1131
|
+
detail: { ...singlePostDetail(), pendingAttachments: [] },
|
|
1132
|
+
}),
|
|
1133
|
+
);
|
|
1134
|
+
await flushBridgeWork();
|
|
1135
|
+
|
|
1136
|
+
expect(bodies).toHaveLength(2);
|
|
1137
|
+
expect(JSON.parse(bodies[1]).status).toBe("draft");
|
|
1138
|
+
});
|
|
1139
|
+
|
|
1140
|
+
it("names the publish in a toast while the request is still running", async () => {
|
|
1141
|
+
const composeEl = document.createElement(
|
|
1142
|
+
"jant-compose-dialog",
|
|
1143
|
+
) as ComposeHarness;
|
|
1144
|
+
composeEl.refreshCollections = vi.fn(async () => true);
|
|
1145
|
+
composeEl.pageMode = true;
|
|
1146
|
+
composeEl.reset = vi.fn();
|
|
1147
|
+
composeEl.updateComplete = Promise.resolve();
|
|
1148
|
+
document.body.appendChild(composeEl);
|
|
1149
|
+
|
|
1150
|
+
let release: (() => void) | undefined;
|
|
1151
|
+
const inFlight = new Promise<void>((resolve) => {
|
|
1152
|
+
release = resolve;
|
|
1153
|
+
});
|
|
1154
|
+
|
|
1155
|
+
vi.spyOn(globalThis, "fetch").mockImplementation(async (input) => {
|
|
1156
|
+
const raw =
|
|
1157
|
+
typeof input === "string"
|
|
1158
|
+
? input
|
|
1159
|
+
: input instanceof URL
|
|
1160
|
+
? input.toString()
|
|
1161
|
+
: input.url;
|
|
1162
|
+
if (new URL(raw, "http://localhost").pathname !== "/compose") {
|
|
1163
|
+
throw new Error(`Unexpected fetch: ${raw}`);
|
|
1164
|
+
}
|
|
1165
|
+
// A Link post waits on the server while its preview thumbnail is
|
|
1166
|
+
// fetched and stored — the toast has to survive that whole wait.
|
|
1167
|
+
await inFlight;
|
|
1168
|
+
return new Response(
|
|
1169
|
+
JSON.stringify({ status: "published", permalink: "/a-link" }),
|
|
1170
|
+
{ status: 200, headers: { "Content-Type": "application/json" } },
|
|
1171
|
+
);
|
|
1172
|
+
});
|
|
1173
|
+
|
|
1174
|
+
composeEl.dispatchEvent(
|
|
1175
|
+
new CustomEvent("jant:compose-submit-deferred", {
|
|
1176
|
+
bubbles: true,
|
|
1177
|
+
detail: {
|
|
1178
|
+
...singlePostDetail({
|
|
1179
|
+
format: "link",
|
|
1180
|
+
url: "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
|
|
1181
|
+
quoteText: "",
|
|
1182
|
+
}),
|
|
1183
|
+
pendingAttachments: [],
|
|
1184
|
+
},
|
|
1185
|
+
}),
|
|
1186
|
+
);
|
|
1187
|
+
|
|
1188
|
+
await flushBridgeWork();
|
|
1189
|
+
expect(progressToastText()).toBe("Publishing...");
|
|
1190
|
+
|
|
1191
|
+
release?.();
|
|
1192
|
+
await flushBridgeWork();
|
|
1193
|
+
expect(progressToastText()).toBe("Published!");
|
|
1194
|
+
});
|
|
1195
|
+
|
|
1196
|
+
it("names a draft save while the request is still running", async () => {
|
|
1197
|
+
const composeEl = document.createElement(
|
|
1198
|
+
"jant-compose-dialog",
|
|
1199
|
+
) as ComposeHarness;
|
|
1200
|
+
composeEl.refreshCollections = vi.fn(async () => true);
|
|
1201
|
+
composeEl.pageMode = false;
|
|
1202
|
+
document.body.appendChild(composeEl);
|
|
1203
|
+
|
|
1204
|
+
let release: (() => void) | undefined;
|
|
1205
|
+
const inFlight = new Promise<void>((resolve) => {
|
|
1206
|
+
release = resolve;
|
|
1207
|
+
});
|
|
1208
|
+
|
|
1209
|
+
vi.spyOn(globalThis, "fetch").mockImplementation(async (input) => {
|
|
1210
|
+
const raw =
|
|
1211
|
+
typeof input === "string"
|
|
1212
|
+
? input
|
|
1213
|
+
: input instanceof URL
|
|
1214
|
+
? input.toString()
|
|
1215
|
+
: input.url;
|
|
1216
|
+
if (new URL(raw, "http://localhost").pathname !== "/compose") {
|
|
1217
|
+
throw new Error(`Unexpected fetch: ${raw}`);
|
|
1218
|
+
}
|
|
1219
|
+
await inFlight;
|
|
1220
|
+
return new Response(
|
|
1221
|
+
JSON.stringify({ status: "draft", toast: "Draft saved." }),
|
|
1222
|
+
{ status: 200, headers: { "Content-Type": "application/json" } },
|
|
1223
|
+
);
|
|
1224
|
+
});
|
|
1225
|
+
|
|
1226
|
+
composeEl.dispatchEvent(
|
|
1227
|
+
new CustomEvent("jant:compose-submit-deferred", {
|
|
1228
|
+
bubbles: true,
|
|
1229
|
+
detail: {
|
|
1230
|
+
...singlePostDetail({ status: "draft" }),
|
|
1231
|
+
pendingAttachments: [],
|
|
1232
|
+
},
|
|
1233
|
+
}),
|
|
1234
|
+
);
|
|
1235
|
+
|
|
1236
|
+
await flushBridgeWork();
|
|
1237
|
+
expect(progressToastText()).toBe("Saving...");
|
|
1238
|
+
|
|
1239
|
+
release?.();
|
|
1240
|
+
await flushBridgeWork();
|
|
1241
|
+
expect(progressToastText()).toBe("Draft saved.");
|
|
1242
|
+
});
|
|
971
1243
|
});
|
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
// @vitest-environment happy-dom
|
|
2
2
|
|
|
3
|
-
import { afterEach, describe, expect, it } from "vitest";
|
|
3
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
|
|
5
|
+
const lazyEntries = vi.hoisted(() => ({
|
|
6
|
+
ensureCompose: vi.fn(async () => {}),
|
|
7
|
+
}));
|
|
8
|
+
|
|
9
|
+
vi.mock("../lazy-entries.js", () => lazyEntries);
|
|
10
|
+
|
|
4
11
|
import {
|
|
12
|
+
ensureComposeDialog,
|
|
5
13
|
getReplyRefreshTarget,
|
|
6
14
|
getReplyTargetArticle,
|
|
15
|
+
readComposeDialogLabels,
|
|
7
16
|
} from "../compose-launch.js";
|
|
8
17
|
|
|
9
18
|
function getArticle(): HTMLElement {
|
|
@@ -167,3 +176,55 @@ describe("getReplyTargetArticle", () => {
|
|
|
167
176
|
);
|
|
168
177
|
});
|
|
169
178
|
});
|
|
179
|
+
|
|
180
|
+
describe("ensureComposeDialog", () => {
|
|
181
|
+
afterEach(() => {
|
|
182
|
+
document.body.innerHTML = "";
|
|
183
|
+
lazyEntries.ensureCompose.mockClear();
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("loads the composer's bundle before handing the element back", async () => {
|
|
187
|
+
document.body.innerHTML = `<jant-compose-dialog></jant-compose-dialog>`;
|
|
188
|
+
|
|
189
|
+
const dialog = await ensureComposeDialog();
|
|
190
|
+
|
|
191
|
+
expect(lazyEntries.ensureCompose).toHaveBeenCalledTimes(1);
|
|
192
|
+
expect(dialog).toBe(document.querySelector("jant-compose-dialog"));
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it("loads nothing on a page without a composer", async () => {
|
|
196
|
+
expect(await ensureComposeDialog()).toBeNull();
|
|
197
|
+
expect(lazyEntries.ensureCompose).not.toHaveBeenCalled();
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
describe("readComposeDialogLabels", () => {
|
|
202
|
+
afterEach(() => {
|
|
203
|
+
document.body.innerHTML = "";
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it("reads the server-rendered attribute before the element upgrades", () => {
|
|
207
|
+
document.body.innerHTML = `<jant-compose-dialog labels='{"addCollection":"Add Collection"}'></jant-compose-dialog>`;
|
|
208
|
+
|
|
209
|
+
expect(readComposeDialogLabels()?.addCollection).toBe("Add Collection");
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it("prefers the upgraded element's property", () => {
|
|
213
|
+
document.body.innerHTML = `<jant-compose-dialog labels='{"addCollection":"stale"}'></jant-compose-dialog>`;
|
|
214
|
+
const dialog = document.querySelector(
|
|
215
|
+
"jant-compose-dialog",
|
|
216
|
+
) as HTMLElement & {
|
|
217
|
+
labels?: unknown;
|
|
218
|
+
};
|
|
219
|
+
dialog.labels = { addCollection: "Add Collection" };
|
|
220
|
+
|
|
221
|
+
expect(readComposeDialogLabels()?.addCollection).toBe("Add Collection");
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it("returns null without a composer or with unreadable labels", () => {
|
|
225
|
+
expect(readComposeDialogLabels()).toBeNull();
|
|
226
|
+
|
|
227
|
+
document.body.innerHTML = `<jant-compose-dialog labels="{not json"></jant-compose-dialog>`;
|
|
228
|
+
expect(readComposeDialogLabels()).toBeNull();
|
|
229
|
+
});
|
|
230
|
+
});
|
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
// @vitest-environment happy-dom
|
|
2
2
|
|
|
3
3
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
|
|
5
|
+
// The composer's bundle loads on first use; the harness element below stands
|
|
6
|
+
// in for the upgraded dialog, so loading is a no-op here.
|
|
7
|
+
vi.mock("../lazy-entries.js", () => ({
|
|
8
|
+
ensureCompose: async () => {},
|
|
9
|
+
ensureSettings: async () => {},
|
|
10
|
+
ensureManage: async () => {},
|
|
11
|
+
loadEntriesForPage: () => {},
|
|
12
|
+
}));
|
|
13
|
+
|
|
4
14
|
import { __testOnly as composeDiscoveryTestOnly } from "../compose-discovery.js";
|
|
5
15
|
import { __testOnly as composeShortcutsTestOnly } from "../compose-shortcuts.js";
|
|
6
16
|
|
|
@@ -24,6 +34,11 @@ function dispatchShortcut(
|
|
|
24
34
|
return event;
|
|
25
35
|
}
|
|
26
36
|
|
|
37
|
+
/** Let a shortcut's trip through the lazy loader settle. */
|
|
38
|
+
async function flush(): Promise<void> {
|
|
39
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
40
|
+
}
|
|
41
|
+
|
|
27
42
|
function createComposeHarness(): ComposeHarness {
|
|
28
43
|
const composeEl = document.createElement(
|
|
29
44
|
"jant-compose-dialog",
|
|
@@ -108,7 +123,7 @@ describe("compose shortcuts", () => {
|
|
|
108
123
|
vi.stubGlobal("fetch", vi.fn().mockResolvedValue({ ok: true }));
|
|
109
124
|
});
|
|
110
125
|
|
|
111
|
-
it("opens a collection-scoped composer on collection pages with n", () => {
|
|
126
|
+
it("opens a collection-scoped composer on collection pages with n", async () => {
|
|
112
127
|
const composeEl = createComposeHarness();
|
|
113
128
|
|
|
114
129
|
const collectionPage = document.createElement("div");
|
|
@@ -118,6 +133,8 @@ describe("compose shortcuts", () => {
|
|
|
118
133
|
|
|
119
134
|
const event = dispatchShortcut(document, "n");
|
|
120
135
|
|
|
136
|
+
await flush();
|
|
137
|
+
|
|
121
138
|
expect(event.defaultPrevented).toBe(true);
|
|
122
139
|
expect(composeEl.openNew).toHaveBeenCalledWith({ collectionId: "col-2" });
|
|
123
140
|
});
|
|
@@ -126,7 +143,8 @@ describe("compose shortcuts", () => {
|
|
|
126
143
|
createComposeHarness();
|
|
127
144
|
|
|
128
145
|
dispatchShortcut(document, "n");
|
|
129
|
-
|
|
146
|
+
|
|
147
|
+
await flush();
|
|
130
148
|
|
|
131
149
|
expect(globalThis.fetch).toHaveBeenCalledWith(
|
|
132
150
|
composeDiscoveryTestOnly.COMPOSE_OPEN_SHORTCUT_DISCOVERY_API_PATH,
|
|
@@ -139,7 +157,7 @@ describe("compose shortcuts", () => {
|
|
|
139
157
|
});
|
|
140
158
|
});
|
|
141
159
|
|
|
142
|
-
it("ignores n while focus is inside an input", () => {
|
|
160
|
+
it("ignores n while focus is inside an input", async () => {
|
|
143
161
|
const composeEl = createComposeHarness();
|
|
144
162
|
|
|
145
163
|
const input = document.createElement("textarea");
|
|
@@ -147,10 +165,12 @@ describe("compose shortcuts", () => {
|
|
|
147
165
|
|
|
148
166
|
dispatchShortcut(input, "n");
|
|
149
167
|
|
|
168
|
+
await flush();
|
|
169
|
+
|
|
150
170
|
expect(composeEl.openNew).not.toHaveBeenCalled();
|
|
151
171
|
});
|
|
152
172
|
|
|
153
|
-
it("opens a reply composer for the latest thread post on detail pages with r", () => {
|
|
173
|
+
it("opens a reply composer for the latest thread post on detail pages with r", async () => {
|
|
154
174
|
const composeEl = createComposeHarness();
|
|
155
175
|
|
|
156
176
|
const postView = document.createElement("div");
|
|
@@ -201,6 +221,8 @@ describe("compose shortcuts", () => {
|
|
|
201
221
|
|
|
202
222
|
const event = dispatchShortcut(document, "r");
|
|
203
223
|
|
|
224
|
+
await flush();
|
|
225
|
+
|
|
204
226
|
expect(event.defaultPrevented).toBe(true);
|
|
205
227
|
expect(composeEl.openReply).toHaveBeenCalledTimes(1);
|
|
206
228
|
|
|
@@ -224,7 +246,7 @@ describe("compose shortcuts", () => {
|
|
|
224
246
|
);
|
|
225
247
|
});
|
|
226
248
|
|
|
227
|
-
it("targets the latest thread post for reply shortcuts even when an older post is hovered", () => {
|
|
249
|
+
it("targets the latest thread post for reply shortcuts even when an older post is hovered", async () => {
|
|
228
250
|
const composeEl = createComposeHarness();
|
|
229
251
|
const { hoveredArticle } = renderThreadDetailPage();
|
|
230
252
|
const originalQuerySelector = document.querySelector.bind(document);
|
|
@@ -240,6 +262,8 @@ describe("compose shortcuts", () => {
|
|
|
240
262
|
|
|
241
263
|
const event = dispatchShortcut(document, "r");
|
|
242
264
|
|
|
265
|
+
await flush();
|
|
266
|
+
|
|
243
267
|
expect(event.defaultPrevented).toBe(true);
|
|
244
268
|
expect(composeEl.openReply).toHaveBeenCalledWith(
|
|
245
269
|
"post-latest",
|
|
@@ -265,7 +289,8 @@ describe("compose shortcuts", () => {
|
|
|
265
289
|
);
|
|
266
290
|
|
|
267
291
|
const event = dispatchShortcut(document, "e");
|
|
268
|
-
|
|
292
|
+
|
|
293
|
+
await flush();
|
|
269
294
|
|
|
270
295
|
expect(event.defaultPrevented).toBe(true);
|
|
271
296
|
expect(composeEl.openEdit).toHaveBeenCalledWith("post-hovered");
|
|
@@ -279,7 +304,8 @@ describe("compose shortcuts", () => {
|
|
|
279
304
|
globalThis.history.pushState({}, "", "/about?edit=1");
|
|
280
305
|
|
|
281
306
|
composeShortcutsTestOnly.openEditFromQueryParam();
|
|
282
|
-
|
|
307
|
+
|
|
308
|
+
await flush();
|
|
283
309
|
|
|
284
310
|
expect(composeEl.openEdit).toHaveBeenCalledWith("post-current");
|
|
285
311
|
expect(window.location.pathname).toBe("/about");
|
|
@@ -296,7 +322,8 @@ describe("compose shortcuts", () => {
|
|
|
296
322
|
globalThis.history.pushState({}, "", "/preview/draft-post?edit=1");
|
|
297
323
|
|
|
298
324
|
composeShortcutsTestOnly.openEditFromQueryParam();
|
|
299
|
-
|
|
325
|
+
|
|
326
|
+
await flush();
|
|
300
327
|
|
|
301
328
|
expect(composeEl.openDraft).toHaveBeenCalledWith("post-current");
|
|
302
329
|
expect(composeEl.openEdit).not.toHaveBeenCalled();
|
|
@@ -304,7 +331,7 @@ describe("compose shortcuts", () => {
|
|
|
304
331
|
expect(window.location.search).toBe("");
|
|
305
332
|
});
|
|
306
333
|
|
|
307
|
-
it("keeps using the hovered post for reply shortcuts in the timeline", () => {
|
|
334
|
+
it("keeps using the hovered post for reply shortcuts in the timeline", async () => {
|
|
308
335
|
const composeEl = createComposeHarness();
|
|
309
336
|
const article = document.createElement("article");
|
|
310
337
|
article.dataset.post = "";
|
|
@@ -332,6 +359,8 @@ describe("compose shortcuts", () => {
|
|
|
332
359
|
|
|
333
360
|
const event = dispatchShortcut(document, "r");
|
|
334
361
|
|
|
362
|
+
await flush();
|
|
363
|
+
|
|
335
364
|
expect(event.defaultPrevented).toBe(true);
|
|
336
365
|
expect(composeEl.openReply).toHaveBeenCalledTimes(1);
|
|
337
366
|
|