@jant/core 0.6.15 → 0.6.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/commands/import-site.js +7 -0
- package/dist/app-C75Zc8dD.js +6 -0
- package/dist/{app-C-oi_t8W.js → app-C7_4oab9.js} +480 -196
- package/dist/client/.vite/manifest.json +8 -8
- package/dist/client/_assets/chunks/{url-pLre2DM_.js → url-CU9pEbk5.js} +1 -1
- package/dist/client/_assets/{client-DxY5BFe8.js → client-Bq0vre8Y.js} +3 -3
- package/dist/client/_assets/client-D3VZyG4L.css +2 -0
- package/dist/client/_assets/{client-auth-CakPESv9.js → client-auth-F2rb9WfF.js} +1015 -977
- package/dist/{env-OHRKGcMj.js → env-BPYViDJJ.js} +1 -1
- package/dist/{export-CsFx6F_M.js → export-Dl5KCiEs.js} +93 -7
- package/dist/{github-api-BgSiE71w.js → github-api-BNF35Lkx.js} +1 -1
- package/dist/{github-app-BbklkFmU.js → github-app-DdiD-bC4.js} +1 -1
- package/dist/{github-sync-ppWXN3jb.js → github-sync-Bqg62IvV.js} +3 -3
- package/dist/{github-sync-Cq1pzzOI.js → github-sync-HdK2EgvJ.js} +3 -3
- package/dist/index.js +5 -5
- package/dist/node.js +6 -6
- package/dist/{url-BMYO-Zlt.js → url-CbLAtlZe.js} +517 -12
- package/package.json +1 -1
- package/src/__tests__/mise-config.test.ts +22 -0
- package/src/client/__tests__/compose-bridge.test.ts +252 -2
- package/src/client/__tests__/compose-shortcuts.test.ts +20 -0
- package/src/client/__tests__/toast.test.ts +65 -1
- package/src/client/components/__tests__/compose-format-convert.test.ts +0 -19
- package/src/client/components/__tests__/jant-command-palette.test.ts +51 -0
- package/src/client/components/__tests__/jant-compose-dialog.test.ts +2015 -318
- package/src/client/components/__tests__/jant-compose-editor.test.ts +53 -62
- package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +35 -89
- package/src/client/components/compose-format-convert.ts +5 -1
- package/src/client/components/compose-types.ts +47 -3
- package/src/client/components/jant-command-palette.ts +12 -6
- package/src/client/components/jant-compose-dialog.ts +1464 -680
- package/src/client/components/jant-compose-editor.ts +225 -93
- package/src/client/components/jant-compose-fullscreen.ts +3 -5
- package/src/client/components/jant-post-menu.ts +91 -21
- package/src/client/compose-bridge.ts +143 -122
- package/src/client/compose-launch.ts +13 -0
- package/src/client/compose-shortcuts.ts +5 -1
- package/src/client/post-refresh.ts +135 -0
- package/src/client/tiptap/__tests__/link-input-rules.test.ts +121 -0
- package/src/client/tiptap/link-input-rules.ts +4 -4
- package/src/client/toast.ts +130 -38
- package/src/db/__tests__/backfill-thread-activity.test.ts +234 -0
- package/src/db/__tests__/thread-activity.test.ts +113 -0
- package/src/db/backfills/0005_split_thread_activity_from_quiet_replies.sql +68 -0
- package/src/db/migrations/0031_many_ego.sql +3 -0
- package/src/db/migrations/meta/0031_snapshot.json +2562 -0
- package/src/db/migrations/meta/_journal.json +7 -0
- package/src/db/migrations/pg/0029_rare_hairball.sql +3 -0
- package/src/db/migrations/pg/meta/0029_snapshot.json +3289 -0
- package/src/db/migrations/pg/meta/_journal.json +7 -0
- package/src/db/pg/schema.ts +18 -0
- package/src/db/schema.ts +20 -0
- package/src/db/thread-activity.ts +101 -0
- package/src/i18n/locales/public/en.po +89 -21
- package/src/i18n/locales/public/en.ts +1 -1
- package/src/i18n/locales/public/zh-Hans.po +88 -20
- package/src/i18n/locales/public/zh-Hans.ts +1 -1
- package/src/i18n/locales/public/zh-Hant.po +88 -20
- package/src/i18n/locales/public/zh-Hant.ts +1 -1
- package/src/lib/__tests__/feed.test.ts +168 -0
- package/src/lib/__tests__/markdown.test.ts +16 -0
- package/src/lib/__tests__/slug.test.ts +18 -0
- package/src/lib/__tests__/translit.test.ts +87 -0
- package/src/lib/__tests__/url.test.ts +53 -0
- package/src/lib/feed.ts +113 -11
- package/src/lib/hugo-markdown.ts +5 -0
- package/src/lib/link-preview.ts +25 -0
- package/src/lib/markdown-manager.ts +6 -1
- package/src/lib/post-display.ts +42 -9
- package/src/lib/timeline.ts +39 -9
- package/src/lib/tiptap-render.ts +4 -2
- package/src/lib/translit.ts +288 -0
- package/src/lib/url.ts +123 -12
- package/src/lib/view.ts +28 -0
- package/src/routes/api/__tests__/palette.test.ts +44 -0
- package/src/routes/api/posts.ts +13 -9
- package/src/routes/api/public/__tests__/posts.test.ts +38 -0
- package/src/routes/api/public/posts.ts +7 -0
- package/src/routes/compose.tsx +6 -2
- package/src/routes/feed/__tests__/sitemap.test.ts +68 -1
- package/src/routes/pages/__tests__/archive-params.test.ts +292 -0
- package/src/routes/pages/__tests__/featured.test.ts +6 -2
- package/src/routes/pages/__tests__/preview.test.ts +3 -1
- package/src/routes/pages/archive.tsx +65 -17
- package/src/routes/pages/page.tsx +5 -2
- package/src/services/__tests__/collection.test.ts +32 -1
- package/src/services/__tests__/post-timeline.test.ts +142 -11
- package/src/services/__tests__/post.test.ts +277 -0
- package/src/services/collection.ts +8 -10
- package/src/services/export-theme/assets/client-site.css +1 -1
- package/src/services/export.ts +3 -5
- package/src/services/path.ts +4 -1
- package/src/services/post.ts +297 -100
- package/src/services/search.ts +9 -0
- package/src/styles/components.css +0 -14
- package/src/styles/site-media.css +4 -4
- package/src/styles/tokens.css +62 -1
- package/src/styles/ui.css +1363 -1283
- package/src/types/entities.ts +5 -0
- package/src/types/props.ts +12 -0
- package/src/types/views.ts +6 -0
- package/src/ui/compose/ComposeDialog.tsx +17 -17
- package/src/ui/feed/LinkPreview.tsx +2 -7
- package/src/ui/feed/PostStatusBadges.tsx +42 -3
- package/src/ui/pages/ArchivePage.tsx +131 -24
- package/src/ui/pages/__tests__/ArchivePage.test.tsx +63 -0
- package/src/ui/shared/DraftPreviewBar.tsx +25 -9
- package/src/ui/shared/PostFooter.tsx +35 -12
- package/src/ui/shared/custom-icons.ts +5 -0
- package/src/ui/shared/post-article-attributes.ts +6 -0
- package/dist/app-BxCOR3Uc.js +0 -6
- package/dist/client/_assets/client-B_eGKN5p.css +0 -2
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
FEATURED_SPARKLE_PATH,
|
|
17
17
|
} from "../../lib/featured-icons.js";
|
|
18
18
|
import { showConfirmDialog } from "../confirm.js";
|
|
19
|
+
import { refreshArticleView } from "../post-refresh.js";
|
|
19
20
|
import { showToast } from "../toast.js";
|
|
20
21
|
import { publicPath } from "../runtime-paths.js";
|
|
21
22
|
import {
|
|
@@ -33,6 +34,7 @@ interface PostMenuData {
|
|
|
33
34
|
featured: boolean;
|
|
34
35
|
visibility: string;
|
|
35
36
|
isReply: boolean;
|
|
37
|
+
isDraft: boolean;
|
|
36
38
|
}
|
|
37
39
|
|
|
38
40
|
interface CollectionItem {
|
|
@@ -104,6 +106,7 @@ function readPostMenuData(article: HTMLElement): PostMenuData | null {
|
|
|
104
106
|
featured: article.hasAttribute("data-post-featured"),
|
|
105
107
|
visibility: article.dataset.postVisibility ?? "public",
|
|
106
108
|
isReply: threadId !== id || article.hasAttribute("data-post-reply"),
|
|
109
|
+
isDraft: article.hasAttribute("data-post-draft"),
|
|
107
110
|
};
|
|
108
111
|
}
|
|
109
112
|
|
|
@@ -529,6 +532,36 @@ export class JantPostMenu extends LitElement {
|
|
|
529
532
|
}
|
|
530
533
|
}
|
|
531
534
|
|
|
535
|
+
async #publish() {
|
|
536
|
+
if (!this._data) return;
|
|
537
|
+
const postId = this._data.id;
|
|
538
|
+
const trigger = this._triggerEl;
|
|
539
|
+
this.#close({ restoreFocus: false });
|
|
540
|
+
|
|
541
|
+
const article = document.querySelector<HTMLElement>(
|
|
542
|
+
`article[data-post-id="${postId}"]`,
|
|
543
|
+
);
|
|
544
|
+
|
|
545
|
+
try {
|
|
546
|
+
const res = await fetch(`/api/posts/${postId}`, {
|
|
547
|
+
method: "PUT",
|
|
548
|
+
headers: { "Content-Type": "application/json" },
|
|
549
|
+
body: JSON.stringify({ status: "published" }),
|
|
550
|
+
});
|
|
551
|
+
if (!res.ok) throw new Error();
|
|
552
|
+
|
|
553
|
+
showToast("Draft published.");
|
|
554
|
+
// The badge, the timestamp, and any drafts published alongside this one
|
|
555
|
+
// all change at once — re-render the surface rather than patching it.
|
|
556
|
+
if (!article || !(await refreshArticleView(article))) {
|
|
557
|
+
globalThis.location.reload();
|
|
558
|
+
}
|
|
559
|
+
} catch {
|
|
560
|
+
showToast("Could not publish the draft. Try again.", "error");
|
|
561
|
+
trigger?.focus();
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
532
565
|
#setVisibility(newVisibility: string) {
|
|
533
566
|
if (!this._data) return;
|
|
534
567
|
const postId = this._data.id;
|
|
@@ -711,8 +744,10 @@ export class JantPostMenu extends LitElement {
|
|
|
711
744
|
this.#close({ restoreFocus: false });
|
|
712
745
|
|
|
713
746
|
const confirmed = await showConfirmDialog({
|
|
714
|
-
message:
|
|
715
|
-
|
|
747
|
+
message: this._data.isDraft
|
|
748
|
+
? "Discard this draft permanently? This can't be undone."
|
|
749
|
+
: "Delete this post permanently? This can't be undone.",
|
|
750
|
+
confirmLabel: this._data.isDraft ? "Discard" : "Delete",
|
|
716
751
|
cancelLabel: "Cancel",
|
|
717
752
|
tone: "danger",
|
|
718
753
|
});
|
|
@@ -992,6 +1027,21 @@ export class JantPostMenu extends LitElement {
|
|
|
992
1027
|
|
|
993
1028
|
// --- Icons (inline SVG) ---
|
|
994
1029
|
|
|
1030
|
+
#iconPublish() {
|
|
1031
|
+
return html`<svg
|
|
1032
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
1033
|
+
viewBox="0 0 24 24"
|
|
1034
|
+
fill="none"
|
|
1035
|
+
stroke="currentColor"
|
|
1036
|
+
stroke-width="1.75"
|
|
1037
|
+
stroke-linecap="round"
|
|
1038
|
+
stroke-linejoin="round"
|
|
1039
|
+
>
|
|
1040
|
+
<path d="M12 19V5" />
|
|
1041
|
+
<path d="m5 12 7-7 7 7" />
|
|
1042
|
+
</svg>`;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
995
1045
|
#iconEdit() {
|
|
996
1046
|
return html`<svg
|
|
997
1047
|
xmlns="http://www.w3.org/2000/svg"
|
|
@@ -1501,6 +1551,21 @@ export class JantPostMenu extends LitElement {
|
|
|
1501
1551
|
<span class="post-menu-item-trailing">${this.#iconEdit()}</span>
|
|
1502
1552
|
</button>
|
|
1503
1553
|
|
|
1554
|
+
${this._data.isDraft
|
|
1555
|
+
? html`
|
|
1556
|
+
<button
|
|
1557
|
+
type="button"
|
|
1558
|
+
role="menuitem"
|
|
1559
|
+
class="post-menu-item"
|
|
1560
|
+
@click=${() => this.#publish()}
|
|
1561
|
+
>
|
|
1562
|
+
<span class="post-menu-item-label">Publish</span>
|
|
1563
|
+
<span class="post-menu-item-trailing"
|
|
1564
|
+
>${this.#iconPublish()}</span
|
|
1565
|
+
>
|
|
1566
|
+
</button>
|
|
1567
|
+
`
|
|
1568
|
+
: nothing}
|
|
1504
1569
|
${this._data.isReply
|
|
1505
1570
|
? nothing
|
|
1506
1571
|
: html`
|
|
@@ -1539,24 +1604,27 @@ export class JantPostMenu extends LitElement {
|
|
|
1539
1604
|
</div>
|
|
1540
1605
|
|
|
1541
1606
|
<div class="post-menu-section">
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1607
|
+
${this._data.isDraft
|
|
1608
|
+
? nothing
|
|
1609
|
+
: html`
|
|
1610
|
+
<button
|
|
1611
|
+
type="button"
|
|
1612
|
+
role="menuitem"
|
|
1613
|
+
class="post-menu-item"
|
|
1614
|
+
@click=${() => this.#setFeatured(!isFeatured)}
|
|
1615
|
+
>
|
|
1616
|
+
<span class="post-menu-item-label"
|
|
1617
|
+
>${isFeatured
|
|
1618
|
+
? "Remove from Featured"
|
|
1619
|
+
: "Add to Featured"}</span
|
|
1620
|
+
>
|
|
1621
|
+
<span class="post-menu-item-trailing"
|
|
1622
|
+
>${isFeatured
|
|
1623
|
+
? this.#iconFeaturedOff()
|
|
1624
|
+
: this.#iconFeatured()}</span
|
|
1625
|
+
>
|
|
1626
|
+
</button>
|
|
1627
|
+
`}
|
|
1560
1628
|
${this._data.isReply
|
|
1561
1629
|
? nothing
|
|
1562
1630
|
: html`
|
|
@@ -1604,7 +1672,9 @@ export class JantPostMenu extends LitElement {
|
|
|
1604
1672
|
class="post-menu-item post-menu-item-danger"
|
|
1605
1673
|
@click=${() => this.#delete()}
|
|
1606
1674
|
>
|
|
1607
|
-
<span class="post-menu-item-label"
|
|
1675
|
+
<span class="post-menu-item-label"
|
|
1676
|
+
>${this._data.isDraft ? "Discard" : "Delete"}</span
|
|
1677
|
+
>
|
|
1608
1678
|
<span class="post-menu-item-trailing">${this.#iconTrash()}</span>
|
|
1609
1679
|
</button>
|
|
1610
1680
|
</div>
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import type { ComposeSubmitDetail } from "./components/compose-types.js";
|
|
9
9
|
import type { ComposeAttachment } from "./components/compose-types.js";
|
|
10
10
|
import type { ComposeSubmitAttachment } from "./components/compose-types.js";
|
|
11
|
+
import type { LocalDraftMedia } from "./components/compose-types.js";
|
|
11
12
|
import type { JantComposeDialog } from "./components/jant-compose-dialog.js";
|
|
12
13
|
import type { JantComposeEditor } from "./components/jant-compose-editor.js";
|
|
13
14
|
import type { PostAttachmentInput } from "../types.js";
|
|
@@ -25,10 +26,15 @@ import {
|
|
|
25
26
|
replaceWithAutoClose,
|
|
26
27
|
queueToastForNextPage,
|
|
27
28
|
} from "./toast.js";
|
|
28
|
-
import { openReplyForArticle } from "./compose-launch.js";
|
|
29
|
-
import {
|
|
29
|
+
import { getComposeDialog, openReplyForArticle } from "./compose-launch.js";
|
|
30
|
+
import {
|
|
31
|
+
refreshPostCardView,
|
|
32
|
+
refreshPostPageView,
|
|
33
|
+
refreshTimelineThreadView,
|
|
34
|
+
} from "./post-refresh.js";
|
|
30
35
|
import { getJsonString, readJsonObject } from "./json.js";
|
|
31
36
|
import { uploadViaSession } from "./upload-session.js";
|
|
37
|
+
import type { UploadSessionResult } from "./upload-session.js";
|
|
32
38
|
import { publicPath } from "./runtime-paths.js";
|
|
33
39
|
import { tiptapJsonToMarkdown } from "../lib/tiptap-to-markdown.js";
|
|
34
40
|
import { getMediaCategory } from "../lib/upload.js";
|
|
@@ -66,104 +72,6 @@ function getComposeDialogFromEventTarget(
|
|
|
66
72
|
: null;
|
|
67
73
|
}
|
|
68
74
|
|
|
69
|
-
async function fetchPartialHtml(path: string): Promise<string | null> {
|
|
70
|
-
const res = await fetch(path, {
|
|
71
|
-
headers: { Accept: "text/html" },
|
|
72
|
-
});
|
|
73
|
-
if (!res.ok) return null;
|
|
74
|
-
return res.text();
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
async function refreshTimelineThreadView(
|
|
78
|
-
threadRootId: string,
|
|
79
|
-
): Promise<boolean> {
|
|
80
|
-
try {
|
|
81
|
-
const timelineItem = document.querySelector<HTMLElement>(
|
|
82
|
-
`[data-timeline-item][data-thread-root-id="${threadRootId}"]`,
|
|
83
|
-
);
|
|
84
|
-
const content = timelineItem?.querySelector<HTMLElement>(
|
|
85
|
-
"[data-timeline-item-content]",
|
|
86
|
-
);
|
|
87
|
-
if (!content) return false;
|
|
88
|
-
|
|
89
|
-
const html = await fetchPartialHtml(
|
|
90
|
-
`/_/timeline-item/${encodeURIComponent(threadRootId)}`,
|
|
91
|
-
);
|
|
92
|
-
if (!html) return false;
|
|
93
|
-
|
|
94
|
-
content.innerHTML = html;
|
|
95
|
-
// Swapped-in markup carries interactions whose per-element setup only runs
|
|
96
|
-
// on DOMContentLoaded (thread "Show more" toggle, feed video autoplay, audio
|
|
97
|
-
// waveform); re-initialize them or they stay inert until a full reload.
|
|
98
|
-
hydratePartial(content);
|
|
99
|
-
return true;
|
|
100
|
-
} catch {
|
|
101
|
-
return false;
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
async function refreshPostCardView(postId: string): Promise<boolean> {
|
|
106
|
-
try {
|
|
107
|
-
const timelineItem = document
|
|
108
|
-
.querySelector<HTMLElement>(`article[data-post-id="${postId}"]`)
|
|
109
|
-
?.closest<HTMLElement>("[data-timeline-item]");
|
|
110
|
-
const html = await fetchPartialHtml(
|
|
111
|
-
`/_/post-card/${encodeURIComponent(postId)}`,
|
|
112
|
-
);
|
|
113
|
-
if (!html) return false;
|
|
114
|
-
|
|
115
|
-
if (timelineItem) {
|
|
116
|
-
const content = timelineItem.querySelector<HTMLElement>(
|
|
117
|
-
"[data-timeline-item-content]",
|
|
118
|
-
);
|
|
119
|
-
if (!content) return false;
|
|
120
|
-
content.innerHTML = html;
|
|
121
|
-
hydratePartial(content);
|
|
122
|
-
return true;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
const article = document.querySelector<HTMLElement>(
|
|
126
|
-
`article[data-post-id="${postId}"]`,
|
|
127
|
-
);
|
|
128
|
-
if (!article) return false;
|
|
129
|
-
|
|
130
|
-
article.outerHTML = html;
|
|
131
|
-
// outerHTML detaches `article`; re-query the replacement to hydrate it.
|
|
132
|
-
const nextArticle = document.querySelector<HTMLElement>(
|
|
133
|
-
`article[data-post-id="${postId}"]`,
|
|
134
|
-
);
|
|
135
|
-
if (nextArticle) hydratePartial(nextArticle);
|
|
136
|
-
return true;
|
|
137
|
-
} catch {
|
|
138
|
-
return false;
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
async function refreshPostPageView(postId: string): Promise<boolean> {
|
|
143
|
-
try {
|
|
144
|
-
const container = document.querySelector<HTMLElement>(
|
|
145
|
-
`[data-post-view][data-post-view-id="${postId}"]`,
|
|
146
|
-
);
|
|
147
|
-
if (!container) return false;
|
|
148
|
-
|
|
149
|
-
const html = await fetchPartialHtml(
|
|
150
|
-
`/_/post-view/${encodeURIComponent(postId)}`,
|
|
151
|
-
);
|
|
152
|
-
if (!html) return false;
|
|
153
|
-
|
|
154
|
-
container.outerHTML = html;
|
|
155
|
-
// outerHTML detaches `container`; re-query the replacement to hydrate it
|
|
156
|
-
// (see refreshTimelineThreadView).
|
|
157
|
-
const next = document.querySelector<HTMLElement>(
|
|
158
|
-
`[data-post-view][data-post-view-id="${postId}"]`,
|
|
159
|
-
);
|
|
160
|
-
if (next) hydratePartial(next);
|
|
161
|
-
return true;
|
|
162
|
-
} catch {
|
|
163
|
-
return false;
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
75
|
async function refreshReplyTarget(
|
|
168
76
|
detail: ComposeSubmitDetail,
|
|
169
77
|
): Promise<boolean> {
|
|
@@ -233,7 +141,7 @@ function refreshUploadToast() {
|
|
|
233
141
|
}
|
|
234
142
|
|
|
235
143
|
/**
|
|
236
|
-
* Track completed upload
|
|
144
|
+
* Track completed upload results by clientId.
|
|
237
145
|
*
|
|
238
146
|
* When an attachment is migrated between editor instances (e.g. entering thread
|
|
239
147
|
* mode while an upload is in-flight), `updateAttachmentStatus` fires on the old
|
|
@@ -242,8 +150,11 @@ function refreshUploadToast() {
|
|
|
242
150
|
* handler can't look it up. This map provides a fallback: we record the result
|
|
243
151
|
* here as soon as any upload succeeds, and `buildRequestAttachments` reads it
|
|
244
152
|
* when `attachment.mediaId` is null and the clientId isn't in `uploadPromises`.
|
|
153
|
+
*
|
|
154
|
+
* The full result (not just the id) is kept so a failed publish can restore
|
|
155
|
+
* uploaded attachments into the reopened compose dialog.
|
|
245
156
|
*/
|
|
246
|
-
const
|
|
157
|
+
const completedUploads = new Map<string, UploadSessionResult>();
|
|
247
158
|
|
|
248
159
|
/**
|
|
249
160
|
* Quickly grab the very first decoded frame of a video as a small poster.
|
|
@@ -495,8 +406,14 @@ async function uploadFile(
|
|
|
495
406
|
|
|
496
407
|
uploadProgress.set(clientId, 1);
|
|
497
408
|
refreshUploadToast();
|
|
498
|
-
editor?.updateAttachmentStatus(
|
|
499
|
-
|
|
409
|
+
editor?.updateAttachmentStatus(
|
|
410
|
+
clientId,
|
|
411
|
+
"done",
|
|
412
|
+
result.id,
|
|
413
|
+
null,
|
|
414
|
+
result.url,
|
|
415
|
+
);
|
|
416
|
+
completedUploads.set(clientId, result);
|
|
500
417
|
return result.id;
|
|
501
418
|
} catch (error) {
|
|
502
419
|
uploadProgress.delete(clientId);
|
|
@@ -516,7 +433,7 @@ document.addEventListener("jant:attachment-removed", (e: Event) => {
|
|
|
516
433
|
e as CustomEvent<{ clientId: string; mediaId: string | null }>
|
|
517
434
|
).detail;
|
|
518
435
|
|
|
519
|
-
|
|
436
|
+
completedUploads.delete(clientId);
|
|
520
437
|
|
|
521
438
|
if (mediaId) {
|
|
522
439
|
// Upload already finished — fire-and-forget delete
|
|
@@ -565,6 +482,25 @@ document.addEventListener("click", (e: MouseEvent) => {
|
|
|
565
482
|
void openReplyForArticle(article);
|
|
566
483
|
});
|
|
567
484
|
|
|
485
|
+
// ── Draft badge → editor ────────────────────────────────────────────
|
|
486
|
+
//
|
|
487
|
+
// The badge on an inline draft doubles as its most likely next action. Publish
|
|
488
|
+
// and Delete live in the post menu with every other post action.
|
|
489
|
+
|
|
490
|
+
document.addEventListener("click", (e: MouseEvent) => {
|
|
491
|
+
const trigger = (e.target as HTMLElement).closest<HTMLElement>(
|
|
492
|
+
"[data-draft-continue]",
|
|
493
|
+
);
|
|
494
|
+
if (!trigger) return;
|
|
495
|
+
|
|
496
|
+
const postId =
|
|
497
|
+
trigger.dataset.postId ??
|
|
498
|
+
trigger.closest<HTMLElement>("article[data-post]")?.dataset.postId;
|
|
499
|
+
if (!postId) return;
|
|
500
|
+
|
|
501
|
+
void getComposeDialog()?.openEdit(postId);
|
|
502
|
+
});
|
|
503
|
+
|
|
568
504
|
// ── Submit handler ──────────────────────────────────────────────────
|
|
569
505
|
|
|
570
506
|
/** Build the JSON body for both create and update requests */
|
|
@@ -651,7 +587,7 @@ function buildRequestAttachments(
|
|
|
651
587
|
const mediaId =
|
|
652
588
|
attachment.mediaId ??
|
|
653
589
|
pendingMediaIds.get(attachment.clientId) ??
|
|
654
|
-
|
|
590
|
+
completedUploads.get(attachment.clientId)?.id;
|
|
655
591
|
if (!mediaId) continue;
|
|
656
592
|
|
|
657
593
|
requestAttachments.push({
|
|
@@ -694,6 +630,7 @@ document.addEventListener("jant:compose-submit-deferred", async (e: Event) => {
|
|
|
694
630
|
getComposeDialogFromEventTarget(event.target) ??
|
|
695
631
|
(document.querySelector("jant-compose-dialog") as JantComposeDialog | null);
|
|
696
632
|
const isPageMode = !!composeEl?.pageMode;
|
|
633
|
+
const isDraftPreviewPage = !!document.querySelector("[data-preview-status]");
|
|
697
634
|
|
|
698
635
|
// Get labels for toast messages
|
|
699
636
|
const labels = composeEl?.labels;
|
|
@@ -751,12 +688,48 @@ document.addEventListener("jant:compose-submit-deferred", async (e: Event) => {
|
|
|
751
688
|
}
|
|
752
689
|
composeEl?.clearLocalDraftFromStorage?.();
|
|
753
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
|
+
|
|
754
721
|
const reopenComposeAfterFailure = async () => {
|
|
755
722
|
if (!composeEl || isPageMode) return;
|
|
756
723
|
|
|
724
|
+
if (detail.draftSourceId) {
|
|
725
|
+
if (typeof composeEl.openDraft !== "function") return;
|
|
726
|
+
await composeEl.openDraft(detail.draftSourceId);
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
|
|
757
730
|
if (isEdit && detail.editPostId) {
|
|
758
731
|
if (typeof composeEl.openEdit !== "function") return;
|
|
759
|
-
await composeEl.openEdit(detail.editPostId);
|
|
732
|
+
await composeEl.openEdit(detail.editPostId, { restoreToast: false });
|
|
760
733
|
return;
|
|
761
734
|
}
|
|
762
735
|
|
|
@@ -772,13 +745,22 @@ document.addEventListener("jant:compose-submit-deferred", async (e: Event) => {
|
|
|
772
745
|
id: detail.replyRefreshId,
|
|
773
746
|
}
|
|
774
747
|
: undefined,
|
|
775
|
-
{
|
|
748
|
+
{
|
|
749
|
+
restoreDraft: true,
|
|
750
|
+
initialFormat: detail.format,
|
|
751
|
+
restoreToast: false,
|
|
752
|
+
restoreMedia: collectRestorableMedia(),
|
|
753
|
+
},
|
|
776
754
|
);
|
|
777
755
|
return;
|
|
778
756
|
}
|
|
779
757
|
|
|
780
758
|
if (typeof composeEl.openNew !== "function") return;
|
|
781
|
-
await composeEl.openNew({
|
|
759
|
+
await composeEl.openNew({
|
|
760
|
+
restoreDraft: true,
|
|
761
|
+
restoreToast: false,
|
|
762
|
+
restoreMedia: collectRestorableMedia(),
|
|
763
|
+
});
|
|
782
764
|
};
|
|
783
765
|
const handleSubmitError = async (message: string) => {
|
|
784
766
|
clearPageLoading();
|
|
@@ -818,10 +800,10 @@ document.addEventListener("jant:compose-submit-deferred", async (e: Event) => {
|
|
|
818
800
|
const results = await Promise.all(pendingPromises);
|
|
819
801
|
|
|
820
802
|
// If any pending upload failed (null result where we expected a mediaId
|
|
821
|
-
// AND the upload wasn't already tracked in
|
|
803
|
+
// AND the upload wasn't already tracked in completedUploads):
|
|
822
804
|
const failedCount = results.filter(
|
|
823
805
|
(id, i) =>
|
|
824
|
-
id === null && !
|
|
806
|
+
id === null && !completedUploads.has(pendingClientIds[i] ?? ""),
|
|
825
807
|
).length;
|
|
826
808
|
if (failedCount > 0) {
|
|
827
809
|
if (detail.status === "published" && !isEdit) {
|
|
@@ -930,7 +912,12 @@ document.addEventListener("jant:compose-submit-deferred", async (e: Event) => {
|
|
|
930
912
|
|
|
931
913
|
if (threadStatus === "published") {
|
|
932
914
|
clearRecoveredLocalDraft();
|
|
933
|
-
if (
|
|
915
|
+
if (isDraftPreviewPage && detail.draftSourceId) {
|
|
916
|
+
queueSuccessToast(publishedMsg);
|
|
917
|
+
globalThis.location.assign(
|
|
918
|
+
threadPermalink ?? globalThis.location.pathname,
|
|
919
|
+
);
|
|
920
|
+
} else if (isPageMode) {
|
|
934
921
|
await refreshComposeCollections();
|
|
935
922
|
resetPageCompose();
|
|
936
923
|
toastMsg(publishedMsg);
|
|
@@ -945,6 +932,11 @@ document.addEventListener("jant:compose-submit-deferred", async (e: Event) => {
|
|
|
945
932
|
}
|
|
946
933
|
} else {
|
|
947
934
|
clearRecoveredLocalDraft();
|
|
935
|
+
if (isDraftPreviewPage && detail.draftSourceId) {
|
|
936
|
+
queueSuccessToast(threadToast ?? "Draft saved.");
|
|
937
|
+
globalThis.location.assign(globalThis.location.pathname);
|
|
938
|
+
return;
|
|
939
|
+
}
|
|
948
940
|
await refreshComposeCollections();
|
|
949
941
|
if (!leavePageAfterConfirmSave()) resetPageCompose();
|
|
950
942
|
toastMsg(threadToast ?? "Draft saved.");
|
|
@@ -1033,21 +1025,50 @@ document.addEventListener("jant:compose-submit-deferred", async (e: Event) => {
|
|
|
1033
1025
|
const editData = await readJsonObject(res);
|
|
1034
1026
|
const editPostId = detail.editPostId ?? "";
|
|
1035
1027
|
const newSlug = getJsonString(editData, "slug");
|
|
1036
|
-
const
|
|
1028
|
+
const nextStatus = getJsonString(editData, "status") ?? detail.status;
|
|
1029
|
+
const newPath = newSlug
|
|
1030
|
+
? publicPath(
|
|
1031
|
+
nextStatus === "draft" ? `/preview/${newSlug}` : `/${newSlug}`,
|
|
1032
|
+
)
|
|
1033
|
+
: null;
|
|
1034
|
+
const editSuccessMessage =
|
|
1035
|
+
nextStatus === "draft"
|
|
1036
|
+
? "Draft saved."
|
|
1037
|
+
: detail.draftSourceId
|
|
1038
|
+
? publishedMsg
|
|
1039
|
+
: "Post updated.";
|
|
1040
|
+
|
|
1041
|
+
// Draft preview pages cannot use the public post-view refresh endpoint.
|
|
1042
|
+
// Reload the authenticated preview so saved content stays visible.
|
|
1043
|
+
if (nextStatus === "draft") {
|
|
1044
|
+
if (isDraftPreviewPage) {
|
|
1045
|
+
queueSuccessToast(editSuccessMessage);
|
|
1046
|
+
globalThis.location.assign(newPath ?? globalThis.location.pathname);
|
|
1047
|
+
return;
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
// Drafts opened from the compose panel stay in that workflow. The
|
|
1051
|
+
// completion event lets a pending "save and open drafts" action fetch
|
|
1052
|
+
// the updated list only after the write has finished.
|
|
1053
|
+
await refreshComposeCollections();
|
|
1054
|
+
toastMsg(editSuccessMessage);
|
|
1055
|
+
dispatchSubmitComplete(composeEl, "draft");
|
|
1056
|
+
return;
|
|
1057
|
+
}
|
|
1037
1058
|
|
|
1038
1059
|
if (isPageMode) {
|
|
1039
1060
|
// On the post detail page: refresh or navigate if slug changed
|
|
1040
1061
|
if (newPath && newPath !== globalThis.location.pathname) {
|
|
1041
|
-
queueSuccessToast(
|
|
1062
|
+
queueSuccessToast(editSuccessMessage);
|
|
1042
1063
|
globalThis.location.assign(newPath);
|
|
1043
1064
|
} else {
|
|
1044
1065
|
const refreshed =
|
|
1045
1066
|
editPostId && (await refreshPostPageView(editPostId));
|
|
1046
1067
|
if (!refreshed) {
|
|
1047
|
-
queueSuccessToast(
|
|
1068
|
+
queueSuccessToast(editSuccessMessage);
|
|
1048
1069
|
globalThis.location.assign(globalThis.location.pathname);
|
|
1049
1070
|
} else {
|
|
1050
|
-
toastMsg(
|
|
1071
|
+
toastMsg(editSuccessMessage);
|
|
1051
1072
|
}
|
|
1052
1073
|
}
|
|
1053
1074
|
} else if (editPostId) {
|
|
@@ -1057,15 +1078,15 @@ document.addEventListener("jant:compose-submit-deferred", async (e: Event) => {
|
|
|
1057
1078
|
);
|
|
1058
1079
|
if (postView) {
|
|
1059
1080
|
if (newPath && newPath !== globalThis.location.pathname) {
|
|
1060
|
-
queueSuccessToast(
|
|
1081
|
+
queueSuccessToast(editSuccessMessage);
|
|
1061
1082
|
globalThis.location.assign(newPath);
|
|
1062
1083
|
return;
|
|
1063
1084
|
}
|
|
1064
1085
|
const refreshed = await refreshPostPageView(editPostId);
|
|
1065
1086
|
if (refreshed) {
|
|
1066
|
-
toastMsg(
|
|
1087
|
+
toastMsg(editSuccessMessage);
|
|
1067
1088
|
} else {
|
|
1068
|
-
queueSuccessToast(
|
|
1089
|
+
queueSuccessToast(editSuccessMessage);
|
|
1069
1090
|
globalThis.location.reload();
|
|
1070
1091
|
}
|
|
1071
1092
|
} else {
|
|
@@ -1080,14 +1101,14 @@ document.addEventListener("jant:compose-submit-deferred", async (e: Event) => {
|
|
|
1080
1101
|
? await refreshTimelineThreadView(threadRootId)
|
|
1081
1102
|
: await refreshPostCardView(editPostId);
|
|
1082
1103
|
if (refreshed) {
|
|
1083
|
-
toastMsg(
|
|
1104
|
+
toastMsg(editSuccessMessage);
|
|
1084
1105
|
} else {
|
|
1085
|
-
queueSuccessToast(
|
|
1106
|
+
queueSuccessToast(editSuccessMessage);
|
|
1086
1107
|
globalThis.location.reload();
|
|
1087
1108
|
}
|
|
1088
1109
|
}
|
|
1089
1110
|
} else {
|
|
1090
|
-
queueSuccessToast(
|
|
1111
|
+
queueSuccessToast(editSuccessMessage);
|
|
1091
1112
|
globalThis.location.reload();
|
|
1092
1113
|
}
|
|
1093
1114
|
return;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { JantComposeDialog } from "./components/jant-compose-dialog.js";
|
|
2
|
+
import { showToast } from "./toast.js";
|
|
2
3
|
|
|
3
4
|
interface ReplyToData {
|
|
4
5
|
contentHtml: string;
|
|
@@ -142,6 +143,18 @@ export async function openReplyForArticle(article: HTMLElement): Promise<void> {
|
|
|
142
143
|
const dialog = getComposeDialog();
|
|
143
144
|
if (!dialog) return;
|
|
144
145
|
|
|
146
|
+
// Only surfaces that hide the trailing draft redirect to it — the feed hands
|
|
147
|
+
// its ID down on the last published post. Where the draft is rendered the
|
|
148
|
+
// author can already see it, so Reply keeps its ordinary meaning: continue
|
|
149
|
+
// the thread after this post. (`create` keeps that continuation a draft, so
|
|
150
|
+
// the chain never publishes past an unpublished post.)
|
|
151
|
+
const hiddenDraftTailId = article.dataset.threadDraftTailId;
|
|
152
|
+
if (hiddenDraftTailId) {
|
|
153
|
+
showToast("Picking up the unfinished draft at the end of this thread.");
|
|
154
|
+
await dialog.openEdit(hiddenDraftTailId);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
145
158
|
const threadRootId = article.dataset.threadRootId ?? postId;
|
|
146
159
|
await dialog.openReply(
|
|
147
160
|
postId,
|
|
@@ -68,7 +68,11 @@ function openEditFromQueryParam() {
|
|
|
68
68
|
if (!postId || !composeEl) return;
|
|
69
69
|
|
|
70
70
|
consumeEditQueryParam();
|
|
71
|
-
|
|
71
|
+
if (document.querySelector("[data-preview-status]")) {
|
|
72
|
+
void composeEl.openDraft(postId);
|
|
73
|
+
} else {
|
|
74
|
+
void composeEl.openEdit(postId);
|
|
75
|
+
}
|
|
72
76
|
}
|
|
73
77
|
|
|
74
78
|
async function toggleFeatured(
|