mirror of
https://github.com/lumehq/lume.git
synced 2025-04-09 12:29:49 +02:00
wip: update design
This commit is contained in:
parent
296b11b7b8
commit
f28a7ae82f
@ -63,13 +63,13 @@ function Home() {
|
||||
{data.map((item) => renderItem(item))}
|
||||
</Virtualizer>
|
||||
)}
|
||||
<div className="flex h-16 items-center justify-center">
|
||||
<div className="flex h-20 items-center justify-center">
|
||||
{hasNextPage ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => fetchNextPage()}
|
||||
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 ? (
|
||||
<LoaderIcon className="size-5 animate-spin" />
|
||||
|
@ -1,4 +1,6 @@
|
||||
import { ReplyList, ThreadNote } from "@lume/ui";
|
||||
import { createLazyFileRoute } from "@tanstack/react-router";
|
||||
import { WindowVirtualizer } from "virtua";
|
||||
|
||||
export const Route = createLazyFileRoute("/events/$eventId")({
|
||||
component: Event,
|
||||
@ -7,5 +9,15 @@ export const Route = createLazyFileRoute("/events/$eventId")({
|
||||
function Event() {
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
@ -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 { 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 {
|
||||
const cmd: string = await invoke("repost", { id, pubkey });
|
||||
const cmd: string = await invoke("repost", { id, pubkey: author });
|
||||
return cmd;
|
||||
} catch (e) {
|
||||
console.error(String(e));
|
||||
}
|
||||
}
|
||||
|
||||
public async upvote(id: string, pubkey: string) {
|
||||
public async upvote(id: string, author: string) {
|
||||
try {
|
||||
const cmd: string = await invoke("upvote", { id, pubkey });
|
||||
const cmd: string = await invoke("upvote", { id, pubkey: author });
|
||||
return cmd;
|
||||
} catch (e) {
|
||||
console.error(String(e));
|
||||
}
|
||||
}
|
||||
|
||||
public async downvote(id: string, pubkey: string) {
|
||||
public async downvote(id: string, author: string) {
|
||||
try {
|
||||
const cmd: string = await invoke("downvote", { id, pubkey });
|
||||
const cmd: string = await invoke("downvote", { id, pubkey: author });
|
||||
return cmd;
|
||||
} catch (e) {
|
||||
console.error(String(e));
|
||||
@ -162,8 +168,36 @@ export class Ark {
|
||||
|
||||
public async get_event_thread(id: string) {
|
||||
try {
|
||||
const cmd: Event[] = await invoke("get_event_thread", { id });
|
||||
return cmd;
|
||||
const events: EventWithReplies[] = await invoke("get_event_thread", {
|
||||
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) {
|
||||
return [];
|
||||
}
|
||||
@ -250,7 +284,8 @@ export class Ark {
|
||||
return new WebviewWindow(`event-${id}`, {
|
||||
title: "Thread",
|
||||
url: `/events/${id}`,
|
||||
width: 600,
|
||||
minWidth: 500,
|
||||
width: 500,
|
||||
height: 800,
|
||||
hiddenTitle: true,
|
||||
titleBarStyle: "overlay",
|
||||
@ -261,7 +296,8 @@ export class Ark {
|
||||
return new WebviewWindow(`user-${pubkey}`, {
|
||||
title: "Profile",
|
||||
url: `/users/${pubkey}`,
|
||||
width: 600,
|
||||
minWidth: 500,
|
||||
width: 500,
|
||||
height: 800,
|
||||
hiddenTitle: true,
|
||||
titleBarStyle: "overlay",
|
||||
|
@ -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 (
|
||||
|
@ -26,6 +26,7 @@
|
||||
"clipboard-manager:allow-write",
|
||||
"clipboard-manager:allow-read",
|
||||
"webview:allow-create-webview-window",
|
||||
"webview:allow-create-webview",
|
||||
{
|
||||
"identifier": "http:default",
|
||||
"allow": [
|
||||
|
@ -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"]}}
|
@ -87,7 +87,7 @@ fn main() {
|
||||
.await
|
||||
.expect("Failed to add bootstrap relay.");
|
||||
client
|
||||
.add_relay("wss://bostr.nokotaro.com")
|
||||
.add_relay("wss://bostr.yonle.lecturify.net")
|
||||
.await
|
||||
.expect("Failed to add bootstrap relay.");
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user