feat: improve tauri commands

This commit is contained in:
reya 2024-05-15 13:58:03 +07:00
parent 8ea2335225
commit 7724eccd72
15 changed files with 701 additions and 711 deletions

View File

@ -4,39 +4,39 @@ import { useRouteContext } from "@tanstack/react-router";
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
export function Balance({ account }: { account: string }) { export function Balance({ account }: { account: string }) {
const { ark } = useRouteContext({ strict: false }); const { ark } = useRouteContext({ strict: false });
const [balance, setBalance] = useState(0); const [balance, setBalance] = useState(0);
const value = useMemo(() => getBitcoinDisplayValues(balance), [balance]); const value = useMemo(() => getBitcoinDisplayValues(balance), [balance]);
useEffect(() => { useEffect(() => {
async function getBalance() { async function getBalance() {
const val = await ark.get_balance(); const val = await ark.get_balance();
setBalance(val); setBalance(val);
} }
getBalance(); getBalance();
}, []); }, []);
return ( return (
<div <div
data-tauri-drag-region data-tauri-drag-region
className="flex h-16 items-center justify-end px-3" className="flex h-16 items-center justify-end px-3"
> >
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="text-end"> <div className="text-end">
<div className="text-sm leading-tight text-neutral-700 dark:text-neutral-300"> <div className="text-sm leading-tight text-neutral-700 dark:text-neutral-300">
Your balance Your balance
</div> </div>
<div className="font-medium leading-tight"> <div className="font-medium leading-tight">
{value.bitcoinFormatted} {value.bitcoinFormatted}
</div> </div>
</div> </div>
<User.Provider pubkey={account}> <User.Provider pubkey={account}>
<User.Root> <User.Root>
<User.Avatar className="size-9 rounded-full" /> <User.Avatar className="size-9 rounded-full" />
</User.Root> </User.Root>
</User.Provider> </User.Provider>
</div> </div>
</div> </div>
); );
} }

View File

@ -6,142 +6,142 @@ import { getCurrent } from "@tauri-apps/api/webviewWindow";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
export function Column({ export function Column({
column, column,
account, account,
isScroll, isScroll,
isResize, isResize,
}: { }: {
column: LumeColumn; column: LumeColumn;
account: string; account: string;
isScroll: boolean; isScroll: boolean;
isResize: boolean; isResize: boolean;
}) { }) {
const container = useRef<HTMLDivElement>(null); const container = useRef<HTMLDivElement>(null);
const webviewLabel = `column-${account}_${column.label}`; const webviewLabel = `column-${account}_${column.label}`;
const [isCreated, setIsCreated] = useState(false); const [isCreated, setIsCreated] = useState(false);
const repositionWebview = async () => { const repositionWebview = async () => {
const newRect = container.current.getBoundingClientRect(); const newRect = container.current.getBoundingClientRect();
await invoke("reposition_column", { await invoke("reposition_column", {
label: webviewLabel, label: webviewLabel,
x: newRect.x, x: newRect.x,
y: newRect.y, y: newRect.y,
}); });
}; };
const resizeWebview = async () => { const resizeWebview = async () => {
const newRect = container.current.getBoundingClientRect(); const newRect = container.current.getBoundingClientRect();
await invoke("resize_column", { await invoke("resize_column", {
label: webviewLabel, label: webviewLabel,
width: newRect.width, width: newRect.width,
height: newRect.height, height: newRect.height,
}); });
}; };
useEffect(() => { useEffect(() => {
if (isCreated) resizeWebview(); if (isCreated) resizeWebview();
}, [isResize]); }, [isResize]);
useEffect(() => { useEffect(() => {
if (isScroll && isCreated) repositionWebview(); if (isScroll && isCreated) repositionWebview();
}, [isScroll]); }, [isScroll]);
useEffect(() => { useEffect(() => {
const rect = container.current.getBoundingClientRect(); const rect = container.current.getBoundingClientRect();
const url = `${column.content}?account=${account}&label=${column.label}&name=${column.name}`; const url = `${column.content}?account=${account}&label=${column.label}&name=${column.name}`;
// create new webview // create new webview
invoke("create_column", { invoke("create_column", {
label: webviewLabel, label: webviewLabel,
x: rect.x, x: rect.x,
y: rect.y, y: rect.y,
width: rect.width, width: rect.width,
height: rect.height, height: rect.height,
url, url,
}).then(() => setIsCreated(true)); }).then(() => setIsCreated(true));
// close webview when unmounted // close webview when unmounted
return () => { return () => {
invoke("close_column", { label: webviewLabel }); invoke("close_column", { label: webviewLabel });
}; };
}, [account]); }, [account]);
return ( return (
<div className="h-full w-[440px] shrink-0 p-2"> <div className="h-full w-[440px] shrink-0 p-2">
<div <div
className={cn( className={cn(
"flex flex-col w-full h-full rounded-xl", "flex flex-col w-full h-full rounded-xl",
column.label !== "open" column.label !== "open"
? "bg-black/5 dark:bg-white/5 backdrop-blur-sm" ? "bg-black/5 dark:bg-white/5 backdrop-blur-sm"
: "", : "",
)} )}
> >
{column.label !== "open" ? ( {column.label !== "open" ? (
<Header label={column.label} name={column.name} /> <Header label={column.label} name={column.name} />
) : null} ) : null}
<div ref={container} className="flex-1 w-full h-full" /> <div ref={container} className="flex-1 w-full h-full" />
</div> </div>
</div> </div>
); );
} }
function Header({ label, name }: { label: string; name: string }) { function Header({ label, name }: { label: string; name: string }) {
const [title, setTitle] = useState(name); const [title, setTitle] = useState(name);
const [isChanged, setIsChanged] = useState(false); const [isChanged, setIsChanged] = useState(false);
const saveNewTitle = async () => { const saveNewTitle = async () => {
const mainWindow = getCurrent(); const mainWindow = getCurrent();
await mainWindow.emit("columns", { type: "set_title", label, title }); await mainWindow.emit("columns", { type: "set_title", label, title });
// update search params // update search params
// @ts-ignore, hahaha // @ts-ignore, hahaha
search.name = title; search.name = title;
// reset state // reset state
setIsChanged(false); setIsChanged(false);
}; };
const close = async () => { const close = async () => {
const mainWindow = getCurrent(); const mainWindow = getCurrent();
await mainWindow.emit("columns", { type: "remove", label }); await mainWindow.emit("columns", { type: "remove", label });
}; };
useEffect(() => { useEffect(() => {
if (title.length !== name.length) setIsChanged(true); if (title.length !== name.length) setIsChanged(true);
}, [title]); }, [title]);
return ( return (
<div className="h-9 w-full flex items-center justify-between shrink-0 px-1"> <div className="h-9 w-full flex items-center justify-between shrink-0 px-1">
<div className="size-7" /> <div className="size-7" />
<div className="shrink-0 h-9 flex items-center justify-center"> <div className="shrink-0 h-9 flex items-center justify-center">
<div className="relative flex gap-2 items-center"> <div className="relative flex gap-2 items-center">
<div <div
contentEditable contentEditable
suppressContentEditableWarning={true} suppressContentEditableWarning={true}
onBlur={(e) => setTitle(e.currentTarget.textContent)} onBlur={(e) => setTitle(e.currentTarget.textContent)}
className="text-sm font-medium focus:outline-none" className="text-sm font-medium focus:outline-none"
> >
{name} {name}
</div> </div>
{isChanged ? ( {isChanged ? (
<button <button
type="button" type="button"
onClick={() => saveNewTitle()} onClick={() => saveNewTitle()}
className="text-teal-500 hover:text-teal-600" className="text-teal-500 hover:text-teal-600"
> >
<CheckIcon className="size-4" /> <CheckIcon className="size-4" />
</button> </button>
) : null} ) : null}
</div> </div>
</div> </div>
<button <button
type="button" type="button"
onClick={() => close()} onClick={() => close()}
className="size-7 inline-flex hover:bg-black/10 rounded-lg dark:hover:bg-white/10 items-center justify-center text-neutral-600 dark:text-neutral-400 hover:text-neutral-800 dark:hover:text-neutral-200" className="size-7 inline-flex hover:bg-black/10 rounded-lg dark:hover:bg-white/10 items-center justify-center text-neutral-600 dark:text-neutral-400 hover:text-neutral-800 dark:hover:text-neutral-200"
> >
<CancelIcon className="size-4" /> <CancelIcon className="size-4" />
</button> </button>
</div> </div>
); );
} }

