final design (#184)
* feat: redesign * feat: update other columns to new design * chore: small fixes * fix: better manage external webview * feat: redesign note * feat: update ui * chore: update * chore: update * chore: polish ui * chore: update auth ui * feat: finalize note design * chore: small fixes * feat: add window management in rust * chore: format * feat: update ui for event screen * feat: update event screen * feat: final
This commit is contained in:
@@ -1,69 +0,0 @@
|
||||
import { Account } from "@lume/types";
|
||||
import { User } from "@lume/ui";
|
||||
import {
|
||||
useNavigate,
|
||||
useParams,
|
||||
useRouteContext,
|
||||
} from "@tanstack/react-router";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export function Accounts() {
|
||||
const { ark } = useRouteContext({ strict: false });
|
||||
const params = useParams({ strict: false });
|
||||
|
||||
const [accounts, setAccounts] = useState<Account[]>(null);
|
||||
|
||||
useEffect(() => {
|
||||
async function getAllAccounts() {
|
||||
const data = await ark.get_all_accounts();
|
||||
if (data) setAccounts(data);
|
||||
}
|
||||
|
||||
getAllAccounts();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div data-tauri-drag-region className="flex items-center gap-3">
|
||||
{accounts
|
||||
? accounts.map((account) =>
|
||||
// @ts-ignore, useless
|
||||
account.npub === params.account ? (
|
||||
<Active key={account.npub} pubkey={account.npub} />
|
||||
) : (
|
||||
<Inactive key={account.npub} pubkey={account.npub} />
|
||||
),
|
||||
)
|
||||
: null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Inactive({ pubkey }: { pubkey: string }) {
|
||||
const { ark } = useRouteContext({ strict: false });
|
||||
const navigate = useNavigate();
|
||||
|
||||
const changeAccount = async (npub: string) => {
|
||||
const select = await ark.load_selected_account(npub);
|
||||
if (select) navigate({ to: "/$account/home", params: { account: npub } });
|
||||
};
|
||||
|
||||
return (
|
||||
<button type="button" onClick={() => changeAccount(pubkey)}>
|
||||
<User.Provider pubkey={pubkey}>
|
||||
<User.Root className="rounded-full">
|
||||
<User.Avatar className="aspect-square h-auto w-8 rounded-full object-cover" />
|
||||
</User.Root>
|
||||
</User.Provider>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function Active({ pubkey }: { pubkey: string }) {
|
||||
return (
|
||||
<User.Provider pubkey={pubkey}>
|
||||
<User.Root className="rounded-full ring-1 ring-teal-500 ring-offset-2 ring-offset-neutral-200 dark:ring-offset-neutral-950">
|
||||
<User.Avatar className="aspect-square h-auto w-7 rounded-full object-cover" />
|
||||
</User.Root>
|
||||
</User.Provider>
|
||||
);
|
||||
}
|
||||
@@ -1,42 +1,47 @@
|
||||
import { Spinner } from "@lume/ui";
|
||||
import { cn } from "@lume/utils";
|
||||
import { useRouteContext } from "@tanstack/react-router";
|
||||
import { Dispatch, ReactNode, SetStateAction, useState } from "react";
|
||||
import {
|
||||
type Dispatch,
|
||||
type ReactNode,
|
||||
type SetStateAction,
|
||||
useState,
|
||||
} from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export function AvatarUploader({
|
||||
setPicture,
|
||||
children,
|
||||
className,
|
||||
setPicture,
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
setPicture: Dispatch<SetStateAction<string>>;
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
setPicture: Dispatch<SetStateAction<string>>;
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}) {
|
||||
const { ark } = useRouteContext({ strict: false });
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { ark } = useRouteContext({ strict: false });
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const uploadAvatar = async () => {
|
||||
// start loading
|
||||
setLoading(true);
|
||||
try {
|
||||
const image = await ark.upload();
|
||||
setPicture(image);
|
||||
} catch (e) {
|
||||
toast.error(String(e));
|
||||
}
|
||||
const uploadAvatar = async () => {
|
||||
// start loading
|
||||
setLoading(true);
|
||||
try {
|
||||
const image = await ark.upload();
|
||||
setPicture(image);
|
||||
} catch (e) {
|
||||
toast.error(String(e));
|
||||
}
|
||||
|
||||
// stop loading
|
||||
setLoading(false);
|
||||
};
|
||||
// stop loading
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => uploadAvatar()}
|
||||
className={cn("size-4", className)}
|
||||
>
|
||||
{loading ? <Spinner className="size-4" /> : children}
|
||||
</button>
|
||||
);
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => uploadAvatar()}
|
||||
className={cn("size-4", className)}
|
||||
>
|
||||
{loading ? <Spinner className="size-4" /> : children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,39 +4,39 @@ import { useRouteContext } from "@tanstack/react-router";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
|
||||
export function Balance({ account }: { account: string }) {
|
||||
const { ark } = useRouteContext({ strict: false });
|
||||
const [balance, setBalance] = useState(0);
|
||||
const value = useMemo(() => getBitcoinDisplayValues(balance), [balance]);
|
||||
const { ark } = useRouteContext({ strict: false });
|
||||
const [balance, setBalance] = useState(0);
|
||||
const value = useMemo(() => getBitcoinDisplayValues(balance), [balance]);
|
||||
|
||||
useEffect(() => {
|
||||
async function getBalance() {
|
||||
const val = await ark.get_balance();
|
||||
setBalance(val);
|
||||
}
|
||||
useEffect(() => {
|
||||
async function getBalance() {
|
||||
const val = await ark.get_balance();
|
||||
setBalance(val);
|
||||
}
|
||||
|
||||
getBalance();
|
||||
}, []);
|
||||
getBalance();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
data-tauri-drag-region
|
||||
className="flex h-16 items-center justify-end px-3"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="text-end">
|
||||
<div className="text-sm leading-tight text-neutral-700 dark:text-neutral-300">
|
||||
Your balance
|
||||
</div>
|
||||
<div className="font-medium leading-tight">
|
||||
₿ {value.bitcoinFormatted}
|
||||
</div>
|
||||
</div>
|
||||
<User.Provider pubkey={account}>
|
||||
<User.Root>
|
||||
<User.Avatar className="size-9 rounded-full" />
|
||||
</User.Root>
|
||||
</User.Provider>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div
|
||||
data-tauri-drag-region
|
||||
className="flex h-16 items-center justify-end px-3"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="text-end">
|
||||
<div className="text-sm leading-tight text-neutral-700 dark:text-neutral-300">
|
||||
Your balance
|
||||
</div>
|
||||
<div className="font-medium leading-tight">
|
||||
₿ {value.bitcoinFormatted}
|
||||
</div>
|
||||
</div>
|
||||
<User.Provider pubkey={account}>
|
||||
<User.Root>
|
||||
<User.Avatar className="size-9 rounded-full" />
|
||||
</User.Root>
|
||||
</User.Provider>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,83 +1,160 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
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 { getCurrent } from "@tauri-apps/api/webviewWindow";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
export function Col({
|
||||
column,
|
||||
account,
|
||||
isScroll,
|
||||
isResize,
|
||||
column,
|
||||
account,
|
||||
isScroll,
|
||||
isResize,
|
||||
}: {
|
||||
column: LumeColumn;
|
||||
account: string;
|
||||
isScroll: boolean;
|
||||
isResize: boolean;
|
||||
column: LumeColumn;
|
||||
account: string;
|
||||
isScroll: boolean;
|
||||
isResize: boolean;
|
||||
}) {
|
||||
const container = useRef<HTMLDivElement>(null);
|
||||
const [webview, setWebview] = useState<string | undefined>(undefined);
|
||||
const container = useRef<HTMLDivElement>(null);
|
||||
const [webview, setWebview] = useState<string | undefined>(undefined);
|
||||
|
||||
const repositionWebview = async () => {
|
||||
if (webview && webview.length > 1) {
|
||||
const newRect = container.current.getBoundingClientRect();
|
||||
await invoke("reposition_column", {
|
||||
label: webview,
|
||||
x: newRect.x,
|
||||
y: newRect.y,
|
||||
});
|
||||
}
|
||||
};
|
||||
const repositionWebview = async () => {
|
||||
if (webview && webview.length > 1) {
|
||||
const newRect = container.current.getBoundingClientRect();
|
||||
await invoke("reposition_column", {
|
||||
label: webview,
|
||||
x: newRect.x,
|
||||
y: newRect.y,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const resizeWebview = async () => {
|
||||
if (webview && webview.length > 1) {
|
||||
const newRect = container.current.getBoundingClientRect();
|
||||
await invoke("resize_column", {
|
||||
label: webview,
|
||||
width: newRect.width,
|
||||
height: newRect.height,
|
||||
});
|
||||
}
|
||||
};
|
||||
const resizeWebview = async () => {
|
||||
if (webview && webview.length > 1) {
|
||||
const newRect = container.current.getBoundingClientRect();
|
||||
await invoke("resize_column", {
|
||||
label: webview,
|
||||
width: newRect.width,
|
||||
height: newRect.height,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
resizeWebview();
|
||||
}, [isResize]);
|
||||
useEffect(() => {
|
||||
resizeWebview();
|
||||
}, [isResize]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isScroll) repositionWebview();
|
||||
}, [isScroll]);
|
||||
useEffect(() => {
|
||||
if (isScroll) repositionWebview();
|
||||
}, [isScroll]);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
if (webview && webview.length > 1) return;
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
if (webview && webview.length > 1) return;
|
||||
|
||||
const rect = container.current.getBoundingClientRect();
|
||||
const windowLabel = `column-${column.label}`;
|
||||
const url =
|
||||
column.content +
|
||||
`?account=${account}&label=${column.label}&name=${column.name}`;
|
||||
const rect = container.current.getBoundingClientRect();
|
||||
const windowLabel = `column-${column.label}`;
|
||||
const url = `${column.content}?account=${account}&label=${column.label}&name=${column.name}`;
|
||||
|
||||
// create new webview
|
||||
const label: string = await invoke("create_column", {
|
||||
label: windowLabel,
|
||||
x: rect.x,
|
||||
y: rect.y,
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
url,
|
||||
});
|
||||
// create new webview
|
||||
const label: string = await invoke("create_column", {
|
||||
label: windowLabel,
|
||||
x: rect.x,
|
||||
y: rect.y,
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
url,
|
||||
});
|
||||
|
||||
setWebview(label);
|
||||
})();
|
||||
setWebview(label);
|
||||
})();
|
||||
|
||||
// close webview when unmounted
|
||||
return () => {
|
||||
if (webview && webview.length > 1) {
|
||||
invoke("close_column", {
|
||||
label: webview,
|
||||
});
|
||||
}
|
||||
};
|
||||
}, [webview]);
|
||||
// close webview when unmounted
|
||||
return () => {
|
||||
if (webview && webview.length > 1) {
|
||||
invoke("close_column", {
|
||||
label: webview,
|
||||
});
|
||||
}
|
||||
};
|
||||
}, [webview]);
|
||||
|
||||
return <div ref={container} className="h-full w-[440px] shrink-0 p-2" />;
|
||||
return (
|
||||
<div className="h-full w-[440px] shrink-0 p-2">
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col w-full h-full rounded-xl",
|
||||
column.label !== "open"
|
||||
? "bg-black/5 dark:bg-white/5 backdrop-blur-sm"
|
||||
: "",
|
||||
)}
|
||||
>
|
||||
{column.label !== "open" ? (
|
||||
<Header label={column.label} name={column.name} />
|
||||
) : null}
|
||||
<div ref={container} className="flex-1 w-full h-full" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Header({ label, name }: { label: string; name: string }) {
|
||||
const [title, setTitle] = useState(name);
|
||||
const [isChanged, setIsChanged] = useState(false);
|
||||
|
||||
const saveNewTitle = async () => {
|
||||
const mainWindow = getCurrent();
|
||||
await mainWindow.emit("columns", { type: "set_title", label, title });
|
||||
|
||||
// update search params
|
||||
// @ts-ignore, hahaha
|
||||
search.name = title;
|
||||
|
||||
// reset state
|
||||
setIsChanged(false);
|
||||
};
|
||||
|
||||
const close = async () => {
|
||||
const mainWindow = getCurrent();
|
||||
await mainWindow.emit("columns", { type: "remove", label });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (title.length !== name.length) setIsChanged(true);
|
||||
}, [title]);
|
||||
|
||||
return (
|
||||
<div className="h-9 w-full flex items-center justify-between shrink-0 px-1">
|
||||
<div className="size-7" />
|
||||
<div className="shrink-0 h-9 flex items-center justify-center">
|
||||
<div className="relative flex gap-2 items-center">
|
||||
<div
|
||||
contentEditable
|
||||
suppressContentEditableWarning={true}
|
||||
onBlur={(e) => setTitle(e.currentTarget.textContent)}
|
||||
className="text-sm font-medium focus:outline-none"
|
||||
>
|
||||
{name}
|
||||
</div>
|
||||
{isChanged ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => saveNewTitle()}
|
||||
className="text-teal-500 hover:text-teal-600"
|
||||
>
|
||||
<CheckIcon className="size-4" />
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
<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"
|
||||
>
|
||||
<CancelIcon className="size-4" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
55
apps/desktop2/src/components/conversation.tsx
Normal file
55
apps/desktop2/src/components/conversation.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import { ThreadIcon } from "@lume/icons";
|
||||
import type { Event } from "@lume/types";
|
||||
import { Note } from "@lume/ui";
|
||||
import { cn } from "@lume/utils";
|
||||
import { useRouteContext } from "@tanstack/react-router";
|
||||
|
||||
export function Conversation({
|
||||
event,
|
||||
className,
|
||||
}: {
|
||||
event: Event;
|
||||
className?: string;
|
||||
}) {
|
||||
const { ark } = useRouteContext({ strict: false });
|
||||
const thread = ark.parse_event_thread({
|
||||
content: event.content,
|
||||
tags: event.tags,
|
||||
});
|
||||
|
||||
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 className="flex flex-col gap-3">
|
||||
{thread?.rootEventId ? (
|
||||
<Note.Child eventId={thread?.rootEventId} 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" />
|
||||
Thread
|
||||
</div>
|
||||
<div className="flex-1 h-px bg-neutral-100 dark:bg-white/5" />
|
||||
</div>
|
||||
{thread?.replyEventId ? (
|
||||
<Note.Child eventId={thread?.replyEventId} />
|
||||
) : null}
|
||||
<div>
|
||||
<div className="px-3 h-14 flex items-center justify-between">
|
||||
<Note.User />
|
||||
</div>
|
||||
<Note.Content className="px-3" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center h-14 px-3">
|
||||
<Note.Open />
|
||||
</div>
|
||||
</Note.Root>
|
||||
</Note.Provider>
|
||||
);
|
||||
}
|
||||
47
apps/desktop2/src/components/quote.tsx
Normal file
47
apps/desktop2/src/components/quote.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import { QuoteIcon } from "@lume/icons";
|
||||
import type { Event } from "@lume/types";
|
||||
import { Note } from "@lume/ui";
|
||||
import { cn } from "@lume/utils";
|
||||
|
||||
export function Quote({
|
||||
event,
|
||||
className,
|
||||
}: {
|
||||
event: Event;
|
||||
className?: string;
|
||||
}) {
|
||||
const quoteEventId = event.tags.find(
|
||||
(tag) => tag[0] === "q" || tag[3] === "mention",
|
||||
)?.[1];
|
||||
|
||||
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 className="flex flex-col gap-3">
|
||||
<Note.Child eventId={quoteEventId} 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" />
|
||||
Quote
|
||||
</div>
|
||||
<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">
|
||||
<Note.User />
|
||||
</div>
|
||||
<Note.Content className="px-3" quote={false} clean />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center h-14 px-3">
|
||||
<Note.Open />
|
||||
</div>
|
||||
</Note.Root>
|
||||
</Note.Provider>
|
||||
);
|
||||
}
|
||||
@@ -1,91 +1,83 @@
|
||||
import { RepostIcon } from "@lume/icons";
|
||||
import { Event } from "@lume/types";
|
||||
import type { Event } from "@lume/types";
|
||||
import { Note, Spinner, User } from "@lume/ui";
|
||||
import { cn } from "@lume/utils";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Note, Spinner, User } from "@lume/ui";
|
||||
import { useRouteContext } from "@tanstack/react-router";
|
||||
|
||||
export function RepostNote({
|
||||
event,
|
||||
className,
|
||||
event,
|
||||
className,
|
||||
}: {
|
||||
event: Event;
|
||||
className?: string;
|
||||
event: Event;
|
||||
className?: string;
|
||||
}) {
|
||||
const { ark, settings } = useRouteContext({ strict: false });
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
isLoading,
|
||||
isError,
|
||||
data: repostEvent,
|
||||
} = useQuery({
|
||||
queryKey: ["repost", event.id],
|
||||
queryFn: async () => {
|
||||
try {
|
||||
if (event.content.length > 50) {
|
||||
const embed: Event = JSON.parse(event.content);
|
||||
return embed;
|
||||
}
|
||||
const id = event.tags.find((el) => el[0] === "e")?.[1];
|
||||
if (id) return await ark.get_event(id);
|
||||
} catch (e) {
|
||||
throw new Error(e);
|
||||
}
|
||||
},
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnMount: false,
|
||||
});
|
||||
const { ark } = useRouteContext({ strict: false });
|
||||
const {
|
||||
isLoading,
|
||||
isError,
|
||||
data: repostEvent,
|
||||
} = useQuery({
|
||||
queryKey: ["repost", event.id],
|
||||
queryFn: async () => {
|
||||
try {
|
||||
if (event.content.length > 50) {
|
||||
const embed: Event = JSON.parse(event.content);
|
||||
return embed;
|
||||
}
|
||||
|
||||
return (
|
||||
<Note.Root
|
||||
className={cn(
|
||||
"flex flex-col gap-2 border-b border-neutral-100 px-3 py-5 dark:border-neutral-900",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<User.Provider pubkey={event.pubkey}>
|
||||
<User.Root className="flex gap-3">
|
||||
<div className="inline-flex w-11 shrink-0 items-center justify-center">
|
||||
<RepostIcon className="h-5 w-5 text-blue-500" />
|
||||
</div>
|
||||
<div className="inline-flex items-center gap-2">
|
||||
<User.Avatar className="size-6 shrink-0 rounded-full object-cover" />
|
||||
<div className="inline-flex items-baseline gap-1">
|
||||
<User.Name className="font-medium text-neutral-900 dark:text-neutral-100" />
|
||||
<span className="text-blue-500">{t("note.reposted")}</span>
|
||||
</div>
|
||||
</div>
|
||||
</User.Root>
|
||||
</User.Provider>
|
||||
{isLoading ? (
|
||||
<Spinner />
|
||||
) : isError ? (
|
||||
<div className="w-full h-16 flex items-center px-3 border border-neutral-100 dark:border-neutral-900">
|
||||
<p>Event not found</p>
|
||||
</div>
|
||||
) : (
|
||||
<Note.Provider event={repostEvent}>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Note.User />
|
||||
<div className="flex gap-3">
|
||||
<div className="size-11 shrink-0" />
|
||||
<div className="min-w-0 flex-1">
|
||||
<Note.Content />
|
||||
<div className="mt-4 flex items-center justify-between">
|
||||
<div className="-ml-1 inline-flex items-center gap-4">
|
||||
<Note.Reply />
|
||||
<Note.Repost />
|
||||
<Note.Pin />
|
||||
{settings.zap ? <Note.Zap /> : null}
|
||||
</div>
|
||||
<Note.Menu />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Note.Provider>
|
||||
)}
|
||||
</Note.Root>
|
||||
);
|
||||
const id = event.tags.find((el) => el[0] === "e")?.[1];
|
||||
const repostEvent = await ark.get_event(id);
|
||||
|
||||
return repostEvent;
|
||||
} catch (e) {
|
||||
throw new Error(e);
|
||||
}
|
||||
},
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnMount: false,
|
||||
});
|
||||
|
||||
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",
|
||||
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="size-6 shrink-0 rounded-full object-cover ring-1 ring-neutral-200/50 dark:ring-neutral-800/50" />
|
||||
</User.Root>
|
||||
</User.Provider>
|
||||
{isLoading ? (
|
||||
<div className="flex h-20 items-center justify-center gap-2">
|
||||
<Spinner />
|
||||
Loading event...
|
||||
</div>
|
||||
) : isError || !repostEvent ? (
|
||||
<div className="flex h-20 items-center justify-center">
|
||||
Event not found within your current relay set
|
||||
</div>
|
||||
) : (
|
||||
<Note.Provider event={repostEvent}>
|
||||
<Note.Root>
|
||||
<div className="px-3 h-14 flex items-center justify-between">
|
||||
<Note.User />
|
||||
<Note.Menu />
|
||||
</div>
|
||||
<Note.Content className="px-3" />
|
||||
<div className="mt-3 flex items-center gap-4 h-14 px-3">
|
||||
<Note.Open />
|
||||
<Note.Reply />
|
||||
<Note.Repost />
|
||||
<Note.Zap />
|
||||
</div>
|
||||
</Note.Root>
|
||||
</Note.Provider>
|
||||
)}
|
||||
</Note.Root>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,42 +1,34 @@
|
||||
import { Event } from "@lume/types";
|
||||
import type { Event } from "@lume/types";
|
||||
import { Note } from "@lume/ui";
|
||||
import { cn } from "@lume/utils";
|
||||
import { useRouteContext } from "@tanstack/react-router";
|
||||
|
||||
export function TextNote({
|
||||
event,
|
||||
className,
|
||||
event,
|
||||
className,
|
||||
}: {
|
||||
event: Event;
|
||||
className?: string;
|
||||
event: Event;
|
||||
className?: string;
|
||||
}) {
|
||||
const { settings } = useRouteContext({ strict: false });
|
||||
|
||||
return (
|
||||
<Note.Provider event={event}>
|
||||
<Note.Root
|
||||
className={cn(
|
||||
"flex flex-col gap-2 border-b border-neutral-100 px-3 py-5 dark:border-neutral-900",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<Note.User />
|
||||
<div className="flex gap-3">
|
||||
<div className="size-11 shrink-0" />
|
||||
<div className="min-w-0 flex-1">
|
||||
<Note.Content className="mb-2" />
|
||||
<Note.Thread />
|
||||
<div className="mt-4 flex items-center justify-between">
|
||||
<div className="-ml-1 inline-flex items-center gap-4">
|
||||
<Note.Reply />
|
||||
<Note.Repost />
|
||||
{settings.zap ? <Note.Zap /> : null}
|
||||
</div>
|
||||
<Note.Menu />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Note.Root>
|
||||
</Note.Provider>
|
||||
);
|
||||
return (
|
||||
<Note.Provider event={event}>
|
||||
<Note.Root
|
||||
className={cn(
|
||||
"bg-white dark:bg-black/20 backdrop-blur-lg rounded-xl shadow-primary dark:ring-1 ring-neutral-800/50",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className="px-3 h-14 flex items-center justify-between">
|
||||
<Note.User />
|
||||
<Note.Menu />
|
||||
</div>
|
||||
<Note.Content className="px-3" />
|
||||
<div className="mt-3 flex items-center gap-4 h-14 px-3">
|
||||
<Note.Open />
|
||||
<Note.Reply />
|
||||
<Note.Repost />
|
||||
<Note.Zap />
|
||||
</div>
|
||||
</Note.Root>
|
||||
</Note.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { ReactNode } from "@tanstack/react-router";
|
||||
import type { ReactNode } from "@tanstack/react-router";
|
||||
import { useLayoutEffect, useState } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
|
||||
export function Toolbar({ children }: { children: ReactNode }) {
|
||||
const [domReady, setDomReady] = useState(false);
|
||||
const [domReady, setDomReady] = useState(false);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
setDomReady(true);
|
||||
}, []);
|
||||
useLayoutEffect(() => {
|
||||
setDomReady(true);
|
||||
}, []);
|
||||
|
||||
return domReady
|
||||
? createPortal(children, document.getElementById("toolbar"))
|
||||
: null;
|
||||
return domReady
|
||||
? createPortal(children, document.getElementById("toolbar"))
|
||||
: null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user