Make Lume Faster (#208)

* chore: fix some lint issues

* feat: refactor contact list

* feat: refactor relay hint

* feat: add missing commands

* feat: use new cache layer for react query

* feat: refactor column

* feat: improve relay hint

* fix: replace break with continue in parser

* refactor: publish function

* feat: add reply command

* feat: improve editor

* fix: quote

* chore: update deps

* refactor: note component

* feat: improve repost

* feat: improve cache

* fix: backup screen

* refactor: column manager
This commit is contained in:
雨宮蓮
2024-06-17 13:52:06 +07:00
committed by GitHub
parent 7c99ed39e4
commit 843895d876
79 changed files with 1738 additions and 1975 deletions

View File

@@ -2,53 +2,57 @@ import { CancelIcon, CheckIcon } from "@lume/icons";
import type { LumeColumn } from "@lume/types";
import { cn } from "@lume/utils";
import { invoke } from "@tauri-apps/api/core";
import { listen } from "@tauri-apps/api/event";
import { getCurrent } from "@tauri-apps/api/webviewWindow";
import { useEffect, useMemo, useRef, useState } from "react";
import { useCallback, useEffect, useRef, useState } from "react";
type WindowEvent = {
scroll: boolean;
resize: boolean;
};
export function Column({
column,
account,
isScroll,
isResize,
}: {
column: LumeColumn;
account: string;
isScroll: boolean;
isResize: boolean;
}) {
const container = useRef<HTMLDivElement>(null);
const webviewLabel = useMemo(
() => `column-${account}_${column.label}`,
[account],
);
const webviewLabel = `column-${account}_${column.label}`;
const [isCreated, setIsCreated] = useState(false);
const repositionWebview = async () => {
const repositionWebview = useCallback(async () => {
const newRect = container.current.getBoundingClientRect();
await invoke("reposition_column", {
label: webviewLabel,
x: newRect.x,
y: newRect.y,
});
};
}, []);
const resizeWebview = async () => {
const resizeWebview = useCallback(async () => {
const newRect = container.current.getBoundingClientRect();
await invoke("resize_column", {
label: webviewLabel,
width: newRect.width,
height: newRect.height,
});
};
}, []);
useEffect(() => {
if (isCreated) resizeWebview();
}, [isResize]);
if (!isCreated) return;
useEffect(() => {
if (isScroll && isCreated) repositionWebview();
}, [isScroll]);
const unlisten = listen<WindowEvent>("window", (data) => {
if (data.payload.scroll) repositionWebview();
if (data.payload.resize) resizeWebview();
});
return () => {
unlisten.then((f) => f());
};
}, [isCreated]);
useEffect(() => {
if (!container?.current) return;
@@ -78,7 +82,7 @@ export function Column({
}, [account]);
return (
<div className="h-full w-[440px] shrink-0 p-2">
<div className="h-full w-[500px] shrink-0 p-2">
<div
className={cn(
"flex flex-col w-full h-full rounded-xl",
@@ -87,9 +91,7 @@ export function Column({
: "",
)}
>
{column.label !== "open" ? (
<Header label={column.label} name={column.name} />
) : null}
<Header label={column.label} name={column.name} />
<div ref={container} className="flex-1 w-full h-full" />
</div>
</div>
@@ -122,10 +124,10 @@ function Header({ label, name }: { label: string; name: string }) {
}, [title]);
return (
<div className="h-9 w-full flex items-center justify-between shrink-0 px-1">
<div className="flex items-center justify-between w-full px-1 h-9 shrink-0">
<div className="size-7" />
<div className="shrink-0 h-9 flex items-center justify-center">
<div className="relative flex gap-2 items-center">
<div className="flex items-center justify-center shrink-0 h-9">
<div className="relative flex items-center gap-2">
<div
contentEditable
suppressContentEditableWarning={true}
@@ -148,7 +150,7 @@ function Header({ label, name }: { label: string; name: string }) {
<button
type="button"
onClick={() => close()}
className="size-7 inline-flex hover:bg-black/10 rounded-lg dark:hover:bg-white/10 items-center justify-center text-neutral-600 dark:text-neutral-400 hover:text-neutral-800 dark:hover:text-neutral-200"
className="inline-flex items-center justify-center rounded-lg size-7 hover:bg-black/10 dark:hover:bg-white/10 text-neutral-600 dark:text-neutral-400 hover:text-neutral-800 dark:hover:text-neutral-200"
>
<CancelIcon className="size-4" />
</button>

View File

@@ -1,23 +1,17 @@
import { ThreadIcon } from "@lume/icons";
import type { NostrEvent } from "@lume/types";
import { Note } from "@/components/note";
import { cn } from "@lume/utils";
import { LumeEvent } from "@lume/system";
import type { LumeEvent } from "@lume/system";
import { useMemo } from "react";
export function Conversation({
event,
gossip,
className,
}: {
event: NostrEvent;
gossip?: boolean;
event: LumeEvent;
className?: string;
}) {
const thread = useMemo(
() => LumeEvent.getEventThread(event.tags, gossip),
[event],
);
const thread = useMemo(() => event.thread, [event]);
return (
<Note.Provider event={event}>
@@ -28,7 +22,7 @@ export function Conversation({
)}
>
<div className="flex flex-col gap-3">
{thread?.root ? <Note.Child eventId={thread?.root} isRoot /> : null}
{thread?.root?.id ? <Note.Child event={thread?.root} isRoot /> : null}
<div className="flex items-center gap-2 px-3">
<div className="inline-flex items-center gap-1.5 shrink-0 text-sm font-medium text-neutral-600 dark:text-neutral-400">
<ThreadIcon className="size-4" />
@@ -36,15 +30,15 @@ export function Conversation({
</div>
<div className="flex-1 h-px bg-neutral-100 dark:bg-white/5" />
</div>
{thread?.reply ? <Note.Child eventId={thread?.reply} /> : null}
{thread?.reply?.id ? <Note.Child event={thread?.reply} /> : null}
<div>
<div className="px-3 h-14 flex items-center justify-between">
<div className="flex items-center justify-between px-3 h-14">
<Note.User />
</div>
<Note.Content className="px-3" />
</div>
</div>
<div className="flex items-center h-14 px-3">
<div className="flex items-center px-3 h-14">
<Note.Open />
</div>
</Note.Root>

View File

@@ -1,24 +0,0 @@
import { cn } from "@lume/utils";
import { useNoteContext } from "./provider";
import { User } from "../user";
export function NoteActivity({ className }: { className?: string }) {
const event = useNoteContext();
const mentions = event.mentions;
return (
<div className={cn("-mt-3 mb-2", className)}>
<div className="text-neutral-700 dark:text-neutral-300 inline-flex items-baseline gap-2 w-full overflow-hidden">
<div className="shrink-0 text-sm font-medium">To:</div>
{mentions.splice(0, 4).map((mention) => (
<User.Provider key={mention} pubkey={mention}>
<User.Root>
<User.Name className="text-sm font-medium" prefix="@" />
</User.Root>
</User.Provider>
))}
{mentions.length > 4 ? "..." : ""}
</div>
</div>
);
}

View File

@@ -76,7 +76,7 @@ export function NoteRepost({ large = false }: { large?: boolean }) {
<button
type="button"
onClick={() => repost()}
className="inline-flex h-9 items-center gap-2 rounded-lg px-3 text-sm font-medium text-white hover:bg-neutral-900 focus:outline-none dark:text-black dark:hover:bg-neutral-100"
className="inline-flex items-center gap-2 px-3 text-sm font-medium text-white rounded-lg h-9 hover:bg-neutral-900 focus:outline-none dark:text-black dark:hover:bg-neutral-100"
>
<RepostIcon className="size-4" />
{t("note.buttons.repost")}
@@ -85,8 +85,8 @@ export function NoteRepost({ large = false }: { large?: boolean }) {
<DropdownMenu.Item asChild>
<button
type="button"
onClick={() => LumeWindow.openEditor(event.id, true)}
className="inline-flex h-9 items-center gap-2 rounded-lg px-3 text-sm font-medium text-white hover:bg-neutral-900 focus:outline-none dark:text-black dark:hover:bg-neutral-100"
onClick={() => LumeWindow.openEditor(null, event.id)}
className="inline-flex items-center gap-2 px-3 text-sm font-medium text-white rounded-lg h-9 hover:bg-neutral-900 focus:outline-none dark:text-black dark:hover:bg-neutral-100"
>
<QuoteIcon className="size-4" />
{t("note.buttons.quote")}

View File

@@ -1,14 +1,10 @@
import { ZapIcon } from "@lume/icons";
import { useRouteContext } from "@tanstack/react-router";
import { useNoteContext } from "../provider";
import { cn } from "@lume/utils";
import { LumeWindow } from "@lume/system";
export function NoteZap({ large = false }: { large?: boolean }) {
const event = useNoteContext();
const { settings } = useRouteContext({ strict: false });
if (!settings.zap) return null;
return (
<button

View File

@@ -2,32 +2,33 @@ import { useEvent } from "@lume/system";
import { cn } from "@lume/utils";
import { Note } from ".";
import { InfoIcon } from "@lume/icons";
import type { EventTag } from "@lume/types";
export function NoteChild({
eventId,
event,
isRoot,
}: {
eventId: string;
event: EventTag;
isRoot?: boolean;
}) {
const { isLoading, isError, data } = useEvent(eventId);
const { isLoading, isError, data } = useEvent(event.id, event.relayHint);
if (isLoading) {
return (
<div className="pt-3 px-3 flex items-center gap-2">
<div className="size-8 shrink-0 rounded-full bg-neutral-200 dark:bg-neutral-800 animate-pulse" />
<div className="animate-pulse rounded-md h-4 w-2/3 bg-neutral-200 dark:bg-neutral-800" />
<div className="flex items-center gap-2 px-3 pt-3">
<div className="rounded-full size-8 shrink-0 bg-neutral-200 dark:bg-neutral-800 animate-pulse" />
<div className="w-2/3 h-4 rounded-md animate-pulse bg-neutral-200 dark:bg-neutral-800" />
</div>
);
}
if (isError || !data) {
return (
<div className="pt-3 px-3 flex items-center gap-2">
<div className="size-8 shrink-0 rounded-full bg-red-500 text-white inline-flex items-center justify-center">
<div className="flex items-center gap-2 px-3 pt-3">
<div className="inline-flex items-center justify-center text-white bg-red-500 rounded-full size-8 shrink-0">
<InfoIcon className="size-5" />
</div>
<p className="text-red-500 text-sm">
<p className="text-sm text-red-500">
Event not found with your current relay set
</p>
</div>
@@ -37,7 +38,7 @@ export function NoteChild({
return (
<Note.Provider event={data}>
<Note.Root className={cn(isRoot ? "mb-3" : "")}>
<div className="h-14 px-3 flex items-center justify-between">
<div className="flex items-center justify-between px-3 h-14">
<Note.User />
</div>
<Note.Content className="px-3" />

View File

@@ -25,7 +25,7 @@ export function NoteContent({
try {
// Get parsed meta
const { content, hashtags, events, mentions } = event.meta;
// Define rich content
let richContent: ReactNode[] | string = content;
@@ -69,7 +69,7 @@ export function NoteContent({
href={match}
target="_blank"
rel="noreferrer"
className="text-blue-500 line-clamp-1 hover:text-blue-600"
className="inline text-blue-500 hover:text-blue-600"
>
{match}
</a>
@@ -92,7 +92,7 @@ export function NoteContent({
<div
className={cn(
"select-text text-pretty content-break overflow-hidden",
event.content.length > 420 ? "max-h-[250px] gradient-mask-b-0" : "",
event.content.length > 620 ? "max-h-[250px] gradient-mask-b-0" : "",
className,
)}
>

View File

@@ -1,4 +1,3 @@
import { NoteActivity } from "./activity";
import { NoteOpenThread } from "./buttons/open";
import { NoteReply } from "./buttons/reply";
import { NoteRepost } from "./buttons/repost";
@@ -23,5 +22,4 @@ export const Note = {
Zap: NoteZap,
Open: NoteOpenThread,
Child: NoteChild,
Activity: NoteActivity,
};

View File

@@ -1,13 +1,10 @@
export function Hashtag({ tag }: { tag: string }) {
return (
<button
type="button"
className="break-all cursor-default leading-normal group text-start"
>
<span className="leading-normal break-all cursor-default group text-start">
<span className="text-blue-500">#</span>
<span className="underline-offset-1 underline decoration-2 decoration-blue-200 dark:decoration-blue-800 group-hover:decoration-blue-500">
<span className="underline underline-offset-1 decoration-2 decoration-blue-200 dark:decoration-blue-800 group-hover:decoration-blue-500">
{tag.replace("#", "")}
</span>
</button>
</span>
);
}

View File

@@ -17,7 +17,7 @@ export function MentionNote({
if (isLoading) {
return (
<div className="mt-2 w-full flex h-20 items-center justify-center rounded-xl border border-black/10 dark:border-white/10">
<div className="flex items-center justify-center w-full h-20 mt-2 border rounded-xl border-black/10 dark:border-white/10">
<Spinner className="size-5" />
</div>
);
@@ -25,18 +25,18 @@ export function MentionNote({
if (isError || !data) {
return (
<div className="mt-2 w-full rounded-xl border border-black/10 p-3 dark:border-white/10">
<div className="w-full p-3 mt-2 border rounded-xl border-black/10 dark:border-white/10">
{t("note.error")}
</div>
);
}
return (
<div className="mt-2 flex w-full cursor-default flex-col rounded-xl border border-black/10 dark:border-white/10">
<div className="flex flex-col w-full border rounded-lg cursor-default border-black/10 dark:border-white/10">
<User.Provider pubkey={data.pubkey}>
<User.Root className="flex h-12 items-center gap-2 px-3">
<User.Avatar className="size-6 shrink-0 rounded-full object-cover" />
<div className="inline-flex flex-1 items-center gap-2">
<User.Root className="flex items-center gap-2 px-3 h-11">
<User.Avatar className="object-cover rounded-full size-6 shrink-0" />
<div className="inline-flex items-center flex-1 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
@@ -49,20 +49,20 @@ export function MentionNote({
<div
className={cn(
"px-3 select-text whitespace-normal text-pretty content-break leading-normal",
data.content.length > 100 ? "max-h-[150px] gradient-mask-b-0" : "",
data.content.length > 400 ? "max-h-[150px] gradient-mask-b-0" : "",
)}
>
{data.content}
</div>
{openable ? (
<div className="flex h-14 items-center justify-end px-3">
<div className="flex items-center justify-end px-2 h-11">
<button
type="button"
onClick={(e) => {
e.stopPropagation();
LumeWindow.openEvent(data);
}}
className="z-10 h-7 w-28 inline-flex items-center justify-center gap-1 text-sm bg-black/10 dark:bg-white/10 rounded-full text-neutral-600 hover:text-blue-500 dark:text-neutral-400"
className="z-10 inline-flex items-center justify-center gap-1 text-sm rounded-full h-7 w-28 bg-black/10 dark:bg-white/10 text-neutral-600 hover:text-blue-500 dark:text-neutral-400"
>
View post
<LinkIcon className="size-4" />

View File

@@ -30,7 +30,7 @@ export function NoteMenu() {
<DropdownMenu.Trigger asChild>
<button
type="button"
className="group inline-flex size-7 items-center justify-center text-neutral-600 dark:text-neutral-400"
className="inline-flex items-center justify-center group size-7 text-neutral-600 dark:text-neutral-400"
>
<HorizontalDotsIcon className="size-5" />
</button>
@@ -41,7 +41,7 @@ export function NoteMenu() {
<button
type="button"
onClick={() => LumeWindow.openEvent(event)}
className="inline-flex h-9 items-center gap-2 rounded-lg px-3 text-sm font-medium text-white hover:bg-neutral-900 focus:outline-none dark:text-black dark:hover:bg-neutral-100"
className="inline-flex items-center gap-2 px-3 text-sm font-medium text-white rounded-lg h-9 hover:bg-neutral-900 focus:outline-none dark:text-black dark:hover:bg-neutral-100"
>
{t("note.menu.viewThread")}
</button>
@@ -50,7 +50,7 @@ export function NoteMenu() {
<button
type="button"
onClick={() => copyLink()}
className="inline-flex h-9 items-center gap-2 rounded-lg px-3 text-sm font-medium text-white hover:bg-neutral-900 focus:outline-none dark:text-black dark:hover:bg-neutral-100"
className="inline-flex items-center gap-2 px-3 text-sm font-medium text-white rounded-lg h-9 hover:bg-neutral-900 focus:outline-none dark:text-black dark:hover:bg-neutral-100"
>
{t("note.menu.copyLink")}
</button>
@@ -59,7 +59,7 @@ export function NoteMenu() {
<button
type="button"
onClick={() => copyID()}
className="inline-flex h-9 items-center gap-2 rounded-lg px-3 text-sm font-medium text-white hover:bg-neutral-900 focus:outline-none dark:text-black dark:hover:bg-neutral-100"
className="inline-flex items-center gap-2 px-3 text-sm font-medium text-white rounded-lg h-9 hover:bg-neutral-900 focus:outline-none dark:text-black dark:hover:bg-neutral-100"
>
{t("note.menu.copyNoteId")}
</button>
@@ -68,25 +68,26 @@ export function NoteMenu() {
<button
type="button"
onClick={() => copyNpub()}
className="inline-flex h-9 items-center gap-2 rounded-lg px-3 text-sm font-medium text-white hover:bg-neutral-900 focus:outline-none dark:text-black dark:hover:bg-neutral-100"
className="inline-flex items-center gap-2 px-3 text-sm font-medium text-white rounded-lg h-9 hover:bg-neutral-900 focus:outline-none dark:text-black dark:hover:bg-neutral-100"
>
{t("note.menu.copyAuthorId")}
</button>
</DropdownMenu.Item>
<DropdownMenu.Item asChild>
<button
type="button"
onClick={() => LumeWindow.openProfile(event.pubkey)}
className="inline-flex h-9 items-center gap-2 rounded-lg px-3 text-sm font-medium text-white hover:bg-neutral-900 focus:outline-none dark:text-black dark:hover:bg-neutral-100"
className="inline-flex items-center gap-2 px-3 text-sm font-medium text-white rounded-lg h-9 hover:bg-neutral-900 focus:outline-none dark:text-black dark:hover:bg-neutral-100"
>
{t("note.menu.viewAuthor")}
</button>
</DropdownMenu.Item>
<DropdownMenu.Separator className="my-1 h-px bg-neutral-900 dark:bg-neutral-100" />
<DropdownMenu.Separator className="h-px my-1 bg-neutral-900 dark:bg-neutral-100" />
<DropdownMenu.Item asChild>
<button
type="button"
onClick={() => copyRaw()}
className="inline-flex h-9 items-center gap-2 rounded-lg px-3 text-sm font-medium text-white hover:bg-neutral-900 focus:outline-none dark:text-black dark:hover:bg-neutral-100"
className="inline-flex items-center gap-2 px-3 text-sm font-medium text-white rounded-lg h-9 hover:bg-neutral-900 focus:outline-none dark:text-black dark:hover:bg-neutral-100"
>
{t("note.menu.copyRaw")}
</button>

View File

@@ -25,7 +25,7 @@ export function ImagePreview({ url }: { url: string }) {
};
return (
<div className="group relative my-1">
<div className="relative my-1 group">
<img
src={url}
alt={url}
@@ -34,6 +34,7 @@ export function ImagePreview({ url }: { url: string }) {
style={{ contentVisibility: "auto" }}
className="max-h-[600px] w-auto object-cover rounded-lg outline outline-1 -outline-offset-1 outline-black/15"
onClick={() => open(url)}
onKeyDown={() => open(url)}
onError={({ currentTarget }) => {
currentTarget.onerror = null;
currentTarget.src = "/404.jpg";

View File

@@ -27,7 +27,7 @@ export function Images({ urls }: { urls: string[] }) {
if (urls.length === 1) {
return (
<div className="group px-3">
<div className="px-3 group">
<img
src={urls[0]}
alt={urls[0]}
@@ -36,6 +36,7 @@ export function Images({ urls }: { urls: string[] }) {
style={{ contentVisibility: "auto" }}
className="max-h-[400px] w-auto object-cover rounded-lg outline outline-1 -outline-offset-1 outline-black/15"
onClick={() => open(urls[0])}
onKeyDown={() => open(urls[0])}
onError={({ currentTarget }) => {
currentTarget.onerror = null;
currentTarget.src = "/404.jpg";
@@ -56,8 +57,9 @@ export function Images({ urls }: { urls: string[] }) {
loading="lazy"
decoding="async"
style={{ contentVisibility: "auto" }}
className="w-full h-full object-cover rounded-lg outline outline-1 -outline-offset-1 outline-black/15"
className="object-cover w-full h-full rounded-lg outline outline-1 -outline-offset-1 outline-black/15"
onClick={() => open(item)}
onKeyDown={() => open(item)}
onError={({ currentTarget }) => {
currentTarget.onerror = null;
currentTarget.src = "/404.jpg";

View File

@@ -1,8 +1,8 @@
export function VideoPreview({ url }: { url: string }) {
return (
<div className="my-1 overflow-hidden rounded-xl">
<div className="my-1">
<video
className="h-auto w-full bg-neutral-100 text-sm dark:bg-neutral-900"
className="max-h-[600px] w-auto object-cover rounded-lg outline outline-1 -outline-offset-1 outline-black/15"
controls
muted
>

View File

@@ -1,5 +1,4 @@
import { LumeEvent } from "@lume/system";
import type { NostrEvent } from "@lume/types";
import type { LumeEvent } from "@lume/system";
import { type ReactNode, createContext, useContext } from "react";
const NoteContext = createContext<LumeEvent>(null);
@@ -8,14 +7,10 @@ export function NoteProvider({
event,
children,
}: {
event: NostrEvent;
event: LumeEvent;
children: ReactNode;
}) {
const lumeEvent = new LumeEvent(event);
return (
<NoteContext.Provider value={lumeEvent}>{children}</NoteContext.Provider>
);
return <NoteContext.Provider value={event}>{children}</NoteContext.Provider>;
}
export function useNoteContext() {

View File

@@ -15,9 +15,9 @@ export function NoteUser({ className }: { className?: string }) {
>
<div className="flex w-full gap-2">
<HoverCard.Trigger className="shrink-0">
<User.Avatar className="size-8 rounded-full object-cover outline outline-1 -outline-offset-1 outline-black/15" />
<User.Avatar className="object-cover rounded-full size-8 outline outline-1 -outline-offset-1 outline-black/15" />
</HoverCard.Trigger>
<div className="flex w-full items-center gap-3">
<div className="flex items-center w-full gap-3">
<div className="flex items-center gap-1">
<User.Name className="font-semibold text-neutral-950 dark:text-neutral-50" />
<User.NIP05 />
@@ -37,16 +37,17 @@ export function NoteUser({ className }: { className?: string }) {
side="right"
>
<div className="flex flex-col gap-2">
<User.Avatar className="size-11 rounded-lg object-cover" />
<User.Avatar className="object-cover rounded-lg size-11" />
<div className="flex flex-col gap-2">
<div className="inline-flex items-center gap-1">
<User.Name className="font-semibold leading-tight text-white dark:text-neutral-900" />
<User.NIP05 />
</div>
<User.About className="line-clamp-3 text-sm text-white dark:text-neutral-900" />
<User.About className="text-sm text-white line-clamp-3 dark:text-neutral-900" />
<button
type="button"
onClick={() => LumeWindow.openProfile(event.pubkey)}
className="mt-2 inline-flex h-9 w-full items-center justify-center rounded-lg bg-white text-sm font-medium hover:bg-neutral-200 dark:bg-neutral-100 dark:text-neutral-900 dark:hover:bg-neutral-200"
className="inline-flex items-center justify-center w-full mt-2 text-sm font-medium bg-white rounded-lg h-9 hover:bg-neutral-200 dark:bg-neutral-100 dark:text-neutral-900 dark:hover:bg-neutral-200"
>
View profile
</button>

View File

@@ -1,32 +0,0 @@
import type { NostrEvent } from "@lume/types";
import { Note } from "@/components/note";
import { cn } from "@lume/utils";
export function Notification({
event,
className,
}: {
event: NostrEvent;
className?: string;
}) {
return (
<Note.Provider event={event}>
<Note.Root
className={cn(
"bg-white dark:bg-black/20 backdrop-blur-lg rounded-xl flex flex-col gap-3 shadow-primary dark:ring-1 ring-neutral-800/50",
className,
)}
>
<div>
<div className="px-3 h-14 flex items-center justify-between">
<Note.User />
</div>
<Note.Content className="px-3" />
</div>
<div className="flex items-center h-14 px-3">
<Note.Open />
</div>
</Note.Root>
</Note.Provider>
);
}

View File

@@ -1,19 +1,15 @@
import { QuoteIcon } from "@lume/icons";
import type { NostrEvent } from "@lume/types";
import { Note } from "@/components/note";
import { cn } from "@lume/utils";
import type { LumeEvent } from "@lume/system";
export function Quote({
event,
className,
}: {
event: NostrEvent;
event: LumeEvent;
className?: string;
}) {
const quoteEventId = event.tags.find(
(tag) => tag[0] === "q" || tag[3] === "mention",
)?.[1];
return (
<Note.Provider event={event}>
<Note.Root
@@ -23,7 +19,7 @@ export function Quote({
)}
>
<div className="flex flex-col gap-3">
<Note.Child eventId={quoteEventId} isRoot />
<Note.Child event={event.quote} isRoot />
<div className="flex items-center gap-2 px-3">
<div className="inline-flex items-center gap-1.5 shrink-0 text-sm font-medium text-neutral-600 dark:text-neutral-400">
<QuoteIcon className="size-4" />
@@ -32,13 +28,13 @@ export function Quote({
<div className="flex-1 h-px bg-neutral-100 dark:bg-white/5" />
</div>
<div>
<div className="px-3 h-14 flex items-center justify-between">
<div className="flex items-center justify-between px-3 h-14">
<Note.User />
</div>
<Note.Content className="px-3" quote={false} clean />
</div>
</div>
<div className="flex items-center h-14 px-3">
<div className="flex items-center px-3 h-14">
<Note.Open />
</div>
</Note.Root>

View File

@@ -3,73 +3,75 @@ import { Note } from "@/components/note";
import { User } from "@/components/user";
import { cn } from "@lume/utils";
import { useQuery } from "@tanstack/react-query";
import type { NostrEvent } from "@lume/types";
import { NostrQuery } from "@lume/system";
import { type LumeEvent, NostrQuery } from "@lume/system";
export function RepostNote({
event,
className,
}: {
event: NostrEvent;
event: LumeEvent;
className?: string;
}) {
const {
isLoading,
isError,
data: repostEvent,
} = useQuery({
queryKey: ["repost", event.id],
const { isLoading, isError, data } = useQuery({
queryKey: ["event", event.repostId],
queryFn: async () => {
try {
const id = event.tags.find((el) => el[0] === "e")[1];
const repostEvent = await NostrQuery.getEvent(id);
return repostEvent;
const data = await NostrQuery.getRepostEvent(event);
return data;
} catch (e) {
throw new Error(e);
}
},
refetchOnWindowFocus: false,
refetchOnMount: false,
refetchOnReconnect: false,
staleTime: Number.POSITIVE_INFINITY,
retry: 2,
});
return (
<Note.Root
className={cn(
"bg-white dark:bg-black/20 backdrop-blur-lg rounded-xl mb-3 shadow-primary dark:ring-1 ring-neutral-800/50",
"bg-white dark:bg-black/20 backdrop-blur-lg rounded-xl shadow-primary dark:ring-1 ring-neutral-800/50",
className,
)}
>
<User.Provider pubkey={event.pubkey}>
<User.Root className="flex items-center gap-2 px-3 py-3 border-b border-neutral-100 dark:border-neutral-800/50 rounded-t-xl">
<div className="text-sm font-semibold text-neutral-700 dark:text-neutral-300">
Reposted by
</div>
<User.Avatar className="object-cover rounded-full size-6 shrink-0 ring-1 ring-neutral-200/50 dark:ring-neutral-800/50" />
</User.Root>
</User.Provider>
{isLoading ? (
<div className="flex items-center justify-center h-20 gap-2">
<Spinner />
Loading event...
<span className="text-sm font-medium text-neutral-700 dark:text-neutral-300">
Loading event...
</span>
</div>
) : isError || !repostEvent ? (
) : isError || !data ? (
<div className="flex items-center justify-center h-20">
Event not found within your current relay set
</div>
) : (
<Note.Provider event={repostEvent}>
<Note.Provider event={data}>
<Note.Root>
<div className="flex items-center justify-between px-3 h-14">
<Note.User />
<Note.Menu />
</div>
<Note.Content className="px-3" />
<div className="flex items-center gap-4 px-3 mt-3 h-14">
<Note.Open />
<Note.Reply />
<Note.Repost />
<Note.Zap />
<div className="flex items-center justify-between px-3 mt-3 h-14">
<div className="inline-flex items-center gap-3">
<Note.Open />
<Note.Reply />
<Note.Repost />
<Note.Zap />
</div>
<div>
<User.Provider pubkey={event.pubkey}>
<User.Root className="flex items-center gap-2">
<div className="text-sm font-medium text-neutral-800 dark:text-neutral-200">
Reposted by
</div>
<User.Avatar className="object-cover rounded-full size-6 shrink-0 ring-1 ring-neutral-200/50 dark:ring-neutral-800/50" />
</User.Root>
</User.Provider>
</div>
</div>
</Note.Root>
</Note.Provider>

View File

@@ -1,12 +1,12 @@
import type { NostrEvent } from "@lume/types";
import { cn } from "@lume/utils";
import { Note } from "@/components/note";
import type { LumeEvent } from "@lume/system";
export function TextNote({
event,
className,
}: {
event: NostrEvent;
event: LumeEvent;
className?: string;
}) {
return (
@@ -17,12 +17,12 @@ export function TextNote({
className,
)}
>
<div className="px-3 h-14 flex items-center justify-between">
<div className="flex items-center justify-between px-3 h-14">
<Note.User />
<Note.Menu />
</div>
<Note.Content className="px-3" />
<div className="mt-3 flex items-center gap-4 h-14 px-3">
<div className="flex items-center gap-4 px-3 mt-3 h-14">
<Note.Open />
<Note.Reply />
<Note.Repost />

View File

@@ -1,6 +1,5 @@
import { cn } from "@lume/utils";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { Spinner } from "@lume/ui";
import { useUserContext } from "./provider";
import { NostrAccount } from "@lume/system";
@@ -14,34 +13,30 @@ export function UserFollowButton({
}) {
const user = useUserContext();
const [t] = useTranslation();
const [loading, setLoading] = useState(false);
const [followed, setFollowed] = useState(false);
const toggleFollow = async () => {
setLoading(true);
if (!followed) {
const add = await NostrAccount.follow(user.pubkey, user.profile?.name);
if (add) setFollowed(true);
} else {
const remove = await NostrAccount.unfollow(user.pubkey);
if (remove) setFollowed(false);
const toggle = await NostrAccount.toggleContact(user.pubkey);
if (toggle) {
setFollowed((prev) => !prev);
setLoading(false);
}
setLoading(false);
};
useEffect(() => {
async function status() {
setLoading(true);
let mounted = true;
const contacts = await NostrAccount.getContactList();
if (contacts?.includes(user.pubkey)) {
setFollowed(true);
}
NostrAccount.checkContact(user.pubkey).then((status) => {
if (mounted) setFollowed(status);
});
setLoading(false);
}
status();
return () => {
mounted = false;
};
}, []);
return (
@@ -55,10 +50,10 @@ export function UserFollowButton({
<Spinner className="size-4" />
) : followed ? (
!simple ? (
t("user.unfollow")
"Unfollow"
) : null
) : (
t("user.follow")
"Follow"
)}
</button>
);

View File

@@ -4,19 +4,32 @@ import * as Tooltip from "@radix-ui/react-tooltip";
import { useQuery } from "@tanstack/react-query";
import { useUserContext } from "./provider";
import { NostrQuery } from "@lume/system";
import { experimental_createPersister } from "@tanstack/query-persist-client-core";
export function UserNip05() {
const user = useUserContext();
const { isLoading, data: verified } = useQuery({
queryKey: ["nip05", user?.pubkey],
queryFn: async () => {
if (!user.profile?.nip05?.length) return false;
const verify = await NostrQuery.verifyNip05(
user.pubkey,
user.profile?.nip05,
);
return verify;
},
enabled: !!user.profile?.nip05,
refetchOnMount: false,
refetchOnWindowFocus: false,
refetchOnReconnect: false,
staleTime: Number.POSITIVE_INFINITY,
retry: false,
persister: experimental_createPersister({
storage: localStorage,
maxAge: 1000 * 60 * 60 * 72, // 72 hours
}),
});
if (!user.profile?.nip05?.length) return;
@@ -26,7 +39,7 @@ export function UserNip05() {
<Tooltip.Root delayDuration={150}>
<Tooltip.Trigger>
{!isLoading && verified ? (
<VerifiedIcon className="size-4 text-teal-500" />
<VerifiedIcon className="text-teal-500 size-4" />
) : null}
</Tooltip.Trigger>
<Tooltip.Portal>