View File

@ -5,44 +5,44 @@ import { cn } from "@lume/utils";
import { useRouteContext } from "@tanstack/react-router"; import { useRouteContext } from "@tanstack/react-router";
export function Conversation({ export function Conversation({
event, event,
className, className,
}: { }: {
event: Event; event: Event;
className?: string; className?: string;
}) { }) {
const { ark } = useRouteContext({ strict: false }); const { ark } = useRouteContext({ strict: false });
const thread = ark.parse_event_thread(event.tags); const thread = ark.parse_event_thread(event.tags);
return ( return (
<Note.Provider event={event}> <Note.Provider event={event}>
<Note.Root <Note.Root
className={cn( className={cn(
"bg-white dark:bg-black/20 backdrop-blur-lg rounded-xl flex flex-col gap-3 shadow-primary dark:ring-1 ring-neutral-800/50", "bg-white dark:bg-black/20 backdrop-blur-lg rounded-xl flex flex-col gap-3 shadow-primary dark:ring-1 ring-neutral-800/50",
className, className,
)} )}
> >
<div className="flex flex-col gap-3"> <div className="flex flex-col gap-3">
{thread?.root ? <Note.Child eventId={thread?.root} isRoot /> : null} {thread?.root ? <Note.Child eventId={thread?.root} isRoot /> : null}
<div className="flex items-center gap-2 px-3"> <div className="flex items-center gap-2 px-3">
<div className="inline-flex items-center gap-1.5 shrink-0 text-sm font-medium text-neutral-600 dark:text-neutral-400"> <div className="inline-flex items-center gap-1.5 shrink-0 text-sm font-medium text-neutral-600 dark:text-neutral-400">
<ThreadIcon className="size-4" /> <ThreadIcon className="size-4" />
Thread Thread
</div> </div>
<div className="flex-1 h-px bg-neutral-100 dark:bg-white/5" /> <div className="flex-1 h-px bg-neutral-100 dark:bg-white/5" />
</div> </div>
{thread?.reply ? <Note.Child eventId={thread?.reply} /> : null} {thread?.reply ? <Note.Child eventId={thread?.reply} /> : null}
<div> <div>
<div className="px-3 h-14 flex items-center justify-between"> <div className="px-3 h-14 flex items-center justify-between">
<Note.User /> <Note.User />
</div> </div>
<Note.Content className="px-3" /> <Note.Content className="px-3" />
</div> </div>
</div> </div>
<div className="flex items-center h-14 px-3"> <div className="flex items-center h-14 px-3">
<Note.Open /> <Note.Open />
</div> </div>
</Note.Root> </Note.Root>
</Note.Provider> </Note.Provider>
); );
} }

