wip: new activity sidebar
This commit is contained in:
33
packages/ui/src/activity/column.tsx
Normal file
33
packages/ui/src/activity/column.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import { activityAtom } from "@lume/utils";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { useAtomValue } from "jotai";
|
||||
import { ActivityContent } from "./content";
|
||||
|
||||
export function Activity() {
|
||||
const isActivityOpen = useAtomValue(activityAtom);
|
||||
|
||||
return (
|
||||
<AnimatePresence initial={false} mode="wait">
|
||||
{isActivityOpen ? (
|
||||
<motion.div
|
||||
key={isActivityOpen ? "activity-open" : "activity-close"}
|
||||
layout
|
||||
initial={{ scale: 0.9, opacity: 0, translateX: -20 }}
|
||||
animate={{
|
||||
scale: [0.95, 1],
|
||||
opacity: [0.5, 1],
|
||||
translateX: [-10, 0],
|
||||
}}
|
||||
exit={{
|
||||
scale: [0.95, 0.9],
|
||||
opacity: [0.5, 0],
|
||||
translateX: [-10, -20],
|
||||
}}
|
||||
className="h-full w-[350px] px-1 pb-1 shrink-0"
|
||||
>
|
||||
<ActivityContent />
|
||||
</motion.div>
|
||||
) : null}
|
||||
</AnimatePresence>
|
||||
);
|
||||
}
|
||||
101
packages/ui/src/activity/content.tsx
Normal file
101
packages/ui/src/activity/content.tsx
Normal file
@@ -0,0 +1,101 @@
|
||||
import { useArk, useStorage } from "@lume/ark";
|
||||
import { LoaderIcon } from "@lume/icons";
|
||||
import { FETCH_LIMIT } from "@lume/utils";
|
||||
import { NDKEvent, NDKKind } from "@nostr-dev-kit/ndk";
|
||||
import { useInfiniteQuery } from "@tanstack/react-query";
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { VList } from "virtua";
|
||||
import { ReplyActivity } from "./reply";
|
||||
import { RepostActivity } from "./repost";
|
||||
import { ZapActivity } from "./zap";
|
||||
|
||||
export function ActivityContent() {
|
||||
const ark = useArk();
|
||||
const storage = useStorage();
|
||||
|
||||
const { isLoading, data, hasNextPage, isFetchingNextPage, fetchNextPage } =
|
||||
useInfiniteQuery({
|
||||
queryKey: ["activity"],
|
||||
initialPageParam: 0,
|
||||
queryFn: async ({
|
||||
signal,
|
||||
pageParam,
|
||||
}: {
|
||||
signal: AbortSignal;
|
||||
pageParam: number;
|
||||
}) => {
|
||||
const events = await ark.getInfiniteEvents({
|
||||
filter: {
|
||||
kinds: [NDKKind.Zap],
|
||||
"#p": [storage.account.pubkey],
|
||||
},
|
||||
limit: 100,
|
||||
pageParam,
|
||||
signal,
|
||||
});
|
||||
|
||||
return events;
|
||||
},
|
||||
getNextPageParam: (lastPage) => {
|
||||
const lastEvent = lastPage.at(-1);
|
||||
if (!lastEvent) return;
|
||||
return lastEvent.created_at - 1;
|
||||
},
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnMount: false,
|
||||
refetchOnReconnect: false,
|
||||
});
|
||||
|
||||
const allEvents = useMemo(
|
||||
() => (data ? data.pages.flatMap((page) => page) : []),
|
||||
[data],
|
||||
);
|
||||
|
||||
const renderEvent = useCallback((event: NDKEvent) => {
|
||||
if (event.pubkey === storage.account.pubkey) return null;
|
||||
|
||||
switch (event.kind) {
|
||||
case NDKKind.Text:
|
||||
return <ReplyActivity key={event.id} event={event} />;
|
||||
case NDKKind.Repost:
|
||||
return <RepostActivity key={event.id} event={event} />;
|
||||
case NDKKind.Zap:
|
||||
return <ZapActivity key={event.id} event={event} />;
|
||||
default:
|
||||
return <ReplyActivity key={event.id} event={event} />;
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="w-full h-full flex flex-col justify-between rounded-xl overflow-hidden 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/10">
|
||||
<div className="h-full w-full min-h-0">
|
||||
{isLoading ? (
|
||||
<div className="w-[350px] h-full flex items-center justify-center">
|
||||
<LoaderIcon className="size-5 animate-spin" />
|
||||
</div>
|
||||
) : allEvents.length < 1 ? (
|
||||
<div className="w-full h-full flex flex-col items-center justify-center">
|
||||
<p className="mb-2 text-2xl">🎉</p>
|
||||
<p className="text-center font-medium">Yo! Nothing new yet.</p>
|
||||
</div>
|
||||
) : (
|
||||
renderEvent(allEvents[0])
|
||||
)}
|
||||
</div>
|
||||
<div className="h-16 shrink-0 px-3 flex items-center gap-3 bg-neutral-100 dark:bg-neutral-900">
|
||||
<button
|
||||
type="button"
|
||||
className="h-11 flex-1 inline-flex items-center justify-center rounded-xl font-medium bg-neutral-200 dark:bg-neutral-800 hover:bg-neutral-300 dark:hover:bg-neutral-700"
|
||||
>
|
||||
Previous
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="h-11 flex-1 inline-flex items-center justify-center rounded-xl font-medium bg-blue-500 text-white hover:bg-blue-600"
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
48
packages/ui/src/activity/reply.tsx
Normal file
48
packages/ui/src/activity/reply.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import { Note, useArk } from "@lume/ark";
|
||||
import { NDKEvent } from "@nostr-dev-kit/ndk";
|
||||
import { ActivityRootNote } from "./rootNote";
|
||||
|
||||
export function ReplyActivity({ event }: { event: NDKEvent }) {
|
||||
const ark = useArk();
|
||||
const thread = ark.getEventThread({ tags: event.tags });
|
||||
|
||||
return (
|
||||
<div className="h-full pb-3 flex flex-col justify-between">
|
||||
<div className="h-14 border-b border-neutral-100 dark:border-neutral-900 flex flex-col items-center justify-center px-3">
|
||||
<h3 className="text-center font-semibold leading-tight">
|
||||
Conversation
|
||||
</h3>
|
||||
<p className="text-sm text-blue-500 font-medium leading-tight">
|
||||
@ Someone has replied to your note
|
||||
</p>
|
||||
</div>
|
||||
<div className="px-3">
|
||||
{thread ? (
|
||||
<div className="flex flex-col gap-3 mb-1">
|
||||
<ActivityRootNote eventId={thread.rootEventId} />
|
||||
<ActivityRootNote eventId={thread.replyEventId} />
|
||||
</div>
|
||||
) : null}
|
||||
<div className="mt-3 flex flex-col gap-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<p className="text-teal-500 font-medium">New reply</p>
|
||||
<div className="flex-1 h-px bg-teal-300" />
|
||||
<div className="w-4 shrink-0 h-px bg-teal-300" />
|
||||
</div>
|
||||
<Note.Provider event={event}>
|
||||
<Note.Root>
|
||||
<div className="flex items-center justify-between px-3 h-14">
|
||||
<Note.User className="flex-1 pr-1" />
|
||||
</div>
|
||||
<Note.Content className="min-w-0 px-3" />
|
||||
<div className="flex items-center justify-between px-3 h-14">
|
||||
<Note.Pin />
|
||||
<div className="inline-flex items-center gap-10" />
|
||||
</div>
|
||||
</Note.Root>
|
||||
</Note.Provider>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
33
packages/ui/src/activity/repost.tsx
Normal file
33
packages/ui/src/activity/repost.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import { formatCreatedAt } from "@lume/utils";
|
||||
import { NDKEvent } from "@nostr-dev-kit/ndk";
|
||||
import { User } from "../user";
|
||||
import { ActivityRootNote } from "./rootNote";
|
||||
|
||||
export function RepostActivity({ event }: { event: NDKEvent }) {
|
||||
const repostId = event.tags.find((el) => el[0] === "e")[1];
|
||||
const createdAt = formatCreatedAt(event.created_at);
|
||||
|
||||
return (
|
||||
<div className="h-full pb-3 flex flex-col justify-between">
|
||||
<div className="h-14 border-b border-neutral-100 dark:border-neutral-900 flex flex-col items-center justify-center px-3">
|
||||
<h3 className="text-center font-semibold leading-tight">Boost</h3>
|
||||
<p className="text-sm text-blue-500 font-medium leading-tight">
|
||||
@ Someone has reposted to your note
|
||||
</p>
|
||||
</div>
|
||||
<div className="px-3">
|
||||
<div className="flex flex-col gap-3">
|
||||
<User pubkey={event.pubkey} variant="notify2" />
|
||||
<div className="flex items-center gap-3">
|
||||
<p className="text-teal-500 font-medium">Reposted</p>
|
||||
<div className="flex-1 h-px bg-teal-300" />
|
||||
<div className="w-4 shrink-0 h-px bg-teal-300" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-3 flex flex-col gap-3">
|
||||
<ActivityRootNote eventId={repostId} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
40
packages/ui/src/activity/rootNote.tsx
Normal file
40
packages/ui/src/activity/rootNote.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import { Note, useEvent } from "@lume/ark";
|
||||
|
||||
export function ActivityRootNote({ eventId }: { eventId: string }) {
|
||||
const { isLoading, isError, data } = useEvent(eventId);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="relative flex gap-3">
|
||||
<div className="relative flex-1 rounded-md bg-neutral-200 px-2 py-2 dark:bg-neutral-800">
|
||||
<div className="h-4 w-full animate-pulse bg-neutral-300 dark:bg-neutral-700" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
return (
|
||||
<div className="relative flex gap-3">
|
||||
<div className="relative flex-1 rounded-md bg-neutral-200 px-2 py-2 dark:bg-neutral-800">
|
||||
Failed to fetch event
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Note.Provider event={data}>
|
||||
<Note.Root>
|
||||
<div className="flex items-center justify-between px-3 h-14">
|
||||
<Note.User className="flex-1 pr-1" />
|
||||
</div>
|
||||
<Note.Content className="min-w-0 px-3" />
|
||||
<div className="flex items-center justify-between px-3 h-14">
|
||||
<Note.Pin />
|
||||
<div className="inline-flex items-center gap-10" />
|
||||
</div>
|
||||
</Note.Root>
|
||||
</Note.Provider>
|
||||
);
|
||||
}
|
||||
37
packages/ui/src/activity/zap.tsx
Normal file
37
packages/ui/src/activity/zap.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import { compactNumber, formatCreatedAt } from "@lume/utils";
|
||||
import { NDKEvent, zapInvoiceFromEvent } from "@nostr-dev-kit/ndk";
|
||||
import { User } from "../user";
|
||||
import { ActivityRootNote } from "./rootNote";
|
||||
|
||||
export function ZapActivity({ event }: { event: NDKEvent }) {
|
||||
const zapEventId = event.tags.find((tag) => tag[0] === "e")[1];
|
||||
const invoice = zapInvoiceFromEvent(event);
|
||||
|
||||
return (
|
||||
<div className="h-full pb-3 flex flex-col justify-between">
|
||||
<div className="h-14 border-b border-neutral-100 dark:border-neutral-900 flex flex-col items-center justify-center px-3">
|
||||
<h3 className="text-center font-semibold leading-tight">Zap</h3>
|
||||
<p className="text-sm text-blue-500 font-medium leading-tight">
|
||||
@ Someone love your note
|
||||
</p>
|
||||
</div>
|
||||
<div className="px-3">
|
||||
<div className="flex flex-col gap-3">
|
||||
<User
|
||||
pubkey={event.pubkey}
|
||||
variant="notify2"
|
||||
subtext={`${compactNumber.format(invoice.amount)} sats`}
|
||||
/>
|
||||
<div className="flex items-center gap-3">
|
||||
<p className="text-teal-500 font-medium">Zapped</p>
|
||||
<div className="flex-1 h-px bg-teal-300" />
|
||||
<div className="w-4 shrink-0 h-px bg-teal-300" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-3 flex flex-col gap-3">
|
||||
<ActivityRootNote eventId={zapEventId} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -288,7 +288,7 @@ export function EditorForm() {
|
||||
}, [filters.length, editor, index, search, target]);
|
||||
|
||||
return (
|
||||
<div className="w-full h-full flex flex-col justify-between rounded-xl overflow-hidden 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-[inset_0_0_0.5px_1px_hsla(0,0%,100%,0.075),0_0_0_1px_hsla(0,0%,0%,0.05),0_0.3px_0.4px_hsla(0,0%,0%,0.02),0_0.9px_1.5px_hsla(0,0%,0%,0.045),0_3.5px_6px_hsla(0,0%,0%,0.09)]">
|
||||
<div className="w-full h-full flex flex-col justify-between rounded-xl overflow-hidden 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/10">
|
||||
<Slate
|
||||
editor={editor}
|
||||
initialValue={editorValue}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { type Platform } from "@tauri-apps/plugin-os";
|
||||
import { Outlet } from "react-router-dom";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { Activity } from "../activity/column";
|
||||
import { Editor } from "../editor/column";
|
||||
import { Navigation } from "../navigation";
|
||||
import { WindowTitleBar } from "../titlebar";
|
||||
@@ -21,6 +22,7 @@ export function AppLayout({ platform }: { platform: Platform }) {
|
||||
<div className="flex w-full h-full min-h-0">
|
||||
<Navigation />
|
||||
<Editor />
|
||||
<Activity />
|
||||
<div className="flex-1 h-full px-1 pb-1">
|
||||
<Outlet />
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,7 @@ export function HomeLayout() {
|
||||
return (
|
||||
<>
|
||||
<OnboardingModal />
|
||||
<div className="h-full w-full rounded-xl overflow-hidden 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-[inset_0_0_0.5px_1px_hsla(0,0%,100%,0.075),0_0_0_1px_hsla(0,0%,0%,0.05),0_0.3px_0.4px_hsla(0,0%,0%,0.02),0_0.9px_1.5px_hsla(0,0%,0%,0.045),0_3.5px_6px_hsla(0,0%,0%,0.09)]">
|
||||
<div className="h-full w-full rounded-xl overflow-hidden 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/10">
|
||||
<Outlet />
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
import {
|
||||
BellFilledIcon,
|
||||
BellIcon,
|
||||
ComposeFilledIcon,
|
||||
ComposeIcon,
|
||||
DepotFilledIcon,
|
||||
DepotIcon,
|
||||
HomeFilledIcon,
|
||||
HomeIcon,
|
||||
NwcFilledIcon,
|
||||
NwcIcon,
|
||||
RelayFilledIcon,
|
||||
RelayIcon,
|
||||
SettingsFilledIcon,
|
||||
SettingsIcon,
|
||||
} from "@lume/icons";
|
||||
import { cn, editorAtom } from "@lume/utils";
|
||||
import { activityAtom, cn, editorAtom } from "@lume/utils";
|
||||
import { useAtom } from "jotai";
|
||||
import { useHotkeys } from "react-hotkeys-hook";
|
||||
import { NavLink } from "react-router-dom";
|
||||
@@ -20,6 +21,8 @@ import { ActiveAccount } from "./account/active";
|
||||
|
||||
export function Navigation() {
|
||||
const [isEditorOpen, setIsEditorOpen] = useAtom(editorAtom);
|
||||
const [isActvityOpen, setIsActvityOpen] = useAtom(activityAtom);
|
||||
|
||||
useHotkeys("meta+n", () => setIsEditorOpen((state) => !state), []);
|
||||
|
||||
return (
|
||||
@@ -29,7 +32,10 @@ export function Navigation() {
|
||||
<ActiveAccount />
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsEditorOpen((prev) => !prev)}
|
||||
onClick={() => {
|
||||
setIsEditorOpen((state) => !state);
|
||||
setIsActvityOpen(false);
|
||||
}}
|
||||
className="flex items-center justify-center h-auto w-full text-black aspect-square rounded-xl bg-black/5 hover:bg-blue-500 hover:text-white dark:bg-white/5 dark:text-white dark:hover:bg-blue-500"
|
||||
>
|
||||
{isEditorOpen ? (
|
||||
@@ -63,50 +69,30 @@ export function Navigation() {
|
||||
</div>
|
||||
)}
|
||||
</NavLink>
|
||||
<NavLink
|
||||
to="/relays"
|
||||
preventScrollReset={true}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setIsActvityOpen((state) => !state);
|
||||
setIsEditorOpen(false);
|
||||
}}
|
||||
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 ? (
|
||||
<RelayFilledIcon className="size-6" />
|
||||
) : (
|
||||
<RelayIcon className="size-6" />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</NavLink>
|
||||
<NavLink
|
||||
to="/depot"
|
||||
preventScrollReset={true}
|
||||
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 ? (
|
||||
<DepotFilledIcon className="text-black size-6 dark:text-white" />
|
||||
) : (
|
||||
<DepotIcon className="size-6" />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</NavLink>
|
||||
<div
|
||||
className={cn(
|
||||
"inline-flex aspect-square h-auto w-full items-center justify-center rounded-xl",
|
||||
isActvityOpen
|
||||
? "bg-black/10 text-black dark:bg-white/10 dark:text-white"
|
||||
: "text-black/50 dark:text-neutral-400",
|
||||
)}
|
||||
>
|
||||
{isActvityOpen ? (
|
||||
<BellFilledIcon className="size-6" />
|
||||
) : (
|
||||
<BellIcon className="size-6" />
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<NavLink
|
||||
to="/nwc"
|
||||
preventScrollReset={true}
|
||||
|
||||
@@ -2,11 +2,8 @@ import { useProfile } from "@lume/ark";
|
||||
import { RepostIcon } from "@lume/icons";
|
||||
import { displayNpub, formatCreatedAt } from "@lume/utils";
|
||||
import * as Avatar from "@radix-ui/react-avatar";
|
||||
import * as HoverCard from "@radix-ui/react-hover-card";
|
||||
import { minidenticon } from "minidenticons";
|
||||
import { memo, useMemo } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { NIP05 } from "./nip05";
|
||||
|
||||
export const User = memo(function User({
|
||||
pubkey,
|
||||
@@ -21,6 +18,7 @@ export const User = memo(function User({
|
||||
| "simple"
|
||||
| "mention"
|
||||
| "notify"
|
||||
| "notify2"
|
||||
| "repost"
|
||||
| "chat"
|
||||
| "large"
|
||||
@@ -105,6 +103,55 @@ export const User = memo(function User({
|
||||
);
|
||||
}
|
||||
|
||||
if (variant === "notify2") {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<Avatar.Root className="h-8 w-8 shrink-0">
|
||||
<Avatar.Image
|
||||
src={fallbackAvatar}
|
||||
alt={pubkey}
|
||||
className="h-8 w-8 rounded-md bg-black dark:bg-white"
|
||||
/>
|
||||
</Avatar.Root>
|
||||
<h5 className="max-w-[10rem] truncate font-semibold text-neutral-900 dark:text-neutral-100">
|
||||
{fallbackName}
|
||||
</h5>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<Avatar.Root className="h-8 w-8 shrink-0">
|
||||
<Avatar.Image
|
||||
src={user?.picture || user?.image}
|
||||
alt={pubkey}
|
||||
loading="eager"
|
||||
decoding="async"
|
||||
className="h-8 w-8 rounded-md"
|
||||
/>
|
||||
<Avatar.Fallback delayMs={300}>
|
||||
<img
|
||||
src={fallbackAvatar}
|
||||
alt={pubkey}
|
||||
className="h-8 w-8 rounded-md bg-black dark:bg-white"
|
||||
/>
|
||||
</Avatar.Fallback>
|
||||
</Avatar.Root>
|
||||
<div className="inline-flex items-center gap-1">
|
||||
<h5 className="max-w-[8rem] truncate font-semibold text-neutral-900 dark:text-neutral-100">
|
||||
{user?.name ||
|
||||
user?.display_name ||
|
||||
user?.displayName ||
|
||||
fallbackName}
|
||||
</h5>
|
||||
<p>{subtext}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (variant === "notify") {
|
||||
if (isLoading) {
|
||||
return (
|
||||
@@ -129,7 +176,7 @@ export const User = memo(function User({
|
||||
<Avatar.Image
|
||||
src={user?.picture || user?.image}
|
||||
alt={pubkey}
|
||||
loading="lazy"
|
||||
loading="eager"
|
||||
decoding="async"
|
||||
className="h-8 w-8 rounded-md"
|
||||
/>
|
||||
@@ -525,105 +572,36 @@ export const User = memo(function User({
|
||||
}
|
||||
|
||||
return (
|
||||
<HoverCard.Root>
|
||||
<div className="flex items-center gap-3 px-3">
|
||||
<HoverCard.Trigger asChild>
|
||||
<Avatar.Root className="h-9 w-9 shrink-0">
|
||||
<Avatar.Image
|
||||
src={user?.picture || user?.image}
|
||||
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"
|
||||
/>
|
||||
<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"
|
||||
/>
|
||||
</Avatar.Fallback>
|
||||
</Avatar.Root>
|
||||
</HoverCard.Trigger>
|
||||
<div className="flex h-6 flex-1 items-start 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="text-neutral-500 dark:text-neutral-400">
|
||||
{createdAt}
|
||||
</div>
|
||||
<div className="flex items-center gap-3 px-3">
|
||||
<Avatar.Root className="h-9 w-9 shrink-0">
|
||||
<Avatar.Image
|
||||
src={user?.picture || user?.image}
|
||||
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"
|
||||
/>
|
||||
<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"
|
||||
/>
|
||||
</Avatar.Fallback>
|
||||
</Avatar.Root>
|
||||
<div className="flex h-6 flex-1 items-start 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="text-neutral-500 dark:text-neutral-400">
|
||||
{createdAt}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<HoverCard.Portal>
|
||||
<HoverCard.Content
|
||||
className="ml-4 w-[300px] overflow-hidden rounded-xl border border-neutral-200 bg-neutral-100 shadow-lg data-[side=bottom]:animate-slideUpAndFade data-[side=left]:animate-slideRightAndFade data-[side=right]:animate-slideLeftAndFade data-[side=top]:animate-slideDownAndFade data-[state=open]:transition-all focus:outline-none dark:border-neutral-800 dark:bg-neutral-900"
|
||||
sideOffset={5}
|
||||
>
|
||||
<div className="flex gap-2.5 border-b border-neutral-200 px-3 py-3 dark:border-neutral-800">
|
||||
<Avatar.Root className="shrink-0">
|
||||
<Avatar.Image
|
||||
src={user?.picture || user?.image}
|
||||
alt={pubkey}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
className="h-10 w-10 rounded-lg object-cover"
|
||||
/>
|
||||
<Avatar.Fallback delayMs={300}>
|
||||
<img
|
||||
src={fallbackAvatar}
|
||||
alt={pubkey}
|
||||
className="h-10 w-10 rounded-lg bg-black dark:bg-white"
|
||||
/>
|
||||
</Avatar.Fallback>
|
||||
</Avatar.Root>
|
||||
<div className="flex flex-1 flex-col gap-2">
|
||||
<div className="inline-flex flex-col">
|
||||
<h5 className="text-sm font-semibold">
|
||||
{user?.name ||
|
||||
user?.display_name ||
|
||||
user?.displayName ||
|
||||
user?.username}
|
||||
</h5>
|
||||
{user?.nip05 ? (
|
||||
<NIP05
|
||||
pubkey={pubkey}
|
||||
nip05={user.nip05}
|
||||
className="max-w-[15rem] truncate text-sm text-neutral-500 dark:text-neutral-300"
|
||||
/>
|
||||
) : (
|
||||
<span className="max-w-[15rem] truncate text-sm text-neutral-500 dark:text-neutral-300">
|
||||
{fallbackName}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<p className="line-clamp-3 break-all text-sm leading-tight text-neutral-900 dark:text-neutral-100">
|
||||
{user?.about}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 px-3 py-3">
|
||||
<Link
|
||||
to={`/users/${pubkey}`}
|
||||
className="inline-flex h-9 flex-1 items-center justify-center rounded-lg bg-neutral-200 text-sm font-semibold hover:bg-blue-500 hover:text-white dark:bg-neutral-800"
|
||||
>
|
||||
View profile
|
||||
</Link>
|
||||
<Link
|
||||
to={`/chats/${pubkey}`}
|
||||
className="inline-flex h-9 flex-1 items-center justify-center rounded-lg bg-neutral-200 text-sm font-semibold hover:bg-blue-500 hover:text-white dark:bg-neutral-800"
|
||||
>
|
||||
Message
|
||||
</Link>
|
||||
</div>
|
||||
</HoverCard.Content>
|
||||
</HoverCard.Portal>
|
||||
</HoverCard.Root>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user