import { createContext, createRef, forwardRef, useCallback, useContext, useEffect, useRef, useState, } from "react"; import { dnsClient, ethClient, handshakeClient, ipfsClient, networkRegistryClient, peerDiscoveryClient, swarmClient, } from "@/clients.ts"; import * as kernel from "@lumeweb/libkernel/kernel"; import { kernelLoaded } from "@lumeweb/libkernel/kernel"; import Arrow from "@/components/Arrow.tsx"; import React from "react"; import { Input } from "@/components/ui/input.tsx"; import { Button } from "@/components/ui/button.tsx"; import { type AuthContextType, type LumeStatusContextType, useAuth, useLumeStatus, } from "@lumeweb/sdk"; let BOOT_FUNCTIONS: (() => Promise)[] = []; interface BrowserContextType { url: string; setUrl: React.Dispatch>; isLoadingPage: boolean; setIsLoadingPage: React.Dispatch>; } const BrowserStateContext = createContext( undefined, ); export function BrowserStateProvider({ children, }: { children: React.ReactElement; }) { const [url, setUrl] = useState(""); const [isLoadingPage, setIsLoadingPage] = useState(false); return ( {children} ); } export function useBrowserState() { const context = useContext(BrowserStateContext); if (!context) { throw new Error( "useBrowserState must be used within a BrowserStateProvider", ); } return context; } async function boot({ onInit, onAuth, onBoot, }: { onInit: (inited: boolean) => Promise | void; onAuth: (authed: boolean) => Promise | void; onBoot: (booted: boolean) => Promise | void; }) { const reg = await navigator.serviceWorker.register("/sw.js"); await reg.update(); await kernel.serviceWorkerReady(); await kernel.init().catch((err) => { console.error("[Browser.tsx] Failed to init kernel", { error: err }); }); await onInit(true); await kernelLoaded().catch((err) => { console.error("[Browser.tsx] Failed to load kernel", { error: err }); }); await onAuth(true); BOOT_FUNCTIONS.push( async () => await swarmClient.addRelay( "2d7ae1517caf4aae4de73c6d6f400765d2dd00b69d65277a29151437ef1c7d1d", ), ); // IRC BOOT_FUNCTIONS.push( async () => await peerDiscoveryClient.register( "zrjHTx8tSQFWnmZ9JzK7XmJirqJQi2WRBLYp3fASaL2AfBQ", ), ); BOOT_FUNCTIONS.push( async () => await networkRegistryClient.registerType("content"), ); BOOT_FUNCTIONS.push( async () => await networkRegistryClient.registerType("blockchain"), ); BOOT_FUNCTIONS.push(async () => await handshakeClient.register()); BOOT_FUNCTIONS.push(async () => await ethClient.register()); BOOT_FUNCTIONS.push(async () => await ipfsClient.register()); const resolvers = [ "zrjCnUBqmBqXXcc2yPnq517sXQtNcfZ2BHgnVTcbhSYxko7", // CID "zrjEYq154PS7boERAbRAKMyRGzAR6CTHVRG6mfi5FV4q9FA", // ENS "zrjEH3iojPLr7986o7iCn9THBmJmHiuDWmS1G6oT8DnfuFM", // HNS ]; for (const resolver of resolvers) { BOOT_FUNCTIONS.push(async () => dnsClient.registerResolver(resolver)); } BOOT_FUNCTIONS.push(async () => onBoot(true)); await bootup(); await Promise.all([ ethClient.ready(), handshakeClient.ready(), ipfsClient.ready(), ]); } async function bootup() { for (const entry of Object.entries(BOOT_FUNCTIONS)) { console.log(entry[1].toString()); await entry[1](); } } const NavInput = forwardRef( (props: React.InputHTMLAttributes, ref) => { return ; }, ); export function Navigator() { const { url: contextUrl, setUrl } = useBrowserState(); const { ready } = useLumeStatus(); const inputEl = useRef(); const browse = (inputValue: string) => { let input = inputValue.trim(); // If the input doesn't contain a protocol, assume it's http if (!input?.match(/^https?:\/\//)) { input = `http://${input}`; } try { // Try to parse it as a URL const url = new URL(input); setUrl(url.toString() || "about:blank"); } catch (e) { // Handle invalid URLs here, if needed console.error("Invalid URL:", e); } }; useEffect(() => { if (inputEl.current) { inputEl.current.value = contextUrl; } }, [contextUrl]); return (
{ e.preventDefault(); const inputElement = e.target as HTMLFormElement; const inputValue = ( inputElement?.elements.namedItem("url") as HTMLInputElement )?.value; if (inputValue) { browse(inputValue); } }} className="relative h-full w-full rounded-full bg-neutral-800 border border-neutral-700 flex items-center [&>input:focus]:ring-2 [&>input:focus]:ring-primary [&>input:focus+button]:ring-2 [&>input:focus+button]:ring-primary" > (inputEl.current = el)} disabled={!ready} className={`rounded-l-full bg-neutral-800 text-white border-none focus-visible:ring-offset-0 ${!ready ? "bg-neutral-950" : ""}`} name="url" /> ); } export function Browser() { const { url, setUrl, isLoadingPage, setIsLoadingPage } = useBrowserState(); const status = useLumeStatus(); const auth = useAuth(); const iframeRef = useRef(null); useEffect(() => { boot({ onAuth(authed) { auth.setIsLoggedIn(authed); }, onBoot(booted) { status.setReady(booted); }, onInit(inited) { status.setInited(inited); }, }).catch((err) => console.error("[Browser.tsx] Failed to Boot Lume", { error: err }), ); }, []); const handleIframeLoad = () => { try { const newUrl = iframeRef?.current?.contentWindow?.location.href as string; const urlObj = new URL(newUrl); let realUrl = urlObj.pathname .replace(/^\/browse\//, "") .replace(/\/$/, ""); if (url !== realUrl) { setUrl(realUrl); } setIsLoadingPage(false); } catch (e) { // This will catch errors related to cross-origin requests, in which case we can't access the iframe's contentWindow.location console.warn( "Couldn't access iframe URL due to cross-origin restrictions:", e, ); } }; useEffect(() => { const iframe = iframeRef.current; if (iframe) { const observer = new MutationObserver((mutationsList, observer) => { for (let mutation of mutationsList) { if ( mutation.type === "attributes" && mutation.attributeName === "src" ) { setIsLoadingPage(true); } } }); observer.observe(iframe, { attributes: true }); return () => observer.disconnect(); // Clean up on unmount } }, []); return ( <> {isLoadingPage ? (
Loading {url}...
) : null} ); }