update single note screen
This commit is contained in:
@@ -50,3 +50,4 @@ export * from './horizontalDots';
|
||||
export * from './arrowRightCircle';
|
||||
export * from './hashtag';
|
||||
export * from './file';
|
||||
export * from './share';
|
||||
|
||||
21
src/shared/icons/share.tsx
Normal file
21
src/shared/icons/share.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
10
src/shared/layouts/note.tsx
Normal file
10
src/shared/layouts/note.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
37
src/shared/notes/kinds/articleDetail.tsx
Normal file
37
src/shared/notes/kinds/articleDetail.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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`}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
) : (
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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' ? (
|
||||
|
||||
Reference in New Issue
Block a user