refactor: use specta for commands (#192)

* feat: add tauri-specta

* refactor: system library

* chore: format
This commit is contained in:
雨宮蓮 2024-05-25 15:21:40 +07:00 committed by GitHub
parent 7449000f5f
commit bba324ea53
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
92 changed files with 2164 additions and 2071 deletions

View File

@ -9,7 +9,7 @@
"preview": "vite preview"
},
"dependencies": {
"@lume/ark": "workspace:^",
"@lume/system": "workspace:^",
"@lume/icons": "workspace:^",
"@lume/ui": "workspace:^",
"@lume/utils": "workspace:^",

View File

@ -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 (

View File

@ -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 (

View File

@ -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);
}

View File

@ -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 (
<Note.Provider event={event}>

View File

@ -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 (
<div className={cn("-mt-3 mb-2", className)}>

View File

@ -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 (
<Tooltip.Provider>
@ -13,7 +12,7 @@ export function NoteOpenThread() {
<Tooltip.Trigger asChild>
<button
type="button"
onClick={() => ark.open_event(event)}
onClick={() => LumeWindow.openEvent(event)}
className="group inline-flex h-7 w-14 bg-neutral-100 dark:bg-white/10 rounded-full items-center justify-center text-sm font-medium text-neutral-800 dark:text-neutral-200 hover:text-blue-500 hover:bg-neutral-200 dark:hover:bg-white/20"
>
<VisitIcon className="shrink-0 size-4" />

View File

@ -1,12 +1,11 @@
import { ReplyIcon } from "@lume/icons";
import * as Tooltip from "@radix-ui/react-tooltip";
import { useRouteContext } from "@tanstack/react-router";
import { useNoteContext } from "../provider";
import { cn } from "@lume/utils";
import { LumeWindow } from "@lume/system";
export function NoteReply({ large = false }: { large?: boolean }) {
const event = useNoteContext();
const { ark } = useRouteContext({ strict: false });
return (
<Tooltip.Provider>
@ -14,7 +13,7 @@ export function NoteReply({ large = false }: { large?: boolean }) {
<Tooltip.Trigger asChild>
<button
type="button"
onClick={() => ark.open_editor(event.id)}
onClick={() => LumeWindow.openEditor(event.id)}
className={cn(
"inline-flex items-center justify-center text-neutral-800 dark:text-neutral-200",
large

View File

@ -2,15 +2,14 @@ import { QuoteIcon, RepostIcon } from "@lume/icons";
import { cn } from "@lume/utils";
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
import * as Tooltip from "@radix-ui/react-tooltip";
import { useRouteContext } from "@tanstack/react-router";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
import { Spinner } from "@lume/ui";
import { useNoteContext } from "../provider";
import { LumeWindow } from "@lume/system";
export function NoteRepost({ large = false }: { large?: boolean }) {
const { ark } = useRouteContext({ strict: false });
const event = useNoteContext();
const [t] = useTranslation();
@ -23,7 +22,7 @@ export function NoteRepost({ large = false }: { large?: boolean }) {
setLoading(true);
// repost
await ark.repost(event.id, event.pubkey);
await event.repost();
// update state
setLoading(false);
@ -86,7 +85,7 @@ export function NoteRepost({ large = false }: { large?: boolean }) {
<DropdownMenu.Item asChild>
<button
type="button"
onClick={() => ark.open_editor(event.id, true)}
onClick={() => LumeWindow.openEditor(event.id, true)}
className="inline-flex h-9 items-center gap-2 rounded-lg px-3 text-sm font-medium text-white hover:bg-neutral-900 focus:outline-none dark:text-black dark:hover:bg-neutral-100"
>
<QuoteIcon className="size-4" />

View File

@ -1,33 +1,19 @@
import { ZapIcon } from "@lume/icons";
import { useRouteContext, useSearch } from "@tanstack/react-router";
import { toast } from "sonner";
import { useRouteContext } from "@tanstack/react-router";
import { useNoteContext } from "../provider";
import { cn } from "@lume/utils";
import { LumeWindow } from "@lume/system";
export function NoteZap({ large = false }: { large?: boolean }) {
const event = useNoteContext();
const { ark, settings } = useRouteContext({ strict: false });
const { account } = useSearch({ strict: false });
const zap = async () => {
try {
const nwc = await ark.load_nwc();
if (!nwc) {
ark.open_nwc();
} else {
ark.open_zap(event.id, event.pubkey, account);
}
} catch (e) {
toast.error(String(e));
}
};
const { settings } = useRouteContext({ strict: false });
if (!settings.zap) return null;
return (
<button
type="button"
onClick={() => zap()}
onClick={() => LumeWindow.openZap(event.id, event.pubkey)}
className={cn(
"inline-flex items-center justify-center text-neutral-800 dark:text-neutral-200",
large

View File

@ -1,4 +1,4 @@
import { useEvent } from "@lume/ark";
import { useEvent } from "@lume/system";
import { cn } from "@lume/utils";
import { Note } from ".";
import { InfoIcon } from "@lume/icons";

View File

@ -1,6 +1,5 @@
import { useEvent } from "@lume/ark";
import { LumeWindow, useEvent } from "@lume/system";
import { LinkIcon } from "@lume/icons";
import { useRouteContext } from "@tanstack/react-router";
import { useTranslation } from "react-i18next";
import { cn } from "@lume/utils";
import { User } from "@/components/user";
@ -13,7 +12,6 @@ export function MentionNote({
eventId: string;
openable?: boolean;
}) {
const { ark } = useRouteContext({ strict: false });
const { t } = useTranslation();
const { isLoading, isError, data } = useEvent(eventId);
@ -62,9 +60,9 @@ export function MentionNote({
type="button"
onClick={(e) => {
e.stopPropagation();
ark.open_event_id(data.id);
LumeWindow.openEvent(data);
}}
className="z-10 h-7 w-28 inline-flex items-center justify-center gap-1 text-sm bg-neutral-100 dark:bg-white/10 rounded-full text-neutral-600 hover:text-blue-500 dark:text-neutral-400"
className="z-10 h-7 w-28 inline-flex items-center justify-center gap-1 text-sm bg-black/10 dark:bg-white/10 rounded-full text-neutral-600 hover:text-blue-500 dark:text-neutral-400"
>
View post
<LinkIcon className="size-4" />

View File

@ -1,24 +1,20 @@
import { useProfile } from "@lume/ark";
import { LumeWindow, useProfile } from "@lume/system";
import { displayNpub } from "@lume/utils";
import { useRouteContext } from "@tanstack/react-router";
export function MentionUser({ pubkey }: { pubkey: string }) {
const { ark } = useRouteContext({ strict: false });
const { isLoading, isError, profile } = useProfile(pubkey);
return (
<button
type="button"
onClick={() => ark.open_profile(pubkey)}
onClick={() => LumeWindow.openProfile(pubkey)}
className="break-words text-start text-blue-500 hover:text-blue-600"
>
{isLoading
? "@anon"
: isError
? displayNpub(pubkey, 16)
: `@${
profile?.name || profile?.display_name || profile?.name || "anon"
}`}
: `@${profile?.name || profile?.display_name || "anon"}`}
</button>
);
}

View File

@ -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() {
<DropdownMenu.Item asChild>
<button
type="button"
onClick={() => ark.open_event(event)}
onClick={() => LumeWindow.openEvent(event)}
className="inline-flex h-9 items-center gap-2 rounded-lg px-3 text-sm font-medium text-white hover:bg-neutral-900 focus:outline-none dark:text-black dark:hover:bg-neutral-100"
>
{t("note.menu.viewThread")}
@ -84,7 +75,7 @@ export function NoteMenu() {
</DropdownMenu.Item>
<DropdownMenu.Item asChild>
<button
onClick={() => ark.open_profile(event.pubkey)}
onClick={() => LumeWindow.openProfile(event.pubkey)}
className="inline-flex h-9 items-center gap-2 rounded-lg px-3 text-sm font-medium text-white hover:bg-neutral-900 focus:outline-none dark:text-black dark:hover:bg-neutral-100"
>
{t("note.menu.viewAuthor")}

View File

@ -1,19 +1,25 @@
import type { Event } from "@lume/types";
import { LumeEvent } from "@lume/system";
import type { NostrEvent } from "@lume/types";
import { type ReactNode, createContext, useContext } from "react";
const EventContext = createContext<Event>(null);
const NoteContext = createContext<LumeEvent>(null);
export function NoteProvider({
event,
children,
}: { event: Event; children: ReactNode }) {
}: {
event: NostrEvent;
children: ReactNode;
}) {
const lumeEvent = new LumeEvent(event);
return (
<EventContext.Provider value={event}>{children}</EventContext.Provider>
<NoteContext.Provider value={lumeEvent}>{children}</NoteContext.Provider>
);
}
export function useNoteContext() {
const context = useContext(EventContext);
const context = useContext(NoteContext);
if (!context) {
throw new Error("Please import Note Provider to use useNoteContext() hook");
}

View File

@ -1,11 +1,10 @@
import { cn } from "@lume/utils";
import * as HoverCard from "@radix-ui/react-hover-card";
import { useRouteContext } from "@tanstack/react-router";
import { User } from "../user";
import { useNoteContext } from "./provider";
import { LumeWindow } from "@lume/system";
export function NoteUser({ className }: { className?: string }) {
const { ark } = useRouteContext({ strict: false });
const event = useNoteContext();
return (
@ -46,7 +45,7 @@ export function NoteUser({ className }: { className?: string }) {
</div>
<User.About className="line-clamp-3 text-sm text-white dark:text-neutral-900" />
<button
onClick={() => ark.open_profile(event.pubkey)}
onClick={() => LumeWindow.openProfile(event.pubkey)}
className="mt-2 inline-flex h-9 w-full items-center justify-center rounded-lg bg-white text-sm font-medium hover:bg-neutral-200 dark:bg-neutral-100 dark:text-neutral-900 dark:hover:bg-neutral-200"
>
View profile

View File

@ -1,4 +1,4 @@
import type { Event } from "@lume/types";
import type { NostrEvent } from "@lume/types";
import { Note } from "@/components/note";
import { cn } from "@lume/utils";
@ -6,7 +6,7 @@ export function Notification({
event,
className,
}: {
event: Event;
event: NostrEvent;
className?: string;
}) {
return (

View File

@ -1,5 +1,5 @@
import { QuoteIcon } 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";
@ -7,7 +7,7 @@ export function Quote({
event,
className,
}: {
event: Event;
event: NostrEvent;
className?: string;
}) {
const quoteEventId = event.tags.find(

View File

@ -1,19 +1,18 @@
import type { Event } from "@lume/types";
import { Spinner } from "@lume/ui";
import { Note } from "@/components/note";
import { User } from "@/components/user";
import { cn } from "@lume/utils";
import { useQuery } from "@tanstack/react-query";
import { useRouteContext } from "@tanstack/react-router";
import { NostrEvent } from "@lume/types";
import { NostrQuery } from "@lume/system";
export function RepostNote({
event,
className,
}: {
event: Event;
event: NostrEvent;
className?: string;
}) {
const { ark } = useRouteContext({ strict: false });
const {
isLoading,
isError,
@ -23,12 +22,12 @@ export function RepostNote({
queryFn: async () => {
try {
if (event.content.length > 50) {
const embed: Event = JSON.parse(event.content);
const embed: NostrEvent = JSON.parse(event.content);
return embed;
}
const id = event.tags.find((el) => el[0] === "e")?.[1];
const repostEvent = await ark.get_event(id);
const repostEvent = await NostrQuery.getEvent(id);
return repostEvent;
} catch (e) {

View File

@ -1,4 +1,4 @@
import type { Event } from "@lume/types";
import type { NostrEvent } from "@lume/types";
import { cn } from "@lume/utils";
import { Note } from "@/components/note";
@ -6,7 +6,7 @@ export function TextNote({
event,
className,
}: {
event: Event;
event: NostrEvent;
className?: string;
}) {
return (

View File

@ -1,9 +1,9 @@
import { cn } from "@lume/utils";
import { useRouteContext } from "@tanstack/react-router";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { Spinner } from "@lume/ui";
import { useUserContext } from "./provider";
import { NostrAccount } from "@lume/system";
export function UserFollowButton({
simple = false,
@ -12,7 +12,6 @@ export function UserFollowButton({
simple?: boolean;
className?: string;
}) {
const { ark } = useRouteContext({ strict: false });
const user = useUserContext();
const [t] = useTranslation();
@ -22,10 +21,10 @@ export function UserFollowButton({
const toggleFollow = async () => {
setLoading(true);
if (!followed) {
const add = await ark.follow(user.pubkey);
const add = await NostrAccount.follow(user.pubkey, user.profile?.name);
if (add) setFollowed(true);
} else {
const remove = await ark.unfollow(user.pubkey);
const remove = await NostrAccount.unfollow(user.pubkey);
if (remove) setFollowed(false);
}
setLoading(false);
@ -35,7 +34,7 @@ export function UserFollowButton({
async function status() {
setLoading(true);
const contacts = await ark.get_contact_list();
const contacts = await NostrAccount.getContactList();
if (contacts?.includes(user.pubkey)) {
setFollowed(true);
}

View File

@ -2,21 +2,21 @@ import { VerifiedIcon } from "@lume/icons";
import { displayLongHandle, displayNpub } from "@lume/utils";
import * as Tooltip from "@radix-ui/react-tooltip";
import { useQuery } from "@tanstack/react-query";
import { useRouteContext } from "@tanstack/react-router";
import { useUserContext } from "./provider";
import { NostrQuery } from "@lume/system";
export function UserNip05() {
const user = useUserContext();
const { ark } = useRouteContext({ strict: false });
const { isLoading, data: verified } = useQuery({
queryKey: ["nip05", user?.pubkey],
queryFn: async () => {
if (!user.profile?.nip05) return false;
const verify = await ark.verify_nip05(user.pubkey, user.profile?.nip05);
const verify = await NostrQuery.verifyNip05(
user.pubkey,
user.profile?.nip05,
);
return verify;
},
enabled: !!user.profile,
enabled: !!user.profile?.nip05,
});
if (!user.profile?.nip05?.length) return;

View File

@ -1,12 +1,12 @@
import { useProfile } from "@lume/ark";
import { useProfile } from "@lume/system";
import type { Metadata } from "@lume/types";
import { type ReactNode, createContext, useContext } from "react";
const UserContext = createContext<{
pubkey: string;
profile: Metadata;
isError: boolean;
isLoading: boolean;
profile: Metadata;
}>(null);
export function UserProvider({
@ -21,7 +21,7 @@ export function UserProvider({
const { isLoading, isError, profile } = useProfile(pubkey, embedProfile);
return (
<UserContext.Provider value={{ pubkey, isError, isLoading, profile }}>
<UserContext.Provider value={{ pubkey, profile, isError, isLoading }}>
{children}
</UserContext.Provider>
);

View File

@ -1,44 +1,28 @@
import { Column } from "@/components/column";
import { Toolbar } from "@/components/toolbar";
import { ArrowLeftIcon, ArrowRightIcon } from "@lume/icons";
import { NostrQuery } from "@lume/system";
import type { EventColumns, LumeColumn } from "@lume/types";
import { createFileRoute } from "@tanstack/react-router";
import { listen } from "@tauri-apps/api/event";
import { resolveResource } from "@tauri-apps/api/path";
import { getCurrent } from "@tauri-apps/api/window";
import { readTextFile } from "@tauri-apps/plugin-fs";
import { nanoid } from "nanoid";
import { useEffect, useRef, useState } from "react";
import { useDebouncedCallback } from "use-debounce";
import { VList, type VListHandle } from "virtua";
export const Route = createFileRoute("/$account/home")({
loader: async ({ context }) => {
try {
const userColumns = await context.ark.get_columns();
if (userColumns.length > 0) {
return userColumns;
} else {
const systemPath = "resources/system_columns.json";
const resourcePath = await resolveResource(systemPath);
const resourceFile = await readTextFile(resourcePath);
const systemColumns: LumeColumn[] = JSON.parse(resourceFile);
return systemColumns;
}
} catch (e) {
console.error(String(e));
}
loader: async () => {
const columns = NostrQuery.getColumns();
return columns;
},
component: Screen,
});
function Screen() {
const userSavedColumns = Route.useLoaderData();
const vlistRef = useRef<VListHandle>(null);
const { account } = Route.useParams();
const { ark } = Route.useRouteContext();
const initialColumnList = Route.useLoaderData();
const vlistRef = useRef<VListHandle>(null);
const [selectedIndex, setSelectedIndex] = useState(-1);
const [columns, setColumns] = useState([]);
@ -115,12 +99,12 @@ function Screen() {
);
useEffect(() => {
setColumns(userSavedColumns);
}, [userSavedColumns]);
setColumns(initialColumnList);
}, [initialColumnList]);
useEffect(() => {
// save state
ark.set_columns(columns);
NostrQuery.setColumns(columns);
}, [columns]);
useEffect(() => {

View File

@ -5,7 +5,7 @@ import {
PlusIcon,
SearchIcon,
} from "@lume/icons";
import { type Event, Kind } from "@lume/types";
import { type NostrEvent, Kind } from "@lume/types";
import { User } from "@/components/user";
import {
cn,
@ -19,20 +19,24 @@ import { getCurrent } from "@tauri-apps/api/window";
import { useEffect, useMemo, useState } from "react";
import { toast } from "sonner";
import * as Popover from "@radix-ui/react-popover";
import { LumeWindow, NostrAccount, NostrQuery } from "@lume/system";
import { Link } from "@tanstack/react-router";
type AccountSearch = {
accounts?: string[];
};
export const Route = createFileRoute("/$account")({
beforeLoad: async ({ context }) => {
const ark = context.ark;
const accounts = await ark.get_accounts();
return { accounts };
validateSearch: (search: Record<string, unknown>): AccountSearch => {
return {
accounts: (search?.accounts as string[]) || [],
};
},
component: Screen,
});
function Screen() {
const { ark, platform } = Route.useRouteContext();
const navigate = Route.useNavigate();
const { platform } = Route.useRouteContext();
return (
<div className="flex h-screen w-screen flex-col">
@ -45,18 +49,17 @@ function Screen() {
>
<div className="flex items-center gap-3">
<Accounts />
<button
type="button"
onClick={() => navigate({ to: "/landing/" })}
<Link
to="/landing/"
className="inline-flex size-8 shrink-0 items-center justify-center rounded-full bg-black/10 text-neutral-800 hover:bg-black/20 dark:bg-white/10 dark:text-neutral-200 dark:hover:bg-white/20"
>
<PlusIcon className="size-5" />
</button>
</Link>
</div>
<div className="flex items-center gap-2">
<button
type="button"
onClick={() => ark.open_editor()}
onClick={() => LumeWindow.openEditor()}
className="inline-flex h-8 w-max items-center justify-center gap-1 rounded-full bg-blue-500 px-3 text-sm font-medium text-white hover:bg-blue-600"
>
<ComposeFilledIcon className="size-4" />
@ -65,7 +68,7 @@ function Screen() {
<Bell />
<button
type="button"
onClick={() => ark.open_search()}
onClick={() => LumeWindow.openSearch()}
className="inline-flex size-8 items-center justify-center rounded-full text-neutral-800 hover:bg-black/10 dark:text-neutral-200 dark:hover:bg-white/10"
>
<SearchIcon className="size-5" />
@ -81,7 +84,7 @@ function Screen() {
}
function Accounts() {
const { ark, accounts } = Route.useRouteContext();
const { accounts } = Route.useSearch();
const { account } = Route.useParams();
const [windowWidth, setWindowWidth] = useState<number>(null);
@ -102,11 +105,11 @@ function Accounts() {
const changeAccount = async (npub: string) => {
if (npub === account) {
return await ark.open_profile(account);
return await LumeWindow.openProfile(account);
}
// change current account and update signer
const select = await ark.load_account(npub);
const select = await NostrAccount.loadAccount(npub);
if (select) {
return navigate({ to: "/$account/home", params: { account: npub } });
@ -190,9 +193,7 @@ function Accounts() {
}
function Bell() {
const { ark } = Route.useRouteContext();
const { account } = Route.useParams();
const [count, setCount] = useState(0);
useEffect(() => {
@ -202,8 +203,8 @@ function Bell() {
setCount((prevCount) => prevCount + 1);
await invoke("set_badge", { count });
const event: Event = JSON.parse(payload.payload);
const user = await ark.get_profile(event.pubkey);
const event: NostrEvent = JSON.parse(payload.payload);
const user = await NostrQuery.getProfile(event.pubkey);
const userName =
user.display_name || user.name || displayNpub(event.pubkey, 16);
@ -240,7 +241,7 @@ function Bell() {
type="button"
onClick={() => {
setCount(0);
ark.open_activity(account);
LumeWindow.openActivity(account);
}}
className="relative inline-flex size-8 items-center justify-center rounded-full text-neutral-800 hover:bg-black/10 dark:text-neutral-200 dark:hover:bg-white/10"
>

View File

@ -1,4 +1,3 @@
import type { Ark } from "@lume/ark";
import { CheckCircleIcon, InfoCircleIcon, CancelCircleIcon } from "@lume/icons";
import type { Interests, Metadata, Settings } from "@lume/types";
import { Spinner } from "@lume/ui";
@ -16,7 +15,6 @@ type EditorElement = {
interface RouterContext {
// System
ark: Ark;
queryClient: QueryClient;
// App info
platform?: Platform;

View File

@ -3,11 +3,11 @@ import { Note } from "@/components/note";
import { Await, createFileRoute, defer } from "@tanstack/react-router";
import { Suspense } from "react";
import { Virtualizer } from "virtua";
import { NostrQuery } from "@lume/system";
export const Route = createFileRoute("/activity/$account/texts")({
loader: async ({ context, params }) => {
const ark = context.ark;
return { data: defer(ark.get_activities(params.account, "1")) };
loader: async ({ params }) => {
return { data: defer(NostrQuery.getUserActivities(params.account, "1")) };
},
component: Screen,
});

View File

@ -11,7 +11,7 @@ function Screen() {
const { account } = Route.useParams();
return (
<Container withDrag withNavigate={false}>
<Container withDrag>
<Box className="scrollbar-none shadow-none bg-black/5 dark:bg-white/5 backdrop-blur-sm flex flex-col overflow-y-auto">
<div className="h-14 shrink-0 flex w-full items-center gap-1 px-3">
<div className="inline-flex h-full w-full items-center gap-1">

View File

@ -1,4 +1,5 @@
import { User } from "@/components/user";
import { NostrQuery } from "@lume/system";
import { Spinner } from "@lume/ui";
import { decodeZapInvoice } from "@lume/utils";
import { Await, createFileRoute, defer } from "@tanstack/react-router";
@ -6,9 +7,10 @@ import { Suspense } from "react";
import { Virtualizer } from "virtua";
export const Route = createFileRoute("/activity/$account/zaps")({
loader: async ({ context, params }) => {
const ark = context.ark;
return { data: defer(ark.get_activities(params.account, "9735")) };
loader: async ({ params }) => {
return {
data: defer(NostrQuery.getUserActivities(params.account, "9735")),
};
},
component: Screen,
});

View File

@ -1,43 +1,29 @@
import { LaurelIcon } from "@lume/icons";
import type { AppRouteSearch, Settings } from "@lume/types";
import { NostrQuery } from "@lume/system";
import { Spinner } from "@lume/ui";
import * as Switch from "@radix-ui/react-switch";
import { createFileRoute, useNavigate } from "@tanstack/react-router";
import {
isPermissionGranted,
requestPermission,
} from "@tauri-apps/plugin-notification";
import { createFileRoute } from "@tanstack/react-router";
import { requestPermission } from "@tauri-apps/plugin-notification";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
export const Route = createFileRoute("/auth/settings")({
validateSearch: (search: Record<string, string>): AppRouteSearch => {
return {
account: search.account,
};
},
beforeLoad: async ({ context }) => {
const permissionGranted = await isPermissionGranted(); // get notification permission
const ark = context.ark;
const settings = await ark.get_settings();
return {
settings: { ...settings, notification: permissionGranted },
};
export const Route = createFileRoute("/auth/$account/settings")({
beforeLoad: async () => {
const settings = await NostrQuery.getSettings();
return { settings };
},
component: Screen,
pendingComponent: Pending,
});
function Screen() {
const navigate = useNavigate();
const { account } = Route.useSearch();
const navigate = Route.useNavigate();
const { account } = Route.useParams();
const { settings } = Route.useRouteContext();
const { t } = useTranslation();
const { ark, settings } = Route.useRouteContext();
const [newSettings, setNewSettings] = useState<Settings>(settings);
const [newSettings, setNewSettings] = useState(settings);
const [loading, setLoading] = useState(false);
const toggleNofitication = async () => {
@ -82,7 +68,7 @@ function Screen() {
setLoading(true);
// publish settings
const eventId = await ark.set_settings(newSettings);
const eventId = await NostrQuery.setSettings(newSettings);
if (eventId) {
return navigate({

View File

@ -1,5 +1,4 @@
import { CheckIcon } from "@lume/icons";
import type { AppRouteSearch } from "@lume/types";
import { Spinner } from "@lume/ui";
import { displayNsec } from "@lume/utils";
import * as Checkbox from "@radix-ui/react-checkbox";
@ -10,17 +9,12 @@ import { useState } from "react";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
export const Route = createFileRoute("/auth/new/backup")({
validateSearch: (search: Record<string, string>): AppRouteSearch => {
return {
account: search.account,
};
},
export const Route = createFileRoute("/auth/new/$account/backup")({
component: Screen,
});
function Screen() {
const { account } = Route.useSearch();
const { account } = Route.useParams();
const { t } = useTranslation();
const [key, setKey] = useState(null);
@ -39,8 +33,8 @@ function Screen() {
}
return navigate({
to: "/auth/settings",
search: { account },
to: "/auth/$account/settings",
params: { account },
});
}

View File

@ -1,5 +1,6 @@
import { AvatarUploader } from "@/components/avatarUploader";
import { PlusIcon } from "@lume/icons";
import { NostrAccount } from "@lume/system";
import type { Metadata } from "@lume/types";
import { Spinner } from "@lume/ui";
import { createFileRoute, useNavigate } from "@tanstack/react-router";
@ -10,17 +11,17 @@ import { toast } from "sonner";
export const Route = createFileRoute("/auth/new/profile")({
component: Screen,
loader: ({ context }) => {
return context.ark.create_keys();
loader: async () => {
const account = await NostrAccount.createAccount();
return account;
},
});
function Screen() {
const keys = Route.useLoaderData();
const account = Route.useLoaderData();
const navigate = useNavigate();
const { t } = useTranslation();
const { ark } = Route.useRouteContext();
const { register, handleSubmit } = useForm();
const [picture, setPicture] = useState<string>("");
@ -35,17 +36,17 @@ function Screen() {
try {
// Save account keys
const save = await ark.save_account(keys.nsec);
const save = await NostrAccount.saveAccount(account.nsec);
// Then create profile
if (save) {
const profile: Metadata = { ...data, picture };
const eventId = await ark.create_profile(profile);
const eventId = await NostrAccount.createProfile(profile);
if (eventId) {
navigate({
to: "/auth/new/backup",
search: { account: keys.npub },
to: "/auth/new/$account/backup",
params: { account: account.npub },
replace: true,
});
}

View File

@ -1,3 +1,4 @@
import { NostrAccount } from "@lume/system";
import { Spinner } from "@lume/ui";
import { createLazyFileRoute } from "@tanstack/react-router";
import { useState } from "react";
@ -8,7 +9,6 @@ export const Route = createLazyFileRoute("/auth/privkey")({
});
function Screen() {
const { ark } = Route.useRouteContext();
const navigate = Route.useNavigate();
const [key, setKey] = useState("");
@ -24,12 +24,12 @@ function Screen() {
try {
setLoading(true);
const npub = await ark.save_account(key, password);
const npub = await NostrAccount.saveAccount(key, password);
if (npub) {
navigate({
to: "/auth/settings",
search: { account: npub },
to: "/auth/$account/settings",
params: { account: npub },
replace: true,
});
}

View File

@ -1,3 +1,4 @@
import { NostrAccount } from "@lume/system";
import { Spinner } from "@lume/ui";
import { createLazyFileRoute } from "@tanstack/react-router";
import { useState } from "react";
@ -8,7 +9,6 @@ export const Route = createLazyFileRoute("/auth/remote")({
});
function Screen() {
const { ark } = Route.useRouteContext();
const navigate = Route.useNavigate();
const [uri, setUri] = useState("");
@ -23,12 +23,12 @@ function Screen() {
try {
setLoading(true);
const npub = await ark.nostr_connect(uri);
const npub = await NostrAccount.connectRemoteAccount(uri);
if (npub) {
navigate({
to: "/auth/settings",
search: { account: npub },
to: "/auth/$account/settings",
params: { account: npub },
replace: true,
});
}

View File

@ -1,10 +1,11 @@
import { CancelIcon, CheckCircleIcon, PlusIcon } from "@lume/icons";
import { CancelIcon, PlusIcon } from "@lume/icons";
import type { ColumnRouteSearch } from "@lume/types";
import { Spinner } from "@lume/ui";
import { User } from "@/components/user";
import { createFileRoute } from "@tanstack/react-router";
import { useState } from "react";
import { toast } from "sonner";
import { NostrAccount, NostrQuery } from "@lume/system";
export const Route = createFileRoute("/create-group")({
validateSearch: (search: Record<string, string>): ColumnRouteSearch => {
@ -14,17 +15,14 @@ export const Route = createFileRoute("/create-group")({
name: search.name,
};
},
loader: async ({ context }) => {
const ark = context.ark;
const contacts = await ark.get_contact_list();
loader: async () => {
const contacts = await NostrAccount.getContactList();
return contacts;
},
component: Screen,
});
function Screen() {
const { ark } = Route.useRouteContext();
const [title, setTitle] = useState("");
const [npub, setNpub] = useState("");
const [users, setUsers] = useState<string[]>([
@ -57,7 +55,10 @@ function Screen() {
setIsLoading(true);
const key = `lume_group_${search.label}`;
const createGroup = await ark.set_nstore(key, JSON.stringify(users));
const createGroup = await NostrQuery.setNstore(
key,
JSON.stringify(users),
);
if (createGroup) {
return navigate({ to: search.redirect, search: { ...search } });

View File

@ -1,7 +1,7 @@
import { NostrAccount } from "@lume/system";
import type { ColumnRouteSearch } from "@lume/types";
import { Spinner } from "@lume/ui";
import { createFileRoute } from "@tanstack/react-router";
import { invoke } from "@tauri-apps/api/core";
import { useState } from "react";
import { toast } from "sonner";
@ -30,7 +30,7 @@ function Screen() {
try {
setIsLoading(true);
const sync: boolean = await invoke("friend_to_friend", { npub });
const sync = await NostrAccount.f2f(npub);
if (sync) {
return navigate({ to: redirect });

View File

@ -5,6 +5,7 @@ import { User } from "@/components/user";
import { Spinner } from "@lume/ui";
import { toast } from "sonner";
import type { ColumnRouteSearch } from "@lume/types";
import { NostrAccount } from "@lume/system";
export const Route = createFileRoute("/create-newsfeed/users")({
validateSearch: (search: Record<string, string>): ColumnRouteSearch => {
@ -31,7 +32,6 @@ export const Route = createFileRoute("/create-newsfeed/users")({
});
function Screen() {
const { ark } = Route.useRouteContext();
const { data } = Route.useLoaderData();
const { redirect } = Route.useSearch();
@ -52,7 +52,7 @@ function Screen() {
try {
setIsLoading(true);
const newContactList = await ark.set_contact_list(follows);
const newContactList = await NostrAccount.setContactList(follows);
if (newContactList) {
return navigate({ to: redirect });

View File

@ -1,4 +1,5 @@
import { CheckCircleIcon } from "@lume/icons";
import { NostrQuery } from "@lume/system";
import type { ColumnRouteSearch, Topic } from "@lume/types";
import { Spinner } from "@lume/ui";
import { TOPICS } from "@lume/utils";
@ -18,8 +19,6 @@ export const Route = createFileRoute("/create-topic")({
});
function Screen() {
const { ark } = Route.useRouteContext();
const [topics, setTopics] = useState<Topic[]>([]);
const [isLoading, setIsLoading] = useState(false);
@ -39,7 +38,10 @@ function Screen() {
setIsLoading(true);
const key = `lume_topic_${search.label}`;
const createTopic = await ark.set_nstore(key, JSON.stringify(topics));
const createTopic = await NostrQuery.setNstore(
key,
JSON.stringify(topics),
);
if (createTopic) {
return navigate({ to: search.redirect, search: { ...search } });

View File

@ -1,8 +1,8 @@
import { AddMediaIcon } from "@lume/icons";
import { NostrQuery } from "@lume/system";
import { Spinner } from "@lume/ui";
import { cn, insertImage, isImagePath } from "@lume/utils";
import * as Tooltip from "@radix-ui/react-tooltip";
import { useRouteContext } from "@tanstack/react-router";
import type { UnlistenFn } from "@tauri-apps/api/event";
import { getCurrent } from "@tauri-apps/api/window";
import { useEffect, useState } from "react";
@ -11,7 +11,6 @@ import { toast } from "sonner";
export function MediaButton({ className }: { className?: string }) {
const editor = useSlateStatic();
const { ark } = useRouteContext({ strict: false });
const [loading, setLoading] = useState(false);
const uploadToNostrBuild = async () => {
@ -19,7 +18,7 @@ export function MediaButton({ className }: { className?: string }) {
// start loading
setLoading(true);
const image = await ark.upload();
const image = await NostrQuery.upload();
insertImage(editor, image);
// reset loading
@ -44,7 +43,7 @@ export function MediaButton({ className }: { className?: string }) {
// upload all images
for (const item of items) {
if (isImagePath(item)) {
const image = await ark.upload(item);
const image = await NostrQuery.upload(item);
insertImage(editor, image);
}
}

View File

@ -3,20 +3,19 @@ import { cn, insertMention } from "@lume/utils";
import * as Tooltip from "@radix-ui/react-tooltip";
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
import { useEffect, useState } from "react";
import { useRouteContext } from "@tanstack/react-router";
import { useSlateStatic } from "slate-react";
import type { Contact } from "@lume/types";
import { toast } from "sonner";
import { User } from "@/components/user";
import { NostrAccount, NostrQuery } from "@lume/system";
export function MentionButton({ className }: { className?: string }) {
const editor = useSlateStatic();
const { ark } = useRouteContext({ strict: false });
const [contacts, setContacts] = useState<string[]>([]);
const select = async (user: string) => {
try {
const metadata = await ark.get_profile(user);
const metadata = await NostrQuery.getProfile(user);
const contact: Contact = { pubkey: user, profile: metadata };
insertMention(editor, contact);
@ -27,7 +26,7 @@ export function MentionButton({ className }: { className?: string }) {
useEffect(() => {
async function getContacts() {
const data = await ark.get_contact_list();
const data = await NostrAccount.getContactList();
setContacts(data);
}

View File

@ -25,7 +25,7 @@ import { MediaButton } from "./-components/media";
import { NsfwToggle } from "./-components/nsfw";
import { MentionButton } from "./-components/mention";
import { MentionNote } from "@/components/note/mentions/note";
import { toast } from "sonner";
import { LumeEvent } from "@lume/system";
type EditorSearch = {
reply_to: string;
@ -70,7 +70,7 @@ export const Route = createFileRoute("/editor/")({
function Screen() {
const { reply_to, quote } = Route.useSearch();
const { ark, initialValue } = Route.useRouteContext();
const { initialValue } = Route.useRouteContext();
const [t] = useTranslation();
const [editorValue, setEditorValue] = useState(initialValue);
@ -116,7 +116,7 @@ function Screen() {
setLoading(true);
const content = serialize(editor.children);
const eventId = await ark.publish(content, reply_to, quote);
const eventId = await LumeEvent.publish(content, reply_to, quote);
if (eventId) {
await sendNativeNotification(

View File

@ -1,5 +1,5 @@
import { useEvent } from "@lume/ark";
import type { Event } from "@lume/types";
import { NostrQuery, useEvent } from "@lume/system";
import type { NostrEvent } from "@lume/types";
import { Box, Container, Spinner } from "@lume/ui";
import { Note } from "@/components/note";
import { createFileRoute } from "@tanstack/react-router";
@ -7,10 +7,8 @@ import { WindowVirtualizer } from "virtua";
import { ReplyList } from "./-components/replyList";
export const Route = createFileRoute("/events/$eventId")({
beforeLoad: async ({ context }) => {
const ark = context.ark;
const settings = await ark.get_settings();
beforeLoad: async () => {
const settings = await NostrQuery.getSettings();
return { settings };
},
component: Screen,
@ -52,7 +50,7 @@ function Screen() {
);
}
function MainNote({ data }: { data: Event }) {
function MainNote({ data }: { data: NostrEvent }) {
return (
<Note.Provider event={data}>
<Note.Root>

View File

@ -1,10 +1,10 @@
import type { EventWithReplies } from "@lume/types";
import { Spinner } from "@lume/ui";
import { cn } from "@lume/utils";
import { useRouteContext } from "@tanstack/react-router";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { Reply } from "./reply";
import { LumeEvent } from "@lume/system";
export function ReplyList({
eventId,
@ -13,13 +13,12 @@ export function ReplyList({
eventId: string;
className?: string;
}) {
const { ark } = useRouteContext({ strict: false });
const [t] = useTranslation();
const [data, setData] = useState<null | EventWithReplies[]>(null);
useEffect(() => {
async function getReplies() {
const events = await ark.get_event_thread(eventId);
const events = await LumeEvent.getReplies(eventId);
setData(events);
}
getReplies();

View File

@ -1,7 +1,12 @@
import type { Event } from "@lume/types";
import type { NostrEvent } from "@lume/types";
import { Note } from "@/components/note";
export function SubReply({ event }: { event: Event; rootEventId?: string }) {
export function SubReply({
event,
}: {
event: NostrEvent;
rootEventId?: string;
}) {
return (
<Note.Provider event={event}>
<Note.Root>

View File

@ -3,7 +3,8 @@ import { Quote } from "@/components/quote";
import { RepostNote } from "@/components/repost";
import { TextNote } from "@/components/text";
import { ArrowRightCircleIcon, ArrowRightIcon } from "@lume/icons";
import { type ColumnRouteSearch, type Event, Kind } from "@lume/types";
import { NostrQuery } from "@lume/system";
import { type ColumnRouteSearch, type NostrEvent, Kind } from "@lume/types";
import { Spinner } from "@lume/ui";
import { useInfiniteQuery } from "@tanstack/react-query";
import { Link, createFileRoute } from "@tanstack/react-router";
@ -17,10 +18,8 @@ export const Route = createFileRoute("/global")({
name: search.name,
};
},
beforeLoad: async ({ context }) => {
const ark = context.ark;
const settings = await ark.get_settings();
beforeLoad: async () => {
const settings = await NostrQuery.getSettings();
return { settings };
},
component: Screen,
@ -28,7 +27,6 @@ export const Route = createFileRoute("/global")({
export function Screen() {
const { label, account } = Route.useSearch();
const { ark } = Route.useRouteContext();
const {
data,
isLoading,
@ -40,7 +38,7 @@ export function Screen() {
queryKey: [label, account],
initialPageParam: 0,
queryFn: async ({ pageParam }: { pageParam: number }) => {
const events = await ark.get_global_events(20, pageParam);
const events = await NostrQuery.getGlobalEvents(pageParam);
return events;
},
getNextPageParam: (lastPage) => lastPage?.at(-1)?.created_at - 1,
@ -48,7 +46,7 @@ export function Screen() {
refetchOnWindowFocus: false,
});
const renderItem = (event: Event) => {
const renderItem = (event: NostrEvent) => {
if (!event) return;
switch (event.kind) {
case Kind.Repost:

View File

@ -3,7 +3,8 @@ import { Quote } from "@/components/quote";
import { RepostNote } from "@/components/repost";
import { TextNote } from "@/components/text";
import { ArrowRightCircleIcon, ArrowRightIcon } from "@lume/icons";
import { type ColumnRouteSearch, type Event, Kind } from "@lume/types";
import { NostrAccount, NostrQuery } from "@lume/system";
import { type ColumnRouteSearch, type NostrEvent, Kind } from "@lume/types";
import { Spinner } from "@lume/ui";
import { useInfiniteQuery } from "@tanstack/react-query";
import { Link, createFileRoute, redirect } from "@tanstack/react-router";
@ -17,11 +18,10 @@ export const Route = createFileRoute("/group")({
name: search.name,
};
},
beforeLoad: async ({ search, context }) => {
const ark = context.ark;
beforeLoad: async ({ search }) => {
const key = `lume_group_${search.label}`;
const groups = (await ark.get_nstore(key)) as string[];
const settings = await ark.get_settings();
const groups = (await NostrQuery.getNstore(key)) as string[];
const settings = await NostrAccount.getAccounts();
if (!groups?.length) {
throw redirect({
@ -43,7 +43,7 @@ export const Route = createFileRoute("/group")({
export function Screen() {
const { label, account } = Route.useSearch();
const { ark, groups } = Route.useRouteContext();
const { groups } = Route.useRouteContext();
const {
data,
isLoading,
@ -55,7 +55,7 @@ export function Screen() {
queryKey: [label, account],
initialPageParam: 0,
queryFn: async ({ pageParam }: { pageParam: number }) => {
const events = await ark.get_local_events(groups, 20, pageParam);
const events = await NostrQuery.getLocalEvents(groups, pageParam);
return events;
},
getNextPageParam: (lastPage) => lastPage?.at(-1)?.created_at - 1,
@ -64,7 +64,7 @@ export function Screen() {
refetchOnWindowFocus: false,
});
const renderItem = (event: Event) => {
const renderItem = (event: NostrEvent) => {
if (!event) return;
switch (event.kind) {
case Kind.Repost:

View File

@ -7,24 +7,21 @@ import { createFileRoute, redirect } from "@tanstack/react-router";
import { invoke } from "@tauri-apps/api/core";
import { useState } from "react";
import { toast } from "sonner";
import { NostrAccount } from "@lume/system";
export const Route = createFileRoute("/")({
beforeLoad: async ({ context }) => {
// check for app updates
await checkForAppUpdates(true);
beforeLoad: async () => {
const accounts = await NostrAccount.getAccounts();
const ark = context.ark;
const accounts = await ark.get_accounts();
if (!accounts.length) {
if (accounts.length < 1) {
throw redirect({
to: "/landing/",
replace: true,
});
}
// Run notification service
await invoke("run_notification", { accounts });
await checkForAppUpdates(true); // check for app updates
await invoke("run_notification", { accounts }); // Run notification service
return { accounts };
},
@ -33,7 +30,7 @@ export const Route = createFileRoute("/")({
function Screen() {
const navigate = Route.useNavigate();
const { ark, accounts } = Route.useRouteContext();
const context = Route.useRouteContext();
const [loading, setLoading] = useState(false);
@ -41,11 +38,15 @@ function Screen() {
try {
setLoading(true);
const loadAccount = await ark.load_account(npub);
if (loadAccount) {
const status = await NostrAccount.loadAccount(npub);
if (status) {
return navigate({
to: "/$account/home",
params: { account: npub },
search: {
accounts: context.accounts,
},
replace: true,
});
}
@ -75,10 +76,10 @@ function Screen() {
</div>
) : (
<>
{accounts.map((account) => (
{context.accounts.map((account) => (
<button
type="button"
key={account}
type="button"
onClick={() => select(account)}
>
<User.Provider pubkey={account}>

View File

@ -2,11 +2,12 @@ import { Conversation } from "@/components/conversation";
import { Quote } from "@/components/quote";
import { RepostNote } from "@/components/repost";
import { TextNote } from "@/components/text";
import { ArrowRightCircleIcon, ArrowRightIcon } from "@lume/icons";
import { type ColumnRouteSearch, type Event, Kind } from "@lume/types";
import { ArrowRightCircleIcon } from "@lume/icons";
import { NostrAccount, NostrQuery } from "@lume/system";
import { type ColumnRouteSearch, type NostrEvent, Kind } from "@lume/types";
import { Spinner } from "@lume/ui";
import { useInfiniteQuery } from "@tanstack/react-query";
import { Link, redirect } from "@tanstack/react-router";
import { redirect } from "@tanstack/react-router";
import { createFileRoute } from "@tanstack/react-router";
import { Virtualizer } from "virtua";
@ -18,10 +19,9 @@ export const Route = createFileRoute("/newsfeed")({
name: search.name,
};
},
beforeLoad: async ({ search, context }) => {
const ark = context.ark;
const settings = await ark.get_settings();
const contacts = await ark.get_contact_list();
beforeLoad: async ({ search }) => {
const settings = await NostrQuery.getSettings();
const contacts = await NostrAccount.getContactList();
if (!contacts.length) {
throw redirect({
@ -40,7 +40,7 @@ export const Route = createFileRoute("/newsfeed")({
export function Screen() {
const { label, account } = Route.useSearch();
const { ark, contacts } = Route.useRouteContext();
const { contacts } = Route.useRouteContext();
const {
data,
isLoading,
@ -52,7 +52,7 @@ export function Screen() {
queryKey: [label, account],
initialPageParam: 0,
queryFn: async ({ pageParam }: { pageParam: number }) => {
const events = await ark.get_local_events(contacts, 20, pageParam);
const events = await NostrQuery.getLocalEvents(contacts, pageParam);
return events;
},
getNextPageParam: (lastPage) => lastPage?.at(-1)?.created_at - 1,
@ -60,7 +60,7 @@ export function Screen() {
refetchOnWindowFocus: false,
});
const renderItem = (event: Event) => {
const renderItem = (event: NostrEvent) => {
if (!event) return;
switch (event.kind) {
case Kind.Repost:

View File

@ -1,4 +1,5 @@
import { ZapIcon } from "@lume/icons";
import { NostrAccount } from "@lume/system";
import { Container } from "@lume/ui";
import { createLazyFileRoute } from "@tanstack/react-router";
import { useState } from "react";
@ -8,18 +9,16 @@ export const Route = createLazyFileRoute("/nwc")({
});
function Screen() {
const { ark } = Route.useRouteContext();
const [uri, setUri] = useState("");
const [isDone, setIsDone] = useState(false);
const save = async () => {
const nwc = await ark.set_nwc(uri);
const nwc = await NostrAccount.setWallet(uri);
setIsDone(nwc);
};
return (
<Container withDrag withNavigate={false}>
<Container withDrag>
<div className="h-full w-full flex-1 px-5">
{!isDone ? (
<>

View File

@ -1,5 +1,5 @@
import { SearchIcon } from "@lume/icons";
import { type Event, Kind } from "@lume/types";
import { type NostrEvent, Kind } from "@lume/types";
import { Spinner } from "@lume/ui";
import { Note } from "@/components/note";
import { User } from "@/components/user";
@ -7,6 +7,7 @@ import { createFileRoute } from "@tanstack/react-router";
import { useEffect, useState } from "react";
import { toast } from "sonner";
import { useDebounce } from "use-debounce";
import { LumeWindow } from "@lume/system";
export const Route = createFileRoute("/search")({
component: Screen,
@ -14,7 +15,7 @@ export const Route = createFileRoute("/search")({
function Screen() {
const [loading, setLoading] = useState(false);
const [events, setEvents] = useState<Event[]>([]);
const [events, setEvents] = useState<NostrEvent[]>([]);
const [search, setSearch] = useState("");
const [searchValue] = useDebounce(search, 500);
@ -25,7 +26,7 @@ function Screen() {
const query = `https://api.nostr.wine/search?query=${searchValue}&kind=0,1`;
const res = await fetch(query);
const content = await res.json();
const events = content.data as Event[];
const events = content.data as NostrEvent[];
const sorted = events.sort((a, b) => b.created_at - a.created_at);
setLoading(false);
@ -102,14 +103,12 @@ function Screen() {
);
}
function SearchUser({ event }: { event: Event }) {
const { ark } = Route.useRouteContext();
function SearchUser({ event }: { event: NostrEvent }) {
return (
<button
key={event.id}
type="button"
onClick={() => ark.open_profile(event.pubkey)}
onClick={() => LumeWindow.openProfile(event.pubkey)}
className="col-span-1 p-2 hover:bg-black/10 dark:hover:bg-white/10 rounded-lg"
>
<User.Provider pubkey={event.pubkey} embedProfile={event.content}>
@ -125,7 +124,7 @@ function SearchUser({ event }: { event: Event }) {
);
}
function SearchNote({ event }: { event: Event }) {
function SearchNote({ event }: { event: NostrEvent }) {
return (
<div className="bg-white dark:bg-black/20 backdrop-blur-lg rounded-xl shadow-primary dark:ring-1 ring-neutral-800/50">
<Note.Provider event={event}>

View File

@ -1,5 +1,5 @@
import { User } from "@/components/user";
import type { Account } from "@lume/types";
import { NostrAccount } from "@lume/system";
import { displayNsec } from "@lume/utils";
import { createFileRoute } from "@tanstack/react-router";
import { invoke } from "@tauri-apps/api/core";
@ -7,19 +7,20 @@ import { writeText } from "@tauri-apps/plugin-clipboard-manager";
import { useState } from "react";
import { toast } from "sonner";
interface Account {
npub: string;
nsec: string;
}
export const Route = createFileRoute("/settings/backup")({
component: Screen,
loader: async ({ context }) => {
const ark = context.ark;
const npubs = await ark.get_accounts();
loader: async () => {
const npubs = await NostrAccount.getAccounts();
const accounts: Account[] = [];
for (const account of npubs) {
const nsec: string = await invoke("get_stored_nsec", {
npub: account.npub,
});
accounts.push({ ...account, nsec });
for (const npub of npubs) {
const nsec: string = await invoke("get_stored_nsec", { npub });
accounts.push({ npub, nsec });
}
return accounts;
@ -33,14 +34,14 @@ function Screen() {
<div className="mx-auto w-full max-w-xl">
<div className="flex flex-col gap-3 divide-y divide-neutral-300 dark:divide-neutral-700">
{accounts.map((account) => (
<NostrAccount key={account.npub} account={account} />
<List key={account.npub} account={account} />
))}
</div>
</div>
);
}
function NostrAccount({ account }: { account: Account }) {
function List({ account }: { account: Account }) {
const [key, setKey] = useState(account.nsec);
const [copied, setCopied] = useState(false);
const [passphase, setPassphase] = useState("");

View File

@ -1,28 +1,24 @@
import { NostrQuery } from "@lume/system";
import type { Settings } from "@lume/types";
import * as Switch from "@radix-ui/react-switch";
import { createFileRoute } from "@tanstack/react-router";
import {
isPermissionGranted,
requestPermission,
} from "@tauri-apps/plugin-notification";
import { requestPermission } from "@tauri-apps/plugin-notification";
import { useEffect, useState } from "react";
import { useDebouncedCallback } from "use-debounce";
export const Route = createFileRoute("/settings/general")({
beforeLoad: async ({ context }) => {
const permissionGranted = await isPermissionGranted(); // get notification permission
const ark = context.ark;
const settings = await ark.get_settings();
beforeLoad: async () => {
const settings = await NostrQuery.getSettings();
return {
settings: { ...settings, notification: permissionGranted },
settings,
};
},
component: Screen,
});
function Screen() {
const { ark, settings } = Route.useRouteContext();
const { settings } = Route.useRouteContext();
const [newSettings, setNewSettings] = useState<Settings>(settings);
const toggleNofitication = async () => {
@ -62,7 +58,7 @@ function Screen() {
};
const updateSettings = useDebouncedCallback(() => {
ark.set_settings(newSettings);
NostrQuery.setSettings(newSettings);
}, 200);
useEffect(() => {

View File

@ -1,14 +1,13 @@
import { CancelIcon, PlusIcon } from "@lume/icons";
import { NostrQuery } from "@lume/system";
import { createFileRoute } from "@tanstack/react-router";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
export const Route = createFileRoute("/settings/relay")({
loader: async ({ context }) => {
const ark = context.ark;
const relays = await ark.get_relays();
loader: async () => {
const relays = await NostrQuery.getRelays();
return relays;
},
component: Screen,
@ -18,12 +17,11 @@ function Screen() {
const relayList = Route.useLoaderData();
const [relays, setRelays] = useState(relayList.connected);
const { ark } = Route.useRouteContext();
const { register, reset, handleSubmit } = useForm();
const onSubmit = async (data: { url: string }) => {
try {
const add = await ark.add_relay(data.url);
const add = await NostrQuery.connectRelay(data.url);
if (add) {
setRelays((prev) => [...prev, data.url]);
reset();
@ -56,6 +54,7 @@ function Screen() {
<div>
<button
type="button"
onClick={() => NostrQuery.removeRelay(relay)}
className="inline-flex items-center justify-center size-7 rounded-md hover:bg-black/10 dark:hover:bg-white/10"
>
<CancelIcon className="size-4" />

View File

@ -1,5 +1,6 @@
import { AvatarUploader } from "@/components/avatarUploader";
import { PlusIcon } from "@lume/icons";
import { NostrAccount } from "@lume/system";
import type { Metadata } from "@lume/types";
import { Spinner } from "@lume/ui";
import { Link } from "@tanstack/react-router";
@ -9,16 +10,15 @@ import { useForm } from "react-hook-form";
import { toast } from "sonner";
export const Route = createFileRoute("/settings/user")({
beforeLoad: async ({ context }) => {
const ark = context.ark;
const profile = await ark.get_current_user_profile();
beforeLoad: async () => {
const profile = await NostrAccount.getProfile();
return { profile };
},
component: Screen,
});
function Screen() {
const { ark, profile } = Route.useRouteContext();
const { profile } = Route.useRouteContext();
const { register, handleSubmit } = useForm({ defaultValues: profile });
const [loading, setLoading] = useState(false);
@ -29,7 +29,7 @@ function Screen() {
setLoading(true);
const newProfile: Metadata = { ...profile, ...data, picture };
await ark.create_profile(newProfile);
await NostrAccount.createProfile(newProfile);
setLoading(false);
} catch (e) {

View File

@ -3,7 +3,13 @@ import { Quote } from "@/components/quote";
import { RepostNote } from "@/components/repost";
import { TextNote } from "@/components/text";
import { ArrowRightCircleIcon, ArrowRightIcon } from "@lume/icons";
import { type ColumnRouteSearch, type Event, Kind, Topic } from "@lume/types";
import { NostrQuery } from "@lume/system";
import {
type ColumnRouteSearch,
type NostrEvent,
Kind,
Topic,
} from "@lume/types";
import { Spinner } from "@lume/ui";
import { useInfiniteQuery } from "@tanstack/react-query";
import { Link, createFileRoute, redirect } from "@tanstack/react-router";
@ -17,11 +23,10 @@ export const Route = createFileRoute("/topic")({
name: search.name,
};
},
beforeLoad: async ({ search, context }) => {
const ark = context.ark;
beforeLoad: async ({ search }) => {
const key = `lume_topic_${search.label}`;
const topics = (await ark.get_nstore(key)) as unknown as Topic[];
const settings = await ark.get_settings();
const topics = (await NostrQuery.getNstore(key)) as unknown as Topic[];
const settings = await NostrQuery.getSettings();
if (!topics?.length) {
throw redirect({
@ -49,7 +54,7 @@ export const Route = createFileRoute("/topic")({
export function Screen() {
const { label, account } = Route.useSearch();
const { ark, hashtags } = Route.useRouteContext();
const { hashtags } = Route.useRouteContext();
const {
data,
isLoading,
@ -61,7 +66,7 @@ export function Screen() {
queryKey: [label, account],
initialPageParam: 0,
queryFn: async ({ pageParam }: { pageParam: number }) => {
const events = ark.get_hashtag_events(hashtags, 20, pageParam);
const events = NostrQuery.getHashtagEvents(hashtags, pageParam);
return events;
},
getNextPageParam: (lastPage) => lastPage?.at(-1)?.created_at - 1,
@ -69,7 +74,7 @@ export function Screen() {
refetchOnWindowFocus: false,
});
const renderItem = (event: Event) => {
const renderItem = (event: NostrEvent) => {
if (!event) return;
switch (event.kind) {
case Kind.Repost:

View File

@ -1,5 +1,5 @@
import { TextNote } from "@/components/text";
import type { Event } from "@lume/types";
import type { NostrEvent } from "@lume/types";
import { Spinner } from "@lume/ui";
import { Await, createFileRoute } from "@tanstack/react-router";
import { defer } from "@tanstack/react-router";
@ -15,7 +15,7 @@ export const Route = createFileRoute("/trending/notes")({
signal: abortController.signal,
})
.then((res) => res.json())
.then((res) => res.notes.map((item) => item.event) as Event[]),
.then((res) => res.notes.map((item) => item.event) as NostrEvent[]),
),
};
} catch (e) {

View File

@ -1,4 +1,5 @@
import { ArticleIcon, GroupFeedsIcon } from "@lume/icons";
import { NostrQuery } from "@lume/system";
import type { ColumnRouteSearch } from "@lume/types";
import { cn } from "@lume/utils";
import { Link, Outlet } from "@tanstack/react-router";
@ -12,10 +13,8 @@ export const Route = createFileRoute("/trending")({
name: search.name,
};
},
beforeLoad: async ({ context }) => {
const ark = context.ark;
const settings = await ark.get_settings();
beforeLoad: async () => {
const settings = await NostrQuery.getSettings();
return { settings };
},
component: Screen,

View File

@ -6,20 +6,18 @@ import { Conversation } from "@/components/conversation";
import { Quote } from "@/components/quote";
import { RepostNote } from "@/components/repost";
import { TextNote } from "@/components/text";
import { type Event, Kind } from "@lume/types";
import { type NostrEvent, Kind } from "@lume/types";
import { Suspense } from "react";
import { Await } from "@tanstack/react-router";
import { NostrQuery } from "@lume/system";
export const Route = createFileRoute("/users/$pubkey")({
beforeLoad: async ({ context }) => {
const ark = context.ark;
const settings = await ark.get_settings();
beforeLoad: async () => {
const settings = await NostrQuery.getSettings();
return { settings };
},
loader: async ({ params, context }) => {
const ark = context.ark;
return { data: defer(ark.get_events_by(params.pubkey, 50)) };
loader: async ({ params }) => {
return { data: defer(NostrQuery.getUserEvents(params.pubkey)) };
},
component: Screen,
});
@ -28,7 +26,7 @@ function Screen() {
const { pubkey } = Route.useParams();
const { data } = Route.useLoaderData();
const renderItem = (event: Event) => {
const renderItem = (event: NostrEvent) => {
if (!event) return;
switch (event.kind) {
case Kind.Repost:

View File

@ -1,71 +0,0 @@
import { RepostNote } from "@/components/repost";
import { TextNote } from "@/components/text";
import { ArrowRightCircleIcon, InfoIcon } from "@lume/icons";
import { type Event, Kind } from "@lume/types";
import { Spinner } from "@lume/ui";
import { useInfiniteQuery } from "@tanstack/react-query";
import { useRouteContext } from "@tanstack/react-router";
export function EventList({ id }: { id: string }) {
const { ark } = useRouteContext({ strict: false });
const { data, hasNextPage, isLoading, isFetchingNextPage, fetchNextPage } =
useInfiniteQuery({
queryKey: ["events", id],
initialPageParam: 0,
queryFn: async ({ pageParam }: { pageParam: number }) => {
const events = await ark.get_events_by(id, 20, pageParam);
return events;
},
getNextPageParam: (lastPage) => {
const lastEvent = lastPage?.at(-1);
return lastEvent ? lastEvent.created_at - 1 : null;
},
refetchOnWindowFocus: false,
});
const renderItem = (event: Event) => {
if (!event) return;
switch (event.kind) {
case Kind.Repost:
return <RepostNote key={event.id} event={event} />;
default:
return <TextNote key={event.id} event={event} />;
}
};
return (
<div>
{isLoading ? (
<div className="flex h-20 w-full flex-col items-center justify-center gap-1">
<Spinner className="size-5" />
</div>
) : !data.length ? (
<div className="flex items-center gap-2 rounded-xl bg-neutral-50 p-5 dark:bg-neutral-950">
<InfoIcon className="size-6" />
<p>Empty newsfeed.</p>
</div>
) : (
data.map((item) => renderItem(item))
)}
<div className="flex h-20 items-center justify-center">
{hasNextPage ? (
<button
type="button"
onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetchingNextPage}
className="inline-flex h-12 w-36 items-center justify-center gap-2 rounded-full bg-neutral-100 px-3 font-medium hover:bg-neutral-200 focus:outline-none dark:bg-neutral-900 dark:hover:bg-neutral-800"
>
{isFetchingNextPage ? (
<Spinner className="size-5" />
) : (
<>
<ArrowRightCircleIcon className="size-5" />
Load more
</>
)}
</button>
) : null}
</div>
</div>
);
}

View File

@ -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);

View File

@ -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<string>();
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}&quote=${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));
}
}
}

View File

@ -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 };
}

View File

@ -1,3 +0,0 @@
export * from "./ark";
export * from "./hooks/useEvent";
export * from "./hooks/useProfile";

View File

@ -1,5 +1,5 @@
{
"name": "@lume/ark",
"name": "@lume/system",
"version": "0.0.0",
"private": true,
"main": "./src/index.ts",

View File

@ -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);
}
}
}

View File

@ -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<Result<Relays, null>> {
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<Result<boolean, null>> {
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<Result<boolean, null>> {
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<Result<string, string>> {
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<Result<Account, null>> {
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<Result<string, string>> {
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<Result<string, string>> {
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<Result<string, string>> {
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<Result<boolean, string>> {
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<Result<string, null>> {
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<Result<string, null>> {
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<Result<string, null>> {
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<Result<boolean, string>> {
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<Result<null, null>> {
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<Result<string[], string>> {
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<Result<string, string>> {
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<Result<string, string>> {
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<Result<string[], string>> {
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<Result<boolean, string>> {
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<Result<string, string>> {
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<Result<string, string>> {
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<Result<string, string>> {
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<Result<string, string>> {
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<Result<string, string>> {
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<Result<boolean, string>> {
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<Result<boolean, string>> {
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<Result<string, string>> {
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<Result<boolean, string>> {
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<Result<boolean, string>> {
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<Result<boolean, string>> {
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<Result<string, string>> {
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<Result<string[], string>> {
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<Result<string[], string>> {
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<Result<string[], string>> {
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<Result<string[], string>> {
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<Result<string[], string>> {
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<Result<string, string>> {
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<Result<string, string>> {
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<void> {
await TAURI_INVOKE("show_in_folder", { path });
},
async createColumn(
label: string,
x: number,
y: number,
width: number,
height: number,
url: string,
): Promise<Result<string, string>> {
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<Result<boolean, null>> {
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<Result<null, string>> {
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<Result<null, string>> {
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<Result<null, string>> {
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<void> {
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__<T> = {
listen: (
cb: TAURI_API_EVENT.EventCallback<T>,
) => ReturnType<typeof TAURI_API_EVENT.listen<T>>;
once: (
cb: TAURI_API_EVENT.EventCallback<T>,
) => ReturnType<typeof TAURI_API_EVENT.once<T>>;
emit: T extends null
? (payload?: T) => ReturnType<typeof TAURI_API_EVENT.emit>
: (payload: T) => ReturnType<typeof TAURI_API_EVENT.emit>;
};
export type Result<T, E> =
| { status: "ok"; data: T }
| { status: "error"; error: E };
function __makeEvents__<T extends Record<string, any>>(
mappings: Record<keyof T, string>,
) {
return new Proxy(
{} as unknown as {
[K in keyof T]: __EventObj__<T[K]> & {
(handle: __WebviewWindow__): __EventObj__<T[K]>;
};
},
{
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__<any>) => {
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);
}
},
});
},
},
);
}

View File

@ -0,0 +1,28 @@
import { NostrEvent } from "@lume/types";
export function dedupEvents(nostrEvents: NostrEvent[], nsfw: boolean = false) {
const seens = new Set<string>();
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;
}

View File

@ -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);
}
}
}

View File

@ -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);

View File

@ -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,
};
}

View File

@ -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);
}

View File

@ -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";

View File

@ -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);
}
}
}
}

View File

@ -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}&quote=${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);
}
}
}

View File

@ -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<Event>;
export interface EventWithReplies extends NostrEvent {
replies: Array<NostrEvent>;
}
export interface Metadata {

View File

@ -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:^",

View File

@ -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'}

74
src-tauri/Cargo.lock generated
View File

@ -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"

View File

@ -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"

View File

@ -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/*"
}
]
}
]
}

View File

@ -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"]}}
{"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"]}}

View File

@ -1,3 +1,2 @@
pub mod folder;
pub mod opg;
pub mod window;

View File

@ -1,6 +1,7 @@
use std::process::Command;
#[tauri::command]
#[specta::specta]
pub async fn show_in_folder(path: String) {
#[cfg(target_os = "windows")]
{

View File

@ -1,16 +0,0 @@
use std::time::Duration;
use webpage::{Opengraph, Webpage, WebpageOptions};
#[tauri::command]
pub fn fetch_opg(url: String) -> Result<Opengraph, String> {
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())
}
}

View File

@ -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<bool, ()> {
match app_handle.get_webview(label) {
Some(webview) => {
@ -59,6 +61,7 @@ pub fn close_column(label: &str, app_handle: tauri::AppHandle) -> Result<bool, (
}
#[tauri::command]
#[specta::specta]
pub fn reposition_column(
label: &str,
x: f32,
@ -78,6 +81,7 @@ pub fn reposition_column(
}
#[tauri::command]
#[specta::specta]
pub fn resize_column(
label: &str,
width: f32,
@ -97,6 +101,7 @@ pub fn resize_column(
}
#[tauri::command]
#[specta::specta]
pub fn open_window(
label: &str,
title: &str,
@ -166,6 +171,7 @@ pub fn open_window(
}
#[tauri::command]
#[specta::specta]
pub fn set_badge(count: i32) {
#[cfg(target_os = "macos")]
unsafe {

View File

@ -24,6 +24,61 @@ pub struct Nostr {
}
fn main() {
let invoke_handler = {
let builder = tauri_specta::ts::builder().commands(tauri_specta::collect_commands![
nostr::relay::get_relays,
nostr::relay::connect_relay,
nostr::relay::remove_relay,
nostr::keys::get_accounts,
nostr::keys::create_account,
nostr::keys::save_account,
nostr::keys::get_encrypted_key,
nostr::keys::nostr_connect,
nostr::keys::load_account,
nostr::keys::event_to_bech32,
nostr::keys::user_to_bech32,
nostr::keys::to_npub,
nostr::keys::verify_nip05,
nostr::metadata::run_notification,
nostr::metadata::get_activities,
nostr::metadata::get_current_user_profile,
nostr::metadata::get_profile,
nostr::metadata::get_contact_list,
nostr::metadata::set_contact_list,
nostr::metadata::create_profile,
nostr::metadata::follow,
nostr::metadata::unfollow,
nostr::metadata::get_nstore,
nostr::metadata::set_nstore,
nostr::metadata::set_nwc,
nostr::metadata::load_nwc,
nostr::metadata::get_balance,
nostr::metadata::zap_profile,
nostr::metadata::zap_event,
nostr::metadata::friend_to_friend,
nostr::event::get_event,
nostr::event::get_replies,
nostr::event::get_events_by,
nostr::event::get_local_events,
nostr::event::get_global_events,
nostr::event::get_hashtag_events,
nostr::event::publish,
nostr::event::repost,
commands::folder::show_in_folder,
commands::window::create_column,
commands::window::close_column,
commands::window::reposition_column,
commands::window::resize_column,
commands::window::open_window,
commands::window::set_badge
]);
#[cfg(debug_assertions)]
let builder = builder.path("../packages/system/src/commands.ts");
builder.build().unwrap()
};
tauri::Builder::default()
.setup(|app| {
let main_window = app.get_webview_window("main").unwrap();
@ -98,55 +153,7 @@ fn main() {
.plugin(tauri_plugin_shell::init())
.plugin(tauri_plugin_upload::init())
.plugin(tauri_plugin_updater::Builder::new().build())
.invoke_handler(tauri::generate_handler![
nostr::relay::get_relays,
nostr::relay::connect_relay,
nostr::relay::remove_relay,
nostr::keys::get_accounts,
nostr::keys::create_account,
nostr::keys::save_account,
nostr::keys::get_encrypted_key,
nostr::keys::nostr_connect,
nostr::keys::load_account,
nostr::keys::event_to_bech32,
nostr::keys::user_to_bech32,
nostr::keys::to_npub,
nostr::keys::verify_nip05,
nostr::metadata::run_notification,
nostr::metadata::get_activities,
nostr::metadata::get_current_user_profile,
nostr::metadata::get_profile,
nostr::metadata::get_contact_list,
nostr::metadata::set_contact_list,
nostr::metadata::create_profile,
nostr::metadata::follow,
nostr::metadata::unfollow,
nostr::metadata::get_nstore,
nostr::metadata::set_nstore,
nostr::metadata::set_nwc,
nostr::metadata::load_nwc,
nostr::metadata::get_balance,
nostr::metadata::zap_profile,
nostr::metadata::zap_event,
nostr::metadata::friend_to_friend,
nostr::event::get_event,
nostr::event::get_thread,
nostr::event::get_events_by,
nostr::event::get_local_events,
nostr::event::get_global_events,
nostr::event::get_hashtag_events,
nostr::event::get_group_events,
nostr::event::publish,
nostr::event::repost,
commands::folder::show_in_folder,
commands::window::create_column,
commands::window::close_column,
commands::window::reposition_column,
commands::window::resize_column,
commands::window::open_window,
commands::window::set_badge,
commands::opg::fetch_opg,
])
.invoke_handler(invoke_handler)
.run(tauri::generate_context!())
.expect("error while running tauri application")
}

View File

@ -4,6 +4,7 @@ use std::{str::FromStr, time::Duration};
use tauri::State;
#[tauri::command]
#[specta::specta]
pub async fn get_event(id: &str, state: State<'_, Nostr>) -> Result<String, String> {
let client = &state.client;
let event_id: Option<EventId> = match Nip19::from_bech32(id) {
@ -49,7 +50,8 @@ pub async fn get_event(id: &str, state: State<'_, Nostr>) -> Result<String, Stri
}
#[tauri::command]
pub async fn get_thread(id: &str, state: State<'_, Nostr>) -> Result<Vec<Event>, String> {
#[specta::specta]
pub async fn get_replies(id: &str, state: State<'_, Nostr>) -> Result<Vec<String>, 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<Vec<Event>,
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<Vec<Event>,
}
#[tauri::command]
#[specta::specta]
pub async fn get_events_by(
public_key: &str,
limit: usize,
as_of: Option<&str>,
state: State<'_, Nostr>,
) -> Result<Vec<Event>, String> {
) -> Result<Vec<String>, 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<String>,
limit: usize,
until: Option<&str>,
state: State<'_, Nostr>,
) -> Result<Vec<Event>, String> {
) -> Result<Vec<String>, 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<Vec<Event>, String> {
) -> Result<Vec<String>, 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<Vec<Event>, String> {
) -> Result<Vec<String>, 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<Vec<Event>, String> {
let client = &state.client;
let as_of = match until {
Some(until) => Timestamp::from_str(until).unwrap(),
None => Timestamp::now(),
};
let authors: Vec<PublicKey> = 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<Vec<&str>>,
@ -226,12 +201,13 @@ pub async fn publish(
}
#[tauri::command]
pub async fn repost(raw: &str, state: State<'_, Nostr>) -> Result<EventId, String> {
#[specta::specta]
pub async fn repost(raw: &str, state: State<'_, Nostr>) -> Result<String, String> {
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()),
}
}

View File

@ -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<String, String> {
let search = Search::new().unwrap();
let results = search.by("Account", "nostr_secret");
@ -24,6 +27,7 @@ pub fn get_accounts() -> Result<String, String> {
}
#[tauri::command]
#[specta::specta]
pub fn create_account() -> Result<Account, ()> {
let keys = Keys::generate();
let public_key = keys.public_key();
@ -38,6 +42,7 @@ pub fn create_account() -> Result<Account, ()> {
}
#[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<bool, String> {
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<bool, S
}
#[tauri::command]
#[specta::specta]
pub async fn nostr_connect(
npub: &str,
uri: &str,
@ -203,6 +210,7 @@ pub async fn nostr_connect(
}
#[tauri::command(async)]
#[specta::specta]
pub fn get_encrypted_key(npub: &str, password: &str) -> Result<String, String> {
let keyring = Entry::new(npub, "nostr_secret").unwrap();
@ -221,6 +229,7 @@ pub fn get_encrypted_key(npub: &str, password: &str) -> Result<String, String> {
}
#[tauri::command]
#[specta::specta]
pub fn event_to_bech32(id: &str, relays: Vec<String>) -> Result<String, ()> {
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<String>) -> Result<String, ()> {
}
#[tauri::command]
#[specta::specta]
pub fn user_to_bech32(key: &str, relays: Vec<String>) -> Result<String, ()> {
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<String>) -> Result<String, ()> {
}
#[tauri::command]
#[specta::specta]
pub fn to_npub(hex: &str) -> Result<String, ()> {
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<String, ()> {
}
#[tauri::command]
#[specta::specta]
pub async fn verify_nip05(key: &str, nip05: &str) -> Result<bool, String> {
match PublicKey::from_str(key) {
Ok(public_key) => {

View File

@ -6,6 +6,7 @@ use tauri::{Manager, State};
use url::Url;
#[tauri::command]
#[specta::specta]
pub fn run_notification(accounts: Vec<String>, 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<String>, app: tauri::AppHandle) -> Result<
}
#[tauri::command]
#[specta::specta]
pub async fn get_activities(
account: &str,
kind: &str,
state: State<'_, Nostr>,
) -> Result<Vec<Event>, String> {
) -> Result<Vec<String>, 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<bool, String> {
let client = &state.client;
@ -114,7 +117,8 @@ pub async fn friend_to_friend(npub: &str, state: State<'_, Nostr>) -> Result<boo
}
#[tauri::command]
pub async fn get_current_user_profile(state: State<'_, Nostr>) -> Result<Metadata, String> {
#[specta::specta]
pub async fn get_current_user_profile(state: State<'_, Nostr>) -> Result<String, String> {
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<Metadat
Ok(events) => {
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<Metadat
}
#[tauri::command]
pub async fn get_profile(id: &str, state: State<'_, Nostr>) -> Result<Metadata, String> {
#[specta::specta]
pub async fn get_profile(id: &str, state: State<'_, Nostr>) -> Result<String, String> {
let client = &state.client;
let public_key: Option<PublicKey> = match Nip19::from_bech32(id) {
Ok(val) => match val {
@ -166,13 +171,13 @@ pub async fn get_profile(id: &str, state: State<'_, Nostr>) -> Result<Metadata,
if let Ok(events) = query {
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())
}
} else {
let rand_metadata = Metadata::new();
Ok(rand_metadata)
Ok(rand_metadata.as_json())
}
} else {
Err("Get metadata failed".into())
@ -183,6 +188,7 @@ pub async fn get_profile(id: &str, state: State<'_, Nostr>) -> Result<Metadata,
}
#[tauri::command]
#[specta::specta]
pub async fn set_contact_list(pubkeys: Vec<&str>, state: State<'_, Nostr>) -> Result<bool, String> {
let client = &state.client;
let contact_list: Vec<Contact> = 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<Vec<String>, String> {
let client = &state.client;
@ -218,6 +225,7 @@ pub async fn get_contact_list(state: State<'_, Nostr>) -> Result<Vec<String>, 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<EventId, String> {
) -> Result<String, String> {
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<EventId, String> {
) -> Result<String, String> {
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<EventId, String> {
#[specta::specta]
pub async fn unfollow(id: &str, state: State<'_, Nostr>) -> Result<String, String> {
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<EventId, String> {
) -> Result<String, String> {
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<String, String> {
let client = &state.client;
@ -366,6 +378,7 @@ pub async fn get_nstore(key: &str, state: State<'_, Nostr>) -> Result<String, St
}
#[tauri::command]
#[specta::specta]
pub async fn set_nwc(uri: &str, state: State<'_, Nostr>) -> Result<bool, String> {
let client = &state.client;
@ -385,6 +398,7 @@ pub async fn set_nwc(uri: &str, state: State<'_, Nostr>) -> Result<bool, String>
}
#[tauri::command]
#[specta::specta]
pub async fn load_nwc(state: State<'_, Nostr>) -> Result<bool, String> {
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<bool, String> {
}
#[tauri::command]
pub async fn get_balance() -> Result<u64, String> {
#[specta::specta]
pub async fn get_balance() -> Result<String, String> {
let keyring = Entry::new("Lume Secret Storage", "NWC").unwrap();
match keyring.get_password() {
@ -412,7 +427,7 @@ pub async fn get_balance() -> Result<u64, String> {
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<u64, String> {
}
#[tauri::command]
#[specta::specta]
pub async fn zap_profile(
id: &str,
amount: u64,
amount: &str,
message: &str,
state: State<'_, Nostr>,
) -> Result<bool, String> {
@ -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::<u64>().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<bool, String> {
@ -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::<u64>().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())

View File

@ -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<String>,
read: Option<Vec<String>>,
@ -11,6 +13,7 @@ pub struct Relays {
}
#[tauri::command]
#[specta::specta]
pub async fn get_relays(state: State<'_, Nostr>) -> Result<Relays, ()> {
let client = &state.client;
@ -73,6 +76,7 @@ pub async fn get_relays(state: State<'_, Nostr>) -> Result<Relays, ()> {
}
#[tauri::command]
#[specta::specta]
pub async fn connect_relay(relay: &str, state: State<'_, Nostr>) -> Result<bool, ()> {
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<bool,
}
#[tauri::command]
#[specta::specta]
pub async fn remove_relay(relay: &str, state: State<'_, Nostr>) -> Result<bool, ()> {
let client = &state.client;
if let Ok(_) = client.remove_relay(relay).await {