View File

@ -12,16 +12,16 @@ import { NoteRoot } from "./root";
import { NoteUser } from "./user"; import { NoteUser } from "./user";
export const Note = { export const Note = {
Provider: NoteProvider, Provider: NoteProvider,
Root: NoteRoot, Root: NoteRoot,
User: NoteUser, User: NoteUser,
Menu: NoteMenu, Menu: NoteMenu,
Reply: NoteReply, Reply: NoteReply,
Repost: NoteRepost, Repost: NoteRepost,
Content: NoteContent, Content: NoteContent,
ContentLarge: NoteContentLarge, ContentLarge: NoteContentLarge,
Zap: NoteZap, Zap: NoteZap,
Open: NoteOpenThread, Open: NoteOpenThread,
Child: NoteChild, Child: NoteChild,
Activity: NoteActivity, Activity: NoteActivity,
}; };

View File

@ -3,30 +3,30 @@ import { Note } from "@/components/note";
import { cn } from "@lume/utils"; import { cn } from "@lume/utils";
export function Notification({ export function Notification({
event, event,
className, className,
}: { }: {
event: Event; event: Event;
className?: string; className?: string;
}) { }) {
return ( return (
<Note.Provider event={event}> <Note.Provider event={event}>
<Note.Root <Note.Root
className={cn( className={cn(
"bg-white dark:bg-black/20 backdrop-blur-lg rounded-xl flex flex-col gap-3 shadow-primary dark:ring-1 ring-neutral-800/50", "bg-white dark:bg-black/20 backdrop-blur-lg rounded-xl flex flex-col gap-3 shadow-primary dark:ring-1 ring-neutral-800/50",
className, className,
)} )}
> >
<div> <div>
<div className="px-3 h-14 flex items-center justify-between"> <div className="px-3 h-14 flex items-center justify-between">
<Note.User /> <Note.User />
</div> </div>
<Note.Content className="px-3" /> <Note.Content className="px-3" />
</div> </div>
<div className="flex items-center h-14 px-3"> <div className="flex items-center h-14 px-3">
<Note.Open /> <Note.Open />
</div> </div>
</Note.Root> </Note.Root>
</Note.Provider> </Note.Provider>
); );
} }

View File

@ -4,44 +4,44 @@ import { Note } from "@/components/note";
import { cn } from "@lume/utils"; import { cn } from "@lume/utils";
export function Quote({ export function Quote({
event, event,
className, className,
}: { }: {
event: Event; event: Event;
className?: string; className?: string;
}) { }) {
const quoteEventId = event.tags.find( const quoteEventId = event.tags.find(
(tag) => tag[0] === "q" || tag[3] === "mention", (tag) => tag[0] === "q" || tag[3] === "mention",
)?.[1]; )?.[1];
return ( return (
<Note.Provider event={event}> <Note.Provider event={event}>
<Note.Root <Note.Root
className={cn( className={cn(
"bg-white dark:bg-black/20 backdrop-blur-lg rounded-xl flex flex-col gap-3 shadow-primary dark:ring-1 ring-neutral-800/50", "bg-white dark:bg-black/20 backdrop-blur-lg rounded-xl flex flex-col gap-3 shadow-primary dark:ring-1 ring-neutral-800/50",
className, className,
)} )}
> >
<div className="flex flex-col gap-3"> <div className="flex flex-col gap-3">
<Note.Child eventId={quoteEventId} isRoot /> <Note.Child eventId={quoteEventId} isRoot />
<div className="flex items-center gap-2 px-3"> <div className="flex items-center gap-2 px-3">
<div className="inline-flex items-center gap-1.5 shrink-0 text-sm font-medium text-neutral-600 dark:text-neutral-400"> <div className="inline-flex items-center gap-1.5 shrink-0 text-sm font-medium text-neutral-600 dark:text-neutral-400">
<QuoteIcon className="size-4" /> <QuoteIcon className="size-4" />
Quote Quote
</div> </div>
<div className="flex-1 h-px bg-neutral-100 dark:bg-white/5" /> <div className="flex-1 h-px bg-neutral-100 dark:bg-white/5" />
</div> </div>
<div> <div>
<div className="px-3 h-14 flex items-center justify-between"> <div className="px-3 h-14 flex items-center justify-between">
<Note.User /> <Note.User />
</div> </div>
<Note.Content className="px-3" quote={false} clean /> <Note.Content className="px-3" quote={false} clean />
</div> </div>
</div> </div>
<div className="flex items-center h-14 px-3"> <div className="flex items-center h-14 px-3">
<Note.Open /> <Note.Open />
</div> </div>
</Note.Root> </Note.Root>
</Note.Provider> </Note.Provider>
); );
} }

View File

