1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111 | /**
* #/p/<shard>/<cid> โ public post permalink (M5). Resolves the post from the
* shard's feed rooms by plain mailbox replay (owner offline is fine, like
* every public view) and renders the same card + comments the feed shows.
* Honest terminal states: invalid link, mailbox unreachable, or not found
* within the TTL window (ยง7 โ the link may simply have expired).
*/
import { RuntimeLimits } from "../../config";
import { openFeed, type FeedHandle } from "../../public/feed-service";
import { isShard } from "../../public/public-doc";
import { t } from "../i18n";
import { h } from "../ui-kit";
import { postCard } from "./feed-tab";
const CID_RE = /^[a-z0-9]{10,128}$/;
function frame(...children: (HTMLElement | null)[]): HTMLElement {
return h(
"div",
{ class: "profile-view", "data-testid": "pv-view" },
h(
"div",
{ class: "profile-view-inner" },
h("a", { class: "btn ghost small", href: "#/" }, t("backHome")),
...children.filter((c): c is HTMLElement => c !== null),
),
);
}
function terminal(root: HTMLElement, glyph: string, msg: string, testid: string): void {
root.replaceChildren(
frame(
h(
"section",
{ class: "card placeholder" },
h("div", { class: "glyph" }, glyph),
h("p", { class: "add-msg", "data-testid": testid }, msg),
),
),
);
}
let openHandle: FeedHandle | null = null;
/** Run `cleanup` the moment `el` leaves the document (route change). */
function onDisconnect(el: HTMLElement, cleanup: () => void): void {
const obs = new MutationObserver(() => {
if (!el.isConnected) {
obs.disconnect();
cleanup();
}
});
obs.observe(document.body, { childList: true, subtree: true });
}
export function renderPostView(root: HTMLElement, shard: string, cid: string): void {
openHandle?.close();
openHandle = null;
// a STABLE host owns every draw: internal redraws replace the host's children
// (host stays connected), while leaving the route replaces the host itself in
// `root` โ that disconnect fires the cleanup and closes the feed handle.
const host = h("div", { class: "pv-host" });
root.replaceChildren(host);
if (!isShard(shard) || !CID_RE.test(cid)) {
terminal(host, "๐ซ", t("upInvalid"), "pv-invalid");
return;
}
host.replaceChildren(
frame(
h(
"section",
{ class: "card placeholder" },
h("div", { class: "glyph" }, "๐"),
h("p", { class: "muted", "data-testid": "pv-loading" }, t("pvLoading")),
),
),
);
void (async () => {
const handle = await openFeed(shard);
if (!host.isConnected) {
handle.close();
return;
}
openHandle = handle;
// leaving the permalink route drops `host` from the DOM: close the handle so
// the gossipsub feedTopic is left and background refresh stops (no leak).
onDisconnect(host, () => {
handle.close();
if (openHandle === handle) openHandle = null;
});
const draw = (): void => {
const post = handle.post(cid);
if (!post) {
terminal(host, "โณ", t("pvNotFound", { hours: RuntimeLimits.ttlHours }), "pv-notfound");
return;
}
host.replaceChildren(frame(h("div", { class: "feed-list pv-single" }, postCard(handle, post, { refresh: draw }))));
};
const post = handle.post(cid);
if (!post) {
// the initial page budget may not have reached it โ page further back
let more = true;
while (more && !handle.post(cid)) more = await handle.loadOlder();
}
if (!host.isConnected) return;
draw();
})().catch(() => {
if (host.isConnected) terminal(host, "๐ก", t("upNet"), "pv-net");
});
}
|