import type { BaseUrl } from "@http-client"; import type { LoadedRoute, Route } from "@app/lib/router/definitions"; import { get, writable } from "svelte/store"; import * as mutexExecutor from "@app/lib/mutexExecutor"; import * as utils from "@app/lib/utils"; import config from "@app/lib/config"; import { repoRouteToPath, repoTitle, resolveRepoRoute, } from "@app/views/repos/router"; import { loadRoute } from "@app/lib/router/definitions"; import { nodePath } from "@app/views/nodes/router"; import { userRouteToPath, userTitle } from "@app/views/users/router"; export { type Route }; const InitialStore = { resource: "booting" as const }; export const isLoading = writable(true); export const activeRouteStore = writable(InitialStore); export const activeUnloadedRouteStore = writable(InitialStore); let currentUrl: URL | undefined; export function useDefaultNavigation(event: MouseEvent) { return ( event.button !== 0 || event.altKey || event.ctrlKey || event.metaKey || event.shiftKey ); } export async function loadFromLocation(): Promise { await navigateToUrl("replace", new URL(window.location.href)); } export async function navigateToUrl( action: "push" | "replace", url: URL, ): Promise { const { pathname, hash } = url; if (url.origin !== window.origin) { throw new Error("Cannot navigate to other origin"); } if ( currentUrl && currentUrl.pathname === pathname && currentUrl.search === url.search ) { return; } const relativeUrl = pathname + url.search + (hash || ""); url = new URL(relativeUrl, window.origin); const route = urlToRoute(url); if (route) { await navigate(action, route); } else { await navigate(action, { resource: "notFound", params: { title: "Page not found" }, }); } } window.addEventListener("popstate", () => loadFromLocation()); const loadExecutor = mutexExecutor.create(); async function navigate( action: "push" | "replace", newRoute: Route, ): Promise { isLoading.set(true); const path = routeToPath(newRoute); if (action === "push") { window.history.pushState(newRoute, "", path); } else if (action === "replace") { window.history.replaceState(newRoute, ""); } currentUrl = new URL(window.location.href); const currentLoadedRoute = get(activeRouteStore); const loadedRoute = await loadExecutor.run(async () => { return loadRoute(newRoute, currentLoadedRoute); }); // Only let the last request through. if (loadedRoute === undefined) { return; } setTitle(loadedRoute); activeRouteStore.set(loadedRoute); activeUnloadedRouteStore.set(newRoute); isLoading.set(false); } function setTitle(loadedRoute: LoadedRoute) { const title: string[] = []; if (loadedRoute.resource === "booting") { title.push("Radicle"); } else if (loadedRoute.resource === "error") { title.push("Error"); title.push("Radicle"); } else if (loadedRoute.resource === "users") { title.push(...userTitle(loadedRoute)); } else if (loadedRoute.resource === "notFound") { title.push("Page not found"); title.push("Radicle"); } else if ( loadedRoute.resource === "repo.source" || loadedRoute.resource === "repo.history" || loadedRoute.resource === "repo.commit" || loadedRoute.resource === "repo.issue" || loadedRoute.resource === "repo.issues" || loadedRoute.resource === "repo.patches" || loadedRoute.resource === "repo.patch" ) { title.push(...repoTitle(loadedRoute)); } else if (loadedRoute.resource === "nodes") { title.push(loadedRoute.params.baseUrl.hostname); } else { utils.unreachable(loadedRoute); } document.title = title.join(" ยท "); } export async function push(newRoute: Route): Promise { await navigate("push", newRoute); } export async function replace(newRoute: Route): Promise { await navigate("replace", newRoute); } export function extractBaseUrl(hostAndPort: string): BaseUrl { const [hostname, portString] = decodeURIComponent(hostAndPort).split(":"); let port; if (portString !== undefined) { port = Number(portString); } else if (globalThis.__PLAYWRIGHT__ === true) { port = config.nodes.defaultHttpdPort; } else { port = utils.isLocal(hostname) ? config.nodes.defaultLocalHttpdPort : config.nodes.defaultHttpdPort; } const scheme = utils.isLocal(hostname) || utils.isOnion(hostname) ? "http" : config.nodes.defaultHttpdScheme; return { hostname, port, scheme, }; } function urlToRoute(url: URL): Route | null { const segments = url.pathname.substring(1).split("/"); const resource = segments.shift(); switch (resource) { case "nodes": case "seeds": { const hostAndPort = segments.shift(); if (hostAndPort) { const baseUrl = extractBaseUrl(hostAndPort); const id = segments.shift(); if (id === "users") { const did = segments.shift(); if (did) { return { resource: "users", baseUrl, did }; } return null; } else if (id) { return resolveRepoRoute(baseUrl, id, segments, url.search); } else { return { resource: "nodes", params: { baseUrl, repoPageIndex: 0 }, }; } } else { return { resource: "nodes", params: undefined, }; } } case "": { return { resource: "nodes", params: undefined }; } default: { return null; } } } export function routeToPath(route: Route): string { if (route.resource === "nodes") { if (route.params === undefined) { return "/"; } else { return nodePath(route.params.baseUrl); } } else if (route.resource === "users") { return userRouteToPath(route); } else if ( route.resource === "repo.source" || route.resource === "repo.history" || route.resource === "repo.commit" || route.resource === "repo.issues" || route.resource === "repo.issue" || route.resource === "repo.patches" || route.resource === "repo.patch" ) { return repoRouteToPath(route); } else if ( route.resource === "booting" || route.resource === "notFound" || route.resource === "error" ) { return ""; } else { return utils.unreachable(route); } } export const testExports = { urlToRoute, routeToPath, extractBaseUrl };