mirror of
https://github.com/luminous-devs/lume.git
synced 2024-09-29 16:30:55 +00:00
feat: improve tauri commands
This commit is contained in:
parent
8ea2335225
commit
7724eccd72
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
};
|
};
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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 [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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])
|
||||||
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
Loading…
Reference in New Issue
Block a user