chore: clean up

This commit is contained in:
reya
2024-08-28 08:48:17 +07:00
parent f6eb5eea44
commit d128af1db8
27 changed files with 311 additions and 741 deletions

View File

@@ -6,7 +6,7 @@ use std::{str::FromStr, time::Duration};
use tauri::State;
use tauri_specta::Event;
use crate::{NewSettings, Nostr, Settings};
use crate::{common::get_latest_event, NewSettings, Nostr, Settings};
#[derive(Clone, Serialize, Deserialize, Type)]
pub struct Profile {
@@ -229,14 +229,14 @@ pub async fn get_lume_store(key: String, state: State<'_, Nostr>) -> Result<Stri
.author(public_key)
.kind(Kind::ApplicationSpecificData)
.identifier(key)
.limit(1);
.limit(10);
match client
.get_events_of(vec![filter], EventSource::Database)
.await
{
Ok(events) => {
if let Some(event) = events.first() {
if let Some(event) = get_latest_event(&events) {
match signer.nip44_decrypt(public_key, event.content()).await {
Ok(decrypted) => Ok(decrypted),
Err(_) => Err(event.content.to_string()),

View File

@@ -4,7 +4,8 @@ import type {
PersistedQuery,
} from "@tanstack/query-persist-client-core";
import { Store } from "@tanstack/store";
import { ask, message } from "@tauri-apps/plugin-dialog";
import { ask, message, open } from "@tauri-apps/plugin-dialog";
import { readFile } from "@tauri-apps/plugin-fs";
import { relaunch } from "@tauri-apps/plugin-process";
import type { Store as TauriStore } from "@tauri-apps/plugin-store";
import { check } from "@tauri-apps/plugin-updater";
@@ -241,6 +242,61 @@ export async function checkForAppUpdates(silent: boolean) {
}
}
export async function 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));
}
}
export function toLumeEvents(richEvents: RichEvent[]) {
const events = richEvents.map((item) => {
const nostrEvent: NostrEvent = JSON.parse(item.raw);

View File

@@ -1,10 +1,12 @@
import { appSettings, cn } from "@/commons";
import { LumeWindow } from "@/system";
import { Lightning } from "@phosphor-icons/react";
import { useSearch } from "@tanstack/react-router";
import { useStore } from "@tanstack/react-store";
import { useNoteContext } from "../provider";
export function NoteZap({ large = false }: { large?: boolean }) {
const search = useSearch({ strict: false });
const visible = useStore(appSettings, (state) => state.display_zap_button);
const event = useNoteContext();
@@ -13,7 +15,7 @@ export function NoteZap({ large = false }: { large?: boolean }) {
return (
<button
type="button"
onClick={() => LumeWindow.openZap(event.id)}
onClick={() => LumeWindow.openZap(event.id, search.account)}
className={cn(
"inline-flex items-center justify-center text-neutral-800 dark:text-neutral-200",
large

View File

@@ -2,8 +2,7 @@ import { cn } from "@/commons";
import { Spinner } from "@/components";
import { Note } from "@/components/note";
import { User } from "@/components/user";
import { type LumeEvent, NostrQuery } from "@/system";
import { useQuery } from "@tanstack/react-query";
import { type LumeEvent, useEvent } from "@/system";
import { memo } from "react";
export const RepostNote = memo(function RepostNote({
@@ -13,22 +12,7 @@ export const RepostNote = memo(function RepostNote({
event: LumeEvent;
className?: string;
}) {
const { isLoading, isError, data } = useQuery({
queryKey: ["event", event.repostId],
queryFn: async () => {
try {
const data = await NostrQuery.getRepostEvent(event);
return data;
} catch (e) {
throw new Error(e);
}
},
refetchOnWindowFocus: false,
refetchOnMount: false,
refetchOnReconnect: false,
staleTime: Number.POSITIVE_INFINITY,
retry: 2,
});
const { isLoading, isError, data } = useEvent(event.repostId);
return (
<Note.Root

View File

@@ -1,21 +1,34 @@
import { commands } from "@/commands.gen";
import { cn } from "@/commons";
import { Spinner } from "@/components";
import { NostrAccount } from "@/system";
import { useQuery } from "@tanstack/react-query";
import { useRouteContext } from "@tanstack/react-router";
import { message } from "@tauri-apps/plugin-dialog";
import { useEffect, useState, useTransition } from "react";
import { useTransition } from "react";
import { useUserContext } from "./provider";
export function UserFollowButton({
simple = false,
className,
}: {
simple?: boolean;
className?: string;
}) {
export function UserFollowButton({ className }: { className?: string }) {
const user = useUserContext();
const [followed, setFollowed] = useState(false);
const { queryClient } = useRouteContext({ strict: false });
const {
isLoading,
isError,
data: isFollow,
} = useQuery({
queryKey: ["status", user.pubkey],
queryFn: async () => {
const res = await commands.checkContact(user.pubkey);
if (res.status === "ok") {
return res.data;
} else {
throw new Error(res.error);
}
},
refetchOnWindowFocus: false,
});
const [isPending, startTransition] = useTransition();
const toggleFollow = () => {
@@ -23,7 +36,17 @@ export function UserFollowButton({
const res = await commands.toggleContact(user.pubkey, null);
if (res.status === "ok") {
setFollowed((prev) => !prev);
queryClient.setQueryData(
["status", user.pubkey],
(prev: boolean) => !prev,
);
// invalidate cache
await queryClient.invalidateQueries({
queryKey: ["status", user.pubkey],
});
return;
} else {
await message(res.error, { kind: "error" });
return;
@@ -31,18 +54,6 @@ export function UserFollowButton({
});
};
useEffect(() => {
let mounted = true;
NostrAccount.checkContact(user.pubkey).then((status) => {
if (mounted) setFollowed(status);
});
return () => {
mounted = false;
};
}, []);
return (
<button
type="button"
@@ -50,15 +61,9 @@ export function UserFollowButton({
onClick={() => toggleFollow()}
className={cn("w-max", className)}
>
{isPending ? (
<Spinner className="size-4" />
) : followed ? (
!simple ? (
"Unfollow"
) : null
) : (
"Follow"
)}
{isError ? "Error" : null}
{isPending || isLoading ? <Spinner className="size-4" /> : null}
{isFollow ? "Unfollow" : "Follow"}
</button>
);
}

View File

@@ -1,5 +1,5 @@
import { commands } from "@/commands.gen";
import { displayLongHandle, displayNpub } from "@/commons";
import { NostrQuery } from "@/system";
import { SealCheck } from "@phosphor-icons/react";
import * as Tooltip from "@radix-ui/react-tooltip";
import { useQuery } from "@tanstack/react-query";
@@ -10,14 +10,13 @@ export function UserNip05() {
const { isLoading, data: verified } = useQuery({
queryKey: ["nip05", user?.pubkey],
queryFn: async () => {
if (!user.profile?.nip05?.length) return false;
const res = await commands.verifyNip05(user.pubkey, user.profile?.nip05);
const verify = await NostrQuery.verifyNip05(
user.pubkey,
user.profile?.nip05,
);
return verify;
if (res.status === "ok") {
return res.data;
} else {
throw new Error(res.error);
}
},
enabled: !!user.profile?.nip05?.length,
refetchOnMount: false,

View File

@@ -1,6 +1,7 @@
import { commands } from "@/commands.gen";
import { Spinner } from "@/components";
import { Column } from "@/components/column";
import { LumeWindow, NostrQuery } from "@/system";
import { LumeWindow } from "@/system";
import type { ColumnEvent, LumeColumn } from "@/types";
import { ArrowLeft, ArrowRight, Plus, StackPlus } from "@phosphor-icons/react";
import { createLazyFileRoute } from "@tanstack/react-router";
@@ -24,7 +25,7 @@ export const Route = createLazyFileRoute("/$account/_app/home")({
});
function Screen() {
const initialColumnList = Route.useLoaderData();
const { initialColumns } = Route.useRouteContext();
const [columns, setColumns] = useState<LumeColumn[]>([]);
const [emblaRef, emblaApi] = useEmblaCarousel({
@@ -105,6 +106,18 @@ function Screen() {
event.preventDefault();
}, 150);
const saveAllColumns = useDebouncedCallback(async () => {
const key = "lume_v4:columns";
const content = JSON.stringify(columns);
const res = await commands.setLumeStore(key, content);
if (res.status === "ok") {
return res.data;
} else {
console.log(res.error);
}
}, 200);
useEffect(() => {
if (emblaApi) {
emblaApi.on("scroll", emitScrollEvent);
@@ -120,14 +133,12 @@ function Screen() {
}, [emblaApi, emitScrollEvent, emitResizeEvent]);
useEffect(() => {
if (columns) {
NostrQuery.setColumns(columns).then(() => console.log("saved"));
}
if (columns) saveAllColumns();
}, [columns]);
useEffect(() => {
setColumns(initialColumnList);
}, [initialColumnList]);
setColumns(initialColumns);
}, [initialColumns]);
// Listen for keyboard event
useEffect(() => {

View File

@@ -3,16 +3,18 @@ import type { LumeColumn } from "@/types";
import { createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute("/$account/_app/home")({
loader: async ({ context }) => {
beforeLoad: async ({ context }) => {
const key = "lume_v4:columns";
const defaultColumns = context.systemColumns.filter((col) => col.default);
const query = await commands.getLumeStore(key);
let initialColumns: LumeColumn[] = defaultColumns;
if (query.status === "ok") {
const columns: LumeColumn[] = JSON.parse(query.data);
return columns;
} else {
return defaultColumns;
initialColumns = JSON.parse(query.data);
return { initialColumns };
}
return { initialColumns };
},
});

View File

@@ -1,3 +1,4 @@
import { commands } from "@/commands.gen";
import { NostrAccount } from "@/system";
import { Button } from "@getalby/bitcoin-connect-react";
import { createLazyFileRoute } from "@tanstack/react-router";
@@ -11,8 +12,13 @@ export const Route = createLazyFileRoute("/$account/_settings/bitcoin-connect")(
function Screen() {
const setNwcUri = async (uri: string) => {
const cmd = await NostrAccount.setWallet(uri);
if (cmd) getCurrentWebviewWindow().close();
const res = await commands.setWallet(uri);
if (res.status === "ok") {
await getCurrentWebviewWindow().close();
} else {
throw new Error(res.error);
}
};
return (

View File

@@ -1,8 +1,6 @@
import { commands } from "@/commands.gen";
import { cn } from "@/commons";
import { type Profile, commands } from "@/commands.gen";
import { cn, upload } from "@/commons";
import { Spinner } from "@/components";
import { NostrAccount, NostrQuery } from "@/system";
import type { Metadata } from "@/types";
import { Plus } from "@phosphor-icons/react";
import { createLazyFileRoute } from "@tanstack/react-router";
import { writeText } from "@tauri-apps/plugin-clipboard-manager";
@@ -27,15 +25,16 @@ function Screen() {
const [isPending, startTransition] = useTransition();
const [picture, setPicture] = useState<string>("");
const onSubmit = (data: Metadata) => {
const onSubmit = (data: Profile) => {
startTransition(async () => {
try {
const newProfile: Metadata = { ...profile, ...data, picture };
await NostrAccount.createProfile(newProfile);
} catch (e) {
await message(String(e), { title: "Profile", kind: "error" });
return;
const newProfile: Profile = { ...profile, ...data, picture };
const res = await commands.setProfile(newProfile);
if (res.status === "error") {
await message(res.error, { title: "Profile", kind: "error" });
}
return;
});
};
@@ -220,17 +219,18 @@ function AvatarUploader({
children: ReactNode;
className?: string;
}) {
const [loading, setLoading] = useState(false);
const [isPending, startTransition] = useTransition();
const uploadAvatar = async () => {
try {
setLoading(true);
const image = await NostrQuery.upload();
setPicture(image);
} catch (e) {
setLoading(false);
await message(String(e), { title: "Lume", kind: "error" });
}
const uploadAvatar = () => {
startTransition(async () => {
try {
const image = await upload();
setPicture(image);
} catch (e) {
await message(String(e));
return;
}
});
};
return (
@@ -239,7 +239,7 @@ function AvatarUploader({
onClick={() => uploadAvatar()}
className={cn("size-4", className)}
>
{loading ? <Spinner className="size-4" /> : children}
{isPending ? <Spinner className="size-4" /> : children}
</button>
);
}

View File

@@ -1,5 +1,4 @@
import { commands } from "@/commands.gen";
import type { Metadata } from "@/types";
import { type Profile, commands } from "@/commands.gen";
import { createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute("/$account/_settings/profile")({
@@ -7,7 +6,7 @@ export const Route = createFileRoute("/$account/_settings/profile")({
const res = await commands.getProfile(params.account);
if (res.status === "ok") {
const profile: Metadata = JSON.parse(res.data);
const profile: Profile = JSON.parse(res.data);
return { profile };
} else {
throw new Error(res.error);

View File

@@ -1,5 +1,4 @@
import { commands } from "@/commands.gen";
import { NostrQuery } from "@/system";
import { Plus, X } from "@phosphor-icons/react";
import { createLazyFileRoute } from "@tanstack/react-router";
import { message } from "@tauri-apps/plugin-dialog";
@@ -16,6 +15,16 @@ function Screen() {
const [newRelay, setNewRelay] = useState("");
const [isPending, startTransition] = useTransition();
const removeRelay = async (relay: string) => {
const res = await commands.removeRelay(relay);
if (res.status === "ok") {
return res.data;
} else {
throw new Error(res.error);
}
};
const addNewRelay = () => {
startTransition(async () => {
try {
@@ -69,7 +78,7 @@ function Screen() {
<div>
<button
type="button"
onClick={() => NostrQuery.removeRelay(relay)}
onClick={() => removeRelay(relay)}
className="inline-flex items-center justify-center rounded-md size-7 hover:bg-black/10 dark:hover:bg-white/10"
>
<X className="size-4" />

View File

@@ -1,4 +1,4 @@
import { NostrAccount } from "@/system";
import { commands } from "@/commands.gen";
import { createLazyFileRoute, redirect } from "@tanstack/react-router";
export const Route = createLazyFileRoute("/$account/_settings/wallet")({
@@ -10,10 +10,14 @@ function Screen() {
const { balance } = Route.useRouteContext();
const disconnect = async () => {
window.localStorage.removeItem("bc:config");
await NostrAccount.removeWallet();
const res = await commands.removeWallet();
return redirect({ to: "/$account/bitcoin-connect", params: { account } });
if (res.status === "ok") {
window.localStorage.removeItem("bc:config");
return redirect({ to: "/$account/bitcoin-connect", params: { account } });
} else {
throw new Error(res.error);
}
};
return (

View File

@@ -1,6 +1,6 @@
import { commands } from "@/commands.gen";
import { upload } from "@/commons";
import { Frame, GoBack, Spinner } from "@/components";
import { NostrQuery } from "@/system";
import { Plus } from "@phosphor-icons/react";
import { createLazyFileRoute } from "@tanstack/react-router";
import { message } from "@tauri-apps/plugin-dialog";
@@ -20,7 +20,7 @@ function Screen() {
const [isPending, startTransition] = useTransition();
const uploadAvatar = async () => {
const file = await NostrQuery.upload();
const file = await upload();
if (file) {
setPicture(file);

View File

@@ -1,9 +1,14 @@
import { NostrAccount } from "@/system";
import { commands } from "@/commands.gen";
import { createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute("/columns/_layout/create-group")({
loader: async () => {
const contacts = await NostrAccount.getContactList();
return contacts;
const res = await commands.getContactList();
if (res.status === "ok") {
return res.data;
} else {
throw new Error(res.error);
}
},
});

View File

@@ -56,9 +56,9 @@ function Screen() {
<button
type="button"
onClick={() => install(column)}
className="text-xs uppercase font-semibold w-max h-7 pl-2.5 pr-2 hidden group-hover:inline-flex items-center justify-center rounded-full bg-neutral-200 hover:bg-blue-500 hover:text-white dark:bg-black/10"
className="text-xs uppercase font-semibold w-16 h-7 hidden group-hover:inline-flex items-center justify-center rounded-full bg-neutral-200 hover:bg-blue-500 hover:text-white dark:bg-black/10"
>
Install
Open
</button>
</div>
))}

View File

@@ -1,10 +1,8 @@
import { Spinner } from "@/components";
import { Conversation } from "@/components/conversation";
import { Quote } from "@/components/quote";
import { RepostNote } from "@/components/repost";
import { TextNote } from "@/components/text";
import { type LumeEvent, NostrQuery } from "@/system";
import { type ColumnRouteSearch, Kind } from "@/types";
import { commands } from "@/commands.gen";
import { toLumeEvents } from "@/commons";
import { Quote, RepostNote, Spinner, TextNote } from "@/components";
import type { LumeEvent } from "@/system";
import { Kind } from "@/types";
import { ArrowCircleRight } from "@phosphor-icons/react";
import * as ScrollArea from "@radix-ui/react-scroll-area";
import { useInfiniteQuery } from "@tanstack/react-query";
@@ -13,17 +11,6 @@ import { useCallback, useRef } from "react";
import { Virtualizer } from "virtua";
export const Route = createFileRoute("/columns/_layout/global")({
validateSearch: (search: Record<string, string>): ColumnRouteSearch => {
return {
account: search.account,
label: search.label,
name: search.name,
};
},
beforeLoad: async () => {
const settings = await NostrQuery.getUserSettings();
return { settings };
},
component: Screen,
});
@@ -40,8 +27,14 @@ export function Screen() {
queryKey: [label, account],
initialPageParam: 0,
queryFn: async ({ pageParam }: { pageParam: number }) => {
const events = await NostrQuery.getGlobalEvents(pageParam);
return events;
const until = pageParam > 0 ? pageParam.toString() : undefined;
const res = await commands.getGlobalEvents(until);
if (res.status === "error") {
throw new Error(res.error);
}
return toLumeEvents(res.data);
},
getNextPageParam: (lastPage) => lastPage?.at(-1)?.created_at - 1,
select: (data) => data?.pages.flat(),
@@ -57,15 +50,11 @@ export function Screen() {
case Kind.Repost:
return <RepostNote key={event.id} event={event} className="mb-3" />;
default: {
if (event.isConversation) {
return (
<Conversation key={event.id} className="mb-3" event={event} />
);
}
if (event.isQuote) {
return <Quote key={event.id} event={event} className="mb-3" />;
} else {
return <TextNote key={event.id} event={event} className="mb-3" />;
}
return <TextNote key={event.id} event={event} className="mb-3" />;
}
}
},
@@ -81,12 +70,10 @@ export function Screen() {
<ScrollArea.Viewport ref={ref} className="h-full px-3 pb-3">
<Virtualizer scrollRef={ref}>
{isFetching && !isLoading && !isFetchingNextPage ? (
<div className="flex items-center justify-center w-full mb-3 h-12 bg-black/5 dark:bg-white/5 rounded-xl">
<div className="flex items-center justify-center gap-2">
<Spinner className="size-5" />
<span className="text-sm font-medium">
Getting new notes...
</span>
<div className="z-50 fixed top-0 left-0 w-full h-14 flex items-center justify-center px-3">
<div className="w-max h-8 pl-2 pr-3 inline-flex items-center justify-center gap-1.5 rounded-full shadow-lg text-sm font-medium text-white bg-black dark:text-black dark:bg-white">
<Spinner className="size-4" />
Getting new notes...
</div>
</div>
) : null}
@@ -96,13 +83,13 @@ export function Screen() {
<span className="text-sm font-medium">Loading...</span>
</div>
) : !data.length ? (
<div className="flex items-center justify-center">
Yo. You're catching up on all the things happening around you.
<div className="mb-3 flex items-center justify-center h-20 text-sm rounded-xl bg-black/5 dark:bg-white/5">
🎉 Yo. You're catching up on all latest notes.
</div>
) : (
data.map((item) => renderItem(item))
)}
{data?.length && hasNextPage ? (
{hasNextPage ? (
<div>
<button
type="button"

View File

@@ -1,9 +1,8 @@
import { commands } from "@/commands.gen";
import { decodeZapInvoice, formatCreatedAt } from "@/commons";
import { Spinner } from "@/components";
import { Note } from "@/components/note";
import { User } from "@/components/user";
import { type LumeEvent, NostrQuery, useEvent } from "@/system";
import { Kind } from "@/types";
import { Note, Spinner, User } from "@/components";
import { LumeEvent, useEvent } from "@/system";
import { Kind, type NostrEvent } from "@/types";
import { Info, Repeat } from "@phosphor-icons/react";
import * as ScrollArea from "@radix-ui/react-scroll-area";
import * as Tabs from "@radix-ui/react-tabs";
@@ -24,7 +23,15 @@ function Screen() {
const { isLoading, data } = useQuery({
queryKey: ["notification", account],
queryFn: async () => {
const events = await NostrQuery.getNotifications();
const res = await commands.getNotifications();
if (res.status === "error") {
throw new Error(res.error);
}
const data: NostrEvent[] = res.data.map((item) => JSON.parse(item));
const events = data.map((ev) => new LumeEvent(ev));
return events;
},
select: (events) => {

View File

@@ -1,72 +1,62 @@
import { insertImage, isImagePath } from "@/commons";
import { insertImage, isImagePath, upload } from "@/commons";
import { Spinner } from "@/components";
import { NostrQuery } from "@/system";
import { Images } from "@phosphor-icons/react";
import type { UnlistenFn } from "@tauri-apps/api/event";
import { getCurrentWindow } from "@tauri-apps/api/window";
import { message } from "@tauri-apps/plugin-dialog";
import { useEffect, useState } from "react";
import { useEffect, useTransition } from "react";
import { useSlateStatic } from "slate-react";
export function MediaButton() {
const editor = useSlateStatic();
const [loading, setLoading] = useState(false);
const [isPending, startTransition] = useTransition();
const upload = async () => {
try {
// start loading
setLoading(true);
const image = await NostrQuery.upload();
insertImage(editor, image);
// reset loading
setLoading(false);
} catch (e) {
setLoading(false);
await message(String(e), { title: "Upload", kind: "error" });
}
const uploadMedia = () => {
startTransition(async () => {
try {
const image = await upload();
return insertImage(editor, image);
} catch (e) {
await message(String(e), { title: "Upload", kind: "error" });
return;
}
});
};
useEffect(() => {
let unlisten: UnlistenFn = undefined;
const unlisten = getCurrentWindow().listen("tauri://file-drop", (event) => {
startTransition(async () => {
// @ts-ignore, lfg !!!
const items: string[] = event.payload.paths;
async function listenFileDrop() {
const window = getCurrentWindow();
if (!unlisten) {
unlisten = await window.listen("tauri://file-drop", async (event) => {
// @ts-ignore, lfg !!!
const items: string[] = event.payload.paths;
// start loading
setLoading(true);
// upload all images
for (const item of items) {
if (isImagePath(item)) {
const image = await NostrQuery.upload(item);
insertImage(editor, image);
}
// upload all images
for (const item of items) {
if (isImagePath(item)) {
const image = await upload(item);
insertImage(editor, image);
}
// stop loading
setLoading(false);
});
}
}
}
listenFileDrop();
return;
});
});
return () => {
if (unlisten) unlisten();
unlisten.then((f) => f());
};
}, []);
return (
<button
type="button"
onClick={() => upload()}
disabled={loading}
onClick={() => uploadMedia()}
disabled={isPending}
className="inline-flex items-center h-8 gap-2 px-2.5 text-sm rounded-lg text-black/70 dark:text-white/70 w-max hover:bg-black/10 dark:hover:bg-white/10"
>
{loading ? <Spinner className="size-4" /> : <Images className="size-4" />}
{isPending ? (
<Spinner className="size-4" />
) : (
<Images className="size-4" />
)}
Add media
</button>
);

View File

@@ -1,5 +1,5 @@
import { commands } from "@/commands.gen";
import { checkForAppUpdates } from "@/commons";
import { NostrAccount } from "@/system";
import { createFileRoute, redirect } from "@tanstack/react-router";
export const Route = createFileRoute("/")({
@@ -8,7 +8,7 @@ export const Route = createFileRoute("/")({
await checkForAppUpdates(true);
// Get all accounts
const accounts = await NostrAccount.getAccounts();
const accounts = await commands.getAccounts();
if (accounts.length < 1) {
throw redirect({

View File

@@ -2,7 +2,7 @@ import { User } from "@/components/user";
import { createLazyFileRoute } from "@tanstack/react-router";
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
import { message } from "@tauri-apps/plugin-dialog";
import { useState } from "react";
import { useState, useTransition } from "react";
import CurrencyInput from "react-currency-input-field";
const DEFAULT_VALUES = [21, 50, 100, 200];
@@ -17,28 +17,26 @@ function Screen() {
const [amount, setAmount] = useState(21);
const [content, setContent] = useState("");
const [isCompleted, setIsCompleted] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [isPending, startTransition] = useTransition();
const submit = async () => {
try {
// start loading
setIsLoading(true);
const submit = () => {
startTransition(async () => {
try {
const val = await event.zap(amount, content);
// Zap
const val = await event.zap(amount, content);
if (val) {
setIsCompleted(true);
// close current window
await getCurrentWebviewWindow().close();
if (val) {
setIsCompleted(true);
// close current window
await getCurrentWebviewWindow().close();
}
} catch (e) {
await message(String(e), {
title: "Zap",
kind: "error",
});
return;
}
} catch (e) {
setIsLoading(false);
await message(String(e), {
title: "Zap",
kind: "error",
});
}
});
};
return (
@@ -104,7 +102,7 @@ function Screen() {
onClick={() => submit()}
className="inline-flex items-center justify-center w-full h-10 font-medium rounded-xl bg-neutral-950 text-neutral-50 hover:bg-neutral-900 dark:bg-white/20 dark:hover:bg-white/30"
>
{isCompleted ? "Zapped" : isLoading ? "Processing..." : "Zap"}
{isCompleted ? "Zapped" : isPending ? "Processing..." : "Zap"}
</button>
</div>
</div>

View File

@@ -1,9 +1,23 @@
import { NostrQuery } from "@/system";
import { commands } from "@/commands.gen";
import { LumeEvent } from "@/system";
import type { NostrEvent } from "@/types";
import { createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute("/zap/$id")({
beforeLoad: async ({ params }) => {
const event = await NostrQuery.getEvent(params.id);
return { event };
const res = await commands.getEvent(params.id);
if (res.status === "ok") {
const data = res.data;
const raw: NostrEvent = JSON.parse(data.raw);
if (data.parsed) {
raw.meta = data.parsed;
}
return { event: new LumeEvent(raw) };
} else {
throw new Error(res.error);
}
},
});

View File

@@ -1,167 +0,0 @@
import { type Result, commands } from "@/commands.gen";
import type { Metadata } from "@/types";
export const NostrAccount = {
getAccounts: async () => {
const query = await commands.getAccounts();
return query;
},
loadAccount: async (npub: string) => {
const bunker: string = localStorage.getItem(`${npub}_bunker`);
let query: Result<boolean, string>;
if (bunker?.length && bunker?.startsWith("bunker://")) {
query = await commands.loadAccount(npub, bunker);
} else {
query = await commands.loadAccount(npub, null);
}
if (query.status === "ok") {
return query.data;
} else {
throw new Error(query.error);
}
},
createAccount: async () => {
const query = await commands.createAccount();
if (query.status === "ok") {
return query.data;
} else {
throw new Error(query.error);
}
},
createProfile: async (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);
}
},
saveAccount: async (nsec: string, password = "") => {
const query = await commands.saveAccount(nsec, password);
if (query.status === "ok") {
return query.data;
} else {
throw new Error(query.error);
}
},
connectRemoteAccount: async (uri: string) => {
const connect = await commands.connectRemoteAccount(uri);
if (connect.status === "ok") {
const npub = connect.data;
const parsed = new URL(uri);
parsed.searchParams.delete("secret");
// save connection string
localStorage.setItem(`${npub}_bunker`, parsed.toString());
return npub;
} else {
throw new Error(connect.error);
}
},
setContactList: async (pubkeys: string[]) => {
const query = await commands.setContactList(pubkeys);
if (query.status === "ok") {
return query.data;
} else {
throw new Error(query.error);
}
},
loadWallet: async () => {
const query = await commands.loadWallet();
if (query.status === "ok") {
return Number.parseInt(query.data);
} else {
throw new Error(query.error);
}
},
setWallet: async (uri: string) => {
const query = await commands.setWallet(uri);
if (query.status === "ok") {
return query.data;
} else {
throw new Error(query.error);
}
},
removeWallet: async () => {
const query = await commands.removeWallet();
if (query.status === "ok") {
return query.data;
} else {
throw new Error(query.error);
}
},
getProfile: async () => {
const query = await commands.getCurrentProfile();
if (query.status === "ok") {
return JSON.parse(query.data) as Metadata;
} else {
return null;
}
},
getContactList: async () => {
const query = await commands.getContactList();
if (query.status === "ok") {
return query.data;
} else {
return [];
}
},
isContactListEmpty: async () => {
const query = await commands.isContactListEmpty();
if (query.status === "ok") {
return query.data;
} else {
return true;
}
},
checkContact: async (pubkey: string) => {
const query = await commands.checkContact(pubkey);
if (query.status === "ok") {
return query.data;
} else {
throw new Error(query.error);
}
},
toggleContact: async (pubkey: string, alias?: string) => {
const query = await commands.toggleContact(pubkey, alias);
if (query.status === "ok") {
return query.data;
} else {
throw new Error(query.error);
}
},
f2f: async (npub: string) => {
const query = await commands.copyFriend(npub);
if (query.status === "ok") {
return query.data;
} else {
throw new Error(query.error);
}
},
};

View File

@@ -16,7 +16,7 @@ export function useEvent(id: string) {
// Define query
let query: Result<RichEvent, string>;
let relayHint: string;
let relayHint: string = null;
if (normalizeId.startsWith("nevent1")) {
const decoded = nip19.decode(normalizeId);
@@ -27,14 +27,11 @@ export function useEvent(id: string) {
}
}
console.log(relayHint);
// Build query
if (relayHint) {
try {
const url = new URL(relayHint);
query = await commands.getEventFrom(normalizeId, url.toString());
} catch {
query = await commands.getEvent(normalizeId);
}
if (relayHint?.length) {
query = await commands.getEventFrom(normalizeId, relayHint);
} else {
query = await commands.getEvent(normalizeId);
}

View File

@@ -1,6 +1,4 @@
export * from "./event";
export * from "./account";
export * from "./query";
export * from "./window";
export * from "./hooks/useEvent";
export * from "./hooks/useProfile";

View File

@@ -1,336 +0,0 @@
import { type Result, type RichEvent, commands } from "@/commands.gen";
import type { LumeColumn, Metadata, NostrEvent, Relay } from "@/types";
import { open } from "@tauri-apps/plugin-dialog";
import { readFile } from "@tauri-apps/plugin-fs";
import { relaunch } from "@tauri-apps/plugin-process";
import { nip19 } from "nostr-tools";
import { LumeEvent } from "./event";
function toLumeEvents(richEvents: RichEvent[]) {
const events = richEvents.map((item) => {
const nostrEvent: NostrEvent = JSON.parse(item.raw);
if (item.parsed) {
nostrEvent.meta = item.parsed;
} else {
nostrEvent.meta = null;
}
const lumeEvent = new LumeEvent(nostrEvent);
return lumeEvent;
});
return events;
}
export const NostrQuery = {
upload: async (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));
}
},
getNotifications: async () => {
const query = await commands.getNotifications();
if (query.status === "ok") {
const data = query.data.map((item) => JSON.parse(item) as NostrEvent);
const events = data.map((ev) => new LumeEvent(ev));
return events;
} else {
console.error(query.error);
return [];
}
},
getProfile: async (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;
}
},
getEvent: async (id: string, hint?: string) => {
// Validate ID
const normalizeId: string = id
.replace("nostr:", "")
.replace(/[^\w\s]/gi, "");
// Define query
let query: Result<RichEvent, string>;
let relayHint: string = hint;
if (normalizeId.startsWith("nevent1")) {
const decoded = nip19.decode(normalizeId);
if (decoded.type === "nevent") relayHint = decoded.data.relays[0];
}
// Build query
if (relayHint) {
try {
const url = new URL(relayHint);
query = await commands.getEventFrom(normalizeId, url.toString());
} catch {
query = await commands.getEvent(normalizeId);
}
} else {
query = await commands.getEvent(normalizeId);
}
if (query.status === "ok") {
const data = query.data;
const raw = JSON.parse(data.raw) as NostrEvent;
if (data?.parsed) {
raw.meta = data.parsed;
}
const event = new LumeEvent(raw);
return event;
} else {
console.log("[getEvent]: ", query.error);
return null;
}
},
getRepostEvent: async (event: LumeEvent) => {
try {
const embed: NostrEvent = JSON.parse(event.content);
const query = await commands.getEventMeta(embed.content);
if (query.status === "ok") {
embed.meta = query.data;
const lumeEvent = new LumeEvent(embed);
return lumeEvent;
}
} catch {
const query = await commands.getEvent(event.repostId);
if (query.status === "ok") {
const data = query.data;
const raw = JSON.parse(data.raw) as NostrEvent;
if (data?.parsed) {
raw.meta = data.parsed;
}
const event = new LumeEvent(raw);
return event;
} else {
console.log("[getRepostEvent]: ", query.error);
return null;
}
}
},
getGroupEvents: async (pubkeys: string[], asOf?: number) => {
const until: string = asOf && asOf > 0 ? asOf.toString() : undefined;
const query = await commands.getGroupEvents(pubkeys, until);
if (query.status === "ok") {
const data = toLumeEvents(query.data);
return data;
} else {
return [];
}
},
getGlobalEvents: async (asOf?: number) => {
const until: string = asOf && asOf > 0 ? asOf.toString() : undefined;
const query = await commands.getGlobalEvents(until);
if (query.status === "ok") {
const data = toLumeEvents(query.data);
return data;
} else {
return [];
}
},
getHashtagEvents: async (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 data = toLumeEvents(query.data);
return data;
} else {
return [];
}
},
verifyNip05: async (pubkey: string, nip05?: string) => {
const query = await commands.verifyNip05(pubkey, nip05);
if (query.status === "ok") {
return query.data;
} else {
return false;
}
},
getNstore: async (key: string) => {
const query = await commands.getLumeStore(key);
if (query.status === "ok") {
const data = query.data ? JSON.parse(query.data) : null;
return data;
} else {
return null;
}
},
setNstore: async (key: string, value: string) => {
const query = await commands.setLumeStore(key, value);
if (query.status === "ok") {
return query.data;
} else {
throw new Error(query.error);
}
},
getUserSettings: async () => {
const query = await commands.getSettings();
if (query.status === "ok") {
return query.data;
} else {
return query.error;
}
},
setUserSettings: async (newSettings: string) => {
const query = await commands.setNewSettings(newSettings);
if (query.status === "ok") {
return query.data;
} else {
return query.error;
}
},
setColumns: async (columns: LumeColumn[]) => {
const key = "lume_v4:columns";
const content = JSON.stringify(columns);
const query = await commands.setLumeStore(key, content);
if (query.status === "ok") {
return query.data;
} else {
throw new Error(query.error);
}
},
getRelays: async () => {
const query = await commands.getRelays();
if (query.status === "ok") {
return query.data;
} else {
throw new Error(query.error);
}
},
connectRelay: async (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);
}
}
},
removeRelay: async (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);
}
}
},
getBootstrapRelays: async () => {
const query = await commands.getBootstrapRelays();
if (query.status === "ok") {
const relays: Relay[] = [];
for (const item of query.data) {
const line = item.split(",");
const url = line[0];
const purpose = line[1] ?? "";
relays.push({ url, purpose });
}
return relays;
} else {
return [];
}
},
saveBootstrapRelays: async (relays: string[]) => {
const text = relays
.map((relay) => Object.values(relay).join(","))
.join("\n");
const query = await commands.saveBootstrapRelays(text);
if (query.status === "ok") {
return await relaunch();
} else {
throw new Error(query.error);
}
},
};

View File

@@ -114,7 +114,7 @@ export const LumeWindow = {
throw new Error(query.error);
}
},
openZap: async (id: string) => {
openZap: async (id: string, account?: string) => {
const wallet = await commands.loadWallet();
if (wallet.status === "ok") {
@@ -129,7 +129,7 @@ export const LumeWindow = {
hidden_title: true,
});
} else {
await LumeWindow.openSettings("bitcoin-connect");
await LumeWindow.openSettings(account, "bitcoin-connect");
}
},
openSettings: async (account: string, path?: string) => {