mirror of
https://github.com/luminous-devs/lume.git
synced 2024-09-29 16:30:55 +00:00
feat: add user screen
This commit is contained in:
parent
98ef1927f2
commit
2403231ac4
@ -21,7 +21,6 @@
|
||||
"@tanstack/react-router": "^1.16.6",
|
||||
"i18next": "^23.10.0",
|
||||
"i18next-resources-to-backend": "^1.2.0",
|
||||
"idb-keyval": "^6.2.1",
|
||||
"nostr-tools": "^2.3.1",
|
||||
"react": "^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 w-full flex-1 flex-col gap-2 px-2 pb-2">
|
||||
{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>
|
||||
<MentionNote eventId={reply_to} />
|
||||
</div>
|
||||
|
@ -1,11 +1,46 @@
|
||||
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")({
|
||||
component: User,
|
||||
component: Screen,
|
||||
});
|
||||
|
||||
function User() {
|
||||
function Screen() {
|
||||
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(
|
||||
type: "local" | "global",
|
||||
limit: number,
|
||||
|
@ -40,12 +40,12 @@ export function NoteMenu() {
|
||||
</button>
|
||||
</DropdownMenu.Trigger>
|
||||
<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>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => copyLink()}
|
||||
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={() => ark.open_thread(event.id)}
|
||||
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")}
|
||||
</button>
|
||||
@ -53,7 +53,8 @@ export function NoteMenu() {
|
||||
<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"
|
||||
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")}
|
||||
</button>
|
||||
@ -62,7 +63,7 @@ export function NoteMenu() {
|
||||
<button
|
||||
type="button"
|
||||
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")}
|
||||
</button>
|
||||
@ -71,33 +72,25 @@ export function NoteMenu() {
|
||||
<button
|
||||
type="button"
|
||||
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")}
|
||||
</button>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item asChild>
|
||||
<a
|
||||
href={`/users/${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"
|
||||
<button
|
||||
onClick={() => ark.open_profile(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"
|
||||
>
|
||||
{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>
|
||||
</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>
|
||||
<button
|
||||
type="button"
|
||||
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")}
|
||||
</button>
|
||||
|
@ -96,9 +96,6 @@ importers:
|
||||
i18next-resources-to-backend:
|
||||
specifier: ^1.2.0
|
||||
version: 1.2.0
|
||||
idb-keyval:
|
||||
specifier: ^6.2.1
|
||||
version: 6.2.1
|
||||
nostr-tools:
|
||||
specifier: ^2.3.1
|
||||
version: 2.3.1(typescript@5.3.3)
|
||||
@ -4579,10 +4576,6 @@ packages:
|
||||
'@babel/runtime': 7.23.9
|
||||
dev: false
|
||||
|
||||
/idb-keyval@6.2.1:
|
||||
resolution: {integrity: sha512-8Sb3veuYCyrZL+VBt9LJfZjLUPWVvqn8tG28VqYNFCo43KHcKuq+b4EiXGeuaLAQWL2YmyDgMp2aSpH9JHsEQg==}
|
||||
dev: false
|
||||
|
||||
/ieee754@1.2.1:
|
||||
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
|
||||
dev: false
|
||||
|
@ -107,6 +107,7 @@ fn main() {
|
||||
nostr::metadata::set_settings,
|
||||
nostr::metadata::get_settings,
|
||||
nostr::event::get_event,
|
||||
nostr::event::get_events_from,
|
||||
nostr::event::get_local_events,
|
||||
nostr::event::get_global_events,
|
||||
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]
|
||||
pub async fn get_local_events(
|
||||
limit: usize,
|
||||
|
Loading…
Reference in New Issue
Block a user