@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.
Files changed (350) hide show
  1. package/bin/commands/import-site.js +88 -13
  2. package/bin/commands/migrate.js +55 -0
  3. package/bin/commands/posts/rebuild-html.js +184 -0
  4. package/bin/commands/site/snapshot/import.js +14 -10
  5. package/bin/lib/d1-query.js +23 -5
  6. package/bin/lib/migration-runner.js +29 -7
  7. package/bin/lib/node-database.js +32 -0
  8. package/bin/lib/site-snapshot.js +347 -46
  9. package/bin/lib/sql-export.js +1 -1
  10. package/bin/lib/thread-collection-migration.js +326 -0
  11. package/dist/app-C75Zc8dD.js +6 -0
  12. package/dist/{app-2i7-WQHg.js → app-C7_4oab9.js} +7774 -6425
  13. package/dist/client/.vite/manifest.json +8 -8
  14. package/dist/client/_assets/chunks/{url-pLre2DM_.js → url-CU9pEbk5.js} +1 -1
  15. package/dist/client/_assets/{client-BvID5ucz.js → client-Bq0vre8Y.js} +59 -38
  16. package/dist/client/_assets/client-D3VZyG4L.css +2 -0
  17. package/dist/client/_assets/client-auth-F2rb9WfF.js +5571 -0
  18. package/dist/{env-OHRKGcMj.js → env-BPYViDJJ.js} +1 -1
  19. package/dist/{export-DWn149b7.js → export-Dl5KCiEs.js} +1348 -128
  20. package/dist/{github-api-BgSiE71w.js → github-api-BNF35Lkx.js} +1 -1
  21. package/dist/{github-app-BbklkFmU.js → github-app-DdiD-bC4.js} +1 -1
  22. package/dist/{github-sync-C0zyF6XS.js → github-sync-Bqg62IvV.js} +3 -3
  23. package/dist/{github-sync-Ckk0sJxK.js → github-sync-HdK2EgvJ.js} +3 -3
  24. package/dist/index.js +5 -5
  25. package/dist/node.js +7 -7
  26. package/dist/{url-BMYO-Zlt.js → url-CbLAtlZe.js} +517 -12
  27. package/package.json +11 -10
  28. package/src/__tests__/bin/posts-rebuild-html.test.ts +137 -0
  29. package/src/__tests__/dev-navigation-defaults.test.ts +22 -0
  30. package/src/__tests__/export-hugo-build.test.ts +63 -4
  31. package/src/__tests__/export-import-roundtrip.test.ts +6 -4
  32. package/src/__tests__/export-service.test.ts +142 -9
  33. package/src/__tests__/helpers/app.ts +3 -0
  34. package/src/__tests__/helpers/export-fixtures.ts +2 -0
  35. package/src/__tests__/import-site-command.test.ts +65 -0
  36. package/src/__tests__/mise-config.test.ts +22 -0
  37. package/src/__tests__/vite-config.test.ts +45 -0
  38. package/src/app.tsx +16 -3
  39. package/src/client/__tests__/collection-created-notice.test.ts +36 -0
  40. package/src/client/__tests__/collection-form-bridge.test.ts +10 -1
  41. package/src/client/__tests__/collection-page-actions.test.ts +71 -2
  42. package/src/client/__tests__/compose-bridge.test.ts +316 -2
  43. package/src/client/__tests__/compose-shortcuts.test.ts +20 -0
  44. package/src/client/__tests__/feed-video-player.test.ts +16 -1
  45. package/src/client/__tests__/footnote-rail.test.ts +472 -0
  46. package/src/client/__tests__/hydrate-partial.test.ts +3 -0
  47. package/src/client/__tests__/toast.test.ts +82 -1
  48. package/src/client/collection-created-notice.ts +66 -0
  49. package/src/client/collection-form-bridge.ts +17 -0
  50. package/src/client/collection-navigation.ts +44 -0
  51. package/src/client/collection-page-actions.ts +57 -1
  52. package/src/client/components/__tests__/compose-format-convert.test.ts +0 -19
  53. package/src/client/components/__tests__/jant-collection-directory.test.ts +155 -6
  54. package/src/client/components/__tests__/jant-command-palette.test.ts +51 -0
  55. package/src/client/components/__tests__/jant-compose-dialog.test.ts +2268 -206
  56. package/src/client/components/__tests__/jant-compose-editor.test.ts +147 -59
  57. package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +114 -64
  58. package/src/client/components/__tests__/jant-config-editor.test.ts +405 -0
  59. package/src/client/components/__tests__/jant-media-lightbox.test.ts +121 -0
  60. package/src/client/components/__tests__/jant-nav-manager.test.ts +538 -2
  61. package/src/client/components/__tests__/jant-post-form.test.ts +2 -2
  62. package/src/client/components/__tests__/jant-post-menu.test.ts +47 -5
  63. package/src/client/components/__tests__/jant-settings-avatar.test.ts +2 -2
  64. package/src/client/components/__tests__/jant-settings-general.test.ts +3 -3
  65. package/src/client/components/collection-manager-types.ts +9 -3
  66. package/src/client/components/compose-format-convert.ts +5 -1
  67. package/src/client/components/compose-types.ts +55 -3
  68. package/src/client/components/jant-collection-directory.ts +165 -30
  69. package/src/client/components/jant-command-palette.ts +14 -7
  70. package/src/client/components/jant-compose-dialog.ts +1542 -694
  71. package/src/client/components/jant-compose-editor.ts +237 -99
  72. package/src/client/components/jant-compose-fullscreen.ts +27 -9
  73. package/src/client/components/jant-config-editor.ts +722 -0
  74. package/src/client/components/jant-media-lightbox.ts +100 -18
  75. package/src/client/components/jant-nav-manager.ts +1283 -54
  76. package/src/client/components/jant-post-form.ts +3 -3
  77. package/src/client/components/jant-post-menu.ts +177 -95
  78. package/src/client/components/nav-manager-types.ts +51 -2
  79. package/src/client/components/post-form-types.ts +1 -1
  80. package/src/client/compose-bridge.ts +147 -123
  81. package/src/client/compose-launch.ts +13 -0
  82. package/src/client/compose-shortcuts.ts +5 -1
  83. package/src/client/feed-video-player.ts +62 -2
  84. package/src/client/footnote-rail.ts +478 -0
  85. package/src/client/hydrate-partial.ts +2 -0
  86. package/src/client/media-lightbox-events.ts +63 -0
  87. package/src/client/post-refresh.ts +135 -0
  88. package/src/client/site-header-fragment.ts +38 -0
  89. package/src/client/tiptap/__tests__/blockquote-editing.test.ts +201 -0
  90. package/src/client/tiptap/__tests__/clear-formatting.test.ts +168 -0
  91. package/src/client/tiptap/__tests__/link-input-rules.test.ts +121 -0
  92. package/src/client/tiptap/__tests__/list-editing.test.ts +447 -0
  93. package/src/client/tiptap/__tests__/markdown-clipboard.test.ts +727 -4
  94. package/src/client/tiptap/__tests__/table-controls.test.ts +263 -0
  95. package/src/client/tiptap/__tests__/table-size-picker.test.ts +179 -0
  96. package/src/client/tiptap/bubble-menu.ts +88 -10
  97. package/src/client/tiptap/continuous-lists.ts +102 -0
  98. package/src/client/tiptap/create-editor.ts +3 -0
  99. package/src/client/tiptap/extensions.ts +14 -5
  100. package/src/client/tiptap/link-input-rules.ts +4 -4
  101. package/src/client/tiptap/markdown-clipboard.ts +608 -11
  102. package/src/client/tiptap/slash-commands.ts +8 -1
  103. package/src/client/tiptap/structural-keymap.ts +211 -0
  104. package/src/client/tiptap/tab-indent.ts +8 -22
  105. package/src/client/tiptap/table-control-labels.ts +30 -0
  106. package/src/client/tiptap/table-controls.ts +537 -0
  107. package/src/client/tiptap/table-size-picker.ts +254 -0
  108. package/src/client/toast.ts +133 -39
  109. package/src/client-auth.ts +1 -0
  110. package/src/client.ts +1 -0
  111. package/src/db/__tests__/backfill-thread-activity.test.ts +234 -0
  112. package/src/db/__tests__/d1-query.test.ts +43 -1
  113. package/src/db/__tests__/migration-runner.test.ts +63 -0
  114. package/src/db/__tests__/migrations.test.ts +469 -0
  115. package/src/db/__tests__/thread-activity.test.ts +113 -0
  116. package/src/db/__tests__/thread-collection-migration.test.ts +364 -0
  117. package/src/db/backfills/0005_split_thread_activity_from_quiet_replies.sql +68 -0
  118. package/src/db/migrations/0027_old_blue_blade.sql +39 -0
  119. package/src/db/migrations/0028_superb_post.sql +2 -0
  120. package/src/db/migrations/0029_boring_magma.sql +1 -0
  121. package/src/db/migrations/0030_typical_vivisector.sql +50 -0
  122. package/src/db/migrations/0031_many_ego.sql +3 -0
  123. package/src/db/migrations/meta/0027_snapshot.json +2511 -0
  124. package/src/db/migrations/meta/0028_snapshot.json +2511 -0
  125. package/src/db/migrations/meta/0029_snapshot.json +2519 -0
  126. package/src/db/migrations/meta/0030_snapshot.json +2541 -0
  127. package/src/db/migrations/meta/0031_snapshot.json +2562 -0
  128. package/src/db/migrations/meta/_journal.json +35 -0
  129. package/src/db/migrations/pg/0025_moaning_epoch.sql +39 -0
  130. package/src/db/migrations/pg/0026_talented_killer_shrike.sql +2 -0
  131. package/src/db/migrations/pg/0027_short_cannonball.sql +1 -0
  132. package/src/db/migrations/pg/0028_perfect_sharon_carter.sql +27 -0
  133. package/src/db/migrations/pg/0029_rare_hairball.sql +3 -0
  134. package/src/db/migrations/pg/meta/0025_snapshot.json +3204 -0
  135. package/src/db/migrations/pg/meta/0026_snapshot.json +3204 -0
  136. package/src/db/migrations/pg/meta/0027_snapshot.json +3211 -0
  137. package/src/db/migrations/pg/meta/0028_snapshot.json +3248 -0
  138. package/src/db/migrations/pg/meta/0029_snapshot.json +3289 -0
  139. package/src/db/migrations/pg/meta/_journal.json +35 -0
  140. package/src/db/pg/schema.ts +46 -20
  141. package/src/db/rehearsal-fixtures/demo-current.json +39 -4
  142. package/src/db/rehearsal-fixtures/demo-current.sql +121 -6
  143. package/src/db/schema.ts +48 -20
  144. package/src/db/thread-activity.ts +101 -0
  145. package/src/i18n/__tests__/detect.test.ts +8 -2
  146. package/src/i18n/detect.ts +14 -5
  147. package/src/i18n/locales/public/en.po +222 -51
  148. package/src/i18n/locales/public/en.ts +1 -1
  149. package/src/i18n/locales/public/zh-Hans.po +216 -45
  150. package/src/i18n/locales/public/zh-Hans.ts +1 -1
  151. package/src/i18n/locales/public/zh-Hant.po +216 -45
  152. package/src/i18n/locales/public/zh-Hant.ts +1 -1
  153. package/src/i18n/locales/settings/en.po +513 -10
  154. package/src/i18n/locales/settings/en.ts +1 -1
  155. package/src/i18n/locales/settings/zh-Hans.po +515 -12
  156. package/src/i18n/locales/settings/zh-Hans.ts +1 -1
  157. package/src/i18n/locales/settings/zh-Hant.po +515 -12
  158. package/src/i18n/locales/settings/zh-Hant.ts +1 -1
  159. package/src/lib/__tests__/api-settings.test.ts +299 -0
  160. package/src/lib/__tests__/constants.test.ts +5 -0
  161. package/src/lib/__tests__/feed-policy.test.ts +25 -0
  162. package/src/lib/__tests__/feed.test.ts +168 -0
  163. package/src/lib/__tests__/footnotes-upgrade.test.ts +181 -0
  164. package/src/lib/__tests__/markdown-roundtrip-embed.test.ts +61 -0
  165. package/src/lib/__tests__/markdown-to-tiptap.test.ts +32 -4
  166. package/src/lib/__tests__/markdown.test.ts +24 -6
  167. package/src/lib/__tests__/navigation.test.ts +59 -2
  168. package/src/lib/__tests__/post-body-html.test.ts +176 -0
  169. package/src/lib/__tests__/post-display.test.ts +64 -0
  170. package/src/lib/__tests__/resolve-config.test.ts +113 -1
  171. package/src/lib/__tests__/slug.test.ts +18 -0
  172. package/src/lib/__tests__/summary.test.ts +82 -0
  173. package/src/lib/__tests__/timeline.test.ts +109 -70
  174. package/src/lib/__tests__/timezones.test.ts +31 -3
  175. package/src/lib/__tests__/tiptap-render.test.ts +268 -9
  176. package/src/lib/__tests__/tiptap-to-markdown.test.ts +6 -0
  177. package/src/lib/__tests__/translit.test.ts +87 -0
  178. package/src/lib/__tests__/url.test.ts +53 -0
  179. package/src/lib/__tests__/view.test.ts +53 -0
  180. package/src/lib/__tests__/worker-response-cache.test.ts +18 -1
  181. package/src/lib/api-settings.ts +239 -12
  182. package/src/lib/collection-sort.ts +7 -5
  183. package/src/lib/constants.ts +2 -0
  184. package/src/lib/feed-path.ts +25 -0
  185. package/src/lib/feed-policy.ts +64 -0
  186. package/src/lib/feed.ts +113 -11
  187. package/src/lib/footnotes.ts +264 -0
  188. package/src/lib/github-sync-site-config.ts +2 -0
  189. package/src/lib/hugo-markdown.ts +15 -3
  190. package/src/lib/jant-branding.ts +1 -0
  191. package/src/lib/link-preview.ts +25 -0
  192. package/src/lib/markdown-manager.ts +193 -5
  193. package/src/lib/markdown.ts +10 -3
  194. package/src/lib/navigation.ts +16 -3
  195. package/src/lib/post-body-html.ts +131 -0
  196. package/src/lib/post-display.ts +47 -8
  197. package/src/lib/render.tsx +9 -2
  198. package/src/lib/resolve-config.ts +100 -16
  199. package/src/lib/schemas.ts +133 -1
  200. package/src/lib/settings-paths.ts +5 -0
  201. package/src/lib/site-skill.ts +78 -0
  202. package/src/lib/summary.ts +16 -5
  203. package/src/lib/timeline.ts +137 -131
  204. package/src/lib/timezones.ts +54 -14
  205. package/src/lib/tiptap-render.ts +359 -83
  206. package/src/lib/translit.ts +288 -0
  207. package/src/lib/url.ts +123 -12
  208. package/src/lib/view.ts +58 -20
  209. package/src/lib/worker-response-cache.ts +11 -18
  210. package/src/middleware/__tests__/public-content-access.test.ts +91 -0
  211. package/src/middleware/config.ts +2 -2
  212. package/src/middleware/public-content-access.ts +87 -0
  213. package/src/node/__tests__/cli-export.test.ts +14 -0
  214. package/src/node/__tests__/cli-migrate.test.ts +6 -0
  215. package/src/node/__tests__/cli-site-snapshot.test.ts +205 -2
  216. package/src/node/__tests__/cli-snapshot-meta.test.ts +100 -0
  217. package/src/node/index.ts +2 -1
  218. package/src/preset.css +16 -2
  219. package/src/routes/__tests__/compose.test.ts +51 -0
  220. package/src/routes/__tests__/site-skill.test.ts +112 -0
  221. package/src/routes/api/__tests__/collections.test.ts +91 -19
  222. package/src/routes/api/__tests__/mcp.test.ts +65 -1
  223. package/src/routes/api/__tests__/nav-items.test.ts +91 -0
  224. package/src/routes/api/__tests__/palette.test.ts +44 -0
  225. package/src/routes/api/__tests__/public-access.test.ts +81 -0
  226. package/src/routes/api/__tests__/settings.test.ts +217 -0
  227. package/src/routes/api/collections.ts +22 -26
  228. package/src/routes/api/export.ts +2 -0
  229. package/src/routes/api/internal/__tests__/post-body-html.test.ts +163 -0
  230. package/src/routes/api/internal/__tests__/sites.test.ts +35 -2
  231. package/src/routes/api/internal/post-body-html.ts +43 -0
  232. package/src/routes/api/internal/sites.ts +24 -0
  233. package/src/routes/api/nav-items.ts +24 -1
  234. package/src/routes/api/posts.ts +14 -10
  235. package/src/routes/api/public/__tests__/archive.test.ts +35 -0
  236. package/src/routes/api/public/__tests__/posts.test.ts +246 -0
  237. package/src/routes/api/public/archive.ts +4 -0
  238. package/src/routes/api/public/posts.ts +48 -24
  239. package/src/routes/api/search.ts +3 -0
  240. package/src/routes/api/settings.ts +33 -0
  241. package/src/routes/auth/__tests__/setup.test.ts +7 -4
  242. package/src/routes/compose.tsx +8 -3
  243. package/src/routes/dash/settings.tsx +45 -2
  244. package/src/routes/feed/__tests__/feed.test.ts +210 -6
  245. package/src/routes/feed/__tests__/sitemap.test.ts +68 -1
  246. package/src/routes/feed/feed.ts +23 -14
  247. package/src/routes/pages/__tests__/archive-params.test.ts +384 -3
  248. package/src/routes/pages/__tests__/collection-routing.test.ts +63 -5
  249. package/src/routes/pages/__tests__/collections.test.ts +9 -9
  250. package/src/routes/pages/__tests__/featured.test.ts +6 -2
  251. package/src/routes/pages/__tests__/preview.test.ts +176 -0
  252. package/src/routes/pages/__tests__/search.test.ts +69 -0
  253. package/src/routes/pages/archive.tsx +90 -22
  254. package/src/routes/pages/collection.tsx +32 -19
  255. package/src/routes/pages/collections.tsx +6 -1
  256. package/src/routes/pages/page.tsx +86 -11
  257. package/src/routes/site-skill.ts +18 -0
  258. package/src/services/__tests__/auth.test.ts +7 -5
  259. package/src/services/__tests__/collection.test.ts +190 -44
  260. package/src/services/__tests__/navigation.test.ts +229 -21
  261. package/src/services/__tests__/post-timeline.test.ts +249 -39
  262. package/src/services/__tests__/post.test.ts +844 -5
  263. package/src/services/__tests__/search.test.ts +48 -1
  264. package/src/services/__tests__/settings.test.ts +8 -0
  265. package/src/services/__tests__/site-admin.test.ts +121 -1
  266. package/src/services/auth.ts +2 -2
  267. package/src/services/bootstrap.ts +3 -2
  268. package/src/services/collection.ts +224 -167
  269. package/src/services/export-theme/assets/client-site.css +1 -1
  270. package/src/services/export-theme/assets/client-site.js +46 -25
  271. package/src/services/export-theme/layouts/_default/list.html +18 -4
  272. package/src/services/export-theme/layouts/_default/rss.xml +8 -12
  273. package/src/services/export-theme/layouts/featured/list.html +5 -4
  274. package/src/services/export-theme/layouts/partials/featured-thread.html +56 -0
  275. package/src/services/export-theme/layouts/partials/footer.html +1 -1
  276. package/src/services/export-theme/layouts/partials/media-gallery.html +1 -0
  277. package/src/services/export-theme/styles/main.css +30 -2
  278. package/src/services/export.ts +94 -85
  279. package/src/services/mcp.ts +37 -27
  280. package/src/services/media.ts +1 -1
  281. package/src/services/navigation.ts +185 -46
  282. package/src/services/path.ts +4 -1
  283. package/src/services/post.ts +1142 -342
  284. package/src/services/search.ts +17 -2
  285. package/src/services/settings.ts +2 -10
  286. package/src/services/site-admin.ts +63 -43
  287. package/src/style-cjk-jp.css +1 -1
  288. package/src/style-cjk-kr.css +1 -1
  289. package/src/style-cjk-tc.css +1 -1
  290. package/src/style-cjk.css +1 -1
  291. package/src/styles/components.css +540 -14
  292. package/src/styles/site-media.css +111 -19
  293. package/src/styles/tokens.css +76 -10
  294. package/src/styles/ui.css +2227 -1347
  295. package/src/types/bindings.ts +3 -0
  296. package/src/types/config.ts +243 -12
  297. package/src/types/constants.ts +22 -1
  298. package/src/types/entities.ts +14 -8
  299. package/src/types/operations.ts +18 -10
  300. package/src/types/props.ts +20 -2
  301. package/src/types/raw-assets.d.ts +5 -0
  302. package/src/types/views.ts +22 -4
  303. package/src/ui/__tests__/font-themes.test.ts +42 -30
  304. package/src/ui/compose/ComposeDialog.tsx +109 -19
  305. package/src/ui/dash/appearance/NavigationContent.tsx +332 -4
  306. package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +23 -0
  307. package/src/ui/dash/settings/ConfigEditorContent.tsx +578 -0
  308. package/src/ui/dash/settings/GeneralContent.tsx +15 -5
  309. package/src/ui/dash/settings/SettingsRootContent.tsx +21 -1
  310. package/src/ui/dash/settings/__tests__/ConfigEditorContent.test.tsx +98 -0
  311. package/src/ui/dash/settings/__tests__/SettingsRootContent.test.tsx +15 -0
  312. package/src/ui/feed/CuratedThreadPreview.tsx +16 -4
  313. package/src/ui/feed/LinkCard.tsx +2 -12
  314. package/src/ui/feed/LinkPreview.tsx +2 -7
  315. package/src/ui/feed/NoteCard.tsx +10 -20
  316. package/src/ui/feed/PostStatusBadges.tsx +42 -3
  317. package/src/ui/feed/QuoteCard.tsx +2 -12
  318. package/src/ui/feed/ThreadPreview.tsx +26 -34
  319. package/src/ui/feed/TimelineFeed.tsx +2 -2
  320. package/src/ui/feed/__tests__/thread-preview.test.ts +197 -56
  321. package/src/ui/feed/__tests__/timeline-cards.test.ts +178 -3
  322. package/src/ui/feed/thread-preview-state.ts +5 -7
  323. package/src/ui/font-themes.ts +74 -26
  324. package/src/ui/layouts/BaseLayout.tsx +22 -8
  325. package/src/ui/layouts/__tests__/BaseLayout.test.tsx +64 -1
  326. package/src/ui/pages/ArchivePage.tsx +131 -24
  327. package/src/ui/pages/CollectionPage.tsx +101 -22
  328. package/src/ui/pages/CollectionsPage.tsx +3 -1
  329. package/src/ui/pages/PostPage.tsx +39 -12
  330. package/src/ui/pages/SearchPage.tsx +6 -14
  331. package/src/ui/pages/__tests__/ArchivePage.test.tsx +70 -0
  332. package/src/ui/pages/__tests__/PostPage.test.tsx +88 -0
  333. package/src/ui/shared/CollectionDirectory.tsx +13 -9
  334. package/src/ui/shared/CollectionsManager.tsx +17 -7
  335. package/src/ui/shared/DraftPreviewBar.tsx +49 -0
  336. package/src/ui/shared/HomePageBranding.tsx +2 -2
  337. package/src/ui/shared/MediaGallery.tsx +1 -0
  338. package/src/ui/shared/PostFooter.tsx +38 -13
  339. package/src/ui/shared/__tests__/home-page-branding.test.tsx +1 -1
  340. package/src/ui/shared/__tests__/media-gallery.test.ts +1 -0
  341. package/src/ui/shared/__tests__/navigation-labels.test.ts +2 -2
  342. package/src/ui/shared/__tests__/post-footer.test.ts +15 -0
  343. package/src/ui/shared/collection-management-labels.ts +33 -2
  344. package/src/ui/shared/custom-icons.ts +5 -0
  345. package/src/ui/shared/navigation-labels.ts +2 -4
  346. package/src/ui/shared/post-article-attributes.ts +52 -0
  347. package/dist/app-DZDlmh7C.js +0 -6
  348. package/dist/client/_assets/client-Cevfpm8H.css +0 -2
  349. package/dist/client/_assets/client-auth-v3yvhbgZ.js +0 -4736
  350. 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 { hydratePartial } from "./hydrate-partial.js";
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 mediaIds by clientId.
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 completedMediaIds = new Map<string, string>();
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(clientId, "done", result.id, null);
499
- completedMediaIds.set(clientId, result.id);
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
- completedMediaIds.delete(clientId);
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
- collectionIds: detail.collectionIds,
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
- completedMediaIds.get(attachment.clientId);
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
- { restoreDraft: true, initialFormat: detail.format },
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({ restoreDraft: true });
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 completedMediaIds):
803
+ // AND the upload wasn't already tracked in completedUploads):
819
804
  const failedCount = results.filter(
820
805
  (id, i) =>
821
- id === null && !completedMediaIds.has(pendingClientIds[i] ?? ""),
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 (isPageMode) {
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 newPath = newSlug ? publicPath(`/${newSlug}`) : null;
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("Post updated.");
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("Post updated.");
1068
+ queueSuccessToast(editSuccessMessage);
1045
1069
  globalThis.location.assign(globalThis.location.pathname);
1046
1070
  } else {
1047
- toastMsg("Post updated.");
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("Post updated.");
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("Post updated.");
1087
+ toastMsg(editSuccessMessage);
1064
1088
  } else {
1065
- queueSuccessToast("Post updated.");
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("Post updated.");
1104
+ toastMsg(editSuccessMessage);
1081
1105
  } else {
1082
- queueSuccessToast("Post updated.");
1106
+ queueSuccessToast(editSuccessMessage);
1083
1107
  globalThis.location.reload();
1084
1108
  }
1085
1109
  }
1086
1110
  } else {
1087
- queueSuccessToast("Post updated.");
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
- void composeEl.openEdit(postId);
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 { MEDIA_LIGHTBOX_TOGGLE_EVENT } from "./media-lightbox-events.js";
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);