Files
lume/packages/ark/src/provider.tsx

250 lines
6.3 KiB
TypeScript

import { LoaderIcon } from "@lume/icons";
import { NDKCacheAdapterTauri } from "@lume/ndk-cache-tauri";
import { LumeStorage } from "@lume/storage";
import { QUOTES, delay, sendNativeNotification } from "@lume/utils";
import NDK, {
NDKNip46Signer,
NDKPrivateKeySigner,
NDKRelay,
NDKRelayAuthPolicies,
} from "@nostr-dev-kit/ndk";
import { ndkAdapter } from "@nostr-fetch/adapter-ndk";
import { platform } from "@tauri-apps/plugin-os";
import { relaunch } from "@tauri-apps/plugin-process";
import Database from "@tauri-apps/plugin-sql";
import { check } from "@tauri-apps/plugin-updater";
import Markdown from "markdown-to-jsx";
import {
NostrFetcher,
normalizeRelayUrl,
normalizeRelayUrlSet,
} from "nostr-fetch";
import { PropsWithChildren, useEffect, useState } from "react";
import { createContext, useContextSelector } from "use-context-selector";
import { Ark } from "./ark";
type Context = {
storage: LumeStorage;
ark: Ark;
};
const LumeContext = createContext<Context>({
storage: undefined,
ark: undefined,
});
const LumeProvider = ({ children }: PropsWithChildren<object>) => {
const [context, setContext] = useState<Context>(undefined);
const [isNewVersion, setIsNewVersion] = useState(false);
async function initNostrSigner({
storage,
nsecbunker,
}: {
storage: LumeStorage;
nsecbunker?: boolean;
}) {
try {
if (!storage.account) return null;
// NIP-46 Signer
if (nsecbunker) {
const localSignerPrivkey = await storage.loadPrivkey(
`${storage.account.id}-nsecbunker`,
);
if (!localSignerPrivkey) return null;
const localSigner = new NDKPrivateKeySigner(localSignerPrivkey);
const bunker = new NDK({
explicitRelayUrls: normalizeRelayUrlSet([
"wss://relay.nsecbunker.com/",
"wss://nostr.vulpem.com/",
]),
});
await bunker.connect(3000);
const remoteSigner = new NDKNip46Signer(
bunker,
storage.account.pubkey,
localSigner,
);
await remoteSigner.blockUntilReady();
return remoteSigner;
}
// Privkey Signer
const userPrivkey = await storage.loadPrivkey(storage.account.pubkey);
if (!userPrivkey) {
return null;
}
return new NDKPrivateKeySigner(userPrivkey);
} catch (e) {
console.error(e);
return null;
}
}
async function init() {
const platformName = await platform();
const sqliteAdapter = await Database.load("sqlite:lume_v2.db");
const storage = new LumeStorage(sqliteAdapter, platformName);
await storage.init();
// check for new update
if (storage.settings.autoupdate) {
const update = await check();
// install new version
if (update) {
setIsNewVersion(true);
await update.downloadAndInstall();
await relaunch();
}
}
const explicitRelayUrls = normalizeRelayUrlSet([
"wss://bostr.nokotaro.com/",
"wss://nostr.mutinywallet.com",
]);
if (storage.settings.depot) {
await storage.launchDepot();
await delay(2000);
explicitRelayUrls.push(normalizeRelayUrl("ws://localhost:6090"));
}
// #TODO: user should config outbox relays
const outboxRelayUrls = normalizeRelayUrlSet(["wss://purplepag.es"]);
// #TODO: user should config blacklist relays
// No need to connect depot tunnel url
const blacklistRelayUrls = storage.settings.tunnelUrl.length
? [storage.settings.tunnelUrl, `${storage.settings.tunnelUrl}/`]
: [];
const cacheAdapter = new NDKCacheAdapterTauri(storage);
const ndk = new NDK({
cacheAdapter,
explicitRelayUrls,
outboxRelayUrls,
blacklistRelayUrls,
enableOutboxModel: !storage.settings.lowPowerMode,
autoConnectUserRelays: !storage.settings.lowPowerMode,
autoFetchUserMutelist: !storage.settings.lowPowerMode,
// clientName: 'Lume',
// clientNip89: '',
});
// add signer
const signer = await initNostrSigner({
storage,
nsecbunker: storage.settings.bunker,
});
if (signer) ndk.signer = signer;
// connect
await ndk.connect(3000);
// auth
ndk.relayAuthDefaultPolicy = async (relay: NDKRelay, challenge: string) => {
const signIn = NDKRelayAuthPolicies.signIn({ ndk, signer });
const event = await signIn(relay, challenge);
if (event) {
sendNativeNotification(
`You've sign in sucessfully to relay: ${relay.url}`,
);
return event;
}
};
// update account's metadata
if (storage.account) {
const user = ndk.getUser({ pubkey: storage.account.pubkey });
ndk.activeUser = user;
const contacts = await user.follows();
storage.account.contacts = [...contacts].map((user) => user.pubkey);
}
// init nostr fetcher
const fetcher = NostrFetcher.withCustomPool(ndkAdapter(ndk));
// ark utils
const ark = new Ark({ storage, ndk, fetcher });
// update context
setContext({ ark, storage });
}
useEffect(() => {
if (!context && !isNewVersion) init();
}, []);
if (!context) {
return (
<div
data-tauri-drag-region
className="relative flex items-center justify-center w-screen h-screen bg-neutral-50 dark:bg-neutral-950"
>
<div className="flex flex-col items-start max-w-2xl gap-1">
<h5 className="font-semibold uppercase">TIP:</h5>
<Markdown
options={{
overrides: {
a: {
props: {
className: "text-blue-500 hover:text-blue-600",
target: "_blank",
},
},
},
}}
className="text-4xl font-semibold leading-snug text-neutral-300 dark:text-neutral-700"
>
{QUOTES[Math.floor(Math.random() * QUOTES.length)]}
</Markdown>
</div>
<div className="absolute bottom-5 right-5 inline-flex items-center gap-2.5">
<LoaderIcon className="w-6 h-6 text-blue-500 animate-spin" />
<p className="font-semibold">
{isNewVersion ? "Found a new version, updating..." : "Starting..."}
</p>
</div>
</div>
);
}
return (
<LumeContext.Provider
value={{ ark: context.ark, storage: context.storage }}
>
{children}
</LumeContext.Provider>
);
};
const useArk = () => {
const context = useContextSelector(LumeContext, (state) => state.ark);
if (context === undefined) {
throw new Error("Please import Ark Provider to use useArk() hook");
}
return context;
};
const useStorage = () => {
const context = useContextSelector(LumeContext, (state) => state.storage);
if (context === undefined) {
throw new Error("Please import Ark Provider to use useStorage() hook");
}
return context;
};
export { LumeProvider, useArk, useStorage };