add thread
This commit is contained in:
@@ -1 +0,0 @@
|
||||
export { LayoutNewsfeed as Layout } from "./layout";
|
||||
@@ -1,41 +0,0 @@
|
||||
import { Kind1 } from "@app/note/components/kind1";
|
||||
import { Kind1063 } from "@app/note/components/kind1063";
|
||||
import { NoteMetadata } from "@app/note/components/metadata";
|
||||
import { NoteParent } from "@app/note/components/parent";
|
||||
import { NoteDefaultUser } from "@app/note/components/user/default";
|
||||
import { NoteWrapper } from "@app/note/components/wrapper";
|
||||
|
||||
import { noteParser } from "@utils/parser";
|
||||
import { isTagsIncludeID } from "@utils/transform";
|
||||
|
||||
import { useMemo } from "react";
|
||||
|
||||
export function NoteBase({ event }: { event: any }) {
|
||||
const content = useMemo(() => noteParser(event), [event]);
|
||||
const checkParentID = isTagsIncludeID(event.parent_id, event.tags);
|
||||
|
||||
const href = event.parent_id
|
||||
? `/app/note?id=${event.parent_id}`
|
||||
: `/app/note?id=${event.event_id}`;
|
||||
|
||||
return (
|
||||
<NoteWrapper href={href} className="h-min w-full px-3 py-1.5">
|
||||
<div className="rounded-md bg-zinc-900 px-5 pt-5">
|
||||
{event.parent_id &&
|
||||
(event.parent_id !== event.event_id || checkParentID) ? (
|
||||
<NoteParent id={event.parent_id} />
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
<div className="flex flex-col">
|
||||
<NoteDefaultUser pubkey={event.pubkey} time={event.created_at} />
|
||||
<div className="-mt-5 pl-[49px]">
|
||||
{event.kind === 1 && <Kind1 content={content} />}
|
||||
{event.kind === 1063 && <Kind1063 metadata={event.tags} />}
|
||||
<NoteMetadata id={event.event_id} eventPubkey={event.pubkey} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</NoteWrapper>
|
||||
);
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
import { LinkPreview } from "./preview/link";
|
||||
import { MentionNote } from "@app/note/components/mentions/note";
|
||||
import { MentionUser } from "@app/note/components/mentions/user";
|
||||
import { ImagePreview } from "@app/note/components/preview/image";
|
||||
import { VideoPreview } from "@app/note/components/preview/video";
|
||||
import { truncateContent } from "@utils/transform";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import remarkGfm from "remark-gfm";
|
||||
|
||||
export function Kind1({
|
||||
content,
|
||||
truncate = false,
|
||||
}: { content: any; truncate?: boolean }) {
|
||||
return (
|
||||
<>
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[[remarkGfm]]}
|
||||
linkTarget="_blank"
|
||||
className="markdown"
|
||||
components={{
|
||||
em: ({ ...props }) => <MentionUser {...props} />,
|
||||
}}
|
||||
>
|
||||
{truncate ? truncateContent(content.parsed, 120) : content.parsed}
|
||||
</ReactMarkdown>
|
||||
{Array.isArray(content.images) && content.images.length ? (
|
||||
<ImagePreview urls={content.images} />
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
{Array.isArray(content.videos) && content.videos.length ? (
|
||||
<VideoPreview urls={content.videos} />
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
{Array.isArray(content.links) && content.links.length ? (
|
||||
<LinkPreview urls={content.links} />
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
{Array.isArray(content.notes) && content.notes.length ? (
|
||||
content.notes.map((note: string) => (
|
||||
<MentionNote key={note} id={note} />
|
||||
))
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
import { Image } from "@shared/image";
|
||||
|
||||
function isImage(url: string) {
|
||||
return /\.(jpg|jpeg|gif|png|webp|avif)$/.test(url);
|
||||
}
|
||||
|
||||
export function Kind1063({ metadata }: { metadata: string[] }) {
|
||||
const url = metadata[0][1];
|
||||
|
||||
return (
|
||||
<div className="mt-3">
|
||||
{isImage(url) && (
|
||||
<Image
|
||||
src={url}
|
||||
alt="image"
|
||||
className="h-auto w-full rounded-lg object-cover"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
import { Kind1 } from "@app/note/components/kind1";
|
||||
import { Kind1063 } from "@app/note/components/kind1063";
|
||||
import { NoteSkeleton } from "@app/note/components/skeleton";
|
||||
import { NoteQuoteUser } from "@app/note/components/user/quote";
|
||||
import { NoteWrapper } from "@app/note/components/wrapper";
|
||||
import { useEvent } from "@utils/hooks/useEvent";
|
||||
import { noteParser } from "@utils/parser";
|
||||
import { memo } from "react";
|
||||
|
||||
export const MentionNote = memo(function MentionNote({ id }: { id: string }) {
|
||||
const data = useEvent(id);
|
||||
|
||||
const kind1 = data?.kind === 1 ? noteParser(data) : null;
|
||||
const kind1063 = data?.kind === 1063 ? data.tags : null;
|
||||
|
||||
return (
|
||||
<NoteWrapper
|
||||
href={`/app/note?id=${id}`}
|
||||
className="mt-3 rounded-lg border border-zinc-800 px-3 py-3"
|
||||
>
|
||||
{data ? (
|
||||
<>
|
||||
<NoteQuoteUser pubkey={data.pubkey} time={data.created_at} />
|
||||
<div className="mt-2">
|
||||
{kind1 && <Kind1 content={kind1} truncate={true} />}
|
||||
{kind1063 && <Kind1063 metadata={kind1063} />}
|
||||
{!kind1 && !kind1063 && (
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="px-2 py-2 inline-flex flex-col gap-1 bg-zinc-800 rounded-md">
|
||||
<span className="text-zinc-500 text-sm font-medium leading-none">
|
||||
Kind: {data.kind}
|
||||
</span>
|
||||
<p className="text-fuchsia-500 text-sm leading-none">
|
||||
Lume isn't fully support this kind in newsfeed
|
||||
</p>
|
||||
</div>
|
||||
<div className="markdown">
|
||||
<p>{data.content}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<NoteSkeleton />
|
||||
)}
|
||||
</NoteWrapper>
|
||||
);
|
||||
});
|
||||
@@ -1,20 +0,0 @@
|
||||
import { useProfile } from "@utils/hooks/useProfile";
|
||||
import { shortenKey } from "@utils/shortenKey";
|
||||
|
||||
const hexRegex = /[0-9A-Fa-f]{6}/g;
|
||||
|
||||
export function MentionUser(props: { children: any[] }) {
|
||||
const pubkey = props.children[0].match(hexRegex) ? props.children[0] : null;
|
||||
|
||||
if (!pubkey) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { user } = useProfile(pubkey);
|
||||
|
||||
return (
|
||||
<span className="text-fuchsia-500">
|
||||
@{user?.name || user?.displayName || shortenKey(pubkey)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
import { NoteReply } from "@app/note/components/metadata/reply";
|
||||
import { NoteRepost } from "@app/note/components/metadata/repost";
|
||||
import { NoteZap } from "@app/note/components/metadata/zap";
|
||||
import { createReplyNote } from "@libs/storage";
|
||||
import { NDKEvent } from "@nostr-dev-kit/ndk";
|
||||
import { NDKSubscription } from "@nostr-dev-kit/ndk";
|
||||
import { RelayContext } from "@shared/relayProvider";
|
||||
import { decode } from "light-bolt11-decoder";
|
||||
import { useContext, useState } from "react";
|
||||
import useSWRSubscription from "swr/subscription";
|
||||
|
||||
export function NoteMetadata({
|
||||
id,
|
||||
eventPubkey,
|
||||
}: {
|
||||
id: string;
|
||||
eventPubkey: string;
|
||||
}) {
|
||||
const ndk = useContext(RelayContext);
|
||||
|
||||
const [replies, setReplies] = useState(0);
|
||||
const [reposts, setReposts] = useState(0);
|
||||
const [zaps, setZaps] = useState(0);
|
||||
|
||||
useSWRSubscription(id ? ["note-metadata", id] : null, () => {
|
||||
const sub: NDKSubscription = ndk.subscribe(
|
||||
{
|
||||
"#e": [id],
|
||||
kinds: [1, 6, 9735],
|
||||
limit: 20,
|
||||
},
|
||||
{ closeOnEose: false },
|
||||
);
|
||||
|
||||
sub.addListener("event", (event: NDKEvent) => {
|
||||
switch (event.kind) {
|
||||
case 1:
|
||||
setReplies((replies) => replies + 1);
|
||||
createReplyNote(
|
||||
event.id,
|
||||
event.pubkey,
|
||||
event.kind,
|
||||
event.tags,
|
||||
event.content,
|
||||
event.created_at,
|
||||
id,
|
||||
);
|
||||
break;
|
||||
case 6:
|
||||
setReposts((reposts) => reposts + 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",
|
||||
);
|
||||
setZaps(amount.value / 1000);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
sub.stop();
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="inline-flex items-center gap-2 w-full h-12 mt-4">
|
||||
<NoteReply id={id} replies={replies} />
|
||||
<NoteRepost id={id} pubkey={eventPubkey} reposts={reposts} />
|
||||
<div className="ml-auto">
|
||||
<NoteZap zaps={zaps} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,140 +0,0 @@
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
import { NDKEvent, NDKPrivateKeySigner } from "@nostr-dev-kit/ndk";
|
||||
import { ReplyIcon } from "@shared/icons";
|
||||
import { Image } from "@shared/image";
|
||||
import { RelayContext } from "@shared/relayProvider";
|
||||
import { useActiveAccount } from "@stores/accounts";
|
||||
import { dateToUnix } from "@utils/date";
|
||||
import { compactNumber } from "@utils/number";
|
||||
import { Fragment, useContext, useEffect, useState } from "react";
|
||||
|
||||
export function NoteReply({ id, replies }: { id: string; replies: number }) {
|
||||
const ndk = useContext(RelayContext);
|
||||
const account = useActiveAccount((state: any) => state.account);
|
||||
|
||||
const [count, setCount] = useState(0);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [value, setValue] = useState("");
|
||||
|
||||
const closeModal = () => {
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
const openModal = () => {
|
||||
setIsOpen(true);
|
||||
};
|
||||
|
||||
const submitEvent = () => {
|
||||
const signer = new NDKPrivateKeySigner(account.privkey);
|
||||
ndk.signer = signer;
|
||||
|
||||
const event = new NDKEvent(ndk);
|
||||
// build event
|
||||
event.content = value;
|
||||
event.kind = 1;
|
||||
event.created_at = dateToUnix();
|
||||
event.pubkey = account.pubkey;
|
||||
event.tags = [["e", id]];
|
||||
|
||||
// publish event
|
||||
event.publish();
|
||||
|
||||
// close modal
|
||||
setIsOpen(false);
|
||||
// increment replies
|
||||
setCount(count + 1);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setCount(replies);
|
||||
}, [replies]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => openModal()}
|
||||
className="w-20 group inline-flex items-center gap-1.5"
|
||||
>
|
||||
<ReplyIcon
|
||||
width={16}
|
||||
height={16}
|
||||
className="text-zinc-400 group-hover:text-green-400"
|
||||
/>
|
||||
<span className="text-base leading-none text-zinc-400 group-hover:text-white">
|
||||
{compactNumber.format(count)}
|
||||
</span>
|
||||
</button>
|
||||
<Transition appear show={isOpen} as={Fragment}>
|
||||
<Dialog as="div" className="relative z-10" onClose={closeModal}>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="fixed inset-0 z-50 bg-black bg-opacity-30 backdrop-blur-md" />
|
||||
</Transition.Child>
|
||||
<div className="fixed inset-0 z-50 flex min-h-full items-center justify-center">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 scale-95"
|
||||
enterTo="opacity-100 scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 scale-100"
|
||||
leaveTo="opacity-0 scale-95"
|
||||
>
|
||||
<Dialog.Panel className="relative flex h-min w-full max-w-lg flex-col gap-2 rounded-lg border border-zinc-800 bg-zinc-900 p-3">
|
||||
{/* root note */}
|
||||
{/* comment form */}
|
||||
<div className="flex gap-2">
|
||||
<div>
|
||||
<div className="relative h-11 w-11 shrink-0 overflow-hidden rounded-md border border-white/10">
|
||||
<Image
|
||||
src={account?.image}
|
||||
alt="user's avatar"
|
||||
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-blue-500 before:opacity-0 before:ring-2 before:ring-blue-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-blue-500/100 dark:focus-within:after:shadow-blue-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-base shadow-input shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-white 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 className="flex items-center gap-2 pl-2" />
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="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-base font-medium shadow-md shadow-fuchsia-900/50 hover:bg-fuchsia-600"
|
||||
>
|
||||
<span className="text-white drop-shadow">Send</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
import { NDKEvent, NDKPrivateKeySigner } from "@nostr-dev-kit/ndk";
|
||||
import { RepostIcon } from "@shared/icons";
|
||||
import { RelayContext } from "@shared/relayProvider";
|
||||
import { useActiveAccount } from "@stores/accounts";
|
||||
import { dateToUnix } from "@utils/date";
|
||||
import { compactNumber } from "@utils/number";
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
|
||||
export function NoteRepost({
|
||||
id,
|
||||
pubkey,
|
||||
reposts,
|
||||
}: { id: string; pubkey: string; reposts: number }) {
|
||||
const ndk = useContext(RelayContext);
|
||||
const account = useActiveAccount((state: any) => state.account);
|
||||
|
||||
const [count, setCount] = useState(0);
|
||||
|
||||
const submitEvent = (e: any) => {
|
||||
e.stopPropagation();
|
||||
|
||||
const signer = new NDKPrivateKeySigner(account.privkey);
|
||||
ndk.signer = signer;
|
||||
|
||||
const event = new NDKEvent(ndk);
|
||||
// build event
|
||||
event.content = "";
|
||||
event.kind = 6;
|
||||
event.created_at = dateToUnix();
|
||||
event.pubkey = account.pubkey;
|
||||
event.tags = [
|
||||
["e", id],
|
||||
["p", pubkey],
|
||||
];
|
||||
|
||||
// publish event
|
||||
event.publish();
|
||||
|
||||
// update state
|
||||
setCount(count + 1);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setCount(reposts);
|
||||
}, [reposts]);
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => submitEvent(e)}
|
||||
className="w-20 group inline-flex items-center gap-1.5"
|
||||
>
|
||||
<RepostIcon
|
||||
width={16}
|
||||
height={16}
|
||||
className="text-zinc-400 group-hover:text-blue-400"
|
||||
/>
|
||||
<span className="text-base leading-none text-zinc-400 group-hover:text-white">
|
||||
{compactNumber.format(count)}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import { compactNumber } from "@utils/number";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export function NoteZap({ zaps }: { zaps: number }) {
|
||||
const [count, setCount] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
setCount(zaps);
|
||||
}, [zaps]);
|
||||
|
||||
return (
|
||||
<button type="button" className="group inline-flex items-center gap-1.5">
|
||||
<span className="text-base leading-none text-zinc-400 group-hover:text-white">
|
||||
{compactNumber.format(count)} sats zapped
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
import { Kind1 } from "@app/note/components/kind1";
|
||||
import { Kind1063 } from "@app/note/components/kind1063";
|
||||
import { NoteMetadata } from "@app/note/components/metadata";
|
||||
import { NoteSkeleton } from "@app/note/components/skeleton";
|
||||
import { NoteDefaultUser } from "@app/note/components/user/default";
|
||||
import { useEvent } from "@utils/hooks/useEvent";
|
||||
import { noteParser } from "@utils/parser";
|
||||
import { memo } from "react";
|
||||
|
||||
export const NoteParent = memo(function NoteParent({ id }: { id: string }) {
|
||||
const data = useEvent(id);
|
||||
|
||||
const kind1 = data?.kind === 1 ? noteParser(data) : null;
|
||||
const kind1063 = data?.kind === 1063 ? data.tags : null;
|
||||
|
||||
return (
|
||||
<div className="relative overflow-hidden flex flex-col pb-6">
|
||||
<div className="absolute left-[18px] top-0 h-full w-0.5 bg-gradient-to-t from-zinc-800 to-zinc-600" />
|
||||
{data ? (
|
||||
<>
|
||||
<NoteDefaultUser pubkey={data.pubkey} time={data.created_at} />
|
||||
<div className="-mt-5 pl-[49px]">
|
||||
{kind1 && <Kind1 content={kind1} />}
|
||||
{kind1063 && <Kind1063 metadata={kind1063} />}
|
||||
{!kind1 && !kind1063 && (
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="px-2 py-2 inline-flex flex-col gap-1 bg-zinc-800 rounded-md">
|
||||
<span className="text-zinc-500 text-sm font-medium leading-none">
|
||||
Kind: {data.kind}
|
||||
</span>
|
||||
<p className="text-fuchsia-500 text-sm leading-none">
|
||||
Lume isn't fully support this kind in newsfeed
|
||||
</p>
|
||||
</div>
|
||||
<div className="markdown">
|
||||
<p>{data.content}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<NoteMetadata
|
||||
id={data.event_id || data.id}
|
||||
eventPubkey={data.pubkey}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<NoteSkeleton />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -1,19 +0,0 @@
|
||||
import { Image } from "@shared/image";
|
||||
|
||||
export function ImagePreview({ urls }: { urls: string[] }) {
|
||||
return (
|
||||
<div className="mt-3 overflow-hidden">
|
||||
<div className="flex flex-col gap-2">
|
||||
{urls.map((url) => (
|
||||
<div key={url} className="min-w-0 grow-0 shrink-0 basis-full">
|
||||
<Image
|
||||
src={url}
|
||||
alt="image"
|
||||
className="h-auto w-full rounded-lg object-cover"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
import { Image } from "@shared/image";
|
||||
import { useOpenGraph } from "@utils/hooks/useOpenGraph";
|
||||
|
||||
export function LinkPreview({ urls }: { urls: string[] }) {
|
||||
const domain = new URL(urls[0]);
|
||||
const { data, error, isLoading } = useOpenGraph(urls[0]);
|
||||
|
||||
return (
|
||||
<div className="mt-3 overflow-hidden rounded-lg bg-zinc-800">
|
||||
{error && <p>failed to load</p>}
|
||||
{isLoading || !data ? (
|
||||
<p>Loading...</p>
|
||||
) : (
|
||||
<a
|
||||
className="flex flex-col"
|
||||
href={urls[0]}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{data["og:image"] && (
|
||||
<Image
|
||||
src={data["og:image"]}
|
||||
alt={urls[0]}
|
||||
className="w-full h-auto border-t-lg object-cover"
|
||||
/>
|
||||
)}
|
||||
<div className="flex flex-col gap-2 px-3 py-3">
|
||||
<h5 className="leading-none font-medium text-zinc-200">
|
||||
{data["og:title"]}
|
||||
</h5>
|
||||
{data["og:description"] ? (
|
||||
<p className="leading-none text-sm text-zinc-400 line-clamp-3">
|
||||
{data["og:description"]}
|
||||
</p>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
<span className="mt-2.5 leading-none text-sm text-zinc-500">
|
||||
{domain.hostname}
|
||||
</span>
|
||||
</div>
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import { MediaOutlet, MediaPlayer } from "@vidstack/react";
|
||||
|
||||
export function VideoPreview({ urls }: { urls: string[] }) {
|
||||
return (
|
||||
<div
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onKeyDown={(e) => e.stopPropagation()}
|
||||
className="relative mt-2 flex w-full flex-col overflow-hidden rounded-lg bg-zinc-950"
|
||||
>
|
||||
<MediaPlayer src={urls[0]} poster="" controls>
|
||||
<MediaOutlet />
|
||||
</MediaPlayer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
import { RootNote } from "@app/note/components/rootNote";
|
||||
import { NoteRepostUser } from "@app/note/components/user/repost";
|
||||
import { NoteWrapper } from "@app/note/components/wrapper";
|
||||
import { getQuoteID } from "@utils/transform";
|
||||
|
||||
export function NoteQuoteRepost({ event }: { event: any }) {
|
||||
const rootID = getQuoteID(event.tags);
|
||||
|
||||
return (
|
||||
<NoteWrapper
|
||||
href={`/app/note?id=${rootID}`}
|
||||
className="h-min w-full px-3 py-1.5"
|
||||
>
|
||||
<div className="rounded-md bg-zinc-900">
|
||||
<div className="relative px-5 pb-5 pt-5">
|
||||
<div className="absolute left-[35px] top-[20px] h-[70px] w-0.5 bg-gradient-to-t from-zinc-800 to-zinc-600" />
|
||||
<NoteRepostUser pubkey={event.pubkey} time={event.created_at} />
|
||||
</div>
|
||||
<RootNote id={rootID} fallback={event.content} />
|
||||
</div>
|
||||
</NoteWrapper>
|
||||
);
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
import { NDKEvent, NDKPrivateKeySigner } from "@nostr-dev-kit/ndk";
|
||||
import { RelayContext } from "@shared/relayProvider";
|
||||
import { useActiveAccount } from "@stores/accounts";
|
||||
import { dateToUnix } from "@utils/date";
|
||||
import { useContext, useState } from "react";
|
||||
|
||||
export function NoteReplyForm({ id }: { id: string }) {
|
||||
const ndk = useContext(RelayContext);
|
||||
const account = useActiveAccount((state: any) => state.account);
|
||||
|
||||
const [value, setValue] = useState("");
|
||||
|
||||
const submitEvent = () => {
|
||||
const signer = new NDKPrivateKeySigner(account.privkey);
|
||||
ndk.signer = signer;
|
||||
|
||||
const event = new NDKEvent(ndk);
|
||||
// build event
|
||||
event.content = value;
|
||||
event.kind = 1;
|
||||
event.created_at = dateToUnix();
|
||||
event.pubkey = account.pubkey;
|
||||
event.tags = [["e", id]];
|
||||
|
||||
// publish event
|
||||
event.publish();
|
||||
|
||||
// reset form
|
||||
setValue("");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex gap-2.5 py-4">
|
||||
<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-base shadow-input shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-white 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 className="flex items-center gap-2">
|
||||
<button
|
||||
type="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-base 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>
|
||||
);
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import { Kind1 } from "@app/note/components/kind1";
|
||||
import { NoteMetadata } from "@app/note/components/metadata";
|
||||
import { NoteReplyUser } from "@app/note/components/user/reply";
|
||||
import { noteParser } from "@utils/parser";
|
||||
|
||||
export function Reply({ data }: { data: any }) {
|
||||
const content = noteParser(data);
|
||||
|
||||
return (
|
||||
<div className="flex h-min min-h-min w-full select-text flex-col px-3 pt-5 mb-3 rounded-md bg-zinc-900">
|
||||
<div className="flex flex-col">
|
||||
<NoteReplyUser pubkey={data.pubkey} time={data.created_at} />
|
||||
<div className="-mt-[20px] pl-[47px]">
|
||||
<Kind1 content={content} />
|
||||
<NoteMetadata id={data.id} eventPubkey={data.pubkey} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
import { NoteReplyForm } from "@app/note/components/replies/form";
|
||||
import { Reply } from "@app/note/components/replies/item";
|
||||
import { NostrEvent } from "@nostr-dev-kit/ndk";
|
||||
import { RelayContext } from "@shared/relayProvider";
|
||||
import { sortEvents } from "@utils/transform";
|
||||
import { useContext } from "react";
|
||||
import useSWRSubscription from "swr/subscription";
|
||||
|
||||
export function RepliesList({ id }: { id: string }) {
|
||||
const ndk = useContext(RelayContext);
|
||||
|
||||
const { data, error } = useSWRSubscription(
|
||||
id ? ["note-replies", id] : null,
|
||||
([, key], { next }) => {
|
||||
// subscribe to note
|
||||
const sub = ndk.subscribe(
|
||||
{
|
||||
"#e": [key],
|
||||
kinds: [1],
|
||||
limit: 20,
|
||||
},
|
||||
{
|
||||
closeOnEose: true,
|
||||
},
|
||||
);
|
||||
|
||||
sub.addListener("event", (event: NostrEvent) => {
|
||||
next(null, (prev: any) => (prev ? [...prev, event] : [event]));
|
||||
});
|
||||
|
||||
return () => {
|
||||
sub.stop();
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="mt-5">
|
||||
<div className="mb-2">
|
||||
<h5 className="text-lg font-semibold text-zinc-300">Replies</h5>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<NoteReplyForm id={id} />
|
||||
{error && <div>failed to load</div>}
|
||||
{!data ? (
|
||||
<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>
|
||||
) : (
|
||||
sortEvents(data).map((event: any) => {
|
||||
return <Reply key={event.id} data={event} />;
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
import { Kind1 } from "@app/note/components/kind1";
|
||||
import { Kind1063 } from "@app/note/components/kind1063";
|
||||
import { NoteMetadata } from "@app/note/components/metadata";
|
||||
import { NoteSkeleton } from "@app/note/components/skeleton";
|
||||
import { NoteDefaultUser } from "@app/note/components/user/default";
|
||||
import { NDKEvent } from "@nostr-dev-kit/ndk";
|
||||
import { RelayContext } from "@shared/relayProvider";
|
||||
import { noteParser } from "@utils/parser";
|
||||
import { memo, useContext } from "react";
|
||||
import useSWRSubscription from "swr/subscription";
|
||||
import { navigate } from "vite-plugin-ssr/client/router";
|
||||
|
||||
function isJSON(str: string) {
|
||||
try {
|
||||
JSON.parse(str);
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export const RootNote = memo(function RootNote({
|
||||
id,
|
||||
fallback,
|
||||
}: { id: string; fallback?: any }) {
|
||||
const ndk = useContext(RelayContext);
|
||||
const parseFallback = isJSON(fallback) ? JSON.parse(fallback) : null;
|
||||
|
||||
const { data, error } = useSWRSubscription(
|
||||
parseFallback ? null : id,
|
||||
(key, { next }) => {
|
||||
const sub = ndk.subscribe({
|
||||
ids: [key],
|
||||
});
|
||||
|
||||
sub.addListener("event", (event: NDKEvent) => {
|
||||
next(null, event);
|
||||
});
|
||||
|
||||
return () => {
|
||||
sub.stop();
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
const openNote = (e) => {
|
||||
const selection = window.getSelection();
|
||||
if (selection.toString().length === 0) {
|
||||
navigate(`/app/note?id=${id}`);
|
||||
} else {
|
||||
e.stopPropagation();
|
||||
}
|
||||
};
|
||||
|
||||
const kind1 = !error && data?.kind === 1 ? noteParser(data) : null;
|
||||
const kind1063 = !error && data?.kind === 1063 ? data.tags : null;
|
||||
|
||||
if (parseFallback) {
|
||||
const contentFallback = noteParser(parseFallback);
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={(e) => openNote(e)}
|
||||
onKeyDown={(e) => openNote(e)}
|
||||
className="flex flex-col px-5"
|
||||
>
|
||||
<NoteDefaultUser
|
||||
pubkey={parseFallback.pubkey}
|
||||
time={parseFallback.created_at}
|
||||
/>
|
||||
<div className="-mt-5 pl-[49px]">
|
||||
<Kind1 content={contentFallback} />
|
||||
<NoteMetadata
|
||||
id={parseFallback.id}
|
||||
eventPubkey={parseFallback.pubkey}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={(e) => openNote(e)}
|
||||
onKeyDown={(e) => openNote(e)}
|
||||
className="flex flex-col px-5"
|
||||
>
|
||||
{data ? (
|
||||
<>
|
||||
<NoteDefaultUser pubkey={data.pubkey} time={data.created_at} />
|
||||
<div className="-mt-5 pl-[49px]">
|
||||
{kind1 && <Kind1 content={kind1} />}
|
||||
{kind1063 && <Kind1063 metadata={kind1063} />}
|
||||
{!kind1 && !kind1063 && (
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="px-2 py-2 inline-flex flex-col gap-1 bg-zinc-800 rounded-md">
|
||||
<span className="text-zinc-500 text-sm font-medium leading-none">
|
||||
Kind: {data.kind}
|
||||
</span>
|
||||
<p className="text-fuchsia-500 text-sm leading-none">
|
||||
Lume isn't fully support this kind in newsfeed
|
||||
</p>
|
||||
</div>
|
||||
<div className="markdown">
|
||||
<p>{data.content}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<NoteMetadata id={data.id} eventPubkey={data.pubkey} />
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<NoteSkeleton />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -1,19 +0,0 @@
|
||||
export function NoteSkeleton() {
|
||||
return (
|
||||
<div className="flex h-min flex-col pb-3">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="relative h-11 w-11 shrink overflow-hidden rounded-md bg-zinc-700" />
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<div className="h-4 w-20 rounded bg-zinc-700" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="-mt-5 animate-pulse pl-[49px]">
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="h-4 w-full rounded-sm bg-zinc-700" />
|
||||
<div className="h-3 w-2/3 rounded-sm bg-zinc-700" />
|
||||
<div className="h-3 w-1/2 rounded-sm bg-zinc-700" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
import { Popover, Transition } from "@headlessui/react";
|
||||
import { Image } from "@shared/image";
|
||||
import { DEFAULT_AVATAR } from "@stores/constants";
|
||||
import { useProfile } from "@utils/hooks/useProfile";
|
||||
import { shortenKey } from "@utils/shortenKey";
|
||||
import dayjs from "dayjs";
|
||||
import relativeTime from "dayjs/plugin/relativeTime";
|
||||
import { Fragment } from "react";
|
||||
|
||||
dayjs.extend(relativeTime);
|
||||
|
||||
export function NoteDefaultUser({
|
||||
pubkey,
|
||||
time,
|
||||
}: { pubkey: string; time: number }) {
|
||||
const { user } = useProfile(pubkey);
|
||||
|
||||
return (
|
||||
<Popover className="relative flex items-start gap-3">
|
||||
<Popover.Button className="h-11 w-11 shrink-0 overflow-hidden rounded-md bg-zinc-900">
|
||||
<Image
|
||||
src={user?.image || DEFAULT_AVATAR}
|
||||
alt={pubkey}
|
||||
className="h-11 w-11 object-cover"
|
||||
/>
|
||||
</Popover.Button>
|
||||
<div className="flex flex-wrap items-baseline gap-1">
|
||||
<h5 className="max-w-[10rem] text-base font-semibold leading-none truncate">
|
||||
{user?.nip05 || user?.name || shortenKey(pubkey)}
|
||||
</h5>
|
||||
<span className="leading-none text-zinc-500">·</span>
|
||||
<span className="leading-none text-zinc-500">
|
||||
{dayjs().to(dayjs.unix(time), true)}
|
||||
</span>
|
||||
</div>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-200"
|
||||
enterFrom="opacity-0 translate-y-1"
|
||||
enterTo="opacity-100 translate-y-0"
|
||||
leave="transition ease-in duration-150"
|
||||
leaveFrom="opacity-100 translate-y-0"
|
||||
leaveTo="opacity-0 translate-y-1"
|
||||
>
|
||||
<Popover.Panel className="absolute left-0 top-8 z-10 mt-3 w-screen max-w-sm px-4 sm:px-0 lg:max-w-3xl">
|
||||
<div
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onKeyDown={(e) => e.stopPropagation()}
|
||||
className="w-full max-w-xs overflow-hidden rounded-lg border border-zinc-700 bg-zinc-900 shadow-input ring-1 ring-black ring-opacity-5"
|
||||
>
|
||||
<div className="flex items-start gap-2.5 border-b border-zinc-800 px-3 py-3">
|
||||
<Image
|
||||
src={user?.image || DEFAULT_AVATAR}
|
||||
alt={pubkey}
|
||||
className="h-14 w-14 shrink-0 rounded-lg object-cover"
|
||||
/>
|
||||
<div className="flex w-full flex-1 flex-col gap-2">
|
||||
<div className="inline-flex w-2/3 flex-col gap-0.5">
|
||||
<h5 className="text-base font-semibold leading-none">
|
||||
{user?.displayName || user?.name || (
|
||||
<div className="h-3 w-20 animate-pulse rounded-sm bg-zinc-700" />
|
||||
)}
|
||||
</h5>
|
||||
<span className="truncate text-base leading-none text-zinc-500">
|
||||
{user?.nip05 || shortenKey(pubkey)}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<p className="line-clamp-3 text-base leading-tight text-white">
|
||||
{user?.about}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 px-3 py-3">
|
||||
<a
|
||||
href={`/app/user?pubkey=${pubkey}`}
|
||||
className="inline-flex h-10 flex-1 items-center justify-center rounded-md bg-zinc-800 text-base font-medium"
|
||||
>
|
||||
View full profile
|
||||
</a>
|
||||
<a
|
||||
href={`/app/chat?pubkey=${pubkey}`}
|
||||
className="inline-flex h-10 flex-1 items-center justify-center rounded-md bg-zinc-800 text-base font-medium"
|
||||
>
|
||||
Message
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</Popover.Panel>
|
||||
</Transition>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
import { Image } from "@shared/image";
|
||||
import { DEFAULT_AVATAR } from "@stores/constants";
|
||||
import { useProfile } from "@utils/hooks/useProfile";
|
||||
import { shortenKey } from "@utils/shortenKey";
|
||||
import dayjs from "dayjs";
|
||||
import relativeTime from "dayjs/plugin/relativeTime";
|
||||
|
||||
dayjs.extend(relativeTime);
|
||||
|
||||
export function NoteQuoteUser({
|
||||
pubkey,
|
||||
time,
|
||||
}: {
|
||||
pubkey: string;
|
||||
time: number;
|
||||
}) {
|
||||
const { user } = useProfile(pubkey);
|
||||
|
||||
return (
|
||||
<div className="group flex items-center gap-2">
|
||||
<div className="relative h-6 w-6 shrink-0 rounded">
|
||||
<Image
|
||||
src={user?.image || DEFAULT_AVATAR}
|
||||
alt={pubkey}
|
||||
className="h-6 w-6 rounded object-cover"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex w-full flex-1 items-start justify-between">
|
||||
<div className="flex items-baseline gap-2 text-base">
|
||||
<span className="max-w-[10rem] truncate font-semibold leading-none text-white">
|
||||
{user?.nip05 || 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), true)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
import { Image } from "@shared/image";
|
||||
import { DEFAULT_AVATAR } from "@stores/constants";
|
||||
import { useProfile } from "@utils/hooks/useProfile";
|
||||
import { shortenKey } from "@utils/shortenKey";
|
||||
import dayjs from "dayjs";
|
||||
import relativeTime from "dayjs/plugin/relativeTime";
|
||||
|
||||
dayjs.extend(relativeTime);
|
||||
|
||||
export function NoteReplyUser({
|
||||
pubkey,
|
||||
time,
|
||||
}: {
|
||||
pubkey: string;
|
||||
time: number;
|
||||
}) {
|
||||
const { user } = useProfile(pubkey);
|
||||
|
||||
return (
|
||||
<div className="group flex items-start gap-2.5">
|
||||
<div className="relative h-11 w-11 shrink-0 rounded-md">
|
||||
<Image
|
||||
src={user?.image || DEFAULT_AVATAR}
|
||||
alt={pubkey}
|
||||
className="h-11 w-11 rounded-md object-cover"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex w-full flex-1 items-start justify-between">
|
||||
<div className="flex items-baseline gap-2 text-base">
|
||||
<span className="max-w-[10rem] truncate font-semibold leading-none text-white">
|
||||
{user?.nip05 || 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), true)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
import { Popover, Transition } from "@headlessui/react";
|
||||
import { Image } from "@shared/image";
|
||||
import { DEFAULT_AVATAR } from "@stores/constants";
|
||||
import { useProfile } from "@utils/hooks/useProfile";
|
||||
import { shortenKey } from "@utils/shortenKey";
|
||||
import dayjs from "dayjs";
|
||||
import relativeTime from "dayjs/plugin/relativeTime";
|
||||
import { Fragment } from "react";
|
||||
|
||||
dayjs.extend(relativeTime);
|
||||
|
||||
export function NoteRepostUser({
|
||||
pubkey,
|
||||
time,
|
||||
}: { pubkey: string; time: number }) {
|
||||
const { user } = useProfile(pubkey);
|
||||
|
||||
return (
|
||||
<Popover className="relative flex items-start gap-3">
|
||||
<Popover.Button className="h-11 w-11 shrink-0 overflow-hidden rounded-md bg-zinc-900">
|
||||
<Image
|
||||
src={user?.image || DEFAULT_AVATAR}
|
||||
alt={pubkey}
|
||||
className="h-11 w-11 rounded-md object-cover"
|
||||
/>
|
||||
</Popover.Button>
|
||||
<div className="flex flex-wrap items-baseline gap-1">
|
||||
<h5 className="max-w-[10rem] text-base font-semibold leading-none truncate">
|
||||
{user?.nip05 || user?.name || shortenKey(pubkey)}
|
||||
</h5>
|
||||
<span className="font-semibold leading-none text-fuchsia-500">
|
||||
{" "}
|
||||
reposted
|
||||
</span>
|
||||
<span className="leading-none text-zinc-500">·</span>
|
||||
<span className="leading-none text-zinc-500">
|
||||
{dayjs().to(dayjs.unix(time), true)}
|
||||
</span>
|
||||
</div>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-200"
|
||||
enterFrom="opacity-0 translate-y-1"
|
||||
enterTo="opacity-100 translate-y-0"
|
||||
leave="transition ease-in duration-150"
|
||||
leaveFrom="opacity-100 translate-y-0"
|
||||
leaveTo="opacity-0 translate-y-1"
|
||||
>
|
||||
<Popover.Panel className="absolute left-0 top-8 z-10 mt-3 w-screen max-w-sm px-4 sm:px-0 lg:max-w-3xl">
|
||||
<div
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onKeyDown={(e) => e.stopPropagation()}
|
||||
className="w-full max-w-xs overflow-hidden rounded-lg border border-zinc-700 bg-zinc-900 shadow-input ring-1 ring-black ring-opacity-5"
|
||||
>
|
||||
<div className="flex items-start gap-2.5 border-b border-zinc-800 px-3 py-3">
|
||||
<Image
|
||||
src={user?.image || DEFAULT_AVATAR}
|
||||
alt={pubkey}
|
||||
className="h-14 w-14 shrink-0 rounded-lg object-cover"
|
||||
/>
|
||||
<div className="flex w-full flex-1 flex-col gap-2">
|
||||
<div className="inline-flex w-2/3 flex-col gap-0.5">
|
||||
<h5 className="text-base font-semibold leading-none">
|
||||
{user?.displayName || user?.name || (
|
||||
<div className="h-3 w-20 animate-pulse rounded-sm bg-zinc-700" />
|
||||
)}
|
||||
</h5>
|
||||
<span className="truncate text-base leading-none text-zinc-500">
|
||||
{user?.nip05 || shortenKey(pubkey)}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<p className="line-clamp-3 text-base leading-tight text-white">
|
||||
{user?.about}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 px-3 py-3">
|
||||
<a
|
||||
href={`/app/user?pubkey=${pubkey}`}
|
||||
className="inline-flex h-10 flex-1 items-center justify-center rounded-md bg-zinc-800 text-base font-medium"
|
||||
>
|
||||
View full profile
|
||||
</a>
|
||||
<a
|
||||
href={`/app/chat?pubkey=${pubkey}`}
|
||||
className="inline-flex h-10 flex-1 items-center justify-center rounded-md bg-zinc-800 text-base font-medium"
|
||||
>
|
||||
Message
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</Popover.Panel>
|
||||
</Transition>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
import { navigate } from "vite-plugin-ssr/client/router";
|
||||
|
||||
export function NoteWrapper({
|
||||
children,
|
||||
href,
|
||||
className,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
href: string;
|
||||
className: string;
|
||||
}) {
|
||||
const openThread = (event: any, href: string) => {
|
||||
const selection = window.getSelection();
|
||||
if (selection.toString().length === 0) {
|
||||
navigate(href, { keepScrollPosition: true });
|
||||
} else {
|
||||
event.stopPropagation();
|
||||
}
|
||||
};
|
||||
|
||||
return <div className={className}>{children}</div>;
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import { MultiAccounts } from "@shared/multiAccounts";
|
||||
import { Navigation } from "@shared/navigation";
|
||||
|
||||
export function LayoutNewsfeed({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="flex w-screen h-screen">
|
||||
<div className="relative flex flex-row shrink-0">
|
||||
<MultiAccounts />
|
||||
<Navigation />
|
||||
</div>
|
||||
<div className="w-full h-full">{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
import { Kind1 } from "@app/note/components/kind1";
|
||||
import { NoteMetadata } from "@app/note/components/metadata";
|
||||
import { RepliesList } from "@app/note/components/replies/list";
|
||||
import { NoteDefaultUser } from "@app/note/components/user/default";
|
||||
import { getNoteByID } from "@libs/storage";
|
||||
import { usePageContext } from "@utils/hooks/usePageContext";
|
||||
import { noteParser } from "@utils/parser";
|
||||
import useSWR from "swr";
|
||||
|
||||
const fetcher = ([, id]) => getNoteByID(id);
|
||||
|
||||
export function Page() {
|
||||
const pageContext = usePageContext();
|
||||
const searchParams: any = pageContext.urlParsed.search;
|
||||
const noteID = searchParams.id;
|
||||
|
||||
const { data, error } = useSWR(["note", noteID], fetcher);
|
||||
|
||||
const content = !error && data ? noteParser(data) : null;
|
||||
|
||||
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">
|
||||
{!data || error ? (
|
||||
<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-base">
|
||||
<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-3 pt-3">
|
||||
<NoteDefaultUser pubkey={data.pubkey} time={data.created_at} />
|
||||
<div className="mt-3">
|
||||
<Kind1 content={content} />
|
||||
<NoteMetadata id={noteID} eventPubkey={data.pubkey} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<RepliesList id={noteID} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user