wip: update design
This commit is contained in:
@@ -298,9 +298,9 @@ export function ReplyForm({
|
||||
|
||||
return (
|
||||
<div className={cn("flex gap-3", className)}>
|
||||
<User.Provider pubkey={ark.account.pubkey}>
|
||||
<User.Provider pubkey={ark.account.npub}>
|
||||
<User.Root>
|
||||
<User.Avatar className="size-9 shrink-0 rounded-lg object-cover" />
|
||||
<User.Avatar className="size-10 shrink-0 rounded-full object-cover" />
|
||||
</User.Root>
|
||||
</User.Provider>
|
||||
<div className="flex-1">
|
||||
|
||||
@@ -6,6 +6,7 @@ export * from "./column";
|
||||
// Note Primities
|
||||
export * from "./note/primitives/text";
|
||||
export * from "./note/primitives/repost";
|
||||
export * from "./note/primitives/thread";
|
||||
|
||||
// Deprecated
|
||||
export * from "./routes/event";
|
||||
|
||||
@@ -1,22 +1,50 @@
|
||||
import { ArrowDownIcon, ArrowUpIcon } from "@lume/icons";
|
||||
import { useState } from "react";
|
||||
import { useNoteContext } from "../provider";
|
||||
import { useArk } from "@lume/ark";
|
||||
import { cn } from "@lume/utils";
|
||||
|
||||
export function NoteReaction() {
|
||||
const ark = useArk();
|
||||
const event = useNoteContext();
|
||||
|
||||
const [reaction, setReaction] = useState<"+" | "-">(null);
|
||||
|
||||
const up = async () => {
|
||||
const res = await ark.upvote(event.id, event.pubkey);
|
||||
if (res) setReaction("+");
|
||||
};
|
||||
|
||||
const down = async () => {
|
||||
const res = await ark.downvote(event.id, event.pubkey);
|
||||
if (res) setReaction("-");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="inline-flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex size-7 items-center justify-center rounded-full bg-neutral-100 text-neutral-700 hover:bg-blue-500 hover:text-white dark:bg-neutral-900 dark:text-neutral-300"
|
||||
onClick={up}
|
||||
disabled={!!reaction}
|
||||
className={cn(
|
||||
"inline-flex size-7 items-center justify-center rounded-full",
|
||||
reaction === "+"
|
||||
? "bg-blue-500 text-white"
|
||||
: "bg-neutral-100 text-neutral-700 hover:bg-blue-500 hover:text-white dark:bg-neutral-900 dark:text-neutral-300",
|
||||
)}
|
||||
>
|
||||
<ArrowUpIcon className="size-4" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex size-7 items-center justify-center rounded-full bg-neutral-100 text-neutral-700 hover:bg-blue-500 hover:text-white dark:bg-neutral-900 dark:text-neutral-300"
|
||||
onClick={down}
|
||||
disabled={!!reaction}
|
||||
className={cn(
|
||||
"inline-flex size-7 items-center justify-center rounded-full",
|
||||
reaction === "-"
|
||||
? "bg-blue-500 text-white"
|
||||
: "bg-neutral-100 text-neutral-700 hover:bg-blue-500 hover:text-white dark:bg-neutral-900 dark:text-neutral-300",
|
||||
)}
|
||||
>
|
||||
<ArrowDownIcon className="size-4" />
|
||||
</button>
|
||||
|
||||
@@ -7,8 +7,10 @@ import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { toast } from "sonner";
|
||||
import { useNoteContext } from "../provider";
|
||||
import { useArk } from "@lume/ark";
|
||||
|
||||
export function NoteRepost() {
|
||||
const ark = useArk();
|
||||
const event = useNoteContext();
|
||||
const setEditorValue = useSetAtom(editorValueAtom);
|
||||
const setIsEditorOpen = useSetAtom(editorAtom);
|
||||
@@ -23,7 +25,7 @@ export function NoteRepost() {
|
||||
setLoading(true);
|
||||
|
||||
// repost
|
||||
await event.repost(true);
|
||||
await ark.repost(event.id, event.pubkey);
|
||||
|
||||
// update state
|
||||
setLoading(false);
|
||||
|
||||
@@ -39,9 +39,7 @@ export function NoteContent({ className }: { className?: string }) {
|
||||
const richContent = useMemo(() => {
|
||||
if (event.kind !== Kind.Text) return content;
|
||||
|
||||
let parsedContent: string | ReactNode[] = stripHtml(
|
||||
content.replace(/\n{2,}\s*/g, "\n"),
|
||||
).result;
|
||||
let parsedContent: string | ReactNode[] = stripHtml(content).result;
|
||||
let linkPreview: string = undefined;
|
||||
let images: string[] = [];
|
||||
let videos: string[] = [];
|
||||
@@ -176,7 +174,7 @@ export function NoteContent({ className }: { className?: string }) {
|
||||
);
|
||||
|
||||
parsedContent = reactStringReplace(parsedContent, "\n", () => {
|
||||
return <div key={nanoid()} className="h-3" />;
|
||||
return <div key={nanoid()} />;
|
||||
});
|
||||
|
||||
if (typeof parsedContent[0] === "string") {
|
||||
|
||||
@@ -21,10 +21,7 @@ export function MentionNote({
|
||||
const richContent = useMemo(() => {
|
||||
if (!data) return "";
|
||||
|
||||
let parsedContent: string | ReactNode[] = data.content.replace(
|
||||
/\n+/g,
|
||||
"\n",
|
||||
);
|
||||
let parsedContent: string | ReactNode[] = data.content;
|
||||
|
||||
const text = parsedContent as string;
|
||||
const words = text.split(/( |\n)/);
|
||||
@@ -106,11 +103,11 @@ export function MentionNote({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="my-1 flex w-full cursor-default flex-col rounded-lg border border-black/5 bg-neutral-100 dark:border-white/5 dark:bg-neutral-900">
|
||||
<div className="my-1.5 flex w-full cursor-default flex-col rounded-xl bg-neutral-100 pt-1 ring-1 ring-black/5 dark:bg-neutral-900 dark:ring-white/5">
|
||||
<User.Provider pubkey={data.pubkey}>
|
||||
<User.Root className="flex h-10 items-center gap-2 px-3">
|
||||
<User.Avatar className="size-6 shrink-0 rounded-md object-cover" />
|
||||
<div className="inline-flex flex-1 gap-2">
|
||||
<User.Avatar className="size-6 shrink-0 rounded-full object-cover" />
|
||||
<div className="inline-flex flex-1 items-center gap-2">
|
||||
<User.Name className="font-semibold text-neutral-900 dark:text-neutral-100" />
|
||||
<span className="text-neutral-600 dark:text-neutral-400">·</span>
|
||||
<User.Time
|
||||
@@ -127,16 +124,10 @@ export function MentionNote({
|
||||
<div className="flex h-10 items-center justify-between px-3">
|
||||
<a
|
||||
href={`/events/${data.id}`}
|
||||
className="text-sm text-blue-500 hover:text-blue-600"
|
||||
className="text-blue-500 hover:text-blue-600"
|
||||
>
|
||||
{t("note.showMore")}
|
||||
</a>
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex size-6 items-center justify-center rounded-md bg-neutral-200 text-neutral-600 hover:bg-neutral-300 dark:bg-neutral-800 dark:text-neutral-400 dark:hover:bg-neutral-700"
|
||||
>
|
||||
<PinIcon className="size-4" />
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="h-3" />
|
||||
|
||||
@@ -32,8 +32,11 @@ export function NoteMenu() {
|
||||
return (
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger asChild>
|
||||
<button type="button">
|
||||
<HorizontalDotsIcon className="size-4 hover:text-blue-500 dark:text-neutral-200" />
|
||||
<button
|
||||
type="button"
|
||||
className="text-neutral-500 hover:text-blue-500 dark:text-neutral-400"
|
||||
>
|
||||
<HorizontalDotsIcon className="size-5" />
|
||||
</button>
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Portal>
|
||||
|
||||
@@ -5,57 +5,60 @@ import { download } from "@tauri-apps/plugin-upload";
|
||||
import { SyntheticEvent, useState } from "react";
|
||||
|
||||
export function ImagePreview({ url }: { url: string }) {
|
||||
const [downloaded, setDownloaded] = useState(false);
|
||||
const [downloaded, setDownloaded] = useState(false);
|
||||
|
||||
const downloadImage = async (e: { stopPropagation: () => void }) => {
|
||||
try {
|
||||
e.stopPropagation();
|
||||
const downloadImage = async (e: { stopPropagation: () => void }) => {
|
||||
try {
|
||||
e.stopPropagation();
|
||||
|
||||
const downloadDirPath = await downloadDir();
|
||||
const filename = url.substring(url.lastIndexOf("/") + 1);
|
||||
await download(url, `${downloadDirPath}/${filename}`);
|
||||
const downloadDirPath = await downloadDir();
|
||||
const filename = url.substring(url.lastIndexOf("/") + 1);
|
||||
await download(url, `${downloadDirPath}/${filename}`);
|
||||
|
||||
setDownloaded(true);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
};
|
||||
setDownloaded(true);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
};
|
||||
|
||||
const open = async () => {
|
||||
const name = new URL(url).pathname.split("/").pop();
|
||||
return new WebviewWindow("image-viewer", {
|
||||
url,
|
||||
title: name,
|
||||
});
|
||||
};
|
||||
const open = async () => {
|
||||
const name = new URL(url).pathname.split("/").pop();
|
||||
return new WebviewWindow("image-viewer", {
|
||||
url,
|
||||
title: name,
|
||||
});
|
||||
};
|
||||
|
||||
const fallback = (event: SyntheticEvent<HTMLImageElement, Event>) => {
|
||||
event.currentTarget.src = "/fallback-image.jpg";
|
||||
};
|
||||
const fallback = (event: SyntheticEvent<HTMLImageElement, Event>) => {
|
||||
event.currentTarget.src = "/fallback-image.jpg";
|
||||
};
|
||||
|
||||
return (
|
||||
// biome-ignore lint/a11y/useKeyWithClickEvents: <explanation>
|
||||
<div onClick={open} className="relative mt-1 mb-2.5 group">
|
||||
<img
|
||||
src={url}
|
||||
alt={url}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
style={{ contentVisibility: "auto" }}
|
||||
onError={fallback}
|
||||
className="object-cover w-full h-auto border rounded-xl border-neutral-200/50 dark:border-neutral-800/50"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => downloadImage(e)}
|
||||
className="absolute z-10 items-center justify-center hidden size-10 bg-white/10 text-black/70 backdrop-blur-xl rounded-lg right-2 top-2 group-hover:inline-flex hover:bg-blue-500 hover:text-white"
|
||||
>
|
||||
{downloaded ? (
|
||||
<CheckCircleIcon className="size-5" />
|
||||
) : (
|
||||
<DownloadIcon className="size-5" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
// biome-ignore lint/a11y/useKeyWithClickEvents: <explanation>
|
||||
<div
|
||||
onClick={open}
|
||||
className="group relative my-1.5 rounded-xl ring-1 ring-black/5 dark:ring-white/5"
|
||||
>
|
||||
<img
|
||||
src={url}
|
||||
alt={url}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
style={{ contentVisibility: "auto" }}
|
||||
onError={fallback}
|
||||
className="h-auto w-full rounded-xl object-cover"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => downloadImage(e)}
|
||||
className="absolute right-2 top-2 z-10 hidden size-10 items-center justify-center rounded-lg bg-white/20 text-black/70 backdrop-blur-2xl hover:bg-blue-500 hover:text-white group-hover:inline-flex"
|
||||
>
|
||||
{downloaded ? (
|
||||
<CheckCircleIcon className="size-5" />
|
||||
) : (
|
||||
<DownloadIcon className="size-5" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ export function LinkPreview({ url }: { url: string }) {
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="mb-2.5 mt-1 flex w-full flex-col overflow-hidden rounded-xl border border-black/5 bg-neutral-100 dark:border-white/5 dark:bg-neutral-900">
|
||||
<div className="my-1.5 flex w-full flex-col overflow-hidden rounded-xl bg-neutral-100 ring-1 ring-black/5 dark:bg-neutral-900 dark:ring-white/5">
|
||||
<div className="h-48 w-full shrink-0 animate-pulse bg-neutral-300 dark:bg-neutral-700" />
|
||||
<div className="flex flex-col gap-2 px-3 py-3">
|
||||
<div className="h-3 w-2/3 animate-pulse rounded bg-neutral-300 dark:bg-neutral-700" />
|
||||
@@ -29,7 +29,7 @@ export function LinkPreview({ url }: { url: string }) {
|
||||
href={url}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-blue-500 hover:text-blue-600"
|
||||
className="inline-block text-blue-500 hover:text-blue-600"
|
||||
>
|
||||
{url}
|
||||
</a>
|
||||
@@ -42,7 +42,7 @@ export function LinkPreview({ url }: { url: string }) {
|
||||
href={url}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-blue-500 hover:text-blue-600"
|
||||
className="inline-block text-blue-500 hover:text-blue-600"
|
||||
>
|
||||
{url}
|
||||
</a>
|
||||
@@ -54,7 +54,7 @@ export function LinkPreview({ url }: { url: string }) {
|
||||
href={url}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="mb-2.5 mt-1 flex w-full flex-col overflow-hidden rounded-xl border border-black/5 bg-neutral-100 dark:border-white/5 dark:bg-neutral-900"
|
||||
className="my-1.5 flex w-full flex-col overflow-hidden rounded-xl bg-neutral-100 ring-1 ring-black/5 dark:bg-neutral-900 dark:ring-white/5"
|
||||
>
|
||||
{isImage(data.image) ? (
|
||||
<img
|
||||
|
||||
@@ -1,30 +1,30 @@
|
||||
import {
|
||||
MediaControlBar,
|
||||
MediaController,
|
||||
MediaMuteButton,
|
||||
MediaPlayButton,
|
||||
MediaTimeDisplay,
|
||||
MediaTimeRange,
|
||||
MediaControlBar,
|
||||
MediaController,
|
||||
MediaMuteButton,
|
||||
MediaPlayButton,
|
||||
MediaTimeDisplay,
|
||||
MediaTimeRange,
|
||||
} from "media-chrome/dist/react";
|
||||
|
||||
export function VideoPreview({ url }: { url: string }) {
|
||||
return (
|
||||
<div className="mt-1 mb-2.5 w-full rounded-xl overflow-hidden">
|
||||
<MediaController>
|
||||
<video
|
||||
slot="media"
|
||||
src={url}
|
||||
preload="auto"
|
||||
muted
|
||||
className="w-full h-auto"
|
||||
/>
|
||||
<MediaControlBar>
|
||||
<MediaPlayButton />
|
||||
<MediaTimeRange />
|
||||
<MediaTimeDisplay showDuration />
|
||||
<MediaMuteButton />
|
||||
</MediaControlBar>
|
||||
</MediaController>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div className="my-1.5 w-full overflow-hidden rounded-xl ring-1 ring-black/5 dark:ring-white/5">
|
||||
<MediaController>
|
||||
<video
|
||||
slot="media"
|
||||
src={url}
|
||||
preload="auto"
|
||||
muted
|
||||
className="h-auto w-full rounded-xl"
|
||||
/>
|
||||
<MediaControlBar>
|
||||
<MediaPlayButton />
|
||||
<MediaTimeRange />
|
||||
<MediaTimeDisplay showDuration />
|
||||
<MediaMuteButton />
|
||||
</MediaControlBar>
|
||||
</MediaController>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -11,11 +11,11 @@ export function ThreadNote({ eventId }: { eventId: string }) {
|
||||
|
||||
return (
|
||||
<Note.Provider event={data}>
|
||||
<Note.Root className="flex flex-col rounded-xl bg-neutral-50 dark:bg-neutral-950">
|
||||
<div className="flex h-16 items-center justify-between px-3">
|
||||
<Note.Root className="flex flex-col">
|
||||
<div className="flex h-16 items-center justify-between">
|
||||
<User.Provider pubkey={data.pubkey}>
|
||||
<User.Root className="flex h-16 flex-1 items-center gap-3">
|
||||
<User.Avatar className="size-10 shrink-0 rounded-lg object-cover ring-1 ring-neutral-200/50 dark:ring-neutral-800/50" />
|
||||
<User.Avatar className="size-11 shrink-0 rounded-full object-cover ring-1 ring-neutral-200/50 dark:ring-neutral-800/50" />
|
||||
<div className="flex flex-1 flex-col">
|
||||
<User.Name className="font-semibold text-neutral-900 dark:text-neutral-100" />
|
||||
<div className="inline-flex items-center gap-2 text-sm text-neutral-600 dark:text-neutral-400">
|
||||
@@ -29,9 +29,9 @@ export function ThreadNote({ eventId }: { eventId: string }) {
|
||||
<Note.Menu />
|
||||
</div>
|
||||
<Note.Thread className="mb-2" />
|
||||
<Note.Content className="min-w-0 px-3" />
|
||||
<div className="flex h-14 items-center justify-between px-3">
|
||||
<Note.Pin />
|
||||
<Note.Content className="min-w-0" />
|
||||
<div className="flex h-14 items-center justify-between">
|
||||
<Note.Reaction />
|
||||
<div className="inline-flex items-center gap-4">
|
||||
<Note.Repost />
|
||||
<Note.Zap />
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { PinIcon } from "@lume/icons";
|
||||
import { cn } from "@lume/utils";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Note } from ".";
|
||||
@@ -19,7 +18,7 @@ export function NoteThread({ className }: { className?: string }) {
|
||||
|
||||
return (
|
||||
<div className={cn("w-full", className)}>
|
||||
<div className="flex h-min w-full flex-col gap-3 rounded-lg bg-neutral-100 p-3 dark:bg-neutral-900">
|
||||
<div className="flex h-min w-full flex-col gap-3 rounded-xl bg-neutral-100 p-3 ring-1 ring-black/5 dark:bg-neutral-900 dark:ring-white/5">
|
||||
{thread.rootEventId ? (
|
||||
<Note.Child eventId={thread.rootEventId} isRoot />
|
||||
) : null}
|
||||
@@ -27,17 +26,14 @@ export function NoteThread({ className }: { className?: string }) {
|
||||
<Note.Child eventId={thread.replyEventId} />
|
||||
) : null}
|
||||
<div className="inline-flex items-center justify-between">
|
||||
<a
|
||||
href={`/events/${thread?.rootEventId || thread?.replyEventId}`}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
ark.open_thread(thread.rootEventId || thread.rootEventId)
|
||||
}
|
||||
className="self-start text-blue-500 hover:text-blue-600"
|
||||
>
|
||||
{t("note.showThread")}
|
||||
</a>
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex size-6 items-center justify-center rounded-md bg-neutral-200 text-neutral-600 hover:bg-neutral-300 dark:bg-neutral-800 dark:text-neutral-400 dark:hover:bg-neutral-700"
|
||||
>
|
||||
<PinIcon className="size-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,8 +2,10 @@ import { cn } from "@lume/utils";
|
||||
import * as HoverCard from "@radix-ui/react-hover-card";
|
||||
import { User } from "../user";
|
||||
import { useNoteContext } from "./provider";
|
||||
import { useArk } from "@lume/ark";
|
||||
|
||||
export function NoteUser({ className }: { className?: string }) {
|
||||
const ark = useArk();
|
||||
const event = useNoteContext();
|
||||
|
||||
return (
|
||||
@@ -23,12 +25,12 @@ export function NoteUser({ className }: { className?: string }) {
|
||||
</div>
|
||||
<User.Time
|
||||
time={event.created_at}
|
||||
className="text-neutral-500 dark:text-neutral-400"
|
||||
className="text-neutral-600 dark:text-neutral-400"
|
||||
/>
|
||||
</User.Root>
|
||||
<HoverCard.Portal>
|
||||
<HoverCard.Content
|
||||
className="data-[side=bottom]:animate-slideUpAndFade w-[300px] rounded-xl bg-white p-5 shadow-lg shadow-neutral-500/20 data-[state=open]:transition-all dark:border dark:border-neutral-800 dark:bg-neutral-900 dark:shadow-none"
|
||||
className="data-[side=bottom]:animate-slideUpAndFade w-[300px] rounded-xl bg-white p-3 shadow-lg shadow-neutral-500/20 data-[state=open]:transition-all dark:border dark:border-neutral-800 dark:bg-neutral-900 dark:shadow-none"
|
||||
sideOffset={5}
|
||||
>
|
||||
<div className="flex flex-col gap-2">
|
||||
@@ -39,12 +41,12 @@ export function NoteUser({ className }: { className?: string }) {
|
||||
<User.NIP05 className="text-neutral-600 dark:text-neutral-400" />
|
||||
</div>
|
||||
<User.About className="line-clamp-3" />
|
||||
<a
|
||||
href={`/users/${event.pubkey}`}
|
||||
className="mt-3 inline-flex h-8 w-full items-center justify-center rounded-lg bg-neutral-100 text-sm font-medium hover:bg-neutral-200 dark:bg-neutral-900 dark:hover:bg-neutral-800"
|
||||
<button
|
||||
onClick={() => ark.open_profile(event.pubkey)}
|
||||
className="mt-2 inline-flex h-9 w-full items-center justify-center rounded-lg bg-neutral-100 text-sm font-medium hover:bg-neutral-200 dark:bg-neutral-900 dark:hover:bg-neutral-800"
|
||||
>
|
||||
View profile
|
||||
</a>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<HoverCard.Arrow className="fill-white dark:fill-neutral-800" />
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { useArk } from "@lume/ark";
|
||||
import { LoaderIcon } from "@lume/icons";
|
||||
import { cn } from "@lume/utils";
|
||||
import { NDKKind, type NDKSubscription } from "@nostr-dev-kit/ndk";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ReplyForm } from "./editor/replyForm";
|
||||
import { Reply } from "./note/primitives/reply";
|
||||
import { EventWithReplies } from "@lume/types";
|
||||
|
||||
export function ReplyList({
|
||||
eventId,
|
||||
@@ -17,41 +17,14 @@ export function ReplyList({
|
||||
const ark = useArk();
|
||||
|
||||
const [t] = useTranslation();
|
||||
const [data, setData] = useState<null | NDKEventWithReplies[]>(null);
|
||||
const [data, setData] = useState<null | EventWithReplies[]>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let sub: NDKSubscription = undefined;
|
||||
let isCancelled = false;
|
||||
|
||||
async function fetchRepliesAndSub() {
|
||||
const id = ark.getCleanEventId(eventId);
|
||||
const events = await ark.getThreads(id);
|
||||
|
||||
if (!isCancelled) {
|
||||
setData(events);
|
||||
}
|
||||
|
||||
if (!sub) {
|
||||
sub = ark.subscribe({
|
||||
filter: {
|
||||
"#e": [id],
|
||||
kinds: [NDKKind.Text],
|
||||
since: Math.floor(Date.now() / 1000),
|
||||
},
|
||||
closeOnEose: false,
|
||||
cb: (event: NDKEventWithReplies) =>
|
||||
setData((prev) => [event, ...prev]),
|
||||
});
|
||||
}
|
||||
async function getReplies() {
|
||||
const events = await ark.get_event_thread(eventId);
|
||||
setData(events);
|
||||
}
|
||||
|
||||
// subscribe for new replies
|
||||
fetchRepliesAndSub();
|
||||
|
||||
return () => {
|
||||
isCancelled = true;
|
||||
if (sub) sub.stop();
|
||||
};
|
||||
getReplies();
|
||||
}, [eventId]);
|
||||
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user