render reply and sub reply accordingly

This commit is contained in:
Ren Amamiya 2023-07-19 17:07:25 +07:00
parent 22c1eaa541
commit 29d40ed406
14 changed files with 118 additions and 34 deletions

View File

@ -10,11 +10,13 @@ import { RepliesList } from '@shared/notes/replies/list';
import { NoteSkeleton } from '@shared/notes/skeleton';
import { TitleBar } from '@shared/titleBar';
import { useAccount } from '@utils/hooks/useAccount';
import { useEvent } from '@utils/hooks/useEvent';
import { Block } from '@utils/types';
export function ThreadBlock({ params }: { params: Block }) {
const { status, data } = useEvent(params.content);
const { account } = useAccount();
// subscribe to live reply
// useLiveThread(params.content);
@ -22,7 +24,7 @@ export function ThreadBlock({ params }: { params: Block }) {
return (
<div className="w-[400px] shrink-0 border-r border-zinc-900">
<TitleBar id={params.id} title={params.title} />
<div className="scrollbar-hide flex h-full w-full flex-col gap-1.5 overflow-y-auto pb-20 pt-1.5">
<div className="scrollbar-hide flex h-full w-full flex-col gap-3 overflow-y-auto pb-20 pt-1.5">
{status === 'loading' ? (
<div className="px-3 py-1.5">
<div className="rounded-xl border-t border-zinc-800/50 bg-zinc-900 px-3 py-3">
@ -30,7 +32,7 @@ export function ThreadBlock({ params }: { params: Block }) {
</div>
</div>
) : (
<div className="h-min w-full px-3 py-1.5">
<div className="h-min w-full px-3 pt-1.5">
<div className="rounded-xl border-t border-zinc-800/50 bg-zinc-900 px-3 pt-3">
<ThreadUser pubkey={data.pubkey} time={data.created_at} />
<div className="mt-2">
@ -44,7 +46,7 @@ export function ThreadBlock({ params }: { params: Block }) {
</div>
)}
<div className="px-3">
<NoteReplyForm id={params.content} />
<NoteReplyForm id={params.content} pubkey={account.pubkey} />
<RepliesList id={params.content} />
</div>
</div>

View File

@ -94,7 +94,7 @@ export function ActiveAccount({ data }: { data: any }) {
return (
<Link to={`/app/user/${data.pubkey}`} className="relative inline-block h-9 w-9">
<Image
src={user.image}
src={user?.picture || user?.image}
fallback={DEFAULT_AVATAR}
alt={data.npub}
className="h-9 w-9 rounded-md object-cover"

View File

@ -23,7 +23,7 @@ export function NoteActions({
return (
<Tooltip.Provider>
<div className="-ml-1 mt-4 inline-flex w-full items-center">
<div className="-ml-1 mt-2 inline-flex w-full items-center">
<div className="inline-flex items-center gap-2">
<NoteReply id={id} pubkey={pubkey} />
<NoteReaction id={id} pubkey={pubkey} />

View File

@ -10,6 +10,7 @@ export * from './preview/video';
export * from './replies/form';
export * from './replies/item';
export * from './replies/list';
export * from './replies/sub';
export * from './kinds/kind1';
export * from './kinds/kind1063';
export * from './metadata';

View File

@ -29,7 +29,7 @@ export const MentionNote = memo(function MentionNote({ id }: { id: string }) {
onKeyDown={(e) => openThread(e, id)}
role="button"
tabIndex={0}
className="mt-3 rounded-lg border-t border-zinc-700/50 bg-zinc-800/50 px-3 py-3"
className="mb-2 mt-3 rounded-lg border-t border-zinc-700/50 bg-zinc-800/50 px-3 py-3"
>
{status === 'loading' ? (
<NoteSkeleton />

View File

@ -2,7 +2,7 @@ import { Image } from '@shared/image';
export function ImagePreview({ urls, truncate }: { urls: string[]; truncate?: boolean }) {
return (
<div className="mt-3 max-w-[420px] overflow-hidden">
<div className="mb-2 mt-3 max-w-[420px] overflow-hidden">
<div className="flex flex-col gap-2">
{urls.map((url) => (
<div key={url} className="relative min-w-0 shrink-0 grow-0 basis-full">

View File

@ -7,7 +7,7 @@ export function LinkPreview({ urls }: { urls: string[] }) {
const domain = new URL(urls[0]);
return (
<div className="mt-3 max-w-[420px] overflow-hidden rounded-lg bg-zinc-800">
<div className="mb-2 mt-3 max-w-[420px] overflow-hidden rounded-lg bg-zinc-800">
{status === 'loading' ? (
<div className="flex flex-col">
<div className="h-44 w-full animate-pulse bg-zinc-700" />

View File

@ -2,7 +2,7 @@ import ReactPlayer from 'react-player/es6';
export function VideoPreview({ urls }: { urls: string[] }) {
return (
<div className="relative mt-3 flex w-full max-w-[420px] flex-col gap-2">
<div className="relative mb-2 mt-3 flex w-full max-w-[420px] flex-col gap-2">
{urls.map((url) => (
<ReactPlayer
key={url}

View File

@ -1,13 +1,18 @@
import { useState } from 'react';
import { Button } from '@shared/button';
import { Image } from '@shared/image';
import { FULL_RELAYS } from '@stores/constants';
import { DEFAULT_AVATAR, FULL_RELAYS } from '@stores/constants';
import { useProfile } from '@utils/hooks/useProfile';
import { usePublish } from '@utils/hooks/usePublish';
import { displayNpub } from '@utils/shortenKey';
export function NoteReplyForm({ id }: { id: string }) {
export function NoteReplyForm({ id, pubkey }: { id: string; pubkey: string }) {
const publish = usePublish();
const { status, user } = useProfile(pubkey);
const [value, setValue] = useState('');
const submit = () => {
@ -21,23 +26,39 @@ export function NoteReplyForm({ id }: { id: string }) {
};
return (
<div className="flex flex-col">
<div className="flex flex-col rounded-xl border-t border-zinc-800/50 bg-zinc-900">
<div className="relative w-full flex-1 overflow-hidden">
<textarea
value={value}
onChange={(e) => setValue(e.target.value)}
placeholder="Reply to this thread..."
className="relative h-20 w-full resize-none rounded-md bg-transparent px-5 py-3 text-base !outline-none placeholder:text-zinc-400 dark:text-zinc-100 dark:placeholder:text-zinc-500"
className=" relative h-24 w-full resize-none rounded-md bg-transparent px-3 py-3 text-base !outline-none placeholder:text-zinc-400 dark:text-zinc-100 dark:placeholder:text-zinc-500"
spellCheck={false}
/>
</div>
<div className="w-full border-t border-zinc-800 px-5 py-3">
<div className="w-full border-t border-zinc-800 px-3 py-3">
{status === 'loading' ? (
<div>
<p>Loading...</p>
</div>
) : (
<div className="flex w-full items-center justify-between">
<div className="inline-flex items-center gap-3">
<div className="relative h-11 w-11 shrink-0 rounded">
<Image
src={user?.picture || user?.image}
fallback={DEFAULT_AVATAR}
alt={pubkey}
className="h-11 w-11 rounded-lg bg-white object-cover"
/>
</div>
<div>
<p className="mb-1 text-sm leading-none text-zinc-400">Reply as</p>
<p className="text-sm font-medium leading-none text-zinc-100">
{user?.nip05 || user?.name || displayNpub(pubkey, 16)}
</p>
</div>
</div>
<div className="flex items-center gap-2">
<Button
onClick={() => submit()}

View File

@ -1,25 +1,33 @@
import { NoteActions, NoteContent } from '@shared/notes';
import { useMemo } from 'react';
import { NoteActions, NoteContent, SubReply } from '@shared/notes';
import { User } from '@shared/user';
import { parser } from '@utils/parser';
import { LumeEvent } from '@utils/types';
export function Reply({ data }: { data: LumeEvent }) {
const content = parser(data);
export function Reply({ event }: { event: LumeEvent }) {
const content = useMemo(() => parser(event), [event]);
return (
<div className="h-min w-full py-1.5">
<div className="relative overflow-hidden rounded-xl border-t border-zinc-800/50 bg-zinc-900 px-3 pt-3">
<div className="relative flex flex-col">
<User pubkey={data.pubkey} time={data.created_at} />
<User pubkey={event.pubkey} time={event.created_at} />
<div className="relative z-20 -mt-6 flex items-start gap-3">
<div className="w-11 shrink-0" />
<div className="flex-1">
<NoteContent content={content} />
<NoteActions id={data.event_id || data.id} pubkey={data.pubkey} />
<NoteActions id={event.event_id || event.id} pubkey={event.pubkey} />
</div>
</div>
<div className="pb-3" />
<div>
{event.replies ? (
event.replies.map((sub) => <SubReply key={sub.id} event={sub} />)
) : (
<div className="pb-3" />
)}
</div>
</div>
</div>
</div>

View File

@ -3,7 +3,7 @@ import { useQuery } from '@tanstack/react-query';
import { useNDK } from '@libs/ndk/provider';
import { Reply } from '@shared/notes/replies/item';
import { NoteSkeleton, Reply } from '@shared/notes';
import { LumeEvent } from '@utils/types';
@ -15,26 +15,50 @@ export function RepliesList({ id }: { id: string }) {
{ kinds: [1], '#e': [id] },
{ since: 0 }
)) as unknown as LumeEvent[];
if (events.length > 0) {
const replies = new Set();
events.forEach((event) => {
const tags = event.tags.filter((el) => el[0] === 'e' && el[1] !== id);
if (tags.length > 0) {
tags.forEach((tag) => {
const rootIndex = events.findIndex((el) => el.id === tag[1]);
if (rootIndex) {
const rootEvent = events[rootIndex];
if (rootEvent.replies) {
rootEvent.replies.push(event);
} else {
rootEvent.replies = [event];
}
replies.add(event.id);
}
});
}
});
const cleanEvents = events.filter((ev) => !replies.has(ev.id));
return cleanEvents;
}
return events;
});
if (status === 'loading') {
return (
<div className="mt-3">
<div className="flex flex-col">
<div className="rounded-xl border-t border-zinc-800/50 bg-zinc-900 px-3 py-3">
<NoteSkeleton />
</div>
</div>
</div>
);
}
return (
<div className="mt-3">
<div className="mb-2">
<h5 className="text-lg font-semibold text-zinc-300">Replies</h5>
<h5 className="text-lg font-semibold text-zinc-300">{data.length} replies</h5>
</div>
<div className="flex flex-col">
{status === 'loading' ? (
<div className="flex gap-2 px-3 py-4">
<div className="relative h-9 w-9 shrink animate-pulse rounded-md bg-zinc-800" />
<div className="flex w-full flex-1 flex-col justify-center gap-1">
<div className="flex items-baseline gap-2 text-base">
<div className="h-2.5 w-20 animate-pulse rounded-sm bg-zinc-800" />
</div>
<div className="h-4 w-44 animate-pulse rounded-sm bg-zinc-800" />
</div>
</div>
) : data.length === 0 ? (
{data?.length === 0 ? (
<div className="px=3">
<div className="flex w-full items-center justify-center rounded-xl bg-zinc-900">
<div className="flex flex-col items-center justify-center gap-2 py-6">
@ -44,7 +68,7 @@ export function RepliesList({ id }: { id: string }) {
</div>
</div>
) : (
data.map((event: NDKEvent) => <Reply key={event.id} data={event} />)
data.reverse().map((event: NDKEvent) => <Reply key={event.id} event={event} />)
)}
</div>
</div>

View File

@ -0,0 +1,24 @@
import { useMemo } from 'react';
import { NoteActions, NoteContent } from '@shared/notes';
import { User } from '@shared/user';
import { parser } from '@utils/parser';
import { LumeEvent } from '@utils/types';
export function SubReply({ event }: { event: LumeEvent }) {
const content = useMemo(() => parser(event), [event]);
return (
<div className="relative 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="w-11 shrink-0" />
<div className="flex-1">
<NoteContent content={content} />
<NoteActions id={event.event_id || event.id} pubkey={event.pubkey} />
</div>
</div>
</div>
);
}

View File

@ -16,6 +16,7 @@ export function useProfile(pubkey: string, fallback?: string) {
const current = Math.floor(Date.now() / 1000);
const cache = await getUserMetadata(pubkey);
if (cache && parseInt(cache.created_at) + 86400 >= current) {
console.log('cache hit - ', cache);
return cache;
} else {
const filter: NDKFilter = { kinds: [0], authors: [pubkey] };
@ -24,6 +25,8 @@ export function useProfile(pubkey: string, fallback?: string) {
if (latest) {
await createMetadata(pubkey, pubkey, latest.content);
return JSON.parse(latest.content);
} else {
return null;
}
}
} else {

View File

@ -3,6 +3,7 @@ import { NDKEvent } from '@nostr-dev-kit/ndk';
export interface LumeEvent extends NDKEvent {
event_id?: string;
parent_id?: string;
replies?: LumeEvent[];
}
export interface Account {