wip: update design

This commit is contained in:
reya 2024-02-16 14:11:49 +07:00
parent 296b11b7b8
commit f28a7ae82f
20 changed files with 218 additions and 172 deletions

View File

@ -63,13 +63,13 @@ function Home() {
{data.map((item) => renderItem(item))} {data.map((item) => renderItem(item))}
</Virtualizer> </Virtualizer>
)} )}
<div className="flex h-16 items-center justify-center"> <div className="flex h-20 items-center justify-center">
{hasNextPage ? ( {hasNextPage ? (
<button <button
type="button" type="button"
onClick={() => fetchNextPage()} onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetchingNextPage} disabled={!hasNextPage || isFetchingNextPage}
className="inline-flex h-12 w-full items-center justify-center gap-2 rounded-xl bg-neutral-100 font-medium hover:bg-neutral-200 focus:outline-none dark:bg-neutral-900 dark:hover:bg-neutral-800" className="inline-flex h-11 w-max items-center justify-center gap-2 rounded-full bg-neutral-100 px-3 font-medium hover:bg-neutral-200 focus:outline-none dark:bg-neutral-900 dark:hover:bg-neutral-800"
> >
{isFetchingNextPage ? ( {isFetchingNextPage ? (
<LoaderIcon className="size-5 animate-spin" /> <LoaderIcon className="size-5 animate-spin" />

View File

@ -1,4 +1,6 @@
import { ReplyList, ThreadNote } from "@lume/ui";
import { createLazyFileRoute } from "@tanstack/react-router"; import { createLazyFileRoute } from "@tanstack/react-router";
import { WindowVirtualizer } from "virtua";
export const Route = createLazyFileRoute("/events/$eventId")({ export const Route = createLazyFileRoute("/events/$eventId")({
component: Event, component: Event,
@ -7,5 +9,15 @@ export const Route = createLazyFileRoute("/events/$eventId")({
function Event() { function Event() {
const { eventId } = Route.useParams(); const { eventId } = Route.useParams();
return <div>{eventId}</div>; return (
<div className="relative h-screen w-screen overflow-y-auto overflow-x-hidden">
<div data-tauri-drag-region className="h-11 w-full" />
<WindowVirtualizer>
<div className="px-6">
<ThreadNote eventId={eventId} />
<ReplyList eventId={eventId} />
</div>
</WindowVirtualizer>
</div>
);
} }

View File

@ -1,4 +1,10 @@
import type { CurrentAccount, Event, Keys, Metadata } from "@lume/types"; import type {
CurrentAccount,
Event,
EventWithReplies,
Keys,
Metadata,
} from "@lume/types";
import { invoke } from "@tauri-apps/api/core"; import { invoke } from "@tauri-apps/api/core";
import { WebviewWindow } from "@tauri-apps/api/webview"; import { WebviewWindow } from "@tauri-apps/api/webview";
@ -133,27 +139,27 @@ export class Ark {
} }
} }
public async repost(id: string, pubkey: string) { public async repost(id: string, author: string) {
try { try {
const cmd: string = await invoke("repost", { id, pubkey }); const cmd: string = await invoke("repost", { id, pubkey: author });
return cmd; return cmd;
} catch (e) { } catch (e) {
console.error(String(e)); console.error(String(e));
} }
} }
public async upvote(id: string, pubkey: string) { public async upvote(id: string, author: string) {
try { try {
const cmd: string = await invoke("upvote", { id, pubkey }); const cmd: string = await invoke("upvote", { id, pubkey: author });
return cmd; return cmd;
} catch (e) { } catch (e) {
console.error(String(e)); console.error(String(e));
} }
} }
public async downvote(id: string, pubkey: string) { public async downvote(id: string, author: string) {
try { try {
const cmd: string = await invoke("downvote", { id, pubkey }); const cmd: string = await invoke("downvote", { id, pubkey: author });
return cmd; return cmd;
} catch (e) { } catch (e) {
console.error(String(e)); console.error(String(e));
@ -162,8 +168,36 @@ export class Ark {
public async get_event_thread(id: string) { public async get_event_thread(id: string) {
try { try {
const cmd: Event[] = await invoke("get_event_thread", { id }); const events: EventWithReplies[] = await invoke("get_event_thread", {
return cmd; id,
});
if (events.length > 0) {
const replies = new Set();
for (const event of events) {
const tags = event.tags.filter(
(el) => el[0] === "e" && el[1] !== id && el[3] !== "mention",
);
if (tags.length > 0) {
for (const tag of tags) {
const rootIndex = events.findIndex((el) => el.id === tag[1]);
if (rootIndex !== -1) {
const rootEvent = events[rootIndex];
if (rootEvent?.replies) {
rootEvent.replies.push(event);
} else {
rootEvent.replies = [event];
}
replies.add(event.id);
}
}
}
}
const cleanEvents = events.filter((ev) => !replies.has(ev.id));
return cleanEvents;
}
return events;
} catch (e) { } catch (e) {
return []; return [];
} }
@ -250,7 +284,8 @@ export class Ark {
return new WebviewWindow(`event-${id}`, { return new WebviewWindow(`event-${id}`, {
title: "Thread", title: "Thread",
url: `/events/${id}`, url: `/events/${id}`,
width: 600, minWidth: 500,
width: 500,
height: 800, height: 800,
hiddenTitle: true, hiddenTitle: true,
titleBarStyle: "overlay", titleBarStyle: "overlay",
@ -261,7 +296,8 @@ export class Ark {
return new WebviewWindow(`user-${pubkey}`, { return new WebviewWindow(`user-${pubkey}`, {
title: "Profile", title: "Profile",
url: `/users/${pubkey}`, url: `/users/${pubkey}`,
width: 600, minWidth: 500,
width: 500,
height: 800, height: 800,
hiddenTitle: true, hiddenTitle: true,
titleBarStyle: "overlay", titleBarStyle: "overlay",

View File

@ -298,9 +298,9 @@ export function ReplyForm({
return ( return (
<div className={cn("flex gap-3", className)}> <div className={cn("flex gap-3", className)}>
<User.Provider pubkey={ark.account.pubkey}> <User.Provider pubkey={ark.account.npub}>
<User.Root> <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.Root>
</User.Provider> </User.Provider>
<div className="flex-1"> <div className="flex-1">

View File

@ -6,6 +6,7 @@ export * from "./column";
// Note Primities // Note Primities
export * from "./note/primitives/text"; export * from "./note/primitives/text";
export * from "./note/primitives/repost"; export * from "./note/primitives/repost";
export * from "./note/primitives/thread";
// Deprecated // Deprecated
export * from "./routes/event"; export * from "./routes/event";

View File

@ -1,22 +1,50 @@
import { ArrowDownIcon, ArrowUpIcon } from "@lume/icons"; import { ArrowDownIcon, ArrowUpIcon } from "@lume/icons";
import { useState } from "react"; import { useState } from "react";
import { useNoteContext } from "../provider"; import { useNoteContext } from "../provider";
import { useArk } from "@lume/ark";
import { cn } from "@lume/utils";
export function NoteReaction() { export function NoteReaction() {
const ark = useArk();
const event = useNoteContext(); const event = useNoteContext();
const [reaction, setReaction] = useState<"+" | "-">(null); 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 ( return (
<div className="inline-flex items-center gap-2"> <div className="inline-flex items-center gap-2">
<button <button
type="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" /> <ArrowUpIcon className="size-4" />
</button> </button>
<button <button
type="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" /> <ArrowDownIcon className="size-4" />
</button> </button>

View File

@ -7,8 +7,10 @@ import { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { toast } from "sonner"; import { toast } from "sonner";
import { useNoteContext } from "../provider"; import { useNoteContext } from "../provider";
import { useArk } from "@lume/ark";
export function NoteRepost() { export function NoteRepost() {
const ark = useArk();
const event = useNoteContext(); const event = useNoteContext();
const setEditorValue = useSetAtom(editorValueAtom); const setEditorValue = useSetAtom(editorValueAtom);
const setIsEditorOpen = useSetAtom(editorAtom); const setIsEditorOpen = useSetAtom(editorAtom);
@ -23,7 +25,7 @@ export function NoteRepost() {
setLoading(true); setLoading(true);
// repost // repost
await event.repost(true); await ark.repost(event.id, event.pubkey);
// update state // update state
setLoading(false); setLoading(false);

View File

@ -39,9 +39,7 @@ export function NoteContent({ className }: { className?: string }) {
const richContent = useMemo(() => { const richContent = useMemo(() => {
if (event.kind !== Kind.Text) return content; if (event.kind !== Kind.Text) return content;
let parsedContent: string | ReactNode[] = stripHtml( let parsedContent: string | ReactNode[] = stripHtml(content).result;
content.replace(/\n{2,}\s*/g, "\n"),
).result;
let linkPreview: string = undefined; let linkPreview: string = undefined;
let images: string[] = []; let images: string[] = [];
let videos: string[] = []; let videos: string[] = [];
@ -176,7 +174,7 @@ export function NoteContent({ className }: { className?: string }) {
); );
parsedContent = reactStringReplace(parsedContent, "\n", () => { parsedContent = reactStringReplace(parsedContent, "\n", () => {
return <div key={nanoid()} className="h-3" />; return <div key={nanoid()} />;
}); });
if (typeof parsedContent[0] === "string") { if (typeof parsedContent[0] === "string") {

View File

@ -21,10 +21,7 @@ export function MentionNote({
const richContent = useMemo(() => { const richContent = useMemo(() => {
if (!data) return ""; if (!data) return "";
let parsedContent: string | ReactNode[] = data.content.replace( let parsedContent: string | ReactNode[] = data.content;
/\n+/g,
"\n",
);
const text = parsedContent as string; const text = parsedContent as string;
const words = text.split(/( |\n)/); const words = text.split(/( |\n)/);
@ -106,11 +103,11 @@ export function MentionNote({
} }
return ( 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.Provider pubkey={data.pubkey}>
<User.Root className="flex h-10 items-center gap-2 px-3"> <User.Root className="flex h-10 items-center gap-2 px-3">
<User.Avatar className="size-6 shrink-0 rounded-md object-cover" /> <User.Avatar className="size-6 shrink-0 rounded-full object-cover" />
<div className="inline-flex flex-1 gap-2"> <div className="inline-flex flex-1 items-center gap-2">
<User.Name className="font-semibold text-neutral-900 dark:text-neutral-100" /> <User.Name className="font-semibold text-neutral-900 dark:text-neutral-100" />
<span className="text-neutral-600 dark:text-neutral-400">·</span> <span className="text-neutral-600 dark:text-neutral-400">·</span>
<User.Time <User.Time
@ -127,16 +124,10 @@ export function MentionNote({
<div className="flex h-10 items-center justify-between px-3"> <div className="flex h-10 items-center justify-between px-3">
<a <a
href={`/events/${data.id}`} 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")} {t("note.showMore")}
</a> </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>
) : ( ) : (
<div className="h-3" /> <div className="h-3" />

View File

@ -32,8 +32,11 @@ export function NoteMenu() {
return ( return (
<DropdownMenu.Root> <DropdownMenu.Root>
<DropdownMenu.Trigger asChild> <DropdownMenu.Trigger asChild>
<button type="button"> <button
<HorizontalDotsIcon className="size-4 hover:text-blue-500 dark:text-neutral-200" /> type="button"
className="text-neutral-500 hover:text-blue-500 dark:text-neutral-400"
>
<HorizontalDotsIcon className="size-5" />
</button> </button>
</DropdownMenu.Trigger> </DropdownMenu.Trigger>
<DropdownMenu.Portal> <DropdownMenu.Portal>

View File

@ -35,7 +35,10 @@ export function ImagePreview({ url }: { url: string }) {
return ( return (
// biome-ignore lint/a11y/useKeyWithClickEvents: <explanation> // biome-ignore lint/a11y/useKeyWithClickEvents: <explanation>
<div onClick={open} className="relative mt-1 mb-2.5 group"> <div
onClick={open}
className="group relative my-1.5 rounded-xl ring-1 ring-black/5 dark:ring-white/5"
>
<img <img
src={url} src={url}
alt={url} alt={url}
@ -43,12 +46,12 @@ export function ImagePreview({ url }: { url: string }) {
decoding="async" decoding="async"
style={{ contentVisibility: "auto" }} style={{ contentVisibility: "auto" }}
onError={fallback} onError={fallback}
className="object-cover w-full h-auto border rounded-xl border-neutral-200/50 dark:border-neutral-800/50" className="h-auto w-full rounded-xl object-cover"
/> />
<button <button
type="button" type="button"
onClick={(e) => downloadImage(e)} 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" 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 ? ( {downloaded ? (
<CheckCircleIcon className="size-5" /> <CheckCircleIcon className="size-5" />

View File

@ -10,7 +10,7 @@ export function LinkPreview({ url }: { url: string }) {
if (isLoading) { if (isLoading) {
return ( 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="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="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" /> <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} href={url}
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
className="text-blue-500 hover:text-blue-600" className="inline-block text-blue-500 hover:text-blue-600"
> >
{url} {url}
</a> </a>
@ -42,7 +42,7 @@ export function LinkPreview({ url }: { url: string }) {
href={url} href={url}
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
className="text-blue-500 hover:text-blue-600" className="inline-block text-blue-500 hover:text-blue-600"
> >
{url} {url}
</a> </a>
@ -54,7 +54,7 @@ export function LinkPreview({ url }: { url: string }) {
href={url} href={url}
target="_blank" target="_blank"
rel="noreferrer" 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) ? ( {isImage(data.image) ? (
<img <img

View File

@ -9,14 +9,14 @@ import {
export function VideoPreview({ url }: { url: string }) { export function VideoPreview({ url }: { url: string }) {
return ( return (
<div className="mt-1 mb-2.5 w-full rounded-xl overflow-hidden"> <div className="my-1.5 w-full overflow-hidden rounded-xl ring-1 ring-black/5 dark:ring-white/5">
<MediaController> <MediaController>
<video <video
slot="media" slot="media"
src={url} src={url}
preload="auto" preload="auto"
muted muted
className="w-full h-auto" className="h-auto w-full rounded-xl"
/> />
<MediaControlBar> <MediaControlBar>
<MediaPlayButton /> <MediaPlayButton />

View File

@ -11,11 +11,11 @@ export function ThreadNote({ eventId }: { eventId: string }) {
return ( return (
<Note.Provider event={data}> <Note.Provider event={data}>
<Note.Root className="flex flex-col rounded-xl bg-neutral-50 dark:bg-neutral-950"> <Note.Root className="flex flex-col">
<div className="flex h-16 items-center justify-between px-3"> <div className="flex h-16 items-center justify-between">
<User.Provider pubkey={data.pubkey}> <User.Provider pubkey={data.pubkey}>
<User.Root className="flex h-16 flex-1 items-center gap-3"> <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"> <div className="flex flex-1 flex-col">
<User.Name className="font-semibold text-neutral-900 dark:text-neutral-100" /> <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"> <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 /> <Note.Menu />
</div> </div>
<Note.Thread className="mb-2" /> <Note.Thread className="mb-2" />
<Note.Content className="min-w-0 px-3" /> <Note.Content className="min-w-0" />
<div className="flex h-14 items-center justify-between px-3"> <div className="flex h-14 items-center justify-between">
<Note.Pin /> <Note.Reaction />
<div className="inline-flex items-center gap-4"> <div className="inline-flex items-center gap-4">
<Note.Repost /> <Note.Repost />
<Note.Zap /> <Note.Zap />

View File

@ -1,4 +1,3 @@
import { PinIcon } from "@lume/icons";
import { cn } from "@lume/utils"; import { cn } from "@lume/utils";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Note } from "."; import { Note } from ".";
@ -19,7 +18,7 @@ export function NoteThread({ className }: { className?: string }) {
return ( return (
<div className={cn("w-full", className)}> <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 ? ( {thread.rootEventId ? (
<Note.Child eventId={thread.rootEventId} isRoot /> <Note.Child eventId={thread.rootEventId} isRoot />
) : null} ) : null}
@ -27,17 +26,14 @@ export function NoteThread({ className }: { className?: string }) {
<Note.Child eventId={thread.replyEventId} /> <Note.Child eventId={thread.replyEventId} />
) : null} ) : null}
<div className="inline-flex items-center justify-between"> <div className="inline-flex items-center justify-between">
<a <button
href={`/events/${thread?.rootEventId || thread?.replyEventId}`} type="button"
onClick={() =>
ark.open_thread(thread.rootEventId || thread.rootEventId)
}
className="self-start text-blue-500 hover:text-blue-600" className="self-start text-blue-500 hover:text-blue-600"
> >
{t("note.showThread")} {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> </button>
</div> </div>
</div> </div>

View File

@ -2,8 +2,10 @@ import { cn } from "@lume/utils";
import * as HoverCard from "@radix-ui/react-hover-card"; import * as HoverCard from "@radix-ui/react-hover-card";
import { User } from "../user"; import { User } from "../user";
import { useNoteContext } from "./provider"; import { useNoteContext } from "./provider";
import { useArk } from "@lume/ark";
export function NoteUser({ className }: { className?: string }) { export function NoteUser({ className }: { className?: string }) {
const ark = useArk();
const event = useNoteContext(); const event = useNoteContext();
return ( return (
@ -23,12 +25,12 @@ export function NoteUser({ className }: { className?: string }) {
</div> </div>
<User.Time <User.Time
time={event.created_at} time={event.created_at}
className="text-neutral-500 dark:text-neutral-400" className="text-neutral-600 dark:text-neutral-400"
/> />
</User.Root> </User.Root>
<HoverCard.Portal> <HoverCard.Portal>
<HoverCard.Content <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} sideOffset={5}
> >
<div className="flex flex-col gap-2"> <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" /> <User.NIP05 className="text-neutral-600 dark:text-neutral-400" />
</div> </div>
<User.About className="line-clamp-3" /> <User.About className="line-clamp-3" />
<a <button
href={`/users/${event.pubkey}`} onClick={() => ark.open_profile(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" 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 View profile
</a> </button>
</div> </div>
</div> </div>
<HoverCard.Arrow className="fill-white dark:fill-neutral-800" /> <HoverCard.Arrow className="fill-white dark:fill-neutral-800" />

View File

@ -1,11 +1,11 @@
import { useArk } from "@lume/ark"; import { useArk } from "@lume/ark";
import { LoaderIcon } from "@lume/icons"; import { LoaderIcon } from "@lume/icons";
import { cn } from "@lume/utils"; import { cn } from "@lume/utils";
import { NDKKind, type NDKSubscription } from "@nostr-dev-kit/ndk";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { ReplyForm } from "./editor/replyForm"; import { ReplyForm } from "./editor/replyForm";
import { Reply } from "./note/primitives/reply"; import { Reply } from "./note/primitives/reply";
import { EventWithReplies } from "@lume/types";
export function ReplyList({ export function ReplyList({
eventId, eventId,
@ -17,41 +17,14 @@ export function ReplyList({
const ark = useArk(); const ark = useArk();
const [t] = useTranslation(); const [t] = useTranslation();
const [data, setData] = useState<null | NDKEventWithReplies[]>(null); const [data, setData] = useState<null | EventWithReplies[]>(null);
useEffect(() => { useEffect(() => {
let sub: NDKSubscription = undefined; async function getReplies() {
let isCancelled = false; const events = await ark.get_event_thread(eventId);
async function fetchRepliesAndSub() {
const id = ark.getCleanEventId(eventId);
const events = await ark.getThreads(id);
if (!isCancelled) {
setData(events); setData(events);
} }
getReplies();
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]),
});
}
}
// subscribe for new replies
fetchRepliesAndSub();
return () => {
isCancelled = true;
if (sub) sub.stop();
};
}, [eventId]); }, [eventId]);
return ( return (

View File

@ -26,6 +26,7 @@
"clipboard-manager:allow-write", "clipboard-manager:allow-write",
"clipboard-manager:allow-read", "clipboard-manager:allow-read",
"webview:allow-create-webview-window", "webview:allow-create-webview-window",
"webview:allow-create-webview",
{ {
"identifier": "http:default", "identifier": "http:default",
"allow": [ "allow": [

View File

@ -1 +1 @@
{"desktop-capability":{"identifier":"desktop-capability","description":"Capability for the desktop","context":"local","windows":["main","settings","event-*","user-*","column-*"],"permissions":["path:default","event:default","window:default","app:default","resources:default","menu:default","tray:default","theme:allow-set-theme","theme:allow-get-theme","notification:allow-is-permission-granted","notification:allow-request-permission","notification:default","os:allow-locale","os:allow-platform","updater:allow-check","updater:default","window:allow-start-dragging","store:allow-get","clipboard-manager:allow-write","clipboard-manager:allow-read","webview:allow-create-webview-window",{"identifier":"http:default","allow":[{"url":"http://**/"},{"url":"https://**/"}]},{"identifier":"fs:allow-read-text-file","allow":[{"path":"$RESOURCE/locales/*"}]}],"platforms":["linux","macOS","windows"]}} {"desktop-capability":{"identifier":"desktop-capability","description":"Capability for the desktop","context":"local","windows":["main","settings","event-*","user-*","column-*"],"permissions":["path:default","event:default","window:default","app:default","resources:default","menu:default","tray:default","theme:allow-set-theme","theme:allow-get-theme","notification:allow-is-permission-granted","notification:allow-request-permission","notification:default","os:allow-locale","os:allow-platform","updater:allow-check","updater:default","window:allow-start-dragging","store:allow-get","clipboard-manager:allow-write","clipboard-manager:allow-read","webview:allow-create-webview-window","webview:allow-create-webview",{"identifier":"http:default","allow":[{"url":"http://**/"},{"url":"https://**/"}]},{"identifier":"fs:allow-read-text-file","allow":[{"path":"$RESOURCE/locales/*"}]}],"platforms":["linux","macOS","windows"]}}

View File

@ -87,7 +87,7 @@ fn main() {
.await .await
.expect("Failed to add bootstrap relay."); .expect("Failed to add bootstrap relay.");
client client
.add_relay("wss://bostr.nokotaro.com") .add_relay("wss://bostr.yonle.lecturify.net")
.await .await
.expect("Failed to add bootstrap relay."); .expect("Failed to add bootstrap relay.");