mirror of
https://github.com/luminous-devs/lume.git
synced 2024-10-01 01:10:48 +00:00
wip
This commit is contained in:
parent
e9ce932646
commit
64286aa354
@ -14,10 +14,13 @@
|
|||||||
"@lume/ui": "workspace:^",
|
"@lume/ui": "workspace:^",
|
||||||
"@lume/utils": "workspace:^",
|
"@lume/utils": "workspace:^",
|
||||||
"@radix-ui/react-checkbox": "^1.0.4",
|
"@radix-ui/react-checkbox": "^1.0.4",
|
||||||
|
"@radix-ui/react-collapsible": "^1.0.3",
|
||||||
"@tanstack/react-query": "^5.20.5",
|
"@tanstack/react-query": "^5.20.5",
|
||||||
|
"@tanstack/react-query-persist-client": "^5.22.2",
|
||||||
"@tanstack/react-router": "^1.16.2",
|
"@tanstack/react-router": "^1.16.2",
|
||||||
"i18next": "^23.8.2",
|
"i18next": "^23.8.2",
|
||||||
"i18next-resources-to-backend": "^1.2.0",
|
"i18next-resources-to-backend": "^1.2.0",
|
||||||
|
"idb-keyval": "^6.2.1",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-i18next": "^14.0.5",
|
"react-i18next": "^14.0.5",
|
||||||
|
@ -10,8 +10,35 @@ import i18n from "./locale";
|
|||||||
import { Toaster } from "sonner";
|
import { Toaster } from "sonner";
|
||||||
import { locale, platform } from "@tauri-apps/plugin-os";
|
import { locale, platform } from "@tauri-apps/plugin-os";
|
||||||
import { routeTree } from "./router.gen"; // auto generated file
|
import { routeTree } from "./router.gen"; // auto generated file
|
||||||
|
import { get, set, del } from "idb-keyval";
|
||||||
|
import {
|
||||||
|
PersistedClient,
|
||||||
|
Persister,
|
||||||
|
} from "@tanstack/react-query-persist-client";
|
||||||
|
import { PersistQueryClientProvider } from "@tanstack/react-query-persist-client";
|
||||||
|
|
||||||
const queryClient = new QueryClient();
|
function createIDBPersister(idbValidKey: IDBValidKey = "reactQuery") {
|
||||||
|
return {
|
||||||
|
persistClient: async (client: PersistedClient) => {
|
||||||
|
await set(idbValidKey, client);
|
||||||
|
},
|
||||||
|
restoreClient: async () => {
|
||||||
|
return await get<PersistedClient>(idbValidKey);
|
||||||
|
},
|
||||||
|
removeClient: async () => {
|
||||||
|
await del(idbValidKey);
|
||||||
|
},
|
||||||
|
} as Persister;
|
||||||
|
}
|
||||||
|
|
||||||
|
const persister = createIDBPersister();
|
||||||
|
const queryClient = new QueryClient({
|
||||||
|
defaultOptions: {
|
||||||
|
queries: {
|
||||||
|
gcTime: 1000 * 60 * 60 * 24, // 24 hours
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
const platformName = await platform();
|
const platformName = await platform();
|
||||||
const osLocale = (await locale()).slice(0, 2);
|
const osLocale = (await locale()).slice(0, 2);
|
||||||
|
|
||||||
@ -53,12 +80,15 @@ if (!rootElement.innerHTML) {
|
|||||||
const root = ReactDOM.createRoot(rootElement);
|
const root = ReactDOM.createRoot(rootElement);
|
||||||
root.render(
|
root.render(
|
||||||
<I18nextProvider i18n={i18n} defaultNS={"translation"}>
|
<I18nextProvider i18n={i18n} defaultNS={"translation"}>
|
||||||
<QueryClientProvider client={queryClient}>
|
<PersistQueryClientProvider
|
||||||
|
client={queryClient}
|
||||||
|
persistOptions={{ persister }}
|
||||||
|
>
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<Toaster position="top-center" richColors />
|
<Toaster position="top-center" richColors />
|
||||||
<App />
|
<App />
|
||||||
</StrictMode>
|
</StrictMode>
|
||||||
</QueryClientProvider>
|
</PersistQueryClientProvider>
|
||||||
</I18nextProvider>,
|
</I18nextProvider>,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
38
apps/desktop2/src/components/accounts.tsx
Normal file
38
apps/desktop2/src/components/accounts.tsx
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import { useArk } from "@lume/ark";
|
||||||
|
import { User } from "@lume/ui";
|
||||||
|
|
||||||
|
export function Accounts() {
|
||||||
|
const ark = useArk();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div data-tauri-drag-region className="flex items-center gap-4">
|
||||||
|
{ark.accounts.map((account) =>
|
||||||
|
account.npub === ark.account.npub ? (
|
||||||
|
<Active pubkey={account.npub} />
|
||||||
|
) : (
|
||||||
|
<Inactive pubkey={ark.account.npub} />
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Inactive({ pubkey }: { pubkey: string }) {
|
||||||
|
return (
|
||||||
|
<User.Provider pubkey={pubkey}>
|
||||||
|
<User.Root className="rounded-full ring-offset-2 ring-offset-neutral-200 hover:ring-1 hover:ring-blue-500 dark:ring-offset-neutral-950">
|
||||||
|
<User.Avatar className="aspect-square h-auto w-7 rounded-full object-cover" />
|
||||||
|
</User.Root>
|
||||||
|
</User.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Active({ pubkey }: { pubkey: string }) {
|
||||||
|
return (
|
||||||
|
<User.Provider pubkey={pubkey}>
|
||||||
|
<User.Root className="rounded-full ring-1 ring-teal-500 ring-offset-2 ring-offset-neutral-200 dark:ring-offset-neutral-950">
|
||||||
|
<User.Avatar className="aspect-square h-auto w-7 rounded-full object-cover" />
|
||||||
|
</User.Root>
|
||||||
|
</User.Provider>
|
||||||
|
);
|
||||||
|
}
|
@ -9,7 +9,7 @@ import {
|
|||||||
import { Link } from "@tanstack/react-router";
|
import { Link } from "@tanstack/react-router";
|
||||||
import { Outlet, createFileRoute } from "@tanstack/react-router";
|
import { Outlet, createFileRoute } from "@tanstack/react-router";
|
||||||
import { cn } from "@lume/utils";
|
import { cn } from "@lume/utils";
|
||||||
import { ActiveAccount } from "@lume/ui";
|
import { Accounts } from "@/components/accounts";
|
||||||
|
|
||||||
export const Route = createFileRoute("/app")({
|
export const Route = createFileRoute("/app")({
|
||||||
component: App,
|
component: App,
|
||||||
@ -27,65 +27,8 @@ function App() {
|
|||||||
context.platform === "macos" ? "pl-24" : "pl-4",
|
context.platform === "macos" ? "pl-24" : "pl-4",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div
|
<Navigation />
|
||||||
data-tauri-drag-region
|
<Accounts />
|
||||||
className="flex h-full flex-1 items-center gap-2"
|
|
||||||
>
|
|
||||||
<Link to="/app/home">
|
|
||||||
{({ isActive }) => (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"inline-flex h-9 w-max items-center justify-center gap-2 rounded-lg px-3 hover:bg-black/10 dark:hover:bg-white/10",
|
|
||||||
isActive ? "bg-white shadow dark:bg-neutral-950" : "",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{isActive ? (
|
|
||||||
<HomeFilledIcon className="size-5" />
|
|
||||||
) : (
|
|
||||||
<HomeIcon className="size-5" />
|
|
||||||
)}
|
|
||||||
<span className="text-sm font-medium">Home</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Link>
|
|
||||||
<Link to="/app/space">
|
|
||||||
{({ isActive }) => (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"inline-flex h-9 w-max items-center justify-center gap-2 rounded-lg px-3 hover:bg-black/10 dark:hover:bg-white/10",
|
|
||||||
isActive ? "bg-white shadow dark:bg-neutral-950" : "",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{isActive ? (
|
|
||||||
<SpaceFilledIcon className="size-5" />
|
|
||||||
) : (
|
|
||||||
<SpaceIcon className="size-5" />
|
|
||||||
)}
|
|
||||||
<span className="text-sm font-medium">Space</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Link>
|
|
||||||
<Link to="/app/activity">
|
|
||||||
{({ isActive }) => (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"inline-flex h-9 w-max items-center justify-center gap-2 rounded-lg px-3 hover:bg-black/10 dark:hover:bg-white/10",
|
|
||||||
isActive ? "bg-white shadow dark:bg-neutral-950" : "",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{isActive ? (
|
|
||||||
<BellFilledIcon className="size-5" />
|
|
||||||
) : (
|
|
||||||
<BellIcon className="size-5" />
|
|
||||||
)}
|
|
||||||
<span className="text-sm font-medium">Activity</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
<div data-tauri-drag-region className="flex items-center gap-2">
|
|
||||||
<ActiveAccount />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex h-full min-h-0 w-full">
|
<div className="flex h-full min-h-0 w-full">
|
||||||
<div className="h-full w-full flex-1 px-2 pb-2">
|
<div className="h-full w-full flex-1 px-2 pb-2">
|
||||||
@ -95,3 +38,64 @@ function App() {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Navigation() {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-tauri-drag-region
|
||||||
|
className="flex h-full flex-1 items-center gap-2"
|
||||||
|
>
|
||||||
|
<Link to="/app/home">
|
||||||
|
{({ isActive }) => (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"inline-flex h-9 w-max items-center justify-center gap-2 rounded-lg px-3 hover:bg-black/10 dark:hover:bg-white/10",
|
||||||
|
isActive ? "bg-white shadow dark:bg-neutral-950" : "",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{isActive ? (
|
||||||
|
<HomeFilledIcon className="size-5" />
|
||||||
|
) : (
|
||||||
|
<HomeIcon className="size-5" />
|
||||||
|
)}
|
||||||
|
<span className="text-sm font-medium">Home</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Link>
|
||||||
|
<Link to="/app/space">
|
||||||
|
{({ isActive }) => (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"inline-flex h-9 w-max items-center justify-center gap-2 rounded-lg px-3 hover:bg-black/10 dark:hover:bg-white/10",
|
||||||
|
isActive ? "bg-white shadow dark:bg-neutral-950" : "",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{isActive ? (
|
||||||
|
<SpaceFilledIcon className="size-5" />
|
||||||
|
) : (
|
||||||
|
<SpaceIcon className="size-5" />
|
||||||
|
)}
|
||||||
|
<span className="text-sm font-medium">Space</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Link>
|
||||||
|
<Link to="/app/activity">
|
||||||
|
{({ isActive }) => (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"inline-flex h-9 w-max items-center justify-center gap-2 rounded-lg px-3 hover:bg-black/10 dark:hover:bg-white/10",
|
||||||
|
isActive ? "bg-white shadow dark:bg-neutral-950" : "",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{isActive ? (
|
||||||
|
<BellFilledIcon className="size-5" />
|
||||||
|
) : (
|
||||||
|
<BellIcon className="size-5" />
|
||||||
|
)}
|
||||||
|
<span className="text-sm font-medium">Activity</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@ -1,94 +0,0 @@
|
|||||||
import { useArk } from "@lume/ark";
|
|
||||||
import { ArrowRightCircleIcon, LoaderIcon, SearchIcon } from "@lume/icons";
|
|
||||||
import { Event, Kind } from "@lume/types";
|
|
||||||
import { EmptyFeed, RepostNote, TextNote } from "@lume/ui";
|
|
||||||
import { FETCH_LIMIT } from "@lume/utils";
|
|
||||||
import { useInfiniteQuery } from "@tanstack/react-query";
|
|
||||||
import { createLazyFileRoute } from "@tanstack/react-router";
|
|
||||||
import { Virtualizer } from "virtua";
|
|
||||||
|
|
||||||
export const Route = createLazyFileRoute("/app/home")({
|
|
||||||
component: Home,
|
|
||||||
});
|
|
||||||
|
|
||||||
function Home() {
|
|
||||||
const ark = useArk();
|
|
||||||
const { data, hasNextPage, isLoading, isFetchingNextPage, fetchNextPage } =
|
|
||||||
useInfiniteQuery({
|
|
||||||
queryKey: ["local_timeline"],
|
|
||||||
initialPageParam: 0,
|
|
||||||
queryFn: async ({ pageParam }: { pageParam: number }) => {
|
|
||||||
const events = await ark.get_events(
|
|
||||||
"local",
|
|
||||||
FETCH_LIMIT,
|
|
||||||
pageParam,
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
return events;
|
|
||||||
},
|
|
||||||
getNextPageParam: (lastPage) => {
|
|
||||||
const lastEvent = lastPage.at(-1);
|
|
||||||
if (!lastEvent) return;
|
|
||||||
return lastEvent.created_at - 1;
|
|
||||||
},
|
|
||||||
select: (data) => data?.pages.flatMap((page) => page),
|
|
||||||
refetchOnWindowFocus: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const renderItem = (event: Event) => {
|
|
||||||
switch (event.kind) {
|
|
||||||
case Kind.Repost:
|
|
||||||
return <RepostNote key={event.id} event={event} />;
|
|
||||||
default:
|
|
||||||
return <TextNote key={event.id} event={event} />;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="h-full w-full overflow-hidden rounded-xl bg-white shadow-[rgba(50,_50,_105,_0.15)_0px_2px_5px_0px,_rgba(0,_0,_0,_0.05)_0px_1px_1px_0px] dark:bg-black dark:shadow-none dark:ring-1 dark:ring-white/5">
|
|
||||||
<div className="h-full w-full overflow-y-auto pt-10">
|
|
||||||
<div className="mx-auto w-full max-w-xl">
|
|
||||||
{isLoading ? (
|
|
||||||
<div className="flex h-20 w-full items-center justify-center">
|
|
||||||
<LoaderIcon className="size-5 animate-spin" />
|
|
||||||
</div>
|
|
||||||
) : !data.length ? (
|
|
||||||
<div className="flex flex-col gap-3">
|
|
||||||
<EmptyFeed />
|
|
||||||
<a
|
|
||||||
href="/suggest"
|
|
||||||
className="inline-flex h-9 w-full items-center justify-center gap-2 rounded-lg bg-blue-500 text-sm font-medium text-white hover:bg-blue-600"
|
|
||||||
>
|
|
||||||
<SearchIcon className="size-5" />
|
|
||||||
Find accounts to follow
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<Virtualizer overscan={3}>
|
|
||||||
{data.map((item) => renderItem(item))}
|
|
||||||
</Virtualizer>
|
|
||||||
)}
|
|
||||||
<div className="flex h-20 items-center justify-center">
|
|
||||||
{hasNextPage ? (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => fetchNextPage()}
|
|
||||||
disabled={!hasNextPage || isFetchingNextPage}
|
|
||||||
className="inline-flex h-11 w-max items-center justify-center gap-2 rounded-full bg-neutral-100 px-3 font-medium hover:bg-neutral-200 focus:outline-none dark:bg-neutral-900 dark:hover:bg-neutral-800"
|
|
||||||
>
|
|
||||||
{isFetchingNextPage ? (
|
|
||||||
<LoaderIcon className="size-5 animate-spin" />
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<ArrowRightCircleIcon className="size-5" />
|
|
||||||
Load more
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
51
apps/desktop2/src/routes/app/home.tsx
Normal file
51
apps/desktop2/src/routes/app/home.tsx
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import { cn } from "@lume/utils";
|
||||||
|
import { Link } from "@tanstack/react-router";
|
||||||
|
import { Outlet, createFileRoute } from "@tanstack/react-router";
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/app/home")({
|
||||||
|
component: Home,
|
||||||
|
});
|
||||||
|
|
||||||
|
function Home() {
|
||||||
|
return (
|
||||||
|
<div className="h-full w-full overflow-hidden overflow-y-auto rounded-xl bg-white shadow-[rgba(50,_50,_105,_0.15)_0px_2px_5px_0px,_rgba(0,_0,_0,_0.05)_0px_1px_1px_0px] dark:bg-black dark:shadow-none dark:ring-1 dark:ring-white/5">
|
||||||
|
<div className="mx-auto flex w-full max-w-xl flex-col">
|
||||||
|
<div className="mx-auto flex h-28 w-1/2 items-center">
|
||||||
|
<div className="flex h-11 w-full flex-1 items-center rounded-full bg-neutral-100 dark:bg-neutral-900">
|
||||||
|
<Link to="/app/home/local" className="h-11 flex-1 p-1">
|
||||||
|
{({ isActive }) => (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"inline-flex h-full w-full items-center justify-center rounded-full text-sm font-medium",
|
||||||
|
isActive
|
||||||
|
? "bg-white shadow shadow-neutral-500/20 dark:bg-black dark:shadow-none dark:ring-1 dark:ring-neutral-800"
|
||||||
|
: "",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
Local
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Link>
|
||||||
|
<Link to="/app/home/global" className="h-11 flex-1 p-1">
|
||||||
|
{({ isActive }) => (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"inline-flex h-full w-full items-center justify-center rounded-full text-sm font-medium",
|
||||||
|
isActive
|
||||||
|
? "bg-white shadow shadow-neutral-500/20 dark:bg-black dark:shadow-none dark:ring-1 dark:ring-neutral-800"
|
||||||
|
: "",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
Global
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<Outlet />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
114
apps/desktop2/src/routes/app/home/-components/repost.tsx
Normal file
114
apps/desktop2/src/routes/app/home/-components/repost.tsx
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
import { RepostIcon } from "@lume/icons";
|
||||||
|
import { Event } from "@lume/types";
|
||||||
|
import { cn } from "@lume/utils";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useArk } from "@lume/ark";
|
||||||
|
import { Note, User } from "@lume/ui";
|
||||||
|
|
||||||
|
export function RepostNote({
|
||||||
|
event,
|
||||||
|
className,
|
||||||
|
}: {
|
||||||
|
event: Event;
|
||||||
|
className?: string;
|
||||||
|
}) {
|
||||||
|
const ark = useArk();
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const {
|
||||||
|
isLoading,
|
||||||
|
isError,
|
||||||
|
data: repostEvent,
|
||||||
|
} = useQuery({
|
||||||
|
queryKey: ["repost", event.id],
|
||||||
|
queryFn: async () => {
|
||||||
|
try {
|
||||||
|
if (event.content.length > 50) {
|
||||||
|
const embed: Event = JSON.parse(event.content);
|
||||||
|
return embed;
|
||||||
|
}
|
||||||
|
const id = event.tags.find((el) => el[0] === "e")[1];
|
||||||
|
return await ark.get_event(id);
|
||||||
|
} catch {
|
||||||
|
throw new Error("Failed to get repost event");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
refetchOnMount: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return <div className="w-full px-3 pb-3">Loading...</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isError || !repostEvent) {
|
||||||
|
return (
|
||||||
|
<Note.Root className={className}>
|
||||||
|
<User.Provider pubkey={event.pubkey}>
|
||||||
|
<User.Root className="flex h-14 gap-2 px-3">
|
||||||
|
<div className="inline-flex w-10 shrink-0 items-center justify-center">
|
||||||
|
<RepostIcon className="h-5 w-5 text-blue-500" />
|
||||||
|
</div>
|
||||||
|
<div className="inline-flex items-center gap-2">
|
||||||
|
<User.Avatar className="size-6 shrink-0 rounded object-cover" />
|
||||||
|
<div className="inline-flex items-baseline gap-1">
|
||||||
|
<User.Name className="font-medium text-neutral-900 dark:text-neutral-100" />
|
||||||
|
<span className="text-blue-500">{t("note.reposted")}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</User.Root>
|
||||||
|
</User.Provider>
|
||||||
|
<div className="mb-3 select-text px-3">
|
||||||
|
<div className="flex flex-col items-start justify-start rounded-lg bg-red-100 px-3 py-3 dark:bg-red-900">
|
||||||
|
<p className="text-red-500">Failed to get event</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Note.Root>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Note.Root
|
||||||
|
className={cn(
|
||||||
|
"mb-3 flex flex-col gap-2 border-b border-neutral-100 pb-3 dark:border-neutral-900",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<User.Provider pubkey={event.pubkey}>
|
||||||
|
<User.Root className="flex gap-3">
|
||||||
|
<div className="inline-flex w-10 shrink-0 items-center justify-center">
|
||||||
|
<RepostIcon className="h-5 w-5 text-blue-500" />
|
||||||
|
</div>
|
||||||
|
<div className="inline-flex items-center gap-2">
|
||||||
|
<User.Avatar className="size-6 shrink-0 rounded-full object-cover" />
|
||||||
|
<div className="inline-flex items-baseline gap-1">
|
||||||
|
<User.Name className="font-medium text-neutral-900 dark:text-neutral-100" />
|
||||||
|
<span className="text-blue-500">{t("note.reposted")}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</User.Root>
|
||||||
|
</User.Provider>
|
||||||
|
<Note.Provider event={repostEvent}>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Note.User />
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<div className="size-10 shrink-0" />
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<Note.Content />
|
||||||
|
<div className="mt-5 flex items-center justify-between">
|
||||||
|
<Note.Reaction />
|
||||||
|
<div className="inline-flex items-center gap-4">
|
||||||
|
<Note.Reply />
|
||||||
|
<Note.Repost />
|
||||||
|
<Note.Zap />
|
||||||
|
<Note.Menu />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Note.Provider>
|
||||||
|
</Note.Root>
|
||||||
|
);
|
||||||
|
}
|
40
apps/desktop2/src/routes/app/home/-components/text.tsx
Normal file
40
apps/desktop2/src/routes/app/home/-components/text.tsx
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import { Event } from "@lume/types";
|
||||||
|
import { Note } from "@lume/ui";
|
||||||
|
import { cn } from "@lume/utils";
|
||||||
|
|
||||||
|
export function TextNote({
|
||||||
|
event,
|
||||||
|
className,
|
||||||
|
}: {
|
||||||
|
event: Event;
|
||||||
|
className?: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Note.Provider event={event}>
|
||||||
|
<Note.Root
|
||||||
|
className={cn(
|
||||||
|
"mb-3 flex flex-col gap-2 border-b border-neutral-100 pb-3 dark:border-neutral-900",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Note.User />
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<div className="size-11 shrink-0" />
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<Note.Thread className="mb-2" />
|
||||||
|
<Note.Content />
|
||||||
|
<div className="mt-5 flex items-center justify-between">
|
||||||
|
<Note.Reaction />
|
||||||
|
<div className="inline-flex items-center gap-4">
|
||||||
|
<Note.Reply />
|
||||||
|
<Note.Repost />
|
||||||
|
<Note.Zap />
|
||||||
|
<Note.Menu />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Note.Root>
|
||||||
|
</Note.Provider>
|
||||||
|
);
|
||||||
|
}
|
92
apps/desktop2/src/routes/app/home/global.lazy.tsx
Normal file
92
apps/desktop2/src/routes/app/home/global.lazy.tsx
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
import { useArk } from "@lume/ark";
|
||||||
|
import { ArrowRightCircleIcon, LoaderIcon, SearchIcon } from "@lume/icons";
|
||||||
|
import { Event, Kind } from "@lume/types";
|
||||||
|
import { EmptyFeed } from "@lume/ui";
|
||||||
|
import { FETCH_LIMIT } from "@lume/utils";
|
||||||
|
import { useInfiniteQuery } from "@tanstack/react-query";
|
||||||
|
import { createLazyFileRoute } from "@tanstack/react-router";
|
||||||
|
import { Virtualizer } from "virtua";
|
||||||
|
import { TextNote } from "./-components/text";
|
||||||
|
import { RepostNote } from "./-components/repost";
|
||||||
|
|
||||||
|
export const Route = createLazyFileRoute("/app/home/global")({
|
||||||
|
component: GlobalTimeline,
|
||||||
|
});
|
||||||
|
|
||||||
|
function GlobalTimeline() {
|
||||||
|
const ark = useArk();
|
||||||
|
const { data, hasNextPage, isLoading, isFetchingNextPage, fetchNextPage } =
|
||||||
|
useInfiniteQuery({
|
||||||
|
queryKey: ["events", "global"],
|
||||||
|
initialPageParam: 0,
|
||||||
|
queryFn: async ({ pageParam }: { pageParam: number }) => {
|
||||||
|
const events = await ark.get_events(
|
||||||
|
"global",
|
||||||
|
FETCH_LIMIT,
|
||||||
|
pageParam,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
return events;
|
||||||
|
},
|
||||||
|
getNextPageParam: (lastPage) => {
|
||||||
|
const lastEvent = lastPage.at(-1);
|
||||||
|
if (!lastEvent) return;
|
||||||
|
return lastEvent.created_at - 1;
|
||||||
|
},
|
||||||
|
select: (data) => data?.pages.flatMap((page) => page),
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const renderItem = (event: Event) => {
|
||||||
|
switch (event.kind) {
|
||||||
|
case Kind.Repost:
|
||||||
|
return <RepostNote key={event.id} event={event} />;
|
||||||
|
default:
|
||||||
|
return <TextNote key={event.id} event={event} />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="flex h-20 w-full items-center justify-center">
|
||||||
|
<LoaderIcon className="size-5 animate-spin" />
|
||||||
|
</div>
|
||||||
|
) : !data.length ? (
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<EmptyFeed />
|
||||||
|
<a
|
||||||
|
href="/suggest"
|
||||||
|
className="inline-flex h-9 w-full items-center justify-center gap-2 rounded-lg bg-blue-500 text-sm font-medium text-white hover:bg-blue-600"
|
||||||
|
>
|
||||||
|
<SearchIcon className="size-5" />
|
||||||
|
Find accounts to follow
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Virtualizer overscan={3}>
|
||||||
|
{data.map((item) => renderItem(item))}
|
||||||
|
</Virtualizer>
|
||||||
|
)}
|
||||||
|
<div className="flex h-20 items-center justify-center">
|
||||||
|
{hasNextPage ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => fetchNextPage()}
|
||||||
|
disabled={!hasNextPage || isFetchingNextPage}
|
||||||
|
className="inline-flex h-12 w-36 items-center justify-center gap-2 rounded-full bg-neutral-100 px-3 font-medium hover:bg-neutral-200 focus:outline-none dark:bg-neutral-900 dark:hover:bg-neutral-800"
|
||||||
|
>
|
||||||
|
{isFetchingNextPage ? (
|
||||||
|
<LoaderIcon className="size-5 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<ArrowRightCircleIcon className="size-5" />
|
||||||
|
Load more
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
92
apps/desktop2/src/routes/app/home/local.lazy.tsx
Normal file
92
apps/desktop2/src/routes/app/home/local.lazy.tsx
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
import { useArk } from "@lume/ark";
|
||||||
|
import { ArrowRightCircleIcon, LoaderIcon, SearchIcon } from "@lume/icons";
|
||||||
|
import { Event, Kind } from "@lume/types";
|
||||||
|
import { EmptyFeed } from "@lume/ui";
|
||||||
|
import { FETCH_LIMIT } from "@lume/utils";
|
||||||
|
import { useInfiniteQuery } from "@tanstack/react-query";
|
||||||
|
import { createLazyFileRoute } from "@tanstack/react-router";
|
||||||
|
import { Virtualizer } from "virtua";
|
||||||
|
import { TextNote } from "./-components/text";
|
||||||
|
import { RepostNote } from "./-components/repost";
|
||||||
|
|
||||||
|
export const Route = createLazyFileRoute("/app/home/local")({
|
||||||
|
component: LocalTimeline,
|
||||||
|
});
|
||||||
|
|
||||||
|
function LocalTimeline() {
|
||||||
|
const ark = useArk();
|
||||||
|
const { data, hasNextPage, isLoading, isFetchingNextPage, fetchNextPage } =
|
||||||
|
useInfiniteQuery({
|
||||||
|
queryKey: ["events", "local"],
|
||||||
|
initialPageParam: 0,
|
||||||
|
queryFn: async ({ pageParam }: { pageParam: number }) => {
|
||||||
|
const events = await ark.get_events(
|
||||||
|
"local",
|
||||||
|
FETCH_LIMIT,
|
||||||
|
pageParam,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
return events;
|
||||||
|
},
|
||||||
|
getNextPageParam: (lastPage) => {
|
||||||
|
const lastEvent = lastPage.at(-1);
|
||||||
|
if (!lastEvent) return;
|
||||||
|
return lastEvent.created_at - 1;
|
||||||
|
},
|
||||||
|
select: (data) => data?.pages.flatMap((page) => page),
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const renderItem = (event: Event) => {
|
||||||
|
switch (event.kind) {
|
||||||
|
case Kind.Repost:
|
||||||
|
return <RepostNote key={event.id} event={event} />;
|
||||||
|
default:
|
||||||
|
return <TextNote key={event.id} event={event} />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="flex h-20 w-full items-center justify-center">
|
||||||
|
<LoaderIcon className="size-5 animate-spin" />
|
||||||
|
</div>
|
||||||
|
) : !data.length ? (
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<EmptyFeed />
|
||||||
|
<a
|
||||||
|
href="/suggest"
|
||||||
|
className="inline-flex h-9 w-full items-center justify-center gap-2 rounded-lg bg-blue-500 text-sm font-medium text-white hover:bg-blue-600"
|
||||||
|
>
|
||||||
|
<SearchIcon className="size-5" />
|
||||||
|
Find accounts to follow
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Virtualizer overscan={3}>
|
||||||
|
{data.map((item) => renderItem(item))}
|
||||||
|
</Virtualizer>
|
||||||
|
)}
|
||||||
|
<div className="flex h-20 items-center justify-center">
|
||||||
|
{hasNextPage ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => fetchNextPage()}
|
||||||
|
disabled={!hasNextPage || isFetchingNextPage}
|
||||||
|
className="inline-flex h-12 w-36 items-center justify-center gap-2 rounded-full bg-neutral-100 px-3 font-medium hover:bg-neutral-200 focus:outline-none dark:bg-neutral-900 dark:hover:bg-neutral-800"
|
||||||
|
>
|
||||||
|
{isFetchingNextPage ? (
|
||||||
|
<LoaderIcon className="size-5 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<ArrowRightCircleIcon className="size-5" />
|
||||||
|
Load more
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -1,6 +1,9 @@
|
|||||||
import { ReplyList, ThreadNote } from "@lume/ui";
|
import { useEvent } from "@lume/ark";
|
||||||
|
import { LoaderIcon } from "@lume/icons";
|
||||||
|
import { Note, User } from "@lume/ui";
|
||||||
import { createLazyFileRoute } from "@tanstack/react-router";
|
import { createLazyFileRoute } from "@tanstack/react-router";
|
||||||
import { WindowVirtualizer } from "virtua";
|
import { WindowVirtualizer } from "virtua";
|
||||||
|
import { ReplyList } from "./-components/replyList";
|
||||||
|
|
||||||
export const Route = createLazyFileRoute("/events/$eventId")({
|
export const Route = createLazyFileRoute("/events/$eventId")({
|
||||||
component: Event,
|
component: Event,
|
||||||
@ -8,16 +11,63 @@ export const Route = createLazyFileRoute("/events/$eventId")({
|
|||||||
|
|
||||||
function Event() {
|
function Event() {
|
||||||
const { eventId } = Route.useParams();
|
const { eventId } = Route.useParams();
|
||||||
|
const { isLoading, isError, data } = useEvent(eventId);
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div className="flex h-full w-full items-center justify-center">
|
||||||
|
<LoaderIcon className="size-5 animate-spin" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isError) {
|
||||||
|
<div className="flex h-full w-full items-center justify-center">
|
||||||
|
<p>Not found.</p>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative h-screen w-screen overflow-y-auto overflow-x-hidden">
|
<WindowVirtualizer>
|
||||||
<div data-tauri-drag-region className="h-11 w-full" />
|
<div className="flex h-screen w-screen flex-col bg-gradient-to-tr from-neutral-200 to-neutral-100 dark:from-neutral-950 dark:to-neutral-900">
|
||||||
<WindowVirtualizer>
|
<div data-tauri-drag-region className="h-11 w-full shrink-0" />
|
||||||
<div className="px-6">
|
<div className="flex h-full min-h-0 w-full">
|
||||||
<ThreadNote eventId={eventId} />
|
<div className="h-full w-full flex-1 px-2 pb-2">
|
||||||
<ReplyList eventId={eventId} />
|
<div className="h-full w-full overflow-hidden overflow-y-auto rounded-xl bg-white px-3 shadow-[rgba(50,_50,_105,_0.15)_0px_2px_5px_0px,_rgba(0,_0,_0,_0.05)_0px_1px_1px_0px] dark:bg-black dark:shadow-none dark:ring-1 dark:ring-white/5">
|
||||||
|
<Note.Provider event={data}>
|
||||||
|
<Note.Root className="flex flex-col">
|
||||||
|
<div className="mb-2 flex items-center justify-between pt-4">
|
||||||
|
<User.Provider pubkey={data.pubkey}>
|
||||||
|
<User.Root className="flex flex-1 items-center gap-3">
|
||||||
|
<User.Avatar className="size-11 shrink-0 rounded-full object-cover ring-1 ring-neutral-200/50 dark:ring-neutral-800/50" />
|
||||||
|
<div className="flex flex-1 flex-col">
|
||||||
|
<User.Name className="font-semibold text-neutral-900 dark:text-neutral-100" />
|
||||||
|
<div className="inline-flex items-center gap-2 text-sm text-neutral-600 dark:text-neutral-400">
|
||||||
|
<User.Time time={data.created_at} />
|
||||||
|
<span>·</span>
|
||||||
|
<User.NIP05 />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</User.Root>
|
||||||
|
</User.Provider>
|
||||||
|
<Note.Menu />
|
||||||
|
</div>
|
||||||
|
<Note.Thread className="mb-2" />
|
||||||
|
<Note.Content className="min-w-0" />
|
||||||
|
<div className="flex h-14 items-center justify-between">
|
||||||
|
<Note.Reaction />
|
||||||
|
<div className="inline-flex items-center gap-4">
|
||||||
|
<Note.Repost />
|
||||||
|
<Note.Zap />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Note.Root>
|
||||||
|
</Note.Provider>
|
||||||
|
{data ? <ReplyList eventId={eventId} /> : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</WindowVirtualizer>
|
</div>
|
||||||
</div>
|
</WindowVirtualizer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
65
apps/desktop2/src/routes/events/-components/reply.tsx
Normal file
65
apps/desktop2/src/routes/events/-components/reply.tsx
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import { NavArrowDownIcon } from "@lume/icons";
|
||||||
|
import { EventWithReplies } from "@lume/types";
|
||||||
|
import { cn } from "@lume/utils";
|
||||||
|
import * as Collapsible from "@radix-ui/react-collapsible";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { Note } from "@lume/ui";
|
||||||
|
import { SubReply } from "./subReply";
|
||||||
|
|
||||||
|
export function Reply({ event }: { event: EventWithReplies }) {
|
||||||
|
const [t] = useTranslation();
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Collapsible.Root open={open} onOpenChange={setOpen}>
|
||||||
|
<Note.Provider event={event}>
|
||||||
|
<Note.Root className="pt-2">
|
||||||
|
<div className="flex h-14 items-center justify-between">
|
||||||
|
<Note.User className="flex-1 pr-2" />
|
||||||
|
<Note.Menu />
|
||||||
|
</div>
|
||||||
|
<Note.Content />
|
||||||
|
<div className="flex h-14 items-center justify-between">
|
||||||
|
{event.replies?.length > 0 ? (
|
||||||
|
<Collapsible.Trigger asChild>
|
||||||
|
<div className="inline-flex h-14 items-center gap-1 text-sm font-semibold text-neutral-600 dark:text-neutral-400">
|
||||||
|
<NavArrowDownIcon
|
||||||
|
className={cn("size-5", open ? "rotate-180 transform" : "")}
|
||||||
|
/>
|
||||||
|
{`${event.replies?.length} ${
|
||||||
|
event.replies?.length === 1
|
||||||
|
? t("note.reply.single")
|
||||||
|
: t("note.reply.plural")
|
||||||
|
}`}
|
||||||
|
</div>
|
||||||
|
</Collapsible.Trigger>
|
||||||
|
) : (
|
||||||
|
<div />
|
||||||
|
)}
|
||||||
|
<div className="inline-flex items-center gap-4">
|
||||||
|
<Note.Reply />
|
||||||
|
<Note.Repost />
|
||||||
|
<Note.Zap />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
open
|
||||||
|
? "border-t border-neutral-100 pb-3 dark:border-neutral-900"
|
||||||
|
: "",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{event.replies?.length > 0 ? (
|
||||||
|
<Collapsible.Content className="divide-y divide-neutral-100 pl-6 dark:divide-neutral-900">
|
||||||
|
{event.replies?.map((childEvent) => (
|
||||||
|
<SubReply key={childEvent.id} event={childEvent} />
|
||||||
|
))}
|
||||||
|
</Collapsible.Content>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</Note.Root>
|
||||||
|
</Note.Provider>
|
||||||
|
</Collapsible.Root>
|
||||||
|
);
|
||||||
|
}
|
53
apps/desktop2/src/routes/events/-components/replyList.tsx
Normal file
53
apps/desktop2/src/routes/events/-components/replyList.tsx
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import { useArk } from "@lume/ark";
|
||||||
|
import { LoaderIcon } from "@lume/icons";
|
||||||
|
import { cn } from "@lume/utils";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { EventWithReplies } from "@lume/types";
|
||||||
|
|
||||||
|
export function ReplyList({
|
||||||
|
eventId,
|
||||||
|
className,
|
||||||
|
}: {
|
||||||
|
eventId: string;
|
||||||
|
className?: string;
|
||||||
|
}) {
|
||||||
|
const ark = useArk();
|
||||||
|
|
||||||
|
const [t] = useTranslation();
|
||||||
|
const [data, setData] = useState<null | EventWithReplies[]>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function getReplies() {
|
||||||
|
const events = await ark.get_event_thread(eventId);
|
||||||
|
setData(events);
|
||||||
|
}
|
||||||
|
getReplies();
|
||||||
|
}, [eventId]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex flex-col divide-y divide-neutral-100 dark:divide-neutral-900",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{!data ? (
|
||||||
|
<div className="mt-4 flex h-16 items-center justify-center p-3">
|
||||||
|
<LoaderIcon className="h-5 w-5 animate-spin" />
|
||||||
|
</div>
|
||||||
|
) : data.length === 0 ? (
|
||||||
|
<div className="mt-4 flex w-full items-center justify-center">
|
||||||
|
<div className="flex flex-col items-center justify-center gap-2 py-6">
|
||||||
|
<h3 className="text-3xl">👋</h3>
|
||||||
|
<p className="leading-none text-neutral-600 dark:text-neutral-400">
|
||||||
|
{t("note.reply.empty")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
data.map((event) => <Reply key={event.id} event={event} />)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
20
apps/desktop2/src/routes/events/-components/subReply.tsx
Normal file
20
apps/desktop2/src/routes/events/-components/subReply.tsx
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { Event } from "@lume/types";
|
||||||
|
import { Note } from "@lume/ui";
|
||||||
|
|
||||||
|
export function SubReply({ event }: { event: Event; rootEventId?: string }) {
|
||||||
|
return (
|
||||||
|
<Note.Provider event={event}>
|
||||||
|
<Note.Root className="py-2">
|
||||||
|
<div className="flex h-14 items-center justify-between">
|
||||||
|
<Note.User className="flex-1 pr-2" />
|
||||||
|
<Note.Menu />
|
||||||
|
</div>
|
||||||
|
<Note.Content />
|
||||||
|
<div className="mt-2 flex items-center justify-end gap-4">
|
||||||
|
<Note.Repost />
|
||||||
|
<Note.Zap />
|
||||||
|
</div>
|
||||||
|
</Note.Root>
|
||||||
|
</Note.Provider>
|
||||||
|
);
|
||||||
|
}
|
@ -23,7 +23,7 @@ export const Route = createFileRoute("/")({
|
|||||||
const loadAccount = await ark.load_selected_account(accounts[0].npub);
|
const loadAccount = await ark.load_selected_account(accounts[0].npub);
|
||||||
if (loadAccount) {
|
if (loadAccount) {
|
||||||
throw redirect({
|
throw redirect({
|
||||||
to: "/app/home",
|
to: "/app/home/local",
|
||||||
search: {
|
search: {
|
||||||
redirect: location.href,
|
redirect: location.href,
|
||||||
},
|
},
|
||||||
|
@ -1,8 +1,12 @@
|
|||||||
{
|
{
|
||||||
"extends": "@lume/tsconfig/base.json",
|
"extends": "@lume/tsconfig/base.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"outDir": "dist"
|
"outDir": "dist",
|
||||||
},
|
"baseUrl": "./",
|
||||||
"include": ["src"],
|
"paths": {
|
||||||
"exclude": ["node_modules", "dist"]
|
"@/*": ["./src/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["src"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
}
|
}
|
||||||
|
@ -98,8 +98,7 @@ export class Ark {
|
|||||||
.split("'")[0]
|
.split("'")[0]
|
||||||
.split(".")[0];
|
.split(".")[0];
|
||||||
const cmd: string = await invoke("get_event", { id: eventId });
|
const cmd: string = await invoke("get_event", { id: eventId });
|
||||||
const event = JSON.parse(cmd) as Event;
|
const event: Event = JSON.parse(cmd);
|
||||||
|
|
||||||
return event;
|
return event;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return null;
|
return null;
|
||||||
|
@ -6,7 +6,7 @@ export function useProfile(pubkey: string) {
|
|||||||
const {
|
const {
|
||||||
isLoading,
|
isLoading,
|
||||||
isError,
|
isError,
|
||||||
data: user,
|
data: profile,
|
||||||
} = useQuery({
|
} = useQuery({
|
||||||
queryKey: ["user", pubkey],
|
queryKey: ["user", pubkey],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
@ -24,5 +24,5 @@ export function useProfile(pubkey: string) {
|
|||||||
retry: 2,
|
retry: 2,
|
||||||
});
|
});
|
||||||
|
|
||||||
return { isLoading, isError, user };
|
return { isLoading, isError, profile };
|
||||||
}
|
}
|
||||||
|
@ -20,7 +20,7 @@ export function ActiveAccount() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { user } = useProfile(ark.account.npub);
|
const { profile } = useProfile(ark.account.npub);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenu.Root>
|
<DropdownMenu.Root>
|
||||||
@ -32,7 +32,7 @@ export function ActiveAccount() {
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Avatar.Image
|
<Avatar.Image
|
||||||
src={user?.picture}
|
src={profile?.picture}
|
||||||
alt={ark.account.npub}
|
alt={ark.account.npub}
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
decoding="async"
|
decoding="async"
|
||||||
|
@ -3,17 +3,11 @@ export * from "./user";
|
|||||||
export * from "./note";
|
export * from "./note";
|
||||||
export * from "./column";
|
export * from "./column";
|
||||||
|
|
||||||
// Note Primities
|
|
||||||
export * from "./note/primitives/text";
|
|
||||||
export * from "./note/primitives/repost";
|
|
||||||
export * from "./note/primitives/thread";
|
|
||||||
|
|
||||||
// Deprecated
|
// Deprecated
|
||||||
export * from "./routes/event";
|
export * from "./routes/event";
|
||||||
export * from "./routes/user";
|
export * from "./routes/user";
|
||||||
export * from "./routes/suggest";
|
export * from "./routes/suggest";
|
||||||
export * from "./mentions";
|
export * from "./mentions";
|
||||||
export * from "./replyList";
|
|
||||||
export * from "./emptyFeed";
|
export * from "./emptyFeed";
|
||||||
export * from "./translateRegisterModal";
|
export * from "./translateRegisterModal";
|
||||||
export * from "./account/active";
|
export * from "./account/active";
|
||||||
|
44
packages/ui/src/note/buttons/downvote.tsx
Normal file
44
packages/ui/src/note/buttons/downvote.tsx
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import { ArrowDownIcon, ArrowUpIcon, LoaderIcon } from "@lume/icons";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useNoteContext } from "../provider";
|
||||||
|
import { useArk } from "@lume/ark";
|
||||||
|
import { cn } from "@lume/utils";
|
||||||
|
|
||||||
|
export function NoteDownvote() {
|
||||||
|
const ark = useArk();
|
||||||
|
const event = useNoteContext();
|
||||||
|
|
||||||
|
const [reaction, setReaction] = useState<"-" | null>(null);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const down = async () => {
|
||||||
|
// start loading
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
const res = await ark.downvote(event.id, event.pubkey);
|
||||||
|
if (res) setReaction("-");
|
||||||
|
|
||||||
|
// stop loading
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={down}
|
||||||
|
disabled={!!reaction || loading}
|
||||||
|
className={cn(
|
||||||
|
"inline-flex size-7 items-center justify-center rounded-full",
|
||||||
|
reaction === "-"
|
||||||
|
? "bg-blue-500 text-white"
|
||||||
|
: "bg-neutral-100 text-neutral-700 hover:bg-blue-500 hover:text-white dark:bg-neutral-900 dark:text-neutral-300",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<LoaderIcon className="size-4 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<ArrowDownIcon className="size-4" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
@ -1,66 +1,11 @@
|
|||||||
import { ArrowDownIcon, ArrowUpIcon } from "@lume/icons";
|
import { NoteUpvote } from "./upvote";
|
||||||
import { useState } from "react";
|
import { NoteDownvote } from "./downvote";
|
||||||
import { useNoteContext } from "../provider";
|
|
||||||
import { useArk } from "@lume/ark";
|
|
||||||
import { cn } from "@lume/utils";
|
|
||||||
|
|
||||||
export function NoteReaction() {
|
export function NoteReaction() {
|
||||||
const ark = useArk();
|
|
||||||
const event = useNoteContext();
|
|
||||||
|
|
||||||
const [reaction, setReaction] = useState<"+" | "-">(null);
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
|
|
||||||
const up = async () => {
|
|
||||||
// start loading
|
|
||||||
setLoading(false);
|
|
||||||
|
|
||||||
const res = await ark.upvote(event.id, event.pubkey);
|
|
||||||
if (res) setReaction("+");
|
|
||||||
|
|
||||||
// stop loading
|
|
||||||
setLoading(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const down = async () => {
|
|
||||||
// start loading
|
|
||||||
setLoading(false);
|
|
||||||
|
|
||||||
const res = await ark.downvote(event.id, event.pubkey);
|
|
||||||
if (res) setReaction("-");
|
|
||||||
|
|
||||||
// stop loading
|
|
||||||
setLoading(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="inline-flex items-center gap-2">
|
<div className="inline-flex items-center gap-2">
|
||||||
<button
|
<NoteUpvote />
|
||||||
type="button"
|
<NoteDownvote />
|
||||||
onClick={up}
|
|
||||||
disabled={!!reaction || loading}
|
|
||||||
className={cn(
|
|
||||||
"inline-flex size-7 items-center justify-center rounded-full",
|
|
||||||
reaction === "+"
|
|
||||||
? "bg-blue-500 text-white"
|
|
||||||
: "bg-neutral-100 text-neutral-700 hover:bg-blue-500 hover:text-white dark:bg-neutral-900 dark:text-neutral-300",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<ArrowUpIcon className="size-4" />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={down}
|
|
||||||
disabled={!!reaction || loading}
|
|
||||||
className={cn(
|
|
||||||
"inline-flex size-7 items-center justify-center rounded-full",
|
|
||||||
reaction === "-"
|
|
||||||
? "bg-blue-500 text-white"
|
|
||||||
: "bg-neutral-100 text-neutral-700 hover:bg-blue-500 hover:text-white dark:bg-neutral-900 dark:text-neutral-300",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<ArrowDownIcon className="size-4" />
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
44
packages/ui/src/note/buttons/upvote.tsx
Normal file
44
packages/ui/src/note/buttons/upvote.tsx
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import { ArrowUpIcon, LoaderIcon } from "@lume/icons";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useNoteContext } from "../provider";
|
||||||
|
import { useArk } from "@lume/ark";
|
||||||
|
import { cn } from "@lume/utils";
|
||||||
|
|
||||||
|
export function NoteUpvote() {
|
||||||
|
const ark = useArk();
|
||||||
|
const event = useNoteContext();
|
||||||
|
|
||||||
|
const [reaction, setReaction] = useState<"+" | null>(null);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const up = async () => {
|
||||||
|
// start loading
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
const res = await ark.upvote(event.id, event.pubkey);
|
||||||
|
if (res) setReaction("+");
|
||||||
|
|
||||||
|
// stop loading
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={up}
|
||||||
|
disabled={!!reaction || loading}
|
||||||
|
className={cn(
|
||||||
|
"inline-flex size-7 items-center justify-center rounded-full",
|
||||||
|
reaction === "+"
|
||||||
|
? "bg-blue-500 text-white"
|
||||||
|
: "bg-neutral-100 text-neutral-700 hover:bg-blue-500 hover:text-white dark:bg-neutral-900 dark:text-neutral-300",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<LoaderIcon className="size-4 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<ArrowUpIcon className="size-4" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
@ -1,4 +1,3 @@
|
|||||||
import { PinIcon } from "@lume/icons";
|
|
||||||
import { NOSTR_MENTIONS } from "@lume/utils";
|
import { NOSTR_MENTIONS } from "@lume/utils";
|
||||||
import { ReactNode, useMemo } from "react";
|
import { ReactNode, useMemo } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
@ -6,7 +5,7 @@ import reactStringReplace from "react-string-replace";
|
|||||||
import { User } from "../../user";
|
import { User } from "../../user";
|
||||||
import { Hashtag } from "./hashtag";
|
import { Hashtag } from "./hashtag";
|
||||||
import { MentionUser } from "./user";
|
import { MentionUser } from "./user";
|
||||||
import { useEvent } from "@lume/ark";
|
import { useArk, useEvent } from "@lume/ark";
|
||||||
|
|
||||||
export function MentionNote({
|
export function MentionNote({
|
||||||
eventId,
|
eventId,
|
||||||
@ -18,6 +17,7 @@ export function MentionNote({
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { isLoading, isError, data } = useEvent(eventId);
|
const { isLoading, isError, data } = useEvent(eventId);
|
||||||
|
|
||||||
|
const ark = useArk();
|
||||||
const richContent = useMemo(() => {
|
const richContent = useMemo(() => {
|
||||||
if (!data) return "";
|
if (!data) return "";
|
||||||
|
|
||||||
@ -117,17 +117,18 @@ export function MentionNote({
|
|||||||
</div>
|
</div>
|
||||||
</User.Root>
|
</User.Root>
|
||||||
</User.Provider>
|
</User.Provider>
|
||||||
<div className="line-clamp-4 select-text whitespace-pre-line text-balance px-3 leading-normal">
|
<div className="line-clamp-4 select-text whitespace-normal text-balance px-3 leading-normal">
|
||||||
{richContent}
|
{richContent}
|
||||||
</div>
|
</div>
|
||||||
{openable ? (
|
{openable ? (
|
||||||
<div className="flex h-10 items-center justify-between px-3">
|
<div className="flex h-10 items-center justify-between px-3">
|
||||||
<a
|
<button
|
||||||
href={`/events/${data.id}`}
|
type="button"
|
||||||
|
onClick={() => ark.open_thread(data.id)}
|
||||||
className="text-blue-500 hover:text-blue-600"
|
className="text-blue-500 hover:text-blue-600"
|
||||||
>
|
>
|
||||||
{t("note.showMore")}
|
{t("note.showMore")}
|
||||||
</a>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="h-3" />
|
<div className="h-3" />
|
||||||
|
@ -34,7 +34,7 @@ export function NoteMenu() {
|
|||||||
<DropdownMenu.Trigger asChild>
|
<DropdownMenu.Trigger asChild>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="text-neutral-500 hover:text-blue-500 dark:text-neutral-400"
|
className="group inline-flex size-7 items-center justify-center text-neutral-800 dark:text-neutral-200"
|
||||||
>
|
>
|
||||||
<HorizontalDotsIcon className="size-5" />
|
<HorizontalDotsIcon className="size-5" />
|
||||||
</button>
|
</button>
|
||||||
|
@ -22,7 +22,7 @@ export function TextNote({
|
|||||||
<Note.Menu />
|
<Note.Menu />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-3">
|
<div className="flex gap-3">
|
||||||
<div className="size-10 shrink-0" />
|
<div className="size-11 shrink-0" />
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<Note.Thread className="mb-2" />
|
<Note.Thread className="mb-2" />
|
||||||
<Note.Content />
|
<Note.Content />
|
||||||
|
@ -16,7 +16,7 @@ export function NoteUser({ className }: { className?: string }) {
|
|||||||
>
|
>
|
||||||
<div className="flex gap-3">
|
<div className="flex gap-3">
|
||||||
<HoverCard.Trigger>
|
<HoverCard.Trigger>
|
||||||
<User.Avatar className="size-10 shrink-0 rounded-full object-cover ring-1 ring-neutral-200/50 dark:ring-neutral-800/50" />
|
<User.Avatar className="size-11 shrink-0 rounded-full object-cover ring-1 ring-neutral-200/50 dark:ring-neutral-800/50" />
|
||||||
</HoverCard.Trigger>
|
</HoverCard.Trigger>
|
||||||
<div>
|
<div>
|
||||||
<User.Name className="font-semibold leading-tight text-neutral-950 dark:text-neutral-50" />
|
<User.Name className="font-semibold leading-tight text-neutral-950 dark:text-neutral-50" />
|
||||||
|
@ -34,10 +34,6 @@ export function ReplyList({
|
|||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<ReplyForm
|
|
||||||
eventId={eventId}
|
|
||||||
className="border-t border-neutral-100 py-4 dark:border-neutral-900"
|
|
||||||
/>
|
|
||||||
{!data ? (
|
{!data ? (
|
||||||
<div className="mt-4 flex h-16 items-center justify-center p-3">
|
<div className="mt-4 flex h-16 items-center justify-center p-3">
|
||||||
<LoaderIcon className="h-5 w-5 animate-spin" />
|
<LoaderIcon className="h-5 w-5 animate-spin" />
|
||||||
|
@ -4,34 +4,9 @@ import { useUserContext } from "./provider";
|
|||||||
export function UserAbout({ className }: { className?: string }) {
|
export function UserAbout({ className }: { className?: string }) {
|
||||||
const user = useUserContext();
|
const user = useUserContext();
|
||||||
|
|
||||||
if (!user.profile) {
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col gap-1">
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"h-4 w-20 animate-pulse rounded bg-black/20 dark:bg-white/20",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"h-4 w-full animate-pulse rounded bg-black/20 dark:bg-white/20",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"h-4 w-24 animate-pulse rounded bg-black/20 dark:bg-white/20",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn("content-break select-text", className)}>
|
<div className={cn("content-break select-text", className)}>
|
||||||
{user.profile.about?.trim() || "No bio"}
|
{user.profile?.about?.trim() || "No bio"}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -11,28 +11,15 @@ export function UserAvatar({ className }: { className?: string }) {
|
|||||||
const fallbackAvatar = useMemo(
|
const fallbackAvatar = useMemo(
|
||||||
() =>
|
() =>
|
||||||
`data:image/svg+xml;utf8,${encodeURIComponent(
|
`data:image/svg+xml;utf8,${encodeURIComponent(
|
||||||
minidenticon(user?.pubkey || nanoid(), 90, 50),
|
minidenticon(user.pubkey || nanoid(), 90, 50),
|
||||||
)}`,
|
)}`,
|
||||||
[user],
|
[user],
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!user.profile) {
|
|
||||||
return (
|
|
||||||
<div className="shrink-0">
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"animate-pulse rounded bg-black/20 dark:bg-white/20",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Avatar.Root className="shrink-0">
|
<Avatar.Root className="shrink-0">
|
||||||
<Avatar.Image
|
<Avatar.Image
|
||||||
src={user.profile.picture}
|
src={user.profile?.picture}
|
||||||
alt={user.pubkey}
|
alt={user.pubkey}
|
||||||
loading="eager"
|
loading="eager"
|
||||||
decoding="async"
|
decoding="async"
|
||||||
|
@ -1,23 +1,14 @@
|
|||||||
import { cn } from "@lume/utils";
|
import { cn, displayNpub } from "@lume/utils";
|
||||||
import { useUserContext } from "./provider";
|
import { useUserContext } from "./provider";
|
||||||
|
|
||||||
export function UserName({ className }: { className?: string }) {
|
export function UserName({ className }: { className?: string }) {
|
||||||
const user = useUserContext();
|
const user = useUserContext();
|
||||||
|
|
||||||
if (!user.profile) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"mb-1 h-3 w-20 animate-pulse self-center rounded bg-black/20 dark:bg-white/20",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn("max-w-[12rem] truncate", className)}>
|
<div className={cn("max-w-[12rem] truncate", className)}>
|
||||||
{user.profile.display_name || user.profile.name || "Anon"}
|
{user.profile?.display_name ||
|
||||||
|
user.profile?.name ||
|
||||||
|
displayNpub(user.pubkey, 16)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -18,25 +18,12 @@ export function UserNip05({ className }: { className?: string }) {
|
|||||||
enabled: !!user.profile,
|
enabled: !!user.profile,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!user.profile) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"h-3 w-20 animate-pulse rounded bg-black/20 dark:bg-white/20",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="inline-flex items-center gap-1">
|
<div className="inline-flex items-center gap-1">
|
||||||
<p className={cn("text-sm", className)}>
|
<p className={cn("text-sm", className)}>
|
||||||
{!user?.profile?.nip05
|
{!user.profile?.nip05
|
||||||
? displayNpub(user.pubkey, 16)
|
? displayNpub(user.pubkey, 16)
|
||||||
: user?.profile?.nip05?.startsWith("_@")
|
: user.profile?.nip05.replace("_@", "")}
|
||||||
? user?.profile?.nip05?.replace("_@", "")
|
|
||||||
: user?.profile?.nip05}
|
|
||||||
</p>
|
</p>
|
||||||
{!isLoading && verified ? (
|
{!isLoading && verified ? (
|
||||||
<VerifiedIcon className="size-4 text-teal-500" />
|
<VerifiedIcon className="size-4 text-teal-500" />
|
||||||
|
@ -1,40 +1,25 @@
|
|||||||
import { useArk } from "@lume/ark";
|
import { useProfile } from "@lume/ark";
|
||||||
import { Metadata } from "@lume/types";
|
import { Metadata } from "@lume/types";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
|
||||||
import { ReactNode, createContext, useContext } from "react";
|
import { ReactNode, createContext, useContext } from "react";
|
||||||
|
|
||||||
const UserContext = createContext<{ pubkey: string; profile: Metadata }>(null);
|
const UserContext = createContext<{
|
||||||
|
pubkey: string;
|
||||||
|
isError: boolean;
|
||||||
|
isLoading: boolean;
|
||||||
|
profile: Metadata;
|
||||||
|
}>(null);
|
||||||
|
|
||||||
export function UserProvider({
|
export function UserProvider({
|
||||||
pubkey,
|
pubkey,
|
||||||
children,
|
children,
|
||||||
embed,
|
|
||||||
}: {
|
}: {
|
||||||
pubkey: string;
|
pubkey: string;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
embed?: string;
|
|
||||||
}) {
|
}) {
|
||||||
const ark = useArk();
|
const { isLoading, isError, profile } = useProfile(pubkey);
|
||||||
const { data: profile } = useQuery({
|
|
||||||
queryKey: ["user", pubkey],
|
|
||||||
queryFn: async () => {
|
|
||||||
if (embed) return JSON.parse(embed) as Metadata;
|
|
||||||
try {
|
|
||||||
const profile: Metadata = await ark.get_profile(pubkey);
|
|
||||||
return profile;
|
|
||||||
} catch (e) {
|
|
||||||
throw new Error(e);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
refetchOnMount: false,
|
|
||||||
refetchOnWindowFocus: false,
|
|
||||||
refetchOnReconnect: false,
|
|
||||||
staleTime: Infinity,
|
|
||||||
retry: 2,
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<UserContext.Provider value={{ pubkey, profile }}>
|
<UserContext.Provider value={{ pubkey, isError, isLoading, profile }}>
|
||||||
{children}
|
{children}
|
||||||
</UserContext.Provider>
|
</UserContext.Provider>
|
||||||
);
|
);
|
||||||
|
@ -75,9 +75,15 @@ importers:
|
|||||||
'@radix-ui/react-checkbox':
|
'@radix-ui/react-checkbox':
|
||||||
specifier: ^1.0.4
|
specifier: ^1.0.4
|
||||||
version: 1.0.4(@types/react-dom@18.2.19)(@types/react@18.2.55)(react-dom@18.2.0)(react@18.2.0)
|
version: 1.0.4(@types/react-dom@18.2.19)(@types/react@18.2.55)(react-dom@18.2.0)(react@18.2.0)
|
||||||
|
'@radix-ui/react-collapsible':
|
||||||
|
specifier: ^1.0.3
|
||||||
|
version: 1.0.3(@types/react-dom@18.2.19)(@types/react@18.2.55)(react-dom@18.2.0)(react@18.2.0)
|
||||||
'@tanstack/react-query':
|
'@tanstack/react-query':
|
||||||
specifier: ^5.20.5
|
specifier: ^5.20.5
|
||||||
version: 5.20.5(react@18.2.0)
|
version: 5.20.5(react@18.2.0)
|
||||||
|
'@tanstack/react-query-persist-client':
|
||||||
|
specifier: ^5.22.2
|
||||||
|
version: 5.22.2(@tanstack/react-query@5.20.5)(react@18.2.0)
|
||||||
'@tanstack/react-router':
|
'@tanstack/react-router':
|
||||||
specifier: ^1.16.2
|
specifier: ^1.16.2
|
||||||
version: 1.16.2(react-dom@18.2.0)(react@18.2.0)
|
version: 1.16.2(react-dom@18.2.0)(react@18.2.0)
|
||||||
@ -87,6 +93,9 @@ importers:
|
|||||||
i18next-resources-to-backend:
|
i18next-resources-to-backend:
|
||||||
specifier: ^1.2.0
|
specifier: ^1.2.0
|
||||||
version: 1.2.0
|
version: 1.2.0
|
||||||
|
idb-keyval:
|
||||||
|
specifier: ^6.2.1
|
||||||
|
version: 6.2.1
|
||||||
react:
|
react:
|
||||||
specifier: ^18.2.0
|
specifier: ^18.2.0
|
||||||
version: 18.2.0
|
version: 18.2.0
|
||||||
@ -202,7 +211,7 @@ importers:
|
|||||||
version: 1.0.4(@types/react@18.2.55)(react-dom@18.2.0)(react@18.2.0)
|
version: 1.0.4(@types/react@18.2.55)(react-dom@18.2.0)(react@18.2.0)
|
||||||
'@radix-ui/react-collapsible':
|
'@radix-ui/react-collapsible':
|
||||||
specifier: ^1.0.3
|
specifier: ^1.0.3
|
||||||
version: 1.0.3(@types/react@18.2.55)(react-dom@18.2.0)(react@18.2.0)
|
version: 1.0.3(@types/react-dom@18.2.19)(@types/react@18.2.55)(react-dom@18.2.0)(react@18.2.0)
|
||||||
'@radix-ui/react-dialog':
|
'@radix-ui/react-dialog':
|
||||||
specifier: ^1.0.5
|
specifier: ^1.0.5
|
||||||
version: 1.0.5(@types/react@18.2.55)(react-dom@18.2.0)(react@18.2.0)
|
version: 1.0.5(@types/react@18.2.55)(react-dom@18.2.0)(react@18.2.0)
|
||||||
@ -896,7 +905,7 @@ importers:
|
|||||||
version: 1.0.4(@types/react@18.2.55)(react-dom@18.2.0)(react@18.2.0)
|
version: 1.0.4(@types/react@18.2.55)(react-dom@18.2.0)(react@18.2.0)
|
||||||
'@radix-ui/react-collapsible':
|
'@radix-ui/react-collapsible':
|
||||||
specifier: ^1.0.3
|
specifier: ^1.0.3
|
||||||
version: 1.0.3(@types/react@18.2.55)(react-dom@18.2.0)(react@18.2.0)
|
version: 1.0.3(@types/react-dom@18.2.19)(@types/react@18.2.55)(react-dom@18.2.0)(react@18.2.0)
|
||||||
'@radix-ui/react-dialog':
|
'@radix-ui/react-dialog':
|
||||||
specifier: ^1.0.5
|
specifier: ^1.0.5
|
||||||
version: 1.0.5(@types/react@18.2.55)(react-dom@18.2.0)(react@18.2.0)
|
version: 1.0.5(@types/react@18.2.55)(react-dom@18.2.0)(react@18.2.0)
|
||||||
@ -1893,7 +1902,7 @@ packages:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@babel/runtime': 7.23.9
|
'@babel/runtime': 7.23.9
|
||||||
'@radix-ui/primitive': 1.0.1
|
'@radix-ui/primitive': 1.0.1
|
||||||
'@radix-ui/react-collapsible': 1.0.3(@types/react@18.2.55)(react-dom@18.2.0)(react@18.2.0)
|
'@radix-ui/react-collapsible': 1.0.3(@types/react-dom@18.2.19)(@types/react@18.2.55)(react-dom@18.2.0)(react@18.2.0)
|
||||||
'@radix-ui/react-collection': 1.0.3(@types/react@18.2.55)(react-dom@18.2.0)(react@18.2.0)
|
'@radix-ui/react-collection': 1.0.3(@types/react@18.2.55)(react-dom@18.2.0)(react@18.2.0)
|
||||||
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.55)(react@18.2.0)
|
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.55)(react@18.2.0)
|
||||||
'@radix-ui/react-context': 1.0.1(@types/react@18.2.55)(react@18.2.0)
|
'@radix-ui/react-context': 1.0.1(@types/react@18.2.55)(react@18.2.0)
|
||||||
@ -2002,7 +2011,7 @@ packages:
|
|||||||
react-dom: 18.2.0(react@18.2.0)
|
react-dom: 18.2.0(react@18.2.0)
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/@radix-ui/react-collapsible@1.0.3(@types/react@18.2.55)(react-dom@18.2.0)(react@18.2.0):
|
/@radix-ui/react-collapsible@1.0.3(@types/react-dom@18.2.19)(@types/react@18.2.55)(react-dom@18.2.0)(react@18.2.0):
|
||||||
resolution: {integrity: sha512-UBmVDkmR6IvDsloHVN+3rtx4Mi5TFvylYXpluuv0f37dtaz3H99bp8No0LGXRigVpl3UAT4l9j6bIchh42S/Gg==}
|
resolution: {integrity: sha512-UBmVDkmR6IvDsloHVN+3rtx4Mi5TFvylYXpluuv0f37dtaz3H99bp8No0LGXRigVpl3UAT4l9j6bIchh42S/Gg==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@types/react': '*'
|
'@types/react': '*'
|
||||||
@ -2025,6 +2034,7 @@ packages:
|
|||||||
'@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.55)(react@18.2.0)
|
'@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.55)(react@18.2.0)
|
||||||
'@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.2.55)(react@18.2.0)
|
'@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.2.55)(react@18.2.0)
|
||||||
'@types/react': 18.2.55
|
'@types/react': 18.2.55
|
||||||
|
'@types/react-dom': 18.2.19
|
||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
react-dom: 18.2.0(react@18.2.0)
|
react-dom: 18.2.0(react@18.2.0)
|
||||||
dev: false
|
dev: false
|
||||||
@ -2902,6 +2912,27 @@ packages:
|
|||||||
resolution: {integrity: sha512-T1W28gGgWn0A++tH3lxj3ZuUVZZorsiKcv+R50RwmPYz62YoDEkG4/aXHZELGkRp4DfrW07dyq2K5dvJ4Wl1aA==}
|
resolution: {integrity: sha512-T1W28gGgWn0A++tH3lxj3ZuUVZZorsiKcv+R50RwmPYz62YoDEkG4/aXHZELGkRp4DfrW07dyq2K5dvJ4Wl1aA==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/@tanstack/query-core@5.22.2:
|
||||||
|
resolution: {integrity: sha512-z3PwKFUFACMUqe1eyesCIKg3Jv1mysSrYfrEW5ww5DCDUD4zlpTKBvUDaEjsfZzL3ULrFLDM9yVUxI/fega1Qg==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@tanstack/query-persist-client-core@5.22.2:
|
||||||
|
resolution: {integrity: sha512-sFDgWoN54uclIDIoImPmDzxTq8HhZEt9pO0JbVHjI6LPZqunMMF9yAq9zFKrpH//jD5f+rBCQsdGyhdpUo9e8Q==}
|
||||||
|
dependencies:
|
||||||
|
'@tanstack/query-core': 5.22.2
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@tanstack/react-query-persist-client@5.22.2(@tanstack/react-query@5.20.5)(react@18.2.0):
|
||||||
|
resolution: {integrity: sha512-osAaQn2PDTaa2ApTLOAus7g8Y96LHfS2+Pgu/RoDlEJUEkX7xdEn0YuurxbnJaDJDESMfr+CH/eAX2y+lx02Fg==}
|
||||||
|
peerDependencies:
|
||||||
|
'@tanstack/react-query': ^5.22.2
|
||||||
|
react: ^18.0.0
|
||||||
|
dependencies:
|
||||||
|
'@tanstack/query-persist-client-core': 5.22.2
|
||||||
|
'@tanstack/react-query': 5.20.5(react@18.2.0)
|
||||||
|
react: 18.2.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@tanstack/react-query@5.20.5(react@18.2.0):
|
/@tanstack/react-query@5.20.5(react@18.2.0):
|
||||||
resolution: {integrity: sha512-6MHwJ8G9cnOC/XKrwt56QMc91vN7hLlAQNUA0ubP7h9Jj3a/CmkUwT6ALdFbnVP+PsYdhW3WONa8WQ4VcTaSLQ==}
|
resolution: {integrity: sha512-6MHwJ8G9cnOC/XKrwt56QMc91vN7hLlAQNUA0ubP7h9Jj3a/CmkUwT6ALdFbnVP+PsYdhW3WONa8WQ4VcTaSLQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@ -4551,6 +4582,10 @@ packages:
|
|||||||
safer-buffer: 2.1.2
|
safer-buffer: 2.1.2
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/idb-keyval@6.2.1:
|
||||||
|
resolution: {integrity: sha512-8Sb3veuYCyrZL+VBt9LJfZjLUPWVvqn8tG28VqYNFCo43KHcKuq+b4EiXGeuaLAQWL2YmyDgMp2aSpH9JHsEQg==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/ieee754@1.2.1:
|
/ieee754@1.2.1:
|
||||||
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
|
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
|
||||||
dev: false
|
dev: false
|
||||||
|
Loading…
Reference in New Issue
Block a user