@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
|
@@ -8,16 +8,14 @@
|
|
|
8
8
|
import type { ComposeSubmitDetail } from "./components/compose-types.js";
|
|
9
9
|
import type { ComposeAttachment } from "./components/compose-types.js";
|
|
10
10
|
import type { ComposeSubmitAttachment } from "./components/compose-types.js";
|
|
11
|
+
import type { LocalDraftMedia } from "./components/compose-types.js";
|
|
11
12
|
import type { JantComposeDialog } from "./components/jant-compose-dialog.js";
|
|
12
13
|
import type { JantComposeEditor } from "./components/jant-compose-editor.js";
|
|
13
14
|
import type { PostAttachmentInput } from "../types.js";
|
|
14
15
|
import { AudioProcessor } from "./audio-processor.js";
|
|
15
16
|
import { ImageProcessor } from "./image-processor.js";
|
|
16
17
|
import { VideoProcessor } from "./video-processor.js";
|
|
17
|
-
import {
|
|
18
|
-
extractMediaMetadata,
|
|
19
|
-
extractAudioWaveform,
|
|
20
|
-
} from "./media-metadata.js";
|
|
18
|
+
import { extractAudioWaveform } from "./audio-waveform.js";
|
|
21
19
|
import {
|
|
22
20
|
showToast,
|
|
23
21
|
showPersistentToast,
|
|
@@ -25,18 +23,89 @@ import {
|
|
|
25
23
|
replaceWithAutoClose,
|
|
26
24
|
queueToastForNextPage,
|
|
27
25
|
} from "./toast.js";
|
|
28
|
-
import {
|
|
29
|
-
|
|
26
|
+
import {
|
|
27
|
+
refreshPostCardView,
|
|
28
|
+
refreshPostPageView,
|
|
29
|
+
refreshTimelineThreadView,
|
|
30
|
+
} from "./post-refresh.js";
|
|
30
31
|
import { getJsonString, readJsonObject } from "./json.js";
|
|
31
32
|
import { uploadViaSession } from "./upload-session.js";
|
|
33
|
+
import type { UploadSessionResult } from "./upload-session.js";
|
|
32
34
|
import { publicPath } from "./runtime-paths.js";
|
|
33
35
|
import { tiptapJsonToMarkdown } from "../lib/tiptap-to-markdown.js";
|
|
34
|
-
import { getMediaCategory } from "../lib/upload.js";
|
|
36
|
+
import { getMediaCategory, sniffImageMimeType } from "../lib/upload.js";
|
|
35
37
|
import {
|
|
36
38
|
resolveInlineImageUrls,
|
|
37
39
|
hasPendingInlineImagePlaceholders,
|
|
38
40
|
} from "./tiptap/inline-image-upload.js";
|
|
39
41
|
|
|
42
|
+
/**
|
|
43
|
+
* POST a compose payload, treating a dropped connection as a failed response.
|
|
44
|
+
*
|
|
45
|
+
* A publish that never reaches the server and a publish the server rejects are
|
|
46
|
+
* the same event to the author: the post did not go up. Letting `fetch` reject
|
|
47
|
+
* here would send only one of them down the recovery path, so the request that
|
|
48
|
+
* failed hardest — no connection at all — would be the one that skipped saving
|
|
49
|
+
* a draft.
|
|
50
|
+
*
|
|
51
|
+
* @param url - Compose endpoint to post to
|
|
52
|
+
* @param body - Request body, already serialized
|
|
53
|
+
* @param method - HTTP method, defaulting to POST
|
|
54
|
+
* @returns The response, or null when the request never completed
|
|
55
|
+
* @example
|
|
56
|
+
* const res = await postCompose("/compose/thread", JSON.stringify(threadBody));
|
|
57
|
+
* if (!res?.ok) await saveAsDraftInstead();
|
|
58
|
+
*/
|
|
59
|
+
async function postCompose(
|
|
60
|
+
url: string,
|
|
61
|
+
body: string,
|
|
62
|
+
method = "POST",
|
|
63
|
+
): Promise<globalThis.Response | null> {
|
|
64
|
+
try {
|
|
65
|
+
return await fetch(url, {
|
|
66
|
+
method,
|
|
67
|
+
headers: {
|
|
68
|
+
"Content-Type": "application/json",
|
|
69
|
+
Accept: "application/json",
|
|
70
|
+
},
|
|
71
|
+
body,
|
|
72
|
+
});
|
|
73
|
+
} catch {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* What to tell the author when a compose request did not land.
|
|
80
|
+
*
|
|
81
|
+
* Prefers the server's own words, because it knows what was wrong with the
|
|
82
|
+
* post. A request that never completed has no words of its own, so it says so
|
|
83
|
+
* — "no connection" and "the server refused this" call for different next
|
|
84
|
+
* steps from the author.
|
|
85
|
+
*
|
|
86
|
+
* @param res - The response, or null when the request never completed
|
|
87
|
+
* @param labels - Composer labels, absent before the element has upgraded
|
|
88
|
+
* @returns A message ready to show in a toast
|
|
89
|
+
* @example
|
|
90
|
+
* await handleSubmitError(await submitErrorMessage(res, labels));
|
|
91
|
+
*/
|
|
92
|
+
async function submitErrorMessage(
|
|
93
|
+
res: globalThis.Response | null,
|
|
94
|
+
labels: JantComposeDialog["labels"] | undefined,
|
|
95
|
+
): Promise<string> {
|
|
96
|
+
if (!res) {
|
|
97
|
+
return (
|
|
98
|
+
labels?.publishFailedOffline ?? "Couldn't reach the server. Try again."
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
const data = await readJsonObject(res);
|
|
102
|
+
return (
|
|
103
|
+
getJsonString(data, "error") ??
|
|
104
|
+
labels?.publishFailed ??
|
|
105
|
+
"Couldn't publish. Try again."
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
40
109
|
/**
|
|
41
110
|
* Whether a serialized post body still references inline image placeholders
|
|
42
111
|
* pending upload or paste-rehost. Drives the "uploading" toast and whether to
|
|
@@ -66,104 +135,6 @@ function getComposeDialogFromEventTarget(
|
|
|
66
135
|
: null;
|
|
67
136
|
}
|
|
68
137
|
|
|
69
|
-
async function fetchPartialHtml(path: string): Promise<string | null> {
|
|
70
|
-
const res = await fetch(path, {
|
|
71
|
-
headers: { Accept: "text/html" },
|
|
72
|
-
});
|
|
73
|
-
if (!res.ok) return null;
|
|
74
|
-
return res.text();
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
async function refreshTimelineThreadView(
|
|
78
|
-
threadRootId: string,
|
|
79
|
-
): Promise<boolean> {
|
|
80
|
-
try {
|
|
81
|
-
const timelineItem = document.querySelector<HTMLElement>(
|
|
82
|
-
`[data-timeline-item][data-thread-root-id="${threadRootId}"]`,
|
|
83
|
-
);
|
|
84
|
-
const content = timelineItem?.querySelector<HTMLElement>(
|
|
85
|
-
"[data-timeline-item-content]",
|
|
86
|
-
);
|
|
87
|
-
if (!content) return false;
|
|
88
|
-
|
|
89
|
-
const html = await fetchPartialHtml(
|
|
90
|
-
`/_/timeline-item/${encodeURIComponent(threadRootId)}`,
|
|
91
|
-
);
|
|
92
|
-
if (!html) return false;
|
|
93
|
-
|
|
94
|
-
content.innerHTML = html;
|
|
95
|
-
// Swapped-in markup carries interactions whose per-element setup only runs
|
|
96
|
-
// on DOMContentLoaded (thread "Show more" toggle, feed video autoplay, audio
|
|
97
|
-
// waveform); re-initialize them or they stay inert until a full reload.
|
|
98
|
-
hydratePartial(content);
|
|
99
|
-
return true;
|
|
100
|
-
} catch {
|
|
101
|
-
return false;
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
async function refreshPostCardView(postId: string): Promise<boolean> {
|
|
106
|
-
try {
|
|
107
|
-
const timelineItem = document
|
|
108
|
-
.querySelector<HTMLElement>(`article[data-post-id="${postId}"]`)
|
|
109
|
-
?.closest<HTMLElement>("[data-timeline-item]");
|
|
110
|
-
const html = await fetchPartialHtml(
|
|
111
|
-
`/_/post-card/${encodeURIComponent(postId)}`,
|
|
112
|
-
);
|
|
113
|
-
if (!html) return false;
|
|
114
|
-
|
|
115
|
-
if (timelineItem) {
|
|
116
|
-
const content = timelineItem.querySelector<HTMLElement>(
|
|
117
|
-
"[data-timeline-item-content]",
|
|
118
|
-
);
|
|
119
|
-
if (!content) return false;
|
|
120
|
-
content.innerHTML = html;
|
|
121
|
-
hydratePartial(content);
|
|
122
|
-
return true;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
const article = document.querySelector<HTMLElement>(
|
|
126
|
-
`article[data-post-id="${postId}"]`,
|
|
127
|
-
);
|
|
128
|
-
if (!article) return false;
|
|
129
|
-
|
|
130
|
-
article.outerHTML = html;
|
|
131
|
-
// outerHTML detaches `article`; re-query the replacement to hydrate it.
|
|
132
|
-
const nextArticle = document.querySelector<HTMLElement>(
|
|
133
|
-
`article[data-post-id="${postId}"]`,
|
|
134
|
-
);
|
|
135
|
-
if (nextArticle) hydratePartial(nextArticle);
|
|
136
|
-
return true;
|
|
137
|
-
} catch {
|
|
138
|
-
return false;
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
async function refreshPostPageView(postId: string): Promise<boolean> {
|
|
143
|
-
try {
|
|
144
|
-
const container = document.querySelector<HTMLElement>(
|
|
145
|
-
`[data-post-view][data-post-view-id="${postId}"]`,
|
|
146
|
-
);
|
|
147
|
-
if (!container) return false;
|
|
148
|
-
|
|
149
|
-
const html = await fetchPartialHtml(
|
|
150
|
-
`/_/post-view/${encodeURIComponent(postId)}`,
|
|
151
|
-
);
|
|
152
|
-
if (!html) return false;
|
|
153
|
-
|
|
154
|
-
container.outerHTML = html;
|
|
155
|
-
// outerHTML detaches `container`; re-query the replacement to hydrate it
|
|
156
|
-
// (see refreshTimelineThreadView).
|
|
157
|
-
const next = document.querySelector<HTMLElement>(
|
|
158
|
-
`[data-post-view][data-post-view-id="${postId}"]`,
|
|
159
|
-
);
|
|
160
|
-
if (next) hydratePartial(next);
|
|
161
|
-
return true;
|
|
162
|
-
} catch {
|
|
163
|
-
return false;
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
138
|
async function refreshReplyTarget(
|
|
168
139
|
detail: ComposeSubmitDetail,
|
|
169
140
|
): Promise<boolean> {
|
|
@@ -233,17 +204,47 @@ function refreshUploadToast() {
|
|
|
233
204
|
}
|
|
234
205
|
|
|
235
206
|
/**
|
|
236
|
-
* Track completed upload
|
|
207
|
+
* Track completed upload results by clientId.
|
|
208
|
+
*
|
|
209
|
+
* By the time the user submits, a finished upload's promise has been deleted
|
|
210
|
+
* from `uploadPromises`, so the deferred handler can't look it up. This map
|
|
211
|
+
* provides a fallback: we record the result here as soon as any upload
|
|
212
|
+
* succeeds, and `buildRequestAttachments` reads it when `attachment.mediaId`
|
|
213
|
+
* is null and the clientId isn't in `uploadPromises`.
|
|
237
214
|
*
|
|
238
|
-
*
|
|
239
|
-
*
|
|
240
|
-
* detached editor — not the new thread editor. By the time the user submits,
|
|
241
|
-
* the upload promise has been deleted from `uploadPromises`, so the deferred
|
|
242
|
-
* handler can't look it up. This map provides a fallback: we record the result
|
|
243
|
-
* here as soon as any upload succeeds, and `buildRequestAttachments` reads it
|
|
244
|
-
* when `attachment.mediaId` is null and the clientId isn't in `uploadPromises`.
|
|
215
|
+
* The full result (not just the id) is kept so a failed publish can restore
|
|
216
|
+
* uploaded attachments into the reopened compose dialog.
|
|
245
217
|
*/
|
|
246
|
-
const
|
|
218
|
+
const completedUploads = new Map<string, UploadSessionResult>();
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* The editor holding an attachment now.
|
|
222
|
+
*
|
|
223
|
+
* Entering or leaving thread mode renders the post in a new editor element,
|
|
224
|
+
* which takes the attachments over from the one it replaced. An upload that
|
|
225
|
+
* outlives the swap has to follow the attachment: reporting to the editor it
|
|
226
|
+
* started in leaves the new one showing "uploading" forever, never showing an
|
|
227
|
+
* error, and keeping the finished file out of the local draft. Until a
|
|
228
|
+
* connected editor holds the attachment — the moment between the swap and the
|
|
229
|
+
* handover — the starting editor is still the one with the latest copy.
|
|
230
|
+
*
|
|
231
|
+
* @param started - The editor the upload was started from
|
|
232
|
+
* @param clientId - The attachment's client-side id
|
|
233
|
+
* @returns The editor to report progress and status to
|
|
234
|
+
*/
|
|
235
|
+
function attachmentOwner(
|
|
236
|
+
started: JantComposeEditor | null,
|
|
237
|
+
clientId: string,
|
|
238
|
+
): JantComposeEditor | null {
|
|
239
|
+
if (!started || started.isConnected) return started;
|
|
240
|
+
const editors = document.querySelectorAll<JantComposeEditor>(
|
|
241
|
+
"jant-compose-editor",
|
|
242
|
+
);
|
|
243
|
+
for (const editor of editors) {
|
|
244
|
+
if (editor.hasAttachment(clientId)) return editor;
|
|
245
|
+
}
|
|
246
|
+
return started;
|
|
247
|
+
}
|
|
247
248
|
|
|
248
249
|
/**
|
|
249
250
|
* Quickly grab the very first decoded frame of a video as a small poster.
|
|
@@ -314,6 +315,19 @@ function captureQuickPoster(file: File): Promise<Blob | null> {
|
|
|
314
315
|
});
|
|
315
316
|
}
|
|
316
317
|
|
|
318
|
+
/** Bytes that settle whether a file is HEIC: the `ftyp` box and its brand. */
|
|
319
|
+
const HEIC_SNIFF_BYTES = 12;
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Whether the file's bytes say HEIC/HEIF, whatever its name or type claims.
|
|
323
|
+
* Reads the header only — the decoder's own check pulled the whole file into
|
|
324
|
+
* memory to look at four bytes of it.
|
|
325
|
+
*/
|
|
326
|
+
async function isHeicFile(file: File): Promise<boolean> {
|
|
327
|
+
const head = await file.slice(0, HEIC_SNIFF_BYTES).arrayBuffer();
|
|
328
|
+
return sniffImageMimeType(new Uint8Array(head)) === "image/heic";
|
|
329
|
+
}
|
|
330
|
+
|
|
317
331
|
/**
|
|
318
332
|
* Upload a single file: process locally, then send it through the upload
|
|
319
333
|
* session API so the backend can choose relay vs direct transport.
|
|
@@ -324,6 +338,7 @@ async function uploadFile(
|
|
|
324
338
|
clientId: string,
|
|
325
339
|
editor: JantComposeEditor | null,
|
|
326
340
|
): Promise<string | null> {
|
|
341
|
+
const owner = () => attachmentOwner(editor, clientId);
|
|
327
342
|
// Capture cheap metadata up-front so we can release `file` (the original
|
|
328
343
|
// potentially-huge blob) as soon as transcoding finishes. On iOS Safari
|
|
329
344
|
// holding a 300MB+ source blob alongside the transcoded output, upload
|
|
@@ -343,7 +358,7 @@ async function uploadFile(
|
|
|
343
358
|
if (fileType.startsWith("video/")) {
|
|
344
359
|
// Video: transcode with mediabunny (requires WebCodecs)
|
|
345
360
|
if (!VideoProcessor.isSupported()) {
|
|
346
|
-
|
|
361
|
+
owner()?.updateAttachmentStatus(
|
|
347
362
|
clientId,
|
|
348
363
|
"error",
|
|
349
364
|
null,
|
|
@@ -354,14 +369,19 @@ async function uploadFile(
|
|
|
354
369
|
|
|
355
370
|
// Capture the first frame quickly so Safari shows a preview while
|
|
356
371
|
// the heavy transcoding runs. Chrome shows it natively via
|
|
357
|
-
// <video preload="metadata">, Safari does not.
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
372
|
+
// <video preload="metadata">, Safari does not. Kept, rather than shown
|
|
373
|
+
// and dropped: it is also the fallback for the upload below, so the
|
|
374
|
+
// card and the stored media can never disagree about having a poster.
|
|
375
|
+
const quickPoster = captureQuickPoster(file)
|
|
376
|
+
.then((blob) => {
|
|
377
|
+
if (blob) owner()?.updateAttachmentPoster(clientId, blob);
|
|
378
|
+
return blob;
|
|
379
|
+
})
|
|
380
|
+
.catch(() => null);
|
|
381
|
+
|
|
382
|
+
owner()?.updateAttachmentStatus(clientId, "processing", null, null);
|
|
363
383
|
const result = await VideoProcessor.processToFile(file, (progress) => {
|
|
364
|
-
|
|
384
|
+
owner()?.updateAttachmentProgress(clientId, progress);
|
|
365
385
|
});
|
|
366
386
|
toUpload = result.file;
|
|
367
387
|
// Drop the original blob ref now that we have the transcoded output.
|
|
@@ -370,14 +390,18 @@ async function uploadFile(
|
|
|
370
390
|
height = result.height;
|
|
371
391
|
durationSeconds = result.durationSeconds;
|
|
372
392
|
blurhash = result.blurhash;
|
|
373
|
-
poster
|
|
393
|
+
// The probe's poster is the better one — a frame from a tenth of the way
|
|
394
|
+
// in, at full decode quality. Falling back to the quick one costs a
|
|
395
|
+
// slightly worse still; not falling back costs a thumbnail entirely, on
|
|
396
|
+
// exactly the files whose frames the probe could not read.
|
|
397
|
+
poster = result.poster ?? (await quickPoster) ?? undefined;
|
|
374
398
|
if (poster) {
|
|
375
|
-
|
|
399
|
+
owner()?.updateAttachmentPoster(clientId, poster);
|
|
376
400
|
}
|
|
377
401
|
} else if (fileType.startsWith("audio/")) {
|
|
378
402
|
// Audio: transcode to AAC (.m4a) (requires WebCodecs)
|
|
379
403
|
if (!AudioProcessor.isSupported()) {
|
|
380
|
-
|
|
404
|
+
owner()?.updateAttachmentStatus(
|
|
381
405
|
clientId,
|
|
382
406
|
"error",
|
|
383
407
|
null,
|
|
@@ -393,9 +417,9 @@ async function uploadFile(
|
|
|
393
417
|
// Waveform extraction is best-effort
|
|
394
418
|
}
|
|
395
419
|
|
|
396
|
-
|
|
420
|
+
owner()?.updateAttachmentStatus(clientId, "processing", null, null);
|
|
397
421
|
const result = await AudioProcessor.processToFile(file, (progress) => {
|
|
398
|
-
|
|
422
|
+
owner()?.updateAttachmentProgress(clientId, progress);
|
|
399
423
|
});
|
|
400
424
|
toUpload = result.file;
|
|
401
425
|
file = null as unknown as File;
|
|
@@ -407,9 +431,11 @@ async function uploadFile(
|
|
|
407
431
|
// Image: convert HEIC/HEIF if needed, then resize + convert to WebP
|
|
408
432
|
let imageFile = file;
|
|
409
433
|
try {
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
434
|
+
if (await isHeicFile(imageFile)) {
|
|
435
|
+
// heic-to carries libheif — a 3MB chunk — so it is fetched for a
|
|
436
|
+
// file whose bytes say HEIC, not for every photo on the off chance.
|
|
437
|
+
const { heicTo } = await import("heic-to");
|
|
438
|
+
owner()?.updateAttachmentStatus(clientId, "processing", null, null);
|
|
413
439
|
const blob = await heicTo({
|
|
414
440
|
blob: imageFile,
|
|
415
441
|
type: "image/jpeg",
|
|
@@ -418,16 +444,17 @@ async function uploadFile(
|
|
|
418
444
|
imageFile = new File([blob], fileName.replace(/\.heic$/i, ".jpg"), {
|
|
419
445
|
type: "image/jpeg",
|
|
420
446
|
});
|
|
421
|
-
|
|
447
|
+
owner()?.updateAttachmentPreview(clientId, imageFile);
|
|
422
448
|
}
|
|
423
449
|
const result = await ImageProcessor.processToFile(imageFile);
|
|
424
450
|
toUpload = result.file;
|
|
425
451
|
width = result.width;
|
|
426
452
|
height = result.height;
|
|
453
|
+
blurhash = result.blurhash;
|
|
427
454
|
file = null as unknown as File;
|
|
428
455
|
imageFile = null as unknown as File;
|
|
429
456
|
} catch {
|
|
430
|
-
|
|
457
|
+
owner()?.removeAttachment(clientId);
|
|
431
458
|
showToast("Image format not supported.", "error");
|
|
432
459
|
return null;
|
|
433
460
|
}
|
|
@@ -436,21 +463,7 @@ async function uploadFile(
|
|
|
436
463
|
}
|
|
437
464
|
|
|
438
465
|
// Update status to uploading
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
// Extract metadata for non-video files (video metadata comes from VideoProcessor)
|
|
442
|
-
// Audio waveform is already extracted above (before AudioProcessor runs).
|
|
443
|
-
if (!fileType.startsWith("video/")) {
|
|
444
|
-
const meta = await extractMediaMetadata(toUpload);
|
|
445
|
-
width ??= meta.width;
|
|
446
|
-
height ??= meta.height;
|
|
447
|
-
blurhash ??= meta.blurhash;
|
|
448
|
-
waveform ??= meta.waveform;
|
|
449
|
-
if (!poster && meta.poster) {
|
|
450
|
-
poster = meta.poster;
|
|
451
|
-
editor?.updateAttachmentPoster(clientId, poster);
|
|
452
|
-
}
|
|
453
|
-
}
|
|
466
|
+
owner()?.updateAttachmentStatus(clientId, "uploading", null, null);
|
|
454
467
|
|
|
455
468
|
// Text attachments keep summary/chars in the media record. This covers
|
|
456
469
|
// plain text-file uploads (.md, .txt, .csv). Jant-composed rich text
|
|
@@ -487,7 +500,7 @@ async function uploadFile(
|
|
|
487
500
|
chars,
|
|
488
501
|
},
|
|
489
502
|
(progress) => {
|
|
490
|
-
|
|
503
|
+
owner()?.updateAttachmentProgress(clientId, progress);
|
|
491
504
|
uploadProgress.set(clientId, progress);
|
|
492
505
|
refreshUploadToast();
|
|
493
506
|
},
|
|
@@ -495,14 +508,20 @@ async function uploadFile(
|
|
|
495
508
|
|
|
496
509
|
uploadProgress.set(clientId, 1);
|
|
497
510
|
refreshUploadToast();
|
|
498
|
-
|
|
499
|
-
|
|
511
|
+
owner()?.updateAttachmentStatus(
|
|
512
|
+
clientId,
|
|
513
|
+
"done",
|
|
514
|
+
result.id,
|
|
515
|
+
null,
|
|
516
|
+
result.url,
|
|
517
|
+
);
|
|
518
|
+
completedUploads.set(clientId, result);
|
|
500
519
|
return result.id;
|
|
501
520
|
} catch (error) {
|
|
502
521
|
uploadProgress.delete(clientId);
|
|
503
522
|
refreshUploadToast();
|
|
504
523
|
const message = error instanceof Error ? error.message : "Upload failed";
|
|
505
|
-
|
|
524
|
+
owner()?.updateAttachmentStatus(clientId, "error", null, message);
|
|
506
525
|
// Error is shown on the attachment thumbnail; only toast when there's no editor context.
|
|
507
526
|
if (!editor) showToast(message, "error");
|
|
508
527
|
return null;
|
|
@@ -516,7 +535,7 @@ document.addEventListener("jant:attachment-removed", (e: Event) => {
|
|
|
516
535
|
e as CustomEvent<{ clientId: string; mediaId: string | null }>
|
|
517
536
|
).detail;
|
|
518
537
|
|
|
519
|
-
|
|
538
|
+
completedUploads.delete(clientId);
|
|
520
539
|
|
|
521
540
|
if (mediaId) {
|
|
522
541
|
// Upload already finished — fire-and-forget delete
|
|
@@ -552,19 +571,6 @@ document.addEventListener("jant:files-selected", (e: Event) => {
|
|
|
552
571
|
}
|
|
553
572
|
});
|
|
554
573
|
|
|
555
|
-
// ── Reply trigger handler ───────────────────────────────────────────
|
|
556
|
-
|
|
557
|
-
document.addEventListener("click", (e: MouseEvent) => {
|
|
558
|
-
const trigger = (e.target as HTMLElement).closest<HTMLButtonElement>(
|
|
559
|
-
"[data-reply-trigger]",
|
|
560
|
-
);
|
|
561
|
-
if (!trigger) return;
|
|
562
|
-
|
|
563
|
-
const article = trigger.closest<HTMLElement>("article[data-post]");
|
|
564
|
-
if (!article) return;
|
|
565
|
-
void openReplyForArticle(article);
|
|
566
|
-
});
|
|
567
|
-
|
|
568
574
|
// ── Submit handler ──────────────────────────────────────────────────
|
|
569
575
|
|
|
570
576
|
/** Build the JSON body for both create and update requests */
|
|
@@ -628,6 +634,12 @@ function buildPostBody(
|
|
|
628
634
|
attachments: attachments.length > 0 ? attachments : undefined,
|
|
629
635
|
replyToId: detail.replyToId || undefined,
|
|
630
636
|
quietReply: detail.quietReply || undefined,
|
|
637
|
+
// Absent means the author left the language to detection; the server runs
|
|
638
|
+
// the same detector against the final text.
|
|
639
|
+
language: detail.language || undefined,
|
|
640
|
+
// A translation group is only ever minted on create — an edit that carried
|
|
641
|
+
// this would silently regroup an existing post.
|
|
642
|
+
translationOfId: isEdit ? undefined : detail.translationOfId || undefined,
|
|
631
643
|
};
|
|
632
644
|
}
|
|
633
645
|
|
|
@@ -651,7 +663,7 @@ function buildRequestAttachments(
|
|
|
651
663
|
const mediaId =
|
|
652
664
|
attachment.mediaId ??
|
|
653
665
|
pendingMediaIds.get(attachment.clientId) ??
|
|
654
|
-
|
|
666
|
+
completedUploads.get(attachment.clientId)?.id;
|
|
655
667
|
if (!mediaId) continue;
|
|
656
668
|
|
|
657
669
|
requestAttachments.push({
|
|
@@ -694,36 +706,54 @@ document.addEventListener("jant:compose-submit-deferred", async (e: Event) => {
|
|
|
694
706
|
getComposeDialogFromEventTarget(event.target) ??
|
|
695
707
|
(document.querySelector("jant-compose-dialog") as JantComposeDialog | null);
|
|
696
708
|
const isPageMode = !!composeEl?.pageMode;
|
|
709
|
+
const isDraftPreviewPage = !!document.querySelector("[data-preview-status]");
|
|
697
710
|
|
|
698
711
|
// Get labels for toast messages
|
|
699
712
|
const labels = composeEl?.labels;
|
|
700
713
|
const uploadingMsg = labels?.uploading ?? "Uploading...";
|
|
714
|
+
const publishingMsg = labels?.publishing ?? "Publishing...";
|
|
715
|
+
const savingMsg = labels?.saving ?? "Saving...";
|
|
701
716
|
const hasInlinePending = detail.threadPosts
|
|
702
717
|
? detail.threadPosts.some((p) => bodyHasPendingInline(p.body))
|
|
703
718
|
: bodyHasPendingInline(detail.body);
|
|
704
719
|
const hasPending = detail.pendingAttachments.length > 0 || hasInlinePending;
|
|
705
720
|
const publishedMsg = labels?.published ?? "Published!";
|
|
706
721
|
const viewLabel = labels?.view ?? "View";
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
722
|
+
const isEdit = !!detail.editPostId;
|
|
723
|
+
const isThread = !!(detail.threadPosts && detail.threadPosts.length >= 2);
|
|
724
|
+
// Publishing a draft goes through the edit endpoint but still ends published.
|
|
725
|
+
const willPublish =
|
|
726
|
+
detail.status === "published" && (!isEdit || !!detail.draftSourceId);
|
|
727
|
+
|
|
728
|
+
// The composer closes the moment it dispatches, so this toast is the only
|
|
729
|
+
// sign that work is still running. Uploads name themselves and grow a
|
|
730
|
+
// percentage; everything else says what it is doing — a Link post can sit on
|
|
731
|
+
// the server for seconds while its preview thumbnail is fetched and stored.
|
|
732
|
+
let progressToastOpen = true;
|
|
733
|
+
showPersistentToast(
|
|
734
|
+
"compose-deferred",
|
|
735
|
+
hasPending ? uploadingMsg : willPublish ? publishingMsg : savingMsg,
|
|
736
|
+
);
|
|
737
|
+
if (detail.pendingAttachments.length > 0) {
|
|
738
|
+
activeUploadToast = {
|
|
739
|
+
clientIds: detail.pendingAttachments.map((a) => a.clientId),
|
|
740
|
+
baseMsg: uploadingMsg,
|
|
741
|
+
};
|
|
742
|
+
refreshUploadToast();
|
|
718
743
|
}
|
|
719
744
|
|
|
720
|
-
/**
|
|
745
|
+
/**
|
|
746
|
+
* Show a result toast. The first one takes over the progress toast in place;
|
|
747
|
+
* a later message (a retry result, say) gets its own toast instead of
|
|
748
|
+
* overwriting one that has not been read yet.
|
|
749
|
+
*/
|
|
721
750
|
const toastMsg = (msg: string, type: "success" | "error" = "success") => {
|
|
722
751
|
if (activeUploadToast) {
|
|
723
752
|
for (const id of activeUploadToast.clientIds) uploadProgress.delete(id);
|
|
724
753
|
activeUploadToast = null;
|
|
725
754
|
}
|
|
726
|
-
if (
|
|
755
|
+
if (progressToastOpen) {
|
|
756
|
+
progressToastOpen = false;
|
|
727
757
|
replaceWithAutoClose("compose-deferred", msg, type);
|
|
728
758
|
} else {
|
|
729
759
|
showToast(msg, type);
|
|
@@ -751,12 +781,48 @@ document.addEventListener("jant:compose-submit-deferred", async (e: Event) => {
|
|
|
751
781
|
}
|
|
752
782
|
composeEl?.clearLocalDraftFromStorage?.();
|
|
753
783
|
};
|
|
784
|
+
/**
|
|
785
|
+
* Uploads that completed before the publish failed. The local draft's own
|
|
786
|
+
* media snapshot misses uploads that finished after the dialog closed, so
|
|
787
|
+
* the reopened compose merges these over it.
|
|
788
|
+
*/
|
|
789
|
+
const collectRestorableMedia = (): LocalDraftMedia[] => {
|
|
790
|
+
const details = detail.threadPosts?.length ? detail.threadPosts : [detail];
|
|
791
|
+
const media: LocalDraftMedia[] = [];
|
|
792
|
+
const seen = new Set<string>();
|
|
793
|
+
for (const d of details) {
|
|
794
|
+
for (const attachment of d.attachments) {
|
|
795
|
+
if (attachment.type !== "media" || seen.has(attachment.clientId)) {
|
|
796
|
+
continue;
|
|
797
|
+
}
|
|
798
|
+
seen.add(attachment.clientId);
|
|
799
|
+
const upload = completedUploads.get(attachment.clientId);
|
|
800
|
+
if (!upload) continue;
|
|
801
|
+
media.push({
|
|
802
|
+
clientId: attachment.clientId,
|
|
803
|
+
mediaId: upload.id,
|
|
804
|
+
url: upload.url,
|
|
805
|
+
mimeType: upload.mimeType,
|
|
806
|
+
name: upload.filename || undefined,
|
|
807
|
+
alt: attachment.alt || undefined,
|
|
808
|
+
});
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
return media;
|
|
812
|
+
};
|
|
813
|
+
|
|
754
814
|
const reopenComposeAfterFailure = async () => {
|
|
755
815
|
if (!composeEl || isPageMode) return;
|
|
756
816
|
|
|
817
|
+
if (detail.draftSourceId) {
|
|
818
|
+
if (typeof composeEl.openDraft !== "function") return;
|
|
819
|
+
await composeEl.openDraft(detail.draftSourceId);
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
|
|
757
823
|
if (isEdit && detail.editPostId) {
|
|
758
824
|
if (typeof composeEl.openEdit !== "function") return;
|
|
759
|
-
await composeEl.openEdit(detail.editPostId);
|
|
825
|
+
await composeEl.openEdit(detail.editPostId, { restoreToast: false });
|
|
760
826
|
return;
|
|
761
827
|
}
|
|
762
828
|
|
|
@@ -772,13 +838,22 @@ document.addEventListener("jant:compose-submit-deferred", async (e: Event) => {
|
|
|
772
838
|
id: detail.replyRefreshId,
|
|
773
839
|
}
|
|
774
840
|
: undefined,
|
|
775
|
-
{
|
|
841
|
+
{
|
|
842
|
+
restoreDraft: true,
|
|
843
|
+
initialFormat: detail.format,
|
|
844
|
+
restoreToast: false,
|
|
845
|
+
restoreMedia: collectRestorableMedia(),
|
|
846
|
+
},
|
|
776
847
|
);
|
|
777
848
|
return;
|
|
778
849
|
}
|
|
779
850
|
|
|
780
851
|
if (typeof composeEl.openNew !== "function") return;
|
|
781
|
-
await composeEl.openNew({
|
|
852
|
+
await composeEl.openNew({
|
|
853
|
+
restoreDraft: true,
|
|
854
|
+
restoreToast: false,
|
|
855
|
+
restoreMedia: collectRestorableMedia(),
|
|
856
|
+
});
|
|
782
857
|
};
|
|
783
858
|
const handleSubmitError = async (message: string) => {
|
|
784
859
|
clearPageLoading();
|
|
@@ -801,8 +876,6 @@ document.addEventListener("jant:compose-submit-deferred", async (e: Event) => {
|
|
|
801
876
|
globalThis.location.assign(composeEl.closeHref || publicPath("/"));
|
|
802
877
|
return true;
|
|
803
878
|
};
|
|
804
|
-
const isEdit = !!detail.editPostId;
|
|
805
|
-
const isThread = !!(detail.threadPosts && detail.threadPosts.length >= 2);
|
|
806
879
|
let draftFallback: "upload" | "server" | null = null;
|
|
807
880
|
|
|
808
881
|
try {
|
|
@@ -818,10 +891,10 @@ document.addEventListener("jant:compose-submit-deferred", async (e: Event) => {
|
|
|
818
891
|
const results = await Promise.all(pendingPromises);
|
|
819
892
|
|
|
820
893
|
// If any pending upload failed (null result where we expected a mediaId
|
|
821
|
-
// AND the upload wasn't already tracked in
|
|
894
|
+
// AND the upload wasn't already tracked in completedUploads):
|
|
822
895
|
const failedCount = results.filter(
|
|
823
896
|
(id, i) =>
|
|
824
|
-
id === null && !
|
|
897
|
+
id === null && !completedUploads.has(pendingClientIds[i] ?? ""),
|
|
825
898
|
).length;
|
|
826
899
|
if (failedCount > 0) {
|
|
827
900
|
if (detail.status === "published" && !isEdit) {
|
|
@@ -872,30 +945,23 @@ document.addEventListener("jant:compose-submit-deferred", async (e: Event) => {
|
|
|
872
945
|
threadBody.replaceThreadId = detail.editPostId;
|
|
873
946
|
}
|
|
874
947
|
|
|
875
|
-
const res = await
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
Accept: "application/json",
|
|
880
|
-
},
|
|
881
|
-
body: JSON.stringify(threadBody),
|
|
882
|
-
});
|
|
948
|
+
const res = await postCompose(
|
|
949
|
+
"/compose/thread",
|
|
950
|
+
JSON.stringify(threadBody),
|
|
951
|
+
);
|
|
883
952
|
|
|
884
|
-
if (!res
|
|
885
|
-
//
|
|
953
|
+
if (!res?.ok) {
|
|
954
|
+
// The publish did not land — whether the server refused it or the
|
|
955
|
+
// request never arrived. Try to keep the work as a draft either way.
|
|
886
956
|
if (detail.status === "published" && !draftFallback) {
|
|
887
957
|
const retryPayload = {
|
|
888
958
|
posts: postsPayload.map((p) => ({ ...p, status: "draft" })),
|
|
889
959
|
};
|
|
890
|
-
const retryRes = await
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
},
|
|
896
|
-
body: JSON.stringify(retryPayload),
|
|
897
|
-
});
|
|
898
|
-
if (retryRes.ok) {
|
|
960
|
+
const retryRes = await postCompose(
|
|
961
|
+
"/compose/thread",
|
|
962
|
+
JSON.stringify(retryPayload),
|
|
963
|
+
);
|
|
964
|
+
if (retryRes?.ok) {
|
|
899
965
|
draftFallback = "server";
|
|
900
966
|
clearRecoveredLocalDraft();
|
|
901
967
|
const fallbackMsg =
|
|
@@ -906,10 +972,7 @@ document.addEventListener("jant:compose-submit-deferred", async (e: Event) => {
|
|
|
906
972
|
return;
|
|
907
973
|
}
|
|
908
974
|
}
|
|
909
|
-
|
|
910
|
-
await handleSubmitError(
|
|
911
|
-
getJsonString(data, "error") ?? "Something went wrong",
|
|
912
|
-
);
|
|
975
|
+
await handleSubmitError(await submitErrorMessage(res, labels));
|
|
913
976
|
return;
|
|
914
977
|
}
|
|
915
978
|
|
|
@@ -930,7 +993,12 @@ document.addEventListener("jant:compose-submit-deferred", async (e: Event) => {
|
|
|
930
993
|
|
|
931
994
|
if (threadStatus === "published") {
|
|
932
995
|
clearRecoveredLocalDraft();
|
|
933
|
-
if (
|
|
996
|
+
if (isDraftPreviewPage && detail.draftSourceId) {
|
|
997
|
+
queueSuccessToast(publishedMsg);
|
|
998
|
+
globalThis.location.assign(
|
|
999
|
+
threadPermalink ?? globalThis.location.pathname,
|
|
1000
|
+
);
|
|
1001
|
+
} else if (isPageMode) {
|
|
934
1002
|
await refreshComposeCollections();
|
|
935
1003
|
resetPageCompose();
|
|
936
1004
|
toastMsg(publishedMsg);
|
|
@@ -945,6 +1013,11 @@ document.addEventListener("jant:compose-submit-deferred", async (e: Event) => {
|
|
|
945
1013
|
}
|
|
946
1014
|
} else {
|
|
947
1015
|
clearRecoveredLocalDraft();
|
|
1016
|
+
if (isDraftPreviewPage && detail.draftSourceId) {
|
|
1017
|
+
queueSuccessToast(threadToast ?? "Draft saved.");
|
|
1018
|
+
globalThis.location.assign(globalThis.location.pathname);
|
|
1019
|
+
return;
|
|
1020
|
+
}
|
|
948
1021
|
await refreshComposeCollections();
|
|
949
1022
|
if (!leavePageAfterConfirmSave()) resetPageCompose();
|
|
950
1023
|
toastMsg(threadToast ?? "Draft saved.");
|
|
@@ -982,29 +1055,24 @@ document.addEventListener("jant:compose-submit-deferred", async (e: Event) => {
|
|
|
982
1055
|
requestAttachments,
|
|
983
1056
|
);
|
|
984
1057
|
|
|
985
|
-
const res = await
|
|
1058
|
+
const res = await postCompose(
|
|
1059
|
+
endpoint,
|
|
1060
|
+
JSON.stringify(bodyPayload),
|
|
986
1061
|
method,
|
|
987
|
-
|
|
988
|
-
"Content-Type": "application/json",
|
|
989
|
-
Accept: "application/json",
|
|
990
|
-
},
|
|
991
|
-
body: JSON.stringify(bodyPayload),
|
|
992
|
-
});
|
|
1062
|
+
);
|
|
993
1063
|
|
|
994
|
-
if (!res
|
|
995
|
-
//
|
|
1064
|
+
if (!res?.ok) {
|
|
1065
|
+
// As in thread mode: a refused request and a request that never arrived
|
|
1066
|
+
// both mean the post is not up, so both get the save-as-draft attempt.
|
|
996
1067
|
if (detail.status === "published" && !isEdit && !draftFallback) {
|
|
997
1068
|
const retryPayload = { ...bodyPayload, status: "draft" };
|
|
998
|
-
const retryRes = await
|
|
1069
|
+
const retryRes = await postCompose(
|
|
1070
|
+
endpoint,
|
|
1071
|
+
JSON.stringify(retryPayload),
|
|
999
1072
|
method,
|
|
1000
|
-
|
|
1001
|
-
"Content-Type": "application/json",
|
|
1002
|
-
Accept: "application/json",
|
|
1003
|
-
},
|
|
1004
|
-
body: JSON.stringify(retryPayload),
|
|
1005
|
-
});
|
|
1073
|
+
);
|
|
1006
1074
|
|
|
1007
|
-
if (retryRes
|
|
1075
|
+
if (retryRes?.ok) {
|
|
1008
1076
|
draftFallback = "server";
|
|
1009
1077
|
clearRecoveredLocalDraft();
|
|
1010
1078
|
const retryData = await readJsonObject(retryRes);
|
|
@@ -1021,10 +1089,7 @@ document.addEventListener("jant:compose-submit-deferred", async (e: Event) => {
|
|
|
1021
1089
|
}
|
|
1022
1090
|
}
|
|
1023
1091
|
|
|
1024
|
-
|
|
1025
|
-
await handleSubmitError(
|
|
1026
|
-
getJsonString(data, "error") ?? "Something went wrong",
|
|
1027
|
-
);
|
|
1092
|
+
await handleSubmitError(await submitErrorMessage(res, labels));
|
|
1028
1093
|
return;
|
|
1029
1094
|
}
|
|
1030
1095
|
|
|
@@ -1033,21 +1098,50 @@ document.addEventListener("jant:compose-submit-deferred", async (e: Event) => {
|
|
|
1033
1098
|
const editData = await readJsonObject(res);
|
|
1034
1099
|
const editPostId = detail.editPostId ?? "";
|
|
1035
1100
|
const newSlug = getJsonString(editData, "slug");
|
|
1036
|
-
const
|
|
1101
|
+
const nextStatus = getJsonString(editData, "status") ?? detail.status;
|
|
1102
|
+
const newPath = newSlug
|
|
1103
|
+
? publicPath(
|
|
1104
|
+
nextStatus === "draft" ? `/preview/${newSlug}` : `/${newSlug}`,
|
|
1105
|
+
)
|
|
1106
|
+
: null;
|
|
1107
|
+
const editSuccessMessage =
|
|
1108
|
+
nextStatus === "draft"
|
|
1109
|
+
? "Draft saved."
|
|
1110
|
+
: detail.draftSourceId
|
|
1111
|
+
? publishedMsg
|
|
1112
|
+
: "Post updated.";
|
|
1113
|
+
|
|
1114
|
+
// Draft preview pages cannot use the public post-view refresh endpoint.
|
|
1115
|
+
// Reload the authenticated preview so saved content stays visible.
|
|
1116
|
+
if (nextStatus === "draft") {
|
|
1117
|
+
if (isDraftPreviewPage) {
|
|
1118
|
+
queueSuccessToast(editSuccessMessage);
|
|
1119
|
+
globalThis.location.assign(newPath ?? globalThis.location.pathname);
|
|
1120
|
+
return;
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
// Drafts opened from the compose panel stay in that workflow. The
|
|
1124
|
+
// completion event lets a pending "save and open drafts" action fetch
|
|
1125
|
+
// the updated list only after the write has finished.
|
|
1126
|
+
await refreshComposeCollections();
|
|
1127
|
+
toastMsg(editSuccessMessage);
|
|
1128
|
+
dispatchSubmitComplete(composeEl, "draft");
|
|
1129
|
+
return;
|
|
1130
|
+
}
|
|
1037
1131
|
|
|
1038
1132
|
if (isPageMode) {
|
|
1039
1133
|
// On the post detail page: refresh or navigate if slug changed
|
|
1040
1134
|
if (newPath && newPath !== globalThis.location.pathname) {
|
|
1041
|
-
queueSuccessToast(
|
|
1135
|
+
queueSuccessToast(editSuccessMessage);
|
|
1042
1136
|
globalThis.location.assign(newPath);
|
|
1043
1137
|
} else {
|
|
1044
1138
|
const refreshed =
|
|
1045
1139
|
editPostId && (await refreshPostPageView(editPostId));
|
|
1046
1140
|
if (!refreshed) {
|
|
1047
|
-
queueSuccessToast(
|
|
1141
|
+
queueSuccessToast(editSuccessMessage);
|
|
1048
1142
|
globalThis.location.assign(globalThis.location.pathname);
|
|
1049
1143
|
} else {
|
|
1050
|
-
toastMsg(
|
|
1144
|
+
toastMsg(editSuccessMessage);
|
|
1051
1145
|
}
|
|
1052
1146
|
}
|
|
1053
1147
|
} else if (editPostId) {
|
|
@@ -1057,15 +1151,15 @@ document.addEventListener("jant:compose-submit-deferred", async (e: Event) => {
|
|
|
1057
1151
|
);
|
|
1058
1152
|
if (postView) {
|
|
1059
1153
|
if (newPath && newPath !== globalThis.location.pathname) {
|
|
1060
|
-
queueSuccessToast(
|
|
1154
|
+
queueSuccessToast(editSuccessMessage);
|
|
1061
1155
|
globalThis.location.assign(newPath);
|
|
1062
1156
|
return;
|
|
1063
1157
|
}
|
|
1064
1158
|
const refreshed = await refreshPostPageView(editPostId);
|
|
1065
1159
|
if (refreshed) {
|
|
1066
|
-
toastMsg(
|
|
1160
|
+
toastMsg(editSuccessMessage);
|
|
1067
1161
|
} else {
|
|
1068
|
-
queueSuccessToast(
|
|
1162
|
+
queueSuccessToast(editSuccessMessage);
|
|
1069
1163
|
globalThis.location.reload();
|
|
1070
1164
|
}
|
|
1071
1165
|
} else {
|
|
@@ -1080,14 +1174,14 @@ document.addEventListener("jant:compose-submit-deferred", async (e: Event) => {
|
|
|
1080
1174
|
? await refreshTimelineThreadView(threadRootId)
|
|
1081
1175
|
: await refreshPostCardView(editPostId);
|
|
1082
1176
|
if (refreshed) {
|
|
1083
|
-
toastMsg(
|
|
1177
|
+
toastMsg(editSuccessMessage);
|
|
1084
1178
|
} else {
|
|
1085
|
-
queueSuccessToast(
|
|
1179
|
+
queueSuccessToast(editSuccessMessage);
|
|
1086
1180
|
globalThis.location.reload();
|
|
1087
1181
|
}
|
|
1088
1182
|
}
|
|
1089
1183
|
} else {
|
|
1090
|
-
queueSuccessToast(
|
|
1184
|
+
queueSuccessToast(editSuccessMessage);
|
|
1091
1185
|
globalThis.location.reload();
|
|
1092
1186
|
}
|
|
1093
1187
|
return;
|
|
@@ -1147,7 +1241,7 @@ document.addEventListener("jant:compose-submit-deferred", async (e: Event) => {
|
|
|
1147
1241
|
dispatchSubmitComplete(composeEl, "draft");
|
|
1148
1242
|
}
|
|
1149
1243
|
} catch {
|
|
1150
|
-
await handleSubmitError(
|
|
1244
|
+
await handleSubmitError(await submitErrorMessage(null, labels));
|
|
1151
1245
|
}
|
|
1152
1246
|
});
|
|
1153
1247
|
|