@jant/core 0.6.15 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (537) hide show
  1. package/README.md +11 -11
  2. package/bin/commands/assets/upload.js +192 -15
  3. package/bin/commands/import-site.js +67 -4
  4. package/bin/commands/reset-password.js +21 -8
  5. package/bin/commands/site/snapshot/export.js +1 -1
  6. package/bin/commands/site/snapshot/import.js +1 -1
  7. package/bin/lib/d1-query.js +5 -16
  8. package/bin/lib/node-env.js +40 -4
  9. package/bin/lib/site-selection.js +85 -29
  10. package/bin/lib/wrangler-cli.js +28 -2
  11. package/bin/lib/wrangler-json.js +35 -0
  12. package/dist/{app-C-oi_t8W.js → app-K_Aa1MMn.js} +52623 -13193
  13. package/dist/client/.vite/manifest.json +255 -90
  14. package/dist/client/_assets/chunks/confirm-D7GrwsCN.js +45 -0
  15. package/dist/client/_assets/chunks/create-editor-CD3FhrOB.js +257 -0
  16. package/dist/client/_assets/chunks/{native-DpcrFAPh.js → emoji-mart-C4RpRKcU.js} +716 -1
  17. package/dist/client/_assets/chunks/footnote-rail-CmeoVtnU.js +1 -0
  18. package/dist/client/_assets/chunks/heic-to-CuP2Qlsw.js +1 -0
  19. package/dist/client/_assets/chunks/jant-locale-picker-DL6mIOT5.js +69 -0
  20. package/dist/client/_assets/chunks/lit-DW8VmJAT.js +2 -0
  21. package/dist/client/_assets/chunks/mediabunny-DRaCQ1r-.js +31 -0
  22. package/dist/client/_assets/chunks/rolldown-runtime-hePW80VL.js +1 -0
  23. package/dist/client/_assets/chunks/settings-paths-BcdySNx6.js +84 -0
  24. package/dist/client/_assets/chunks/site-header-nav-DzXsFk05.js +1 -0
  25. package/dist/client/_assets/chunks/slug-format-CRXO1AL5.js +1 -0
  26. package/dist/client/_assets/chunks/slugify-373citbs.js +1 -0
  27. package/dist/client/_assets/chunks/sortable-list-CgaL2jCs.js +2 -0
  28. package/dist/client/_assets/chunks/types-D5iF3H1a.js +1 -0
  29. package/dist/client/_assets/chunks/unsafe-html-B5ZEXaxg.js +1 -0
  30. package/dist/client/_assets/chunks/unsafe-svg-0QCkP0vZ.js +2 -0
  31. package/dist/client/_assets/client-GiYENVw8.css +2 -0
  32. package/dist/client/_assets/client-VnFxJN7G.js +295 -0
  33. package/dist/client/_assets/client-auth-k8gJ6QJj.js +828 -0
  34. package/dist/client/_assets/client-author-DQ8R-8KR.css +2 -0
  35. package/dist/client/_assets/client-compose-B_kDtWkW.js +2004 -0
  36. package/dist/client/_assets/client-manage-M90aSTOk.js +2170 -0
  37. package/dist/client/_assets/client-settings-DfYs9n1F.js +1112 -0
  38. package/dist/{export-CsFx6F_M.js → github-sync-BPAvT999.js} +2343 -110
  39. package/dist/index.js +2 -5
  40. package/dist/node.js +5 -7
  41. package/package.json +3 -3
  42. package/src/__tests__/auth-session-freshness.test.ts +57 -0
  43. package/src/__tests__/basecoat-variants.test.ts +121 -0
  44. package/src/__tests__/bin/assets-upload.test.ts +68 -0
  45. package/src/__tests__/export-hugo-build.test.ts +260 -0
  46. package/src/__tests__/export-service.test.ts +231 -0
  47. package/src/__tests__/helpers/component-css.ts +91 -0
  48. package/src/__tests__/helpers/db.ts +33 -0
  49. package/src/__tests__/helpers/export-fixtures.ts +3 -0
  50. package/src/__tests__/mise-config.test.ts +22 -0
  51. package/src/__tests__/stylesheet-audience.test.ts +159 -0
  52. package/src/__tests__/stylesheet-breakpoints.test.ts +183 -0
  53. package/src/app.tsx +71 -16
  54. package/src/auth.ts +18 -1
  55. package/src/client/__tests__/collection-page-actions.test.ts +51 -2
  56. package/src/client/__tests__/compose-bridge-image-upload.test.ts +196 -0
  57. package/src/client/__tests__/compose-bridge-video-poster.test.ts +159 -0
  58. package/src/client/__tests__/compose-bridge.test.ts +524 -2
  59. package/src/client/__tests__/compose-launch.test.ts +62 -1
  60. package/src/client/__tests__/compose-shortcuts.test.ts +57 -8
  61. package/src/client/__tests__/compose-triggers.test.ts +90 -0
  62. package/src/client/__tests__/copy-field.test.ts +162 -0
  63. package/src/client/__tests__/custom-url-menu.test.ts +127 -6
  64. package/src/client/__tests__/image-processor.test.ts +324 -2
  65. package/src/client/__tests__/lazy-entries.test.ts +92 -0
  66. package/src/client/__tests__/media-scroll-hint.test.ts +155 -0
  67. package/src/client/__tests__/runtime-paths.test.ts +84 -0
  68. package/src/client/__tests__/smart-collection-page-actions.test.ts +158 -0
  69. package/src/client/__tests__/toast.test.ts +88 -1
  70. package/src/client/__tests__/video-processor-probe.test.ts +175 -0
  71. package/src/client/__tests__/video-processor.test.ts +181 -0
  72. package/src/client/audio-processor.ts +24 -15
  73. package/src/client/audio-waveform.ts +49 -0
  74. package/src/client/collection-dialog-host.ts +72 -0
  75. package/src/client/collection-navigation.ts +51 -16
  76. package/src/client/collection-page-actions.ts +26 -0
  77. package/src/client/components/__tests__/compose-format-convert.test.ts +0 -19
  78. package/src/client/components/__tests__/jant-collection-dialog.test.ts +329 -0
  79. package/src/client/components/__tests__/jant-collection-directory.test.ts +274 -10
  80. package/src/client/components/__tests__/jant-collection-form.test.ts +59 -178
  81. package/src/client/components/__tests__/jant-command-palette.test.ts +79 -0
  82. package/src/client/components/__tests__/jant-compose-dialog-draft-storage.test.ts +340 -0
  83. package/src/client/components/__tests__/jant-compose-dialog.test.ts +3283 -389
  84. package/src/client/components/__tests__/jant-compose-editor.test.ts +581 -86
  85. package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +34 -89
  86. package/src/client/components/__tests__/jant-locale-picker.test.ts +207 -0
  87. package/src/client/components/__tests__/jant-nav-manager.test.ts +234 -15
  88. package/src/client/components/__tests__/jant-post-menu.test.ts +413 -2
  89. package/src/client/components/__tests__/jant-post-picker.test.ts +197 -0
  90. package/src/client/components/__tests__/jant-settings-avatar.test.ts +15 -12
  91. package/src/client/components/__tests__/jant-settings-general.test.ts +541 -172
  92. package/src/client/components/__tests__/jant-settings-language.test.ts +851 -0
  93. package/src/client/components/__tests__/jant-smart-collection-dialog.test.ts +665 -0
  94. package/src/client/components/collection-dialog-types.ts +49 -0
  95. package/src/client/components/collection-manager-types.ts +32 -1
  96. package/src/client/components/collection-types.ts +1 -15
  97. package/src/client/components/compose-format-convert.ts +5 -1
  98. package/src/client/components/compose-types.ts +121 -3
  99. package/src/client/components/jant-collection-dialog.ts +722 -0
  100. package/src/client/components/jant-collection-directory.ts +567 -182
  101. package/src/client/components/jant-collection-form.ts +53 -232
  102. package/src/client/components/jant-command-palette.ts +106 -49
  103. package/src/client/components/jant-compose-dialog.ts +2757 -1194
  104. package/src/client/components/jant-compose-editor.ts +866 -397
  105. package/src/client/components/jant-compose-fullscreen.ts +53 -47
  106. package/src/client/components/jant-config-editor.ts +65 -53
  107. package/src/client/components/jant-confirm-dialog.ts +13 -11
  108. package/src/client/components/jant-locale-picker.ts +418 -0
  109. package/src/client/components/jant-media-lightbox.ts +146 -136
  110. package/src/client/components/jant-nav-manager.ts +690 -366
  111. package/src/client/components/jant-post-menu.ts +1142 -268
  112. package/src/client/components/jant-post-picker.ts +324 -0
  113. package/src/client/components/jant-repo-picker.ts +138 -106
  114. package/src/client/components/jant-settings-avatar.ts +31 -27
  115. package/src/client/components/jant-settings-general.ts +617 -543
  116. package/src/client/components/jant-settings-language.ts +1087 -0
  117. package/src/client/components/jant-smart-collection-dialog.ts +1021 -0
  118. package/src/client/components/jant-text-preview.ts +94 -86
  119. package/src/client/components/nav-manager-types.ts +62 -1
  120. package/src/client/components/settings-types.ts +14 -29
  121. package/src/client/components/smart-collection-conditions.ts +285 -0
  122. package/src/client/components/smart-collection-dialog-types.ts +68 -0
  123. package/src/client/compose-bridge.ts +336 -242
  124. package/src/client/compose-launch.ts +89 -2
  125. package/src/client/compose-shortcuts.ts +9 -7
  126. package/src/client/compose-triggers.ts +50 -0
  127. package/src/client/copy-field.ts +108 -0
  128. package/src/client/custom-url-menu.ts +60 -4
  129. package/src/client/icons.ts +14 -0
  130. package/src/client/image-plan.ts +165 -0
  131. package/src/client/image-processor.ts +195 -117
  132. package/src/client/image-worker-client.ts +45 -0
  133. package/src/client/image-worker.ts +135 -0
  134. package/src/client/lazy-entries.ts +95 -0
  135. package/src/client/lazy-slugify.ts +2 -2
  136. package/src/client/media-scroll-hint.ts +43 -2
  137. package/src/client/mediabunny.ts +29 -0
  138. package/src/client/post-picker.ts +41 -0
  139. package/src/client/post-refresh.ts +135 -0
  140. package/src/client/runtime-paths.ts +74 -1
  141. package/src/client/settings-bridge.ts +4 -10
  142. package/src/client/site-header-nav.js +18 -6
  143. package/src/client/smart-collection-dialog-host.ts +116 -0
  144. package/src/client/smart-collection-page-actions.ts +208 -0
  145. package/src/client/tiptap/__tests__/bubble-menu.test.ts +110 -0
  146. package/src/client/tiptap/__tests__/floating-position.test.ts +136 -1
  147. package/src/client/tiptap/__tests__/inline-image-upload.test.ts +37 -0
  148. package/src/client/tiptap/__tests__/link-input-rules.test.ts +121 -0
  149. package/src/client/tiptap/bubble-menu.ts +47 -16
  150. package/src/client/tiptap/floating-position.ts +216 -2
  151. package/src/client/tiptap/inline-image-upload.ts +7 -3
  152. package/src/client/tiptap/link-input-rules.ts +4 -4
  153. package/src/client/tiptap/link-toolbar.ts +22 -9
  154. package/src/client/toast.ts +143 -38
  155. package/src/client/types/vite-worker.d.ts +12 -0
  156. package/src/client/upload-with-metadata.ts +3 -13
  157. package/src/client/video-processor.ts +351 -107
  158. package/src/client-auth.ts +14 -31
  159. package/src/client-compose.ts +19 -0
  160. package/src/client-manage.ts +13 -0
  161. package/src/client-settings.ts +14 -0
  162. package/src/client-site.ts +10 -3
  163. package/src/client.ts +4 -0
  164. package/src/db/__tests__/backfill-nav-label-mirrors.test.ts +149 -0
  165. package/src/db/__tests__/backfill-thread-activity.test.ts +234 -0
  166. package/src/db/__tests__/schema-enum-sources.test.ts +33 -0
  167. package/src/db/__tests__/smart-collection-placement.test.ts +402 -0
  168. package/src/db/__tests__/thread-activity.test.ts +113 -0
  169. package/src/db/backfills/0005_split_thread_activity_from_quiet_replies.sql +68 -0
  170. package/src/db/backfills/0006_clear_mirrored_page_nav_labels.sql +31 -0
  171. package/src/db/migrations/0031_many_ego.sql +3 -0
  172. package/src/db/migrations/0032_married_tomas.sql +3 -0
  173. package/src/db/migrations/0033_busy_johnny_storm.sql +198 -0
  174. package/src/db/migrations/0034_cheerful_rumiko_fujikawa.sql +35 -0
  175. package/src/db/migrations/meta/0031_snapshot.json +2562 -0
  176. package/src/db/migrations/meta/0032_snapshot.json +2582 -0
  177. package/src/db/migrations/meta/0033_snapshot.json +2816 -0
  178. package/src/db/migrations/meta/0034_snapshot.json +2816 -0
  179. package/src/db/migrations/meta/_journal.json +28 -0
  180. package/src/db/migrations/pg/0029_rare_hairball.sql +3 -0
  181. package/src/db/migrations/pg/0030_abandoned_nighthawk.sql +3 -0
  182. package/src/db/migrations/pg/0031_strong_ultimatum.sql +128 -0
  183. package/src/db/migrations/pg/0032_furry_multiple_man.sql +6 -0
  184. package/src/db/migrations/pg/meta/0029_snapshot.json +3289 -0
  185. package/src/db/migrations/pg/meta/0030_snapshot.json +3329 -0
  186. package/src/db/migrations/pg/meta/0031_snapshot.json +3660 -0
  187. package/src/db/migrations/pg/meta/0032_snapshot.json +3660 -0
  188. package/src/db/migrations/pg/meta/_journal.json +28 -0
  189. package/src/db/pg/schema.ts +213 -27
  190. package/src/db/post-visibility.ts +117 -0
  191. package/src/db/schema.ts +242 -27
  192. package/src/db/thread-activity.ts +101 -0
  193. package/src/i18n/__tests__/detect.test.ts +26 -81
  194. package/src/i18n/__tests__/supported-locales.test.ts +83 -0
  195. package/src/i18n/context.tsx +12 -2
  196. package/src/i18n/detect.ts +7 -66
  197. package/src/i18n/locales/glossary.zh-Hans.yml +44 -4
  198. package/src/i18n/locales/glossary.zh-Hant.yml +44 -4
  199. package/src/i18n/locales/public/en.po +568 -168
  200. package/src/i18n/locales/public/en.ts +1 -1
  201. package/src/i18n/locales/public/zh-Hans.po +562 -162
  202. package/src/i18n/locales/public/zh-Hans.ts +1 -1
  203. package/src/i18n/locales/public/zh-Hant.po +562 -162
  204. package/src/i18n/locales/public/zh-Hant.ts +1 -1
  205. package/src/i18n/locales/settings/en.po +551 -69
  206. package/src/i18n/locales/settings/en.ts +1 -1
  207. package/src/i18n/locales/settings/zh-Hans.po +597 -115
  208. package/src/i18n/locales/settings/zh-Hans.ts +1 -1
  209. package/src/i18n/locales/settings/zh-Hant.po +610 -128
  210. package/src/i18n/locales/settings/zh-Hant.ts +1 -1
  211. package/src/i18n/locales.ts +94 -0
  212. package/src/i18n/middleware.ts +12 -2
  213. package/src/i18n/supported-locales.ts +80 -0
  214. package/src/lib/__tests__/api-settings.test.ts +11 -2
  215. package/src/lib/__tests__/asset-path.test.ts +92 -0
  216. package/src/lib/__tests__/deferred.test.ts +90 -0
  217. package/src/lib/__tests__/discover-ping.test.ts +89 -0
  218. package/src/lib/__tests__/discover.test.ts +410 -0
  219. package/src/lib/__tests__/feed-policy.test.ts +26 -0
  220. package/src/lib/__tests__/feed.test.ts +1691 -12
  221. package/src/lib/__tests__/filter-dimensions.test.ts +341 -0
  222. package/src/lib/__tests__/hosted-signin.test.ts +1 -1
  223. package/src/lib/__tests__/http-cache.test.ts +162 -0
  224. package/src/lib/__tests__/jant-docs-dev.test.ts +21 -0
  225. package/src/lib/__tests__/jant-docs.test.ts +23 -0
  226. package/src/lib/__tests__/lang-detect.test.ts +231 -0
  227. package/src/lib/__tests__/markdown.test.ts +16 -0
  228. package/src/lib/__tests__/media-helpers.test.ts +81 -0
  229. package/src/lib/__tests__/navigation.test.ts +135 -1
  230. package/src/lib/__tests__/resolve-config.test.ts +64 -3
  231. package/src/lib/__tests__/schemas.test.ts +42 -4
  232. package/src/lib/__tests__/site-unavailable-page.test.ts +25 -0
  233. package/src/lib/__tests__/slug.test.ts +88 -0
  234. package/src/lib/__tests__/slugify.test.ts +85 -0
  235. package/src/lib/__tests__/storage.test.ts +32 -0
  236. package/src/lib/__tests__/thread-fold.test.ts +113 -0
  237. package/src/lib/__tests__/translit.test.ts +87 -0
  238. package/src/lib/__tests__/upload-sideload.test.ts +14 -0
  239. package/src/lib/__tests__/url-fetch.test.ts +1 -2
  240. package/src/lib/__tests__/url.test.ts +93 -34
  241. package/src/lib/__tests__/view-language.test.ts +413 -0
  242. package/src/lib/__tests__/worker-response-cache.test.ts +90 -0
  243. package/src/lib/address.ts +57 -0
  244. package/src/lib/api-posts.ts +12 -0
  245. package/src/lib/asset-path.ts +98 -1
  246. package/src/lib/build-env.ts +16 -0
  247. package/src/lib/collection-groups.ts +5 -1
  248. package/src/lib/collection-paths.ts +3 -10
  249. package/src/lib/constants.ts +28 -3
  250. package/src/lib/copy-field.ts +36 -0
  251. package/src/lib/crypto.ts +40 -0
  252. package/src/lib/csp-builder.ts +2 -1
  253. package/src/lib/deferred.ts +79 -0
  254. package/src/lib/discover-ping.ts +141 -0
  255. package/src/lib/discover.ts +437 -0
  256. package/src/lib/env.ts +134 -0
  257. package/src/lib/errors.ts +33 -0
  258. package/src/lib/feed-policy.ts +236 -2
  259. package/src/lib/feed.ts +1021 -66
  260. package/src/lib/filter-dimensions.ts +1349 -0
  261. package/src/lib/github-sync-site-config.ts +3 -0
  262. package/src/lib/hosted-signin.ts +1 -1
  263. package/src/lib/http-cache.ts +148 -0
  264. package/src/lib/hugo-markdown.ts +21 -0
  265. package/src/lib/icons.ts +9 -2
  266. package/src/lib/ids.ts +3 -0
  267. package/src/lib/jant-docs.ts +42 -0
  268. package/src/lib/lang-detect.ts +308 -0
  269. package/src/lib/link-preview.ts +25 -0
  270. package/src/lib/markdown-manager.ts +6 -1
  271. package/src/lib/media-helpers.ts +13 -1
  272. package/src/lib/navigation.ts +78 -11
  273. package/src/lib/pagination.ts +28 -24
  274. package/src/lib/per-language-surfaces.ts +67 -0
  275. package/src/lib/post-display.ts +42 -9
  276. package/src/lib/post-meta.ts +19 -0
  277. package/src/lib/render.tsx +55 -7
  278. package/src/lib/resolve-config.ts +53 -5
  279. package/src/lib/schemas.ts +201 -10
  280. package/src/lib/site-header-fragment.tsx +2 -1
  281. package/src/lib/site-unavailable-page.ts +47 -0
  282. package/src/lib/slug.ts +51 -7
  283. package/src/lib/slugify.ts +113 -0
  284. package/src/lib/startup-config.ts +4 -6
  285. package/src/lib/storage.ts +7 -0
  286. package/src/lib/summary.ts +50 -0
  287. package/src/lib/thread-fold.ts +161 -0
  288. package/src/lib/time.ts +32 -0
  289. package/src/lib/timeline.ts +81 -10
  290. package/src/lib/tiptap-render.ts +5 -4
  291. package/src/lib/translit.ts +288 -0
  292. package/src/lib/upload.ts +16 -13
  293. package/src/lib/url.ts +188 -24
  294. package/src/lib/version.ts +10 -8
  295. package/src/lib/view-language.ts +436 -0
  296. package/src/lib/view.ts +79 -23
  297. package/src/lib/viewer-context.ts +62 -0
  298. package/src/middleware/__tests__/auth.test.ts +70 -2
  299. package/src/middleware/__tests__/cache-control.test.ts +71 -3
  300. package/src/middleware/__tests__/onboarding.test.ts +76 -5
  301. package/src/middleware/__tests__/secure-headers.test.ts +1 -1
  302. package/src/middleware/__tests__/session.test.ts +181 -33
  303. package/src/middleware/auth.ts +50 -31
  304. package/src/middleware/cache-control.ts +75 -10
  305. package/src/middleware/config.ts +6 -6
  306. package/src/middleware/error-handler.ts +12 -0
  307. package/src/middleware/onboarding.ts +32 -10
  308. package/src/middleware/secure-headers.ts +1 -1
  309. package/src/middleware/session.ts +96 -8
  310. package/src/node/__tests__/cli-migrate.test.ts +21 -19
  311. package/src/node/__tests__/cli-reset-password.test.ts +81 -8
  312. package/src/node/__tests__/cli-runtime-target.test.ts +48 -1
  313. package/src/node/__tests__/cli-site-selection.test.ts +128 -38
  314. package/src/node/__tests__/runtime.test.ts +37 -0
  315. package/src/node/index.ts +1 -0
  316. package/src/preset.css +76 -22
  317. package/src/routes/__tests__/feed-validator.test.ts +156 -0
  318. package/src/routes/api/__tests__/collections.test.ts +114 -0
  319. package/src/routes/api/__tests__/discover.test.ts +222 -0
  320. package/src/routes/api/__tests__/nav-items.test.ts +212 -2
  321. package/src/routes/api/__tests__/palette.test.ts +44 -0
  322. package/src/routes/api/__tests__/post-translations.test.ts +829 -0
  323. package/src/routes/api/__tests__/settings.test.ts +24 -3
  324. package/src/routes/api/__tests__/telegram.test.ts +6 -5
  325. package/src/routes/api/collections.ts +49 -6
  326. package/src/routes/api/custom-urls.ts +0 -1
  327. package/src/routes/api/discover.ts +95 -0
  328. package/src/routes/api/export.ts +3 -0
  329. package/src/routes/api/internal/__tests__/sites.test.ts +42 -2
  330. package/src/routes/api/nav-items.ts +55 -8
  331. package/src/routes/api/posts.ts +172 -9
  332. package/src/routes/api/public/__tests__/archive.test.ts +111 -0
  333. package/src/routes/api/public/__tests__/posts.test.ts +38 -0
  334. package/src/routes/api/public/archive.ts +77 -81
  335. package/src/routes/api/public/posts.ts +30 -6
  336. package/src/routes/api/smart-collections.ts +132 -0
  337. package/src/routes/api/telegram.ts +2 -40
  338. package/src/routes/api/uploads.ts +1 -2
  339. package/src/routes/auth/__tests__/setup-auth.test.ts +137 -0
  340. package/src/routes/auth/__tests__/setup-discover.test.ts +204 -0
  341. package/src/routes/auth/__tests__/setup-flow.test.ts +270 -0
  342. package/src/routes/auth/__tests__/setup-page.test.tsx +329 -0
  343. package/src/routes/auth/__tests__/setup-steps.test.ts +310 -0
  344. package/src/routes/auth/__tests__/setup.test.ts +278 -26
  345. package/src/routes/auth/__tests__/signin.test.ts +135 -0
  346. package/src/routes/auth/setup.tsx +934 -186
  347. package/src/routes/auth/signin.tsx +10 -8
  348. package/src/routes/compose.tsx +11 -2
  349. package/src/routes/dash/__tests__/custom-urls-archive.test.ts +44 -0
  350. package/src/routes/dash/__tests__/custom-urls-page.test.ts +49 -0
  351. package/src/routes/dash/__tests__/delete-account.test.ts +106 -0
  352. package/src/routes/dash/__tests__/settings-discover.test.ts +213 -0
  353. package/src/routes/dash/custom-urls.tsx +1 -37
  354. package/src/routes/dash/settings.tsx +502 -41
  355. package/src/routes/discover-announce.ts +96 -0
  356. package/src/routes/feed/__tests__/feed.test.ts +165 -2
  357. package/src/routes/feed/__tests__/sitemap.test.ts +68 -1
  358. package/src/routes/feed/feed.ts +69 -40
  359. package/src/routes/feed/sitemap.ts +95 -11
  360. package/src/routes/pages/__tests__/archive-indexing.test.ts +200 -0
  361. package/src/routes/pages/__tests__/archive-params.test.ts +657 -1
  362. package/src/routes/pages/__tests__/collection-routing.test.ts +16 -0
  363. package/src/routes/pages/__tests__/collections-feed-links.test.ts +72 -0
  364. package/src/routes/pages/__tests__/collections.test.ts +9 -3
  365. package/src/routes/pages/__tests__/custom-archive-url.test.ts +119 -0
  366. package/src/routes/pages/__tests__/featured.test.ts +6 -2
  367. package/src/routes/pages/__tests__/feed-autodiscovery.test.ts +148 -0
  368. package/src/routes/pages/__tests__/language-routing.test.ts +679 -0
  369. package/src/routes/pages/__tests__/post-preview-partial.test.ts +96 -0
  370. package/src/routes/pages/__tests__/preview.test.ts +3 -1
  371. package/src/routes/pages/__tests__/smart-collection.test.ts +298 -0
  372. package/src/routes/pages/__tests__/subscribe.test.ts +248 -0
  373. package/src/routes/pages/archive.tsx +507 -357
  374. package/src/routes/pages/collection.tsx +94 -96
  375. package/src/routes/pages/collections.tsx +38 -50
  376. package/src/routes/pages/featured.tsx +32 -13
  377. package/src/routes/pages/home.tsx +25 -6
  378. package/src/routes/pages/language.tsx +113 -0
  379. package/src/routes/pages/latest.tsx +26 -16
  380. package/src/routes/pages/new.tsx +3 -0
  381. package/src/routes/pages/page.tsx +183 -19
  382. package/src/routes/pages/partials.tsx +41 -0
  383. package/src/routes/pages/search.tsx +18 -3
  384. package/src/routes/pages/smart-collection.tsx +340 -0
  385. package/src/routes/pages/subscribe.tsx +182 -0
  386. package/src/runtime/__tests__/index.test.ts +19 -0
  387. package/src/runtime/__tests__/node.test.ts +129 -4
  388. package/src/runtime/cloudflare.ts +21 -2
  389. package/src/runtime/index.ts +27 -1
  390. package/src/runtime/node.ts +18 -1
  391. package/src/runtime/site.ts +95 -24
  392. package/src/services/__tests__/auth.test.ts +116 -7
  393. package/src/services/__tests__/collection.test.ts +139 -4
  394. package/src/services/__tests__/language.test.ts +495 -0
  395. package/src/services/__tests__/navigation.test.ts +213 -10
  396. package/src/services/__tests__/path.test.ts +180 -0
  397. package/src/services/__tests__/post-language.test.ts +830 -0
  398. package/src/services/__tests__/post-timeline.test.ts +142 -11
  399. package/src/services/__tests__/post.test.ts +323 -9
  400. package/src/services/__tests__/settings.test.ts +228 -17
  401. package/src/services/__tests__/site-admin.test.ts +5 -5
  402. package/src/services/__tests__/smart-collection.test.ts +651 -0
  403. package/src/services/about-page.ts +22 -1
  404. package/src/services/auth.ts +51 -49
  405. package/src/services/bootstrap.ts +188 -38
  406. package/src/services/collection-directory-position.ts +76 -0
  407. package/src/services/collection.ts +330 -47
  408. package/src/services/custom-url.ts +27 -22
  409. package/src/services/export-theme/assets/client-site.css +1 -1
  410. package/src/services/export-theme/assets/client-site.js +123 -123
  411. package/src/services/export-theme/layouts/_default/rss.xml +77 -1
  412. package/src/services/export-theme/layouts/partials/feed-post-content.xml +105 -2
  413. package/src/services/export-theme/layouts/partials/media-gallery.html +35 -21
  414. package/src/services/export-theme/layouts/partials/pagination.html +14 -13
  415. package/src/services/export-theme/layouts/partials/post-card.html +8 -4
  416. package/src/services/export-theme/layouts/partials/reply.html +2 -1
  417. package/src/services/export-theme/layouts/partials/thread-preview.html +40 -30
  418. package/src/services/export-theme/styles/main.css +13 -1
  419. package/src/services/export.ts +281 -24
  420. package/src/services/index.ts +34 -3
  421. package/src/services/language.ts +412 -0
  422. package/src/services/mcp.ts +3 -1
  423. package/src/services/media.ts +14 -6
  424. package/src/services/navigation.ts +429 -67
  425. package/src/services/path.ts +225 -7
  426. package/src/services/post.ts +1318 -252
  427. package/src/services/search.ts +48 -2
  428. package/src/services/settings.ts +238 -43
  429. package/src/services/site-admin.ts +24 -7
  430. package/src/services/site.ts +77 -1
  431. package/src/services/smart-collection.ts +636 -0
  432. package/src/services/telegram.ts +20 -22
  433. package/src/services/upload-session.ts +1 -3
  434. package/src/style-author.css +27 -0
  435. package/src/styles/components-author.css +1622 -0
  436. package/src/styles/components.css +24 -1585
  437. package/src/styles/site-media.css +20 -6
  438. package/src/styles/tokens.css +144 -13
  439. package/src/styles/ui-author.css +7602 -0
  440. package/src/styles/ui.css +1224 -7276
  441. package/src/types/app-context.ts +7 -0
  442. package/src/types/bindings.ts +3 -0
  443. package/src/types/config.ts +94 -15
  444. package/src/types/constants.ts +188 -16
  445. package/src/types/entities.ts +92 -1
  446. package/src/types/operations.ts +64 -2
  447. package/src/types/props.ts +168 -30
  448. package/src/types/views.ts +61 -1
  449. package/src/ui/__tests__/font-themes.test.ts +52 -12
  450. package/src/ui/compose/ComposeDialog.tsx +215 -21
  451. package/src/ui/compose/ComposePrompt.tsx +1 -5
  452. package/src/ui/compose/__tests__/ComposeDialog.test.tsx +125 -0
  453. package/src/ui/dash/appearance/AdvancedContent.tsx +2 -2
  454. package/src/ui/dash/appearance/CodeInjectionContent.tsx +12 -16
  455. package/src/ui/dash/appearance/NavigationContent.tsx +117 -16
  456. package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +33 -11
  457. package/src/ui/dash/appearance/__tests__/system-nav-descriptions.test.ts +44 -0
  458. package/src/ui/dash/appearance/system-nav-descriptions.ts +75 -0
  459. package/src/ui/dash/settings/AccountMenuContent.tsx +2 -2
  460. package/src/ui/dash/settings/ApiTokensContent.tsx +2 -1
  461. package/src/ui/dash/settings/ConfigEditorContent.tsx +35 -16
  462. package/src/ui/dash/settings/GeneralContent.tsx +275 -113
  463. package/src/ui/dash/settings/LanguageContent.tsx +409 -0
  464. package/src/ui/dash/settings/SettingsRootContent.tsx +19 -1
  465. package/src/ui/dash/settings/__tests__/GeneralContent.test.tsx +324 -4
  466. package/src/ui/dash/settings/__tests__/LanguageContent.test.tsx +144 -0
  467. package/src/ui/dash/settings/__tests__/discover-copy.test.ts +66 -0
  468. package/src/ui/dash/settings/discover-copy.ts +116 -0
  469. package/src/ui/feed/LinkCard.tsx +55 -45
  470. package/src/ui/feed/LinkPreview.tsx +2 -7
  471. package/src/ui/feed/NoteCard.tsx +9 -4
  472. package/src/ui/feed/PostStatusBadges.tsx +103 -17
  473. package/src/ui/feed/QuoteCard.tsx +55 -30
  474. package/src/ui/feed/ThreadPreview.tsx +6 -2
  475. package/src/ui/feed/__tests__/thread-preview.test.ts +13 -16
  476. package/src/ui/feed/__tests__/timeline-cards.test.ts +314 -49
  477. package/src/ui/feed/thread-preview-state.ts +20 -6
  478. package/src/ui/font-themes.ts +39 -29
  479. package/src/ui/layouts/BaseLayout.tsx +155 -10
  480. package/src/ui/layouts/SiteLayout.tsx +192 -34
  481. package/src/ui/layouts/__tests__/BaseLayout.test.tsx +196 -17
  482. package/src/ui/layouts/__tests__/SiteLayout.test.tsx +109 -85
  483. package/src/ui/pages/ArchivePage.tsx +401 -301
  484. package/src/ui/pages/CollectionPage.tsx +86 -76
  485. package/src/ui/pages/CollectionsPage.tsx +5 -0
  486. package/src/ui/pages/ComposePage.tsx +8 -0
  487. package/src/ui/pages/PostPage.tsx +47 -0
  488. package/src/ui/pages/SearchPage.tsx +3 -3
  489. package/src/ui/pages/SmartCollectionPage.tsx +434 -0
  490. package/src/ui/pages/SubscribePage.tsx +236 -0
  491. package/src/ui/pages/__tests__/ArchivePage.test.tsx +122 -3
  492. package/src/ui/pages/__tests__/collections-directory-rows.test.tsx +92 -0
  493. package/src/ui/pages/__tests__/collections-view-links.test.tsx +81 -0
  494. package/src/ui/shared/CollectionDirectory.tsx +178 -3
  495. package/src/ui/shared/CollectionsManager.tsx +70 -17
  496. package/src/ui/shared/CopyField.tsx +127 -0
  497. package/src/ui/shared/DraftPreviewBar.tsx +25 -9
  498. package/src/ui/shared/EmptyPostContent.tsx +23 -0
  499. package/src/ui/shared/MediaGallery.tsx +125 -46
  500. package/src/ui/shared/Pagination.tsx +20 -5
  501. package/src/ui/shared/PostFooter.tsx +55 -23
  502. package/src/ui/shared/__tests__/archive-labels.test.ts +100 -0
  503. package/src/ui/shared/__tests__/media-gallery.test.ts +210 -2
  504. package/src/ui/shared/__tests__/navigation-labels.test.ts +16 -22
  505. package/src/ui/shared/__tests__/pagination.test.ts +38 -24
  506. package/src/ui/shared/__tests__/post-footer.test.ts +32 -0
  507. package/src/ui/shared/archive-labels.ts +98 -0
  508. package/src/ui/shared/collection-management-labels.ts +94 -16
  509. package/src/ui/shared/custom-icons.ts +5 -0
  510. package/src/ui/shared/navigation-labels.ts +45 -71
  511. package/src/ui/shared/post-article-attributes.ts +8 -0
  512. package/src/ui/shared/smart-collection-labels.ts +343 -0
  513. package/dist/app-BxCOR3Uc.js +0 -6
  514. package/dist/client/_assets/chunks/heic-to-DUUaO23q.js +0 -1
  515. package/dist/client/_assets/chunks/module-DcsAZQZ_.js +0 -716
  516. package/dist/client/_assets/chunks/url-pLre2DM_.js +0 -1
  517. package/dist/client/_assets/client-B_eGKN5p.css +0 -2
  518. package/dist/client/_assets/client-DxY5BFe8.js +0 -296
  519. package/dist/client/_assets/client-auth-CakPESv9.js +0 -5533
  520. package/dist/env-OHRKGcMj.js +0 -281
  521. package/dist/github-api-BgSiE71w.js +0 -176
  522. package/dist/github-app-BbklkFmU.js +0 -275
  523. package/dist/github-sync-Cq1pzzOI.js +0 -431
  524. package/dist/github-sync-ppWXN3jb.js +0 -4
  525. package/dist/url-BMYO-Zlt.js +0 -395
  526. package/src/client/__tests__/collection-created-notice.test.ts +0 -36
  527. package/src/client/__tests__/collection-form-bridge.test.ts +0 -216
  528. package/src/client/__tests__/post-form-bridge.test.ts +0 -82
  529. package/src/client/collection-created-notice.ts +0 -66
  530. package/src/client/collection-form-bridge.ts +0 -191
  531. package/src/client/components/__tests__/jant-post-form.test.ts +0 -193
  532. package/src/client/components/jant-post-form.ts +0 -422
  533. package/src/client/components/post-form-template.ts +0 -352
  534. package/src/client/components/post-form-types.ts +0 -96
  535. package/src/client/media-metadata.ts +0 -247
  536. package/src/client/post-form-bridge.ts +0 -193
  537. package/src/ui/pages/CollectionEditorPage.tsx +0 -175
