feat: improve

This commit is contained in:
reya 2024-03-02 15:21:28 +07:00
parent cfcb9bc6ed
commit ca0e041731
62 changed files with 1353 additions and 1631 deletions

View File

@ -15,10 +15,12 @@
"@lume/utils": "workspace:^",
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-collapsible": "^1.0.3",
"@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-popover": "^1.0.7",
"@tanstack/query-sync-storage-persister": "^5.24.1",
"@tanstack/react-query": "^5.22.2",
"@tanstack/react-query-persist-client": "^5.22.2",
"@tanstack/react-router": "^1.16.6",
"@tanstack/react-query": "^5.24.1",
"@tanstack/react-query-persist-client": "^5.24.1",
"@tanstack/react-router": "^1.17.4",
"i18next": "^23.10.0",
"i18next-resources-to-backend": "^1.2.0",
"nostr-tools": "^2.3.1",
@ -27,16 +29,16 @@
"react-i18next": "^14.0.5",
"slate": "^0.101.5",
"slate-react": "^0.101.6",
"sonner": "^1.4.1",
"sonner": "^1.4.2",
"virtua": "^0.27.5"
},
"devDependencies": {
"@lume/tailwindcss": "workspace:^",
"@lume/tsconfig": "workspace:^",
"@lume/types": "workspace:^",
"@tanstack/router-devtools": "^1.16.6",
"@tanstack/router-devtools": "^1.17.4",
"@tanstack/router-vite-plugin": "^1.16.5",
"@types/react": "^18.2.58",
"@types/react": "^18.2.60",
"@types/react-dom": "^18.2.19",
"@vitejs/plugin-react-swc": "^3.6.0",
"autoprefixer": "^10.4.17",

View File

@ -1,9 +1,12 @@
import { useArk } from "@lume/ark";
import { PlusIcon } from "@lume/icons";
import { Account } from "@lume/types";
import { User } from "@lume/ui";
import { Link, useNavigate, useParams } from "@tanstack/react-router";
import { useNavigate, useParams, useSearch } from "@tanstack/react-router";
import { useEffect, useState } from "react";
import * as Popover from "@radix-ui/react-popover";
import { Link } from "@tanstack/react-router";
import { useTranslation } from "react-i18next";
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
export function Accounts() {
const ark = useArk();
@ -22,12 +25,6 @@ export function Accounts() {
return (
<div data-tauri-drag-region className="flex items-center gap-4">
<Link
to="/landing"
className="inline-flex size-7 items-center justify-center rounded-full bg-neutral-300 ring-offset-2 ring-offset-neutral-200 hover:ring-1 hover:ring-blue-500 dark:bg-neutral-700 dark:ring-offset-neutral-950"
>
<PlusIcon className="size-4" />
</Link>
{accounts
? accounts.map((account) =>
// @ts-ignore, useless
@ -48,15 +45,14 @@ function Inactive({ pubkey }: { pubkey: string }) {
const changeAccount = async (npub: string) => {
const select = await ark.load_selected_account(npub);
if (select)
navigate({ to: "/$account/home/local", params: { account: npub } });
if (select) navigate({ to: "/$account/home", params: { account: npub } });
};
return (
<button type="button" onClick={() => changeAccount(pubkey)}>
<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 className="rounded-full">
<User.Avatar className="aspect-square h-auto w-8 rounded-full object-cover" />
</User.Root>
</User.Provider>
</button>
@ -64,11 +60,99 @@ function Inactive({ pubkey }: { pubkey: string }) {
}
function Active({ pubkey }: { pubkey: string }) {
const [open, setOpen] = useState(true);
// @ts-ignore, magic !!!
const { guest } = useSearch({ strict: false });
const { t } = useTranslation();
if (guest) {
return (
<Popover.Root open={open} onOpenChange={setOpen}>
<Popover.Trigger asChild>
<button type="button">
<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>
</button>
</Popover.Trigger>
<Popover.Portal>
<Popover.Content
className="flex w-[280px] flex-col gap-4 rounded-xl bg-black p-5 text-neutral-100 focus:outline-none dark:bg-white dark:text-neutral-900 dark:shadow-none"
sideOffset={10}
side="bottom"
>
<div>
<h1 className="mb-1 font-semibold">You're using guest account</h1>
<p className="text-sm text-neutral-500 dark:text-neutral-600">
You can continue by claim and backup this account, or you can
import your own account key.
</p>
</div>
<div className="flex flex-col gap-2">
<Link
to="/backup"
className="inline-flex h-9 w-full items-center justify-center rounded-lg bg-white text-sm font-medium leading-tight text-neutral-900 hover:bg-neutral-100"
>
Claim & Backup
</Link>
<Link
to="/login"
className="inline-flex h-9 w-full items-center justify-center rounded-lg bg-neutral-900 text-sm font-medium leading-tight text-neutral-100 hover:bg-neutral-800"
>
{t("welcome.login")}
</Link>
</div>
<Popover.Arrow className="fill-black dark:fill-white" />
</Popover.Content>
</Popover.Portal>
</Popover.Root>
);
}
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>
<DropdownMenu.Root>
<DropdownMenu.Trigger>
<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>
</DropdownMenu.Trigger>
<DropdownMenu.Portal>
<DropdownMenu.Content
className="flex w-[220px] flex-col rounded-xl bg-black p-2 text-neutral-100 focus:outline-none dark:bg-white dark:text-neutral-900 dark:shadow-none"
sideOffset={10}
side="bottom"
>
<DropdownMenu.Item className="group relative flex h-9 select-none items-center rounded-md px-3 text-sm font-medium leading-none outline-none hover:bg-neutral-900 dark:hover:bg-neutral-100">
Add account
<div className="ml-auto pl-5 text-xs text-neutral-800 dark:text-neutral-200">
+Shift+N
</div>
</DropdownMenu.Item>
<DropdownMenu.Item className="group relative flex h-9 select-none items-center rounded-md px-3 text-sm font-medium leading-none outline-none hover:bg-neutral-900 dark:hover:bg-neutral-100">
Profile
<div className="ml-auto pl-5 text-xs text-neutral-800 dark:text-neutral-200">
+Shift+P
</div>
</DropdownMenu.Item>
<DropdownMenu.Item className="group relative flex h-9 select-none items-center rounded-md px-3 text-sm font-medium leading-none outline-none hover:bg-neutral-900 dark:hover:bg-neutral-100">
Settings
<div className="ml-auto pl-5 text-xs text-neutral-800 dark:text-neutral-200">
+Shift+S
</div>
</DropdownMenu.Item>
<DropdownMenu.Item className="group relative flex h-9 select-none items-center rounded-md px-3 text-sm font-medium leading-none outline-none hover:bg-neutral-900 dark:hover:bg-neutral-100">
Logout
<div className="ml-auto pl-5 text-xs text-neutral-800 dark:text-neutral-200">
+Shift+L
</div>
</DropdownMenu.Item>
<DropdownMenu.Arrow className="fill-black dark:fill-white" />
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu.Root>
);
}

View File

@ -0,0 +1,56 @@
import { LoaderIcon } from "@lume/icons";
import { useQuery } from "@tanstack/react-query";
import { useTranslation } from "react-i18next";
import { User } from "@lume/ui";
export function Suggest() {
const { t } = useTranslation();
const { isLoading, isError, data } = useQuery({
queryKey: ["trending-users"],
queryFn: async ({ signal }: { signal: AbortSignal }) => {
const res = await fetch("https://api.nostr.band/v0/trending/profiles", {
signal,
});
if (!res.ok) {
throw new Error("Failed to fetch trending users from nostr.band API.");
}
return res.json();
},
});
return (
<div className="flex flex-col divide-y divide-neutral-100 dark:divide-neutral-900">
<div className="h-10 shrink-0 text-lg font-semibold">
Suggested Follows
</div>
{isLoading ? (
<div className="flex h-44 w-full items-center justify-center">
<LoaderIcon className="size-4 animate-spin" />
</div>
) : isError ? (
<div className="flex h-44 w-full items-center justify-center">
{t("suggestion.error")}
</div>
) : (
data?.profiles.map((item: { pubkey: string }) => (
<div key={item.pubkey} className="h-max w-full overflow-hidden py-5">
<User.Provider pubkey={item.pubkey}>
<User.Root>
<div className="flex h-full w-full flex-col gap-2">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2.5">
<User.Avatar className="size-10 shrink-0 rounded-full" />
<User.Name className="leadning-tight max-w-[15rem] truncate font-semibold" />
</div>
<User.Button className="inline-flex h-8 w-20 items-center justify-center rounded-lg bg-neutral-100 text-sm font-medium hover:bg-neutral-200 dark:bg-neutral-900 dark:hover:bg-neutral-800" />
</div>
<User.About className="mt-1 line-clamp-3 max-w-none select-text text-neutral-800 dark:text-neutral-400" />
</div>
</User.Root>
</User.Provider>
</div>
))
)}
</div>
);
}

View File

@ -1,17 +1,19 @@
import {
BellFilledIcon,
BellIcon,
EditIcon,
ComposeFilledIcon,
HomeFilledIcon,
HomeIcon,
HorizontalDotsIcon,
SpaceFilledIcon,
SpaceIcon,
} from "@lume/icons";
import { Link, useParams } from "@tanstack/react-router";
import { Link } from "@tanstack/react-router";
import { Outlet, createFileRoute } from "@tanstack/react-router";
import { cn } from "@lume/utils";
import { Accounts } from "@/components/accounts";
import { useArk } from "@lume/ark";
import { Box } from "@lume/ui";
export const Route = createFileRoute("/$account")({
component: App,
@ -36,37 +38,37 @@ function App() {
<button
type="button"
onClick={() => ark.open_editor()}
className="inline-flex h-7 w-max items-center justify-center gap-1 rounded-full bg-blue-500 px-2.5 text-sm font-medium text-white hover:bg-blue-600"
className="inline-flex h-8 w-max items-center justify-center gap-1 rounded-full bg-blue-500 px-3 text-sm font-medium text-white hover:bg-blue-600"
>
<EditIcon className="size-4" />
New
<ComposeFilledIcon className="size-4" />
New post
</button>
</div>
</div>
<div className="flex h-full min-h-0 w-full">
<div className="h-full w-full flex-1 px-2 pb-2">
<Outlet />
</div>
</div>
<Box>
<Outlet />
</Box>
</div>
);
}
function Navigation() {
// @ts-ignore, useless
const { account } = useParams({ strict: false });
const { account } = Route.useParams();
return (
<div
data-tauri-drag-region
className="flex h-full flex-1 items-center gap-2"
>
<Link to="/$account/home/local" params={{ account }}>
<Link to="/$account/home" params={{ account }}>
{({ 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" : "",
"inline-flex h-8 w-max items-center justify-center gap-2 rounded-full px-3",
isActive
? "bg-neutral-300 hover:bg-neutral-400 dark:bg-neutral-800 dark:hover:bg-neutral-700"
: "hover:bg-black/10 dark:hover:bg-white/10",
)}
>
{isActive ? (
@ -82,8 +84,10 @@ function Navigation() {
{({ 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" : "",
"inline-flex h-8 w-max items-center justify-center gap-2 rounded-full px-3 hover:bg-black/10 dark:hover:bg-white/10",
isActive
? "bg-neutral-300 hover:bg-neutral-400 dark:bg-neutral-800 dark:hover:bg-neutral-700"
: "hover:bg-black/10 dark:hover:bg-white/10",
)}
>
{isActive ? (
@ -99,8 +103,10 @@ function Navigation() {
{({ 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" : "",
"inline-flex h-8 w-max items-center justify-center gap-2 rounded-full px-3 hover:bg-black/10 dark:hover:bg-white/10",
isActive
? "bg-neutral-300 hover:bg-neutral-400 dark:bg-neutral-800 dark:hover:bg-neutral-700"
: "hover:bg-black/10 dark:hover:bg-white/10",
)}
>
{isActive ? (

View File

@ -0,0 +1,133 @@
import { RepostNote } from "@/components/repost";
import { Suggest } from "@/components/suggest";
import { TextNote } from "@/components/text";
import { useArk } from "@lume/ark";
import {
LoaderIcon,
ArrowRightCircleIcon,
RefreshIcon,
InfoIcon,
} 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";
export const Route = createLazyFileRoute("/$account/home")({
component: Home,
});
function Home() {
const ark = useArk();
const currentDate = new Date().toLocaleString("default", {
weekday: "long",
month: "long",
day: "numeric",
});
const { account } = Route.useParams();
const {
data,
hasNextPage,
isLoading,
isRefetching,
isFetchingNextPage,
fetchNextPage,
refetch,
} = useInfiniteQuery({
queryKey: ["local_newsfeed", account],
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) => {
if (!event) return;
switch (event.kind) {
case Kind.Repost:
return <RepostNote key={event.id} event={event} />;
default:
return <TextNote key={event.id} event={event} />;
}
};
return (
<div className="flex flex-col gap-3">
<div className="mx-auto flex h-12 w-full max-w-xl shrink-0 items-center justify-between border-b border-neutral-100 dark:border-neutral-900">
<h3 className="text-sm font-medium uppercase leading-tight text-neutral-600 dark:text-neutral-400">
{currentDate}
</h3>
<div className="flex items-center gap-3">
<button
type="button"
onClick={() => refetch()}
className="text-neutral-700 hover:text-blue-500 dark:text-neutral-300"
>
<RefreshIcon className="size-4" />
</button>
</div>
</div>
<div className="mx-auto flex w-full max-w-xl flex-1 flex-col">
<div className="flex-1">
{isLoading || isRefetching ? (
<div className="flex h-20 w-full flex-col items-center justify-center gap-1">
<LoaderIcon className="size-5 animate-spin" />
</div>
) : !data.length ? (
<div className="flex flex-col gap-3">
<div className="flex items-center gap-2 rounded-xl bg-neutral-100 p-3 dark:bg-neutral-900">
<InfoIcon className="size-5" />
<p>
Empty newsfeed. Or you can go to{" "}
<a href="" className="text-blue-500 hover:text-blue-600">
Discover
</a>
</p>
</div>
<Suggest />
</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>
</div>
</div>
);
}

View File

@ -1,66 +0,0 @@
import { cn } from "@lume/utils";
import {
Outlet,
Link,
createFileRoute,
useParams,
} from "@tanstack/react-router";
export const Route = createFileRoute("/$account/home")({
component: Home,
});
function Home() {
// @ts-ignore, useless
const { account } = useParams({ strict: false });
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="/$account/home/local"
params={{ account }}
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="/$account/home/global"
params={{ account }}
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>
);
}

View File

@ -1,92 +0,0 @@
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("/$account/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>
);
}

View File

@ -1,89 +0,0 @@
import { useArk } from "@lume/ark";
import { ArrowRightCircleIcon, ArrowRightIcon, LoaderIcon } 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("/$account/home/local")({
component: LocalTimeline,
});
function LocalTimeline() {
const ark = useArk();
const { account } = Route.useParams();
const { data, hasNextPage, isLoading, isFetchingNextPage, fetchNextPage } =
useInfiniteQuery({
queryKey: ["local_newsfeed", account],
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) => {
if (!event) return;
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 flex-col items-center justify-center gap-1">
<LoaderIcon className="size-5 animate-spin" />
<p>Loading...</p>
</div>
) : !data.length ? (
<div className="flex flex-col gap-3">
<EmptyFeed />
</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>
);
}

View File

@ -26,7 +26,7 @@ function Create() {
try {
await ark.save_account(keys);
navigate({
to: "/$account/home/local",
to: "/$account/home",
params: { account: keys.npub },
search: { onboarding: true },
replace: true,

View File

@ -32,7 +32,7 @@ function Import() {
nsec: key,
});
navigate({
to: "/$account/home/local",
to: "/$account/home",
params: { account: npub },
search: { onboarding: true },
replace: true,

View File

@ -0,0 +1,20 @@
import { createLazyFileRoute } from "@tanstack/react-router";
import { useTranslation } from "react-i18next";
export const Route = createLazyFileRoute("/backup")({
component: Screen,
});
function Screen() {
const { t } = useTranslation();
return (
<div className="flex h-full w-full items-center justify-center">
<div className="mx-auto flex w-full max-w-md flex-col gap-8">
<div className="flex flex-col items-center gap-1 text-center">
<h1 className="text-2xl font-semibold">{t("backup.title")}</h1>
</div>
</div>
</div>
);
}

View File

@ -31,7 +31,7 @@ function Event() {
return (
<WindowVirtualizer>
<Container withDrag>
<Box>
<Box className="px-3 pt-3">
<MainNote data={data} />
{data ? <ReplyList eventId={eventId} /> : null}
</Box>

View File

@ -11,13 +11,14 @@ export const Route = createFileRoute("/")({
const accounts = await ark.get_all_accounts();
switch (accounts.length) {
// Empty account
// Guest account
case 0:
const guest = await ark.create_guest_account();
throw redirect({
to: "/landing",
search: {
redirect: location.href,
},
to: "/$account/home",
params: { account: guest },
search: { guest: true },
replace: true,
});
// Only 1 account, skip account selection screen
case 1:
@ -25,11 +26,9 @@ export const Route = createFileRoute("/")({
const loadAccount = await ark.load_selected_account(account);
if (loadAccount) {
throw redirect({
to: "/$account/home/local",
to: "/$account/home",
params: { account },
search: {
redirect: location.href,
},
replace: true,
});
}
// Account selection
@ -51,24 +50,24 @@ function Screen() {
const loadAccount = await ark.load_selected_account(npub);
if (loadAccount) {
navigate({
to: "/$account/home/local",
to: "/$account/home",
params: { account: npub },
replace: true,
});
}
};
const weekday = new Date().toLocaleString("default", { weekday: "long" });
const day = new Date().getDate();
const month = new Date()
.toLocaleString("default", { month: "long" })
.toString();
const currentDate = new Date().toLocaleString("default", {
weekday: "long",
month: "long",
day: "numeric",
});
return (
<div className="relative flex h-full w-full items-center justify-center">
<div className="relative z-20 flex flex-col items-center gap-16">
<div className="text-center text-white">
<h2 className="mb-1 text-2xl">{`${weekday}, ${month} ${day}`}</h2>
<h2 className="mb-1 text-2xl">{currentDate}</h2>
<h2 className="text-2xl font-semibold">Welcome back!</h2>
</div>
<div className="flex items-center justify-center gap-6">

View File

@ -0,0 +1,14 @@
import { Outlet, createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute("/login")({
component: Screen,
});
function Screen() {
return (
<div>
<h1>Login</h1>
<Outlet />
</div>
);
}

View File

@ -0,0 +1,9 @@
import { createLazyFileRoute } from "@tanstack/react-router";
export const Route = createLazyFileRoute("/settings/")({
component: Screen,
});
function Screen() {
return <div>Settings</div>;
}

View File

@ -13,7 +13,7 @@
"@astrojs/check": "^0.4.1",
"@astrojs/tailwind": "^5.1.0",
"@fontsource/geist-mono": "^5.0.1",
"astro": "^4.4.4",
"astro": "^4.4.6",
"astro-seo-meta": "^4.1.0",
"astro-seo-schema": "^4.0.0",
"schema-dts": "^1.1.2",

View File

@ -14,9 +14,8 @@
"@radix-ui/react-hover-card": "^1.0.7",
"@radix-ui/react-popover": "^1.0.7",
"@radix-ui/react-tooltip": "^1.0.7",
"@tanstack/react-query": "^5.22.2",
"@tanstack/react-query": "^5.24.1",
"get-urls": "^12.1.0",
"jotai": "^2.6.5",
"media-chrome": "^2.2.5",
"minidenticons": "^4.2.0",
"nanoid": "^5.0.6",
@ -26,7 +25,7 @@
"react-currency-input-field": "^3.8.0",
"react-i18next": "^14.0.5",
"react-string-replace": "^1.1.1",
"sonner": "^1.4.1",
"sonner": "^1.4.2",
"string-strip-html": "^13.4.6",
"virtua": "^0.27.5"
},
@ -34,7 +33,7 @@
"@lume/tailwindcss": "workspace:^",
"@lume/tsconfig": "workspace:^",
"@lume/types": "workspace:^",
"@types/react": "^18.2.58",
"@types/react": "^18.2.60",
"tailwindcss": "^3.4.1",
"typescript": "^5.3.3"
}

View File

@ -50,6 +50,17 @@ export class Ark {
}
}
public async create_guest_account() {
try {
const keys = await this.create_keys();
await this.save_account(keys);
return keys.npub;
} catch (e) {
console.error(e);
}
}
public async create_keys() {
try {
const cmd: Keys = await invoke("create_keys");

View File

@ -8,7 +8,7 @@
},
"devDependencies": {
"@lume/tsconfig": "workspace:*",
"@types/react": "^18.2.58",
"@types/react": "^18.2.60",
"typescript": "^5.3.3"
}
}

View File

@ -1,24 +1,16 @@
import { SVGProps } from "react";
export function BellIcon(
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>,
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>,
) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
fill="none"
viewBox="0 0 24 24"
{...props}
>
<path
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M9.159 17.724a59.522 59.522 0 01-3.733-.297 1.587 1.587 0 01-1.33-2.08c.161-.485.324-.963.367-1.478l.355-4.26a7.207 7.207 0 0114.365 0l.355 4.262c.043.515.206.993.367 1.479a1.587 1.587 0 01-1.33 2.077 59.5 59.5 0 01-3.732.297m-5.684 0c1.893.09 3.79.09 5.684 0m-5.684 0v.434a2.842 2.842 0 105.684 0v-.434"
/>
</svg>
);
return (
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" {...props}>
<path
stroke="currentColor"
strokeLinejoin="round"
strokeWidth="2"
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>
);
}

View File

@ -1,23 +1,16 @@
import { SVGProps } from "react";
export function BellFilledIcon(
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>,
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>,
) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
fill="none"
viewBox="0 0 24 24"
{...props}
>
<path
fill="currentColor"
fillRule="evenodd"
d="M3.822 9.526a8.207 8.207 0 0116.358 0l.355 4.262c.03.374.15.735.32 1.246a2.587 2.587 0 01-2.17 3.387c-.957.106-1.916.19-2.876.25a3.843 3.843 0 01-7.616 0c-.96-.06-1.92-.143-2.877-.25a2.588 2.588 0 01-2.17-3.39c.17-.51.29-.872.32-1.245l.356-4.26zm6.44 9.24a1.843 1.843 0 003.478 0l-.294.008a60.587 60.587 0 01-3.184-.008z"
clipRule="evenodd"
/>
</svg>
);
return (
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" {...props}>
<path
fill="currentColor"
fillRule="evenodd"
d="M12 2a7.853 7.853 0 0 0-7.784 6.815l-.905 6.789A3 3 0 0 0 6.284 19h1.07c.904 1.748 2.607 3 4.646 3 2.039 0 3.742-1.252 4.646-3h1.07a3 3 0 0 0 2.973-3.396l-.905-6.789A7.853 7.853 0 0 0 12 2Zm2.222 17H9.778c.61.637 1.399 1 2.222 1s1.613-.363 2.222-1Z"
clipRule="evenodd"
/>
</svg>
);
}

View File

@ -1,21 +1,16 @@
import { SVGProps } from "react";
export function ComposeFilledIcon(
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>,
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>,
) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
fill="none"
viewBox="0 0 24 24"
{...props}
>
<path
fill="currentColor"
d="M15.27 2.637a1.51 1.51 0 01.831-.589c.771-.206 1.39.273 1.71.585.373.366.723.879 1.042 1.452.644 1.16 1.273 2.78 1.756 4.585.484 1.806.75 3.522.771 4.85.012.655-.035 1.274-.176 1.779-.12.43-.417 1.154-1.188 1.36a1.51 1.51 0 01-1.015-.093 26.427 26.427 0 00-6.508-.37l1.755 5.5A1 1 0 0113.296 23H8.92a1 1 0 01-.944-.67L6.135 17.07l-.812.124a1 1 0 01-.727-.172 6.126 6.126 0 01-2.03-7.576 1 1 0 01.544-.512l4.67-1.824a26.429 26.429 0 007.49-4.472zm4.093 11.584c.015-.189.022-.411.018-.668-.02-1.12-.249-2.67-.703-4.365-.455-1.696-1.03-3.152-1.574-4.132a6.928 6.928 0 00-.35-.57c-.026.325-.026.73.006 1.204.07 1.06.295 2.395.68 3.83.384 1.434.857 2.702 1.326 3.656.21.427.412.777.597 1.045zM9.629 21h2.298l-1.46-4.575-2.319.343L9.63 21z"
/>
</svg>
);
return (
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" {...props}>
<path
fill="currentColor"
fillRule="evenodd"
d="M19.367 2.814c.565.603.814 1.49.489 2.391-.614 1.705-1.793 3.098-2.765 4.04-.266.256-.52.484-.752.68.894.77 1.345 2.1.652 3.301-1.22 2.116-4.304 5.716-10.928 5.756A32 32 0 0 0 6 21a1 1 0 1 1-2 0c0-4.329.793-8.748 2.831-12.259 2.063-3.553 5.386-6.14 10.288-6.721a2.645 2.645 0 0 1 2.248.794Z"
clipRule="evenodd"
/>
</svg>
);
}

View File

@ -1,19 +1,12 @@
export function HomeIcon(props: JSX.IntrinsicElements["svg"]) {
return (
<svg
xmlns="http://www.w3.org/2000/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
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M3 12.759c0-1.017 0-1.526.119-2.002a4 4 0 01.513-1.19c.265-.414.634-.763 1.374-1.461l2.6-2.456c1.546-1.46 2.32-2.19 3.201-2.466a4 4 0 012.386 0c.882.275 1.655 1.006 3.201 2.466l2.6 2.456c.74.698 1.11 1.047 1.374 1.46a4 4 0 01.513 1.191c.119.476.119.985.119 2.002V14.6c0 2.24 0 3.36-.436 4.216a4 4 0 01-1.748 1.748C17.96 21 16.84 21 14.6 21H9.4c-2.24 0-3.36 0-4.216-.436a4 4 0 01-1.748-1.748C3 17.96 3 16.84 3 14.6v-1.841z"
d="m5 7.511 6.069-4.344c.335-.24.502-.36.685-.406a1 1 0 0 1 .492 0c.183.046.35.166.685.406L19 7.51m-14 0v9.29c0 1.12 0 1.68.218 2.108a2 2 0 0 0 .874.874C6.52 20 7.08 20 8.2 20h7.6c1.12 0 1.68 0 2.108-.218a2 2 0 0 0 .874-.874C19 18.48 19 17.92 19 16.8V7.511m-14 0-2.5 1.79M19 7.511l2.5 1.79"
/>
</svg>
);

View File

@ -1,16 +1,9 @@
export function HomeFilledIcon(props: JSX.IntrinsicElements["svg"]) {
return (
<svg
xmlns="http://www.w3.org/2000/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
fill="currentColor"
d="M13.49 2.23a5 5 0 00-2.98 0c-.61.19-1.136.525-1.68.963-.529.425-1.133.996-1.88 1.702L4.232 7.46c-.657.62-1.111 1.049-1.443 1.567a5 5 0 00-.642 1.488C2 11.113 2 11.737 2 12.64v2.003c0 1.084 0 1.958.058 2.666.06.729.185 1.369.487 1.96a5 5 0 002.185 2.186c.592.302 1.233.428 1.961.487C7.4 22 8.273 22 9.357 22h5.286c1.084 0 1.958 0 2.666-.058.729-.06 1.369-.185 1.961-.487a5 5 0 002.185-2.185c.302-.592.428-1.232.487-1.961C22 16.6 22 15.727 22 14.643V12.64c0-.903 0-1.527-.148-2.125a5.002 5.002 0 00-.642-1.488c-.332-.518-.786-.947-1.443-1.567l-2.716-2.565c-.748-.706-1.352-1.277-1.88-1.702-.545-.438-1.071-.773-1.68-.964z"
d="M12.492 1.791a2 2 0 0 0-.984 0c-.374.095-.695.327-.95.512l-.071.05-8.569 6.135a1 1 0 0 0 1.164 1.626L4 9.457v7.381c0 .528 0 .982.03 1.357.033.395.104.789.297 1.167a3 3 0 0 0 1.311 1.311c.378.193.772.264 1.167.296.375.031.83.031 1.357.031h7.677c.527 0 .982 0 1.356-.03.395-.033.789-.104 1.167-.297a3 3 0 0 0 1.311-1.311c.193-.378.264-.772.297-1.167.03-.375.03-.83.03-1.356V9.457l.918.657a1 1 0 1 0 1.164-1.626l-8.568-6.134-.071-.051c-.256-.185-.577-.417-.95-.512Z"
/>
</svg>
);

View File

@ -1,18 +1,13 @@
export function HorizontalDotsIcon(props: JSX.IntrinsicElements["svg"]) {
return (
<svg
{...props}
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
>
<path d="M6 12a1 1 0 11-2 0 1 1 0 012 0zM13 12a1 1 0 11-2 0 1 1 0 012 0zM20 12a1 1 0 11-2 0 1 1 0 012 0z" />
</svg>
);
return (
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" {...props}>
<path
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M12 13a1 1 0 1 0 0-2 1 1 0 0 0 0 2Zm8 0a1 1 0 1 0 0-2 1 1 0 0 0 0 2ZM4 13a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z"
/>
</svg>
);
}

View File

@ -4,21 +4,14 @@ export function LinkIcon(
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>,
) {
return (
<svg
xmlns="http://www.w3.org/2000/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
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M20.5 13.5c0 1.395 0 2.092-.138 2.667a5 5 0 01-3.695 3.695C16.092 20 15.394 20 14 20h-1.5c-2.8 0-4.2 0-5.27-.545a5 5 0 01-2.185-2.185C4.5 16.2 4.5 14.8 4.5 12v-.5c0-2.33 0-3.495.38-4.413A5 5 0 017.588 4.38C8.363 4.059 9.317 4.009 11 4m9.26 5.454c.262-1.633.31-3.285.142-4.914a.495.495 0 00-.142-.3m0 0a.496.496 0 00-.301-.143 18.815 18.815 0 00-4.913.142m5.214 0L10.5 14"
></path>
d="M9 6H7.2c-1.12 0-1.68 0-2.108.218a2 2 0 0 0-.874.874C4 7.52 4 8.08 4 9.2v7.6c0 1.12 0 1.68.218 2.108a2 2 0 0 0 .874.874C5.52 20 6.08 20 7.2 20h7.6c1.12 0 1.68 0 2.108-.218a2 2 0 0 0 .874-.874C18 18.48 18 17.92 18 16.8V15M14 4h6m0 0v6m0-6-9 9"
/>
</svg>
);
}

View File

@ -1,18 +1,12 @@
export function PlusIcon(props: JSX.IntrinsicElements['svg']) {
export function PlusIcon(props: JSX.IntrinsicElements["svg"]) {
return (
<svg
{...props}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="24"
height="24"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
>
<path d="M12 19v-7m0 0V5m0 7H5m7 0h7" />
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" {...props}>
<path
stroke="currentColor"
strokeLinecap="round"
strokeWidth="2"
d="M12 4v8m0 0v8m0-8H4m8 0h8"
/>
</svg>
);
}

View File

@ -1,20 +1,13 @@
export function ReplyIcon(props: JSX.IntrinsicElements["svg"]) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="25"
height="24"
fill="none"
viewBox="0 0 25 24"
{...props}
>
<path
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M21.5 12a9.002 9.002 0 01-4.682 7.897 9 9 0 01-5.59 1.013c-1.203-.17-1.805-.255-1.964-.267-.257-.02-.165-.016-.423-.014-.159 0-.34.014-.702.04l-2.153.153c-.857.062-1.286.092-1.607-.06a1.348 1.348 0 01-.641-.64c-.152-.32-.122-.75-.06-1.608l.153-2.153c.026-.362.04-.542.04-.702.002-.258.006-.166-.014-.423-.012-.159-.098-.76-.268-1.964A9 9 0 1121.5 12z"
/>
</svg>
);
return (
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" {...props}>
<path
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M12 21a9 9 0 1 0-9-9c0 1.354.3 2.639.835 3.791.102.219.133.465.076.7l-.778 3.191a1 1 0 0 0 1.191 1.213l3.33-.752c.224-.05.458-.02.667.073A8.969 8.969 0 0 0 12 21Z"
/>
</svg>
);
}

View File

@ -1,18 +1,13 @@
export function RepostIcon(props: JSX.IntrinsicElements["svg"]) {
return (
<svg
{...props}
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
>
<path d="M12 2a15.267 15.267 0 012.92 2.777c.054.066.08.145.08.225M12 8a15.266 15.266 0 002.92-2.777.356.356 0 00.08-.221M12 16a15.264 15.264 0 00-2.92 2.777.356.356 0 00-.08.221M12 22a15.264 15.264 0 01-2.92-2.777.355.355 0 01-.08-.225m6-13.996C14.7 5 14.368 5 14 5h-4c-1.861 0-2.792 0-3.545.245a5 5 0 00-3.21 3.21C3 9.208 3 10.139 3 12s0 2.792.245 3.545A5 5 0 005 18m4 .998c.3.002.632.002 1 .002h4c1.861 0 2.792 0 3.545-.245a5 5 0 003.21-3.21C21 14.792 21 13.861 21 12s0-2.792-.245-3.545A5 5 0 0019 6" />
</svg>
);
return (
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" {...props}>
<path
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="m17.25 21 2.47-2.47a.75.75 0 0 0 0-1.06L17.25 15M6.75 3 4.28 5.47a.75.75 0 0 0 0 1.06L6.75 9M5 6h13a2 2 0 0 1 2 2v3M4 13v3a2 2 0 0 0 2 2h13"
/>
</svg>
);
}

View File

@ -1,31 +1,22 @@
import { SVGProps } from "react";
export function SettingsIcon(
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>,
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>,
) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
fill="none"
viewBox="0 0 24 24"
{...props}
>
<path
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M11 12a1 1 0 112 0 1 1 0 01-2 0z"
/>
<path
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M8.562 4.32c1.252-.723 1.879-1.085 2.544-1.226a4.3 4.3 0 011.788 0c.665.141 1.292.503 2.544 1.225l1.499.865c1.252.723 1.878 1.084 2.334 1.59.403.447.707.974.894 1.546.21.647.21 1.37.21 2.816v1.728c0 1.446 0 2.169-.21 2.816-.186.572-.49 1.1-.894 1.547-.456.505-1.082.866-2.334 1.59l-1.499.864c-1.252.722-1.879 1.084-2.544 1.225a4.299 4.299 0 01-1.788 0c-.665-.141-1.292-.503-2.544-1.225l-1.499-.865c-1.252-.723-1.879-1.084-2.334-1.59a4.296 4.296 0 01-.894-1.546c-.21-.647-.21-1.37-.21-2.816v-1.728c0-1.446 0-2.169.21-2.816.186-.572.49-1.1.894-1.546.455-.506 1.082-.867 2.334-1.59l1.499-.865z"
/>
</svg>
);
return (
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" {...props}>
<path
stroke="currentColor"
stroke-linejoin="round"
stroke-width="2"
d="m7.99 5.398-.685-.158A1.722 1.722 0 0 0 5.24 7.305l.158.684a1.946 1.946 0 0 1-.817 2.057l-.832.555a1.682 1.682 0 0 0 0 2.798l.832.555c.673.449.999 1.268.817 2.057l-.158.684a1.722 1.722 0 0 0 2.065 2.065l.684-.158a1.946 1.946 0 0 1 2.057.817l.555.832a1.682 1.682 0 0 0 2.798 0l.555-.832a1.946 1.946 0 0 1 2.057-.817l.684.158a1.722 1.722 0 0 0 2.065-2.065l-.158-.684a1.946 1.946 0 0 1 .817-2.057l.832-.555a1.682 1.682 0 0 0 0-2.798l-.832-.555a1.946 1.946 0 0 1-.817-2.057l.158-.684a1.722 1.722 0 0 0-2.065-2.065l-.684.158a1.946 1.946 0 0 1-2.057-.817l-.555-.832a1.682 1.682 0 0 0-2.798 0l-.555.832a1.946 1.946 0 0 1-2.057.817Z"
/>
<path
stroke="currentColor"
stroke-linejoin="round"
stroke-width="2"
d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"
/>
</svg>
);
}

View File

@ -4,21 +4,11 @@ export function SpaceIcon(
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>,
) {
return (
<svg
xmlns="http://www.w3.org/2000/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
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M8 7v10m4-10v4m4-4v7m-5 7h2c2.8 0 4.2 0 5.27-.545a5 5 0 002.185-2.185C21 17.2 21 15.8 21 13v-2c0-2.8 0-4.2-.545-5.27a5 5 0 00-2.185-2.185C17.2 3 15.8 3 13 3h-2c-2.8 0-4.2 0-5.27.545A5 5 0 003.545 5.73C3 6.8 3 8.2 3 11v2c0 2.8 0 4.2.545 5.27a5 5 0 002.185 2.185C6.8 21 8.2 21 11 21z"
></path>
fill="currentColor"
d="m5.092 19.782.454-.891-.454.891Zm-.874-.874.891-.454-.891.454Zm15.564 0-.891-.454.891.454Zm-.874.874-.454-.891.454.891Zm.874-14.69-.891.454.891-.454Zm-.874-.874-.454.891.454-.891Zm-14.69.874-.891-.454.891.454Zm.874-.874-.454-.891.454.891ZM13 4V3h-2v1h2Zm-2 16v1h2v-1h-2Zm8-12.8v9.6h2V7.2h-2ZM16.8 19H7.2v2h9.6v-2ZM5 16.8V7.2H3v9.6h2ZM7.2 5h9.6V3H7.2v2Zm0 14c-.577 0-.949 0-1.232-.024-.272-.022-.373-.06-.422-.085l-.908 1.782c.378.193.772.264 1.167.296.384.032.851.031 1.395.031v-2ZM3 16.8c0 .543 0 1.011.03 1.395.033.395.104.789.297 1.167l1.782-.908c-.025-.05-.063-.15-.085-.422C5 17.75 5 17.377 5 16.8H3Zm2.546 2.091a1 1 0 0 1-.437-.437l-1.782.908a3 3 0 0 0 1.311 1.311l.908-1.782ZM19 16.8c0 .576 0 .949-.024 1.232-.022.272-.06.372-.085.422l1.782.908c.193-.378.264-.772.296-1.167.032-.384.031-.852.031-1.395h-2ZM16.8 21c.544 0 1.011 0 1.395-.03.395-.033.789-.104 1.167-.297l-.908-1.782c-.05.025-.15.063-.422.085C17.75 19 17.377 19 16.8 19v2Zm2.091-2.546a1 1 0 0 1-.437.437l.908 1.782a3 3 0 0 0 1.311-1.311l-1.782-.908ZM21 7.2c0-.544 0-1.011-.03-1.395-.033-.395-.104-.789-.297-1.167l-1.782.908c.025.05.063.15.085.422C19 6.25 19 6.623 19 7.2h2ZM16.8 5c.577 0 .949 0 1.232.024.272.022.372.06.422.085l.908-1.782c-.378-.193-.772-.264-1.167-.296C17.811 2.999 17.344 3 16.8 3v2Zm3.873-.362a3 3 0 0 0-1.311-1.311l-.908 1.782a1 1 0 0 1 .437.437l1.782-.908ZM5 7.2c0-.577 0-.949.024-1.232.022-.272.06-.373.085-.422l-1.782-.908c-.193.378-.264.772-.296 1.167C2.999 6.189 3 6.656 3 7.2h2ZM7.2 3c-.544 0-1.011 0-1.395.03-.395.033-.789.104-1.167.297l.908 1.782c.05-.025.15-.063.422-.085C6.25 5 6.623 5 7.2 5V3ZM5.109 5.546a1 1 0 0 1 .437-.437l-.908-1.782a3 3 0 0 0-1.311 1.311l1.782.908ZM11 4v16h2V4h-2Z"
/>
</svg>
);
}

View File

@ -4,18 +4,11 @@ export function SpaceFilledIcon(
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>,
) {
return (
<svg
xmlns="http://www.w3.org/2000/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
fill="currentColor"
d="M10.956 2h2.088c1.363 0 2.447 0 3.321.071.896.074 1.66.227 2.359.583a6 6 0 012.622 2.622c.356.7.51 1.463.583 2.359.071.874.071 1.958.071 3.321v2.088c0 1.363 0 2.447-.071 3.321-.074.896-.227 1.66-.583 2.359a6 6 0 01-2.622 2.622c-.7.356-1.463.51-2.359.583-.874.071-1.958.071-3.321.071h-2.088c-1.363 0-2.447 0-3.321-.071-.896-.074-1.66-.227-2.359-.583a6 6 0 01-2.622-2.622c-.356-.7-.51-1.463-.583-2.359C2 15.491 2 14.407 2 13.044v-2.088c0-1.363 0-2.447.071-3.321.074-.896.227-1.66.583-2.359a6 6 0 012.622-2.622c.7-.356 1.463-.51 2.359-.583C8.509 2 9.593 2 10.956 2zM9 7a1 1 0 00-2 0v10a1 1 0 102 0V7zm3-1a1 1 0 00-1 1v4a1 1 0 102 0V7a1 1 0 00-1-1zm5 1a1 1 0 10-2 0v7a1 1 0 102 0V7z"
></path>
d="M7.161 3H11v18H7.161c-.527 0-.981 0-1.356-.03-.395-.033-.789-.104-1.167-.297a3 3 0 0 1-1.311-1.311c-.193-.378-.264-.772-.296-1.167A17.9 17.9 0 0 1 3 16.839V7.16c0-.527 0-.981.03-1.356.033-.395.104-.789.297-1.167a3 3 0 0 1 1.311-1.311c.378-.193.772-.264 1.167-.296C6.18 3 6.635 3 7.161 3ZM13 21h3.839c.527 0 .982 0 1.356-.03.395-.033.789-.104 1.167-.297a3 3 0 0 0 1.311-1.311c.193-.378.264-.772.296-1.167.031-.375.031-.83.031-1.356V7.16c0-.527 0-.981-.03-1.356-.033-.395-.104-.789-.297-1.167a3 3 0 0 0-1.311-1.311c-.378-.193-.772-.264-1.167-.296A17.9 17.9 0 0 0 16.839 3H13v18Z"
/>
</svg>
);
}

View File

@ -1,18 +1,12 @@
export function ZapIcon(props: JSX.IntrinsicElements["svg"]) {
return (
<svg
{...props}
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
>
<path d="M6.054 8.38l5.756-4.513c1.638-1.284 2.457-1.926 3.018-1.863.485.055.912.366 1.136.828.26.533-.003 1.58-.53 3.673l-.44 1.753c-.211.838-.316 1.257-.244 1.62.064.32.221.611.45.83.259.249.652.361 1.438.585l.51.146c1.512.432 2.268.648 2.57 1.087.264.382.348.872.23 1.328-.137.526-.772 1.014-2.041 1.99l-5.652 4.342c-1.628 1.25-2.442 1.876-3 1.81a1.458 1.458 0 01-1.129-.831c-.257-.532.003-1.565.522-3.63l.394-1.568c.211-.838.316-1.257.244-1.621a1.575 1.575 0 00-.45-.83c-.259-.248-.652-.36-1.438-.585l-.568-.162c-1.496-.427-2.244-.64-2.546-1.077a1.631 1.631 0 01-.234-1.322c.132-.524.756-1.013 2.004-1.99z" />
</svg>
);
return (
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" {...props}>
<path
stroke="currentColor"
strokeLinejoin="round"
strokeWidth="2"
d="M19.566 9H13.5a.5.5 0 0 1-.5-.5V2.401a.5.5 0 0 0-.916-.277L4.018 14.223a.5.5 0 0 0 .416.777H10.5a.5.5 0 0 1 .5.5v6.099a.5.5 0 0 0 .916.277l8.066-12.099A.5.5 0 0 0 19.566 9Z"
/>
</svg>
);
}

View File

@ -8,16 +8,16 @@
"@lume/icons": "workspace:^",
"@lume/ui": "workspace:^",
"@lume/utils": "workspace:^",
"@tanstack/react-query": "^5.22.2",
"@tanstack/react-query": "^5.24.1",
"react": "^18.2.0",
"sonner": "^1.4.1",
"sonner": "^1.4.2",
"virtua": "^0.27.5"
},
"devDependencies": {
"@lume/tailwindcss": "workspace:^",
"@lume/tsconfig": "workspace:^",
"@lume/types": "workspace:^",
"@types/react": "^18.2.58",
"@types/react": "^18.2.60",
"tailwindcss": "^3.4.1",
"typescript": "^5.3.3"
}

View File

@ -8,16 +8,16 @@
"@lume/icons": "workspace:^",
"@lume/ui": "workspace:^",
"@lume/utils": "workspace:^",
"@tanstack/react-query": "^5.22.2",
"@tanstack/react-query": "^5.24.1",
"react": "^18.2.0",
"sonner": "^1.4.1",
"sonner": "^1.4.2",
"virtua": "^0.27.5"
},
"devDependencies": {
"@lume/tailwindcss": "workspace:^",
"@lume/tsconfig": "workspace:^",
"@lume/types": "workspace:^",
"@types/react": "^18.2.58",
"@types/react": "^18.2.60",
"tailwindcss": "^3.4.1",
"typescript": "^5.3.3"
}

View File

@ -8,16 +8,16 @@
"@lume/icons": "workspace:^",
"@lume/ui": "workspace:^",
"@lume/utils": "workspace:^",
"@tanstack/react-query": "^5.22.2",
"@tanstack/react-query": "^5.24.1",
"react": "^18.2.0",
"sonner": "^1.4.1",
"sonner": "^1.4.2",
"virtua": "^0.27.5"
},
"devDependencies": {
"@lume/tailwindcss": "workspace:^",
"@lume/tsconfig": "workspace:^",
"@lume/types": "workspace:^",
"@types/react": "^18.2.58",
"@types/react": "^18.2.60",
"tailwindcss": "^3.4.1",
"typescript": "^5.3.3"
}

View File

@ -8,16 +8,16 @@
"@lume/icons": "workspace:^",
"@lume/ui": "workspace:^",
"@lume/utils": "workspace:^",
"@tanstack/react-query": "^5.22.2",
"@tanstack/react-query": "^5.24.1",
"react": "^18.2.0",
"sonner": "^1.4.1",
"sonner": "^1.4.2",
"virtua": "^0.27.5"
},
"devDependencies": {
"@lume/tailwindcss": "workspace:^",
"@lume/tsconfig": "workspace:^",
"@lume/types": "workspace:^",
"@types/react": "^18.2.58",
"@types/react": "^18.2.60",
"tailwindcss": "^3.4.1",
"typescript": "^5.3.3"
}

View File

@ -8,16 +8,16 @@
"@lume/icons": "workspace:^",
"@lume/ui": "workspace:^",
"@lume/utils": "workspace:^",
"@tanstack/react-query": "^5.22.2",
"@tanstack/react-query": "^5.24.1",
"react": "^18.2.0",
"sonner": "^1.4.1",
"sonner": "^1.4.2",
"virtua": "^0.27.5"
},
"devDependencies": {
"@lume/tailwindcss": "workspace:^",
"@lume/tsconfig": "workspace:^",
"@lume/types": "workspace:^",
"@types/react": "^18.2.58",
"@types/react": "^18.2.60",
"tailwindcss": "^3.4.1",
"typescript": "^5.3.3"
}

View File

@ -8,16 +8,16 @@
"@lume/icons": "workspace:^",
"@lume/ui": "workspace:^",
"@lume/utils": "workspace:^",
"@tanstack/react-query": "^5.22.2",
"@tanstack/react-query": "^5.24.1",
"react": "^18.2.0",
"sonner": "^1.4.1",
"sonner": "^1.4.2",
"virtua": "^0.27.5"
},
"devDependencies": {
"@lume/tailwindcss": "workspace:^",
"@lume/tsconfig": "workspace:^",
"@lume/types": "workspace:^",
"@types/react": "^18.2.58",
"@types/react": "^18.2.60",
"tailwindcss": "^3.4.1",
"typescript": "^5.3.3"
}

View File

@ -8,16 +8,16 @@
"@lume/icons": "workspace:^",
"@lume/ui": "workspace:^",
"@lume/utils": "workspace:^",
"@tanstack/react-query": "^5.22.2",
"@tanstack/react-query": "^5.24.1",
"react": "^18.2.0",
"sonner": "^1.4.1",
"sonner": "^1.4.2",
"virtua": "^0.27.5"
},
"devDependencies": {
"@lume/tailwindcss": "workspace:^",
"@lume/tsconfig": "workspace:^",
"@lume/types": "workspace:^",
"@types/react": "^18.2.58",
"@types/react": "^18.2.60",
"tailwindcss": "^3.4.1",
"typescript": "^5.3.3"
}

View File

@ -8,16 +8,16 @@
"@lume/icons": "workspace:^",
"@lume/ui": "workspace:^",
"@lume/utils": "workspace:^",
"@tanstack/react-query": "^5.22.2",
"@tanstack/react-query": "^5.24.1",
"react": "^18.2.0",
"sonner": "^1.4.1",
"sonner": "^1.4.2",
"virtua": "^0.27.5"
},
"devDependencies": {
"@lume/tailwindcss": "workspace:^",
"@lume/tsconfig": "workspace:^",
"@lume/types": "workspace:^",
"@types/react": "^18.2.58",
"@types/react": "^18.2.60",
"tailwindcss": "^3.4.1",
"typescript": "^5.3.3"
}

View File

@ -8,16 +8,16 @@
"@lume/icons": "workspace:^",
"@lume/ui": "workspace:^",
"@lume/utils": "workspace:^",
"@tanstack/react-query": "^5.22.2",
"@tanstack/react-query": "^5.24.1",
"react": "^18.2.0",
"sonner": "^1.4.1",
"sonner": "^1.4.2",
"virtua": "^0.27.5"
},
"devDependencies": {
"@lume/tailwindcss": "workspace:^",
"@lume/tsconfig": "workspace:^",
"@lume/types": "workspace:^",
"@types/react": "^18.2.58",
"@types/react": "^18.2.60",
"tailwindcss": "^3.4.1",
"typescript": "^5.3.3"
}

View File

@ -8,16 +8,16 @@
"@lume/icons": "workspace:^",
"@lume/ui": "workspace:^",
"@lume/utils": "workspace:^",
"@tanstack/react-query": "^5.22.2",
"@tanstack/react-query": "^5.24.1",
"react": "^18.2.0",
"sonner": "^1.4.1",
"sonner": "^1.4.2",
"virtua": "^0.27.5"
},
"devDependencies": {
"@lume/tailwindcss": "workspace:^",
"@lume/tsconfig": "workspace:^",
"@lume/types": "workspace:^",
"@types/react": "^18.2.58",
"@types/react": "^18.2.60",
"tailwindcss": "^3.4.1",
"typescript": "^5.3.3"
}

View File

@ -8,14 +8,14 @@
"@lume/icons": "workspace:^",
"@lume/ui": "workspace:^",
"@lume/utils": "workspace:^",
"@tanstack/react-query": "^5.22.2",
"@tanstack/react-query": "^5.24.1",
"react": "^18.2.0"
},
"devDependencies": {
"@lume/tailwindcss": "workspace:^",
"@lume/tsconfig": "workspace:^",
"@lume/types": "workspace:^",
"@types/react": "^18.2.58",
"@types/react": "^18.2.60",
"tailwindcss": "^3.4.1",
"typescript": "^5.3.3"
}

View File

@ -18,11 +18,10 @@
"@radix-ui/react-hover-card": "^1.0.7",
"@radix-ui/react-popover": "^1.0.7",
"@radix-ui/react-tooltip": "^1.0.7",
"@tanstack/react-query": "^5.22.2",
"@tanstack/react-router": "^1.16.6",
"@tanstack/react-query": "^5.24.1",
"@tanstack/react-router": "^1.17.4",
"framer-motion": "^11.0.6",
"get-urls": "^12.1.0",
"jotai": "^2.6.5",
"media-chrome": "^2.2.5",
"minidenticons": "^4.2.0",
"nanoid": "^5.0.6",
@ -34,11 +33,11 @@
"react-hook-form": "^7.50.1",
"react-hotkeys-hook": "^4.5.0",
"react-i18next": "^14.0.5",
"react-router-dom": "^6.22.1",
"react-router-dom": "^6.22.2",
"react-string-replace": "^1.1.1",
"slate": "^0.101.5",
"slate-react": "^0.101.6",
"sonner": "^1.4.1",
"sonner": "^1.4.2",
"string-strip-html": "^13.4.6",
"uqr": "^0.1.2",
"use-debounce": "^10.0.0",
@ -48,7 +47,7 @@
"@lume/tailwindcss": "workspace:^",
"@lume/tsconfig": "workspace:^",
"@lume/types": "workspace:^",
"@types/react": "^18.2.58",
"@types/react": "^18.2.60",
"tailwindcss": "^3.4.1",
"typescript": "^5.3.3"
}

View File

@ -1,81 +0,0 @@
import { useArk, useProfile } from "@lume/ark";
import { SettingsIcon, UserIcon } from "@lume/icons";
import { cn, useNetworkStatus } from "@lume/utils";
import * as Avatar from "@radix-ui/react-avatar";
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
import { minidenticon } from "minidenticons";
import { useMemo } from "react";
import { useTranslation } from "react-i18next";
import { LogoutDialog } from "./logoutDialog";
export function ActiveAccount() {
const ark = useArk();
const isOnline = useNetworkStatus();
const svgURI = useMemo(
() =>
`data:image/svg+xml;utf8,${encodeURIComponent(
minidenticon(ark.account.npub, 90, 50),
)}`,
[],
);
const { t } = useTranslation();
const { profile } = useProfile(ark.account.npub);
return (
<DropdownMenu.Root>
<DropdownMenu.Trigger asChild>
<Avatar.Root
className={cn(
"rounded-full ring-1 ring-offset-2 ring-offset-neutral-200 dark:ring-offset-neutral-950",
isOnline ? "ring-teal-500" : "ring-red-500",
)}
>
<Avatar.Image
src={profile?.picture}
alt={ark.account.npub}
loading="lazy"
decoding="async"
style={{ contentVisibility: "auto" }}
className="aspect-square h-auto w-7 rounded-full object-cover"
/>
<Avatar.Fallback delayMs={150}>
<img
src={svgURI}
alt={ark.account.npub}
className="aspect-square h-auto w-7 rounded-full bg-black dark:bg-white"
/>
</Avatar.Fallback>
</Avatar.Root>
</DropdownMenu.Trigger>
<DropdownMenu.Portal>
<DropdownMenu.Content
side="left"
sideOffset={10}
className="relative top-2 flex w-[200px] flex-col overflow-hidden rounded-2xl bg-white/50 p-2 ring-1 ring-black/10 backdrop-blur-2xl focus:outline-none dark:bg-black/50 dark:ring-white/10"
>
<DropdownMenu.Item asChild>
<a
href="/settings/profile"
className="inline-flex h-9 items-center gap-3 rounded-lg px-3 text-sm font-medium text-black/70 hover:bg-black/10 hover:text-black focus:outline-none dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white"
>
<UserIcon className="size-4" />
{t("user.editProfile")}
</a>
</DropdownMenu.Item>
<DropdownMenu.Item asChild>
<a
href="/settings/"
className="inline-flex h-9 items-center gap-3 rounded-lg px-3 text-sm font-medium text-black/70 hover:bg-black/10 hover:text-black focus:outline-none dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white"
>
<SettingsIcon className="size-4" />
{t("user.settings")}
</a>
</DropdownMenu.Item>
<DropdownMenu.Separator className="my-1 h-px bg-black/10 dark:bg-white/10" />
<LogoutDialog />
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu.Root>
);
}

View File

@ -1,76 +0,0 @@
import { useArk } from "@lume/ark";
import { LogoutIcon } from "@lume/icons";
import * as AlertDialog from "@radix-ui/react-alert-dialog";
import { useQueryClient } from "@tanstack/react-query";
import { useNavigate } from "@tanstack/react-router";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
export function LogoutDialog() {
const ark = useArk();
const queryClient = useQueryClient();
const navigate = useNavigate();
const { t } = useTranslation();
const logout = async () => {
try {
// clear cache
queryClient.clear();
ark.account = null;
// redirect to welcome screen
navigate({ to: "/auth/" });
} catch (e) {
toast.error(String(e));
}
};
return (
<AlertDialog.Root>
<AlertDialog.Trigger asChild>
<button
type="button"
className="inline-flex h-9 items-center gap-3 rounded-lg px-3 text-sm font-medium text-black/70 hover:bg-black/10 hover:text-black focus:outline-none dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white"
>
<LogoutIcon className="size-4" />
{t("user.logout")}
</button>
</AlertDialog.Trigger>
<AlertDialog.Portal>
<AlertDialog.Overlay className="fixed inset-0 z-50 bg-black/20 backdrop-blur-sm dark:bg-black/20" />
<AlertDialog.Content className="fixed inset-0 z-50 flex min-h-full items-center justify-center">
<div className="relative h-min w-full max-w-md rounded-xl bg-neutral-100 dark:bg-neutral-900">
<div className="flex flex-col gap-1 border-b border-white/5 px-5 py-4">
<AlertDialog.Title className="text-lg font-semibold text-neutral-900 dark:text-neutral-100">
{t("user.logoutConfirmTitle")}
</AlertDialog.Title>
<AlertDialog.Description className="text-sm leading-tight text-neutral-600 dark:text-neutral-400">
{t("user.logoutConfirmSubtitle")}
</AlertDialog.Description>
</div>
<div className="flex justify-end gap-2 px-5 py-3">
<AlertDialog.Cancel asChild>
<button
type="button"
className="inline-flex h-9 items-center justify-center rounded-lg px-4 text-sm font-medium text-neutral-900 outline-none hover:bg-neutral-200 dark:text-neutral-100 dark:hover:bg-neutral-800"
>
{t("global.cancel")}
</button>
</AlertDialog.Cancel>
<AlertDialog.Action asChild>
<button
type="button"
onClick={() => logout()}
className="inline-flex h-9 items-center justify-center rounded-lg bg-red-500 px-4 text-sm font-medium text-white outline-none hover:bg-red-600"
>
{t("user.logout")}
</button>
</AlertDialog.Action>
</div>
</div>
</AlertDialog.Content>
</AlertDialog.Portal>
</AlertDialog.Root>
);
}

View File

@ -9,9 +9,14 @@ export function Box({
className?: string;
}) {
return (
<div className={cn("flex h-full min-h-0 w-full", className)}>
<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 overflow-hidden overflow-y-auto rounded-xl bg-white px-3 pt-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">
<div
className={cn(
"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",
className,
)}
>
{children}
</div>
</div>

View File

@ -3,28 +3,32 @@ import { cn } from "@lume/utils";
import { useTranslation } from "react-i18next";
export function EmptyFeed({
text,
subtext,
className,
}: { text?: string; subtext?: string; className?: string }) {
const { t } = useTranslation();
text,
subtext,
className,
}: {
text?: string;
subtext?: string;
className?: string;
}) {
const { t } = useTranslation();
return (
<div
className={cn(
"w-full py-5 flex items-center justify-center flex-col gap-2 rounded-xl bg-neutral-50 dark:bg-neutral-950",
className,
)}
>
<InfoIcon className="size-8 text-blue-500" />
<div className="text-center">
<p className="font-semibold text-lg">
{text ? text : t("global.emptyFeedTitle")}
</p>
<p className="leading-tight text-sm">
{subtext ? subtext : t("global.emptyFeedSubtitle")}
</p>
</div>
</div>
);
return (
<div
className={cn(
"flex w-full flex-col items-center justify-center gap-2 rounded-xl bg-neutral-50 py-3 dark:bg-neutral-950",
className,
)}
>
<InfoIcon className="size-8 text-blue-500" />
<div className="text-center">
<p className="text-lg font-semibold">
{text ? text : t("global.emptyFeedTitle")}
</p>
<p className="text-sm leading-tight">
{subtext ? subtext : t("global.emptyFeedSubtitle")}
</p>
</div>
</div>
);
}

View File

@ -1,4 +1,4 @@
import { ReplyIcon, ShareIcon } from "@lume/icons";
import { LinkIcon, ReplyIcon } from "@lume/icons";
import * as Tooltip from "@radix-ui/react-tooltip";
import { useTranslation } from "react-i18next";
import { useNoteContext } from "../provider";
@ -41,8 +41,8 @@ export function NoteReply() {
onClick={() => ark.open_thread(event.id)}
className="inline-flex h-9 items-center gap-2 rounded-lg px-3 text-sm font-medium text-white hover:bg-neutral-900 focus:outline-none dark:text-black dark:hover:bg-neutral-100"
>
<ShareIcon className="size-4" />
{t("note.buttons.view")}
<LinkIcon className="size-4" />
{t("note.buttons.open")}
</button>
</DropdownMenu.Item>
<DropdownMenu.Item asChild>

View File

@ -105,7 +105,7 @@ export function MentionNote({
}
return (
<div className="my-1 flex w-full cursor-default flex-col rounded-2xl border border-black/10 px-3 pt-1 dark:border-white/10">
<div className="my-1 flex w-full cursor-default flex-col rounded-xl border border-black/10 px-3 pt-1 dark:border-white/10">
<User.Provider pubkey={data.pubkey}>
<User.Root className="flex h-10 items-center gap-2">
<User.Avatar className="size-6 shrink-0 rounded-full object-cover" />

View File

@ -35,7 +35,10 @@ export function ImagePreview({ url }: { url: string }) {
return (
// biome-ignore lint/a11y/useKeyWithClickEvents: <explanation>
<div onClick={open} className="group relative my-1 rounded-2xl">
<div
onClick={open}
className="group relative my-1 overflow-hidden rounded-xl"
>
<img
src={url}
alt={url}
@ -43,7 +46,7 @@ export function ImagePreview({ url }: { url: string }) {
decoding="async"
style={{ contentVisibility: "auto" }}
onError={fallback}
className="h-auto w-full rounded-2xl object-cover"
className="h-auto w-full object-cover"
/>
<button
type="button"

View File

@ -9,4 +9,3 @@ export * from "./src/hooks/useNetworkStatus";
export * from "./src/hooks/useOpenGraph";
export * from "./src/cn";
export * from "./src/image";
export * from "./src/state";

View File

@ -8,10 +8,9 @@
"access": "public"
},
"dependencies": {
"@tanstack/react-query": "^5.22.2",
"@tanstack/react-query": "^5.24.1",
"clsx": "^2.1.0",
"dayjs": "^1.11.10",
"jotai": "^2.6.5",
"nostr-tools": "^2.3.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
@ -21,7 +20,7 @@
"devDependencies": {
"@lume/tsconfig": "workspace:^",
"@lume/types": "workspace:^",
"@types/react": "^18.2.58",
"@types/react": "^18.2.60",
"@types/react-dom": "^18.2.19",
"tailwind-merge": "^2.2.1",
"typescript": "^5.3.3"

View File

@ -1,27 +0,0 @@
import { atom } from "jotai";
import { atomWithStorage } from "jotai/utils";
// Editor
export const editorAtom = atom(false);
export const editorValueAtom = atom([
{
type: "paragraph",
children: [{ text: "" }],
},
]);
// Onboarding
export const onboardingAtom = atomWithStorage("onboarding", {
open: true,
newUser: false,
});
// Activity
export const activityAtom = atom(false);
export const activityUnreadAtom = atom(0);
// Tutorial
export const tutorialAtom = atomWithStorage("tutorial", true);
// Search
export const searchAtom = atom(false);

File diff suppressed because it is too large Load Diff

441
src-tauri/Cargo.lock generated

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -1 +1 @@
{"desktop-capability":{"identifier":"desktop-capability","description":"Capability for the desktop","remote":null,"local":true,"windows":["main","splash","editor","settings","event-*","user-*","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","store:allow-get","clipboard-manager:allow-write","clipboard-manager:allow-read","webview:allow-create-webview-window","webview:allow-create-webview","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/*"}]}],"platforms":["linux","macOS","windows"]}}
{"desktop-capability":{"identifier":"desktop-capability","description":"Capability for the desktop","local":true,"windows":["main","splash","editor","settings","event-*","user-*","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","store:allow-get","clipboard-manager:allow-write","clipboard-manager:allow-read","webview:allow-create-webview-window","webview:allow-create-webview","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/*"}]}],"platforms":["linux","macOS","windows"]}}

View File

@ -85,7 +85,7 @@
}
},
"platforms": {
"description": "Target platforms this capability applies. By default all platforms applies.",
"description": "Target platforms this capability applies. By default all platforms are affected by this capability.",
"default": [
"linux",
"macOS",

View File

@ -85,7 +85,7 @@
}
},
"platforms": {
"description": "Target platforms this capability applies. By default all platforms applies.",
"description": "Target platforms this capability applies. By default all platforms are affected by this capability.",
"default": [
"linux",
"macOS",

View File

@ -49,7 +49,9 @@
"pinTooltip": "Pin Note",
"repost": "Repost",
"quote": "Quote",
"viewProfile": "View profile"
"viewProfile": "View profile",
"reply": "Reply this note",
"open": "Open in new window"
},
"zap": {
"zap": "Zap",
@ -130,8 +132,8 @@
"providerMethod": "Managed by Provider",
"providerMethodDescription": "A 3rd party provider will handle your sign in keys for you."
},
"signupWithSelfManage": {
"title": "This is your new Account Key",
"backup": {
"title": "This is your new sign in key",
"subtitle": "Keep your key in safe place. If you lose this key, you will lose access to your account.",
"confirm1": "I understand the risk of lost private key.",
"confirm2": "I will make sure keep it safe and not sharing with anyone.",