diff --git a/apps/desktop2/package.json b/apps/desktop2/package.json index faeca37b..6085cd8e 100644 --- a/apps/desktop2/package.json +++ b/apps/desktop2/package.json @@ -9,7 +9,7 @@ "preview": "vite preview" }, "dependencies": { - "@lume/ark": "workspace:^", + "@lume/system": "workspace:^", "@lume/icons": "workspace:^", "@lume/ui": "workspace:^", "@lume/utils": "workspace:^", diff --git a/apps/desktop2/src/app.tsx b/apps/desktop2/src/app.tsx index 15f3d945..7dcddd18 100644 --- a/apps/desktop2/src/app.tsx +++ b/apps/desktop2/src/app.tsx @@ -1,20 +1,17 @@ -import { Ark } from "@lume/ark"; import { createSyncStoragePersister } from "@tanstack/query-sync-storage-persister"; import { QueryClient } from "@tanstack/react-query"; import { PersistQueryClientProvider } from "@tanstack/react-query-persist-client"; import { RouterProvider, createRouter } from "@tanstack/react-router"; -import { platform } from "@tauri-apps/plugin-os"; import React, { StrictMode } from "react"; import ReactDOM from "react-dom/client"; import { I18nextProvider } from "react-i18next"; import "./app.css"; import i18n from "./locale"; import { routeTree } from "./router.gen"; // auto generated file +import { type } from "@tauri-apps/plugin-os"; -const ark = new Ark(); +const os = await type(); const queryClient = new QueryClient(); -const platformName = await platform(); - const persister = createSyncStoragePersister({ storage: window.localStorage, }); @@ -23,9 +20,8 @@ const persister = createSyncStoragePersister({ const router = createRouter({ routeTree, context: { - ark, queryClient, - platform: platformName, + platform: os, }, Wrap: ({ children }) => { return ( diff --git a/apps/desktop2/src/components/avatarUploader.tsx b/apps/desktop2/src/components/avatarUploader.tsx index 1851a2ed..2463b4f1 100644 --- a/apps/desktop2/src/components/avatarUploader.tsx +++ b/apps/desktop2/src/components/avatarUploader.tsx @@ -1,6 +1,6 @@ +import { NostrQuery } from "@lume/system"; import { Spinner } from "@lume/ui"; import { cn } from "@lume/utils"; -import { useRouteContext } from "@tanstack/react-router"; import { type Dispatch, type ReactNode, @@ -18,21 +18,17 @@ export function AvatarUploader({ children: ReactNode; className?: string; }) { - const { ark } = useRouteContext({ strict: false }); const [loading, setLoading] = useState(false); const uploadAvatar = async () => { - // start loading - setLoading(true); try { - const image = await ark.upload(); + setLoading(true); + const image = await NostrQuery.upload(); setPicture(image); } catch (e) { + setLoading(false); toast.error(String(e)); } - - // stop loading - setLoading(false); }; return ( diff --git a/apps/desktop2/src/components/balance.tsx b/apps/desktop2/src/components/balance.tsx index f117f619..44e27de9 100644 --- a/apps/desktop2/src/components/balance.tsx +++ b/apps/desktop2/src/components/balance.tsx @@ -1,16 +1,15 @@ import { User } from "@/components/user"; +import { NostrAccount } from "@lume/system"; import { getBitcoinDisplayValues } from "@lume/utils"; -import { useRouteContext } from "@tanstack/react-router"; import { useEffect, useMemo, useState } from "react"; export function Balance({ account }: { account: string }) { - const { ark } = useRouteContext({ strict: false }); const [balance, setBalance] = useState(0); const value = useMemo(() => getBitcoinDisplayValues(balance), [balance]); useEffect(() => { async function getBalance() { - const val = await ark.get_balance(); + const val = await NostrAccount.getBalance(); setBalance(val); } diff --git a/apps/desktop2/src/components/conversation.tsx b/apps/desktop2/src/components/conversation.tsx index 335bd3ec..a3dd9974 100644 --- a/apps/desktop2/src/components/conversation.tsx +++ b/apps/desktop2/src/components/conversation.tsx @@ -1,18 +1,17 @@ import { ThreadIcon } from "@lume/icons"; -import type { Event } from "@lume/types"; +import type { NostrEvent } from "@lume/types"; import { Note } from "@/components/note"; import { cn } from "@lume/utils"; -import { useRouteContext } from "@tanstack/react-router"; +import { LumeEvent } from "@lume/system"; export function Conversation({ event, className, }: { - event: Event; + event: NostrEvent; className?: string; }) { - const { ark } = useRouteContext({ strict: false }); - const thread = ark.get_thread(event.tags); + const thread = LumeEvent.getEventThread(event.tags); return ( diff --git a/apps/desktop2/src/components/note/activity.tsx b/apps/desktop2/src/components/note/activity.tsx index d6cb45e5..d8febfdb 100644 --- a/apps/desktop2/src/components/note/activity.tsx +++ b/apps/desktop2/src/components/note/activity.tsx @@ -4,9 +4,7 @@ import { User } from "../user"; export function NoteActivity({ className }: { className?: string }) { const event = useNoteContext(); - const mentions = event.tags - .filter((tag) => tag[0] === "p") - .map((tag) => tag[1]); + const mentions = event.mentions; return (
diff --git a/apps/desktop2/src/components/note/buttons/open.tsx b/apps/desktop2/src/components/note/buttons/open.tsx index 23da71fb..43e9d4bd 100644 --- a/apps/desktop2/src/components/note/buttons/open.tsx +++ b/apps/desktop2/src/components/note/buttons/open.tsx @@ -1,11 +1,10 @@ import { VisitIcon } from "@lume/icons"; import * as Tooltip from "@radix-ui/react-tooltip"; -import { useRouteContext } from "@tanstack/react-router"; import { useNoteContext } from "../provider"; +import { LumeWindow } from "@lume/system"; export function NoteOpenThread() { const event = useNoteContext(); - const { ark } = useRouteContext({ strict: false }); return ( @@ -13,7 +12,7 @@ export function NoteOpenThread() { ); } diff --git a/apps/desktop2/src/components/note/menu.tsx b/apps/desktop2/src/components/note/menu.tsx index 59d9ebe3..db44e832 100644 --- a/apps/desktop2/src/components/note/menu.tsx +++ b/apps/desktop2/src/components/note/menu.tsx @@ -1,37 +1,28 @@ import { HorizontalDotsIcon } from "@lume/icons"; import * as DropdownMenu from "@radix-ui/react-dropdown-menu"; -import { useRouteContext } from "@tanstack/react-router"; import { writeText } from "@tauri-apps/plugin-clipboard-manager"; import { useTranslation } from "react-i18next"; -import { toast } from "sonner"; import { useNoteContext } from "./provider"; +import { LumeWindow } from "@lume/system"; export function NoteMenu() { + const { t } = useTranslation(); const event = useNoteContext(); - const { ark } = useRouteContext({ strict: false }); - const { t } = useTranslation(); - const copyID = async () => { - await writeText(await ark.event_to_bech32(event.id, [""])); - toast.success("Copied"); + await writeText(await event.idAsBech32()); }; const copyRaw = async () => { await writeText(JSON.stringify(event)); - toast.success("Copied"); }; const copyNpub = async () => { - await writeText(await ark.user_to_bech32(event.pubkey, [""])); - toast.success("Copied"); + await writeText(await event.pubkeyAsBech32()); }; const copyLink = async () => { - await writeText( - `https://njump.me/${await ark.event_to_bech32(event.id, [""])}`, - ); - toast.success("Copied"); + await writeText(`https://njump.me/${await event.idAsBech32()}`); }; return ( @@ -49,7 +40,7 @@ export function NoteMenu() {
+
- ) : null} -
- - ); -} diff --git a/apps/desktop2/src/routes/zap.$id.lazy.tsx b/apps/desktop2/src/routes/zap.$id.lazy.tsx index 1b1b68e9..5e334de0 100644 --- a/apps/desktop2/src/routes/zap.$id.lazy.tsx +++ b/apps/desktop2/src/routes/zap.$id.lazy.tsx @@ -7,6 +7,7 @@ import { useState } from "react"; import CurrencyInput from "react-currency-input-field"; import { useTranslation } from "react-i18next"; import { toast } from "sonner"; +import { LumeEvent } from "@lume/system"; const DEFAULT_VALUES = [69, 100, 200, 500]; @@ -16,7 +17,6 @@ export const Route = createLazyFileRoute("/zap/$id")({ function Screen() { const { t } = useTranslation(); - const { ark } = Route.useRouteContext(); const { id } = Route.useParams(); // @ts-ignore, magic !!! const { pubkey, account } = Route.useSearch(); @@ -31,7 +31,7 @@ function Screen() { // start loading setIsLoading(true); - const val = await ark.zap_event(id, amount, message); + const val = await LumeEvent.zap(id, amount, message); if (val) { setIsCompleted(true); diff --git a/packages/ark/src/ark.ts b/packages/ark/src/ark.ts deleted file mode 100644 index 6c875acb..00000000 --- a/packages/ark/src/ark.ts +++ /dev/null @@ -1,902 +0,0 @@ -import { - type Event, - type EventWithReplies, - type Interests, - type Keys, - type LumeColumn, - type Metadata, - type Settings, - Relays, -} from "@lume/types"; -import { generateContentTags } from "@lume/utils"; -import { invoke } from "@tauri-apps/api/core"; -import type { WebviewWindow } from "@tauri-apps/api/webviewWindow"; -import { open } from "@tauri-apps/plugin-dialog"; -import { readFile } from "@tauri-apps/plugin-fs"; - -enum NSTORE_KEYS { - settings = "lume_user_settings", - columns = "lume_user_columns", -} - -export class Ark { - public windows: WebviewWindow[]; - public settings: Settings; - public accounts: string[]; - - constructor() { - this.windows = []; - this.settings = undefined; - } - - public async get_accounts() { - try { - const cmd: string = await invoke("get_accounts"); - const parse = cmd.split(/\s+/).filter((v) => v.startsWith("npub1")); - const accounts = [...new Set(parse)]; - - if (!this.accounts) { - this.accounts = accounts; - } - - return accounts; - } catch (e) { - console.info(String(e)); - return []; - } - } - - public async load_account(npub: string) { - try { - const cmd: boolean = await invoke("load_account", { - npub, - }); - return cmd; - } catch (e) { - throw new Error(String(e)); - } - } - - public async nostr_connect(uri: string) { - try { - const remoteKey = uri.replace("bunker://", "").split("?")[0]; - const npub: string = await invoke("to_npub", { hex: remoteKey }); - - if (npub) { - const connect: string = await invoke("nostr_connect", { - npub, - uri, - }); - - return connect; - } - } catch (e) { - throw new Error(String(e)); - } - } - - public async create_keys() { - try { - const cmd: Keys = await invoke("create_account"); - return cmd; - } catch (e) { - console.error(String(e)); - } - } - - public async save_account(nsec: string, password = "") { - try { - const cmd: string = await invoke("save_account", { - nsec, - password, - }); - - return cmd; - } catch (e) { - throw new Error(String(e)); - } - } - - public async event_to_bech32(id: string, relays: string[]) { - try { - const cmd: string = await invoke("event_to_bech32", { - id, - relays, - }); - return cmd; - } catch (e) { - throw new Error(String(e)); - } - } - - public async get_relays() { - try { - const cmd: Relays = await invoke("get_relays"); - return cmd; - } catch (e) { - console.error(String(e)); - return null; - } - } - - public async add_relay(url: string) { - try { - const relayUrl = new URL(url); - - if (relayUrl.protocol === "wss:" || relayUrl.protocol === "ws:") { - const cmd: boolean = await invoke("connect_relay", { relay: relayUrl }); - return cmd; - } - } catch (e) { - throw new Error(String(e)); - } - } - - public async remove_relay(url: string) { - try { - const relayUrl = new URL(url); - - if (relayUrl.protocol === "wss:" || relayUrl.protocol === "ws:") { - const cmd: boolean = await invoke("remove_relay", { relay: relayUrl }); - return cmd; - } - } catch (e) { - throw new Error(String(e)); - } - } - - public async get_activities(account: string, kind: "1" | "6" | "9735" = "1") { - try { - const events: Event[] = await invoke("get_activities", { account, kind }); - return events; - } catch (e) { - console.error(String(e)); - return null; - } - } - - public async get_event(id: string) { - try { - const eventId: string = id.replace("nostr:", "").replace(/[^\w\s]/gi, ""); - const cmd: string = await invoke("get_event", { id: eventId }); - const event: Event = JSON.parse(cmd); - return event; - } catch (e) { - console.error(id, String(e)); - throw new Error(String(e)); - } - } - - public async search(content: string, limit: number) { - try { - if (content.length < 1) return []; - - const events: Event[] = await invoke("search", { - content: content.trim(), - limit, - }); - - return events; - } catch (e) { - console.info(String(e)); - return []; - } - } - - private dedup_events(nostrEvents: Event[]) { - const seens = new Set(); - const events = nostrEvents.filter((event) => { - const eTags = event.tags.filter((el) => el[0] === "e"); - const ids = eTags.map((item) => item[1]); - const isDup = ids.some((id) => seens.has(id)); - - // Add found ids to seen list - for (const id of ids) { - seens.add(id); - } - - // Filter NSFW event - if (this.settings?.nsfw) { - const wTags = event.tags.filter((t) => t[0] === "content-warning"); - const isLewd = wTags.length > 0; - - return !isDup && !isLewd; - } - - // Filter duplicate event - return !isDup; - }); - - return events; - } - - public async get_local_events( - pubkeys: string[], - limit: number, - asOf?: number, - ) { - try { - const until: string = asOf && asOf > 0 ? asOf.toString() : undefined; - const nostrEvents: Event[] = await invoke("get_local_events", { - pubkeys, - limit, - until, - }); - const events = this.dedup_events(nostrEvents); - - return events; - } catch (e) { - console.error("[get_local_events] failed", String(e)); - return []; - } - } - - public async get_global_events(limit: number, asOf?: number) { - try { - const until: string = asOf && asOf > 0 ? asOf.toString() : undefined; - const nostrEvents: Event[] = await invoke("get_global_events", { - limit, - until, - }); - const events = this.dedup_events(nostrEvents); - - return events; - } catch (e) { - console.error("[get_global_events] failed", String(e)); - return []; - } - } - - public async get_hashtag_events( - hashtags: string[], - limit: number, - asOf?: number, - ) { - try { - const until: string = asOf && asOf > 0 ? asOf.toString() : undefined; - const nostrTags = hashtags.map((tag) => tag.replace("#", "")); - const nostrEvents: Event[] = await invoke("get_hashtag_events", { - hashtags: nostrTags, - limit, - until, - }); - const events = this.dedup_events(nostrEvents); - - return events; - } catch (e) { - console.error("[get_hashtag_events] failed", String(e)); - return []; - } - } - - public async get_group_events( - contacts: string[], - limit: number, - asOf?: number, - ) { - try { - const until: string = asOf && asOf > 0 ? asOf.toString() : undefined; - const nostrEvents: Event[] = await invoke("get_group_events", { - list: contacts, - limit, - until, - }); - const events = this.dedup_events(nostrEvents); - - return events; - } catch (e) { - console.error("[get_group_events] failed", String(e)); - return []; - } - } - - public async get_events_by(pubkey: string, limit: number, asOf?: number) { - try { - const until: string = asOf && asOf > 0 ? asOf.toString() : undefined; - const nostrEvents: Event[] = await invoke("get_events_by", { - publicKey: pubkey, - limit, - as_of: until, - }); - - return nostrEvents.sort((a, b) => b.created_at - a.created_at); - } catch (e) { - console.error("[get_events_by] failed", String(e)); - return []; - } - } - - public async publish( - content: string, - reply_to?: string, - quote?: boolean, - nsfw?: boolean, - ) { - try { - const g = await generateContentTags(content); - - const eventContent = g.content; - const eventTags = g.tags; - - if (reply_to) { - const replyEvent = await this.get_event(reply_to); - const relayHint = - replyEvent.tags.find((ev) => ev[0] === "e")?.[0][2] ?? ""; - - if (quote) { - eventTags.push(["e", replyEvent.id, relayHint, "mention"]); - eventTags.push(["q", replyEvent.id]); - } else { - const rootEvent = replyEvent.tags.find((ev) => ev[3] === "root"); - - if (rootEvent) { - eventTags.push([ - "e", - rootEvent[1], - rootEvent[2] || relayHint, - "root", - ]); - } - - eventTags.push(["e", replyEvent.id, relayHint, "reply"]); - eventTags.push(["p", replyEvent.pubkey]); - } - } - - if (nsfw) { - eventTags.push(["L", "content-warning"]); - eventTags.push(["l", "reason", "content-warning"]); - eventTags.push(["content-warning", "nsfw"]); - } - - const cmd: string = await invoke("publish", { - content: eventContent, - tags: eventTags, - }); - - return cmd; - } catch (e) { - throw new Error(String(e)); - } - } - - public async reply_to(content: string, tags: string[]) { - try { - const cmd: string = await invoke("reply_to", { content, tags }); - return cmd; - } catch (e) { - throw new Error(String(e)); - } - } - - public async repost(id: string, author: string) { - try { - const cmd: string = await invoke("repost", { id, pubkey: author }); - return cmd; - } catch (e) { - throw new Error(String(e)); - } - } - - public async get_event_thread(id: string) { - try { - const events: EventWithReplies[] = await invoke("get_thread", { - id, - }); - - 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; - } catch (e) { - return []; - } - } - - public get_thread(tags: string[][], gossip: boolean = false) { - let root: string = null; - let reply: string = null; - - // Get all event references from tags, ignore mention - const events = tags.filter((el) => el[0] === "e" && el[3] !== "mention"); - - if (gossip) { - const relays = tags.filter((el) => el[0] === "e" && el[2]?.length); - - if (relays.length >= 1) { - for (const relay of relays) { - if (relay[2]?.length) this.add_relay(relay[2]); - } - } - } - - if (events.length === 1) { - root = events[0][1]; - } - - if (events.length > 1) { - root = events.find((el) => el[3] === "root")?.[1] ?? events[0][1]; - reply = events.find((el) => el[3] === "reply")?.[1] ?? events[1][1]; - } - - // Fix some rare case when root === reply - if (root && reply && root === reply) { - reply = null; - } - - return { - root, - reply, - }; - } - - public async get_profile(pubkey: string) { - try { - const id = pubkey.replace("nostr:", "").replace(/[^\w\s]/gi, ""); - const cmd: Metadata = await invoke("get_profile", { id }); - - return cmd; - } catch (e) { - console.error(pubkey, String(e)); - return null; - } - } - - public async get_current_user_profile() { - try { - const cmd: Metadata = await invoke("get_current_user_profile"); - return cmd; - } catch { - return null; - } - } - - public async create_profile(profile: Metadata) { - try { - const event: string = await invoke("create_profile", { - name: profile.name || "", - display_name: profile.display_name || "", - displayName: profile.display_name || "", - about: profile.about || "", - picture: profile.picture || "", - banner: profile.banner || "", - nip05: profile.nip05 || "", - lud16: profile.lud16 || "", - website: profile.website || "", - }); - return event; - } catch (e) { - throw new Error(String(e)); - } - } - - public async set_contact_list(pubkeys: string[]) { - try { - const cmd: boolean = await invoke("set_contact_list", { pubkeys }); - return cmd; - } catch (e) { - throw new Error(String(e)); - } - } - - public async get_contact_list() { - try { - const cmd: string[] = await invoke("get_contact_list"); - return cmd; - } catch (e) { - console.error(e); - return []; - } - } - - public async follow(id: string, alias?: string) { - try { - const cmd: string = await invoke("follow", { id, alias }); - return cmd; - } catch (e) { - throw new Error(String(e)); - } - } - - public async unfollow(id: string) { - try { - const cmd: string = await invoke("unfollow", { id }); - return cmd; - } catch (e) { - throw new Error(String(e)); - } - } - - public async user_to_bech32(key: string, relays: string[]) { - try { - const cmd: string = await invoke("user_to_bech32", { - key, - relays, - }); - return cmd; - } catch (e) { - throw new Error(String(e)); - } - } - - public async verify_nip05(pubkey: string, nip05: string) { - try { - const cmd: boolean = await invoke("verify_nip05", { - key: pubkey, - nip05, - }); - return cmd; - } catch { - return false; - } - } - - public async set_nwc(uri: string) { - try { - const cmd: boolean = await invoke("set_nwc", { uri }); - return cmd; - } catch (e) { - throw new Error(String(e)); - } - } - - public async load_nwc() { - try { - const cmd: boolean = await invoke("load_nwc"); - return cmd; - } catch (e) { - throw new Error(String(e)); - } - } - - public async get_balance() { - try { - const cmd: number = await invoke("get_balance"); - return cmd; - } catch (e) { - throw new Error(String(e)); - } - } - - public async zap_profile(id: string, amount: number, message: string) { - try { - const cmd: boolean = await invoke("zap_profile", { id, amount, message }); - return cmd; - } catch (e) { - throw new Error(String(e)); - } - } - - public async zap_event(id: string, amount: number, message: string) { - try { - const cmd: boolean = await invoke("zap_event", { id, amount, message }); - return cmd; - } catch (e) { - throw new Error(String(e)); - } - } - - public async upload(filePath?: string) { - const allowExts = [ - "png", - "jpeg", - "jpg", - "gif", - "mp4", - "mp3", - "webm", - "mkv", - "avi", - "mov", - ]; - - const selected = - filePath || - ( - await open({ - multiple: false, - filters: [ - { - name: "Media", - extensions: allowExts, - }, - ], - }) - ).path; - - // User cancelled action - if (!selected) return null; - - try { - const file = await readFile(selected); - 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; - } catch (e) { - throw new Error(String(e)); - } - } - - public async get_columns() { - try { - const cmd: string = await invoke("get_nstore", { - key: NSTORE_KEYS.columns, - }); - const columns: LumeColumn[] = cmd ? JSON.parse(cmd) : []; - return columns; - } catch { - return []; - } - } - - public async set_columns(columns: LumeColumn[]) { - try { - const cmd: string = await invoke("set_nstore", { - key: NSTORE_KEYS.columns, - content: JSON.stringify(columns), - }); - return cmd; - } catch (e) { - throw new Error(e); - } - } - - public async get_settings() { - try { - if (this.settings) return this.settings; - - const cmd: string = await invoke("get_nstore", { - key: NSTORE_KEYS.settings, - }); - const settings: Settings = cmd ? JSON.parse(cmd) : null; - - this.settings = settings; - - return settings; - } catch { - const defaultSettings: Settings = { - autoUpdate: false, - enhancedPrivacy: false, - notification: false, - zap: false, - nsfw: false, - }; - this.settings = defaultSettings; - return defaultSettings; - } - } - - public async set_settings(settings: Settings) { - try { - const cmd: string = await invoke("set_nstore", { - key: NSTORE_KEYS.settings, - content: JSON.stringify(settings), - }); - return cmd; - } catch (e) { - throw new Error(e); - } - } - - public async get_nstore(key: string) { - try { - const cmd: string = await invoke("get_nstore", { - key, - }); - const parse: string | string[] = cmd ? JSON.parse(cmd) : null; - if (!parse.length) return null; - return parse; - } catch { - return null; - } - } - - public async set_nstore(key: string, content: string) { - try { - const cmd: string = await invoke("set_nstore", { - key, - content, - }); - return cmd; - } catch (e) { - throw new Error(String(e)); - } - } - - public async open_event_id(id: string) { - try { - const label = `event-${id}`; - const url = `/events/${id}`; - - await invoke("open_window", { - label, - title: "Thread", - url, - width: 500, - height: 800, - }); - } catch (e) { - throw new Error(String(e)); - } - } - - public async open_event(event: Event) { - try { - let root: string = undefined; - let reply: string = undefined; - - const eTags = event.tags.filter( - (tag) => tag[0] === "e" || tag[0] === "q", - ); - - root = eTags.find((el) => el[3] === "root")?.[1]; - reply = eTags.find((el) => el[3] === "reply")?.[1]; - - if (!root) root = eTags[0]?.[1]; - if (!reply) reply = eTags[1]?.[1]; - - const label = `event-${event.id}`; - const url = `/events/${root ?? reply ?? event.id}`; - - await invoke("open_window", { - label, - title: "Thread", - url, - width: 500, - height: 800, - }); - } catch (e) { - throw new Error(String(e)); - } - } - - public async open_profile(pubkey: string) { - try { - const label = `user-${pubkey}`; - await invoke("open_window", { - label, - title: "Profile", - url: `/users/${pubkey}`, - width: 500, - height: 800, - }); - } catch (e) { - throw new Error(String(e)); - } - } - - public async open_editor(reply_to?: string, quote = false) { - try { - let url: string; - - if (reply_to) { - url = `/editor?reply_to=${reply_to}"e=${quote}`; - } else { - url = "/editor"; - } - - const label = `editor-${reply_to ? reply_to : 0}`; - - await invoke("open_window", { - label, - title: "Editor", - url, - width: 560, - height: 340, - }); - } catch (e) { - throw new Error(String(e)); - } - } - - public async open_nwc() { - try { - const label = "nwc"; - await invoke("open_window", { - label, - title: "Nostr Wallet Connect", - url: "/nwc", - width: 400, - height: 600, - }); - } catch (e) { - throw new Error(String(e)); - } - } - - public async open_zap(id: string, pubkey: string, account: string) { - try { - const label = `zap-${id}`; - await invoke("open_window", { - label, - title: "Zap", - url: `/zap/${id}?pubkey=${pubkey}&account=${account}`, - width: 400, - height: 500, - }); - } catch (e) { - throw new Error(String(e)); - } - } - - public async open_settings() { - try { - const label = "settings"; - await invoke("open_window", { - label, - title: "Settings", - url: "/settings", - width: 800, - height: 500, - }); - } catch (e) { - throw new Error(String(e)); - } - } - - public async open_search() { - try { - const label = "search"; - await invoke("open_window", { - label, - title: "Search", - url: "/search", - width: 400, - height: 600, - }); - } catch (e) { - throw new Error(String(e)); - } - } - - public async open_activity(account: string) { - try { - const label = "activity"; - await invoke("open_window", { - label, - title: "Activity", - url: `/activity/${account}/texts`, - width: 400, - height: 600, - }); - } catch (e) { - throw new Error(String(e)); - } - } -} diff --git a/packages/ark/src/hooks/usePreview.ts b/packages/ark/src/hooks/usePreview.ts deleted file mode 100644 index d473606b..00000000 --- a/packages/ark/src/hooks/usePreview.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { useQuery } from "@tanstack/react-query"; -import { invoke } from "@tauri-apps/api/core"; - -export function usePreview(url: string) { - const { isLoading, isError, data } = useQuery({ - queryKey: ["url", url], - queryFn: async () => { - try { - const cmd = await invoke("fetch_opg", { url }); - console.log(cmd); - return cmd; - } catch (e) { - throw new Error(e); - } - }, - refetchOnWindowFocus: false, - refetchOnMount: false, - refetchOnReconnect: false, - staleTime: Number.POSITIVE_INFINITY, - retry: 2, - }); - - return { isLoading, isError, data }; -} diff --git a/packages/ark/src/index.ts b/packages/ark/src/index.ts deleted file mode 100644 index d506ecaa..00000000 --- a/packages/ark/src/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from "./ark"; -export * from "./hooks/useEvent"; -export * from "./hooks/useProfile"; diff --git a/packages/ark/package.json b/packages/system/package.json similarity index 92% rename from packages/ark/package.json rename to packages/system/package.json index fe613e59..073a3591 100644 --- a/packages/ark/package.json +++ b/packages/system/package.json @@ -1,5 +1,5 @@ { - "name": "@lume/ark", + "name": "@lume/system", "version": "0.0.0", "private": true, "main": "./src/index.ts", diff --git a/packages/system/src/account.ts b/packages/system/src/account.ts new file mode 100644 index 00000000..e26f1c58 --- /dev/null +++ b/packages/system/src/account.ts @@ -0,0 +1,164 @@ +import { Metadata } from "@lume/types"; +import { commands } from "./commands"; + +export class NostrAccount { + static async getAccounts() { + const query = await commands.getAccounts(); + + if (query.status === "ok") { + const accounts = query.data + .split(/\s+/) + .filter((v) => v.startsWith("npub1")); + + return [...new Set(accounts)]; + } else { + return []; + } + } + + static async loadAccount(npub: string) { + const query = await commands.loadAccount(npub); + + if (query.status === "ok") { + return query.data; + } else { + throw new Error(query.error); + } + } + + static async createAccount() { + const query = await commands.createAccount(); + + if (query.status === "ok") { + return query.data; + } else { + throw new Error(query.error); + } + } + + static async createProfile(profile: Metadata) { + const query = await commands.createProfile( + profile.name || "", + profile.display_name || "", + profile.about || "", + profile.picture || "", + profile.banner || "", + profile.nip05 || "", + profile.lud16 || "", + profile.website || "", + ); + + if (query.status === "ok") { + return query.data; + } else { + throw new Error(query.error); + } + } + + static async saveAccount(nsec: string, password = "") { + const query = await commands.saveAccount(nsec, password); + + if (query.status === "ok") { + return query.data; + } else { + throw new Error(query.error); + } + } + + static async connectRemoteAccount(uri: string) { + const remoteKey = uri.replace("bunker://", "").split("?")[0]; + const npub = await commands.toNpub(remoteKey); + + if (npub.status === "ok") { + const connect = await commands.nostrConnect(npub.data, uri); + + if (connect.status === "ok") { + return connect.data; + } else { + throw new Error(connect.error); + } + } else { + throw new Error(npub.error); + } + } + + static async setContactList(pubkeys: string[]) { + const query = await commands.setContactList(pubkeys); + + if (query.status === "ok") { + return query.data; + } else { + throw new Error(query.error); + } + } + + static async setWallet(uri: string) { + const query = await commands.setNwc(uri); + + if (query.status === "ok") { + return query.data; + } else { + throw new Error(query.error); + } + } + + static async getProfile() { + const query = await commands.getCurrentUserProfile(); + + if (query.status === "ok") { + return JSON.parse(query.data) as Metadata; + } else { + return null; + } + } + + static async getBalance() { + const query = await commands.getBalance(); + + if (query.status === "ok") { + return parseInt(query.data); + } else { + return 0; + } + } + + static async getContactList() { + const query = await commands.getContactList(); + + if (query.status === "ok") { + return query.data; + } else { + return []; + } + } + + static async follow(pubkey: string, alias?: string) { + const query = await commands.follow(pubkey, alias); + + if (query.status === "ok") { + return query.data; + } else { + throw new Error(query.error); + } + } + + static async unfollow(pubkey: string) { + const query = await commands.unfollow(pubkey); + + if (query.status === "ok") { + return query.data; + } else { + throw new Error(query.error); + } + } + + static async f2f(npub: string) { + const query = await commands.friendToFriend(npub); + + if (query.status === "ok") { + return query.data; + } else { + throw new Error(query.error); + } + } +} diff --git a/packages/system/src/commands.ts b/packages/system/src/commands.ts new file mode 100644 index 00000000..6ecc71c8 --- /dev/null +++ b/packages/system/src/commands.ts @@ -0,0 +1,609 @@ +// This file was generated by [tauri-specta](https://github.com/oscartbeaumont/tauri-specta). Do not edit this file manually. + +export const commands = { + async getRelays(): Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("get_relays") }; + } catch (e) { + if (e instanceof Error) throw e; + else return { status: "error", error: e as any }; + } + }, + async connectRelay(relay: string): Promise> { + try { + return { + status: "ok", + data: await TAURI_INVOKE("connect_relay", { relay }), + }; + } catch (e) { + if (e instanceof Error) throw e; + else return { status: "error", error: e as any }; + } + }, + async removeRelay(relay: string): Promise> { + try { + return { + status: "ok", + data: await TAURI_INVOKE("remove_relay", { relay }), + }; + } catch (e) { + if (e instanceof Error) throw e; + else return { status: "error", error: e as any }; + } + }, + async getAccounts(): Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("get_accounts") }; + } catch (e) { + if (e instanceof Error) throw e; + else return { status: "error", error: e as any }; + } + }, + async createAccount(): Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("create_account") }; + } catch (e) { + if (e instanceof Error) throw e; + else return { status: "error", error: e as any }; + } + }, + async saveAccount( + nsec: string, + password: string, + ): Promise> { + try { + return { + status: "ok", + data: await TAURI_INVOKE("save_account", { nsec, password }), + }; + } catch (e) { + if (e instanceof Error) throw e; + else return { status: "error", error: e as any }; + } + }, + async getEncryptedKey( + npub: string, + password: string, + ): Promise> { + try { + return { + status: "ok", + data: await TAURI_INVOKE("get_encrypted_key", { npub, password }), + }; + } catch (e) { + if (e instanceof Error) throw e; + else return { status: "error", error: e as any }; + } + }, + async nostrConnect( + npub: string, + uri: string, + ): Promise> { + try { + return { + status: "ok", + data: await TAURI_INVOKE("nostr_connect", { npub, uri }), + }; + } catch (e) { + if (e instanceof Error) throw e; + else return { status: "error", error: e as any }; + } + }, + async loadAccount(npub: string): Promise> { + try { + return { + status: "ok", + data: await TAURI_INVOKE("load_account", { npub }), + }; + } catch (e) { + if (e instanceof Error) throw e; + else return { status: "error", error: e as any }; + } + }, + async eventToBech32( + id: string, + relays: string[], + ): Promise> { + try { + return { + status: "ok", + data: await TAURI_INVOKE("event_to_bech32", { id, relays }), + }; + } catch (e) { + if (e instanceof Error) throw e; + else return { status: "error", error: e as any }; + } + }, + async userToBech32( + key: string, + relays: string[], + ): Promise> { + try { + return { + status: "ok", + data: await TAURI_INVOKE("user_to_bech32", { key, relays }), + }; + } catch (e) { + if (e instanceof Error) throw e; + else return { status: "error", error: e as any }; + } + }, + async toNpub(hex: string): Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("to_npub", { hex }) }; + } catch (e) { + if (e instanceof Error) throw e; + else return { status: "error", error: e as any }; + } + }, + async verifyNip05( + key: string, + nip05: string, + ): Promise> { + try { + return { + status: "ok", + data: await TAURI_INVOKE("verify_nip05", { key, nip05 }), + }; + } catch (e) { + if (e instanceof Error) throw e; + else return { status: "error", error: e as any }; + } + }, + async runNotification(accounts: string[]): Promise> { + try { + return { + status: "ok", + data: await TAURI_INVOKE("run_notification", { accounts }), + }; + } catch (e) { + if (e instanceof Error) throw e; + else return { status: "error", error: e as any }; + } + }, + async getActivities( + account: string, + kind: string, + ): Promise> { + try { + return { + status: "ok", + data: await TAURI_INVOKE("get_activities", { account, kind }), + }; + } catch (e) { + if (e instanceof Error) throw e; + else return { status: "error", error: e as any }; + } + }, + async getCurrentUserProfile(): Promise> { + try { + return { + status: "ok", + data: await TAURI_INVOKE("get_current_user_profile"), + }; + } catch (e) { + if (e instanceof Error) throw e; + else return { status: "error", error: e as any }; + } + }, + async getProfile(id: string): Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("get_profile", { id }) }; + } catch (e) { + if (e instanceof Error) throw e; + else return { status: "error", error: e as any }; + } + }, + async getContactList(): Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("get_contact_list") }; + } catch (e) { + if (e instanceof Error) throw e; + else return { status: "error", error: e as any }; + } + }, + async setContactList(pubkeys: string[]): Promise> { + try { + return { + status: "ok", + data: await TAURI_INVOKE("set_contact_list", { pubkeys }), + }; + } catch (e) { + if (e instanceof Error) throw e; + else return { status: "error", error: e as any }; + } + }, + async createProfile( + name: string, + displayName: string, + about: string, + picture: string, + banner: string, + nip05: string, + lud16: string, + website: string, + ): Promise> { + try { + return { + status: "ok", + data: await TAURI_INVOKE("create_profile", { + name, + displayName, + about, + picture, + banner, + nip05, + lud16, + website, + }), + }; + } catch (e) { + if (e instanceof Error) throw e; + else return { status: "error", error: e as any }; + } + }, + async follow( + id: string, + alias: string | null, + ): Promise> { + try { + return { + status: "ok", + data: await TAURI_INVOKE("follow", { id, alias }), + }; + } catch (e) { + if (e instanceof Error) throw e; + else return { status: "error", error: e as any }; + } + }, + async unfollow(id: string): Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("unfollow", { id }) }; + } catch (e) { + if (e instanceof Error) throw e; + else return { status: "error", error: e as any }; + } + }, + async getNstore(key: string): Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("get_nstore", { key }) }; + } catch (e) { + if (e instanceof Error) throw e; + else return { status: "error", error: e as any }; + } + }, + async setNstore( + key: string, + content: string, + ): Promise> { + try { + return { + status: "ok", + data: await TAURI_INVOKE("set_nstore", { key, content }), + }; + } catch (e) { + if (e instanceof Error) throw e; + else return { status: "error", error: e as any }; + } + }, + async setNwc(uri: string): Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("set_nwc", { uri }) }; + } catch (e) { + if (e instanceof Error) throw e; + else return { status: "error", error: e as any }; + } + }, + async loadNwc(): Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("load_nwc") }; + } catch (e) { + if (e instanceof Error) throw e; + else return { status: "error", error: e as any }; + } + }, + async getBalance(): Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("get_balance") }; + } catch (e) { + if (e instanceof Error) throw e; + else return { status: "error", error: e as any }; + } + }, + async zapProfile( + id: string, + amount: string, + message: string, + ): Promise> { + try { + return { + status: "ok", + data: await TAURI_INVOKE("zap_profile", { id, amount, message }), + }; + } catch (e) { + if (e instanceof Error) throw e; + else return { status: "error", error: e as any }; + } + }, + async zapEvent( + id: string, + amount: string, + message: string, + ): Promise> { + try { + return { + status: "ok", + data: await TAURI_INVOKE("zap_event", { id, amount, message }), + }; + } catch (e) { + if (e instanceof Error) throw e; + else return { status: "error", error: e as any }; + } + }, + async friendToFriend(npub: string): Promise> { + try { + return { + status: "ok", + data: await TAURI_INVOKE("friend_to_friend", { npub }), + }; + } catch (e) { + if (e instanceof Error) throw e; + else return { status: "error", error: e as any }; + } + }, + async getEvent(id: string): Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("get_event", { id }) }; + } catch (e) { + if (e instanceof Error) throw e; + else return { status: "error", error: e as any }; + } + }, + async getReplies(id: string): Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("get_replies", { id }) }; + } catch (e) { + if (e instanceof Error) throw e; + else return { status: "error", error: e as any }; + } + }, + async getEventsBy( + publicKey: string, + asOf: string | null, + ): Promise> { + try { + return { + status: "ok", + data: await TAURI_INVOKE("get_events_by", { publicKey, asOf }), + }; + } catch (e) { + if (e instanceof Error) throw e; + else return { status: "error", error: e as any }; + } + }, + async getLocalEvents( + pubkeys: string[], + until: string | null, + ): Promise> { + try { + return { + status: "ok", + data: await TAURI_INVOKE("get_local_events", { pubkeys, until }), + }; + } catch (e) { + if (e instanceof Error) throw e; + else return { status: "error", error: e as any }; + } + }, + async getGlobalEvents( + until: string | null, + ): Promise> { + try { + return { + status: "ok", + data: await TAURI_INVOKE("get_global_events", { until }), + }; + } catch (e) { + if (e instanceof Error) throw e; + else return { status: "error", error: e as any }; + } + }, + async getHashtagEvents( + hashtags: string[], + until: string | null, + ): Promise> { + try { + return { + status: "ok", + data: await TAURI_INVOKE("get_hashtag_events", { hashtags, until }), + }; + } catch (e) { + if (e instanceof Error) throw e; + else return { status: "error", error: e as any }; + } + }, + async publish( + content: string, + tags: string[][], + ): Promise> { + try { + return { + status: "ok", + data: await TAURI_INVOKE("publish", { content, tags }), + }; + } catch (e) { + if (e instanceof Error) throw e; + else return { status: "error", error: e as any }; + } + }, + async repost(raw: string): Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("repost", { raw }) }; + } catch (e) { + if (e instanceof Error) throw e; + else return { status: "error", error: e as any }; + } + }, + async showInFolder(path: string): Promise { + await TAURI_INVOKE("show_in_folder", { path }); + }, + async createColumn( + label: string, + x: number, + y: number, + width: number, + height: number, + url: string, + ): Promise> { + try { + return { + status: "ok", + data: await TAURI_INVOKE("create_column", { + label, + x, + y, + width, + height, + url, + }), + }; + } catch (e) { + if (e instanceof Error) throw e; + else return { status: "error", error: e as any }; + } + }, + async closeColumn(label: string): Promise> { + try { + return { + status: "ok", + data: await TAURI_INVOKE("close_column", { label }), + }; + } catch (e) { + if (e instanceof Error) throw e; + else return { status: "error", error: e as any }; + } + }, + async repositionColumn( + label: string, + x: number, + y: number, + ): Promise> { + try { + return { + status: "ok", + data: await TAURI_INVOKE("reposition_column", { label, x, y }), + }; + } catch (e) { + if (e instanceof Error) throw e; + else return { status: "error", error: e as any }; + } + }, + async resizeColumn( + label: string, + width: number, + height: number, + ): Promise> { + try { + return { + status: "ok", + data: await TAURI_INVOKE("resize_column", { label, width, height }), + }; + } catch (e) { + if (e instanceof Error) throw e; + else return { status: "error", error: e as any }; + } + }, + async openWindow( + label: string, + title: string, + url: string, + width: number, + height: number, + ): Promise> { + try { + return { + status: "ok", + data: await TAURI_INVOKE("open_window", { + label, + title, + url, + width, + height, + }), + }; + } catch (e) { + if (e instanceof Error) throw e; + else return { status: "error", error: e as any }; + } + }, + async setBadge(count: number): Promise { + await TAURI_INVOKE("set_badge", { count }); + }, +}; + +/** user-defined types **/ + +export type Account = { npub: string; nsec: string }; +export type Relays = { + connected: string[]; + read: string[] | null; + write: string[] | null; + both: string[] | null; +}; + +/** tauri-specta globals **/ + +import { invoke as TAURI_INVOKE } from "@tauri-apps/api/core"; +import * as TAURI_API_EVENT from "@tauri-apps/api/event"; +import { type WebviewWindow as __WebviewWindow__ } from "@tauri-apps/api/webviewWindow"; + +type __EventObj__ = { + listen: ( + cb: TAURI_API_EVENT.EventCallback, + ) => ReturnType>; + once: ( + cb: TAURI_API_EVENT.EventCallback, + ) => ReturnType>; + emit: T extends null + ? (payload?: T) => ReturnType + : (payload: T) => ReturnType; +}; + +export type Result = + | { status: "ok"; data: T } + | { status: "error"; error: E }; + +function __makeEvents__>( + mappings: Record, +) { + return new Proxy( + {} as unknown as { + [K in keyof T]: __EventObj__ & { + (handle: __WebviewWindow__): __EventObj__; + }; + }, + { + get: (_, event) => { + const name = mappings[event as keyof T]; + + return new Proxy((() => {}) as any, { + apply: (_, __, [window]: [__WebviewWindow__]) => ({ + listen: (arg: any) => window.listen(name, arg), + once: (arg: any) => window.once(name, arg), + emit: (arg: any) => window.emit(name, arg), + }), + get: (_, command: keyof __EventObj__) => { + switch (command) { + case "listen": + return (arg: any) => TAURI_API_EVENT.listen(name, arg); + case "once": + return (arg: any) => TAURI_API_EVENT.once(name, arg); + case "emit": + return (arg: any) => TAURI_API_EVENT.emit(name, arg); + } + }, + }); + }, + }, + ); +} diff --git a/packages/system/src/dedup.ts b/packages/system/src/dedup.ts new file mode 100644 index 00000000..ddb4f633 --- /dev/null +++ b/packages/system/src/dedup.ts @@ -0,0 +1,28 @@ +import { NostrEvent } from "@lume/types"; + +export function dedupEvents(nostrEvents: NostrEvent[], nsfw: boolean = false) { + const seens = new Set(); + const events = nostrEvents.filter((event) => { + const eTags = event.tags.filter((el) => el[0] === "e"); + const ids = eTags.map((item) => item[1]); + const isDup = ids.some((id) => seens.has(id)); + + // Add found ids to seen list + for (const id of ids) { + seens.add(id); + } + + // Filter NSFW event + if (nsfw) { + const wTags = event.tags.filter((t) => t[0] === "content-warning"); + const isLewd = wTags.length > 0; + + return !isDup && !isLewd; + } + + // Filter duplicate event + return !isDup; + }); + + return events; +} diff --git a/packages/system/src/event.ts b/packages/system/src/event.ts new file mode 100644 index 00000000..1148d6ea --- /dev/null +++ b/packages/system/src/event.ts @@ -0,0 +1,200 @@ +import { EventWithReplies, Kind, NostrEvent } from "@lume/types"; +import { commands } from "./commands"; +import { generateContentTags } from "@lume/utils"; + +export class LumeEvent { + public id: string; + public pubkey: string; + public created_at: number; + public kind: Kind; + public tags: string[][]; + public content: string; + public sig: string; + public relay?: string; + #raw: NostrEvent; + + constructor(event: NostrEvent) { + this.#raw = event; + Object.assign(this, event); + } + + get mentions() { + return this.tags.filter((tag) => tag[0] === "p").map((tag) => tag[1]); + } + + static getEventThread(tags: string[][]) { + let root: string = null; + let reply: string = null; + + // Get all event references from tags, ignore mention + const events = tags.filter((el) => el[0] === "e" && el[3] !== "mention"); + + /* + if (gossip) { + const relays = tags.filter((el) => el[0] === "e" && el[2]?.length); + + if (relays.length >= 1) { + for (const relay of relays) { + if (relay[2]?.length) this.add_relay(relay[2]); + } + } + } + */ + + if (events.length === 1) { + root = events[0][1]; + } + + if (events.length > 1) { + root = events.find((el) => el[3] === "root")?.[1] ?? events[0][1]; + reply = events.find((el) => el[3] === "reply")?.[1] ?? events[1][1]; + } + + // Fix some rare case when root === reply + if (root && reply && root === reply) { + reply = null; + } + + return { + root, + reply, + }; + } + + static async getReplies(id: string) { + const query = await commands.getReplies(id); + + if (query.status === "ok") { + const events = query.data.map( + (item) => JSON.parse(item) as EventWithReplies, + ); + + 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); + } + } + } + } + + return events.filter((ev) => !replies.has(ev.id)); + } + + return events; + } + } + + static async publish( + content: string, + reply_to?: string, + quote?: boolean, + nsfw?: boolean, + ) { + const g = await generateContentTags(content); + + const eventContent = g.content; + const eventTags = g.tags; + + if (reply_to) { + const queryReply = await commands.getEvent(reply_to); + + if (queryReply.status === "ok") { + const replyEvent = JSON.parse(queryReply.data) as NostrEvent; + const relayHint = + replyEvent.tags.find((ev) => ev[0] === "e")?.[0][2] ?? ""; + + if (quote) { + eventTags.push(["e", replyEvent.id, relayHint, "mention"]); + eventTags.push(["q", replyEvent.id]); + } else { + const rootEvent = replyEvent.tags.find((ev) => ev[3] === "root"); + + if (rootEvent) { + eventTags.push([ + "e", + rootEvent[1], + rootEvent[2] || relayHint, + "root", + ]); + } + + eventTags.push(["e", replyEvent.id, relayHint, "reply"]); + eventTags.push(["p", replyEvent.pubkey]); + } + } + } + + if (nsfw) { + eventTags.push(["L", "content-warning"]); + eventTags.push(["l", "reason", "content-warning"]); + eventTags.push(["content-warning", "nsfw"]); + } + + const query = await commands.publish(eventContent, eventTags); + + if (query.status === "ok") { + return query.data; + } else { + throw new Error(query.error); + } + } + + static async zap(id: string, amount: number, message: string) { + const query = await commands.zapEvent(id, amount.toString(), message); + + if (query.status === "ok") { + return query.data; + } else { + throw new Error(query.error); + } + } + + public async idAsBech32() { + const query = await commands.eventToBech32(this.id, []); + + if (query.status === "ok") { + return query.data; + } else { + throw new Error(query.error); + } + } + + public async pubkeyAsBech32() { + const query = await commands.userToBech32(this.pubkey, []); + + if (query.status === "ok") { + return query.data; + } else { + throw new Error(query.error); + } + } + + public async repost() { + const query = await commands.repost(JSON.stringify(this.#raw)); + + if (query.status === "ok") { + return query.data; + } else { + throw new Error(query.error); + } + } +} diff --git a/packages/ark/src/hooks/useEvent.ts b/packages/system/src/hooks/useEvent.ts similarity index 86% rename from packages/ark/src/hooks/useEvent.ts rename to packages/system/src/hooks/useEvent.ts index 074a7938..2362de7d 100644 --- a/packages/ark/src/hooks/useEvent.ts +++ b/packages/system/src/hooks/useEvent.ts @@ -1,4 +1,4 @@ -import type { Event } from "@lume/types"; +import type { Event, NostrEvent } from "@lume/types"; import { useQuery } from "@tanstack/react-query"; import { invoke } from "@tauri-apps/api/core"; @@ -12,7 +12,7 @@ export function useEvent(id: string) { .split("'")[0] .split(".")[0]; const cmd: string = await invoke("get_event", { id: eventId }); - const event: Event = JSON.parse(cmd); + const event: NostrEvent = JSON.parse(cmd); return event; } catch (e) { throw new Error(e); diff --git a/packages/system/src/hooks/useInfiniteEvents.tsx b/packages/system/src/hooks/useInfiniteEvents.tsx new file mode 100644 index 00000000..6ce86551 --- /dev/null +++ b/packages/system/src/hooks/useInfiniteEvents.tsx @@ -0,0 +1,53 @@ +import { useInfiniteQuery } from "@tanstack/react-query"; +import { commands } from "../commands"; +import { dedupEvents } from "../dedup"; +import { NostrEvent } from "@lume/types"; + +export function useInfiniteEvents( + contacts: string[], + label: string, + account: string, + nsfw?: boolean, +) { + const pubkeys = contacts; + const { + data, + isLoading, + isFetching, + isFetchingNextPage, + hasNextPage, + fetchNextPage, + } = useInfiniteQuery({ + queryKey: [label, account], + initialPageParam: 0, + queryFn: async ({ pageParam }: { pageParam: number }) => { + try { + const until: string = pageParam > 0 ? pageParam.toString() : undefined; + const query = await commands.getLocalEvents(pubkeys, until); + + if (query.status === "ok") { + const nostrEvents = query.data as unknown as NostrEvent[]; + const events = dedupEvents(nostrEvents, nsfw); + + return events; + } else { + throw new Error(query.error); + } + } catch (e) { + throw new Error(e); + } + }, + getNextPageParam: (lastPage) => lastPage?.at(-1)?.created_at - 1, + select: (data) => data?.pages.flatMap((page) => page), + refetchOnWindowFocus: false, + }); + + return { + data, + isLoading, + isFetching, + isFetchingNextPage, + hasNextPage, + fetchNextPage, + }; +} diff --git a/packages/ark/src/hooks/useProfile.ts b/packages/system/src/hooks/useProfile.ts similarity index 59% rename from packages/ark/src/hooks/useProfile.ts rename to packages/system/src/hooks/useProfile.ts index 036ba5d3..fea44201 100644 --- a/packages/ark/src/hooks/useProfile.ts +++ b/packages/system/src/hooks/useProfile.ts @@ -1,6 +1,6 @@ import type { Metadata } from "@lume/types"; import { useQuery } from "@tanstack/react-query"; -import { invoke } from "@tauri-apps/api/core"; +import { commands } from "../commands"; export function useProfile(pubkey: string, embed?: string) { const { @@ -11,15 +11,16 @@ export function useProfile(pubkey: string, embed?: string) { queryKey: ["user", pubkey], queryFn: async () => { try { - if (embed) { - const profile: Metadata = JSON.parse(embed); - return profile; + if (embed) return JSON.parse(embed) as Metadata; + + const normalize = pubkey.replace("nostr:", "").replace(/[^\w\s]/gi, ""); + const query = await commands.getProfile(normalize); + + if (query.status === "ok") { + return JSON.parse(query.data) as Metadata; + } else { + throw new Error(query.error); } - - const id = pubkey.replace("nostr:", "").replace(/[^\w\s]/gi, ""); - const cmd: Metadata = await invoke("get_profile", { id }); - - return cmd; } catch (e) { throw new Error(e); } diff --git a/packages/system/src/index.ts b/packages/system/src/index.ts new file mode 100644 index 00000000..af7f23f3 --- /dev/null +++ b/packages/system/src/index.ts @@ -0,0 +1,8 @@ +export * from "./event"; +export * from "./account"; +export * from "./query"; +export * from "./window"; +export * from "./commands"; +export * from "./hooks/useEvent"; +export * from "./hooks/useInfiniteEvents"; +export * from "./hooks/useProfile"; diff --git a/packages/system/src/query.ts b/packages/system/src/query.ts new file mode 100644 index 00000000..6c42fff4 --- /dev/null +++ b/packages/system/src/query.ts @@ -0,0 +1,303 @@ +import { LumeColumn, Metadata, NostrEvent, Settings } from "@lume/types"; +import { commands } from "./commands"; +import { resolveResource } from "@tauri-apps/api/path"; +import { readFile, readTextFile } from "@tauri-apps/plugin-fs"; +import { isPermissionGranted } from "@tauri-apps/plugin-notification"; +import { open } from "@tauri-apps/plugin-dialog"; +import { dedupEvents } from "./dedup"; + +enum NSTORE_KEYS { + settings = "lume_user_settings", + columns = "lume_user_columns", +} + +export class NostrQuery { + static async upload(filePath?: string) { + const allowExts = [ + "png", + "jpeg", + "jpg", + "gif", + "mp4", + "mp3", + "webm", + "mkv", + "avi", + "mov", + ]; + + const selected = + filePath || + ( + await open({ + multiple: false, + filters: [ + { + name: "Media", + extensions: allowExts, + }, + ], + }) + ).path; + + // User cancelled action + if (!selected) return null; + + try { + const file = await readFile(selected); + 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; + } catch (e) { + throw new Error(String(e)); + } + } + + static async getProfile(pubkey: string) { + const normalize = pubkey.replace("nostr:", "").replace(/[^\w\s]/gi, ""); + const query = await commands.getProfile(normalize); + + if (query.status === "ok") { + const profile: Metadata = JSON.parse(query.data); + return profile; + } else { + return null; + } + } + + static async getEvent(id: string) { + const normalize: string = id.replace("nostr:", "").replace(/[^\w\s]/gi, ""); + const query = await commands.getEvent(normalize); + + if (query.status === "ok") { + const event: NostrEvent = JSON.parse(query.data); + return event; + } else { + return null; + } + } + + static async getUserEvents(pubkey: string, asOf?: number) { + const until: string = asOf && asOf > 0 ? asOf.toString() : undefined; + const query = await commands.getEventsBy(pubkey, until); + + if (query.status === "ok") { + const events = query.data.map((item) => JSON.parse(item) as NostrEvent); + return events; + } else { + return []; + } + } + + static async getUserActivities( + account: string, + kind: "1" | "6" | "9735" = "1", + ) { + const query = await commands.getActivities(account, kind); + + if (query.status === "ok") { + const events = query.data.map((item) => JSON.parse(item) as NostrEvent); + return events; + } else { + return []; + } + } + + static async getLocalEvents(pubkeys: string[], asOf?: number) { + const until: string = asOf && asOf > 0 ? asOf.toString() : undefined; + const query = await commands.getLocalEvents(pubkeys, until); + + if (query.status === "ok") { + const events = query.data.map((item) => JSON.parse(item) as NostrEvent); + const dedup = dedupEvents(events); + + return dedup; + } else { + return []; + } + } + + static async getGlobalEvents(asOf?: number) { + const until: string = asOf && asOf > 0 ? asOf.toString() : undefined; + const query = await commands.getGlobalEvents(until); + + if (query.status === "ok") { + const events = query.data.map((item) => JSON.parse(item) as NostrEvent); + const dedup = dedupEvents(events); + + return dedup; + } else { + return []; + } + } + + static async getHashtagEvents(hashtags: string[], asOf?: number) { + const until: string = asOf && asOf > 0 ? asOf.toString() : undefined; + const nostrTags = hashtags.map((tag) => tag.replace("#", "")); + const query = await commands.getHashtagEvents(nostrTags, until); + + if (query.status === "ok") { + const events = query.data.map((item) => JSON.parse(item) as NostrEvent); + const dedup = dedupEvents(events); + + return dedup; + } else { + return []; + } + } + + static async verifyNip05(pubkey: string, nip05?: string) { + if (!nip05) return false; + + const query = await commands.verifyNip05(pubkey, nip05); + + if (query.status === "ok") { + return query.data; + } else { + return false; + } + } + + static async getNstore(key: string) { + const query = await commands.getNstore(key); + + if (query.status === "ok") { + const data: string | string[] = query.data + ? JSON.parse(query.data) + : null; + return data; + } else { + return null; + } + } + + static async setNstore(key: string, value: string) { + const query = await commands.setNstore(key, value); + + if (query.status === "ok") { + return query.data; + } else { + throw new Error(query.error); + } + } + + static async getSettings() { + const query = await commands.getNstore(NSTORE_KEYS.settings); + + if (query.status === "ok") { + const settings: Settings = query.data ? JSON.parse(query.data) : null; + const isGranted = await isPermissionGranted(); + + return { ...settings, notification: isGranted }; + } else { + const initial: Settings = { + autoUpdate: false, + enhancedPrivacy: false, + notification: false, + zap: false, + nsfw: false, + }; + + return initial; + } + } + + static async setSettings(settings: Settings) { + const query = await commands.setNstore( + NSTORE_KEYS.settings, + JSON.stringify(settings), + ); + + if (query.status === "ok") { + return query.data; + } else { + throw new Error(query.error); + } + } + + static async getColumns() { + const query = await commands.getNstore(NSTORE_KEYS.columns); + + if (query.status === "ok") { + const columns: LumeColumn[] = query.data ? JSON.parse(query.data) : []; + + if (columns.length < 1) { + const systemPath = "resources/system_columns.json"; + const resourcePath = await resolveResource(systemPath); + const resourceFile = await readTextFile(resourcePath); + const systemColumns: LumeColumn[] = JSON.parse(resourceFile); + + return systemColumns; + } + + return columns; + } else { + return []; + } + } + + static async setColumns(columns: LumeColumn[]) { + const query = await commands.setNstore( + NSTORE_KEYS.columns, + JSON.stringify(columns), + ); + + if (query.status === "ok") { + return query.data; + } else { + throw new Error(query.error); + } + } + + static async getRelays() { + const query = await commands.getRelays(); + + if (query.status === "ok") { + return query.data; + } else { + throw new Error(query.error); + } + } + + static async connectRelay(url: string) { + const relayUrl = new URL(url); + + if (relayUrl.protocol === "wss:" || relayUrl.protocol === "ws:") { + const query = await commands.connectRelay(relayUrl.toString()); + + if (query.status === "ok") { + return query.data; + } else { + throw new Error(query.error); + } + } + } + + static async removeRelay(url: string) { + const relayUrl = new URL(url); + + if (relayUrl.protocol === "wss:" || relayUrl.protocol === "ws:") { + const query = await commands.removeRelay(relayUrl.toString()); + + if (query.status === "ok") { + return query.data; + } else { + throw new Error(query.error); + } + } + } +} diff --git a/packages/system/src/window.ts b/packages/system/src/window.ts new file mode 100644 index 00000000..e7de3957 --- /dev/null +++ b/packages/system/src/window.ts @@ -0,0 +1,140 @@ +import { NostrEvent } from "@lume/types"; +import { commands } from "./commands"; + +export class LumeWindow { + static async openEvent(event: NostrEvent) { + const eTags = event.tags.filter((tag) => tag[0] === "e" || tag[0] === "q"); + const root: string = + eTags.find((el) => el[3] === "root")?.[1] ?? eTags[0]?.[1]; + const reply: string = + eTags.find((el) => el[3] === "reply")?.[1] ?? eTags[1]?.[1]; + + const label = `event-${event.id}`; + const url = `/events/${root ?? reply ?? event.id}`; + + const query = await commands.openWindow(label, "Thread", url, 500, 800); + + if (query.status === "ok") { + return query.data; + } else { + throw new Error(query.error); + } + } + + static async openProfile(pubkey: string) { + const label = `user-${pubkey}`; + const query = await commands.openWindow( + label, + "Profile", + `/users/${pubkey}`, + 500, + 800, + ); + + if (query.status === "ok") { + return query.data; + } else { + throw new Error(query.error); + } + } + + static async openEditor(reply_to?: string, quote = false) { + let url: string; + + if (reply_to) { + url = `/editor?reply_to=${reply_to}"e=${quote}`; + } else { + url = "/editor"; + } + + const label = `editor-${reply_to ? reply_to : 0}`; + const query = await commands.openWindow(label, "Editor", url, 560, 340); + + if (query.status === "ok") { + return query.data; + } else { + throw new Error(query.error); + } + } + + static async openZap(id: string, pubkey: string) { + const nwc = await commands.loadNwc(); + + if (nwc.status === "ok") { + const status = nwc.data; + + if (!status) { + const label = "nwc"; + await commands.openWindow( + label, + "Nostr Wallet Connect", + "/nwc", + 400, + 600, + ); + } else { + const label = `zap-${id}`; + await commands.openWindow( + label, + "Zap", + `/zap/${id}?pubkey=${pubkey}`, + 400, + 500, + ); + } + } else { + throw new Error(nwc.error); + } + } + + static async openSettings() { + const label = "settings"; + const query = await commands.openWindow( + label, + "Settings", + "/settings", + 800, + 500, + ); + + if (query.status === "ok") { + return query.data; + } else { + throw new Error(query.error); + } + } + + static async openSearch() { + const label = "search"; + const query = await commands.openWindow( + label, + "Search", + "/search", + 400, + 600, + ); + + if (query.status === "ok") { + return query.data; + } else { + throw new Error(query.error); + } + } + + static async openActivity(account: string) { + const label = "activity"; + const query = await commands.openWindow( + label, + "Activity", + `/activity/${account}/texts`, + 400, + 600, + ); + + if (query.status === "ok") { + return query.data; + } else { + throw new Error(query.error); + } + } +} diff --git a/packages/ark/tailwind.config.js b/packages/system/tailwind.config.js similarity index 100% rename from packages/ark/tailwind.config.js rename to packages/system/tailwind.config.js diff --git a/packages/ark/tsconfig.json b/packages/system/tsconfig.json similarity index 100% rename from packages/ark/tsconfig.json rename to packages/system/tsconfig.json diff --git a/packages/types/index.d.ts b/packages/types/index.d.ts index 27092107..c8eccbcf 100644 --- a/packages/types/index.d.ts +++ b/packages/types/index.d.ts @@ -26,7 +26,7 @@ export enum Kind { // #TODO: Add all nostr kinds } -export interface Event { +export interface NostrEvent { id: string; pubkey: string; created_at: number; @@ -34,11 +34,10 @@ export interface Event { tags: string[][]; content: string; sig: string; - relay?: string; } -export interface EventWithReplies extends Event { - replies: Array; +export interface EventWithReplies extends NostrEvent { + replies: Array; } export interface Metadata { diff --git a/packages/ui/package.json b/packages/ui/package.json index 340f3155..36fa3cdd 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -4,43 +4,11 @@ "private": true, "main": "./src/index.ts", "dependencies": { - "@getalby/sdk": "^3.5.1", - "@lume/ark": "workspace:^", "@lume/icons": "workspace:^", "@lume/utils": "workspace:^", - "@radix-ui/react-accordion": "^1.1.2", - "@radix-ui/react-alert-dialog": "^1.0.5", - "@radix-ui/react-avatar": "^1.0.4", - "@radix-ui/react-collapsible": "^1.0.3", - "@radix-ui/react-dialog": "^1.0.5", - "@radix-ui/react-dropdown-menu": "^2.0.6", - "@radix-ui/react-hover-card": "^1.0.7", - "@radix-ui/react-popover": "^1.0.7", - "@radix-ui/react-tooltip": "^1.0.7", - "@tanstack/react-query": "^5.36.0", - "@tanstack/react-router": "^1.32.5", - "framer-motion": "^11.2.0", - "get-urls": "^12.1.0", - "media-chrome": "^3.2.2", - "minidenticons": "^4.2.1", - "nanoid": "^5.0.7", - "qrcode.react": "^3.1.0", - "re-resizable": "^6.9.16", "react": "^18.3.1", - "react-currency-input-field": "^3.8.0", "react-dom": "^18.3.1", - "react-hook-form": "^7.51.4", - "react-hotkeys-hook": "^4.5.0", - "react-i18next": "^14.1.1", - "react-snap-carousel": "^0.4.0", - "react-string-replace": "^1.1.1", - "slate": "^0.103.0", - "slate-react": "^0.102.0", - "sonner": "^1.4.41", - "string-strip-html": "^13.4.8", - "uqr": "^0.1.2", - "use-debounce": "^10.0.0", - "virtua": "^0.31.0" + "react-snap-carousel": "^0.4.0" }, "devDependencies": { "@lume/tailwindcss": "workspace:^", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 342b2bc1..1593058a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -57,12 +57,12 @@ importers: apps/desktop2: dependencies: - '@lume/ark': - specifier: workspace:^ - version: link:../../packages/ark '@lume/icons': specifier: workspace:^ version: link:../../packages/icons + '@lume/system': + specifier: workspace:^ + version: link:../../packages/system '@lume/ui': specifier: workspace:^ version: link:../../packages/ui @@ -240,7 +240,23 @@ importers: specifier: ^0.5.13 version: 0.5.13(tailwindcss@3.4.3) - packages/ark: + packages/icons: + dependencies: + react: + specifier: ^18.3.1 + version: 18.3.1 + devDependencies: + '@lume/tsconfig': + specifier: workspace:* + version: link:../tsconfig + '@types/react': + specifier: ^18.3.2 + version: 18.3.2 + typescript: + specifier: ^5.4.5 + version: 5.4.5 + + packages/system: dependencies: '@lume/utils': specifier: workspace:^ @@ -265,22 +281,6 @@ importers: specifier: ^5.4.5 version: 5.4.5 - packages/icons: - dependencies: - react: - specifier: ^18.3.1 - version: 18.3.1 - devDependencies: - '@lume/tsconfig': - specifier: workspace:* - version: link:../tsconfig - '@types/react': - specifier: ^18.3.2 - version: 18.3.2 - typescript: - specifier: ^5.4.5 - version: 5.4.5 - packages/tailwindcss: devDependencies: '@evilmartians/harmony': @@ -312,117 +312,21 @@ importers: packages/ui: dependencies: - '@getalby/sdk': - specifier: ^3.5.1 - version: 3.5.1(typescript@5.4.5) - '@lume/ark': - specifier: workspace:^ - version: link:../ark '@lume/icons': specifier: workspace:^ version: link:../icons '@lume/utils': specifier: workspace:^ version: link:../utils - '@radix-ui/react-accordion': - specifier: ^1.1.2 - version: 1.1.2(@types/react@18.3.2)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-alert-dialog': - specifier: ^1.0.5 - version: 1.0.5(@types/react@18.3.2)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-avatar': - specifier: ^1.0.4 - version: 1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-collapsible': - specifier: ^1.0.3 - version: 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-dialog': - specifier: ^1.0.5 - version: 1.0.5(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-dropdown-menu': - specifier: ^2.0.6 - version: 2.0.6(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-hover-card': - specifier: ^1.0.7 - version: 1.0.7(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-popover': - specifier: ^1.0.7 - version: 1.0.7(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-tooltip': - specifier: ^1.0.7 - version: 1.0.7(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.3.1)(react@18.3.1) - '@tanstack/react-query': - specifier: ^5.36.0 - version: 5.36.0(react@18.3.1) - '@tanstack/react-router': - specifier: ^1.32.5 - version: 1.32.5(react-dom@18.3.1)(react@18.3.1) - framer-motion: - specifier: ^11.2.0 - version: 11.2.0(react-dom@18.3.1)(react@18.3.1) - get-urls: - specifier: ^12.1.0 - version: 12.1.0 - media-chrome: - specifier: ^3.2.2 - version: 3.2.2 - minidenticons: - specifier: ^4.2.1 - version: 4.2.1 - nanoid: - specifier: ^5.0.7 - version: 5.0.7 - qrcode.react: - specifier: ^3.1.0 - version: 3.1.0(react@18.3.1) - re-resizable: - specifier: ^6.9.16 - version: 6.9.16(react-dom@18.3.1)(react@18.3.1) react: specifier: ^18.3.1 version: 18.3.1 - react-currency-input-field: - specifier: ^3.8.0 - version: 3.8.0(react@18.3.1) react-dom: specifier: ^18.3.1 version: 18.3.1(react@18.3.1) - react-hook-form: - specifier: ^7.51.4 - version: 7.51.4(react@18.3.1) - react-hotkeys-hook: - specifier: ^4.5.0 - version: 4.5.0(react-dom@18.3.1)(react@18.3.1) - react-i18next: - specifier: ^14.1.1 - version: 14.1.1(i18next@23.11.4)(react-dom@18.3.1)(react@18.3.1) react-snap-carousel: specifier: ^0.4.0 version: 0.4.0(react@18.3.1) - react-string-replace: - specifier: ^1.1.1 - version: 1.1.1 - slate: - specifier: ^0.103.0 - version: 0.103.0 - slate-react: - specifier: ^0.102.0 - version: 0.102.0(react-dom@18.3.1)(react@18.3.1)(slate@0.103.0) - sonner: - specifier: ^1.4.41 - version: 1.4.41(react-dom@18.3.1)(react@18.3.1) - string-strip-html: - specifier: ^13.4.8 - version: 13.4.8 - uqr: - specifier: ^0.1.2 - version: 0.1.2 - use-debounce: - specifier: ^10.0.0 - version: 10.0.0(react@18.3.1) - virtua: - specifier: ^0.31.0 - version: 0.31.0(react-dom@18.3.1)(react@18.3.1) devDependencies: '@lume/tailwindcss': specifier: workspace:^ @@ -1469,16 +1373,6 @@ packages: resolution: {integrity: sha512-7ncjjSpRSRKvjJEoru092iFiEoC89lz4oG4+SGg9hh7DI/5SXf+kE+dg+6Fv/bwiK/WJCo4Q2gvPZGRlCE5mcA==} dev: false - /@getalby/sdk@3.5.1(typescript@5.4.5): - resolution: {integrity: sha512-Qz9GgXMoVpupDLqbzA2CHpru+9yqijQrxeRN7CDfV6l39js/BGwin93MFTh7eFj2TsMo+i8JeM3BVn+SJn/iRg==} - engines: {node: '>=14'} - dependencies: - eventemitter3: 5.0.1 - nostr-tools: 1.17.0(typescript@5.4.5) - transitivePeerDependencies: - - typescript - dev: false - /@img/sharp-darwin-arm64@0.33.3: resolution: {integrity: sha512-FaNiGX1MrOuJ3hxuNzWgsT/mg5OHG/Izh59WW2mk1UwYHUwtfbhk5QNKYZgxf0pLOhx9ctGiGa2OykD71vOnSw==} engines: {glibc: '>=2.26', node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} @@ -1716,10 +1610,6 @@ packages: resolution: {integrity: sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==} dev: false - /@noble/ciphers@0.2.0: - resolution: {integrity: sha512-6YBxJDAapHSdd3bLDv6x2wRPwq4QFMUaB3HvljNBUTThDd12eSm7/3F+2lnfzx2jvM+S6Nsy0jEt9QbPqSwqRw==} - dev: false - /@noble/ciphers@0.5.3: resolution: {integrity: sha512-B0+6IIHiqEs3BPMT0hcRmHvEj2QHOLu+uwt+tqDDeVd0oyVzh7BPrDcPjRnV1PV/5LaknXJJQvOuRGR0zQJz+w==} dev: false @@ -1776,59 +1666,6 @@ packages: '@babel/runtime': 7.24.5 dev: false - /@radix-ui/react-accordion@1.1.2(@types/react@18.3.2)(react-dom@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-fDG7jcoNKVjSK6yfmuAs0EnPDro0WMXIhMtXdTBWqEioVW206ku+4Lw07e+13lUkFkpoEQ2PdeMIAGpdqEAmDg==} - 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.24.5 - '@radix-ui/primitive': 1.0.1 - '@radix-ui/react-collapsible': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-collection': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.2)(react@18.3.1) - '@radix-ui/react-context': 1.0.1(@types/react@18.3.2)(react@18.3.1) - '@radix-ui/react-direction': 1.0.1(@types/react@18.3.2)(react@18.3.1) - '@radix-ui/react-id': 1.0.1(@types/react@18.3.2)(react@18.3.1) - '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.3.2)(react@18.3.1) - '@types/react': 18.3.2 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - dev: false - - /@radix-ui/react-alert-dialog@1.0.5(@types/react@18.3.2)(react-dom@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-OrVIOcZL0tl6xibeuGt5/+UxoT2N27KCFOPjFyfXMnchxSHZ/OW7cCX2nGlIYJrbHK/fczPcFzAwvNBB6XBNMA==} - 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.24.5 - '@radix-ui/primitive': 1.0.1 - '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.2)(react@18.3.1) - '@radix-ui/react-context': 1.0.1(@types/react@18.3.2)(react@18.3.1) - '@radix-ui/react-dialog': 1.0.5(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-slot': 1.0.2(@types/react@18.3.2)(react@18.3.1) - '@types/react': 18.3.2 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - dev: false - /@radix-ui/react-arrow@1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.3.1)(react@18.3.1): resolution: {integrity: sha512-wSP+pHsB/jQRaL6voubsQ/ZlrGBHHrOjmBnr19hxYgtS0WvAFwZhK2WP/YY5yF9uKECCEEDGxuLxq1NBK51wFA==} peerDependencies: @@ -3203,12 +3040,6 @@ packages: resolution: {integrity: sha512-RvC8KMw5BCac1NvRRyaHgMMEtBaZ6wh0pyPTBu7izn4Sj/AX9Y4aXU5c7rX8PnM/knsuUpC1IeoBkANtxBypsQ==} dev: false - /@types/lodash-es@4.17.12: - resolution: {integrity: sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==} - dependencies: - '@types/lodash': 4.17.1 - dev: false - /@types/lodash@4.17.1: resolution: {integrity: sha512-X+2qazGS3jxLAIz5JDXDzglAF3KpijdhFxlf/V1+hEsOUc+HnWi81L/uv/EvGuV90WY+7mPGFCUDGfQC3Gj95Q==} dev: false @@ -3707,24 +3538,10 @@ packages: wrap-ansi: 7.0.0 dev: false - /clone-regexp@3.0.0: - resolution: {integrity: sha512-ujdnoq2Kxb8s3ItNBtnYeXdm07FcU0u8ARAT1lQ2YdMwQC+cdiXX8KoqMVuglztILivceTtp4ivqGSmEmhBUJw==} - engines: {node: '>=12'} - dependencies: - is-regexp: 3.1.0 - dev: false - /clsx@2.1.1: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} - /codsen-utils@1.6.4: - resolution: {integrity: sha512-PDyvQ5f2PValmqZZIJATimcokDt4JjIev8cKbZgEOoZm+U1IJDYuLeTcxZPQdep99R/X0RIlQ6ReQgPOVnPbNw==} - engines: {node: '>=14.18.0'} - dependencies: - rfdc: 1.3.1 - dev: false - /color-convert@1.9.3: resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} dependencies: @@ -3778,11 +3595,6 @@ packages: resolution: {integrity: sha512-rj8l8pD4bJ1nx+dAkMhV1xB5RuZEyVysfxJqB1pRchh1KVvwOv9b7CGB8ZfjTImVv2oF+sYMUkMZq6Na5Ftmbg==} dev: false - /convert-hrtime@5.0.0: - resolution: {integrity: sha512-lOETlkIeYSJWcbbcvjRKGxVMXJR+8+OQb/mTPbA4ObPMytYIsUbuOE0Jzy60hjARYszq1id0j8KgVhC+WGZVTg==} - engines: {node: '>=12'} - dev: false - /convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} @@ -4096,25 +3908,6 @@ packages: /fraction.js@4.3.7: resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} - /framer-motion@11.2.0(react-dom@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-LRfLVPEwtO9IXJCAsWvtj3XZxrdZDcTxNNkZEq30aQ8p7/wimfUkDy67TDWdtzPiyKDkqOHDhaQC6XVrQ4Fh7A==} - peerDependencies: - '@emotion/is-prop-valid': '*' - react: ^18.0.0 - react-dom: ^18.0.0 - peerDependenciesMeta: - '@emotion/is-prop-valid': - optional: true - react: - optional: true - react-dom: - optional: true - dependencies: - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - tslib: 2.6.2 - dev: false - /fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -4125,11 +3918,6 @@ packages: /function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} - /function-timeout@0.1.1: - resolution: {integrity: sha512-0NVVC0TaP7dSTvn1yMiy6d6Q8gifzbvQafO46RtLG/kHJUBNd+pVRGOBoK44wNBvtSPUJRfdVvkFdD3p0xvyZg==} - engines: {node: '>=14.16'} - dev: false - /gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} @@ -4154,17 +3942,6 @@ packages: engines: {node: '>=16'} dev: false - /get-urls@12.1.0: - resolution: {integrity: sha512-qHO+QmPiI1bEw0Y/m+WMAAx/UoEEXLZwEx0DVaKMtlHNrKbMeV960LryIpd+E2Ykb9XkVHmVtpbCsmul3GhR0g==} - engines: {node: '>=16'} - dependencies: - normalize-url: 8.0.1 - super-regex: 0.2.0 - url-regex-safe: 4.0.0 - transitivePeerDependencies: - - re2 - dev: false - /github-slugger@2.0.0: resolution: {integrity: sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==} dev: false @@ -4340,10 +4117,6 @@ packages: space-separated-tokens: 2.0.2 dev: false - /html-entities@2.5.2: - resolution: {integrity: sha512-K//PSRMQk4FZ78Kyau+mZurHn3FH0Vwr+H36eE0rPbeYkRRi9YxceYPhuN60UwWorxyKHhqoAJl2OFKa4BVtaA==} - dev: false - /html-escaper@3.0.3: resolution: {integrity: sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==} dev: false @@ -4393,11 +4166,6 @@ packages: loose-envify: 1.4.0 dev: false - /ip-regex@4.3.0: - resolution: {integrity: sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q==} - engines: {node: '>=8'} - dev: false - /is-arrayish@0.3.2: resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} requiresBuild: true @@ -4476,11 +4244,6 @@ packages: engines: {node: '>=0.10.0'} dev: false - /is-regexp@3.1.0: - resolution: {integrity: sha512-rbku49cWloU5bSMI+zaRaXdQHXnthP6DZ/vLnfdSKyL4zUzuWnomtOEiZZOd+ioQ+avFo/qau3KPTc7Fjy1uPA==} - engines: {node: '>=12'} - dev: false - /is-stream@3.0.0: resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -4606,10 +4369,6 @@ packages: p-locate: 5.0.0 dev: false - /lodash-es@4.17.21: - resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==} - dev: false - /lodash.castarray@4.4.0: resolution: {integrity: sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==} dev: true @@ -4808,10 +4567,6 @@ packages: '@types/mdast': 4.0.4 dev: false - /media-chrome@3.2.2: - resolution: {integrity: sha512-Fjf1rNxlZqVR5nj7a9ay+XpzeKVPMMBltK2XiMOTc5bomxAvyo3vJZ9JKzmAYVoiZ6sbDNWJJVSVf6b3laFwew==} - dev: false - /merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} dev: false @@ -5151,28 +4906,6 @@ packages: resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} engines: {node: '>=0.10.0'} - /normalize-url@8.0.1: - resolution: {integrity: sha512-IO9QvjUMWxPQQhs60oOu10CRkWCiZzSUkzbXGGV9pviYl1fXYcvkzQ5jV9z8Y6un8ARoVRl4EtC6v6jNqbaJ/w==} - engines: {node: '>=14.16'} - dev: false - - /nostr-tools@1.17.0(typescript@5.4.5): - resolution: {integrity: sha512-LZmR8GEWKZeElbFV5Xte75dOeE9EFUW/QLI1Ncn3JKn0kFddDKEfBbFN8Mu4TMs+L4HR/WTPha2l+PPuRnJcMw==} - peerDependencies: - typescript: '>=5.0.0' - peerDependenciesMeta: - typescript: - optional: true - dependencies: - '@noble/ciphers': 0.2.0 - '@noble/curves': 1.1.0 - '@noble/hashes': 1.3.1 - '@scure/base': 1.1.1 - '@scure/bip32': 1.3.1 - '@scure/bip39': 1.2.1 - typescript: 5.4.5 - dev: false - /nostr-tools@2.5.2(typescript@5.4.5): resolution: {integrity: sha512-Ls2FKh694eudBye6q89yJ5JhXjQle1MWp1yD2sBZ5j9M3IOBEW8ia9IED5W6daSAjlT/Z/pV77yTkdF45c1Rbg==} peerDependencies: @@ -5472,58 +5205,9 @@ packages: resolution: {integrity: sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==} dev: false - /qrcode.react@3.1.0(react@18.3.1): - resolution: {integrity: sha512-oyF+Urr3oAMUG/OiOuONL3HXM+53wvuH3mtIWQrYmsXoAq0DkvZp2RYUWFSMFtbdOpuS++9v+WAkzNVkMlNW6Q==} - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - dependencies: - react: 18.3.1 - dev: false - /queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} - /ranges-apply@7.0.16: - resolution: {integrity: sha512-4rGJHOyA7qatiMDg3vcETkc/TVBPU86/xZRTXff6o7a2neYLmj0EXUUAlhLVuiWAzTPHDPHOQxtk8EDrIF4ohg==} - engines: {node: '>=14.18.0'} - dependencies: - ranges-merge: 9.0.15 - tiny-invariant: 1.3.3 - dev: false - - /ranges-merge@9.0.15: - resolution: {integrity: sha512-hvt4hx0FKIaVfjd1oKx0poL57ljxdL2KHC6bXBrAdsx2iCsH+x7nO/5J0k2veM/isnOcFZKp0ZKkiCjCtzy74Q==} - engines: {node: '>=14.18.0'} - dependencies: - ranges-push: 7.0.15 - ranges-sort: 6.0.11 - dev: false - - /ranges-push@7.0.15: - resolution: {integrity: sha512-gXpBYQ5Umf3uG6jkJnw5ddok2Xfo5p22rAJBLrqzNKa7qkj3q5AOCoxfRPXEHUVaJutfXc9K9eGXdIzdyQKPkw==} - engines: {node: '>=14.18.0'} - dependencies: - codsen-utils: 1.6.4 - ranges-sort: 6.0.11 - string-collapse-leading-whitespace: 7.0.7 - string-trim-spaces-only: 5.0.10 - dev: false - - /ranges-sort@6.0.11: - resolution: {integrity: sha512-fhNEG0vGi7bESitNNqNBAfYPdl2efB+1paFlI8BQDCNkruERKuuhG8LkQClDIVqUJLkrmKuOSPQ3xZHqVnVo3Q==} - engines: {node: '>=14.18.0'} - dev: false - - /re-resizable@6.9.16(react-dom@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-D9+ofwgPQRC6PL6cwavCZO9MUR8TKKxV1nHjbutSdNaFHK9v5k8m6DcESMXrw1+mRJn7fBHJRhZpa7EQ1ZWEEA==} - peerDependencies: - react: ^16.13.1 || ^17.0.0 || ^18.0.0 - react-dom: ^16.13.1 || ^17.0.0 || ^18.0.0 - dependencies: - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - dev: false - /react-currency-input-field@3.8.0(react@18.3.1): resolution: {integrity: sha512-DKSIjacrvgUDOpuB16b+OVDvp5pbCt+s+RHJgpRZCHNhzg1yBpRUoy4fbnXpeOj0kdbwf5BaXCr2mAtxEujfhg==} peerDependencies: @@ -5818,10 +5502,6 @@ packages: resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - /rfdc@1.3.1: - resolution: {integrity: sha512-r5a3l5HzYlIC68TpmYKlxWjmOP6wiPJ1vWv2HeLhNsRZMrCkxeqxiHlQ21oXmQ4F3SiryXBHhAD7JZqvOJjFmg==} - dev: false - /rollup@4.17.2: resolution: {integrity: sha512-/9ClTJPByC0U4zNLowV1tMBe8yMEAxewtR3cUNX5BoEpGH3dQEWpJLr6CLp0fPdYRF/fzVOgvDb1zXuakwF5kQ==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} @@ -6012,37 +5692,6 @@ packages: engines: {node: '>=18'} dev: false - /string-collapse-leading-whitespace@7.0.7: - resolution: {integrity: sha512-jF9eynJoE6ezTCdYI8Qb02/ij/DlU9ItG93Dty4SWfJeLFrotOr+wH9IRiWHTqO3mjCyqBWEiU3uSTIbxYbAEQ==} - engines: {node: '>=14.18.0'} - dev: false - - /string-left-right@6.0.17: - resolution: {integrity: sha512-nuyIV4D4ivnwT64E0TudmCRg52NfkumuEUilyoOrHb/Z2wEOF5I+9SI6P+veFKqWKZfGpAs6OqKe4nAjujARyw==} - engines: {node: '>=14.18.0'} - dependencies: - codsen-utils: 1.6.4 - rfdc: 1.3.1 - dev: false - - /string-strip-html@13.4.8: - resolution: {integrity: sha512-vlcRAtx5DN6zXGUx3EYGFg0/JOQWM65mqLgDaBHviQPP+ovUFzqZ30iQ+674JHWr9wNgnzFGxx9TGipPZMnZXg==} - engines: {node: '>=14.18.0'} - dependencies: - '@types/lodash-es': 4.17.12 - codsen-utils: 1.6.4 - html-entities: 2.5.2 - lodash-es: 4.17.21 - ranges-apply: 7.0.16 - ranges-push: 7.0.15 - string-left-right: 6.0.17 - dev: false - - /string-trim-spaces-only@5.0.10: - resolution: {integrity: sha512-MhmjE5jNqb1Ylo+BARPRlsdChGLrnPpAUWrT1VOxo9WhWwKVUU6CbZTfjwKaQPYTGS/wsX/4Zek88FM2rEb5iA==} - engines: {node: '>=14.18.0'} - dev: false - /string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -6115,15 +5764,6 @@ packages: pirates: 4.0.6 ts-interface-checker: 0.1.13 - /super-regex@0.2.0: - resolution: {integrity: sha512-WZzIx3rC1CvbMDloLsVw0lkZVKJWbrkJ0k1ghKFmcnPrW1+jWbgTkTEWVtD9lMdmI4jZEz40+naBxl1dCUhXXw==} - engines: {node: '>=14.16'} - dependencies: - clone-regexp: 3.0.0 - function-timeout: 0.1.1 - time-span: 5.1.0 - dev: false - /supports-color@5.5.0: resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} engines: {node: '>=4'} @@ -6194,13 +5834,6 @@ packages: dependencies: any-promise: 1.3.0 - /time-span@5.1.0: - resolution: {integrity: sha512-75voc/9G4rDIJleOo4jPvN4/YC4GRZrY8yy1uU4lwrB3XEQbWve8zXoO5No4eFrGcTAMYyoY67p8jRQdtA1HbA==} - engines: {node: '>=12'} - dependencies: - convert-hrtime: 5.0.0 - dev: false - /tiny-invariant@1.3.1: resolution: {integrity: sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw==} dev: false @@ -6211,11 +5844,6 @@ packages: /tiny-warning@1.0.3: resolution: {integrity: sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==} - /tlds@1.252.0: - resolution: {integrity: sha512-GA16+8HXvqtfEnw/DTcwB0UU354QE1n3+wh08oFjr6Znl7ZLAeUgYzCcK+/CCrOyE0vnHR8/pu3XXG3vDijXpQ==} - hasBin: true - dev: false - /to-fast-properties@2.0.0: resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} engines: {node: '>=4'} @@ -6459,23 +6087,6 @@ packages: escalade: 3.1.2 picocolors: 1.0.1 - /uqr@0.1.2: - resolution: {integrity: sha512-MJu7ypHq6QasgF5YRTjqscSzQp/W11zoUk6kvmlH+fmWEs63Y0Eib13hYFwAzagRJcVY8WVnlV+eBDUGMJ5IbA==} - dev: false - - /url-regex-safe@4.0.0: - resolution: {integrity: sha512-BrnFCWKNFrFnRzKD66NtJqQepfJrUHNPvPxE5y5NSAhXBb4OlobQjt7907Jm4ItPiXaeX+dDWMkcnOd4jR9N8A==} - engines: {node: '>= 14'} - peerDependencies: - re2: ^1.20.1 - peerDependenciesMeta: - re2: - optional: true - dependencies: - ip-regex: 4.3.0 - tlds: 1.252.0 - dev: false - /use-callback-ref@1.3.2(@types/react@18.3.2)(react@18.3.1): resolution: {integrity: sha512-elOQwe6Q8gqZgDA8mrh44qRTQqpIHDcZ3hXTLjBe1i4ph8XpNJnO+aQf3NaG+lriLopI4HMx9VjQLfPQ6vhnoA==} engines: {node: '>=10'} diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index b933532a..8af2f0d6 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -2,6 +2,12 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "Inflector" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" + [[package]] name = "addr2line" version = "0.21.0" @@ -2481,6 +2487,12 @@ dependencies = [ "serde", ] +[[package]] +name = "indoc" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5" + [[package]] name = "infer" version = "0.15.0" @@ -2883,6 +2895,7 @@ dependencies = [ "rand 0.8.5", "serde", "serde_json", + "specta", "tauri", "tauri-build", "tauri-plugin-clipboard-manager", @@ -2897,6 +2910,7 @@ dependencies = [ "tauri-plugin-updater", "tauri-plugin-upload", "tauri-plugin-window-state", + "tauri-specta", "tokio", "webpage", ] @@ -3698,6 +3712,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + [[package]] name = "pathdiff" version = "0.2.1" @@ -4960,6 +4980,31 @@ dependencies = [ "system-deps", ] +[[package]] +name = "specta" +version = "2.0.0-rc.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3624a07cbde326fdf1ec37cbd39d06a224660fa0199b7db7316f2349583df981" +dependencies = [ + "once_cell", + "paste", + "serde", + "specta-macros", + "thiserror", +] + +[[package]] +name = "specta-macros" +version = "2.0.0-rc.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef33e9678ae36993fcbfc46aa29568ef10d32fd54428808759c6a450998c43ec" +dependencies = [ + "Inflector", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "spin" version = "0.9.8" @@ -5209,6 +5254,7 @@ dependencies = [ "serde_json", "serde_repr", "serialize-to-javascript", + "specta", "state", "swift-rs", "tauri-build", @@ -5574,6 +5620,34 @@ dependencies = [ "wry", ] +[[package]] +name = "tauri-specta" +version = "2.0.0-rc.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "856a2bbbbd4d39ae2c1e6d22aec50623596708ff8f7e4c598123dbc5165f5f76" +dependencies = [ + "heck 0.5.0", + "indoc", + "serde", + "serde_json", + "specta", + "tauri", + "tauri-specta-macros", + "thiserror", +] + +[[package]] +name = "tauri-specta-macros" +version = "2.0.0-rc.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6f9e90bf2012877e2c4029a1bf756277183e9c7c77b850ef965711553998012" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.63", +] + [[package]] name = "tauri-utils" version = "2.0.0-beta.15" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 543c1a92..2f693ef1 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -37,6 +37,8 @@ tauri-plugin-decorum = "0.1.0" webpage = { version = "2.0", features = ["serde"] } keyring = "2" keyring-search = "0.2.0" +specta = "=2.0.0-rc.12" +tauri-specta = { version = "=2.0.0-rc.10", features = ["typescript"] } [target.'cfg(target_os = "macos")'.dependencies] cocoa = "0.25.0" diff --git a/src-tauri/capabilities/main.json b/src-tauri/capabilities/main.json index 314127ec..abfc60b8 100644 --- a/src-tauri/capabilities/main.json +++ b/src-tauri/capabilities/main.json @@ -1,82 +1,83 @@ { - "$schema": "../gen/schemas/desktop-schema.json", - "identifier": "desktop-capability", - "description": "Capability for the desktop", - "platforms": ["linux", "macOS", "windows"], - "windows": [ - "main", - "splash", - "settings", - "search", - "nwc", - "activity", - "zap-*", - "event-*", - "user-*", - "editor-*", - "column-*" - ], - "permissions": [ - "path:default", - "event:default", - "window:default", - "app:default", - "resources:default", - "menu:default", - "tray:default", - "notification:allow-is-permission-granted", - "notification:allow-request-permission", - "notification:default", - "os:allow-locale", - "os:allow-platform", - "updater:default", - "updater:allow-check", - "updater:allow-download-and-install", - "window:allow-start-dragging", - "window:allow-create", - "window:allow-close", - "window:allow-set-focus", - "window:allow-center", - "window:allow-minimize", - "window:allow-maximize", - "window:allow-set-size", - "window:allow-set-focus", - "window:allow-start-dragging", - "decorum:allow-show-snap-overlay", - "clipboard-manager:allow-write-text", - "clipboard-manager:allow-read-text", - "webview:allow-create-webview-window", - "webview:allow-create-webview", - "webview:allow-set-webview-size", - "webview:allow-set-webview-position", - "webview:allow-webview-close", - "dialog:allow-open", - "dialog:allow-ask", - "dialog:allow-message", - "process:allow-restart", - "fs:allow-read-file", - "shell:allow-open", - { - "identifier": "http:default", - "allow": [ - { - "url": "http://**/" - }, - { - "url": "https://**/" - } - ] - }, - { - "identifier": "fs:allow-read-text-file", - "allow": [ - { - "path": "$RESOURCE/locales/*" - }, - { - "path": "$RESOURCE/resources/*" - } - ] - } - ] + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "desktop-capability", + "description": "Capability for the desktop", + "platforms": ["linux", "macOS", "windows"], + "windows": [ + "main", + "splash", + "settings", + "search", + "nwc", + "activity", + "zap-*", + "event-*", + "user-*", + "editor-*", + "column-*" + ], + "permissions": [ + "path:default", + "event:default", + "window:default", + "app:default", + "resources:default", + "menu:default", + "tray:default", + "notification:allow-is-permission-granted", + "notification:allow-request-permission", + "notification:default", + "os:allow-locale", + "os:allow-platform", + "os:allow-os-type", + "updater:default", + "updater:allow-check", + "updater:allow-download-and-install", + "window:allow-start-dragging", + "window:allow-create", + "window:allow-close", + "window:allow-set-focus", + "window:allow-center", + "window:allow-minimize", + "window:allow-maximize", + "window:allow-set-size", + "window:allow-set-focus", + "window:allow-start-dragging", + "decorum:allow-show-snap-overlay", + "clipboard-manager:allow-write-text", + "clipboard-manager:allow-read-text", + "webview:allow-create-webview-window", + "webview:allow-create-webview", + "webview:allow-set-webview-size", + "webview:allow-set-webview-position", + "webview:allow-webview-close", + "dialog:allow-open", + "dialog:allow-ask", + "dialog:allow-message", + "process:allow-restart", + "fs:allow-read-file", + "shell:allow-open", + { + "identifier": "http:default", + "allow": [ + { + "url": "http://**/" + }, + { + "url": "https://**/" + } + ] + }, + { + "identifier": "fs:allow-read-text-file", + "allow": [ + { + "path": "$RESOURCE/locales/*" + }, + { + "path": "$RESOURCE/resources/*" + } + ] + } + ] } diff --git a/src-tauri/gen/schemas/capabilities.json b/src-tauri/gen/schemas/capabilities.json index e99e5d0e..8a37271a 100644 --- a/src-tauri/gen/schemas/capabilities.json +++ b/src-tauri/gen/schemas/capabilities.json @@ -1 +1 @@ -{"desktop-capability":{"identifier":"desktop-capability","description":"Capability for the desktop","local":true,"windows":["main","splash","settings","search","nwc","activity","zap-*","event-*","user-*","editor-*","column-*"],"permissions":["path:default","event:default","window:default","app:default","resources:default","menu:default","tray:default","notification:allow-is-permission-granted","notification:allow-request-permission","notification:default","os:allow-locale","os:allow-platform","updater:default","updater:allow-check","updater:allow-download-and-install","window:allow-start-dragging","window:allow-create","window:allow-close","window:allow-set-focus","window:allow-center","window:allow-minimize","window:allow-maximize","window:allow-set-size","window:allow-set-focus","window:allow-start-dragging","decorum:allow-show-snap-overlay","clipboard-manager:allow-write-text","clipboard-manager:allow-read-text","webview:allow-create-webview-window","webview:allow-create-webview","webview:allow-set-webview-size","webview:allow-set-webview-position","webview:allow-webview-close","dialog:allow-open","dialog:allow-ask","dialog:allow-message","process:allow-restart","fs:allow-read-file","shell:allow-open",{"identifier":"http:default","allow":[{"url":"http://**/"},{"url":"https://**/"}]},{"identifier":"fs:allow-read-text-file","allow":[{"path":"$RESOURCE/locales/*"},{"path":"$RESOURCE/resources/*"}]}],"platforms":["linux","macOS","windows"]}} \ No newline at end of file +{"desktop-capability":{"identifier":"desktop-capability","description":"Capability for the desktop","local":true,"windows":["main","splash","settings","search","nwc","activity","zap-*","event-*","user-*","editor-*","column-*"],"permissions":["path:default","event:default","window:default","app:default","resources:default","menu:default","tray:default","notification:allow-is-permission-granted","notification:allow-request-permission","notification:default","os:allow-locale","os:allow-platform","os:allow-os-type","updater:default","updater:allow-check","updater:allow-download-and-install","window:allow-start-dragging","window:allow-create","window:allow-close","window:allow-set-focus","window:allow-center","window:allow-minimize","window:allow-maximize","window:allow-set-size","window:allow-set-focus","window:allow-start-dragging","decorum:allow-show-snap-overlay","clipboard-manager:allow-write-text","clipboard-manager:allow-read-text","webview:allow-create-webview-window","webview:allow-create-webview","webview:allow-set-webview-size","webview:allow-set-webview-position","webview:allow-webview-close","dialog:allow-open","dialog:allow-ask","dialog:allow-message","process:allow-restart","fs:allow-read-file","shell:allow-open",{"identifier":"http:default","allow":[{"url":"http://**/"},{"url":"https://**/"}]},{"identifier":"fs:allow-read-text-file","allow":[{"path":"$RESOURCE/locales/*"},{"path":"$RESOURCE/resources/*"}]}],"platforms":["linux","macOS","windows"]}} \ No newline at end of file diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 99001bdc..5b07a021 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -1,3 +1,2 @@ pub mod folder; -pub mod opg; pub mod window; diff --git a/src-tauri/src/commands/folder.rs b/src-tauri/src/commands/folder.rs index 2bea7d0b..94b13cff 100644 --- a/src-tauri/src/commands/folder.rs +++ b/src-tauri/src/commands/folder.rs @@ -1,6 +1,7 @@ use std::process::Command; #[tauri::command] +#[specta::specta] pub async fn show_in_folder(path: String) { #[cfg(target_os = "windows")] { diff --git a/src-tauri/src/commands/opg.rs b/src-tauri/src/commands/opg.rs deleted file mode 100644 index 050960db..00000000 --- a/src-tauri/src/commands/opg.rs +++ /dev/null @@ -1,16 +0,0 @@ -use std::time::Duration; -use webpage::{Opengraph, Webpage, WebpageOptions}; - -#[tauri::command] -pub fn fetch_opg(url: String) -> Result { - let mut options = WebpageOptions::default(); - options.allow_insecure = true; - options.max_redirections = 2; - options.timeout = Duration::from_secs(10); - - if let Ok(data) = Webpage::from_url(&url, options) { - Ok(data.html.opengraph) - } else { - Err("Get open graph failed".into()) - } -} diff --git a/src-tauri/src/commands/window.rs b/src-tauri/src/commands/window.rs index 34d0158d..91ae3524 100644 --- a/src-tauri/src/commands/window.rs +++ b/src-tauri/src/commands/window.rs @@ -10,6 +10,7 @@ use tauri::{LogicalPosition, LogicalSize, Manager, WebviewUrl}; use tauri_plugin_decorum::WebviewWindowExt; #[tauri::command] +#[specta::specta] pub fn create_column( label: &str, x: f32, @@ -45,6 +46,7 @@ pub fn create_column( } #[tauri::command] +#[specta::specta] pub fn close_column(label: &str, app_handle: tauri::AppHandle) -> Result { match app_handle.get_webview(label) { Some(webview) => { @@ -59,6 +61,7 @@ pub fn close_column(label: &str, app_handle: tauri::AppHandle) -> Result) -> Result { let client = &state.client; let event_id: Option = match Nip19::from_bech32(id) { @@ -49,7 +50,8 @@ pub async fn get_event(id: &str, state: State<'_, Nostr>) -> Result) -> Result, String> { +#[specta::specta] +pub async fn get_replies(id: &str, state: State<'_, Nostr>) -> Result, String> { let client = &state.client; match EventId::from_hex(id) { @@ -57,7 +59,7 @@ pub async fn get_thread(id: &str, state: State<'_, Nostr>) -> Result, let filter = Filter::new().kinds(vec![Kind::TextNote]).event(event_id); match client.get_events_of(vec![filter], None).await { - Ok(events) => Ok(events), + Ok(events) => Ok(events.into_iter().map(|ev| ev.as_json()).collect()), Err(err) => Err(err.to_string()), } } @@ -66,12 +68,12 @@ pub async fn get_thread(id: &str, state: State<'_, Nostr>) -> Result, } #[tauri::command] +#[specta::specta] pub async fn get_events_by( public_key: &str, - limit: usize, as_of: Option<&str>, state: State<'_, Nostr>, -) -> Result, String> { +) -> Result, String> { let client = &state.client; match PublicKey::from_str(public_key) { @@ -83,11 +85,11 @@ pub async fn get_events_by( let filter = Filter::new() .kinds(vec![Kind::TextNote, Kind::Repost]) .author(author) - .limit(limit) + .limit(20) .until(until); match client.get_events_of(vec![filter], None).await { - Ok(events) => Ok(events), + Ok(events) => Ok(events.into_iter().map(|ev| ev.as_json()).collect()), Err(err) => Err(err.to_string()), } } @@ -96,12 +98,12 @@ pub async fn get_events_by( } #[tauri::command] +#[specta::specta] pub async fn get_local_events( pubkeys: Vec, - limit: usize, until: Option<&str>, state: State<'_, Nostr>, -) -> Result, String> { +) -> Result, String> { let client = &state.client; let as_of = match until { Some(until) => Timestamp::from_str(until).unwrap(), @@ -119,7 +121,7 @@ pub async fn get_local_events( .collect(); let filter = Filter::new() .kinds(vec![Kind::TextNote, Kind::Repost]) - .limit(limit) + .limit(20) .authors(authors) .until(as_of); @@ -127,17 +129,17 @@ pub async fn get_local_events( .get_events_of(vec![filter], Some(Duration::from_secs(8))) .await { - Ok(events) => Ok(events), + Ok(events) => Ok(events.into_iter().map(|ev| ev.as_json()).collect()), Err(err) => Err(err.to_string()), } } #[tauri::command] +#[specta::specta] pub async fn get_global_events( - limit: usize, until: Option<&str>, state: State<'_, Nostr>, -) -> Result, String> { +) -> Result, String> { let client = &state.client; let as_of = match until { Some(until) => Timestamp::from_str(until).unwrap(), @@ -146,25 +148,25 @@ pub async fn get_global_events( let filter = Filter::new() .kinds(vec![Kind::TextNote, Kind::Repost]) - .limit(limit) + .limit(20) .until(as_of); match client .get_events_of(vec![filter], Some(Duration::from_secs(8))) .await { - Ok(events) => Ok(events), + Ok(events) => Ok(events.into_iter().map(|ev| ev.as_json()).collect()), Err(err) => Err(err.to_string()), } } #[tauri::command] +#[specta::specta] pub async fn get_hashtag_events( hashtags: Vec<&str>, - limit: usize, until: Option<&str>, state: State<'_, Nostr>, -) -> Result, String> { +) -> Result, String> { let client = &state.client; let as_of = match until { Some(until) => Timestamp::from_str(until).unwrap(), @@ -172,45 +174,18 @@ pub async fn get_hashtag_events( }; let filter = Filter::new() .kinds(vec![Kind::TextNote, Kind::Repost]) - .limit(limit) + .limit(20) .until(as_of) .hashtags(hashtags); match client.get_events_of(vec![filter], None).await { - Ok(events) => Ok(events), - Err(err) => Err(err.to_string()), - } -} - -#[tauri::command] -pub async fn get_group_events( - list: Vec<&str>, - limit: usize, - until: Option<&str>, - state: State<'_, Nostr>, -) -> Result, String> { - let client = &state.client; - let as_of = match until { - Some(until) => Timestamp::from_str(until).unwrap(), - None => Timestamp::now(), - }; - let authors: Vec = list - .into_iter() - .map(|hex| PublicKey::from_hex(hex).unwrap()) - .collect(); - let filter = Filter::new() - .kinds(vec![Kind::TextNote, Kind::Repost]) - .limit(limit) - .until(as_of) - .authors(authors); - - match client.get_events_of(vec![filter], None).await { - Ok(events) => Ok(events), + Ok(events) => Ok(events.into_iter().map(|ev| ev.as_json()).collect()), Err(err) => Err(err.to_string()), } } #[tauri::command] +#[specta::specta] pub async fn publish( content: &str, tags: Vec>, @@ -226,12 +201,13 @@ pub async fn publish( } #[tauri::command] -pub async fn repost(raw: &str, state: State<'_, Nostr>) -> Result { +#[specta::specta] +pub async fn repost(raw: &str, state: State<'_, Nostr>) -> Result { let client = &state.client; let event = Event::from_json(raw).unwrap(); match client.repost(&event, None).await { - Ok(event_id) => Ok(event_id), + Ok(event_id) => Ok(event_id.to_string()), Err(err) => Err(err.to_string()), } } diff --git a/src-tauri/src/nostr/keys.rs b/src-tauri/src/nostr/keys.rs index 7650e8c3..02518aa1 100644 --- a/src-tauri/src/nostr/keys.rs +++ b/src-tauri/src/nostr/keys.rs @@ -2,17 +2,20 @@ use crate::Nostr; use keyring::Entry; use keyring_search::{Limit, List, Search}; use nostr_sdk::prelude::*; +use serde::Serialize; +use specta::Type; use std::str::FromStr; use std::time::Duration; use tauri::State; -#[derive(serde::Serialize)] +#[derive(Serialize, Type)] pub struct Account { npub: String, nsec: String, } #[tauri::command] +#[specta::specta] pub fn get_accounts() -> Result { let search = Search::new().unwrap(); let results = search.by("Account", "nostr_secret"); @@ -24,6 +27,7 @@ pub fn get_accounts() -> Result { } #[tauri::command] +#[specta::specta] pub fn create_account() -> Result { let keys = Keys::generate(); let public_key = keys.public_key(); @@ -38,6 +42,7 @@ pub fn create_account() -> Result { } #[tauri::command] +#[specta::specta] pub async fn save_account( nsec: &str, password: &str, @@ -80,6 +85,7 @@ pub async fn save_account( } #[tauri::command] +#[specta::specta] pub async fn load_account(npub: &str, state: State<'_, Nostr>) -> Result { let client = &state.client; let keyring = Entry::new(&npub, "nostr_secret").unwrap(); @@ -165,6 +171,7 @@ pub async fn load_account(npub: &str, state: State<'_, Nostr>) -> Result Result { let keyring = Entry::new(npub, "nostr_secret").unwrap(); @@ -221,6 +229,7 @@ pub fn get_encrypted_key(npub: &str, password: &str) -> Result { } #[tauri::command] +#[specta::specta] 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); @@ -229,6 +238,7 @@ pub fn event_to_bech32(id: &str, relays: Vec) -> Result { } #[tauri::command] +#[specta::specta] pub fn user_to_bech32(key: &str, relays: Vec) -> Result { let pubkey = PublicKey::from_str(key).unwrap(); let profile = Nip19Profile::new(pubkey, relays).unwrap(); @@ -237,6 +247,7 @@ pub fn user_to_bech32(key: &str, relays: Vec) -> Result { } #[tauri::command] +#[specta::specta] pub fn to_npub(hex: &str) -> Result { let public_key = PublicKey::from_str(hex).unwrap(); let npub = Nip19::Pubkey(public_key); @@ -245,6 +256,7 @@ pub fn to_npub(hex: &str) -> Result { } #[tauri::command] +#[specta::specta] pub async fn verify_nip05(key: &str, nip05: &str) -> Result { match PublicKey::from_str(key) { Ok(public_key) => { diff --git a/src-tauri/src/nostr/metadata.rs b/src-tauri/src/nostr/metadata.rs index 3718f0c5..e94eb036 100644 --- a/src-tauri/src/nostr/metadata.rs +++ b/src-tauri/src/nostr/metadata.rs @@ -6,6 +6,7 @@ use tauri::{Manager, State}; use url::Url; #[tauri::command] +#[specta::specta] pub fn run_notification(accounts: Vec, app: tauri::AppHandle) -> Result<(), ()> { tauri::async_runtime::spawn(async move { let window = app.get_window("main").unwrap(); @@ -49,11 +50,12 @@ pub fn run_notification(accounts: Vec, app: tauri::AppHandle) -> Result< } #[tauri::command] +#[specta::specta] pub async fn get_activities( account: &str, kind: &str, state: State<'_, Nostr>, -) -> Result, String> { +) -> Result, String> { let client = &state.client; if let Ok(pubkey) = PublicKey::from_str(account) { @@ -65,7 +67,7 @@ pub async fn get_activities( .until(Timestamp::now()); match client.get_events_of(vec![filter], None).await { - Ok(events) => Ok(events), + Ok(events) => Ok(events.into_iter().map(|ev| ev.as_json()).collect()), Err(err) => Err(err.to_string()), } } else { @@ -77,6 +79,7 @@ pub async fn get_activities( } #[tauri::command] +#[specta::specta] pub async fn friend_to_friend(npub: &str, state: State<'_, Nostr>) -> Result { let client = &state.client; @@ -114,7 +117,8 @@ pub async fn friend_to_friend(npub: &str, state: State<'_, Nostr>) -> Result) -> Result { +#[specta::specta] +pub async fn get_current_user_profile(state: State<'_, Nostr>) -> Result { let client = &state.client; let signer = client.signer().await.unwrap(); let public_key = signer.public_key().await.unwrap(); @@ -130,7 +134,7 @@ pub async fn get_current_user_profile(state: State<'_, Nostr>) -> Result { if let Some(event) = events.first() { if let Ok(metadata) = Metadata::from_json(&event.content) { - Ok(metadata) + Ok(metadata.as_json()) } else { Err("Parse metadata failed".into()) } @@ -143,7 +147,8 @@ pub async fn get_current_user_profile(state: State<'_, Nostr>) -> Result) -> Result { +#[specta::specta] +pub async fn get_profile(id: &str, state: State<'_, Nostr>) -> Result { let client = &state.client; let public_key: Option = match Nip19::from_bech32(id) { Ok(val) => match val { @@ -166,13 +171,13 @@ pub async fn get_profile(id: &str, state: State<'_, Nostr>) -> Result) -> Result, state: State<'_, Nostr>) -> Result { let client = &state.client; let contact_list: Vec = pubkeys @@ -197,6 +203,7 @@ pub async fn set_contact_list(pubkeys: Vec<&str>, state: State<'_, Nostr>) -> Re } #[tauri::command] +#[specta::specta] pub async fn get_contact_list(state: State<'_, Nostr>) -> Result, String> { let client = &state.client; @@ -218,6 +225,7 @@ pub async fn get_contact_list(state: State<'_, Nostr>) -> Result, St } #[tauri::command] +#[specta::specta] pub async fn create_profile( name: &str, display_name: &str, @@ -228,7 +236,7 @@ pub async fn create_profile( lud16: &str, website: &str, state: State<'_, Nostr>, -) -> Result { +) -> Result { let client = &state.client; let mut metadata = Metadata::new() .name(name) @@ -250,68 +258,71 @@ pub async fn create_profile( } if let Ok(event_id) = client.set_metadata(&metadata).await { - Ok(event_id) + Ok(event_id.to_string()) } else { Err("Create profile failed".into()) } } #[tauri::command] +#[specta::specta] pub async fn follow( id: &str, alias: Option<&str>, state: State<'_, Nostr>, -) -> Result { +) -> Result { let client = &state.client; let public_key = PublicKey::from_str(id).unwrap(); - let contact = Contact::new(public_key, None, alias); + let contact = Contact::new(public_key, None, alias); // #TODO: Add relay_url let contact_list = client.get_contact_list(Some(Duration::from_secs(10))).await; - if let Ok(mut old_list) = contact_list { - old_list.push(contact); - let new_list = old_list.into_iter(); + match contact_list { + Ok(mut old_list) => { + old_list.push(contact); + let new_list = old_list.into_iter(); - if let Ok(event_id) = client.set_contact_list(new_list).await { - Ok(event_id) - } else { - Err("Follow failed".into()) + match client.set_contact_list(new_list).await { + Ok(event_id) => Ok(event_id.to_string()), + Err(err) => Err(err.to_string()), + } } - } else { - Err("Follow failed".into()) + Err(err) => Err(err.to_string()), } } #[tauri::command] -pub async fn unfollow(id: &str, state: State<'_, Nostr>) -> Result { +#[specta::specta] +pub async fn unfollow(id: &str, state: State<'_, Nostr>) -> Result { let client = &state.client; let public_key = PublicKey::from_str(id).unwrap(); let contact_list = client.get_contact_list(Some(Duration::from_secs(10))).await; - if let Ok(mut old_list) = contact_list { - let index = old_list - .iter() - .position(|x| x.public_key == public_key) - .unwrap(); - old_list.remove(index); + match contact_list { + Ok(mut old_list) => { + let index = old_list + .iter() + .position(|x| x.public_key == public_key) + .unwrap(); + old_list.remove(index); - let new_list = old_list.into_iter(); + let new_list = old_list.into_iter(); - if let Ok(event_id) = client.set_contact_list(new_list).await { - Ok(event_id) - } else { - Err("Follow failed".into()) + match client.set_contact_list(new_list).await { + Ok(event_id) => Ok(event_id.to_string()), + Err(err) => Err(err.to_string()), + } } - } else { - Err("Follow failed".into()) + Err(err) => Err(err.to_string()), } } #[tauri::command] +#[specta::specta] pub async fn set_nstore( key: &str, content: &str, state: State<'_, Nostr>, -) -> Result { +) -> Result { let client = &state.client; match client.signer().await { @@ -323,7 +334,7 @@ pub async fn set_nstore( let builder = EventBuilder::new(Kind::ApplicationSpecificData, encrypted, vec![tag]); match client.send_event_builder(builder).await { - Ok(event_id) => Ok(event_id), + Ok(event_id) => Ok(event_id.to_string()), Err(err) => Err(err.to_string()), } } @@ -332,6 +343,7 @@ pub async fn set_nstore( } #[tauri::command] +#[specta::specta] pub async fn get_nstore(key: &str, state: State<'_, Nostr>) -> Result { let client = &state.client; @@ -366,6 +378,7 @@ pub async fn get_nstore(key: &str, state: State<'_, Nostr>) -> Result) -> Result { let client = &state.client; @@ -385,6 +398,7 @@ pub async fn set_nwc(uri: &str, state: State<'_, Nostr>) -> Result } #[tauri::command] +#[specta::specta] pub async fn load_nwc(state: State<'_, Nostr>) -> Result { let client = &state.client; let keyring = Entry::new("Lume Secret Storage", "NWC").unwrap(); @@ -404,7 +418,8 @@ pub async fn load_nwc(state: State<'_, Nostr>) -> Result { } #[tauri::command] -pub async fn get_balance() -> Result { +#[specta::specta] +pub async fn get_balance() -> Result { let keyring = Entry::new("Lume Secret Storage", "NWC").unwrap(); match keyring.get_password() { @@ -412,7 +427,7 @@ pub async fn get_balance() -> Result { let uri = NostrWalletConnectURI::from_str(&val).unwrap(); if let Ok(nwc) = NWC::new(uri).await { if let Ok(balance) = nwc.get_balance().await { - Ok(balance) + Ok(balance.to_string()) } else { Err("Get balance failed".into()) } @@ -425,9 +440,10 @@ pub async fn get_balance() -> Result { } #[tauri::command] +#[specta::specta] pub async fn zap_profile( id: &str, - amount: u64, + amount: &str, message: &str, state: State<'_, Nostr>, ) -> Result { @@ -446,8 +462,9 @@ pub async fn zap_profile( if let Some(recipient) = public_key { let details = ZapDetails::new(ZapType::Public).message(message); + let num = amount.parse::().unwrap(); - if (client.zap(recipient, amount, Some(details)).await).is_ok() { + if (client.zap(recipient, num, Some(details)).await).is_ok() { Ok(true) } else { Err("Zap profile failed".into()) @@ -458,9 +475,10 @@ pub async fn zap_profile( } #[tauri::command] +#[specta::specta] pub async fn zap_event( id: &str, - amount: u64, + amount: &str, message: &str, state: State<'_, Nostr>, ) -> Result { @@ -479,8 +497,9 @@ pub async fn zap_event( if let Some(recipient) = event_id { let details = ZapDetails::new(ZapType::Public).message(message); + let num = amount.parse::().unwrap(); - if (client.zap(recipient, amount, Some(details)).await).is_ok() { + if (client.zap(recipient, num, Some(details)).await).is_ok() { Ok(true) } else { Err("Zap event failed".into()) diff --git a/src-tauri/src/nostr/relay.rs b/src-tauri/src/nostr/relay.rs index bec6394e..958d8d39 100644 --- a/src-tauri/src/nostr/relay.rs +++ b/src-tauri/src/nostr/relay.rs @@ -1,8 +1,10 @@ use crate::Nostr; use nostr_sdk::prelude::*; +use serde::Serialize; +use specta::Type; use tauri::State; -#[derive(serde::Serialize)] +#[derive(Serialize, Type)] pub struct Relays { connected: Vec, read: Option>, @@ -11,6 +13,7 @@ pub struct Relays { } #[tauri::command] +#[specta::specta] pub async fn get_relays(state: State<'_, Nostr>) -> Result { let client = &state.client; @@ -73,6 +76,7 @@ pub async fn get_relays(state: State<'_, Nostr>) -> Result { } #[tauri::command] +#[specta::specta] pub async fn connect_relay(relay: &str, state: State<'_, Nostr>) -> Result { let client = &state.client; if let Ok(status) = client.add_relay(relay).await { @@ -89,6 +93,7 @@ pub async fn connect_relay(relay: &str, state: State<'_, Nostr>) -> Result) -> Result { let client = &state.client; if let Ok(_) = client.remove_relay(relay).await {