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

View File

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

View File

@ -5,44 +5,44 @@ import { cn } from "@lume/utils";
import { useRouteContext } from "@tanstack/react-router";
export function Conversation({
event,
className,
event,
className,
}: {
event: Event;
className?: string;
event: Event;
className?: string;
}) {
const { ark } = useRouteContext({ strict: false });
const thread = ark.parse_event_thread(event.tags);
const { ark } = useRouteContext({ strict: false });
const thread = ark.parse_event_thread(event.tags);
return (
<Note.Provider event={event}>
<Note.Root
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",
className,
)}
>
<div className="flex flex-col gap-3">
{thread?.root ? <Note.Child eventId={thread?.root} isRoot /> : null}
<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">
<ThreadIcon className="size-4" />
Thread
</div>
<div className="flex-1 h-px bg-neutral-100 dark:bg-white/5" />
</div>
{thread?.reply ? <Note.Child eventId={thread?.reply} /> : null}
<div>
<div className="px-3 h-14 flex items-center justify-between">
<Note.User />
</div>
<Note.Content className="px-3" />
</div>
</div>
<div className="flex items-center h-14 px-3">
<Note.Open />
</div>
</Note.Root>
</Note.Provider>
);
return (
<Note.Provider event={event}>
<Note.Root
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",
className,
)}
>
<div className="flex flex-col gap-3">
{thread?.root ? <Note.Child eventId={thread?.root} isRoot /> : null}
<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">
<ThreadIcon className="size-4" />
Thread
</div>
<div className="flex-1 h-px bg-neutral-100 dark:bg-white/5" />
</div>
{thread?.reply ? <Note.Child eventId={thread?.reply} /> : null}
<div>
<div className="px-3 h-14 flex items-center justify-between">
<Note.User />
</div>
<Note.Content className="px-3" />
</div>
</div>
<div className="flex items-center h-14 px-3">
<Note.Open />
</div>
</Note.Root>
</Note.Provider>
);
}

View File

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

View File

@ -3,30 +3,30 @@ import { Note } from "@/components/note";
import { cn } from "@lume/utils";
export function Notification({
event,
className,
event,
className,
}: {
event: Event;
className?: string;
event: Event;
className?: string;
}) {
return (
<Note.Provider event={event}>
<Note.Root
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",
className,
)}
>
<div>
<div className="px-3 h-14 flex items-center justify-between">
<Note.User />
</div>
<Note.Content className="px-3" />
</div>
<div className="flex items-center h-14 px-3">
<Note.Open />
</div>
</Note.Root>
</Note.Provider>
);
return (
<Note.Provider event={event}>
<Note.Root
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",
className,
)}
>
<div>
<div className="px-3 h-14 flex items-center justify-between">
<Note.User />
</div>
<Note.Content className="px-3" />
</div>
<div className="flex items-center h-14 px-3">
<Note.Open />
</div>
</Note.Root>
</Note.Provider>
);
}

View File

@ -4,44 +4,44 @@ import { Note } from "@/components/note";
import { cn } from "@lume/utils";
export function Quote({
event,
className,
event,
className,
}: {
event: Event;
className?: string;
event: Event;
className?: string;
}) {
const quoteEventId = event.tags.find(
(tag) => tag[0] === "q" || tag[3] === "mention",
)?.[1];
const quoteEventId = event.tags.find(
(tag) => tag[0] === "q" || tag[3] === "mention",
)?.[1];
return (
<Note.Provider event={event}>
<Note.Root
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",
className,
)}
>
<div className="flex flex-col gap-3">
<Note.Child eventId={quoteEventId} isRoot />
<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">
<QuoteIcon className="size-4" />
Quote
</div>
<div className="flex-1 h-px bg-neutral-100 dark:bg-white/5" />
</div>
<div>
<div className="px-3 h-14 flex items-center justify-between">
<Note.User />
</div>
<Note.Content className="px-3" quote={false} clean />
</div>
</div>
<div className="flex items-center h-14 px-3">
<Note.Open />
</div>
</Note.Root>
</Note.Provider>
);
return (
<Note.Provider event={event}>
<Note.Root
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",
className,
)}
>
<div className="flex flex-col gap-3">
<Note.Child eventId={quoteEventId} isRoot />
<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">
<QuoteIcon className="size-4" />
Quote
</div>
<div className="flex-1 h-px bg-neutral-100 dark:bg-white/5" />
</div>
<div>
<div className="px-3 h-14 flex items-center justify-between">
<Note.User />
</div>
<Note.Content className="px-3" quote={false} clean />
</div>
</div>
<div className="flex items-center h-14 px-3">
<Note.Open />
</div>
</Note.Root>
</Note.Provider>
);
}

