2023-10-11 13:46:46 +00:00
|
|
|
import {
|
|
|
|
createContext,
|
|
|
|
createRef,
|
|
|
|
forwardRef,
|
2023-10-16 21:43:13 +00:00
|
|
|
useCallback,
|
2023-10-11 13:46:46 +00:00
|
|
|
useContext,
|
|
|
|
useEffect,
|
2023-10-17 00:37:59 +00:00
|
|
|
useRef,
|
2023-10-11 13:46:46 +00:00
|
|
|
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";
|
2023-11-01 22:01:55 +00:00
|
|
|
import React from "react";
|
2023-10-11 13:46:46 +00:00
|
|
|
import { Input } from "@/components/ui/input.tsx";
|
|
|
|
import { Button } from "@/components/ui/button.tsx";
|
2023-10-16 19:20:00 +00:00
|
|
|
import {
|
|
|
|
type AuthContextType,
|
|
|
|
type LumeStatusContextType,
|
|
|
|
useAuth,
|
|
|
|
useLumeStatus,
|
|
|
|
} from "@lumeweb/sdk";
|
2023-10-11 13:46:46 +00:00
|
|
|
|
|
|
|
let BOOT_FUNCTIONS: (() => Promise<any>)[] = [];
|
|
|
|
|
|
|
|
interface BrowserContextType {
|
|
|
|
url: string;
|
|
|
|
setUrl: React.Dispatch<React.SetStateAction<string>>;
|
2023-11-01 22:01:55 +00:00
|
|
|
isLoadingPage: boolean;
|
|
|
|
setIsLoadingPage: React.Dispatch<React.SetStateAction<boolean>>;
|
2023-10-11 13:46:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
const BrowserStateContext = createContext<BrowserContextType | undefined>(
|
|
|
|
undefined,
|
|
|
|
);
|
|
|
|
|
|
|
|
export function BrowserStateProvider({
|
|
|
|
children,
|
|
|
|
}: {
|
|
|
|
children: React.ReactElement;
|
|
|
|
}) {
|
2023-10-16 23:45:15 +00:00
|
|
|
const [url, setUrl] = useState("");
|
2023-11-01 22:01:55 +00:00
|
|
|
const [isLoadingPage, setIsLoadingPage] = useState<boolean>(false);
|
2023-10-11 13:46:46 +00:00
|
|
|
|
|
|
|
return (
|
2023-11-01 22:01:55 +00:00
|
|
|
<BrowserStateContext.Provider
|
|
|
|
value={{ url, setUrl, isLoadingPage, setIsLoadingPage }}
|
|
|
|
>
|
2023-10-11 13:46:46 +00:00
|
|
|
{children}
|
|
|
|
</BrowserStateContext.Provider>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
export function useBrowserState() {
|
|
|
|
const context = useContext(BrowserStateContext);
|
|
|
|
if (!context) {
|
|
|
|
throw new Error(
|
|
|
|
"useBrowserState must be used within a BrowserStateProvider",
|
|
|
|
);
|
|
|
|
}
|
|
|
|
return context;
|
|
|
|
}
|
|
|
|
|
2023-10-20 08:07:20 +00:00
|
|
|
async function boot({
|
|
|
|
onInit,
|
|
|
|
onAuth,
|
|
|
|
onBoot,
|
2023-11-01 22:01:55 +00:00
|
|
|
}: {
|
|
|
|
onInit: (inited: boolean) => Promise<void> | void;
|
|
|
|
onAuth: (authed: boolean) => Promise<void> | void;
|
|
|
|
onBoot: (booted: boolean) => Promise<void> | void;
|
|
|
|
}) {
|
2023-10-11 13:46:46 +00:00
|
|
|
const reg = await navigator.serviceWorker.register("/sw.js");
|
|
|
|
await reg.update();
|
|
|
|
|
|
|
|
await kernel.serviceWorkerReady();
|
2023-10-12 16:56:28 +00:00
|
|
|
|
2023-10-20 08:07:20 +00:00
|
|
|
await kernel.init().catch((err) => {
|
2023-11-01 22:01:55 +00:00
|
|
|
console.error("[Browser.tsx] Failed to init kernel", { error: err });
|
2023-10-12 16:56:28 +00:00
|
|
|
});
|
2023-10-20 08:07:20 +00:00
|
|
|
await onInit(true);
|
|
|
|
await kernelLoaded().catch((err) => {
|
2023-11-01 22:01:55 +00:00
|
|
|
console.error("[Browser.tsx] Failed to load kernel", { error: err });
|
2023-10-20 08:07:20 +00:00
|
|
|
});
|
|
|
|
await onAuth(true);
|
2023-10-12 17:20:46 +00:00
|
|
|
|
2023-10-11 13:46:46 +00:00
|
|
|
BOOT_FUNCTIONS.push(
|
|
|
|
async () =>
|
|
|
|
await swarmClient.addRelay(
|
|
|
|
"2d7ae1517caf4aae4de73c6d6f400765d2dd00b69d65277a29151437ef1c7d1d",
|
|
|
|
),
|
|
|
|
);
|
|
|
|
|
|
|
|
// IRC
|
|
|
|
BOOT_FUNCTIONS.push(
|
|
|
|
async () =>
|
|
|
|
await peerDiscoveryClient.register(
|
2023-10-13 09:37:44 +00:00
|
|
|
"zrjHTx8tSQFWnmZ9JzK7XmJirqJQi2WRBLYp3fASaL2AfBQ",
|
2023-10-11 13:46:46 +00:00
|
|
|
),
|
|
|
|
);
|
|
|
|
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 = [
|
2023-10-12 14:04:14 +00:00
|
|
|
"zrjCnUBqmBqXXcc2yPnq517sXQtNcfZ2BHgnVTcbhSYxko7", // CID
|
|
|
|
"zrjEYq154PS7boERAbRAKMyRGzAR6CTHVRG6mfi5FV4q9FA", // ENS
|
2023-10-11 13:46:46 +00:00
|
|
|
"zrjEH3iojPLr7986o7iCn9THBmJmHiuDWmS1G6oT8DnfuFM", // HNS
|
|
|
|
];
|
|
|
|
|
|
|
|
for (const resolver of resolvers) {
|
|
|
|
BOOT_FUNCTIONS.push(async () => dnsClient.registerResolver(resolver));
|
|
|
|
}
|
2023-10-20 08:07:20 +00:00
|
|
|
BOOT_FUNCTIONS.push(async () => onBoot(true));
|
2023-10-11 13:46:46 +00:00
|
|
|
|
|
|
|
await bootup();
|
|
|
|
|
|
|
|
await Promise.all([
|
|
|
|
ethClient.ready(),
|
|
|
|
handshakeClient.ready(),
|
|
|
|
ipfsClient.ready(),
|
|
|
|
]);
|
|
|
|
}
|
|
|
|
|
|
|
|
async function bootup() {
|
|
|
|
for (const entry of Object.entries(BOOT_FUNCTIONS)) {
|
2023-10-13 06:51:11 +00:00
|
|
|
console.log(entry[1].toString());
|
2023-10-11 13:46:46 +00:00
|
|
|
await entry[1]();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-11-01 22:01:55 +00:00
|
|
|
const NavInput = forwardRef<HTMLInputElement>(
|
|
|
|
(props: React.InputHTMLAttributes<HTMLInputElement>, ref) => {
|
|
|
|
return <Input ref={ref} {...props} />;
|
|
|
|
},
|
|
|
|
);
|
2023-10-20 08:48:22 +00:00
|
|
|
|
2023-10-11 13:46:46 +00:00
|
|
|
export function Navigator() {
|
2023-10-16 22:06:47 +00:00
|
|
|
const { url: contextUrl, setUrl } = useBrowserState();
|
2023-10-18 13:29:40 +00:00
|
|
|
const { ready } = useLumeStatus();
|
2023-11-01 22:01:55 +00:00
|
|
|
const inputEl = useRef<HTMLInputElement | null>();
|
2023-10-11 13:46:46 +00:00
|
|
|
|
2023-10-20 08:48:22 +00:00
|
|
|
const browse = (inputValue: string) => {
|
2023-10-16 22:12:20 +00:00
|
|
|
let input = inputValue.trim();
|
2023-10-11 13:46:46 +00:00
|
|
|
|
|
|
|
// 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);
|
|
|
|
|
2023-10-16 23:50:58 +00:00
|
|
|
setUrl(url.toString() || "about:blank");
|
2023-10-11 13:46:46 +00:00
|
|
|
} catch (e) {
|
|
|
|
// Handle invalid URLs here, if needed
|
|
|
|
console.error("Invalid URL:", e);
|
|
|
|
}
|
2023-10-16 22:37:10 +00:00
|
|
|
};
|
2023-10-11 13:46:46 +00:00
|
|
|
|
2023-10-16 22:07:35 +00:00
|
|
|
useEffect(() => {
|
2023-11-01 22:01:55 +00:00
|
|
|
if (inputEl.current) {
|
2023-10-20 08:48:22 +00:00
|
|
|
inputEl.current.value = contextUrl;
|
|
|
|
}
|
2023-10-16 22:07:35 +00:00
|
|
|
}, [contextUrl]);
|
|
|
|
|
2023-10-11 13:46:46 +00:00
|
|
|
return (
|
2023-11-01 22:01:55 +00:00
|
|
|
<form
|
|
|
|
onSubmit={(e) => {
|
|
|
|
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"
|
|
|
|
>
|
2023-10-16 22:06:47 +00:00
|
|
|
<NavInput
|
2023-11-01 22:01:55 +00:00
|
|
|
ref={(el) => (inputEl.current = el)}
|
2023-10-18 13:29:40 +00:00
|
|
|
disabled={!ready}
|
2023-11-01 22:23:00 +00:00
|
|
|
className={`rounded-l-full bg-neutral-800 text-white border-none focus-visible:ring-offset-0 ${!ready ? "bg-neutral-950" : ""}`}
|
2023-10-20 08:48:22 +00:00
|
|
|
name="url"
|
2023-10-16 22:06:47 +00:00
|
|
|
/>
|
2023-11-01 22:01:55 +00:00
|
|
|
<Button
|
|
|
|
disabled={!ready}
|
|
|
|
className="rounded-r-full focus-visible:ring-offset-0"
|
|
|
|
>
|
2023-10-11 13:46:46 +00:00
|
|
|
Navigate
|
|
|
|
<Arrow />
|
|
|
|
</Button>
|
2023-10-20 08:48:22 +00:00
|
|
|
</form>
|
2023-10-11 13:46:46 +00:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
export function Browser() {
|
2023-11-01 22:01:55 +00:00
|
|
|
const { url, setUrl, isLoadingPage, setIsLoadingPage } = useBrowserState();
|
2023-10-16 19:20:00 +00:00
|
|
|
const status = useLumeStatus();
|
|
|
|
const auth = useAuth();
|
2023-10-17 00:37:59 +00:00
|
|
|
const iframeRef = useRef<HTMLIFrameElement>(null);
|
2023-10-11 13:46:46 +00:00
|
|
|
|
|
|
|
useEffect(() => {
|
2023-10-20 08:07:20 +00:00
|
|
|
boot({
|
|
|
|
onAuth(authed) {
|
2023-11-01 22:01:55 +00:00
|
|
|
auth.setIsLoggedIn(authed);
|
2023-10-20 08:07:20 +00:00
|
|
|
},
|
|
|
|
onBoot(booted) {
|
2023-11-01 22:01:55 +00:00
|
|
|
status.setReady(booted);
|
2023-10-20 08:07:20 +00:00
|
|
|
},
|
|
|
|
onInit(inited) {
|
2023-11-01 22:01:55 +00:00
|
|
|
status.setInited(inited);
|
|
|
|
},
|
|
|
|
}).catch((err) =>
|
|
|
|
console.error("[Browser.tsx] Failed to Boot Lume", { error: err }),
|
|
|
|
);
|
2023-10-11 13:46:46 +00:00
|
|
|
}, []);
|
|
|
|
|
2023-10-17 00:37:59 +00:00
|
|
|
const handleIframeLoad = () => {
|
|
|
|
try {
|
|
|
|
const newUrl = iframeRef?.current?.contentWindow?.location.href as string;
|
2023-10-17 01:46:30 +00:00
|
|
|
const urlObj = new URL(newUrl);
|
|
|
|
let realUrl = urlObj.pathname
|
|
|
|
.replace(/^\/browse\//, "")
|
|
|
|
.replace(/\/$/, "");
|
|
|
|
if (url !== realUrl) {
|
|
|
|
setUrl(realUrl);
|
2023-10-17 00:49:20 +00:00
|
|
|
}
|
2023-11-02 18:34:16 +00:00
|
|
|
setIsLoadingPage(false);
|
2023-10-17 00:37:59 +00:00
|
|
|
} 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,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2023-11-01 22:01:55 +00:00
|
|
|
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"
|
2023-11-02 18:34:16 +00:00
|
|
|
) {
|
|
|
|
setIsLoadingPage(true);
|
|
|
|
}
|
2023-11-01 22:01:55 +00:00
|
|
|
}
|
2023-11-02 18:34:16 +00:00
|
|
|
});
|
|
|
|
|
2023-11-01 22:01:55 +00:00
|
|
|
observer.observe(iframe, { attributes: true });
|
|
|
|
return () => observer.disconnect(); // Clean up on unmount
|
|
|
|
}
|
|
|
|
}, []);
|
|
|
|
|
2023-10-16 23:45:15 +00:00
|
|
|
return (
|
2023-11-01 22:01:55 +00:00
|
|
|
<>
|
|
|
|
{isLoadingPage ? (
|
|
|
|
<div className="fixed bottom-2 left-3">
|
2023-11-02 18:34:16 +00:00
|
|
|
<span className="max-w-4xl w-full block my-2 py-1 px-4 rounded-lg bg-gray-900/70 border border-gray-600 text-gray-400">
|
2023-11-01 22:01:55 +00:00
|
|
|
Loading {url}...
|
|
|
|
</span>
|
|
|
|
</div>
|
|
|
|
) : null}
|
|
|
|
<iframe
|
|
|
|
ref={iframeRef}
|
|
|
|
onLoad={handleIframeLoad}
|
|
|
|
src={url ? `/browse/${url}` : "about:blank"}
|
|
|
|
className="w-full h-full"
|
|
|
|
></iframe>
|
|
|
|
</>
|
2023-10-16 23:45:15 +00:00
|
|
|
);
|
2023-10-11 13:46:46 +00:00
|
|
|
}
|