feat: link resolver and spinner
This commit is contained in:
@@ -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>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user