wip: new design

This commit is contained in:
2024-02-14 08:29:24 +07:00
parent 4292def206
commit 60fd09000b
131 changed files with 2026 additions and 5853 deletions

View File

@@ -4,7 +4,6 @@
"private": true,
"main": "./src/index.ts",
"dependencies": {
"@dnd-kit/core": "^6.1.0",
"@lume/ark": "workspace:^",
"@lume/icons": "workspace:^",
"@lume/storage": "workspace:^",
@@ -22,7 +21,6 @@
"framer-motion": "^11.0.3",
"jotai": "^2.6.4",
"minidenticons": "^4.2.0",
"nostr-tools": "~1.17.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-hook-form": "^7.50.0",
@@ -41,7 +39,6 @@
"@lume/tsconfig": "workspace:^",
"@lume/types": "workspace:^",
"@types/react": "^18.2.52",
"tailwind-merge": "^2.2.1",
"tailwindcss": "^3.4.1",
"typescript": "^5.3.3"
}

View File

@@ -6,80 +6,80 @@ import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
import { minidenticon } from "minidenticons";
import { useMemo } from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import { Logout } from "./logout";
import { Link } from "@tanstack/react-router";
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 ark = useArk();
const isOnline = useNetworkStatus();
const svgURI = useMemo(
() =>
`data:image/svg+xml;utf8,${encodeURIComponent(
minidenticon(ark.account.npub, 90, 50),
)}`,
[],
);
const { t } = useTranslation();
const { user } = useProfile(ark.account.npub);
const { t } = useTranslation();
const { user } = useProfile(ark.account.npub);
return (
<DropdownMenu.Root>
<DropdownMenu.Trigger asChild>
<div className="relative">
<Avatar.Root>
<Avatar.Image
src={user?.picture}
alt={ark.account.npub}
loading="lazy"
decoding="async"
style={{ contentVisibility: "auto" }}
className="object-cover w-full h-auto aspect-square rounded-xl"
/>
<Avatar.Fallback delayMs={150}>
<img
src={svgURI}
alt={ark.account.npub}
className="w-full h-auto bg-black aspect-square rounded-xl dark:bg-white"
/>
</Avatar.Fallback>
</Avatar.Root>
<span
className={cn(
"absolute bottom-0 right-0 block h-2 w-2 rounded-full ring-2 ring-gray-1 dark:ring-graydark-1",
isOnline ? "bg-green-9" : "bg-red-9",
)}
/>
</div>
</DropdownMenu.Trigger>
<DropdownMenu.Portal>
<DropdownMenu.Content
side="right"
sideOffset={5}
className="relative top-5 flex w-[200px] p-2 flex-col overflow-hidden rounded-2xl bg-white/50 dark:bg-black/50 ring-1 ring-black/10 dark:ring-white/10 backdrop-blur-2xl focus:outline-none"
>
<DropdownMenu.Item asChild>
<Link
to="/settings/profile"
className="inline-flex items-center gap-3 px-3 text-sm font-medium rounded-lg h-9 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")}
</Link>
</DropdownMenu.Item>
<DropdownMenu.Item asChild>
<Link
to="/settings/"
className="inline-flex items-center gap-3 px-3 text-sm font-medium rounded-lg h-9 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")}
</Link>
</DropdownMenu.Item>
<DropdownMenu.Separator className="h-px my-1 bg-black/10 dark:bg-white/10" />
<Logout />
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu.Root>
);
return (
<DropdownMenu.Root>
<DropdownMenu.Trigger asChild>
<div className="relative">
<Avatar.Root>
<Avatar.Image
src={user?.picture}
alt={ark.account.npub}
loading="lazy"
decoding="async"
style={{ contentVisibility: "auto" }}
className="aspect-square h-auto w-full rounded-xl object-cover"
/>
<Avatar.Fallback delayMs={150}>
<img
src={svgURI}
alt={ark.account.npub}
className="aspect-square h-auto w-full rounded-xl bg-black dark:bg-white"
/>
</Avatar.Fallback>
</Avatar.Root>
<span
className={cn(
"absolute bottom-0 right-0 block size-3 rounded-full ring-2 ring-neutral-200 dark:ring-neutral-800",
isOnline ? "bg-teal-500" : "bg-red-500",
)}
/>
</div>
</DropdownMenu.Trigger>
<DropdownMenu.Portal>
<DropdownMenu.Content
side="right"
sideOffset={5}
className="relative top-5 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>
<Link
to="/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")}
</Link>
</DropdownMenu.Item>
<DropdownMenu.Item asChild>
<Link
to="/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")}
</Link>
</DropdownMenu.Item>
<DropdownMenu.Separator className="my-1 h-px bg-black/10 dark:bg-white/10" />
<Logout />
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu.Root>
);
}

