update single note screen

This commit is contained in:
Ren Amamiya 2023-08-26 09:45:39 +07:00
parent 92d49c306b
commit bfb7d7915f
25 changed files with 455 additions and 172 deletions

View File

@ -5,10 +5,11 @@ import { AuthImportScreen } from '@app/auth/import';
import { OnboardingScreen } from '@app/auth/onboarding';
import { ErrorScreen } from '@app/error';
import { AppLayout } from '@shared/appLayout';
import { AuthLayout } from '@shared/authLayout';
import { LoaderIcon } from '@shared/icons';
import { SettingsLayout } from '@shared/settingsLayout';
import { AppLayout } from '@shared/layouts/app';
import { AuthLayout } from '@shared/layouts/auth';
import { NoteLayout } from '@shared/layouts/note';
import { SettingsLayout } from '@shared/layouts/settings';
import { checkActiveAccount } from '@utils/checkActiveAccount';
@ -54,13 +55,6 @@ const router = createBrowserRouter([
return { Component: SpaceScreen };
},
},
{
path: 'events/:id',
async lazy() {
const { EventScreen } = await import('@app/events');
return { Component: EventScreen };
},
},
{
path: 'users/:pubkey',
async lazy() {
@ -84,6 +78,27 @@ const router = createBrowserRouter([
},
],
},
{
path: '/notes',
element: <NoteLayout />,
errorElement: <ErrorScreen />,
children: [
{
path: 'text/:id',
async lazy() {
const { TextNoteScreen } = await import('@app/notes/text');
return { Component: TextNoteScreen };
},
},
{
path: 'article/:id',
async lazy() {
const { ArticleNoteScreen } = await import('@app/notes/article');
return { Component: ArticleNoteScreen };
},
},
],
},
{
path: '/splashscreen',
errorElement: <ErrorScreen />,

View File

@ -28,11 +28,11 @@ export function User({ pubkey, fallback }: { pubkey: string; fallback?: string }
/>
</div>
<div className="flex w-full flex-1 flex-col items-start text-start">
<span className="truncate font-medium leading-tight text-white">
<p className="truncate font-medium leading-tight text-white">
{user?.name || user?.display_name || user?.nip05}
</span>
</p>
<span className="max-w-[15rem] truncate text-base leading-tight text-white/50">
{user?.nip05?.toLowerCase() || displayNpub(pubkey, 16)}
{displayNpub(pubkey, 16)}
</span>
</div>
</div>

View File

@ -40,7 +40,7 @@ export function ErrorScreen() {
Sorry, an unexpected error has occurred.
</h1>
<div className="mt-4 inline-flex h-16 items-center justify-center rounded-xl border border-dashed border-red-400 bg-red-200/10 px-5">
<p className="text-sm font-medium text-red-400">
<p className="select-text text-sm font-medium text-red-400">
{error.statusText || error.message}
</p>
</div>

View File

@ -1,67 +0,0 @@
import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk';
import { useParams } from 'react-router-dom';
import { useStorage } from '@libs/storage/provider';
import {
ArticleNote,
FileNote,
NoteActions,
NoteReplyForm,
NoteStats,
TextNote,
ThreadUser,
UnknownNote,
} from '@shared/notes';
import { RepliesList } from '@shared/notes/replies/list';
import { NoteSkeleton } from '@shared/notes/skeleton';
import { useEvent } from '@utils/hooks/useEvent';
export function EventScreen() {
const { id } = useParams();
const { db } = useStorage();
const { status, data } = useEvent(id);
const renderKind = (event: NDKEvent) => {
switch (event.kind) {
case NDKKind.Text:
return <TextNote event={event} />;
case NDKKind.Article:
return <ArticleNote event={event} />;
case 1063:
return <FileNote event={event} />;
default:
return <UnknownNote event={event} />;
}
};
return (
<div className="mx-auto w-[600px]">
<div className="scrollbar-hide flex h-full w-full flex-col gap-1.5 overflow-y-auto pt-11">
{status === 'loading' ? (
<div className="px-3 py-1.5">
<div className="rounded-xl bg-white/10 px-3 py-3">
<NoteSkeleton />
</div>
</div>
) : (
<div className="h-min w-full px-3 pt-1.5">
<div className="rounded-xl bg-white/10 px-3 pt-3">
<ThreadUser pubkey={data.pubkey} time={data.created_at} />
<div className="mt-2">{renderKind(data)}</div>
<div>
<NoteActions id={id} pubkey={data.pubkey} noOpenThread={true} />
<NoteStats id={id} />
</div>
</div>
</div>
)}
<div className="px-3">
<NoteReplyForm id={id} pubkey={db.account.pubkey} />
<RepliesList id={id} />
</div>
</div>
</div>
);
}

122
src/app/notes/article.tsx Normal file
View File

@ -0,0 +1,122 @@
import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk';
import { writeText } from '@tauri-apps/plugin-clipboard-manager';
import { nip19 } from 'nostr-tools';
import { EventPointer } from 'nostr-tools/lib/nip19';
import { useRef, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { useStorage } from '@libs/storage/provider';
import { ArrowLeftIcon, CheckCircleIcon, ReplyIcon, ShareIcon } from '@shared/icons';
import {
ArticleDetailNote,
NoteActions,
NoteReplyForm,
NoteStats,
ThreadUser,
UnknownNote,
} from '@shared/notes';
import { RepliesList } from '@shared/notes/replies/list';
import { NoteSkeleton } from '@shared/notes/skeleton';
import { useEvent } from '@utils/hooks/useEvent';
export function ArticleNoteScreen() {
const navigate = useNavigate();
const replyRef = useRef(null);
const { id } = useParams();
const { db } = useStorage();
const { status, data } = useEvent(id);
const [isCopy, setIsCopy] = useState(false);
const share = async () => {
await writeText(
'https://nostr.com/' +
nip19.neventEncode({ id: data.id, author: data.pubkey } as EventPointer)
);
// update state
setIsCopy(true);
// reset state after 2 sec
setTimeout(() => setIsCopy(false), 2000);
};
const scrollToReply = () => {
replyRef.current.scrollIntoView();
};
const renderKind = (event: NDKEvent) => {
switch (event.kind) {
case NDKKind.Article:
return <ArticleDetailNote event={event} />;
default:
return <UnknownNote event={event} />;
}
};
return (
<div className="scrollbar-hide h-full w-full overflow-y-auto scroll-smooth">
<div className="container mx-auto px-4 pt-16 sm:px-6 lg:px-8">
<div className="grid grid-cols-5">
<div className="col-span-1 pr-8">
<div className="sticky top-16 flex flex-col items-end gap-4">
<button
type="button"
onClick={() => navigate(-1)}
className="inline-flex h-12 w-12 items-center justify-center rounded-xl border border-white/10 bg-white/5"
>
<ArrowLeftIcon className="h-5 w-5 text-white" />
</button>
<div className="flex flex-col divide-y divide-white/5 rounded-xl border border-white/10 bg-white/5">
<button
type="button"
onClick={share}
className="sticky top-16 inline-flex h-12 w-12 items-center justify-center rounded-t-xl "
>
{isCopy ? (
<CheckCircleIcon className="h-5 w-5 text-green-500" />
) : (
<ShareIcon className="h-5 w-5 text-white" />
)}
</button>
<button
type="button"
onClick={scrollToReply}
className="sticky top-16 inline-flex h-12 w-12 items-center justify-center rounded-b-xl"
>
<ReplyIcon className="h-5 w-5 text-white" />
</button>
</div>
</div>
</div>
<div className="col-span-3 flex flex-col gap-1.5">
{status === 'loading' ? (
<div className="px-3 py-1.5">
<div className="rounded-xl bg-white/10 px-3 py-3">
<NoteSkeleton />
</div>
</div>
) : (
<div className="h-min w-full px-3">
<div className="rounded-xl bg-white/10 px-3 pt-3">
<ThreadUser pubkey={data.pubkey} time={data.created_at} />
<div className="mt-2">{renderKind(data)}</div>
<div>
<NoteActions id={id} pubkey={data.pubkey} noOpenThread={true} />
<NoteStats id={id} />
</div>
</div>
</div>
)}
<div ref={replyRef} className="px-3">
<NoteReplyForm id={id} pubkey={db.account.pubkey} />
<RepliesList id={id} />
</div>
</div>
<div className="col-span-1" />
</div>
</div>
</div>
);
}

128
src/app/notes/text.tsx Normal file
View File

@ -0,0 +1,128 @@
import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk';
import { writeText } from '@tauri-apps/plugin-clipboard-manager';
import { nip19 } from 'nostr-tools';
import { EventPointer } from 'nostr-tools/lib/nip19';
import { useRef, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { useStorage } from '@libs/storage/provider';
import { ArrowLeftIcon, CheckCircleIcon, ReplyIcon, ShareIcon } from '@shared/icons';
import {
ArticleNote,
FileNote,
NoteActions,
NoteReplyForm,
NoteStats,
TextNote,
ThreadUser,
UnknownNote,
} from '@shared/notes';
import { RepliesList } from '@shared/notes/replies/list';
import { NoteSkeleton } from '@shared/notes/skeleton';
import { useEvent } from '@utils/hooks/useEvent';
export function TextNoteScreen() {
const navigate = useNavigate();
const replyRef = useRef(null);
const { id } = useParams();
const { db } = useStorage();
const { status, data } = useEvent(id);
const [isCopy, setIsCopy] = useState(false);
const share = async () => {
await writeText(
'https://nostr.com/' +
nip19.neventEncode({ id: data.id, author: data.pubkey } as EventPointer)
);
// update state
setIsCopy(true);
// reset state after 2 sec
setTimeout(() => setIsCopy(false), 2000);
};
const scrollToReply = () => {
replyRef.current.scrollIntoView();
};
const renderKind = (event: NDKEvent) => {
switch (event.kind) {
case NDKKind.Text:
return <TextNote event={event} />;
case NDKKind.Article:
return <ArticleNote event={event} />;
case 1063:
return <FileNote event={event} />;
default:
return <UnknownNote event={event} />;
}
};
return (
<div className="scrollbar-hide h-full w-full overflow-y-auto scroll-smooth">
<div className="container mx-auto px-4 pt-16 sm:px-6 lg:px-8">
<div className="grid grid-cols-5">
<div className="col-span-1 pr-8">
<div className="sticky top-16 flex flex-col items-end gap-4">
<button
type="button"
onClick={() => navigate(-1)}
className="inline-flex h-12 w-12 items-center justify-center rounded-xl border border-white/10 bg-white/5"
>
<ArrowLeftIcon className="h-5 w-5 text-white" />
</button>
<div className="flex flex-col divide-y divide-white/5 rounded-xl border border-white/10 bg-white/5">
<button
type="button"
onClick={share}
className="sticky top-16 inline-flex h-12 w-12 items-center justify-center rounded-t-xl "
>
{isCopy ? (
<CheckCircleIcon className="h-5 w-5 text-green-500" />
) : (
<ShareIcon className="h-5 w-5 text-white" />
)}
</button>
<button
type="button"
onClick={scrollToReply}
className="sticky top-16 inline-flex h-12 w-12 items-center justify-center rounded-b-xl"
>
<ReplyIcon className="h-5 w-5 text-white" />
</button>
</div>
</div>
</div>
<div className="col-span-3 flex flex-col gap-1.5">
{status === 'loading' ? (
<div className="px-3 py-1.5">
<div className="rounded-xl bg-white/10 px-3 py-3">
<NoteSkeleton />
</div>
</div>
) : (
<div className="h-min w-full px-3">
<div className="rounded-xl bg-white/10 px-3 pt-3">
<ThreadUser pubkey={data.pubkey} time={data.created_at} />
<div className="mt-2">{renderKind(data)}</div>
<div>
<NoteActions id={id} pubkey={data.pubkey} noOpenThread={true} />
<NoteStats id={id} />
</div>
</div>
</div>
)}
<div ref={replyRef} className="px-3">
<NoteReplyForm id={id} pubkey={db.account.pubkey} />
<RepliesList id={id} />
</div>
</div>
<div className="col-span-1" />
</div>
</div>
</div>
);
}

View File

@ -50,3 +50,4 @@ export * from './horizontalDots';
export * from './arrowRightCircle';
export * from './hashtag';
export * from './file';
export * from './share';

View File

@ -0,0 +1,21 @@
import { SVGProps } from 'react';
export function ShareIcon(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"
strokeLinejoin="round"
strokeWidth="1.5"
d="M22.085 11.628l-8.501-7.63a.5.5 0 00-.834.373V8.5C4.25 8.5 2 11.75 2 20.25c1.5-3 2.25-4.75 10.75-4.75v4.129a.5.5 0 00.834.372l8.501-7.63a.5.5 0 000-.744z"
></path>
</svg>
);
}

View File

@ -0,0 +1,10 @@
import { Outlet } from 'react-router-dom';
export function NoteLayout() {
return (
<div className="relative h-screen w-screen bg-black/90">
<div className="absolute left-0 top-0 z-50 h-16 w-full" data-tauri-drag-region />
<Outlet />
</div>
);
}

View File

@ -48,7 +48,7 @@ export function MoreActions({ id, pubkey }: { id: string; pubkey: string }) {
<DropdownMenu.Content className="flex w-[200px] flex-col overflow-hidden rounded-md bg-white/10 p-2 backdrop-blur-3xl focus:outline-none">
<DropdownMenu.Item asChild>
<Link
to={`/events/${id}`}
to={`/notes/text/${id}`}
className="inline-flex h-10 items-center rounded-md px-2 text-sm font-medium text-white hover:bg-white/10"
>
Open as new screen

View File

@ -14,6 +14,7 @@ export * from './replies/sub';
export * from './kinds/text';
export * from './kinds/file';
export * from './kinds/article';
export * from './kinds/articleDetail';
export * from './kinds/unknown';
export * from './metadata';
export * from './users/mini';

View File

@ -1,5 +1,6 @@
import { NDKEvent } from '@nostr-dev-kit/ndk';
import { useMemo } from 'react';
import { Link } from 'react-router-dom';
import { Image } from '@shared/image';
@ -27,26 +28,31 @@ export function ArticleNote({ event }: { event: NDKEvent }) {
}, [event.id]);
return (
<div className="mb-2 mt-3 rounded-lg bg-white/10">
<Link
to={`/notes/article/${event.id}`}
preventScrollReset={true}
className="mb-2 mt-3 rounded-lg"
>
<div className="flex flex-col rounded-lg">
<Image
src={metadata.image}
alt={metadata.title}
className="h-44 w-full rounded-t-lg object-cover"
/>
<div className="flex flex-col gap-2 px-3 py-3">
{metadata.image && (
<Image
src={metadata.image}
alt={metadata.title}
className="h-44 w-full rounded-t-lg object-cover"
/>
)}
<div className="flex flex-col gap-2 rounded-b-lg bg-white/10 px-3 py-3">
<h5 className="line-clamp-1 font-medium leading-none text-white">
{metadata.title}
</h5>
<p className="line-clamp-3 break-all text-sm text-white/50">
{metadata.summary}
</p>
<span className="mt-2.5 text-sm leading-none text-white/50">
{metadata.publishedAt.toString()}
</span>
</div>
</div>
</div>
</Link>
);
}

View File

@ -0,0 +1,37 @@
import { NDKEvent } from '@nostr-dev-kit/ndk';
import { useMemo } from 'react';
import ReactMarkdown from 'react-markdown';
import { Link } from 'react-router-dom';
import remarkGfm from 'remark-gfm';
import { Image } from '@shared/image';
export function ArticleDetailNote({ event }: { event: NDKEvent }) {
const metadata = useMemo(() => {
const title = event.tags.find((tag) => tag[0] === 'title')?.[1];
const image = event.tags.find((tag) => tag[0] === 'image')?.[1];
const summary = event.tags.find((tag) => tag[0] === 'summary')?.[1];
let publishedAt: Date | string | number = event.tags.find(
(tag) => tag[0] === 'published_at'
)?.[1];
if (publishedAt) {
publishedAt = new Date(parseInt(publishedAt)).toLocaleDateString('en-US');
} else {
publishedAt = new Date(event.created_at * 1000).toLocaleDateString('en-US');
}
return {
title,
image,
publishedAt,
summary,
};
}, [event.id]);
return (
<ReactMarkdown className="markdown" remarkPlugins={[remarkGfm]}>
{event.content}
</ReactMarkdown>
);
}

View File

@ -26,11 +26,16 @@ export function TextNote({ event }: { event: NDKEvent }) {
del: ({ children }) => {
const key = children[0] as string;
if (typeof key !== 'string') return;
if (key.startsWith('pub') && key.length > 50 && key.length < 100)
if (key.startsWith('pub') && key.length > 50 && key.length < 100) {
return <MentionUser pubkey={key.replace('pub-', '')} />;
if (key.startsWith('tag')) return <Hashtag tag={key.replace('tag-', '')} />;
}
if (key.startsWith('tag')) {
return <Hashtag tag={key.replace('tag-', '')} />;
}
},
}}
disallowedElements={['h1', 'h2', 'h3', 'h4', 'h5', 'h6']}
unwrapDisallowed={true}
>
{content?.parsed}
</ReactMarkdown>

View File

@ -12,14 +12,13 @@ export function ImagePreview({ urls, truncate }: { urls: string[]; truncate?: bo
};
return (
<div className="mb-2 mt-3 max-w-[420px] overflow-hidden">
<div className="mb-2 mt-3 overflow-hidden">
<div className="flex flex-col gap-2">
{urls.map((url) => (
<div key={url} className="group relative min-w-0 shrink-0 grow-0 basis-full">
<Image
src={url}
fallback="https://void.cat/d/XTmrMkpid8DGLjv1AzdvcW"
alt="image"
alt={url}
className={`${
truncate ? 'h-auto max-h-[300px]' : 'h-auto'
} w-full rounded-lg border border-white/10 object-cover`}

View File

@ -5,25 +5,26 @@ import { NDKEventWithReplies } from '@utils/types';
export function Reply({ event, root }: { event: NDKEventWithReplies; root?: string }) {
return (
<div className="h-min w-full py-1.5">
<div className="relative overflow-hidden rounded-xl bg-white/10 px-3 pt-3">
<div className="relative h-min w-full">
{event?.replies?.length > 0 && (
<div className="absolute -left-3 top-0 h-[calc(100%-1.2rem)] w-px bg-gradient-to-t from-fuchsia-200 via-red-200 to-orange-300" />
)}
<div className="relative z-10">
<div className="relative flex flex-col">
<User pubkey={event.pubkey} time={event.created_at} />
<div className="relative z-20 -mt-6 flex items-start gap-3">
<div className="-mt-6 flex items-start gap-3">
<div className="w-11 shrink-0" />
<div className="flex-1">
<TextNote event={event} />
<NoteActions id={event.id} pubkey={event.pubkey} root={root} />
</div>
</div>
<div>
{event.replies ? (
event.replies.map((sub) => <SubReply key={sub.id} event={sub} />)
) : (
<div className="pb-3" />
)}
</div>
</div>
{event.replies ? (
event.replies.map((sub) => <SubReply key={sub.id} event={sub} />)
) : (
<div className="pb-3" />
)}
</div>
</div>
);

View File

@ -8,42 +8,51 @@ import { NDKEventWithReplies } from '@utils/types';
export function RepliesList({ id }: { id: string }) {
const { ndk } = useNDK();
const { status, data } = useQuery(['note-replies', id], async () => {
const events = await ndk.fetchEvents({
kinds: [1],
'#e': [id],
});
const { status, data } = useQuery(
['note-replies', id],
async () => {
try {
const events = await ndk.fetchEvents({
kinds: [1],
'#e': [id],
});
const array = [...events] as unknown as NDKEventWithReplies[];
const array = [...events] as unknown as NDKEventWithReplies[];
if (array.length > 0) {
const replies = new Set();
array.forEach((event) => {
const tags = event.tags.filter((el) => el[0] === 'e' && el[1] !== id);
if (tags.length > 0) {
tags.forEach((tag) => {
const rootIndex = array.findIndex((el) => el.id === tag[1]);
if (rootIndex) {
const rootEvent = array[rootIndex];
if (rootEvent.replies) {
rootEvent.replies.push(event);
} else {
rootEvent.replies = [event];
}
replies.add(event.id);
if (array.length > 0) {
const replies = new Set();
array.forEach((event) => {
const tags = event.tags.filter((el) => el[0] === 'e' && el[1] !== id);
if (tags.length > 0) {
tags.forEach((tag) => {
const rootIndex = array.findIndex((el) => el.id === tag[1]);
if (rootIndex !== -1) {
const rootEvent = array[rootIndex];
if (rootEvent && rootEvent.replies) {
rootEvent.replies.push(event);
} else {
rootEvent.replies = [event];
}
replies.add(event.id);
}
});
}
});
const cleanEvents = array.filter((ev) => !replies.has(ev.id));
return cleanEvents;
}
});
const cleanEvents = array.filter((ev) => !replies.has(ev.id));
return cleanEvents;
}
return array;
});
return array;
} catch (e) {
throw new Error(e);
}
},
{ enabled: !!ndk }
);
if (status === 'loading') {
return (
<div className="mt-3">
<div className="mt-5 pb-10">
<div className="flex flex-col">
<div className="rounded-xl bg-white/10 px-3 py-3">
<NoteSkeleton />
@ -53,19 +62,29 @@ export function RepliesList({ id }: { id: string }) {
);
}
return (
<div className="mt-3 pb-10">
<div className="mb-2">
<h5 className="text-lg font-semibold text-white">{data?.length || 0} replies</h5>
if (status === 'error') {
return (
<div className="mt-5 pb-10">
<div className="flex flex-col">
<div className="rounded-xl bg-white/10 px-3 py-3">
<p>Error: failed to get replies</p>
</div>
</div>
</div>
<div className="flex flex-col">
);
}
return (
<div className="mt-5 pb-10">
<h5 className="mb-5 text-lg font-semibold text-white">
{data?.length || 0} replies
</h5>
<div className="flex flex-col gap-3">
{data?.length === 0 ? (
<div className="px=3">
<div className="flex w-full items-center justify-center rounded-xl bg-white/10">
<div className="flex flex-col items-center justify-center gap-2 py-6">
<h3 className="text-3xl">👋</h3>
<p className="leading-none text-white/50">Share your thought on it...</p>
</div>
<div className="mt-2 flex w-full items-center justify-center rounded-xl bg-white/10">
<div className="flex flex-col items-center justify-center gap-2 py-6">
<h3 className="text-3xl">👋</h3>
<p className="leading-none text-white/50">Share your thought on it...</p>
</div>
</div>
) : (

View File

@ -5,9 +5,9 @@ import { User } from '@shared/user';
export function SubReply({ event }: { event: NDKEvent }) {
return (
<div className="relative mb-3 mt-5 flex flex-col">
<div className="relative z-10 mb-3 mt-5 flex flex-col">
<User pubkey={event.pubkey} time={event.created_at} />
<div className="relative z-20 -mt-6 flex items-start gap-3">
<div className="-mt-6 flex items-start gap-3">
<div className="w-11 shrink-0" />
<div className="flex-1">
<TextNote event={event} />

View File

@ -19,10 +19,7 @@ export function RepostUser({ pubkey }: { pubkey: string }) {
/>
<div className="inline-flex items-baseline gap-1">
<h5 className="max-w-[18rem] truncate text-white/50">
{user?.nip05?.toLowerCase() ||
user?.name ||
user?.display_name ||
shortenKey(pubkey)}
{user?.nip05 || user?.name || user?.display_name || shortenKey(pubkey)}
</h5>
<span className="text-white/50">reposted</span>
</div>

View File

@ -1,4 +1,3 @@
import { VerticalDotsIcon } from '@shared/icons';
import { Image } from '@shared/image';
import { formatCreatedAt } from '@utils/createdAt';
@ -16,23 +15,15 @@ export function ThreadUser({ pubkey, time }: { pubkey: string; time: number }) {
return (
<div className="flex items-center gap-3">
<Image
src={user?.picture || user?.image}
src={user.picture || user.image}
alt={pubkey}
className="relative z-20 inline-block h-11 w-11 rounded-lg"
/>
<div className="lex flex-1 items-baseline justify-between">
<div className="inline-flex w-full items-center justify-between">
<h5 className="max-w-[15rem] truncate font-semibold leading-none text-white">
{user?.nip05?.toLowerCase() || user?.name || user?.display_name}
</h5>
<button
type="button"
className="inline-flex h-5 w-max items-center justify-center rounded px-1 hover:bg-white/10"
>
<VerticalDotsIcon className="h-4 w-4 rotate-90 transform text-white/50" />
</button>
</div>
<div className="mt-1 inline-flex items-center gap-2">
<div className="flex flex-1 flex-col gap-2">
<h5 className="max-w-[15rem] truncate font-semibold leading-none text-white">
{user.display_name || user.name}
</h5>
<div className="inline-flex items-center gap-2">
<span className="leading-none text-white/50">{createdAt}</span>
<span className="leading-none text-white/50">·</span>
<span className="leading-none text-white/50">{displayNpub(pubkey, 16)}</span>

View File

@ -79,10 +79,7 @@ export function User({
size === 'small' ? 'max-w-[10rem]' : 'max-w-[15rem]'
)}
>
{user?.nip05?.toLowerCase() ||
user?.name ||
user?.display_name ||
displayNpub(pubkey, 16)}
{user?.nip05 || user?.name || user?.display_name || displayNpub(pubkey, 16)}
</h5>
<span className="leading-none text-white/50">·</span>
<span className="leading-none text-white/50">{createdAt}</span>

View File

@ -1,7 +1,7 @@
import { useEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import { UserMetadata } from '@app/users/components/stats';
import { UserStats } from '@app/users/components/stats';
import { Image } from '@shared/image';
@ -65,7 +65,7 @@ export function UserProfile({ pubkey }: { pubkey: string }) {
<p className="mt-2 max-w-[500px] select-text break-words text-white">
{user?.about}
</p>
<UserMetadata pubkey={pubkey} />
<UserStats pubkey={pubkey} />
</div>
<div className="mt-4 inline-flex items-center gap-2">
{status === 'loading' ? (