diff --git a/src/components/embeded-note.tsx b/src/components/embeded-note.tsx index b72e2f31a..e3fac256e 100644 --- a/src/components/embeded-note.tsx +++ b/src/components/embeded-note.tsx @@ -14,12 +14,14 @@ import { convertTimestampToDate } from "../helpers/date"; import useSubject from "../hooks/use-subject"; import settings from "../services/settings"; import EventVerificationIcon from "./event-verification-icon"; +import { useReadRelayUrls } from "../hooks/use-client-relays"; const EmbeddedNote = ({ note }: { note: NostrEvent }) => { const account = useCurrentAccount(); const showSignatureVerification = useSubject(settings.showSignatureVerification); - const contacts = useUserContacts(account.pubkey); + const readRelays = useReadRelayUrls(); + const contacts = useUserContacts(account.pubkey, readRelays); const following = contacts?.contacts || []; return ( diff --git a/src/components/note/index.tsx b/src/components/note/index.tsx index e82294e88..75c89b270 100644 --- a/src/components/note/index.tsx +++ b/src/components/note/index.tsx @@ -35,6 +35,7 @@ import EventVerificationIcon from "../event-verification-icon"; import { ReplyButton } from "./buttons/reply-button"; import { RepostButton } from "./buttons/repost-button"; import { QuoteRepostButton } from "./buttons/quote-repost-button"; +import { useReadRelayUrls } from "../../hooks/use-client-relays"; export type NoteProps = { event: NostrEvent; @@ -47,7 +48,8 @@ export const Note = React.memo(({ event, maxHeight, variant = "outline" }: NoteP const showReactions = useSubject(settings.showReactions); const showSignatureVerification = useSubject(settings.showSignatureVerification); - const contacts = useUserContacts(account.pubkey); + const readRelays = useReadRelayUrls(); + const contacts = useUserContacts(account.pubkey, readRelays); const following = contacts?.contacts || []; return ( diff --git a/src/hooks/use-user-contacts.ts b/src/hooks/use-user-contacts.ts index 00c7551ed..f2cf6793a 100644 --- a/src/hooks/use-user-contacts.ts +++ b/src/hooks/use-user-contacts.ts @@ -1,15 +1,11 @@ import { useMemo } from "react"; -import { unique } from "../helpers/array"; import userContactsService from "../services/user-contacts"; -import { useReadRelayUrls } from "./use-client-relays"; import useSubject from "./use-subject"; -export function useUserContacts(pubkey: string, additionalRelays: string[] = [], alwaysRequest = false) { - const readRelays = useReadRelayUrls(additionalRelays); - +export function useUserContacts(pubkey: string, relays: string[], alwaysRequest = false) { const observable = useMemo( - () => userContactsService.requestContacts(pubkey, readRelays, alwaysRequest), - [pubkey, readRelays, alwaysRequest] + () => userContactsService.requestContacts(pubkey, relays, alwaysRequest), + [pubkey, relays.join("|"), alwaysRequest] ); const contacts = useSubject(observable); diff --git a/src/hooks/use-user-followers.ts b/src/hooks/use-user-followers.ts index 9b2632827..440623489 100644 --- a/src/hooks/use-user-followers.ts +++ b/src/hooks/use-user-followers.ts @@ -2,7 +2,7 @@ import { useMemo } from "react"; import userFollowersService from "../services/user-followers"; import useSubject from "./use-subject"; -export function useUserFollowers(pubkey: string, relays: string[] = [], alwaysRequest = false) { +export function useUserFollowers(pubkey: string, relays: string[], alwaysRequest = false) { const subject = useMemo( () => userFollowersService.requestFollowers(pubkey, relays, alwaysRequest), [pubkey, alwaysRequest] diff --git a/src/providers/additional-relay-context.tsx b/src/providers/additional-relay-context.tsx new file mode 100644 index 000000000..2ae0fd927 --- /dev/null +++ b/src/providers/additional-relay-context.tsx @@ -0,0 +1,24 @@ +import React, { useContext } from "react"; +import { unique } from "../helpers/array"; +import { safeRelayUrl } from "../helpers/url"; + +export const RelayContext = React.createContext([]); + +export function useAdditionalRelayContext() { + return useContext(RelayContext) ?? []; +} + +export function AdditionalRelayProvider({ + relays, + children, + extend = true, +}: { + relays: string[]; + children: React.ReactNode; + extend?: boolean; +}) { + const parentRelays = useAdditionalRelayContext(); + const safeUrls = (extend ? [...parentRelays, ...relays] : relays).map(safeRelayUrl).filter(Boolean) as string[]; + + return {children}; +} diff --git a/src/services/user-followers.ts b/src/services/user-followers.ts index 240f36eb7..0b368cf84 100644 --- a/src/services/user-followers.ts +++ b/src/services/user-followers.ts @@ -24,10 +24,10 @@ function mergeNext(subject: Subject, next: string[]) { subject.next(arr); } -function requestFollowers(pubkey: string, additionalRelays: string[] = [], alwaysRequest = false) { +function requestFollowers(pubkey: string, relays: string[], alwaysRequest = false) { let subject = subjects.getSubject(pubkey); - if (additionalRelays.length) subjects.addRelays(pubkey, additionalRelays); + if (relays.length) subjects.addRelays(pubkey, relays); db.getAllKeysFromIndex("userFollows", "follows", pubkey).then((cached) => { mergeNext(subject, cached); @@ -50,9 +50,6 @@ function flushRequests() { if (pubkeys.size === 0) return; - const clientRelays = clientRelaysService.getReadUrls(); - for (const url of clientRelays) relayUrls.add(url); - const query: NostrQuery = { kinds: [3], "#p": Array.from(pubkeys) }; subscription.setRelays(Array.from(relayUrls)); diff --git a/src/services/user-relays-fallback.ts b/src/services/user-relays-fallback.ts index 6e24ac72c..cc3c60afd 100644 --- a/src/services/user-relays-fallback.ts +++ b/src/services/user-relays-fallback.ts @@ -1,5 +1,4 @@ import Subject from "../classes/subject"; -import { normalizeRelayConfigs } from "../helpers/relay"; import userContactsService from "./user-contacts"; import userRelaysService, { UserRelays } from "./user-relays"; diff --git a/src/views/home/following-tab.tsx b/src/views/home/following-tab.tsx index 0b953c6ff..4294f9943 100644 --- a/src/views/home/following-tab.tsx +++ b/src/views/home/following-tab.tsx @@ -14,9 +14,9 @@ import RepostNote from "../../components/repost-note"; export default function FollowingTab() { const account = useCurrentAccount(); - const relays = useReadRelayUrls(); + const readRelays = useReadRelayUrls(); const { openModal } = useContext(PostModalContext); - const contacts = useUserContacts(account.pubkey); + const contacts = useUserContacts(account.pubkey, readRelays); const [search, setSearch] = useSearchParams(); const showReplies = search.has("replies"); const onToggle = () => { @@ -26,7 +26,7 @@ export default function FollowingTab() { const following = contacts?.contacts || []; const { events, loading, loadMore } = useTimelineLoader( `${account.pubkey}-following-posts`, - relays, + readRelays, { authors: following, kinds: [1, 6], since: moment().subtract(2, "hour").unix() }, { pageSize: moment.duration(2, "hour").asSeconds(), enabled: following.length > 0 } ); diff --git a/src/views/user/components/user-card.tsx b/src/views/user/components/user-card.tsx index 11ecf3631..0201a4cfd 100644 --- a/src/views/user/components/user-card.tsx +++ b/src/views/user/components/user-card.tsx @@ -8,7 +8,7 @@ import { Bech32Prefix, normalizeToBech32 } from "../../../helpers/nip19"; import { UserDnsIdentityIcon } from "../../../components/user-dns-identity"; export const UserCard = ({ pubkey, relay }: { pubkey: string; relay?: string }) => { - const metadata = useUserMetadata(pubkey); + const metadata = useUserMetadata(pubkey, relay ? [relay] : []); return ( diff --git a/src/views/user/followers.tsx b/src/views/user/followers.tsx index c36a9468c..394673759 100644 --- a/src/views/user/followers.tsx +++ b/src/views/user/followers.tsx @@ -5,11 +5,15 @@ import { useUserFollowers } from "../../hooks/use-user-followers"; import { useOutletContext } from "react-router-dom"; import { usePaginatedList } from "../../hooks/use-paginated-list"; import { PaginationControls } from "../../components/pagination-controls"; +import { useAdditionalRelayContext } from "../../providers/additional-relay-context"; +import { useReadRelayUrls } from "../../hooks/use-client-relays"; const UserFollowersTab = () => { const { pubkey } = useOutletContext() as { pubkey: string }; const { isOpen, onToggle } = useDisclosure(); - const followers = useUserFollowers(pubkey, [], isOpen); + const contextRelays = useAdditionalRelayContext(); + const relays = useReadRelayUrls(contextRelays); + const followers = useUserFollowers(pubkey, relays, isOpen); const pagination = usePaginatedList(followers ?? [], { pageSize: 3 * 10 }); diff --git a/src/views/user/following.tsx b/src/views/user/following.tsx index 3f85fa332..d4bef254d 100644 --- a/src/views/user/following.tsx +++ b/src/views/user/following.tsx @@ -6,10 +6,12 @@ import { useUserContacts } from "../../hooks/use-user-contacts"; import { useOutletContext } from "react-router-dom"; import { usePaginatedList } from "../../hooks/use-paginated-list"; import { PaginationControls } from "../../components/pagination-controls"; +import { useAdditionalRelayContext } from "../../providers/additional-relay-context"; const UserFollowingTab = () => { const { pubkey } = useOutletContext() as { pubkey: string }; - const contacts = useUserContacts(pubkey, [], true); + const contextRelays = useAdditionalRelayContext(); + const contacts = useUserContacts(pubkey, contextRelays, true); const pagination = usePaginatedList(contacts?.contacts ?? [], { pageSize: 3 * 10 }); diff --git a/src/views/user/index.tsx b/src/views/user/index.tsx index 5af09a9fd..d567289c3 100644 --- a/src/views/user/index.tsx +++ b/src/views/user/index.tsx @@ -1,4 +1,4 @@ -import { Flex, Spinner, Tab, TabList, TabPanel, TabPanels, Tabs } from "@chakra-ui/react"; +import { Flex, Image, Spinner, Tab, TabList, TabPanel, TabPanels, Tabs } from "@chakra-ui/react"; import { Outlet, useLoaderData, useMatches, useNavigate } from "react-router-dom"; import { useUserMetadata } from "../../hooks/use-user-metadata"; import { getUserDisplayName } from "../../helpers/user-metadata"; @@ -7,6 +7,11 @@ import { Bech32Prefix, normalizeToBech32 } from "../../helpers/nip19"; import { useAppTitle } from "../../hooks/use-app-title"; import Header from "./components/header"; import { Suspense } from "react"; +import useFallbackUserRelays from "../../hooks/use-fallback-user-relays"; +import { useReadRelayUrls } from "../../hooks/use-client-relays"; +import relayScoreboardService from "../../services/relay-scoreboard"; +import { RelayMode } from "../../classes/relay"; +import { AdditionalRelayProvider } from "../../providers/additional-relay-context"; const tabs = [ { label: "Notes", path: "notes" }, @@ -17,10 +22,22 @@ const tabs = [ { label: "Reports", path: "reports" }, ]; +function useUserTop4Relays(pubkey: string) { + // get user relays + const userRelays = useFallbackUserRelays(pubkey) + .filter((r) => r.mode & RelayMode.WRITE) + .map((r) => r.url); + // merge the users relays with client relays + const readRelays = useReadRelayUrls(); + // find the top 4 + return userRelays.length === 0 ? readRelays : relayScoreboardService.getRankedRelays(userRelays).slice(0, 4); +} + const UserView = () => { const isMobile = useIsMobile(); const navigate = useNavigate(); const { pubkey } = useLoaderData() as { pubkey: string }; + const userTopRelays = useUserTop4Relays(pubkey); const matches = useMatches(); const lastMatch = matches[matches.length - 1]; @@ -33,35 +50,37 @@ const UserView = () => { useAppTitle(getUserDisplayName(metadata, npub ?? pubkey)); return ( - - {/* {metadata?.banner && } */} -
- navigate(tabs[v].path)} - > - - {tabs.map(({ label }) => ( - {label} - ))} - + + + {/* {metadata?.banner && } */} +
+ navigate(tabs[v].path)} + > + + {tabs.map(({ label }) => ( + {label} + ))} + - - {tabs.map(({ label }) => ( - - }> - - - - ))} - - - + + {tabs.map(({ label }) => ( + + }> + + + + ))} + + + + ); }; diff --git a/src/views/user/notes.tsx b/src/views/user/notes.tsx index 9cf65c3a4..ae9c17a11 100644 --- a/src/views/user/notes.tsx +++ b/src/views/user/notes.tsx @@ -18,33 +18,23 @@ import { } from "@chakra-ui/react"; import moment from "moment"; import { useOutletContext } from "react-router-dom"; -import { RelayMode } from "../../classes/relay"; import { RelayIcon } from "../../components/icons"; import { Note } from "../../components/note"; import RepostNote from "../../components/repost-note"; import { isReply, isRepost, truncatedId } from "../../helpers/nostr-event"; -import { useReadRelayUrls } from "../../hooks/use-client-relays"; -import useFallbackUserRelays from "../../hooks/use-fallback-user-relays"; import { useTimelineLoader } from "../../hooks/use-timeline-loader"; -import relayScoreboardService from "../../services/relay-scoreboard"; +import { useAdditionalRelayContext } from "../../providers/additional-relay-context"; const UserNotesTab = () => { const { pubkey } = useOutletContext() as { pubkey: string }; - // get user relays - const userRelays = useFallbackUserRelays(pubkey) - .filter((r) => r.mode & RelayMode.WRITE) - .map((r) => r.url); - // merge the users relays with client relays - const readRelays = useReadRelayUrls(); - // find the top 4 - const relays = userRelays.length === 0 ? readRelays : relayScoreboardService.getRankedRelays(userRelays).slice(0, 4); + const contextRelays = useAdditionalRelayContext(); const { isOpen: showReplies, onToggle: toggleReplies } = useDisclosure(); const { isOpen: hideReposts, onToggle: toggleReposts } = useDisclosure(); const { events, loading, loadMore } = useTimelineLoader( `${truncatedId(pubkey)}-notes`, - relays, + contextRelays, { authors: [pubkey], kinds: [1, 6] }, { pageSize: moment.duration(2, "day").asSeconds(), startLimit: 20 } ); @@ -77,7 +67,7 @@ const UserNotesTab = () => { - {relays.map((url) => ( + {contextRelays.map((url) => ( {url} ))} diff --git a/src/views/user/zaps.tsx b/src/views/user/zaps.tsx index 041c044b2..5fe6175b9 100644 --- a/src/views/user/zaps.tsx +++ b/src/views/user/zaps.tsx @@ -2,7 +2,6 @@ import { Box, Button, Flex, Select, Spinner, Text, useDisclosure } from "@chakra import moment from "moment"; import { useState } from "react"; import { useOutletContext } from "react-router-dom"; -import { RelayMode } from "../../classes/relay"; import { ErrorBoundary, ErrorFallback } from "../../components/error-boundary"; import { LightningIcon } from "../../components/icons"; import { NoteLink } from "../../components/note-link"; @@ -12,10 +11,10 @@ import { readablizeSats } from "../../helpers/bolt11"; import { convertTimestampToDate } from "../../helpers/date"; import { truncatedId } from "../../helpers/nostr-event"; import { isProfileZap, isNoteZap, parseZapNote, totalZaps } from "../../helpers/zaps"; -import { useReadRelayUrls } from "../../hooks/use-client-relays"; -import useFallbackUserRelays from "../../hooks/use-fallback-user-relays"; import { useTimelineLoader } from "../../hooks/use-timeline-loader"; import { NostrEvent } from "../../types/nostr-event"; +import { useAdditionalRelayContext } from "../../providers/additional-relay-context"; +import { useReadRelayUrls } from "../../hooks/use-client-relays"; const Zap = ({ zapEvent }: { zapEvent: NostrEvent }) => { const { isOpen, onToggle } = useDisclosure(); @@ -66,11 +65,8 @@ const Zap = ({ zapEvent }: { zapEvent: NostrEvent }) => { const UserZapsTab = () => { const { pubkey } = useOutletContext() as { pubkey: string }; const [filter, setFilter] = useState("both"); - // get user relays - const userRelays = useFallbackUserRelays(pubkey) - .filter((r) => r.mode & RelayMode.WRITE) - .map((r) => r.url); - const relays = useReadRelayUrls(userRelays); + const contextRelays = useAdditionalRelayContext(); + const relays = useReadRelayUrls(contextRelays); const { events, loading, loadMore } = useTimelineLoader( `${truncatedId(pubkey)}-zaps`,