feat: improve multi-account switching
This commit is contained in:
@@ -1,42 +1,42 @@
|
|||||||
import { User } from "./user";
|
import { User } from "@/components/user";
|
||||||
import { getBitcoinDisplayValues } from "@lume/utils";
|
import { getBitcoinDisplayValues } from "@lume/utils";
|
||||||
import { useRouteContext } from "@tanstack/react-router";
|
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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,160 +0,0 @@
|
|||||||
import { CancelIcon, CheckIcon } from "@lume/icons";
|
|
||||||
import type { LumeColumn } from "@lume/types";
|
|
||||||
import { cn } from "@lume/utils";
|
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
|
||||||
import { getCurrent } from "@tauri-apps/api/webviewWindow";
|
|
||||||
import { useEffect, useRef, useState } from "react";
|
|
||||||
|
|
||||||
export function Col({
|
|
||||||
column,
|
|
||||||
account,
|
|
||||||
isScroll,
|
|
||||||
isResize,
|
|
||||||
}: {
|
|
||||||
column: LumeColumn;
|
|
||||||
account: string;
|
|
||||||
isScroll: boolean;
|
|
||||||
isResize: boolean;
|
|
||||||
}) {
|
|
||||||
const container = useRef<HTMLDivElement>(null);
|
|
||||||
const [webview, setWebview] = useState<string | undefined>(undefined);
|
|
||||||
|
|
||||||
const repositionWebview = async () => {
|
|
||||||
if (webview && webview.length > 1) {
|
|
||||||
const newRect = container.current.getBoundingClientRect();
|
|
||||||
await invoke("reposition_column", {
|
|
||||||
label: webview,
|
|
||||||
x: newRect.x,
|
|
||||||
y: newRect.y,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const resizeWebview = async () => {
|
|
||||||
if (webview && webview.length > 1) {
|
|
||||||
const newRect = container.current.getBoundingClientRect();
|
|
||||||
await invoke("resize_column", {
|
|
||||||
label: webview,
|
|
||||||
width: newRect.width,
|
|
||||||
height: newRect.height,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
resizeWebview();
|
|
||||||
}, [isResize]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isScroll) repositionWebview();
|
|
||||||
}, [isScroll]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
(async () => {
|
|
||||||
if (webview && webview.length > 1) return;
|
|
||||||
|
|
||||||
const rect = container.current.getBoundingClientRect();
|
|
||||||
const windowLabel = `column-${column.label}`;
|
|
||||||
const url = `${column.content}?account=${account}&label=${column.label}&name=${column.name}`;
|
|
||||||
|
|
||||||
// create new webview
|
|
||||||
const label: string = await invoke("create_column", {
|
|
||||||
label: windowLabel,
|
|
||||||
x: rect.x,
|
|
||||||
y: rect.y,
|
|
||||||
width: rect.width,
|
|
||||||
height: rect.height,
|
|
||||||
url,
|
|
||||||
});
|
|
||||||
|
|
||||||
setWebview(label);
|
|
||||||
})();
|
|
||||||
|
|
||||||
// close webview when unmounted
|
|
||||||
return () => {
|
|
||||||
if (webview && webview.length > 1) {
|
|
||||||
invoke("close_column", {
|
|
||||||
label: webview,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, [webview]);
|
|
||||||
|
|
||||||
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 saveNewTitle = async () => {
|
|
||||||
const mainWindow = getCurrent();
|
|
||||||
await mainWindow.emit("columns", { type: "set_title", label, title });
|
|
||||||
|
|
||||||
// update search params
|
|
||||||
// @ts-ignore, hahaha
|
|
||||||
search.name = title;
|
|
||||||
|
|
||||||
// reset state
|
|
||||||
setIsChanged(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const close = async () => {
|
|
||||||
const mainWindow = getCurrent();
|
|
||||||
await mainWindow.emit("columns", { type: "remove", label });
|
|
||||||
};
|
|
||||||
|
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
147
apps/desktop2/src/components/column.tsx
Normal file
147
apps/desktop2/src/components/column.tsx
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
import { CancelIcon, CheckIcon } from "@lume/icons";
|
||||||
|
import type { LumeColumn } from "@lume/types";
|
||||||
|
import { cn } from "@lume/utils";
|
||||||
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
|
import { getCurrent } from "@tauri-apps/api/webviewWindow";
|
||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
|
export function Column({
|
||||||
|
column,
|
||||||
|
account,
|
||||||
|
isScroll,
|
||||||
|
isResize,
|
||||||
|
}: {
|
||||||
|
column: LumeColumn;
|
||||||
|
account: string;
|
||||||
|
isScroll: boolean;
|
||||||
|
isResize: boolean;
|
||||||
|
}) {
|
||||||
|
const container = useRef<HTMLDivElement>(null);
|
||||||
|
const webviewLabel = `column-${account}_${column.label}`;
|
||||||
|
|
||||||
|
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 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 (isScroll && isCreated) repositionWebview();
|
||||||
|
}, [isScroll]);
|
||||||
|
|
||||||
|
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));
|
||||||
|
|
||||||
|
// 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Header({ label, name }: { label: string; name: string }) {
|
||||||
|
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 });
|
||||||
|
|
||||||
|
// update search params
|
||||||
|
// @ts-ignore, hahaha
|
||||||
|
search.name = title;
|
||||||
|
|
||||||
|
// reset state
|
||||||
|
setIsChanged(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const close = async () => {
|
||||||
|
const mainWindow = getCurrent();
|
||||||
|
await mainWindow.emit("columns", { type: "remove", label });
|
||||||
|
};
|
||||||
|
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,48 +1,48 @@
|
|||||||
import { ThreadIcon } from "@lume/icons";
|
import { ThreadIcon } from "@lume/icons";
|
||||||
import type { Event } from "@lume/types";
|
import type { Event } from "@lume/types";
|
||||||
import { Note } from "./note";
|
import { Note } from "@/components/note";
|
||||||
import { cn } from "@lume/utils";
|
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,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,32 +1,32 @@
|
|||||||
import type { Event } from "@lume/types";
|
import type { Event } from "@lume/types";
|
||||||
import { Note } from "./note";
|
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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,47 +1,47 @@
|
|||||||
import { QuoteIcon } from "@lume/icons";
|
import { QuoteIcon } from "@lume/icons";
|
||||||
import type { Event } from "@lume/types";
|
import type { Event } from "@lume/types";
|
||||||
import { Note } from "./note";
|
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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,85 +1,85 @@
|
|||||||
import type { Event } from "@lume/types";
|
import type { Event } from "@lume/types";
|
||||||
import { Spinner } from "@lume/ui";
|
import { Spinner } from "@lume/ui";
|
||||||
import { Note } from "./note";
|
import { Note } from "@/components/note";
|
||||||
import { User } from "./user";
|
import { User } from "@/components/user";
|
||||||
import { cn } from "@lume/utils";
|
import { cn } from "@lume/utils";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,34 +1,34 @@
|
|||||||
import type { Event } from "@lume/types";
|
import type { Event } from "@lume/types";
|
||||||
import { cn } from "@lume/utils";
|
import { cn } from "@lume/utils";
|
||||||
import { Note } from "./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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Col } from "@/components/col";
|
import { Column } from "@/components/column";
|
||||||
import { Toolbar } from "@/components/toolbar";
|
import { Toolbar } from "@/components/toolbar";
|
||||||
import { ArrowLeftIcon, ArrowRightIcon } from "@lume/icons";
|
import { ArrowLeftIcon, ArrowRightIcon } from "@lume/icons";
|
||||||
import type { EventColumns, LumeColumn } from "@lume/types";
|
import type { EventColumns, LumeColumn } from "@lume/types";
|
||||||
@@ -13,170 +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")({
|
||||||
beforeLoad: async ({ context }) => {
|
loader: async ({ context }) => {
|
||||||
try {
|
try {
|
||||||
const ark = context.ark;
|
const userColumns = await context.ark.get_columns();
|
||||||
const resourcePath = await resolveResource(
|
if (userColumns.length > 0) {
|
||||||
"resources/system_columns.json",
|
return userColumns;
|
||||||
);
|
} else {
|
||||||
const systemColumns: LumeColumn[] = JSON.parse(
|
const systemPath = "resources/system_columns.json";
|
||||||
await readTextFile(resourcePath),
|
const resourcePath = await resolveResource(systemPath);
|
||||||
);
|
const resourceFile = await readTextFile(resourcePath);
|
||||||
const userColumns = await ark.get_columns();
|
const systemColumns: LumeColumn[] = JSON.parse(resourceFile);
|
||||||
|
|
||||||
return {
|
return systemColumns;
|
||||||
storedColumns: !userColumns.length ? systemColumns : userColumns,
|
}
|
||||||
};
|
} catch (e) {
|
||||||
} catch (e) {
|
console.error(String(e));
|
||||||
console.error(String(e));
|
}
|
||||||
}
|
},
|
||||||
},
|
component: Screen,
|
||||||
component: Screen,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
function Screen() {
|
function Screen() {
|
||||||
const vlistRef = useRef<VListHandle>(null);
|
const userSavedColumns = Route.useLoaderData();
|
||||||
|
const vlistRef = useRef<VListHandle>(null);
|
||||||
|
|
||||||
const { account } = Route.useParams();
|
const { account } = Route.useParams();
|
||||||
const { ark, storedColumns } = Route.useRouteContext();
|
const { ark } = Route.useRouteContext();
|
||||||
|
|
||||||
const [selectedIndex, setSelectedIndex] = useState(-1);
|
const [selectedIndex, setSelectedIndex] = useState(-1);
|
||||||
const [columns, setColumns] = useState(storedColumns);
|
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(() => {
|
||||||
// save state
|
setColumns(userSavedColumns);
|
||||||
ark.set_columns(columns);
|
}, [userSavedColumns]);
|
||||||
}, [columns]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const unlistenColEvent = listen<EventColumns>("columns", (data) => {
|
// save state
|
||||||
if (data.payload.type === "add") add(data.payload.column);
|
ark.set_columns(columns);
|
||||||
if (data.payload.type === "remove") remove(data.payload.label);
|
}, [columns]);
|
||||||
if (data.payload.type === "set_title")
|
|
||||||
updateName(data.payload.label, data.payload.title);
|
|
||||||
});
|
|
||||||
|
|
||||||
const unlistenWindowResize = getCurrent().listen("tauri://resize", () => {
|
useEffect(() => {
|
||||||
startResize();
|
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);
|
||||||
|
});
|
||||||
|
|
||||||
return () => {
|
const unlistenWindowResize = getCurrent().listen("tauri://resize", () => {
|
||||||
unlistenColEvent.then((f) => f());
|
startResize();
|
||||||
unlistenWindowResize.then((f) => f());
|
});
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return () => {
|
||||||
<div className="h-full w-full">
|
unlistenColEvent.then((f) => f());
|
||||||
<VList
|
unlistenWindowResize.then((f) => f());
|
||||||
ref={vlistRef}
|
};
|
||||||
horizontal
|
}, []);
|
||||||
tabIndex={-1}
|
|
||||||
itemSize={440}
|
return (
|
||||||
overscan={3}
|
<div className="h-full w-full">
|
||||||
onScroll={() => setIsScroll(true)}
|
<VList
|
||||||
onScrollEnd={() => setIsScroll(false)}
|
ref={vlistRef}
|
||||||
className="scrollbar-none h-full w-full overflow-x-auto focus:outline-none"
|
horizontal
|
||||||
>
|
tabIndex={-1}
|
||||||
{columns.map((column) => (
|
itemSize={440}
|
||||||
<Col
|
overscan={3}
|
||||||
key={column.label}
|
onScroll={() => setIsScroll(true)}
|
||||||
column={column}
|
onScrollEnd={() => setIsScroll(false)}
|
||||||
account={account}
|
className="scrollbar-none h-full w-full overflow-x-auto focus:outline-none"
|
||||||
isScroll={isScroll}
|
cache={null}
|
||||||
isResize={isResize}
|
>
|
||||||
/>
|
{columns.map((column) => (
|
||||||
))}
|
<Column
|
||||||
</VList>
|
key={column.label}
|
||||||
<Toolbar>
|
column={column}
|
||||||
<div className="flex items-center gap-1">
|
account={account}
|
||||||
<button
|
isScroll={isScroll}
|
||||||
type="button"
|
isResize={isResize}
|
||||||
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"
|
))}
|
||||||
>
|
</VList>
|
||||||
<ArrowLeftIcon className="size-5" />
|
<Toolbar>
|
||||||
</button>
|
<div className="flex items-center gap-1">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => goRight()}
|
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"
|
||||||
>
|
>
|
||||||
<ArrowRightIcon className="size-5" />
|
<ArrowLeftIcon className="size-5" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
<button
|
||||||
</Toolbar>
|
type="button"
|
||||||
</div>
|
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>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,177 +2,183 @@ 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";
|
||||||
import { getCurrent } from "@tauri-apps/api/window";
|
import { getCurrent } from "@tauri-apps/api/window";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
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) return;
|
if (npub === account) {
|
||||||
|
return await ark.open_profile(account);
|
||||||
|
}
|
||||||
|
|
||||||
const select = await ark.load_selected_account(npub);
|
// change current account and update signer
|
||||||
|
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 {
|
||||||
};
|
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",
|
"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",
|
"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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
{"desktop-capability":{"identifier":"desktop-capability","description":"Capability for the desktop","local":true,"windows":["main","splash","settings","search","nwc","activity","zap-*","event-*","user-*","editor-*","column-*"],"permissions":["path:default","event:default","window:default","app:default","resources:default","menu:default","tray:default","notification:allow-is-permission-granted","notification:allow-request-permission","notification:default","os:allow-locale","os:allow-platform","updater:default","updater:allow-check","updater:allow-download-and-install","window:allow-start-dragging","window:allow-create","window:allow-close","window:allow-set-focus","clipboard-manager:allow-write","clipboard-manager:allow-read","webview:allow-create-webview-window","webview:allow-create-webview","webview:allow-set-webview-size","webview:allow-set-webview-position","webview:allow-webview-close","dialog:default","dialog:allow-ask","dialog:allow-message","fs:allow-read-file","shell:allow-open",{"identifier":"http:default","allow":[{"url":"http://**/"},{"url":"https://**/"}]},{"identifier":"fs:allow-read-text-file","allow":[{"path":"$RESOURCE/locales/*"},{"path":"$RESOURCE/resources/*"}]}],"platforms":["linux","macOS","windows"]}}
|
{"desktop-capability":{"identifier":"desktop-capability","description":"Capability for the desktop","local":true,"windows":["main","splash","settings","search","nwc","activity","zap-*","event-*","user-*","editor-*","column-*"],"permissions":["path:default","event:default","window:default","app:default","resources:default","menu:default","tray:default","notification:allow-is-permission-granted","notification:allow-request-permission","notification:default","os:allow-locale","os:allow-platform","updater:default","updater:allow-check","updater:allow-download-and-install","window:allow-start-dragging","window:allow-create","window:allow-close","window:allow-set-focus","clipboard-manager:allow-write","clipboard-manager:allow-read","webview:allow-create-webview-window","webview:allow-create-webview","webview:allow-set-webview-size","webview:allow-set-webview-position","webview:allow-webview-close","dialog:default","dialog:allow-ask","dialog:allow-message","process:allow-restart","fs:allow-read-file","shell:allow-open",{"identifier":"http:default","allow":[{"url":"http://**/"},{"url":"https://**/"}]},{"identifier":"fs:allow-read-text-file","allow":[{"path":"$RESOURCE/locales/*"},{"path":"$RESOURCE/resources/*"}]}],"platforms":["linux","macOS","windows"]}}
|
||||||
@@ -289,7 +289,7 @@ pub async fn unfollow(id: &str, state: State<'_, Nostr>) -> Result<EventId, Stri
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command(async)]
|
||||||
pub async fn set_nstore(
|
pub async fn set_nstore(
|
||||||
key: &str,
|
key: &str,
|
||||||
content: &str,
|
content: &str,
|
||||||
@@ -306,10 +306,7 @@ pub async fn set_nstore(
|
|||||||
let builder = EventBuilder::new(Kind::ApplicationSpecificData, encrypted, vec![tag]);
|
let builder = EventBuilder::new(Kind::ApplicationSpecificData, encrypted, vec![tag]);
|
||||||
|
|
||||||
match client.send_event_builder(builder).await {
|
match client.send_event_builder(builder).await {
|
||||||
Ok(event_id) => {
|
Ok(event_id) => Ok(event_id),
|
||||||
println!("set nstore: {}", event_id);
|
|
||||||
Ok(event_id)
|
|
||||||
}
|
|
||||||
Err(err) => Err(err.to_string()),
|
Err(err) => Err(err.to_string()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -322,38 +319,29 @@ pub async fn get_nstore(key: &str, state: State<'_, Nostr>) -> Result<String, St
|
|||||||
let client = &state.client;
|
let client = &state.client;
|
||||||
|
|
||||||
if let Ok(signer) = client.signer().await {
|
if let Ok(signer) = client.signer().await {
|
||||||
let public_key = signer.public_key().await;
|
let public_key = signer.public_key().await.unwrap();
|
||||||
|
let filter = Filter::new()
|
||||||
|
.author(public_key)
|
||||||
|
.kind(Kind::ApplicationSpecificData)
|
||||||
|
.identifier(key)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
if let Ok(author) = public_key {
|
match client
|
||||||
let filter = Filter::new()
|
.get_events_of(vec![filter], Some(Duration::from_secs(5)))
|
||||||
.author(author)
|
.await
|
||||||
.kind(Kind::ApplicationSpecificData)
|
{
|
||||||
.identifier(key)
|
Ok(events) => {
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
let query = client
|
|
||||||
.get_events_of(vec![filter], Some(Duration::from_secs(10)))
|
|
||||||
.await;
|
|
||||||
|
|
||||||
if let Ok(events) = query {
|
|
||||||
if let Some(event) = events.first() {
|
if let Some(event) = events.first() {
|
||||||
println!("get nstore key: {} - received: {}", key, event.id);
|
|
||||||
|
|
||||||
let content = event.content();
|
let content = event.content();
|
||||||
|
match signer.nip44_decrypt(public_key, content).await {
|
||||||
match signer.nip44_decrypt(author, content).await {
|
|
||||||
Ok(decrypted) => Ok(decrypted),
|
Ok(decrypted) => Ok(decrypted),
|
||||||
Err(_) => Err(event.content.to_string()),
|
Err(_) => Err(event.content.to_string()),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
println!("get nstore key: {}", key);
|
|
||||||
Err("Value not found".into())
|
Err("Value not found".into())
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
Err("Query nstore event failed".into())
|
|
||||||
}
|
}
|
||||||
} else {
|
Err(err) => Err(err.to_string()),
|
||||||
Err("Something is wrong".into())
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Err("Signer is required".into())
|
Err("Signer is required".into())
|
||||||
|
|||||||
Reference in New Issue
Block a user