diff --git a/.changeset/fluffy-pillows-juggle.md b/.changeset/fluffy-pillows-juggle.md new file mode 100644 index 000000000..9ebf3abe2 --- /dev/null +++ b/.changeset/fluffy-pillows-juggle.md @@ -0,0 +1,5 @@ +--- +"nostrudel": patch +--- + +cleanup fetching user relays diff --git a/src/classes/cached-pubkey-event-requester.ts b/src/classes/cached-pubkey-event-requester.ts index 82cef4a85..ccd9cc429 100644 --- a/src/classes/cached-pubkey-event-requester.ts +++ b/src/classes/cached-pubkey-event-requester.ts @@ -19,22 +19,22 @@ export class CachedPubkeyEventRequester extends PubkeyEventRequester { requestEvent(pubkey: string, relays: string[], alwaysRequest = false) { const sub = this.getSubject(pubkey); - if (!sub.value || alwaysRequest) { + if (!sub.value) { // only call this.readCache once per pubkey - const promise = this.readCacheDedupe.get(pubkey) || this.readCache(pubkey); - this.readCacheDedupe.set(pubkey, promise); + if (!this.readCacheDedupe.has(pubkey)) { + const promise = this.readCacheDedupe.get(pubkey) || this.readCache(pubkey); + this.readCacheDedupe.set(pubkey, promise); - promise.then((cached) => { - this.readCacheDedupe.delete(pubkey); + promise.then((cached) => { + this.readCacheDedupe.delete(pubkey); - if (cached && (!sub.value || cached.created_at > sub.value.created_at)) { - sub.next(cached); - } + if (cached) this.handleEvent(cached); - if (!sub.value || alwaysRequest) { - super.requestEvent(pubkey, relays, alwaysRequest); - } - }); + if (!sub.value || alwaysRequest) super.requestEvent(pubkey, relays); + }); + } + } else if (alwaysRequest) { + super.requestEvent(pubkey, relays); } return sub; diff --git a/src/classes/pubkey-event-requester.ts b/src/classes/pubkey-event-requester.ts index cca796ae0..98de114c7 100644 --- a/src/classes/pubkey-event-requester.ts +++ b/src/classes/pubkey-event-requester.ts @@ -53,10 +53,10 @@ class PubkeyEventRequestSubscription { return this.subjects.get(pubkey); } - requestEvent(pubkey: string, alwaysRequest = false) { + requestEvent(pubkey: string) { const sub = this.subjects.get(pubkey); - if (!sub.value || alwaysRequest) { + if (!sub.value) { this.requestNext.add(pubkey); } @@ -129,11 +129,11 @@ export class PubkeyEventRequester { } private connected = new WeakSet(); - requestEvent(pubkey: string, relays: string[], alwaysRequest = false) { + requestEvent(pubkey: string, relays: string[]) { const sub = this.subjects.get(pubkey); for (const relay of relays) { - const relaySub = this.subscriptions.get(relay).requestEvent(pubkey, alwaysRequest); + const relaySub = this.subscriptions.get(relay).requestEvent(pubkey); if (!this.connected.has(relaySub)) { relaySub.subscribe((event) => event && this.handleEvent(event)); diff --git a/src/components/debug-modals/user-debug-modal.tsx b/src/components/debug-modals/user-debug-modal.tsx index add5e1f92..b90b74c40 100644 --- a/src/components/debug-modals/user-debug-modal.tsx +++ b/src/components/debug-modals/user-debug-modal.tsx @@ -5,10 +5,14 @@ import { Bech32Prefix, normalizeToBech32 } from "../../helpers/nip19"; import { useUserMetadata } from "../../hooks/use-user-metadata"; import RawValue from "./raw-value"; import RawJson from "./raw-json"; +import { useSharableProfileId } from "../../hooks/use-shareable-profile-id"; +import userRelaysService from "../../services/user-relays"; export default function UserDebugModal({ pubkey, ...props }: { pubkey: string } & Omit) { const npub = useMemo(() => normalizeToBech32(pubkey, Bech32Prefix.Pubkey), [pubkey]); const metadata = useUserMetadata(pubkey); + const nprofile = useSharableProfileId(pubkey); + const relays = userRelaysService.requester.getSubject(pubkey).value; return ( @@ -18,8 +22,10 @@ export default function UserDebugModal({ pubkey, ...props }: { pubkey: string } - {npub && } - + {npub && } + + + {relays && } diff --git a/src/hooks/use-fallback-user-relays.tsx b/src/hooks/use-fallback-user-relays.tsx deleted file mode 100644 index d28504d9e..000000000 --- a/src/hooks/use-fallback-user-relays.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { useMemo } from "react"; -import { normalizeRelayConfigs } from "../helpers/relay"; -import userRelaysFallbackService from "../services/user-relays-fallback"; -import { useReadRelayUrls } from "./use-client-relays"; -import useSubject from "./use-subject"; - -export default function useFallbackUserRelays(pubkey: string, additionalRelays: string[] = [], alwaysFetch = false) { - const readRelays = useReadRelayUrls(additionalRelays); - - const observable = useMemo( - () => userRelaysFallbackService.requestRelays(pubkey, readRelays, alwaysFetch), - [pubkey, readRelays.join("|"), alwaysFetch] - ); - const userRelays = useSubject(observable); - - return userRelays ? normalizeRelayConfigs(userRelays.relays) : []; -} diff --git a/src/hooks/use-shareable-profile-id.ts b/src/hooks/use-shareable-profile-id.ts index 208a0fa94..11e2fd4e0 100644 --- a/src/hooks/use-shareable-profile-id.ts +++ b/src/hooks/use-shareable-profile-id.ts @@ -1,11 +1,11 @@ import { useMemo } from "react"; -import useFallbackUserRelays from "./use-fallback-user-relays"; import relayScoreboardService from "../services/relay-scoreboard"; import { RelayMode } from "../classes/relay"; import { nip19 } from "nostr-tools"; +import { useUserRelays } from "./use-user-relays"; export function useSharableProfileId(pubkey: string) { - const userRelays = useFallbackUserRelays(pubkey); + const userRelays = useUserRelays(pubkey); return useMemo(() => { const writeUrls = userRelays.filter((r) => r.mode & RelayMode.WRITE).map((r) => r.url); diff --git a/src/hooks/use-user-relays.ts b/src/hooks/use-user-relays.ts index 3bcf7c1d3..72e7e27ab 100644 --- a/src/hooks/use-user-relays.ts +++ b/src/hooks/use-user-relays.ts @@ -1,16 +1,15 @@ import { useMemo } from "react"; import userRelaysService from "../services/user-relays"; -import { useReadRelayUrls } from "./use-client-relays"; import useSubject from "./use-subject"; +import { useReadRelayUrls } from "./use-client-relays"; export function useUserRelays(pubkey: string, additionalRelays: string[] = [], alwaysRequest = false) { - const readRelays = useReadRelayUrls(additionalRelays); - - const observable = useMemo( - () => userRelaysService.requestRelays(pubkey, readRelays, alwaysRequest), - [pubkey, readRelays.join("|"), alwaysRequest] + const relays = useReadRelayUrls(additionalRelays); + const subject = useMemo( + () => userRelaysService.requestRelays(pubkey, relays, alwaysRequest), + [pubkey, relays.join("|"), alwaysRequest] ); - const userRelays = useSubject(observable); + const userRelays = useSubject(subject); - return userRelays; + return userRelays?.relays ?? []; } diff --git a/src/services/client-relays.ts b/src/services/client-relays.ts index 990d9e25a..982671b49 100644 --- a/src/services/client-relays.ts +++ b/src/services/client-relays.ts @@ -4,7 +4,7 @@ import { unique } from "../helpers/array"; import { DraftNostrEvent, RTag } from "../types/nostr-event"; import accountService from "./account"; import { RelayConfig, RelayMode } from "../classes/relay"; -import userRelaysService, { UserRelays } from "./user-relays"; +import userRelaysService, { ParsedUserRelays } from "./user-relays"; import { PersistentSubject, Subject } from "../classes/subject"; import signingService from "./signing"; @@ -17,7 +17,7 @@ class ClientRelayService { readRelays = new PersistentSubject([]); constructor() { - let lastSubject: Subject | undefined; + let lastSubject: Subject | undefined; accountService.current.subscribe((account) => { this.relays.next([]); @@ -49,7 +49,7 @@ class ClientRelayService { this.relays.subscribe((relays) => this.readRelays.next(relays.filter((r) => r.mode & RelayMode.READ))); } - private handleRelayChanged(relays: UserRelays) { + private handleRelayChanged(relays: ParsedUserRelays) { this.relays.next(relays.relays); } diff --git a/src/services/pubkey-relay-assignment.ts b/src/services/pubkey-relay-assignment.ts index 4ace63e9b..00b55094a 100644 --- a/src/services/pubkey-relay-assignment.ts +++ b/src/services/pubkey-relay-assignment.ts @@ -6,15 +6,14 @@ import accountService from "./account"; import clientRelaysService from "./client-relays"; import relayScoreboardService from "./relay-scoreboard"; import userContactsService, { UserContacts } from "./user-contacts"; -import { UserRelays } from "./user-relays"; -import userRelaysFallbackService from "./user-relays-fallback"; +import userRelaysService, { ParsedUserRelays } from "./user-relays"; type pubkey = string; type relay = string; class PubkeyRelayAssignmentService { pubkeys = new Map(); - pubkeyRelays = new SuperMap>(() => new Subject()); + pubkeyRelays = new SuperMap>(() => new Subject()); assignments = new PersistentSubject>({}); constructor() { @@ -46,7 +45,7 @@ class PubkeyRelayAssignmentService { this.pubkeys.set(pubkey, relays); const readRelays = clientRelaysService.getReadUrls(); - const subject = userRelaysFallbackService.requestRelays(pubkey, unique([...readRelays, ...relays])); + const subject = userRelaysService.requestRelays(pubkey, unique([...readRelays, ...relays])); this.pubkeyRelays.set(pubkey, subject); // subject.subscribe(this.updateAssignments, this); } diff --git a/src/services/user-relays-fallback.ts b/src/services/user-relays-fallback.ts deleted file mode 100644 index cc3c60afd..000000000 --- a/src/services/user-relays-fallback.ts +++ /dev/null @@ -1,40 +0,0 @@ -import Subject from "../classes/subject"; -import userContactsService from "./user-contacts"; -import userRelaysService, { UserRelays } from "./user-relays"; - -class UserRelaysFallbackService { - subjects = new Map>(); - - requestRelays(pubkey: string, relays: string[], alwaysFetch = false) { - let subject = this.subjects.get(pubkey); - if (!subject) { - subject = new Subject(); - this.subjects.set(pubkey, subject); - - subject.connectWithHandler(userRelaysService.getSubject(pubkey), (userRelays, next, value) => { - if (!value || userRelays.created_at > value.created_at) { - next(userRelays); - } - }); - subject.connectWithHandler(userContactsService.getSubject(pubkey), (contacts, next, value) => { - if (contacts.relays.length > 0 && (!value || contacts.created_at > value.created_at)) { - next({ pubkey: contacts.pubkey, relays: contacts.relays, created_at: contacts.created_at }); - } - }); - } - - userRelaysService.requestRelays(pubkey, relays, alwaysFetch); - userContactsService.requestContacts(pubkey, relays, alwaysFetch); - - return subject; - } -} - -const userRelaysFallbackService = new UserRelaysFallbackService(); - -if (import.meta.env.DEV) { - // @ts-ignore - window.userRelaysFallbackService = userRelaysFallbackService; -} - -export default userRelaysFallbackService; diff --git a/src/services/user-relays.ts b/src/services/user-relays.ts index dfe46c9a0..d8f172ee2 100644 --- a/src/services/user-relays.ts +++ b/src/services/user-relays.ts @@ -6,14 +6,15 @@ import { CachedPubkeyEventRequester } from "../classes/cached-pubkey-event-reque import { SuperMap } from "../classes/super-map"; import Subject from "../classes/subject"; import { normalizeRelayConfigs } from "../helpers/relay"; +import userContactsService from "./user-contacts"; -export type UserRelays = { +export type ParsedUserRelays = { pubkey: string; relays: RelayConfig[]; created_at: number; }; -function parseRelaysEvent(event: NostrEvent): UserRelays { +function parseRelaysEvent(event: NostrEvent): ParsedUserRelays { return { pubkey: event.pubkey, relays: normalizeRelayConfigs(event.tags.filter(isRTag).map(parseRTag)), @@ -25,25 +26,27 @@ class UserRelaysService { requester: CachedPubkeyEventRequester; constructor() { this.requester = new CachedPubkeyEventRequester(10002, "user-relays"); - this.requester.readCache = this.readCache; - this.requester.writeCache = this.writeCache; + this.requester.readCache = (pubkey) => db.get("userRelays", pubkey); + this.requester.writeCache = (pubkey, event) => db.put("userRelays", event); } - readCache(pubkey: string) { - return db.get("userRelays", pubkey); - } - writeCache(pubkey: string, event: NostrEvent) { - return db.put("userRelays", event); - } - - private subjects = new SuperMap>(() => new Subject()); - getSubject(pubkey: string) { + private subjects = new SuperMap>(() => new Subject()); + getRelays(pubkey: string) { return this.subjects.get(pubkey); } requestRelays(pubkey: string, relays: string[], alwaysRequest = false) { const sub = this.subjects.get(pubkey); const requestSub = this.requester.requestEvent(pubkey, relays, alwaysRequest); sub.connectWithHandler(requestSub, (event, next) => next(parseRelaysEvent(event))); + + // also fetch the relays from the users contacts + const contactsSub = userContactsService.requestContacts(pubkey, relays, alwaysRequest); + sub.connectWithHandler(contactsSub, (contacts, next, value) => { + if (contacts.relays.length > 0 && (!value || contacts.created_at > value.created_at)) { + next({ pubkey: contacts.pubkey, relays: contacts.relays, created_at: contacts.created_at }); + } + }); + return sub; } diff --git a/src/views/user/components/header.tsx b/src/views/user/components/header.tsx index cd6679f29..119f0c3a0 100644 --- a/src/views/user/components/header.tsx +++ b/src/views/user/components/header.tsx @@ -1,8 +1,5 @@ -import { Flex, Heading, SkeletonText, Text, Link, IconButton, Image } from "@chakra-ui/react"; -import { nip19 } from "nostr-tools"; -import { useMemo } from "react"; +import { Flex, Heading, SkeletonText, Text, Link, IconButton } from "@chakra-ui/react"; import { useNavigate, Link as RouterLink } from "react-router-dom"; -import { RelayMode } from "../../../classes/relay"; import { CopyIconButton } from "../../../components/copy-icon-button"; import { ChatIcon, ExternalLinkIcon, KeyIcon, SettingsIcon } from "../../../components/icons"; import { QrIconButton } from "./share-qr-button"; @@ -15,23 +12,9 @@ import { truncatedId } from "../../../helpers/nostr-event"; import { fixWebsiteUrl, getUserDisplayName } from "../../../helpers/user-metadata"; import { useCurrentAccount } from "../../../hooks/use-current-account"; import { useIsMobile } from "../../../hooks/use-is-mobile"; -import useFallbackUserRelays from "../../../hooks/use-fallback-user-relays"; import { useUserMetadata } from "../../../hooks/use-user-metadata"; -import relayScoreboardService from "../../../services/relay-scoreboard"; import { UserProfileMenu } from "./user-profile-menu"; -function useUserShareLink(pubkey: string) { - const userRelays = useFallbackUserRelays(pubkey); - - return useMemo(() => { - const writeUrls = userRelays.filter((r) => r.mode & RelayMode.WRITE).map((r) => r.url); - const ranked = relayScoreboardService.getRankedRelays(writeUrls); - const onlyTwo = ranked.slice(0, 2); - - return onlyTwo.length > 0 ? nip19.nprofileEncode({ pubkey, relays: onlyTwo }) : nip19.npubEncode(pubkey); - }, [userRelays]); -} - export default function Header({ pubkey, showRelaySelectionModal, diff --git a/src/views/user/components/share-qr-button.tsx b/src/views/user/components/share-qr-button.tsx index d6f5edfd4..af3f9a3c1 100644 --- a/src/views/user/components/share-qr-button.tsx +++ b/src/views/user/components/share-qr-button.tsx @@ -15,33 +15,19 @@ import { Input, Flex, } from "@chakra-ui/react"; -import { useMemo } from "react"; -import { RelayMode } from "../../../classes/relay"; import { QrCodeIcon } from "../../../components/icons"; import QrCodeSvg from "../../../components/qr-code-svg"; import { Bech32Prefix, normalizeToBech32 } from "../../../helpers/nip19"; -import useFallbackUserRelays from "../../../hooks/use-fallback-user-relays"; -import relayScoreboardService from "../../../services/relay-scoreboard"; -import { nip19 } from "nostr-tools"; import { CopyIconButton } from "../../../components/copy-icon-button"; - -function useUserShareLink(pubkey: string) { - const userRelays = useFallbackUserRelays(pubkey); - - return useMemo(() => { - const writeUrls = userRelays.filter((r) => r.mode & RelayMode.WRITE).map((r) => r.url); - const ranked = relayScoreboardService.getRankedRelays(writeUrls); - const onlyTwo = ranked.slice(0, 2); - - return onlyTwo.length > 0 ? nip19.nprofileEncode({ pubkey, relays: onlyTwo }) : nip19.npubEncode(pubkey); - }, [userRelays]); -} +import { useSharableProfileId } from "../../../hooks/use-shareable-profile-id"; export const QrIconButton = ({ pubkey, ...props }: { pubkey: string } & Omit) => { const { isOpen, onOpen, onClose } = useDisclosure(); const npub = normalizeToBech32(pubkey, Bech32Prefix.Pubkey) || pubkey; - const nprofile = useUserShareLink(pubkey); + const npubLink = "nostr:" + npub; + const nprofile = useSharableProfileId(pubkey); + const nprofileLink = "nostr:" + nprofile; return ( <> @@ -59,17 +45,17 @@ export const QrIconButton = ({ pubkey, ...props }: { pubkey: string } & Omit - + - - + + - + - - + + diff --git a/src/views/user/components/user-profile-menu.tsx b/src/views/user/components/user-profile-menu.tsx index 111283b7c..ddc4c6b13 100644 --- a/src/views/user/components/user-profile-menu.tsx +++ b/src/views/user/components/user-profile-menu.tsx @@ -24,7 +24,7 @@ export const UserProfileMenu = ({ const [_clipboardState, copyToClipboard] = useCopyToClipboard(); const loginAsUser = () => { - const readRelays = userRelays?.relays.filter((r) => r.mode === RelayMode.READ).map((r) => r.url) ?? []; + const readRelays = userRelays.filter((r) => r.mode === RelayMode.READ).map((r) => r.url) ?? []; if (!accountService.hasAccount(pubkey)) { accountService.addAccount({ pubkey, diff --git a/src/views/user/index.tsx b/src/views/user/index.tsx index b5e14cd86..406a1efaa 100644 --- a/src/views/user/index.tsx +++ b/src/views/user/index.tsx @@ -32,7 +32,6 @@ import { Bech32Prefix, isHex, normalizeToBech32 } from "../../helpers/nip19"; import { useAppTitle } from "../../hooks/use-app-title"; import Header from "./components/header"; import { Suspense, useState } 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"; @@ -40,6 +39,7 @@ import { AdditionalRelayProvider } from "../../providers/additional-relay-contex import { nip19 } from "nostr-tools"; import { unique } from "../../helpers/array"; import { RelayFavicon } from "../../components/relay-favicon"; +import { useUserRelays } from "../../hooks/use-user-relays"; const tabs = [ { label: "Notes", path: "notes" }, @@ -68,12 +68,12 @@ function useUserPointer() { } function useUserTopRelays(pubkey: string, count: number = 4) { + const readRelays = useReadRelayUrls(); // get user relays - const userRelays = useFallbackUserRelays(pubkey) + const userRelays = useUserRelays(pubkey, readRelays) .filter((r) => r.mode & RelayMode.WRITE) .map((r) => r.url); // merge the users relays with client relays - const readRelays = useReadRelayUrls(); if (userRelays.length === 0) return readRelays; const sorted = relayScoreboardService.getRankedRelays(userRelays); diff --git a/src/views/user/relays.tsx b/src/views/user/relays.tsx index 3afb3eb4f..d5e26d72c 100644 --- a/src/views/user/relays.tsx +++ b/src/views/user/relays.tsx @@ -2,13 +2,14 @@ import { Text, Box, IconButton, Flex, Badge } from "@chakra-ui/react"; import { useNavigate, useOutletContext } from "react-router-dom"; import { GlobalIcon } from "../../components/icons"; import { RelayMode } from "../../classes/relay"; -import useFallbackUserRelays from "../../hooks/use-fallback-user-relays"; import { RelayScoreBreakdown } from "../../components/relay-score-breakdown"; import useRankedRelayConfigs from "../../hooks/use-ranked-relay-configs"; +import { useUserRelays } from "../../hooks/use-user-relays"; +import { useReadRelayUrls } from "../../hooks/use-client-relays"; const UserRelaysTab = () => { const { pubkey } = useOutletContext() as { pubkey: string }; - const userRelays = useFallbackUserRelays(pubkey); + const userRelays = useUserRelays(pubkey); const navigate = useNavigate(); const ranked = useRankedRelayConfigs(userRelays); diff --git a/src/views/user/reports.tsx b/src/views/user/reports.tsx index 15c17752e..1f01b8b84 100644 --- a/src/views/user/reports.tsx +++ b/src/views/user/reports.tsx @@ -1,15 +1,12 @@ -import { Box, Button, Flex, Spinner, Text } from "@chakra-ui/react"; +import { Button, Flex, Spinner, Text } from "@chakra-ui/react"; import moment from "moment"; import { useOutletContext } from "react-router-dom"; -import { RelayMode } from "../../classes/relay"; import { NoteLink } from "../../components/note-link"; import { UserLink } from "../../components/user-link"; import { filterTagsByContentRefs, 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 { isETag, isPTag, NostrEvent } from "../../types/nostr-event"; +import { useAdditionalRelayContext } from "../../providers/additional-relay-context"; function ReportEvent({ report }: { report: NostrEvent }) { const reportedEvent = report.tags.filter(isETag)[0]?.[1]; @@ -39,14 +36,7 @@ function ReportEvent({ report }: { report: NostrEvent }) { export default function UserReportsTab() { 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 = relayScoreboardService.getRankedRelays(userRelays.length === 0 ? readRelays : userRelays).slice(0, 4); + const contextRelays = useAdditionalRelayContext(); const { events: reports, @@ -54,7 +44,7 @@ export default function UserReportsTab() { loadMore, } = useTimelineLoader( `${truncatedId(pubkey)}-reports`, - relays, + contextRelays, { authors: [pubkey], kinds: [1984] }, { pageSize: moment.duration(1, "week").asSeconds() } );