diff --git a/apps/desktop/src/router.tsx b/apps/desktop/src/router.tsx index e2f4c144..47bc921a 100644 --- a/apps/desktop/src/router.tsx +++ b/apps/desktop/src/router.tsx @@ -42,30 +42,6 @@ export default function Router() { return { Component: NWCScreen }; }, }, - { - path: "relays", - async lazy() { - const { RelaysScreen } = await import("./routes/relays"); - return { Component: RelaysScreen }; - }, - }, - { - path: "relays/:url", - loader: async ({ params }) => { - return defer({ - relay: fetch(`https://${params.url}`, { - method: "GET", - headers: { - Accept: "application/nostr+json", - }, - }).then((res) => res.json()), - }); - }, - async lazy() { - const { RelayScreen } = await import("./routes/relays/relay"); - return { Component: RelayScreen }; - }, - }, { path: "settings", element: , @@ -155,6 +131,51 @@ export default function Router() { }, ], }, + { + path: "relays", + async lazy() { + const { RelaysScreen } = await import("./routes/relays"); + return { Component: RelaysScreen }; + }, + children: [ + { + index: true, + async lazy() { + const { RelayGlobalScreen } = await import( + "./routes/relays/global" + ); + return { Component: RelayGlobalScreen }; + }, + }, + { + path: "follows", + async lazy() { + const { RelayFollowsScreen } = await import( + "./routes/relays/follows" + ); + return { Component: RelayFollowsScreen }; + }, + }, + { + path: ":url", + loader: async ({ request, params }) => { + return defer({ + relay: fetch(`https://${params.url}`, { + method: "GET", + headers: { + Accept: "application/nostr+json", + }, + signal: request.signal, + }).then((res) => res.json()), + }); + }, + async lazy() { + const { RelayUrlScreen } = await import("./routes/relays/url"); + return { Component: RelayUrlScreen }; + }, + }, + ], + }, { path: "depot", children: [ diff --git a/apps/desktop/src/routes/activty/components/list.tsx b/apps/desktop/src/routes/activty/components/list.tsx index b0ee6e39..61755fa5 100644 --- a/apps/desktop/src/routes/activty/components/list.tsx +++ b/apps/desktop/src/routes/activty/components/list.tsx @@ -91,13 +91,13 @@ export function ActivityList() { ) : ( allEvents.map((event) => renderEvenKind(event)) )} -
+
{hasNextPage ? ( -
+
+ setRelay((prev) => ({ ...prev, url: e.target.value }))} + /> +
); } diff --git a/apps/desktop/src/routes/relays/components/relayItem.tsx b/apps/desktop/src/routes/relays/components/relayItem.tsx new file mode 100644 index 00000000..171f404f --- /dev/null +++ b/apps/desktop/src/routes/relays/components/relayItem.tsx @@ -0,0 +1,38 @@ +import { useRelaylist } from "@lume/ark"; +import { PlusIcon, ShareIcon } from "@lume/icons"; +import { normalizeRelayUrl } from "nostr-fetch"; +import { Link } from "react-router-dom"; + +export function RelayItem({ url }: { url: string }) { + const domain = new URL(url).hostname; + const { connectRelay } = useRelaylist(); + + return ( +
+
+ + Relay:{" "} + + + {url} + +
+
+ + + Inspect + + +
+
+ ); +} diff --git a/apps/desktop/src/routes/relays/components/relayList.tsx b/apps/desktop/src/routes/relays/components/relayList.tsx index 04da838b..ef84d8cd 100644 --- a/apps/desktop/src/routes/relays/components/relayList.tsx +++ b/apps/desktop/src/routes/relays/components/relayList.tsx @@ -27,7 +27,7 @@ export function RelayList() { }; return ( -
+
{status === "pending" ? (
diff --git a/apps/desktop/src/routes/relays/components/userRelayList.tsx b/apps/desktop/src/routes/relays/components/sidebar.tsx similarity index 86% rename from apps/desktop/src/routes/relays/components/userRelayList.tsx rename to apps/desktop/src/routes/relays/components/sidebar.tsx index cf6617ad..93fd441d 100644 --- a/apps/desktop/src/routes/relays/components/userRelayList.tsx +++ b/apps/desktop/src/routes/relays/components/sidebar.tsx @@ -1,16 +1,15 @@ import { useArk } from "@lume/ark"; import { CancelIcon, RefreshIcon } from "@lume/icons"; -import { useStorage } from "@lume/storage"; +import { cn } from "@lume/utils"; import { NDKKind } from "@nostr-dev-kit/ndk"; import { useQuery } from "@tanstack/react-query"; import { RelayForm } from "./relayForm"; -export function UserRelayList() { +export function RelaySidebar({ className }: { className?: string }) { const ark = useArk(); - const storage = useStorage(); const { status, data, refetch } = useQuery({ - queryKey: ["relays", ark.account.pubkey], + queryKey: ["relay-personal"], queryFn: async () => { const event = await ark.getEventByFilter({ filter: { @@ -20,7 +19,7 @@ export function UserRelayList() { }); if (!event) return []; - return event.tags; + return event.tags.filter((tag) => tag[0] === "r"); }, refetchOnWindowFocus: false, }); @@ -30,8 +29,13 @@ export function UserRelayList() { ); return ( -
-
+
+

Connected relays

-

- Global events -

-
- -
-
-
-

- Information -

-
-
- - - Loading... -
- } - > - -

Could not load relay information 😬

-
- } - > - {(resolvedRelay: NIP11) => ( -
-
-

- {resolvedRelay.name} -

-

- {resolvedRelay.description} -

-
- {resolvedRelay.pubkey ? ( -
-
- Owner: -
-
- -
-
- ) : null} - {resolvedRelay.contact ? ( - - ) : null} - -
-
- Supported NIPs: -
-
- {resolvedRelay.supported_nips.map((item) => ( - - {item} - - ))} -
-
- {resolvedRelay.limitation ? ( -
-
- Limitation -
-
- {Object.keys(resolvedRelay.limitation).map( - (key, index) => { - return ( -
-

- {titleCase(key)}: -

-

- {resolvedRelay.limitation[key].toString()} -

-
- ); - }, - )} -
-
- ) : null} - {resolvedRelay.payments_url ? ( -
- - Open payment website - - - You need to make a payment to connect this relay - -
- ) : null} -
- )} - - -
-
-
- ); -} diff --git a/apps/desktop/src/routes/relays/url.tsx b/apps/desktop/src/routes/relays/url.tsx new file mode 100644 index 00000000..91238c58 --- /dev/null +++ b/apps/desktop/src/routes/relays/url.tsx @@ -0,0 +1,161 @@ +import { ArrowLeftIcon, LoaderIcon } from "@lume/icons"; +import { NIP11 } from "@lume/types"; +import { User } from "@lume/ui"; +import { Suspense } from "react"; +import { Await, useLoaderData, useNavigate, useParams } from "react-router-dom"; +import { RelayEventList } from "./components/relayEventList"; + +export function RelayUrlScreen() { + const { url } = useParams(); + + const data: { relay?: { [key: string]: string } } = useLoaderData(); + const navigate = useNavigate(); + + const getSoftwareName = (url: string) => { + const filename = url.substring(url.lastIndexOf("/") + 1); + return filename.replace(".git", ""); + }; + + const titleCase = (s: string) => { + return s + .replace(/^[-_]*(.)/, (_, c) => c.toUpperCase()) + .replace(/[-_]+(.)/g, (_, c) => ` ${c.toUpperCase()}`); + }; + + return ( +
+
+ +
+
+ + + Loading... +
+ } + > + +

Could not load relay information 😬

+
+ } + > + {(resolvedRelay: NIP11) => ( +
+
+

{resolvedRelay.name}

+

+ {resolvedRelay.description} +

+
+ {resolvedRelay.pubkey ? ( +
+
+ Owner: +
+
+ +
+
+ ) : null} + {resolvedRelay.contact ? ( + + ) : null} + +
+
+ Supported NIPs: +
+
+ {resolvedRelay.supported_nips.map((item) => ( + + {item} + + ))} +
+
+ {resolvedRelay.limitation ? ( +
+
+ Limitation +
+
+ {Object.keys(resolvedRelay.limitation).map( + (key, index) => { + return ( +
+

+ {titleCase(key)}: +

+

+ {resolvedRelay.limitation[key].toString()} +

+
+ ); + }, + )} +
+
+ ) : null} + {resolvedRelay.payments_url ? ( +
+ + Open payment website + + + You need to make a payment to connect this relay + +
+ ) : null} +
+ )} + + +
+
+ ); +} diff --git a/packages/ark/src/ark.ts b/packages/ark/src/ark.ts index 119f085b..d23d61ef 100644 --- a/packages/ark/src/ark.ts +++ b/packages/ark/src/ark.ts @@ -355,11 +355,11 @@ export class Ark { public async getAllRelaysFromContacts() { const fetcher = NostrFetcher.withCustomPool(ndkAdapter(this.ndk)); + const connectedRelays = this.ndk.pool + .connectedRelays() + .map((item) => item.url); + try { - const LIMIT = 1; - const connectedRelays = this.ndk.pool - .connectedRelays() - .map((item) => item.url); const relayMap = new Map(); const relayEvents = fetcher.fetchLatestEventsPerAuthor( { @@ -367,7 +367,7 @@ export class Ark { relayUrls: connectedRelays, }, { kinds: [NDKKind.RelayList] }, - LIMIT, + 1, ); for await (const { author, events } of relayEvents) { diff --git a/packages/ark/src/hooks/useRelay.ts b/packages/ark/src/hooks/useRelayList.ts similarity index 80% rename from packages/ark/src/hooks/useRelay.ts rename to packages/ark/src/hooks/useRelayList.ts index 8d29697e..19817ffc 100644 --- a/packages/ark/src/hooks/useRelay.ts +++ b/packages/ark/src/hooks/useRelayList.ts @@ -1,11 +1,9 @@ -import { useStorage } from "@lume/storage"; import { NDKKind, NDKRelayUrl, NDKTag } from "@nostr-dev-kit/ndk"; import { useMutation, useQueryClient } from "@tanstack/react-query"; import { useArk } from "./useArk"; -export function useRelay() { +export function useRelaylist() { const ark = useArk(); - const storage = useStorage(); const queryClient = useQueryClient(); const connectRelay = useMutation({ @@ -15,7 +13,7 @@ export function useRelay() { ) => { // Cancel any outgoing refetches await queryClient.cancelQueries({ - queryKey: ["relays", ark.account.pubkey], + queryKey: ["relay-personal"], }); // Snapshot the previous value @@ -42,17 +40,17 @@ export function useRelay() { }); // Optimistically update to the new value - queryClient.setQueryData( - ["relays", ark.account.pubkey], - (prev: NDKTag[]) => [...prev, ["r", relay, purpose ?? ""]], - ); + queryClient.setQueryData(["relay-personal"], (prev: NDKTag[]) => [ + ...prev, + ["r", relay, purpose ?? ""], + ]); // Return a context object with the snapshotted value return { prevRelays }; }, onSettled: () => { queryClient.invalidateQueries({ - queryKey: ["relays", ark.account.pubkey], + queryKey: ["relay-personal"], }); }, }); @@ -61,7 +59,7 @@ export function useRelay() { mutationFn: async (relay: NDKRelayUrl) => { // Cancel any outgoing refetches await queryClient.cancelQueries({ - queryKey: ["relays", ark.account.pubkey], + queryKey: ["relay-personal"], }); // Snapshot the previous value @@ -81,14 +79,14 @@ export function useRelay() { }); // Optimistically update to the new value - queryClient.setQueryData(["relays", ark.account.pubkey], prevRelays); + queryClient.setQueryData(["relay-personal"], prevRelays); // Return a context object with the snapshotted value return { prevRelays }; }, onSettled: () => { queryClient.invalidateQueries({ - queryKey: ["relays", ark.account.pubkey], + queryKey: ["relay-personal"], }); }, }); diff --git a/packages/ark/src/index.ts b/packages/ark/src/index.ts index 8147d450..5d591207 100644 --- a/packages/ark/src/index.ts +++ b/packages/ark/src/index.ts @@ -4,7 +4,7 @@ export * from "./provider"; export * from "./hooks/useEvent"; export * from "./hooks/useArk"; export * from "./hooks/useProfile"; -export * from "./hooks/useRelay"; +export * from "./hooks/useRelayList"; export * from "./components/user"; export * from "./components/column"; export * from "./components/column/provider"; diff --git a/packages/ark/src/provider.tsx b/packages/ark/src/provider.tsx index b2d3dd89..7f192ddc 100644 --- a/packages/ark/src/provider.tsx +++ b/packages/ark/src/provider.tsx @@ -1,7 +1,7 @@ import { LoaderIcon } from "@lume/icons"; import { NDKCacheAdapterTauri } from "@lume/ndk-cache-tauri"; import { useStorage } from "@lume/storage"; -import { QUOTES, sendNativeNotification } from "@lume/utils"; +import { FETCH_LIMIT, QUOTES, sendNativeNotification } from "@lume/utils"; import NDK, { NDKEvent, NDKKind, @@ -11,6 +11,7 @@ import NDK, { NDKRelayAuthPolicies, NDKUser, } from "@nostr-dev-kit/ndk"; +import { useQueryClient } from "@tanstack/react-query"; import { fetch } from "@tauri-apps/plugin-http"; import Linkify from "linkify-react"; import { normalizeRelayUrlSet } from "nostr-fetch"; @@ -20,6 +21,7 @@ import { LumeContext } from "./context"; export const LumeProvider = ({ children }: PropsWithChildren) => { const storage = useStorage(); + const queryClient = useQueryClient(); const [ark, setArk] = useState(undefined); const [ndk, setNDK] = useState(undefined); @@ -151,6 +153,56 @@ export const LumeProvider = ({ children }: PropsWithChildren) => { { closeOnEose: false, groupable: false }, ); + // prefetch activty + await queryClient.prefetchInfiniteQuery({ + queryKey: ["activity"], + initialPageParam: 0, + queryFn: async ({ + signal, + pageParam, + }: { + signal: AbortSignal; + pageParam: number; + }) => { + const events = await ark.getInfiniteEvents({ + filter: { + kinds: [NDKKind.Text, NDKKind.Repost, NDKKind.Zap], + "#p": [ark.account.pubkey], + }, + limit: FETCH_LIMIT, + pageParam, + signal, + }); + + return events; + }, + }); + + // prefetch timeline + await queryClient.prefetchInfiniteQuery({ + queryKey: ["timeline-9999"], + initialPageParam: 0, + queryFn: async ({ + signal, + pageParam, + }: { + signal: AbortSignal; + pageParam: number; + }) => { + const events = await ark.getInfiniteEvents({ + filter: { + kinds: [NDKKind.Text, NDKKind.Repost], + authors: ark.account.contacts, + }, + limit: FETCH_LIMIT, + pageParam, + signal, + }); + + return events; + }, + }); + sub.addListener("event", async (event: NDKEvent) => { const profile = await ark.getUserProfile(event.pubkey); switch (event.kind) { diff --git a/packages/ui/src/navigation.tsx b/packages/ui/src/navigation.tsx index 048e6386..7192aab9 100644 --- a/packages/ui/src/navigation.tsx +++ b/packages/ui/src/navigation.tsx @@ -3,9 +3,13 @@ import { BellIcon, ComposeFilledIcon, ComposeIcon, + DepotFilledIcon, + DepotIcon, HomeFilledIcon, HomeIcon, NwcIcon, + RelayFilledIcon, + RelayIcon, SettingsFilledIcon, SettingsIcon, } from "@lume/icons"; @@ -27,7 +31,12 @@ export function Navigation() {