@ -7,79 +7,79 @@ import { useQuery } from "@tanstack/react-query";
import { useRouteContext } from "@tanstack/react-router"; import { useRouteContext } from "@tanstack/react-router";
export function RepostNote({ export function RepostNote({
event, event,
className, className,
}: { }: {
event: Event; event: Event;
className?: string; className?: string;
}) { }) {
const { ark } = useRouteContext({ strict: false }); const { ark } = useRouteContext({ strict: false });
const { const {
isLoading, isLoading,
isError, isError,
data: repostEvent, data: repostEvent,
} = useQuery({ } = useQuery({
queryKey: ["repost", event.id], queryKey: ["repost", event.id],
queryFn: async () => { queryFn: async () => {
try { try {
if (event.content.length > 50) { if (event.content.length > 50) {
const embed: Event = JSON.parse(event.content); const embed: Event = JSON.parse(event.content);
return embed; return embed;
} }
const id = event.tags.find((el) => el[0] === "e")?.[1]; const id = event.tags.find((el) => el[0] === "e")?.[1];
const repostEvent = await ark.get_event(id); const repostEvent = await ark.get_event(id);
return repostEvent; return repostEvent;
} catch (e) { } catch (e) {
throw new Error(e); throw new Error(e);
} }
}, },
refetchOnWindowFocus: false, refetchOnWindowFocus: false,
refetchOnMount: false, refetchOnMount: false,
}); });
return ( return (
<Note.Root <Note.Root
className={cn( className={cn(
"bg-white dark:bg-black/20 backdrop-blur-lg rounded-xl mb-3 shadow-primary dark:ring-1 ring-neutral-800/50", "bg-white dark:bg-black/20 backdrop-blur-lg rounded-xl mb-3 shadow-primary dark:ring-1 ring-neutral-800/50",
className, className,
)} )}
> >
<User.Provider pubkey={event.pubkey}> <User.Provider pubkey={event.pubkey}>
<User.Root className="flex items-center gap-2 px-3 py-3 border-b border-neutral-100 dark:border-neutral-800/50 rounded-t-xl"> <User.Root className="flex items-center gap-2 px-3 py-3 border-b border-neutral-100 dark:border-neutral-800/50 rounded-t-xl">
<div className="text-sm font-semibold text-neutral-700 dark:text-neutral-300"> <div className="text-sm font-semibold text-neutral-700 dark:text-neutral-300">
Reposted by Reposted by
</div> </div>
<User.Avatar className="size-6 shrink-0 rounded-full object-cover ring-1 ring-neutral-200/50 dark:ring-neutral-800/50" /> <User.Avatar className="size-6 shrink-0 rounded-full object-cover ring-1 ring-neutral-200/50 dark:ring-neutral-800/50" />
</User.Root> </User.Root>
</User.Provider> </User.Provider>
{isLoading ? ( {isLoading ? (
<div className="flex h-20 items-center justify-center gap-2"> <div className="flex h-20 items-center justify-center gap-2">
<Spinner /> <Spinner />
Loading event... Loading event...
</div> </div>
) : isError || !repostEvent ? ( ) : isError || !repostEvent ? (
<div className="flex h-20 items-center justify-center"> <div className="flex h-20 items-center justify-center">
Event not found within your current relay set Event not found within your current relay set
</div> </div>
) : ( ) : (
<Note.Provider event={repostEvent}> <Note.Provider event={repostEvent}>
<Note.Root> <Note.Root>
<div className="px-3 h-14 flex items-center justify-between"> <div className="px-3 h-14 flex items-center justify-between">
<Note.User /> <Note.User />
<Note.Menu /> <Note.Menu />
</div> </div>
<Note.Content className="px-3" /> <Note.Content className="px-3" />
<div className="mt-3 flex items-center gap-4 h-14 px-3"> <div className="mt-3 flex items-center gap-4 h-14 px-3">
<Note.Open /> <Note.Open />
<Note.Reply /> <Note.Reply />
<Note.Repost /> <Note.Repost />
<Note.Zap /> <Note.Zap />
</div> </div>
</Note.Root> </Note.Root>
</Note.Provider> </Note.Provider>
)} )}
</Note.Root> </Note.Root>
); );
} }

View File

@ -3,32 +3,32 @@ import { cn } from "@lume/utils";
import { Note } from "@/components/note"; import { Note } from "@/components/note";
export function TextNote({ export function TextNote({
event, event,
className, className,
}: { }: {
event: Event; event: Event;
className?: string; className?: string;
}) { }) {
return ( return (
<Note.Provider event={event}> <Note.Provider event={event}>
<Note.Root <Note.Root
className={cn( className={cn(
"bg-white dark:bg-black/20 backdrop-blur-lg rounded-xl shadow-primary dark:ring-1 ring-neutral-800/50", "bg-white dark:bg-black/20 backdrop-blur-lg rounded-xl shadow-primary dark:ring-1 ring-neutral-800/50",
className, className,
)} )}
> >
<div className="px-3 h-14 flex items-center justify-between"> <div className="px-3 h-14 flex items-center justify-between">
<Note.User /> <Note.User />
<Note.Menu /> <Note.Menu />
</div> </div>
<Note.Content className="px-3" /> <Note.Content className="px-3" />
<div className="mt-3 flex items-center gap-4 h-14 px-3"> <div className="mt-3 flex items-center gap-4 h-14 px-3">
<Note.Open /> <Note.Open />
<Note.Reply /> <Note.Reply />
<Note.Repost /> <Note.Repost />
<Note.Zap /> <Note.Zap />
</div> </div>
</Note.Root> </Note.Root>
</Note.Provider> </Note.Provider>
); );
} }

View File

