@jant/core 0.6.14 → 0.6.16
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/bin/commands/import-site.js +88 -13
- package/bin/commands/migrate.js +55 -0
- package/bin/commands/posts/rebuild-html.js +184 -0
- package/bin/commands/site/snapshot/import.js +14 -10
- package/bin/lib/d1-query.js +23 -5
- package/bin/lib/migration-runner.js +29 -7
- package/bin/lib/node-database.js +32 -0
- package/bin/lib/site-snapshot.js +347 -46
- package/bin/lib/sql-export.js +1 -1
- package/bin/lib/thread-collection-migration.js +326 -0
- package/dist/app-C75Zc8dD.js +6 -0
- package/dist/{app-2i7-WQHg.js → app-C7_4oab9.js} +7774 -6425
- package/dist/client/.vite/manifest.json +8 -8
- package/dist/client/_assets/chunks/{url-pLre2DM_.js → url-CU9pEbk5.js} +1 -1
- package/dist/client/_assets/{client-BvID5ucz.js → client-Bq0vre8Y.js} +59 -38
- package/dist/client/_assets/client-D3VZyG4L.css +2 -0
- package/dist/client/_assets/client-auth-F2rb9WfF.js +5571 -0
- package/dist/{env-OHRKGcMj.js → env-BPYViDJJ.js} +1 -1
- package/dist/{export-DWn149b7.js → export-Dl5KCiEs.js} +1348 -128
- package/dist/{github-api-BgSiE71w.js → github-api-BNF35Lkx.js} +1 -1
- package/dist/{github-app-BbklkFmU.js → github-app-DdiD-bC4.js} +1 -1
- package/dist/{github-sync-C0zyF6XS.js → github-sync-Bqg62IvV.js} +3 -3
- package/dist/{github-sync-Ckk0sJxK.js → github-sync-HdK2EgvJ.js} +3 -3
- package/dist/index.js +5 -5
- package/dist/node.js +7 -7
- package/dist/{url-BMYO-Zlt.js → url-CbLAtlZe.js} +517 -12
- package/package.json +11 -10
- package/src/__tests__/bin/posts-rebuild-html.test.ts +137 -0
- package/src/__tests__/dev-navigation-defaults.test.ts +22 -0
- package/src/__tests__/export-hugo-build.test.ts +63 -4
- package/src/__tests__/export-import-roundtrip.test.ts +6 -4
- package/src/__tests__/export-service.test.ts +142 -9
- package/src/__tests__/helpers/app.ts +3 -0
- package/src/__tests__/helpers/export-fixtures.ts +2 -0
- package/src/__tests__/import-site-command.test.ts +65 -0
- package/src/__tests__/mise-config.test.ts +22 -0
- package/src/__tests__/vite-config.test.ts +45 -0
- package/src/app.tsx +16 -3
- package/src/client/__tests__/collection-created-notice.test.ts +36 -0
- package/src/client/__tests__/collection-form-bridge.test.ts +10 -1
- package/src/client/__tests__/collection-page-actions.test.ts +71 -2
- package/src/client/__tests__/compose-bridge.test.ts +316 -2
- package/src/client/__tests__/compose-shortcuts.test.ts +20 -0
- package/src/client/__tests__/feed-video-player.test.ts +16 -1
- package/src/client/__tests__/footnote-rail.test.ts +472 -0
- package/src/client/__tests__/hydrate-partial.test.ts +3 -0
- package/src/client/__tests__/toast.test.ts +82 -1
- package/src/client/collection-created-notice.ts +66 -0
- package/src/client/collection-form-bridge.ts +17 -0
- package/src/client/collection-navigation.ts +44 -0
- package/src/client/collection-page-actions.ts +57 -1
- package/src/client/components/__tests__/compose-format-convert.test.ts +0 -19
- package/src/client/components/__tests__/jant-collection-directory.test.ts +155 -6
- package/src/client/components/__tests__/jant-command-palette.test.ts +51 -0
- package/src/client/components/__tests__/jant-compose-dialog.test.ts +2268 -206
- package/src/client/components/__tests__/jant-compose-editor.test.ts +147 -59
- package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +114 -64
- package/src/client/components/__tests__/jant-config-editor.test.ts +405 -0
- package/src/client/components/__tests__/jant-media-lightbox.test.ts +121 -0
- package/src/client/components/__tests__/jant-nav-manager.test.ts +538 -2
- package/src/client/components/__tests__/jant-post-form.test.ts +2 -2
- package/src/client/components/__tests__/jant-post-menu.test.ts +47 -5
- package/src/client/components/__tests__/jant-settings-avatar.test.ts +2 -2
- package/src/client/components/__tests__/jant-settings-general.test.ts +3 -3
- package/src/client/components/collection-manager-types.ts +9 -3
- package/src/client/components/compose-format-convert.ts +5 -1
- package/src/client/components/compose-types.ts +55 -3
- package/src/client/components/jant-collection-directory.ts +165 -30
- package/src/client/components/jant-command-palette.ts +14 -7
- package/src/client/components/jant-compose-dialog.ts +1542 -694
- package/src/client/components/jant-compose-editor.ts +237 -99
- package/src/client/components/jant-compose-fullscreen.ts +27 -9
- package/src/client/components/jant-config-editor.ts +722 -0
- package/src/client/components/jant-media-lightbox.ts +100 -18
- package/src/client/components/jant-nav-manager.ts +1283 -54
- package/src/client/components/jant-post-form.ts +3 -3
- package/src/client/components/jant-post-menu.ts +177 -95
- package/src/client/components/nav-manager-types.ts +51 -2
- package/src/client/components/post-form-types.ts +1 -1
- package/src/client/compose-bridge.ts +147 -123
- package/src/client/compose-launch.ts +13 -0
- package/src/client/compose-shortcuts.ts +5 -1
- package/src/client/feed-video-player.ts +62 -2
- package/src/client/footnote-rail.ts +478 -0
- package/src/client/hydrate-partial.ts +2 -0
- package/src/client/media-lightbox-events.ts +63 -0
- package/src/client/post-refresh.ts +135 -0
- package/src/client/site-header-fragment.ts +38 -0
- package/src/client/tiptap/__tests__/blockquote-editing.test.ts +201 -0
- package/src/client/tiptap/__tests__/clear-formatting.test.ts +168 -0
- package/src/client/tiptap/__tests__/link-input-rules.test.ts +121 -0
- package/src/client/tiptap/__tests__/list-editing.test.ts +447 -0
- package/src/client/tiptap/__tests__/markdown-clipboard.test.ts +727 -4
- package/src/client/tiptap/__tests__/table-controls.test.ts +263 -0
- package/src/client/tiptap/__tests__/table-size-picker.test.ts +179 -0
- package/src/client/tiptap/bubble-menu.ts +88 -10
- package/src/client/tiptap/continuous-lists.ts +102 -0
- package/src/client/tiptap/create-editor.ts +3 -0
- package/src/client/tiptap/extensions.ts +14 -5
- package/src/client/tiptap/link-input-rules.ts +4 -4
- package/src/client/tiptap/markdown-clipboard.ts +608 -11
- package/src/client/tiptap/slash-commands.ts +8 -1
- package/src/client/tiptap/structural-keymap.ts +211 -0
- package/src/client/tiptap/tab-indent.ts +8 -22
- package/src/client/tiptap/table-control-labels.ts +30 -0
- package/src/client/tiptap/table-controls.ts +537 -0
- package/src/client/tiptap/table-size-picker.ts +254 -0
- package/src/client/toast.ts +133 -39
- package/src/client-auth.ts +1 -0
- package/src/client.ts +1 -0
- package/src/db/__tests__/backfill-thread-activity.test.ts +234 -0
- package/src/db/__tests__/d1-query.test.ts +43 -1
- package/src/db/__tests__/migration-runner.test.ts +63 -0
- package/src/db/__tests__/migrations.test.ts +469 -0
- package/src/db/__tests__/thread-activity.test.ts +113 -0
- package/src/db/__tests__/thread-collection-migration.test.ts +364 -0
- package/src/db/backfills/0005_split_thread_activity_from_quiet_replies.sql +68 -0
- package/src/db/migrations/0027_old_blue_blade.sql +39 -0
- package/src/db/migrations/0028_superb_post.sql +2 -0
- package/src/db/migrations/0029_boring_magma.sql +1 -0
- package/src/db/migrations/0030_typical_vivisector.sql +50 -0
- package/src/db/migrations/0031_many_ego.sql +3 -0
- package/src/db/migrations/meta/0027_snapshot.json +2511 -0
- package/src/db/migrations/meta/0028_snapshot.json +2511 -0
- package/src/db/migrations/meta/0029_snapshot.json +2519 -0
- package/src/db/migrations/meta/0030_snapshot.json +2541 -0
- package/src/db/migrations/meta/0031_snapshot.json +2562 -0
- package/src/db/migrations/meta/_journal.json +35 -0
- package/src/db/migrations/pg/0025_moaning_epoch.sql +39 -0
- package/src/db/migrations/pg/0026_talented_killer_shrike.sql +2 -0
- package/src/db/migrations/pg/0027_short_cannonball.sql +1 -0
- package/src/db/migrations/pg/0028_perfect_sharon_carter.sql +27 -0
- package/src/db/migrations/pg/0029_rare_hairball.sql +3 -0
- package/src/db/migrations/pg/meta/0025_snapshot.json +3204 -0
- package/src/db/migrations/pg/meta/0026_snapshot.json +3204 -0
- package/src/db/migrations/pg/meta/0027_snapshot.json +3211 -0
- package/src/db/migrations/pg/meta/0028_snapshot.json +3248 -0
- package/src/db/migrations/pg/meta/0029_snapshot.json +3289 -0
- package/src/db/migrations/pg/meta/_journal.json +35 -0
- package/src/db/pg/schema.ts +46 -20
- package/src/db/rehearsal-fixtures/demo-current.json +39 -4
- package/src/db/rehearsal-fixtures/demo-current.sql +121 -6
- package/src/db/schema.ts +48 -20
- package/src/db/thread-activity.ts +101 -0
- package/src/i18n/__tests__/detect.test.ts +8 -2
- package/src/i18n/detect.ts +14 -5
- package/src/i18n/locales/public/en.po +222 -51
- package/src/i18n/locales/public/en.ts +1 -1
- package/src/i18n/locales/public/zh-Hans.po +216 -45
- package/src/i18n/locales/public/zh-Hans.ts +1 -1
- package/src/i18n/locales/public/zh-Hant.po +216 -45
- package/src/i18n/locales/public/zh-Hant.ts +1 -1
- package/src/i18n/locales/settings/en.po +513 -10
- package/src/i18n/locales/settings/en.ts +1 -1
- package/src/i18n/locales/settings/zh-Hans.po +515 -12
- package/src/i18n/locales/settings/zh-Hans.ts +1 -1
- package/src/i18n/locales/settings/zh-Hant.po +515 -12
- package/src/i18n/locales/settings/zh-Hant.ts +1 -1
- package/src/lib/__tests__/api-settings.test.ts +299 -0
- package/src/lib/__tests__/constants.test.ts +5 -0
- package/src/lib/__tests__/feed-policy.test.ts +25 -0
- package/src/lib/__tests__/feed.test.ts +168 -0
- package/src/lib/__tests__/footnotes-upgrade.test.ts +181 -0
- package/src/lib/__tests__/markdown-roundtrip-embed.test.ts +61 -0
- package/src/lib/__tests__/markdown-to-tiptap.test.ts +32 -4
- package/src/lib/__tests__/markdown.test.ts +24 -6
- package/src/lib/__tests__/navigation.test.ts +59 -2
- package/src/lib/__tests__/post-body-html.test.ts +176 -0
- package/src/lib/__tests__/post-display.test.ts +64 -0
- package/src/lib/__tests__/resolve-config.test.ts +113 -1
- package/src/lib/__tests__/slug.test.ts +18 -0
- package/src/lib/__tests__/summary.test.ts +82 -0
- package/src/lib/__tests__/timeline.test.ts +109 -70
- package/src/lib/__tests__/timezones.test.ts +31 -3
- package/src/lib/__tests__/tiptap-render.test.ts +268 -9
- package/src/lib/__tests__/tiptap-to-markdown.test.ts +6 -0
- package/src/lib/__tests__/translit.test.ts +87 -0
- package/src/lib/__tests__/url.test.ts +53 -0
- package/src/lib/__tests__/view.test.ts +53 -0
- package/src/lib/__tests__/worker-response-cache.test.ts +18 -1
- package/src/lib/api-settings.ts +239 -12
- package/src/lib/collection-sort.ts +7 -5
- package/src/lib/constants.ts +2 -0
- package/src/lib/feed-path.ts +25 -0
- package/src/lib/feed-policy.ts +64 -0
- package/src/lib/feed.ts +113 -11
- package/src/lib/footnotes.ts +264 -0
- package/src/lib/github-sync-site-config.ts +2 -0
- package/src/lib/hugo-markdown.ts +15 -3
- package/src/lib/jant-branding.ts +1 -0
- package/src/lib/link-preview.ts +25 -0
- package/src/lib/markdown-manager.ts +193 -5
- package/src/lib/markdown.ts +10 -3
- package/src/lib/navigation.ts +16 -3
- package/src/lib/post-body-html.ts +131 -0
- package/src/lib/post-display.ts +47 -8
- package/src/lib/render.tsx +9 -2
- package/src/lib/resolve-config.ts +100 -16
- package/src/lib/schemas.ts +133 -1
- package/src/lib/settings-paths.ts +5 -0
- package/src/lib/site-skill.ts +78 -0
- package/src/lib/summary.ts +16 -5
- package/src/lib/timeline.ts +137 -131
- package/src/lib/timezones.ts +54 -14
- package/src/lib/tiptap-render.ts +359 -83
- package/src/lib/translit.ts +288 -0
- package/src/lib/url.ts +123 -12
- package/src/lib/view.ts +58 -20
- package/src/lib/worker-response-cache.ts +11 -18
- package/src/middleware/__tests__/public-content-access.test.ts +91 -0
- package/src/middleware/config.ts +2 -2
- package/src/middleware/public-content-access.ts +87 -0
- package/src/node/__tests__/cli-export.test.ts +14 -0
- package/src/node/__tests__/cli-migrate.test.ts +6 -0
- package/src/node/__tests__/cli-site-snapshot.test.ts +205 -2
- package/src/node/__tests__/cli-snapshot-meta.test.ts +100 -0
- package/src/node/index.ts +2 -1
- package/src/preset.css +16 -2
- package/src/routes/__tests__/compose.test.ts +51 -0
- package/src/routes/__tests__/site-skill.test.ts +112 -0
- package/src/routes/api/__tests__/collections.test.ts +91 -19
- package/src/routes/api/__tests__/mcp.test.ts +65 -1
- package/src/routes/api/__tests__/nav-items.test.ts +91 -0
- package/src/routes/api/__tests__/palette.test.ts +44 -0
- package/src/routes/api/__tests__/public-access.test.ts +81 -0
- package/src/routes/api/__tests__/settings.test.ts +217 -0
- package/src/routes/api/collections.ts +22 -26
- package/src/routes/api/export.ts +2 -0
- package/src/routes/api/internal/__tests__/post-body-html.test.ts +163 -0
- package/src/routes/api/internal/__tests__/sites.test.ts +35 -2
- package/src/routes/api/internal/post-body-html.ts +43 -0
- package/src/routes/api/internal/sites.ts +24 -0
- package/src/routes/api/nav-items.ts +24 -1
- package/src/routes/api/posts.ts +14 -10
- package/src/routes/api/public/__tests__/archive.test.ts +35 -0
- package/src/routes/api/public/__tests__/posts.test.ts +246 -0
- package/src/routes/api/public/archive.ts +4 -0
- package/src/routes/api/public/posts.ts +48 -24
- package/src/routes/api/search.ts +3 -0
- package/src/routes/api/settings.ts +33 -0
- package/src/routes/auth/__tests__/setup.test.ts +7 -4
- package/src/routes/compose.tsx +8 -3
- package/src/routes/dash/settings.tsx +45 -2
- package/src/routes/feed/__tests__/feed.test.ts +210 -6
- package/src/routes/feed/__tests__/sitemap.test.ts +68 -1
- package/src/routes/feed/feed.ts +23 -14
- package/src/routes/pages/__tests__/archive-params.test.ts +384 -3
- package/src/routes/pages/__tests__/collection-routing.test.ts +63 -5
- package/src/routes/pages/__tests__/collections.test.ts +9 -9
- package/src/routes/pages/__tests__/featured.test.ts +6 -2
- package/src/routes/pages/__tests__/preview.test.ts +176 -0
- package/src/routes/pages/__tests__/search.test.ts +69 -0
- package/src/routes/pages/archive.tsx +90 -22
- package/src/routes/pages/collection.tsx +32 -19
- package/src/routes/pages/collections.tsx +6 -1
- package/src/routes/pages/page.tsx +86 -11
- package/src/routes/site-skill.ts +18 -0
- package/src/services/__tests__/auth.test.ts +7 -5
- package/src/services/__tests__/collection.test.ts +190 -44
- package/src/services/__tests__/navigation.test.ts +229 -21
- package/src/services/__tests__/post-timeline.test.ts +249 -39
- package/src/services/__tests__/post.test.ts +844 -5
- package/src/services/__tests__/search.test.ts +48 -1
- package/src/services/__tests__/settings.test.ts +8 -0
- package/src/services/__tests__/site-admin.test.ts +121 -1
- package/src/services/auth.ts +2 -2
- package/src/services/bootstrap.ts +3 -2
- package/src/services/collection.ts +224 -167
- package/src/services/export-theme/assets/client-site.css +1 -1
- package/src/services/export-theme/assets/client-site.js +46 -25
- package/src/services/export-theme/layouts/_default/list.html +18 -4
- package/src/services/export-theme/layouts/_default/rss.xml +8 -12
- package/src/services/export-theme/layouts/featured/list.html +5 -4
- package/src/services/export-theme/layouts/partials/featured-thread.html +56 -0
- package/src/services/export-theme/layouts/partials/footer.html +1 -1
- package/src/services/export-theme/layouts/partials/media-gallery.html +1 -0
- package/src/services/export-theme/styles/main.css +30 -2
- package/src/services/export.ts +94 -85
- package/src/services/mcp.ts +37 -27
- package/src/services/media.ts +1 -1
- package/src/services/navigation.ts +185 -46
- package/src/services/path.ts +4 -1
- package/src/services/post.ts +1142 -342
- package/src/services/search.ts +17 -2
- package/src/services/settings.ts +2 -10
- package/src/services/site-admin.ts +63 -43
- package/src/style-cjk-jp.css +1 -1
- package/src/style-cjk-kr.css +1 -1
- package/src/style-cjk-tc.css +1 -1
- package/src/style-cjk.css +1 -1
- package/src/styles/components.css +540 -14
- package/src/styles/site-media.css +111 -19
- package/src/styles/tokens.css +76 -10
- package/src/styles/ui.css +2227 -1347
- package/src/types/bindings.ts +3 -0
- package/src/types/config.ts +243 -12
- package/src/types/constants.ts +22 -1
- package/src/types/entities.ts +14 -8
- package/src/types/operations.ts +18 -10
- package/src/types/props.ts +20 -2
- package/src/types/raw-assets.d.ts +5 -0
- package/src/types/views.ts +22 -4
- package/src/ui/__tests__/font-themes.test.ts +42 -30
- package/src/ui/compose/ComposeDialog.tsx +109 -19
- package/src/ui/dash/appearance/NavigationContent.tsx +332 -4
- package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +23 -0
- package/src/ui/dash/settings/ConfigEditorContent.tsx +578 -0
- package/src/ui/dash/settings/GeneralContent.tsx +15 -5
- package/src/ui/dash/settings/SettingsRootContent.tsx +21 -1
- package/src/ui/dash/settings/__tests__/ConfigEditorContent.test.tsx +98 -0
- package/src/ui/dash/settings/__tests__/SettingsRootContent.test.tsx +15 -0
- package/src/ui/feed/CuratedThreadPreview.tsx +16 -4
- package/src/ui/feed/LinkCard.tsx +2 -12
- package/src/ui/feed/LinkPreview.tsx +2 -7
- package/src/ui/feed/NoteCard.tsx +10 -20
- package/src/ui/feed/PostStatusBadges.tsx +42 -3
- package/src/ui/feed/QuoteCard.tsx +2 -12
- package/src/ui/feed/ThreadPreview.tsx +26 -34
- package/src/ui/feed/TimelineFeed.tsx +2 -2
- package/src/ui/feed/__tests__/thread-preview.test.ts +197 -56
- package/src/ui/feed/__tests__/timeline-cards.test.ts +178 -3
- package/src/ui/feed/thread-preview-state.ts +5 -7
- package/src/ui/font-themes.ts +74 -26
- package/src/ui/layouts/BaseLayout.tsx +22 -8
- package/src/ui/layouts/__tests__/BaseLayout.test.tsx +64 -1
- package/src/ui/pages/ArchivePage.tsx +131 -24
- package/src/ui/pages/CollectionPage.tsx +101 -22
- package/src/ui/pages/CollectionsPage.tsx +3 -1
- package/src/ui/pages/PostPage.tsx +39 -12
- package/src/ui/pages/SearchPage.tsx +6 -14
- package/src/ui/pages/__tests__/ArchivePage.test.tsx +70 -0
- package/src/ui/pages/__tests__/PostPage.test.tsx +88 -0
- package/src/ui/shared/CollectionDirectory.tsx +13 -9
- package/src/ui/shared/CollectionsManager.tsx +17 -7
- package/src/ui/shared/DraftPreviewBar.tsx +49 -0
- package/src/ui/shared/HomePageBranding.tsx +2 -2
- package/src/ui/shared/MediaGallery.tsx +1 -0
- package/src/ui/shared/PostFooter.tsx +38 -13
- package/src/ui/shared/__tests__/home-page-branding.test.tsx +1 -1
- package/src/ui/shared/__tests__/media-gallery.test.ts +1 -0
- package/src/ui/shared/__tests__/navigation-labels.test.ts +2 -2
- package/src/ui/shared/__tests__/post-footer.test.ts +15 -0
- package/src/ui/shared/collection-management-labels.ts +33 -2
- package/src/ui/shared/custom-icons.ts +5 -0
- package/src/ui/shared/navigation-labels.ts +2 -4
- package/src/ui/shared/post-article-attributes.ts +52 -0
- package/dist/app-DZDlmh7C.js +0 -6
- package/dist/client/_assets/client-Cevfpm8H.css +0 -2
- package/dist/client/_assets/client-auth-v3yvhbgZ.js +0 -4736
- package/src/client/tiptap/wrapping-input-rules.ts +0 -102
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import { LitElement, html, nothing } from "lit";
|
|
11
11
|
import { classMap } from "lit/directives/class-map.js";
|
|
12
|
+
import { repeat } from "lit/directives/repeat.js";
|
|
12
13
|
import { unsafeHTML } from "lit/directives/unsafe-html.js";
|
|
13
14
|
import { unsafeSVG } from "lit/directives/unsafe-svg.js";
|
|
14
15
|
import type { Editor, JSONContent } from "@tiptap/core";
|
|
@@ -20,8 +21,10 @@ import type {
|
|
|
20
21
|
ComposeSubmitDetail,
|
|
21
22
|
ComposeSubmitAttachment,
|
|
22
23
|
ComposeAttachment,
|
|
24
|
+
ComposeRowStatus,
|
|
23
25
|
DraftItem,
|
|
24
26
|
LocalDraft,
|
|
27
|
+
LocalDraftMedia,
|
|
25
28
|
ComposeFullscreenOpenDetail,
|
|
26
29
|
ComposeFullscreenReplyContext,
|
|
27
30
|
ComposeFullscreenCloseDetail,
|
|
@@ -61,6 +64,18 @@ interface ReplyToData {
|
|
|
61
64
|
interface ThreadItem {
|
|
62
65
|
id: string;
|
|
63
66
|
format: ComposeFormat;
|
|
67
|
+
/**
|
|
68
|
+
* Per-post publish date. Empty means "follows the thread" — the server fills
|
|
69
|
+
* it from the root (see `createThreadWithAttachments`). The root's own date
|
|
70
|
+
* stays on `_publishedAtInput` so every existing edit/draft/submit path keeps
|
|
71
|
+
* working unchanged.
|
|
72
|
+
*/
|
|
73
|
+
publishedAtInput?: string;
|
|
74
|
+
publishedAtTimeMinutes?: number | null;
|
|
75
|
+
/** Per-post permalink. Empty means the server assigns a random id. */
|
|
76
|
+
slug?: string;
|
|
77
|
+
/** Last availability answer for this post's slug. */
|
|
78
|
+
slugTaken?: boolean;
|
|
64
79
|
}
|
|
65
80
|
|
|
66
81
|
interface ApiMediaAttachment {
|
|
@@ -91,6 +106,7 @@ type ApiAttachment = ApiMediaAttachment | ApiTextAttachment;
|
|
|
91
106
|
interface ComposePostResponse {
|
|
92
107
|
id: string;
|
|
93
108
|
threadId?: string;
|
|
109
|
+
threadPosition?: number;
|
|
94
110
|
format: ComposeFormat;
|
|
95
111
|
slug?: string | null;
|
|
96
112
|
visibility?: ComposeVisibility | null;
|
|
@@ -116,11 +132,17 @@ interface ComposeOpenOptions {
|
|
|
116
132
|
collectionId?: string;
|
|
117
133
|
restoreDraft?: boolean;
|
|
118
134
|
initialFormat?: ComposeFormat;
|
|
135
|
+
/** false suppresses the "Draft restored." toast (failure reopen: the error toast is enough) */
|
|
136
|
+
restoreToast?: boolean;
|
|
137
|
+
/** Uploads the bridge knows completed — merged over the draft's own media snapshot */
|
|
138
|
+
restoreMedia?: LocalDraftMedia[];
|
|
119
139
|
}
|
|
120
140
|
|
|
121
141
|
interface ComposeReplyOpenOptions {
|
|
122
142
|
restoreDraft?: boolean;
|
|
123
143
|
initialFormat?: ComposeFormat;
|
|
144
|
+
restoreToast?: boolean;
|
|
145
|
+
restoreMedia?: LocalDraftMedia[];
|
|
124
146
|
}
|
|
125
147
|
|
|
126
148
|
interface ComposeStateSnapshot {
|
|
@@ -136,7 +158,6 @@ interface ComposeStateSnapshot {
|
|
|
136
158
|
quoteText: string;
|
|
137
159
|
quoteAuthor: string;
|
|
138
160
|
rating: number;
|
|
139
|
-
showTitle: boolean;
|
|
140
161
|
showRating: boolean;
|
|
141
162
|
attachments: Array<{
|
|
142
163
|
clientId: string;
|
|
@@ -160,21 +181,20 @@ interface ComposeStateSnapshot {
|
|
|
160
181
|
|
|
161
182
|
const EDITOR_FLOATING_UI_SELECTOR = "[data-editor-floating-ui]";
|
|
162
183
|
|
|
184
|
+
/**
|
|
185
|
+
* Row key for the single-post composer. Thread posts key on their own id; a
|
|
186
|
+
* lone post has no thread item to borrow one from.
|
|
187
|
+
*/
|
|
188
|
+
const SINGLE_ROW_ID = "single";
|
|
189
|
+
|
|
163
190
|
interface ComposeFilePickerCloseDetail {
|
|
164
191
|
cancelled?: boolean;
|
|
165
192
|
}
|
|
166
193
|
|
|
167
|
-
interface ComposePublishSummaryChip {
|
|
168
|
-
kind: "publishedAt" | "slug";
|
|
169
|
-
text: string;
|
|
170
|
-
actionLabel: string;
|
|
171
|
-
value: string;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
194
|
const COMPOSE_PUBLISH_PANEL_FULLSCREEN_QUERY =
|
|
175
195
|
"(max-width: 700px), (max-height: 760px), (hover: none) and (pointer: coarse)";
|
|
176
196
|
|
|
177
|
-
const
|
|
197
|
+
const COMPOSE_SHEET_ROW_ICONS = {
|
|
178
198
|
drafts: `
|
|
179
199
|
<rect x="3.85" y="3.45" width="7.85" height="8.35" rx="2.35" />
|
|
180
200
|
<rect
|
|
@@ -183,7 +203,7 @@ const COMPOSE_DIALOG_HEADER_ICONS = {
|
|
|
183
203
|
width="7.85"
|
|
184
204
|
height="8.35"
|
|
185
205
|
rx="2.35"
|
|
186
|
-
fill="var(--compose-
|
|
206
|
+
fill="var(--compose-icon-paper-fill)"
|
|
187
207
|
stroke="none"
|
|
188
208
|
/>
|
|
189
209
|
<rect x="6.15" y="5.75" width="7.85" height="8.35" rx="2.35" />
|
|
@@ -191,6 +211,14 @@ const COMPOSE_DIALOG_HEADER_ICONS = {
|
|
|
191
211
|
<path d="M8.55 10.8h3.95" stroke-width="1.2" />
|
|
192
212
|
<path d="M8.55 13.05h2.45" stroke-width="1.2" />
|
|
193
213
|
`,
|
|
214
|
+
/* A bookmark: keep this where you can come back to it. One silhouette holds
|
|
215
|
+
up at the 16px these rows render at, where the page-and-pencil it replaced
|
|
216
|
+
turned to mush — and it stays clearly apart from the stacked pages above,
|
|
217
|
+
which the page alone did not. The down-arrow/tray before that read as a
|
|
218
|
+
download. */
|
|
219
|
+
saveDraft: `
|
|
220
|
+
<path d="M6.65 3.6h4.7a2 2 0 0 1 2 2v8.7L9 11.75l-4.35 2.55V5.6a2 2 0 0 1 2-2Z" />
|
|
221
|
+
`,
|
|
194
222
|
} as const;
|
|
195
223
|
|
|
196
224
|
const COMPOSE_PUBLISH_VISIBILITY_ICONS: Record<ComposeVisibility, string> = {
|
|
@@ -216,15 +244,43 @@ const COMPOSE_PUBLISH_ACTION_ICONS = {
|
|
|
216
244
|
check: `
|
|
217
245
|
<path d="M4.35 8.2 6.9 10.7 11.65 5.95" />
|
|
218
246
|
`,
|
|
219
|
-
|
|
220
|
-
<path d="
|
|
247
|
+
caretRight: `
|
|
248
|
+
<path d="M6.45 5.1 9.3 8l-2.85 2.9" />
|
|
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" />
|
|
253
|
+
`,
|
|
254
|
+
calendar: `
|
|
255
|
+
<rect x="2.75" y="3.45" width="10.5" height="9.8" rx="2.2" />
|
|
256
|
+
<path d="M5.35 2.55v2.1" />
|
|
257
|
+
<path d="M10.65 2.55v2.1" />
|
|
258
|
+
<path d="M2.75 6.2h10.5" />
|
|
259
|
+
`,
|
|
260
|
+
/* Two sliders — "settings for this thing", without borrowing the gear, which
|
|
261
|
+
elsewhere means site settings. No enclosing box: the trigger is already a
|
|
262
|
+
round button, and a rounded square inside a circle read as two competing
|
|
263
|
+
frames. Drawn on a 24 grid so the tracks have room to separate. */
|
|
264
|
+
options: `
|
|
265
|
+
<path d="M4 8.5h3.4" stroke-width="1.8" />
|
|
266
|
+
<path d="M12.6 8.5H20" stroke-width="1.8" />
|
|
267
|
+
<circle cx="10" cy="8.5" r="2.2" stroke-width="1.8" />
|
|
268
|
+
<path d="M4 15.5h7.4" stroke-width="1.8" />
|
|
269
|
+
<path d="M16.6 15.5H20" stroke-width="1.8" />
|
|
270
|
+
<circle cx="14" cy="15.5" r="2.2" stroke-width="1.8" />
|
|
221
271
|
`,
|
|
222
272
|
} as const;
|
|
223
273
|
|
|
224
274
|
const COMPOSE_COLLECTION_PICKER_ICONS = {
|
|
275
|
+
/* A stack seen edge-on, not a folder: a collection is a post filed under
|
|
276
|
+
several topics at once, and the folder glyph promises a single container
|
|
277
|
+
the model does not have. Drawn spanning y 3.5–12.5 so the artwork's centre
|
|
278
|
+
is the viewBox's — it used to sit at 6.9 against a centre of 8, and since
|
|
279
|
+
layout centres the svg *box*, that 1.1-unit drift rode straight through as
|
|
280
|
+
an icon floating above its own label. */
|
|
225
281
|
collection: `
|
|
226
|
-
<
|
|
227
|
-
<path d="
|
|
282
|
+
<path d="M2.5 6.3 8 3.5l5.5 2.8L8 9.1 2.5 6.3Z" />
|
|
283
|
+
<path d="M2.5 9.7 8 12.5l5.5-2.8" />
|
|
228
284
|
`,
|
|
229
285
|
search: `
|
|
230
286
|
<circle cx="7.1" cy="7.1" r="3.65" />
|
|
@@ -239,18 +295,15 @@ const COMPOSE_COLLECTION_PICKER_ICONS = {
|
|
|
239
295
|
<path d="M8 5.55v4.9" />
|
|
240
296
|
<path d="M5.55 8h4.9" />
|
|
241
297
|
`,
|
|
242
|
-
chevron: `
|
|
243
|
-
<path d="M5.1 6.45 8 9.3l2.9-2.85" />
|
|
244
|
-
`,
|
|
245
298
|
} as const;
|
|
246
299
|
|
|
247
|
-
function
|
|
248
|
-
icon: (typeof
|
|
300
|
+
function renderComposeSheetRowIcon(
|
|
301
|
+
icon: (typeof COMPOSE_SHEET_ROW_ICONS)[keyof typeof COMPOSE_SHEET_ROW_ICONS],
|
|
249
302
|
) {
|
|
250
303
|
return html`<svg
|
|
251
|
-
class="compose-
|
|
252
|
-
width="
|
|
253
|
-
height="
|
|
304
|
+
class="compose-sheet-row-icon"
|
|
305
|
+
width="18"
|
|
306
|
+
height="18"
|
|
254
307
|
viewBox="0 0 18 18"
|
|
255
308
|
fill="none"
|
|
256
309
|
stroke="currentColor"
|
|
@@ -528,6 +581,38 @@ export class JantComposeDialog extends LitElement {
|
|
|
528
581
|
}
|
|
529
582
|
}
|
|
530
583
|
|
|
584
|
+
/**
|
|
585
|
+
* Whether a note's title field starts visible, remembered per browser.
|
|
586
|
+
* Titling notes is a habit rather than a per-post decision, so the last
|
|
587
|
+
* deliberate answer carries over. One key, not one per collection: where the
|
|
588
|
+
* note was started says nothing about whether it wants a name.
|
|
589
|
+
*/
|
|
590
|
+
private static _NOTE_TITLE_KEY = "jant:compose-note-title";
|
|
591
|
+
|
|
592
|
+
/** Defaults to on, so a browser with nothing stored behaves as it always has. */
|
|
593
|
+
private static _getNoteTitleDefault(): boolean {
|
|
594
|
+
try {
|
|
595
|
+
return (
|
|
596
|
+
globalThis.localStorage.getItem(JantComposeDialog._NOTE_TITLE_KEY) !==
|
|
597
|
+
"0"
|
|
598
|
+
);
|
|
599
|
+
} catch {
|
|
600
|
+
// localStorage unavailable
|
|
601
|
+
return true;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
private static _setNoteTitleDefault(show: boolean) {
|
|
606
|
+
try {
|
|
607
|
+
globalThis.localStorage.setItem(
|
|
608
|
+
JantComposeDialog._NOTE_TITLE_KEY,
|
|
609
|
+
show ? "1" : "0",
|
|
610
|
+
);
|
|
611
|
+
} catch {
|
|
612
|
+
// localStorage unavailable
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
531
616
|
static properties = {
|
|
532
617
|
collections: { type: Array },
|
|
533
618
|
labels: { type: Object },
|
|
@@ -561,13 +646,16 @@ export class JantComposeDialog extends LitElement {
|
|
|
561
646
|
_addCollectionPanelOpen: { state: true },
|
|
562
647
|
_replyToId: { state: true },
|
|
563
648
|
_replyToData: { state: true },
|
|
649
|
+
_replyParentPosition: { state: true },
|
|
564
650
|
_replyExpanded: { state: true },
|
|
565
651
|
_threadItems: { state: true },
|
|
652
|
+
_rowStatus: { state: true },
|
|
566
653
|
_focusedThreadIndex: { state: true },
|
|
567
654
|
_slug: { state: true },
|
|
568
655
|
_publishedAtInput: { state: true },
|
|
569
656
|
_visibility: { state: true },
|
|
570
657
|
_showPublishPanel: { state: true },
|
|
658
|
+
_postMetaIndex: { state: true },
|
|
571
659
|
_publishPanelFullscreen: { state: true },
|
|
572
660
|
_suggestedSlug: { state: true },
|
|
573
661
|
_suggestedSlugLoading: { state: true },
|
|
@@ -606,13 +694,20 @@ export class JantComposeDialog extends LitElement {
|
|
|
606
694
|
declare _addCollectionPanelOpen: boolean;
|
|
607
695
|
declare _replyToId: string | null;
|
|
608
696
|
declare _replyToData: ReplyToData | null;
|
|
697
|
+
/** Where the post being replied to sits in its own chain; see
|
|
698
|
+
* `_positionLabel`. Null until the parent has been read back. */
|
|
699
|
+
declare _replyParentPosition: number | null;
|
|
609
700
|
declare _replyExpanded: boolean;
|
|
610
701
|
declare _threadItems: ThreadItem[];
|
|
702
|
+
/** Each row's own answer, keyed by row id. See `_rowIds`. */
|
|
703
|
+
declare _rowStatus: ReadonlyMap<string, ComposeRowStatus>;
|
|
611
704
|
declare _focusedThreadIndex: number;
|
|
612
705
|
declare _slug: string;
|
|
613
706
|
declare _publishedAtInput: string;
|
|
614
707
|
declare _visibility: ComposeVisibility;
|
|
615
708
|
declare _showPublishPanel: boolean;
|
|
709
|
+
/** Index of the post whose date/permalink panel is open; null when closed. */
|
|
710
|
+
declare _postMetaIndex: number | null;
|
|
616
711
|
declare _publishPanelFullscreen: boolean;
|
|
617
712
|
declare _suggestedSlug: string;
|
|
618
713
|
declare _suggestedSlugLoading: boolean;
|
|
@@ -696,8 +791,10 @@ export class JantComposeDialog extends LitElement {
|
|
|
696
791
|
this._replyToData = null;
|
|
697
792
|
this._replyExpanded = false;
|
|
698
793
|
this._threadItems = [];
|
|
794
|
+
this._rowStatus = new Map();
|
|
699
795
|
this._focusedThreadIndex = 0;
|
|
700
796
|
this._replyThreadRootId = null;
|
|
797
|
+
this._replyParentPosition = null;
|
|
701
798
|
this._replyRefreshKind = null;
|
|
702
799
|
this._replyRefreshId = null;
|
|
703
800
|
this._slug = "";
|
|
@@ -711,6 +808,7 @@ export class JantComposeDialog extends LitElement {
|
|
|
711
808
|
this._sourceCollectionId = null;
|
|
712
809
|
this._showPublishPanel = false;
|
|
713
810
|
this._publishPanelFullscreen = false;
|
|
811
|
+
this._postMetaIndex = null;
|
|
714
812
|
this._suggestedSlug = "";
|
|
715
813
|
this._suggestedSlugLoading = false;
|
|
716
814
|
this._slugCheckLoading = false;
|
|
@@ -723,11 +821,62 @@ export class JantComposeDialog extends LitElement {
|
|
|
723
821
|
return this.querySelector("jant-compose-editor");
|
|
724
822
|
}
|
|
725
823
|
|
|
824
|
+
/**
|
|
825
|
+
* The rows the composer currently has, in order — thread posts by their own
|
|
826
|
+
* id, or the single composer's one row.
|
|
827
|
+
*
|
|
828
|
+
* Everything derived from row content reduces over this list, so a row that
|
|
829
|
+
* has been removed stops being asked the moment `_threadItems` changes. No
|
|
830
|
+
* DOM read is involved, which is the point: a render reads the DOM as it
|
|
831
|
+
* stands *before* that render's own changes are committed.
|
|
832
|
+
*/
|
|
833
|
+
private get _rowIds(): string[] {
|
|
834
|
+
return this._threadItems.length > 0
|
|
835
|
+
? this._threadItems.map((item) => item.id)
|
|
836
|
+
: [SINGLE_ROW_ID];
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
private _handleRowStatus(rowId: string, status: ComposeRowStatus) {
|
|
840
|
+
const previous = this._rowStatus.get(rowId);
|
|
841
|
+
if (
|
|
842
|
+
previous?.hasContent === status.hasContent &&
|
|
843
|
+
previous?.publishable === status.publishable
|
|
844
|
+
) {
|
|
845
|
+
return;
|
|
846
|
+
}
|
|
847
|
+
const next = new Map(this._rowStatus);
|
|
848
|
+
next.set(rowId, status);
|
|
849
|
+
this._rowStatus = next;
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
protected willUpdate(changed: Map<string, unknown>) {
|
|
853
|
+
super.willUpdate(changed);
|
|
854
|
+
|
|
855
|
+
// An answer belongs to the row that gave it. When the set of rows changes,
|
|
856
|
+
// drop the ones that are gone so a row can never inherit an answer that
|
|
857
|
+
// was never about it — leaving thread mode, in particular, hands the single
|
|
858
|
+
// row's key back to a fresh editor.
|
|
859
|
+
if (changed.has("_threadItems")) {
|
|
860
|
+
const live = new Set(this._rowIds);
|
|
861
|
+
if ([...this._rowStatus.keys()].some((id) => !live.has(id))) {
|
|
862
|
+
this._rowStatus = new Map(
|
|
863
|
+
[...this._rowStatus].filter(([id]) => live.has(id)),
|
|
864
|
+
);
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
|
|
726
869
|
protected updated(changed: Map<string, unknown>) {
|
|
727
870
|
super.updated(changed);
|
|
728
871
|
if (this._initialSnapshot === null && this._editor) {
|
|
729
872
|
this._captureInitialSnapshot();
|
|
730
873
|
}
|
|
874
|
+
// The visible title used to be the dialog's accessible name. Nothing on
|
|
875
|
+
// screen needs to replace it, but a screen reader announcing an unnamed
|
|
876
|
+
// dialog does — so the name moves to an attribute, where it costs no space.
|
|
877
|
+
if (changed.has("labels") && this._dialogEl && this.labels) {
|
|
878
|
+
this._dialogEl.setAttribute("aria-label", this.labels.composeDialogLabel);
|
|
879
|
+
}
|
|
731
880
|
if (
|
|
732
881
|
changed.has("_addCollectionPanelOpen") &&
|
|
733
882
|
this._addCollectionPanelOpen
|
|
@@ -762,6 +911,9 @@ export class JantComposeDialog extends LitElement {
|
|
|
762
911
|
if (this._showCollection) {
|
|
763
912
|
this._updateCollectionPopoverSide();
|
|
764
913
|
}
|
|
914
|
+
if (this._postMetaIndex !== null) {
|
|
915
|
+
this._updatePostMetaPanelLayout();
|
|
916
|
+
}
|
|
765
917
|
}
|
|
766
918
|
|
|
767
919
|
reset() {
|
|
@@ -789,8 +941,10 @@ export class JantComposeDialog extends LitElement {
|
|
|
789
941
|
this._replyToData = null;
|
|
790
942
|
this._replyExpanded = false;
|
|
791
943
|
this._threadItems = [];
|
|
944
|
+
this._rowStatus = new Map();
|
|
792
945
|
this._focusedThreadIndex = 0;
|
|
793
946
|
this._replyThreadRootId = null;
|
|
947
|
+
this._replyParentPosition = null;
|
|
794
948
|
this._replyRefreshKind = null;
|
|
795
949
|
this._replyRefreshId = null;
|
|
796
950
|
this._slug = "";
|
|
@@ -846,7 +1000,7 @@ export class JantComposeDialog extends LitElement {
|
|
|
846
1000
|
}
|
|
847
1001
|
}
|
|
848
1002
|
|
|
849
|
-
async openEdit(id: string) {
|
|
1003
|
+
async openEdit(id: string, options?: { restoreToast?: boolean }) {
|
|
850
1004
|
this.reset();
|
|
851
1005
|
const requestId = ++this._openEditRequestId;
|
|
852
1006
|
this._openingEdit = true;
|
|
@@ -891,7 +1045,7 @@ export class JantComposeDialog extends LitElement {
|
|
|
891
1045
|
if (requestId !== this._openEditRequestId) return;
|
|
892
1046
|
}
|
|
893
1047
|
|
|
894
|
-
if (post.collectionIds?.length) {
|
|
1048
|
+
if (!post.replyToId && post.collectionIds?.length) {
|
|
895
1049
|
this._collectionIds = post.collectionIds;
|
|
896
1050
|
}
|
|
897
1051
|
|
|
@@ -905,7 +1059,10 @@ export class JantComposeDialog extends LitElement {
|
|
|
905
1059
|
if (requestId !== this._openEditRequestId) return;
|
|
906
1060
|
|
|
907
1061
|
// Check for a local edit draft (unsaved changes from a previous session)
|
|
908
|
-
const restored = this._restoreEditDraftIfAvailable(
|
|
1062
|
+
const restored = this._restoreEditDraftIfAvailable(
|
|
1063
|
+
id,
|
|
1064
|
+
options?.restoreToast,
|
|
1065
|
+
);
|
|
909
1066
|
|
|
910
1067
|
if (!restored) {
|
|
911
1068
|
this._editor?.populate({
|
|
@@ -943,11 +1100,30 @@ export class JantComposeDialog extends LitElement {
|
|
|
943
1100
|
}
|
|
944
1101
|
}
|
|
945
1102
|
|
|
1103
|
+
async openDraft(id: string) {
|
|
1104
|
+
const dialog = this.closest("dialog");
|
|
1105
|
+
if (dialog && !dialog.open) {
|
|
1106
|
+
dialog.showModal();
|
|
1107
|
+
}
|
|
1108
|
+
await this.updateComplete;
|
|
1109
|
+
this._focusDialogShell();
|
|
1110
|
+
await this._loadDraft(id);
|
|
1111
|
+
}
|
|
1112
|
+
|
|
946
1113
|
async openNew(options?: ComposeOpenOptions) {
|
|
947
1114
|
this.reset();
|
|
948
1115
|
|
|
1116
|
+
// `reset()` seeds the toggle from `titleByDefault`, which Lit only refreshes
|
|
1117
|
+
// on the next render — after a reply, that value is still the reply's. Say
|
|
1118
|
+
// it outright here. A restored draft overwrites it below with its own
|
|
1119
|
+
// answer, which is right: that one belongs to the draft, not to the habit.
|
|
1120
|
+
this._editor?.setTitleDefault(JantComposeDialog._getNoteTitleDefault());
|
|
1121
|
+
|
|
949
1122
|
if (options?.restoreDraft !== false) {
|
|
950
|
-
await this.restoreLocalDraft(
|
|
1123
|
+
await this.restoreLocalDraft({
|
|
1124
|
+
notify: options?.restoreToast,
|
|
1125
|
+
extraMedia: options?.restoreMedia,
|
|
1126
|
+
});
|
|
951
1127
|
}
|
|
952
1128
|
|
|
953
1129
|
if (options?.initialFormat) {
|
|
@@ -1006,13 +1182,44 @@ export class JantComposeDialog extends LitElement {
|
|
|
1006
1182
|
this._format = options?.initialFormat ?? "note";
|
|
1007
1183
|
|
|
1008
1184
|
if (options?.restoreDraft !== false) {
|
|
1009
|
-
await this.restoreLocalDraft({
|
|
1185
|
+
await this.restoreLocalDraft({
|
|
1186
|
+
expectedReplyToId: id,
|
|
1187
|
+
notify: options?.restoreToast,
|
|
1188
|
+
extraMedia: options?.restoreMedia,
|
|
1189
|
+
});
|
|
1010
1190
|
}
|
|
1191
|
+
// Collection membership belongs to the existing Thread. Reply compose
|
|
1192
|
+
// must not restore or submit stale per-compose Collection state.
|
|
1193
|
+
this._collectionIds = [];
|
|
1011
1194
|
|
|
1012
1195
|
this.closest("dialog")?.showModal();
|
|
1013
1196
|
await this.updateComplete;
|
|
1014
1197
|
this._editor?.focusInput();
|
|
1015
1198
|
this._captureInitialSnapshot();
|
|
1199
|
+
// Deliberately not awaited: this only feeds the `3/3` marker, and blocking
|
|
1200
|
+
// the composer's open on it would trade a visible delay for a decoration.
|
|
1201
|
+
// The marker stays hidden until the number is known rather than counting
|
|
1202
|
+
// from 1 and then correcting itself.
|
|
1203
|
+
void this._loadReplyParentPosition(id);
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
/**
|
|
1207
|
+
* Read back where the parent sits in its own chain, so the editors below it
|
|
1208
|
+
* can be numbered from there. Failure is silent — a missing marker is a much
|
|
1209
|
+
* smaller problem than a wrong one.
|
|
1210
|
+
*/
|
|
1211
|
+
private async _loadReplyParentPosition(replyToId: string) {
|
|
1212
|
+
try {
|
|
1213
|
+
const res = await fetch(`/api/posts/${replyToId}`);
|
|
1214
|
+
if (!res.ok) return;
|
|
1215
|
+
const post = (await res.json()) as ComposePostResponse;
|
|
1216
|
+
// The composer may have been closed, or pointed at another post, while
|
|
1217
|
+
// this was in flight.
|
|
1218
|
+
if (this._replyToId !== replyToId) return;
|
|
1219
|
+
this._replyParentPosition = post.threadPosition ?? null;
|
|
1220
|
+
} catch {
|
|
1221
|
+
// Leave the marker hidden.
|
|
1222
|
+
}
|
|
1016
1223
|
}
|
|
1017
1224
|
|
|
1018
1225
|
/**
|
|
@@ -1027,6 +1234,7 @@ export class JantComposeDialog extends LitElement {
|
|
|
1027
1234
|
this._replyThreadRootId = (post.replyToId as string | null)
|
|
1028
1235
|
? (post.threadId as string)
|
|
1029
1236
|
: (post.id as string);
|
|
1237
|
+
this._replyParentPosition = post.threadPosition ?? null;
|
|
1030
1238
|
const dateText = post.publishedAt
|
|
1031
1239
|
? new Date(post.publishedAt * 1000).toLocaleDateString(undefined, {
|
|
1032
1240
|
month: "short",
|
|
@@ -1100,39 +1308,16 @@ export class JantComposeDialog extends LitElement {
|
|
|
1100
1308
|
return false;
|
|
1101
1309
|
}
|
|
1102
1310
|
|
|
1311
|
+
/**
|
|
1312
|
+
* Whether any row holds something worth keeping.
|
|
1313
|
+
*
|
|
1314
|
+
* Collection selection alone isn't content — it's metadata that only matters
|
|
1315
|
+
* when paired with actual post content.
|
|
1316
|
+
*/
|
|
1103
1317
|
private _hasContent(): boolean {
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
"jant-compose-editor",
|
|
1108
|
-
);
|
|
1109
|
-
if (!firstEditor) return false;
|
|
1110
|
-
const data = firstEditor.getData();
|
|
1111
|
-
return (
|
|
1112
|
-
!!data.body ||
|
|
1113
|
-
!!data.title.trim() ||
|
|
1114
|
-
!!data.url.trim() ||
|
|
1115
|
-
!!data.quoteText.trim() ||
|
|
1116
|
-
data.attachments.length > 0
|
|
1117
|
-
);
|
|
1118
|
-
}
|
|
1119
|
-
|
|
1120
|
-
const editor = this._editor;
|
|
1121
|
-
if (!editor) return false;
|
|
1122
|
-
|
|
1123
|
-
const data = editor.getData();
|
|
1124
|
-
if (data.body) return true;
|
|
1125
|
-
if (data.title.trim()) return true;
|
|
1126
|
-
if (data.url.trim()) return true;
|
|
1127
|
-
if (data.quoteText.trim()) return true;
|
|
1128
|
-
if (data.quoteAuthor.trim()) return true;
|
|
1129
|
-
if (data.attachedTexts.length > 0) return true;
|
|
1130
|
-
if (data.rating > 0) return true;
|
|
1131
|
-
if (data.attachments.length > 0) return true;
|
|
1132
|
-
// Collection selection alone isn't content — it's metadata that
|
|
1133
|
-
// only matters when paired with actual post content above.
|
|
1134
|
-
|
|
1135
|
-
return false;
|
|
1318
|
+
return this._rowIds.some(
|
|
1319
|
+
(id) => this._rowStatus.get(id)?.hasContent === true,
|
|
1320
|
+
);
|
|
1136
1321
|
}
|
|
1137
1322
|
|
|
1138
1323
|
private _buildSnapshot(): ComposeStateSnapshot | null {
|
|
@@ -1141,10 +1326,8 @@ export class JantComposeDialog extends LitElement {
|
|
|
1141
1326
|
|
|
1142
1327
|
const editorData = editor.getData();
|
|
1143
1328
|
const attachedTexts = editor.getEffectiveAttachedTexts();
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
? editor._showTitle
|
|
1147
|
-
: false;
|
|
1329
|
+
// No `showTitle` here: a hidden title reads back as an empty one, so the
|
|
1330
|
+
// toggle is already visible in `title`.
|
|
1148
1331
|
const showRating = editorData.rating > 0 ? editor._showRating : false;
|
|
1149
1332
|
|
|
1150
1333
|
return {
|
|
@@ -1160,7 +1343,6 @@ export class JantComposeDialog extends LitElement {
|
|
|
1160
1343
|
quoteText: editorData.quoteText,
|
|
1161
1344
|
quoteAuthor: editorData.quoteAuthor,
|
|
1162
1345
|
rating: editorData.rating,
|
|
1163
|
-
showTitle,
|
|
1164
1346
|
showRating,
|
|
1165
1347
|
attachments: editor._attachments.map((attachment) => ({
|
|
1166
1348
|
clientId: attachment.clientId,
|
|
@@ -1363,17 +1545,23 @@ export class JantComposeDialog extends LitElement {
|
|
|
1363
1545
|
quoteText: editorData.quoteText,
|
|
1364
1546
|
quoteAuthor: editorData.quoteAuthor,
|
|
1365
1547
|
status,
|
|
1366
|
-
slug:
|
|
1367
|
-
|
|
1548
|
+
slug: this._getPostSlug(index).trim() || undefined,
|
|
1549
|
+
// Every post carries its own date; a reply that left it blank sends
|
|
1550
|
+
// undefined and the server fills it from the root.
|
|
1551
|
+
publishedAt: this._getPostPublishedAtSubmitValue(index, status),
|
|
1368
1552
|
visibility: isRoot
|
|
1369
1553
|
? this._visibilityLocked
|
|
1370
1554
|
? undefined
|
|
1371
1555
|
: this._visibility
|
|
1372
1556
|
: undefined,
|
|
1373
1557
|
rating: editorData.rating,
|
|
1374
|
-
collectionIds: isRoot ? [...this._collectionIds] : [],
|
|
1558
|
+
collectionIds: isRoot && !this._replyToId ? [...this._collectionIds] : [],
|
|
1375
1559
|
attachments: orderedAttachments,
|
|
1376
1560
|
replyToId: isRoot ? (this._replyToId ?? undefined) : undefined,
|
|
1561
|
+
quietReply:
|
|
1562
|
+
isRoot && this._canReplyQuietly()
|
|
1563
|
+
? this._quietReply || undefined
|
|
1564
|
+
: undefined,
|
|
1377
1565
|
replyThreadRootId: isRoot
|
|
1378
1566
|
? (this._replyThreadRootId ?? undefined)
|
|
1379
1567
|
: undefined,
|
|
@@ -1441,11 +1629,14 @@ export class JantComposeDialog extends LitElement {
|
|
|
1441
1629
|
status,
|
|
1442
1630
|
visibility: this._visibilityLocked ? undefined : this._visibility,
|
|
1443
1631
|
rating: editorData.rating,
|
|
1444
|
-
collectionIds: [...this._collectionIds],
|
|
1632
|
+
collectionIds: this._replyToId ? [] : [...this._collectionIds],
|
|
1445
1633
|
attachments: orderedAttachments,
|
|
1446
1634
|
editPostId: this._editPostId ?? this._draftSourceId ?? undefined,
|
|
1635
|
+
draftSourceId: this._draftSourceId ?? undefined,
|
|
1447
1636
|
replyToId: this._replyToId ?? undefined,
|
|
1448
|
-
quietReply: this.
|
|
1637
|
+
quietReply: this._canReplyQuietly()
|
|
1638
|
+
? this._quietReply || undefined
|
|
1639
|
+
: undefined,
|
|
1449
1640
|
replyThreadRootId: this._replyThreadRootId ?? undefined,
|
|
1450
1641
|
replyRefreshKind: this._replyRefreshKind ?? undefined,
|
|
1451
1642
|
replyRefreshId: this._replyRefreshId ?? undefined,
|
|
@@ -1453,17 +1644,24 @@ export class JantComposeDialog extends LitElement {
|
|
|
1453
1644
|
}
|
|
1454
1645
|
|
|
1455
1646
|
private _focusBlockedSubmitField(status: "published" | "draft"): boolean {
|
|
1456
|
-
if (
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1647
|
+
if (status === "published") {
|
|
1648
|
+
const posts = Math.max(1, this._threadItems.length);
|
|
1649
|
+
for (let i = 0; i < posts; i++) {
|
|
1650
|
+
if (this._getPostPublishedAtValidationMessage(i) !== null) {
|
|
1651
|
+
this._revealPublishedAtField(i);
|
|
1652
|
+
return true;
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1462
1655
|
}
|
|
1463
1656
|
|
|
1464
|
-
|
|
1465
|
-
this.
|
|
1466
|
-
|
|
1657
|
+
{
|
|
1658
|
+
const posts = Math.max(1, this._threadItems.length);
|
|
1659
|
+
for (let i = 0; i < posts; i++) {
|
|
1660
|
+
if (this._getPostSlugValidationMessage(i)) {
|
|
1661
|
+
this._revealSlugField(i);
|
|
1662
|
+
return true;
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1467
1665
|
}
|
|
1468
1666
|
|
|
1469
1667
|
// ── Thread mode: validate each editor against its own format ──────
|
|
@@ -1562,6 +1760,7 @@ export class JantComposeDialog extends LitElement {
|
|
|
1562
1760
|
detail: {
|
|
1563
1761
|
...threadPosts[0],
|
|
1564
1762
|
editPostId: this._editPostId ?? this._draftSourceId ?? undefined,
|
|
1763
|
+
draftSourceId: this._draftSourceId ?? undefined,
|
|
1565
1764
|
threadPosts,
|
|
1566
1765
|
pendingAttachments: allPending,
|
|
1567
1766
|
},
|
|
@@ -1971,7 +2170,9 @@ export class JantComposeDialog extends LitElement {
|
|
|
1971
2170
|
}
|
|
1972
2171
|
|
|
1973
2172
|
private _scheduleSuggestedSlugRefresh(immediate = false) {
|
|
1974
|
-
|
|
2173
|
+
// Only worth fetching while the slug UI is on screen — which is the post's
|
|
2174
|
+
// own panel now, not the publish panel.
|
|
2175
|
+
if (this._postMetaIndex !== 0 || this._hasManualSlug()) return;
|
|
1975
2176
|
if (!this._canSuggestSlug()) {
|
|
1976
2177
|
this._slugSuggestRequestId += 1;
|
|
1977
2178
|
this._suggestedSlug = "";
|
|
@@ -2026,7 +2227,7 @@ export class JantComposeDialog extends LitElement {
|
|
|
2026
2227
|
if (
|
|
2027
2228
|
requestId !== this._slugSuggestRequestId ||
|
|
2028
2229
|
this._hasManualSlug() ||
|
|
2029
|
-
|
|
2230
|
+
this._postMetaIndex !== 0
|
|
2030
2231
|
) {
|
|
2031
2232
|
return;
|
|
2032
2233
|
}
|
|
@@ -2041,9 +2242,9 @@ export class JantComposeDialog extends LitElement {
|
|
|
2041
2242
|
}
|
|
2042
2243
|
}
|
|
2043
2244
|
|
|
2044
|
-
private _scheduleSlugAvailabilityCheck() {
|
|
2045
|
-
if (!this.
|
|
2046
|
-
this.
|
|
2245
|
+
private _scheduleSlugAvailabilityCheck(index = 0) {
|
|
2246
|
+
if (!this._hasPostManualSlug(index)) {
|
|
2247
|
+
this._setPostSlugTaken(index, false);
|
|
2047
2248
|
this._slugCheckLoading = false;
|
|
2048
2249
|
return;
|
|
2049
2250
|
}
|
|
@@ -2053,21 +2254,20 @@ export class JantComposeDialog extends LitElement {
|
|
|
2053
2254
|
this._slugCheckTimer = null;
|
|
2054
2255
|
}
|
|
2055
2256
|
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
this._slugTaken = false;
|
|
2257
|
+
if (this._getPostSlugSyncValidationMessage(index)) {
|
|
2258
|
+
this._setPostSlugTaken(index, false);
|
|
2059
2259
|
this._slugCheckLoading = false;
|
|
2060
2260
|
return;
|
|
2061
2261
|
}
|
|
2062
2262
|
|
|
2063
2263
|
this._slugCheckLoading = true;
|
|
2064
|
-
const slug = this.
|
|
2264
|
+
const slug = this._getPostSlug(index).trim();
|
|
2065
2265
|
this._slugCheckTimer = setTimeout(() => {
|
|
2066
|
-
void this._checkSlugAvailability(slug);
|
|
2266
|
+
void this._checkSlugAvailability(slug, index);
|
|
2067
2267
|
}, 250);
|
|
2068
2268
|
}
|
|
2069
2269
|
|
|
2070
|
-
private async _checkSlugAvailability(slug: string) {
|
|
2270
|
+
private async _checkSlugAvailability(slug: string, index = 0) {
|
|
2071
2271
|
this._slugCheckTimer = null;
|
|
2072
2272
|
const requestId = ++this._slugCheckRequestId;
|
|
2073
2273
|
|
|
@@ -2084,11 +2284,11 @@ export class JantComposeDialog extends LitElement {
|
|
|
2084
2284
|
const json = (await res.json()) as { available?: boolean };
|
|
2085
2285
|
if (
|
|
2086
2286
|
requestId !== this._slugCheckRequestId ||
|
|
2087
|
-
this.
|
|
2287
|
+
this._getPostSlug(index).trim() !== slug
|
|
2088
2288
|
) {
|
|
2089
2289
|
return;
|
|
2090
2290
|
}
|
|
2091
|
-
this.
|
|
2291
|
+
this._setPostSlugTaken(index, json.available === false);
|
|
2092
2292
|
} catch {
|
|
2093
2293
|
// Server-side create/update remains the final authority.
|
|
2094
2294
|
} finally {
|
|
@@ -2110,9 +2310,9 @@ export class JantComposeDialog extends LitElement {
|
|
|
2110
2310
|
});
|
|
2111
2311
|
}
|
|
2112
2312
|
|
|
2113
|
-
private _resetCustomSlug() {
|
|
2114
|
-
this.
|
|
2115
|
-
this.
|
|
2313
|
+
private _resetCustomSlug(index = 0) {
|
|
2314
|
+
this._setPostSlug(index, "");
|
|
2315
|
+
this._setPostSlugTaken(index, false);
|
|
2116
2316
|
this._slugCheckLoading = false;
|
|
2117
2317
|
this._scheduleSuggestedSlugRefresh(true);
|
|
2118
2318
|
this.updateComplete.then(() => {
|
|
@@ -2136,6 +2336,10 @@ export class JantComposeDialog extends LitElement {
|
|
|
2136
2336
|
"jant:compose-content-changed",
|
|
2137
2337
|
this._onContentChanged,
|
|
2138
2338
|
);
|
|
2339
|
+
this.addEventListener(
|
|
2340
|
+
"jant:title-toggle",
|
|
2341
|
+
this._handleTitleToggle as EventListener,
|
|
2342
|
+
);
|
|
2139
2343
|
this.addEventListener("jant:file-picker-open", this._handleFilePickerOpen);
|
|
2140
2344
|
this.addEventListener(
|
|
2141
2345
|
"jant:file-picker-close",
|
|
@@ -2158,6 +2362,10 @@ export class JantComposeDialog extends LitElement {
|
|
|
2158
2362
|
window.addEventListener("scroll", this._handleViewportChange, {
|
|
2159
2363
|
passive: true,
|
|
2160
2364
|
});
|
|
2365
|
+
document.addEventListener("scroll", this._handleAnyScroll, {
|
|
2366
|
+
capture: true,
|
|
2367
|
+
passive: true,
|
|
2368
|
+
});
|
|
2161
2369
|
globalThis.visualViewport?.addEventListener(
|
|
2162
2370
|
"resize",
|
|
2163
2371
|
this._handleViewportChange,
|
|
@@ -2193,6 +2401,10 @@ export class JantComposeDialog extends LitElement {
|
|
|
2193
2401
|
"jant:compose-content-changed",
|
|
2194
2402
|
this._onContentChanged,
|
|
2195
2403
|
);
|
|
2404
|
+
this.removeEventListener(
|
|
2405
|
+
"jant:title-toggle",
|
|
2406
|
+
this._handleTitleToggle as EventListener,
|
|
2407
|
+
);
|
|
2196
2408
|
this.removeEventListener(
|
|
2197
2409
|
"jant:file-picker-open",
|
|
2198
2410
|
this._handleFilePickerOpen,
|
|
@@ -2213,6 +2425,9 @@ export class JantComposeDialog extends LitElement {
|
|
|
2213
2425
|
window.removeEventListener("beforeunload", this._onBeforeUnload);
|
|
2214
2426
|
window.removeEventListener("resize", this._handleViewportChange);
|
|
2215
2427
|
window.removeEventListener("scroll", this._handleViewportChange);
|
|
2428
|
+
document.removeEventListener("scroll", this._handleAnyScroll, {
|
|
2429
|
+
capture: true,
|
|
2430
|
+
});
|
|
2216
2431
|
globalThis.visualViewport?.removeEventListener(
|
|
2217
2432
|
"resize",
|
|
2218
2433
|
this._handleViewportChange,
|
|
@@ -2269,6 +2484,20 @@ export class JantComposeDialog extends LitElement {
|
|
|
2269
2484
|
if (this._showCollection) {
|
|
2270
2485
|
this.updateComplete.then(() => this._updateCollectionPopoverSide());
|
|
2271
2486
|
}
|
|
2487
|
+
if (this._postMetaIndex !== null) {
|
|
2488
|
+
this.updateComplete.then(() => this._updatePostMetaPanelLayout());
|
|
2489
|
+
}
|
|
2490
|
+
};
|
|
2491
|
+
|
|
2492
|
+
/**
|
|
2493
|
+
* The date/permalink panel is pinned to a pill inside `.compose-scroll`, and
|
|
2494
|
+
* a `scroll` event on that box never reaches `window` — it doesn't bubble.
|
|
2495
|
+
* Capture phase catches it wherever it fires, so the panel travels with its
|
|
2496
|
+
* pill instead of hanging in place once the composer moves under it.
|
|
2497
|
+
*/
|
|
2498
|
+
private _handleAnyScroll = () => {
|
|
2499
|
+
if (this._postMetaIndex === null) return;
|
|
2500
|
+
this._updatePostMetaPanelLayout();
|
|
2272
2501
|
};
|
|
2273
2502
|
|
|
2274
2503
|
private _handleDialogCancel = (e: Event) => {
|
|
@@ -2393,6 +2622,12 @@ export class JantComposeDialog extends LitElement {
|
|
|
2393
2622
|
return true;
|
|
2394
2623
|
}
|
|
2395
2624
|
|
|
2625
|
+
if (this._postMetaIndex !== null) {
|
|
2626
|
+
// `_closePostMeta` hands focus back to the post itself.
|
|
2627
|
+
this._closePostMeta();
|
|
2628
|
+
return true;
|
|
2629
|
+
}
|
|
2630
|
+
|
|
2396
2631
|
if (this._altPanelOpen) {
|
|
2397
2632
|
this._closeAltPanel();
|
|
2398
2633
|
this._restorePageEditorFocus();
|
|
@@ -2437,15 +2672,7 @@ export class JantComposeDialog extends LitElement {
|
|
|
2437
2672
|
this._handleConfirmSave();
|
|
2438
2673
|
} else if ((ke.metaKey || ke.ctrlKey) && ke.key === "Enter") {
|
|
2439
2674
|
e.preventDefault();
|
|
2440
|
-
|
|
2441
|
-
this._doneAttachedPanel();
|
|
2442
|
-
return;
|
|
2443
|
-
}
|
|
2444
|
-
if (!this._canPublish()) {
|
|
2445
|
-
this._focusBlockedSubmitField("published");
|
|
2446
|
-
return;
|
|
2447
|
-
}
|
|
2448
|
-
void this._submit("published");
|
|
2675
|
+
this._publishFromShortcut();
|
|
2449
2676
|
} else if (
|
|
2450
2677
|
(ke.metaKey || ke.ctrlKey) &&
|
|
2451
2678
|
!ke.altKey &&
|
|
@@ -2471,6 +2698,18 @@ export class JantComposeDialog extends LitElement {
|
|
|
2471
2698
|
}
|
|
2472
2699
|
};
|
|
2473
2700
|
|
|
2701
|
+
private _publishFromShortcut() {
|
|
2702
|
+
if (this._attachedPanelOpen) {
|
|
2703
|
+
this._doneAttachedPanel();
|
|
2704
|
+
return;
|
|
2705
|
+
}
|
|
2706
|
+
if (!this._canPublish()) {
|
|
2707
|
+
this._focusBlockedSubmitField("published");
|
|
2708
|
+
return;
|
|
2709
|
+
}
|
|
2710
|
+
void this._submit("published");
|
|
2711
|
+
}
|
|
2712
|
+
|
|
2474
2713
|
private _handleAltPanelOpen = (e: Event) => {
|
|
2475
2714
|
const detail = (e as CustomEvent<{ index: number }>).detail;
|
|
2476
2715
|
this._altPanelIndex = detail.index;
|
|
@@ -2500,7 +2739,10 @@ export class JantComposeDialog extends LitElement {
|
|
|
2500
2739
|
private _handleFullscreenClose = (
|
|
2501
2740
|
e: CustomEvent<ComposeFullscreenCloseDetail>,
|
|
2502
2741
|
) => {
|
|
2503
|
-
const
|
|
2742
|
+
const editors = Array.from(
|
|
2743
|
+
this.querySelectorAll<JantComposeEditor>("jant-compose-editor"),
|
|
2744
|
+
);
|
|
2745
|
+
const editor = editors[e.detail.editorIndex ?? 0] ?? this._editor;
|
|
2504
2746
|
if (editor) {
|
|
2505
2747
|
editor.setEditorState(
|
|
2506
2748
|
e.detail.json as import("@tiptap/core").JSONContent,
|
|
@@ -2511,9 +2753,16 @@ export class JantComposeDialog extends LitElement {
|
|
|
2511
2753
|
// Adopt any in-flight inline image uploads from the fullscreen editor
|
|
2512
2754
|
// so blob: placeholder URLs get replaced when uploads complete.
|
|
2513
2755
|
editor.adoptPendingUploads();
|
|
2514
|
-
|
|
2756
|
+
if (e.detail.intent !== "publish") {
|
|
2757
|
+
this.updateComplete.then(() =>
|
|
2758
|
+
editor.focusSelection(e.detail.selection),
|
|
2759
|
+
);
|
|
2760
|
+
}
|
|
2515
2761
|
}
|
|
2516
2762
|
this._replyExpanded = e.detail.replyExpanded;
|
|
2763
|
+
if (e.detail.intent === "publish") {
|
|
2764
|
+
this._publishFromShortcut();
|
|
2765
|
+
}
|
|
2517
2766
|
};
|
|
2518
2767
|
|
|
2519
2768
|
private _buildFullscreenReplyContext(): ComposeFullscreenReplyContext | null {
|
|
@@ -2530,6 +2779,11 @@ export class JantComposeDialog extends LitElement {
|
|
|
2530
2779
|
e: CustomEvent<ComposeFullscreenOpenDetail>,
|
|
2531
2780
|
) => {
|
|
2532
2781
|
e.detail.replyContext = this._buildFullscreenReplyContext();
|
|
2782
|
+
const editors = Array.from(
|
|
2783
|
+
this.querySelectorAll<JantComposeEditor>("jant-compose-editor"),
|
|
2784
|
+
);
|
|
2785
|
+
const editorIndex = editors.indexOf(e.target as JantComposeEditor);
|
|
2786
|
+
e.detail.editorIndex = editorIndex >= 0 ? editorIndex : 0;
|
|
2533
2787
|
};
|
|
2534
2788
|
|
|
2535
2789
|
private _handleAttachedPanelOpen = (e: Event) => {
|
|
@@ -2551,6 +2805,7 @@ export class JantComposeDialog extends LitElement {
|
|
|
2551
2805
|
placeholder: this.labels.attachedTextPlaceholder,
|
|
2552
2806
|
content,
|
|
2553
2807
|
toolbarMode: "compose",
|
|
2808
|
+
tableControlLabels: this.labels.tableControls,
|
|
2554
2809
|
});
|
|
2555
2810
|
this._focusAttachedEditorBoundary(content);
|
|
2556
2811
|
});
|
|
@@ -2638,6 +2893,7 @@ export class JantComposeDialog extends LitElement {
|
|
|
2638
2893
|
const allDraftItems = (posts as Record<string, unknown>[]).map(
|
|
2639
2894
|
(p): DraftItem => ({
|
|
2640
2895
|
id: p.id as string,
|
|
2896
|
+
slug: p.slug as string,
|
|
2641
2897
|
format: p.format as ComposeFormat,
|
|
2642
2898
|
title:
|
|
2643
2899
|
((p.format as ComposeFormat) === "quote"
|
|
@@ -2744,7 +3000,7 @@ export class JantComposeDialog extends LitElement {
|
|
|
2744
3000
|
this._visibility = post.visibility ?? "public";
|
|
2745
3001
|
this._visibilityLocked = Boolean(post.replyToId);
|
|
2746
3002
|
|
|
2747
|
-
if (post.collectionIds?.length) {
|
|
3003
|
+
if (!post.replyToId && post.collectionIds?.length) {
|
|
2748
3004
|
this._collectionIds = post.collectionIds;
|
|
2749
3005
|
}
|
|
2750
3006
|
|
|
@@ -2903,9 +3159,16 @@ export class JantComposeDialog extends LitElement {
|
|
|
2903
3159
|
}
|
|
2904
3160
|
|
|
2905
3161
|
private _getDraftPreview(draft: DraftItem): string | null {
|
|
2906
|
-
if (draft.
|
|
3162
|
+
if (draft.format === "quote") {
|
|
3163
|
+
if (draft.quoteText) return draft.quoteText;
|
|
3164
|
+
if (draft.title) return draft.title;
|
|
3165
|
+
if (draft.bodyText) return draft.bodyText;
|
|
3166
|
+
if (draft.url) return draft.url;
|
|
3167
|
+
return null;
|
|
3168
|
+
}
|
|
3169
|
+
|
|
2907
3170
|
if (draft.title) return draft.title;
|
|
2908
|
-
if (draft.
|
|
3171
|
+
if (draft.bodyText) return draft.bodyText;
|
|
2909
3172
|
if (draft.url) return draft.url;
|
|
2910
3173
|
return null;
|
|
2911
3174
|
}
|
|
@@ -2941,6 +3204,18 @@ export class JantComposeDialog extends LitElement {
|
|
|
2941
3204
|
}
|
|
2942
3205
|
};
|
|
2943
3206
|
|
|
3207
|
+
/**
|
|
3208
|
+
* Remember the note title toggle for the next new note. Same guard as
|
|
3209
|
+
* `_setVisibility`: an edit, a loaded draft, or a reply answers for that one
|
|
3210
|
+
* post, not for how this author writes. Thread rows are excluded too — the
|
|
3211
|
+
* toggle there answers for a position in a thread.
|
|
3212
|
+
*/
|
|
3213
|
+
private _handleTitleToggle = (e: CustomEvent<{ showTitle: boolean }>) => {
|
|
3214
|
+
if (this._editPostId || this._draftSourceId || this._replyToId) return;
|
|
3215
|
+
if ((e.target as JantComposeEditor | null)?.threadItem) return;
|
|
3216
|
+
JantComposeDialog._setNoteTitleDefault(e.detail.showTitle);
|
|
3217
|
+
};
|
|
3218
|
+
|
|
2944
3219
|
private _cancelDraftSaveTimer() {
|
|
2945
3220
|
if (this._draftSaveTimer !== null) {
|
|
2946
3221
|
clearTimeout(this._draftSaveTimer);
|
|
@@ -3003,6 +3278,9 @@ export class JantComposeDialog extends LitElement {
|
|
|
3003
3278
|
const data = editor.getData();
|
|
3004
3279
|
return {
|
|
3005
3280
|
format: item.format,
|
|
3281
|
+
publishedAtInput: item.publishedAtInput,
|
|
3282
|
+
publishedAtTimeMinutes: item.publishedAtTimeMinutes,
|
|
3283
|
+
slug: item.slug,
|
|
3006
3284
|
title: data.title,
|
|
3007
3285
|
bodyJson: editor.getNormalizedBodyJson(),
|
|
3008
3286
|
url: data.url,
|
|
@@ -3015,6 +3293,9 @@ export class JantComposeDialog extends LitElement {
|
|
|
3015
3293
|
summary: t.summary,
|
|
3016
3294
|
})),
|
|
3017
3295
|
attachmentOrder: [...data.attachmentOrder],
|
|
3296
|
+
mediaAttachments: JantComposeDialog._mediaSnapshotFromAttachments(
|
|
3297
|
+
data.attachments,
|
|
3298
|
+
),
|
|
3018
3299
|
};
|
|
3019
3300
|
})
|
|
3020
3301
|
.filter((item): item is NonNullable<typeof item> => item !== null);
|
|
@@ -3072,6 +3353,7 @@ export class JantComposeDialog extends LitElement {
|
|
|
3072
3353
|
!!data.quoteText.trim() ||
|
|
3073
3354
|
!!data.quoteAuthor.trim() ||
|
|
3074
3355
|
data.rating > 0 ||
|
|
3356
|
+
data.attachments.length > 0 ||
|
|
3075
3357
|
data.attachedTexts.length > 0;
|
|
3076
3358
|
|
|
3077
3359
|
if (!hasContent) {
|
|
@@ -3091,10 +3373,10 @@ export class JantComposeDialog extends LitElement {
|
|
|
3091
3373
|
publishedAtTimeMinutes: this._publishedAtTimeMinutes,
|
|
3092
3374
|
visibility: this._visibility,
|
|
3093
3375
|
rating: data.rating,
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3376
|
+
// The toggle as it stands, not "does a title exist" — restoring a draft
|
|
3377
|
+
// should put the editor back the way it was left, including an open but
|
|
3378
|
+
// still-empty title field.
|
|
3379
|
+
showTitle: this._format === "note" ? editor._showTitle : false,
|
|
3098
3380
|
showRating: data.rating > 0 ? editor._showRating : false,
|
|
3099
3381
|
collectionIds: [...this._collectionIds],
|
|
3100
3382
|
replyToId: this._replyToId,
|
|
@@ -3105,6 +3387,9 @@ export class JantComposeDialog extends LitElement {
|
|
|
3105
3387
|
summary: t.summary,
|
|
3106
3388
|
})),
|
|
3107
3389
|
attachmentOrder: [...data.attachmentOrder],
|
|
3390
|
+
mediaAttachments: JantComposeDialog._mediaSnapshotFromAttachments(
|
|
3391
|
+
data.attachments,
|
|
3392
|
+
),
|
|
3108
3393
|
savedAt: Date.now(),
|
|
3109
3394
|
};
|
|
3110
3395
|
|
|
@@ -3133,7 +3418,67 @@ export class JantComposeDialog extends LitElement {
|
|
|
3133
3418
|
);
|
|
3134
3419
|
}
|
|
3135
3420
|
|
|
3136
|
-
|
|
3421
|
+
/**
|
|
3422
|
+
* Snapshot attachments whose upload completed. Only these can be restored
|
|
3423
|
+
* later — for the rest the bytes exist nowhere but in-memory.
|
|
3424
|
+
*/
|
|
3425
|
+
private static _mediaSnapshotFromAttachments(
|
|
3426
|
+
attachments: ComposeAttachment[],
|
|
3427
|
+
): LocalDraftMedia[] {
|
|
3428
|
+
return attachments.flatMap((a) =>
|
|
3429
|
+
a.status === "done" && a.mediaId && a.remoteUrl
|
|
3430
|
+
? [
|
|
3431
|
+
{
|
|
3432
|
+
clientId: a.clientId,
|
|
3433
|
+
mediaId: a.mediaId,
|
|
3434
|
+
url: a.remoteUrl,
|
|
3435
|
+
mimeType: a.file.type,
|
|
3436
|
+
name: a.file.name || undefined,
|
|
3437
|
+
alt: a.alt || undefined,
|
|
3438
|
+
summary: a.summary,
|
|
3439
|
+
chars: a.chars,
|
|
3440
|
+
},
|
|
3441
|
+
]
|
|
3442
|
+
: [],
|
|
3443
|
+
);
|
|
3444
|
+
}
|
|
3445
|
+
|
|
3446
|
+
/** Convert stored media snapshots into `populate()` media entries. */
|
|
3447
|
+
private static _restoredMediaToPopulate(media: LocalDraftMedia[]) {
|
|
3448
|
+
return media.map((m) => ({
|
|
3449
|
+
id: m.mediaId,
|
|
3450
|
+
clientId: m.clientId,
|
|
3451
|
+
previewUrl: m.url,
|
|
3452
|
+
mimeType: m.mimeType,
|
|
3453
|
+
originalName: m.name,
|
|
3454
|
+
alt: m.alt,
|
|
3455
|
+
summary: m.summary ?? undefined,
|
|
3456
|
+
chars: m.chars ?? undefined,
|
|
3457
|
+
}));
|
|
3458
|
+
}
|
|
3459
|
+
|
|
3460
|
+
/**
|
|
3461
|
+
* Merge a draft's media snapshot with bridge-supplied completed uploads.
|
|
3462
|
+
* Bridge entries win: they include uploads that finished after the dialog
|
|
3463
|
+
* closed, which the autosaved snapshot predates.
|
|
3464
|
+
*/
|
|
3465
|
+
private static _mergeRestoredMedia(
|
|
3466
|
+
snapshot: LocalDraftMedia[] | undefined,
|
|
3467
|
+
extra: LocalDraftMedia[] | undefined,
|
|
3468
|
+
): Map<string, LocalDraftMedia> {
|
|
3469
|
+
const merged = new Map<string, LocalDraftMedia>();
|
|
3470
|
+
for (const m of snapshot ?? []) merged.set(m.clientId, m);
|
|
3471
|
+
for (const m of extra ?? []) merged.set(m.clientId, m);
|
|
3472
|
+
return merged;
|
|
3473
|
+
}
|
|
3474
|
+
|
|
3475
|
+
async restoreLocalDraft(options?: {
|
|
3476
|
+
expectedReplyToId?: string;
|
|
3477
|
+
/** false suppresses the "Draft restored." toast */
|
|
3478
|
+
notify?: boolean;
|
|
3479
|
+
/** Completed uploads known to the bridge at failure time */
|
|
3480
|
+
extraMedia?: LocalDraftMedia[];
|
|
3481
|
+
}) {
|
|
3137
3482
|
// Don't restore if already in edit or draft-load mode
|
|
3138
3483
|
if (this._editPostId || this._draftSourceId) return;
|
|
3139
3484
|
// Don't restore if the editor already has content (e.g. reopened dialog)
|
|
@@ -3168,7 +3513,9 @@ export class JantComposeDialog extends LitElement {
|
|
|
3168
3513
|
return;
|
|
3169
3514
|
}
|
|
3170
3515
|
|
|
3171
|
-
this._collectionIds =
|
|
3516
|
+
this._collectionIds = draft.replyToId
|
|
3517
|
+
? []
|
|
3518
|
+
: [...(draft.collectionIds ?? [])];
|
|
3172
3519
|
this._slug = draft.slug ?? "";
|
|
3173
3520
|
this._slugTaken = false;
|
|
3174
3521
|
this._slugCheckLoading = false;
|
|
@@ -3192,6 +3539,9 @@ export class JantComposeDialog extends LitElement {
|
|
|
3192
3539
|
this._threadItems = draft.threadItems.map((item) => ({
|
|
3193
3540
|
id: randomUUID(),
|
|
3194
3541
|
format: item.format,
|
|
3542
|
+
publishedAtInput: item.publishedAtInput,
|
|
3543
|
+
publishedAtTimeMinutes: item.publishedAtTimeMinutes,
|
|
3544
|
+
slug: item.slug,
|
|
3195
3545
|
}));
|
|
3196
3546
|
this._focusedThreadIndex = 0;
|
|
3197
3547
|
|
|
@@ -3218,6 +3568,18 @@ export class JantComposeDialog extends LitElement {
|
|
|
3218
3568
|
];
|
|
3219
3569
|
});
|
|
3220
3570
|
|
|
3571
|
+
// Bridge entries are a flat list — this item owns the clientIds in
|
|
3572
|
+
// its own attachmentOrder.
|
|
3573
|
+
const itemExtraMedia = options?.extraMedia?.filter((m) =>
|
|
3574
|
+
item.attachmentOrder?.includes(m.clientId),
|
|
3575
|
+
);
|
|
3576
|
+
const media = [
|
|
3577
|
+
...JantComposeDialog._mergeRestoredMedia(
|
|
3578
|
+
item.mediaAttachments,
|
|
3579
|
+
itemExtraMedia,
|
|
3580
|
+
).values(),
|
|
3581
|
+
];
|
|
3582
|
+
|
|
3221
3583
|
editor.populate({
|
|
3222
3584
|
format: item.format,
|
|
3223
3585
|
title: item.title || undefined,
|
|
@@ -3225,6 +3587,9 @@ export class JantComposeDialog extends LitElement {
|
|
|
3225
3587
|
url: item.url || undefined,
|
|
3226
3588
|
quoteText: item.quoteText || undefined,
|
|
3227
3589
|
quoteAuthor: item.quoteAuthor || undefined,
|
|
3590
|
+
media: media.length
|
|
3591
|
+
? JantComposeDialog._restoredMediaToPopulate(media)
|
|
3592
|
+
: undefined,
|
|
3228
3593
|
textAttachments: textAttachments?.length
|
|
3229
3594
|
? textAttachments
|
|
3230
3595
|
: undefined,
|
|
@@ -3233,7 +3598,7 @@ export class JantComposeDialog extends LitElement {
|
|
|
3233
3598
|
}
|
|
3234
3599
|
|
|
3235
3600
|
this._draftRestored = true;
|
|
3236
|
-
showToast(this.labels.draftRestored);
|
|
3601
|
+
if (options?.notify !== false) showToast(this.labels.draftRestored);
|
|
3237
3602
|
globalThis.requestAnimationFrame(() => {
|
|
3238
3603
|
this._captureInitialSnapshot();
|
|
3239
3604
|
});
|
|
@@ -3258,6 +3623,13 @@ export class JantComposeDialog extends LitElement {
|
|
|
3258
3623
|
];
|
|
3259
3624
|
});
|
|
3260
3625
|
|
|
3626
|
+
const media = [
|
|
3627
|
+
...JantComposeDialog._mergeRestoredMedia(
|
|
3628
|
+
draft.mediaAttachments,
|
|
3629
|
+
options?.extraMedia,
|
|
3630
|
+
).values(),
|
|
3631
|
+
];
|
|
3632
|
+
|
|
3261
3633
|
this._editor?.populate({
|
|
3262
3634
|
format: draft.format,
|
|
3263
3635
|
title: draft.title || undefined,
|
|
@@ -3268,12 +3640,15 @@ export class JantComposeDialog extends LitElement {
|
|
|
3268
3640
|
rating: draft.rating || undefined,
|
|
3269
3641
|
showTitle: draft.showTitle,
|
|
3270
3642
|
showRating: draft.showRating,
|
|
3643
|
+
media: media.length
|
|
3644
|
+
? JantComposeDialog._restoredMediaToPopulate(media)
|
|
3645
|
+
: undefined,
|
|
3271
3646
|
textAttachments: textAttachments?.length ? textAttachments : undefined,
|
|
3272
3647
|
attachmentOrder: draft.attachmentOrder,
|
|
3273
3648
|
});
|
|
3274
3649
|
|
|
3275
3650
|
this._draftRestored = true;
|
|
3276
|
-
showToast(this.labels.draftRestored);
|
|
3651
|
+
if (options?.notify !== false) showToast(this.labels.draftRestored);
|
|
3277
3652
|
globalThis.requestAnimationFrame(() => {
|
|
3278
3653
|
this._captureInitialSnapshot();
|
|
3279
3654
|
});
|
|
@@ -3283,7 +3658,10 @@ export class JantComposeDialog extends LitElement {
|
|
|
3283
3658
|
* Check for a local edit draft for the given post ID and restore it if valid.
|
|
3284
3659
|
* Returns true if a draft was restored, false otherwise.
|
|
3285
3660
|
*/
|
|
3286
|
-
private _restoreEditDraftIfAvailable(
|
|
3661
|
+
private _restoreEditDraftIfAvailable(
|
|
3662
|
+
postId: string,
|
|
3663
|
+
notify?: boolean,
|
|
3664
|
+
): boolean {
|
|
3287
3665
|
const key = JantComposeDialog._EDIT_DRAFT_KEY_PREFIX + postId;
|
|
3288
3666
|
|
|
3289
3667
|
let raw: string | null;
|
|
@@ -3309,7 +3687,9 @@ export class JantComposeDialog extends LitElement {
|
|
|
3309
3687
|
|
|
3310
3688
|
// Restore metadata
|
|
3311
3689
|
this._format = draft.format;
|
|
3312
|
-
this._collectionIds =
|
|
3690
|
+
this._collectionIds = this._replyToId
|
|
3691
|
+
? []
|
|
3692
|
+
: [...(draft.collectionIds ?? [])];
|
|
3313
3693
|
this._slug = draft.slug ?? "";
|
|
3314
3694
|
this._publishedAtInput = draft.publishedAtInput ?? "";
|
|
3315
3695
|
this._publishedAtTimeMinutes = draft.publishedAtTimeMinutes ?? null;
|
|
@@ -3344,7 +3724,7 @@ export class JantComposeDialog extends LitElement {
|
|
|
3344
3724
|
});
|
|
3345
3725
|
|
|
3346
3726
|
this._draftRestored = true;
|
|
3347
|
-
showToast(this.labels.draftRestored);
|
|
3727
|
+
if (notify !== false) showToast(this.labels.draftRestored);
|
|
3348
3728
|
return true;
|
|
3349
3729
|
}
|
|
3350
3730
|
|
|
@@ -3426,9 +3806,12 @@ export class JantComposeDialog extends LitElement {
|
|
|
3426
3806
|
|
|
3427
3807
|
private _renderDraftItem(draft: DraftItem) {
|
|
3428
3808
|
const preview = this._getDraftPreview(draft);
|
|
3809
|
+
const formatLabel = this.labels[draft.format];
|
|
3810
|
+
const menuId = `draft-actions-${draft.id}`;
|
|
3811
|
+
const menuOpen = this._draftMenuOpenId === draft.id;
|
|
3429
3812
|
|
|
3430
3813
|
return html`
|
|
3431
|
-
<div class="compose-draft-item" @click=${() => this.
|
|
3814
|
+
<div class="compose-draft-item" @click=${() => this.openDraft(draft.id)}>
|
|
3432
3815
|
<div class="compose-draft-content">
|
|
3433
3816
|
${preview
|
|
3434
3817
|
? html`<div class="compose-draft-preview">${preview}</div>`
|
|
@@ -3438,11 +3821,13 @@ export class JantComposeDialog extends LitElement {
|
|
|
3438
3821
|
Empty draft
|
|
3439
3822
|
</div>`}
|
|
3440
3823
|
<div class="compose-draft-meta">
|
|
3824
|
+
<span class="compose-draft-format">${formatLabel}</span>
|
|
3825
|
+
<span aria-hidden="true">·</span>
|
|
3441
3826
|
${this._formatDraftDate(draft.updatedAt)}
|
|
3442
3827
|
</div>
|
|
3443
3828
|
</div>
|
|
3444
3829
|
<div class="relative">
|
|
3445
|
-
${
|
|
3830
|
+
${menuOpen
|
|
3446
3831
|
? html`<div
|
|
3447
3832
|
class="compose-dropdown-backdrop"
|
|
3448
3833
|
@click=${(e: Event) => {
|
|
@@ -3454,6 +3839,10 @@ export class JantComposeDialog extends LitElement {
|
|
|
3454
3839
|
<button
|
|
3455
3840
|
type="button"
|
|
3456
3841
|
class="compose-draft-more"
|
|
3842
|
+
aria-label=${this.labels.draftActions}
|
|
3843
|
+
aria-haspopup="menu"
|
|
3844
|
+
aria-expanded=${menuOpen ? "true" : "false"}
|
|
3845
|
+
aria-controls=${menuId}
|
|
3457
3846
|
@click=${(e: Event) => {
|
|
3458
3847
|
e.stopPropagation();
|
|
3459
3848
|
this._draftMenuOpenId =
|
|
@@ -3466,12 +3855,34 @@ export class JantComposeDialog extends LitElement {
|
|
|
3466
3855
|
<circle cx="12" cy="8" r="1.2" />
|
|
3467
3856
|
</svg>
|
|
3468
3857
|
</button>
|
|
3469
|
-
${
|
|
3858
|
+
${menuOpen
|
|
3470
3859
|
? html`
|
|
3471
|
-
<div
|
|
3860
|
+
<div
|
|
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>
|
|
3878
|
+
<div
|
|
3879
|
+
class="compose-dropdown-separator"
|
|
3880
|
+
role="separator"
|
|
3881
|
+
></div>
|
|
3472
3882
|
<button
|
|
3473
3883
|
type="button"
|
|
3474
3884
|
class="compose-dropdown-item compose-dropdown-item-danger"
|
|
3885
|
+
role="menuitem"
|
|
3475
3886
|
@click=${(e: Event) => {
|
|
3476
3887
|
e.stopPropagation();
|
|
3477
3888
|
this._deleteDraft(draft.id);
|
|
@@ -3592,12 +4003,6 @@ export class JantComposeDialog extends LitElement {
|
|
|
3592
4003
|
// already pending from loading the post.
|
|
3593
4004
|
this._cancelDraftSaveTimer();
|
|
3594
4005
|
this._format = target;
|
|
3595
|
-
// Sync the editor's format this tick. Lit commits the `.format` binding
|
|
3596
|
-
// only after this render returns, so `_canPublish()` (which reads
|
|
3597
|
-
// `editor.getData()`, keyed on the editor's format) would otherwise compute
|
|
3598
|
-
// against the stale format for one render and leave the submit button
|
|
3599
|
-
// wrongly disabled.
|
|
3600
|
-
if (editor) editor.format = target;
|
|
3601
4006
|
this._showPublishPanel = false;
|
|
3602
4007
|
if (this._shouldAutofocusFormatInput()) {
|
|
3603
4008
|
globalThis.requestAnimationFrame(() => this._editor?.focusInput());
|
|
@@ -3606,82 +4011,40 @@ export class JantComposeDialog extends LitElement {
|
|
|
3606
4011
|
|
|
3607
4012
|
// ── Render helpers ────────────────────────────────────────────────
|
|
3608
4013
|
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
|
|
3614
|
-
|
|
3615
|
-
|
|
3616
|
-
|
|
3617
|
-
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
|
|
3621
|
-
// header then shows a plain title instead of the format selector.
|
|
3622
|
-
const isReply = !!(this._replyToId && this._replyToData);
|
|
3623
|
-
const showTitle = isReply || this._threadItems.length > 0;
|
|
3624
|
-
const headerTitle = this._editPostId
|
|
3625
|
-
? this.labels.editTitle
|
|
3626
|
-
: isReply
|
|
3627
|
-
? this.labels.replyTitle
|
|
3628
|
-
: this.labels.newThread;
|
|
4014
|
+
/**
|
|
4015
|
+
* The way out, sitting at the end of the post header row where a thread post
|
|
4016
|
+
* keeps its own ×. Compose has no title bar to hang a Cancel off any more:
|
|
4017
|
+
* "New post" restated what an empty composer already says, and Publish
|
|
4018
|
+
* already reads "Update" when editing, so the row was carrying nothing but
|
|
4019
|
+
* two controls that belong closer to the work.
|
|
4020
|
+
*
|
|
4021
|
+
* A thread's posts each own that slot for removing themselves, so there the
|
|
4022
|
+
* exit lives in the options panel instead — see `_renderCloseComposeRow`.
|
|
4023
|
+
*/
|
|
4024
|
+
private _renderCloseComposeControl() {
|
|
4025
|
+
if (this.pageMode || this._threadItems.length > 0) return nothing;
|
|
3629
4026
|
|
|
3630
4027
|
return html`
|
|
3631
|
-
<
|
|
3632
|
-
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
|
|
4028
|
+
<button
|
|
4029
|
+
type="button"
|
|
4030
|
+
class="compose-close-btn"
|
|
4031
|
+
aria-label=${this.labels.cancel}
|
|
4032
|
+
title=${this.labels.cancel}
|
|
4033
|
+
@click=${() => this.requestClose()}
|
|
4034
|
+
>
|
|
4035
|
+
<svg
|
|
4036
|
+
width="14"
|
|
4037
|
+
height="14"
|
|
4038
|
+
viewBox="0 0 16 16"
|
|
4039
|
+
fill="none"
|
|
4040
|
+
stroke="currentColor"
|
|
4041
|
+
stroke-width="1.8"
|
|
4042
|
+
stroke-linecap="round"
|
|
4043
|
+
aria-hidden="true"
|
|
3636
4044
|
>
|
|
3637
|
-
|
|
3638
|
-
</
|
|
3639
|
-
|
|
3640
|
-
<div class="compose-dialog-header-center">
|
|
3641
|
-
${showTitle
|
|
3642
|
-
? html`<span class="compose-dialog-title">${headerTitle}</span>`
|
|
3643
|
-
: html`
|
|
3644
|
-
<div class="compose-segmented">
|
|
3645
|
-
<div
|
|
3646
|
-
class=${classMap({
|
|
3647
|
-
"compose-format-pill": true,
|
|
3648
|
-
"compose-format-pill-link": this._format === "link",
|
|
3649
|
-
"compose-format-pill-quote": this._format === "quote",
|
|
3650
|
-
})}
|
|
3651
|
-
></div>
|
|
3652
|
-
${formats.map(
|
|
3653
|
-
(f) => html`
|
|
3654
|
-
<button
|
|
3655
|
-
type="button"
|
|
3656
|
-
class=${classMap({
|
|
3657
|
-
"compose-segmented-item": true,
|
|
3658
|
-
"compose-segmented-item-active": this._format === f,
|
|
3659
|
-
})}
|
|
3660
|
-
@click=${() => this._switchFormat(f)}
|
|
3661
|
-
>
|
|
3662
|
-
${formatLabels[f]}
|
|
3663
|
-
</button>
|
|
3664
|
-
`,
|
|
3665
|
-
)}
|
|
3666
|
-
</div>
|
|
3667
|
-
`}
|
|
3668
|
-
</div>
|
|
3669
|
-
|
|
3670
|
-
<div class="compose-dialog-header-actions">
|
|
3671
|
-
${this._editPostId
|
|
3672
|
-
? nothing
|
|
3673
|
-
: html`<button
|
|
3674
|
-
type="button"
|
|
3675
|
-
class="compose-dialog-header-btn compose-dialog-draft-btn"
|
|
3676
|
-
aria-label=${draftButtonLabel}
|
|
3677
|
-
title=${draftButtonLabel}
|
|
3678
|
-
?disabled=${this._loading}
|
|
3679
|
-
@click=${() => this._handleDraftButtonClick()}
|
|
3680
|
-
>
|
|
3681
|
-
${renderComposeHeaderIcon(COMPOSE_DIALOG_HEADER_ICONS.drafts)}
|
|
3682
|
-
</button>`}
|
|
3683
|
-
</div>
|
|
3684
|
-
</header>
|
|
4045
|
+
<path d="M4.5 4.5 11.5 11.5M11.5 4.5 4.5 11.5" />
|
|
4046
|
+
</svg>
|
|
4047
|
+
</button>
|
|
3685
4048
|
`;
|
|
3686
4049
|
}
|
|
3687
4050
|
|
|
@@ -3738,17 +4101,13 @@ export class JantComposeDialog extends LitElement {
|
|
|
3738
4101
|
}
|
|
3739
4102
|
}}
|
|
3740
4103
|
>
|
|
3741
|
-
|
|
3742
|
-
|
|
3743
|
-
COMPOSE_COLLECTION_PICKER_ICONS.collection,
|
|
3744
|
-
"compose-collection-trigger-svg",
|
|
3745
|
-
)}
|
|
3746
|
-
</span>
|
|
3747
|
-
<span class="compose-collection-label">${selectedLabel}</span>
|
|
4104
|
+
<!-- No chevron: the glyph already says "picker", and a second one
|
|
4105
|
+
pointing down only made the control wider. -->
|
|
3748
4106
|
${renderComposeCollectionPickerIcon(
|
|
3749
|
-
COMPOSE_COLLECTION_PICKER_ICONS.
|
|
3750
|
-
"compose-collection-
|
|
4107
|
+
COMPOSE_COLLECTION_PICKER_ICONS.collection,
|
|
4108
|
+
"compose-collection-trigger-svg",
|
|
3751
4109
|
)}
|
|
4110
|
+
<span class="compose-collection-label">${selectedLabel}</span>
|
|
3752
4111
|
</button>
|
|
3753
4112
|
<div
|
|
3754
4113
|
class="compose-collection-popover"
|
|
@@ -4318,56 +4677,24 @@ export class JantComposeDialog extends LitElement {
|
|
|
4318
4677
|
};
|
|
4319
4678
|
}
|
|
4320
4679
|
|
|
4321
|
-
private
|
|
4322
|
-
|
|
4323
|
-
|
|
4324
|
-
|
|
4325
|
-
|
|
4326
|
-
|
|
4327
|
-
|
|
4328
|
-
if (this._editPostId || this._draftSourceId) {
|
|
4329
|
-
if (currentSlug === this._initialSlug) {
|
|
4330
|
-
return null;
|
|
4331
|
-
}
|
|
4332
|
-
|
|
4333
|
-
if (!currentSlug) {
|
|
4334
|
-
return {
|
|
4335
|
-
kind: "slug",
|
|
4336
|
-
text: this.labels.publishSlugSummaryAuto,
|
|
4337
|
-
actionLabel: this.labels.publishSlugSummaryAction,
|
|
4338
|
-
value: currentSlug,
|
|
4339
|
-
};
|
|
4340
|
-
}
|
|
4341
|
-
}
|
|
4342
|
-
|
|
4343
|
-
if (!currentSlug) return null;
|
|
4344
|
-
|
|
4345
|
-
return {
|
|
4346
|
-
kind: "slug",
|
|
4347
|
-
text: `/${currentSlug}`,
|
|
4348
|
-
actionLabel: this.labels.publishSlugSummaryAction,
|
|
4349
|
-
value: currentSlug,
|
|
4350
|
-
};
|
|
4351
|
-
}
|
|
4352
|
-
|
|
4353
|
-
private _getPublishSummaryChips(): ComposePublishSummaryChip[] {
|
|
4354
|
-
const chips: ComposePublishSummaryChip[] = [];
|
|
4355
|
-
const publishedAtSummary = this._getPublishedAtSummary();
|
|
4356
|
-
if (publishedAtSummary !== null) {
|
|
4357
|
-
chips.push({
|
|
4358
|
-
kind: "publishedAt",
|
|
4359
|
-
text: publishedAtSummary.text,
|
|
4360
|
-
actionLabel: this.labels.publishDateSummaryAction,
|
|
4361
|
-
value: publishedAtSummary.input,
|
|
4362
|
-
});
|
|
4363
|
-
}
|
|
4680
|
+
private _getPostPublishedAtSubmitValue(
|
|
4681
|
+
index: number,
|
|
4682
|
+
status: "published" | "draft",
|
|
4683
|
+
): number | undefined {
|
|
4684
|
+
if (index === 0) return this._getPublishedAtSubmitValue(status);
|
|
4685
|
+
if (status === "draft") return undefined;
|
|
4364
4686
|
|
|
4365
|
-
const
|
|
4366
|
-
|
|
4367
|
-
|
|
4368
|
-
}
|
|
4687
|
+
const item = this._threadItems[index];
|
|
4688
|
+
const input = item?.publishedAtInput ?? "";
|
|
4689
|
+
if (!input.trim()) return undefined;
|
|
4369
4690
|
|
|
4370
|
-
|
|
4691
|
+
const timestamp = buildTimestampFromLocalDate(
|
|
4692
|
+
input,
|
|
4693
|
+
item?.publishedAtTimeMinutes ??
|
|
4694
|
+
getTimestampTimeMinutes(this._getCurrentTimestamp()),
|
|
4695
|
+
);
|
|
4696
|
+
if (timestamp === null) return undefined;
|
|
4697
|
+
return Math.min(timestamp, this._getCurrentTimestamp());
|
|
4371
4698
|
}
|
|
4372
4699
|
|
|
4373
4700
|
private _getPublishedAtSubmitValue(
|
|
@@ -4403,67 +4730,34 @@ export class JantComposeDialog extends LitElement {
|
|
|
4403
4730
|
return undefined;
|
|
4404
4731
|
}
|
|
4405
4732
|
|
|
4406
|
-
private _openPublishPanelAndFocus(selector: string) {
|
|
4733
|
+
private _openPublishPanelAndFocus(selector: string, index = 0) {
|
|
4407
4734
|
this._showCollection = false;
|
|
4408
4735
|
this._collectionSearch = "";
|
|
4409
|
-
this._showPublishPanel =
|
|
4736
|
+
this._showPublishPanel = false;
|
|
4737
|
+
this._postMetaIndex = index;
|
|
4410
4738
|
this._confirmPanelOpen = false;
|
|
4411
4739
|
this._scheduleSuggestedSlugRefresh(true);
|
|
4412
|
-
this.
|
|
4413
|
-
this.querySelector<HTMLElement>(selector)?.focus();
|
|
4414
|
-
});
|
|
4740
|
+
this._placeAndFocusPostMetaPanel(selector);
|
|
4415
4741
|
}
|
|
4416
4742
|
|
|
4417
|
-
private _revealSlugField() {
|
|
4418
|
-
this._openPublishPanelAndFocus(".compose-publish-slug-input");
|
|
4743
|
+
private _revealSlugField(index = 0) {
|
|
4744
|
+
this._openPublishPanelAndFocus(".compose-publish-slug-input", index);
|
|
4419
4745
|
}
|
|
4420
4746
|
|
|
4421
|
-
private _revealPublishedAtField() {
|
|
4422
|
-
this._openPublishPanelAndFocus(".compose-publish-date-input");
|
|
4747
|
+
private _revealPublishedAtField(index = 0) {
|
|
4748
|
+
this._openPublishPanelAndFocus(".compose-publish-date-input", index);
|
|
4423
4749
|
}
|
|
4424
4750
|
|
|
4751
|
+
/** Every row ready, and every row's own date and permalink accepted. */
|
|
4425
4752
|
private _canPublish(): boolean {
|
|
4426
4753
|
if (this._loading) return false;
|
|
4427
|
-
|
|
4428
|
-
|
|
4429
|
-
|
|
4430
|
-
|
|
4431
|
-
// Thread mode: validate all editors
|
|
4432
|
-
const editors = Array.from(
|
|
4433
|
-
this.querySelectorAll<JantComposeEditor>("jant-compose-editor"),
|
|
4434
|
-
);
|
|
4435
|
-
if (editors.length === 0) return false;
|
|
4436
|
-
for (const editor of editors) {
|
|
4437
|
-
if (editor.getUrlValidationMessage()) return false;
|
|
4438
|
-
if (editor.getLinkTitleValidationMessage()) return false;
|
|
4439
|
-
}
|
|
4440
|
-
// Every editor in the thread must have content
|
|
4441
|
-
return editors.every((editor) => {
|
|
4442
|
-
const data = editor.getData();
|
|
4443
|
-
return (
|
|
4444
|
-
!!data.body ||
|
|
4445
|
-
!!data.title.trim() ||
|
|
4446
|
-
!!data.url.trim() ||
|
|
4447
|
-
!!data.quoteText.trim() ||
|
|
4448
|
-
data.attachments.length > 0
|
|
4449
|
-
);
|
|
4450
|
-
});
|
|
4754
|
+
const rowIds = this._rowIds;
|
|
4755
|
+
for (let i = 0; i < rowIds.length; i++) {
|
|
4756
|
+
if (this._getPostPublishedAtValidationMessage(i)) return false;
|
|
4757
|
+
if (this._getPostSlugValidationMessage(i)) return false;
|
|
4451
4758
|
}
|
|
4452
|
-
|
|
4453
|
-
|
|
4454
|
-
if (!editor) return false;
|
|
4455
|
-
if (editor.getUrlValidationMessage()) return false;
|
|
4456
|
-
if (editor.getLinkTitleValidationMessage()) return false;
|
|
4457
|
-
|
|
4458
|
-
const data = editor.getData();
|
|
4459
|
-
if (this._format === "link") {
|
|
4460
|
-
return data.url.trim().length > 0;
|
|
4461
|
-
}
|
|
4462
|
-
if (this._format === "quote") {
|
|
4463
|
-
return data.quoteText.trim().length > 0;
|
|
4464
|
-
}
|
|
4465
|
-
return this._hasContent();
|
|
4466
|
-
}
|
|
4759
|
+
return rowIds.every((id) => this._rowStatus.get(id)?.publishable === true);
|
|
4760
|
+
}
|
|
4467
4761
|
|
|
4468
4762
|
private _focusPublishPanelInitialField() {
|
|
4469
4763
|
const selector = this._visibilityLocked
|
|
@@ -4494,6 +4788,9 @@ export class JantComposeDialog extends LitElement {
|
|
|
4494
4788
|
private _setVisibility(visibility: ComposeVisibility) {
|
|
4495
4789
|
if (this._visibilityLocked) return;
|
|
4496
4790
|
this._visibility = visibility;
|
|
4791
|
+
// Choosing is the whole job of the panel, so dismiss it. Nothing here
|
|
4792
|
+
// needs confirming — a radio selection is not a form.
|
|
4793
|
+
if (this._showPublishPanel) this._closePublishPanel(true);
|
|
4497
4794
|
if (!this._editPostId && !this._draftSourceId && !this._replyToId) {
|
|
4498
4795
|
JantComposeDialog._lastNewPostVisibility = visibility;
|
|
4499
4796
|
if (this._sourceCollectionId) {
|
|
@@ -4505,26 +4802,28 @@ export class JantComposeDialog extends LitElement {
|
|
|
4505
4802
|
}
|
|
4506
4803
|
}
|
|
4507
4804
|
|
|
4508
|
-
private _onSlugInput(e: Event) {
|
|
4805
|
+
private _onSlugInput(e: Event, index = 0) {
|
|
4509
4806
|
const value = (e.target as HTMLInputElement).value;
|
|
4510
|
-
this.
|
|
4511
|
-
this.
|
|
4807
|
+
this._setPostSlug(index, value.toLowerCase());
|
|
4808
|
+
this._setPostSlugTaken(index, false);
|
|
4512
4809
|
this._slugCheckLoading = false;
|
|
4513
|
-
if (this.
|
|
4514
|
-
this._scheduleSlugAvailabilityCheck();
|
|
4810
|
+
if (this._hasPostManualSlug(index)) {
|
|
4811
|
+
this._scheduleSlugAvailabilityCheck(index);
|
|
4515
4812
|
return;
|
|
4516
4813
|
}
|
|
4517
4814
|
this._scheduleSuggestedSlugRefresh();
|
|
4518
4815
|
}
|
|
4519
4816
|
|
|
4520
|
-
private
|
|
4521
|
-
this._publishedAtInput = (e.target as HTMLInputElement).value;
|
|
4522
|
-
}
|
|
4523
|
-
|
|
4524
|
-
private _resetPublishedAt() {
|
|
4817
|
+
private _resetPublishedAt(index = 0) {
|
|
4525
4818
|
const currentTimestamp = this._getCurrentTimestamp();
|
|
4526
|
-
this.
|
|
4527
|
-
|
|
4819
|
+
this._setPostPublishedAtInput(
|
|
4820
|
+
index,
|
|
4821
|
+
toLocalDateInputValue(currentTimestamp),
|
|
4822
|
+
);
|
|
4823
|
+
this._setPostPublishedAtTimeMinutes(
|
|
4824
|
+
index,
|
|
4825
|
+
getTimestampTimeMinutes(currentTimestamp),
|
|
4826
|
+
);
|
|
4528
4827
|
this.updateComplete.then(() => {
|
|
4529
4828
|
this.querySelector<HTMLInputElement>(
|
|
4530
4829
|
".compose-publish-date-input",
|
|
@@ -4550,9 +4849,15 @@ export class JantComposeDialog extends LitElement {
|
|
|
4550
4849
|
);
|
|
4551
4850
|
}
|
|
4552
4851
|
|
|
4553
|
-
|
|
4852
|
+
/**
|
|
4853
|
+
* One choice in the options sheet: title, one-line explanation, and a check
|
|
4854
|
+
* on the selected row. Every option carries its own hint so they can be
|
|
4855
|
+
* compared before choosing, which a single hint under a chip group can't do.
|
|
4856
|
+
*/
|
|
4857
|
+
private _renderVisibilityRow(
|
|
4554
4858
|
visibility: ComposeVisibility,
|
|
4555
4859
|
label: string,
|
|
4860
|
+
hint: string,
|
|
4556
4861
|
) {
|
|
4557
4862
|
const selected = this._visibility === visibility;
|
|
4558
4863
|
|
|
@@ -4560,8 +4865,8 @@ export class JantComposeDialog extends LitElement {
|
|
|
4560
4865
|
<button
|
|
4561
4866
|
type="button"
|
|
4562
4867
|
class=${classMap({
|
|
4563
|
-
"compose-
|
|
4564
|
-
"compose-
|
|
4868
|
+
"compose-sheet-row": true,
|
|
4869
|
+
"compose-sheet-row-selected": selected,
|
|
4565
4870
|
})}
|
|
4566
4871
|
role="radio"
|
|
4567
4872
|
aria-checked=${selected ? "true" : "false"}
|
|
@@ -4569,168 +4874,212 @@ export class JantComposeDialog extends LitElement {
|
|
|
4569
4874
|
@click=${() => this._setVisibility(visibility)}
|
|
4570
4875
|
>
|
|
4571
4876
|
${this._renderVisibilityIcon(visibility)}
|
|
4572
|
-
<span class="compose-
|
|
4877
|
+
<span class="compose-sheet-main">
|
|
4878
|
+
<span class="compose-sheet-title">${label}</span>
|
|
4879
|
+
<span class="compose-sheet-sub">${hint}</span>
|
|
4880
|
+
</span>
|
|
4881
|
+
${selected
|
|
4882
|
+
? html`<span class="compose-sheet-check" aria-hidden="true">
|
|
4883
|
+
${renderComposePublishActionIcon(
|
|
4884
|
+
COMPOSE_PUBLISH_ACTION_ICONS.check,
|
|
4885
|
+
"compose-sheet-check-icon",
|
|
4886
|
+
)}
|
|
4887
|
+
</span>`
|
|
4888
|
+
: nothing}
|
|
4573
4889
|
</button>
|
|
4574
4890
|
`;
|
|
4575
4891
|
}
|
|
4576
4892
|
|
|
4577
|
-
|
|
4578
|
-
|
|
4579
|
-
|
|
4580
|
-
|
|
4581
|
-
|
|
4582
|
-
|
|
4583
|
-
|
|
4584
|
-
|
|
4893
|
+
/**
|
|
4894
|
+
* The label above a field, with its reset on the right. The reset only shows
|
|
4895
|
+
* once there is something to reset, so the default state is a bare label.
|
|
4896
|
+
*/
|
|
4897
|
+
private _renderMetaFieldHead(label: string, reset: unknown) {
|
|
4898
|
+
return html`
|
|
4899
|
+
<div class="compose-meta-field-head">
|
|
4900
|
+
<span class="compose-meta-label">${label}</span>
|
|
4901
|
+
${reset}
|
|
4902
|
+
</div>
|
|
4903
|
+
`;
|
|
4904
|
+
}
|
|
4905
|
+
|
|
4906
|
+
/**
|
|
4907
|
+
* Chrome opens the calendar only from its own indicator glyph — a ~13px
|
|
4908
|
+
* target that reads as a browser default next to everything else in this
|
|
4909
|
+
* panel, and that other engines place differently or not at all. The
|
|
4910
|
+
* indicator is hidden in CSS and this button drives the same picker through
|
|
4911
|
+
* `showPicker()`, so there is one target, it is ours, and it is the height of
|
|
4912
|
+
* the field. Typing a date into the field still works either way, which is
|
|
4913
|
+
* why the button is the only thing that opens the picker: taking over the
|
|
4914
|
+
* whole field would put the calendar in front of anyone reaching for the
|
|
4915
|
+
* keyboard.
|
|
4916
|
+
*/
|
|
4917
|
+
private _openDatePicker(index: number) {
|
|
4918
|
+
const fields = this.querySelectorAll<HTMLInputElement>(
|
|
4919
|
+
".compose-publish-date-input",
|
|
4920
|
+
);
|
|
4921
|
+
// One panel is open at a time, so the panel's own field is the only one in
|
|
4922
|
+
// the DOM; `index` is kept for the thread case if that ever changes.
|
|
4923
|
+
const input = fields[index] ?? fields[0];
|
|
4924
|
+
if (!input) return;
|
|
4925
|
+
|
|
4926
|
+
input.focus();
|
|
4927
|
+
try {
|
|
4928
|
+
input.showPicker();
|
|
4929
|
+
} catch {
|
|
4930
|
+
// Older engines, or a browser that declined: the field is still typable.
|
|
4585
4931
|
}
|
|
4586
4932
|
}
|
|
4587
4933
|
|
|
4588
|
-
private _renderPublishDateSection() {
|
|
4589
|
-
const publishedAtError = this.
|
|
4934
|
+
private _renderPublishDateSection(index: number) {
|
|
4935
|
+
const publishedAtError = this._getPostPublishedAtValidationMessage(index);
|
|
4936
|
+
const hasValue = this._hasPostPublishedAtValue(index);
|
|
4590
4937
|
|
|
4591
4938
|
return html`
|
|
4592
|
-
<
|
|
4593
|
-
|
|
4594
|
-
|
|
4595
|
-
|
|
4596
|
-
|
|
4597
|
-
|
|
4598
|
-
|
|
4599
|
-
|
|
4600
|
-
</p>
|
|
4601
|
-
</div>
|
|
4602
|
-
${this._hasPublishedAtValue()
|
|
4603
|
-
? html`
|
|
4604
|
-
<button
|
|
4605
|
-
type="button"
|
|
4606
|
-
class="compose-publish-section-action"
|
|
4607
|
-
@click=${() => this._resetPublishedAt()}
|
|
4608
|
-
>
|
|
4609
|
-
${this.labels.publishDateReset}
|
|
4610
|
-
</button>
|
|
4611
|
-
`
|
|
4612
|
-
: nothing}
|
|
4613
|
-
</div>
|
|
4614
|
-
<div class="compose-publish-date-field">
|
|
4615
|
-
<div class="compose-publish-date-input-wrap">
|
|
4616
|
-
<input
|
|
4617
|
-
type="date"
|
|
4618
|
-
class="compose-input compose-publish-date-input"
|
|
4619
|
-
.value=${this._publishedAtInput}
|
|
4620
|
-
max=${this._getPublishedAtMaxInput()}
|
|
4621
|
-
aria-invalid=${publishedAtError ? "true" : "false"}
|
|
4622
|
-
@input=${(e: Event) => this._onPublishedAtInput(e)}
|
|
4623
|
-
/>
|
|
4624
|
-
</div>
|
|
4625
|
-
${publishedAtError
|
|
4626
|
-
? html`<p
|
|
4627
|
-
class=${classMap({
|
|
4628
|
-
"compose-publish-date-status": true,
|
|
4629
|
-
"compose-publish-date-status-error": true,
|
|
4630
|
-
})}
|
|
4939
|
+
<div class="compose-meta-field compose-publish-date-field">
|
|
4940
|
+
${this._renderMetaFieldHead(
|
|
4941
|
+
this.labels.publishDateLabel,
|
|
4942
|
+
hasValue
|
|
4943
|
+
? html`<button
|
|
4944
|
+
type="button"
|
|
4945
|
+
class="compose-publish-section-action"
|
|
4946
|
+
@click=${() => this._resetPublishedAt(index)}
|
|
4631
4947
|
>
|
|
4632
|
-
${
|
|
4633
|
-
</
|
|
4634
|
-
: nothing
|
|
4948
|
+
${this.labels.publishDateReset}
|
|
4949
|
+
</button>`
|
|
4950
|
+
: nothing,
|
|
4951
|
+
)}
|
|
4952
|
+
<div class="compose-publish-date-input-wrap">
|
|
4953
|
+
<input
|
|
4954
|
+
type="date"
|
|
4955
|
+
class=${classMap({
|
|
4956
|
+
"compose-input": true,
|
|
4957
|
+
"compose-publish-date-input": true,
|
|
4958
|
+
// The browser draws `yyyy-mm-dd` in the value's own colour, so an
|
|
4959
|
+
// empty field reads as a date that has been set. Dim it instead.
|
|
4960
|
+
"compose-publish-date-input-empty": !hasValue,
|
|
4961
|
+
})}
|
|
4962
|
+
.value=${this._getPostPublishedAtInput(index)}
|
|
4963
|
+
max=${this._getPublishedAtMaxInput()}
|
|
4964
|
+
aria-label=${this.labels.publishDateLabel}
|
|
4965
|
+
aria-invalid=${publishedAtError ? "true" : "false"}
|
|
4966
|
+
@input=${(e: Event) =>
|
|
4967
|
+
this._setPostPublishedAtInput(
|
|
4968
|
+
index,
|
|
4969
|
+
(e.target as HTMLInputElement).value,
|
|
4970
|
+
)}
|
|
4971
|
+
/>
|
|
4972
|
+
<button
|
|
4973
|
+
type="button"
|
|
4974
|
+
class="compose-publish-date-picker"
|
|
4975
|
+
aria-label=${this.labels.publishDateSummaryAction}
|
|
4976
|
+
data-compose-date-picker
|
|
4977
|
+
@click=${() => this._openDatePicker(index)}
|
|
4978
|
+
>
|
|
4979
|
+
${renderComposePublishActionIcon(
|
|
4980
|
+
COMPOSE_PUBLISH_ACTION_ICONS.calendar,
|
|
4981
|
+
"compose-publish-date-picker-icon",
|
|
4982
|
+
)}
|
|
4983
|
+
</button>
|
|
4635
4984
|
</div>
|
|
4636
|
-
|
|
4985
|
+
${publishedAtError
|
|
4986
|
+
? html`<p
|
|
4987
|
+
class="compose-publish-date-status compose-publish-date-status-error"
|
|
4988
|
+
>
|
|
4989
|
+
${publishedAtError}
|
|
4990
|
+
</p>`
|
|
4991
|
+
: hasValue
|
|
4992
|
+
? nothing
|
|
4993
|
+
: html`<p class="compose-sheet-hint">
|
|
4994
|
+
${this.labels.publishDateHint}
|
|
4995
|
+
</p>`}
|
|
4996
|
+
</div>
|
|
4637
4997
|
`;
|
|
4638
4998
|
}
|
|
4639
4999
|
|
|
4640
|
-
private _renderPublishSlugSection() {
|
|
4641
|
-
const
|
|
4642
|
-
const
|
|
4643
|
-
const
|
|
5000
|
+
private _renderPublishSlugSection(index: number) {
|
|
5001
|
+
const isRoot = index === 0;
|
|
5002
|
+
const slugError = this._getPostSlugValidationMessage(index);
|
|
5003
|
+
const slugStatus = isRoot ? this._getSlugStatusMessage() : slugError;
|
|
5004
|
+
const slugPreview = isRoot ? this._getSlugPreviewParts() : null;
|
|
5005
|
+
// Suggestions come from the title, which replies do not have.
|
|
4644
5006
|
const showSuggestion =
|
|
5007
|
+
isRoot &&
|
|
4645
5008
|
!this._hasManualSlug() &&
|
|
4646
5009
|
!this._suggestedSlugLoading &&
|
|
4647
5010
|
Boolean(this._suggestedSlug);
|
|
4648
5011
|
|
|
4649
5012
|
return html`
|
|
4650
|
-
<
|
|
4651
|
-
|
|
4652
|
-
|
|
4653
|
-
|
|
4654
|
-
|
|
4655
|
-
|
|
4656
|
-
|
|
4657
|
-
|
|
4658
|
-
|
|
4659
|
-
|
|
4660
|
-
|
|
4661
|
-
|
|
4662
|
-
|
|
4663
|
-
|
|
4664
|
-
|
|
4665
|
-
|
|
4666
|
-
|
|
4667
|
-
|
|
4668
|
-
|
|
4669
|
-
|
|
4670
|
-
|
|
5013
|
+
<div class="compose-meta-field compose-publish-slug-field">
|
|
5014
|
+
${this._renderMetaFieldHead(
|
|
5015
|
+
this.labels.publishSlugLabel,
|
|
5016
|
+
this._hasManualSlug()
|
|
5017
|
+
? html`<button
|
|
5018
|
+
type="button"
|
|
5019
|
+
class="compose-publish-section-action"
|
|
5020
|
+
@click=${() => this._resetCustomSlug()}
|
|
5021
|
+
>
|
|
5022
|
+
${this.labels.publishSlugReset}
|
|
5023
|
+
</button>`
|
|
5024
|
+
: nothing,
|
|
5025
|
+
)}
|
|
5026
|
+
<div class="compose-publish-slug-input-wrap">
|
|
5027
|
+
<span class="compose-publish-slug-prefix" aria-hidden="true">/</span>
|
|
5028
|
+
<input
|
|
5029
|
+
type="text"
|
|
5030
|
+
class="compose-input compose-publish-slug-input"
|
|
5031
|
+
.value=${this._getPostSlug(index)}
|
|
5032
|
+
placeholder=${this.labels.publishSlugPlaceholder}
|
|
5033
|
+
aria-label=${this.labels.publishSlugLabel}
|
|
5034
|
+
aria-invalid=${slugError ? "true" : "false"}
|
|
5035
|
+
spellcheck="false"
|
|
5036
|
+
autocapitalize="off"
|
|
5037
|
+
autocomplete="off"
|
|
5038
|
+
@input=${(e: Event) => this._onSlugInput(e, index)}
|
|
5039
|
+
/>
|
|
4671
5040
|
</div>
|
|
4672
|
-
|
|
4673
|
-
|
|
4674
|
-
|
|
4675
|
-
|
|
4676
|
-
|
|
4677
|
-
|
|
4678
|
-
.value=${this._slug}
|
|
4679
|
-
placeholder=${this.labels.publishSlugPlaceholder}
|
|
4680
|
-
aria-invalid=${slugError ? "true" : "false"}
|
|
4681
|
-
spellcheck="false"
|
|
4682
|
-
autocapitalize="off"
|
|
4683
|
-
autocomplete="off"
|
|
4684
|
-
@input=${(e: Event) => this._onSlugInput(e)}
|
|
4685
|
-
/>
|
|
4686
|
-
</div>
|
|
4687
|
-
${showSuggestion
|
|
4688
|
-
? html`
|
|
4689
|
-
<button
|
|
4690
|
-
type="button"
|
|
4691
|
-
class="compose-slug-suggestion"
|
|
4692
|
-
@click=${() => this._useSuggestedSlug()}
|
|
4693
|
-
>
|
|
4694
|
-
<span class="compose-slug-suggestion-copy">
|
|
4695
|
-
<span class="compose-slug-suggestion-label"
|
|
4696
|
-
>${this.labels.publishSlugSuggested}</span
|
|
4697
|
-
>
|
|
4698
|
-
<span class="compose-slug-suggestion-chip">
|
|
4699
|
-
<span class="compose-slug-suggestion-value"
|
|
4700
|
-
>/${this._suggestedSlug}</span
|
|
4701
|
-
>
|
|
4702
|
-
</span>
|
|
4703
|
-
</span>
|
|
4704
|
-
<span class="compose-slug-suggestion-icon" aria-hidden="true">
|
|
4705
|
-
<svg
|
|
4706
|
-
width="14"
|
|
4707
|
-
height="14"
|
|
4708
|
-
viewBox="0 0 14 14"
|
|
4709
|
-
fill="none"
|
|
4710
|
-
stroke="currentColor"
|
|
4711
|
-
stroke-width="1.4"
|
|
4712
|
-
stroke-linecap="round"
|
|
4713
|
-
stroke-linejoin="round"
|
|
4714
|
-
>
|
|
4715
|
-
<path d="M3 7h8" />
|
|
4716
|
-
<path d="m8 3 4 4-4 4" />
|
|
4717
|
-
</svg>
|
|
4718
|
-
</span>
|
|
4719
|
-
</button>
|
|
4720
|
-
`
|
|
4721
|
-
: nothing}
|
|
4722
|
-
${slugStatus
|
|
4723
|
-
? html`<p
|
|
4724
|
-
class=${classMap({
|
|
4725
|
-
"compose-publish-slug-status": true,
|
|
4726
|
-
"compose-publish-slug-status-error": Boolean(slugError),
|
|
4727
|
-
})}
|
|
4728
|
-
data-compose-slug-error=${slugError ? "true" : nothing}
|
|
5041
|
+
${showSuggestion
|
|
5042
|
+
? html`
|
|
5043
|
+
<button
|
|
5044
|
+
type="button"
|
|
5045
|
+
class="compose-slug-suggestion"
|
|
5046
|
+
@click=${() => this._useSuggestedSlug()}
|
|
4729
5047
|
>
|
|
4730
|
-
|
|
4731
|
-
|
|
4732
|
-
|
|
4733
|
-
|
|
5048
|
+
<span class="compose-slug-suggestion-label"
|
|
5049
|
+
>${this.labels.publishSlugSuggested}</span
|
|
5050
|
+
>
|
|
5051
|
+
<span class="compose-slug-suggestion-value"
|
|
5052
|
+
>/${this._suggestedSlug}</span
|
|
5053
|
+
>
|
|
5054
|
+
<span class="compose-slug-suggestion-icon" aria-hidden="true">
|
|
5055
|
+
<svg
|
|
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"
|
|
5064
|
+
>
|
|
5065
|
+
<path d="M3 7h8" />
|
|
5066
|
+
<path d="m8 3 4 4-4 4" />
|
|
5067
|
+
</svg>
|
|
5068
|
+
</span>
|
|
5069
|
+
</button>
|
|
5070
|
+
`
|
|
5071
|
+
: nothing}
|
|
5072
|
+
${slugStatus
|
|
5073
|
+
? html`<p
|
|
5074
|
+
class=${classMap({
|
|
5075
|
+
"compose-publish-slug-status": true,
|
|
5076
|
+
"compose-publish-slug-status-error": Boolean(slugError),
|
|
5077
|
+
})}
|
|
5078
|
+
data-compose-slug-error=${slugError ? "true" : nothing}
|
|
5079
|
+
>
|
|
5080
|
+
${slugStatus}
|
|
5081
|
+
</p>`
|
|
5082
|
+
: slugPreview
|
|
4734
5083
|
? html`<p
|
|
4735
5084
|
class="compose-publish-slug-preview"
|
|
4736
5085
|
data-compose-slug-preview
|
|
@@ -4742,79 +5091,576 @@ export class JantComposeDialog extends LitElement {
|
|
|
4742
5091
|
>${slugPreview.path}</span
|
|
4743
5092
|
>
|
|
4744
5093
|
</p>`
|
|
4745
|
-
:
|
|
4746
|
-
|
|
4747
|
-
|
|
5094
|
+
: this._hasPostManualSlug(index)
|
|
5095
|
+
? nothing
|
|
5096
|
+
: html`<p class="compose-sheet-hint">
|
|
5097
|
+
${this.labels.publishSlugHint}
|
|
5098
|
+
</p>`}
|
|
5099
|
+
</div>
|
|
4748
5100
|
`;
|
|
4749
5101
|
}
|
|
4750
5102
|
|
|
5103
|
+
/**
|
|
5104
|
+
* Replying quietly is an instruction for the moment a reply is written: the
|
|
5105
|
+
* server's only job is to skip the thread-activity bump it would otherwise
|
|
5106
|
+
* do on create. Saving an existing post goes through the update path, which
|
|
5107
|
+
* has no such bump to skip and no way to carry the intent — so the switch is
|
|
5108
|
+
* not offered there rather than sitting dead. A thread draft is the one
|
|
5109
|
+
* exception: saving it deletes and recreates every post, so the flag lands.
|
|
5110
|
+
*/
|
|
5111
|
+
private _canReplyQuietly(): boolean {
|
|
5112
|
+
if (!this._replyToId) return false;
|
|
5113
|
+
const editsExistingPost = !!(this._editPostId || this._draftSourceId);
|
|
5114
|
+
return !editsExistingPost || this._threadItems.length >= 2;
|
|
5115
|
+
}
|
|
5116
|
+
|
|
4751
5117
|
private _renderQuietReplySection() {
|
|
4752
|
-
if (!this.
|
|
5118
|
+
if (!this._canReplyQuietly()) return nothing;
|
|
4753
5119
|
|
|
4754
5120
|
return html`
|
|
4755
|
-
<
|
|
4756
|
-
<
|
|
4757
|
-
<
|
|
4758
|
-
|
|
4759
|
-
|
|
4760
|
-
|
|
4761
|
-
|
|
4762
|
-
|
|
4763
|
-
|
|
4764
|
-
|
|
4765
|
-
|
|
4766
|
-
|
|
4767
|
-
|
|
4768
|
-
|
|
4769
|
-
|
|
4770
|
-
|
|
4771
|
-
|
|
4772
|
-
}}
|
|
4773
|
-
/>
|
|
4774
|
-
</label>
|
|
4775
|
-
</section>
|
|
5121
|
+
<label class="compose-sheet-row compose-sheet-row-switch">
|
|
5122
|
+
<span class="compose-sheet-main">
|
|
5123
|
+
<span class="compose-sheet-title"
|
|
5124
|
+
>${this.labels.quietReplyLabel}</span
|
|
5125
|
+
>
|
|
5126
|
+
<span class="compose-sheet-sub">${this.labels.quietReplyHint}</span>
|
|
5127
|
+
</span>
|
|
5128
|
+
<input
|
|
5129
|
+
type="checkbox"
|
|
5130
|
+
role="switch"
|
|
5131
|
+
class="input"
|
|
5132
|
+
.checked=${this._quietReply}
|
|
5133
|
+
@change=${(e: Event) => {
|
|
5134
|
+
this._quietReply = (e.target as HTMLInputElement).checked;
|
|
5135
|
+
}}
|
|
5136
|
+
/>
|
|
5137
|
+
</label>
|
|
4776
5138
|
`;
|
|
4777
5139
|
}
|
|
4778
5140
|
|
|
4779
|
-
|
|
5141
|
+
/**
|
|
5142
|
+
* Value shown on the collapsed "Published on" row. Always reflects the
|
|
5143
|
+
* current date — unlike the old summary chip, which deliberately stayed
|
|
5144
|
+
* silent when nothing had changed.
|
|
5145
|
+
*/
|
|
5146
|
+
private _getPublishedAtRowValue(): string {
|
|
5147
|
+
const parsed = parseLocalDateInputValue(this._publishedAtInput);
|
|
5148
|
+
if (parsed === null) return this.labels.publishDateSummaryNow;
|
|
5149
|
+
|
|
5150
|
+
return new Intl.DateTimeFormat(undefined, {
|
|
5151
|
+
month: "short",
|
|
5152
|
+
day: "numeric",
|
|
5153
|
+
year: "numeric",
|
|
5154
|
+
}).format(new Date(parsed.year, parsed.monthIndex, parsed.day));
|
|
5155
|
+
}
|
|
5156
|
+
|
|
5157
|
+
/** Value shown on the collapsed "Custom link" row. */
|
|
5158
|
+
private _getPostSlugRowValue(index: number): string {
|
|
5159
|
+
const slug = this._getPostSlug(index).trim();
|
|
5160
|
+
return slug ? `/${slug}` : this.labels.publishSlugSummaryAuto;
|
|
5161
|
+
}
|
|
5162
|
+
|
|
5163
|
+
/* ── Per-post publish date ───────────────────────────────────────────
|
|
5164
|
+
Only the root carries a permalink control: a reply's slug is a random id
|
|
5165
|
+
assigned at publish time, and overriding it is not a thing people do. The
|
|
5166
|
+
date is — backfilling an old thread is the whole reason this exists. */
|
|
5167
|
+
|
|
5168
|
+
private _getPostSlug(index: number): string {
|
|
5169
|
+
if (index === 0) return this._slug;
|
|
5170
|
+
return this._threadItems[index]?.slug ?? "";
|
|
5171
|
+
}
|
|
5172
|
+
|
|
5173
|
+
private _setPostSlug(index: number, value: string) {
|
|
5174
|
+
if (index === 0) {
|
|
5175
|
+
this._slug = value;
|
|
5176
|
+
return;
|
|
5177
|
+
}
|
|
5178
|
+
this._threadItems = this._threadItems.map((item, i) =>
|
|
5179
|
+
i === index ? { ...item, slug: value } : item,
|
|
5180
|
+
);
|
|
5181
|
+
}
|
|
5182
|
+
|
|
5183
|
+
private _setPostSlugTaken(index: number, taken: boolean) {
|
|
5184
|
+
if (index === 0) {
|
|
5185
|
+
this._slugTaken = taken;
|
|
5186
|
+
return;
|
|
5187
|
+
}
|
|
5188
|
+
this._threadItems = this._threadItems.map((item, i) =>
|
|
5189
|
+
i === index ? { ...item, slugTaken: taken } : item,
|
|
5190
|
+
);
|
|
5191
|
+
}
|
|
5192
|
+
|
|
5193
|
+
private _hasPostManualSlug(index: number): boolean {
|
|
5194
|
+
return this._getPostSlug(index).trim().length > 0;
|
|
5195
|
+
}
|
|
5196
|
+
|
|
5197
|
+
private _getPostSlugSyncValidationMessage(index: number): string | null {
|
|
5198
|
+
const issue = getSlugValidationIssue(this._getPostSlug(index));
|
|
5199
|
+
if (issue === "invalid") return this.labels.publishSlugInvalid;
|
|
5200
|
+
if (issue === "reserved") return this.labels.publishSlugReserved;
|
|
5201
|
+
return null;
|
|
5202
|
+
}
|
|
5203
|
+
|
|
5204
|
+
private _getPostSlugValidationMessage(index: number): string | null {
|
|
5205
|
+
const sync = this._getPostSlugSyncValidationMessage(index);
|
|
5206
|
+
if (sync) return sync;
|
|
5207
|
+
const taken =
|
|
5208
|
+
index === 0 ? this._slugTaken : this._threadItems[index]?.slugTaken;
|
|
5209
|
+
if (this._hasPostManualSlug(index) && taken) {
|
|
5210
|
+
return this.labels.publishSlugTaken;
|
|
5211
|
+
}
|
|
5212
|
+
return null;
|
|
5213
|
+
}
|
|
5214
|
+
|
|
5215
|
+
private _getPostPublishedAtInput(index: number): string {
|
|
5216
|
+
if (index === 0) return this._publishedAtInput;
|
|
5217
|
+
return this._threadItems[index]?.publishedAtInput ?? "";
|
|
5218
|
+
}
|
|
5219
|
+
|
|
5220
|
+
private _setPostPublishedAtInput(index: number, value: string) {
|
|
5221
|
+
if (index === 0) {
|
|
5222
|
+
this._publishedAtInput = value;
|
|
5223
|
+
return;
|
|
5224
|
+
}
|
|
5225
|
+
this._threadItems = this._threadItems.map((item, i) =>
|
|
5226
|
+
i === index ? { ...item, publishedAtInput: value } : item,
|
|
5227
|
+
);
|
|
5228
|
+
}
|
|
5229
|
+
|
|
5230
|
+
private _setPostPublishedAtTimeMinutes(index: number, value: number | null) {
|
|
5231
|
+
if (index === 0) {
|
|
5232
|
+
this._publishedAtTimeMinutes = value;
|
|
5233
|
+
return;
|
|
5234
|
+
}
|
|
5235
|
+
this._threadItems = this._threadItems.map((item, i) =>
|
|
5236
|
+
i === index ? { ...item, publishedAtTimeMinutes: value } : item,
|
|
5237
|
+
);
|
|
5238
|
+
}
|
|
5239
|
+
|
|
5240
|
+
private _hasPostPublishedAtValue(index: number): boolean {
|
|
5241
|
+
return this._getPostPublishedAtInput(index).trim().length > 0;
|
|
5242
|
+
}
|
|
5243
|
+
|
|
5244
|
+
/** Same rules as the root's, applied to any post in the thread. */
|
|
5245
|
+
private _getPostPublishedAtValidationMessage(index: number): string | null {
|
|
5246
|
+
const input = this._getPostPublishedAtInput(index);
|
|
5247
|
+
if (!input.trim()) return null;
|
|
5248
|
+
if (parseLocalDateInputValue(input) === null) {
|
|
5249
|
+
return this.labels.publishDateInvalid;
|
|
5250
|
+
}
|
|
5251
|
+
if (input > this._getPublishedAtMaxInput()) {
|
|
5252
|
+
return this.labels.publishDateFutureError;
|
|
5253
|
+
}
|
|
5254
|
+
return null;
|
|
5255
|
+
}
|
|
5256
|
+
|
|
5257
|
+
private _getPostPublishedAtRowValue(index: number): string {
|
|
5258
|
+
const parsed = parseLocalDateInputValue(
|
|
5259
|
+
this._getPostPublishedAtInput(index),
|
|
5260
|
+
);
|
|
5261
|
+
if (parsed !== null) {
|
|
5262
|
+
return new Intl.DateTimeFormat(undefined, {
|
|
5263
|
+
month: "short",
|
|
5264
|
+
day: "numeric",
|
|
5265
|
+
year: "numeric",
|
|
5266
|
+
}).format(new Date(parsed.year, parsed.monthIndex, parsed.day));
|
|
5267
|
+
}
|
|
5268
|
+
// A reply with no date of its own inherits the root's — say so rather than
|
|
5269
|
+
// implying it publishes "now" independently.
|
|
5270
|
+
if (index > 0 && this._hasPublishedAtValue()) {
|
|
5271
|
+
return this._getPublishedAtRowValue();
|
|
5272
|
+
}
|
|
5273
|
+
return this.labels.publishDateSummaryNow;
|
|
5274
|
+
}
|
|
5275
|
+
|
|
5276
|
+
/**
|
|
5277
|
+
* Date (and, on the root, permalink) describe one post, so they live on that
|
|
5278
|
+
* post rather than in the publish panel, which speaks for the whole
|
|
5279
|
+
* submission. The control stays quiet until the post is hovered, focused, or
|
|
5280
|
+
* carries a non-default value.
|
|
5281
|
+
*/
|
|
5282
|
+
private _renderPostMetaControl(index: number) {
|
|
5283
|
+
if (this._openingEdit) return nothing;
|
|
5284
|
+
|
|
5285
|
+
const isRoot = index === 0;
|
|
5286
|
+
const open = this._postMetaIndex === index;
|
|
5287
|
+
const custom =
|
|
5288
|
+
this._hasPostPublishedAtValue(index) || this._hasPostManualSlug(index);
|
|
5289
|
+
|
|
4780
5290
|
return html`
|
|
4781
|
-
|
|
4782
|
-
|
|
4783
|
-
|
|
4784
|
-
|
|
4785
|
-
|
|
4786
|
-
|
|
4787
|
-
|
|
4788
|
-
|
|
4789
|
-
|
|
4790
|
-
|
|
4791
|
-
|
|
5291
|
+
<div
|
|
5292
|
+
class=${classMap({
|
|
5293
|
+
"compose-post-meta": true,
|
|
5294
|
+
"compose-post-meta-set": custom,
|
|
5295
|
+
"compose-post-meta-open": open,
|
|
5296
|
+
})}
|
|
5297
|
+
>
|
|
5298
|
+
${open
|
|
5299
|
+
? html`<div
|
|
5300
|
+
class="compose-dropdown-backdrop"
|
|
5301
|
+
@click=${() => this._closePostMeta()}
|
|
5302
|
+
></div>`
|
|
5303
|
+
: nothing}
|
|
5304
|
+
<button
|
|
5305
|
+
type="button"
|
|
5306
|
+
class="compose-post-meta-pill"
|
|
5307
|
+
aria-haspopup="dialog"
|
|
5308
|
+
aria-expanded=${open ? "true" : "false"}
|
|
5309
|
+
data-compose-post-meta-pill
|
|
5310
|
+
data-post-index=${index}
|
|
5311
|
+
@click=${() => {
|
|
5312
|
+
if (open) return this._closePostMeta();
|
|
5313
|
+
this._showPublishPanel = false;
|
|
5314
|
+
this._showCollection = false;
|
|
5315
|
+
this._postMetaIndex = index;
|
|
5316
|
+
if (isRoot) this._scheduleSuggestedSlugRefresh(true);
|
|
5317
|
+
// Focus the panel, not its first field: a focused `type="date"`
|
|
5318
|
+
// opens with its year segment highlighted in the OS accent colour,
|
|
5319
|
+
// which Chrome will not let us restyle. Tab reaches the fields.
|
|
5320
|
+
this._placeAndFocusPostMetaPanel();
|
|
5321
|
+
}}
|
|
5322
|
+
>
|
|
5323
|
+
${renderComposePublishActionIcon(
|
|
5324
|
+
COMPOSE_PUBLISH_ACTION_ICONS.calendar,
|
|
5325
|
+
"compose-post-meta-icon",
|
|
5326
|
+
)}
|
|
5327
|
+
<span class="compose-post-meta-value"
|
|
5328
|
+
>${this._getPostPublishedAtRowValue(index)}</span
|
|
5329
|
+
>
|
|
5330
|
+
<span class="compose-post-meta-sep" aria-hidden="true">·</span>
|
|
5331
|
+
<!-- No link icon: the permalink already renders with its leading
|
|
5332
|
+
slash, and two icons in a control this quiet read as clutter. -->
|
|
5333
|
+
<span class="compose-post-meta-value compose-post-meta-value-slug"
|
|
5334
|
+
>${this._getPostSlugRowValue(index)}</span
|
|
5335
|
+
>
|
|
5336
|
+
</button>
|
|
5337
|
+
${open
|
|
5338
|
+
? html`<div
|
|
5339
|
+
class="compose-post-meta-panel"
|
|
5340
|
+
role="dialog"
|
|
5341
|
+
aria-label=${this.labels.publishSettings}
|
|
5342
|
+
tabindex="-1"
|
|
5343
|
+
data-compose-post-meta-panel
|
|
5344
|
+
>
|
|
5345
|
+
${this._renderPublishDateSection(index)}
|
|
5346
|
+
${this._renderPublishSlugSection(index)}
|
|
5347
|
+
<div class="compose-post-meta-footer">
|
|
5348
|
+
<button
|
|
5349
|
+
type="button"
|
|
5350
|
+
class="compose-publish-done"
|
|
5351
|
+
data-compose-post-meta-done
|
|
5352
|
+
@click=${() => this._closePostMeta()}
|
|
5353
|
+
>
|
|
5354
|
+
${this.labels.done}
|
|
5355
|
+
</button>
|
|
4792
5356
|
</div>
|
|
4793
|
-
|
|
4794
|
-
|
|
5357
|
+
</div>`
|
|
5358
|
+
: nothing}
|
|
5359
|
+
</div>
|
|
5360
|
+
`;
|
|
5361
|
+
}
|
|
5362
|
+
|
|
5363
|
+
/**
|
|
5364
|
+
* Places the just-opened date/permalink panel, then puts focus in it.
|
|
5365
|
+
*
|
|
5366
|
+
* Both wait on `updateComplete` because the panel is not ours to render: it
|
|
5367
|
+
* is handed to the post's editor row as `headerExtra`, so it only reaches the
|
|
5368
|
+
* DOM on the editor's own update, one below this one. Focus is `preventScroll`
|
|
5369
|
+
* — the panel is already pinned where it should be, and without it the browser
|
|
5370
|
+
* scrolls `.compose-scroll` to "reveal" a panel that is not in that box,
|
|
5371
|
+
* taking the format switcher off the top of the composer with it.
|
|
5372
|
+
*/
|
|
5373
|
+
private _placeAndFocusPostMetaPanel(focusSelector?: string) {
|
|
5374
|
+
this.updateComplete.then(() => {
|
|
5375
|
+
this._updatePostMetaPanelLayout();
|
|
5376
|
+
this.querySelector<HTMLElement>(
|
|
5377
|
+
focusSelector ?? "[data-compose-post-meta-panel]",
|
|
5378
|
+
)?.focus({ preventScroll: true });
|
|
5379
|
+
});
|
|
5380
|
+
}
|
|
5381
|
+
|
|
5382
|
+
/**
|
|
5383
|
+
* Pins the open date/permalink panel under the pill it belongs to.
|
|
5384
|
+
*
|
|
5385
|
+
* The pill sits inside `.compose-scroll`, and that box scrolls: an absolutely
|
|
5386
|
+
* positioned panel is clipped by it, and an empty post makes the box shorter
|
|
5387
|
+
* than the panel every time, so the whole footer — Done included — used to be
|
|
5388
|
+
* cut off. `position: fixed` takes the panel out of the scroller, which hands
|
|
5389
|
+
* the coordinates to us.
|
|
5390
|
+
*
|
|
5391
|
+
* They are not viewport coordinates. The dialog's open animation leaves a
|
|
5392
|
+
* transform behind, and a transformed ancestor becomes the containing block
|
|
5393
|
+
* for `fixed` descendants — so the panel is parked at 0,0 first and the rect
|
|
5394
|
+
* that comes back is the origin everything else is measured from. Reading it
|
|
5395
|
+
* beats naming the ancestor: the page-mode composer has no dialog at all.
|
|
5396
|
+
*/
|
|
5397
|
+
private _updatePostMetaPanelLayout() {
|
|
5398
|
+
const index = this._postMetaIndex;
|
|
5399
|
+
if (index === null) return;
|
|
5400
|
+
|
|
5401
|
+
const panel = this.querySelector<HTMLElement>(
|
|
5402
|
+
"[data-compose-post-meta-panel]",
|
|
5403
|
+
);
|
|
5404
|
+
const pill = this.querySelector<HTMLElement>(
|
|
5405
|
+
`[data-compose-post-meta-pill][data-post-index="${index}"]`,
|
|
5406
|
+
);
|
|
5407
|
+
if (!panel || !pill) return;
|
|
5408
|
+
|
|
5409
|
+
const visualViewport = globalThis.visualViewport;
|
|
5410
|
+
const viewportTop = visualViewport?.offsetTop ?? 0;
|
|
5411
|
+
const viewportLeft = visualViewport?.offsetLeft ?? 0;
|
|
5412
|
+
const viewportBottom =
|
|
5413
|
+
viewportTop + (visualViewport?.height ?? globalThis.innerHeight);
|
|
5414
|
+
const viewportRight =
|
|
5415
|
+
viewportLeft + (visualViewport?.width ?? globalThis.innerWidth);
|
|
5416
|
+
const edgePadding = 12;
|
|
5417
|
+
const gap = 6;
|
|
5418
|
+
|
|
5419
|
+
// Same rule as the publish panel: open into whichever side has more room,
|
|
5420
|
+
// and cap the panel at what that side actually offers so a short window
|
|
5421
|
+
// scrolls the panel's own body instead of hiding its footer.
|
|
5422
|
+
const pillRect = pill.getBoundingClientRect();
|
|
5423
|
+
const availableAbove = Math.max(
|
|
5424
|
+
0,
|
|
5425
|
+
pillRect.top - (viewportTop + edgePadding) - gap,
|
|
5426
|
+
);
|
|
5427
|
+
const availableBelow = Math.max(
|
|
5428
|
+
0,
|
|
5429
|
+
viewportBottom - edgePadding - pillRect.bottom - gap,
|
|
5430
|
+
);
|
|
5431
|
+
const direction = availableBelow >= availableAbove ? "down" : "up";
|
|
5432
|
+
const maxHeight = Math.max(
|
|
5433
|
+
1,
|
|
5434
|
+
Math.floor(direction === "up" ? availableAbove : availableBelow),
|
|
5435
|
+
);
|
|
5436
|
+
|
|
5437
|
+
panel.style.top = "0px";
|
|
5438
|
+
panel.style.left = "0px";
|
|
5439
|
+
const origin = panel.getBoundingClientRect();
|
|
5440
|
+
// The dialog opens with a `scale(0.97)` animation, so for a third of a
|
|
5441
|
+
// second every measured rect is smaller than the box it describes while the
|
|
5442
|
+
// offsets we write are not. `offsetWidth` is the same box before the
|
|
5443
|
+
// transform, so their ratio is the scale to divide back out.
|
|
5444
|
+
const scale = origin.width / (panel.offsetWidth || origin.width || 1) || 1;
|
|
5445
|
+
|
|
5446
|
+
panel.style.setProperty(
|
|
5447
|
+
"--compose-post-meta-panel-max-height",
|
|
5448
|
+
`${maxHeight / scale}px`,
|
|
5449
|
+
);
|
|
5450
|
+
// Read back rather than reuse `origin.height`: the cap above is what
|
|
5451
|
+
// decides how tall the panel actually is, and a panel opening upwards is
|
|
5452
|
+
// placed from its own bottom edge.
|
|
5453
|
+
const height = panel.offsetHeight * scale;
|
|
5454
|
+
|
|
5455
|
+
const top =
|
|
5456
|
+
direction === "down"
|
|
5457
|
+
? pillRect.bottom + gap
|
|
5458
|
+
: pillRect.top - gap - height;
|
|
5459
|
+
// Right-aligned to the pill, which is itself flush with the composer's
|
|
5460
|
+
// right edge — then held inside the viewport, since a narrow screen can
|
|
5461
|
+
// put the panel's left edge past it.
|
|
5462
|
+
const left = Math.min(
|
|
5463
|
+
Math.max(pillRect.right - origin.width, viewportLeft + edgePadding),
|
|
5464
|
+
Math.max(viewportRight - edgePadding - origin.width, viewportLeft),
|
|
5465
|
+
);
|
|
5466
|
+
|
|
5467
|
+
panel.style.top = `${(top - origin.top) / scale}px`;
|
|
5468
|
+
panel.style.left = `${(left - origin.left) / scale}px`;
|
|
5469
|
+
}
|
|
5470
|
+
|
|
5471
|
+
/**
|
|
5472
|
+
* Hands focus back to the post the panel belongs to. The panel takes focus
|
|
5473
|
+
* when it opens, so without this the closing panel takes the focus with it as
|
|
5474
|
+
* it leaves the DOM: focus lands on `<body>`, the editor loses
|
|
5475
|
+
* `:focus-within`, and on a wide screen the pill — quiet until hovered or
|
|
5476
|
+
* focused — fades out from under the pointer. It also leaves the compose
|
|
5477
|
+
* element entirely, which matters more: the `keydown` listener is bound to
|
|
5478
|
+
* this component, so Escape would stop closing compose afterwards.
|
|
5479
|
+
*/
|
|
5480
|
+
private _closePostMeta() {
|
|
5481
|
+
const index = this._postMetaIndex;
|
|
5482
|
+
if (index === null) return;
|
|
5483
|
+
|
|
5484
|
+
const heldFocus =
|
|
5485
|
+
this.querySelector("[data-compose-post-meta-panel]")?.contains(
|
|
5486
|
+
document.activeElement,
|
|
5487
|
+
) ?? false;
|
|
5488
|
+
this._postMetaIndex = null;
|
|
5489
|
+
if (!heldFocus) return;
|
|
5490
|
+
|
|
5491
|
+
this.updateComplete.then(() => {
|
|
5492
|
+
const editors = this.querySelectorAll<JantComposeEditor>(
|
|
5493
|
+
"jant-compose-editor",
|
|
5494
|
+
);
|
|
5495
|
+
(editors[index] ?? editors[0])?.focusInput();
|
|
5496
|
+
});
|
|
5497
|
+
}
|
|
5498
|
+
|
|
5499
|
+
/**
|
|
5500
|
+
* Whether the settings panel would have anything in it.
|
|
5501
|
+
*
|
|
5502
|
+
* Asks the row renderers instead of restating their conditions, so it cannot
|
|
5503
|
+
* drift out of sync with what the panel actually shows — the same reason
|
|
5504
|
+
* `_renderQuickActionsRow` inspects its children rather than recomputing
|
|
5505
|
+
* them. Editing a thread reply empties every row (visibility belongs to the
|
|
5506
|
+
* root, the draft rows only apply to unsaved posts), and an Options button
|
|
5507
|
+
* that opens a blank sheet is worse than no button.
|
|
5508
|
+
*/
|
|
5509
|
+
private _hasPublishPanelContent(): boolean {
|
|
5510
|
+
if (!this._visibilityLocked) return true;
|
|
5511
|
+
if (this._replyToId && this._renderQuietReplySection() !== nothing) {
|
|
5512
|
+
return true;
|
|
5513
|
+
}
|
|
5514
|
+
return (
|
|
5515
|
+
this._renderSaveDraftRow() !== nothing ||
|
|
5516
|
+
this._renderDraftsRow() !== nothing ||
|
|
5517
|
+
this._renderCloseComposeRow() !== nothing
|
|
5518
|
+
);
|
|
5519
|
+
}
|
|
5520
|
+
|
|
5521
|
+
/**
|
|
5522
|
+
* A vertical list, not a toolbar: grouped by a muted label, one row per
|
|
5523
|
+
* setting, value or control on the right.
|
|
5524
|
+
*/
|
|
5525
|
+
private _renderPublishPanelSections() {
|
|
5526
|
+
const divider = html`<div
|
|
5527
|
+
class="compose-sheet-divider"
|
|
5528
|
+
aria-hidden="true"
|
|
5529
|
+
></div>`;
|
|
5530
|
+
const saveDraftRow = this._renderSaveDraftRow();
|
|
5531
|
+
const draftsRow = this._renderDraftsRow();
|
|
5532
|
+
const closeRow = this._renderCloseComposeRow();
|
|
5533
|
+
const hasSessionRows =
|
|
5534
|
+
saveDraftRow !== nothing || draftsRow !== nothing || closeRow !== nothing;
|
|
5535
|
+
|
|
5536
|
+
return html`
|
|
5537
|
+
<div class="compose-sheet">
|
|
5538
|
+
${this._visibilityLocked
|
|
5539
|
+
? nothing
|
|
5540
|
+
: html`
|
|
5541
|
+
<p class="compose-sheet-label">
|
|
5542
|
+
${this.labels.publishVisibilityLabel}
|
|
5543
|
+
</p>
|
|
5544
|
+
<div
|
|
5545
|
+
role="radiogroup"
|
|
5546
|
+
aria-label=${this.labels.publishVisibilityLabel}
|
|
5547
|
+
>
|
|
5548
|
+
${this._renderVisibilityRow(
|
|
4795
5549
|
"public",
|
|
4796
5550
|
this.labels.publishVisibilityPublic,
|
|
5551
|
+
this.labels.publishVisibilityPublicHint,
|
|
4797
5552
|
)}
|
|
4798
|
-
${this.
|
|
5553
|
+
${this._renderVisibilityRow(
|
|
4799
5554
|
"latest_hidden",
|
|
4800
5555
|
this.labels.publishVisibilityHiddenFromLatest,
|
|
5556
|
+
this.labels.publishVisibilityHiddenFromLatestHint,
|
|
4801
5557
|
)}
|
|
4802
|
-
${this.
|
|
5558
|
+
${this._renderVisibilityRow(
|
|
4803
5559
|
"private",
|
|
4804
5560
|
this.labels.publishVisibilityPrivate,
|
|
5561
|
+
this.labels.publishVisibilityPrivateHint,
|
|
4805
5562
|
)}
|
|
4806
5563
|
</div>
|
|
4807
|
-
|
|
4808
|
-
|
|
4809
|
-
|
|
4810
|
-
|
|
4811
|
-
|
|
4812
|
-
|
|
4813
|
-
|
|
4814
|
-
|
|
4815
|
-
|
|
4816
|
-
|
|
4817
|
-
|
|
5564
|
+
`}
|
|
5565
|
+
${this._replyToId
|
|
5566
|
+
? html`${this._visibilityLocked ? nothing : divider}
|
|
5567
|
+
${this._renderQuietReplySection()}`
|
|
5568
|
+
: nothing}
|
|
5569
|
+
${hasSessionRows
|
|
5570
|
+
? html`${divider} ${saveDraftRow} ${draftsRow} ${closeRow}`
|
|
5571
|
+
: nothing}
|
|
5572
|
+
</div>
|
|
5573
|
+
`;
|
|
5574
|
+
}
|
|
5575
|
+
|
|
5576
|
+
/**
|
|
5577
|
+
* "Save as draft" is an action on what is in front of you; "Drafts" is a
|
|
5578
|
+
* place to go. One row cannot be both, which is what the old title-bar button
|
|
5579
|
+
* tried to do: it read "Save as draft" and then dropped you in the list. Once
|
|
5580
|
+
* there is something to save, both are offered and each does one thing.
|
|
5581
|
+
*/
|
|
5582
|
+
private _renderSaveDraftRow() {
|
|
5583
|
+
if (this._editPostId || !this._hasContent()) return nothing;
|
|
5584
|
+
|
|
5585
|
+
return html`
|
|
5586
|
+
<button
|
|
5587
|
+
type="button"
|
|
5588
|
+
class="compose-sheet-row"
|
|
5589
|
+
?disabled=${this._loading}
|
|
5590
|
+
@click=${() => {
|
|
5591
|
+
this._closePublishPanel(false);
|
|
5592
|
+
void this._submit("draft");
|
|
5593
|
+
}}
|
|
5594
|
+
>
|
|
5595
|
+
${renderComposeSheetRowIcon(COMPOSE_SHEET_ROW_ICONS.saveDraft)}
|
|
5596
|
+
<span class="compose-sheet-main">
|
|
5597
|
+
<span class="compose-sheet-title">${this.labels.saveAsDraft}</span>
|
|
5598
|
+
</span>
|
|
5599
|
+
</button>
|
|
5600
|
+
`;
|
|
5601
|
+
}
|
|
5602
|
+
|
|
5603
|
+
/**
|
|
5604
|
+
* Drafts moved off the title bar and in here when that bar went away. It is
|
|
5605
|
+
* the rarer of the two things the bar held — a session action, not part of
|
|
5606
|
+
* writing — so it belongs behind the same trigger as the rest of them.
|
|
5607
|
+
*
|
|
5608
|
+
* Leaving unsaved work behind still asks first, but that prompt is now the
|
|
5609
|
+
* fallback rather than the only path: `_renderSaveDraftRow` is sitting right
|
|
5610
|
+
* above it for anyone who already knows what they want.
|
|
5611
|
+
*/
|
|
5612
|
+
private _renderDraftsRow() {
|
|
5613
|
+
if (this._editPostId) return nothing;
|
|
5614
|
+
|
|
5615
|
+
return html`
|
|
5616
|
+
<button
|
|
5617
|
+
type="button"
|
|
5618
|
+
class="compose-sheet-row"
|
|
5619
|
+
?disabled=${this._loading}
|
|
5620
|
+
@click=${() => {
|
|
5621
|
+
this._closePublishPanel(false);
|
|
5622
|
+
this._handleDraftButtonClick();
|
|
5623
|
+
}}
|
|
5624
|
+
>
|
|
5625
|
+
${renderComposeSheetRowIcon(COMPOSE_SHEET_ROW_ICONS.drafts)}
|
|
5626
|
+
<span class="compose-sheet-main">
|
|
5627
|
+
<span class="compose-sheet-title">${this.labels.drafts}</span>
|
|
5628
|
+
</span>
|
|
5629
|
+
${renderComposePublishActionIcon(
|
|
5630
|
+
COMPOSE_PUBLISH_ACTION_ICONS.caretRight,
|
|
5631
|
+
"compose-sheet-caret",
|
|
5632
|
+
)}
|
|
5633
|
+
</button>
|
|
5634
|
+
`;
|
|
5635
|
+
}
|
|
5636
|
+
|
|
5637
|
+
/**
|
|
5638
|
+
* A thread's posts each spend their × on removing themselves, so this is the
|
|
5639
|
+
* only visible exit there — and the only one at all on a phone, which has no
|
|
5640
|
+
* Escape key and no backdrop to tap. It routes through `requestClose`, so
|
|
5641
|
+
* unsaved work still gets the "Save to drafts?" prompt.
|
|
5642
|
+
*/
|
|
5643
|
+
private _renderCloseComposeRow() {
|
|
5644
|
+
if (this._threadItems.length === 0) return nothing;
|
|
5645
|
+
|
|
5646
|
+
return html`
|
|
5647
|
+
<button
|
|
5648
|
+
type="button"
|
|
5649
|
+
class="compose-sheet-row"
|
|
5650
|
+
?disabled=${this._loading}
|
|
5651
|
+
@click=${() => {
|
|
5652
|
+
this._closePublishPanel(false);
|
|
5653
|
+
this.requestClose();
|
|
5654
|
+
}}
|
|
5655
|
+
>
|
|
5656
|
+
${renderComposePublishActionIcon(
|
|
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>
|
|
5663
|
+
</button>
|
|
4818
5664
|
`;
|
|
4819
5665
|
}
|
|
4820
5666
|
|
|
@@ -4833,15 +5679,6 @@ export class JantComposeDialog extends LitElement {
|
|
|
4833
5679
|
data-compose-publish-panel-desktop
|
|
4834
5680
|
>
|
|
4835
5681
|
${this._renderPublishPanelSections()}
|
|
4836
|
-
<div class="compose-publish-panel-footer">
|
|
4837
|
-
<button
|
|
4838
|
-
type="button"
|
|
4839
|
-
class="compose-publish-done"
|
|
4840
|
-
@click=${() => this._closePublishPanel(true)}
|
|
4841
|
-
>
|
|
4842
|
-
${this.labels.done}
|
|
4843
|
-
</button>
|
|
4844
|
-
</div>
|
|
4845
5682
|
</div>
|
|
4846
5683
|
`;
|
|
4847
5684
|
}
|
|
@@ -4896,75 +5733,6 @@ export class JantComposeDialog extends LitElement {
|
|
|
4896
5733
|
`;
|
|
4897
5734
|
}
|
|
4898
5735
|
|
|
4899
|
-
private _renderPublishSummary(summary: ComposePublishSummaryChip) {
|
|
4900
|
-
const isPublishedAt = summary.kind === "publishedAt";
|
|
4901
|
-
|
|
4902
|
-
return html`
|
|
4903
|
-
<button
|
|
4904
|
-
type="button"
|
|
4905
|
-
class="compose-publish-summary"
|
|
4906
|
-
data-compose-publish-summary=${summary.kind}
|
|
4907
|
-
data-compose-publish-date-summary=${isPublishedAt ? "" : nothing}
|
|
4908
|
-
data-compose-publish-slug-summary=${isPublishedAt ? nothing : ""}
|
|
4909
|
-
data-publish-summary-value=${summary.value}
|
|
4910
|
-
data-publish-date=${isPublishedAt ? summary.value : nothing}
|
|
4911
|
-
data-publish-slug=${isPublishedAt ? nothing : summary.value}
|
|
4912
|
-
aria-label=${summary.actionLabel}
|
|
4913
|
-
title=${summary.actionLabel}
|
|
4914
|
-
?disabled=${this._loading}
|
|
4915
|
-
@click=${() =>
|
|
4916
|
-
isPublishedAt
|
|
4917
|
-
? this._revealPublishedAtField()
|
|
4918
|
-
: this._revealSlugField()}
|
|
4919
|
-
>
|
|
4920
|
-
<span class="compose-publish-summary-icon" aria-hidden="true">
|
|
4921
|
-
${isPublishedAt
|
|
4922
|
-
? html`<svg
|
|
4923
|
-
width="14"
|
|
4924
|
-
height="14"
|
|
4925
|
-
viewBox="0 0 16 16"
|
|
4926
|
-
fill="none"
|
|
4927
|
-
stroke="currentColor"
|
|
4928
|
-
stroke-width="1.35"
|
|
4929
|
-
stroke-linecap="round"
|
|
4930
|
-
stroke-linejoin="round"
|
|
4931
|
-
>
|
|
4932
|
-
<rect x="2.75" y="3.45" width="10.5" height="9.8" rx="2.2" />
|
|
4933
|
-
<path d="M5.35 2.55v2.1" />
|
|
4934
|
-
<path d="M10.65 2.55v2.1" />
|
|
4935
|
-
<path d="M2.75 6.2h10.5" />
|
|
4936
|
-
</svg>`
|
|
4937
|
-
: html`<svg
|
|
4938
|
-
width="14"
|
|
4939
|
-
height="14"
|
|
4940
|
-
viewBox="0 0 16 16"
|
|
4941
|
-
fill="none"
|
|
4942
|
-
stroke="currentColor"
|
|
4943
|
-
stroke-width="1.35"
|
|
4944
|
-
stroke-linecap="round"
|
|
4945
|
-
stroke-linejoin="round"
|
|
4946
|
-
>
|
|
4947
|
-
<path d="M9.75 4.15h1.35a2.75 2.75 0 0 1 0 5.5H9.75" />
|
|
4948
|
-
<path d="M6.25 9.65H4.9a2.75 2.75 0 1 1 0-5.5h1.35" />
|
|
4949
|
-
<path d="M5.65 8h4.7" />
|
|
4950
|
-
</svg>`}
|
|
4951
|
-
</span>
|
|
4952
|
-
<span class="compose-publish-summary-text">${summary.text}</span>
|
|
4953
|
-
</button>
|
|
4954
|
-
`;
|
|
4955
|
-
}
|
|
4956
|
-
|
|
4957
|
-
private _renderPublishSummaries() {
|
|
4958
|
-
const summaries = this._getPublishSummaryChips();
|
|
4959
|
-
if (summaries.length === 0) return nothing;
|
|
4960
|
-
|
|
4961
|
-
return html`
|
|
4962
|
-
<div class="compose-publish-summaries">
|
|
4963
|
-
${summaries.map((summary) => this._renderPublishSummary(summary))}
|
|
4964
|
-
</div>
|
|
4965
|
-
`;
|
|
4966
|
-
}
|
|
4967
|
-
|
|
4968
5736
|
private _updatePublishPanelLayout() {
|
|
4969
5737
|
if (this._publishPanelFullscreen) return;
|
|
4970
5738
|
|
|
@@ -5018,7 +5786,6 @@ export class JantComposeDialog extends LitElement {
|
|
|
5018
5786
|
|
|
5019
5787
|
return html`
|
|
5020
5788
|
<div class="compose-publish-shell">
|
|
5021
|
-
${this._renderPublishSummaries()}
|
|
5022
5789
|
<div
|
|
5023
5790
|
class=${classMap({
|
|
5024
5791
|
"compose-publish-group": true,
|
|
@@ -5049,31 +5816,50 @@ export class JantComposeDialog extends LitElement {
|
|
|
5049
5816
|
>
|
|
5050
5817
|
${this._loading ? spinner : nothing} ${this._getSubmitLabel()}
|
|
5051
5818
|
</button>
|
|
5052
|
-
<button
|
|
5053
|
-
type="button"
|
|
5054
|
-
class=${classMap({
|
|
5055
|
-
"compose-publish-toggle": true,
|
|
5056
|
-
"compose-publish-toggle-loading": this._loading,
|
|
5057
|
-
})}
|
|
5058
|
-
?disabled=${this._loading}
|
|
5059
|
-
aria-haspopup="dialog"
|
|
5060
|
-
aria-expanded=${this._showPublishPanel ? "true" : "false"}
|
|
5061
|
-
aria-label=${this.labels.publishSettings}
|
|
5062
|
-
title=${this.labels.publishSettings}
|
|
5063
|
-
@click=${() => this._togglePublishPanel()}
|
|
5064
|
-
>
|
|
5065
|
-
${renderComposePublishActionIcon(
|
|
5066
|
-
COMPOSE_PUBLISH_ACTION_ICONS.chevron,
|
|
5067
|
-
"compose-publish-toggle-chevron",
|
|
5068
|
-
)}
|
|
5069
|
-
</button>
|
|
5070
5819
|
</div>
|
|
5820
|
+
<!-- Options sits past Publish as its own control rather than a
|
|
5821
|
+
chevron welded to it: a split button reads as one button, which
|
|
5822
|
+
is why nobody found the settings. -->
|
|
5823
|
+
${this._hasPublishPanelContent()
|
|
5824
|
+
? html`<button
|
|
5825
|
+
type="button"
|
|
5826
|
+
class=${classMap({
|
|
5827
|
+
"compose-options-trigger": true,
|
|
5828
|
+
"compose-options-trigger-open": this._showPublishPanel,
|
|
5829
|
+
})}
|
|
5830
|
+
?disabled=${this._loading}
|
|
5831
|
+
aria-haspopup="dialog"
|
|
5832
|
+
aria-expanded=${this._showPublishPanel ? "true" : "false"}
|
|
5833
|
+
aria-label=${this.labels.publishSettings}
|
|
5834
|
+
title=${this.labels.publishSettings}
|
|
5835
|
+
@click=${() => this._togglePublishPanel()}
|
|
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"
|
|
5847
|
+
>
|
|
5848
|
+
${unsafeSVG(COMPOSE_PUBLISH_ACTION_ICONS.options)}
|
|
5849
|
+
</svg>
|
|
5850
|
+
</button>`
|
|
5851
|
+
: nothing}
|
|
5071
5852
|
${this._renderDesktopPublishPanel()}
|
|
5072
5853
|
</div>
|
|
5073
5854
|
</div>
|
|
5074
5855
|
`;
|
|
5075
5856
|
}
|
|
5076
5857
|
|
|
5858
|
+
/**
|
|
5859
|
+
* Shortcuts for the two settings that get reached for constantly, sitting
|
|
5860
|
+
* under Publish where they can be flipped without opening the panel. Both are
|
|
5861
|
+
* mirrors of a row inside it, not a second source of truth.
|
|
5862
|
+
*/
|
|
5077
5863
|
private _renderQuickActionsRow() {
|
|
5078
5864
|
const hideFromLatest = this._renderHideFromLatestQuickToggle();
|
|
5079
5865
|
const quietReply = this._renderQuietReplyQuickToggle();
|
|
@@ -5086,7 +5872,7 @@ export class JantComposeDialog extends LitElement {
|
|
|
5086
5872
|
}
|
|
5087
5873
|
|
|
5088
5874
|
private _renderQuietReplyQuickToggle() {
|
|
5089
|
-
if (!this.
|
|
5875
|
+
if (!this._canReplyQuietly()) return nothing;
|
|
5090
5876
|
return html`
|
|
5091
5877
|
<label class="compose-publish-quick-toggle">
|
|
5092
5878
|
<input
|
|
@@ -5103,6 +5889,11 @@ export class JantComposeDialog extends LitElement {
|
|
|
5103
5889
|
`;
|
|
5104
5890
|
}
|
|
5105
5891
|
|
|
5892
|
+
/**
|
|
5893
|
+
* Visibility has three states, so a checkbox cannot speak for all of them: it
|
|
5894
|
+
* covers the public ↔ hidden-from-Latest pair and steps aside once the post
|
|
5895
|
+
* is private, where the panel's radio list is the only honest control.
|
|
5896
|
+
*/
|
|
5106
5897
|
private _renderHideFromLatestQuickToggle() {
|
|
5107
5898
|
if (this._visibilityLocked) return nothing;
|
|
5108
5899
|
if (this._visibility === "private") return nothing;
|
|
@@ -5133,6 +5924,12 @@ export class JantComposeDialog extends LitElement {
|
|
|
5133
5924
|
aria-live="polite"
|
|
5134
5925
|
aria-busy="true"
|
|
5135
5926
|
>
|
|
5927
|
+
<!-- The post header this normally rides on has not rendered yet, so the
|
|
5928
|
+
close control gets its own row rather than leaving a fetch with no
|
|
5929
|
+
way out on a phone, where there is no Escape key. -->
|
|
5930
|
+
<div class="compose-edit-loading-close">
|
|
5931
|
+
${this._renderCloseComposeControl()}
|
|
5932
|
+
</div>
|
|
5136
5933
|
<div class="compose-edit-loading-status">
|
|
5137
5934
|
<svg
|
|
5138
5935
|
class="animate-spin size-5"
|
|
@@ -5254,6 +6051,10 @@ export class JantComposeDialog extends LitElement {
|
|
|
5254
6051
|
"jant-compose-editor",
|
|
5255
6052
|
);
|
|
5256
6053
|
editors[this._focusedThreadIndex]?.focusInput();
|
|
6054
|
+
const scroller = this.querySelector<HTMLElement>(".compose-scroll");
|
|
6055
|
+
if (scroller) {
|
|
6056
|
+
scroller.scrollTop = scroller.scrollHeight;
|
|
6057
|
+
}
|
|
5257
6058
|
});
|
|
5258
6059
|
}
|
|
5259
6060
|
|
|
@@ -5323,7 +6124,6 @@ export class JantComposeDialog extends LitElement {
|
|
|
5323
6124
|
singleEditor._showRating = capturedShowRating;
|
|
5324
6125
|
}
|
|
5325
6126
|
singleEditor.focusInput();
|
|
5326
|
-
this.requestUpdate();
|
|
5327
6127
|
});
|
|
5328
6128
|
} else {
|
|
5329
6129
|
this._threadItems = newItems;
|
|
@@ -5334,14 +6134,46 @@ export class JantComposeDialog extends LitElement {
|
|
|
5334
6134
|
}
|
|
5335
6135
|
}
|
|
5336
6136
|
|
|
6137
|
+
/**
|
|
6138
|
+
* Where this editor sits in the thread it is extending, as `3/3`.
|
|
6139
|
+
*
|
|
6140
|
+
* A reply continues a chain that already exists, so it counts from the end of
|
|
6141
|
+
* that chain, not from 1: replying to the second post of a thread makes the
|
|
6142
|
+
* third. Only the parent's own depth can tell us this, so the marker stays
|
|
6143
|
+
* hidden until `_loadReplyParentPosition` has read it back — a number that
|
|
6144
|
+
* starts at 2 and jumps to 3 is worse than one that arrives late.
|
|
6145
|
+
*
|
|
6146
|
+
* Empty when there is no chain to place the post in: a lone new post is not
|
|
6147
|
+
* "1/1", and an edit says the more useful thing through its "Editing" marker
|
|
6148
|
+
* (the post being edited may have replies below that compose cannot see, so a
|
|
6149
|
+
* total would be a guess).
|
|
6150
|
+
*/
|
|
6151
|
+
private _positionLabel(index: number): string {
|
|
6152
|
+
if (this._editPostId) return "";
|
|
6153
|
+
const isReply = !!(this._replyToId && this._replyToData);
|
|
6154
|
+
if (isReply && this._replyParentPosition === null) return "";
|
|
6155
|
+
const posted = isReply ? (this._replyParentPosition ?? 0) : 0;
|
|
6156
|
+
const total = posted + (this._threadItems.length || 1);
|
|
6157
|
+
if (total < 2) return "";
|
|
6158
|
+
return `${posted + index + 1}/${total}`;
|
|
6159
|
+
}
|
|
6160
|
+
|
|
5337
6161
|
private _renderThreadPost(
|
|
5338
6162
|
item: ThreadItem,
|
|
5339
6163
|
index: number,
|
|
5340
6164
|
showRemove: boolean,
|
|
6165
|
+
startsThread: boolean,
|
|
5341
6166
|
) {
|
|
5342
6167
|
return html`
|
|
5343
6168
|
<div
|
|
5344
|
-
class
|
|
6169
|
+
class=${classMap({
|
|
6170
|
+
"compose-editor-row": true,
|
|
6171
|
+
"compose-thread-post": true,
|
|
6172
|
+
// Marks the post the cursor is in, which is what the rail's accent
|
|
6173
|
+
// dot points at. Every post in a thread is equally "new", so being an
|
|
6174
|
+
// editor row is not what makes one of them current.
|
|
6175
|
+
"is-current": index === this._focusedThreadIndex,
|
|
6176
|
+
})}
|
|
5345
6177
|
data-thread-index=${index}
|
|
5346
6178
|
@focusin=${() => {
|
|
5347
6179
|
this._focusedThreadIndex = index;
|
|
@@ -5369,6 +6201,10 @@ export class JantComposeDialog extends LitElement {
|
|
|
5369
6201
|
e.stopPropagation();
|
|
5370
6202
|
this._removeThreadItem(index);
|
|
5371
6203
|
}}
|
|
6204
|
+
@jant:compose-status=${(e: CustomEvent<ComposeRowStatus>) => {
|
|
6205
|
+
e.stopPropagation();
|
|
6206
|
+
this._handleRowStatus(item.id, e.detail);
|
|
6207
|
+
}}
|
|
5372
6208
|
>
|
|
5373
6209
|
<div class="compose-thread-dot"></div>
|
|
5374
6210
|
<jant-compose-editor
|
|
@@ -5377,6 +6213,9 @@ export class JantComposeDialog extends LitElement {
|
|
|
5377
6213
|
.uploadMaxFileSize=${this.uploadMaxFileSize}
|
|
5378
6214
|
.threadItem=${true}
|
|
5379
6215
|
.removable=${showRemove}
|
|
6216
|
+
.titleByDefault=${startsThread}
|
|
6217
|
+
.positionLabel=${this._positionLabel(index)}
|
|
6218
|
+
.headerExtra=${this._renderPostMetaControl(index)}
|
|
5380
6219
|
.slashCommandDiscovered=${this.slashCommandDiscovered}
|
|
5381
6220
|
data-thread-id=${item.id}
|
|
5382
6221
|
></jant-compose-editor>
|
|
@@ -5385,19 +6224,9 @@ export class JantComposeDialog extends LitElement {
|
|
|
5385
6224
|
}
|
|
5386
6225
|
|
|
5387
6226
|
private _renderAddToThreadRow() {
|
|
5388
|
-
const
|
|
5389
|
-
const
|
|
5390
|
-
|
|
5391
|
-
);
|
|
5392
|
-
const lastEditor = editors[editors.length - 1];
|
|
5393
|
-
const lastData = lastEditor?.getData();
|
|
5394
|
-
const lastEmpty =
|
|
5395
|
-
!lastData ||
|
|
5396
|
-
(!lastData.body &&
|
|
5397
|
-
!lastData.title.trim() &&
|
|
5398
|
-
!lastData.url.trim() &&
|
|
5399
|
-
!lastData.quoteText.trim() &&
|
|
5400
|
-
lastData.attachments.length === 0);
|
|
6227
|
+
const rowIds = this._rowIds;
|
|
6228
|
+
const lastId = rowIds[rowIds.length - 1];
|
|
6229
|
+
const lastEmpty = !this._rowStatus.get(lastId)?.hasContent;
|
|
5401
6230
|
const atLimit = this._threadItems.length >= MAX_THREAD_POSTS;
|
|
5402
6231
|
return html`
|
|
5403
6232
|
<div class="compose-thread-add-row">
|
|
@@ -5408,19 +6237,6 @@ export class JantComposeDialog extends LitElement {
|
|
|
5408
6237
|
?disabled=${lastEmpty || atLimit}
|
|
5409
6238
|
@click=${() => this._addThreadItem()}
|
|
5410
6239
|
>
|
|
5411
|
-
<svg
|
|
5412
|
-
width="14"
|
|
5413
|
-
height="14"
|
|
5414
|
-
viewBox="0 0 16 16"
|
|
5415
|
-
fill="none"
|
|
5416
|
-
stroke="currentColor"
|
|
5417
|
-
stroke-width="1.65"
|
|
5418
|
-
stroke-linecap="round"
|
|
5419
|
-
stroke-linejoin="round"
|
|
5420
|
-
aria-hidden="true"
|
|
5421
|
-
>
|
|
5422
|
-
${unsafeSVG(PLUS_ICON)}
|
|
5423
|
-
</svg>
|
|
5424
6240
|
Add to thread
|
|
5425
6241
|
</button>
|
|
5426
6242
|
</div>
|
|
@@ -5442,40 +6258,54 @@ export class JantComposeDialog extends LitElement {
|
|
|
5442
6258
|
@jant:compose-content-changed=${() => this._scheduleDraftSave()}
|
|
5443
6259
|
>
|
|
5444
6260
|
${isReply ? this._renderReplyContext() : nothing}
|
|
5445
|
-
|
|
6261
|
+
<!-- Keyed by row id, not by position: an editor holds its own content,
|
|
6262
|
+
so an unkeyed list would reuse elements by position and drop the
|
|
6263
|
+
last one when a middle post is removed — taking that post's text
|
|
6264
|
+
with it, and pairing the survivors with the wrong dates and
|
|
6265
|
+
permalinks. -->
|
|
6266
|
+
${repeat(
|
|
6267
|
+
items,
|
|
6268
|
+
(item) => item.id,
|
|
6269
|
+
(item, i) =>
|
|
6270
|
+
this._renderThreadPost(
|
|
6271
|
+
item,
|
|
6272
|
+
i,
|
|
6273
|
+
showRemove,
|
|
6274
|
+
// Only the post that opens a thread of its own gets a title field
|
|
6275
|
+
// by default — everything downstream of it continues a thought
|
|
6276
|
+
// that is already named.
|
|
6277
|
+
i === 0 && !isReply,
|
|
6278
|
+
),
|
|
6279
|
+
)}
|
|
5446
6280
|
${this._renderAddToThreadRow()}
|
|
5447
6281
|
</div>
|
|
5448
6282
|
`;
|
|
5449
6283
|
}
|
|
5450
6284
|
|
|
5451
|
-
|
|
5452
|
-
|
|
6285
|
+
// `onRail` is for the reply composer, which is already a thread layout: the
|
|
6286
|
+
// row joins the rail with the same dashed dot thread compose uses, so the
|
|
6287
|
+
// placeholder sits where the next post will land instead of floating out at
|
|
6288
|
+
// the dialog's own left edge, a rail's width clear of everything above it.
|
|
6289
|
+
private _renderAddThreadTrigger(onRail: boolean) {
|
|
5453
6290
|
const disabled = !this._hasContent();
|
|
5454
|
-
|
|
5455
|
-
<
|
|
5456
|
-
|
|
5457
|
-
|
|
5458
|
-
|
|
5459
|
-
|
|
5460
|
-
|
|
5461
|
-
|
|
5462
|
-
|
|
5463
|
-
width="14"
|
|
5464
|
-
height="14"
|
|
5465
|
-
viewBox="0 0 16 16"
|
|
5466
|
-
fill="none"
|
|
5467
|
-
stroke="currentColor"
|
|
5468
|
-
stroke-width="1.65"
|
|
5469
|
-
stroke-linecap="round"
|
|
5470
|
-
stroke-linejoin="round"
|
|
5471
|
-
aria-hidden="true"
|
|
5472
|
-
>
|
|
5473
|
-
${unsafeSVG(PLUS_ICON)}
|
|
5474
|
-
</svg>
|
|
5475
|
-
Add to thread
|
|
5476
|
-
</button>
|
|
5477
|
-
</div>
|
|
6291
|
+
const button = html`
|
|
6292
|
+
<button
|
|
6293
|
+
type="button"
|
|
6294
|
+
class="compose-thread-add-btn"
|
|
6295
|
+
?disabled=${disabled}
|
|
6296
|
+
@click=${() => this._addThreadItem()}
|
|
6297
|
+
>
|
|
6298
|
+
Add to thread
|
|
6299
|
+
</button>
|
|
5478
6300
|
`;
|
|
6301
|
+
return onRail
|
|
6302
|
+
? html`
|
|
6303
|
+
<div class="compose-thread-add-row">
|
|
6304
|
+
<div class="compose-thread-add-dot"></div>
|
|
6305
|
+
${button}
|
|
6306
|
+
</div>
|
|
6307
|
+
`
|
|
6308
|
+
: html`<div class="compose-add-thread-trigger">${button}</div>`;
|
|
5479
6309
|
}
|
|
5480
6310
|
|
|
5481
6311
|
render() {
|
|
@@ -5487,8 +6317,34 @@ export class JantComposeDialog extends LitElement {
|
|
|
5487
6317
|
.labels=${this.labels}
|
|
5488
6318
|
.uploadMaxFileSize=${this.uploadMaxFileSize}
|
|
5489
6319
|
.inlineFormat=${isReply}
|
|
6320
|
+
.titleByDefault=${!isReply && JantComposeDialog._getNoteTitleDefault()}
|
|
6321
|
+
.positionLabel=${this._positionLabel(0)}
|
|
6322
|
+
.badgeLabel=${this._editPostId ? this.labels.editing : ""}
|
|
6323
|
+
.headerExtra=${html`${this._renderPostMetaControl(0)}
|
|
6324
|
+
${this._renderCloseComposeControl()}`}
|
|
5490
6325
|
.slashCommandDiscovered=${this.slashCommandDiscovered}
|
|
6326
|
+
@jant:format-change=${(e: CustomEvent<{ format: ComposeFormat }>) => {
|
|
6327
|
+
e.stopPropagation();
|
|
6328
|
+
this._switchFormat(e.detail.format);
|
|
6329
|
+
}}
|
|
6330
|
+
@jant:compose-status=${(e: CustomEvent<ComposeRowStatus>) => {
|
|
6331
|
+
e.stopPropagation();
|
|
6332
|
+
this._handleRowStatus(SINGLE_ROW_ID, e.detail);
|
|
6333
|
+
}}
|
|
5491
6334
|
></jant-compose-editor>`;
|
|
6335
|
+
// The handler sits on the editor itself rather than a delegating wrapper.
|
|
6336
|
+
// Single-post mode renders this editor as a direct child of
|
|
6337
|
+
// `.compose-scroll`, and the rules that lay it out are keyed on that child
|
|
6338
|
+
// relationship — a wrapper breaks them even at `display: contents`, which
|
|
6339
|
+
// drops the box but not the DOM parentage a selector matches on.
|
|
6340
|
+
// (Thread mode builds its own editors in `_renderThreadPost`.)
|
|
6341
|
+
|
|
6342
|
+
// Thread mode grows its own row at the end of the layout, and editing an
|
|
6343
|
+
// existing post has no next slot to offer at all.
|
|
6344
|
+
const addThreadRow =
|
|
6345
|
+
isOpeningEdit || isThreadMode || this._editPostId
|
|
6346
|
+
? nothing
|
|
6347
|
+
: this._renderAddThreadTrigger(isReply);
|
|
5492
6348
|
|
|
5493
6349
|
return html`
|
|
5494
6350
|
<div
|
|
@@ -5501,45 +6357,37 @@ export class JantComposeDialog extends LitElement {
|
|
|
5501
6357
|
aria-hidden=${this._addCollectionPanelOpen ? "true" : "false"}
|
|
5502
6358
|
?inert=${this._addCollectionPanelOpen}
|
|
5503
6359
|
>
|
|
5504
|
-
|
|
5505
|
-
|
|
5506
|
-
|
|
5507
|
-
|
|
5508
|
-
|
|
5509
|
-
|
|
5510
|
-
|
|
5511
|
-
<div
|
|
5512
|
-
class="compose-thread-layout compose-reply-compose-layout"
|
|
5513
|
-
>
|
|
5514
|
-
${this._renderReplyContext()}
|
|
6360
|
+
<div class="compose-scroll">
|
|
6361
|
+
${isOpeningEdit
|
|
6362
|
+
? this._renderEditLoadingState()
|
|
6363
|
+
: isThreadMode
|
|
6364
|
+
? this._renderThreadComposeLayout()
|
|
6365
|
+
: isReply
|
|
6366
|
+
? html`
|
|
5515
6367
|
<div
|
|
5516
|
-
class="compose-
|
|
5517
|
-
@jant:format-change=${(
|
|
5518
|
-
e: CustomEvent<{ format: ComposeFormat }>,
|
|
5519
|
-
) => {
|
|
5520
|
-
e.stopPropagation();
|
|
5521
|
-
this._switchFormat(e.detail.format);
|
|
5522
|
-
}}
|
|
6368
|
+
class="compose-thread-layout compose-reply-compose-layout"
|
|
5523
6369
|
>
|
|
5524
|
-
|
|
5525
|
-
|
|
6370
|
+
${this._renderReplyContext()}
|
|
6371
|
+
<div class="compose-editor-row is-current">
|
|
6372
|
+
<div class="compose-thread-dot"></div>
|
|
6373
|
+
${editor}
|
|
6374
|
+
</div>
|
|
6375
|
+
${addThreadRow}
|
|
5526
6376
|
</div>
|
|
5527
|
-
|
|
5528
|
-
`
|
|
5529
|
-
|
|
5530
|
-
${isOpeningEdit || isThreadMode || this._editPostId
|
|
5531
|
-
? nothing
|
|
5532
|
-
: this._renderAddThreadTrigger()}
|
|
6377
|
+
`
|
|
6378
|
+
: html`${editor}${addThreadRow}`}
|
|
6379
|
+
</div>
|
|
5533
6380
|
${isOpeningEdit
|
|
5534
6381
|
? nothing
|
|
5535
6382
|
: html`<div
|
|
5536
6383
|
class=${classMap({
|
|
5537
6384
|
"compose-action-row": true,
|
|
6385
|
+
"compose-action-row-without-collection": !!this._replyToId,
|
|
5538
6386
|
"compose-action-row-overlay-open":
|
|
5539
6387
|
this._showPublishPanel || this._showCollection,
|
|
5540
6388
|
})}
|
|
5541
6389
|
>
|
|
5542
|
-
${this._renderCollectionSelector()}
|
|
6390
|
+
${this._replyToId ? nothing : this._renderCollectionSelector()}
|
|
5543
6391
|
${this._renderPublishButton()}
|
|
5544
6392
|
</div>
|
|
5545
6393
|
${this._renderQuickActionsRow()}`}
|