mirror of
https://github.com/lumehq/lume.git
synced 2025-03-28 02:31:49 +01:00
render reply and sub reply accordingly
This commit is contained in:
parent
22c1eaa541
commit
29d40ed406
@ -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>
|
||||
|
@ -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"
|
||||
|
@ -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} />
|
||||
|
@ -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';
|
||||
|
@ -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 />
|
||||
|
@ -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">
|
||||
|
@ -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" />
|
||||
|
@ -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}
|
||||
|
@ -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()}
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
24
src/shared/notes/replies/sub.tsx
Normal file
24
src/shared/notes/replies/sub.tsx
Normal 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>
|
||||
);
|
||||
}
|
@ -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 {
|
||||
|
1
src/utils/types.d.ts
vendored
1
src/utils/types.d.ts
vendored
@ -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 {
|
||||
|
Loading…
x
Reference in New Issue
Block a user