feat: add user screen
This commit is contained in:
@@ -21,7 +21,6 @@
|
|||||||
"@tanstack/react-router": "^1.16.6",
|
"@tanstack/react-router": "^1.16.6",
|
||||||
"i18next": "^23.10.0",
|
"i18next": "^23.10.0",
|
||||||
"i18next-resources-to-backend": "^1.2.0",
|
"i18next-resources-to-backend": "^1.2.0",
|
||||||
"idb-keyval": "^6.2.1",
|
|
||||||
"nostr-tools": "^2.3.1",
|
"nostr-tools": "^2.3.1",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
|
|||||||
@@ -222,7 +222,7 @@ function Screen() {
|
|||||||
<div className="flex h-full min-h-0 w-full">
|
<div className="flex h-full min-h-0 w-full">
|
||||||
<div className="flex h-full w-full flex-1 flex-col gap-2 px-2 pb-2">
|
<div className="flex h-full w-full flex-1 flex-col gap-2 px-2 pb-2">
|
||||||
{reply_to && !quote ? (
|
{reply_to && !quote ? (
|
||||||
<div className="flex flex-col gap-2 rounded-xl bg-white p-5 shadow-[rgba(50,_50,_105,_0.15)_0px_2px_5px_0px,_rgba(0,_0,_0,_0.05)_0px_1px_1px_0px] dark:bg-black dark:shadow-none dark:ring-1 dark:ring-white/5">
|
<div className="flex flex-col rounded-xl bg-white p-5 shadow-[rgba(50,_50,_105,_0.15)_0px_2px_5px_0px,_rgba(0,_0,_0,_0.05)_0px_1px_1px_0px] dark:bg-black dark:shadow-none dark:ring-1 dark:ring-white/5">
|
||||||
<h3 className="font-medium">Reply to:</h3>
|
<h3 className="font-medium">Reply to:</h3>
|
||||||
<MentionNote eventId={reply_to} />
|
<MentionNote eventId={reply_to} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,11 +1,46 @@
|
|||||||
import { createLazyFileRoute } from "@tanstack/react-router";
|
import { createLazyFileRoute } from "@tanstack/react-router";
|
||||||
|
import { WindowVirtualizer } from "virtua";
|
||||||
|
import { User } from "@lume/ui";
|
||||||
|
import { EventList } from "./-components/eventList";
|
||||||
|
|
||||||
export const Route = createLazyFileRoute("/users/$pubkey")({
|
export const Route = createLazyFileRoute("/users/$pubkey")({
|
||||||
component: User,
|
component: Screen,
|
||||||
});
|
});
|
||||||
|
|
||||||
function User() {
|
function Screen() {
|
||||||
const { pubkey } = Route.useParams();
|
const { pubkey } = Route.useParams();
|
||||||
|
|
||||||
return <div>{pubkey}</div>;
|
return (
|
||||||
|
<WindowVirtualizer>
|
||||||
|
<div className="flex h-screen w-screen flex-col bg-gradient-to-tr from-neutral-200 to-neutral-100 dark:from-neutral-950 dark:to-neutral-900">
|
||||||
|
<div data-tauri-drag-region className="h-11 w-full shrink-0" />
|
||||||
|
<div className="flex h-full min-h-0 w-full">
|
||||||
|
<div className="h-full w-full flex-1 px-2 pb-2">
|
||||||
|
<div className="h-full w-full overflow-hidden overflow-y-auto rounded-xl bg-white shadow-[rgba(50,_50,_105,_0.15)_0px_2px_5px_0px,_rgba(0,_0,_0,_0.05)_0px_1px_1px_0px] dark:bg-black dark:shadow-none dark:ring-1 dark:ring-white/5">
|
||||||
|
<User.Provider pubkey={pubkey}>
|
||||||
|
<User.Root>
|
||||||
|
<User.Cover className="h-44 w-full object-cover" />
|
||||||
|
<div className="relative -mt-8 flex flex-col gap-4 px-5">
|
||||||
|
<User.Avatar className="size-14 rounded-full" />
|
||||||
|
<div className="inline-flex items-start justify-between">
|
||||||
|
<div>
|
||||||
|
<User.Name className="font-semibold leading-tight" />
|
||||||
|
<User.NIP05 className="text-sm leading-tight text-neutral-600 dark:text-neutral-400" />
|
||||||
|
</div>
|
||||||
|
<User.Button className="h-9 w-24 rounded-full bg-black text-sm font-medium text-white hover:bg-neutral-900 dark:bg-neutral-900" />
|
||||||
|
</div>
|
||||||
|
<User.About />
|
||||||
|
</div>
|
||||||
|
</User.Root>
|
||||||
|
</User.Provider>
|
||||||
|
<div className="mt-4 px-5">
|
||||||
|
<h3 className="mb-4 text-lg font-semibold">Notes</h3>
|
||||||
|
<EventList id={pubkey} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</WindowVirtualizer>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
71
apps/desktop2/src/routes/users/-components/eventList.tsx
Normal file
71
apps/desktop2/src/routes/users/-components/eventList.tsx
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import { RepostNote } from "@/routes/$account/home/-components/repost";
|
||||||
|
import { TextNote } from "@/routes/$account/home/-components/text";
|
||||||
|
import { useArk } from "@lume/ark";
|
||||||
|
import { ArrowRightCircleIcon, LoaderIcon } from "@lume/icons";
|
||||||
|
import { Event, Kind } from "@lume/types";
|
||||||
|
import { EmptyFeed } from "@lume/ui";
|
||||||
|
import { FETCH_LIMIT } from "@lume/utils";
|
||||||
|
import { useInfiniteQuery } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
export function EventList({ id }: { id: string }) {
|
||||||
|
const ark = useArk();
|
||||||
|
const { data, hasNextPage, isLoading, isFetchingNextPage, fetchNextPage } =
|
||||||
|
useInfiniteQuery({
|
||||||
|
queryKey: ["events", id],
|
||||||
|
initialPageParam: 0,
|
||||||
|
queryFn: async ({ pageParam }: { pageParam: number }) => {
|
||||||
|
const events = await ark.get_events_from(id, FETCH_LIMIT, pageParam);
|
||||||
|
return events;
|
||||||
|
},
|
||||||
|
getNextPageParam: (lastPage) => {
|
||||||
|
const lastEvent = lastPage?.at(-1);
|
||||||
|
if (!lastEvent) return;
|
||||||
|
return lastEvent.created_at - 1;
|
||||||
|
},
|
||||||
|
select: (data) => data?.pages.flatMap((page) => page),
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const renderItem = (event: Event) => {
|
||||||
|
if (!event) return;
|
||||||
|
switch (event.kind) {
|
||||||
|
case Kind.Repost:
|
||||||
|
return <RepostNote key={event.id} event={event} />;
|
||||||
|
default:
|
||||||
|
return <TextNote key={event.id} event={event} />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="flex h-20 w-full flex-col items-center justify-center gap-1">
|
||||||
|
<LoaderIcon className="size-5 animate-spin" />
|
||||||
|
</div>
|
||||||
|
) : !data.length ? (
|
||||||
|
<EmptyFeed />
|
||||||
|
) : (
|
||||||
|
data.map((item) => renderItem(item))
|
||||||
|
)}
|
||||||
|
<div className="flex h-20 items-center justify-center">
|
||||||
|
{hasNextPage ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => fetchNextPage()}
|
||||||
|
disabled={!hasNextPage || isFetchingNextPage}
|
||||||
|
className="inline-flex h-12 w-36 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" />
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<ArrowRightCircleIcon className="size-5" />
|
||||||
|
Load more
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -99,6 +99,23 @@ export class Ark {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async get_events_from(id: string, limit: number, asOf?: number) {
|
||||||
|
try {
|
||||||
|
let until: string = undefined;
|
||||||
|
if (asOf && asOf > 0) until = asOf.toString();
|
||||||
|
|
||||||
|
const nostrEvents: Event[] = await invoke("get_events_from", {
|
||||||
|
id,
|
||||||
|
limit,
|
||||||
|
until,
|
||||||
|
});
|
||||||
|
|
||||||
|
return nostrEvents.sort((a, b) => b.created_at - a.created_at);
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async get_events(
|
public async get_events(
|
||||||
type: "local" | "global",
|
type: "local" | "global",
|
||||||
limit: number,
|
limit: number,
|
||||||
|
|||||||
@@ -40,12 +40,12 @@ export function NoteMenu() {
|
|||||||
</button>
|
</button>
|
||||||
</DropdownMenu.Trigger>
|
</DropdownMenu.Trigger>
|
||||||
<DropdownMenu.Portal>
|
<DropdownMenu.Portal>
|
||||||
<DropdownMenu.Content className="flex w-[200px] flex-col overflow-hidden rounded-2xl bg-white/50 p-2 ring-1 ring-black/10 backdrop-blur-2xl focus:outline-none dark:bg-black/50 dark:ring-white/10">
|
<DropdownMenu.Content className="flex w-[200px] flex-col overflow-hidden rounded-xl bg-black p-1 shadow-md shadow-neutral-500/20 focus:outline-none dark:bg-white">
|
||||||
<DropdownMenu.Item asChild>
|
<DropdownMenu.Item asChild>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => copyLink()}
|
onClick={() => ark.open_thread(event.id)}
|
||||||
className="inline-flex h-9 items-center gap-3 rounded-lg px-3 text-sm font-medium text-black/70 hover:bg-black/10 hover:text-black focus:outline-none dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white"
|
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"
|
||||||
>
|
>
|
||||||
{t("note.menu.viewThread")}
|
{t("note.menu.viewThread")}
|
||||||
</button>
|
</button>
|
||||||
@@ -53,7 +53,8 @@ export function NoteMenu() {
|
|||||||
<DropdownMenu.Item asChild>
|
<DropdownMenu.Item asChild>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="inline-flex h-9 items-center gap-3 rounded-lg px-3 text-sm font-medium text-black/70 hover:bg-black/10 hover:text-black focus:outline-none dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white"
|
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"
|
||||||
>
|
>
|
||||||
{t("note.menu.copyLink")}
|
{t("note.menu.copyLink")}
|
||||||
</button>
|
</button>
|
||||||
@@ -62,7 +63,7 @@ export function NoteMenu() {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => copyID()}
|
onClick={() => copyID()}
|
||||||
className="inline-flex h-9 items-center gap-3 rounded-lg px-3 text-sm font-medium text-black/70 hover:bg-black/10 hover:text-black focus:outline-none dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white"
|
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"
|
||||||
>
|
>
|
||||||
{t("note.menu.copyNoteId")}
|
{t("note.menu.copyNoteId")}
|
||||||
</button>
|
</button>
|
||||||
@@ -71,33 +72,25 @@ export function NoteMenu() {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => copyNpub()}
|
onClick={() => copyNpub()}
|
||||||
className="inline-flex h-9 items-center gap-3 rounded-lg px-3 text-sm font-medium text-black/70 hover:bg-black/10 hover:text-black focus:outline-none dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white"
|
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"
|
||||||
>
|
>
|
||||||
{t("note.menu.copyAuthorId")}
|
{t("note.menu.copyAuthorId")}
|
||||||
</button>
|
</button>
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
<DropdownMenu.Item asChild>
|
<DropdownMenu.Item asChild>
|
||||||
<a
|
<button
|
||||||
href={`/users/${event.pubkey}`}
|
onClick={() => ark.open_profile(event.pubkey)}
|
||||||
className="inline-flex h-9 items-center gap-3 rounded-lg px-3 text-sm font-medium text-black/70 hover:bg-black/10 hover:text-black focus:outline-none dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white"
|
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"
|
||||||
>
|
>
|
||||||
{t("note.menu.viewAuthor")}
|
{t("note.menu.viewAuthor")}
|
||||||
</a>
|
|
||||||
</DropdownMenu.Item>
|
|
||||||
<DropdownMenu.Item asChild>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="inline-flex h-9 items-center gap-3 rounded-lg px-3 text-sm font-medium text-black/70 hover:bg-black/10 hover:text-black focus:outline-none dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white"
|
|
||||||
>
|
|
||||||
{t("note.menu.pinAuthor")}
|
|
||||||
</button>
|
</button>
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
<DropdownMenu.Separator className="my-1 h-px bg-black/10 dark:bg-white/10" />
|
<DropdownMenu.Separator className="my-1 h-px bg-neutral-900 dark:bg-neutral-100" />
|
||||||
<DropdownMenu.Item asChild>
|
<DropdownMenu.Item asChild>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => copyRaw()}
|
onClick={() => copyRaw()}
|
||||||
className="inline-flex h-9 items-center gap-3 rounded-lg px-3 text-sm font-medium text-black/70 hover:bg-black/10 hover:text-black focus:outline-none dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white"
|
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"
|
||||||
>
|
>
|
||||||
{t("note.menu.copyRaw")}
|
{t("note.menu.copyRaw")}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
7
pnpm-lock.yaml
generated
7
pnpm-lock.yaml
generated
@@ -96,9 +96,6 @@ importers:
|
|||||||
i18next-resources-to-backend:
|
i18next-resources-to-backend:
|
||||||
specifier: ^1.2.0
|
specifier: ^1.2.0
|
||||||
version: 1.2.0
|
version: 1.2.0
|
||||||
idb-keyval:
|
|
||||||
specifier: ^6.2.1
|
|
||||||
version: 6.2.1
|
|
||||||
nostr-tools:
|
nostr-tools:
|
||||||
specifier: ^2.3.1
|
specifier: ^2.3.1
|
||||||
version: 2.3.1(typescript@5.3.3)
|
version: 2.3.1(typescript@5.3.3)
|
||||||
@@ -4579,10 +4576,6 @@ packages:
|
|||||||
'@babel/runtime': 7.23.9
|
'@babel/runtime': 7.23.9
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/idb-keyval@6.2.1:
|
|
||||||
resolution: {integrity: sha512-8Sb3veuYCyrZL+VBt9LJfZjLUPWVvqn8tG28VqYNFCo43KHcKuq+b4EiXGeuaLAQWL2YmyDgMp2aSpH9JHsEQg==}
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/ieee754@1.2.1:
|
/ieee754@1.2.1:
|
||||||
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
|
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|||||||
@@ -107,6 +107,7 @@ fn main() {
|
|||||||
nostr::metadata::set_settings,
|
nostr::metadata::set_settings,
|
||||||
nostr::metadata::get_settings,
|
nostr::metadata::get_settings,
|
||||||
nostr::event::get_event,
|
nostr::event::get_event,
|
||||||
|
nostr::event::get_events_from,
|
||||||
nostr::event::get_local_events,
|
nostr::event::get_local_events,
|
||||||
nostr::event::get_global_events,
|
nostr::event::get_global_events,
|
||||||
nostr::event::get_event_thread,
|
nostr::event::get_event_thread,
|
||||||
|
|||||||
@@ -38,6 +38,39 @@ pub async fn get_event(id: &str, state: State<'_, Nostr>) -> Result<String, Stri
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn get_events_from(
|
||||||
|
id: &str,
|
||||||
|
limit: usize,
|
||||||
|
until: Option<&str>,
|
||||||
|
state: State<'_, Nostr>,
|
||||||
|
) -> Result<Vec<Event>, String> {
|
||||||
|
let client = &state.client;
|
||||||
|
let f_until = match until {
|
||||||
|
Some(until) => Timestamp::from_str(until).unwrap(),
|
||||||
|
None => Timestamp::now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Ok(author) = PublicKey::from_str(id) {
|
||||||
|
let filter = Filter::new()
|
||||||
|
.kinds(vec![Kind::TextNote, Kind::Repost])
|
||||||
|
.authors(vec![author])
|
||||||
|
.limit(limit)
|
||||||
|
.until(f_until);
|
||||||
|
|
||||||
|
if let Ok(events) = client
|
||||||
|
.get_events_of(vec![filter], Some(Duration::from_secs(10)))
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(events)
|
||||||
|
} else {
|
||||||
|
Err("Get text event failed".into())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Err("Parse author failed".into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn get_local_events(
|
pub async fn get_local_events(
|
||||||
limit: usize,
|
limit: usize,
|
||||||
|
|||||||
Reference in New Issue
Block a user