mirror of
https://github.com/luminous-devs/lume.git
synced 2024-09-19 11:43:30 +00:00
update replies
This commit is contained in:
parent
b3b790588a
commit
5606dcb32f
@ -1,10 +1,7 @@
|
|||||||
import { useQuery } from '@tanstack/react-query';
|
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
|
|
||||||
import { useLiveThread } from '@app/space/hooks/useLiveThread';
|
import { useLiveThread } from '@app/space/hooks/useLiveThread';
|
||||||
|
|
||||||
import { getNoteByID } from '@libs/storage';
|
|
||||||
|
|
||||||
import { NoteMetadata } from '@shared/notes/metadata';
|
import { NoteMetadata } from '@shared/notes/metadata';
|
||||||
import { NoteReplyForm } from '@shared/notes/replies/form';
|
import { NoteReplyForm } from '@shared/notes/replies/form';
|
||||||
import { RepliesList } from '@shared/notes/replies/list';
|
import { RepliesList } from '@shared/notes/replies/list';
|
||||||
@ -12,16 +9,12 @@ import { NoteSkeleton } from '@shared/notes/skeleton';
|
|||||||
import { User } from '@shared/user';
|
import { User } from '@shared/user';
|
||||||
|
|
||||||
import { useAccount } from '@utils/hooks/useAccount';
|
import { useAccount } from '@utils/hooks/useAccount';
|
||||||
import { parser } from '@utils/parser';
|
import { useEvent } from '@utils/hooks/useEvent';
|
||||||
|
|
||||||
export function NoteScreen() {
|
export function NoteScreen() {
|
||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
const { account } = useAccount();
|
const { account } = useAccount();
|
||||||
const { status, data } = useQuery(['thread', id], async () => {
|
const { status, data } = useEvent(id);
|
||||||
const res = await getNoteByID(id);
|
|
||||||
res['content'] = parser(res);
|
|
||||||
return res;
|
|
||||||
});
|
|
||||||
|
|
||||||
useLiveThread(id);
|
useLiveThread(id);
|
||||||
|
|
||||||
@ -39,7 +32,7 @@ export function NoteScreen() {
|
|||||||
<div className="rounded-md bg-zinc-900 px-5 pt-5">
|
<div className="rounded-md bg-zinc-900 px-5 pt-5">
|
||||||
<User pubkey={data.pubkey} time={data.created_at} />
|
<User pubkey={data.pubkey} time={data.created_at} />
|
||||||
<div className="mt-3">
|
<div className="mt-3">
|
||||||
<NoteMetadata id={data.event_id || id} eventPubkey={data.pubkey} />
|
<NoteMetadata id={data.event_id || id} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-3 rounded-md bg-zinc-900">
|
<div className="mt-3 rounded-md bg-zinc-900">
|
||||||
@ -48,7 +41,7 @@ export function NoteScreen() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="px-3">
|
<div className="px-3">
|
||||||
<RepliesList parent_id={id} />
|
<RepliesList id={id} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,31 +1,20 @@
|
|||||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { Link } from 'react-router-dom';
|
|
||||||
|
|
||||||
import { useLiveThread } from '@app/space/hooks/useLiveThread';
|
// import { useLiveThread } from '@app/space/hooks/useLiveThread';
|
||||||
|
import { removeBlock } from '@libs/storage';
|
||||||
|
|
||||||
import { getNoteByID, removeBlock } from '@libs/storage';
|
import { NoteContent, NoteStats, ThreadUser } from '@shared/notes';
|
||||||
|
|
||||||
import { NoteReplyForm } from '@shared/notes/replies/form';
|
|
||||||
import { RepliesList } from '@shared/notes/replies/list';
|
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 { User } from '@shared/user';
|
|
||||||
|
|
||||||
import { useAccount } from '@utils/hooks/useAccount';
|
import { useEvent } from '@utils/hooks/useEvent';
|
||||||
import { parser } from '@utils/parser';
|
|
||||||
import { Block } from '@utils/types';
|
import { Block } from '@utils/types';
|
||||||
|
|
||||||
export function ThreadBlock({ params }: { params: Block }) {
|
export function ThreadBlock({ params }: { params: Block }) {
|
||||||
useLiveThread(params.content);
|
|
||||||
|
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
const { account } = useAccount();
|
const { status, data } = useEvent(params.content);
|
||||||
const { status, data } = useQuery(['thread', params.content], async () => {
|
|
||||||
const res = await getNoteByID(params.content);
|
|
||||||
res['content'] = parser(res);
|
|
||||||
return res;
|
|
||||||
});
|
|
||||||
|
|
||||||
const block = useMutation({
|
const block = useMutation({
|
||||||
mutationFn: (id: string) => {
|
mutationFn: (id: string) => {
|
||||||
@ -36,33 +25,34 @@ export function ThreadBlock({ params }: { params: Block }) {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// subscribe to live reply
|
||||||
|
// useLiveThread(params.content);
|
||||||
|
|
||||||
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 title={params.title} onClick={() => block.mutate(params.id)} />
|
<TitleBar title={params.title} onClick={() => block.mutate(params.id)} />
|
||||||
<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-1.5 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="shadow-input rounded-md bg-zinc-900 px-3 py-3 shadow-black/20">
|
<div className="rounded-xl border-t border-zinc-800/50 bg-zinc-900 px-3 py-3">
|
||||||
<NoteSkeleton />
|
<NoteSkeleton />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="h-min w-full px-3 py-1.5">
|
<div className="h-min w-full px-3 py-1.5">
|
||||||
<div className="rounded-md bg-zinc-900 px-5 pt-5">
|
<div className="rounded-xl border-t border-zinc-800/50 bg-zinc-900 px-3 pt-3">
|
||||||
<User pubkey={data.pubkey} time={data.created_at} />
|
<ThreadUser pubkey={data.pubkey} time={data.created_at} />
|
||||||
<div className="mt-3">
|
<div className="mt-2">
|
||||||
<Link to={`/app/note/${params.content}`}>Focus</Link>
|
<NoteContent content={data.content} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<NoteStats id={data.id} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div className="mt-3 rounded-md bg-zinc-900">
|
|
||||||
{account && (
|
|
||||||
<NoteReplyForm rootID={params.content} userPubkey={account.pubkey} />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="px-3">
|
<div className="px-3">
|
||||||
<RepliesList parent_id={params.content} />
|
<RepliesList id={params.content} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -169,7 +169,7 @@ export async function createNote(
|
|||||||
event_id: string,
|
event_id: string,
|
||||||
pubkey: string,
|
pubkey: string,
|
||||||
kind: number,
|
kind: number,
|
||||||
tags: string[],
|
tags: string[][],
|
||||||
content: string,
|
content: string,
|
||||||
created_at: number
|
created_at: number
|
||||||
) {
|
) {
|
||||||
@ -186,7 +186,7 @@ export async function createNote(
|
|||||||
// get note replies
|
// get note replies
|
||||||
export async function getReplies(parent_id: string) {
|
export async function getReplies(parent_id: string) {
|
||||||
const db = await connect();
|
const db = await connect();
|
||||||
const result: any = await db.select(
|
const result: Array<LumeEvent> = await db.select(
|
||||||
`SELECT * FROM replies WHERE parent_id = "${parent_id}" ORDER BY created_at DESC;`
|
`SELECT * FROM replies WHERE parent_id = "${parent_id}" ORDER BY created_at DESC;`
|
||||||
);
|
);
|
||||||
return result;
|
return result;
|
||||||
|
@ -15,6 +15,7 @@ export * from './kinds/kind1063';
|
|||||||
export * from './metadata';
|
export * from './metadata';
|
||||||
export * from './users/mini';
|
export * from './users/mini';
|
||||||
export * from './users/repost';
|
export * from './users/repost';
|
||||||
|
export * from './users/thread';
|
||||||
export * from './kinds/thread';
|
export * from './kinds/thread';
|
||||||
export * from './kinds/repost';
|
export * from './kinds/repost';
|
||||||
export * from './kinds/sub';
|
export * from './kinds/sub';
|
||||||
@ -22,3 +23,4 @@ export * from './skeleton';
|
|||||||
export * from './actions';
|
export * from './actions';
|
||||||
export * from './content';
|
export * from './content';
|
||||||
export * from './hashtag';
|
export * from './hashtag';
|
||||||
|
export * from './stats';
|
||||||
|
@ -9,7 +9,7 @@ export function MentionUser({ pubkey }: { pubkey: string }) {
|
|||||||
type="button"
|
type="button"
|
||||||
className="break-words rounded bg-zinc-800 px-2 py-px text-sm font-normal text-blue-400 no-underline hover:bg-zinc-700 hover:text-blue-500"
|
className="break-words rounded bg-zinc-800 px-2 py-px text-sm font-normal text-blue-400 no-underline hover:bg-zinc-700 hover:text-blue-500"
|
||||||
>
|
>
|
||||||
@{user?.name || user?.displayName || shortenKey(pubkey)}
|
{'@' + user?.name || user?.displayName || shortenKey(pubkey)}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,17 +1,25 @@
|
|||||||
import { NoteMetadata } from '@shared/notes/metadata';
|
import { NoteActions, NoteContent } 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';
|
||||||
|
|
||||||
export function Reply({ data }: { data: any }) {
|
export function Reply({ data }: { data: LumeEvent }) {
|
||||||
const content = parser(data);
|
const content = parser(data);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mb-3 flex h-min min-h-min w-full select-text flex-col rounded-md bg-zinc-900 px-3 pt-5">
|
<div className="h-min w-full py-1.5">
|
||||||
<div className="flex flex-col">
|
<div className="relative overflow-hidden rounded-xl border-t border-zinc-800/50 bg-zinc-900 px-3 pt-3">
|
||||||
<User pubkey={data.pubkey} time={data.created_at} />
|
<div className="relative flex flex-col">
|
||||||
<div className="-mt-[20px] pl-[50px]">
|
<User pubkey={data.pubkey} time={data.created_at} />
|
||||||
<NoteMetadata id={data.event_id} eventPubkey={data.pubkey} />
|
<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} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="pb-3" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,17 +1,25 @@
|
|||||||
import { NDKEvent } from '@nostr-dev-kit/ndk';
|
import { NDKEvent } from '@nostr-dev-kit/ndk';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
|
||||||
import { getReplies } from '@libs/storage';
|
import { useNDK } from '@libs/ndk/provider';
|
||||||
|
|
||||||
import { Reply } from '@shared/notes/replies/item';
|
import { Reply } from '@shared/notes/replies/item';
|
||||||
|
|
||||||
export function RepliesList({ parent_id }: { parent_id: string }) {
|
import { LumeEvent } from '@utils/types';
|
||||||
const { status, data } = useQuery(['replies', parent_id], async () => {
|
|
||||||
return await getReplies(parent_id);
|
export function RepliesList({ id }: { id: string }) {
|
||||||
|
const { relayUrls, fetcher } = useNDK();
|
||||||
|
const { status, data } = useQuery(['thread', id], async () => {
|
||||||
|
const events = (await fetcher.fetchAllEvents(
|
||||||
|
relayUrls,
|
||||||
|
{ kinds: [1], '#e': [id] },
|
||||||
|
{ since: 0 }
|
||||||
|
)) as unknown as LumeEvent[];
|
||||||
|
return events;
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mt-5">
|
<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">Replies</h5>
|
||||||
</div>
|
</div>
|
||||||
@ -28,7 +36,7 @@ export function RepliesList({ parent_id }: { parent_id: string }) {
|
|||||||
</div>
|
</div>
|
||||||
) : data.length === 0 ? (
|
) : data.length === 0 ? (
|
||||||
<div className="px=3">
|
<div className="px=3">
|
||||||
<div className="flex w-full items-center justify-center rounded-md 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">
|
||||||
<h3 className="text-3xl">👋</h3>
|
<h3 className="text-3xl">👋</h3>
|
||||||
<p className="leading-none text-zinc-400">Share your thought on it...</p>
|
<p className="leading-none text-zinc-400">Share your thought on it...</p>
|
||||||
|
76
src/shared/notes/stats.tsx
Normal file
76
src/shared/notes/stats.tsx
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
import { NDKEvent, NDKFilter } from '@nostr-dev-kit/ndk';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { decode } from 'light-bolt11-decoder';
|
||||||
|
|
||||||
|
import { useNDK } from '@libs/ndk/provider';
|
||||||
|
|
||||||
|
import { LoaderIcon } from '@shared/icons';
|
||||||
|
|
||||||
|
export function NoteStats({ id }: { id: string }) {
|
||||||
|
const { ndk } = useNDK();
|
||||||
|
const { status, data } = useQuery(
|
||||||
|
['note-stats', id],
|
||||||
|
async () => {
|
||||||
|
let reactions = 0;
|
||||||
|
let reposts = 0;
|
||||||
|
let zaps = 0;
|
||||||
|
|
||||||
|
const filter: NDKFilter = {
|
||||||
|
'#e': [id],
|
||||||
|
kinds: [6, 7, 9735],
|
||||||
|
};
|
||||||
|
|
||||||
|
const events = await ndk.fetchEvents(filter);
|
||||||
|
events.forEach((event: NDKEvent) => {
|
||||||
|
switch (event.kind) {
|
||||||
|
case 6:
|
||||||
|
reposts += 1;
|
||||||
|
break;
|
||||||
|
case 7:
|
||||||
|
reactions += 1;
|
||||||
|
break;
|
||||||
|
case 9735: {
|
||||||
|
const bolt11 = event.tags.find((tag) => tag[0] === 'bolt11')[1];
|
||||||
|
if (bolt11) {
|
||||||
|
const decoded = decode(bolt11);
|
||||||
|
const amount = decoded.sections.find((item) => item.name === 'amount');
|
||||||
|
const sats = amount.value / 1000;
|
||||||
|
zaps += sats;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { reposts, reactions, zaps };
|
||||||
|
},
|
||||||
|
{ refetchOnWindowFocus: false, refetchOnReconnect: false }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (status === 'loading') {
|
||||||
|
return (
|
||||||
|
<div className="flex h-11 items-center">
|
||||||
|
<LoaderIcon className="h-4 w-4 animate-spin text-zinc-100" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex h-11 items-center gap-3">
|
||||||
|
<p className="inline-flex h-6 items-center justify-center gap-1 rounded bg-zinc-800 px-2 text-sm">
|
||||||
|
{data.reactions}
|
||||||
|
<span className="text-zinc-400">reactions</span>
|
||||||
|
</p>
|
||||||
|
<p className="inline-flex h-6 items-center justify-center gap-1 rounded bg-zinc-800 px-2 text-sm">
|
||||||
|
{data.reposts}
|
||||||
|
<span className="text-zinc-400">reposts</span>
|
||||||
|
</p>
|
||||||
|
<p className="inline-flex h-6 items-center justify-center gap-1 rounded bg-zinc-800 px-2 text-sm">
|
||||||
|
{data.zaps}
|
||||||
|
<span className="text-zinc-400">zaps</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
46
src/shared/notes/users/thread.tsx
Normal file
46
src/shared/notes/users/thread.tsx
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import { VerticalDotsIcon } from '@shared/icons';
|
||||||
|
import { Image } from '@shared/image';
|
||||||
|
|
||||||
|
import { DEFAULT_AVATAR } from '@stores/constants';
|
||||||
|
|
||||||
|
import { formatCreatedAt } from '@utils/createdAt';
|
||||||
|
import { useProfile } from '@utils/hooks/useProfile';
|
||||||
|
import { displayNpub } from '@utils/shortenKey';
|
||||||
|
|
||||||
|
export function ThreadUser({ pubkey, time }: { pubkey: string; time: number }) {
|
||||||
|
const { status, user } = useProfile(pubkey);
|
||||||
|
const createdAt = formatCreatedAt(time);
|
||||||
|
|
||||||
|
if (status === 'loading') {
|
||||||
|
return <div className="h-4 w-4 animate-pulse rounded bg-zinc-700"></div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Image
|
||||||
|
src={user?.picture || user?.image || DEFAULT_AVATAR}
|
||||||
|
fallback={DEFAULT_AVATAR}
|
||||||
|
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="truncate font-semibold leading-none text-zinc-100">
|
||||||
|
{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-zinc-800"
|
||||||
|
>
|
||||||
|
<VerticalDotsIcon className="h-4 w-4 rotate-90 transform text-zinc-200" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="inline-flex items-center gap-2">
|
||||||
|
<span className="leading-none text-zinc-500">{createdAt}</span>
|
||||||
|
<span className="leading-none text-zinc-500">·</span>
|
||||||
|
<span className="leading-none text-zinc-500">{displayNpub(pubkey, 16)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -4,3 +4,17 @@ export function shortenKey(pubkey: string) {
|
|||||||
const npub = nip19.npubEncode(pubkey);
|
const npub = nip19.npubEncode(pubkey);
|
||||||
return npub.substring(0, 16).concat('...');
|
return npub.substring(0, 16).concat('...');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function displayNpub(pubkey: string, len: number, separator?: string) {
|
||||||
|
const npub = nip19.npubEncode(pubkey) as string;
|
||||||
|
if (npub.length <= len) return npub;
|
||||||
|
|
||||||
|
separator = separator || ' ... ';
|
||||||
|
|
||||||
|
const sepLen = separator.length,
|
||||||
|
charsToShow = len - sepLen,
|
||||||
|
frontChars = Math.ceil(charsToShow / 2),
|
||||||
|
backChars = Math.floor(charsToShow / 2);
|
||||||
|
|
||||||
|
return npub.substr(0, frontChars) + separator + npub.substr(npub.length - backChars);
|
||||||
|
}
|
||||||
|
@ -47,7 +47,7 @@ export function arrayObjToPureArr(arr: any) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// get parent id from event tags
|
// get parent id from event tags
|
||||||
export function getParentID(arr: string[], fallback: string) {
|
export function getParentID(arr: string[][], fallback: string) {
|
||||||
const tags = destr(arr);
|
const tags = destr(arr);
|
||||||
let parentID = fallback;
|
let parentID = fallback;
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user