@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
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import type { ComposeSubmitDetail } from "./components/compose-types.js";
|
|
9
9
|
import type { ComposeAttachment } from "./components/compose-types.js";
|
|
10
10
|
import type { ComposeSubmitAttachment } from "./components/compose-types.js";
|
|
11
|
+
import type { LocalDraftMedia } from "./components/compose-types.js";
|
|
11
12
|
import type { JantComposeDialog } from "./components/jant-compose-dialog.js";
|
|
12
13
|
import type { JantComposeEditor } from "./components/jant-compose-editor.js";
|
|
13
14
|
import type { PostAttachmentInput } from "../types.js";
|
|
@@ -25,10 +26,15 @@ import {
|
|
|
25
26
|
replaceWithAutoClose,
|
|
26
27
|
queueToastForNextPage,
|
|
27
28
|
} from "./toast.js";
|
|
28
|
-
import { openReplyForArticle } from "./compose-launch.js";
|
|
29
|
-
import {
|
|
29
|
+
import { getComposeDialog, openReplyForArticle } from "./compose-launch.js";
|
|
30
|
+
import {
|
|
31
|
+
refreshPostCardView,
|
|
32
|
+
refreshPostPageView,
|
|
33
|
+
refreshTimelineThreadView,
|
|
34
|
+
} from "./post-refresh.js";
|
|
30
35
|
import { getJsonString, readJsonObject } from "./json.js";
|
|
31
36
|
import { uploadViaSession } from "./upload-session.js";
|
|
37
|
+
import type { UploadSessionResult } from "./upload-session.js";
|
|
32
38
|
import { publicPath } from "./runtime-paths.js";
|
|
33
39
|
import { tiptapJsonToMarkdown } from "../lib/tiptap-to-markdown.js";
|
|
34
40
|
import { getMediaCategory } from "../lib/upload.js";
|
|
@@ -66,104 +72,6 @@ function getComposeDialogFromEventTarget(
|
|
|
66
72
|
: null;
|
|
67
73
|
}
|
|
68
74
|
|
|
69
|
-
async function fetchPartialHtml(path: string): Promise<string | null> {
|
|
70
|
-
const res = await fetch(path, {
|
|
71
|
-
headers: { Accept: "text/html" },
|
|
72
|
-
});
|
|
73
|
-
if (!res.ok) return null;
|
|
74
|
-
return res.text();
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
async function refreshTimelineThreadView(
|
|
78
|
-
threadRootId: string,
|
|
79
|
-
): Promise<boolean> {
|
|
80
|
-
try {
|
|
81
|
-
const timelineItem = document.querySelector<HTMLElement>(
|
|
82
|
-
`[data-timeline-item][data-thread-root-id="${threadRootId}"]`,
|
|
83
|
-
);
|
|
84
|
-
const content = timelineItem?.querySelector<HTMLElement>(
|
|
85
|
-
"[data-timeline-item-content]",
|
|
86
|
-
);
|
|
87
|
-
if (!content) return false;
|
|
88
|
-
|
|
89
|
-
const html = await fetchPartialHtml(
|
|
90
|
-
`/_/timeline-item/${encodeURIComponent(threadRootId)}`,
|
|
91
|
-
);
|
|
92
|
-
if (!html) return false;
|
|
93
|
-
|
|
94
|
-
content.innerHTML = html;
|
|
95
|
-
// Swapped-in markup carries interactions whose per-element setup only runs
|
|
96
|
-
// on DOMContentLoaded (thread "Show more" toggle, feed video autoplay, audio
|
|
97
|
-
// waveform); re-initialize them or they stay inert until a full reload.
|
|
98
|
-
hydratePartial(content);
|
|
99
|
-
return true;
|
|
100
|
-
} catch {
|
|
101
|
-
return false;
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
async function refreshPostCardView(postId: string): Promise<boolean> {
|
|
106
|
-
try {
|
|
107
|
-
const timelineItem = document
|
|
108
|
-
.querySelector<HTMLElement>(`article[data-post-id="${postId}"]`)
|
|
109
|
-
?.closest<HTMLElement>("[data-timeline-item]");
|
|
110
|
-
const html = await fetchPartialHtml(
|
|
111
|
-
`/_/post-card/${encodeURIComponent(postId)}`,
|
|
112
|
-
);
|
|
113
|
-
if (!html) return false;
|
|
114
|
-
|
|
115
|
-
if (timelineItem) {
|
|
116
|
-
const content = timelineItem.querySelector<HTMLElement>(
|
|
117
|
-
"[data-timeline-item-content]",
|
|
118
|
-
);
|
|
119
|
-
if (!content) return false;
|
|
120
|
-
content.innerHTML = html;
|
|
121
|
-
hydratePartial(content);
|
|
122
|
-
return true;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
const article = document.querySelector<HTMLElement>(
|
|
126
|
-
`article[data-post-id="${postId}"]`,
|
|
127
|
-
);
|
|
128
|
-
if (!article) return false;
|
|
129
|
-
|
|
130
|
-
article.outerHTML = html;
|
|
131
|
-
// outerHTML detaches `article`; re-query the replacement to hydrate it.
|
|
132
|
-
const nextArticle = document.querySelector<HTMLElement>(
|
|
133
|
-
`article[data-post-id="${postId}"]`,
|
|
134
|
-
);
|
|
135
|
-
if (nextArticle) hydratePartial(nextArticle);
|
|
136
|
-
return true;
|
|
137
|
-
} catch {
|
|
138
|
-
return false;
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
async function refreshPostPageView(postId: string): Promise<boolean> {
|
|
143
|
-
try {
|
|
144
|
-
const container = document.querySelector<HTMLElement>(
|
|
145
|
-
`[data-post-view][data-post-view-id="${postId}"]`,
|
|
146
|
-
);
|
|
147
|
-
if (!container) return false;
|
|
148
|
-
|
|
149
|
-
const html = await fetchPartialHtml(
|
|
150
|
-
`/_/post-view/${encodeURIComponent(postId)}`,
|
|
151
|
-
);
|
|
152
|
-
if (!html) return false;
|
|
153
|
-
|
|
154
|
-
container.outerHTML = html;
|
|
155
|
-
// outerHTML detaches `container`; re-query the replacement to hydrate it
|
|
156
|
-
// (see refreshTimelineThreadView).
|
|
157
|
-
const next = document.querySelector<HTMLElement>(
|
|
158
|
-
`[data-post-view][data-post-view-id="${postId}"]`,
|
|
159
|
-
);
|
|
160
|
-
if (next) hydratePartial(next);
|
|
161
|
-
return true;
|
|
162
|
-
} catch {
|
|
163
|
-
return false;
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
75
|
async function refreshReplyTarget(
|
|
168
76
|
detail: ComposeSubmitDetail,
|
|
169
77
|
): Promise<boolean> {
|
|
@@ -233,7 +141,7 @@ function refreshUploadToast() {
|
|
|
233
141
|
}
|
|
234
142
|
|
|
235
143
|
/**
|
|
236
|
-
* Track completed upload
|
|
144
|
+
* Track completed upload results by clientId.
|
|
237
145
|
*
|
|
238
146
|
* When an attachment is migrated between editor instances (e.g. entering thread
|
|
239
147
|
* mode while an upload is in-flight), `updateAttachmentStatus` fires on the old
|
|
@@ -242,8 +150,11 @@ function refreshUploadToast() {
|
|
|
242
150
|
* handler can't look it up. This map provides a fallback: we record the result
|
|
243
151
|
* here as soon as any upload succeeds, and `buildRequestAttachments` reads it
|
|
244
152
|
* when `attachment.mediaId` is null and the clientId isn't in `uploadPromises`.
|
|
153
|
+
*
|
|
154
|
+
* The full result (not just the id) is kept so a failed publish can restore
|
|
155
|
+
* uploaded attachments into the reopened compose dialog.
|
|
245
156
|
*/
|
|
246
|
-
const
|
|
157
|
+
const completedUploads = new Map<string, UploadSessionResult>();
|
|
247
158
|
|
|
248
159
|
/**
|
|
249
160
|
* Quickly grab the very first decoded frame of a video as a small poster.
|
|
@@ -495,8 +406,14 @@ async function uploadFile(
|
|
|
495
406
|
|
|
496
407
|
uploadProgress.set(clientId, 1);
|
|
497
408
|
refreshUploadToast();
|
|
498
|
-
editor?.updateAttachmentStatus(
|
|
499
|
-
|
|
409
|
+
editor?.updateAttachmentStatus(
|
|
410
|
+
clientId,
|
|
411
|
+
"done",
|
|
412
|
+
result.id,
|
|
413
|
+
null,
|
|
414
|
+
result.url,
|
|
415
|
+
);
|
|
416
|
+
completedUploads.set(clientId, result);
|
|
500
417
|
return result.id;
|
|
501
418
|
} catch (error) {
|
|
502
419
|
uploadProgress.delete(clientId);
|
|
@@ -516,7 +433,7 @@ document.addEventListener("jant:attachment-removed", (e: Event) => {
|
|
|
516
433
|
e as CustomEvent<{ clientId: string; mediaId: string | null }>
|
|
517
434
|
).detail;
|
|
518
435
|
|
|
519
|
-
|
|
436
|
+
completedUploads.delete(clientId);
|
|
520
437
|
|
|
521
438
|
if (mediaId) {
|
|
522
439
|
// Upload already finished — fire-and-forget delete
|
|
@@ -565,6 +482,25 @@ document.addEventListener("click", (e: MouseEvent) => {
|
|
|
565
482
|
void openReplyForArticle(article);
|
|
566
483
|
});
|
|
567
484
|
|
|
485
|
+
// ── Draft badge → editor ────────────────────────────────────────────
|
|
486
|
+
//
|
|
487
|
+
// The badge on an inline draft doubles as its most likely next action. Publish
|
|
488
|
+
// and Delete live in the post menu with every other post action.
|
|
489
|
+
|
|
490
|
+
document.addEventListener("click", (e: MouseEvent) => {
|
|
491
|
+
const trigger = (e.target as HTMLElement).closest<HTMLElement>(
|
|
492
|
+
"[data-draft-continue]",
|
|
493
|
+
);
|
|
494
|
+
if (!trigger) return;
|
|
495
|
+
|
|
496
|
+
const postId =
|
|
497
|
+
trigger.dataset.postId ??
|
|
498
|
+
trigger.closest<HTMLElement>("article[data-post]")?.dataset.postId;
|
|
499
|
+
if (!postId) return;
|
|
500
|
+
|
|
501
|
+
void getComposeDialog()?.openEdit(postId);
|
|
502
|
+
});
|
|
503
|
+
|
|
568
504
|
// ── Submit handler ──────────────────────────────────────────────────
|
|
569
505
|
|
|
570
506
|
/** Build the JSON body for both create and update requests */
|
|
@@ -621,7 +557,10 @@ function buildPostBody(
|
|
|
621
557
|
? detail.rating
|
|
622
558
|
: null
|
|
623
559
|
: detail.rating || undefined,
|
|
624
|
-
|
|
560
|
+
// Reply creation and Child edits must not reorganize their existing
|
|
561
|
+
// Thread as a side effect. Collection management stays on Thread-level
|
|
562
|
+
// organization surfaces; JSON.stringify omits this undefined field.
|
|
563
|
+
collectionIds: detail.replyToId ? undefined : detail.collectionIds,
|
|
625
564
|
attachments: attachments.length > 0 ? attachments : undefined,
|
|
626
565
|
replyToId: detail.replyToId || undefined,
|
|
627
566
|
quietReply: detail.quietReply || undefined,
|
|
@@ -648,7 +587,7 @@ function buildRequestAttachments(
|
|
|
648
587
|
const mediaId =
|
|
649
588
|
attachment.mediaId ??
|
|
650
589
|
pendingMediaIds.get(attachment.clientId) ??
|
|
651
|
-
|
|
590
|
+
completedUploads.get(attachment.clientId)?.id;
|
|
652
591
|
if (!mediaId) continue;
|
|
653
592
|
|
|
654
593
|
requestAttachments.push({
|
|
@@ -691,6 +630,7 @@ document.addEventListener("jant:compose-submit-deferred", async (e: Event) => {
|
|
|
691
630
|
getComposeDialogFromEventTarget(event.target) ??
|
|
692
631
|
(document.querySelector("jant-compose-dialog") as JantComposeDialog | null);
|
|
693
632
|
const isPageMode = !!composeEl?.pageMode;
|
|
633
|
+
const isDraftPreviewPage = !!document.querySelector("[data-preview-status]");
|
|
694
634
|
|
|
695
635
|
// Get labels for toast messages
|
|
696
636
|
const labels = composeEl?.labels;
|
|
@@ -748,12 +688,48 @@ document.addEventListener("jant:compose-submit-deferred", async (e: Event) => {
|
|
|
748
688
|
}
|
|
749
689
|
composeEl?.clearLocalDraftFromStorage?.();
|
|
750
690
|
};
|
|
691
|
+
/**
|
|
692
|
+
* Uploads that completed before the publish failed. The local draft's own
|
|
693
|
+
* media snapshot misses uploads that finished after the dialog closed, so
|
|
694
|
+
* the reopened compose merges these over it.
|
|
695
|
+
*/
|
|
696
|
+
const collectRestorableMedia = (): LocalDraftMedia[] => {
|
|
697
|
+
const details = detail.threadPosts?.length ? detail.threadPosts : [detail];
|
|
698
|
+
const media: LocalDraftMedia[] = [];
|
|
699
|
+
const seen = new Set<string>();
|
|
700
|
+
for (const d of details) {
|
|
701
|
+
for (const attachment of d.attachments) {
|
|
702
|
+
if (attachment.type !== "media" || seen.has(attachment.clientId)) {
|
|
703
|
+
continue;
|
|
704
|
+
}
|
|
705
|
+
seen.add(attachment.clientId);
|
|
706
|
+
const upload = completedUploads.get(attachment.clientId);
|
|
707
|
+
if (!upload) continue;
|
|
708
|
+
media.push({
|
|
709
|
+
clientId: attachment.clientId,
|
|
710
|
+
mediaId: upload.id,
|
|
711
|
+
url: upload.url,
|
|
712
|
+
mimeType: upload.mimeType,
|
|
713
|
+
name: upload.filename || undefined,
|
|
714
|
+
alt: attachment.alt || undefined,
|
|
715
|
+
});
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
return media;
|
|
719
|
+
};
|
|
720
|
+
|
|
751
721
|
const reopenComposeAfterFailure = async () => {
|
|
752
722
|
if (!composeEl || isPageMode) return;
|
|
753
723
|
|
|
724
|
+
if (detail.draftSourceId) {
|
|
725
|
+
if (typeof composeEl.openDraft !== "function") return;
|
|
726
|
+
await composeEl.openDraft(detail.draftSourceId);
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
|
|
754
730
|
if (isEdit && detail.editPostId) {
|
|
755
731
|
if (typeof composeEl.openEdit !== "function") return;
|
|
756
|
-
await composeEl.openEdit(detail.editPostId);
|
|
732
|
+
await composeEl.openEdit(detail.editPostId, { restoreToast: false });
|
|
757
733
|
return;
|
|
758
734
|
}
|
|
759
735
|
|
|
@@ -769,13 +745,22 @@ document.addEventListener("jant:compose-submit-deferred", async (e: Event) => {
|
|
|
769
745
|
id: detail.replyRefreshId,
|
|
770
746
|
}
|
|
771
747
|
: undefined,
|
|
772
|
-
{
|
|
748
|
+
{
|
|
749
|
+
restoreDraft: true,
|
|
750
|
+
initialFormat: detail.format,
|
|
751
|
+
restoreToast: false,
|
|
752
|
+
restoreMedia: collectRestorableMedia(),
|
|
753
|
+
},
|
|
773
754
|
);
|
|
774
755
|
return;
|
|
775
756
|
}
|
|
776
757
|
|
|
777
758
|
if (typeof composeEl.openNew !== "function") return;
|
|
778
|
-
await composeEl.openNew({
|
|
759
|
+
await composeEl.openNew({
|
|
760
|
+
restoreDraft: true,
|
|
761
|
+
restoreToast: false,
|
|
762
|
+
restoreMedia: collectRestorableMedia(),
|
|
763
|
+
});
|
|
779
764
|
};
|
|
780
765
|
const handleSubmitError = async (message: string) => {
|
|
781
766
|
clearPageLoading();
|
|
@@ -815,10 +800,10 @@ document.addEventListener("jant:compose-submit-deferred", async (e: Event) => {
|
|
|
815
800
|
const results = await Promise.all(pendingPromises);
|
|
816
801
|
|
|
817
802
|
// If any pending upload failed (null result where we expected a mediaId
|
|
818
|
-
// AND the upload wasn't already tracked in
|
|
803
|
+
// AND the upload wasn't already tracked in completedUploads):
|
|
819
804
|
const failedCount = results.filter(
|
|
820
805
|
(id, i) =>
|
|
821
|
-
id === null && !
|
|
806
|
+
id === null && !completedUploads.has(pendingClientIds[i] ?? ""),
|
|
822
807
|
).length;
|
|
823
808
|
if (failedCount > 0) {
|
|
824
809
|
if (detail.status === "published" && !isEdit) {
|
|
@@ -927,7 +912,12 @@ document.addEventListener("jant:compose-submit-deferred", async (e: Event) => {
|
|
|
927
912
|
|
|
928
913
|
if (threadStatus === "published") {
|
|
929
914
|
clearRecoveredLocalDraft();
|
|
930
|
-
if (
|
|
915
|
+
if (isDraftPreviewPage && detail.draftSourceId) {
|
|
916
|
+
queueSuccessToast(publishedMsg);
|
|
917
|
+
globalThis.location.assign(
|
|
918
|
+
threadPermalink ?? globalThis.location.pathname,
|
|
919
|
+
);
|
|
920
|
+
} else if (isPageMode) {
|
|
931
921
|
await refreshComposeCollections();
|
|
932
922
|
resetPageCompose();
|
|
933
923
|
toastMsg(publishedMsg);
|
|
@@ -942,6 +932,11 @@ document.addEventListener("jant:compose-submit-deferred", async (e: Event) => {
|
|
|
942
932
|
}
|
|
943
933
|
} else {
|
|
944
934
|
clearRecoveredLocalDraft();
|
|
935
|
+
if (isDraftPreviewPage && detail.draftSourceId) {
|
|
936
|
+
queueSuccessToast(threadToast ?? "Draft saved.");
|
|
937
|
+
globalThis.location.assign(globalThis.location.pathname);
|
|
938
|
+
return;
|
|
939
|
+
}
|
|
945
940
|
await refreshComposeCollections();
|
|
946
941
|
if (!leavePageAfterConfirmSave()) resetPageCompose();
|
|
947
942
|
toastMsg(threadToast ?? "Draft saved.");
|
|
@@ -1030,21 +1025,50 @@ document.addEventListener("jant:compose-submit-deferred", async (e: Event) => {
|
|
|
1030
1025
|
const editData = await readJsonObject(res);
|
|
1031
1026
|
const editPostId = detail.editPostId ?? "";
|
|
1032
1027
|
const newSlug = getJsonString(editData, "slug");
|
|
1033
|
-
const
|
|
1028
|
+
const nextStatus = getJsonString(editData, "status") ?? detail.status;
|
|
1029
|
+
const newPath = newSlug
|
|
1030
|
+
? publicPath(
|
|
1031
|
+
nextStatus === "draft" ? `/preview/${newSlug}` : `/${newSlug}`,
|
|
1032
|
+
)
|
|
1033
|
+
: null;
|
|
1034
|
+
const editSuccessMessage =
|
|
1035
|
+
nextStatus === "draft"
|
|
1036
|
+
? "Draft saved."
|
|
1037
|
+
: detail.draftSourceId
|
|
1038
|
+
? publishedMsg
|
|
1039
|
+
: "Post updated.";
|
|
1040
|
+
|
|
1041
|
+
// Draft preview pages cannot use the public post-view refresh endpoint.
|
|
1042
|
+
// Reload the authenticated preview so saved content stays visible.
|
|
1043
|
+
if (nextStatus === "draft") {
|
|
1044
|
+
if (isDraftPreviewPage) {
|
|
1045
|
+
queueSuccessToast(editSuccessMessage);
|
|
1046
|
+
globalThis.location.assign(newPath ?? globalThis.location.pathname);
|
|
1047
|
+
return;
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
// Drafts opened from the compose panel stay in that workflow. The
|
|
1051
|
+
// completion event lets a pending "save and open drafts" action fetch
|
|
1052
|
+
// the updated list only after the write has finished.
|
|
1053
|
+
await refreshComposeCollections();
|
|
1054
|
+
toastMsg(editSuccessMessage);
|
|
1055
|
+
dispatchSubmitComplete(composeEl, "draft");
|
|
1056
|
+
return;
|
|
1057
|
+
}
|
|
1034
1058
|
|
|
1035
1059
|
if (isPageMode) {
|
|
1036
1060
|
// On the post detail page: refresh or navigate if slug changed
|
|
1037
1061
|
if (newPath && newPath !== globalThis.location.pathname) {
|
|
1038
|
-
queueSuccessToast(
|
|
1062
|
+
queueSuccessToast(editSuccessMessage);
|
|
1039
1063
|
globalThis.location.assign(newPath);
|
|
1040
1064
|
} else {
|
|
1041
1065
|
const refreshed =
|
|
1042
1066
|
editPostId && (await refreshPostPageView(editPostId));
|
|
1043
1067
|
if (!refreshed) {
|
|
1044
|
-
queueSuccessToast(
|
|
1068
|
+
queueSuccessToast(editSuccessMessage);
|
|
1045
1069
|
globalThis.location.assign(globalThis.location.pathname);
|
|
1046
1070
|
} else {
|
|
1047
|
-
toastMsg(
|
|
1071
|
+
toastMsg(editSuccessMessage);
|
|
1048
1072
|
}
|
|
1049
1073
|
}
|
|
1050
1074
|
} else if (editPostId) {
|
|
@@ -1054,15 +1078,15 @@ document.addEventListener("jant:compose-submit-deferred", async (e: Event) => {
|
|
|
1054
1078
|
);
|
|
1055
1079
|
if (postView) {
|
|
1056
1080
|
if (newPath && newPath !== globalThis.location.pathname) {
|
|
1057
|
-
queueSuccessToast(
|
|
1081
|
+
queueSuccessToast(editSuccessMessage);
|
|
1058
1082
|
globalThis.location.assign(newPath);
|
|
1059
1083
|
return;
|
|
1060
1084
|
}
|
|
1061
1085
|
const refreshed = await refreshPostPageView(editPostId);
|
|
1062
1086
|
if (refreshed) {
|
|
1063
|
-
toastMsg(
|
|
1087
|
+
toastMsg(editSuccessMessage);
|
|
1064
1088
|
} else {
|
|
1065
|
-
queueSuccessToast(
|
|
1089
|
+
queueSuccessToast(editSuccessMessage);
|
|
1066
1090
|
globalThis.location.reload();
|
|
1067
1091
|
}
|
|
1068
1092
|
} else {
|
|
@@ -1077,14 +1101,14 @@ document.addEventListener("jant:compose-submit-deferred", async (e: Event) => {
|
|
|
1077
1101
|
? await refreshTimelineThreadView(threadRootId)
|
|
1078
1102
|
: await refreshPostCardView(editPostId);
|
|
1079
1103
|
if (refreshed) {
|
|
1080
|
-
toastMsg(
|
|
1104
|
+
toastMsg(editSuccessMessage);
|
|
1081
1105
|
} else {
|
|
1082
|
-
queueSuccessToast(
|
|
1106
|
+
queueSuccessToast(editSuccessMessage);
|
|
1083
1107
|
globalThis.location.reload();
|
|
1084
1108
|
}
|
|
1085
1109
|
}
|
|
1086
1110
|
} else {
|
|
1087
|
-
queueSuccessToast(
|
|
1111
|
+
queueSuccessToast(editSuccessMessage);
|
|
1088
1112
|
globalThis.location.reload();
|
|
1089
1113
|
}
|
|
1090
1114
|
return;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { JantComposeDialog } from "./components/jant-compose-dialog.js";
|
|
2
|
+
import { showToast } from "./toast.js";
|
|
2
3
|
|
|
3
4
|
interface ReplyToData {
|
|
4
5
|
contentHtml: string;
|
|
@@ -142,6 +143,18 @@ export async function openReplyForArticle(article: HTMLElement): Promise<void> {
|
|
|
142
143
|
const dialog = getComposeDialog();
|
|
143
144
|
if (!dialog) return;
|
|
144
145
|
|
|
146
|
+
// Only surfaces that hide the trailing draft redirect to it — the feed hands
|
|
147
|
+
// its ID down on the last published post. Where the draft is rendered the
|
|
148
|
+
// author can already see it, so Reply keeps its ordinary meaning: continue
|
|
149
|
+
// the thread after this post. (`create` keeps that continuation a draft, so
|
|
150
|
+
// the chain never publishes past an unpublished post.)
|
|
151
|
+
const hiddenDraftTailId = article.dataset.threadDraftTailId;
|
|
152
|
+
if (hiddenDraftTailId) {
|
|
153
|
+
showToast("Picking up the unfinished draft at the end of this thread.");
|
|
154
|
+
await dialog.openEdit(hiddenDraftTailId);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
145
158
|
const threadRootId = article.dataset.threadRootId ?? postId;
|
|
146
159
|
await dialog.openReply(
|
|
147
160
|
postId,
|
|
@@ -68,7 +68,11 @@ function openEditFromQueryParam() {
|
|
|
68
68
|
if (!postId || !composeEl) return;
|
|
69
69
|
|
|
70
70
|
consumeEditQueryParam();
|
|
71
|
-
|
|
71
|
+
if (document.querySelector("[data-preview-status]")) {
|
|
72
|
+
void composeEl.openDraft(postId);
|
|
73
|
+
} else {
|
|
74
|
+
void composeEl.openEdit(postId);
|
|
75
|
+
}
|
|
72
76
|
}
|
|
73
77
|
|
|
74
78
|
async function toggleFeatured(
|
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
isMediaVideoPlaybackPaused,
|
|
3
|
+
MEDIA_LIGHTBOX_TOGGLE_EVENT,
|
|
4
|
+
MEDIA_VIDEO_PLAYBACK_INTENT_EVENT,
|
|
5
|
+
setMediaVideoPlaybackPaused,
|
|
6
|
+
type MediaVideoPlaybackIntentDetail,
|
|
7
|
+
} from "./media-lightbox-events.js";
|
|
2
8
|
|
|
3
9
|
const FEED_VIDEO_PRELOAD_ROOT_MARGIN = "75% 0px";
|
|
4
10
|
const FEED_VIDEO_PLAY_THRESHOLD = 0.6;
|
|
@@ -72,6 +78,28 @@ function readMetrics(video: HTMLVideoElement): FeedVideoMetrics | undefined {
|
|
|
72
78
|
return videoMetrics.get(video);
|
|
73
79
|
}
|
|
74
80
|
|
|
81
|
+
/**
|
|
82
|
+
* Decide whether a feed video is excluded from autoplay by explicit user
|
|
83
|
+
* intent.
|
|
84
|
+
*
|
|
85
|
+
* @param mediaId - Stable media ID from the rendered video
|
|
86
|
+
* @returns `true` when autoplay should leave this media paused
|
|
87
|
+
*
|
|
88
|
+
* @example
|
|
89
|
+
* ```ts
|
|
90
|
+
* shouldSuppressFeedVideoAutoplay("med_123");
|
|
91
|
+
* ```
|
|
92
|
+
*/
|
|
93
|
+
export function shouldSuppressFeedVideoAutoplay(
|
|
94
|
+
mediaId: string | undefined,
|
|
95
|
+
): boolean {
|
|
96
|
+
return isMediaVideoPlaybackPaused(mediaId);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function isAutoplaySuppressed(video: HTMLVideoElement): boolean {
|
|
100
|
+
return shouldSuppressFeedVideoAutoplay(video.dataset.feedVideoId);
|
|
101
|
+
}
|
|
102
|
+
|
|
75
103
|
function getMuteButton(video: HTMLVideoElement): HTMLButtonElement | null {
|
|
76
104
|
return (
|
|
77
105
|
video
|
|
@@ -120,6 +148,11 @@ function pauseVideo(video: HTMLVideoElement | null): void {
|
|
|
120
148
|
}
|
|
121
149
|
|
|
122
150
|
function playVideo(video: HTMLVideoElement): void {
|
|
151
|
+
if (isAutoplaySuppressed(video)) {
|
|
152
|
+
pauseVideo(video);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
123
156
|
ensureVideoLoaded(video);
|
|
124
157
|
video.muted = soundEnabledVideo !== video;
|
|
125
158
|
video.playsInline = true;
|
|
@@ -159,12 +192,13 @@ function reevaluateAutoplay(): void {
|
|
|
159
192
|
for (const video of registeredVideos) {
|
|
160
193
|
const metrics = readMetrics(video);
|
|
161
194
|
if (!metrics) continue;
|
|
195
|
+
if (isAutoplaySuppressed(video)) continue;
|
|
162
196
|
if (metrics.intersectionRatio < FEED_VIDEO_PLAY_THRESHOLD) continue;
|
|
163
197
|
candidates.push({ video, ...metrics });
|
|
164
198
|
}
|
|
165
199
|
|
|
166
200
|
let winner: HTMLVideoElement | null = null;
|
|
167
|
-
if (preferredVideo?.isConnected) {
|
|
201
|
+
if (preferredVideo?.isConnected && !isAutoplaySuppressed(preferredVideo)) {
|
|
168
202
|
const preferredMetrics = readMetrics(preferredVideo);
|
|
169
203
|
if (
|
|
170
204
|
preferredMetrics &&
|
|
@@ -177,6 +211,8 @@ function reevaluateAutoplay(): void {
|
|
|
177
211
|
) {
|
|
178
212
|
preferredVideo = null;
|
|
179
213
|
}
|
|
214
|
+
} else if (preferredVideo && isAutoplaySuppressed(preferredVideo)) {
|
|
215
|
+
preferredVideo = null;
|
|
180
216
|
}
|
|
181
217
|
|
|
182
218
|
if (!winner) {
|
|
@@ -187,6 +223,7 @@ function reevaluateAutoplay(): void {
|
|
|
187
223
|
const currentMetrics = activeVideo ? readMetrics(activeVideo) : undefined;
|
|
188
224
|
if (
|
|
189
225
|
activeVideo &&
|
|
226
|
+
!isAutoplaySuppressed(activeVideo) &&
|
|
190
227
|
currentMetrics &&
|
|
191
228
|
currentMetrics.intersectionRatio > FEED_VIDEO_PAUSE_THRESHOLD
|
|
192
229
|
) {
|
|
@@ -313,6 +350,10 @@ function handleMuteToggle(event: Event): void {
|
|
|
313
350
|
return;
|
|
314
351
|
}
|
|
315
352
|
|
|
353
|
+
const mediaId = video.dataset.feedVideoId?.trim();
|
|
354
|
+
if (mediaId) {
|
|
355
|
+
setMediaVideoPlaybackPaused(mediaId, false);
|
|
356
|
+
}
|
|
316
357
|
preferredVideo = video;
|
|
317
358
|
|
|
318
359
|
const willEnableSound = soundEnabledVideo !== video || video.muted;
|
|
@@ -358,6 +399,25 @@ document.addEventListener(MEDIA_LIGHTBOX_TOGGLE_EVENT, (event: Event) => {
|
|
|
358
399
|
scheduleReevaluate();
|
|
359
400
|
});
|
|
360
401
|
|
|
402
|
+
document.addEventListener(MEDIA_VIDEO_PLAYBACK_INTENT_EVENT, (event: Event) => {
|
|
403
|
+
const detail = (event as CustomEvent<MediaVideoPlaybackIntentDetail>).detail;
|
|
404
|
+
const mediaId = detail?.mediaId?.trim();
|
|
405
|
+
if (!mediaId || typeof detail.paused !== "boolean") {
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
for (const video of registeredVideos) {
|
|
410
|
+
if (video.dataset.feedVideoId?.trim() !== mediaId) continue;
|
|
411
|
+
if (detail.paused) {
|
|
412
|
+
pauseVideo(video);
|
|
413
|
+
if (activeVideo === video) activeVideo = null;
|
|
414
|
+
if (preferredVideo === video) preferredVideo = null;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
scheduleReevaluate();
|
|
419
|
+
});
|
|
420
|
+
|
|
361
421
|
document.addEventListener("visibilitychange", () => {
|
|
362
422
|
if (document.hidden) {
|
|
363
423
|
pauseVideo(activeVideo);
|