mirror of
https://github.com/luminous-devs/lume.git
synced 2024-09-28 16:00:48 +00:00
feat: add notification screen
This commit is contained in:
parent
28337e5915
commit
c843626bca
@ -78,7 +78,7 @@ export function Col({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [webview]);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full w-[440px] shrink-0 p-2">
|
<div className="h-full w-[440px] shrink-0 p-2">
|
||||||
|
@ -12,10 +12,7 @@ export function Conversation({
|
|||||||
className?: string;
|
className?: string;
|
||||||
}) {
|
}) {
|
||||||
const { ark } = useRouteContext({ strict: false });
|
const { ark } = useRouteContext({ strict: false });
|
||||||
const thread = ark.parse_event_thread({
|
const thread = ark.parse_event_thread(event.tags);
|
||||||
content: event.content,
|
|
||||||
tags: event.tags,
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Note.Provider event={event}>
|
<Note.Provider event={event}>
|
||||||
@ -26,9 +23,7 @@ export function Conversation({
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
{thread?.rootEventId ? (
|
{thread?.root ? <Note.Child eventId={thread?.root} isRoot /> : null}
|
||||||
<Note.Child eventId={thread?.rootEventId} 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" />
|
||||||
@ -36,9 +31,7 @@ export function Conversation({
|
|||||||
</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?.replyEventId ? (
|
{thread?.reply ? <Note.Child eventId={thread?.reply} /> : null}
|
||||||
<Note.Child eventId={thread?.replyEventId} />
|
|
||||||
) : 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 />
|
||||||
|
34
apps/desktop2/src/components/notification.tsx
Normal file
34
apps/desktop2/src/components/notification.tsx
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import type { Event } from "@lume/types";
|
||||||
|
import { Note } from "@lume/ui";
|
||||||
|
import { cn } from "@lume/utils";
|
||||||
|
|
||||||
|
export function Notification({
|
||||||
|
event,
|
||||||
|
className,
|
||||||
|
}: {
|
||||||
|
event: Event;
|
||||||
|
className?: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Note.Provider event={event}>
|
||||||
|
<Note.Root
|
||||||
|
className={cn(
|
||||||
|
"bg-white dark:bg-black/20 backdrop-blur-lg rounded-xl flex flex-col gap-3 shadow-primary dark:ring-1 ring-neutral-800/50",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<div>
|
||||||
|
<div className="px-3 h-14 flex items-center justify-between">
|
||||||
|
<Note.User />
|
||||||
|
</div>
|
||||||
|
<Note.Content className="px-3" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center h-14 px-3">
|
||||||
|
<Note.Open />
|
||||||
|
</div>
|
||||||
|
</Note.Root>
|
||||||
|
</Note.Provider>
|
||||||
|
);
|
||||||
|
}
|
@ -1,5 +1,4 @@
|
|||||||
import { ComposeFilledIcon, PlusIcon, SearchIcon } from "@lume/icons";
|
import { BellIcon, ComposeFilledIcon, PlusIcon, SearchIcon } from "@lume/icons";
|
||||||
import type { Account } from "@lume/types";
|
|
||||||
import { User } from "@lume/ui";
|
import { User } from "@lume/ui";
|
||||||
import { cn } from "@lume/utils";
|
import { cn } from "@lume/utils";
|
||||||
import { Outlet, createFileRoute } from "@tanstack/react-router";
|
import { Outlet, createFileRoute } from "@tanstack/react-router";
|
||||||
@ -10,9 +9,11 @@ export const Route = createFileRoute("/$account")({
|
|||||||
});
|
});
|
||||||
|
|
||||||
function Screen() {
|
function Screen() {
|
||||||
const navigate = Route.useNavigate();
|
const { account } = Route.useParams();
|
||||||
const { ark, platform } = Route.useRouteContext();
|
const { ark, platform } = Route.useRouteContext();
|
||||||
|
|
||||||
|
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
|
||||||
@ -31,15 +32,8 @@ function Screen() {
|
|||||||
>
|
>
|
||||||
<PlusIcon className="size-5" />
|
<PlusIcon className="size-5" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => ark.open_search()}
|
|
||||||
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"
|
|
||||||
>
|
|
||||||
<SearchIcon className="size-4" />
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-2">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => ark.open_editor()}
|
onClick={() => ark.open_editor()}
|
||||||
@ -48,6 +42,21 @@ function Screen() {
|
|||||||
<ComposeFilledIcon className="size-4" />
|
<ComposeFilledIcon className="size-4" />
|
||||||
New Post
|
New Post
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => ark.open_activity(account)}
|
||||||
|
className="relative inline-flex size-8 items-center justify-center rounded-full text-neutral-800 hover:bg-black/10 dark:text-neutral-200 dark:hover:bg-white/10"
|
||||||
|
>
|
||||||
|
<BellIcon className="size-5" />
|
||||||
|
{/* <span className="absolute right-0 top-0 block size-2 rounded-full bg-teal-500 ring-1 ring-black/5"></span> */}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => ark.open_search()}
|
||||||
|
className="inline-flex size-8 items-center justify-center rounded-full text-neutral-800 hover:bg-black/10 dark:text-neutral-200 dark:hover:bg-white/10"
|
||||||
|
>
|
||||||
|
<SearchIcon className="size-5" />
|
||||||
|
</button>
|
||||||
<div id="toolbar" />
|
<div id="toolbar" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -63,7 +72,7 @@ export function Accounts() {
|
|||||||
const { ark } = Route.useRouteContext();
|
const { ark } = Route.useRouteContext();
|
||||||
const { account } = Route.useParams();
|
const { account } = Route.useParams();
|
||||||
|
|
||||||
const [accounts, setAccounts] = useState<Account[]>([]);
|
const [accounts, setAccounts] = useState<string[]>([]);
|
||||||
|
|
||||||
const changeAccount = async (npub: string) => {
|
const changeAccount = async (npub: string) => {
|
||||||
if (npub === account) return;
|
if (npub === account) return;
|
||||||
@ -84,16 +93,12 @@ export function Accounts() {
|
|||||||
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
|
<button key={user} type="button" onClick={() => changeAccount(user)}>
|
||||||
key={user.npub}
|
<User.Provider pubkey={user}>
|
||||||
type="button"
|
|
||||||
onClick={() => changeAccount(user.npub)}
|
|
||||||
>
|
|
||||||
<User.Provider pubkey={user.npub}>
|
|
||||||
<User.Root
|
<User.Root
|
||||||
className={cn(
|
className={cn(
|
||||||
"rounded-full",
|
"rounded-full",
|
||||||
user.npub === 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"
|
||||||
: "",
|
: "",
|
||||||
)}
|
)}
|
||||||
@ -101,7 +106,7 @@ export function Accounts() {
|
|||||||
<User.Avatar
|
<User.Avatar
|
||||||
className={cn(
|
className={cn(
|
||||||
"aspect-square h-auto rounded-full object-cover",
|
"aspect-square h-auto rounded-full object-cover",
|
||||||
user.npub === account ? "w-7" : "w-8",
|
user === account ? "w-7" : "w-8",
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</User.Root>
|
</User.Root>
|
||||||
|
5
apps/desktop2/src/routes/activity.$account.messages.tsx
Normal file
5
apps/desktop2/src/routes/activity.$account.messages.tsx
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/activity/$account/messages")({
|
||||||
|
component: () => <div>Hello /activity/$account/messages!</div>,
|
||||||
|
});
|
60
apps/desktop2/src/routes/activity.$account.texts.tsx
Normal file
60
apps/desktop2/src/routes/activity.$account.texts.tsx
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import { Note, Spinner } from "@lume/ui";
|
||||||
|
import { Await, createFileRoute, defer } from "@tanstack/react-router";
|
||||||
|
import { Suspense } from "react";
|
||||||
|
import { Virtualizer } from "virtua";
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/activity/$account/texts")({
|
||||||
|
loader: async ({ context, params }) => {
|
||||||
|
const ark = context.ark;
|
||||||
|
return { data: defer(ark.get_activities(params.account, "1")) };
|
||||||
|
},
|
||||||
|
component: Screen,
|
||||||
|
});
|
||||||
|
|
||||||
|
function Screen() {
|
||||||
|
const { data } = Route.useLoaderData();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Virtualizer overscan={3}>
|
||||||
|
<Suspense
|
||||||
|
fallback={
|
||||||
|
<div className="flex h-20 w-full flex-col items-center justify-center gap-1">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="inline-flex items-center gap-2 text-sm font-medium"
|
||||||
|
disabled
|
||||||
|
>
|
||||||
|
<Spinner className="size-5" />
|
||||||
|
Loading...
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Await promise={data}>
|
||||||
|
{(events) =>
|
||||||
|
events.map((event) => (
|
||||||
|
<div
|
||||||
|
key={event.id}
|
||||||
|
className="flex flex-col gap-2 mb-3 bg-white dark:bg-black/20 backdrop-blur-lg rounded-xl shadow-primary dark:ring-1 ring-neutral-800/50"
|
||||||
|
>
|
||||||
|
<Note.Provider event={event}>
|
||||||
|
<Note.Root>
|
||||||
|
<div className="px-3 h-14 flex items-center justify-between">
|
||||||
|
<Note.User />
|
||||||
|
<Note.Menu />
|
||||||
|
</div>
|
||||||
|
<Note.Activity className="px-3" />
|
||||||
|
<Note.Content className="px-3" quote={false} clean />
|
||||||
|
<div className="mt-3 flex items-center gap-4 h-14 px-3">
|
||||||
|
<Note.Open />
|
||||||
|
</div>
|
||||||
|
</Note.Root>
|
||||||
|
</Note.Provider>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</Await>
|
||||||
|
</Suspense>
|
||||||
|
</Virtualizer>
|
||||||
|
);
|
||||||
|
}
|
50
apps/desktop2/src/routes/activity.$account.tsx
Normal file
50
apps/desktop2/src/routes/activity.$account.tsx
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import { Box, Container } from "@lume/ui";
|
||||||
|
import { cn } from "@lume/utils";
|
||||||
|
import { Link, Outlet } from "@tanstack/react-router";
|
||||||
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/activity/$account")({
|
||||||
|
component: Screen,
|
||||||
|
});
|
||||||
|
|
||||||
|
function Screen() {
|
||||||
|
const { account } = Route.useParams();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container withDrag withNavigate={false}>
|
||||||
|
<Box className="scrollbar-none shadow-none bg-black/5 dark:bg-white/5 backdrop-blur-sm flex flex-col overflow-y-auto">
|
||||||
|
<div className="h-14 shrink-0 flex w-full items-center gap-1 px-3">
|
||||||
|
<div className="inline-flex h-full w-full items-center gap-1">
|
||||||
|
<Link to="/activity/$account/texts" params={{ account }}>
|
||||||
|
{({ isActive }) => (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"inline-flex h-7 w-max items-center justify-center gap-2 rounded-full px-3 text-sm font-medium",
|
||||||
|
isActive ? "bg-neutral-50 dark:bg-white/10" : "opacity-50",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
Notes
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Link>
|
||||||
|
<Link to="/activity/$account/zaps" params={{ account }}>
|
||||||
|
{({ isActive }) => (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"inline-flex h-7 w-max items-center justify-center gap-2 rounded-full px-3 text-sm font-medium",
|
||||||
|
isActive ? "bg-neutral-50 dark:bg-white/10" : "opacity-50",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
Zaps
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="px-2 flex-1 overflow-y-auto w-full h-full scrollbar-none">
|
||||||
|
<Outlet />
|
||||||
|
</div>
|
||||||
|
</Box>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
64
apps/desktop2/src/routes/activity.$account.zaps.tsx
Normal file
64
apps/desktop2/src/routes/activity.$account.zaps.tsx
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import { Note, Spinner, User } from "@lume/ui";
|
||||||
|
import { decodeZapInvoice } from "@lume/utils";
|
||||||
|
import { Await, createFileRoute, defer } from "@tanstack/react-router";
|
||||||
|
import { Suspense } from "react";
|
||||||
|
import { Virtualizer } from "virtua";
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/activity/$account/zaps")({
|
||||||
|
loader: async ({ context, params }) => {
|
||||||
|
const ark = context.ark;
|
||||||
|
return { data: defer(ark.get_activities(params.account, "9735")) };
|
||||||
|
},
|
||||||
|
component: Screen,
|
||||||
|
});
|
||||||
|
|
||||||
|
function Screen() {
|
||||||
|
const { data } = Route.useLoaderData();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Virtualizer overscan={3}>
|
||||||
|
<Suspense
|
||||||
|
fallback={
|
||||||
|
<div className="flex h-20 w-full flex-col items-center justify-center gap-1">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="inline-flex items-center gap-2 text-sm font-medium"
|
||||||
|
disabled
|
||||||
|
>
|
||||||
|
<Spinner className="size-5" />
|
||||||
|
Loading...
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Await promise={data}>
|
||||||
|
{(events) =>
|
||||||
|
events.map((event) => (
|
||||||
|
<div
|
||||||
|
key={event.id}
|
||||||
|
className="flex flex-col gap-2 mb-3 bg-white dark:bg-black/20 backdrop-blur-lg rounded-xl shadow-primary dark:ring-1 ring-neutral-800/50"
|
||||||
|
>
|
||||||
|
<User.Provider pubkey={event.pubkey}>
|
||||||
|
<User.Root className="flex flex-col">
|
||||||
|
<div className="text-lg h-20 font-medium leading-tight flex w-full items-center justify-center">
|
||||||
|
₿ {decodeZapInvoice(event.tags).bitcoinFormatted}
|
||||||
|
</div>
|
||||||
|
<div className="h-11 border-t border-neutral-100 dark:border-neutral-900 flex items-center gap-1 px-2">
|
||||||
|
<div className="inline-flex items-center gap-2">
|
||||||
|
<User.Avatar className="size-7 rounded-full shrink-0" />
|
||||||
|
<User.Name className="text-sm font-medium" />
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-neutral-700 dark:text-neutral-300">
|
||||||
|
zapped you
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</User.Root>
|
||||||
|
</User.Provider>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</Await>
|
||||||
|
</Suspense>
|
||||||
|
</Virtualizer>
|
||||||
|
);
|
||||||
|
}
|
@ -2,6 +2,7 @@ import { PlusIcon } from "@lume/icons";
|
|||||||
import { Spinner, User } from "@lume/ui";
|
import { Spinner, User } from "@lume/ui";
|
||||||
import { Link } from "@tanstack/react-router";
|
import { Link } from "@tanstack/react-router";
|
||||||
import { createFileRoute, redirect } from "@tanstack/react-router";
|
import { createFileRoute, redirect } from "@tanstack/react-router";
|
||||||
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
export const Route = createFileRoute("/")({
|
export const Route = createFileRoute("/")({
|
||||||
@ -9,6 +10,11 @@ export const Route = createFileRoute("/")({
|
|||||||
const ark = context.ark;
|
const ark = context.ark;
|
||||||
const accounts = await ark.get_all_accounts();
|
const accounts = await ark.get_all_accounts();
|
||||||
|
|
||||||
|
// Run notification service
|
||||||
|
if (accounts.length > 0) {
|
||||||
|
await invoke("run_notification", { accounts });
|
||||||
|
}
|
||||||
|
|
||||||
switch (accounts.length) {
|
switch (accounts.length) {
|
||||||
// Guest account
|
// Guest account
|
||||||
case 0:
|
case 0:
|
||||||
@ -18,7 +24,7 @@ export const Route = createFileRoute("/")({
|
|||||||
});
|
});
|
||||||
// Only 1 account, skip account selection screen
|
// Only 1 account, skip account selection screen
|
||||||
case 1: {
|
case 1: {
|
||||||
const account = accounts[0].npub;
|
const account = accounts[0];
|
||||||
const loadedAccount = await ark.load_selected_account(account);
|
const loadedAccount = await ark.load_selected_account(account);
|
||||||
|
|
||||||
if (loadedAccount) {
|
if (loadedAccount) {
|
||||||
@ -83,10 +89,10 @@ function Screen() {
|
|||||||
{context.accounts.map((account) => (
|
{context.accounts.map((account) => (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
key={account.npub}
|
key={account}
|
||||||
onClick={() => select(account.npub)}
|
onClick={() => select(account)}
|
||||||
>
|
>
|
||||||
<User.Provider pubkey={account.npub}>
|
<User.Provider pubkey={account}>
|
||||||
<User.Root className="flex h-36 w-32 flex-col items-center justify-center gap-4 rounded-2xl p-2 hover:bg-white/10 dark:hover:bg-black/10">
|
<User.Root className="flex h-36 w-32 flex-col items-center justify-center gap-4 rounded-2xl p-2 hover:bg-white/10 dark:hover:bg-black/10">
|
||||||
<User.Avatar className="size-20 rounded-full object-cover" />
|
<User.Avatar className="size-20 rounded-full object-cover" />
|
||||||
<User.Name className="max-w-[5rem] truncate text-lg font-medium leading-tight text-white" />
|
<User.Name className="max-w-[5rem] truncate text-lg font-medium leading-tight text-white" />
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import { RepostNote } from "@/components/repost";
|
|
||||||
import { TextNote } from "@/components/text";
|
import { TextNote } from "@/components/text";
|
||||||
import { type Event, Kind } from "@lume/types";
|
import { type Event } from "@lume/types";
|
||||||
import { Spinner } from "@lume/ui";
|
import { Spinner } from "@lume/ui";
|
||||||
import { Await, createFileRoute } from "@tanstack/react-router";
|
import { Await, createFileRoute } from "@tanstack/react-router";
|
||||||
import { defer } from "@tanstack/react-router";
|
import { defer } from "@tanstack/react-router";
|
||||||
|
@ -21,7 +21,7 @@ export const Route = createFileRoute("/trending")({
|
|||||||
component: Screen,
|
component: Screen,
|
||||||
});
|
});
|
||||||
|
|
||||||
export function Screen() {
|
function Screen() {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full">
|
<div className="flex flex-col h-full">
|
||||||
<div className="h-11 shrink-0 inline-flex w-full items-center gap-1 px-3">
|
<div className="h-11 shrink-0 inline-flex w-full items-center gap-1 px-3">
|
||||||
|
@ -1,43 +0,0 @@
|
|||||||
import { Box, Container, User } from "@lume/ui";
|
|
||||||
import { createLazyFileRoute } from "@tanstack/react-router";
|
|
||||||
import { WindowVirtualizer } from "virtua";
|
|
||||||
import { EventList } from "./-components/eventList";
|
|
||||||
|
|
||||||
export const Route = createLazyFileRoute("/users/$pubkey")({
|
|
||||||
component: Screen,
|
|
||||||
});
|
|
||||||
|
|
||||||
function Screen() {
|
|
||||||
const { pubkey } = Route.useParams();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Container withDrag>
|
|
||||||
<Box className="px-0 scrollbar-none">
|
|
||||||
<WindowVirtualizer>
|
|
||||||
<User.Provider pubkey={pubkey}>
|
|
||||||
<User.Root>
|
|
||||||
<User.Cover className="h-44 w-full object-cover" />
|
|
||||||
<div className="relative -mt-8 flex flex-col gap-4 px-3">
|
|
||||||
<User.Avatar className="size-14 rounded-full" />
|
|
||||||
<div className="inline-flex items-start justify-between">
|
|
||||||
<div>
|
|
||||||
<User.Name className="font-semibold leading-tight" />
|
|
||||||
<User.NIP05 className="text-sm leading-tight text-neutral-600 dark:text-neutral-400" />
|
|
||||||
</div>
|
|
||||||
<User.Button className="h-9 w-24 rounded-full bg-black text-sm font-medium text-white hover:bg-neutral-900 dark:bg-neutral-900" />
|
|
||||||
</div>
|
|
||||||
<User.About />
|
|
||||||
</div>
|
|
||||||
</User.Root>
|
|
||||||
</User.Provider>
|
|
||||||
<div className="mt-4">
|
|
||||||
<div className="px-3">
|
|
||||||
<h3 className="text-lg font-semibold">Latest notes</h3>
|
|
||||||
</div>
|
|
||||||
<EventList id={pubkey} />
|
|
||||||
</div>
|
|
||||||
</WindowVirtualizer>
|
|
||||||
</Box>
|
|
||||||
</Container>
|
|
||||||
);
|
|
||||||
}
|
|
95
apps/desktop2/src/routes/users/$pubkey.tsx
Normal file
95
apps/desktop2/src/routes/users/$pubkey.tsx
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
import { Box, Container, Spinner, User } from "@lume/ui";
|
||||||
|
import { createFileRoute, defer } from "@tanstack/react-router";
|
||||||
|
import { WindowVirtualizer } from "virtua";
|
||||||
|
import { Conversation } from "@/components/conversation";
|
||||||
|
import { Quote } from "@/components/quote";
|
||||||
|
import { RepostNote } from "@/components/repost";
|
||||||
|
import { TextNote } from "@/components/text";
|
||||||
|
import { Event, Kind } from "@lume/types";
|
||||||
|
import { Suspense } from "react";
|
||||||
|
import { Await } from "@tanstack/react-router";
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/users/$pubkey")({
|
||||||
|
beforeLoad: async ({ context }) => {
|
||||||
|
const ark = context.ark;
|
||||||
|
const settings = await ark.get_settings();
|
||||||
|
|
||||||
|
return { settings };
|
||||||
|
},
|
||||||
|
loader: async ({ params, context }) => {
|
||||||
|
const ark = context.ark;
|
||||||
|
return { data: defer(ark.get_events_from(params.pubkey, 50)) };
|
||||||
|
},
|
||||||
|
component: Screen,
|
||||||
|
});
|
||||||
|
|
||||||
|
function Screen() {
|
||||||
|
const { pubkey } = Route.useParams();
|
||||||
|
const { data } = Route.useLoaderData();
|
||||||
|
|
||||||
|
const renderItem = (event: Event) => {
|
||||||
|
if (!event) return;
|
||||||
|
switch (event.kind) {
|
||||||
|
case Kind.Repost:
|
||||||
|
return <RepostNote key={event.id} event={event} />;
|
||||||
|
default: {
|
||||||
|
const isConversation =
|
||||||
|
event.tags.filter((tag) => tag[0] === "e" && tag[3] !== "mention")
|
||||||
|
.length > 0;
|
||||||
|
const isQuote = event.tags.filter((tag) => tag[0] === "q").length > 0;
|
||||||
|
|
||||||
|
if (isConversation) {
|
||||||
|
return <Conversation key={event.id} event={event} className="mb-3" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isQuote) {
|
||||||
|
return <Quote key={event.id} event={event} className="mb-3" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <TextNote key={event.id} event={event} className="mb-3" />;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container withDrag>
|
||||||
|
<Box className="px-0 scrollbar-none bg-black/5 dark:bg-white/5 backdrop-blur-sm">
|
||||||
|
<WindowVirtualizer>
|
||||||
|
<User.Provider pubkey={pubkey}>
|
||||||
|
<User.Root>
|
||||||
|
<User.Cover className="h-44 w-full object-cover" />
|
||||||
|
<div className="relative -mt-8 flex flex-col px-3">
|
||||||
|
<User.Avatar className="size-14 rounded-full" />
|
||||||
|
<div className="mb-4 inline-flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<User.Name className="text-lg font-semibold leading-tight" />
|
||||||
|
<User.NIP05 />
|
||||||
|
</div>
|
||||||
|
<User.Button className="h-9 w-24 rounded-full inline-flex items-center justify-center bg-black text-sm font-medium text-white hover:bg-neutral-900 dark:bg-neutral-900" />
|
||||||
|
</div>
|
||||||
|
<User.About />
|
||||||
|
</div>
|
||||||
|
</User.Root>
|
||||||
|
</User.Provider>
|
||||||
|
<div className="px-3 mt-5">
|
||||||
|
<div className="mb-3">
|
||||||
|
<h3 className="text-lg font-semibold">Latest notes</h3>
|
||||||
|
</div>
|
||||||
|
<Suspense
|
||||||
|
fallback={
|
||||||
|
<div className="flex h-20 w-full items-center justify-center gap-1.5 text-sm font-medium">
|
||||||
|
<Spinner className="size-5" />
|
||||||
|
Loading...
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Await promise={data}>
|
||||||
|
{(events) => events.map((event) => renderItem(event))}
|
||||||
|
</Await>
|
||||||
|
</Suspense>
|
||||||
|
</div>
|
||||||
|
</WindowVirtualizer>
|
||||||
|
</Box>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
@ -19,10 +19,8 @@ export function EventList({ id }: { id: string }) {
|
|||||||
},
|
},
|
||||||
getNextPageParam: (lastPage) => {
|
getNextPageParam: (lastPage) => {
|
||||||
const lastEvent = lastPage?.at(-1);
|
const lastEvent = lastPage?.at(-1);
|
||||||
if (!lastEvent) return;
|
return lastEvent ? lastEvent.created_at - 1 : null;
|
||||||
return lastEvent.created_at - 1;
|
|
||||||
},
|
},
|
||||||
select: (data) => data?.pages.flatMap((page) => page),
|
|
||||||
refetchOnWindowFocus: false,
|
refetchOnWindowFocus: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
Kind,
|
Kind,
|
||||||
type Account,
|
|
||||||
type Contact,
|
type Contact,
|
||||||
type Event,
|
type Event,
|
||||||
type EventWithReplies,
|
type EventWithReplies,
|
||||||
@ -33,17 +32,12 @@ export class Ark {
|
|||||||
|
|
||||||
public async get_all_accounts() {
|
public async get_all_accounts() {
|
||||||
try {
|
try {
|
||||||
const accounts: Account[] = [];
|
|
||||||
const cmd: string[] = await invoke("get_accounts");
|
const cmd: string[] = await invoke("get_accounts");
|
||||||
|
const accounts: string[] = cmd.map((item) => item.replace(".npub", ""));
|
||||||
|
|
||||||
if (cmd) {
|
return accounts;
|
||||||
for (const item of cmd) {
|
} catch (e) {
|
||||||
accounts.push({ npub: item.replace(".npub", "") });
|
throw new Error(String(e));
|
||||||
}
|
|
||||||
return accounts;
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
return [];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -52,14 +46,22 @@ export class Ark {
|
|||||||
const cmd: boolean = await invoke("load_selected_account", {
|
const cmd: boolean = await invoke("load_selected_account", {
|
||||||
npub,
|
npub,
|
||||||
});
|
});
|
||||||
await invoke("connect_user_relays");
|
|
||||||
|
|
||||||
return cmd;
|
return cmd;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new Error(String(e));
|
throw new Error(String(e));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async get_activities(account: string, kind: "1" | "6" | "9735" = "1") {
|
||||||
|
try {
|
||||||
|
const events: Event[] = await invoke("get_activities", { account, kind });
|
||||||
|
return events;
|
||||||
|
} catch (e) {
|
||||||
|
console.error(String(e));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async nostr_connect(uri: string) {
|
public async nostr_connect(uri: string) {
|
||||||
try {
|
try {
|
||||||
const remoteKey = uri.replace("bunker://", "").split("?")[0];
|
const remoteKey = uri.replace("bunker://", "").split("?")[0];
|
||||||
@ -130,13 +132,14 @@ export class Ark {
|
|||||||
if (asOf && asOf > 0) until = asOf.toString();
|
if (asOf && asOf > 0) until = asOf.toString();
|
||||||
|
|
||||||
const nostrEvents: Event[] = await invoke("get_events_from", {
|
const nostrEvents: Event[] = await invoke("get_events_from", {
|
||||||
public_key: pubkey,
|
publicKey: pubkey,
|
||||||
limit,
|
limit,
|
||||||
as_of: until,
|
as_of: until,
|
||||||
});
|
});
|
||||||
|
|
||||||
return nostrEvents.sort((a, b) => b.created_at - a.created_at);
|
return nostrEvents.sort((a, b) => b.created_at - a.created_at);
|
||||||
} catch {
|
} catch (e) {
|
||||||
|
console.error(String(e));
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -377,39 +380,30 @@ export class Ark {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public parse_event_thread({
|
public parse_event_thread(tags: string[][]) {
|
||||||
content,
|
let root: string = null;
|
||||||
tags,
|
let reply: string = null;
|
||||||
}: {
|
|
||||||
content: string;
|
|
||||||
tags: string[][];
|
|
||||||
}) {
|
|
||||||
let rootEventId: string = null;
|
|
||||||
let replyEventId: string = null;
|
|
||||||
|
|
||||||
// Get all event references from tags, ignore mention
|
// Get all event references from tags, ignore mention
|
||||||
const events = tags.filter((el) => el[0] === "e" && el[3] !== "mention");
|
const events = tags.filter((el) => el[0] === "e" && el[3] !== "mention");
|
||||||
|
|
||||||
if (!events.length) return null;
|
|
||||||
if (events.length === 1) {
|
if (events.length === 1) {
|
||||||
return {
|
root = events[0][1];
|
||||||
rootEventId: events[0][1],
|
|
||||||
replyEventId: null,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
if (events.length > 1) {
|
|
||||||
rootEventId = events.find((el) => el[3] === "root")?.[1];
|
|
||||||
replyEventId = events.find((el) => el[3] === "reply")?.[1];
|
|
||||||
|
|
||||||
if (!rootEventId && !replyEventId) {
|
if (events.length > 1) {
|
||||||
rootEventId = events[0][1];
|
root = events.find((el) => el[3] === "root")?.[1] ?? events[0][1];
|
||||||
replyEventId = events[1][1];
|
reply = events.find((el) => el[3] === "reply")?.[1] ?? events[1][1];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fix some rare case when root === reply
|
||||||
|
if (root && reply && root === reply) {
|
||||||
|
reply = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
rootEventId,
|
root,
|
||||||
replyEventId,
|
reply,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -879,4 +873,19 @@ export class Ark {
|
|||||||
throw new Error(String(e));
|
throw new Error(String(e));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async open_activity(account: string) {
|
||||||
|
try {
|
||||||
|
const label = "activity";
|
||||||
|
await invoke("open_window", {
|
||||||
|
label,
|
||||||
|
title: "Activity",
|
||||||
|
url: `/activity/${account}/texts`,
|
||||||
|
width: 400,
|
||||||
|
height: 600,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error(String(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,9 +7,8 @@ export function BellIcon(
|
|||||||
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" {...props}>
|
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" {...props}>
|
||||||
<path
|
<path
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
strokeLinejoin="round"
|
strokeWidth="1.5"
|
||||||
strokeWidth="2"
|
d="M16 18.25c-.673 1.766-2.21 3-4 3s-3.327-1.234-4-3m-2.152 0h12.304a2 2 0 0 0 1.974-2.319l-1.17-7.258a7.045 7.045 0 0 0-13.911 0l-1.171 7.258a2 2 0 0 0 1.974 2.319Z"
|
||||||
d="M16 18c-.673 1.766-2.21 3-4 3s-3.327-1.234-4-3m-1.716 0h11.432a2 2 0 0 0 1.982-2.264l-.905-6.789a6.853 6.853 0 0 0-13.586 0l-.905 6.789A2 2 0 0 0 6.284 18Z"
|
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
|
@ -1,22 +1,22 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/tsconfig",
|
"$schema": "https://json.schemastore.org/tsconfig",
|
||||||
"display": "Default",
|
"display": "Default",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "es2017",
|
"target": "es2017",
|
||||||
"lib": ["dom", "dom.iterable", "esnext"],
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"strict": false,
|
"strict": false,
|
||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"incremental": true,
|
"incremental": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"module": "esnext",
|
"module": "esnext",
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"jsx": "preserve",
|
"jsx": "preserve",
|
||||||
"strictNullChecks": false,
|
"strictNullChecks": false
|
||||||
},
|
},
|
||||||
"exclude": ["node_modules", "src-tauri"]
|
"exclude": ["node_modules", "src-tauri"]
|
||||||
}
|
}
|
||||||
|
@ -14,11 +14,16 @@ export function Container({
|
|||||||
className?: string;
|
className?: string;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className={cn("flex h-screen w-screen flex-col", className)}>
|
<div
|
||||||
|
className={cn(
|
||||||
|
"bg-transparent flex h-screen w-screen flex-col",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
{withDrag ? (
|
{withDrag ? (
|
||||||
<div
|
<div
|
||||||
data-tauri-drag-region
|
data-tauri-drag-region
|
||||||
className="flex h-11 w-full shrink-0 items-center justify-end pr-2"
|
className="bg-transparent flex h-11 w-full shrink-0 items-center justify-end pr-2"
|
||||||
>
|
>
|
||||||
{withNavigate ? (
|
{withNavigate ? (
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
|
26
packages/ui/src/note/activity.tsx
Normal file
26
packages/ui/src/note/activity.tsx
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { cn } from "@lume/utils";
|
||||||
|
import { useNoteContext } from "./provider";
|
||||||
|
import { User } from "../user";
|
||||||
|
|
||||||
|
export function NoteActivity({ className }: { className?: string }) {
|
||||||
|
const event = useNoteContext();
|
||||||
|
const mentions = event.tags
|
||||||
|
.filter((tag) => tag[0] === "p")
|
||||||
|
.map((tag) => tag[1]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn("-mt-3 mb-2", className)}>
|
||||||
|
<div className="text-neutral-700 dark:text-neutral-300 inline-flex items-baseline gap-2 w-full overflow-hidden">
|
||||||
|
<div className="shrink-0 text-sm font-medium">To:</div>
|
||||||
|
{mentions.splice(0, 4).map((mention) => (
|
||||||
|
<User.Provider key={mention} pubkey={mention}>
|
||||||
|
<User.Root>
|
||||||
|
<User.Name className="text-sm font-medium" />
|
||||||
|
</User.Root>
|
||||||
|
</User.Provider>
|
||||||
|
))}
|
||||||
|
{mentions.length > 4 ? "..." : ""}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -18,7 +18,7 @@ export function NoteReply({ large = false }: { large?: boolean }) {
|
|||||||
className={cn(
|
className={cn(
|
||||||
"inline-flex items-center justify-center text-neutral-800 dark:text-neutral-200",
|
"inline-flex items-center justify-center text-neutral-800 dark:text-neutral-200",
|
||||||
large
|
large
|
||||||
? "bg-neutral-100 dark:bg-white/10 h-7 gap-1.5 w-24 text-sm font-medium hover:text-blue-500 hover:bg-neutral-200 dark:hover:bg-white/20"
|
? "rounded-full bg-neutral-100 dark:bg-white/10 h-7 gap-1.5 w-24 text-sm font-medium hover:text-blue-500 hover:bg-neutral-200 dark:hover:bg-white/20"
|
||||||
: "size-7",
|
: "size-7",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
@ -31,7 +31,7 @@ export function NoteZap({ large = false }: { large?: boolean }) {
|
|||||||
className={cn(
|
className={cn(
|
||||||
"inline-flex items-center justify-center text-neutral-800 dark:text-neutral-200",
|
"inline-flex items-center justify-center text-neutral-800 dark:text-neutral-200",
|
||||||
large
|
large
|
||||||
? "bg-neutral-100 dark:bg-white/10 h-7 gap-1.5 w-24 text-sm font-medium hover:text-blue-500 hover:bg-neutral-200 dark:hover:bg-white/20"
|
? "rounded-full bg-neutral-100 dark:bg-white/10 h-7 gap-1.5 w-24 text-sm font-medium hover:text-blue-500 hover:bg-neutral-200 dark:hover:bg-white/20"
|
||||||
: "size-7",
|
: "size-7",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
import { useEvent } from "@lume/ark";
|
import { useEvent } from "@lume/ark";
|
||||||
import { cn } from "@lume/utils";
|
import { cn } from "@lume/utils";
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { Note } from ".";
|
import { Note } from ".";
|
||||||
import { User } from "../user";
|
|
||||||
|
|
||||||
export function NoteChild({
|
export function NoteChild({
|
||||||
eventId,
|
eventId,
|
||||||
|
@ -82,7 +82,6 @@ export function NoteContent({
|
|||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
className="line-clamp-1 text-blue-500 hover:text-blue-600"
|
className="line-clamp-1 text-blue-500 hover:text-blue-600"
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
>
|
>
|
||||||
{match}
|
{match}
|
||||||
</a>
|
</a>
|
||||||
|
@ -145,7 +145,9 @@ export function NoteContentLarge({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn("select-text", className)}>
|
<div className={cn("select-text", className)}>
|
||||||
<div className="text-[15px] text-balance content-break leading-normal">{content}</div>
|
<div className="text-[15px] text-balance content-break leading-normal">
|
||||||
|
{content}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { NoteActivity } from "./activity";
|
||||||
import { NoteOpenThread } from "./buttons/open";
|
import { NoteOpenThread } from "./buttons/open";
|
||||||
import { NoteReply } from "./buttons/reply";
|
import { NoteReply } from "./buttons/reply";
|
||||||
import { NoteRepost } from "./buttons/repost";
|
import { NoteRepost } from "./buttons/repost";
|
||||||
@ -24,4 +25,5 @@ export const Note = {
|
|||||||
Open: NoteOpenThread,
|
Open: NoteOpenThread,
|
||||||
Child: NoteChild,
|
Child: NoteChild,
|
||||||
Thread: NoteThread,
|
Thread: NoteThread,
|
||||||
|
Activity: NoteActivity,
|
||||||
};
|
};
|
||||||
|
@ -8,6 +8,8 @@ export * from "./src/notification";
|
|||||||
export * from "./src/cn";
|
export * from "./src/cn";
|
||||||
export * from "./src/image";
|
export * from "./src/image";
|
||||||
export * from "./src/parser";
|
export * from "./src/parser";
|
||||||
|
export * from "./src/groupBy";
|
||||||
|
export * from "./src/invoice";
|
||||||
|
|
||||||
// Hooks
|
// Hooks
|
||||||
export * from "./src/hooks/useNetworkStatus";
|
export * from "./src/hooks/useNetworkStatus";
|
||||||
|
@ -12,6 +12,7 @@
|
|||||||
"bitcoin-units": "^1.0.0",
|
"bitcoin-units": "^1.0.0",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"dayjs": "^1.11.11",
|
"dayjs": "^1.11.11",
|
||||||
|
"light-bolt11-decoder": "^3.1.1",
|
||||||
"nostr-tools": "^2.5.1",
|
"nostr-tools": "^2.5.1",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
|
21
packages/utils/src/groupBy.ts
Normal file
21
packages/utils/src/groupBy.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
export const groupBy = <T>(
|
||||||
|
array: T[],
|
||||||
|
predicate: (value: T, index: number, array: T[]) => string,
|
||||||
|
) =>
|
||||||
|
array.reduce(
|
||||||
|
(acc, value, index, array) => {
|
||||||
|
(acc[predicate(value, index, array)] ||= []).push(value);
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{} as { [key: string]: T[] },
|
||||||
|
);
|
||||||
|
|
||||||
|
export const groupByToMap = <T, Q>(
|
||||||
|
array: T[],
|
||||||
|
predicate: (value: T, index: number, array: T[]) => Q,
|
||||||
|
) =>
|
||||||
|
array.reduce((map, value, index, array) => {
|
||||||
|
const key = predicate(value, index, array);
|
||||||
|
map.get(key)?.push(value) ?? map.set(key, [value]);
|
||||||
|
return map;
|
||||||
|
}, new Map<Q, T[]>());
|
17
packages/utils/src/invoice.ts
Normal file
17
packages/utils/src/invoice.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { decode } from "light-bolt11-decoder";
|
||||||
|
import { getBitcoinDisplayValues } from "./formater";
|
||||||
|
|
||||||
|
export function decodeZapInvoice(tags?: string[][]) {
|
||||||
|
const invoice = tags.find((tag) => tag[0] === "bolt11")?.[1];
|
||||||
|
if (!invoice) return;
|
||||||
|
|
||||||
|
const decodedInvoice = decode(invoice);
|
||||||
|
const amountSection = decodedInvoice.sections.find(
|
||||||
|
(s: any) => s.name === "amount",
|
||||||
|
);
|
||||||
|
|
||||||
|
const amount = parseInt(amountSection.value);
|
||||||
|
const displayValue = getBitcoinDisplayValues(amount);
|
||||||
|
|
||||||
|
return displayValue;
|
||||||
|
}
|
@ -448,6 +448,9 @@ importers:
|
|||||||
dayjs:
|
dayjs:
|
||||||
specifier: ^1.11.11
|
specifier: ^1.11.11
|
||||||
version: 1.11.11
|
version: 1.11.11
|
||||||
|
light-bolt11-decoder:
|
||||||
|
specifier: ^3.1.1
|
||||||
|
version: 3.1.1
|
||||||
nostr-tools:
|
nostr-tools:
|
||||||
specifier: ^2.5.1
|
specifier: ^2.5.1
|
||||||
version: 2.5.1(typescript@5.4.5)
|
version: 2.5.1(typescript@5.4.5)
|
||||||
@ -4329,6 +4332,12 @@ packages:
|
|||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/light-bolt11-decoder@3.1.1:
|
||||||
|
resolution: {integrity: sha512-sLg/KCwYkgsHWkefWd6KqpCHrLFWWaXTOX3cf6yD2hAzL0SLpX+lFcaFK2spkjbgzG6hhijKfORDc9WoUHwX0A==}
|
||||||
|
dependencies:
|
||||||
|
'@scure/base': 1.1.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
/lilconfig@2.1.0:
|
/lilconfig@2.1.0:
|
||||||
resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==}
|
resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
@ -1,70 +1,75 @@
|
|||||||
{
|
{
|
||||||
"$schema": "../gen/schemas/desktop-schema.json",
|
"$schema": "../gen/schemas/desktop-schema.json",
|
||||||
"identifier": "desktop-capability",
|
"identifier": "desktop-capability",
|
||||||
"description": "Capability for the desktop",
|
"description": "Capability for the desktop",
|
||||||
"platforms": ["linux", "macOS", "windows"],
|
"platforms": [
|
||||||
"windows": [
|
"linux",
|
||||||
"main",
|
"macOS",
|
||||||
"splash",
|
"windows"
|
||||||
"settings",
|
],
|
||||||
"search",
|
"windows": [
|
||||||
"nwc",
|
"main",
|
||||||
"zap-*",
|
"splash",
|
||||||
"event-*",
|
"settings",
|
||||||
"user-*",
|
"search",
|
||||||
"editor-*",
|
"nwc",
|
||||||
"column-*"
|
"activity",
|
||||||
],
|
"zap-*",
|
||||||
"permissions": [
|
"event-*",
|
||||||
"path:default",
|
"user-*",
|
||||||
"event:default",
|
"editor-*",
|
||||||
"window:default",
|
"column-*"
|
||||||
"app:default",
|
],
|
||||||
"resources:default",
|
"permissions": [
|
||||||
"menu:default",
|
"path:default",
|
||||||
"tray:default",
|
"event:default",
|
||||||
"notification:allow-is-permission-granted",
|
"window:default",
|
||||||
"notification:allow-request-permission",
|
"app:default",
|
||||||
"notification:default",
|
"resources:default",
|
||||||
"os:allow-locale",
|
"menu:default",
|
||||||
"os:allow-platform",
|
"tray:default",
|
||||||
"updater:allow-check",
|
"notification:allow-is-permission-granted",
|
||||||
"updater:default",
|
"notification:allow-request-permission",
|
||||||
"window:allow-start-dragging",
|
"notification:default",
|
||||||
"window:allow-create",
|
"os:allow-locale",
|
||||||
"window:allow-close",
|
"os:allow-platform",
|
||||||
"window:allow-set-focus",
|
"updater:allow-check",
|
||||||
"clipboard-manager:allow-write",
|
"updater:default",
|
||||||
"clipboard-manager:allow-read",
|
"window:allow-start-dragging",
|
||||||
"webview:allow-create-webview-window",
|
"window:allow-create",
|
||||||
"webview:allow-create-webview",
|
"window:allow-close",
|
||||||
"webview:allow-set-webview-size",
|
"window:allow-set-focus",
|
||||||
"webview:allow-set-webview-position",
|
"clipboard-manager:allow-write",
|
||||||
"webview:allow-webview-close",
|
"clipboard-manager:allow-read",
|
||||||
"dialog:allow-open",
|
"webview:allow-create-webview-window",
|
||||||
"fs:allow-read-file",
|
"webview:allow-create-webview",
|
||||||
"shell:allow-open",
|
"webview:allow-set-webview-size",
|
||||||
{
|
"webview:allow-set-webview-position",
|
||||||
"identifier": "http:default",
|
"webview:allow-webview-close",
|
||||||
"allow": [
|
"dialog:allow-open",
|
||||||
{
|
"fs:allow-read-file",
|
||||||
"url": "http://**/"
|
"shell:allow-open",
|
||||||
},
|
{
|
||||||
{
|
"identifier": "http:default",
|
||||||
"url": "https://**/"
|
"allow": [
|
||||||
}
|
{
|
||||||
]
|
"url": "http://**/"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"identifier": "fs:allow-read-text-file",
|
"url": "https://**/"
|
||||||
"allow": [
|
}
|
||||||
{
|
]
|
||||||
"path": "$RESOURCE/locales/*"
|
},
|
||||||
},
|
{
|
||||||
{
|
"identifier": "fs:allow-read-text-file",
|
||||||
"path": "$RESOURCE/resources/*"
|
"allow": [
|
||||||
}
|
{
|
||||||
]
|
"path": "$RESOURCE/locales/*"
|
||||||
}
|
},
|
||||||
]
|
{
|
||||||
|
"path": "$RESOURCE/resources/*"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
@ -1 +1 @@
|
|||||||
{"desktop-capability":{"identifier":"desktop-capability","description":"Capability for the desktop","local":true,"windows":["main","splash","settings","search","nwc","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:allow-check","updater:default","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:allow-open","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:allow-check","updater:default","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:allow-open","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"]}}
|
@ -2,6 +2,7 @@ use std::path::PathBuf;
|
|||||||
use tauri::utils::config::WindowEffectsConfig;
|
use tauri::utils::config::WindowEffectsConfig;
|
||||||
use tauri::window::Effect;
|
use tauri::window::Effect;
|
||||||
use tauri::TitleBarStyle;
|
use tauri::TitleBarStyle;
|
||||||
|
use tauri::Url;
|
||||||
use tauri::WebviewWindowBuilder;
|
use tauri::WebviewWindowBuilder;
|
||||||
use tauri::{LogicalPosition, LogicalSize, Manager, WebviewUrl};
|
use tauri::{LogicalPosition, LogicalSize, Manager, WebviewUrl};
|
||||||
|
|
||||||
@ -54,6 +55,30 @@ pub fn close_column(label: &str, app_handle: tauri::AppHandle) -> Result<bool, (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn get_path(label: &str, app_handle: tauri::AppHandle) -> Result<String, String> {
|
||||||
|
match app_handle.get_webview(label) {
|
||||||
|
Some(webview) => Ok(webview.url().to_string()),
|
||||||
|
None => Err("Webview not found".into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn navigate(label: &str, url: &str, app_handle: tauri::AppHandle) -> Result<(), String> {
|
||||||
|
match app_handle.get_webview(label) {
|
||||||
|
Some(mut webview) => {
|
||||||
|
if let Ok(new_url) = Url::parse(url) {
|
||||||
|
println!("navigate to: {}", new_url);
|
||||||
|
webview.navigate(new_url);
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err("URL is not valid".into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => Err("Webview not found".into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn reposition_column(
|
pub fn reposition_column(
|
||||||
label: &str,
|
label: &str,
|
||||||
@ -125,6 +150,11 @@ pub fn open_window(
|
|||||||
})
|
})
|
||||||
.build()
|
.build()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
// [macOS] Custom traffic light possition
|
||||||
|
// #[cfg(target_os = "macos")]
|
||||||
|
// setup_traffic_light_positioner(app_handle.get_window(label).unwrap());
|
||||||
|
|
||||||
#[cfg(not(target_os = "macos"))]
|
#[cfg(not(target_os = "macos"))]
|
||||||
let _ = WebviewWindowBuilder::new(&app_handle, label, WebviewUrl::App(PathBuf::from(url)))
|
let _ = WebviewWindowBuilder::new(&app_handle, label, WebviewUrl::App(PathBuf::from(url)))
|
||||||
.title(title)
|
.title(title)
|
||||||
|
@ -29,20 +29,18 @@ pub struct Nostr {
|
|||||||
fn main() {
|
fn main() {
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
.setup(|app| {
|
.setup(|app| {
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
app.set_activation_policy(tauri::ActivationPolicy::Regular);
|
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
setup_traffic_light_positioner(app.get_window("main").unwrap());
|
setup_traffic_light_positioner(app.get_window("main").unwrap());
|
||||||
|
|
||||||
let _tray = tray::create_tray(app.handle()).unwrap();
|
// Setup app tray
|
||||||
let handle = app.handle().clone();
|
let handle = app.handle().clone();
|
||||||
let home_dir = handle.path().home_dir().unwrap();
|
let _ = tray::create_tray(app.handle()).unwrap();
|
||||||
|
|
||||||
// create data folder if not exist
|
// Create data folder if not exist
|
||||||
|
let home_dir = app.path().home_dir().unwrap();
|
||||||
fs::create_dir_all(home_dir.join("Lume/")).unwrap();
|
fs::create_dir_all(home_dir.join("Lume/")).unwrap();
|
||||||
|
|
||||||
tauri::async_runtime::spawn(async move {
|
tauri::async_runtime::block_on(async move {
|
||||||
// Create nostr database connection
|
// Create nostr database connection
|
||||||
let sqlite = SQLiteDatabase::open(home_dir.join("Lume/lume.db")).await;
|
let sqlite = SQLiteDatabase::open(home_dir.join("Lume/lume.db")).await;
|
||||||
|
|
||||||
@ -59,13 +57,9 @@ fn main() {
|
|||||||
.await
|
.await
|
||||||
.expect("Cannot connect to relay.nostr.net, please try again later.");
|
.expect("Cannot connect to relay.nostr.net, please try again later.");
|
||||||
client
|
client
|
||||||
.add_relay("wss://relay.nostr.band")
|
.add_relay("wss://bostr.nokotaro.work/")
|
||||||
.await
|
.await
|
||||||
.expect("Cannot connect to relay.nostr.band, please try again later.");
|
.expect("Cannot connect to bostr.nokotaro.work, please try again later.");
|
||||||
client
|
|
||||||
.add_relay("wss://welcome.nostr.wine")
|
|
||||||
.await
|
|
||||||
.expect("Cannot connect to welcome.nostr.wine, please try again later.");
|
|
||||||
|
|
||||||
// Connect
|
// Connect
|
||||||
client.connect().await;
|
client.connect().await;
|
||||||
@ -111,7 +105,8 @@ fn main() {
|
|||||||
nostr::keys::user_to_bech32,
|
nostr::keys::user_to_bech32,
|
||||||
nostr::keys::to_npub,
|
nostr::keys::to_npub,
|
||||||
nostr::keys::verify_nip05,
|
nostr::keys::verify_nip05,
|
||||||
nostr::metadata::connect_user_relays,
|
nostr::metadata::run_notification,
|
||||||
|
nostr::metadata::get_activities,
|
||||||
nostr::metadata::get_current_user_profile,
|
nostr::metadata::get_current_user_profile,
|
||||||
nostr::metadata::get_profile,
|
nostr::metadata::get_profile,
|
||||||
nostr::metadata::get_contact_list,
|
nostr::metadata::get_contact_list,
|
||||||
@ -141,7 +136,9 @@ fn main() {
|
|||||||
commands::window::close_column,
|
commands::window::close_column,
|
||||||
commands::window::reposition_column,
|
commands::window::reposition_column,
|
||||||
commands::window::resize_column,
|
commands::window::resize_column,
|
||||||
commands::window::open_window
|
commands::window::open_window,
|
||||||
|
commands::window::get_path,
|
||||||
|
commands::window::navigate
|
||||||
])
|
])
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application")
|
.expect("error while running tauri application")
|
||||||
|
@ -54,7 +54,7 @@ pub async fn get_events_from(
|
|||||||
};
|
};
|
||||||
let filter = Filter::new()
|
let filter = Filter::new()
|
||||||
.kinds(vec![Kind::TextNote, Kind::Repost])
|
.kinds(vec![Kind::TextNote, Kind::Repost])
|
||||||
.authors(vec![author])
|
.author(author)
|
||||||
.limit(limit)
|
.limit(limit)
|
||||||
.until(until);
|
.until(until);
|
||||||
|
|
||||||
|
@ -172,6 +172,41 @@ pub async fn load_selected_account(npub: &str, state: State<'_, Nostr>) -> Resul
|
|||||||
client.set_signer(Some(signer)).await;
|
client.set_signer(Some(signer)).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Verify signer
|
||||||
|
let signer = client.signer().await.unwrap();
|
||||||
|
let public_key = signer.public_key().await.unwrap();
|
||||||
|
|
||||||
|
// Connect to user's relay
|
||||||
|
let filter = Filter::new()
|
||||||
|
.author(public_key)
|
||||||
|
.kind(Kind::RelayList)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
match client
|
||||||
|
.get_events_of(vec![filter], Some(Duration::from_secs(10)))
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(events) => {
|
||||||
|
if let Some(event) = events.first() {
|
||||||
|
let relay_list = nip65::extract_relay_list(&event);
|
||||||
|
for item in relay_list.into_iter() {
|
||||||
|
println!("connecting to relay: {}", item.0.to_string());
|
||||||
|
// Add relay to pool
|
||||||
|
let _ = client
|
||||||
|
.add_relay(item.0.to_string())
|
||||||
|
.await
|
||||||
|
.unwrap_or_default();
|
||||||
|
// Connect relay
|
||||||
|
let _ = client
|
||||||
|
.connect_relay(item.0.to_string())
|
||||||
|
.await
|
||||||
|
.unwrap_or_default();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => todo!(),
|
||||||
|
};
|
||||||
|
|
||||||
Ok(true)
|
Ok(true)
|
||||||
}
|
}
|
||||||
Err(err) => Err(err.to_string()),
|
Err(err) => Err(err.to_string()),
|
||||||
|
@ -2,7 +2,7 @@ use crate::Nostr;
|
|||||||
use keyring::Entry;
|
use keyring::Entry;
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use std::{str::FromStr, time::Duration};
|
use std::{str::FromStr, time::Duration};
|
||||||
use tauri::State;
|
use tauri::{Manager, State};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
#[derive(serde::Serialize)]
|
#[derive(serde::Serialize)]
|
||||||
@ -12,37 +12,80 @@ pub struct CacheContact {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn connect_user_relays(state: State<'_, Nostr>) -> Result<(), ()> {
|
pub fn run_notification(accounts: Vec<String>, app: tauri::AppHandle) -> Result<(), ()> {
|
||||||
let client = &state.client;
|
tauri::async_runtime::spawn(async move {
|
||||||
let signer = client.signer().await.unwrap();
|
let window = app.get_window("main").unwrap();
|
||||||
let public_key = signer.public_key().await.unwrap();
|
let state = window.state::<Nostr>();
|
||||||
|
let client = &state.client;
|
||||||
|
let pubkeys: Vec<PublicKey> = accounts
|
||||||
|
.into_iter()
|
||||||
|
.map(|f| PublicKey::from_bech32(f).unwrap())
|
||||||
|
.collect();
|
||||||
|
let subscription = Filter::new()
|
||||||
|
.pubkeys(pubkeys)
|
||||||
|
.kinds(vec![
|
||||||
|
Kind::TextNote,
|
||||||
|
Kind::Repost,
|
||||||
|
Kind::ZapReceipt,
|
||||||
|
Kind::EncryptedDirectMessage,
|
||||||
|
])
|
||||||
|
.since(Timestamp::now());
|
||||||
|
let activity_id = SubscriptionId::new("activity");
|
||||||
|
|
||||||
// Get user's relay list
|
// Create a subscription for activity
|
||||||
let filter = Filter::new()
|
client
|
||||||
.author(public_key)
|
.subscribe_with_id(activity_id.clone(), vec![subscription], None)
|
||||||
.kind(Kind::RelayList)
|
.await;
|
||||||
.limit(1);
|
|
||||||
let query = client
|
|
||||||
.get_events_of(vec![filter], Some(Duration::from_secs(10)))
|
|
||||||
.await;
|
|
||||||
|
|
||||||
// Connect user's relay list
|
// Handle notifications
|
||||||
if let Ok(events) = query {
|
let _ = client
|
||||||
if let Some(event) = events.first() {
|
.handle_notifications(|notification| async {
|
||||||
let list = nip65::extract_relay_list(&event);
|
if let RelayPoolNotification::Event {
|
||||||
for item in list.into_iter() {
|
subscription_id,
|
||||||
println!("connecting to relay: {}", item.0.to_string());
|
event,
|
||||||
client
|
..
|
||||||
.connect_relay(item.0.to_string())
|
} = notification
|
||||||
.await
|
{
|
||||||
.unwrap_or_default();
|
if subscription_id == activity_id {
|
||||||
}
|
let _ = app.emit_to("main", "activity", event.as_json());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Ok(false)
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
});
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn get_activities(
|
||||||
|
account: &str,
|
||||||
|
kind: &str,
|
||||||
|
state: State<'_, Nostr>,
|
||||||
|
) -> Result<Vec<Event>, String> {
|
||||||
|
let client = &state.client;
|
||||||
|
|
||||||
|
if let Ok(pubkey) = PublicKey::from_str(account) {
|
||||||
|
if let Ok(kind) = Kind::from_str(kind) {
|
||||||
|
let filter = Filter::new()
|
||||||
|
.pubkey(pubkey)
|
||||||
|
.kind(kind)
|
||||||
|
.limit(100)
|
||||||
|
.until(Timestamp::now());
|
||||||
|
|
||||||
|
match client.get_events_of(vec![filter], None).await {
|
||||||
|
Ok(events) => Ok(events),
|
||||||
|
Err(err) => Err(err.to_string()),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Err("Kind is not valid, please check again.".into())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Err("Public Key is not valid, please check again.".into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn get_current_user_profile(state: State<'_, Nostr>) -> Result<Metadata, String> {
|
pub async fn get_current_user_profile(state: State<'_, Nostr>) -> Result<Metadata, String> {
|
||||||
let client = &state.client;
|
let client = &state.client;
|
||||||
|
Loading…
Reference in New Issue
Block a user