@@ -8,16 +8,14 @@
8
8
  import type { ComposeSubmitDetail } from "./components/compose-types.js";
9
9
  import type { ComposeAttachment } from "./components/compose-types.js";
10
10
  import type { ComposeSubmitAttachment } from "./components/compose-types.js";
11
+ import type { LocalDraftMedia } from "./components/compose-types.js";
11
12
  import type { JantComposeDialog } from "./components/jant-compose-dialog.js";
12
13
  import type { JantComposeEditor } from "./components/jant-compose-editor.js";
13
14
  import type { PostAttachmentInput } from "../types.js";
14
15
  import { AudioProcessor } from "./audio-processor.js";
15
16
  import { ImageProcessor } from "./image-processor.js";
16
17
  import { VideoProcessor } from "./video-processor.js";
17
- import {
18
- extractMediaMetadata,
19
- extractAudioWaveform,
20
- } from "./media-metadata.js";
18
+ import { extractAudioWaveform } from "./audio-waveform.js";
21
19
  import {
22
20
  showToast,
23
21
  showPersistentToast,
@@ -25,18 +23,89 @@ import {
25
23
  replaceWithAutoClose,
26
24
  queueToastForNextPage,
27
25
  } from "./toast.js";
28
- import { openReplyForArticle } from "./compose-launch.js";
29
- import { hydratePartial } from "./hydrate-partial.js";
26
+ import {
27
+ refreshPostCardView,
28
+ refreshPostPageView,
29
+ refreshTimelineThreadView,
30
+ } from "./post-refresh.js";
30
31
  import { getJsonString, readJsonObject } from "./json.js";
31
32
  import { uploadViaSession } from "./upload-session.js";
33
+ import type { UploadSessionResult } from "./upload-session.js";
32
34
  import { publicPath } from "./runtime-paths.js";
33
35
  import { tiptapJsonToMarkdown } from "../lib/tiptap-to-markdown.js";
34
- import { getMediaCategory } from "../lib/upload.js";
36
+ import { getMediaCategory, sniffImageMimeType } from "../lib/upload.js";
35
37
  import {
36
38
  resolveInlineImageUrls,
37
39
  hasPendingInlineImagePlaceholders,
38
40
  } from "./tiptap/inline-image-upload.js";
39
41
 
42
+ /**
43
+ * POST a compose payload, treating a dropped connection as a failed response.
44
+ *
45
+ * A publish that never reaches the server and a publish the server rejects are
46
+ * the same event to the author: the post did not go up. Letting `fetch` reject
47
+ * here would send only one of them down the recovery path, so the request that
48
+ * failed hardest — no connection at all — would be the one that skipped saving
49
+ * a draft.
50
+ *
51
+ * @param url - Compose endpoint to post to
52
+ * @param body - Request body, already serialized
53
+ * @param method - HTTP method, defaulting to POST
54
+ * @returns The response, or null when the request never completed
55
+ * @example
56
+ * const res = await postCompose("/compose/thread", JSON.stringify(threadBody));
57
+ * if (!res?.ok) await saveAsDraftInstead();
58
+ */
59
+ async function postCompose(
60
+ url: string,
61
+ body: string,
62
+ method = "POST",
63
+ ): Promise<globalThis.Response | null> {
64
+ try {
65
+ return await fetch(url, {
66
+ method,
67
+ headers: {
68
+ "Content-Type": "application/json",
69
+ Accept: "application/json",
70
+ },
71
+ body,
72
+ });
73
+ } catch {
74
+ return null;
75
+ }
76
+ }
77
+
78
+ /**
79
+ * What to tell the author when a compose request did not land.
80
+ *
81
+ * Prefers the server's own words, because it knows what was wrong with the
82
+ * post. A request that never completed has no words of its own, so it says so
83
+ * — "no connection" and "the server refused this" call for different next
84
+ * steps from the author.
85
+ *
86
+ * @param res - The response, or null when the request never completed
87
+ * @param labels - Composer labels, absent before the element has upgraded
88
+ * @returns A message ready to show in a toast
89
+ * @example
90
+ * await handleSubmitError(await submitErrorMessage(res, labels));
91
+ */
92
+ async function submitErrorMessage(
93
+ res: globalThis.Response | null,
94
+ labels: JantComposeDialog["labels"] | undefined,
95
+ ): Promise<string> {
96
+ if (!res) {
97
+ return (
98
+ labels?.publishFailedOffline ?? "Couldn't reach the server. Try again."
99
+ );
100
+ }
101
+ const data = await readJsonObject(res);
102
+ return (
103
+ getJsonString(data, "error") ??
104
+ labels?.publishFailed ??
105
+ "Couldn't publish. Try again."
106
+ );
107
+ }
108
+
40
109
  /**
41
110
  * Whether a serialized post body still references inline image placeholders
42
111
  * pending upload or paste-rehost. Drives the "uploading" toast and whether to
@@ -66,104 +135,6 @@ function getComposeDialogFromEventTarget(
66
135
  : null;
67
136
  }
68
137
 
69
- async function fetchPartialHtml(path: string): Promise<string | null> {
70
- const res = await fetch(path, {
71
- headers: { Accept: "text/html" },
72
- });
73
- if (!res.ok) return null;
74
- return res.text();
75
- }
76
-
77
- async function refreshTimelineThreadView(
78
- threadRootId: string,
79
- ): Promise<boolean> {
80
- try {
81
- const timelineItem = document.querySelector<HTMLElement>(
82
- `[data-timeline-item][data-thread-root-id="${threadRootId}"]`,
83
- );
84
- const content = timelineItem?.querySelector<HTMLElement>(
85
- "[data-timeline-item-content]",
86
- );
87
- if (!content) return false;
88
-
89
- const html = await fetchPartialHtml(
90
- `/_/timeline-item/${encodeURIComponent(threadRootId)}`,
91
- );
92
- if (!html) return false;
93
-
94
- content.innerHTML = html;
95
- // Swapped-in markup carries interactions whose per-element setup only runs
96
- // on DOMContentLoaded (thread "Show more" toggle, feed video autoplay, audio
97
- // waveform); re-initialize them or they stay inert until a full reload.
98
- hydratePartial(content);
99
- return true;
100
- } catch {
101
- return false;
102
- }
103
- }
104
-
105
- async function refreshPostCardView(postId: string): Promise<boolean> {
106
- try {
107
- const timelineItem = document
108
- .querySelector<HTMLElement>(`article[data-post-id="${postId}"]`)
109
- ?.closest<HTMLElement>("[data-timeline-item]");
110
- const html = await fetchPartialHtml(
111
- `/_/post-card/${encodeURIComponent(postId)}`,
112
- );
113
- if (!html) return false;
114
-
115
- if (timelineItem) {
116
- const content = timelineItem.querySelector<HTMLElement>(
117
- "[data-timeline-item-content]",
118
- );
119
- if (!content) return false;
120
- content.innerHTML = html;
121
- hydratePartial(content);
122
- return true;
123
- }
124
-
125
- const article = document.querySelector<HTMLElement>(
126
- `article[data-post-id="${postId}"]`,
127
- );
128
- if (!article) return false;
129
-
130
- article.outerHTML = html;
131
- // outerHTML detaches `article`; re-query the replacement to hydrate it.
132
- const nextArticle = document.querySelector<HTMLElement>(
133
- `article[data-post-id="${postId}"]`,
134
- );
135
- if (nextArticle) hydratePartial(nextArticle);
136
- return true;
137
- } catch {
138
- return false;
139
- }
140
- }
141
-
142
- async function refreshPostPageView(postId: string): Promise<boolean> {
143
- try {
144
- const container = document.querySelector<HTMLElement>(
145
- `[data-post-view][data-post-view-id="${postId}"]`,
146
- );
147
- if (!container) return false;
148
-
149
- const html = await fetchPartialHtml(
150
- `/_/post-view/${encodeURIComponent(postId)}`,
151
- );
152
- if (!html) return false;
153
-
154
- container.outerHTML = html;
155
- // outerHTML detaches `container`; re-query the replacement to hydrate it
156
- // (see refreshTimelineThreadView).
157
- const next = document.querySelector<HTMLElement>(
158
- `[data-post-view][data-post-view-id="${postId}"]`,
159
- );
160
- if (next) hydratePartial(next);
161
- return true;
162
- } catch {
163
- return false;
164
- }
165
- }
166
-
167
138
  async function refreshReplyTarget(
168
139
  detail: ComposeSubmitDetail,
169
140
  ): Promise<boolean> {
@@ -233,17 +204,47 @@ function refreshUploadToast() {
233
204
  }
234
205
 
235
206
  /**
236
- * Track completed upload mediaIds by clientId.
207
+ * Track completed upload results by clientId.
208
+ *
209
+ * By the time the user submits, a finished upload's promise has been deleted
210
+ * from `uploadPromises`, so the deferred handler can't look it up. This map
211
+ * provides a fallback: we record the result here as soon as any upload
212
+ * succeeds, and `buildRequestAttachments` reads it when `attachment.mediaId`
213
+ * is null and the clientId isn't in `uploadPromises`.
237
214
  *
238
- * When an attachment is migrated between editor instances (e.g. entering thread
239
- * mode while an upload is in-flight), `updateAttachmentStatus` fires on the old
240
- * detached editor — not the new thread editor. By the time the user submits,
241
- * the upload promise has been deleted from `uploadPromises`, so the deferred
242
- * handler can't look it up. This map provides a fallback: we record the result
243
- * here as soon as any upload succeeds, and `buildRequestAttachments` reads it
244
- * when `attachment.mediaId` is null and the clientId isn't in `uploadPromises`.
215
+ * The full result (not just the id) is kept so a failed publish can restore
216
+ * uploaded attachments into the reopened compose dialog.
245
217
  */
246
- const completedMediaIds = new Map<string, string>();
218
+ const completedUploads = new Map<string, UploadSessionResult>();
219
+
220
+ /**
221
+ * The editor holding an attachment now.
222
+ *
223
+ * Entering or leaving thread mode renders the post in a new editor element,
224
+ * which takes the attachments over from the one it replaced. An upload that
225
+ * outlives the swap has to follow the attachment: reporting to the editor it
226
+ * started in leaves the new one showing "uploading" forever, never showing an
227
+ * error, and keeping the finished file out of the local draft. Until a
228
+ * connected editor holds the attachment — the moment between the swap and the
229
+ * handover — the starting editor is still the one with the latest copy.
230
+ *
231
+ * @param started - The editor the upload was started from
232
+ * @param clientId - The attachment's client-side id
233
+ * @returns The editor to report progress and status to
234
+ */
235
+ function attachmentOwner(
236
+ started: JantComposeEditor | null,
237
+ clientId: string,
238
+ ): JantComposeEditor | null {
239
+ if (!started || started.isConnected) return started;
240
+ const editors = document.querySelectorAll<JantComposeEditor>(
241
+ "jant-compose-editor",
242
+ );
243
+ for (const editor of editors) {
244
+ if (editor.hasAttachment(clientId)) return editor;
245
+ }
246
+ return started;
247
+ }
247
248
 
248
249
  /**
249
250
  * Quickly grab the very first decoded frame of a video as a small poster.
@@ -314,6 +315,19 @@ function captureQuickPoster(file: File): Promise<Blob | null> {
314
315
  });
315
316
  }
316
317
 
318
+ /** Bytes that settle whether a file is HEIC: the `ftyp` box and its brand. */
319
+ const HEIC_SNIFF_BYTES = 12;
320
+
321
+ /**
322
+ * Whether the file's bytes say HEIC/HEIF, whatever its name or type claims.
323
+ * Reads the header only — the decoder's own check pulled the whole file into
324
+ * memory to look at four bytes of it.
325
+ */
326
+ async function isHeicFile(file: File): Promise<boolean> {
327
+ const head = await file.slice(0, HEIC_SNIFF_BYTES).arrayBuffer();
328
+ return sniffImageMimeType(new Uint8Array(head)) === "image/heic";
329
+ }
330
+
317
331
  /**
318
332
  * Upload a single file: process locally, then send it through the upload
319
333
  * session API so the backend can choose relay vs direct transport.
@@ -324,6 +338,7 @@ async function uploadFile(
324
338
  clientId: string,
325
339
  editor: JantComposeEditor | null,
326
340
  ): Promise<string | null> {
341
+ const owner = () => attachmentOwner(editor, clientId);
327
342
  // Capture cheap metadata up-front so we can release `file` (the original
328
343
  // potentially-huge blob) as soon as transcoding finishes. On iOS Safari
329
344
  // holding a 300MB+ source blob alongside the transcoded output, upload
@@ -343,7 +358,7 @@ async function uploadFile(
343
358
  if (fileType.startsWith("video/")) {
344
359
  // Video: transcode with mediabunny (requires WebCodecs)
345
360
  if (!VideoProcessor.isSupported()) {
346
- editor?.updateAttachmentStatus(
361
+ owner()?.updateAttachmentStatus(
347
362
  clientId,
348
363
  "error",
349
364
  null,
@@ -354,14 +369,19 @@ async function uploadFile(
354
369
 
355
370
  // Capture the first frame quickly so Safari shows a preview while
356
371
  // the heavy transcoding runs. Chrome shows it natively via
357
- // <video preload="metadata">, Safari does not.
358
- captureQuickPoster(file).then((blob) => {
359
- if (blob) editor?.updateAttachmentPoster(clientId, blob);
360
- });
361
-
362
- editor?.updateAttachmentStatus(clientId, "processing", null, null);
372
+ // <video preload="metadata">, Safari does not. Kept, rather than shown
373
+ // and dropped: it is also the fallback for the upload below, so the
374
+ // card and the stored media can never disagree about having a poster.
375
+ const quickPoster = captureQuickPoster(file)
376
+ .then((blob) => {
377
+ if (blob) owner()?.updateAttachmentPoster(clientId, blob);
378
+ return blob;
379
+ })
380
+ .catch(() => null);
381
+
382
+ owner()?.updateAttachmentStatus(clientId, "processing", null, null);
363
383
  const result = await VideoProcessor.processToFile(file, (progress) => {
364
- editor?.updateAttachmentProgress(clientId, progress);
384
+ owner()?.updateAttachmentProgress(clientId, progress);
365
385
  });
366
386
  toUpload = result.file;
367
387
  // Drop the original blob ref now that we have the transcoded output.
@@ -370,14 +390,18 @@ async function uploadFile(
370
390
  height = result.height;
371
391
  durationSeconds = result.durationSeconds;
372
392
  blurhash = result.blurhash;
373
- poster = result.poster;
393
+ // The probe's poster is the better one — a frame from a tenth of the way
394
+ // in, at full decode quality. Falling back to the quick one costs a
395
+ // slightly worse still; not falling back costs a thumbnail entirely, on
396
+ // exactly the files whose frames the probe could not read.
397
+ poster = result.poster ?? (await quickPoster) ?? undefined;
374
398
  if (poster) {
375
- editor?.updateAttachmentPoster(clientId, poster);
399
+ owner()?.updateAttachmentPoster(clientId, poster);
376
400
  }
377
401
  } else if (fileType.startsWith("audio/")) {
378
402
  // Audio: transcode to AAC (.m4a) (requires WebCodecs)
379
403
  if (!AudioProcessor.isSupported()) {
380
- editor?.updateAttachmentStatus(
404
+ owner()?.updateAttachmentStatus(
381
405
  clientId,
382
406
  "error",
383
407
  null,
@@ -393,9 +417,9 @@ async function uploadFile(
393
417
  // Waveform extraction is best-effort
394
418
  }
395
419
 
396
- editor?.updateAttachmentStatus(clientId, "processing", null, null);
420
+ owner()?.updateAttachmentStatus(clientId, "processing", null, null);
397
421
  const result = await AudioProcessor.processToFile(file, (progress) => {
398
- editor?.updateAttachmentProgress(clientId, progress);
422
+ owner()?.updateAttachmentProgress(clientId, progress);
399
423
  });
400
424
  toUpload = result.file;
401
425
  file = null as unknown as File;
@@ -407,9 +431,11 @@ async function uploadFile(
407
431
  // Image: convert HEIC/HEIF if needed, then resize + convert to WebP
408
432
  let imageFile = file;
409
433
  try {
410
- const { isHeic, heicTo } = await import("heic-to");
411
- if (await isHeic(imageFile)) {
412
- editor?.updateAttachmentStatus(clientId, "processing", null, null);
434
+ if (await isHeicFile(imageFile)) {
435
+ // heic-to carries libheif — a 3MB chunk — so it is fetched for a
436
+ // file whose bytes say HEIC, not for every photo on the off chance.
437
+ const { heicTo } = await import("heic-to");
438
+ owner()?.updateAttachmentStatus(clientId, "processing", null, null);
413
439
  const blob = await heicTo({
414
440
  blob: imageFile,
415
441
  type: "image/jpeg",
@@ -418,16 +444,17 @@ async function uploadFile(
418
444
  imageFile = new File([blob], fileName.replace(/\.heic$/i, ".jpg"), {
419
445
  type: "image/jpeg",
420
446
  });
421
- editor?.updateAttachmentPreview(clientId, imageFile);
447
+ owner()?.updateAttachmentPreview(clientId, imageFile);
422
448
  }
423
449
  const result = await ImageProcessor.processToFile(imageFile);
424
450
  toUpload = result.file;
425
451
  width = result.width;
426
452
  height = result.height;
453
+ blurhash = result.blurhash;
427
454
  file = null as unknown as File;
428
455
  imageFile = null as unknown as File;
429
456
  } catch {
430
- editor?.removeAttachment(clientId);
457
+ owner()?.removeAttachment(clientId);
431
458
  showToast("Image format not supported.", "error");
432
459
  return null;
433
460
  }
@@ -436,21 +463,7 @@ async function uploadFile(
436
463
  }
437
464
 
438
465
  // Update status to uploading
439
- editor?.updateAttachmentStatus(clientId, "uploading", null, null);
440
-
441
- // Extract metadata for non-video files (video metadata comes from VideoProcessor)
442
- // Audio waveform is already extracted above (before AudioProcessor runs).
443
- if (!fileType.startsWith("video/")) {
444
- const meta = await extractMediaMetadata(toUpload);
445
- width ??= meta.width;
446
- height ??= meta.height;
447
- blurhash ??= meta.blurhash;
448
- waveform ??= meta.waveform;
449
- if (!poster && meta.poster) {
450
- poster = meta.poster;
451
- editor?.updateAttachmentPoster(clientId, poster);
452
- }
453
- }
466
+ owner()?.updateAttachmentStatus(clientId, "uploading", null, null);
454
467
 
455
468
  // Text attachments keep summary/chars in the media record. This covers
456
469
  // plain text-file uploads (.md, .txt, .csv). Jant-composed rich text
@@ -487,7 +500,7 @@ async function uploadFile(
487
500
  chars,
488
501
  },
489
502
  (progress) => {
490
- editor?.updateAttachmentProgress(clientId, progress);
503
+ owner()?.updateAttachmentProgress(clientId, progress);
491
504
  uploadProgress.set(clientId, progress);
492
505
  refreshUploadToast();
493
506
  },
@@ -495,14 +508,20 @@ async function uploadFile(
495
508
 
496
509
  uploadProgress.set(clientId, 1);
497
510
  refreshUploadToast();
498
- editor?.updateAttachmentStatus(clientId, "done", result.id, null);
499
- completedMediaIds.set(clientId, result.id);
511
+ owner()?.updateAttachmentStatus(
512
+ clientId,
513
+ "done",
514
+ result.id,
515
+ null,
516
+ result.url,
517
+ );
518
+ completedUploads.set(clientId, result);
500
519
  return result.id;
501
520
  } catch (error) {
502
521
  uploadProgress.delete(clientId);
503
522
  refreshUploadToast();
504
523
  const message = error instanceof Error ? error.message : "Upload failed";
505
- editor?.updateAttachmentStatus(clientId, "error", null, message);
524
+ owner()?.updateAttachmentStatus(clientId, "error", null, message);
506
525
  // Error is shown on the attachment thumbnail; only toast when there's no editor context.
507
526
  if (!editor) showToast(message, "error");
508
527
  return null;
@@ -516,7 +535,7 @@ document.addEventListener("jant:attachment-removed", (e: Event) => {
516
535
  e as CustomEvent<{ clientId: string; mediaId: string | null }>
517
536
  ).detail;
518
537
 
519
- completedMediaIds.delete(clientId);
538
+ completedUploads.delete(clientId);
520
539
 
521
540
  if (mediaId) {
522
541
  // Upload already finished — fire-and-forget delete
@@ -552,19 +571,6 @@ document.addEventListener("jant:files-selected", (e: Event) => {
552
571
  }
553
572
  });
554
573
 
555
- // ── Reply trigger handler ───────────────────────────────────────────
556
-
557
- document.addEventListener("click", (e: MouseEvent) => {
558
- const trigger = (e.target as HTMLElement).closest<HTMLButtonElement>(
559
- "[data-reply-trigger]",
560
- );
561
- if (!trigger) return;
562
-
563
- const article = trigger.closest<HTMLElement>("article[data-post]");
564
- if (!article) return;
565
- void openReplyForArticle(article);
566
- });
567
-
568
574
  // ── Submit handler ──────────────────────────────────────────────────
569
575
 
570
576
  /** Build the JSON body for both create and update requests */
@@ -628,6 +634,12 @@ function buildPostBody(
628
634
  attachments: attachments.length > 0 ? attachments : undefined,
629
635
  replyToId: detail.replyToId || undefined,
630
636
  quietReply: detail.quietReply || undefined,
637
+ // Absent means the author left the language to detection; the server runs
638
+ // the same detector against the final text.
639
+ language: detail.language || undefined,
640
+ // A translation group is only ever minted on create — an edit that carried
641
+ // this would silently regroup an existing post.
642
+ translationOfId: isEdit ? undefined : detail.translationOfId || undefined,
631
643
  };
632
644
  }
