mirror of
https://github.com/luminous-devs/lume.git
synced 2024-09-19 19:46:34 +00: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 { NoteSkeleton } from '@shared/notes/skeleton';
|
||||||
import { TitleBar } from '@shared/titleBar';
|
import { TitleBar } from '@shared/titleBar';
|
||||||
|
|
||||||
|
import { useAccount } from '@utils/hooks/useAccount';
|
||||||
import { useEvent } from '@utils/hooks/useEvent';
|
import { useEvent } from '@utils/hooks/useEvent';
|
||||||
import { Block } from '@utils/types';
|
import { Block } from '@utils/types';
|
||||||
|
|
||||||
export function ThreadBlock({ params }: { params: Block }) {
|
export function ThreadBlock({ params }: { params: Block }) {
|
||||||
const { status, data } = useEvent(params.content);
|
const { status, data } = useEvent(params.content);
|
||||||
|
const { account } = useAccount();
|
||||||
|
|
||||||
// subscribe to live reply
|
// subscribe to live reply
|
||||||
// useLiveThread(params.content);
|
// useLiveThread(params.content);
|
||||||
@ -22,7 +24,7 @@ export function ThreadBlock({ params }: { params: Block }) {
|
|||||||
return (
|
return (
|
||||||
<div className="w-[400px] shrink-0 border-r border-zinc-900">
|
<div className="w-[400px] shrink-0 border-r border-zinc-900">
|
||||||
<TitleBar id={params.id} title={params.title} />
|
<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' ? (
|
{status === 'loading' ? (
|
||||||
<div className="px-3 py-1.5">
|
<div className="px-3 py-1.5">
|
||||||
<div className="rounded-xl border-t border-zinc-800/50 bg-zinc-900 px-3 py-3">
|
<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>
|
</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">
|
<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} />
|
<ThreadUser pubkey={data.pubkey} time={data.created_at} />
|
||||||
<div className="mt-2">
|
<div className="mt-2">
|
||||||
@ -44,7 +46,7 @@ export function ThreadBlock({ params }: { params: Block }) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="px-3">
|
<div className="px-3">
|
||||||
<NoteReplyForm id={params.content} />
|
<NoteReplyForm id={params.content} pubkey={account.pubkey} />
|
||||||
<RepliesList id={params.content} />
|
<RepliesList id={params.content} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -94,7 +94,7 @@ export function ActiveAccount({ data }: { data: any }) {
|
|||||||
return (
|
return (
|
||||||
<Link to={`/app/user/${data.pubkey}`} className="relative inline-block h-9 w-9">
|
<Link to={`/app/user/${data.pubkey}`} className="relative inline-block h-9 w-9">
|
||||||
<Image
|
<Image
|
||||||
src={user.image}
|
src={user?.picture || user?.image}
|
||||||
fallback={DEFAULT_AVATAR}
|
fallback={DEFAULT_AVATAR}
|
||||||
alt={data.npub}
|
alt={data.npub}
|
||||||
className="h-9 w-9 rounded-md object-cover"
|
className="h-9 w-9 rounded-md object-cover"
|
||||||
|
@ -23,7 +23,7 @@ export function NoteActions({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip.Provider>
|
<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">
|
<div className="inline-flex items-center gap-2">
|
||||||
<NoteReply id={id} pubkey={pubkey} />
|
<NoteReply id={id} pubkey={pubkey} />
|
||||||
<NoteReaction id={id} pubkey={pubkey} />
|
<NoteReaction id={id} pubkey={pubkey} />
|
||||||
|
@ -10,6 +10,7 @@ export * from './preview/video';
|
|||||||
export * from './replies/form';
|
export * from './replies/form';
|
||||||
export * from './replies/item';
|
export * from './replies/item';
|
||||||
export * from './replies/list';
|
export * from './replies/list';
|
||||||
|
export * from './replies/sub';
|
||||||
export * from './kinds/kind1';
|
export * from './kinds/kind1';
|
||||||
export * from './kinds/kind1063';
|
export * from './kinds/kind1063';
|
||||||
export * from './metadata';
|
export * from './metadata';
|
||||||
|
@ -29,7 +29,7 @@ export const MentionNote = memo(function MentionNote({ id }: { id: string }) {
|
|||||||
onKeyDown={(e) => openThread(e, id)}
|
onKeyDown={(e) => openThread(e, id)}
|
||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
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' ? (
|
{status === 'loading' ? (
|
||||||
<NoteSkeleton />
|
<NoteSkeleton />
|
||||||
|
@ -2,7 +2,7 @@ import { Image } from '@shared/image';
|
|||||||
|
|
||||||
export function ImagePreview({ urls, truncate }: { urls: string[]; truncate?: boolean }) {
|
export function ImagePreview({ urls, truncate }: { urls: string[]; truncate?: boolean }) {
|
||||||
return (
|
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">
|
<div className="flex flex-col gap-2">
|
||||||
{urls.map((url) => (
|
{urls.map((url) => (
|
||||||
<div key={url} className="relative min-w-0 shrink-0 grow-0 basis-full">
|
<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]);
|
const domain = new URL(urls[0]);
|
||||||
|
|
||||||
return (
|
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' ? (
|
{status === 'loading' ? (
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<div className="h-44 w-full animate-pulse bg-zinc-700" />
|
<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[] }) {
|
export function VideoPreview({ urls }: { urls: string[] }) {
|
||||||
return (
|
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) => (
|
{urls.map((url) => (
|
||||||
<ReactPlayer
|
<ReactPlayer
|
||||||
key={url}
|
key={url}
|
||||||
|
@ -1,13 +1,18 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
import { Button } from '@shared/button';
|
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 { 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 publish = usePublish();
|
||||||
|
|
||||||
|
const { status, user } = useProfile(pubkey);
|
||||||
const [value, setValue] = useState('');
|
const [value, setValue] = useState('');
|
||||||
|
|
||||||
const submit = () => {
|
const submit = () => {
|
||||||
@ -21,23 +26,39 @@ export function NoteReplyForm({ id }: { id: string }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
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">
|
<div className="relative w-full flex-1 overflow-hidden">
|
||||||
<textarea
|
<textarea
|
||||||
value={value}
|
value={value}
|
||||||
onChange={(e) => setValue(e.target.value)}
|
onChange={(e) => setValue(e.target.value)}
|
||||||
placeholder="Reply to this thread..."
|
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}
|
spellCheck={false}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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' ? (
|
{status === 'loading' ? (
|
||||||
<div>
|
<div>
|
||||||
<p>Loading...</p>
|
<p>Loading...</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex w-full items-center justify-between">
|
<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">
|
<div className="flex items-center gap-2">
|
||||||
<Button
|
<Button
|
||||||
onClick={() => submit()}
|
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 { User } from '@shared/user';
|
||||||
|
|
||||||
import { parser } from '@utils/parser';
|
import { parser } from '@utils/parser';
|
||||||
import { LumeEvent } from '@utils/types';
|
import { LumeEvent } from '@utils/types';
|
||||||
|
|
||||||
export function Reply({ data }: { data: LumeEvent }) {
|
export function Reply({ event }: { event: LumeEvent }) {
|
||||||
const content = parser(data);
|
const content = useMemo(() => parser(event), [event]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-min w-full py-1.5">
|
<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 overflow-hidden rounded-xl border-t border-zinc-800/50 bg-zinc-900 px-3 pt-3">
|
||||||
<div className="relative flex flex-col">
|
<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="relative z-20 -mt-6 flex items-start gap-3">
|
||||||
<div className="w-11 shrink-0" />
|
<div className="w-11 shrink-0" />
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<NoteContent content={content} />
|
<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>
|
</div>
|
||||||
|
<div>
|
||||||
|
{event.replies ? (
|
||||||
|
event.replies.map((sub) => <SubReply key={sub.id} event={sub} />)
|
||||||
|
) : (
|
||||||
<div className="pb-3" />
|
<div className="pb-3" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -3,7 +3,7 @@ import { useQuery } from '@tanstack/react-query';
|
|||||||
|
|
||||||
import { useNDK } from '@libs/ndk/provider';
|
import { useNDK } from '@libs/ndk/provider';
|
||||||
|
|
||||||
import { Reply } from '@shared/notes/replies/item';
|
import { NoteSkeleton, Reply } from '@shared/notes';
|
||||||
|
|
||||||
import { LumeEvent } from '@utils/types';
|
import { LumeEvent } from '@utils/types';
|
||||||
|
|
||||||
@ -15,26 +15,50 @@ export function RepliesList({ id }: { id: string }) {
|
|||||||
{ kinds: [1], '#e': [id] },
|
{ kinds: [1], '#e': [id] },
|
||||||
{ since: 0 }
|
{ since: 0 }
|
||||||
)) as unknown as LumeEvent[];
|
)) 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;
|
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 (
|
return (
|
||||||
<div className="mt-3">
|
<div className="mt-3">
|
||||||
<div className="mb-2">
|
<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>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
{status === 'loading' ? (
|
{data?.length === 0 ? (
|
||||||
<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 ? (
|
|
||||||
<div className="px=3">
|
<div className="px=3">
|
||||||
<div className="flex w-full items-center justify-center rounded-xl bg-zinc-900">
|
<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">
|
<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>
|
||||||
</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>
|
||||||
</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 current = Math.floor(Date.now() / 1000);
|
||||||
const cache = await getUserMetadata(pubkey);
|
const cache = await getUserMetadata(pubkey);
|
||||||
if (cache && parseInt(cache.created_at) + 86400 >= current) {
|
if (cache && parseInt(cache.created_at) + 86400 >= current) {
|
||||||
|
console.log('cache hit - ', cache);
|
||||||
return cache;
|
return cache;
|
||||||
} else {
|
} else {
|
||||||
const filter: NDKFilter = { kinds: [0], authors: [pubkey] };
|
const filter: NDKFilter = { kinds: [0], authors: [pubkey] };
|
||||||
@ -24,6 +25,8 @@ export function useProfile(pubkey: string, fallback?: string) {
|
|||||||
if (latest) {
|
if (latest) {
|
||||||
await createMetadata(pubkey, pubkey, latest.content);
|
await createMetadata(pubkey, pubkey, latest.content);
|
||||||
return JSON.parse(latest.content);
|
return JSON.parse(latest.content);
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} 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 {
|
export interface LumeEvent extends NDKEvent {
|
||||||
event_id?: string;
|
event_id?: string;
|
||||||
parent_id?: string;
|
parent_id?: string;
|
||||||
|
replies?: LumeEvent[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Account {
|
export interface Account {
|
||||||
|
Loading…
Reference in New Issue
Block a user