View File

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

View File

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

View File

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

View File

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

View File

@ -25,6 +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";
type EditorSearch = {
reply_to: string;

View File

@ -207,12 +207,10 @@ export class Ark {
global?: boolean,
) {
try {
let until: string = undefined;
const until: string = asOf && asOf > 0 ? asOf.toString() : undefined;
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", {
limit,
until,
@ -220,39 +218,31 @@ export class Ark {
global: isGlobal,
});
// remove duplicate event
for (const event of nostrEvents) {
if (event.kind === Kind.Repost) {
const repostId = event.tags.find((tag) => tag[0] === "e")?.[1];
seenIds.add(repostId);
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);
}
const eventIds = event.tags
.filter((el) => el[0] === "e")
?.map((item) => item[1]);
// Filter NSFW event
if (this.settings?.nsfw) {
const wTags = event.tags.filter((t) => t[0] === "content-warning");
const isLewd = wTags.length > 0;
if (eventIds?.length) {
for (const id of eventIds) {
seenIds.add(id);
}
return !isDup && !isLewd;
}
}
const events = nostrEvents
.filter((event) => !seenIds.has(event.id))
.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,
);
}
// Filter duplicate event
return !isDup;
});
return events;
} catch (e) {
console.info(String(e));
console.error("[get_events] failed", String(e));
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) {
Ok(val) => match val {
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,
},
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 {
let filter = Filter::new().id(id);
match event_id {
Some(id) => {
let filter = Filter::new().id(id);
if let Ok(events) = &client
.get_events_of(vec![filter], Some(Duration::from_secs(10)))
.await
{
if let Some(event) = events.first() {
Ok(event.as_json())
} else {
Err("Event not found with current relay list".into())
match &client
.get_events_of(vec![filter], Some(Duration::from_secs(10)))
.await
{
Ok(events) => {
if let Some(event) = events.first() {
Ok(event.as_json())
} 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 {
Err("EventId is not valid".into())
None => Err("Event ID is not valid.".into()),
}
}
@ -213,8 +223,6 @@ pub async fn search(
limit: usize,
state: State<'_, Nostr>,
) -> Result<Vec<Event>, String> {
println!("search: {}", content);
let client = &state.client;
let filter = Filter::new()
.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
let _ = client
.add_relay_with_opts(relay_url, opts)
.add_relay_with_opts(relay_url.clone(), opts)
.await
.unwrap_or_default();
// Connect relay
client
.connect_relay(item.0.to_string())
.await
.unwrap_or_default();
client.connect_relay(relay_url).await.unwrap_or_default();
}
}
}
@ -251,14 +248,10 @@ pub fn to_npub(hex: &str) -> Result<String, ()> {
Ok(npub.to_bech32().unwrap())
}
#[tauri::command(async)]
#[tauri::command]
pub async fn verify_nip05(key: &str, nip05: &str) -> Result<bool, ()> {
let public_key = PublicKey::from_str(key).unwrap();
let status = nip05::verify(public_key, nip05, None).await;
if status.is_ok() {
Ok(true)
} else {
Ok(false)
}
Ok(status.is_ok())
}

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 {
Ok(_) => Ok(true),
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(
key: &str,
content: &str,