633
645
 
@@ -651,7 +663,7 @@ function buildRequestAttachments(
651
663
  const mediaId =
652
664
  attachment.mediaId ??
653
665
  pendingMediaIds.get(attachment.clientId) ??
654
- completedMediaIds.get(attachment.clientId);
666
+ completedUploads.get(attachment.clientId)?.id;
655
667
  if (!mediaId) continue;
656
668
 
657
669
  requestAttachments.push({
@@ -694,36 +706,54 @@ document.addEventListener("jant:compose-submit-deferred", async (e: Event) => {
694
706
  getComposeDialogFromEventTarget(event.target) ??
695
707
  (document.querySelector("jant-compose-dialog") as JantComposeDialog | null);
696
708
  const isPageMode = !!composeEl?.pageMode;
709
+ const isDraftPreviewPage = !!document.querySelector("[data-preview-status]");
697
710
 
698
711
  // Get labels for toast messages
699
712
  const labels = composeEl?.labels;
700
713
  const uploadingMsg = labels?.uploading ?? "Uploading...";
714
+ const publishingMsg = labels?.publishing ?? "Publishing...";
715
+ const savingMsg = labels?.saving ?? "Saving...";
701
716
  const hasInlinePending = detail.threadPosts
702
717
  ? detail.threadPosts.some((p) => bodyHasPendingInline(p.body))
703
718
  : bodyHasPendingInline(detail.body);
704
719
  const hasPending = detail.pendingAttachments.length > 0 || hasInlinePending;
705
720
  const publishedMsg = labels?.published ?? "Published!";
706
721
  const viewLabel = labels?.view ?? "View";
707
-
708
- // Show persistent toast only when uploads are still in flight
709
- if (hasPending) {
710
- showPersistentToast("compose-deferred", uploadingMsg);
711
- if (detail.pendingAttachments.length > 0) {
712
- activeUploadToast = {
713
- clientIds: detail.pendingAttachments.map((a) => a.clientId),
714
- baseMsg: uploadingMsg,
715
- };
716
- refreshUploadToast();
717
- }
722
+ const isEdit = !!detail.editPostId;
723
+ const isThread = !!(detail.threadPosts && detail.threadPosts.length >= 2);
724
+ // Publishing a draft goes through the edit endpoint but still ends published.
725
+ const willPublish =
726
+ detail.status === "published" && (!isEdit || !!detail.draftSourceId);
727
+
728
+ // The composer closes the moment it dispatches, so this toast is the only
729
+ // sign that work is still running. Uploads name themselves and grow a
730
+ // percentage; everything else says what it is doing — a Link post can sit on
731
+ // the server for seconds while its preview thumbnail is fetched and stored.
732
+ let progressToastOpen = true;
733
+ showPersistentToast(
734
+ "compose-deferred",
735
+ hasPending ? uploadingMsg : willPublish ? publishingMsg : savingMsg,
736
+ );
737
+ if (detail.pendingAttachments.length > 0) {
738
+ activeUploadToast = {
739
+ clientIds: detail.pendingAttachments.map((a) => a.clientId),
740
+ baseMsg: uploadingMsg,
741
+ };
742
+ refreshUploadToast();
718
743
  }
719
744
 
720
- /** Show result toast — replaces persistent toast if one exists, otherwise shows a new one */
745
+ /**
746
+ * Show a result toast. The first one takes over the progress toast in place;
747
+ * a later message (a retry result, say) gets its own toast instead of
748
+ * overwriting one that has not been read yet.
749
+ */
721
750
  const toastMsg = (msg: string, type: "success" | "error" = "success") => {
722
751
  if (activeUploadToast) {
723
752
  for (const id of activeUploadToast.clientIds) uploadProgress.delete(id);
724
753
  activeUploadToast = null;
725
754
  }
726
- if (hasPending) {
755
+ if (progressToastOpen) {
756
+ progressToastOpen = false;
727
757
  replaceWithAutoClose("compose-deferred", msg, type);
728
758
  } else {
729
759
  showToast(msg, type);
@@ -751,12 +781,48 @@ document.addEventListener("jant:compose-submit-deferred", async (e: Event) => {
751
781
  }
752
782
  composeEl?.clearLocalDraftFromStorage?.();
753
783
  };
784
+ /**
785
+ * Uploads that completed before the publish failed. The local draft's own
786
+ * media snapshot misses uploads that finished after the dialog closed, so
787
+ * the reopened compose merges these over it.
788
+ */
789
+ const collectRestorableMedia = (): LocalDraftMedia[] => {
790
+ const details = detail.threadPosts?.length ? detail.threadPosts : [detail];
791
+ const media: LocalDraftMedia[] = [];
792
+ const seen = new Set<string>();
793
+ for (const d of details) {
794
+ for (const attachment of d.attachments) {
795
+ if (attachment.type !== "media" || seen.has(attachment.clientId)) {
796
+ continue;
797
+ }
798
+ seen.add(attachment.clientId);
799
+ const upload = completedUploads.get(attachment.clientId);
800
+ if (!upload) continue;
801
+ media.push({
802
+ clientId: attachment.clientId,
803
+ mediaId: upload.id,
804
+ url: upload.url,
805
+ mimeType: upload.mimeType,
806
+ name: upload.filename || undefined,
807
+ alt: attachment.alt || undefined,
808
+ });
809
+ }
810
+ }
811
+ return media;
812
+ };
813
+
754
814
  const reopenComposeAfterFailure = async () => {
755
815
  if (!composeEl || isPageMode) return;
756
816
 
817
+ if (detail.draftSourceId) {
818
+ if (typeof composeEl.openDraft !== "function") return;
819
+ await composeEl.openDraft(detail.draftSourceId);
820
+ return;
821
+ }
822
+
757
823
  if (isEdit && detail.editPostId) {
758
824
  if (typeof composeEl.openEdit !== "function") return;
759
- await composeEl.openEdit(detail.editPostId);
825
+ await composeEl.openEdit(detail.editPostId, { restoreToast: false });
760
826
  return;
761
827
  }
762
828
 
@@ -772,13 +838,22 @@ document.addEventListener("jant:compose-submit-deferred", async (e: Event) => {
772
838
  id: detail.replyRefreshId,
773
839
  }
774
840
  : undefined,
775
- { restoreDraft: true, initialFormat: detail.format },
841
+ {
842
+ restoreDraft: true,
843
+ initialFormat: detail.format,
844
+ restoreToast: false,
845
+ restoreMedia: collectRestorableMedia(),
846
+ },
776
847
  );
777
848
  return;
778
849
  }
779
850
 
780
851
  if (typeof composeEl.openNew !== "function") return;
781
- await composeEl.openNew({ restoreDraft: true });
852
+ await composeEl.openNew({
853
+ restoreDraft: true,
854
+ restoreToast: false,
855
+ restoreMedia: collectRestorableMedia(),
856
+ });
782
857
  };
783
858
  const handleSubmitError = async (message: string) => {
784
859
  clearPageLoading();
@@ -801,8 +876,6 @@ document.addEventListener("jant:compose-submit-deferred", async (e: Event) => {
801
876
  globalThis.location.assign(composeEl.closeHref || publicPath("/"));
802
877
  return true;
803
878
  };
804
- const isEdit = !!detail.editPostId;
805
- const isThread = !!(detail.threadPosts && detail.threadPosts.length >= 2);
806
879
  let draftFallback: "upload" | "server" | null = null;
807
880
 
808
881
  try {
@@ -818,10 +891,10 @@ document.addEventListener("jant:compose-submit-deferred", async (e: Event) => {
818
891
  const results = await Promise.all(pendingPromises);
819
892
 
820
893
  // If any pending upload failed (null result where we expected a mediaId
821
- // AND the upload wasn't already tracked in completedMediaIds):
894
+ // AND the upload wasn't already tracked in completedUploads):
822
895
  const failedCount = results.filter(
823
896
  (id, i) =>
824
- id === null && !completedMediaIds.has(pendingClientIds[i] ?? ""),
897
+ id === null && !completedUploads.has(pendingClientIds[i] ?? ""),
825
898
  ).length;
826
899
  if (failedCount > 0) {
827
900
  if (detail.status === "published" && !isEdit) {
@@ -872,30 +945,23 @@ document.addEventListener("jant:compose-submit-deferred", async (e: Event) => {
872
945
  threadBody.replaceThreadId = detail.editPostId;
873
946
  }
874
947
 
875
- const res = await fetch("/compose/thread", {
876
- method: "POST",
877
- headers: {
878
- "Content-Type": "application/json",
879
- Accept: "application/json",
880
- },
881
- body: JSON.stringify(threadBody),
882
- });
948
+ const res = await postCompose(
949
+ "/compose/thread",
950
+ JSON.stringify(threadBody),
951
+ );
883
952
 
884
- if (!res.ok) {
885
- // Server error on publish: retry as draft
953
+ if (!res?.ok) {
954
+ // The publish did not land — whether the server refused it or the
955
+ // request never arrived. Try to keep the work as a draft either way.
886
956
  if (detail.status === "published" && !draftFallback) {
887
957
  const retryPayload = {
888
958
  posts: postsPayload.map((p) => ({ ...p, status: "draft" })),
889
959
  };
890
- const retryRes = await fetch("/compose/thread", {
891
- method: "POST",
892
- headers: {
893
- "Content-Type": "application/json",
894
- Accept: "application/json",
895
- },
896
- body: JSON.stringify(retryPayload),
897
- });
898
- if (retryRes.ok) {
960
+ const retryRes = await postCompose(
961
+ "/compose/thread",
962
+ JSON.stringify(retryPayload),
963
+ );
964
+ if (retryRes?.ok) {
899
965
  draftFallback = "server";
900
966
  clearRecoveredLocalDraft();
901
967
  const fallbackMsg =
@@ -906,10 +972,7 @@ document.addEventListener("jant:compose-submit-deferred", async (e: Event) => {
906
972
  return;
907
973
  }
908
974
  }
909
- const data = await readJsonObject(res);
910
- await handleSubmitError(
911
- getJsonString(data, "error") ?? "Something went wrong",
912
- );
975
+ await handleSubmitError(await submitErrorMessage(res, labels));
913
976
  return;
914
977
  }
915
978
 
@@ -930,7 +993,12 @@ document.addEventListener("jant:compose-submit-deferred", async (e: Event) => {
930
993
 
931
994
  if (threadStatus === "published") {
932
995
  clearRecoveredLocalDraft();
933
- if (isPageMode) {
996
+ if (isDraftPreviewPage && detail.draftSourceId) {
997
+ queueSuccessToast(publishedMsg);
998
+ globalThis.location.assign(
999
+ threadPermalink ?? globalThis.location.pathname,
1000
+ );
1001
+ } else if (isPageMode) {
934
1002
  await refreshComposeCollections();
935
1003
  resetPageCompose();
936
1004
  toastMsg(publishedMsg);
@@ -945,6 +1013,11 @@ document.addEventListener("jant:compose-submit-deferred", async (e: Event) => {
945
1013
  }
946
1014
  } else {
947
1015
  clearRecoveredLocalDraft();
1016
+ if (isDraftPreviewPage && detail.draftSourceId) {
1017
+ queueSuccessToast(threadToast ?? "Draft saved.");
1018
+ globalThis.location.assign(globalThis.location.pathname);
1019
+ return;
1020
+ }
948
1021
  await refreshComposeCollections();
949
1022
  if (!leavePageAfterConfirmSave()) resetPageCompose();
950
1023
  toastMsg(threadToast ?? "Draft saved.");
@@ -982,29 +1055,24 @@ document.addEventListener("jant:compose-submit-deferred", async (e: Event) => {
982
1055
  requestAttachments,
983
1056
  );
984
1057
 
985
- const res = await fetch(endpoint, {
1058
+ const res = await postCompose(
1059
+ endpoint,
1060
+ JSON.stringify(bodyPayload),
986
1061
  method,
987
- headers: {
988
- "Content-Type": "application/json",
989
- Accept: "application/json",
990
- },
991
- body: JSON.stringify(bodyPayload),
992
- });
1062
+ );
993
1063
 
994
- if (!res.ok) {
995
- // Server error on a new publish: retry as draft
1064
+ if (!res?.ok) {
1065
+ // As in thread mode: a refused request and a request that never arrived
1066
+ // both mean the post is not up, so both get the save-as-draft attempt.
996
1067
  if (detail.status === "published" && !isEdit && !draftFallback) {
997
1068
  const retryPayload = { ...bodyPayload, status: "draft" };
998
- const retryRes = await fetch(endpoint, {
1069
+ const retryRes = await postCompose(
1070
+ endpoint,
1071
+ JSON.stringify(retryPayload),
999
1072
  method,
1000
- headers: {
1001
- "Content-Type": "application/json",
1002
- Accept: "application/json",
1003
- },
1004
- body: JSON.stringify(retryPayload),
1005
- });
1073
+ );
1006
1074
 
1007
- if (retryRes.ok) {
1075
+ if (retryRes?.ok) {
1008
1076
  draftFallback = "server";
1009
1077
  clearRecoveredLocalDraft();
1010
1078
  const retryData = await readJsonObject(retryRes);
@@ -1021,10 +1089,7 @@ document.addEventListener("jant:compose-submit-deferred", async (e: Event) => {
1021
1089
  }
1022
1090
  }
1023
1091
 
1024
- const data = await readJsonObject(res);
1025
- await handleSubmitError(
1026
- getJsonString(data, "error") ?? "Something went wrong",
1027
- );
1092
+ await handleSubmitError(await submitErrorMessage(res, labels));
1028
1093
  return;
1029
1094
  }
1030
1095
 
@@ -1033,21 +1098,50 @@ document.addEventListener("jant:compose-submit-deferred", async (e: Event) => {
1033
1098
  const editData = await readJsonObject(res);
1034
1099
  const editPostId = detail.editPostId ?? "";
1035
1100
  const newSlug = getJsonString(editData, "slug");
1036
- const newPath = newSlug ? publicPath(`/${newSlug}`) : null;
1101
+ const nextStatus = getJsonString(editData, "status") ?? detail.status;
1102
+ const newPath = newSlug
1103
+ ? publicPath(
1104
+ nextStatus === "draft" ? `/preview/${newSlug}` : `/${newSlug}`,
1105
+ )
1106
+ : null;
1107
+ const editSuccessMessage =
1108
+ nextStatus === "draft"
1109
+ ? "Draft saved."
1110
+ : detail.draftSourceId
1111
+ ? publishedMsg
1112
+ : "Post updated.";
1113
+
1114
+ // Draft preview pages cannot use the public post-view refresh endpoint.
1115
+ // Reload the authenticated preview so saved content stays visible.
1116
+ if (nextStatus === "draft") {
1117
+ if (isDraftPreviewPage) {
1118
+ queueSuccessToast(editSuccessMessage);
1119
+ globalThis.location.assign(newPath ?? globalThis.location.pathname);
1120
+ return;
1121
+ }
1122
+
1123
+ // Drafts opened from the compose panel stay in that workflow. The
1124
+ // completion event lets a pending "save and open drafts" action fetch
1125
+ // the updated list only after the write has finished.
1126
+ await refreshComposeCollections();
1127
+ toastMsg(editSuccessMessage);
1128
+ dispatchSubmitComplete(composeEl, "draft");
1129
+ return;
1130
+ }
1037
1131
 
1038
1132
  if (isPageMode) {
1039
1133
  // On the post detail page: refresh or navigate if slug changed
1040
1134
  if (newPath && newPath !== globalThis.location.pathname) {
1041
- queueSuccessToast("Post updated.");
1135
+ queueSuccessToast(editSuccessMessage);
1042
1136
  globalThis.location.assign(newPath);
1043
1137
  } else {
1044
1138
  const refreshed =
1045
1139
  editPostId && (await refreshPostPageView(editPostId));
1046
1140
  if (!refreshed) {
1047
- queueSuccessToast("Post updated.");
1141
+ queueSuccessToast(editSuccessMessage);
1048
1142
  globalThis.location.assign(globalThis.location.pathname);
1049
1143
  } else {
1050
- toastMsg("Post updated.");
1144
+ toastMsg(editSuccessMessage);
1051
1145
  }
1052
1146
  }
1053
1147
  } else if (editPostId) {
@@ -1057,15 +1151,15 @@ document.addEventListener("jant:compose-submit-deferred", async (e: Event) => {
1057
1151
  );
1058
1152
  if (postView) {
1059
1153
  if (newPath && newPath !== globalThis.location.pathname) {
1060
- queueSuccessToast("Post updated.");
1154
+ queueSuccessToast(editSuccessMessage);
1061
1155
  globalThis.location.assign(newPath);
1062
1156
  return;
1063
1157
  }
1064
1158
  const refreshed = await refreshPostPageView(editPostId);
1065
1159
  if (refreshed) {
1066
- toastMsg("Post updated.");
1160
+ toastMsg(editSuccessMessage);
1067
1161
  } else {
1068
- queueSuccessToast("Post updated.");
1162
+ queueSuccessToast(editSuccessMessage);
1069
1163
  globalThis.location.reload();
1070
1164
  }
1071
1165
  } else {
@@ -1080,14 +1174,14 @@ document.addEventListener("jant:compose-submit-deferred", async (e: Event) => {
1080
1174
  ? await refreshTimelineThreadView(threadRootId)
1081
1175
  : await refreshPostCardView(editPostId);
1082
1176
  if (refreshed) {
1083
- toastMsg("Post updated.");
1177
+ toastMsg(editSuccessMessage);
1084
1178
  } else {
1085
- queueSuccessToast("Post updated.");
1179
+ queueSuccessToast(editSuccessMessage);
1086
1180
  globalThis.location.reload();
1087
1181
  }
1088
1182
  }
1089
1183
  } else {
1090
- queueSuccessToast("Post updated.");
1184
+ queueSuccessToast(editSuccessMessage);
1091
1185
  globalThis.location.reload();
1092
1186
  }
1093
1187
  return;
@@ -1147,7 +1241,7 @@ document.addEventListener("jant:compose-submit-deferred", async (e: Event) => {
1147
1241
  dispatchSubmitComplete(composeEl, "draft");
1148
1242
  }
1149
1243
  } catch {
1150
- await handleSubmitError("Something went wrong");
1244
+ await handleSubmitError(await submitErrorMessage(null, labels));
1151
1245
  }
1152
1246
  });
1153
1247