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 | /**
* Hash router. Routes:
* #/ → lobby (app shell)
* #/u/<pub> → user profile (placeholder until M4)
* #/add/<invite> → accept a contact invite (M2 rendezvous flow)
* #/c/<id> → conversation (M3; pair id = peer pub, group id = chatreg id)
* Anything else falls back to the lobby.
*/
import { renderAbout } from "./views/about";
import { renderActivityView } from "./views/activity-view";
import { renderAddContact } from "./views/add-contact";
import { renderChatView } from "./views/chat-view";
import { renderLobby } from "./views/lobby";
import { renderPostView } from "./views/post-view";
import { renderUserProfile } from "./views/user-profile";
export type Route =
| { name: "lobby" }
| { name: "profile"; pub: string }
| { name: "add"; invite: string }
| { name: "chat"; id: string }
| { name: "post"; shard: string; cid: string }
| { name: "activity"; shard: string; org: string; aid: string }
| { name: "about" };
export function parseRoute(hash: string): Route {
const path = hash.replace(/^#/, "");
// #/despre — the About + honest-limits page (M6)
if (path === "/despre" || path === "/despre/") return { name: "about" };
// #/p/<shard>/<cid> — post permalink (shard is URL-encoded: "cat%7Ccounty")
const p = /^\/p\/([^/]+)\/([^/]+)$/.exec(path);
if (p) {
try {
return { name: "post", shard: decodeURIComponent(p[1]), cid: decodeURIComponent(p[2]) };
} catch {
return { name: "lobby" };
}
}
// #/a/<shard>/<org>/<aid> — activity permalink (P1; shard is URL-encoded)
const a = /^\/a\/([^/]+)\/([^/]+)\/([^/]+)$/.exec(path);
if (a) {
try {
return {
name: "activity",
shard: decodeURIComponent(a[1]),
org: decodeURIComponent(a[2]),
aid: decodeURIComponent(a[3]),
};
} catch {
return { name: "lobby" };
}
}
const m = /^\/(u|add|c)\/(.+)$/.exec(path);
if (m) {
let arg: string;
try {
arg = decodeURIComponent(m[2]);
} catch {
// malformed percent-sequence (e.g. "#/add/abc%2") — never blank the app
return { name: "lobby" };
}
if (m[1] === "u") return { name: "profile", pub: arg };
if (m[1] === "c") return { name: "chat", id: arg };
return { name: "add", invite: arg };
}
return { name: "lobby" }; // "#/", "" and any unknown path → lobby
}
function render(root: HTMLElement): void {
const route = parseRoute(location.hash);
switch (route.name) {
case "profile":
renderUserProfile(root, route.pub);
break;
case "add":
renderAddContact(root, route.invite);
break;
case "chat":
renderChatView(root, route.id);
break;
case "post":
renderPostView(root, route.shard, route.cid);
break;
case "activity":
renderActivityView(root, route.shard, route.org, route.aid);
break;
case "about":
renderAbout(root);
break;
default:
renderLobby(root);
}
}
let routerRoot: HTMLElement | null = null;
export function startRouter(root: HTMLElement): void {
routerRoot = root;
if (!location.hash) history.replaceState(null, "", "#/");
window.addEventListener("hashchange", () => render(root));
render(root);
}
/** Re-render the current route (e.g. after a synced language change). */
export function rerenderRoute(): void {
if (routerRoot) render(routerRoot);
}
|