From 3c4bd393845659e70c3bcee5fda07b652ab5b202 Mon Sep 17 00:00:00 2001 From: reya Date: Tue, 6 Feb 2024 19:28:46 +0700 Subject: [PATCH] feat: update rust nostr --- package.json | 2 +- packages/ark/package.json | 11 - packages/ark/src/ark.ts | 595 +----------------- packages/ark/src/components/note/content.tsx | 6 +- packages/ark/src/components/note/menu.tsx | 40 +- packages/ark/src/components/note/thread.tsx | 2 +- packages/ark/src/components/user/provider.tsx | 9 +- packages/ark/src/hooks/useEvent.ts | 2 +- packages/ark/src/hooks/useProfile.ts | 8 +- packages/ark/src/provider.tsx | 247 +------- packages/tailwindcss/package.json | 2 +- packages/tailwindcss/tailwind.config.js | 4 +- packages/types/index.d.ts | 72 ++- pnpm-lock.yaml | 587 +++++++++++++---- src-tauri/capabilities/main.json | 41 ++ src-tauri/gen/main.json | 74 --- src-tauri/gen/schemas/capabilities.json | 2 +- src-tauri/src/main.rs | 66 +- src-tauri/src/nostr/event.rs | 19 +- src-tauri/src/nostr/keys.rs | 24 +- src-tauri/src/nostr/metadata.rs | 15 +- src-tauri/tauri.conf.json | 7 +- 22 files changed, 721 insertions(+), 1114 deletions(-) create mode 100644 src-tauri/capabilities/main.json delete mode 100644 src-tauri/gen/main.json diff --git a/package.json b/package.json index 8c7c3fa5..6e18a067 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "build": "turbo run build", "dev": "turbo run dev", "web:dev": "turbo run dev --filter web", - "desktop:dev": "turbo run dev --filter desktop2", + "desktop:dev": "turbo run dev --filter desktop", "tauri": "tauri" }, "devDependencies": { diff --git a/packages/ark/package.json b/packages/ark/package.json index c59e8c0e..b547fd37 100644 --- a/packages/ark/package.json +++ b/packages/ark/package.json @@ -6,11 +6,7 @@ "dependencies": { "@getalby/sdk": "^3.2.3", "@lume/icons": "workspace:^", - "@lume/ndk-cache-tauri": "workspace:^", - "@lume/storage": "workspace:^", "@lume/utils": "workspace:^", - "@nostr-dev-kit/ndk": "^2.4.0", - "@nostr-fetch/adapter-ndk": "^0.15.0", "@radix-ui/react-avatar": "^1.0.4", "@radix-ui/react-collapsible": "^1.0.3", "@radix-ui/react-dialog": "^1.0.5", @@ -21,13 +17,9 @@ "@tanstack/react-query": "^5.18.1", "get-urls": "^12.1.0", "jotai": "^2.6.4", - "linkify-react": "^4.1.3", - "linkifyjs": "^4.1.3", "media-chrome": "^2.1.0", "minidenticons": "^4.2.0", "nanoid": "^5.0.5", - "nostr-fetch": "^0.15.0", - "nostr-tools": "1.17.0", "qrcode.react": "^3.1.0", "re-resizable": "^6.9.11", "react": "^18.2.0", @@ -37,8 +29,6 @@ "react-string-replace": "^1.1.1", "sonner": "^1.4.0", "string-strip-html": "^13.4.6", - "tippy.js": "^6.3.7", - "use-context-selector": "^1.4.1", "virtua": "^0.23.3" }, "devDependencies": { @@ -46,7 +36,6 @@ "@lume/tsconfig": "workspace:^", "@lume/types": "workspace:^", "@types/react": "^18.2.52", - "tailwind-merge": "^2.2.1", "tailwindcss": "^3.4.1", "typescript": "^5.3.3" } diff --git a/packages/ark/src/ark.ts b/packages/ark/src/ark.ts index f3e9dc7f..3ea531f8 100644 --- a/packages/ark/src/ark.ts +++ b/packages/ark/src/ark.ts @@ -1,301 +1,56 @@ -import { Account, type NDKEventWithReplies, type NIP05 } from "@lume/types"; -import NDK, { - NDKEvent, - NDKFilter, - NDKKind, - NDKNip46Signer, - NDKPrivateKeySigner, - NDKRelay, - NDKSubscriptionCacheUsage, - NDKTag, - NDKUser, - NostrEvent, -} from "@nostr-dev-kit/ndk"; -import { ndkAdapter } from "@nostr-fetch/adapter-ndk"; -import { open } from "@tauri-apps/plugin-dialog"; -import { readFile } from "@tauri-apps/plugin-fs"; -import { fetch } from "@tauri-apps/plugin-http"; -import { NostrFetcher, normalizeRelayUrl } from "nostr-fetch"; -import { nip19 } from "nostr-tools"; +import { type CurrentAccount, Event, Metadata } from "@lume/types"; +import { invoke } from "@tauri-apps/api/core"; export class Ark { - public ndk: NDK; - public account: Account; + public account: CurrentAccount; - constructor({ - ndk, - account, - }: { - ndk: NDK; - account: Account; - }) { - this.ndk = ndk; + constructor(account: CurrentAccount) { this.account = account; } - public async connectDepot() { - return this.ndk.addExplicitRelay( - new NDKRelay(normalizeRelayUrl("ws://localhost:6090")), - undefined, - true, - ); - } - - public updateNostrSigner({ - signer, - }: { signer: NDKNip46Signer | NDKPrivateKeySigner }) { - this.ndk.signer = signer; - return this.ndk.signer; - } - - public subscribe({ - filter, - closeOnEose = false, - cb, - }: { - filter: NDKFilter; - closeOnEose: boolean; - cb: (event: NDKEvent) => void; - }) { - const sub = this.ndk.subscribe(filter, { closeOnEose }); - sub.addListener("event", (event: NDKEvent) => cb(event)); - return sub; - } - - public getNDKEvent(event: NostrEvent) { - return new NDKEvent(this.ndk, event); - } - - public async createEvent({ - kind, - tags, - content, - rootReplyTo = undefined, - replyTo = undefined, - }: { - kind: NDKKind | number; - tags: NDKTag[]; - content?: string; - rootReplyTo?: string; - replyTo?: string; - }) { + public async event_to_bech32(id: string, relays: string[]) { try { - const event = new NDKEvent(this.ndk); - if (content) event.content = content; - event.kind = kind; - event.tags = tags; - - if (rootReplyTo) { - const rootEvent = await this.ndk.fetchEvent(rootReplyTo); - if (rootEvent) event.tag(rootEvent, "root"); - } - - if (replyTo) { - const replyEvent = await this.ndk.fetchEvent(replyTo); - if (replyEvent) event.tag(replyEvent, "reply"); - } - - const publish = await event.publish(); - - if (!publish) throw new Error("Failed to publish event"); - return { - id: event.id, - seens: [...publish.values()].map((item) => item.url), - }; - } catch (e) { - throw new Error(e); - } - } - - public getCleanPubkey(pubkey: string) { - try { - let hexstring = pubkey - .replace("nostr:", "") - .split("'")[0] - .split(".")[0] - .split(",")[0] - .split("?")[0]; - - if ( - hexstring.startsWith("npub1") || - hexstring.startsWith("nprofile1") || - hexstring.startsWith("naddr1") - ) { - const decoded = nip19.decode(hexstring); - - if (decoded.type === "nprofile") hexstring = decoded.data.pubkey; - if (decoded.type === "npub") hexstring = decoded.data; - if (decoded.type === "naddr") hexstring = decoded.data.pubkey; - } - - return hexstring; - } catch (e) { - console.log(e); - } - } - - public async getUserProfile(pubkey?: string) { - try { - const currentUserPubkey = this.account.pubkey; - const hexstring = pubkey - ? this.getCleanPubkey(pubkey) - : currentUserPubkey; - - const user = this.ndk.getUser({ - pubkey: hexstring, + const cmd: string = await invoke("event_to_bech32", { + id, + relays, }); - - const profile = await user.fetchProfile({ - cacheUsage: NDKSubscriptionCacheUsage.CACHE_FIRST, - }); - - return profile; + return cmd; } catch { - throw new Error("user not found"); + console.error("get nevent id failed"); } } - public async getUserContacts(pubkey?: string) { + public async get_event(id: string) { try { - const currentUserPubkey = this.account.pubkey; - const hexstring = pubkey - ? this.getCleanPubkey(pubkey) - : currentUserPubkey; - - const user = this.ndk.getUser({ - pubkey: hexstring, - }); - - const contacts = [...(await user.follows(undefined, false))].map( - (user) => user.pubkey, - ); - - if (!pubkey || pubkey === this.account.pubkey) { - this.account.contacts = contacts; - } - - return contacts; + const cmd: string = await invoke("get_event", { id }); + const event = JSON.parse(cmd) as Event; + return event; } catch (e) { - console.error(e); + console.error("failed to get event", id); } } - public async getUserRelays({ pubkey }: { pubkey?: string }) { - try { - const user = this.ndk.getUser({ - pubkey: pubkey ? pubkey : this.account.pubkey, - }); - return await user.relayList(); - } catch (e) { - console.error(e); - } - } - - public async newContactList({ tags }: { tags: NDKTag[] }) { - const publish = await this.createEvent({ - kind: NDKKind.Contacts, - tags: tags, - }); - - if (publish) { - this.account.contacts = tags.map((item) => item[1]); - return publish; - } - } - - public async createContact(pubkey: string) { - const user = this.ndk.getUser({ pubkey: this.account.pubkey }); - const contacts = await user.follows(); - return await user.follow(new NDKUser({ pubkey: pubkey }), contacts); - } - - public async deleteContact(pubkey: string) { - const user = this.ndk.getUser({ pubkey: this.account.pubkey }); - const contacts = await user.follows(); - contacts.delete(new NDKUser({ pubkey: pubkey })); - - const event = new NDKEvent(this.ndk); - event.content = ""; - event.kind = NDKKind.Contacts; - event.tags = [...contacts].map((item) => [ - "p", - item.pubkey, - item.relayUrls?.[0] || "", - "", - ]); - - return await event.publish(); - } - - public async getAllEvents({ filter }: { filter: NDKFilter }) { - const events = await this.ndk.fetchEvents(filter); - if (!events) return []; - return [...events]; - } - - public getCleanEventId(id: string) { - let eventId: string = id.replace("nostr:", "").split("'")[0].split(".")[0]; - - if ( - eventId.startsWith("nevent1") || - eventId.startsWith("note1") || - eventId.startsWith("naddr1") - ) { - const decode = nip19.decode(eventId); - if (decode.type === "nevent") eventId = decode.data.id; - if (decode.type === "note") eventId = decode.data; - } - - return eventId; - } - - public async getEventById(id: string) { - try { - const eventId = this.getCleanEventId(id); - return await this.ndk.fetchEvent(eventId); - } catch { - throw new Error("event not found"); - } - } - - public async getEventByFilter({ - filter, - cache, - }: { filter: NDKFilter; cache?: NDKSubscriptionCacheUsage }) { - const event = await this.ndk.fetchEvent(filter, { - cacheUsage: cache || NDKSubscriptionCacheUsage.CACHE_FIRST, - }); - - if (!event) return null; - return event; - } - - public async getEvents(filter: NDKFilter) { - const events = await this.ndk.fetchEvents(filter); - if (!events) return []; - return [...events]; - } - - public getEventThread({ + public parse_event_thread({ content, tags, - }: { content: string; tags: NDKTag[] }) { + }: { content: string; tags: string[][] }) { let rootEventId: string = null; let replyEventId: string = null; + // Ignore quote repost if (content.includes("nostr:note1") || content.includes("nostr:nevent1")) return null; + // Get all event references from tags, ignore mention const events = tags.filter((el) => el[0] === "e" && el[3] !== "mention"); if (!events.length) return null; - - if (events.length === 1) + if (events.length === 1) { return { rootEventId: events[0][1], replyEventId: null, }; - + } if (events.length > 1) { rootEventId = events.find((el) => el[3] === "root")?.[1]; replyEventId = events.find((el) => el[3] === "reply")?.[1]; @@ -312,310 +67,24 @@ export class Ark { }; } - public async getThreads(id: string) { - const eventId = this.getCleanEventId(id); - const fetcher = NostrFetcher.withCustomPool(ndkAdapter(this.ndk)); - const relayUrls = Array.from(this.ndk.pool.relays.keys()); - + public async get_metadata(id: string) { try { - const rawEvents = (await fetcher.fetchAllEvents( - relayUrls, - { - kinds: [NDKKind.Text], - "#e": [eventId], - }, - { since: 0 }, - { sort: true }, - )) as unknown as NostrEvent[]; - - const events = rawEvents.map( - (event) => new NDKEvent(this.ndk, event), - ) as NDKEvent[] as NDKEventWithReplies[]; - - if (events.length > 0) { - const replies = new Set(); - for (const event of events) { - const tags = event.tags.filter( - (el) => el[0] === "e" && el[1] !== id && el[3] !== "mention", - ); - if (tags.length > 0) { - for (const tag of tags) { - const rootIndex = events.findIndex((el) => el.id === tag[1]); - if (rootIndex !== -1) { - const rootEvent = events[rootIndex]; - if (rootEvent?.replies) { - rootEvent.replies.push(event); - } else { - rootEvent.replies = [event]; - } - replies.add(event.id); - } - } - } - } - const cleanEvents = events.filter((ev) => !replies.has(ev.id)); - return cleanEvents; - } - - return events; + const cmd: Metadata = await invoke("get_metadata", { id }); + return cmd; } catch (e) { - console.log(e); - } finally { - fetcher.shutdown(); + console.error("failed to get metadata", id); } } - public async getAllRelaysFromContacts({ signal }: { signal: AbortSignal }) { - const fetcher = NostrFetcher.withCustomPool(ndkAdapter(this.ndk)); - const connectedRelays = Array.from(this.ndk.pool.relays.keys()); - + public async user_to_bech32(key: string, relays: string[]) { try { - const relayMap = new Map(); - const relayEvents = fetcher.fetchLatestEventsPerAuthor( - { - authors: this.account.contacts, - relayUrls: connectedRelays, - }, - { kinds: [NDKKind.RelayList] }, - 1, - { abortSignal: signal }, - ); - - for await (const { author, events } of relayEvents) { - if (events.length) { - const relayTags = events[0].tags.filter((item) => item[0] === "r"); - for (const tag of relayTags) { - const item = relayMap.get(tag[1]); - if (item?.length) { - item.push(author); - } else { - relayMap.set(tag[1], [author]); - } - } - } - } - - return relayMap; - } catch (e) { - console.log(e); - } finally { - fetcher.shutdown(); - } - } - - public async getInfiniteEvents({ - filter, - limit, - pageParam = 0, - signal = undefined, - dedup = true, - }: { - filter: NDKFilter; - limit: number; - pageParam?: number; - signal?: AbortSignal; - dedup?: boolean; - }) { - const fetcher = NostrFetcher.withCustomPool(ndkAdapter(this.ndk)); - const relayUrls = Array.from(this.ndk.pool.relays.keys()); - const seenIds = new Set(); - const dedupQueue = new Set(); - - try { - const events = await fetcher.fetchLatestEvents(relayUrls, filter, limit, { - asOf: pageParam === 0 ? undefined : pageParam, - abortSignal: signal, + const cmd: string = await invoke("user_to_bech32", { + key, + relays, }); - - const ndkEvents = events.map((event) => { - return new NDKEvent(this.ndk, event); - }); - - if (dedup) { - for (const event of ndkEvents) { - const tags = event.tags - .filter((el) => el[0] === "e") - ?.map((item) => item[1]); - - if (tags.length) { - for (const tag of tags) { - if (seenIds.has(tag)) { - dedupQueue.add(event.id); - break; - } - - seenIds.add(tag); - } - } - } - - return ndkEvents - .filter((event) => !dedupQueue.has(event.id)) - .sort((a, b) => b.created_at - a.created_at); - } - - return ndkEvents.sort((a, b) => b.created_at - a.created_at); - } catch (e) { - console.log(e); - } finally { - fetcher.shutdown(); + return cmd; + } catch { + console.error("get nprofile id failed"); } } - - public async getRelayEvents({ - relayUrl, - filter, - limit, - pageParam = 0, - signal = undefined, - }: { - relayUrl: string; - filter: NDKFilter; - limit: number; - pageParam?: number; - signal?: AbortSignal; - dedup?: boolean; - }) { - const fetcher = NostrFetcher.withCustomPool(ndkAdapter(this.ndk)); - - try { - const events = await fetcher.fetchLatestEvents( - [normalizeRelayUrl(relayUrl)], - filter, - limit, - { - asOf: pageParam === 0 ? undefined : pageParam, - abortSignal: signal, - }, - ); - - const ndkEvents = events.map((event) => { - return new NDKEvent(this.ndk, event); - }); - - return ndkEvents.sort((a, b) => b.created_at - a.created_at); - } catch (e) { - console.log(e); - } finally { - fetcher.shutdown(); - } - } - - /** - * Upload media file to nostr.build - * @todo support multiple backends - */ - public async upload({ fileExts }: { fileExts?: string[] }) { - const defaultExts = ["png", "jpeg", "jpg", "gif"].concat(fileExts); - - const selected = await open({ - multiple: false, - filters: [ - { - name: "Image", - extensions: defaultExts, - }, - ], - }); - - if (!selected) return null; - - const file = await readFile(selected.path); - const blob = new Blob([file]); - - const data = new FormData(); - data.append("fileToUpload", blob); - data.append("submit", "Upload Image"); - - const res = await fetch("https://nostr.build/api/v2/upload/files", { - method: "POST", - body: data, - }); - - if (!res.ok) return null; - - const json = await res.json(); - const content = json.data[0]; - - return content.url as string; - } - - public async validateNIP05({ - pubkey, - nip05, - signal, - }: { - pubkey: string; - nip05: string; - signal?: AbortSignal; - }) { - const localPath = nip05.split("@")[0]; - const service = nip05.split("@")[1]; - const verifyURL = `https://${service}/.well-known/nostr.json?name=${localPath}`; - - const res = await fetch(verifyURL, { - method: "GET", - headers: { - "Content-Type": "application/json; charset=utf-8", - }, - signal, - }); - - if (!res.ok) throw new Error(`Failed to fetch NIP-05 service: ${nip05}`); - - const data: NIP05 = await res.json(); - - if (!data.names) return false; - if (data.names[localPath.toLowerCase()] === pubkey) return true; - if (data.names[localPath] === pubkey) return true; - - return false; - } - - public async getAppRecommend({ - unknownKind, - author, - }: { unknownKind: string; author?: string }) { - const event = await this.ndk.fetchEvent({ - kinds: [NDKKind.AppRecommendation], - "#d": [unknownKind], - authors: this.account.contacts || [author], - }); - - if (event) return event.tags.filter((item) => item[0] !== "d"); - - const altEvent = await this.ndk.fetchEvent({ - kinds: [NDKKind.AppHandler], - "#k": [unknownKind], - authors: this.account.contacts || [author], - }); - - if (altEvent) return altEvent.tags.filter((item) => item[0] !== "d"); - - return null; - } - - public async getOAuthServices() { - const trusted: NDKEvent[] = []; - - const services = await this.ndk.fetchEvents({ - kinds: [NDKKind.AppHandler], - "#k": ["24133"], - }); - - for (const service of services) { - const nip05 = JSON.parse(service.content).nip05 as string; - try { - const validate = await this.validateNIP05({ - pubkey: service.pubkey, - nip05, - }); - if (validate) trusted.push(service); - } catch (e) { - console.log(e); - } - } - - return trusted; - } } diff --git a/packages/ark/src/components/note/content.tsx b/packages/ark/src/components/note/content.tsx index 1ef1ffde..cf5bae68 100644 --- a/packages/ark/src/components/note/content.tsx +++ b/packages/ark/src/components/note/content.tsx @@ -1,4 +1,4 @@ -import { useStorage } from "@lume/storage"; +import { Kind } from "@lume/types"; import { AUDIOS, IMAGES, @@ -9,7 +9,6 @@ import { cn, regionNames, } from "@lume/utils"; -import { NDKKind } from "@nostr-dev-kit/ndk"; import { fetch } from "@tauri-apps/plugin-http"; import getUrls from "get-urls"; import { nanoid } from "nanoid"; @@ -32,7 +31,6 @@ export function NoteContent({ }: { className?: string; }) { - const storage = useStorage(); const event = useNoteContext(); const [content, setContent] = useState(event.content); @@ -42,7 +40,7 @@ export function NoteContent({ }); const richContent = useMemo(() => { - if (event.kind !== NDKKind.Text) return content; + if (event.kind !== Kind.Text) return content; let parsedContent: string | ReactNode[] = stripHtml( content.replace(/\n{2,}\s*/g, "\n"), diff --git a/packages/ark/src/components/note/menu.tsx b/packages/ark/src/components/note/menu.tsx index 75f2955d..d852487a 100644 --- a/packages/ark/src/components/note/menu.tsx +++ b/packages/ark/src/components/note/menu.tsx @@ -2,32 +2,22 @@ import { HorizontalDotsIcon } from "@lume/icons"; import { COL_TYPES } from "@lume/utils"; import * as DropdownMenu from "@radix-ui/react-dropdown-menu"; import { writeText } from "@tauri-apps/plugin-clipboard-manager"; -import { nip19 } from "nostr-tools"; -import { type EventPointer } from "nostr-tools/lib/types/nip19"; -import { useState } from "react"; import { useTranslation } from "react-i18next"; import { Link, useNavigate } from "react-router-dom"; -import { toast } from "sonner"; +import { useArk } from "../../hooks/useArk"; import { useColumnContext } from "../column/provider"; import { useNoteContext } from "./provider"; export function NoteMenu() { + const ark = useArk(); const event = useNoteContext(); const navigate = useNavigate(); const { t } = useTranslation(); const { addColumn } = useColumnContext(); - const [open, setOpen] = useState(false); - const copyID = async () => { - await writeText( - nip19.neventEncode({ - id: event.id, - author: event.pubkey, - } as EventPointer), - ); - setOpen(false); + await writeText(await ark.event_to_bech32(event.id, [""])); }; const copyRaw = async () => { @@ -35,26 +25,17 @@ export function NoteMenu() { }; const copyNpub = async () => { - await writeText(nip19.npubEncode(event.pubkey)); + await writeText(await ark.user_to_bech32(event.pubkey, [""])); }; const copyLink = async () => { await writeText( - `https://njump.me/${nip19.neventEncode({ - id: event.id, - author: event.pubkey, - } as EventPointer)}`, + `https://njump.me/${await ark.event_to_bech32(event.id, [""])}`, ); - setOpen(false); - }; - - const muteUser = async () => { - event.muted(); - toast.info("You've muted this user"); }; return ( - + - diff --git a/packages/ark/src/components/note/thread.tsx b/packages/ark/src/components/note/thread.tsx index a112ad9b..61de4b3c 100644 --- a/packages/ark/src/components/note/thread.tsx +++ b/packages/ark/src/components/note/thread.tsx @@ -14,7 +14,7 @@ export function NoteThread({ }) { const ark = useArk(); const event = useNoteContext(); - const thread = ark.getEventThread({ + const thread = ark.parse_event_thread({ content: event.content, tags: event.tags, }); diff --git a/packages/ark/src/components/user/provider.tsx b/packages/ark/src/components/user/provider.tsx index 69b7ef0a..bd97492a 100644 --- a/packages/ark/src/components/user/provider.tsx +++ b/packages/ark/src/components/user/provider.tsx @@ -1,9 +1,9 @@ -import { NDKUserProfile } from "@nostr-dev-kit/ndk"; +import { Metadata } from "@lume/types"; import { useQuery } from "@tanstack/react-query"; import { ReactNode, createContext, useContext } from "react"; import { useArk } from "../../hooks/useArk"; -const UserContext = createContext(null); +const UserContext = createContext(null); export function UserProvider({ pubkey, @@ -14,9 +14,10 @@ export function UserProvider({ const { data: user } = useQuery({ queryKey: ["user", pubkey], queryFn: async () => { - if (embed) return JSON.parse(embed) as NDKUserProfile; + if (embed) return JSON.parse(embed) as Metadata; + + const profile = await ark.get_metadata(pubkey); - const profile = await ark.getUserProfile(pubkey); if (!profile) throw new Error( `Cannot get metadata for ${pubkey}, will be retry after 10 seconds`, diff --git a/packages/ark/src/hooks/useEvent.ts b/packages/ark/src/hooks/useEvent.ts index ec100889..4e4e203e 100644 --- a/packages/ark/src/hooks/useEvent.ts +++ b/packages/ark/src/hooks/useEvent.ts @@ -6,7 +6,7 @@ export function useEvent(id: string) { const { isLoading, isError, data } = useQuery({ queryKey: ["event", id], queryFn: async () => { - const event = await ark.getEventById(id); + const event = await ark.get_event(id); if (!event) throw new Error( `Cannot get event with ${id}, will be retry after 10 seconds`, diff --git a/packages/ark/src/hooks/useProfile.ts b/packages/ark/src/hooks/useProfile.ts index 3aa2d90c..87ff0917 100644 --- a/packages/ark/src/hooks/useProfile.ts +++ b/packages/ark/src/hooks/useProfile.ts @@ -1,11 +1,8 @@ -import { NDKUserProfile } from "@nostr-dev-kit/ndk"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import { useArk } from "./useArk"; export function useProfile(pubkey: string) { const ark = useArk(); - const queryClient = useQueryClient(); - const { isLoading, isError, @@ -13,16 +10,13 @@ export function useProfile(pubkey: string) { } = useQuery({ queryKey: ["user", pubkey], queryFn: async () => { - const profile = await ark.getUserProfile(pubkey); + const profile = await ark.get_metadata(pubkey); if (!profile) throw new Error( `Cannot get metadata for ${pubkey}, will be retry after 10 seconds`, ); return profile; }, - initialData: () => { - return queryClient.getQueryData(["user", pubkey]) as NDKUserProfile; - }, refetchOnMount: false, refetchOnWindowFocus: false, refetchOnReconnect: false, diff --git a/packages/ark/src/provider.tsx b/packages/ark/src/provider.tsx index b7ea57ed..492481bc 100644 --- a/packages/ark/src/provider.tsx +++ b/packages/ark/src/provider.tsx @@ -1,255 +1,18 @@ -import { LoaderIcon } from "@lume/icons"; -import { NDKCacheAdapterTauri } from "@lume/ndk-cache-tauri"; -import { useStorage } from "@lume/storage"; -import { - FETCH_LIMIT, - QUOTES, - activityUnreadAtom, - sendNativeNotification, -} from "@lume/utils"; -import NDK, { - NDKEvent, - NDKKind, - NDKNip46Signer, - NDKPrivateKeySigner, - NDKRelay, - NDKRelayAuthPolicies, - NDKUser, -} from "@nostr-dev-kit/ndk"; -import { useQueryClient } from "@tanstack/react-query"; -import { message } from "@tauri-apps/plugin-dialog"; -import { fetch } from "@tauri-apps/plugin-http"; -import { useSetAtom } from "jotai"; -import Linkify from "linkify-react"; -import { normalizeRelayUrlSet } from "nostr-fetch"; import { PropsWithChildren, useEffect, useState } from "react"; -import { toast } from "sonner"; import { Ark } from "./ark"; import { LumeContext } from "./context"; export const LumeProvider = ({ children }: PropsWithChildren) => { - const storage = useStorage(); - const queryClient = useQueryClient(); - const setUnreadActivity = useSetAtom(activityUnreadAtom); - const [ark, setArk] = useState(undefined); - const [ndk, setNDK] = useState(undefined); - - async function initNostrSigner({ - nsecbunker, - }: { - nsecbunker?: boolean; - }) { - try { - if (!storage.currentUser) return null; - - // NIP-46 Signer - if (nsecbunker) { - const localSignerPrivkey = await storage.loadPrivkey( - storage.currentUser.pubkey, - ); - - 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(2000); - - const remoteSigner = new NDKNip46Signer( - bunker, - storage.currentUser.pubkey, - localSigner, - ); - await remoteSigner.blockUntilReady(); - - return remoteSigner; - } - - // Privkey Signer - const userPrivkey = await storage.loadPrivkey(storage.currentUser.pubkey); - if (!userPrivkey) return null; - - // load nwc - storage.nwc = await storage.loadPrivkey( - `${storage.currentUser.pubkey}.nwc`, - ); - - return new NDKPrivateKeySigner(userPrivkey); - } catch (e) { - toast.error(String(e)); - return null; - } - } - - async function initNDK() { - try { - const explicitRelayUrls = normalizeRelayUrlSet([ - "wss://nostr.mutinywallet.com/", - "wss://bostr.nokotaro.com/", - "wss://purplepag.es/", - ]); - - const outboxRelayUrls = normalizeRelayUrlSet(["wss://purplepag.es/"]); - - const tauriCache = new NDKCacheAdapterTauri(storage); - const ndk = new NDK({ - cacheAdapter: tauriCache, - explicitRelayUrls, - outboxRelayUrls, - enableOutboxModel: !storage.settings.lowPower, - autoConnectUserRelays: !storage.settings.lowPower, - autoFetchUserMutelist: false, // #TODO: add support mute list - clientName: "Lume", - }); - - // use tauri fetch - ndk.httpFetch = fetch; - - // add signer - const signer = await initNostrSigner({ - nsecbunker: storage.settings.nsecbunker, - }); - - if (signer) ndk.signer = signer; - - // connect - await ndk.connect(3000); - - // auth - ndk.relayAuthDefaultPolicy = async ( - relay: NDKRelay, - challenge: string, - ) => { - const signIn = NDKRelayAuthPolicies.signIn({ ndk }); - const event = await signIn(relay, challenge).catch((e) => - console.log("auth failed", e), - ); - if (event) { - await sendNativeNotification( - `You've sign in sucessfully to relay: ${relay.url}`, - ); - return event; - } - }; - - setNDK(ndk); - } catch (e) { - toast.error(String(e)); - } - } - - async function initArk() { - if (!ndk) await message("Something wrong!", { type: "error" }); - - // ark utils - const ark = new Ark({ ndk, account: storage.currentUser }); - - try { - if (ndk && storage.currentUser) { - const user = new NDKUser({ pubkey: storage.currentUser.pubkey }); - ndk.activeUser = user; - - // update contacts - const contacts = await ark.getUserContacts(); - - if (contacts?.length) { - console.log("total contacts: ", contacts.length); - for (const pubkey of ark.account.contacts) { - await queryClient.prefetchQuery({ - queryKey: ["user", pubkey], - queryFn: async () => { - return await ark.getUserProfile(pubkey); - }, - }); - } - } - - // subscribe for new activity - const activitySub = ndk.subscribe( - { - kinds: [NDKKind.Text, NDKKind.Repost, NDKKind.Zap], - since: Math.floor(Date.now() / 1000), - "#p": [ark.account.pubkey], - }, - { closeOnEose: false, groupable: false }, - ); - - activitySub.addListener("event", async (event: NDKEvent) => { - if (event.pubkey === storage.currentUser.pubkey) return; - - setUnreadActivity((state) => state + 1); - const profile = await ark.getUserProfile(event.pubkey); - - switch (event.kind) { - case NDKKind.Text: - return await sendNativeNotification( - `${ - profile.displayName || profile.name || "Anon" - } has replied to your note`, - ); - case NDKKind.Repost: - return await sendNativeNotification( - `${ - profile.displayName || profile.name || "Anon" - } has reposted to your note`, - ); - case NDKKind.Zap: - return await sendNativeNotification( - `${ - profile.displayName || profile.name || "Anon" - } has zapped to your note`, - ); - default: - break; - } - }); - } - } catch (e) { - toast.error(String(e)); - } - - setArk(ark); - } useEffect(() => { - if (ndk) initArk(); - }, [ndk]); + async function setupArk() { + const _ark = new Ark(); + setArk(_ark); + } - useEffect(() => { - if (!ark && !ndk) initNDK(); + if (!ark) setupArk(); }, []); - if (!ark) { - return ( -
-
-
TIP:
- -
- {QUOTES[Math.floor(Math.random() * QUOTES.length)]} -
-
-
-
- -

Starting

-
-
- ); - } - return {children}; }; diff --git a/packages/tailwindcss/package.json b/packages/tailwindcss/package.json index ad99a49a..12460629 100644 --- a/packages/tailwindcss/package.json +++ b/packages/tailwindcss/package.json @@ -16,6 +16,6 @@ "tailwindcss": "^3.4.1" }, "dependencies": { - "tailwindcss-radix-colors": "^1.2.0" + "@evilmartians/harmony": "^1.2.0" } } diff --git a/packages/tailwindcss/tailwind.config.js b/packages/tailwindcss/tailwind.config.js index 151458c3..7833bdb8 100644 --- a/packages/tailwindcss/tailwind.config.js +++ b/packages/tailwindcss/tailwind.config.js @@ -1,5 +1,8 @@ +import harmonyPalette from "@evilmartians/harmony/tailwind"; + const config = { theme: { + colors: harmonyPalette, extend: { keyframes: { slideDownAndFade: { @@ -41,7 +44,6 @@ const config = { }, }, plugins: [ - require("tailwindcss-radix-colors"), require("@tailwindcss/forms"), require("@tailwindcss/typography"), require("tailwind-scrollbar")({ nocompatible: true }), diff --git a/packages/types/index.d.ts b/packages/types/index.d.ts index 2ae39b1e..1a1f9c24 100644 --- a/packages/types/index.d.ts +++ b/packages/types/index.d.ts @@ -1,8 +1,56 @@ -import { - type NDKEvent, - NDKRelayList, - type NDKUserProfile, -} from "@nostr-dev-kit/ndk"; +import { type NDKEvent, type NDKUserProfile } from "@nostr-dev-kit/ndk"; + +export interface Keys { + npub: string; + nsec: string; +} + +export enum Kind { + Metadata = 0, + Text = 1, + RecommendRelay = 2, + Contacts = 3, + Repost = 6, + Reaction = 7, + // NIP-89: App Metadata + AppRecommendation = 31989, + AppHandler = 31990, + // #TODO: Add all nostr kinds +} + +export interface Event { + id: string; + pubkey: string; + created_at: number; + kind: Kind; + tags: string[][]; + content: string; + sig: string; +} + +export interface Metadata { + name: Option; + display_name: Option; + about: Option; + website: Option; + picture: Option; + banner: Option; + nip05: Option; + lud06: Option; + lud16: Option; +} + +export interface CurrentAccount { + npub: string; + contacts: string[]; + interests: Interests; +} + +export interface Interests { + hashtags: string[]; + users: string[]; + words: string[]; +} export interface RichContent { parsed: string; @@ -12,14 +60,6 @@ export interface RichContent { notes: string[]; } -export interface Account { - id: string; - pubkey: string; - is_active: number; - contacts: string[]; - relayList: string[]; -} - export interface IColumn { id?: number; kind: number; @@ -115,9 +155,3 @@ export interface NIP05 { }; }; } - -export interface Interests { - hashtags: string[]; - users: string[]; - words: string[]; -} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1d4ffcb5..60e0ef5c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -345,42 +345,30 @@ importers: '@lume/icons': specifier: workspace:^ version: link:../icons - '@lume/ndk-cache-tauri': - specifier: workspace:^ - version: link:../ndk-cache-tauri - '@lume/storage': - specifier: workspace:^ - version: link:../storage '@lume/utils': specifier: workspace:^ version: link:../utils - '@nostr-dev-kit/ndk': - specifier: ^2.4.0 - version: 2.4.0(typescript@5.3.3) - '@nostr-fetch/adapter-ndk': - specifier: ^0.15.0 - version: 0.15.0(@nostr-dev-kit/ndk@2.4.0)(nostr-fetch@0.15.0) '@radix-ui/react-avatar': specifier: ^1.0.4 - version: 1.0.4(@types/react-dom@18.2.18)(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0) + version: 1.0.4(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-collapsible': specifier: ^1.0.3 - version: 1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0) + version: 1.0.3(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-dialog': specifier: ^1.0.5 - version: 1.0.5(@types/react-dom@18.2.18)(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0) + version: 1.0.5(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-dropdown-menu': specifier: ^2.0.6 - version: 2.0.6(@types/react-dom@18.2.18)(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0) + version: 2.0.6(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-hover-card': specifier: ^1.0.7 - version: 1.0.7(@types/react-dom@18.2.18)(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0) + version: 1.0.7(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-popover': specifier: ^1.0.7 - version: 1.0.7(@types/react-dom@18.2.18)(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0) + version: 1.0.7(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-tooltip': specifier: ^1.0.7 - version: 1.0.7(@types/react-dom@18.2.18)(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0) + version: 1.0.7(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0) '@tanstack/react-query': specifier: ^5.18.1 version: 5.18.1(react@18.2.0) @@ -390,12 +378,6 @@ importers: jotai: specifier: ^2.6.4 version: 2.6.4(@types/react@18.2.52)(react@18.2.0) - linkify-react: - specifier: ^4.1.3 - version: 4.1.3(linkifyjs@4.1.3)(react@18.2.0) - linkifyjs: - specifier: ^4.1.3 - version: 4.1.3 media-chrome: specifier: ^2.1.0 version: 2.1.0 @@ -405,12 +387,6 @@ importers: nanoid: specifier: ^5.0.5 version: 5.0.5 - nostr-fetch: - specifier: ^0.15.0 - version: 0.15.0 - nostr-tools: - specifier: 1.17.0 - version: 1.17.0(typescript@5.3.3) qrcode.react: specifier: ^3.1.0 version: 3.1.0(react@18.2.0) @@ -438,12 +414,6 @@ importers: string-strip-html: specifier: ^13.4.6 version: 13.4.6 - tippy.js: - specifier: ^6.3.7 - version: 6.3.7 - use-context-selector: - specifier: ^1.4.1 - version: 1.4.1(react-dom@18.2.0)(react@18.2.0)(scheduler@0.23.0) virtua: specifier: ^0.23.3 version: 0.23.3(react-dom@18.2.0)(react@18.2.0) @@ -460,9 +430,6 @@ importers: '@types/react': specifier: ^18.2.52 version: 18.2.52 - tailwind-merge: - specifier: ^2.2.1 - version: 2.2.1 tailwindcss: specifier: ^3.4.1 version: 3.4.1 @@ -1110,9 +1077,9 @@ importers: packages/tailwindcss: dependencies: - tailwindcss-radix-colors: + '@evilmartians/harmony': specifier: ^1.2.0 - version: 1.2.0(tailwindcss@3.4.1) + version: 1.2.0 devDependencies: '@tailwindcss/forms': specifier: ^0.5.7 @@ -2275,6 +2242,10 @@ packages: requiresBuild: true optional: true + /@evilmartians/harmony@1.2.0: + resolution: {integrity: sha512-Ua8gpC+28Eo9D2/xynTrrZIrSawgtobwtRLLYq4wH8N19qoMspWZ1vqfsDDVPgQFa+iHsVAk/SbdmoPAj6OH1g==} + dev: false + /@floating-ui/core@1.6.0: resolution: {integrity: sha512-PcF++MykgmTj3CIyOQbKA/hDzOAiqI3mhuoN44WRCopIs1sgoDoU4oty4Jtqaj/y3oDU6fnVSm4QG0a3t5i0+g==} dependencies: @@ -2452,17 +2423,6 @@ packages: - typescript dev: false - /@nostr-fetch/adapter-ndk@0.15.0(@nostr-dev-kit/ndk@2.4.0)(nostr-fetch@0.15.0): - resolution: {integrity: sha512-Mug2yTmX4n4hFWfQV1GWiXZd88PbfbsT29lOtM5/sM59a+eYXeYvNmHddxmk2dK5VAdlT2VwKdHEmebzko6Y7w==} - peerDependencies: - '@nostr-dev-kit/ndk': ^1.0.0 - nostr-fetch: ^0.15.0 - dependencies: - '@nostr-dev-kit/ndk': 2.4.0(typescript@5.3.3) - '@nostr-fetch/kernel': 0.15.0 - nostr-fetch: 0.15.0 - dev: false - /@nostr-fetch/kernel@0.15.0: resolution: {integrity: sha512-Sq3PjSUrPSK9uJzq2yPDe/xMVdcn5PkYxRo6KKBykigefYcuQqk9ulXxUX/Z2FyvsN3QcY+3aEwKsgaQcC+TiA==} dependencies: @@ -2521,14 +2481,6 @@ packages: requiresBuild: true optional: true - /@popperjs/core@2.11.8: - resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} - dev: false - - /@radix-ui/colors@3.0.0: - resolution: {integrity: sha512-FUOsGBkHrYJwCSEtWRCIfQbZG7q1e6DgxCIOe1SUQzDe/7rXXeA47s8yCn6fuTNQAj1Zq4oTFi9Yjp3wzElcxg==} - dev: false - /@radix-ui/number@1.0.1: resolution: {integrity: sha512-T5gIdVO2mmPW3NNhjNgEP3cqMXjXL9UbO0BzWcXfvdBs+BohbQxvd/K5hSVKmn9/lbTdsQVKbUcP5WLCwvUbBg==} dependencies: @@ -2617,6 +2569,26 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@radix-ui/react-arrow@1.0.3(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-wSP+pHsB/jQRaL6voubsQ/ZlrGBHHrOjmBnr19hxYgtS0WvAFwZhK2WP/YY5yF9uKECCEEDGxuLxq1NBK51wFA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.23.9 + '@radix-ui/react-primitive': 1.0.3(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0) + '@types/react': 18.2.52 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@radix-ui/react-avatar@1.0.4(@types/react-dom@18.2.18)(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-kVK2K7ZD3wwj3qhle0ElXhOjbezIgyl2hVvgwfIdexL3rN6zJmy5AqqIf+D31lxVppdzV8CjAfZ6PklkmInZLw==} peerDependencies: @@ -2641,6 +2613,29 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@radix-ui/react-avatar@1.0.4(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-kVK2K7ZD3wwj3qhle0ElXhOjbezIgyl2hVvgwfIdexL3rN6zJmy5AqqIf+D31lxVppdzV8CjAfZ6PklkmInZLw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.23.9 + '@radix-ui/react-context': 1.0.1(@types/react@18.2.52)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.52)(react@18.2.0) + '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.2.52)(react@18.2.0) + '@types/react': 18.2.52 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@radix-ui/react-checkbox@1.0.4(@types/react-dom@18.2.18)(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-CBuGQa52aAYnADZVt/KBQzXrwx6TqnlwtcIPGtVt5JkkzQwMOLJjPukimhfKEr4GQNd43C+djUh5Ikopj8pSLg==} peerDependencies: @@ -2697,6 +2692,33 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@radix-ui/react-collapsible@1.0.3(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-UBmVDkmR6IvDsloHVN+3rtx4Mi5TFvylYXpluuv0f37dtaz3H99bp8No0LGXRigVpl3UAT4l9j6bIchh42S/Gg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.23.9 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.52)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.52)(react@18.2.0) + '@radix-ui/react-id': 1.0.1(@types/react@18.2.52)(react@18.2.0) + '@radix-ui/react-presence': 1.0.1(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.52)(react@18.2.0) + '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.2.52)(react@18.2.0) + '@types/react': 18.2.52 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@radix-ui/react-collection@1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-3SzW+0PW7yBBoQlT8wNcGtaxaD0XSu0uLUFgrtHY08Acx05TaHaOmVLR73c0j/cqpDy53KBMO7s0dx2wmOIDIA==} peerDependencies: @@ -2721,6 +2743,29 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@radix-ui/react-collection@1.0.3(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-3SzW+0PW7yBBoQlT8wNcGtaxaD0XSu0uLUFgrtHY08Acx05TaHaOmVLR73c0j/cqpDy53KBMO7s0dx2wmOIDIA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.23.9 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.52)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.52)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-slot': 1.0.2(@types/react@18.2.52)(react@18.2.0) + '@types/react': 18.2.52 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@radix-ui/react-compose-refs@1.0.1(@types/react@18.2.52)(react@18.2.0): resolution: {integrity: sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==} peerDependencies: @@ -2783,6 +2828,39 @@ packages: react-remove-scroll: 2.5.5(@types/react@18.2.52)(react@18.2.0) dev: false + /@radix-ui/react-dialog@1.0.5(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-GjWJX/AUpB703eEBanuBnIWdIXg6NvJFCXcNlSZk4xdszCdhrJgBoUd1cGk67vFO+WdA2pfI/plOpqz/5GUP6Q==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.23.9 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.52)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.52)(react@18.2.0) + '@radix-ui/react-dismissable-layer': 1.0.5(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-focus-guards': 1.0.1(@types/react@18.2.52)(react@18.2.0) + '@radix-ui/react-focus-scope': 1.0.4(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-id': 1.0.1(@types/react@18.2.52)(react@18.2.0) + '@radix-ui/react-portal': 1.0.4(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-presence': 1.0.1(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-slot': 1.0.2(@types/react@18.2.52)(react@18.2.0) + '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.52)(react@18.2.0) + '@types/react': 18.2.52 + aria-hidden: 1.2.3 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-remove-scroll: 2.5.5(@types/react@18.2.52)(react@18.2.0) + dev: false + /@radix-ui/react-direction@1.0.1(@types/react@18.2.52)(react@18.2.0): resolution: {integrity: sha512-RXcvnXgyvYvBEOhCBuddKecVkoMiI10Jcm5cTI7abJRAHYfFxeu+FBQs/DvdxSYucxR5mna0dNsL6QFlds5TMA==} peerDependencies: @@ -2822,6 +2900,30 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@radix-ui/react-dismissable-layer@1.0.5(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-aJeDjQhywg9LBu2t/At58hCvr7pEm0o2Ke1x33B+MhjNmmZ17sy4KImo0KPLgsnc/zN7GPdce8Cnn0SWvwZO7g==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.23.9 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.52)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.52)(react@18.2.0) + '@radix-ui/react-use-escape-keydown': 1.0.3(@types/react@18.2.52)(react@18.2.0) + '@types/react': 18.2.52 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@radix-ui/react-dropdown-menu@2.0.6(@types/react-dom@18.2.18)(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-i6TuFOoWmLWq+M/eCLGd/bQ2HfAX1RJgvrBQ6AQLmzfvsLdefxbWu8G9zczcPFfcSPehz9GcpF6K9QYreFV8hA==} peerDependencies: @@ -2849,6 +2951,32 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@radix-ui/react-dropdown-menu@2.0.6(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-i6TuFOoWmLWq+M/eCLGd/bQ2HfAX1RJgvrBQ6AQLmzfvsLdefxbWu8G9zczcPFfcSPehz9GcpF6K9QYreFV8hA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.23.9 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.52)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.52)(react@18.2.0) + '@radix-ui/react-id': 1.0.1(@types/react@18.2.52)(react@18.2.0) + '@radix-ui/react-menu': 2.0.6(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.52)(react@18.2.0) + '@types/react': 18.2.52 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@radix-ui/react-focus-guards@1.0.1(@types/react@18.2.52)(react@18.2.0): resolution: {integrity: sha512-Rect2dWbQ8waGzhMavsIbmSVCgYxkXLxxR3ZvCX79JOglzdEy4JXMb98lq4hPxUbLr77nP0UOGf4rcMU+s1pUA==} peerDependencies: @@ -2886,6 +3014,28 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@radix-ui/react-focus-scope@1.0.4(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-sL04Mgvf+FmyvZeYfNu1EPAaaxD+aw7cYeIB9L9Fvq8+urhltTRaEo5ysKOpHuKPclsZcSUMKlN05x4u+CINpA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.23.9 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.52)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.52)(react@18.2.0) + '@types/react': 18.2.52 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@radix-ui/react-hover-card@1.0.7(@types/react-dom@18.2.18)(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-OcUN2FU0YpmajD/qkph3XzMcK/NmSk9hGWnjV68p6QiZMgILugusgQwnLSDs3oFSJYGKf3Y49zgFedhGh04k9A==} peerDependencies: @@ -2915,6 +3065,34 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@radix-ui/react-hover-card@1.0.7(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-OcUN2FU0YpmajD/qkph3XzMcK/NmSk9hGWnjV68p6QiZMgILugusgQwnLSDs3oFSJYGKf3Y49zgFedhGh04k9A==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.23.9 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.52)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.52)(react@18.2.0) + '@radix-ui/react-dismissable-layer': 1.0.5(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-popper': 1.1.3(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-portal': 1.0.4(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-presence': 1.0.1(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.52)(react@18.2.0) + '@types/react': 18.2.52 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@radix-ui/react-id@1.0.1(@types/react@18.2.52)(react@18.2.0): resolution: {integrity: sha512-tI7sT/kqYp8p96yGWY1OAnLHrqDgzHefRBKQ2YAkBS5ja7QLcZ9Z/uY7bEjPUatf8RomoXM8/1sMj1IJaE5UzQ==} peerDependencies: @@ -2968,6 +3146,43 @@ packages: react-remove-scroll: 2.5.5(@types/react@18.2.52)(react@18.2.0) dev: false + /@radix-ui/react-menu@2.0.6(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-BVkFLS+bUC8HcImkRKPSiVumA1VPOOEC5WBMiT+QAVsPzW1FJzI9KnqgGxVDPBcql5xXrHkD3JOVoXWEXD8SYA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.23.9 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-collection': 1.0.3(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.52)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.52)(react@18.2.0) + '@radix-ui/react-direction': 1.0.1(@types/react@18.2.52)(react@18.2.0) + '@radix-ui/react-dismissable-layer': 1.0.5(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-focus-guards': 1.0.1(@types/react@18.2.52)(react@18.2.0) + '@radix-ui/react-focus-scope': 1.0.4(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-id': 1.0.1(@types/react@18.2.52)(react@18.2.0) + '@radix-ui/react-popper': 1.1.3(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-portal': 1.0.4(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-presence': 1.0.1(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-roving-focus': 1.0.4(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-slot': 1.0.2(@types/react@18.2.52)(react@18.2.0) + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.52)(react@18.2.0) + '@types/react': 18.2.52 + aria-hidden: 1.2.3 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-remove-scroll: 2.5.5(@types/react@18.2.52)(react@18.2.0) + dev: false + /@radix-ui/react-popover@1.0.7(@types/react-dom@18.2.18)(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-shtvVnlsxT6faMnK/a7n0wptwBD23xc1Z5mdrtKLwVEfsEMXodS0r5s0/g5P0hX//EKYZS2sxUjqfzlg52ZSnQ==} peerDependencies: @@ -3003,6 +3218,40 @@ packages: react-remove-scroll: 2.5.5(@types/react@18.2.52)(react@18.2.0) dev: false + /@radix-ui/react-popover@1.0.7(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-shtvVnlsxT6faMnK/a7n0wptwBD23xc1Z5mdrtKLwVEfsEMXodS0r5s0/g5P0hX//EKYZS2sxUjqfzlg52ZSnQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.23.9 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.52)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.52)(react@18.2.0) + '@radix-ui/react-dismissable-layer': 1.0.5(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-focus-guards': 1.0.1(@types/react@18.2.52)(react@18.2.0) + '@radix-ui/react-focus-scope': 1.0.4(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-id': 1.0.1(@types/react@18.2.52)(react@18.2.0) + '@radix-ui/react-popper': 1.1.3(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-portal': 1.0.4(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-presence': 1.0.1(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-slot': 1.0.2(@types/react@18.2.52)(react@18.2.0) + '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.52)(react@18.2.0) + '@types/react': 18.2.52 + aria-hidden: 1.2.3 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-remove-scroll: 2.5.5(@types/react@18.2.52)(react@18.2.0) + dev: false + /@radix-ui/react-popper@1.1.3(@types/react-dom@18.2.18)(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-cKpopj/5RHZWjrbF2846jBNacjQVwkP068DfmgrNJXpvVWrOvlAmE9xSiy5OqeE+Gi8D9fP+oDhUnPqNMY8/5w==} peerDependencies: @@ -3033,6 +3282,35 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@radix-ui/react-popper@1.1.3(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-cKpopj/5RHZWjrbF2846jBNacjQVwkP068DfmgrNJXpvVWrOvlAmE9xSiy5OqeE+Gi8D9fP+oDhUnPqNMY8/5w==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.23.9 + '@floating-ui/react-dom': 2.0.8(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-arrow': 1.0.3(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.52)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.52)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.52)(react@18.2.0) + '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.2.52)(react@18.2.0) + '@radix-ui/react-use-rect': 1.0.1(@types/react@18.2.52)(react@18.2.0) + '@radix-ui/react-use-size': 1.0.1(@types/react@18.2.52)(react@18.2.0) + '@radix-ui/rect': 1.0.1 + '@types/react': 18.2.52 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@radix-ui/react-portal@1.0.4(@types/react-dom@18.2.18)(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-Qki+C/EuGUVCQTOTD5vzJzJuMUlewbzuKyUy+/iHM2uwGiru9gZeBJtHAPKAEkB5KWGi9mP/CHKcY0wt1aW45Q==} peerDependencies: @@ -3054,6 +3332,26 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@radix-ui/react-portal@1.0.4(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-Qki+C/EuGUVCQTOTD5vzJzJuMUlewbzuKyUy+/iHM2uwGiru9gZeBJtHAPKAEkB5KWGi9mP/CHKcY0wt1aW45Q==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.23.9 + '@radix-ui/react-primitive': 1.0.3(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0) + '@types/react': 18.2.52 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@radix-ui/react-presence@1.0.1(@types/react-dom@18.2.18)(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-UXLW4UAbIY5ZjcvzjfRFo5gxva8QirC9hF7wRE4U5gz+TP0DbRk+//qyuAQ1McDxBt1xNMBTaciFGvEmJvAZCg==} peerDependencies: @@ -3076,6 +3374,27 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@radix-ui/react-presence@1.0.1(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-UXLW4UAbIY5ZjcvzjfRFo5gxva8QirC9hF7wRE4U5gz+TP0DbRk+//qyuAQ1McDxBt1xNMBTaciFGvEmJvAZCg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.23.9 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.52)(react@18.2.0) + '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.2.52)(react@18.2.0) + '@types/react': 18.2.52 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@radix-ui/react-primitive@1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g==} peerDependencies: @@ -3097,6 +3416,26 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@radix-ui/react-primitive@1.0.3(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.23.9 + '@radix-ui/react-slot': 1.0.2(@types/react@18.2.52)(react@18.2.0) + '@types/react': 18.2.52 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@radix-ui/react-roving-focus@1.0.4(@types/react-dom@18.2.18)(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-2mUg5Mgcu001VkGy+FfzZyzbmuUWzgWkj3rvv4yu+mLw03+mTzbxZHvfcGyFp2b8EkQeMkpRQ5FiA2Vr2O6TeQ==} peerDependencies: @@ -3126,6 +3465,34 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@radix-ui/react-roving-focus@1.0.4(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-2mUg5Mgcu001VkGy+FfzZyzbmuUWzgWkj3rvv4yu+mLw03+mTzbxZHvfcGyFp2b8EkQeMkpRQ5FiA2Vr2O6TeQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.23.9 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-collection': 1.0.3(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.52)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.52)(react@18.2.0) + '@radix-ui/react-direction': 1.0.1(@types/react@18.2.52)(react@18.2.0) + '@radix-ui/react-id': 1.0.1(@types/react@18.2.52)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.52)(react@18.2.0) + '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.52)(react@18.2.0) + '@types/react': 18.2.52 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@radix-ui/react-select@2.0.0(@types/react-dom@18.2.18)(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-RH5b7af4oHtkcHS7pG6Sgv5rk5Wxa7XI8W5gvB1N/yiuDGZxko1ynvOiVhFM7Cis2A8zxF9bTOUVbRDzPepe6w==} peerDependencies: @@ -3241,6 +3608,37 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@radix-ui/react-tooltip@1.0.7(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-lPh5iKNFVQ/jav/j6ZrWq3blfDJ0OH9R6FlNUHPMqdLuQ9vwDgFsRxvl8b7Asuy5c8xmoojHUxKHQSOAvMHxyw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.23.9 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.52)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.52)(react@18.2.0) + '@radix-ui/react-dismissable-layer': 1.0.5(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-id': 1.0.1(@types/react@18.2.52)(react@18.2.0) + '@radix-ui/react-popper': 1.1.3(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-portal': 1.0.4(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-presence': 1.0.1(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-slot': 1.0.2(@types/react@18.2.52)(react@18.2.0) + '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.52)(react@18.2.0) + '@radix-ui/react-visually-hidden': 1.0.3(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0) + '@types/react': 18.2.52 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@radix-ui/react-use-callback-ref@1.0.1(@types/react@18.2.52)(react@18.2.0): resolution: {integrity: sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ==} peerDependencies: @@ -3364,6 +3762,26 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@radix-ui/react-visually-hidden@1.0.3(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-D4w41yN5YRKtu464TLnByKzMDG/JlMPHtfZgQAu9v6mNakUqGUI9vUrfQKz8NK41VMm/xbZbh76NUTVtIYqOMA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.23.9 + '@radix-ui/react-primitive': 1.0.3(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0) + '@types/react': 18.2.52 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@radix-ui/rect@1.0.1: resolution: {integrity: sha512-fyrgCaedtvMg9NK3en0pnOYJdtfwxUcNolezkNPUsoX57X8oQk+NkqcvzHXD2uKNij6GXmWU9NDru2IWjrO4BQ==} dependencies: @@ -6474,20 +6892,6 @@ packages: /lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} - /linkify-react@4.1.3(linkifyjs@4.1.3)(react@18.2.0): - resolution: {integrity: sha512-rhI3zM/fxn5BfRPHfi4r9N7zgac4vOIxub1wHIWXLA5ENTMs+BGaIaFO1D1PhmxgwhIKmJz3H7uCP0Dg5JwSlA==} - peerDependencies: - linkifyjs: ^4.0.0 - react: '>= 15.0.0' - dependencies: - linkifyjs: 4.1.3 - react: 18.2.0 - dev: false - - /linkifyjs@4.1.3: - resolution: {integrity: sha512-auMesunaJ8yfkHvK4gfg1K0SaKX/6Wn9g2Aac/NwX+l5VdmFZzo/hdPGxEOETj+ryRa4/fiOPjeeKURSAJx1sg==} - dev: false - /load-yaml-file@0.2.0: resolution: {integrity: sha512-OfCBkGEw4nN6JLtgRidPX6QxjBQGQf72q3si2uvqyFEMbycSFFHwAZeXx6cJgFM9wmLrf9zBwCP3Ivqa+LLZPw==} engines: {node: '>=6'} @@ -9367,15 +9771,6 @@ packages: tailwindcss: 3.4.1 dev: true - /tailwindcss-radix-colors@1.2.0(tailwindcss@3.4.1): - resolution: {integrity: sha512-2rr3l7NV89UEwRTJFN4LlQIm5aasi7OZeJFF7iaDJ2hKwylR8BNMAC6GoiDl0G7wq18keN1YQ3V5sMbvtREO6w==} - peerDependencies: - tailwindcss: '>=3.0.0' - dependencies: - '@radix-ui/colors': 3.0.0 - tailwindcss: 3.4.1 - dev: false - /tailwindcss@3.4.1: resolution: {integrity: sha512-qAYmXRfk3ENzuPBakNK0SRrUDipP8NQnEY6772uDhflcQz5EhRdD7JNZxyrFHVQNCwULPBn6FNPp9brpO7ctcA==} engines: {node: '>=14.0.0'} @@ -9491,12 +9886,6 @@ packages: resolution: {integrity: sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==} dev: false - /tippy.js@6.3.7: - resolution: {integrity: sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ==} - dependencies: - '@popperjs/core': 2.11.8 - dev: false - /tlds@1.249.0: resolution: {integrity: sha512-PfcE+oqaEhs0U3RDNg4uGg37793cGvlK6+aLAetwR0ImFyV3R2ts1KvU2RfJdtoLn7IFnUEftFFz4br5Wr3caA==} hasBin: true @@ -9880,24 +10269,6 @@ packages: tslib: 2.6.2 dev: false - /use-context-selector@1.4.1(react-dom@18.2.0)(react@18.2.0)(scheduler@0.23.0): - resolution: {integrity: sha512-Io2ArvcRO+6MWIhkdfMFt+WKQX+Vb++W8DS2l03z/Vw/rz3BclKpM0ynr4LYGyU85Eke+Yx5oIhTY++QR0ZDoA==} - peerDependencies: - react: '>=16.8.0' - react-dom: '*' - react-native: '*' - scheduler: '>=0.19.0' - peerDependenciesMeta: - react-dom: - optional: true - react-native: - optional: true - dependencies: - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - scheduler: 0.23.0 - dev: false - /use-debounce@10.0.0(react@18.2.0): resolution: {integrity: sha512-XRjvlvCB46bah9IBXVnq/ACP2lxqXyZj0D9hj4K5OzNroMDpTEBg8Anuh1/UfRTRs7pLhQ+RiNxxwZu9+MVl1A==} engines: {node: '>= 16.0.0'} diff --git a/src-tauri/capabilities/main.json b/src-tauri/capabilities/main.json new file mode 100644 index 00000000..29169e3b --- /dev/null +++ b/src-tauri/capabilities/main.json @@ -0,0 +1,41 @@ +{ + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "desktop-capability", + "description": "Capability for the desktop", + "platforms": ["linux", "macOS", "windows"], + "windows": ["main", "settings", "event-*", "user-*", "column-*"], + "permissions": [ + "path:default", + "event:default", + "window:default", + "app:default", + "resources:default", + "menu:default", + "tray:default", + "theme:allow-set-theme", + "theme:allow-get-theme", + "notification:allow-is-permission-granted", + "notification:allow-request-permission", + "notification:default", + "os:allow-locale", + { + "identifier": "http:default", + "allow": [ + { + "url": "http://**/" + }, + { + "url": "https://**/" + } + ] + }, + { + "identifier": "fs:allow-read-text-file", + "allow": [ + { + "path": "$RESOURCE/locales/*" + } + ] + } + ] +} diff --git a/src-tauri/gen/main.json b/src-tauri/gen/main.json deleted file mode 100644 index 568b05cb..00000000 --- a/src-tauri/gen/main.json +++ /dev/null @@ -1,74 +0,0 @@ -{ - "$schema": "./schemas/desktop-schema.json", - "identifier": "desktop-capability", - "description": "Capability for the desktop", - "platforms": ["linux", "macOS", "windows"], - "windows": ["main", "settings", "event-*", "user-*", "column-*"], - "permissions": [ - "path:default", - "event:default", - "window:default", - "app:default", - "resources:default", - "menu:default", - "tray:default", - "shell:open", - "theme:allow-set-theme", - "theme:allow-get-theme", - "notification:allow-is-permission-granted", - "notification:allow-request-permission", - "notification:allow-notify", - { - "identifier": "http:default", - "allow": [ - { - "url": "http://**/" - }, - { - "url": "https://**/" - } - ] - }, - { - "identifier": "fs:scope", - "allow": [ - { - "path": "$APPDATA/*" - }, - { - "path": "$LOCALDATA/*" - }, - { - "path": "$DESKTOP/*" - }, - { - "path": "$DOCUMENT/*" - }, - { - "path": "$DOWNLOAD/*" - }, - { - "path": "$HOME/*" - }, - { - "path": "$PICTURE/*" - }, - { - "path": "$PUBLIC/*" - }, - { - "path": "$VIDEO/*" - }, - { - "path": "$RESOURCE" - }, - { - "path": "$RESOURCE/*" - }, - { - "path": "$RESOURCE/locales/*" - } - ] - } - ] -} diff --git a/src-tauri/gen/schemas/capabilities.json b/src-tauri/gen/schemas/capabilities.json index 9e26dfee..8423fac5 100644 --- a/src-tauri/gen/schemas/capabilities.json +++ b/src-tauri/gen/schemas/capabilities.json @@ -1 +1 @@ -{} \ No newline at end of file +{"desktop-capability":{"identifier":"desktop-capability","description":"Capability for the desktop","context":"local","windows":["main","settings","event-*","user-*","column-*"],"permissions":["path:default","event:default","window:default","app:default","resources:default","menu:default","tray:default","theme:allow-set-theme","theme:allow-get-theme","notification:allow-is-permission-granted","notification:allow-request-permission","notification:default","os:allow-locale",{"identifier":"http:default","allow":[{"url":"http://**/"},{"url":"https://**/"}]},{"identifier":"fs:allow-read-text-file","allow":[{"path":"$RESOURCE/locales/*"}]}],"platforms":["linux","macOS","windows"]}} \ No newline at end of file diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 6e2c83c2..3f03a370 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -13,11 +13,13 @@ use db::api::v1::Account; use keyring::Entry; use nostr_sdk::prelude::*; use std::sync::Arc; +use std::time::Duration; use tauri::Manager; use tauri_plugin_autostart::MacosLauncher; pub struct Nostr { pub client: Arc, + pub contact_list: Option>, } fn main() { @@ -28,7 +30,7 @@ fn main() { let config_dir = handle.path().app_config_dir().unwrap(); tauri::async_runtime::spawn(async move { - // Create database connection + // Create nostr database connection let nostr_db = SQLiteDatabase::open(config_dir.join("nostr.db")) .await .expect("Open database failed."); @@ -36,40 +38,18 @@ fn main() { // Create nostr connection let client = ClientBuilder::default().database(nostr_db).build(); - // create app database connection + // Create app database connection let db = DATABASE_BUILDER .create(config_dir.join("app.db")) .expect("failed to create app database"); - // run db migrate + // Run migration for app database let rw = db .rw_transaction() .expect("failed to create rw migration transaction"); rw.migrate::().expect("failed to migrate Account"); rw.commit().expect("failed to commit migration"); - // get stored account - let r = db.r_transaction().expect("failed to create ro transaction"); - let accounts: Vec = r - .scan() - .secondary(AccountKey::status) - .expect("failed to scan accounts") - .start_with("active") - .collect(); - - if let Some(account) = accounts.into_iter().nth(0) { - let entry = Entry::new("Lume", &account.pubkey).expect("failed to load secret"); - - if let Ok(key) = entry.get_password() { - let secret_key = SecretKey::from_bech32(key).unwrap(); - let keys = Keys::new(secret_key); - let signer = ClientSigner::Keys(keys); - - // update client's signer - client.set_signer(Some(signer)).await; - } - } - // Add some bootstrap relays // #TODO: Pull bootstrap relays from user's settings client @@ -84,9 +64,43 @@ fn main() { // Connect client.connect().await; + // Get stored account + let r = db.r_transaction().expect("failed to create ro transaction"); + let accounts: Vec = r + .scan() + .secondary(AccountKey::status) + .expect("failed to scan accounts") + .start_with("active") + .collect(); + let mut contact_list = None; + + // Run somethings if account existed + if let Some(account) = accounts.into_iter().nth(0) { + // Add signer with stored private key + let entry = Entry::new("Lume", &account.pubkey).expect("failed to load secret"); + + if let Ok(key) = entry.get_password() { + let secret_key = SecretKey::from_bech32(key).unwrap(); + let keys = Keys::new(secret_key); + let signer = ClientSigner::Keys(keys); + + // Update client's signer + client.set_signer(Some(signer)).await; + + // Get contact list + contact_list = Some( + client + .get_contact_list(Some(Duration::from_secs(10))) + .await + .unwrap(), + ); + } + } + // Init global state handle.manage(Nostr { client: client.into(), + contact_list: contact_list.into(), }) }); @@ -113,6 +127,8 @@ fn main() { nostr::keys::get_public_key, nostr::keys::update_signer, nostr::keys::verify_signer, + nostr::keys::event_to_bech32, + nostr::keys::user_to_bech32, nostr::metadata::get_metadata, nostr::event::get_event, commands::secret::secure_save, diff --git a/src-tauri/src/nostr/event.rs b/src-tauri/src/nostr/event.rs index 1c433007..6fe37aa1 100644 --- a/src-tauri/src/nostr/event.rs +++ b/src-tauri/src/nostr/event.rs @@ -4,17 +4,26 @@ use std::time::Duration; use tauri::State; #[tauri::command(async)] -pub async fn get_event(id: String, nostr: State<'_, Nostr>) -> Result { +pub async fn get_event(id: &str, nostr: State<'_, Nostr>) -> Result { let client = &nostr.client; + let event_id; + + if id.starts_with("note1") { + event_id = EventId::from_bech32(id).unwrap(); + } else if id.starts_with("nevent1") { + event_id = EventId::from_bech32(id).unwrap(); + } else if id.starts_with("naddr1") { + event_id = EventId::from_bech32(id).unwrap(); + } else { + event_id = EventId::from_hex(id).unwrap(); + } - let event_id = EventId::from_bech32(id).unwrap(); let filter = Filter::new().id(event_id); - let events = client .get_events_of(vec![filter], Some(Duration::from_secs(10))) .await - .expect("Get metadata failed"); - let event = events.into_iter().nth(0).unwrap().as_json(); + .expect("Get event failed"); + let event = events.first().unwrap().as_json(); Ok(event) } diff --git a/src-tauri/src/nostr/keys.rs b/src-tauri/src/nostr/keys.rs index 4578d647..c899f102 100644 --- a/src-tauri/src/nostr/keys.rs +++ b/src-tauri/src/nostr/keys.rs @@ -1,5 +1,6 @@ use crate::Nostr; use nostr_sdk::prelude::*; +use std::str::FromStr; use tauri::State; #[derive(serde::Serialize)] @@ -23,15 +24,16 @@ pub fn create_keys() -> Result { } #[tauri::command] -pub fn get_public_key(secret_key: SecretKey) -> Result { +pub fn get_public_key(nsec: String) -> Result { + let secret_key = SecretKey::from_bech32(nsec).unwrap(); let keys = Keys::new(secret_key); Ok(keys.public_key().to_bech32().expect("secret key failed")) } #[tauri::command] -pub async fn update_signer(key: String, nostr: State<'_, Nostr>) -> Result<(), ()> { +pub async fn update_signer(nsec: String, nostr: State<'_, Nostr>) -> Result<(), ()> { let client = &nostr.client; - let secret_key = SecretKey::from_bech32(key).unwrap(); + let secret_key = SecretKey::from_bech32(nsec).unwrap(); let keys = Keys::new(secret_key); let signer = ClientSigner::Keys(keys); @@ -47,3 +49,19 @@ pub async fn verify_signer(nostr: State<'_, Nostr>) -> Result { Ok(status) } + +#[tauri::command] +pub fn event_to_bech32(id: &str, relays: Vec) -> Result { + let event_id = EventId::from_hex(id).unwrap(); + let event = Nip19Event::new(event_id, relays); + + Ok(event.to_bech32().unwrap()) +} + +#[tauri::command] +pub fn user_to_bech32(key: &str, relays: Vec) -> Result { + let pubkey = XOnlyPublicKey::from_str(key).unwrap(); + let profile = Nip19Profile::new(pubkey, relays); + + Ok(profile.to_bech32().unwrap()) +} diff --git a/src-tauri/src/nostr/metadata.rs b/src-tauri/src/nostr/metadata.rs index d7adfb7f..73b84cc5 100644 --- a/src-tauri/src/nostr/metadata.rs +++ b/src-tauri/src/nostr/metadata.rs @@ -1,13 +1,20 @@ use crate::Nostr; use nostr_sdk::prelude::*; -use std::time::Duration; +use std::{str::FromStr, time::Duration}; use tauri::State; #[tauri::command(async)] -pub async fn get_metadata(npub: String, nostr: State<'_, Nostr>) -> Result { +pub async fn get_metadata(id: &str, nostr: State<'_, Nostr>) -> Result { let client = &nostr.client; + let public_key; - let public_key = XOnlyPublicKey::from_bech32(npub).unwrap(); + if id.starts_with("nprofile1") { + public_key = XOnlyPublicKey::from_bech32(id).unwrap(); + } else if id.starts_with("npub1") { + public_key = XOnlyPublicKey::from_bech32(id).unwrap(); + } else { + public_key = XOnlyPublicKey::from_str(id).unwrap(); + } let filter = Filter::new() .author(public_key) @@ -19,7 +26,7 @@ pub async fn get_metadata(npub: String, nostr: State<'_, Nostr>) -> Result