@ -13,175 +13,175 @@ import { useDebouncedCallback } from "use-debounce";
import { VList, type VListHandle } from "virtua"; import { VList, type VListHandle } from "virtua";
export const Route = createFileRoute("/$account/home")({ export const Route = createFileRoute("/$account/home")({
loader: async ({ context }) => { loader: async ({ context }) => {
try { try {
const userColumns = await context.ark.get_columns(); const userColumns = await context.ark.get_columns();
if (userColumns.length > 0) { if (userColumns.length > 0) {
return userColumns; return userColumns;
} else { } else {
const systemPath = "resources/system_columns.json"; const systemPath = "resources/system_columns.json";
const resourcePath = await resolveResource(systemPath); const resourcePath = await resolveResource(systemPath);
const resourceFile = await readTextFile(resourcePath); const resourceFile = await readTextFile(resourcePath);
const systemColumns: LumeColumn[] = JSON.parse(resourceFile); const systemColumns: LumeColumn[] = JSON.parse(resourceFile);
return systemColumns; return systemColumns;
} }
} catch (e) { } catch (e) {
console.error(String(e)); console.error(String(e));
} }
}, },
component: Screen, component: Screen,
}); });
function Screen() { function Screen() {
const userSavedColumns = Route.useLoaderData(); const userSavedColumns = Route.useLoaderData();
const vlistRef = useRef<VListHandle>(null); const vlistRef = useRef<VListHandle>(null);
const { account } = Route.useParams(); const { account } = Route.useParams();
const { ark } = Route.useRouteContext(); const { ark } = Route.useRouteContext();
const [selectedIndex, setSelectedIndex] = useState(-1); const [selectedIndex, setSelectedIndex] = useState(-1);
const [columns, setColumns] = useState([]); const [columns, setColumns] = useState([]);
const [isScroll, setIsScroll] = useState(false); const [isScroll, setIsScroll] = useState(false);
const [isResize, setIsResize] = useState(false); const [isResize, setIsResize] = useState(false);
const goLeft = () => { const goLeft = () => {
const prevIndex = Math.max(selectedIndex - 1, 0); const prevIndex = Math.max(selectedIndex - 1, 0);
setSelectedIndex(prevIndex); setSelectedIndex(prevIndex);
vlistRef.current.scrollToIndex(prevIndex, { vlistRef.current.scrollToIndex(prevIndex, {
align: "center", align: "center",
}); });
}; };
const goRight = () => { const goRight = () => {
const nextIndex = Math.min(selectedIndex + 1, columns.length - 1); const nextIndex = Math.min(selectedIndex + 1, columns.length - 1);
setSelectedIndex(nextIndex); setSelectedIndex(nextIndex);
vlistRef.current.scrollToIndex(nextIndex, { vlistRef.current.scrollToIndex(nextIndex, {
align: "center", align: "center",
}); });
}; };
const add = useDebouncedCallback((column: LumeColumn) => { const add = useDebouncedCallback((column: LumeColumn) => {
// update col label // update col label
column.label = `${column.label}-${nanoid()}`; column.label = `${column.label}-${nanoid()}`;
// create new cols // create new cols
const cols = [...columns]; const cols = [...columns];
const openColIndex = cols.findIndex((col) => col.label === "open"); const openColIndex = cols.findIndex((col) => col.label === "open");
const newCols = [ const newCols = [
...cols.slice(0, openColIndex), ...cols.slice(0, openColIndex),
column, column,
...cols.slice(openColIndex), ...cols.slice(openColIndex),
]; ];
setColumns(newCols); setColumns(newCols);
setSelectedIndex(newCols.length); setSelectedIndex(newCols.length);
setIsScroll(true); setIsScroll(true);
// scroll to the newest column // scroll to the newest column
vlistRef.current.scrollToIndex(newCols.length - 1, { vlistRef.current.scrollToIndex(newCols.length - 1, {
align: "end", align: "end",
}); });
}, 150); }, 150);
const remove = useDebouncedCallback((label: string) => { const remove = useDebouncedCallback((label: string) => {
const newCols = columns.filter((t) => t.label !== label); const newCols = columns.filter((t) => t.label !== label);
setColumns(newCols); setColumns(newCols);
setSelectedIndex(newCols.length); setSelectedIndex(newCols.length);
setIsScroll(true); setIsScroll(true);
// scroll to the first column // scroll to the first column
vlistRef.current.scrollToIndex(newCols.length - 1, { vlistRef.current.scrollToIndex(newCols.length - 1, {
align: "start", align: "start",
}); });
}, 150); }, 150);
const updateName = useDebouncedCallback((label: string, title: string) => { const updateName = useDebouncedCallback((label: string, title: string) => {
const currentColIndex = columns.findIndex((col) => col.label === label); const currentColIndex = columns.findIndex((col) => col.label === label);
const updatedCol = Object.assign({}, columns[currentColIndex]); const updatedCol = Object.assign({}, columns[currentColIndex]);
updatedCol.name = title; updatedCol.name = title;
const newCols = columns.slice(); const newCols = columns.slice();
newCols[currentColIndex] = updatedCol; newCols[currentColIndex] = updatedCol;
setColumns(newCols); setColumns(newCols);
}, 150); }, 150);
const startResize = useDebouncedCallback( const startResize = useDebouncedCallback(
() => setIsResize((prev) => !prev), () => setIsResize((prev) => !prev),
150, 150,
); );
useEffect(() => { useEffect(() => {
setColumns(userSavedColumns); setColumns(userSavedColumns);
}, [userSavedColumns]); }, [userSavedColumns]);
useEffect(() => { useEffect(() => {
// save state // save state
ark.set_columns(columns); ark.set_columns(columns);
}, [columns]); }, [columns]);
useEffect(() => { useEffect(() => {
const unlistenColEvent = listen<EventColumns>("columns", (data) => { const unlistenColEvent = listen<EventColumns>("columns", (data) => {
if (data.payload.type === "add") add(data.payload.column); if (data.payload.type === "add") add(data.payload.column);
if (data.payload.type === "remove") remove(data.payload.label); if (data.payload.type === "remove") remove(data.payload.label);
if (data.payload.type === "set_title") if (data.payload.type === "set_title")
updateName(data.payload.label, data.payload.title); updateName(data.payload.label, data.payload.title);
}); });
const unlistenWindowResize = getCurrent().listen("tauri://resize", () => { const unlistenWindowResize = getCurrent().listen("tauri://resize", () => {
startResize(); startResize();
}); });
return () => { return () => {
unlistenColEvent.then((f) => f()); unlistenColEvent.then((f) => f());
unlistenWindowResize.then((f) => f()); unlistenWindowResize.then((f) => f());
}; };
}, []); }, []);
return ( return (
<div className="h-full w-full"> <div className="h-full w-full">
<VList <VList
ref={vlistRef} ref={vlistRef}
horizontal horizontal
tabIndex={-1} tabIndex={-1}
itemSize={440} itemSize={440}
overscan={3} overscan={3}
onScroll={() => setIsScroll(true)} onScroll={() => setIsScroll(true)}
onScrollEnd={() => setIsScroll(false)} onScrollEnd={() => setIsScroll(false)}
className="scrollbar-none h-full w-full overflow-x-auto focus:outline-none" className="scrollbar-none h-full w-full overflow-x-auto focus:outline-none"
cache={null} cache={null}
> >
{columns.map((column) => ( {columns.map((column) => (
<Column <Column
key={column.label} key={column.label}
column={column} column={column}
account={account} account={account}
isScroll={isScroll} isScroll={isScroll}
isResize={isResize} isResize={isResize}
/> />
))} ))}
</VList> </VList>
<Toolbar> <Toolbar>
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<button <button
type="button" type="button"
onClick={() => goLeft()} onClick={() => goLeft()}
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" 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"
> >
<ArrowLeftIcon className="size-5" /> <ArrowLeftIcon className="size-5" />
</button> </button>
<button <button
type="button" type="button"
onClick={() => goRight()} onClick={() => goRight()}
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" 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"
> >
<ArrowRightIcon className="size-5" /> <ArrowRightIcon className="size-5" />
</button> </button>
</div> </div>
</Toolbar> </Toolbar>
</div> </div>
); );
} }

