@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
|
@@ -12,17 +12,22 @@ import { classMap } from "lit/directives/class-map.js";
|
|
|
12
12
|
import { repeat } from "lit/directives/repeat.js";
|
|
13
13
|
import { unsafeHTML } from "lit/directives/unsafe-html.js";
|
|
14
14
|
import { unsafeSVG } from "lit/directives/unsafe-svg.js";
|
|
15
|
+
import { ifDefined } from "lit/directives/if-defined.js";
|
|
15
16
|
import type { Editor, JSONContent } from "@tiptap/core";
|
|
17
|
+
import { extractBodyText } from "../../lib/summary.js";
|
|
18
|
+
import { readContentLanguage } from "../../lib/lang-detect.js";
|
|
16
19
|
import type {
|
|
17
20
|
ComposeFormat,
|
|
18
21
|
ComposeVisibility,
|
|
19
22
|
ComposeLabels,
|
|
20
23
|
ComposeCollection,
|
|
24
|
+
ComposeLanguage,
|
|
21
25
|
ComposeSubmitDetail,
|
|
22
26
|
ComposeSubmitAttachment,
|
|
23
27
|
ComposeAttachment,
|
|
24
28
|
ComposeRowStatus,
|
|
25
29
|
DraftItem,
|
|
30
|
+
DraftRestoreOutcome,
|
|
26
31
|
LocalDraft,
|
|
27
32
|
LocalDraftMedia,
|
|
28
33
|
ComposeFullscreenOpenDetail,
|
|
@@ -76,6 +81,11 @@ interface ThreadItem {
|
|
|
76
81
|
slug?: string;
|
|
77
82
|
/** Last availability answer for this post's slug. */
|
|
78
83
|
slugTaken?: boolean;
|
|
84
|
+
/**
|
|
85
|
+
* The saved draft post this row was loaded from, if any. Its slug already
|
|
86
|
+
* belongs to that post, so the availability check leaves it out.
|
|
87
|
+
*/
|
|
88
|
+
sourcePostId?: string;
|
|
79
89
|
}
|
|
80
90
|
|
|
81
91
|
interface ApiMediaAttachment {
|
|
@@ -122,6 +132,11 @@ interface ComposePostResponse {
|
|
|
122
132
|
rating?: number | null;
|
|
123
133
|
publishedAt?: number | null;
|
|
124
134
|
bodyHtml?: string | null;
|
|
135
|
+
/**
|
|
136
|
+
* The language this Post is filed under. Null on a single-language site, and
|
|
137
|
+
* on posts written before the feature was turned on.
|
|
138
|
+
*/
|
|
139
|
+
language?: string | null;
|
|
125
140
|
}
|
|
126
141
|
|
|
127
142
|
interface DraftsResponse {
|
|
@@ -152,6 +167,8 @@ interface ComposeStateSnapshot {
|
|
|
152
167
|
publishedAtInput: string;
|
|
153
168
|
publishedAtTimeMinutes: number | null;
|
|
154
169
|
visibility: ComposeVisibility;
|
|
170
|
+
/** The author's explicit language choice, not what detection reads. */
|
|
171
|
+
language: string | null;
|
|
155
172
|
title: string;
|
|
156
173
|
bodyJson: JSONContent | null;
|
|
157
174
|
url: string;
|
|
@@ -240,22 +257,30 @@ const COMPOSE_PUBLISH_VISIBILITY_ICONS: Record<ComposeVisibility, string> = {
|
|
|
240
257
|
`,
|
|
241
258
|
};
|
|
242
259
|
|
|
260
|
+
/* Drawn on a 14 grid, which is also the size these render at: a 16-unit
|
|
261
|
+
drawing shown at 14px scales every coordinate by 0.875, so no edge lands on
|
|
262
|
+
a device pixel and the whole glyph greys out. Diagonals are antialiased
|
|
263
|
+
whatever you do, so the checks and carets only needed the grid; the calendar
|
|
264
|
+
is all horizontals and verticals, and those are what the scale was smearing.
|
|
265
|
+
See its stroke width for the rest of that story. */
|
|
243
266
|
const COMPOSE_PUBLISH_ACTION_ICONS = {
|
|
244
267
|
check: `
|
|
245
|
-
<path d="
|
|
268
|
+
<path d="M3.75 7.25 6 9.5l4.25-4.25" />
|
|
246
269
|
`,
|
|
247
270
|
caretRight: `
|
|
248
|
-
<path d="
|
|
249
|
-
`,
|
|
250
|
-
close: `
|
|
251
|
-
<path d="M4.5 4.5 11.5 11.5" />
|
|
252
|
-
<path d="M11.5 4.5 4.5 11.5" />
|
|
271
|
+
<path d="M5.65 4.5 8.15 7l-2.5 2.5" />
|
|
253
272
|
`,
|
|
273
|
+
/* Every straight stroke is centred on a half unit and drawn 1 wide, which is
|
|
274
|
+
the only combination that lands on whole device pixels at 1x, 2x and 3x
|
|
275
|
+
alike: a 1px stroke centred at x.5 covers exactly one pixel at 1x, two at
|
|
276
|
+
2x, three at 3x. Hence also the flat `stroke-width`, overriding the
|
|
277
|
+
family's 1.35 — the weight is worth less here than the edges are. The
|
|
278
|
+
rounded corners still antialias, as corners should. */
|
|
254
279
|
calendar: `
|
|
255
|
-
<rect x="2.
|
|
256
|
-
<path d="
|
|
257
|
-
<path d="
|
|
258
|
-
<path d="M2.
|
|
280
|
+
<rect x="2.5" y="3.5" width="9" height="8" rx="2" stroke-width="1" />
|
|
281
|
+
<path d="M4.5 2.5v2" stroke-width="1" />
|
|
282
|
+
<path d="M9.5 2.5v2" stroke-width="1" />
|
|
283
|
+
<path d="M2.5 6.5h9" stroke-width="1" />
|
|
259
284
|
`,
|
|
260
285
|
/* Two sliders — "settings for this thing", without borrowing the gear, which
|
|
261
286
|
elsewhere means site settings. No enclosing box: the trigger is already a
|
|
@@ -271,6 +296,17 @@ const COMPOSE_PUBLISH_ACTION_ICONS = {
|
|
|
271
296
|
`,
|
|
272
297
|
} as const;
|
|
273
298
|
|
|
299
|
+
/* The site header's globe, stroke for stroke. Language already has a symbol on
|
|
300
|
+
this site — the switcher in the top right — and the author is the one person
|
|
301
|
+
who sees both. A second glyph for the same idea would only be a second thing
|
|
302
|
+
to learn. Drawn on a 24 grid like its counterpart, so the two are literally
|
|
303
|
+
the same artwork at two sizes. */
|
|
304
|
+
const COMPOSE_LANGUAGE_ICON = `
|
|
305
|
+
<circle cx="12" cy="12" r="10" />
|
|
306
|
+
<path d="M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20" />
|
|
307
|
+
<path d="M2 12h20" />
|
|
308
|
+
`;
|
|
309
|
+
|
|
274
310
|
const COMPOSE_COLLECTION_PICKER_ICONS = {
|
|
275
311
|
/* A stack seen edge-on, not a folder: a collection is a post filed under
|
|
276
312
|
several topics at once, and the folder glyph promises a single container
|
|
@@ -365,10 +401,10 @@ function renderComposePublishActionIcon(
|
|
|
365
401
|
class=${classes}
|
|
366
402
|
width="14"
|
|
367
403
|
height="14"
|
|
368
|
-
viewBox="0 0
|
|
404
|
+
viewBox="0 0 14 14"
|
|
369
405
|
fill="none"
|
|
370
406
|
stroke="currentColor"
|
|
371
|
-
stroke-width="1.
|
|
407
|
+
stroke-width="1.35"
|
|
372
408
|
stroke-linecap="round"
|
|
373
409
|
stroke-linejoin="round"
|
|
374
410
|
aria-hidden="true"
|
|
@@ -615,6 +651,8 @@ export class JantComposeDialog extends LitElement {
|
|
|
615
651
|
|
|
616
652
|
static properties = {
|
|
617
653
|
collections: { type: Array },
|
|
654
|
+
languages: { type: Array },
|
|
655
|
+
contextLanguage: { type: String, attribute: "context-language" },
|
|
618
656
|
labels: { type: Object },
|
|
619
657
|
uploadMaxFileSize: { type: Number, attribute: "upload-max-file-size" },
|
|
620
658
|
pageMode: { type: Boolean, attribute: "page-mode" },
|
|
@@ -663,9 +701,16 @@ export class JantComposeDialog extends LitElement {
|
|
|
663
701
|
_slugTaken: { state: true },
|
|
664
702
|
_visibilityLocked: { state: true },
|
|
665
703
|
_quietReply: { state: true },
|
|
704
|
+
_language: { state: true },
|
|
705
|
+
_showLanguagePicker: { state: true },
|
|
706
|
+
_translationOf: { state: true },
|
|
707
|
+
_translationCollapsed: { state: true },
|
|
666
708
|
};
|
|
667
709
|
|
|
668
710
|
declare collections: ComposeCollection[];
|
|
711
|
+
declare languages: ComposeLanguage[];
|
|
712
|
+
/** Content language of the page the composer opened from. */
|
|
713
|
+
declare contextLanguage: string;
|
|
669
714
|
declare labels: ComposeLabels;
|
|
670
715
|
declare uploadMaxFileSize: number;
|
|
671
716
|
declare pageMode: boolean;
|
|
@@ -715,22 +760,59 @@ export class JantComposeDialog extends LitElement {
|
|
|
715
760
|
declare _slugTaken: boolean;
|
|
716
761
|
declare _visibilityLocked: boolean;
|
|
717
762
|
declare _quietReply: boolean;
|
|
763
|
+
/** Author's explicit language choice. Null means "let Jant read it". */
|
|
764
|
+
declare _language: string | null;
|
|
765
|
+
/** Whether the language pill's list of choices is open. */
|
|
766
|
+
declare _showLanguagePicker: boolean;
|
|
767
|
+
/**
|
|
768
|
+
* Thread root this post is being written as a translation of, with enough of
|
|
769
|
+
* it to read: translating from a title alone means keeping the original open
|
|
770
|
+
* in another tab.
|
|
771
|
+
*/
|
|
772
|
+
declare _translationOf: {
|
|
773
|
+
id: string;
|
|
774
|
+
title: string;
|
|
775
|
+
href: string;
|
|
776
|
+
/** The post rendered server-side, exactly as its own page renders it. */
|
|
777
|
+
previewHtml: string;
|
|
778
|
+
} | null;
|
|
779
|
+
/**
|
|
780
|
+
* Whether the original is folded away.
|
|
781
|
+
*
|
|
782
|
+
* Per composer session, not remembered: an author who folds it to get room
|
|
783
|
+
* for one paragraph should not find it gone the next time they open a
|
|
784
|
+
* translation, when the first thing they need is to read.
|
|
785
|
+
*/
|
|
786
|
+
declare _translationCollapsed: boolean;
|
|
718
787
|
|
|
719
788
|
private _attachedEditor: Editor | null = null;
|
|
720
789
|
private _attachedTextSnapshot: JSONContent | null = null;
|
|
790
|
+
/** Whether this composer was pre-filled from the post it translates. */
|
|
791
|
+
private _seededFromSource = false;
|
|
721
792
|
private _confirmForDrafts = false;
|
|
722
793
|
private _confirmForAttachedText = false;
|
|
723
794
|
private _draftSaveTimer: ReturnType<typeof setTimeout> | null = null;
|
|
724
795
|
private _draftRestored = false;
|
|
796
|
+
/**
|
|
797
|
+
* Whether this composer has ever held content under its current storage key.
|
|
798
|
+
*
|
|
799
|
+
* The stored draft is shared: one key answers for new posts and for replies,
|
|
800
|
+
* across every tab. A composer that opens empty — a reply whose parent does
|
|
801
|
+
* not match the stored draft, a restore that bailed, a second tab sitting
|
|
802
|
+
* idle — is empty because it never loaded anything, not because the author
|
|
803
|
+
* threw the draft away. Only a composer that actually held the content may
|
|
804
|
+
* decide it is gone; without this, any empty composer deletes work it never
|
|
805
|
+
* showed.
|
|
806
|
+
*/
|
|
807
|
+
private _hasOwnedContent = false;
|
|
808
|
+
/** Whether the author has already been told this composer cannot store drafts. */
|
|
809
|
+
private _reportedStorageFailure = false;
|
|
725
810
|
private _initialSnapshot: string | null = null;
|
|
726
811
|
private _pageFocusApplied = false;
|
|
727
812
|
private _pageLeaveRequested = false;
|
|
728
813
|
private _replyThreadRootId: string | null = null;
|
|
729
814
|
private _replyRefreshKind:
|
|
730
|
-
|
|
731
|
-
| "post-card"
|
|
732
|
-
| "post-view"
|
|
733
|
-
| null = null;
|
|
815
|
+
"timeline-item" | "post-card" | "post-view" | null = null;
|
|
734
816
|
private _replyRefreshId: string | null = null;
|
|
735
817
|
private _publishedAtTimeMinutes: number | null = null;
|
|
736
818
|
private _originalPublishedAt: number | null = null;
|
|
@@ -761,6 +843,8 @@ export class JantComposeDialog extends LitElement {
|
|
|
761
843
|
constructor() {
|
|
762
844
|
super();
|
|
763
845
|
this.collections = [];
|
|
846
|
+
this.languages = [];
|
|
847
|
+
this.contextLanguage = "";
|
|
764
848
|
this.labels = {} as ComposeLabels;
|
|
765
849
|
this.uploadMaxFileSize = 1024;
|
|
766
850
|
this.pageMode = false;
|
|
@@ -815,6 +899,11 @@ export class JantComposeDialog extends LitElement {
|
|
|
815
899
|
this._slugTaken = false;
|
|
816
900
|
this._visibilityLocked = false;
|
|
817
901
|
this._quietReply = false;
|
|
902
|
+
this._language = null;
|
|
903
|
+
this._showLanguagePicker = false;
|
|
904
|
+
this._translationOf = null;
|
|
905
|
+
this._translationCollapsed = false;
|
|
906
|
+
this._seededFromSource = false;
|
|
818
907
|
}
|
|
819
908
|
|
|
820
909
|
private get _editor(): JantComposeEditor | null {
|
|
@@ -871,6 +960,7 @@ export class JantComposeDialog extends LitElement {
|
|
|
871
960
|
if (this._initialSnapshot === null && this._editor) {
|
|
872
961
|
this._captureInitialSnapshot();
|
|
873
962
|
}
|
|
963
|
+
this._adoptTranslationPreview();
|
|
874
964
|
// The visible title used to be the dialog's accessible name. Nothing on
|
|
875
965
|
// screen needs to replace it, but a screen reader announcing an unnamed
|
|
876
966
|
// dialog does — so the name moves to an attribute, where it costs no space.
|
|
@@ -964,9 +1054,16 @@ export class JantComposeDialog extends LitElement {
|
|
|
964
1054
|
this._slugSuggestionKey = "";
|
|
965
1055
|
this._visibilityLocked = false;
|
|
966
1056
|
this._quietReply = false;
|
|
1057
|
+
this._language = null;
|
|
1058
|
+
this._showLanguagePicker = false;
|
|
1059
|
+
this._translationOf = null;
|
|
1060
|
+
this._translationCollapsed = false;
|
|
1061
|
+
this._seededFromSource = false;
|
|
967
1062
|
this._confirmForDrafts = false;
|
|
968
1063
|
this._confirmForAttachedText = false;
|
|
969
1064
|
this._initialSnapshot = null;
|
|
1065
|
+
this._hasOwnedContent = false;
|
|
1066
|
+
this._reportedStorageFailure = false;
|
|
970
1067
|
this._pageFocusApplied = false;
|
|
971
1068
|
this._pageLeaveRequested = false;
|
|
972
1069
|
this._slugSuggestRequestId += 1;
|
|
@@ -1038,6 +1135,11 @@ export class JantComposeDialog extends LitElement {
|
|
|
1038
1135
|
this._initialSlug = this._slug.trim();
|
|
1039
1136
|
this._visibility = post.visibility ?? "public";
|
|
1040
1137
|
this._visibilityLocked = Boolean(post.replyToId);
|
|
1138
|
+
// A published post's language is a settled fact, so it comes back as the
|
|
1139
|
+
// author's own choice rather than as something to re-read: editing a
|
|
1140
|
+
// Chinese post from an English page must not quietly re-file it, and the
|
|
1141
|
+
// pill next to Post has to show what this post *is*, not a fresh guess.
|
|
1142
|
+
this._language = post.language ?? null;
|
|
1041
1143
|
|
|
1042
1144
|
if (post.replyToId) {
|
|
1043
1145
|
this._replyToId = post.replyToId;
|
|
@@ -1119,8 +1221,9 @@ export class JantComposeDialog extends LitElement {
|
|
|
1119
1221
|
// answer, which is right: that one belongs to the draft, not to the habit.
|
|
1120
1222
|
this._editor?.setTitleDefault(JantComposeDialog._getNoteTitleDefault());
|
|
1121
1223
|
|
|
1224
|
+
let restored: DraftRestoreOutcome = "none";
|
|
1122
1225
|
if (options?.restoreDraft !== false) {
|
|
1123
|
-
await this.restoreLocalDraft({
|
|
1226
|
+
restored = await this.restoreLocalDraft({
|
|
1124
1227
|
notify: options?.restoreToast,
|
|
1125
1228
|
extraMedia: options?.restoreMedia,
|
|
1126
1229
|
});
|
|
@@ -1137,12 +1240,18 @@ export class JantComposeDialog extends LitElement {
|
|
|
1137
1240
|
this._collectionIds = [options.collectionId, ...this._collectionIds];
|
|
1138
1241
|
}
|
|
1139
1242
|
|
|
1140
|
-
//
|
|
1243
|
+
// The collection's remembered visibility is a default for a fresh post. A
|
|
1244
|
+
// restored draft already carries the answer the author gave it, and a
|
|
1245
|
+
// default must not quietly overwrite a choice — unlike the collection
|
|
1246
|
+
// itself just above, visibility is one value, so applying it here would
|
|
1247
|
+
// replace what they picked rather than add to it, with nothing on screen
|
|
1248
|
+
// to show that it changed.
|
|
1141
1249
|
if (options?.collectionId) {
|
|
1142
1250
|
this._sourceCollectionId = options.collectionId;
|
|
1143
|
-
const saved =
|
|
1144
|
-
|
|
1145
|
-
|
|
1251
|
+
const saved =
|
|
1252
|
+
restored === "restored"
|
|
1253
|
+
? null
|
|
1254
|
+
: JantComposeDialog._getCollectionVisibility(options.collectionId);
|
|
1146
1255
|
if (saved) {
|
|
1147
1256
|
this._visibility = saved;
|
|
1148
1257
|
}
|
|
@@ -1154,6 +1263,37 @@ export class JantComposeDialog extends LitElement {
|
|
|
1154
1263
|
this._captureInitialSnapshot();
|
|
1155
1264
|
}
|
|
1156
1265
|
|
|
1266
|
+
/**
|
|
1267
|
+
* Open the composer on a new post that translates an existing one.
|
|
1268
|
+
*
|
|
1269
|
+
* Nothing is created server-side here: the source ID and the target language
|
|
1270
|
+
* ride along in memory, and the translation group is minted only if the
|
|
1271
|
+
* author saves. Closing the composer leaves no trace — no draft row, no slug,
|
|
1272
|
+
* no single-member group.
|
|
1273
|
+
*
|
|
1274
|
+
* @param sourcePostId - Thread root the new post translates
|
|
1275
|
+
* @param language - Language to write it in
|
|
1276
|
+
*/
|
|
1277
|
+
async openTranslation(sourcePostId: string, language: string) {
|
|
1278
|
+
this.reset();
|
|
1279
|
+
this._editor?.setTitleDefault(JantComposeDialog._getNoteTitleDefault());
|
|
1280
|
+
|
|
1281
|
+
if (this.languages.some((entry) => entry.tag === language)) {
|
|
1282
|
+
this._language = language;
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
this.closest("dialog")?.showModal();
|
|
1286
|
+
await this.updateComplete;
|
|
1287
|
+
this._editor?.focusInput();
|
|
1288
|
+
this._captureInitialSnapshot();
|
|
1289
|
+
|
|
1290
|
+
// Deliberately after the open: the original is context, not a
|
|
1291
|
+
// precondition, and blocking the composer on a fetch would trade a visible
|
|
1292
|
+
// delay for it. `seed` is safe this late because it stands down the moment
|
|
1293
|
+
// the author has typed anything.
|
|
1294
|
+
await this._loadTranslationSource(sourcePostId, { seed: true });
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1157
1297
|
/**
|
|
1158
1298
|
* Open compose dialog in reply mode.
|
|
1159
1299
|
*
|
|
@@ -1320,6 +1460,20 @@ export class JantComposeDialog extends LitElement {
|
|
|
1320
1460
|
);
|
|
1321
1461
|
}
|
|
1322
1462
|
|
|
1463
|
+
/**
|
|
1464
|
+
* Whether closing would throw away something the author wrote.
|
|
1465
|
+
*
|
|
1466
|
+
* Normally "is there content" answers that, because a new composer opens
|
|
1467
|
+
* empty. A translation opens pre-seeded with the original's citation, though,
|
|
1468
|
+
* and being asked to save a draft of a URL you never typed is worse than the
|
|
1469
|
+
* prompt is worth — so once seeded, the question becomes whether anything has
|
|
1470
|
+
* changed since it opened.
|
|
1471
|
+
*/
|
|
1472
|
+
private _hasWorkToLose(): boolean {
|
|
1473
|
+
if (!this._hasContent()) return false;
|
|
1474
|
+
return this._seededFromSource ? this._hasUnsavedChanges() : true;
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1323
1477
|
private _buildSnapshot(): ComposeStateSnapshot | null {
|
|
1324
1478
|
const editor = this._editor;
|
|
1325
1479
|
if (!editor) return null;
|
|
@@ -1337,6 +1491,7 @@ export class JantComposeDialog extends LitElement {
|
|
|
1337
1491
|
publishedAtInput: this._publishedAtInput,
|
|
1338
1492
|
publishedAtTimeMinutes: this._publishedAtTimeMinutes,
|
|
1339
1493
|
visibility: this._visibility,
|
|
1494
|
+
language: this._language,
|
|
1340
1495
|
title: editorData.title,
|
|
1341
1496
|
bodyJson: editor.getNormalizedBodyJson(),
|
|
1342
1497
|
url: editorData.url,
|
|
@@ -1396,6 +1551,7 @@ export class JantComposeDialog extends LitElement {
|
|
|
1396
1551
|
if (this._showPublishPanel) {
|
|
1397
1552
|
this._showPublishPanel = false;
|
|
1398
1553
|
}
|
|
1554
|
+
this._showLanguagePicker = false;
|
|
1399
1555
|
|
|
1400
1556
|
if (this._confirmPanelOpen) {
|
|
1401
1557
|
const restoreAttachedFocus = this._confirmForAttachedText;
|
|
@@ -1426,7 +1582,7 @@ export class JantComposeDialog extends LitElement {
|
|
|
1426
1582
|
return;
|
|
1427
1583
|
}
|
|
1428
1584
|
|
|
1429
|
-
if (this.
|
|
1585
|
+
if (this._hasWorkToLose()) {
|
|
1430
1586
|
this._confirmForDrafts = false;
|
|
1431
1587
|
this._confirmForAttachedText = false;
|
|
1432
1588
|
this._confirmPanelOpen = true;
|
|
@@ -1496,7 +1652,12 @@ export class JantComposeDialog extends LitElement {
|
|
|
1496
1652
|
}
|
|
1497
1653
|
}
|
|
1498
1654
|
|
|
1499
|
-
/**
|
|
1655
|
+
/**
|
|
1656
|
+
* Build the submit payload for the post at `index` — the single post, or one
|
|
1657
|
+
* post of a thread. The one builder for both, so a field the single post
|
|
1658
|
+
* gains reaches a thread's root too: the thread path used to be a copy that
|
|
1659
|
+
* never learned about language or translations.
|
|
1660
|
+
*/
|
|
1500
1661
|
private _buildEditorPostDetail(
|
|
1501
1662
|
editor: JantComposeEditor,
|
|
1502
1663
|
format: ComposeFormat,
|
|
@@ -1569,6 +1730,17 @@ export class JantComposeDialog extends LitElement {
|
|
|
1569
1730
|
? (this._replyRefreshKind ?? undefined)
|
|
1570
1731
|
: undefined,
|
|
1571
1732
|
replyRefreshId: isRoot ? (this._replyRefreshId ?? undefined) : undefined,
|
|
1733
|
+
// Only a Thread's root carries a language: its other posts, and a reply
|
|
1734
|
+
// to an existing Thread, inherit it server-side. An automatic choice
|
|
1735
|
+
// resolves here to whatever the pill beside this button has been showing
|
|
1736
|
+
// — what detection read, or the page's language while it has read
|
|
1737
|
+
// nothing. Absent only on a single-language site, where the server
|
|
1738
|
+
// decides.
|
|
1739
|
+
language:
|
|
1740
|
+
isRoot && !this._replyToId
|
|
1741
|
+
? (this._effectiveLanguage() ?? undefined)
|
|
1742
|
+
: undefined,
|
|
1743
|
+
translationOfId: isRoot ? this._translationOf?.id : undefined,
|
|
1572
1744
|
};
|
|
1573
1745
|
}
|
|
1574
1746
|
|
|
@@ -1578,68 +1750,10 @@ export class JantComposeDialog extends LitElement {
|
|
|
1578
1750
|
const editor = this._editor;
|
|
1579
1751
|
if (!editor) return null;
|
|
1580
1752
|
|
|
1581
|
-
editor.promoteLeadingH1Title({ force: true });
|
|
1582
|
-
const editorData = editor.getData();
|
|
1583
|
-
const mediaAttachments = new Map(
|
|
1584
|
-
(editorData.attachments ?? []).map((attachment) => [
|
|
1585
|
-
attachment.clientId,
|
|
1586
|
-
attachment,
|
|
1587
|
-
]),
|
|
1588
|
-
);
|
|
1589
|
-
const textAttachments = new Map(
|
|
1590
|
-
editorData.attachedTexts.map((item) => [item.clientId, item]),
|
|
1591
|
-
);
|
|
1592
|
-
const orderedAttachments: ComposeSubmitAttachment[] = [];
|
|
1593
|
-
for (const clientId of editorData.attachmentOrder) {
|
|
1594
|
-
const mediaAttachment = mediaAttachments.get(clientId);
|
|
1595
|
-
if (mediaAttachment) {
|
|
1596
|
-
orderedAttachments.push({
|
|
1597
|
-
type: "media",
|
|
1598
|
-
clientId,
|
|
1599
|
-
mediaId: mediaAttachment.mediaId,
|
|
1600
|
-
alt: mediaAttachment.alt || undefined,
|
|
1601
|
-
});
|
|
1602
|
-
continue;
|
|
1603
|
-
}
|
|
1604
|
-
|
|
1605
|
-
const textAttachment = textAttachments.get(clientId);
|
|
1606
|
-
if (textAttachment?.bodyJson) {
|
|
1607
|
-
orderedAttachments.push({
|
|
1608
|
-
type: "text",
|
|
1609
|
-
clientId,
|
|
1610
|
-
bodyJson: textAttachment.bodyJson,
|
|
1611
|
-
summary: textAttachment.summary,
|
|
1612
|
-
mediaId: textAttachment.mediaId,
|
|
1613
|
-
originalBodyJson: normalizeComposeDoc(
|
|
1614
|
-
textAttachment.originalBodyJson ?? null,
|
|
1615
|
-
),
|
|
1616
|
-
});
|
|
1617
|
-
}
|
|
1618
|
-
}
|
|
1619
|
-
|
|
1620
1753
|
return {
|
|
1621
|
-
|
|
1622
|
-
title: editorData.title,
|
|
1623
|
-
body: editorData.body,
|
|
1624
|
-
url: editorData.url,
|
|
1625
|
-
quoteText: editorData.quoteText,
|
|
1626
|
-
quoteAuthor: editorData.quoteAuthor,
|
|
1627
|
-
slug: this._slug.trim() || undefined,
|
|
1628
|
-
publishedAt: this._getPublishedAtSubmitValue(status),
|
|
1629
|
-
status,
|
|
1630
|
-
visibility: this._visibilityLocked ? undefined : this._visibility,
|
|
1631
|
-
rating: editorData.rating,
|
|
1632
|
-
collectionIds: this._replyToId ? [] : [...this._collectionIds],
|
|
1633
|
-
attachments: orderedAttachments,
|
|
1754
|
+
...this._buildEditorPostDetail(editor, this._format, 0, status),
|
|
1634
1755
|
editPostId: this._editPostId ?? this._draftSourceId ?? undefined,
|
|
1635
1756
|
draftSourceId: this._draftSourceId ?? undefined,
|
|
1636
|
-
replyToId: this._replyToId ?? undefined,
|
|
1637
|
-
quietReply: this._canReplyQuietly()
|
|
1638
|
-
? this._quietReply || undefined
|
|
1639
|
-
: undefined,
|
|
1640
|
-
replyThreadRootId: this._replyThreadRootId ?? undefined,
|
|
1641
|
-
replyRefreshKind: this._replyRefreshKind ?? undefined,
|
|
1642
|
-
replyRefreshId: this._replyRefreshId ?? undefined,
|
|
1643
1757
|
};
|
|
1644
1758
|
}
|
|
1645
1759
|
|
|
@@ -2275,7 +2389,12 @@ export class JantComposeDialog extends LitElement {
|
|
|
2275
2389
|
mode: "check",
|
|
2276
2390
|
slug,
|
|
2277
2391
|
});
|
|
2278
|
-
|
|
2392
|
+
// Each post is checked against its own saved copy: a reply's slug is not
|
|
2393
|
+
// the root's to release, and the root's is not the reply's.
|
|
2394
|
+
const postId =
|
|
2395
|
+
index === 0
|
|
2396
|
+
? this._currentSlugOwnerId()
|
|
2397
|
+
: this._threadItems[index]?.sourcePostId;
|
|
2279
2398
|
if (postId) params.set("postId", postId);
|
|
2280
2399
|
|
|
2281
2400
|
try {
|
|
@@ -2612,6 +2731,11 @@ export class JantComposeDialog extends LitElement {
|
|
|
2612
2731
|
return true;
|
|
2613
2732
|
}
|
|
2614
2733
|
|
|
2734
|
+
if (this._showLanguagePicker) {
|
|
2735
|
+
this._closeLanguagePicker(true);
|
|
2736
|
+
return true;
|
|
2737
|
+
}
|
|
2738
|
+
|
|
2615
2739
|
if (this._showCollection) {
|
|
2616
2740
|
this._closeCollectionPicker({ restoreFocus: "editor" });
|
|
2617
2741
|
return true;
|
|
@@ -2869,7 +2993,7 @@ export class JantComposeDialog extends LitElement {
|
|
|
2869
2993
|
|
|
2870
2994
|
private _handleDraftButtonClick() {
|
|
2871
2995
|
if (this._loading) return;
|
|
2872
|
-
if (this.
|
|
2996
|
+
if (this._hasWorkToLose()) {
|
|
2873
2997
|
this._confirmForDrafts = true;
|
|
2874
2998
|
this._confirmPanelOpen = true;
|
|
2875
2999
|
} else {
|
|
@@ -2887,8 +3011,7 @@ export class JantComposeDialog extends LitElement {
|
|
|
2887
3011
|
const res = await fetch("/api/posts?status=draft&limit=50");
|
|
2888
3012
|
if (!res.ok) throw new Error("Failed to load drafts");
|
|
2889
3013
|
const json = (await res.json()) as
|
|
2890
|
-
|
|
|
2891
|
-
| Record<string, unknown>[];
|
|
3014
|
+
DraftsResponse | Record<string, unknown>[];
|
|
2892
3015
|
const posts = Array.isArray(json) ? json : (json.posts ?? []);
|
|
2893
3016
|
const allDraftItems = (posts as Record<string, unknown>[]).map(
|
|
2894
3017
|
(p): DraftItem => ({
|
|
@@ -3019,8 +3142,7 @@ export class JantComposeDialog extends LitElement {
|
|
|
3019
3142
|
const draftsRes = await fetch("/api/posts?status=draft&limit=50");
|
|
3020
3143
|
if (draftsRes.ok) {
|
|
3021
3144
|
const draftsJson = (await draftsRes.json()) as
|
|
3022
|
-
|
|
3023
|
-
| Record<string, unknown>[];
|
|
3145
|
+
{ posts?: Record<string, unknown>[] } | Record<string, unknown>[];
|
|
3024
3146
|
const allDrafts = Array.isArray(draftsJson)
|
|
3025
3147
|
? draftsJson
|
|
3026
3148
|
: (draftsJson.posts ?? []);
|
|
@@ -3036,6 +3158,7 @@ export class JantComposeDialog extends LitElement {
|
|
|
3036
3158
|
.map((p) => ({
|
|
3037
3159
|
id: p.id as string,
|
|
3038
3160
|
format: p.format as ComposeFormat,
|
|
3161
|
+
slug: (p.slug as string | null) ?? null,
|
|
3039
3162
|
replyToId: (p.replyToId as string) ?? null,
|
|
3040
3163
|
title: (p.title as string) ?? null,
|
|
3041
3164
|
body: (p.body as string) ?? null,
|
|
@@ -3063,12 +3186,16 @@ export class JantComposeDialog extends LitElement {
|
|
|
3063
3186
|
if (!ordered.includes(p)) ordered.push(p);
|
|
3064
3187
|
}
|
|
3065
3188
|
|
|
3066
|
-
// Enter thread mode
|
|
3189
|
+
// Enter thread mode. Each reply keeps its own permalink: saving
|
|
3190
|
+
// recreates the thread, so a slug left behind here would come back
|
|
3191
|
+
// as a random id.
|
|
3067
3192
|
this._threadItems = [
|
|
3068
3193
|
{ id: randomUUID(), format: post.format },
|
|
3069
3194
|
...ordered.map((p) => ({
|
|
3070
3195
|
id: randomUUID(),
|
|
3071
3196
|
format: p.format,
|
|
3197
|
+
slug: p.slug ?? "",
|
|
3198
|
+
sourcePostId: p.id,
|
|
3072
3199
|
})),
|
|
3073
3200
|
];
|
|
3074
3201
|
this._focusedThreadIndex = 0;
|
|
@@ -3267,7 +3394,7 @@ export class JantComposeDialog extends LitElement {
|
|
|
3267
3394
|
});
|
|
3268
3395
|
|
|
3269
3396
|
if (!hasContent) {
|
|
3270
|
-
|
|
3397
|
+
this._releaseStoredDraft();
|
|
3271
3398
|
return;
|
|
3272
3399
|
}
|
|
3273
3400
|
|
|
@@ -3286,6 +3413,10 @@ export class JantComposeDialog extends LitElement {
|
|
|
3286
3413
|
url: data.url,
|
|
3287
3414
|
quoteText: data.quoteText,
|
|
3288
3415
|
quoteAuthor: data.quoteAuthor,
|
|
3416
|
+
// Same rules as the single-post draft below.
|
|
3417
|
+
rating: data.rating,
|
|
3418
|
+
showTitle: item.format === "note" ? editor._showTitle : false,
|
|
3419
|
+
showRating: data.rating > 0 ? editor._showRating : false,
|
|
3289
3420
|
attachedTexts: data.attachedTexts.map((t) => ({
|
|
3290
3421
|
clientId: t.clientId,
|
|
3291
3422
|
bodyJson: t.bodyJson,
|
|
@@ -3316,20 +3447,15 @@ export class JantComposeDialog extends LitElement {
|
|
|
3316
3447
|
showRating: false,
|
|
3317
3448
|
collectionIds: [...this._collectionIds],
|
|
3318
3449
|
replyToId: this._replyToId,
|
|
3450
|
+
language: this._language,
|
|
3451
|
+
translationOfId: this._translationOf?.id ?? null,
|
|
3319
3452
|
attachedTexts: [],
|
|
3320
3453
|
attachmentOrder: [],
|
|
3321
3454
|
threadItems,
|
|
3322
3455
|
savedAt: Date.now(),
|
|
3323
3456
|
};
|
|
3324
3457
|
|
|
3325
|
-
|
|
3326
|
-
globalThis.localStorage.setItem(
|
|
3327
|
-
this._currentDraftStorageKey(),
|
|
3328
|
-
JSON.stringify(draft),
|
|
3329
|
-
);
|
|
3330
|
-
} catch {
|
|
3331
|
-
// Storage full or unavailable — silently ignore
|
|
3332
|
-
}
|
|
3458
|
+
this._writeStoredDraft(draft);
|
|
3333
3459
|
return;
|
|
3334
3460
|
}
|
|
3335
3461
|
|
|
@@ -3341,7 +3467,7 @@ export class JantComposeDialog extends LitElement {
|
|
|
3341
3467
|
// for edit (or restoring a draft) would write a local draft of the
|
|
3342
3468
|
// unchanged content, since loading fires content-change events.
|
|
3343
3469
|
if (!this._hasUnsavedChanges()) {
|
|
3344
|
-
|
|
3470
|
+
this._releaseStoredDraft();
|
|
3345
3471
|
return;
|
|
3346
3472
|
}
|
|
3347
3473
|
|
|
@@ -3357,7 +3483,7 @@ export class JantComposeDialog extends LitElement {
|
|
|
3357
3483
|
data.attachedTexts.length > 0;
|
|
3358
3484
|
|
|
3359
3485
|
if (!hasContent) {
|
|
3360
|
-
|
|
3486
|
+
this._releaseStoredDraft();
|
|
3361
3487
|
return;
|
|
3362
3488
|
}
|
|
3363
3489
|
|
|
@@ -3380,6 +3506,8 @@ export class JantComposeDialog extends LitElement {
|
|
|
3380
3506
|
showRating: data.rating > 0 ? editor._showRating : false,
|
|
3381
3507
|
collectionIds: [...this._collectionIds],
|
|
3382
3508
|
replyToId: this._replyToId,
|
|
3509
|
+
language: this._language,
|
|
3510
|
+
translationOfId: this._translationOf?.id ?? null,
|
|
3383
3511
|
attachedTexts: data.attachedTexts.map((t) => ({
|
|
3384
3512
|
clientId: t.clientId,
|
|
3385
3513
|
bodyJson: t.bodyJson,
|
|
@@ -3393,18 +3521,49 @@ export class JantComposeDialog extends LitElement {
|
|
|
3393
3521
|
savedAt: Date.now(),
|
|
3394
3522
|
};
|
|
3395
3523
|
|
|
3524
|
+
this._writeStoredDraft(draft);
|
|
3525
|
+
}
|
|
3526
|
+
|
|
3527
|
+
/**
|
|
3528
|
+
* Persist the draft and take ownership of the key it went to.
|
|
3529
|
+
*
|
|
3530
|
+
* A write that does not stick is the one failure the author must hear about:
|
|
3531
|
+
* the local copy is the last thing standing between a dropped connection and
|
|
3532
|
+
* lost work, and a composer that cannot store one looks exactly like a
|
|
3533
|
+
* composer that can. Said once per composer, not once per keystroke.
|
|
3534
|
+
*/
|
|
3535
|
+
private _writeStoredDraft(draft: LocalDraft) {
|
|
3396
3536
|
try {
|
|
3397
3537
|
globalThis.localStorage.setItem(
|
|
3398
3538
|
this._currentDraftStorageKey(),
|
|
3399
3539
|
JSON.stringify(draft),
|
|
3400
3540
|
);
|
|
3401
|
-
|
|
3402
|
-
|
|
3541
|
+
this._hasOwnedContent = true;
|
|
3542
|
+
} catch (error) {
|
|
3543
|
+
// eslint-disable-next-line no-console -- the local copy is the last safety net; a lost one must be visible
|
|
3544
|
+
console.warn("[jant] compose draft could not be stored", error);
|
|
3545
|
+
if (!this._reportedStorageFailure) {
|
|
3546
|
+
this._reportedStorageFailure = true;
|
|
3547
|
+
showToast(this.labels.draftStoreFailed, "error");
|
|
3548
|
+
}
|
|
3403
3549
|
}
|
|
3404
3550
|
}
|
|
3405
3551
|
|
|
3552
|
+
/**
|
|
3553
|
+
* Drop the stored draft, but only when this composer is the one holding it.
|
|
3554
|
+
*
|
|
3555
|
+
* See `_hasOwnedContent`: an empty composer that never loaded anything has
|
|
3556
|
+
* nothing to say about a draft written by another composer or another tab.
|
|
3557
|
+
*/
|
|
3558
|
+
private _releaseStoredDraft() {
|
|
3559
|
+
if (!this._hasOwnedContent) return;
|
|
3560
|
+
this._hasOwnedContent = false;
|
|
3561
|
+
globalThis.localStorage.removeItem(this._currentDraftStorageKey());
|
|
3562
|
+
}
|
|
3563
|
+
|
|
3406
3564
|
private _clearDraftFromStorage() {
|
|
3407
3565
|
this._cancelDraftSaveTimer();
|
|
3566
|
+
this._hasOwnedContent = false;
|
|
3408
3567
|
globalThis.localStorage.removeItem(this._currentDraftStorageKey());
|
|
3409
3568
|
}
|
|
3410
3569
|
|
|
@@ -3478,39 +3637,44 @@ export class JantComposeDialog extends LitElement {
|
|
|
3478
3637
|
notify?: boolean;
|
|
3479
3638
|
/** Completed uploads known to the bridge at failure time */
|
|
3480
3639
|
extraMedia?: LocalDraftMedia[];
|
|
3481
|
-
}) {
|
|
3640
|
+
}): Promise<DraftRestoreOutcome> {
|
|
3641
|
+
const key = JantComposeDialog._DRAFT_KEY;
|
|
3482
3642
|
// Don't restore if already in edit or draft-load mode
|
|
3483
|
-
if (this._editPostId || this._draftSourceId)
|
|
3643
|
+
if (this._editPostId || this._draftSourceId) {
|
|
3644
|
+
return this._reportRestoreOutcome("composer-busy", key);
|
|
3645
|
+
}
|
|
3484
3646
|
// Don't restore if the editor already has content (e.g. reopened dialog)
|
|
3485
|
-
if (this._hasContent())
|
|
3647
|
+
if (this._hasContent()) {
|
|
3648
|
+
return this._reportRestoreOutcome("composer-has-content", key);
|
|
3649
|
+
}
|
|
3486
3650
|
|
|
3487
3651
|
let raw: string | null;
|
|
3488
3652
|
try {
|
|
3489
|
-
raw = globalThis.localStorage.getItem(
|
|
3653
|
+
raw = globalThis.localStorage.getItem(key);
|
|
3490
3654
|
} catch {
|
|
3491
|
-
return;
|
|
3655
|
+
return "none";
|
|
3492
3656
|
}
|
|
3493
|
-
if (!raw) return;
|
|
3657
|
+
if (!raw) return "none";
|
|
3494
3658
|
|
|
3495
3659
|
let draft: LocalDraft;
|
|
3496
3660
|
try {
|
|
3497
3661
|
draft = JSON.parse(raw) as LocalDraft;
|
|
3498
3662
|
} catch {
|
|
3499
|
-
|
|
3500
|
-
|
|
3663
|
+
// Deliberately kept: bytes we cannot parse are still something the author
|
|
3664
|
+
// wrote, and deleting them here would turn a bad read into a lost draft.
|
|
3665
|
+
return this._reportRestoreOutcome("unreadable", key, raw);
|
|
3501
3666
|
}
|
|
3502
3667
|
|
|
3503
|
-
//
|
|
3668
|
+
// Too old to put back, but still theirs — leave it where it is.
|
|
3504
3669
|
if (Date.now() - draft.savedAt > JantComposeDialog._DRAFT_MAX_AGE) {
|
|
3505
|
-
|
|
3506
|
-
return;
|
|
3670
|
+
return this._reportRestoreOutcome("expired", key, raw, draft);
|
|
3507
3671
|
}
|
|
3508
3672
|
|
|
3509
3673
|
if (
|
|
3510
3674
|
options?.expectedReplyToId !== undefined &&
|
|
3511
3675
|
draft.replyToId !== options.expectedReplyToId
|
|
3512
3676
|
) {
|
|
3513
|
-
return;
|
|
3677
|
+
return this._reportRestoreOutcome("other-reply", key, raw, draft);
|
|
3514
3678
|
}
|
|
3515
3679
|
|
|
3516
3680
|
this._collectionIds = draft.replyToId
|
|
@@ -3525,6 +3689,10 @@ export class JantComposeDialog extends LitElement {
|
|
|
3525
3689
|
this._publishedAtInput = draft.publishedAtInput ?? "";
|
|
3526
3690
|
this._publishedAtTimeMinutes = draft.publishedAtTimeMinutes ?? null;
|
|
3527
3691
|
this._visibility = draft.visibility ?? "public";
|
|
3692
|
+
this._language = draft.language ?? null;
|
|
3693
|
+
if (draft.translationOfId) {
|
|
3694
|
+
await this._loadTranslationSource(draft.translationOfId);
|
|
3695
|
+
}
|
|
3528
3696
|
|
|
3529
3697
|
// Restore reply context if this draft was a reply
|
|
3530
3698
|
if (draft.replyToId) {
|
|
@@ -3587,6 +3755,9 @@ export class JantComposeDialog extends LitElement {
|
|
|
3587
3755
|
url: item.url || undefined,
|
|
3588
3756
|
quoteText: item.quoteText || undefined,
|
|
3589
3757
|
quoteAuthor: item.quoteAuthor || undefined,
|
|
3758
|
+
rating: item.rating || undefined,
|
|
3759
|
+
showTitle: item.showTitle,
|
|
3760
|
+
showRating: item.showRating,
|
|
3590
3761
|
media: media.length
|
|
3591
3762
|
? JantComposeDialog._restoredMediaToPopulate(media)
|
|
3592
3763
|
: undefined,
|
|
@@ -3598,11 +3769,12 @@ export class JantComposeDialog extends LitElement {
|
|
|
3598
3769
|
}
|
|
3599
3770
|
|
|
3600
3771
|
this._draftRestored = true;
|
|
3772
|
+
this._hasOwnedContent = true;
|
|
3601
3773
|
if (options?.notify !== false) showToast(this.labels.draftRestored);
|
|
3602
3774
|
globalThis.requestAnimationFrame(() => {
|
|
3603
3775
|
this._captureInitialSnapshot();
|
|
3604
3776
|
});
|
|
3605
|
-
return;
|
|
3777
|
+
return "restored";
|
|
3606
3778
|
}
|
|
3607
3779
|
|
|
3608
3780
|
// ── Single-post draft restore ────────────────────────────────────
|
|
@@ -3648,10 +3820,59 @@ export class JantComposeDialog extends LitElement {
|
|
|
3648
3820
|
});
|
|
3649
3821
|
|
|
3650
3822
|
this._draftRestored = true;
|
|
3823
|
+
this._hasOwnedContent = true;
|
|
3651
3824
|
if (options?.notify !== false) showToast(this.labels.draftRestored);
|
|
3652
3825
|
globalThis.requestAnimationFrame(() => {
|
|
3653
3826
|
this._captureInitialSnapshot();
|
|
3654
3827
|
});
|
|
3828
|
+
return "restored";
|
|
3829
|
+
}
|
|
3830
|
+
|
|
3831
|
+
/**
|
|
3832
|
+
* Say why a stored draft did not reach the composer.
|
|
3833
|
+
*
|
|
3834
|
+
* A composer that opens empty looks the same whether there was nothing to
|
|
3835
|
+
* restore or something went wrong reading what was there. That costs nothing
|
|
3836
|
+
* until an author asks where their work went, at which point there is nothing
|
|
3837
|
+
* to look at. `none` — the ordinary "no draft stored" case — stays quiet;
|
|
3838
|
+
* everything else means bytes existed and did not come back, which is worth
|
|
3839
|
+
* saying out loud.
|
|
3840
|
+
*
|
|
3841
|
+
* @param outcome - Why the restore stopped
|
|
3842
|
+
* @param key - Storage key that was read
|
|
3843
|
+
* @param raw - The stored string, when one was found
|
|
3844
|
+
* @param draft - The parsed draft, when it parsed
|
|
3845
|
+
* @returns The outcome, so callers can `return` this directly
|
|
3846
|
+
* @example
|
|
3847
|
+
* return this._reportRestoreOutcome("expired", key, raw, draft);
|
|
3848
|
+
*/
|
|
3849
|
+
private _reportRestoreOutcome(
|
|
3850
|
+
outcome: DraftRestoreOutcome,
|
|
3851
|
+
key: string,
|
|
3852
|
+
raw?: string | null,
|
|
3853
|
+
draft?: LocalDraft,
|
|
3854
|
+
): DraftRestoreOutcome {
|
|
3855
|
+
if (outcome === "restored" || outcome === "none") return outcome;
|
|
3856
|
+
|
|
3857
|
+
let stored = raw;
|
|
3858
|
+
if (stored === undefined) {
|
|
3859
|
+
try {
|
|
3860
|
+
stored = globalThis.localStorage.getItem(key);
|
|
3861
|
+
} catch {
|
|
3862
|
+
stored = null;
|
|
3863
|
+
}
|
|
3864
|
+
}
|
|
3865
|
+
// Nothing was stored, so nothing was lost — the composer just opened empty.
|
|
3866
|
+
if (!stored) return "none";
|
|
3867
|
+
|
|
3868
|
+
// eslint-disable-next-line no-console -- the one record of why recovered work did not come back
|
|
3869
|
+
console.warn(`[jant] compose draft not restored: ${outcome}`, {
|
|
3870
|
+
key,
|
|
3871
|
+
bytes: stored.length,
|
|
3872
|
+
savedAt: draft?.savedAt ? new Date(draft.savedAt).toISOString() : null,
|
|
3873
|
+
thread: draft?.threadItems?.length ?? null,
|
|
3874
|
+
});
|
|
3875
|
+
return outcome;
|
|
3655
3876
|
}
|
|
3656
3877
|
|
|
3657
3878
|
/**
|
|
@@ -3676,12 +3897,13 @@ export class JantComposeDialog extends LitElement {
|
|
|
3676
3897
|
try {
|
|
3677
3898
|
draft = JSON.parse(raw) as LocalDraft;
|
|
3678
3899
|
} catch {
|
|
3679
|
-
|
|
3900
|
+
// Kept, not deleted: see `restoreLocalDraft`.
|
|
3901
|
+
this._reportRestoreOutcome("unreadable", key, raw);
|
|
3680
3902
|
return false;
|
|
3681
3903
|
}
|
|
3682
3904
|
|
|
3683
3905
|
if (Date.now() - draft.savedAt > JantComposeDialog._DRAFT_MAX_AGE) {
|
|
3684
|
-
|
|
3906
|
+
this._reportRestoreOutcome("expired", key, raw, draft);
|
|
3685
3907
|
return false;
|
|
3686
3908
|
}
|
|
3687
3909
|
|
|
@@ -3694,6 +3916,10 @@ export class JantComposeDialog extends LitElement {
|
|
|
3694
3916
|
this._publishedAtInput = draft.publishedAtInput ?? "";
|
|
3695
3917
|
this._publishedAtTimeMinutes = draft.publishedAtTimeMinutes ?? null;
|
|
3696
3918
|
this._visibility = draft.visibility ?? "public";
|
|
3919
|
+
// The draft speaks for the language too, and its answer may be that the
|
|
3920
|
+
// author put this post back on automatic — so it replaces the one seeded
|
|
3921
|
+
// from the post rather than filling in for a missing one.
|
|
3922
|
+
this._language = draft.language ?? null;
|
|
3697
3923
|
|
|
3698
3924
|
// Restore editor content
|
|
3699
3925
|
const textAttachments = draft.attachedTexts?.flatMap((t) => {
|
|
@@ -3724,16 +3950,42 @@ export class JantComposeDialog extends LitElement {
|
|
|
3724
3950
|
});
|
|
3725
3951
|
|
|
3726
3952
|
this._draftRestored = true;
|
|
3953
|
+
this._hasOwnedContent = true;
|
|
3727
3954
|
if (notify !== false) showToast(this.labels.draftRestored);
|
|
3728
3955
|
return true;
|
|
3729
3956
|
}
|
|
3730
3957
|
|
|
3958
|
+
/**
|
|
3959
|
+
* Pick up a "write the translation" request from the URL.
|
|
3960
|
+
*
|
|
3961
|
+
* The post menu sends the author here with the source Thread and the target
|
|
3962
|
+
* language in the query string, and nothing else exists yet: no draft row, no
|
|
3963
|
+
* slug, no group. Applied after any local draft restore so a draft the author
|
|
3964
|
+
* already started for this translation wins over the bare request.
|
|
3965
|
+
*/
|
|
3966
|
+
private async _applyTranslationRequestFromUrl(): Promise<void> {
|
|
3967
|
+
const params = new URLSearchParams(globalThis.location?.search ?? "");
|
|
3968
|
+
const translationOf = params.get("translationOf");
|
|
3969
|
+
if (!translationOf) return;
|
|
3970
|
+
|
|
3971
|
+
const lang = params.get("lang");
|
|
3972
|
+
if (lang && this.languages.some((language) => language.tag === lang)) {
|
|
3973
|
+
this._language = lang;
|
|
3974
|
+
}
|
|
3975
|
+
if (!this._translationOf) {
|
|
3976
|
+
// `seed` still stands down on its own if the restore above brought
|
|
3977
|
+
// content back, so a draft the author already started keeps its format.
|
|
3978
|
+
await this._loadTranslationSource(translationOf, { seed: true });
|
|
3979
|
+
}
|
|
3980
|
+
}
|
|
3981
|
+
|
|
3731
3982
|
private async _focusPageEditorOnMount() {
|
|
3732
3983
|
if (this._pageFocusApplied) return;
|
|
3733
3984
|
|
|
3734
3985
|
if (this.autoRestoreDraft) {
|
|
3735
3986
|
await this.restoreLocalDraft();
|
|
3736
3987
|
}
|
|
3988
|
+
await this._applyTranslationRequestFromUrl();
|
|
3737
3989
|
|
|
3738
3990
|
await this.updateComplete;
|
|
3739
3991
|
globalThis.requestAnimationFrame(() => {
|
|
@@ -3769,37 +4021,43 @@ export class JantComposeDialog extends LitElement {
|
|
|
3769
4021
|
</button>
|
|
3770
4022
|
<span class="compose-alt-title">${this.labels.drafts}</span>
|
|
3771
4023
|
</div>
|
|
3772
|
-
${
|
|
3773
|
-
|
|
3774
|
-
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
: this._drafts.length === 0
|
|
4024
|
+
${
|
|
4025
|
+
this._draftsLoading
|
|
4026
|
+
? html`<div class="compose-drafts-loading">
|
|
4027
|
+
<svg
|
|
4028
|
+
class="animate-spin size-5"
|
|
4029
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
4030
|
+
viewBox="0 0 24 24"
|
|
4031
|
+
fill="none"
|
|
4032
|
+
stroke="currentColor"
|
|
4033
|
+
stroke-width="2"
|
|
4034
|
+
stroke-linecap="round"
|
|
4035
|
+
stroke-linejoin="round"
|
|
4036
|
+
>
|
|
4037
|
+
<path d="M21 12a9 9 0 1 1-6.219-8.56" />
|
|
4038
|
+
</svg>
|
|
4039
|
+
</div>`
|
|
4040
|
+
: this._draftsError
|
|
3790
4041
|
? html`<div class="compose-drafts-empty">
|
|
3791
|
-
${this.
|
|
4042
|
+
${this._draftsError}
|
|
3792
4043
|
</div>`
|
|
3793
|
-
:
|
|
3794
|
-
|
|
3795
|
-
|
|
3796
|
-
|
|
3797
|
-
|
|
3798
|
-
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
|
|
3802
|
-
|
|
4044
|
+
: this._drafts.length === 0
|
|
4045
|
+
? html`<div class="compose-drafts-empty">
|
|
4046
|
+
${this.labels.draftsEmpty}
|
|
4047
|
+
</div>`
|
|
4048
|
+
: html`<div class="compose-drafts-list">
|
|
4049
|
+
${this._drafts.map(
|
|
4050
|
+
(draft, i) => html`
|
|
4051
|
+
${
|
|
4052
|
+
i > 0
|
|
4053
|
+
? html`<div class="compose-drafts-divider"></div>`
|
|
4054
|
+
: nothing
|
|
4055
|
+
}
|
|
4056
|
+
${this._renderDraftItem(draft)}
|
|
4057
|
+
`,
|
|
4058
|
+
)}
|
|
4059
|
+
</div>`
|
|
4060
|
+
}
|
|
3803
4061
|
</div>
|
|
3804
4062
|
`;
|
|
3805
4063
|
}
|
|
@@ -3813,13 +4071,15 @@ export class JantComposeDialog extends LitElement {
|
|
|
3813
4071
|
return html`
|
|
3814
4072
|
<div class="compose-draft-item" @click=${() => this.openDraft(draft.id)}>
|
|
3815
4073
|
<div class="compose-draft-content">
|
|
3816
|
-
${
|
|
3817
|
-
|
|
3818
|
-
|
|
3819
|
-
|
|
3820
|
-
|
|
3821
|
-
|
|
3822
|
-
|
|
4074
|
+
${
|
|
4075
|
+
preview
|
|
4076
|
+
? html`<div class="compose-draft-preview">${preview}</div>`
|
|
4077
|
+
: html`<div
|
|
4078
|
+
class="compose-draft-preview compose-draft-preview-empty"
|
|
4079
|
+
>
|
|
4080
|
+
Empty draft
|
|
4081
|
+
</div>`
|
|
4082
|
+
}
|
|
3823
4083
|
<div class="compose-draft-meta">
|
|
3824
4084
|
<span class="compose-draft-format">${formatLabel}</span>
|
|
3825
4085
|
<span aria-hidden="true">·</span>
|
|
@@ -3827,15 +4087,17 @@ export class JantComposeDialog extends LitElement {
|
|
|
3827
4087
|
</div>
|
|
3828
4088
|
</div>
|
|
3829
4089
|
<div class="relative">
|
|
3830
|
-
${
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
e
|
|
3835
|
-
|
|
3836
|
-
|
|
3837
|
-
|
|
3838
|
-
|
|
4090
|
+
${
|
|
4091
|
+
menuOpen
|
|
4092
|
+
? html`<div
|
|
4093
|
+
class="compose-dropdown-backdrop"
|
|
4094
|
+
@click=${(e: Event) => {
|
|
4095
|
+
e.stopPropagation();
|
|
4096
|
+
this._draftMenuOpenId = null;
|
|
4097
|
+
}}
|
|
4098
|
+
></div>`
|
|
4099
|
+
: nothing
|
|
4100
|
+
}
|
|
3839
4101
|
<button
|
|
3840
4102
|
type="button"
|
|
3841
4103
|
class="compose-draft-more"
|
|
@@ -3855,44 +4117,46 @@ export class JantComposeDialog extends LitElement {
|
|
|
3855
4117
|
<circle cx="12" cy="8" r="1.2" />
|
|
3856
4118
|
</svg>
|
|
3857
4119
|
</button>
|
|
3858
|
-
${
|
|
3859
|
-
|
|
3860
|
-
|
|
3861
|
-
id=${menuId}
|
|
3862
|
-
class="compose-dropdown compose-dropdown-right"
|
|
3863
|
-
role="menu"
|
|
3864
|
-
>
|
|
3865
|
-
<a
|
|
3866
|
-
class="compose-dropdown-item"
|
|
3867
|
-
href=${publicPath(`/preview/${draft.slug}`)}
|
|
3868
|
-
target="_blank"
|
|
3869
|
-
rel="noopener noreferrer"
|
|
3870
|
-
role="menuitem"
|
|
3871
|
-
@click=${(e: Event) => {
|
|
3872
|
-
e.stopPropagation();
|
|
3873
|
-
this._draftMenuOpenId = null;
|
|
3874
|
-
}}
|
|
3875
|
-
>
|
|
3876
|
-
${this.labels.previewDraft}
|
|
3877
|
-
</a>
|
|
4120
|
+
${
|
|
4121
|
+
menuOpen
|
|
4122
|
+
? html`
|
|
3878
4123
|
<div
|
|
3879
|
-
|
|
3880
|
-
|
|
3881
|
-
|
|
3882
|
-
<button
|
|
3883
|
-
type="button"
|
|
3884
|
-
class="compose-dropdown-item compose-dropdown-item-danger"
|
|
3885
|
-
role="menuitem"
|
|
3886
|
-
@click=${(e: Event) => {
|
|
3887
|
-
e.stopPropagation();
|
|
3888
|
-
this._deleteDraft(draft.id);
|
|
3889
|
-
}}
|
|
4124
|
+
id=${menuId}
|
|
4125
|
+
class="compose-dropdown compose-dropdown-right"
|
|
4126
|
+
role="menu"
|
|
3890
4127
|
>
|
|
3891
|
-
|
|
3892
|
-
|
|
3893
|
-
|
|
3894
|
-
|
|
3895
|
-
|
|
4128
|
+
<a
|
|
4129
|
+
class="compose-dropdown-item"
|
|
4130
|
+
href=${publicPath(`/preview/${draft.slug}`)}
|
|
4131
|
+
target="_blank"
|
|
4132
|
+
rel="noopener noreferrer"
|
|
4133
|
+
role="menuitem"
|
|
4134
|
+
@click=${(e: Event) => {
|
|
4135
|
+
e.stopPropagation();
|
|
4136
|
+
this._draftMenuOpenId = null;
|
|
4137
|
+
}}
|
|
4138
|
+
>
|
|
4139
|
+
${this.labels.previewDraft}
|
|
4140
|
+
</a>
|
|
4141
|
+
<div
|
|
4142
|
+
class="compose-dropdown-separator"
|
|
4143
|
+
role="separator"
|
|
4144
|
+
></div>
|
|
4145
|
+
<button
|
|
4146
|
+
type="button"
|
|
4147
|
+
class="compose-dropdown-item compose-dropdown-item-danger"
|
|
4148
|
+
role="menuitem"
|
|
4149
|
+
@click=${(e: Event) => {
|
|
4150
|
+
e.stopPropagation();
|
|
4151
|
+
this._deleteDraft(draft.id);
|
|
4152
|
+
}}
|
|
4153
|
+
>
|
|
4154
|
+
${this.labels.deleteDraft}
|
|
4155
|
+
</button>
|
|
4156
|
+
</div>
|
|
4157
|
+
`
|
|
4158
|
+
: nothing
|
|
4159
|
+
}
|
|
3896
4160
|
</div>
|
|
3897
4161
|
</div>
|
|
3898
4162
|
`;
|
|
@@ -3900,6 +4164,167 @@ export class JantComposeDialog extends LitElement {
|
|
|
3900
4164
|
|
|
3901
4165
|
// ── Reply context rendering ──────────────────────────────────────
|
|
3902
4166
|
|
|
4167
|
+
/**
|
|
4168
|
+
* The original, above the composer that is translating it.
|
|
4169
|
+
*
|
|
4170
|
+
* Sits where the reply context sits, for the same reason: a composer that
|
|
4171
|
+
* was opened *about* something should say so before the author starts
|
|
4172
|
+
* typing, not bury it two panels deep.
|
|
4173
|
+
*
|
|
4174
|
+
* Three decisions worth keeping:
|
|
4175
|
+
*
|
|
4176
|
+
* - It shows the whole post as the site renders it — server-rendered by
|
|
4177
|
+
* `/_/post-preview`, so a Quote arrives with its attribution and a Link
|
|
4178
|
+
* with its card. Structure is part of what gets translated, and a
|
|
4179
|
+
* `bodyHtml`-only preview silently dropped everything that is not body.
|
|
4180
|
+
* - It scrolls inside a fixed frame instead of expanding. Translation is
|
|
4181
|
+
* read-a-bit, write-a-bit: a "show more" that hands a long post its full
|
|
4182
|
+
* height pushes the editor off the screen exactly when both need to be
|
|
4183
|
+
* visible. (The reply context expands, and is right to — it is read once,
|
|
4184
|
+
* before writing, not alongside.)
|
|
4185
|
+
* - Nothing is bolted on to say "this is a link somewhere". The post's own
|
|
4186
|
+
* permalink is inside the rendering already; `_adoptTranslationPreview`
|
|
4187
|
+
* sends it to a new tab. An external-link glyph on a title made the whole
|
|
4188
|
+
* card read as a Link post rather than as the article it is.
|
|
4189
|
+
*
|
|
4190
|
+
* The language goes on the divider underneath rather than at the top: it
|
|
4191
|
+
* belongs at the seam, where it reads as "…and below is that, in Japanese".
|
|
4192
|
+
*/
|
|
4193
|
+
private _renderTranslationContext() {
|
|
4194
|
+
const source = this._translationOf;
|
|
4195
|
+
if (!source) return nothing;
|
|
4196
|
+
|
|
4197
|
+
const languageLabel =
|
|
4198
|
+
this.languages.find((entry) => entry.tag === this._language)?.label ?? "";
|
|
4199
|
+
const seam = languageLabel
|
|
4200
|
+
? this.labels.translationContextInLanguage.replace(
|
|
4201
|
+
"{language}",
|
|
4202
|
+
languageLabel,
|
|
4203
|
+
)
|
|
4204
|
+
: this.labels.translationContext;
|
|
4205
|
+
|
|
4206
|
+
const collapsed = this._translationCollapsed;
|
|
4207
|
+
|
|
4208
|
+
return html`
|
|
4209
|
+
<div class="compose-translation-context">
|
|
4210
|
+
${
|
|
4211
|
+
collapsed
|
|
4212
|
+
? nothing
|
|
4213
|
+
: source.previewHtml
|
|
4214
|
+
? html`<div
|
|
4215
|
+
class="compose-translation-original"
|
|
4216
|
+
tabindex="0"
|
|
4217
|
+
role="region"
|
|
4218
|
+
aria-label=${this.labels.translationContextOriginal}
|
|
4219
|
+
>
|
|
4220
|
+
<div class="compose-translation-preview">
|
|
4221
|
+
${unsafeHTML(source.previewHtml)}
|
|
4222
|
+
</div>
|
|
4223
|
+
</div>`
|
|
4224
|
+
: html`<p class="compose-translation-fallback">
|
|
4225
|
+
${
|
|
4226
|
+
source.href
|
|
4227
|
+
? html`<a
|
|
4228
|
+
href=${source.href}
|
|
4229
|
+
target="_blank"
|
|
4230
|
+
rel="noopener noreferrer"
|
|
4231
|
+
title=${this.labels.translationContextOpen}
|
|
4232
|
+
>${source.title}</a
|
|
4233
|
+
>`
|
|
4234
|
+
: source.title
|
|
4235
|
+
}
|
|
4236
|
+
</p>`
|
|
4237
|
+
}
|
|
4238
|
+
<div class="compose-translation-seam">
|
|
4239
|
+
<span class="compose-translation-seam-label">
|
|
4240
|
+
${this._iconArrowDown()}${seam}
|
|
4241
|
+
</span>
|
|
4242
|
+
<span class="compose-translation-seam-dot" aria-hidden="true">·</span>
|
|
4243
|
+
<button
|
|
4244
|
+
type="button"
|
|
4245
|
+
class="compose-translation-seam-toggle"
|
|
4246
|
+
aria-expanded=${collapsed ? "false" : "true"}
|
|
4247
|
+
aria-label=${
|
|
4248
|
+
collapsed
|
|
4249
|
+
? this.labels.translationContextShowLong
|
|
4250
|
+
: this.labels.translationContextHideLong
|
|
4251
|
+
}
|
|
4252
|
+
@click=${() => {
|
|
4253
|
+
this._translationCollapsed = !this._translationCollapsed;
|
|
4254
|
+
}}
|
|
4255
|
+
>
|
|
4256
|
+
${
|
|
4257
|
+
collapsed
|
|
4258
|
+
? this.labels.translationContextShow
|
|
4259
|
+
: this.labels.translationContextHide
|
|
4260
|
+
}
|
|
4261
|
+
</button>
|
|
4262
|
+
</div>
|
|
4263
|
+
</div>
|
|
4264
|
+
`;
|
|
4265
|
+
}
|
|
4266
|
+
|
|
4267
|
+
/**
|
|
4268
|
+
* Make the server-rendered original safe to sit inside the composer.
|
|
4269
|
+
*
|
|
4270
|
+
* Two things have to be undone, both because this markup was written to *be*
|
|
4271
|
+
* a post rather than to be quoted inside one:
|
|
4272
|
+
*
|
|
4273
|
+
* - Every link leaves for a new tab. A link followed in place navigates the
|
|
4274
|
+
* composer away and takes the unsaved translation with it.
|
|
4275
|
+
* - The post's identity comes off. `data-post-id` and friends are how the
|
|
4276
|
+
* post menu, the keyboard shortcuts and `refreshArticleView` find a post;
|
|
4277
|
+
* leaving a second copy of the original's id in the DOM lets any of them
|
|
4278
|
+
* act on the preview believing it is the real card.
|
|
4279
|
+
*
|
|
4280
|
+
* Runs on every update because `unsafeHTML` re-creates the subtree whenever
|
|
4281
|
+
* the markup changes, and is cheap to repeat — the marker attribute makes it
|
|
4282
|
+
* a no-op once a given rendering has been adopted.
|
|
4283
|
+
*/
|
|
4284
|
+
private _adoptTranslationPreview() {
|
|
4285
|
+
const preview = this.querySelector<HTMLElement>(
|
|
4286
|
+
".compose-translation-preview:not([data-preview-adopted])",
|
|
4287
|
+
);
|
|
4288
|
+
if (!preview) return;
|
|
4289
|
+
preview.dataset.previewAdopted = "";
|
|
4290
|
+
|
|
4291
|
+
for (const link of preview.querySelectorAll("a[href]")) {
|
|
4292
|
+
link.setAttribute("target", "_blank");
|
|
4293
|
+
link.setAttribute("rel", "noopener noreferrer");
|
|
4294
|
+
}
|
|
4295
|
+
|
|
4296
|
+
for (const el of preview.querySelectorAll<HTMLElement>("[data-post-id]")) {
|
|
4297
|
+
delete el.dataset.postId;
|
|
4298
|
+
delete el.dataset.post;
|
|
4299
|
+
delete el.dataset.threadRootId;
|
|
4300
|
+
delete el.dataset.postMenuTarget;
|
|
4301
|
+
el.classList.remove("post-menu-target");
|
|
4302
|
+
}
|
|
4303
|
+
for (const trigger of preview.querySelectorAll(
|
|
4304
|
+
"[data-post-menu-trigger], [data-reply-trigger], [data-timeline-item]",
|
|
4305
|
+
)) {
|
|
4306
|
+
trigger.remove();
|
|
4307
|
+
}
|
|
4308
|
+
}
|
|
4309
|
+
|
|
4310
|
+
/** Points at what comes next: the version being written, below. */
|
|
4311
|
+
private _iconArrowDown() {
|
|
4312
|
+
return html`<svg
|
|
4313
|
+
class="compose-translation-seam-icon"
|
|
4314
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
4315
|
+
viewBox="0 0 24 24"
|
|
4316
|
+
fill="none"
|
|
4317
|
+
stroke="currentColor"
|
|
4318
|
+
stroke-width="1.9"
|
|
4319
|
+
stroke-linecap="round"
|
|
4320
|
+
stroke-linejoin="round"
|
|
4321
|
+
aria-hidden="true"
|
|
4322
|
+
>
|
|
4323
|
+
<path d="M12 5v14" />
|
|
4324
|
+
<path d="m19 12-7 7-7-7" />
|
|
4325
|
+
</svg>`;
|
|
4326
|
+
}
|
|
4327
|
+
|
|
3903
4328
|
private _renderReplyContext() {
|
|
3904
4329
|
if (!this._replyToId || !this._replyToData) return nothing;
|
|
3905
4330
|
|
|
@@ -3917,42 +4342,44 @@ export class JantComposeDialog extends LitElement {
|
|
|
3917
4342
|
>
|
|
3918
4343
|
<div class="compose-reply-context-body">
|
|
3919
4344
|
${unsafeHTML(contentHtml)}
|
|
3920
|
-
${
|
|
3921
|
-
|
|
3922
|
-
|
|
3923
|
-
|
|
3924
|
-
|
|
3925
|
-
|
|
3926
|
-
|
|
3927
|
-
|
|
3928
|
-
|
|
3929
|
-
|
|
3930
|
-
|
|
3931
|
-
|
|
3932
|
-
|
|
3933
|
-
|
|
3934
|
-
|
|
3935
|
-
(
|
|
3936
|
-
|
|
3937
|
-
|
|
3938
|
-
|
|
3939
|
-
|
|
3940
|
-
|
|
3941
|
-
|
|
3942
|
-
|
|
3943
|
-
|
|
3944
|
-
|
|
3945
|
-
|
|
3946
|
-
|
|
3947
|
-
|
|
3948
|
-
|
|
3949
|
-
|
|
3950
|
-
|
|
3951
|
-
|
|
4345
|
+
${
|
|
4346
|
+
media?.length
|
|
4347
|
+
? html`<div
|
|
4348
|
+
class="compose-reply-context-media"
|
|
4349
|
+
data-post-media
|
|
4350
|
+
data-lightbox-group=${JSON.stringify(
|
|
4351
|
+
media.map((m) => ({
|
|
4352
|
+
url: m.url,
|
|
4353
|
+
alt: m.alt ?? "",
|
|
4354
|
+
width: m.width,
|
|
4355
|
+
height: m.height,
|
|
4356
|
+
mimeType: m.mimeType,
|
|
4357
|
+
})),
|
|
4358
|
+
)}
|
|
4359
|
+
>
|
|
4360
|
+
${media.map(
|
|
4361
|
+
(m, i) => html`
|
|
4362
|
+
<a
|
|
4363
|
+
href=${m.url}
|
|
4364
|
+
data-lightbox-index=${i}
|
|
4365
|
+
class="compose-reply-context-media-link"
|
|
4366
|
+
>
|
|
4367
|
+
<img
|
|
4368
|
+
src=${m.previewUrl}
|
|
4369
|
+
alt=${m.alt ?? ""}
|
|
4370
|
+
class="compose-reply-context-media-img"
|
|
4371
|
+
loading="lazy"
|
|
4372
|
+
/>
|
|
4373
|
+
</a>
|
|
4374
|
+
`,
|
|
4375
|
+
)}
|
|
4376
|
+
</div>`
|
|
4377
|
+
: nothing
|
|
4378
|
+
}
|
|
3952
4379
|
</div>
|
|
3953
|
-
${
|
|
3954
|
-
? html`<div class="compose-reply-fade"></div>`
|
|
3955
|
-
|
|
4380
|
+
${
|
|
4381
|
+
!isExpanded ? html`<div class="compose-reply-fade"></div>` : nothing
|
|
4382
|
+
}
|
|
3956
4383
|
</div>
|
|
3957
4384
|
</div>
|
|
3958
4385
|
<div class="compose-reply-meta">
|
|
@@ -3982,23 +4409,30 @@ export class JantComposeDialog extends LitElement {
|
|
|
3982
4409
|
return !!(this._editPostId || this._draftSourceId);
|
|
3983
4410
|
}
|
|
3984
4411
|
|
|
4412
|
+
/**
|
|
4413
|
+
* Fold fields the target format can't hold into the body before the format
|
|
4414
|
+
* change recreates the editor from `_bodyJson`. Only a post that already
|
|
4415
|
+
* exists on the server needs it — anything else keeps hidden fields in the
|
|
4416
|
+
* editor, so switching back finds them — and every post of a saved thread
|
|
4417
|
+
* draft is one. Synchronous, so the old Tiptap instance can't fire onUpdate
|
|
4418
|
+
* and clobber what we just wrote. `applyConvertedFields` suppresses the one
|
|
4419
|
+
* content-change event the conversion emits, so the switch itself never
|
|
4420
|
+
* schedules a draft save.
|
|
4421
|
+
*/
|
|
4422
|
+
private _convertForFormatSwitch(
|
|
4423
|
+
editor: JantComposeEditor | null,
|
|
4424
|
+
from: ComposeFormat,
|
|
4425
|
+
to: ComposeFormat,
|
|
4426
|
+
) {
|
|
4427
|
+
if (!editor || from === to || !this._shouldConvertOnFormatSwitch()) return;
|
|
4428
|
+
editor.applyConvertedFields(
|
|
4429
|
+
convertComposeFormat(from, to, editor.getConvertibleFields()),
|
|
4430
|
+
);
|
|
4431
|
+
}
|
|
4432
|
+
|
|
3985
4433
|
private _switchFormat(target: ComposeFormat) {
|
|
3986
4434
|
if (this._format === target) return;
|
|
3987
|
-
|
|
3988
|
-
if (editor && this._shouldConvertOnFormatSwitch()) {
|
|
3989
|
-
// Fold fields the target can't hold into the body before the format
|
|
3990
|
-
// change recreates the editor from `_bodyJson`. Synchronous, so the old
|
|
3991
|
-
// Tiptap instance can't fire onUpdate and clobber what we just wrote.
|
|
3992
|
-
// `applyConvertedFields` suppresses the one content-change event the
|
|
3993
|
-
// conversion emits, so the switch itself never schedules a draft save.
|
|
3994
|
-
editor.applyConvertedFields(
|
|
3995
|
-
convertComposeFormat(
|
|
3996
|
-
this._format,
|
|
3997
|
-
target,
|
|
3998
|
-
editor.getConvertibleFields(),
|
|
3999
|
-
),
|
|
4000
|
-
);
|
|
4001
|
-
}
|
|
4435
|
+
this._convertForFormatSwitch(this._editor, this._format, target);
|
|
4002
4436
|
// A bare format switch shouldn't persist a local draft, so drop any save
|
|
4003
4437
|
// already pending from loading the post.
|
|
4004
4438
|
this._cancelDraftSaveTimer();
|
|
@@ -4028,8 +4462,8 @@ export class JantComposeDialog extends LitElement {
|
|
|
4028
4462
|
<button
|
|
4029
4463
|
type="button"
|
|
4030
4464
|
class="compose-close-btn"
|
|
4031
|
-
aria-label=${this.labels.
|
|
4032
|
-
title=${this.labels.
|
|
4465
|
+
aria-label=${this.labels.close}
|
|
4466
|
+
title=${this.labels.close}
|
|
4033
4467
|
@click=${() => this.requestClose()}
|
|
4034
4468
|
>
|
|
4035
4469
|
<svg
|
|
@@ -4070,12 +4504,14 @@ export class JantComposeDialog extends LitElement {
|
|
|
4070
4504
|
|
|
4071
4505
|
return html`
|
|
4072
4506
|
<div class="flex-1 min-w-0">
|
|
4073
|
-
${
|
|
4074
|
-
|
|
4075
|
-
|
|
4076
|
-
|
|
4077
|
-
|
|
4078
|
-
|
|
4507
|
+
${
|
|
4508
|
+
this._showCollection
|
|
4509
|
+
? html`<div
|
|
4510
|
+
class="compose-dropdown-backdrop"
|
|
4511
|
+
@click=${() => this._closeCollectionPicker()}
|
|
4512
|
+
></div>`
|
|
4513
|
+
: nothing
|
|
4514
|
+
}
|
|
4079
4515
|
<div
|
|
4080
4516
|
class="select compose-collection-select"
|
|
4081
4517
|
data-select-initialized
|
|
@@ -4092,6 +4528,7 @@ export class JantComposeDialog extends LitElement {
|
|
|
4092
4528
|
@click=${() => {
|
|
4093
4529
|
const nextOpen = !this._showCollection;
|
|
4094
4530
|
this._showPublishPanel = false;
|
|
4531
|
+
this._showLanguagePicker = false;
|
|
4095
4532
|
if (nextOpen) {
|
|
4096
4533
|
this._prepareCollectionPickerOrder();
|
|
4097
4534
|
}
|
|
@@ -4114,100 +4551,106 @@ export class JantComposeDialog extends LitElement {
|
|
|
4114
4551
|
data-popover
|
|
4115
4552
|
aria-hidden=${this._showCollection ? "false" : "true"}
|
|
4116
4553
|
>
|
|
4117
|
-
${
|
|
4118
|
-
|
|
4119
|
-
|
|
4120
|
-
|
|
4121
|
-
|
|
4122
|
-
|
|
4123
|
-
|
|
4124
|
-
|
|
4125
|
-
|
|
4126
|
-
|
|
4127
|
-
|
|
4128
|
-
|
|
4129
|
-
|
|
4130
|
-
|
|
4131
|
-
|
|
4132
|
-
|
|
4133
|
-
|
|
4134
|
-
|
|
4135
|
-
|
|
4136
|
-
|
|
4137
|
-
|
|
4138
|
-
|
|
4139
|
-
|
|
4140
|
-
|
|
4141
|
-
|
|
4142
|
-
|
|
4554
|
+
${
|
|
4555
|
+
collections.length > 0
|
|
4556
|
+
? html`<div class="compose-collection-popover-header">
|
|
4557
|
+
<label class="compose-collection-search-shell">
|
|
4558
|
+
${renderComposeCollectionPickerIcon(
|
|
4559
|
+
COMPOSE_COLLECTION_PICKER_ICONS.search,
|
|
4560
|
+
"compose-collection-search-icon",
|
|
4561
|
+
)}
|
|
4562
|
+
<input
|
|
4563
|
+
type="text"
|
|
4564
|
+
role="combobox"
|
|
4565
|
+
class="compose-collection-search-input"
|
|
4566
|
+
placeholder=${this.labels.searchCollections}
|
|
4567
|
+
autocomplete="off"
|
|
4568
|
+
autocorrect="off"
|
|
4569
|
+
spellcheck="false"
|
|
4570
|
+
.value=${this._collectionSearch}
|
|
4571
|
+
@keydown=${this._handleCollectionSearchKeydown}
|
|
4572
|
+
@input=${(e: Event) => {
|
|
4573
|
+
this._collectionSearch = (
|
|
4574
|
+
e.target as HTMLInputElement
|
|
4575
|
+
).value;
|
|
4576
|
+
}}
|
|
4577
|
+
/>
|
|
4578
|
+
</label>
|
|
4579
|
+
</div>`
|
|
4580
|
+
: nothing
|
|
4581
|
+
}
|
|
4143
4582
|
<div
|
|
4144
4583
|
role="listbox"
|
|
4145
4584
|
class="compose-collection-options"
|
|
4146
4585
|
aria-multiselectable="true"
|
|
4147
4586
|
>
|
|
4148
|
-
${
|
|
4149
|
-
|
|
4150
|
-
|
|
4151
|
-
|
|
4152
|
-
|
|
4153
|
-
|
|
4154
|
-
|
|
4155
|
-
|
|
4156
|
-
"compose-collection-option": true,
|
|
4157
|
-
"compose-collection-option-selected": selected,
|
|
4158
|
-
})}
|
|
4159
|
-
role="option"
|
|
4160
|
-
data-value=${col.id}
|
|
4161
|
-
aria-selected=${selected ? "true" : "false"}
|
|
4162
|
-
@keydown=${(event: globalThis.KeyboardEvent) =>
|
|
4163
|
-
this._handleCollectionOptionKeydown(event, col.id)}
|
|
4164
|
-
@click=${() =>
|
|
4165
|
-
this._handleCollectionOptionClick(col.id)}
|
|
4166
|
-
>
|
|
4167
|
-
<span class="compose-collection-option-label"
|
|
4168
|
-
>${col.title}</span
|
|
4169
|
-
>
|
|
4170
|
-
<span
|
|
4587
|
+
${
|
|
4588
|
+
filtered.length > 0
|
|
4589
|
+
? filtered.map((col) => {
|
|
4590
|
+
const selected = this._collectionIds.includes(col.id);
|
|
4591
|
+
|
|
4592
|
+
return html`
|
|
4593
|
+
<button
|
|
4594
|
+
type="button"
|
|
4171
4595
|
class=${classMap({
|
|
4172
|
-
"compose-collection-option
|
|
4173
|
-
"compose-collection-option-
|
|
4174
|
-
selected,
|
|
4175
|
-
"compose-collection-option-marker-add": !selected,
|
|
4596
|
+
"compose-collection-option": true,
|
|
4597
|
+
"compose-collection-option-selected": selected,
|
|
4176
4598
|
})}
|
|
4599
|
+
role="option"
|
|
4600
|
+
data-value=${col.id}
|
|
4601
|
+
aria-selected=${selected ? "true" : "false"}
|
|
4602
|
+
@keydown=${(event: globalThis.KeyboardEvent) =>
|
|
4603
|
+
this._handleCollectionOptionKeydown(event, col.id)}
|
|
4604
|
+
@click=${() =>
|
|
4605
|
+
this._handleCollectionOptionClick(col.id)}
|
|
4177
4606
|
>
|
|
4178
|
-
|
|
4179
|
-
|
|
4180
|
-
|
|
4181
|
-
|
|
4182
|
-
|
|
4183
|
-
|
|
4184
|
-
|
|
4185
|
-
|
|
4186
|
-
|
|
4187
|
-
|
|
4188
|
-
|
|
4189
|
-
|
|
4190
|
-
|
|
4191
|
-
|
|
4192
|
-
|
|
4193
|
-
|
|
4194
|
-
|
|
4195
|
-
|
|
4196
|
-
|
|
4197
|
-
|
|
4198
|
-
|
|
4199
|
-
|
|
4200
|
-
|
|
4201
|
-
|
|
4202
|
-
|
|
4203
|
-
|
|
4204
|
-
|
|
4205
|
-
|
|
4206
|
-
|
|
4207
|
-
|
|
4208
|
-
|
|
4209
|
-
|
|
4210
|
-
|
|
4607
|
+
<span class="compose-collection-option-label"
|
|
4608
|
+
>${col.title}</span
|
|
4609
|
+
>
|
|
4610
|
+
<span
|
|
4611
|
+
class=${classMap({
|
|
4612
|
+
"compose-collection-option-marker": true,
|
|
4613
|
+
"compose-collection-option-marker-selected":
|
|
4614
|
+
selected,
|
|
4615
|
+
"compose-collection-option-marker-add": !selected,
|
|
4616
|
+
})}
|
|
4617
|
+
>
|
|
4618
|
+
${
|
|
4619
|
+
selected
|
|
4620
|
+
? html`<svg
|
|
4621
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
4622
|
+
class="compose-collection-option-check-circle"
|
|
4623
|
+
viewBox="0 0 24 24"
|
|
4624
|
+
fill="none"
|
|
4625
|
+
aria-hidden="true"
|
|
4626
|
+
>
|
|
4627
|
+
<circle
|
|
4628
|
+
cx="12"
|
|
4629
|
+
cy="12"
|
|
4630
|
+
r="10"
|
|
4631
|
+
fill="currentColor"
|
|
4632
|
+
/>
|
|
4633
|
+
<path
|
|
4634
|
+
d="M8 12.5 10.7 15.2 16.4 9.5"
|
|
4635
|
+
stroke="var(--site-page-bg)"
|
|
4636
|
+
stroke-width="2.3"
|
|
4637
|
+
stroke-linecap="round"
|
|
4638
|
+
stroke-linejoin="round"
|
|
4639
|
+
/>
|
|
4640
|
+
</svg>`
|
|
4641
|
+
: renderComposeCollectionPickerIcon(
|
|
4642
|
+
COMPOSE_COLLECTION_PICKER_ICONS.plusCircle,
|
|
4643
|
+
"compose-collection-option-plus-circle",
|
|
4644
|
+
)
|
|
4645
|
+
}
|
|
4646
|
+
</span>
|
|
4647
|
+
</button>
|
|
4648
|
+
`;
|
|
4649
|
+
})
|
|
4650
|
+
: html`<div class="compose-collection-empty">
|
|
4651
|
+
${emptyLabel}
|
|
4652
|
+
</div>`
|
|
4653
|
+
}
|
|
4211
4654
|
</div>
|
|
4212
4655
|
<div class="compose-collection-footer">
|
|
4213
4656
|
<button
|
|
@@ -4251,8 +4694,7 @@ export class JantComposeDialog extends LitElement {
|
|
|
4251
4694
|
if (!detail) return;
|
|
4252
4695
|
|
|
4253
4696
|
const formEl = this.querySelector("jant-collection-form") as
|
|
4254
|
-
|
|
4255
|
-
| null;
|
|
4697
|
+
(HTMLElement & { loading: boolean }) | null;
|
|
4256
4698
|
if (formEl) formEl.loading = true;
|
|
4257
4699
|
|
|
4258
4700
|
try {
|
|
@@ -4314,13 +4756,7 @@ export class JantComposeDialog extends LitElement {
|
|
|
4314
4756
|
private _renderAddCollectionPanel() {
|
|
4315
4757
|
if (!this._addCollectionPanelOpen) return nothing;
|
|
4316
4758
|
|
|
4317
|
-
const initial = {
|
|
4318
|
-
title: "",
|
|
4319
|
-
slug: "",
|
|
4320
|
-
description: "",
|
|
4321
|
-
sortOrder: "newest",
|
|
4322
|
-
icon: "",
|
|
4323
|
-
};
|
|
4759
|
+
const initial = { title: "", slug: "" };
|
|
4324
4760
|
|
|
4325
4761
|
return html`
|
|
4326
4762
|
<div
|
|
@@ -4354,11 +4790,9 @@ export class JantComposeDialog extends LitElement {
|
|
|
4354
4790
|
</div>
|
|
4355
4791
|
<div class="collection-quick-dialog-body">
|
|
4356
4792
|
<jant-collection-form
|
|
4357
|
-
variant="quick"
|
|
4358
4793
|
.labels=${this.labels.collectionFormLabels}
|
|
4359
4794
|
.initial=${initial}
|
|
4360
4795
|
action=${publicPath("/api/collections")}
|
|
4361
|
-
cancel-href="javascript:void(0)"
|
|
4362
4796
|
@jant:collection-submit=${(e: Event) =>
|
|
4363
4797
|
this._handleAddCollectionSubmit(e)}
|
|
4364
4798
|
></jant-collection-form>
|
|
@@ -4441,22 +4875,24 @@ export class JantComposeDialog extends LitElement {
|
|
|
4441
4875
|
<span class="compose-alt-title">${this.labels.addAltTitle}</span>
|
|
4442
4876
|
</div>
|
|
4443
4877
|
<div class="compose-alt-preview">
|
|
4444
|
-
${
|
|
4445
|
-
|
|
4446
|
-
|
|
4447
|
-
alt=""
|
|
4448
|
-
class="compose-alt-preview-img"
|
|
4449
|
-
/>`
|
|
4450
|
-
: category === "video"
|
|
4451
|
-
? html`<video
|
|
4878
|
+
${
|
|
4879
|
+
category === "image"
|
|
4880
|
+
? html`<img
|
|
4452
4881
|
src=${attachment.previewUrl}
|
|
4882
|
+
alt=""
|
|
4453
4883
|
class="compose-alt-preview-img"
|
|
4454
|
-
|
|
4455
|
-
|
|
4456
|
-
|
|
4457
|
-
|
|
4458
|
-
|
|
4459
|
-
|
|
4884
|
+
/>`
|
|
4885
|
+
: category === "video"
|
|
4886
|
+
? html`<video
|
|
4887
|
+
src=${attachment.previewUrl}
|
|
4888
|
+
class="compose-alt-preview-img"
|
|
4889
|
+
preload="metadata"
|
|
4890
|
+
muted
|
|
4891
|
+
></video>`
|
|
4892
|
+
: html`<span class="text-sm text-muted-foreground"
|
|
4893
|
+
>${attachment.file.name}</span
|
|
4894
|
+
>`
|
|
4895
|
+
}
|
|
4460
4896
|
</div>
|
|
4461
4897
|
<div class="compose-alt-input-row">
|
|
4462
4898
|
<input
|
|
@@ -4734,6 +5170,7 @@ export class JantComposeDialog extends LitElement {
|
|
|
4734
5170
|
this._showCollection = false;
|
|
4735
5171
|
this._collectionSearch = "";
|
|
4736
5172
|
this._showPublishPanel = false;
|
|
5173
|
+
this._showLanguagePicker = false;
|
|
4737
5174
|
this._postMetaIndex = index;
|
|
4738
5175
|
this._confirmPanelOpen = false;
|
|
4739
5176
|
this._scheduleSuggestedSlugRefresh(true);
|
|
@@ -4777,6 +5214,7 @@ export class JantComposeDialog extends LitElement {
|
|
|
4777
5214
|
private _togglePublishPanel() {
|
|
4778
5215
|
this._showCollection = false;
|
|
4779
5216
|
this._collectionSearch = "";
|
|
5217
|
+
this._showLanguagePicker = false;
|
|
4780
5218
|
const nextOpen = !this._showPublishPanel;
|
|
4781
5219
|
this._showPublishPanel = nextOpen;
|
|
4782
5220
|
if (nextOpen) {
|
|
@@ -4878,18 +5316,392 @@ export class JantComposeDialog extends LitElement {
|
|
|
4878
5316
|
<span class="compose-sheet-title">${label}</span>
|
|
4879
5317
|
<span class="compose-sheet-sub">${hint}</span>
|
|
4880
5318
|
</span>
|
|
4881
|
-
${
|
|
4882
|
-
|
|
4883
|
-
|
|
4884
|
-
|
|
4885
|
-
|
|
4886
|
-
|
|
4887
|
-
|
|
4888
|
-
|
|
5319
|
+
${
|
|
5320
|
+
selected
|
|
5321
|
+
? html`<span class="compose-sheet-check" aria-hidden="true">
|
|
5322
|
+
${renderComposePublishActionIcon(
|
|
5323
|
+
COMPOSE_PUBLISH_ACTION_ICONS.check,
|
|
5324
|
+
"compose-sheet-check-icon",
|
|
5325
|
+
)}
|
|
5326
|
+
</span>`
|
|
5327
|
+
: nothing
|
|
5328
|
+
}
|
|
5329
|
+
</button>
|
|
5330
|
+
`;
|
|
5331
|
+
}
|
|
5332
|
+
|
|
5333
|
+
/**
|
|
5334
|
+
* Load the post a translation is being written for.
|
|
5335
|
+
*
|
|
5336
|
+
* Only the ID is carried around — through the URL that opens the composer,
|
|
5337
|
+
* and through a local draft picked up later — so everything shown above the
|
|
5338
|
+
* editor is fetched when it is needed. A post that has since been deleted
|
|
5339
|
+
* simply drops the link rather than blocking the composer.
|
|
5340
|
+
*
|
|
5341
|
+
* @param postId - Thread root the new post translates
|
|
5342
|
+
* @param options.seed - Also take the original's shape: its format, and the
|
|
5343
|
+
* citation fields that name a source rather than say anything in a
|
|
5344
|
+
* language. Only for a composer opened fresh on this translation; a
|
|
5345
|
+
* restored draft already carries the author's own answers.
|
|
5346
|
+
*/
|
|
5347
|
+
private async _loadTranslationSource(
|
|
5348
|
+
postId: string,
|
|
5349
|
+
options: { seed?: boolean } = {},
|
|
5350
|
+
): Promise<void> {
|
|
5351
|
+
try {
|
|
5352
|
+
const response = await fetch(publicPath(`/api/posts/${postId}`), {
|
|
5353
|
+
credentials: "same-origin",
|
|
5354
|
+
});
|
|
5355
|
+
if (!response.ok) return;
|
|
5356
|
+
const post = (await response.json()) as ComposePostResponse & {
|
|
5357
|
+
displayTitle?: string;
|
|
5358
|
+
};
|
|
5359
|
+
// `displayTitle` is derived server-side for untitled notes; a slug is a
|
|
5360
|
+
// URL, not a name, and only stands in when there is nothing else.
|
|
5361
|
+
this._translationOf = {
|
|
5362
|
+
id: post.id,
|
|
5363
|
+
title: post.displayTitle || post.slug || "",
|
|
5364
|
+
href: post.slug ? publicPath(`/${post.slug}`) : "",
|
|
5365
|
+
previewHtml: await this._fetchTranslationPreview(post.id),
|
|
5366
|
+
};
|
|
5367
|
+
|
|
5368
|
+
if (options.seed) await this._seedFromTranslationSource(post);
|
|
5369
|
+
} catch {
|
|
5370
|
+
// Offline or the post is gone. The composer still works; the author is
|
|
5371
|
+
// writing a post, just not a linked one.
|
|
5372
|
+
}
|
|
5373
|
+
}
|
|
5374
|
+
|
|
5375
|
+
/**
|
|
5376
|
+
* The original rendered as it renders anywhere else on the site.
|
|
5377
|
+
*
|
|
5378
|
+
* Server-side rather than rebuilt here: a Quote's attribution and a Link's
|
|
5379
|
+
* card are not `bodyHtml`, and reproducing each format's markup in the
|
|
5380
|
+
* composer would be a second renderer to keep in step with the first.
|
|
5381
|
+
*
|
|
5382
|
+
* @param postId - Thread root the new post translates
|
|
5383
|
+
* @returns The post's markup, or "" when it cannot be fetched
|
|
5384
|
+
*/
|
|
5385
|
+
private async _fetchTranslationPreview(postId: string): Promise<string> {
|
|
5386
|
+
try {
|
|
5387
|
+
const response = await fetch(
|
|
5388
|
+
publicPath(`/_/post-preview/${encodeURIComponent(postId)}`),
|
|
5389
|
+
{ headers: { Accept: "text/html" }, credentials: "same-origin" },
|
|
5390
|
+
);
|
|
5391
|
+
if (!response.ok) return "";
|
|
5392
|
+
return await response.text();
|
|
5393
|
+
} catch {
|
|
5394
|
+
return "";
|
|
5395
|
+
}
|
|
5396
|
+
}
|
|
5397
|
+
|
|
5398
|
+
/**
|
|
5399
|
+
* Start the translation in the shape of its original.
|
|
5400
|
+
*
|
|
5401
|
+
* A quote stays a quote — the format is a property of what was said, not of
|
|
5402
|
+
* the language it was said in — and the citation travels with it: the URL a
|
|
5403
|
+
* quote or a link points at is the same source whichever language describes
|
|
5404
|
+
* it. Collections, visibility and rating come along for the same reason:
|
|
5405
|
+
* they describe the post's place and its subject, neither of which changes
|
|
5406
|
+
* when the words do. The prose is the part the author is here to write, so
|
|
5407
|
+
* that is all that starts empty.
|
|
5408
|
+
*
|
|
5409
|
+
* Skipped the moment the author has typed anything: the fetch runs after the
|
|
5410
|
+
* composer opens, and overwriting their first sentence to save them a format
|
|
5411
|
+
* click is a bad trade.
|
|
5412
|
+
*/
|
|
5413
|
+
private async _seedFromTranslationSource(
|
|
5414
|
+
post: ComposePostResponse,
|
|
5415
|
+
): Promise<void> {
|
|
5416
|
+
if (this._hasContent() || this._editPostId || this._draftSourceId) return;
|
|
5417
|
+
|
|
5418
|
+
const format = post.format;
|
|
5419
|
+
this._format = format;
|
|
5420
|
+
if (post.collectionIds?.length) {
|
|
5421
|
+
this._collectionIds = [...post.collectionIds];
|
|
5422
|
+
}
|
|
5423
|
+
if (post.visibility) this._visibility = post.visibility;
|
|
5424
|
+
await this.updateComplete;
|
|
5425
|
+
|
|
5426
|
+
this._editor?.populate({
|
|
5427
|
+
format,
|
|
5428
|
+
url:
|
|
5429
|
+
format === "quote"
|
|
5430
|
+
? (post.sourceUrl ?? undefined)
|
|
5431
|
+
: (post.url ?? undefined),
|
|
5432
|
+
quoteAuthor:
|
|
5433
|
+
format === "quote" ? (post.sourceName ?? undefined) : undefined,
|
|
5434
|
+
rating: post.rating ?? undefined,
|
|
5435
|
+
});
|
|
5436
|
+
await this.updateComplete;
|
|
5437
|
+
// The author has still typed nothing, so none of this counts as a change
|
|
5438
|
+
// they would be asked to discard on close.
|
|
5439
|
+
this._seededFromSource = true;
|
|
5440
|
+
this._captureInitialSnapshot();
|
|
5441
|
+
}
|
|
5442
|
+
|
|
5443
|
+
/**
|
|
5444
|
+
* Plain text of the post being written, for the language suggestion.
|
|
5445
|
+
*
|
|
5446
|
+
* Read on demand rather than tracked: the panel is something the author
|
|
5447
|
+
* opens deliberately, so reading the editor at that moment is both cheaper
|
|
5448
|
+
* than watching every keystroke and more accurate than a cached value.
|
|
5449
|
+
*/
|
|
5450
|
+
private _composeTextForDetection(): string {
|
|
5451
|
+
const editor = this._editor;
|
|
5452
|
+
if (!editor) return "";
|
|
5453
|
+
const data = editor.getData();
|
|
5454
|
+
const bodyText = data.body ? (extractBodyText(data.body) ?? "") : "";
|
|
5455
|
+
return [data.title, data.quoteText, bodyText].filter(Boolean).join(" ");
|
|
5456
|
+
}
|
|
5457
|
+
|
|
5458
|
+
/**
|
|
5459
|
+
* The language of the page the composer opened from, as the automatic
|
|
5460
|
+
* choice's default. Falls back to the primary language when the page's
|
|
5461
|
+
* language is unknown or not one the site publishes.
|
|
5462
|
+
*/
|
|
5463
|
+
private _contextLanguageTag(): string | null {
|
|
5464
|
+
const tags = this.languages.map((language) => language.tag);
|
|
5465
|
+
if (tags.length === 0) return null;
|
|
5466
|
+
return tags.includes(this.contextLanguage)
|
|
5467
|
+
? this.contextLanguage
|
|
5468
|
+
: (tags[0] as string);
|
|
5469
|
+
}
|
|
5470
|
+
|
|
5471
|
+
/**
|
|
5472
|
+
* What detection actually reads out of the text as it stands, or null when
|
|
5473
|
+
* the text does not say yet.
|
|
5474
|
+
*
|
|
5475
|
+
* Null is a distinct answer from "the page's language", and the "Detect" row
|
|
5476
|
+
* has to tell them apart: a row offering to read what you write cannot claim
|
|
5477
|
+
* to have read something out of two words. It also ignores an explicit
|
|
5478
|
+
* choice — the row describes what picking it would do, not what the author
|
|
5479
|
+
* picked instead.
|
|
5480
|
+
*/
|
|
5481
|
+
private _readLanguage(): string | null {
|
|
5482
|
+
return readContentLanguage(this._composeTextForDetection(), {
|
|
5483
|
+
languages: this.languages.map((language) => language.tag),
|
|
5484
|
+
});
|
|
5485
|
+
}
|
|
5486
|
+
|
|
5487
|
+
/** The language this post would be saved with if submitted right now. */
|
|
5488
|
+
private _effectiveLanguage(): string | null {
|
|
5489
|
+
return this._language ?? this._readLanguage() ?? this._contextLanguageTag();
|
|
5490
|
+
}
|
|
5491
|
+
|
|
5492
|
+
private _renderLanguageRow(tag: string | null, label: string, hint: string) {
|
|
5493
|
+
const selected = this._language === tag;
|
|
5494
|
+
|
|
5495
|
+
return html`
|
|
5496
|
+
<button
|
|
5497
|
+
type="button"
|
|
5498
|
+
class=${classMap({
|
|
5499
|
+
"compose-sheet-row": true,
|
|
5500
|
+
"compose-sheet-row-selected": selected,
|
|
5501
|
+
})}
|
|
5502
|
+
role="radio"
|
|
5503
|
+
aria-checked=${selected ? "true" : "false"}
|
|
5504
|
+
lang=${ifDefined(tag ?? undefined)}
|
|
5505
|
+
data-compose-language-row
|
|
5506
|
+
@click=${() => {
|
|
5507
|
+
this._language = tag;
|
|
5508
|
+
this._scheduleDraftSave();
|
|
5509
|
+
// Choosing is the whole job of this list, so it closes behind the
|
|
5510
|
+
// choice — the same as picking a visibility in the options sheet.
|
|
5511
|
+
this._closeLanguagePicker(true);
|
|
5512
|
+
}}
|
|
5513
|
+
>
|
|
5514
|
+
<span class="compose-sheet-main">
|
|
5515
|
+
<span class="compose-sheet-title">${label}</span>
|
|
5516
|
+
${
|
|
5517
|
+
hint
|
|
5518
|
+
? html`<span class="compose-sheet-sub">${hint}</span>`
|
|
5519
|
+
: nothing
|
|
5520
|
+
}
|
|
5521
|
+
</span>
|
|
5522
|
+
${
|
|
5523
|
+
selected
|
|
5524
|
+
? html`<span class="compose-sheet-check" aria-hidden="true">
|
|
5525
|
+
${renderComposePublishActionIcon(
|
|
5526
|
+
COMPOSE_PUBLISH_ACTION_ICONS.check,
|
|
5527
|
+
"compose-sheet-check-icon",
|
|
5528
|
+
)}
|
|
5529
|
+
</span>`
|
|
5530
|
+
: nothing
|
|
5531
|
+
}
|
|
4889
5532
|
</button>
|
|
4890
5533
|
`;
|
|
4891
5534
|
}
|
|
4892
5535
|
|
|
5536
|
+
private _closeLanguagePicker(restoreFocus = false) {
|
|
5537
|
+
if (!this._showLanguagePicker) return;
|
|
5538
|
+
this._showLanguagePicker = false;
|
|
5539
|
+
if (!restoreFocus) return;
|
|
5540
|
+
this.updateComplete.then(() => {
|
|
5541
|
+
this.querySelector<HTMLElement>(
|
|
5542
|
+
"[data-compose-language-trigger]",
|
|
5543
|
+
)?.focus();
|
|
5544
|
+
});
|
|
5545
|
+
}
|
|
5546
|
+
|
|
5547
|
+
private _toggleLanguagePicker() {
|
|
5548
|
+
const nextOpen = !this._showLanguagePicker;
|
|
5549
|
+
if (!nextOpen) {
|
|
5550
|
+
this._closeLanguagePicker(true);
|
|
5551
|
+
return;
|
|
5552
|
+
}
|
|
5553
|
+
this._showCollection = false;
|
|
5554
|
+
this._collectionSearch = "";
|
|
5555
|
+
this._showPublishPanel = false;
|
|
5556
|
+
this._postMetaIndex = null;
|
|
5557
|
+
this._showLanguagePicker = true;
|
|
5558
|
+
this.updateComplete.then(() => {
|
|
5559
|
+
const rows = this.querySelectorAll<HTMLElement>(
|
|
5560
|
+
"[data-compose-language-row]",
|
|
5561
|
+
);
|
|
5562
|
+
const selected = Array.from(rows).find(
|
|
5563
|
+
(row) => row.getAttribute("aria-checked") === "true",
|
|
5564
|
+
);
|
|
5565
|
+
(selected ?? rows[0])?.focus();
|
|
5566
|
+
});
|
|
5567
|
+
}
|
|
5568
|
+
|
|
5569
|
+
/**
|
|
5570
|
+
* The post's content language, offered only once the site publishes more
|
|
5571
|
+
* than one. A single-language author never meets this control — that is the
|
|
5572
|
+
* point of the whole feature being opt-in.
|
|
5573
|
+
*
|
|
5574
|
+
* It sits next to Post rather than inside the options sheet because it is
|
|
5575
|
+
* not a setting so much as a statement about what is being written: the pill
|
|
5576
|
+
* names the language this would publish in right now, so the answer is
|
|
5577
|
+
* visible before the button that makes it public, not two clicks behind it.
|
|
5578
|
+
*
|
|
5579
|
+
* It only *names* it when there is something to say, though. Writing from
|
|
5580
|
+
* /ja in Japanese, the globe alone is the whole message — the answer is the
|
|
5581
|
+
* page you are standing on. The name appears when the answer stops being
|
|
5582
|
+
* obvious: detection has moved it somewhere else, or the author has pinned
|
|
5583
|
+
* it themselves. So a language that changes under you announces itself by
|
|
5584
|
+
* growing a word, and one that never changes never speaks.
|
|
5585
|
+
*
|
|
5586
|
+
* Replies are excluded: a Thread is written in one language, and a reply
|
|
5587
|
+
* takes the root's server-side.
|
|
5588
|
+
*/
|
|
5589
|
+
private _renderLanguageControl() {
|
|
5590
|
+
if (this._replyToId || this.languages.length < 2) return nothing;
|
|
5591
|
+
|
|
5592
|
+
const effective = this._effectiveLanguage();
|
|
5593
|
+
const effectiveLabel =
|
|
5594
|
+
this.languages.find((language) => language.tag === effective)?.label ??
|
|
5595
|
+
"";
|
|
5596
|
+
const chosen = this._language !== null;
|
|
5597
|
+
const named = chosen || effective !== this._contextLanguageTag();
|
|
5598
|
+
const open = this._showLanguagePicker;
|
|
5599
|
+
const accessibleLabel = this.labels.languageTriggerLabel.replace(
|
|
5600
|
+
"{language}",
|
|
5601
|
+
effectiveLabel,
|
|
5602
|
+
);
|
|
5603
|
+
|
|
5604
|
+
return html`
|
|
5605
|
+
<div
|
|
5606
|
+
class=${classMap({
|
|
5607
|
+
"compose-language": true,
|
|
5608
|
+
"compose-language-open": open,
|
|
5609
|
+
})}
|
|
5610
|
+
>
|
|
5611
|
+
${
|
|
5612
|
+
open
|
|
5613
|
+
? html`<div
|
|
5614
|
+
class="compose-dropdown-backdrop"
|
|
5615
|
+
@click=${() => this._closeLanguagePicker(true)}
|
|
5616
|
+
></div>`
|
|
5617
|
+
: nothing
|
|
5618
|
+
}
|
|
5619
|
+
<button
|
|
5620
|
+
type="button"
|
|
5621
|
+
class=${classMap({
|
|
5622
|
+
"compose-language-trigger": true,
|
|
5623
|
+
"compose-language-trigger-bare": !named,
|
|
5624
|
+
})}
|
|
5625
|
+
data-compose-language-trigger
|
|
5626
|
+
data-open=${open ? "true" : nothing}
|
|
5627
|
+
data-chosen=${chosen ? "true" : nothing}
|
|
5628
|
+
?disabled=${this._loading}
|
|
5629
|
+
aria-haspopup="dialog"
|
|
5630
|
+
aria-expanded=${open ? "true" : "false"}
|
|
5631
|
+
aria-label=${accessibleLabel}
|
|
5632
|
+
title=${accessibleLabel}
|
|
5633
|
+
@click=${() => this._toggleLanguagePicker()}
|
|
5634
|
+
>
|
|
5635
|
+
<svg
|
|
5636
|
+
class="compose-language-trigger-svg"
|
|
5637
|
+
width="24"
|
|
5638
|
+
height="24"
|
|
5639
|
+
viewBox="0 0 24 24"
|
|
5640
|
+
fill="none"
|
|
5641
|
+
stroke="currentColor"
|
|
5642
|
+
stroke-width="1.9"
|
|
5643
|
+
stroke-linecap="round"
|
|
5644
|
+
stroke-linejoin="round"
|
|
5645
|
+
aria-hidden="true"
|
|
5646
|
+
>
|
|
5647
|
+
${unsafeSVG(COMPOSE_LANGUAGE_ICON)}
|
|
5648
|
+
</svg>
|
|
5649
|
+
${
|
|
5650
|
+
named
|
|
5651
|
+
? html`<span
|
|
5652
|
+
class="compose-language-label"
|
|
5653
|
+
lang=${ifDefined(effective ?? undefined)}
|
|
5654
|
+
>${effectiveLabel}</span
|
|
5655
|
+
>`
|
|
5656
|
+
: nothing
|
|
5657
|
+
}
|
|
5658
|
+
</button>
|
|
5659
|
+
${open ? this._renderLanguagePicker() : nothing}
|
|
5660
|
+
</div>
|
|
5661
|
+
`;
|
|
5662
|
+
}
|
|
5663
|
+
|
|
5664
|
+
private _renderLanguagePicker() {
|
|
5665
|
+
const labelOf = (tag: string | null) =>
|
|
5666
|
+
this.languages.find((language) => language.tag === tag)?.label ?? "";
|
|
5667
|
+
// Two states, and the row has to be straight about which one it is in: it
|
|
5668
|
+
// has read a language, or it has not read one yet and the page's language
|
|
5669
|
+
// is standing in until it does.
|
|
5670
|
+
const read = this._readLanguage();
|
|
5671
|
+
const pendingLabel = labelOf(this._contextLanguageTag());
|
|
5672
|
+
const autoHint = read
|
|
5673
|
+
? this.labels.languageAutoDetected.replace("{language}", labelOf(read))
|
|
5674
|
+
: pendingLabel
|
|
5675
|
+
? this.labels.languageAutoPending.replace("{language}", pendingLabel)
|
|
5676
|
+
: this.labels.languageAutoHint;
|
|
5677
|
+
|
|
5678
|
+
return html`
|
|
5679
|
+
<div
|
|
5680
|
+
class="compose-language-popover"
|
|
5681
|
+
role="dialog"
|
|
5682
|
+
aria-label=${this.labels.languageLabel}
|
|
5683
|
+
>
|
|
5684
|
+
<p class="compose-sheet-label">${this.labels.languageLabel}</p>
|
|
5685
|
+
${
|
|
5686
|
+
this._translationOf
|
|
5687
|
+
? html`<p class="compose-sheet-note">
|
|
5688
|
+
${this.labels.translationOf.replace(
|
|
5689
|
+
"{title}",
|
|
5690
|
+
this._translationOf.title,
|
|
5691
|
+
)}
|
|
5692
|
+
</p>`
|
|
5693
|
+
: nothing
|
|
5694
|
+
}
|
|
5695
|
+
<div role="radiogroup" aria-label=${this.labels.languageLabel}>
|
|
5696
|
+
${this._renderLanguageRow(null, this.labels.languageAuto, autoHint)}
|
|
5697
|
+
${this.languages.map((language) =>
|
|
5698
|
+
this._renderLanguageRow(language.tag, language.label, ""),
|
|
5699
|
+
)}
|
|
5700
|
+
</div>
|
|
5701
|
+
</div>
|
|
5702
|
+
`;
|
|
5703
|
+
}
|
|
5704
|
+
|
|
4893
5705
|
/**
|
|
4894
5706
|
* The label above a field, with its reset on the right. The reset only shows
|
|
4895
5707
|
* once there is something to reset, so the default state is a bare label.
|
|
@@ -4982,17 +5794,19 @@ export class JantComposeDialog extends LitElement {
|
|
|
4982
5794
|
)}
|
|
4983
5795
|
</button>
|
|
4984
5796
|
</div>
|
|
4985
|
-
${
|
|
4986
|
-
|
|
4987
|
-
|
|
4988
|
-
|
|
4989
|
-
|
|
4990
|
-
|
|
4991
|
-
|
|
4992
|
-
|
|
4993
|
-
|
|
4994
|
-
|
|
4995
|
-
|
|
5797
|
+
${
|
|
5798
|
+
publishedAtError
|
|
5799
|
+
? html`<p
|
|
5800
|
+
class="compose-publish-date-status compose-publish-date-status-error"
|
|
5801
|
+
>
|
|
5802
|
+
${publishedAtError}
|
|
5803
|
+
</p>`
|
|
5804
|
+
: hasValue
|
|
5805
|
+
? nothing
|
|
5806
|
+
: html`<p class="compose-sheet-hint">
|
|
5807
|
+
${this.labels.publishDateHint}
|
|
5808
|
+
</p>`
|
|
5809
|
+
}
|
|
4996
5810
|
</div>
|
|
4997
5811
|
`;
|
|
4998
5812
|
}
|
|
@@ -5038,64 +5852,68 @@ export class JantComposeDialog extends LitElement {
|
|
|
5038
5852
|
@input=${(e: Event) => this._onSlugInput(e, index)}
|
|
5039
5853
|
/>
|
|
5040
5854
|
</div>
|
|
5041
|
-
${
|
|
5042
|
-
|
|
5043
|
-
|
|
5044
|
-
|
|
5045
|
-
|
|
5046
|
-
|
|
5047
|
-
|
|
5048
|
-
<span class="compose-slug-suggestion-label"
|
|
5049
|
-
>${this.labels.publishSlugSuggested}</span
|
|
5050
|
-
>
|
|
5051
|
-
<span class="compose-slug-suggestion-value"
|
|
5052
|
-
>/${this._suggestedSlug}</span
|
|
5855
|
+
${
|
|
5856
|
+
showSuggestion
|
|
5857
|
+
? html`
|
|
5858
|
+
<button
|
|
5859
|
+
type="button"
|
|
5860
|
+
class="compose-slug-suggestion"
|
|
5861
|
+
@click=${() => this._useSuggestedSlug()}
|
|
5053
5862
|
>
|
|
5054
|
-
|
|
5055
|
-
|
|
5056
|
-
width="13"
|
|
5057
|
-
height="13"
|
|
5058
|
-
viewBox="0 0 14 14"
|
|
5059
|
-
fill="none"
|
|
5060
|
-
stroke="currentColor"
|
|
5061
|
-
stroke-width="1.4"
|
|
5062
|
-
stroke-linecap="round"
|
|
5063
|
-
stroke-linejoin="round"
|
|
5863
|
+
<span class="compose-slug-suggestion-label"
|
|
5864
|
+
>${this.labels.publishSlugSuggested}</span
|
|
5064
5865
|
>
|
|
5065
|
-
|
|
5066
|
-
|
|
5067
|
-
|
|
5068
|
-
|
|
5069
|
-
|
|
5070
|
-
|
|
5071
|
-
|
|
5072
|
-
|
|
5073
|
-
|
|
5074
|
-
|
|
5075
|
-
|
|
5076
|
-
|
|
5077
|
-
|
|
5078
|
-
|
|
5079
|
-
|
|
5080
|
-
|
|
5081
|
-
|
|
5082
|
-
|
|
5866
|
+
<span class="compose-slug-suggestion-value"
|
|
5867
|
+
>/${this._suggestedSlug}</span
|
|
5868
|
+
>
|
|
5869
|
+
<span class="compose-slug-suggestion-icon" aria-hidden="true">
|
|
5870
|
+
<svg
|
|
5871
|
+
width="13"
|
|
5872
|
+
height="13"
|
|
5873
|
+
viewBox="0 0 14 14"
|
|
5874
|
+
fill="none"
|
|
5875
|
+
stroke="currentColor"
|
|
5876
|
+
stroke-width="1.4"
|
|
5877
|
+
stroke-linecap="round"
|
|
5878
|
+
stroke-linejoin="round"
|
|
5879
|
+
>
|
|
5880
|
+
<path d="M3 7h8" />
|
|
5881
|
+
<path d="m8 3 4 4-4 4" />
|
|
5882
|
+
</svg>
|
|
5883
|
+
</span>
|
|
5884
|
+
</button>
|
|
5885
|
+
`
|
|
5886
|
+
: nothing
|
|
5887
|
+
}
|
|
5888
|
+
${
|
|
5889
|
+
slugStatus
|
|
5083
5890
|
? html`<p
|
|
5084
|
-
class
|
|
5085
|
-
|
|
5086
|
-
|
|
5891
|
+
class=${classMap({
|
|
5892
|
+
"compose-publish-slug-status": true,
|
|
5893
|
+
"compose-publish-slug-status-error": Boolean(slugError),
|
|
5894
|
+
})}
|
|
5895
|
+
data-compose-slug-error=${slugError ? "true" : nothing}
|
|
5087
5896
|
>
|
|
5088
|
-
|
|
5089
|
-
>${slugPreview.origin}</span
|
|
5090
|
-
><span class="compose-publish-slug-preview-path"
|
|
5091
|
-
>${slugPreview.path}</span
|
|
5092
|
-
>
|
|
5897
|
+
${slugStatus}
|
|
5093
5898
|
</p>`
|
|
5094
|
-
:
|
|
5095
|
-
?
|
|
5096
|
-
|
|
5097
|
-
|
|
5098
|
-
|
|
5899
|
+
: slugPreview
|
|
5900
|
+
? html`<p
|
|
5901
|
+
class="compose-publish-slug-preview"
|
|
5902
|
+
data-compose-slug-preview
|
|
5903
|
+
title=${slugPreview.full}
|
|
5904
|
+
>
|
|
5905
|
+
<span class="compose-publish-slug-preview-origin"
|
|
5906
|
+
>${slugPreview.origin}</span
|
|
5907
|
+
><span class="compose-publish-slug-preview-path"
|
|
5908
|
+
>${slugPreview.path}</span
|
|
5909
|
+
>
|
|
5910
|
+
</p>`
|
|
5911
|
+
: this._hasPostManualSlug(index)
|
|
5912
|
+
? nothing
|
|
5913
|
+
: html`<p class="compose-sheet-hint">
|
|
5914
|
+
${this.labels.publishSlugHint}
|
|
5915
|
+
</p>`
|
|
5916
|
+
}
|
|
5099
5917
|
</div>
|
|
5100
5918
|
`;
|
|
5101
5919
|
}
|
|
@@ -5295,12 +6113,14 @@ export class JantComposeDialog extends LitElement {
|
|
|
5295
6113
|
"compose-post-meta-open": open,
|
|
5296
6114
|
})}
|
|
5297
6115
|
>
|
|
5298
|
-
${
|
|
5299
|
-
|
|
5300
|
-
|
|
5301
|
-
|
|
5302
|
-
|
|
5303
|
-
|
|
6116
|
+
${
|
|
6117
|
+
open
|
|
6118
|
+
? html`<div
|
|
6119
|
+
class="compose-dropdown-backdrop"
|
|
6120
|
+
@click=${() => this._closePostMeta()}
|
|
6121
|
+
></div>`
|
|
6122
|
+
: nothing
|
|
6123
|
+
}
|
|
5304
6124
|
<button
|
|
5305
6125
|
type="button"
|
|
5306
6126
|
class="compose-post-meta-pill"
|
|
@@ -5312,6 +6132,7 @@ export class JantComposeDialog extends LitElement {
|
|
|
5312
6132
|
if (open) return this._closePostMeta();
|
|
5313
6133
|
this._showPublishPanel = false;
|
|
5314
6134
|
this._showCollection = false;
|
|
6135
|
+
this._showLanguagePicker = false;
|
|
5315
6136
|
this._postMetaIndex = index;
|
|
5316
6137
|
if (isRoot) this._scheduleSuggestedSlugRefresh(true);
|
|
5317
6138
|
// Focus the panel, not its first field: a focused `type="date"`
|
|
@@ -5334,28 +6155,30 @@ export class JantComposeDialog extends LitElement {
|
|
|
5334
6155
|
>${this._getPostSlugRowValue(index)}</span
|
|
5335
6156
|
>
|
|
5336
6157
|
</button>
|
|
5337
|
-
${
|
|
5338
|
-
|
|
5339
|
-
|
|
5340
|
-
|
|
5341
|
-
|
|
5342
|
-
|
|
5343
|
-
|
|
5344
|
-
|
|
5345
|
-
|
|
5346
|
-
|
|
5347
|
-
|
|
5348
|
-
<
|
|
5349
|
-
|
|
5350
|
-
|
|
5351
|
-
|
|
5352
|
-
|
|
5353
|
-
|
|
5354
|
-
|
|
5355
|
-
|
|
5356
|
-
|
|
5357
|
-
|
|
5358
|
-
|
|
6158
|
+
${
|
|
6159
|
+
open
|
|
6160
|
+
? html`<div
|
|
6161
|
+
class="compose-post-meta-panel"
|
|
6162
|
+
role="dialog"
|
|
6163
|
+
aria-label=${this.labels.publishSettings}
|
|
6164
|
+
tabindex="-1"
|
|
6165
|
+
data-compose-post-meta-panel
|
|
6166
|
+
>
|
|
6167
|
+
${this._renderPublishDateSection(index)}
|
|
6168
|
+
${this._renderPublishSlugSection(index)}
|
|
6169
|
+
<div class="compose-post-meta-footer">
|
|
6170
|
+
<button
|
|
6171
|
+
type="button"
|
|
6172
|
+
class="compose-publish-done"
|
|
6173
|
+
data-compose-post-meta-done
|
|
6174
|
+
@click=${() => this._closePostMeta()}
|
|
6175
|
+
>
|
|
6176
|
+
${this.labels.done}
|
|
6177
|
+
</button>
|
|
6178
|
+
</div>
|
|
6179
|
+
</div>`
|
|
6180
|
+
: nothing
|
|
6181
|
+
}
|
|
5359
6182
|
</div>
|
|
5360
6183
|
`;
|
|
5361
6184
|
}
|
|
@@ -5513,8 +6336,7 @@ export class JantComposeDialog extends LitElement {
|
|
|
5513
6336
|
}
|
|
5514
6337
|
return (
|
|
5515
6338
|
this._renderSaveDraftRow() !== nothing ||
|
|
5516
|
-
this._renderDraftsRow() !== nothing
|
|
5517
|
-
this._renderCloseComposeRow() !== nothing
|
|
6339
|
+
this._renderDraftsRow() !== nothing
|
|
5518
6340
|
);
|
|
5519
6341
|
}
|
|
5520
6342
|
|
|
@@ -5529,46 +6351,50 @@ export class JantComposeDialog extends LitElement {
|
|
|
5529
6351
|
></div>`;
|
|
5530
6352
|
const saveDraftRow = this._renderSaveDraftRow();
|
|
5531
6353
|
const draftsRow = this._renderDraftsRow();
|
|
5532
|
-
const
|
|
5533
|
-
const hasSessionRows =
|
|
5534
|
-
saveDraftRow !== nothing || draftsRow !== nothing || closeRow !== nothing;
|
|
6354
|
+
const hasSessionRows = saveDraftRow !== nothing || draftsRow !== nothing;
|
|
5535
6355
|
|
|
5536
6356
|
return html`
|
|
5537
6357
|
<div class="compose-sheet">
|
|
5538
|
-
${
|
|
5539
|
-
|
|
5540
|
-
|
|
5541
|
-
|
|
5542
|
-
|
|
5543
|
-
|
|
5544
|
-
|
|
5545
|
-
|
|
5546
|
-
|
|
5547
|
-
|
|
5548
|
-
|
|
5549
|
-
|
|
5550
|
-
|
|
5551
|
-
|
|
5552
|
-
|
|
5553
|
-
|
|
5554
|
-
|
|
5555
|
-
|
|
5556
|
-
|
|
5557
|
-
|
|
5558
|
-
|
|
5559
|
-
|
|
5560
|
-
|
|
5561
|
-
|
|
5562
|
-
|
|
5563
|
-
|
|
5564
|
-
|
|
5565
|
-
|
|
5566
|
-
|
|
5567
|
-
|
|
5568
|
-
|
|
5569
|
-
|
|
5570
|
-
|
|
5571
|
-
|
|
6358
|
+
${
|
|
6359
|
+
this._visibilityLocked
|
|
6360
|
+
? nothing
|
|
6361
|
+
: html`
|
|
6362
|
+
<p class="compose-sheet-label">
|
|
6363
|
+
${this.labels.publishVisibilityLabel}
|
|
6364
|
+
</p>
|
|
6365
|
+
<div
|
|
6366
|
+
role="radiogroup"
|
|
6367
|
+
aria-label=${this.labels.publishVisibilityLabel}
|
|
6368
|
+
>
|
|
6369
|
+
${this._renderVisibilityRow(
|
|
6370
|
+
"public",
|
|
6371
|
+
this.labels.publishVisibilityPublic,
|
|
6372
|
+
this.labels.publishVisibilityPublicHint,
|
|
6373
|
+
)}
|
|
6374
|
+
${this._renderVisibilityRow(
|
|
6375
|
+
"latest_hidden",
|
|
6376
|
+
this.labels.publishVisibilityHiddenFromLatest,
|
|
6377
|
+
this.labels.publishVisibilityHiddenFromLatestHint,
|
|
6378
|
+
)}
|
|
6379
|
+
${this._renderVisibilityRow(
|
|
6380
|
+
"private",
|
|
6381
|
+
this.labels.publishVisibilityPrivate,
|
|
6382
|
+
this.labels.publishVisibilityPrivateHint,
|
|
6383
|
+
)}
|
|
6384
|
+
</div>
|
|
6385
|
+
`
|
|
6386
|
+
}
|
|
6387
|
+
${
|
|
6388
|
+
this._replyToId
|
|
6389
|
+
? html`${this._visibilityLocked ? nothing : divider}
|
|
6390
|
+
${this._renderQuietReplySection()}`
|
|
6391
|
+
: nothing
|
|
6392
|
+
}
|
|
6393
|
+
${
|
|
6394
|
+
hasSessionRows
|
|
6395
|
+
? html`${divider} ${saveDraftRow} ${draftsRow}`
|
|
6396
|
+
: nothing
|
|
6397
|
+
}
|
|
5572
6398
|
</div>
|
|
5573
6399
|
`;
|
|
5574
6400
|
}
|
|
@@ -5580,7 +6406,7 @@ export class JantComposeDialog extends LitElement {
|
|
|
5580
6406
|
* there is something to save, both are offered and each does one thing.
|
|
5581
6407
|
*/
|
|
5582
6408
|
private _renderSaveDraftRow() {
|
|
5583
|
-
if (this._editPostId || !this.
|
|
6409
|
+
if (this._editPostId || !this._hasWorkToLose()) return nothing;
|
|
5584
6410
|
|
|
5585
6411
|
return html`
|
|
5586
6412
|
<button
|
|
@@ -5635,31 +6461,41 @@ export class JantComposeDialog extends LitElement {
|
|
|
5635
6461
|
}
|
|
5636
6462
|
|
|
5637
6463
|
/**
|
|
5638
|
-
* A thread's posts each spend their × on removing themselves, so
|
|
5639
|
-
*
|
|
5640
|
-
*
|
|
5641
|
-
*
|
|
6464
|
+
* A thread's posts each spend their × on removing themselves, so the exit
|
|
6465
|
+
* cannot live in a post header there — and a phone has no Escape key and no
|
|
6466
|
+
* backdrop to tap, so it has to be visible somewhere.
|
|
6467
|
+
*
|
|
6468
|
+
* It goes at the near edge of the quick actions row, the strip that already
|
|
6469
|
+
* carries thread-wide switches: leaving is thread-wide too, and the row's
|
|
6470
|
+
* near half was empty. Diagonally opposite Publish, and in the muted weight
|
|
6471
|
+
* of the switches beside it rather than Publish's — an exit should neither
|
|
6472
|
+
* compete with the thing it is an exit from nor sit next to it.
|
|
6473
|
+
*
|
|
6474
|
+
* The word is "Close", not "Cancel": leaving may keep the work as a draft, so
|
|
6475
|
+
* nothing is cancelled, and a thread puts N posts on screen that each own a ×
|
|
6476
|
+
* — "Cancel" would not say which of them it meant. It also keeps this out of
|
|
6477
|
+
* a Cancel/Post pair, which would read as Publish's peer rather than the
|
|
6478
|
+
* quiet way out. Same word as the single-post ×, which is the same action.
|
|
6479
|
+
*
|
|
6480
|
+
* It routes through `requestClose`, so unsaved work still gets the "Save to
|
|
6481
|
+
* drafts?" prompt.
|
|
6482
|
+
*
|
|
6483
|
+
* Page mode sits this out for the same reason the × does: the page's own
|
|
6484
|
+
* Back link is the exit there, and it already routes through the same
|
|
6485
|
+
* prompt. The row would be the wrong place to argue otherwise — page mode
|
|
6486
|
+
* pins the action row to the viewport bottom and leaves this strip below it.
|
|
5642
6487
|
*/
|
|
5643
|
-
private
|
|
5644
|
-
if (this._threadItems.length === 0) return nothing;
|
|
6488
|
+
private _renderCloseComposeQuickAction() {
|
|
6489
|
+
if (this.pageMode || this._threadItems.length === 0) return nothing;
|
|
5645
6490
|
|
|
5646
6491
|
return html`
|
|
5647
6492
|
<button
|
|
5648
6493
|
type="button"
|
|
5649
|
-
class="compose-
|
|
6494
|
+
class="compose-quick-actions-close"
|
|
5650
6495
|
?disabled=${this._loading}
|
|
5651
|
-
@click=${() =>
|
|
5652
|
-
this._closePublishPanel(false);
|
|
5653
|
-
this.requestClose();
|
|
5654
|
-
}}
|
|
6496
|
+
@click=${() => this.requestClose()}
|
|
5655
6497
|
>
|
|
5656
|
-
${
|
|
5657
|
-
COMPOSE_PUBLISH_ACTION_ICONS.close,
|
|
5658
|
-
"compose-sheet-row-icon",
|
|
5659
|
-
)}
|
|
5660
|
-
<span class="compose-sheet-main">
|
|
5661
|
-
<span class="compose-sheet-title">${this.labels.closeCompose}</span>
|
|
5662
|
-
</span>
|
|
6498
|
+
${this.labels.close}
|
|
5663
6499
|
</button>
|
|
5664
6500
|
`;
|
|
5665
6501
|
}
|
|
@@ -5792,12 +6628,14 @@ export class JantComposeDialog extends LitElement {
|
|
|
5792
6628
|
"compose-publish-group-open": this._showPublishPanel,
|
|
5793
6629
|
})}
|
|
5794
6630
|
>
|
|
5795
|
-
${
|
|
5796
|
-
|
|
5797
|
-
|
|
5798
|
-
|
|
5799
|
-
|
|
5800
|
-
|
|
6631
|
+
${
|
|
6632
|
+
this._showPublishPanel && !this._publishPanelFullscreen
|
|
6633
|
+
? html`<div
|
|
6634
|
+
class="compose-dropdown-backdrop"
|
|
6635
|
+
@click=${() => this._closePublishPanel(true)}
|
|
6636
|
+
></div>`
|
|
6637
|
+
: nothing
|
|
6638
|
+
}
|
|
5801
6639
|
<div
|
|
5802
6640
|
role="group"
|
|
5803
6641
|
class=${classMap({
|
|
@@ -5820,35 +6658,37 @@ export class JantComposeDialog extends LitElement {
|
|
|
5820
6658
|
<!-- Options sits past Publish as its own control rather than a
|
|
5821
6659
|
chevron welded to it: a split button reads as one button, which
|
|
5822
6660
|
is why nobody found the settings. -->
|
|
5823
|
-
${
|
|
5824
|
-
|
|
5825
|
-
|
|
5826
|
-
|
|
5827
|
-
|
|
5828
|
-
|
|
5829
|
-
|
|
5830
|
-
|
|
5831
|
-
|
|
5832
|
-
|
|
5833
|
-
|
|
5834
|
-
|
|
5835
|
-
|
|
5836
|
-
|
|
5837
|
-
<svg
|
|
5838
|
-
class="compose-options-trigger-icon"
|
|
5839
|
-
width="22"
|
|
5840
|
-
height="22"
|
|
5841
|
-
viewBox="0 0 24 24"
|
|
5842
|
-
fill="none"
|
|
5843
|
-
stroke="currentColor"
|
|
5844
|
-
stroke-linecap="round"
|
|
5845
|
-
stroke-linejoin="round"
|
|
5846
|
-
aria-hidden="true"
|
|
6661
|
+
${
|
|
6662
|
+
this._hasPublishPanelContent()
|
|
6663
|
+
? html`<button
|
|
6664
|
+
type="button"
|
|
6665
|
+
class=${classMap({
|
|
6666
|
+
"compose-options-trigger": true,
|
|
6667
|
+
"compose-options-trigger-open": this._showPublishPanel,
|
|
6668
|
+
})}
|
|
6669
|
+
?disabled=${this._loading}
|
|
6670
|
+
aria-haspopup="dialog"
|
|
6671
|
+
aria-expanded=${this._showPublishPanel ? "true" : "false"}
|
|
6672
|
+
aria-label=${this.labels.publishSettings}
|
|
6673
|
+
title=${this.labels.publishSettings}
|
|
6674
|
+
@click=${() => this._togglePublishPanel()}
|
|
5847
6675
|
>
|
|
5848
|
-
|
|
5849
|
-
|
|
5850
|
-
|
|
5851
|
-
|
|
6676
|
+
<svg
|
|
6677
|
+
class="compose-options-trigger-icon"
|
|
6678
|
+
width="22"
|
|
6679
|
+
height="22"
|
|
6680
|
+
viewBox="0 0 24 24"
|
|
6681
|
+
fill="none"
|
|
6682
|
+
stroke="currentColor"
|
|
6683
|
+
stroke-linecap="round"
|
|
6684
|
+
stroke-linejoin="round"
|
|
6685
|
+
aria-hidden="true"
|
|
6686
|
+
>
|
|
6687
|
+
${unsafeSVG(COMPOSE_PUBLISH_ACTION_ICONS.options)}
|
|
6688
|
+
</svg>
|
|
6689
|
+
</button>`
|
|
6690
|
+
: nothing
|
|
6691
|
+
}
|
|
5852
6692
|
${this._renderDesktopPublishPanel()}
|
|
5853
6693
|
</div>
|
|
5854
6694
|
</div>
|
|
@@ -5861,12 +6701,19 @@ export class JantComposeDialog extends LitElement {
|
|
|
5861
6701
|
* mirrors of a row inside it, not a second source of truth.
|
|
5862
6702
|
*/
|
|
5863
6703
|
private _renderQuickActionsRow() {
|
|
6704
|
+
const close = this._renderCloseComposeQuickAction();
|
|
5864
6705
|
const hideFromLatest = this._renderHideFromLatestQuickToggle();
|
|
5865
6706
|
const quietReply = this._renderQuietReplyQuickToggle();
|
|
5866
|
-
if (
|
|
6707
|
+
if (
|
|
6708
|
+
close === nothing &&
|
|
6709
|
+
hideFromLatest === nothing &&
|
|
6710
|
+
quietReply === nothing
|
|
6711
|
+
) {
|
|
6712
|
+
return nothing;
|
|
6713
|
+
}
|
|
5867
6714
|
return html`
|
|
5868
6715
|
<div class="compose-quick-actions-row">
|
|
5869
|
-
${hideFromLatest} ${quietReply}
|
|
6716
|
+
${close} ${hideFromLatest} ${quietReply}
|
|
5870
6717
|
</div>
|
|
5871
6718
|
`;
|
|
5872
6719
|
}
|
|
@@ -5974,70 +6821,21 @@ export class JantComposeDialog extends LitElement {
|
|
|
5974
6821
|
: this._format;
|
|
5975
6822
|
|
|
5976
6823
|
if (this._threadItems.length === 0) {
|
|
5977
|
-
// Entering thread mode
|
|
5978
|
-
|
|
5979
|
-
|
|
5980
|
-
|
|
5981
|
-
const
|
|
5982
|
-
|
|
6824
|
+
// Entering thread mode renders the post in a new editor element, so the
|
|
6825
|
+
// single-post editor hands it everything once the swap has rendered.
|
|
6826
|
+
// Reading the old element then, not now, picks up any upload that
|
|
6827
|
+
// finished during the render.
|
|
6828
|
+
const singleEditor = this._editor;
|
|
5983
6829
|
this._threadItems = [
|
|
5984
6830
|
{ id: randomUUID(), format: this._format },
|
|
5985
6831
|
{ id: randomUUID(), format: lastFormat },
|
|
5986
6832
|
];
|
|
5987
|
-
|
|
5988
|
-
|
|
5989
|
-
|
|
5990
|
-
|
|
5991
|
-
|
|
5992
|
-
|
|
5993
|
-
if (editorState || editorData) {
|
|
5994
|
-
this.updateComplete.then(() => {
|
|
5995
|
-
const editors = this.querySelectorAll<JantComposeEditor>(
|
|
5996
|
-
"jant-compose-editor",
|
|
5997
|
-
);
|
|
5998
|
-
const firstEditor = editors[0];
|
|
5999
|
-
if (!firstEditor) return;
|
|
6000
|
-
if (editorState) {
|
|
6001
|
-
firstEditor.setEditorState(
|
|
6002
|
-
editorState.json,
|
|
6003
|
-
editorState.title,
|
|
6004
|
-
editorState.showTitle,
|
|
6005
|
-
editorState.selection,
|
|
6006
|
-
);
|
|
6007
|
-
}
|
|
6008
|
-
if (editorData) {
|
|
6009
|
-
if (this._format === "link" && editorData.url) {
|
|
6010
|
-
firstEditor._url = editorData.url;
|
|
6011
|
-
} else if (this._format === "quote") {
|
|
6012
|
-
if (editorData.quoteText)
|
|
6013
|
-
firstEditor._quoteText = editorData.quoteText;
|
|
6014
|
-
if (editorData.quoteAuthor)
|
|
6015
|
-
firstEditor._quoteAuthor = editorData.quoteAuthor;
|
|
6016
|
-
}
|
|
6017
|
-
if (bodyJson) {
|
|
6018
|
-
firstEditor._bodyJson = bodyJson;
|
|
6019
|
-
}
|
|
6020
|
-
}
|
|
6021
|
-
// Read attachment state from the old editor NOW (after re-render) so
|
|
6022
|
-
// we get the latest mediaId for any uploads that completed during the
|
|
6023
|
-
// render cycle. The old editor element is still in memory even though
|
|
6024
|
-
// it has been removed from the DOM.
|
|
6025
|
-
const latestAttachments = currentEditor?._attachments ?? [];
|
|
6026
|
-
const latestAttachmentOrder = currentEditor?._attachmentOrder ?? [];
|
|
6027
|
-
const latestAttachedTexts = currentEditor?._attachedTexts ?? [];
|
|
6028
|
-
if (latestAttachments.length > 0) {
|
|
6029
|
-
firstEditor._attachments = [...latestAttachments];
|
|
6030
|
-
firstEditor._attachmentOrder = [...latestAttachmentOrder];
|
|
6031
|
-
}
|
|
6032
|
-
if (latestAttachedTexts.length > 0) {
|
|
6033
|
-
firstEditor._attachedTexts = [...latestAttachedTexts];
|
|
6034
|
-
}
|
|
6035
|
-
if (capturedRating > 0) {
|
|
6036
|
-
firstEditor._rating = capturedRating;
|
|
6037
|
-
firstEditor._showRating = capturedShowRating;
|
|
6038
|
-
}
|
|
6039
|
-
});
|
|
6040
|
-
}
|
|
6833
|
+
this.updateComplete.then(() => {
|
|
6834
|
+
const rootEditor = this.querySelector<JantComposeEditor>(
|
|
6835
|
+
"jant-compose-editor",
|
|
6836
|
+
);
|
|
6837
|
+
if (singleEditor && rootEditor) rootEditor.takeOver(singleEditor);
|
|
6838
|
+
});
|
|
6041
6839
|
} else {
|
|
6042
6840
|
this._threadItems = [
|
|
6043
6841
|
...this._threadItems,
|
|
@@ -6062,67 +6860,32 @@ export class JantComposeDialog extends LitElement {
|
|
|
6062
6860
|
if (this._threadItems.length <= 1) return;
|
|
6063
6861
|
const newItems = this._threadItems.filter((_, i) => i !== index);
|
|
6064
6862
|
|
|
6863
|
+
// The root's permalink and date live on the dialog rather than on its
|
|
6864
|
+
// thread item, so removing the root has to move the next post's own
|
|
6865
|
+
// values up — otherwise the removed post's permalink and date stay behind
|
|
6866
|
+
// on the post that replaces it, and that post's own are dropped.
|
|
6867
|
+
if (index === 0) {
|
|
6868
|
+
const promoted = newItems[0];
|
|
6869
|
+
this._slug = promoted.slug ?? "";
|
|
6870
|
+
this._slugTaken = promoted.slugTaken ?? false;
|
|
6871
|
+
this._publishedAtInput = promoted.publishedAtInput ?? "";
|
|
6872
|
+
this._publishedAtTimeMinutes = promoted.publishedAtTimeMinutes ?? null;
|
|
6873
|
+
}
|
|
6874
|
+
|
|
6065
6875
|
if (newItems.length === 1) {
|
|
6066
|
-
//
|
|
6067
|
-
//
|
|
6068
|
-
const
|
|
6069
|
-
|
|
6876
|
+
// Leaving thread mode renders the last post in the single-post editor,
|
|
6877
|
+
// which takes everything over from the thread editor it replaces.
|
|
6878
|
+
const remainingEditor = this.querySelector<JantComposeEditor>(
|
|
6879
|
+
`jant-compose-editor[data-thread-id="${newItems[0].id}"]`,
|
|
6070
6880
|
);
|
|
6071
|
-
const remainingIndex = index === 0 ? 1 : 0;
|
|
6072
|
-
const remainingEditor = editors[remainingIndex] ?? null;
|
|
6073
|
-
const editorState = remainingEditor?.getEditorState() ?? null;
|
|
6074
|
-
const editorData = remainingEditor?.getData();
|
|
6075
|
-
const bodyJson = remainingEditor?.getNormalizedBodyJson() ?? null;
|
|
6076
|
-
const remainingFormat = newItems[0].format;
|
|
6077
|
-
// Capture rating state before re-render (can't change asynchronously)
|
|
6078
|
-
const capturedRating = remainingEditor?._rating ?? 0;
|
|
6079
|
-
const capturedShowRating = remainingEditor?._showRating ?? false;
|
|
6080
|
-
|
|
6081
6881
|
this._threadItems = [];
|
|
6082
6882
|
this._focusedThreadIndex = 0;
|
|
6083
|
-
this._format =
|
|
6883
|
+
this._format = newItems[0].format;
|
|
6084
6884
|
|
|
6085
6885
|
this.updateComplete.then(() => {
|
|
6086
6886
|
const singleEditor = this._editor;
|
|
6087
6887
|
if (!singleEditor) return;
|
|
6088
|
-
singleEditor.
|
|
6089
|
-
if (editorState) {
|
|
6090
|
-
singleEditor.setEditorState(
|
|
6091
|
-
editorState.json,
|
|
6092
|
-
editorState.title,
|
|
6093
|
-
editorState.showTitle,
|
|
6094
|
-
editorState.selection,
|
|
6095
|
-
);
|
|
6096
|
-
}
|
|
6097
|
-
if (editorData) {
|
|
6098
|
-
if (remainingFormat === "link" && editorData.url) {
|
|
6099
|
-
singleEditor._url = editorData.url;
|
|
6100
|
-
} else if (remainingFormat === "quote") {
|
|
6101
|
-
if (editorData.quoteText)
|
|
6102
|
-
singleEditor._quoteText = editorData.quoteText;
|
|
6103
|
-
if (editorData.quoteAuthor)
|
|
6104
|
-
singleEditor._quoteAuthor = editorData.quoteAuthor;
|
|
6105
|
-
}
|
|
6106
|
-
if (bodyJson) {
|
|
6107
|
-
singleEditor._bodyJson = bodyJson;
|
|
6108
|
-
}
|
|
6109
|
-
}
|
|
6110
|
-
// Read attachment state from the old editor NOW so we capture any
|
|
6111
|
-
// mediaIds set by uploads that completed during the render cycle.
|
|
6112
|
-
const latestAttachments = remainingEditor?._attachments ?? [];
|
|
6113
|
-
const latestAttachmentOrder = remainingEditor?._attachmentOrder ?? [];
|
|
6114
|
-
const latestAttachedTexts = remainingEditor?._attachedTexts ?? [];
|
|
6115
|
-
if (latestAttachments.length > 0) {
|
|
6116
|
-
singleEditor._attachments = [...latestAttachments];
|
|
6117
|
-
singleEditor._attachmentOrder = [...latestAttachmentOrder];
|
|
6118
|
-
}
|
|
6119
|
-
if (latestAttachedTexts.length > 0) {
|
|
6120
|
-
singleEditor._attachedTexts = [...latestAttachedTexts];
|
|
6121
|
-
}
|
|
6122
|
-
if (capturedRating > 0) {
|
|
6123
|
-
singleEditor._rating = capturedRating;
|
|
6124
|
-
singleEditor._showRating = capturedShowRating;
|
|
6125
|
-
}
|
|
6888
|
+
if (remainingEditor) singleEditor.takeOver(remainingEditor);
|
|
6126
6889
|
singleEditor.focusInput();
|
|
6127
6890
|
});
|
|
6128
6891
|
} else {
|
|
@@ -6180,6 +6943,11 @@ export class JantComposeDialog extends LitElement {
|
|
|
6180
6943
|
}}
|
|
6181
6944
|
@jant:format-change=${(e: CustomEvent<{ format: ComposeFormat }>) => {
|
|
6182
6945
|
e.stopPropagation();
|
|
6946
|
+
this._convertForFormatSwitch(
|
|
6947
|
+
e.target as JantComposeEditor,
|
|
6948
|
+
item.format,
|
|
6949
|
+
e.detail.format,
|
|
6950
|
+
);
|
|
6183
6951
|
this._threadItems = this._threadItems.map((it, i) =>
|
|
6184
6952
|
i === index ? { ...it, format: e.detail.format } : it,
|
|
6185
6953
|
);
|
|
@@ -6257,7 +7025,11 @@ export class JantComposeDialog extends LitElement {
|
|
|
6257
7025
|
})}
|
|
6258
7026
|
@jant:compose-content-changed=${() => this._scheduleDraftSave()}
|
|
6259
7027
|
>
|
|
6260
|
-
${
|
|
7028
|
+
${
|
|
7029
|
+
isReply
|
|
7030
|
+
? this._renderReplyContext()
|
|
7031
|
+
: this._renderTranslationContext()
|
|
7032
|
+
}
|
|
6261
7033
|
<!-- Keyed by row id, not by position: an editor holds its own content,
|
|
6262
7034
|
so an unkeyed list would reuse elements by position and drop the
|
|
6263
7035
|
last one when a middle post is removed — taking that post's text
|
|
@@ -6287,7 +7059,7 @@ export class JantComposeDialog extends LitElement {
|
|
|
6287
7059
|
// placeholder sits where the next post will land instead of floating out at
|
|
6288
7060
|
// the dialog's own left edge, a rail's width clear of everything above it.
|
|
6289
7061
|
private _renderAddThreadTrigger(onRail: boolean) {
|
|
6290
|
-
const disabled = !this.
|
|
7062
|
+
const disabled = !this._hasWorkToLose();
|
|
6291
7063
|
const button = html`
|
|
6292
7064
|
<button
|
|
6293
7065
|
type="button"
|
|
@@ -6358,39 +7130,46 @@ export class JantComposeDialog extends LitElement {
|
|
|
6358
7130
|
?inert=${this._addCollectionPanelOpen}
|
|
6359
7131
|
>
|
|
6360
7132
|
<div class="compose-scroll">
|
|
6361
|
-
${
|
|
6362
|
-
|
|
6363
|
-
|
|
6364
|
-
|
|
6365
|
-
|
|
6366
|
-
|
|
6367
|
-
|
|
6368
|
-
|
|
6369
|
-
|
|
6370
|
-
|
|
6371
|
-
|
|
6372
|
-
<div class="compose-
|
|
6373
|
-
|
|
7133
|
+
${
|
|
7134
|
+
isOpeningEdit
|
|
7135
|
+
? this._renderEditLoadingState()
|
|
7136
|
+
: isThreadMode
|
|
7137
|
+
? this._renderThreadComposeLayout()
|
|
7138
|
+
: isReply
|
|
7139
|
+
? html`
|
|
7140
|
+
<div
|
|
7141
|
+
class="compose-thread-layout compose-reply-compose-layout"
|
|
7142
|
+
>
|
|
7143
|
+
${this._renderReplyContext()}
|
|
7144
|
+
<div class="compose-editor-row is-current">
|
|
7145
|
+
<div class="compose-thread-dot"></div>
|
|
7146
|
+
${editor}
|
|
7147
|
+
</div>
|
|
7148
|
+
${addThreadRow}
|
|
6374
7149
|
</div>
|
|
6375
|
-
|
|
6376
|
-
|
|
6377
|
-
|
|
6378
|
-
: html`${editor}${addThreadRow}`}
|
|
7150
|
+
`
|
|
7151
|
+
: html`${this._renderTranslationContext()}${editor}${addThreadRow}`
|
|
7152
|
+
}
|
|
6379
7153
|
</div>
|
|
6380
|
-
${
|
|
6381
|
-
|
|
6382
|
-
|
|
6383
|
-
|
|
6384
|
-
|
|
6385
|
-
|
|
6386
|
-
|
|
6387
|
-
|
|
6388
|
-
|
|
6389
|
-
|
|
6390
|
-
|
|
6391
|
-
|
|
6392
|
-
|
|
6393
|
-
|
|
7154
|
+
${
|
|
7155
|
+
isOpeningEdit
|
|
7156
|
+
? nothing
|
|
7157
|
+
: html`<div
|
|
7158
|
+
class=${classMap({
|
|
7159
|
+
"compose-action-row": true,
|
|
7160
|
+
"compose-action-row-without-collection": !!this._replyToId,
|
|
7161
|
+
"compose-action-row-overlay-open":
|
|
7162
|
+
this._showPublishPanel ||
|
|
7163
|
+
this._showCollection ||
|
|
7164
|
+
this._showLanguagePicker,
|
|
7165
|
+
})}
|
|
7166
|
+
>
|
|
7167
|
+
${this._replyToId ? nothing : this._renderCollectionSelector()}
|
|
7168
|
+
${this._renderLanguageControl()}
|
|
7169
|
+
${this._renderPublishButton()}
|
|
7170
|
+
</div>
|
|
7171
|
+
${this._renderQuickActionsRow()}`
|
|
7172
|
+
}
|
|
6394
7173
|
${this._renderMobilePublishPanel()} ${this._renderAttachedPanel()}
|
|
6395
7174
|
${this._renderAltPanel()} ${this._renderDraftsPanel()}
|
|
6396
7175
|
${this._renderConfirmPanel()}
|