diff --git a/apps/desktop2/package.json b/apps/desktop2/package.json index 7a9ee71b..843ad036 100644 --- a/apps/desktop2/package.json +++ b/apps/desktop2/package.json @@ -14,10 +14,13 @@ "@lume/ui": "workspace:^", "@lume/utils": "workspace:^", "@radix-ui/react-checkbox": "^1.0.4", + "@radix-ui/react-collapsible": "^1.0.3", "@tanstack/react-query": "^5.20.5", + "@tanstack/react-query-persist-client": "^5.22.2", "@tanstack/react-router": "^1.16.2", "i18next": "^23.8.2", "i18next-resources-to-backend": "^1.2.0", + "idb-keyval": "^6.2.1", "react": "^18.2.0", "react-dom": "^18.2.0", "react-i18next": "^14.0.5", diff --git a/apps/desktop2/src/app.tsx b/apps/desktop2/src/app.tsx index 8a483ee6..ee31f923 100644 --- a/apps/desktop2/src/app.tsx +++ b/apps/desktop2/src/app.tsx @@ -10,8 +10,35 @@ import i18n from "./locale"; import { Toaster } from "sonner"; import { locale, platform } from "@tauri-apps/plugin-os"; import { routeTree } from "./router.gen"; // auto generated file +import { get, set, del } from "idb-keyval"; +import { + PersistedClient, + Persister, +} from "@tanstack/react-query-persist-client"; +import { PersistQueryClientProvider } from "@tanstack/react-query-persist-client"; -const queryClient = new QueryClient(); +function createIDBPersister(idbValidKey: IDBValidKey = "reactQuery") { + return { + persistClient: async (client: PersistedClient) => { + await set(idbValidKey, client); + }, + restoreClient: async () => { + return await get(idbValidKey); + }, + removeClient: async () => { + await del(idbValidKey); + }, + } as Persister; +} + +const persister = createIDBPersister(); +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + gcTime: 1000 * 60 * 60 * 24, // 24 hours + }, + }, +}); const platformName = await platform(); const osLocale = (await locale()).slice(0, 2); @@ -53,12 +80,15 @@ if (!rootElement.innerHTML) { const root = ReactDOM.createRoot(rootElement); root.render( - + - + , ); } diff --git a/apps/desktop2/src/components/accounts.tsx b/apps/desktop2/src/components/accounts.tsx new file mode 100644 index 00000000..e09b2de0 --- /dev/null +++ b/apps/desktop2/src/components/accounts.tsx @@ -0,0 +1,38 @@ +import { useArk } from "@lume/ark"; +import { User } from "@lume/ui"; + +export function Accounts() { + const ark = useArk(); + + return ( +
+ {ark.accounts.map((account) => + account.npub === ark.account.npub ? ( + + ) : ( + + ), + )} +
+ ); +} + +function Inactive({ pubkey }: { pubkey: string }) { + return ( + + + + + + ); +} + +function Active({ pubkey }: { pubkey: string }) { + return ( + + + + + + ); +} diff --git a/apps/desktop2/src/routes/app.tsx b/apps/desktop2/src/routes/app.tsx index 45b74937..f74a8654 100644 --- a/apps/desktop2/src/routes/app.tsx +++ b/apps/desktop2/src/routes/app.tsx @@ -9,7 +9,7 @@ import { import { Link } from "@tanstack/react-router"; import { Outlet, createFileRoute } from "@tanstack/react-router"; import { cn } from "@lume/utils"; -import { ActiveAccount } from "@lume/ui"; +import { Accounts } from "@/components/accounts"; export const Route = createFileRoute("/app")({ component: App, @@ -27,65 +27,8 @@ function App() { context.platform === "macos" ? "pl-24" : "pl-4", )} > -
- - {({ isActive }) => ( -
- {isActive ? ( - - ) : ( - - )} - Home -
- )} - - - {({ isActive }) => ( -
- {isActive ? ( - - ) : ( - - )} - Space -
- )} - - - {({ isActive }) => ( -
- {isActive ? ( - - ) : ( - - )} - Activity -
- )} - -
-
- -
+ +
@@ -95,3 +38,64 @@ function App() {
); } + +function Navigation() { + return ( +
+ + {({ isActive }) => ( +
+ {isActive ? ( + + ) : ( + + )} + Home +
+ )} + + + {({ isActive }) => ( +
+ {isActive ? ( + + ) : ( + + )} + Space +
+ )} + + + {({ isActive }) => ( +
+ {isActive ? ( + + ) : ( + + )} + Activity +
+ )} + +
+ ); +} diff --git a/apps/desktop2/src/routes/app/home.lazy.tsx b/apps/desktop2/src/routes/app/home.lazy.tsx deleted file mode 100644 index 0dc351af..00000000 --- a/apps/desktop2/src/routes/app/home.lazy.tsx +++ /dev/null @@ -1,94 +0,0 @@ -import { useArk } from "@lume/ark"; -import { ArrowRightCircleIcon, LoaderIcon, SearchIcon } from "@lume/icons"; -import { Event, Kind } from "@lume/types"; -import { EmptyFeed, RepostNote, TextNote } from "@lume/ui"; -import { FETCH_LIMIT } from "@lume/utils"; -import { useInfiniteQuery } from "@tanstack/react-query"; -import { createLazyFileRoute } from "@tanstack/react-router"; -import { Virtualizer } from "virtua"; - -export const Route = createLazyFileRoute("/app/home")({ - component: Home, -}); - -function Home() { - const ark = useArk(); - const { data, hasNextPage, isLoading, isFetchingNextPage, fetchNextPage } = - useInfiniteQuery({ - queryKey: ["local_timeline"], - initialPageParam: 0, - queryFn: async ({ pageParam }: { pageParam: number }) => { - const events = await ark.get_events( - "local", - FETCH_LIMIT, - pageParam, - true, - ); - 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) => { - switch (event.kind) { - case Kind.Repost: - return ; - default: - return ; - } - }; - - return ( -
-
-
- {isLoading ? ( -
- -
- ) : !data.length ? ( - - ) : ( - - {data.map((item) => renderItem(item))} - - )} -
- {hasNextPage ? ( - - ) : null} -
-
-
-
- ); -} diff --git a/apps/desktop2/src/routes/app/home.tsx b/apps/desktop2/src/routes/app/home.tsx new file mode 100644 index 00000000..1f5a8586 --- /dev/null +++ b/apps/desktop2/src/routes/app/home.tsx @@ -0,0 +1,51 @@ +import { cn } from "@lume/utils"; +import { Link } from "@tanstack/react-router"; +import { Outlet, createFileRoute } from "@tanstack/react-router"; + +export const Route = createFileRoute("/app/home")({ + component: Home, +}); + +function Home() { + return ( +
+
+
+
+ + {({ isActive }) => ( +
+ Local +
+ )} + + + {({ isActive }) => ( +
+ Global +
+ )} + +
+
+
+ +
+
+
+ ); +} diff --git a/apps/desktop2/src/routes/app/home/-components/repost.tsx b/apps/desktop2/src/routes/app/home/-components/repost.tsx new file mode 100644 index 00000000..ea0d665e --- /dev/null +++ b/apps/desktop2/src/routes/app/home/-components/repost.tsx @@ -0,0 +1,114 @@ +import { RepostIcon } from "@lume/icons"; +import { Event } from "@lume/types"; +import { cn } from "@lume/utils"; +import { useQuery } from "@tanstack/react-query"; +import { useTranslation } from "react-i18next"; +import { useArk } from "@lume/ark"; +import { Note, User } from "@lume/ui"; + +export function RepostNote({ + event, + className, +}: { + event: Event; + className?: string; +}) { + const ark = useArk(); + + 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]; + return await ark.get_event(id); + } catch { + throw new Error("Failed to get repost event"); + } + }, + refetchOnWindowFocus: false, + refetchOnMount: false, + }); + + if (isLoading) { + return
Loading...
; + } + + if (isError || !repostEvent) { + return ( + + + +
+ +
+
+ +
+ + {t("note.reposted")} +
+
+
+
+
+
+

Failed to get event

+
+
+
+ ); + } + + return ( + + + +
+ +
+
+ +
+ + {t("note.reposted")} +
+
+
+
+ +
+ +
+
+
+ +
+ +
+ + + + +
+
+
+
+
+ + + ); +} diff --git a/apps/desktop2/src/routes/app/home/-components/text.tsx b/apps/desktop2/src/routes/app/home/-components/text.tsx new file mode 100644 index 00000000..71a9123c --- /dev/null +++ b/apps/desktop2/src/routes/app/home/-components/text.tsx @@ -0,0 +1,40 @@ +import { Event } from "@lume/types"; +import { Note } from "@lume/ui"; +import { cn } from "@lume/utils"; + +export function TextNote({ + event, + className, +}: { + event: Event; + className?: string; +}) { + return ( + + + +
+
+
+ + +
+ +
+ + + + +
+
+
+
+ + + ); +} diff --git a/apps/desktop2/src/routes/app/home/global.lazy.tsx b/apps/desktop2/src/routes/app/home/global.lazy.tsx new file mode 100644 index 00000000..f5f323c8 --- /dev/null +++ b/apps/desktop2/src/routes/app/home/global.lazy.tsx @@ -0,0 +1,92 @@ +import { useArk } from "@lume/ark"; +import { ArrowRightCircleIcon, LoaderIcon, SearchIcon } 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"; +import { createLazyFileRoute } from "@tanstack/react-router"; +import { Virtualizer } from "virtua"; +import { TextNote } from "./-components/text"; +import { RepostNote } from "./-components/repost"; + +export const Route = createLazyFileRoute("/app/home/global")({ + component: GlobalTimeline, +}); + +function GlobalTimeline() { + const ark = useArk(); + const { data, hasNextPage, isLoading, isFetchingNextPage, fetchNextPage } = + useInfiniteQuery({ + queryKey: ["events", "global"], + initialPageParam: 0, + queryFn: async ({ pageParam }: { pageParam: number }) => { + const events = await ark.get_events( + "global", + FETCH_LIMIT, + pageParam, + true, + ); + 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) => { + switch (event.kind) { + case Kind.Repost: + return ; + default: + return ; + } + }; + + return ( +
+ {isLoading ? ( +
+ +
+ ) : !data.length ? ( + + ) : ( + + {data.map((item) => renderItem(item))} + + )} +
+ {hasNextPage ? ( + + ) : null} +
+
+ ); +} diff --git a/apps/desktop2/src/routes/app/home/local.lazy.tsx b/apps/desktop2/src/routes/app/home/local.lazy.tsx new file mode 100644 index 00000000..8034bc9a --- /dev/null +++ b/apps/desktop2/src/routes/app/home/local.lazy.tsx @@ -0,0 +1,92 @@ +import { useArk } from "@lume/ark"; +import { ArrowRightCircleIcon, LoaderIcon, SearchIcon } 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"; +import { createLazyFileRoute } from "@tanstack/react-router"; +import { Virtualizer } from "virtua"; +import { TextNote } from "./-components/text"; +import { RepostNote } from "./-components/repost"; + +export const Route = createLazyFileRoute("/app/home/local")({ + component: LocalTimeline, +}); + +function LocalTimeline() { + const ark = useArk(); + const { data, hasNextPage, isLoading, isFetchingNextPage, fetchNextPage } = + useInfiniteQuery({ + queryKey: ["events", "local"], + initialPageParam: 0, + queryFn: async ({ pageParam }: { pageParam: number }) => { + const events = await ark.get_events( + "local", + FETCH_LIMIT, + pageParam, + true, + ); + 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) => { + switch (event.kind) { + case Kind.Repost: + return ; + default: + return ; + } + }; + + return ( +
+ {isLoading ? ( +
+ +
+ ) : !data.length ? ( + + ) : ( + + {data.map((item) => renderItem(item))} + + )} +
+ {hasNextPage ? ( + + ) : null} +
+
+ ); +} diff --git a/apps/desktop2/src/routes/events/$eventId.lazy.tsx b/apps/desktop2/src/routes/events/$eventId.lazy.tsx index f5f5d359..3b6cc2d5 100644 --- a/apps/desktop2/src/routes/events/$eventId.lazy.tsx +++ b/apps/desktop2/src/routes/events/$eventId.lazy.tsx @@ -1,6 +1,9 @@ -import { ReplyList, ThreadNote } from "@lume/ui"; +import { useEvent } from "@lume/ark"; +import { LoaderIcon } from "@lume/icons"; +import { Note, User } from "@lume/ui"; import { createLazyFileRoute } from "@tanstack/react-router"; import { WindowVirtualizer } from "virtua"; +import { ReplyList } from "./-components/replyList"; export const Route = createLazyFileRoute("/events/$eventId")({ component: Event, @@ -8,16 +11,63 @@ export const Route = createLazyFileRoute("/events/$eventId")({ function Event() { const { eventId } = Route.useParams(); + const { isLoading, isError, data } = useEvent(eventId); + + if (isLoading) { + return ( +
+ +
+ ); + } + + if (isError) { +
+

Not found.

+
; + } return ( -
-
- -
- - + +
+
+
+
+
+ + +
+ + + +
+ +
+ + ยท + +
+
+
+
+ +
+ + +
+ +
+ + +
+
+
+
+ {data ? : null} +
+
- -
+
+
); } diff --git a/apps/desktop2/src/routes/events/-components/reply.tsx b/apps/desktop2/src/routes/events/-components/reply.tsx new file mode 100644 index 00000000..8083299f --- /dev/null +++ b/apps/desktop2/src/routes/events/-components/reply.tsx @@ -0,0 +1,65 @@ +import { NavArrowDownIcon } from "@lume/icons"; +import { EventWithReplies } from "@lume/types"; +import { cn } from "@lume/utils"; +import * as Collapsible from "@radix-ui/react-collapsible"; +import { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { Note } from "@lume/ui"; +import { SubReply } from "./subReply"; + +export function Reply({ event }: { event: EventWithReplies }) { + const [t] = useTranslation(); + const [open, setOpen] = useState(false); + + return ( + + + +
+ + +
+ +
+ {event.replies?.length > 0 ? ( + +
+ + {`${event.replies?.length} ${ + event.replies?.length === 1 + ? t("note.reply.single") + : t("note.reply.plural") + }`} +
+
+ ) : ( +
+ )} +
+ + + +
+
+
+ {event.replies?.length > 0 ? ( + + {event.replies?.map((childEvent) => ( + + ))} + + ) : null} +
+ + + + ); +} diff --git a/apps/desktop2/src/routes/events/-components/replyList.tsx b/apps/desktop2/src/routes/events/-components/replyList.tsx new file mode 100644 index 00000000..ded929ab --- /dev/null +++ b/apps/desktop2/src/routes/events/-components/replyList.tsx @@ -0,0 +1,53 @@ +import { useArk } from "@lume/ark"; +import { LoaderIcon } from "@lume/icons"; +import { cn } from "@lume/utils"; +import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { EventWithReplies } from "@lume/types"; + +export function ReplyList({ + eventId, + className, +}: { + eventId: string; + className?: string; +}) { + const ark = useArk(); + + const [t] = useTranslation(); + const [data, setData] = useState(null); + + useEffect(() => { + async function getReplies() { + const events = await ark.get_event_thread(eventId); + setData(events); + } + getReplies(); + }, [eventId]); + + return ( +
+ {!data ? ( +
+ +
+ ) : data.length === 0 ? ( +
+
+

๐Ÿ‘‹

+

+ {t("note.reply.empty")} +

+
+
+ ) : ( + data.map((event) => ) + )} +
+ ); +} diff --git a/apps/desktop2/src/routes/events/-components/subReply.tsx b/apps/desktop2/src/routes/events/-components/subReply.tsx new file mode 100644 index 00000000..a8558edf --- /dev/null +++ b/apps/desktop2/src/routes/events/-components/subReply.tsx @@ -0,0 +1,20 @@ +import { Event } from "@lume/types"; +import { Note } from "@lume/ui"; + +export function SubReply({ event }: { event: Event; rootEventId?: string }) { + return ( + + +
+ + +
+ +
+ + +
+
+
+ ); +} diff --git a/apps/desktop2/src/routes/index.tsx b/apps/desktop2/src/routes/index.tsx index 54392682..ebba108f 100644 --- a/apps/desktop2/src/routes/index.tsx +++ b/apps/desktop2/src/routes/index.tsx @@ -23,7 +23,7 @@ export const Route = createFileRoute("/")({ const loadAccount = await ark.load_selected_account(accounts[0].npub); if (loadAccount) { throw redirect({ - to: "/app/home", + to: "/app/home/local", search: { redirect: location.href, }, diff --git a/apps/desktop2/tsconfig.json b/apps/desktop2/tsconfig.json index 34a32891..babab986 100644 --- a/apps/desktop2/tsconfig.json +++ b/apps/desktop2/tsconfig.json @@ -1,8 +1,12 @@ { - "extends": "@lume/tsconfig/base.json", - "compilerOptions": { - "outDir": "dist" - }, - "include": ["src"], - "exclude": ["node_modules", "dist"] + "extends": "@lume/tsconfig/base.json", + "compilerOptions": { + "outDir": "dist", + "baseUrl": "./", + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["src"], + "exclude": ["node_modules", "dist"] } diff --git a/packages/ark/src/ark.ts b/packages/ark/src/ark.ts index 2ca11f2b..61d0a24e 100644 --- a/packages/ark/src/ark.ts +++ b/packages/ark/src/ark.ts @@ -98,8 +98,7 @@ export class Ark { .split("'")[0] .split(".")[0]; const cmd: string = await invoke("get_event", { id: eventId }); - const event = JSON.parse(cmd) as Event; - + const event: Event = JSON.parse(cmd); return event; } catch (e) { return null; diff --git a/packages/ark/src/hooks/useProfile.ts b/packages/ark/src/hooks/useProfile.ts index f7bc9d4c..bd9ba697 100644 --- a/packages/ark/src/hooks/useProfile.ts +++ b/packages/ark/src/hooks/useProfile.ts @@ -6,7 +6,7 @@ export function useProfile(pubkey: string) { const { isLoading, isError, - data: user, + data: profile, } = useQuery({ queryKey: ["user", pubkey], queryFn: async () => { @@ -24,5 +24,5 @@ export function useProfile(pubkey: string) { retry: 2, }); - return { isLoading, isError, user }; + return { isLoading, isError, profile }; } diff --git a/packages/ui/src/account/active.tsx b/packages/ui/src/account/active.tsx index f08164bc..eabf1592 100644 --- a/packages/ui/src/account/active.tsx +++ b/packages/ui/src/account/active.tsx @@ -20,7 +20,7 @@ export function ActiveAccount() { ); const { t } = useTranslation(); - const { user } = useProfile(ark.account.npub); + const { profile } = useProfile(ark.account.npub); return ( @@ -32,7 +32,7 @@ export function ActiveAccount() { )} > (null); + const [loading, setLoading] = useState(false); + + const down = async () => { + // start loading + setLoading(true); + + const res = await ark.downvote(event.id, event.pubkey); + if (res) setReaction("-"); + + // stop loading + setLoading(false); + }; + + return ( + + ); +} diff --git a/packages/ui/src/note/buttons/reaction.tsx b/packages/ui/src/note/buttons/reaction.tsx index ed0d9f02..431d8de6 100644 --- a/packages/ui/src/note/buttons/reaction.tsx +++ b/packages/ui/src/note/buttons/reaction.tsx @@ -1,66 +1,11 @@ -import { ArrowDownIcon, ArrowUpIcon } from "@lume/icons"; -import { useState } from "react"; -import { useNoteContext } from "../provider"; -import { useArk } from "@lume/ark"; -import { cn } from "@lume/utils"; +import { NoteUpvote } from "./upvote"; +import { NoteDownvote } from "./downvote"; export function NoteReaction() { - const ark = useArk(); - const event = useNoteContext(); - - const [reaction, setReaction] = useState<"+" | "-">(null); - const [loading, setLoading] = useState(false); - - const up = async () => { - // start loading - setLoading(false); - - const res = await ark.upvote(event.id, event.pubkey); - if (res) setReaction("+"); - - // stop loading - setLoading(true); - }; - - const down = async () => { - // start loading - setLoading(false); - - const res = await ark.downvote(event.id, event.pubkey); - if (res) setReaction("-"); - - // stop loading - setLoading(true); - }; - return (
- - + +
); } diff --git a/packages/ui/src/note/buttons/upvote.tsx b/packages/ui/src/note/buttons/upvote.tsx new file mode 100644 index 00000000..9e760a93 --- /dev/null +++ b/packages/ui/src/note/buttons/upvote.tsx @@ -0,0 +1,44 @@ +import { ArrowUpIcon, LoaderIcon } from "@lume/icons"; +import { useState } from "react"; +import { useNoteContext } from "../provider"; +import { useArk } from "@lume/ark"; +import { cn } from "@lume/utils"; + +export function NoteUpvote() { + const ark = useArk(); + const event = useNoteContext(); + + const [reaction, setReaction] = useState<"+" | null>(null); + const [loading, setLoading] = useState(false); + + const up = async () => { + // start loading + setLoading(true); + + const res = await ark.upvote(event.id, event.pubkey); + if (res) setReaction("+"); + + // stop loading + setLoading(false); + }; + + return ( + + ); +} diff --git a/packages/ui/src/note/mentions/note.tsx b/packages/ui/src/note/mentions/note.tsx index 95097ba7..0984459a 100644 --- a/packages/ui/src/note/mentions/note.tsx +++ b/packages/ui/src/note/mentions/note.tsx @@ -1,4 +1,3 @@ -import { PinIcon } from "@lume/icons"; import { NOSTR_MENTIONS } from "@lume/utils"; import { ReactNode, useMemo } from "react"; import { useTranslation } from "react-i18next"; @@ -6,7 +5,7 @@ import reactStringReplace from "react-string-replace"; import { User } from "../../user"; import { Hashtag } from "./hashtag"; import { MentionUser } from "./user"; -import { useEvent } from "@lume/ark"; +import { useArk, useEvent } from "@lume/ark"; export function MentionNote({ eventId, @@ -18,6 +17,7 @@ export function MentionNote({ const { t } = useTranslation(); const { isLoading, isError, data } = useEvent(eventId); + const ark = useArk(); const richContent = useMemo(() => { if (!data) return ""; @@ -117,17 +117,18 @@ export function MentionNote({
-
+
{richContent}
{openable ? ( ) : (
diff --git a/packages/ui/src/note/menu.tsx b/packages/ui/src/note/menu.tsx index dfae551d..261cb4aa 100644 --- a/packages/ui/src/note/menu.tsx +++ b/packages/ui/src/note/menu.tsx @@ -34,7 +34,7 @@ export function NoteMenu() { diff --git a/packages/ui/src/note/primitives/text.tsx b/packages/ui/src/note/primitives/text.tsx index b30e6933..80af0b8c 100644 --- a/packages/ui/src/note/primitives/text.tsx +++ b/packages/ui/src/note/primitives/text.tsx @@ -22,7 +22,7 @@ export function TextNote({
-
+
diff --git a/packages/ui/src/note/user.tsx b/packages/ui/src/note/user.tsx index f1fe8886..5c428152 100644 --- a/packages/ui/src/note/user.tsx +++ b/packages/ui/src/note/user.tsx @@ -16,7 +16,7 @@ export function NoteUser({ className }: { className?: string }) { >
- +
diff --git a/packages/ui/src/replyList.tsx b/packages/ui/src/replyList.tsx index 1c142550..95c8b8f6 100644 --- a/packages/ui/src/replyList.tsx +++ b/packages/ui/src/replyList.tsx @@ -34,10 +34,6 @@ export function ReplyList({ className, )} > - {!data ? (
diff --git a/packages/ui/src/user/about.tsx b/packages/ui/src/user/about.tsx index 732f5d6c..8680cedb 100644 --- a/packages/ui/src/user/about.tsx +++ b/packages/ui/src/user/about.tsx @@ -4,34 +4,9 @@ import { useUserContext } from "./provider"; export function UserAbout({ className }: { className?: string }) { const user = useUserContext(); - if (!user.profile) { - return ( -
-
-
-
-
- ); - } - return (
- {user.profile.about?.trim() || "No bio"} + {user.profile?.about?.trim() || "No bio"}
); } diff --git a/packages/ui/src/user/avatar.tsx b/packages/ui/src/user/avatar.tsx index f5e0db12..c1327b62 100644 --- a/packages/ui/src/user/avatar.tsx +++ b/packages/ui/src/user/avatar.tsx @@ -11,28 +11,15 @@ export function UserAvatar({ className }: { className?: string }) { const fallbackAvatar = useMemo( () => `data:image/svg+xml;utf8,${encodeURIComponent( - minidenticon(user?.pubkey || nanoid(), 90, 50), + minidenticon(user.pubkey || nanoid(), 90, 50), )}`, [user], ); - if (!user.profile) { - return ( -
-
-
- ); - } - return ( - ); - } - return (
- {user.profile.display_name || user.profile.name || "Anon"} + {user.profile?.display_name || + user.profile?.name || + displayNpub(user.pubkey, 16)}
); } diff --git a/packages/ui/src/user/nip05.tsx b/packages/ui/src/user/nip05.tsx index efe2b3bf..9ec72d3a 100644 --- a/packages/ui/src/user/nip05.tsx +++ b/packages/ui/src/user/nip05.tsx @@ -18,25 +18,12 @@ export function UserNip05({ className }: { className?: string }) { enabled: !!user.profile, }); - if (!user.profile) { - return ( -
- ); - } - return (

- {!user?.profile?.nip05 + {!user.profile?.nip05 ? displayNpub(user.pubkey, 16) - : user?.profile?.nip05?.startsWith("_@") - ? user?.profile?.nip05?.replace("_@", "") - : user?.profile?.nip05} + : user.profile?.nip05.replace("_@", "")}

{!isLoading && verified ? ( diff --git a/packages/ui/src/user/provider.tsx b/packages/ui/src/user/provider.tsx index 6f0fa93d..dde81575 100644 --- a/packages/ui/src/user/provider.tsx +++ b/packages/ui/src/user/provider.tsx @@ -1,40 +1,25 @@ -import { useArk } from "@lume/ark"; +import { useProfile } from "@lume/ark"; import { Metadata } from "@lume/types"; -import { useQuery } from "@tanstack/react-query"; import { ReactNode, createContext, useContext } from "react"; -const UserContext = createContext<{ pubkey: string; profile: Metadata }>(null); +const UserContext = createContext<{ + pubkey: string; + isError: boolean; + isLoading: boolean; + profile: Metadata; +}>(null); export function UserProvider({ pubkey, children, - embed, }: { pubkey: string; children: ReactNode; - embed?: string; }) { - const ark = useArk(); - const { data: profile } = useQuery({ - queryKey: ["user", pubkey], - queryFn: async () => { - if (embed) return JSON.parse(embed) as Metadata; - try { - const profile: Metadata = await ark.get_profile(pubkey); - return profile; - } catch (e) { - throw new Error(e); - } - }, - refetchOnMount: false, - refetchOnWindowFocus: false, - refetchOnReconnect: false, - staleTime: Infinity, - retry: 2, - }); + const { isLoading, isError, profile } = useProfile(pubkey); return ( - + {children} ); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0e8dffd8..030a9c7c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -75,9 +75,15 @@ importers: '@radix-ui/react-checkbox': specifier: ^1.0.4 version: 1.0.4(@types/react-dom@18.2.19)(@types/react@18.2.55)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-collapsible': + specifier: ^1.0.3 + version: 1.0.3(@types/react-dom@18.2.19)(@types/react@18.2.55)(react-dom@18.2.0)(react@18.2.0) '@tanstack/react-query': specifier: ^5.20.5 version: 5.20.5(react@18.2.0) + '@tanstack/react-query-persist-client': + specifier: ^5.22.2 + version: 5.22.2(@tanstack/react-query@5.20.5)(react@18.2.0) '@tanstack/react-router': specifier: ^1.16.2 version: 1.16.2(react-dom@18.2.0)(react@18.2.0) @@ -87,6 +93,9 @@ importers: i18next-resources-to-backend: specifier: ^1.2.0 version: 1.2.0 + idb-keyval: + specifier: ^6.2.1 + version: 6.2.1 react: specifier: ^18.2.0 version: 18.2.0 @@ -202,7 +211,7 @@ importers: version: 1.0.4(@types/react@18.2.55)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-collapsible': specifier: ^1.0.3 - version: 1.0.3(@types/react@18.2.55)(react-dom@18.2.0)(react@18.2.0) + version: 1.0.3(@types/react-dom@18.2.19)(@types/react@18.2.55)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-dialog': specifier: ^1.0.5 version: 1.0.5(@types/react@18.2.55)(react-dom@18.2.0)(react@18.2.0) @@ -896,7 +905,7 @@ importers: version: 1.0.4(@types/react@18.2.55)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-collapsible': specifier: ^1.0.3 - version: 1.0.3(@types/react@18.2.55)(react-dom@18.2.0)(react@18.2.0) + version: 1.0.3(@types/react-dom@18.2.19)(@types/react@18.2.55)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-dialog': specifier: ^1.0.5 version: 1.0.5(@types/react@18.2.55)(react-dom@18.2.0)(react@18.2.0) @@ -1893,7 +1902,7 @@ packages: dependencies: '@babel/runtime': 7.23.9 '@radix-ui/primitive': 1.0.1 - '@radix-ui/react-collapsible': 1.0.3(@types/react@18.2.55)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-collapsible': 1.0.3(@types/react-dom@18.2.19)(@types/react@18.2.55)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-collection': 1.0.3(@types/react@18.2.55)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.55)(react@18.2.0) '@radix-ui/react-context': 1.0.1(@types/react@18.2.55)(react@18.2.0) @@ -2002,7 +2011,7 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false - /@radix-ui/react-collapsible@1.0.3(@types/react@18.2.55)(react-dom@18.2.0)(react@18.2.0): + /@radix-ui/react-collapsible@1.0.3(@types/react-dom@18.2.19)(@types/react@18.2.55)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-UBmVDkmR6IvDsloHVN+3rtx4Mi5TFvylYXpluuv0f37dtaz3H99bp8No0LGXRigVpl3UAT4l9j6bIchh42S/Gg==} peerDependencies: '@types/react': '*' @@ -2025,6 +2034,7 @@ packages: '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.55)(react@18.2.0) '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.2.55)(react@18.2.0) '@types/react': 18.2.55 + '@types/react-dom': 18.2.19 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: false @@ -2902,6 +2912,27 @@ packages: resolution: {integrity: sha512-T1W28gGgWn0A++tH3lxj3ZuUVZZorsiKcv+R50RwmPYz62YoDEkG4/aXHZELGkRp4DfrW07dyq2K5dvJ4Wl1aA==} dev: false + /@tanstack/query-core@5.22.2: + resolution: {integrity: sha512-z3PwKFUFACMUqe1eyesCIKg3Jv1mysSrYfrEW5ww5DCDUD4zlpTKBvUDaEjsfZzL3ULrFLDM9yVUxI/fega1Qg==} + dev: false + + /@tanstack/query-persist-client-core@5.22.2: + resolution: {integrity: sha512-sFDgWoN54uclIDIoImPmDzxTq8HhZEt9pO0JbVHjI6LPZqunMMF9yAq9zFKrpH//jD5f+rBCQsdGyhdpUo9e8Q==} + dependencies: + '@tanstack/query-core': 5.22.2 + dev: false + + /@tanstack/react-query-persist-client@5.22.2(@tanstack/react-query@5.20.5)(react@18.2.0): + resolution: {integrity: sha512-osAaQn2PDTaa2ApTLOAus7g8Y96LHfS2+Pgu/RoDlEJUEkX7xdEn0YuurxbnJaDJDESMfr+CH/eAX2y+lx02Fg==} + peerDependencies: + '@tanstack/react-query': ^5.22.2 + react: ^18.0.0 + dependencies: + '@tanstack/query-persist-client-core': 5.22.2 + '@tanstack/react-query': 5.20.5(react@18.2.0) + react: 18.2.0 + dev: false + /@tanstack/react-query@5.20.5(react@18.2.0): resolution: {integrity: sha512-6MHwJ8G9cnOC/XKrwt56QMc91vN7hLlAQNUA0ubP7h9Jj3a/CmkUwT6ALdFbnVP+PsYdhW3WONa8WQ4VcTaSLQ==} peerDependencies: @@ -4551,6 +4582,10 @@ packages: safer-buffer: 2.1.2 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