View File

@ -2,10 +2,10 @@ import { BellIcon, ComposeFilledIcon, PlusIcon, SearchIcon } from "@lume/icons";
import { Event, Kind } from "@lume/types"; import { Event, Kind } from "@lume/types";
import { User } from "@/components/user"; import { User } from "@/components/user";
import { import {
cn, cn,
decodeZapInvoice, decodeZapInvoice,
displayNpub, displayNpub,
sendNativeNotification, sendNativeNotification,
} from "@lume/utils"; } from "@lume/utils";
import { Outlet, createFileRoute } from "@tanstack/react-router"; import { Outlet, createFileRoute } from "@tanstack/react-router";
import { invoke } from "@tauri-apps/api/core"; import { invoke } from "@tauri-apps/api/core";
@ -14,171 +14,171 @@ import { useEffect, useState } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
export const Route = createFileRoute("/$account")({ export const Route = createFileRoute("/$account")({
beforeLoad: async ({ context }) => { beforeLoad: async ({ context }) => {
const ark = context.ark; const ark = context.ark;
const accounts = await ark.get_all_accounts(); const accounts = await ark.get_all_accounts();
return { accounts }; return { accounts };
}, },
component: Screen, component: Screen,
}); });
function Screen() { function Screen() {
const { ark, platform } = Route.useRouteContext(); const { ark, platform } = Route.useRouteContext();
const navigate = Route.useNavigate(); const navigate = Route.useNavigate();
return ( return (
<div className="flex h-screen w-screen flex-col"> <div className="flex h-screen w-screen flex-col">
<div <div
data-tauri-drag-region data-tauri-drag-region
className={cn( className={cn(
"flex h-11 shrink-0 items-center justify-between pr-2", "flex h-11 shrink-0 items-center justify-between pr-2",
platform === "macos" ? "ml-2 pl-20" : "pl-4", platform === "macos" ? "ml-2 pl-20" : "pl-4",
)} )}
> >
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Accounts /> <Accounts />
<button <button
type="button" type="button"
onClick={() => navigate({ to: "/landing" })} onClick={() => navigate({ to: "/landing" })}
className="inline-flex size-8 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" className="inline-flex size-8 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" /> <PlusIcon className="size-5" />
</button> </button>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<button <button
type="button" type="button"
onClick={() => ark.open_editor()} onClick={() => ark.open_editor()}
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" 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" /> <ComposeFilledIcon className="size-4" />
New Post New Post
</button> </button>
<Bell /> <Bell />
<button <button
type="button" type="button"
onClick={() => ark.open_search()} onClick={() => ark.open_search()}
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" 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" /> <SearchIcon className="size-5" />
</button> </button>
<div id="toolbar" /> <div id="toolbar" />
</div> </div>
</div> </div>
<div className="flex-1"> <div className="flex-1">
<Outlet /> <Outlet />
</div> </div>
</div> </div>
); );
} }
function Accounts() { function Accounts() {
const navigate = Route.useNavigate(); const navigate = Route.useNavigate();
const { ark, accounts } = Route.useRouteContext(); const { ark, accounts } = Route.useRouteContext();
const { account } = Route.useParams(); const { account } = Route.useParams();
const changeAccount = async (npub: string) => { const changeAccount = async (npub: string) => {
if (npub === account) { if (npub === account) {
return await ark.open_profile(account); return await ark.open_profile(account);
} }
// change current account and update signer // change current account and update signer
const select = await ark.load_selected_account(npub); const select = await ark.load_selected_account(npub);
if (select) { if (select) {
return navigate({ to: "/$account/home", params: { account: npub } }); return navigate({ to: "/$account/home", params: { account: npub } });
} else { } else {
toast.warning("Something wrong."); toast.warning("Something wrong.");
} }
}; };
return ( return (
<div data-tauri-drag-region className="flex items-center gap-3"> <div data-tauri-drag-region className="flex items-center gap-3">
{accounts.map((user) => ( {accounts.map((user) => (
<button key={user} type="button" onClick={() => changeAccount(user)}> <button key={user} type="button" onClick={() => changeAccount(user)}>
<User.Provider pubkey={user}> <User.Provider pubkey={user}>
<User.Root <User.Root
className={cn( className={cn(
"rounded-full transition-all ease-in-out duration-150 will-change-auto", "rounded-full transition-all ease-in-out duration-150 will-change-auto",
user === account user === account
? "ring-1 ring-teal-500 ring-offset-2 ring-offset-neutral-200 dark:ring-offset-neutral-950" ? "ring-1 ring-teal-500 ring-offset-2 ring-offset-neutral-200 dark:ring-offset-neutral-950"
: "", : "",
)} )}
> >
<User.Avatar <User.Avatar
className={cn( className={cn(
"aspect-square h-auto rounded-full object-cover transition-all ease-in-out duration-150 will-change-auto", "aspect-square h-auto rounded-full object-cover transition-all ease-in-out duration-150 will-change-auto",
user === account ? "w-7" : "w-8", user === account ? "w-7" : "w-8",
)} )}
/> />
</User.Root> </User.Root>
</User.Provider> </User.Provider>
</button> </button>
))} ))}
</div> </div>
); );
} }
function Bell() { function Bell() {
const { ark } = Route.useRouteContext(); const { ark } = Route.useRouteContext();
const { account } = Route.useParams(); const { account } = Route.useParams();
const [count, setCount] = useState(0); const [count, setCount] = useState(0);
useEffect(() => { useEffect(() => {
const unlisten = getCurrent().listen<string>( const unlisten = getCurrent().listen<string>(
"activity", "activity",
async (payload) => { async (payload) => {
setCount((prevCount) => prevCount + 1); setCount((prevCount) => prevCount + 1);
await invoke("set_badge", { count }); await invoke("set_badge", { count });
const event: Event = JSON.parse(payload.payload); const event: Event = JSON.parse(payload.payload);
const user = await ark.get_profile(event.pubkey); const user = await ark.get_profile(event.pubkey);
const userName = const userName =
user.display_name || user.name || displayNpub(event.pubkey, 16); user.display_name || user.name || displayNpub(event.pubkey, 16);
switch (event.kind) { switch (event.kind) {
case Kind.Text: { case Kind.Text: {
sendNativeNotification("Mentioned you in a note", userName); sendNativeNotification("Mentioned you in a note", userName);
break; break;
} }
case Kind.Repost: { case Kind.Repost: {
sendNativeNotification("Reposted your note", userName); sendNativeNotification("Reposted your note", userName);
break; break;
} }
case Kind.ZapReceipt: { case Kind.ZapReceipt: {
const amount = decodeZapInvoice(event.tags); const amount = decodeZapInvoice(event.tags);
sendNativeNotification( sendNativeNotification(
`Zapped ₿ ${amount.bitcoinFormatted}`, `Zapped ₿ ${amount.bitcoinFormatted}`,
userName, userName,
); );
break; break;
} }
default: default:
break; break;
} }
}, },
); );
return () => { return () => {
unlisten.then((f) => f()); unlisten.then((f) => f());
}; };
}, []); }, []);
return ( return (
<button <button
type="button" type="button"
onClick={() => { onClick={() => {
setCount(0); setCount(0);
ark.open_activity(account); ark.open_activity(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" 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"
> >
<BellIcon className="size-5" /> <BellIcon className="size-5" />
{count > 0 ? ( {count > 0 ? (
<span className="absolute right-0 top-0 block size-2 rounded-full bg-teal-500 ring-1 ring-black/5" /> <span className="absolute right-0 top-0 block size-2 rounded-full bg-teal-500 ring-1 ring-black/5" />
) : null} ) : null}
</button> </button>
); );
} }

View File

@ -25,6 +25,7 @@ import { MediaButton } from "./-components/media";
import { NsfwToggle } from "./-components/nsfw"; import { NsfwToggle } from "./-components/nsfw";
import { MentionButton } from "./-components/mention"; import { MentionButton } from "./-components/mention";
import { MentionNote } from "@/components/note/mentions/note"; import { MentionNote } from "@/components/note/mentions/note";
import { toast } from "sonner";
type EditorSearch = { type EditorSearch = {
reply_to: string; reply_to: string;

View File

@ -207,12 +207,10 @@ export class Ark {
global?: boolean, global?: boolean,
) { ) {
try { try {
let until: string = undefined; const until: string = asOf && asOf > 0 ? asOf.toString() : undefined;
const isGlobal = global ?? false; const isGlobal = global ?? false;
const seens = new Set<string>();
if (asOf && asOf > 0) until = asOf.toString();
const seenIds = new Set<string>();
const nostrEvents: Event[] = await invoke("get_events", { const nostrEvents: Event[] = await invoke("get_events", {
limit, limit,
until, until,
@ -220,39 +218,31 @@ export class Ark {
global: isGlobal, global: isGlobal,
}); });
// remove duplicate event const events = nostrEvents.filter((event) => {
for (const event of nostrEvents) { const eTags = event.tags.filter((el) => el[0] === "e");
if (event.kind === Kind.Repost) { const ids = eTags.map((item) => item[1]);
const repostId = event.tags.find((tag) => tag[0] === "e")?.[1]; const isDup = ids.some((id) => seens.has(id));
seenIds.add(repostId);
// Add found ids to seen list
for (const id of ids) {
seens.add(id);
} }
const eventIds = event.tags // Filter NSFW event
.filter((el) => el[0] === "e") if (this.settings?.nsfw) {
?.map((item) => item[1]); const wTags = event.tags.filter((t) => t[0] === "content-warning");
const isLewd = wTags.length > 0;
if (eventIds?.length) { return !isDup && !isLewd;
for (const id of eventIds) {
seenIds.add(id);
}
} }
}
const events = nostrEvents // Filter duplicate event
.filter((event) => !seenIds.has(event.id)) return !isDup;
.sort((a, b) => b.created_at - a.created_at); });
if (this.settings?.nsfw) {
return events.filter(
(event) =>
event.tags.filter((event) => event[0] === "content-warning")
.length > 0,
);
}
return events; return events;
} catch (e) { } catch (e) {
console.info(String(e)); console.error("[get_events] failed", String(e));
return []; return [];
} }
} }

View File

@ -9,7 +9,15 @@ pub async fn get_event(id: &str, state: State<'_, Nostr>) -> Result<String, Stri
let event_id: Option<EventId> = match Nip19::from_bech32(id) { let event_id: Option<EventId> = match Nip19::from_bech32(id) {
Ok(val) => match val { Ok(val) => match val {
Nip19::EventId(id) => Some(id), Nip19::EventId(id) => Some(id),
Nip19::Event(event) => Some(event.event_id), Nip19::Event(event) => {
let relays = event.relays;
for relay in relays.into_iter() {
let url = Url::from_str(&relay).unwrap();
let _ = client.add_relay(url.clone()).await.unwrap_or_default();
client.connect_relay(url).await.unwrap_or_default();
}
Some(event.event_id)
}
_ => None, _ => None,
}, },
Err(_) => match EventId::from_hex(id) { Err(_) => match EventId::from_hex(id) {
@ -18,23 +26,25 @@ pub async fn get_event(id: &str, state: State<'_, Nostr>) -> Result<String, Stri
}, },
}; };
if let Some(id) = event_id { match event_id {
let filter = Filter::new().id(id); Some(id) => {
let filter = Filter::new().id(id);
if let Ok(events) = &client match &client
.get_events_of(vec![filter], Some(Duration::from_secs(10))) .get_events_of(vec![filter], Some(Duration::from_secs(10)))
.await .await
{ {
if let Some(event) = events.first() { Ok(events) => {
Ok(event.as_json()) if let Some(event) = events.first() {
} else { Ok(event.as_json())
Err("Event not found with current relay list".into()) } else {
Err("Cannot found this event with current relay list".into())
}
}
Err(err) => Err(err.to_string()),
} }
} else {
Err("Event not found with current relay list".into())
} }
} else { None => Err("Event ID is not valid.".into()),
Err("EventId is not valid".into())
} }
} }
@ -213,8 +223,6 @@ pub async fn search(
limit: usize, limit: usize,
state: State<'_, Nostr>, state: State<'_, Nostr>,
) -> Result<Vec<Event>, String> { ) -> Result<Vec<Event>, String> {
println!("search: {}", content);
let client = &state.client; let client = &state.client;
let filter = Filter::new() let filter = Filter::new()
.kinds(vec![Kind::TextNote, Kind::Metadata]) .kinds(vec![Kind::TextNote, Kind::Metadata])

View File

@ -206,15 +206,12 @@ pub async fn load_selected_account(npub: &str, state: State<'_, Nostr>) -> Resul
// Add relay to relay pool // Add relay to relay pool
let _ = client let _ = client
.add_relay_with_opts(relay_url, opts) .add_relay_with_opts(relay_url.clone(), opts)
.await .await
.unwrap_or_default(); .unwrap_or_default();
// Connect relay // Connect relay
client client.connect_relay(relay_url).await.unwrap_or_default();
.connect_relay(item.0.to_string())
.await
.unwrap_or_default();
} }
} }
} }
@ -251,14 +248,10 @@ pub fn to_npub(hex: &str) -> Result<String, ()> {
Ok(npub.to_bech32().unwrap()) Ok(npub.to_bech32().unwrap())
} }
#[tauri::command(async)] #[tauri::command]
pub async fn verify_nip05(key: &str, nip05: &str) -> Result<bool, ()> { pub async fn verify_nip05(key: &str, nip05: &str) -> Result<bool, ()> {
let public_key = PublicKey::from_str(key).unwrap(); let public_key = PublicKey::from_str(key).unwrap();
let status = nip05::verify(public_key, nip05, None).await; let status = nip05::verify(public_key, nip05, None).await;
if status.is_ok() { Ok(status.is_ok())
Ok(true)
} else {
Ok(false)
}
} }

View File

@ -104,8 +104,6 @@ pub async fn friend_to_friend(npub: &str, state: State<'_, Nostr>) -> Result<boo
} }
} }
println!("contact list: {}", contact_list.len());
match client.set_contact_list(contact_list).await { match client.set_contact_list(contact_list).await {
Ok(_) => Ok(true), Ok(_) => Ok(true),
Err(err) => Err(err.to_string()), Err(err) => Err(err.to_string()),
@ -289,7 +287,7 @@ pub async fn unfollow(id: &str, state: State<'_, Nostr>) -> Result<EventId, Stri
} }
} }
#[tauri::command(async)] #[tauri::command]
pub async fn set_nstore( pub async fn set_nstore(
key: &str, key: &str,
content: &str, content: &str,