mirror of
https://github.com/luminous-devs/lume.git
synced 2024-09-19 19:46:34 +00:00
add single note page
This commit is contained in:
parent
2a1b64fdfe
commit
fdbdaaa384
@ -29,7 +29,7 @@ export default function ChatMessageUser({ pubkey, time }: { pubkey: string; time
|
|||||||
alt={pubkey}
|
alt={pubkey}
|
||||||
className="h-9 w-9 rounded-md object-cover"
|
className="h-9 w-9 rounded-md object-cover"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
fetchpriority="high"
|
decoding="async"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex w-full flex-1 items-start justify-between">
|
<div className="flex w-full flex-1 items-start justify-between">
|
||||||
|
@ -18,15 +18,10 @@ export const NoteBase = memo(function NoteBase({ event }: { event: any }) {
|
|||||||
return <></>;
|
return <></>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const openUserPage = (e) => {
|
const openNote = (e) => {
|
||||||
e.stopPropagation();
|
|
||||||
navigate(`/user?pubkey=${event.pubkey}`);
|
|
||||||
};
|
|
||||||
|
|
||||||
const openThread = (e) => {
|
|
||||||
const selection = window.getSelection();
|
const selection = window.getSelection();
|
||||||
if (selection.toString().length === 0) {
|
if (selection.toString().length === 0) {
|
||||||
navigate(`/newsfeed/note?id=${event.parent_id}`);
|
navigate(`/app/note?id=${event.parent_id}`);
|
||||||
} else {
|
} else {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}
|
}
|
||||||
@ -34,14 +29,12 @@ export const NoteBase = memo(function NoteBase({ event }: { event: any }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
onClick={(e) => openThread(e)}
|
onClick={(e) => openNote(e)}
|
||||||
className="relative z-10 flex h-min min-h-min w-full select-text flex-col border-b border-zinc-800 px-3 py-5 hover:bg-black/20"
|
className="relative z-10 flex h-min min-h-min w-full select-text flex-col border-b border-zinc-800 px-3 py-5 hover:bg-black/20"
|
||||||
>
|
>
|
||||||
{parentNote()}
|
{parentNote()}
|
||||||
<div className="relative z-10 flex flex-col">
|
<div className="relative z-10 flex flex-col">
|
||||||
<div onClick={(e) => openUserPage(e)}>
|
|
||||||
<NoteDefaultUser pubkey={event.pubkey} time={event.created_at} />
|
<NoteDefaultUser pubkey={event.pubkey} time={event.created_at} />
|
||||||
</div>
|
|
||||||
<div className="mt-1 pl-[52px]">
|
<div className="mt-1 pl-[52px]">
|
||||||
<div className="whitespace-pre-line break-words text-[15px] leading-tight text-zinc-100">{content}</div>
|
<div className="whitespace-pre-line break-words text-[15px] leading-tight text-zinc-100">{content}</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -16,7 +16,7 @@ export default function NoteMetadata({ id, eventPubkey }: { id: string; eventPub
|
|||||||
const [likes, setLikes] = useState(0);
|
const [likes, setLikes] = useState(0);
|
||||||
const [reposts, setReposts] = useState(0);
|
const [reposts, setReposts] = useState(0);
|
||||||
|
|
||||||
useSWRSubscription(id ? ['metadata', id] : null, ([, key], {}) => {
|
useSWRSubscription(id ? ['note-metadata', id] : null, ([, key], {}) => {
|
||||||
const unsubscribe = pool.subscribe(
|
const unsubscribe = pool.subscribe(
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
|
@ -69,7 +69,7 @@ export const NoteParent = memo(function NoteParent({ id }: { id: string }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div onClick={(e) => e.stopPropagation()} className="mt-5 pl-[52px]">
|
<div onClick={(e) => e.stopPropagation()} className="mt-5 pl-[52px]">
|
||||||
<NoteMetadata id={data.event_id} eventPubkey={data.pubkey} />
|
<NoteMetadata id={data.id} eventPubkey={data.pubkey} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
@ -5,6 +5,7 @@ import { READONLY_RELAYS } from '@lume/stores/constants';
|
|||||||
|
|
||||||
import { memo, useContext } from 'react';
|
import { memo, useContext } from 'react';
|
||||||
import useSWRSubscription from 'swr/subscription';
|
import useSWRSubscription from 'swr/subscription';
|
||||||
|
import { navigate } from 'vite-plugin-ssr/client/router';
|
||||||
|
|
||||||
export const NoteQuote = memo(function NoteQuote({ id }: { id: string }) {
|
export const NoteQuote = memo(function NoteQuote({ id }: { id: string }) {
|
||||||
const pool: any = useContext(RelayContext);
|
const pool: any = useContext(RelayContext);
|
||||||
@ -32,8 +33,20 @@ export const NoteQuote = memo(function NoteQuote({ id }: { id: string }) {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const openNote = (e) => {
|
||||||
|
const selection = window.getSelection();
|
||||||
|
if (selection.toString().length === 0) {
|
||||||
|
navigate(`/app/note?id=${id}`);
|
||||||
|
} else {
|
||||||
|
e.stopPropagation();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative mb-2 mt-3 rounded-lg border border-zinc-700 bg-zinc-800 p-2 py-3">
|
<div
|
||||||
|
onClick={(e) => openNote(e)}
|
||||||
|
className="relative mb-2 mt-3 rounded-lg border border-zinc-700 bg-zinc-800 p-2 py-3"
|
||||||
|
>
|
||||||
{error && <div>failed to load</div>}
|
{error && <div>failed to load</div>}
|
||||||
{!data ? (
|
{!data ? (
|
||||||
<div className="h-6 w-full animate-pulse select-text flex-col rounded bg-zinc-800"></div>
|
<div className="h-6 w-full animate-pulse select-text flex-col rounded bg-zinc-800"></div>
|
||||||
|
@ -35,10 +35,10 @@ export const RootNote = memo(function RootNote({ id, fallback }: { id: string; f
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
const openThread = (e) => {
|
const openNote = (e) => {
|
||||||
const selection = window.getSelection();
|
const selection = window.getSelection();
|
||||||
if (selection.toString().length === 0) {
|
if (selection.toString().length === 0) {
|
||||||
navigate(`/newsfeed/note?id=${id}`);
|
navigate(`/app/note?id=${id}`);
|
||||||
} else {
|
} else {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}
|
}
|
||||||
@ -46,14 +46,16 @@ export const RootNote = memo(function RootNote({ id, fallback }: { id: string; f
|
|||||||
|
|
||||||
if (parseFallback) {
|
if (parseFallback) {
|
||||||
return (
|
return (
|
||||||
<div onClick={(e) => openThread(e)} className="relative z-10 flex flex-col">
|
<div onClick={(e) => openNote(e)} className="relative z-10 flex flex-col">
|
||||||
<NoteDefaultUser pubkey={parseFallback.pubkey} time={parseFallback.created_at} />
|
<NoteDefaultUser pubkey={parseFallback.pubkey} time={parseFallback.created_at} />
|
||||||
<div className="mt-1 pl-[52px]">
|
<div className="mt-1 pl-[52px]">
|
||||||
<div className="whitespace-pre-line break-words text-[15px] leading-tight text-zinc-100">
|
<div className="whitespace-pre-line break-words text-[15px] leading-tight text-zinc-100">
|
||||||
{contentParser(parseFallback.content, parseFallback.tags)}
|
{contentParser(parseFallback.content, parseFallback.tags)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div onClick={(e) => e.stopPropagation()} className="mt-5 pl-[52px]"></div>
|
<div onClick={(e) => e.stopPropagation()} className="mt-5 pl-[52px]">
|
||||||
|
<NoteMetadata id={parseFallback.id} eventPubkey={parseFallback.pubkey} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -84,7 +86,7 @@ export const RootNote = memo(function RootNote({ id, fallback }: { id: string; f
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div onClick={(e) => openThread(e)} className="relative z-10 flex flex-col">
|
<div onClick={(e) => openNote(e)} className="relative z-10 flex flex-col">
|
||||||
<NoteDefaultUser pubkey={data.pubkey} time={data.created_at} />
|
<NoteDefaultUser pubkey={data.pubkey} time={data.created_at} />
|
||||||
<div className="mt-1 pl-[52px]">
|
<div className="mt-1 pl-[52px]">
|
||||||
<div className="whitespace-pre-line break-words text-[15px] leading-tight text-zinc-100">
|
<div className="whitespace-pre-line break-words text-[15px] leading-tight text-zinc-100">
|
||||||
@ -92,7 +94,7 @@ export const RootNote = memo(function RootNote({ id, fallback }: { id: string; f
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div onClick={(e) => e.stopPropagation()} className="mt-5 pl-[52px]">
|
<div onClick={(e) => e.stopPropagation()} className="mt-5 pl-[52px]">
|
||||||
<NoteMetadata id={data.event_id} eventPubkey={data.pubkey} />
|
<NoteMetadata id={data.id} eventPubkey={data.pubkey} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
1
src/app/note/_default.page.tsx
Normal file
1
src/app/note/_default.page.tsx
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { LayoutNewsfeed as Layout } from './layout';
|
71
src/app/note/components/form.tsx
Normal file
71
src/app/note/components/form.tsx
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
import { RelayContext } from '@lume/shared/relayProvider';
|
||||||
|
import { WRITEONLY_RELAYS } from '@lume/stores/constants';
|
||||||
|
import { dateToUnix } from '@lume/utils/getDate';
|
||||||
|
import { useActiveAccount } from '@lume/utils/hooks/useActiveAccount';
|
||||||
|
|
||||||
|
import { getEventHash, signEvent } from 'nostr-tools';
|
||||||
|
import { useContext, useState } from 'react';
|
||||||
|
|
||||||
|
export default function NoteReplyForm({ id }: { id: string }) {
|
||||||
|
const pool: any = useContext(RelayContext);
|
||||||
|
const { account, isLoading, isError } = useActiveAccount();
|
||||||
|
|
||||||
|
const [value, setValue] = useState('');
|
||||||
|
const profile = account ? JSON.parse(account.metadata) : null;
|
||||||
|
|
||||||
|
const submitEvent = () => {
|
||||||
|
if (!isLoading && !isError && account) {
|
||||||
|
const event: any = {
|
||||||
|
content: value,
|
||||||
|
created_at: dateToUnix(),
|
||||||
|
kind: 1,
|
||||||
|
pubkey: account.pubkey,
|
||||||
|
tags: [['e', id]],
|
||||||
|
};
|
||||||
|
event.id = getEventHash(event);
|
||||||
|
event.sig = signEvent(event, account.privkey);
|
||||||
|
|
||||||
|
// publish note
|
||||||
|
pool.publish(event, WRITEONLY_RELAYS);
|
||||||
|
// reset form
|
||||||
|
setValue('');
|
||||||
|
} else {
|
||||||
|
console.log('error');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex gap-3 px-5 py-4">
|
||||||
|
<div>
|
||||||
|
<div className="relative h-9 w-9 shrink-0 overflow-hidden rounded-md">
|
||||||
|
<img src={profile?.picture} alt={account?.pubkey} className="h-9 w-9 rounded-md object-cover" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="relative h-24 w-full flex-1 overflow-hidden before:pointer-events-none before:absolute before:-inset-1 before:rounded-[11px] before:border before:border-fuchsia-500 before:opacity-0 before:ring-2 before:ring-fuchsia-500/20 before:transition after:pointer-events-none after:absolute after:inset-px after:rounded-[7px] after:shadow-highlight after:shadow-white/5 after:transition focus-within:before:opacity-100 focus-within:after:shadow-fuchsia-500/100 dark:focus-within:after:shadow-fuchsia-500/20">
|
||||||
|
<div>
|
||||||
|
<textarea
|
||||||
|
name="content"
|
||||||
|
onChange={(e) => setValue(e.target.value)}
|
||||||
|
placeholder="Reply to this thread..."
|
||||||
|
className="relative h-24 w-full resize-none rounded-md border border-black/5 px-3.5 py-3 text-sm shadow-input shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-zinc-200 dark:shadow-black/10 dark:placeholder:text-zinc-500"
|
||||||
|
spellCheck={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="absolute bottom-2 w-full px-2">
|
||||||
|
<div className="flex w-full items-center justify-between bg-zinc-800">
|
||||||
|
<div className="flex items-center gap-2 divide-x divide-zinc-700"></div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() => submitEvent()}
|
||||||
|
disabled={value.length === 0 ? true : false}
|
||||||
|
className="inline-flex h-8 w-16 items-center justify-center rounded-md bg-fuchsia-500 px-4 text-sm font-medium shadow-button hover:bg-fuchsia-600 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
|
>
|
||||||
|
Reply
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
63
src/app/note/components/replies.tsx
Normal file
63
src/app/note/components/replies.tsx
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import NoteReplyForm from '@lume/app/note/components/form';
|
||||||
|
import NoteReply from '@lume/app/note/components/reply';
|
||||||
|
import { RelayContext } from '@lume/shared/relayProvider';
|
||||||
|
import { READONLY_RELAYS } from '@lume/stores/constants';
|
||||||
|
import { sortEvents } from '@lume/utils/transform';
|
||||||
|
|
||||||
|
import { useContext } from 'react';
|
||||||
|
import useSWRSubscription from 'swr/subscription';
|
||||||
|
|
||||||
|
export default function NoteReplies({ id }: { id: string }) {
|
||||||
|
const pool: any = useContext(RelayContext);
|
||||||
|
|
||||||
|
const { data, error } = useSWRSubscription(id ? ['note-replies', id] : null, ([, key], { next }) => {
|
||||||
|
// subscribe to note
|
||||||
|
const unsubscribe = pool.subscribe(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
'#e': [key],
|
||||||
|
since: 0,
|
||||||
|
kinds: [1],
|
||||||
|
limit: 20,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
READONLY_RELAYS,
|
||||||
|
(event: any) => {
|
||||||
|
next(null, (prev: any) => (prev ? [...prev, event] : [event]));
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unsubscribe();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mt-5">
|
||||||
|
<div className="mb-2">
|
||||||
|
<h5 className="text-lg font-semibold text-zinc-300">Replies</h5>
|
||||||
|
</div>
|
||||||
|
<div className="rounded-lg border border-zinc-800 bg-zinc-900 shadow-input shadow-black/20">
|
||||||
|
<div className="flex flex-col divide-y divide-zinc-800">
|
||||||
|
<NoteReplyForm id={id} />
|
||||||
|
{error && <div>failed to load</div>}
|
||||||
|
{!data ? (
|
||||||
|
<div className="flex gap-2 px-5 py-4">
|
||||||
|
<div className="relative h-9 w-9 shrink animate-pulse rounded-md bg-zinc-800"></div>
|
||||||
|
<div className="flex w-full flex-1 flex-col justify-center gap-1">
|
||||||
|
<div className="flex items-baseline gap-2 text-sm">
|
||||||
|
<div className="h-2.5 w-20 animate-pulse rounded bg-zinc-800"></div>
|
||||||
|
</div>
|
||||||
|
<div className="h-4 w-44 animate-pulse rounded bg-zinc-800"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
sortEvents(data).map((event: any) => {
|
||||||
|
return <NoteReply key={event.id} data={event} />;
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
18
src/app/note/components/reply.tsx
Normal file
18
src/app/note/components/reply.tsx
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { contentParser } from '@lume/app/newsfeed/components/contentParser';
|
||||||
|
|
||||||
|
import NoteReplyUser from './user';
|
||||||
|
|
||||||
|
export default function NoteReply({ data }: { data: any }) {
|
||||||
|
const content = contentParser(data.content, data.tags);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex h-min min-h-min w-full select-text flex-col px-5 py-3.5 hover:bg-black/20">
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<NoteReplyUser pubkey={data.pubkey} time={data.created_at} />
|
||||||
|
<div className="-mt-[17px] pl-[48px]">
|
||||||
|
<div className="whitespace-pre-line break-words break-words text-sm leading-tight">{content}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
48
src/app/note/components/user.tsx
Normal file
48
src/app/note/components/user.tsx
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import { DEFAULT_AVATAR, IMGPROXY_URL } from '@lume/stores/constants';
|
||||||
|
import { useProfile } from '@lume/utils/hooks/useProfile';
|
||||||
|
import { shortenKey } from '@lume/utils/shortenKey';
|
||||||
|
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||||
|
|
||||||
|
dayjs.extend(relativeTime);
|
||||||
|
|
||||||
|
export default function NoteReplyUser({ pubkey, time }: { pubkey: string; time: number }) {
|
||||||
|
const { user, isError, isLoading } = useProfile(pubkey);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="group flex items-start gap-3">
|
||||||
|
{isError || isLoading ? (
|
||||||
|
<>
|
||||||
|
<div className="relative h-9 w-9 shrink animate-pulse rounded-md bg-zinc-800"></div>
|
||||||
|
<div className="flex w-full flex-1 items-start justify-between">
|
||||||
|
<div className="flex items-baseline gap-2 text-sm">
|
||||||
|
<div className="h-4 w-20 animate-pulse rounded bg-zinc-800"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div className="relative h-9 w-9 shrink rounded-md">
|
||||||
|
<img
|
||||||
|
src={`${IMGPROXY_URL}/rs:fit:100:100/plain/${user?.picture ? user.picture : DEFAULT_AVATAR}`}
|
||||||
|
alt={pubkey}
|
||||||
|
className="h-9 w-9 rounded-md object-cover"
|
||||||
|
loading="lazy"
|
||||||
|
decoding="async"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex w-full flex-1 items-start justify-between">
|
||||||
|
<div className="flex items-baseline gap-2 text-sm">
|
||||||
|
<span className="font-semibold leading-none text-zinc-200 group-hover:underline">
|
||||||
|
{user?.display_name || user?.name || shortenKey(pubkey)}
|
||||||
|
</span>
|
||||||
|
<span className="leading-none text-zinc-500">·</span>
|
||||||
|
<span className="leading-none text-zinc-500">{dayjs().to(dayjs.unix(time))}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
29
src/app/note/layout.tsx
Normal file
29
src/app/note/layout.tsx
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import AppHeader from '@lume/shared/appHeader';
|
||||||
|
import MultiAccounts from '@lume/shared/multiAccounts';
|
||||||
|
import Navigation from '@lume/shared/navigation';
|
||||||
|
|
||||||
|
export function LayoutNewsfeed({ children }: { children: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<div className="h-screen w-screen bg-zinc-50 text-zinc-900 dark:bg-zinc-950 dark:text-white">
|
||||||
|
<div className="flex h-screen w-full flex-col">
|
||||||
|
<div
|
||||||
|
data-tauri-drag-region
|
||||||
|
className="relative h-11 shrink-0 border-b border-zinc-100 bg-white dark:border-zinc-900 dark:bg-black"
|
||||||
|
>
|
||||||
|
<AppHeader />
|
||||||
|
</div>
|
||||||
|
<div className="relative flex min-h-0 w-full flex-1">
|
||||||
|
<div className="relative w-[68px] shrink-0 border-r border-zinc-900">
|
||||||
|
<MultiAccounts />
|
||||||
|
</div>
|
||||||
|
<div className="grid w-full grid-cols-4 xl:grid-cols-5">
|
||||||
|
<div className="scrollbar-hide col-span-1 overflow-y-auto overflow-x-hidden border-r border-zinc-900">
|
||||||
|
<Navigation />
|
||||||
|
</div>
|
||||||
|
<div className="col-span-3 overflow-hidden xl:col-span-4">{children}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
87
src/app/note/pages/index.page.tsx
Normal file
87
src/app/note/pages/index.page.tsx
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
import { contentParser } from '@lume/app/newsfeed/components/contentParser';
|
||||||
|
import NoteMetadata from '@lume/app/newsfeed/components/note/metadata';
|
||||||
|
import { NoteDefaultUser } from '@lume/app/newsfeed/components/user/default';
|
||||||
|
import NoteReplies from '@lume/app/note/components/replies';
|
||||||
|
import { RelayContext } from '@lume/shared/relayProvider';
|
||||||
|
import { READONLY_RELAYS } from '@lume/stores/constants';
|
||||||
|
import { usePageContext } from '@lume/utils/hooks/usePageContext';
|
||||||
|
|
||||||
|
import { useContext } from 'react';
|
||||||
|
import useSWRSubscription from 'swr/subscription';
|
||||||
|
|
||||||
|
export function Page() {
|
||||||
|
const pool: any = useContext(RelayContext);
|
||||||
|
const pageContext = usePageContext();
|
||||||
|
const searchParams: any = pageContext.urlParsed.search;
|
||||||
|
|
||||||
|
const noteID = searchParams.id;
|
||||||
|
|
||||||
|
const { data, error } = useSWRSubscription(noteID ? ['note', noteID] : null, ([, key], { next }) => {
|
||||||
|
// subscribe to note
|
||||||
|
const unsubscribe = pool.subscribe(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
ids: [key],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
READONLY_RELAYS,
|
||||||
|
(event: { id: string; pubkey: string }) => {
|
||||||
|
next(null, event);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unsubscribe();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="scrollbar-hide h-full w-full overflow-y-auto">
|
||||||
|
<div className="p-3">
|
||||||
|
<div className="relative w-full rounded-lg border border-zinc-800 bg-zinc-900 shadow-input shadow-black/20">
|
||||||
|
{error && <div>failed to load</div>}
|
||||||
|
{!data ? (
|
||||||
|
<div className="animated-pulse p-3">
|
||||||
|
<div className="flex items-start gap-2">
|
||||||
|
<div className="relative h-11 w-11 shrink overflow-hidden rounded-md bg-zinc-700" />
|
||||||
|
<div className="flex w-full flex-1 items-start justify-between">
|
||||||
|
<div className="flex w-full items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2 text-sm">
|
||||||
|
<div className="h-4 w-16 rounded bg-zinc-700" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-3">
|
||||||
|
<div className="flex flex-col gap-6">
|
||||||
|
<div className="h-16 w-full rounded bg-zinc-700" />
|
||||||
|
<div className="flex items-center gap-8">
|
||||||
|
<div className="h-4 w-12 rounded bg-zinc-700" />
|
||||||
|
<div className="h-4 w-12 rounded bg-zinc-700" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div className="relative z-10 flex flex-col">
|
||||||
|
<div className="px-5 pt-5">
|
||||||
|
<NoteDefaultUser pubkey={data.pubkey} time={data.created_at} />
|
||||||
|
<div className="mt-3">
|
||||||
|
<div className="whitespace-pre-line break-words text-[15px] leading-tight text-zinc-100">
|
||||||
|
{contentParser(data.content, data.tags)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div onClick={(e) => e.stopPropagation()} className="mt-5 border-t border-zinc-800 px-5 py-5">
|
||||||
|
<NoteMetadata id={noteID} eventPubkey={data.pubkey} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<NoteReplies id={noteID} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -1,69 +0,0 @@
|
|||||||
import { RelayContext } from '@lume/shared/relayProvider';
|
|
||||||
import { WRITEONLY_RELAYS } from '@lume/stores/constants';
|
|
||||||
import { dateToUnix } from '@lume/utils/getDate';
|
|
||||||
import { useActiveAccount } from '@lume/utils/hooks/useActiveAccount';
|
|
||||||
|
|
||||||
import { getEventHash, signEvent } from 'nostr-tools';
|
|
||||||
import { useContext, useState } from 'react';
|
|
||||||
|
|
||||||
export default function FormComment({ eventID }: { eventID: any }) {
|
|
||||||
const pool: any = useContext(RelayContext);
|
|
||||||
const { account } = useActiveAccount();
|
|
||||||
|
|
||||||
const [value, setValue] = useState('');
|
|
||||||
const profile = JSON.parse(account.metadata);
|
|
||||||
|
|
||||||
const submitEvent = () => {
|
|
||||||
const event: any = {
|
|
||||||
content: value,
|
|
||||||
created_at: dateToUnix(),
|
|
||||||
kind: 1,
|
|
||||||
pubkey: account.pubkey,
|
|
||||||
tags: [['e', eventID]],
|
|
||||||
};
|
|
||||||
event.id = getEventHash(event);
|
|
||||||
event.sig = signEvent(event, account.privkey);
|
|
||||||
|
|
||||||
// publish note
|
|
||||||
pool.publish(event, WRITEONLY_RELAYS);
|
|
||||||
// send notification
|
|
||||||
// sendNotification('Comment has been published successfully');
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="p-3">
|
|
||||||
<div className="flex gap-1">
|
|
||||||
<div>
|
|
||||||
<div className="relative h-11 w-11 shrink-0 overflow-hidden rounded-md border border-white/10">
|
|
||||||
<img src={profile?.picture} alt={account.pubkey} className="h-11 w-11 rounded-md object-cover" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="relative h-24 w-full flex-1 overflow-hidden before:pointer-events-none before:absolute before:-inset-1 before:rounded-[11px] before:border before:border-fuchsia-500 before:opacity-0 before:ring-2 before:ring-fuchsia-500/20 before:transition after:pointer-events-none after:absolute after:inset-px after:rounded-[7px] after:shadow-highlight after:shadow-white/5 after:transition focus-within:before:opacity-100 focus-within:after:shadow-fuchsia-500/100 dark:focus-within:after:shadow-fuchsia-500/20">
|
|
||||||
<div>
|
|
||||||
<textarea
|
|
||||||
name="content"
|
|
||||||
onChange={(e) => setValue(e.target.value)}
|
|
||||||
placeholder="Send your comment"
|
|
||||||
className="relative h-24 w-full resize-none rounded-md border border-black/5 px-3.5 py-3 text-sm shadow-input shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-zinc-200 dark:shadow-black/10 dark:placeholder:text-zinc-500"
|
|
||||||
spellCheck={false}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="absolute bottom-2 w-full px-2">
|
|
||||||
<div className="flex w-full items-center justify-between bg-zinc-800">
|
|
||||||
<div className="flex items-center gap-2 divide-x divide-zinc-700"></div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<button
|
|
||||||
onClick={() => submitEvent()}
|
|
||||||
disabled={value.length === 0 ? true : false}
|
|
||||||
className="inline-flex h-8 w-16 items-center justify-center rounded-md bg-fuchsia-500 px-4 text-sm font-medium shadow-button hover:bg-fuchsia-600 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50"
|
|
||||||
>
|
|
||||||
Send
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,13 +1,6 @@
|
|||||||
import { atom } from 'jotai';
|
import { atom } from 'jotai';
|
||||||
import { atomWithReset } from 'jotai/utils';
|
import { atomWithReset } from 'jotai/utils';
|
||||||
|
|
||||||
// notes
|
|
||||||
export const notesAtom = atom([]);
|
|
||||||
export const filteredNotesAtom = atom((get) => {
|
|
||||||
const notes = get(notesAtom);
|
|
||||||
return notes.filter((item, index) => index === notes.findIndex((other) => item.parent_id === other.parent_id));
|
|
||||||
});
|
|
||||||
|
|
||||||
// note content
|
// note content
|
||||||
export const noteContentAtom = atomWithReset('');
|
export const noteContentAtom = atomWithReset('');
|
||||||
|
|
||||||
|
@ -70,10 +70,10 @@ export const getQuoteID = (arr: string[]) => {
|
|||||||
return quoteID;
|
return quoteID;
|
||||||
};
|
};
|
||||||
|
|
||||||
// sort messages by timestamp
|
// sort events by timestamp
|
||||||
export const sortMessages = (arr: any) => {
|
export const sortEvents = (arr: any) => {
|
||||||
arr.sort((a, b) => {
|
arr.sort((a, b) => {
|
||||||
return b.created_at - a.created_at;
|
return a.created_at - b.created_at;
|
||||||
});
|
});
|
||||||
|
|
||||||
return arr;
|
return arr;
|
||||||
|
Loading…
Reference in New Issue
Block a user