feat: link resolver and spinner

This commit is contained in:
2026-04-05 11:01:48 +02:00
parent ad89f409bf
commit 3c5856aecb
7 changed files with 91 additions and 9 deletions

View File

@@ -8,6 +8,7 @@ import { buildGraphArrays, SPACE_SIZE } from "@/components/browse/buildGraph";
import type { BrowseWinData, HistoryEntry } from "@/components/browse/types";
import BrowseNodeWindow from "@/components/browse/BrowseNodeWindow";
import BrowseSearchBar from "@/components/browse/BrowseSearchBar";
import Loader from "@/components/shared/Loader";
// ---------------------------------------------------------------------------
// Main component
@@ -355,6 +356,13 @@ export default function BrowseView() {
}, [openWindow, navigateTo]);
// ── Handle micron link clicks via event delegation ──
//
// Micron link destinations come in several forms:
// /page.mu — same-node, absolute path
// page.mu — same-node, relative
// :/page/page.mu — NomadNet "request" link (: prefix + /page/ segment)
// <32-hex-hash>/page.mu — cross-node link
// nomadnetwork://... — already stripped by micron-parser's data-destination
const handleContentClick = useCallback((e: React.MouseEvent, winId: string, data: BrowseWinData) => {
const anchor = (e.target as HTMLElement).closest("a");
if (!anchor) return;
@@ -363,11 +371,35 @@ export default function BrowseView() {
const dest = anchor.getAttribute("data-destination") ?? anchor.getAttribute("href") ?? "";
if (!dest) return;
let path = dest.replace(/^nomadnetwork:\/\//, "").replace(/^\/+/, "");
if (/^[0-9a-f]{32}$/i.test(path)) return;
let raw = dest
.replace(/^nomadnetwork:\/\//, "") // strip scheme if present
.replace(/^:/, "") // strip NomadNet request prefix
.replace(/^\/page\//, "") // strip /page/ path segment
.replace(/^\/+/, ""); // strip remaining leading slashes
// A bare 32-char hex hash with no path — nothing to navigate to
if (/^[0-9a-f]{32}$/i.test(raw)) return;
// If the destination starts with a 32-char hex hash followed by "/",
// it's a cross-node link: <hash>/page.mu → use that node's hash
let targetNode = data.node;
let path = raw;
const crossNodeMatch = raw.match(/^([0-9a-f]{32})\/(.+)$/i);
if (crossNodeMatch) {
const targetHash = crossNodeMatch[1]!;
path = crossNodeMatch[2]!;
// Strip /page/ from the path portion too
path = path.replace(/^\/page\//, "").replace(/^\/+/, "");
const known = nodesMapRef.current.get(targetHash);
if (known) {
targetNode = known;
}
}
if (!path || path === "/") return;
if (!path.endsWith(".mu")) path += ".mu";
navigateTo(winId, data.node, path, data);
navigateTo(winId, targetNode, path, data);
}, [navigateTo]);
const clearSearch = useCallback(() => {
@@ -400,8 +432,9 @@ export default function BrowseView() {
/>
{nodes.length === 0 && (
<div className="absolute inset-0 flex items-center justify-center text-muted-foreground text-sm pointer-events-none">
Listening for nodes on the Reticulum network...
<div className="absolute inset-0 flex flex-col items-center justify-center gap-3 text-muted-foreground text-sm pointer-events-none">
<Loader />
Connecting...
</div>
)}