View File

@@ -1,83 +1,76 @@
import { useArk } from "@lume/ark";
import { LogoutIcon } from "@lume/icons";
import { useStorage } from "@lume/storage";
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 { useNavigate } from "react-router-dom";
import { toast } from "sonner";
export function Logout() {
const ark = useArk();
const storage = useStorage();
const queryClient = useQueryClient();
const navigate = useNavigate();
const ark = useArk();
const queryClient = useQueryClient();
const navigate = useNavigate();
const { t } = useTranslation();
const { t } = useTranslation();
const logout = async () => {
try {
// logout
await storage.logout();
const logout = async () => {
try {
// clear cache
queryClient.clear();
ark.account = null;
// clear cache
queryClient.clear();
ark.account = null;
ark.ndk.signer = null;
ark.ndk.activeUser = null;
// redirect to welcome screen
navigate({ to: "/auth/" });
} catch (e) {
toast.error(String(e));
}
};
// redirect to welcome screen
navigate("/auth/");
} catch (e) {
toast.error(String(e));
}
};
return (
<AlertDialog.Root>
<AlertDialog.Trigger asChild>
<button
type="button"
className="inline-flex items-center gap-3 px-3 text-sm font-medium rounded-lg h-9 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>
);
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

@@ -1,14 +1,14 @@
import {
ArrowUpSquareIcon,
BellFilledIcon,
BellIcon,
HomeFilledIcon,
HomeIcon,
PlusIcon,
SearchFilledIcon,
SearchIcon,
SettingsFilledIcon,
SettingsIcon,
ArrowUpSquareIcon,
BellFilledIcon,
BellIcon,
HomeFilledIcon,
HomeIcon,
PlusIcon,
SearchFilledIcon,
SearchIcon,
SettingsFilledIcon,
SettingsIcon,
} from "@lume/icons";
import { cn, editorAtom, searchAtom } from "@lume/utils";
import { Link } from "@tanstack/react-router";
@@ -22,160 +22,160 @@ import { ActiveAccount } from "./account/active";
import { UnreadActivity } from "./unread";
export function Navigation() {
const [isEditorOpen, setIsEditorOpen] = useAtom(editorAtom);
const [search, setSearch] = useAtom(searchAtom);
const [update, setUpdate] = useState<Update>(null);
const [isEditorOpen, setIsEditorOpen] = useAtom(editorAtom);
const [search, setSearch] = useAtom(searchAtom);
const [update, setUpdate] = useState<Update>(null);
// shortcut for editor
useHotkeys("meta+n", () => setIsEditorOpen((state) => !state), []);
// shortcut for editor
useHotkeys("meta+n", () => setIsEditorOpen((state) => !state), []);
const installNewUpdate = async () => {
if (!update) return;
const installNewUpdate = async () => {
if (!update) return;
const yes = await confirm(update.body, {
title: `v${update.version} is available`,
type: "info",
});
const yes = await confirm(update.body, {
title: `v${update.version} is available`,
type: "info",
});
if (yes) {
await update.downloadAndInstall();
await relaunch();
}
};
if (yes) {
await update.downloadAndInstall();
await relaunch();
}
};
useEffect(() => {
async function checkNewUpdate() {
const newVersion = await check();
setUpdate(newVersion);
}
checkNewUpdate();
}, []);
useEffect(() => {
async function checkNewUpdate() {
const newVersion = await check();
setUpdate(newVersion);
}
checkNewUpdate();
}, []);
return (
<div
data-tauri-drag-region
className="flex flex-col justify-between w-20 h-full px-4 py-3 shrink-0"
>
<div className="flex flex-col flex-1">
<div className="flex flex-col gap-3">
<ActiveAccount />
<button
type="button"
onClick={() => setIsEditorOpen((state) => !state)}
className={cn(
"flex items-center justify-center h-auto w-full aspect-square rounded-xl text-gray-normal",
isEditorOpen
? "bg-blue-solid text-white"
: "bg-gray-4 hover:bg-blue-solid dark:bg-graydark-4",
)}
>
<PlusIcon className="size-5" />
</button>
</div>
<div className="w-2/3 h-px mx-auto my-5 bg-black/10 dark:bg-white/10" />
<div className="flex flex-col gap-2">
<Link
to="/app/space"
className="inline-flex flex-col items-center justify-center"
>
{({ isActive }) => (
<div
className={cn(
"inline-flex aspect-square h-auto w-full items-center justify-center rounded-xl",
isActive
? "bg-black/10 text-black dark:bg-white/10 dark:text-white"
: "text-black/50 dark:text-neutral-400",
)}
>
{isActive ? (
<HomeFilledIcon className="size-6" />
) : (
<HomeIcon className="size-6" />
)}
</div>
)}
</Link>
<Link
to="/app/activity"
className="inline-flex flex-col items-center justify-center"
>
{({ isActive }) => (
<div
className={cn(
"relative inline-flex aspect-square h-auto w-full items-center justify-center rounded-xl",
isActive
? "bg-black/10 text-black dark:bg-white/10 dark:text-white"
: "text-black/50 dark:text-neutral-400",
)}
>
{isActive ? (
<BellFilledIcon className="size-6" />
) : (
<BellIcon className="size-6" />
)}
<UnreadActivity />
</div>
)}
</Link>
</div>
</div>
<div className="flex flex-col gap-2">
{update ? (
<button
type="button"
onClick={installNewUpdate}
className="relative inline-flex flex-col items-center justify-center"
>
<span className="inline-flex items-center px-2 py-1 text-xs font-semibold rounded-full bg-teal-500/60 ring-teal-500/80 text-teal-50 dark:bg-teal-500/10 dark:text-teal-400 ring-1 ring-inset dark:ring-teal-500/20">
Update
</span>
<div className="inline-flex items-center justify-center w-full h-auto aspect-square rounded-xl text-black/50 dark:text-neutral-400">
<ArrowUpSquareIcon className="size-6" />
</div>
</button>
) : null}
<button
type="button"
onClick={() => setSearch((open) => !open)}
className="inline-flex flex-col items-center justify-center"
>
<div
className={cn(
"inline-flex aspect-square h-auto w-full items-center justify-center rounded-xl",
search
? "bg-black/10 text-black dark:bg-white/10 dark:text-white"
: "text-black/50 dark:text-neutral-400",
)}
>
{search ? (
<SearchFilledIcon className="size-6" />
) : (
<SearchIcon className="size-6" />
)}
</div>
</button>
<Link
to="/settings"
className="inline-flex flex-col items-center justify-center"
>
{({ isActive }) => (
<div
className={cn(
"inline-flex aspect-square h-auto w-full items-center justify-center rounded-xl",
isActive
? "bg-black/10 text-black dark:bg-white/10 dark:text-white"
: "text-black/50 dark:text-neutral-400",
)}
>
{isActive ? (
<SettingsFilledIcon className="size-6" />
) : (
<SettingsIcon className="size-6" />
)}
</div>
)}
</Link>
</div>
</div>
);
return (
<div
data-tauri-drag-region
className="flex h-full w-20 shrink-0 flex-col justify-between px-4 py-3"
>
<div className="flex flex-1 flex-col">
<div className="flex flex-col gap-3">
<ActiveAccount />
<button
type="button"
onClick={() => setIsEditorOpen((state) => !state)}
className={cn(
"inline-flex aspect-square h-auto w-full items-center justify-center rounded-xl",
isEditorOpen
? "bg-blue-500 text-white"
: "bg-neutral-300 hover:bg-neutral-400 dark:bg-neutral-700 hover:dark:bg-neutral-600",
)}
>
<PlusIcon className="size-5" />
</button>
</div>
<div className="mx-auto my-5 h-px w-2/3 bg-black/10 dark:bg-white/10" />
<div className="flex flex-col gap-2">
<Link
to="/app/space"
className="inline-flex flex-col items-center justify-center"
>
{({ isActive }) => (
<div
className={cn(
"inline-flex aspect-square h-auto w-full items-center justify-center rounded-xl",
isActive
? "bg-neutral-300 hover:bg-neutral-400 dark:bg-neutral-700 hover:dark:bg-neutral-600"
: "text-neutral-600 dark:text-neutral-400",
)}
>
{isActive ? (
<HomeFilledIcon className="size-6" />
) : (
<HomeIcon className="size-6" />
)}
</div>
)}
</Link>
<Link
to="/app/activity"
className="inline-flex flex-col items-center justify-center"
>
{({ isActive }) => (
<div
className={cn(
"relative inline-flex aspect-square h-auto w-full items-center justify-center rounded-xl",
isActive
? "bg-neutral-300 hover:bg-neutral-400 dark:bg-neutral-700 hover:dark:bg-neutral-600"
: "text-neutral-600 dark:text-neutral-400",
)}
>
{isActive ? (
<BellFilledIcon className="size-6" />
) : (
<BellIcon className="size-6" />
)}
<UnreadActivity />
</div>
)}
</Link>
</div>
</div>
<div className="flex flex-col gap-2">
{update ? (
<button
type="button"
onClick={installNewUpdate}
className="relative inline-flex flex-col items-center justify-center"
>
<span className="inline-flex items-center rounded-full bg-teal-500/60 px-2 py-1 text-xs font-semibold text-teal-50 ring-1 ring-inset ring-teal-500/80 dark:bg-teal-500/10 dark:text-teal-400 dark:ring-teal-500/20">
Update
</span>
<div className="inline-flex aspect-square h-auto w-full items-center justify-center rounded-xl text-black/50 dark:text-neutral-400">
<ArrowUpSquareIcon className="size-6" />
</div>
</button>
) : null}
<button
type="button"
onClick={() => setSearch((open) => !open)}
className="inline-flex flex-col items-center justify-center"
>
<div
className={cn(
"inline-flex aspect-square h-auto w-full items-center justify-center rounded-xl",
search
? "bg-neutral-300 hover:bg-neutral-400 dark:bg-neutral-700 hover:dark:bg-neutral-600"
: "text-neutral-600 dark:text-neutral-400",
)}
>
{search ? (
<SearchFilledIcon className="size-6" />
) : (
<SearchIcon className="size-6" />
)}
</div>
</button>
<Link
to="/settings"
className="inline-flex flex-col items-center justify-center"
>
{({ isActive }) => (
<div
className={cn(
"inline-flex aspect-square h-auto w-full items-center justify-center rounded-xl",
isActive
? "bg-neutral-300 hover:bg-neutral-400 dark:bg-neutral-700 hover:dark:bg-neutral-600"
: "text-neutral-600 dark:text-neutral-400",
)}
>
{isActive ? (
<SettingsFilledIcon className="size-6" />
) : (
<SettingsIcon className="size-6" />
)}
</div>
)}
</Link>
</div>
</div>
);
}

View File

@@ -53,10 +53,10 @@ export const User = memo(function User({
<Avatar.Image
src={fallbackAvatar}
alt={pubkey}
className="h-6 w-6 rounded-md bg-black dark:bg-white"
className="w-6 h-6 bg-black rounded-md dark:bg-white"
/>
</Avatar.Root>
<div className="flex flex-1 items-baseline gap-2">
<div className="flex items-baseline flex-1 gap-2">
<h5 className="max-w-[10rem] truncate font-semibold text-neutral-900 dark:text-neutral-100">
{fallbackName}
</h5>
@@ -70,24 +70,24 @@ export const User = memo(function User({
}
return (
<div className="flex h-6 items-center gap-2">
<div className="flex items-center h-6 gap-2">
<Avatar.Root className="shrink-0">
<Avatar.Image
src={user?.picture || user?.image}
alt={pubkey}
loading="lazy"
decoding="async"
className="h-6 w-6 rounded-md"
className="w-6 h-6 rounded-md"
/>
<Avatar.Fallback delayMs={300}>
<img
src={fallbackAvatar}
alt={pubkey}
className="h-6 w-6 rounded-md bg-black dark:bg-white"
className="w-6 h-6 bg-black rounded-md dark:bg-white"
/>
</Avatar.Fallback>
</Avatar.Root>
<div className="flex flex-1 items-baseline gap-2">
<div className="flex items-baseline flex-1 gap-2">
<h5 className="max-w-[10rem] truncate font-semibold text-neutral-900 dark:text-neutral-100">
{user?.name ||
user?.display_name ||
@@ -107,11 +107,11 @@ export const User = memo(function User({
if (isLoading) {
return (
<div className="flex items-center gap-2">
<Avatar.Root className="h-8 w-8 shrink-0">
<Avatar.Root className="w-8 h-8 shrink-0">
<Avatar.Image
src={fallbackAvatar}
alt={pubkey}
className="h-8 w-8 rounded-md bg-black dark:bg-white"
className="w-8 h-8 bg-black rounded-md dark:bg-white"
/>
</Avatar.Root>
<h5 className="max-w-[10rem] truncate font-semibold text-neutral-900 dark:text-neutral-100">
@@ -123,19 +123,19 @@ export const User = memo(function User({
return (
<div className="flex items-center gap-2">
<Avatar.Root className="h-8 w-8 shrink-0">
<Avatar.Root className="w-8 h-8 shrink-0">
<Avatar.Image
src={user?.picture || user?.image}
alt={pubkey}
loading="eager"
decoding="async"
className="h-8 w-8 rounded-md"
className="w-8 h-8 rounded-md"
/>
<Avatar.Fallback delayMs={300}>
<img
src={fallbackAvatar}
alt={pubkey}
className="h-8 w-8 rounded-md bg-black dark:bg-white"
className="w-8 h-8 bg-black rounded-md dark:bg-white"
/>
</Avatar.Fallback>
</Avatar.Root>
@@ -156,11 +156,11 @@ export const User = memo(function User({
if (isLoading) {
return (
<div className="flex items-center gap-2">
<Avatar.Root className="h-8 w-8 shrink-0">
<Avatar.Root className="w-8 h-8 shrink-0">
<Avatar.Image
src={fallbackAvatar}
alt={pubkey}
className="h-8 w-8 rounded-md bg-black dark:bg-white"
className="w-8 h-8 bg-black rounded-md dark:bg-white"
/>
</Avatar.Root>
<h5 className="max-w-[10rem] truncate font-semibold text-neutral-900 dark:text-neutral-100">
@@ -172,27 +172,24 @@ export const User = memo(function User({
return (
<div className="flex items-center gap-2">
<Avatar.Root className="h-8 w-8 shrink-0">
<Avatar.Root className="w-8 h-8 shrink-0">
<Avatar.Image
src={user?.picture || user?.image}
alt={pubkey}
loading="eager"
decoding="async"
className="h-8 w-8 rounded-md"
className="w-8 h-8 rounded-md"
/>
<Avatar.Fallback delayMs={300}>
<img
src={fallbackAvatar}
alt={pubkey}
className="h-8 w-8 rounded-md bg-black dark:bg-white"
className="w-8 h-8 bg-black rounded-md dark:bg-white"
/>
</Avatar.Fallback>
</Avatar.Root>
<h5 className="max-w-[10rem] truncate font-semibold text-neutral-900 dark:text-neutral-100">
{user?.name ||
user?.display_name ||
user?.displayName ||
fallbackName}
{user?.name || user?.display_name || fallbackName}
</h5>
</div>
);
@@ -202,10 +199,10 @@ export const User = memo(function User({
if (isLoading) {
return (
<div className="flex items-center gap-2.5">
<div className="h-14 w-14 shrink-0 animate-pulse rounded-lg bg-neutral-300 dark:bg-neutral-700" />
<div className="rounded-lg h-14 w-14 shrink-0 animate-pulse bg-neutral-300 dark:bg-neutral-700" />
<div className="flex flex-col gap-1.5">
<div className="h-3.5 w-36 animate-pulse rounded bg-neutral-300 dark:bg-neutral-700" />
<div className="h-4 w-24 animate-pulse rounded bg-neutral-300 dark:bg-neutral-700" />
<div className="w-24 h-4 rounded animate-pulse bg-neutral-300 dark:bg-neutral-700" />
</div>
</div>
);
@@ -213,37 +210,37 @@ export const User = memo(function User({
return (
<div>
<div className="h-20 bg-gray-200 dark:bg-gray-800 rounded-t-lg">
<div className="h-20 bg-gray-200 rounded-t-lg dark:bg-gray-800">
{user?.banner ? (
<img
src={user.banner}
alt="banner"
className="w-full h-full object-cover"
className="object-cover w-full h-full"
/>
) : null}
</div>
<div className="flex h-full w-full flex-col gap-2.5 px-3 -mt-6">
<Avatar.Root className="shrink-0">
<Avatar.Image
src={user?.picture || user?.image}
src={user?.picture}
alt={pubkey}
decoding="async"
className="size-11 rounded-lg object-cover"
className="object-cover rounded-lg size-11"
/>
<Avatar.Fallback delayMs={300}>
<img
src={fallbackAvatar}
alt={pubkey}
className="size-11 rounded-lg bg-black dark:bg-white"
className="bg-black rounded-lg size-11 dark:bg-white"
/>
</Avatar.Fallback>
</Avatar.Root>
<div className="flex flex-col items-start text-start">
<p className="max-w-[15rem] truncate text-lg font-semibold leadning-tight">
{user?.name || user?.display_name || user?.displayName}
{user?.name || user?.display_name}
</p>
<p className="break-p text-neutral-700 dark:text-neutral-600 max-w-none select-text whitespace-pre-line">
{user?.about || user?.bio || "No bio"}
<p className="whitespace-pre-line select-text break-p text-neutral-700 dark:text-neutral-600 max-w-none">
{user?.about || "No bio"}
</p>
</div>
</div>
@@ -255,10 +252,10 @@ export const User = memo(function User({
if (isLoading) {
return (
<div className="flex items-center gap-2.5">
<div className="h-10 w-10 shrink-0 animate-pulse rounded-lg bg-neutral-300 dark:bg-neutral-700" />
<div className="flex w-full flex-col items-start gap-1">
<div className="h-4 w-36 animate-pulse rounded bg-neutral-300 dark:bg-neutral-700" />
<div className="h-4 w-24 animate-pulse rounded bg-neutral-300 dark:bg-neutral-700" />
<div className="w-10 h-10 rounded-lg shrink-0 animate-pulse bg-neutral-300 dark:bg-neutral-700" />
<div className="flex flex-col items-start w-full gap-1">
<div className="h-4 rounded w-36 animate-pulse bg-neutral-300 dark:bg-neutral-700" />
<div className="w-24 h-4 rounded animate-pulse bg-neutral-300 dark:bg-neutral-700" />
</div>
</div>
);
@@ -266,23 +263,23 @@ export const User = memo(function User({
return (
<div className="flex items-center gap-2.5">
<Avatar.Root className="h-10 w-10 shrink-0">
<Avatar.Root className="w-10 h-10 shrink-0">
<Avatar.Image
src={user?.picture || user?.image}
alt={pubkey}
loading="lazy"
decoding="async"
className="h-10 w-10 rounded-lg object-cover"
className="object-cover w-10 h-10 rounded-lg"
/>
<Avatar.Fallback delayMs={300}>
<img
src={fallbackAvatar}
alt={pubkey}
className="h-10 w-10 rounded-lg bg-black dark:bg-white"
className="w-10 h-10 bg-black rounded-lg dark:bg-white"
/>
</Avatar.Fallback>
</Avatar.Root>
<div className="flex w-full flex-col items-start">
<div className="flex flex-col items-start w-full">
<h3 className="max-w-[15rem] truncate text-base font-semibold text-neutral-900 dark:text-neutral-100">
{user?.name || user?.display_name || user?.displayName}
</h3>
@@ -297,7 +294,7 @@ export const User = memo(function User({
if (variant === "avatar") {
if (isLoading) {
return (
<div className="h-12 w-12 animate-pulse rounded-lg bg-neutral-300 dark:bg-neutral-700" />
<div className="w-12 h-12 rounded-lg animate-pulse bg-neutral-300 dark:bg-neutral-700" />
);
}
@@ -308,13 +305,13 @@ export const User = memo(function User({
alt={pubkey}
loading="lazy"
decoding="async"
className="h-12 w-12 rounded-lg"
className="w-12 h-12 rounded-lg"
/>
<Avatar.Fallback delayMs={300}>
<img
src={fallbackAvatar}
alt={pubkey}
className="h-12 w-12 rounded-lg bg-black dark:bg-white"
className="w-12 h-12 bg-black rounded-lg dark:bg-white"
/>
</Avatar.Fallback>
</Avatar.Root>
@@ -324,24 +321,24 @@ export const User = memo(function User({
if (variant === "miniavatar") {
if (isLoading) {
return (
<div className="h-10 w-10 shrink-0 animate-pulse rounded-lg bg-neutral-300 dark:bg-neutral-700" />
<div className="w-10 h-10 rounded-lg shrink-0 animate-pulse bg-neutral-300 dark:bg-neutral-700" />
);
}
return (
<Avatar.Root className="h-10 w-10 shrink-0">
<Avatar.Root className="w-10 h-10 shrink-0">
<Avatar.Image
src={user?.picture || user?.image}
alt={pubkey}
loading="lazy"
decoding="async"
className="h-10 w-10 rounded-lg"
className="w-10 h-10 rounded-lg"
/>
<Avatar.Fallback delayMs={300}>
<img
src={fallbackAvatar}
alt={pubkey}
className="h-10 w-10 rounded-lg bg-black dark:bg-white"
className="w-10 h-10 bg-black rounded-lg dark:bg-white"
/>
</Avatar.Fallback>
</Avatar.Root>
@@ -352,11 +349,11 @@ export const User = memo(function User({
if (isLoading) {
return (
<>
<Avatar.Root className="h-10 w-10 shrink-0">
<Avatar.Root className="w-10 h-10 shrink-0">
<Avatar.Image
src={fallbackAvatar}
alt={pubkey}
className="h-10 w-10 rounded-lg bg-black object-cover dark:bg-white"
className="object-cover w-10 h-10 bg-black rounded-lg dark:bg-white"
/>
</Avatar.Root>
<div className="absolute left-2 top-2 inline-flex items-center gap-1.5 font-semibold leading-tight">
@@ -371,19 +368,19 @@ export const User = memo(function User({
return (
<>
<Avatar.Root className="h-10 w-10 shrink-0">
<Avatar.Root className="w-10 h-10 shrink-0">
<Avatar.Image
src={user?.picture || user?.image}
alt={pubkey}
loading="lazy"
decoding="async"
className="h-10 w-10 rounded-lg object-cover"
className="object-cover w-10 h-10 rounded-lg"
/>
<Avatar.Fallback delayMs={300}>
<img
src={fallbackAvatar}
alt={pubkey}
className="h-10 w-10 rounded-lg bg-black dark:bg-white"
className="w-10 h-10 bg-black rounded-lg dark:bg-white"
/>
</Avatar.Fallback>
</Avatar.Root>
@@ -405,7 +402,7 @@ export const User = memo(function User({
if (variant === "stacked") {
if (isLoading) {
return (
<div className="inline-block h-8 w-8 animate-pulse rounded-full bg-neutral-300 ring-1 ring-neutral-200 dark:bg-neutral-700 dark:ring-neutral-800" />
<div className="inline-block w-8 h-8 rounded-full animate-pulse bg-neutral-300 ring-1 ring-neutral-200 dark:bg-neutral-700 dark:ring-neutral-800" />
);
}
@@ -416,13 +413,13 @@ export const User = memo(function User({
alt={pubkey}
loading="lazy"
decoding="async"
className="inline-block h-8 w-8 rounded-full ring-1 ring-neutral-200 dark:ring-neutral-800"
className="inline-block w-8 h-8 rounded-full ring-1 ring-neutral-200 dark:ring-neutral-800"
/>
<Avatar.Fallback delayMs={300}>
<img
src={fallbackAvatar}
alt={pubkey}
className="inline-block h-8 w-8 rounded-full bg-black ring-1 ring-neutral-200 dark:bg-white dark:ring-neutral-800"
className="inline-block w-8 h-8 bg-black rounded-full ring-1 ring-neutral-200 dark:bg-white dark:ring-neutral-800"
/>
</Avatar.Fallback>
</Avatar.Root>
@@ -432,7 +429,7 @@ export const User = memo(function User({
if (variant === "ministacked") {
if (isLoading) {
return (
<div className="inline-block h-6 w-6 animate-pulse rounded-full bg-neutral-300 ring-1 ring-white dark:ring-black" />
<div className="inline-block w-6 h-6 rounded-full animate-pulse bg-neutral-300 ring-1 ring-white dark:ring-black" />
);
}
@@ -443,13 +440,13 @@ export const User = memo(function User({
alt={pubkey}
loading="lazy"
decoding="async"
className="inline-block h-6 w-6 rounded-full ring-1 ring-white dark:ring-black"
className="inline-block w-6 h-6 rounded-full ring-1 ring-white dark:ring-black"
/>
<Avatar.Fallback delayMs={300}>
<img
src={fallbackAvatar}
alt={pubkey}
className="inline-block h-6 w-6 rounded-full bg-black ring-1 ring-white dark:bg-white dark:ring-black"
className="inline-block w-6 h-6 bg-black rounded-full ring-1 ring-white dark:bg-white dark:ring-black"
/>
</Avatar.Fallback>
</Avatar.Root>
@@ -460,12 +457,12 @@ export const User = memo(function User({
if (isLoading) {
return (
<div className="flex gap-3">
<div className="inline-flex h-10 w-10 items-center justify-center">
<RepostIcon className="h-5 w-5 text-blue-500" />
<div className="inline-flex items-center justify-center w-10 h-10">
<RepostIcon className="w-5 h-5 text-blue-500" />
</div>
<div className="inline-flex items-center gap-2">
<div className="h-6 w-6 animate-pulse rounded bg-neutral-300 dark:bg-neutral-700" />
<div className="h-4 w-24 animate-pulse rounded bg-neutral-300 dark:bg-neutral-700" />
<div className="w-6 h-6 rounded animate-pulse bg-neutral-300 dark:bg-neutral-700" />
<div className="w-24 h-4 rounded animate-pulse bg-neutral-300 dark:bg-neutral-700" />
</div>
</div>
);
@@ -473,8 +470,8 @@ export const User = memo(function User({
return (
<div className="flex gap-2 px-3">
<div className="inline-flex w-10 items-center justify-center">
<RepostIcon className="h-5 w-5 text-blue-500" />
<div className="inline-flex items-center justify-center w-10">
<RepostIcon className="w-5 h-5 text-blue-500" />
</div>
<div className="inline-flex items-center gap-2">
<Avatar.Root className="shrink-0">
@@ -483,13 +480,13 @@ export const User = memo(function User({
alt={pubkey}
loading="lazy"
decoding="async"
className="h-6 w-6 rounded object-cover"
className="object-cover w-6 h-6 rounded"
/>
<Avatar.Fallback delayMs={300}>
<img
src={fallbackAvatar}
alt={pubkey}
className="h-6 w-6 rounded bg-black dark:bg-white"
className="w-6 h-6 bg-black rounded dark:bg-white"
/>
</Avatar.Fallback>
</Avatar.Root>
@@ -510,35 +507,35 @@ export const User = memo(function User({
if (variant === "thread") {
if (isLoading) {
return (
<div className="flex h-16 items-center gap-3 px-3">
<div className="h-10 w-10 shrink-0 animate-pulse rounded-lg bg-neutral-300 dark:bg-neutral-700" />
<div className="flex flex-1 flex-col gap-1">
<div className="h-4 w-36 animate-pulse rounded bg-neutral-300 dark:bg-neutral-700" />
<div className="h-3 w-24 animate-pulse rounded bg-neutral-300 dark:bg-neutral-700" />
<div className="flex items-center h-16 gap-3 px-3">
<div className="w-10 h-10 rounded-lg shrink-0 animate-pulse bg-neutral-300 dark:bg-neutral-700" />
<div className="flex flex-col flex-1 gap-1">
<div className="h-4 rounded w-36 animate-pulse bg-neutral-300 dark:bg-neutral-700" />
<div className="w-24 h-3 rounded animate-pulse bg-neutral-300 dark:bg-neutral-700" />
</div>
</div>
);
}
return (
<div className="flex h-16 items-center gap-3 px-3">
<Avatar.Root className="h-10 w-10 shrink-0">
<div className="flex items-center h-16 gap-3 px-3">
<Avatar.Root className="w-10 h-10 shrink-0">
<Avatar.Image
src={user?.picture || user?.image}
alt={pubkey}
loading="lazy"
decoding="async"
className="h-10 w-10 rounded-lg object-cover ring-1 ring-neutral-200/50 dark:ring-neutral-800/50"
className="object-cover w-10 h-10 rounded-lg ring-1 ring-neutral-200/50 dark:ring-neutral-800/50"
/>
<Avatar.Fallback delayMs={300}>
<img
src={fallbackAvatar}
alt={pubkey}
className="h-10 w-10 rounded-lg bg-black ring-1 ring-neutral-200/50 dark:bg-white dark:ring-neutral-800/50"
className="w-10 h-10 bg-black rounded-lg ring-1 ring-neutral-200/50 dark:bg-white dark:ring-neutral-800/50"
/>
</Avatar.Fallback>
</Avatar.Root>
<div className="flex flex-1 flex-col">
<div className="flex flex-col flex-1">
<h5 className="max-w-[15rem] truncate font-semibold text-neutral-900 dark:text-neutral-100">
{user?.name || user?.display_name || user?.displayName || "Anon"}
</h5>
@@ -559,10 +556,10 @@ export const User = memo(function User({
<Avatar.Image
src={fallbackAvatar}
alt={pubkey}
className="h-9 w-9 rounded-lg bg-black ring-1 ring-neutral-200/50 dark:bg-white dark:ring-neutral-800/50"
className="bg-black rounded-lg h-9 w-9 ring-1 ring-neutral-200/50 dark:bg-white dark:ring-neutral-800/50"
/>
</Avatar.Root>
<div className="h-6 flex-1">
<div className="flex-1 h-6">
<div className="max-w-[15rem] truncate font-semibold text-neutral-950 dark:text-neutral-50">
{fallbackName}
</div>
@@ -579,24 +576,24 @@ export const User = memo(function User({
alt={pubkey}
loading="lazy"
decoding="async"
className="h-9 w-9 rounded-lg bg-white object-cover ring-1 ring-neutral-200/50 dark:ring-neutral-800/50"
className="object-cover bg-white rounded-lg h-9 w-9 ring-1 ring-neutral-200/50 dark:ring-neutral-800/50"
/>
<Avatar.Fallback delayMs={300}>
<img
src={fallbackAvatar}
alt={pubkey}
className="h-9 w-9 rounded-lg bg-black ring-1 ring-neutral-200/50 dark:bg-white dark:ring-neutral-800/50"
className="bg-black rounded-lg h-9 w-9 ring-1 ring-neutral-200/50 dark:bg-white dark:ring-neutral-800/50"
/>
</Avatar.Fallback>
</Avatar.Root>
<div className="flex h-6 flex-1 items-start gap-2">
<div className="flex items-start flex-1 h-6 gap-2">
<div className="max-w-[15rem] truncate font-semibold text-neutral-950 dark:text-neutral-50">
{user?.name ||
user?.display_name ||
user?.displayName ||
fallbackName}
</div>
<div className="ml-auto inline-flex items-center gap-3">
<div className="inline-flex items-center gap-3 ml-auto">
<div className="text-neutral-500 dark:text-neutral-400">
{createdAt}
</div>