From ead17103925aafa45639c44766252c0c55f916de Mon Sep 17 00:00:00 2001 From: Cody Tseng Date: Thu, 7 Nov 2024 21:42:51 +0800 Subject: [PATCH] feat: outbox model (#4) --- .../components/Embedded/EmbeddedHashtag.tsx | 2 +- .../src/components/Embedded/EmbeddedNote.tsx | 2 +- .../components/NoteCard/ShortTextNoteCard.tsx | 2 +- .../src/components/NoteList/index.tsx | 6 +- .../src/components/NoteStats/LikeButton.tsx | 4 +- .../src/components/NoteStats/RepostButton.tsx | 4 +- .../src/components/RelaySettings/RelayUrl.tsx | 18 +--- .../src/components/ReplyNoteList/index.tsx | 3 +- .../src/components/UserAvatar/index.tsx | 2 +- .../src/components/Username/index.tsx | 2 +- src/renderer/src/hooks/useFetchRelayList.tsx | 23 +++++ .../PrimaryPageLayout/AccountButton.tsx | 2 +- src/renderer/src/lib/link.ts | 7 ++ src/renderer/src/lib/url.ts | 21 +++-- .../src/pages/primary/NoteListPage/index.tsx | 4 +- .../src/pages/secondary/HashtagPage/index.tsx | 8 +- .../src/pages/secondary/ProfilePage/index.tsx | 4 +- src/renderer/src/providers/NostrProvider.tsx | 13 ++- .../src/providers/NoteStatsProvider.tsx | 12 ++- src/renderer/src/services/client.service.ts | 86 +++++++++++++++---- src/renderer/src/types.ts | 5 ++ 21 files changed, 171 insertions(+), 59 deletions(-) create mode 100644 src/renderer/src/hooks/useFetchRelayList.tsx create mode 100644 src/renderer/src/lib/link.ts diff --git a/src/renderer/src/components/Embedded/EmbeddedHashtag.tsx b/src/renderer/src/components/Embedded/EmbeddedHashtag.tsx index b45b679a..176db4ca 100644 --- a/src/renderer/src/components/Embedded/EmbeddedHashtag.tsx +++ b/src/renderer/src/components/Embedded/EmbeddedHashtag.tsx @@ -1,4 +1,4 @@ -import { toHashtag } from '@renderer/lib/url' +import { toHashtag } from '@renderer/lib/link' import { SecondaryPageLink } from '@renderer/PageManager' export function EmbeddedHashtag({ hashtag }: { hashtag: string }) { diff --git a/src/renderer/src/components/Embedded/EmbeddedNote.tsx b/src/renderer/src/components/Embedded/EmbeddedNote.tsx index 24f43631..32836719 100644 --- a/src/renderer/src/components/Embedded/EmbeddedNote.tsx +++ b/src/renderer/src/components/Embedded/EmbeddedNote.tsx @@ -1,5 +1,5 @@ import { useFetchEventById } from '@renderer/hooks' -import { toNoStrudelNote } from '@renderer/lib/url' +import { toNoStrudelNote } from '@renderer/lib/link' import { kinds } from 'nostr-tools' import ShortTextNoteCard from '../NoteCard/ShortTextNoteCard' diff --git a/src/renderer/src/components/NoteCard/ShortTextNoteCard.tsx b/src/renderer/src/components/NoteCard/ShortTextNoteCard.tsx index 53bde29d..61899fd0 100644 --- a/src/renderer/src/components/NoteCard/ShortTextNoteCard.tsx +++ b/src/renderer/src/components/NoteCard/ShortTextNoteCard.tsx @@ -1,6 +1,6 @@ import { Event } from 'nostr-tools' import { Card } from '@renderer/components/ui/card' -import { toNote } from '@renderer/lib/url' +import { toNote } from '@renderer/lib/link' import { useSecondaryPage } from '@renderer/PageManager' import Note from '../Note' import { useFetchEventById } from '@renderer/hooks' diff --git a/src/renderer/src/components/NoteList/index.tsx b/src/renderer/src/components/NoteList/index.tsx index d8351f8b..ce4c929f 100644 --- a/src/renderer/src/components/NoteList/index.tsx +++ b/src/renderer/src/components/NoteList/index.tsx @@ -1,7 +1,6 @@ import { Button } from '@renderer/components/ui/button' import { isReplyNoteEvent } from '@renderer/lib/event' import { cn } from '@renderer/lib/utils' -import { useRelaySettings } from '@renderer/providers/RelaySettingsProvider' import client from '@renderer/services/client.service' import dayjs from 'dayjs' import { Event, Filter, kinds } from 'nostr-tools' @@ -9,9 +8,11 @@ import { useEffect, useMemo, useRef, useState } from 'react' import NoteCard from '../NoteCard' export default function NoteList({ + relayUrls, filter = {}, className }: { + relayUrls: string[] filter?: Filter className?: string }) { @@ -22,7 +23,6 @@ export default function NoteList({ const [initialized, setInitialized] = useState(false) const observer = useRef(null) const bottomRef = useRef(null) - const { relayUrls } = useRelaySettings() const noteFilter = useMemo(() => { return { kinds: [kinds.ShortTextNote, kinds.Repost], @@ -87,7 +87,7 @@ export default function NoteList({ }, [until, initialized]) const loadMore = async () => { - const events = await client.fetchEvents({ ...noteFilter, until }) + const events = await client.fetchEvents(relayUrls, { ...noteFilter, until }) const sortedEvents = events.sort((a, b) => b.created_at - a.created_at) if (sortedEvents.length === 0) { setHasMore(false) diff --git a/src/renderer/src/components/NoteStats/LikeButton.tsx b/src/renderer/src/components/NoteStats/LikeButton.tsx index 67fbb1bb..5eec347c 100644 --- a/src/renderer/src/components/NoteStats/LikeButton.tsx +++ b/src/renderer/src/components/NoteStats/LikeButton.tsx @@ -2,6 +2,7 @@ import { createReactionDraftEvent } from '@renderer/lib/draft-event' import { cn } from '@renderer/lib/utils' import { useNostr } from '@renderer/providers/NostrProvider' import { useNoteStats } from '@renderer/providers/NoteStatsProvider' +import client from '@renderer/services/client.service' import { Heart } from 'lucide-react' import { Event } from 'nostr-tools' import { useEffect, useMemo, useState } from 'react' @@ -50,8 +51,9 @@ export default function LikeButton({ ]) if (liked) return + const targetRelayList = await client.fetchRelayList(event.pubkey) const reaction = createReactionDraftEvent(event) - await publish(reaction) + await publish(reaction, targetRelayList.read) markNoteAsLiked(event.id) } catch (error) { console.error('like failed', error) diff --git a/src/renderer/src/components/NoteStats/RepostButton.tsx b/src/renderer/src/components/NoteStats/RepostButton.tsx index 97b93952..88b3578c 100644 --- a/src/renderer/src/components/NoteStats/RepostButton.tsx +++ b/src/renderer/src/components/NoteStats/RepostButton.tsx @@ -13,6 +13,7 @@ import { createRepostDraftEvent } from '@renderer/lib/draft-event' import { cn } from '@renderer/lib/utils' import { useNostr } from '@renderer/providers/NostrProvider' import { useNoteStats } from '@renderer/providers/NoteStatsProvider' +import client from '@renderer/services/client.service' import { Repeat } from 'lucide-react' import { Event } from 'nostr-tools' import { useEffect, useMemo, useState } from 'react' @@ -60,8 +61,9 @@ export default function RepostButton({ ]) if (reposted) return + const targetRelayList = await client.fetchRelayList(event.pubkey) const repost = createRepostDraftEvent(event) - await publish(repost) + await publish(repost, targetRelayList.read) markNoteAsReposted(event.id) } catch (error) { console.error('repost failed', error) diff --git a/src/renderer/src/components/RelaySettings/RelayUrl.tsx b/src/renderer/src/components/RelaySettings/RelayUrl.tsx index 10787e1c..43fed3a0 100644 --- a/src/renderer/src/components/RelaySettings/RelayUrl.tsx +++ b/src/renderer/src/components/RelaySettings/RelayUrl.tsx @@ -1,5 +1,6 @@ import { Button } from '@renderer/components/ui/button' import { Input } from '@renderer/components/ui/input' +import { isWebsocketUrl, normalizeUrl } from '@renderer/lib/url' import { useRelaySettings } from '@renderer/providers/RelaySettingsProvider' import client from '@renderer/services/client.service' import { CircleX } from 'lucide-react' @@ -43,11 +44,11 @@ export default function RelayUrls({ groupName }: { groupName: string }) { const saveNewRelayUrl = () => { if (newRelayUrl === '') return - const normalizedUrl = normalizeURL(newRelayUrl) + const normalizedUrl = normalizeUrl(newRelayUrl) if (relays.some(({ url }) => url === normalizedUrl)) { return setNewRelayUrlError('already exists') } - if (/^wss?:\/\/.+$/.test(normalizedUrl) === false) { + if (!isWebsocketUrl(normalizedUrl)) { return setNewRelayUrlError('invalid URL') } setRelays((pre) => [...pre, { url: normalizedUrl, isConnected: false }]) @@ -130,16 +131,3 @@ function RelayUrl({ ) } - -// copy from nostr-tools/utils -function normalizeURL(url: string): string { - if (url.indexOf('://') === -1) url = 'wss://' + url - const p = new URL(url) - p.pathname = p.pathname.replace(/\/+/g, '/') - if (p.pathname.endsWith('/')) p.pathname = p.pathname.slice(0, -1) - if ((p.port === '80' && p.protocol === 'ws:') || (p.port === '443' && p.protocol === 'wss:')) - p.port = '' - p.searchParams.sort() - p.hash = '' - return p.toString() -} diff --git a/src/renderer/src/components/ReplyNoteList/index.tsx b/src/renderer/src/components/ReplyNoteList/index.tsx index fa5772df..6c635c0c 100644 --- a/src/renderer/src/components/ReplyNoteList/index.tsx +++ b/src/renderer/src/components/ReplyNoteList/index.tsx @@ -20,7 +20,8 @@ export default function ReplyNoteList({ event, className }: { event: Event; clas const loadMore = async () => { setLoading(true) - const events = await client.fetchEvents({ + const relayList = await client.fetchRelayList(event.pubkey) + const events = await client.fetchEvents(relayList.read, { '#e': [event.id], kinds: [1], limit: 100, diff --git a/src/renderer/src/components/UserAvatar/index.tsx b/src/renderer/src/components/UserAvatar/index.tsx index f5eada8b..71594159 100644 --- a/src/renderer/src/components/UserAvatar/index.tsx +++ b/src/renderer/src/components/UserAvatar/index.tsx @@ -3,7 +3,7 @@ import { HoverCard, HoverCardContent, HoverCardTrigger } from '@renderer/compone import { Skeleton } from '@renderer/components/ui/skeleton' import { useFetchProfile } from '@renderer/hooks' import { generateImageByPubkey } from '@renderer/lib/pubkey' -import { toProfile } from '@renderer/lib/url' +import { toProfile } from '@renderer/lib/link' import { cn } from '@renderer/lib/utils' import { SecondaryPageLink } from '@renderer/PageManager' import ProfileCard from '../ProfileCard' diff --git a/src/renderer/src/components/Username/index.tsx b/src/renderer/src/components/Username/index.tsx index ffedfb2e..59343f74 100644 --- a/src/renderer/src/components/Username/index.tsx +++ b/src/renderer/src/components/Username/index.tsx @@ -1,6 +1,6 @@ import { HoverCard, HoverCardContent, HoverCardTrigger } from '@renderer/components/ui/hover-card' import { useFetchProfile } from '@renderer/hooks' -import { toProfile } from '@renderer/lib/url' +import { toProfile } from '@renderer/lib/link' import { cn } from '@renderer/lib/utils' import { SecondaryPageLink } from '@renderer/PageManager' import ProfileCard from '../ProfileCard' diff --git a/src/renderer/src/hooks/useFetchRelayList.tsx b/src/renderer/src/hooks/useFetchRelayList.tsx new file mode 100644 index 00000000..e9021018 --- /dev/null +++ b/src/renderer/src/hooks/useFetchRelayList.tsx @@ -0,0 +1,23 @@ +import { TRelayList } from '@renderer/types' +import { useEffect, useState } from 'react' +import client from '@renderer/services/client.service' + +export function useFetchRelayList(pubkey?: string | null) { + const [relayList, setRelayList] = useState({ write: [], read: [] }) + + useEffect(() => { + const fetchRelayList = async () => { + if (!pubkey) return + try { + const relayList = await client.fetchRelayList(pubkey) + setRelayList(relayList) + } catch (err) { + console.error(err) + } + } + + fetchRelayList() + }, []) + + return relayList +} diff --git a/src/renderer/src/layouts/PrimaryPageLayout/AccountButton.tsx b/src/renderer/src/layouts/PrimaryPageLayout/AccountButton.tsx index e3231afe..b1097bbf 100644 --- a/src/renderer/src/layouts/PrimaryPageLayout/AccountButton.tsx +++ b/src/renderer/src/layouts/PrimaryPageLayout/AccountButton.tsx @@ -17,7 +17,7 @@ import { import { Input } from '@renderer/components/ui/input' import { useFetchProfile } from '@renderer/hooks' import { generateImageByPubkey } from '@renderer/lib/pubkey' -import { toProfile } from '@renderer/lib/url' +import { toProfile } from '@renderer/lib/link' import { useSecondaryPage } from '@renderer/PageManager' import { useNostr } from '@renderer/providers/NostrProvider' import { LogIn } from 'lucide-react' diff --git a/src/renderer/src/lib/link.ts b/src/renderer/src/lib/link.ts new file mode 100644 index 00000000..2a574087 --- /dev/null +++ b/src/renderer/src/lib/link.ts @@ -0,0 +1,7 @@ +import { Event } from 'nostr-tools' + +export const toProfile = (pubkey: string) => ({ pageName: 'profile', props: { pubkey } }) +export const toNoStrudelProfile = (id: string) => `https://nostrudel.ninja/#/u/${id}` +export const toNote = (event: Event) => ({ pageName: 'note', props: { event } }) +export const toNoStrudelNote = (id: string) => `https://nostrudel.ninja/#/n/${id}` +export const toHashtag = (hashtag: string) => ({ pageName: 'hashtag', props: { hashtag } }) diff --git a/src/renderer/src/lib/url.ts b/src/renderer/src/lib/url.ts index 2a574087..b03bbe09 100644 --- a/src/renderer/src/lib/url.ts +++ b/src/renderer/src/lib/url.ts @@ -1,7 +1,16 @@ -import { Event } from 'nostr-tools' +export function isWebsocketUrl(url: string): boolean { + return /^wss?:\/\/.+$/.test(url) +} -export const toProfile = (pubkey: string) => ({ pageName: 'profile', props: { pubkey } }) -export const toNoStrudelProfile = (id: string) => `https://nostrudel.ninja/#/u/${id}` -export const toNote = (event: Event) => ({ pageName: 'note', props: { event } }) -export const toNoStrudelNote = (id: string) => `https://nostrudel.ninja/#/n/${id}` -export const toHashtag = (hashtag: string) => ({ pageName: 'hashtag', props: { hashtag } }) +// copy from nostr-tools/utils +export function normalizeUrl(url: string): string { + if (url.indexOf('://') === -1) url = 'wss://' + url + const p = new URL(url) + p.pathname = p.pathname.replace(/\/+/g, '/') + if (p.pathname.endsWith('/')) p.pathname = p.pathname.slice(0, -1) + if ((p.port === '80' && p.protocol === 'ws:') || (p.port === '443' && p.protocol === 'wss:')) + p.port = '' + p.searchParams.sort() + p.hash = '' + return p.toString() +} diff --git a/src/renderer/src/pages/primary/NoteListPage/index.tsx b/src/renderer/src/pages/primary/NoteListPage/index.tsx index cc301167..5347a523 100644 --- a/src/renderer/src/pages/primary/NoteListPage/index.tsx +++ b/src/renderer/src/pages/primary/NoteListPage/index.tsx @@ -1,10 +1,12 @@ import NoteList from '@renderer/components/NoteList' import PrimaryPageLayout from '@renderer/layouts/PrimaryPageLayout' +import { useRelaySettings } from '@renderer/providers/RelaySettingsProvider' export default function NoteListPage() { + const { relayUrls } = useRelaySettings() return ( - + ) } diff --git a/src/renderer/src/pages/secondary/HashtagPage/index.tsx b/src/renderer/src/pages/secondary/HashtagPage/index.tsx index 4c7c41a3..12f2cf56 100644 --- a/src/renderer/src/pages/secondary/HashtagPage/index.tsx +++ b/src/renderer/src/pages/secondary/HashtagPage/index.tsx @@ -1,7 +1,9 @@ import NoteList from '@renderer/components/NoteList' import SecondaryPageLayout from '@renderer/layouts/SecondaryPageLayout' +import { useRelaySettings } from '@renderer/providers/RelaySettingsProvider' export default function HashtagPage({ hashtag }: { hashtag?: string }) { + const { relayUrls } = useRelaySettings() if (!hashtag) { return null } @@ -9,7 +11,11 @@ export default function HashtagPage({ hashtag }: { hashtag?: string }) { return ( - + ) } diff --git a/src/renderer/src/pages/secondary/ProfilePage/index.tsx b/src/renderer/src/pages/secondary/ProfilePage/index.tsx index adc5780f..04f3c2f5 100644 --- a/src/renderer/src/pages/secondary/ProfilePage/index.tsx +++ b/src/renderer/src/pages/secondary/ProfilePage/index.tsx @@ -4,6 +4,7 @@ import ProfileAbout from '@renderer/components/ProfileAbout' import { Avatar, AvatarFallback, AvatarImage } from '@renderer/components/ui/avatar' import { Separator } from '@renderer/components/ui/separator' import { useFetchProfile } from '@renderer/hooks' +import { useFetchRelayList } from '@renderer/hooks/useFetchRelayList' import SecondaryPageLayout from '@renderer/layouts/SecondaryPageLayout' import { formatNpub, generateImageByPubkey } from '@renderer/lib/pubkey' import { Copy } from 'lucide-react' @@ -12,6 +13,7 @@ import { useEffect, useMemo, useState } from 'react' export default function ProfilePage({ pubkey }: { pubkey?: string }) { const { banner, username, nip05, about, avatar } = useFetchProfile(pubkey) + const relayList = useFetchRelayList(pubkey) const [copied, setCopied] = useState(false) const npub = useMemo(() => (pubkey ? nip19.npubEncode(pubkey) : undefined), [pubkey]) const defaultImage = useMemo(() => (pubkey ? generateImageByPubkey(pubkey) : ''), [pubkey]) @@ -61,7 +63,7 @@ export default function ProfilePage({ pubkey }: { pubkey?: string }) { - + ) } diff --git a/src/renderer/src/providers/NostrProvider.tsx b/src/renderer/src/providers/NostrProvider.tsx index 248ad660..d433a608 100644 --- a/src/renderer/src/providers/NostrProvider.tsx +++ b/src/renderer/src/providers/NostrProvider.tsx @@ -1,13 +1,17 @@ import { TDraftEvent } from '@common/types' -import { createContext, useContext, useEffect, useState } from 'react' +import { useFetchRelayList } from '@renderer/hooks/useFetchRelayList' import client from '@renderer/services/client.service' +import { createContext, useContext, useEffect, useState } from 'react' type TNostrContext = { pubkey: string | null canLogin: boolean login: (nsec: string) => Promise logout: () => Promise - publish: (draftEvent: TDraftEvent) => Promise + /** + * Default publish the event to current relays, user's write relays and additional relays + */ + publish: (draftEvent: TDraftEvent, additionalRelayUrls?: string[]) => Promise } const NostrContext = createContext(undefined) @@ -23,6 +27,7 @@ export const useNostr = () => { export function NostrProvider({ children }: { children: React.ReactNode }) { const [pubkey, setPubkey] = useState(null) const [canLogin, setCanLogin] = useState(false) + const relayList = useFetchRelayList(pubkey) useEffect(() => { window.api.nostr.getPublicKey().then((pubkey) => { @@ -52,12 +57,12 @@ export function NostrProvider({ children }: { children: React.ReactNode }) { setPubkey(null) } - const publish = async (draftEvent: TDraftEvent) => { + const publish = async (draftEvent: TDraftEvent, additionalRelayUrls: string[] = []) => { const event = await window.api.nostr.signEvent(draftEvent) if (!event) { throw new Error('sign event failed') } - await client.publishEvent(event) + await client.publishEvent(relayList.write.concat(additionalRelayUrls), event) } return ( diff --git a/src/renderer/src/providers/NoteStatsProvider.tsx b/src/renderer/src/providers/NoteStatsProvider.tsx index 40327d25..143fbb31 100644 --- a/src/renderer/src/providers/NoteStatsProvider.tsx +++ b/src/renderer/src/providers/NoteStatsProvider.tsx @@ -48,7 +48,8 @@ export function NoteStatsProvider({ children }: { children: React.ReactNode }) { }, [pubkey]) const fetchNoteLikeCount = async (event: Event) => { - const events = await client.fetchEvents({ + const relayList = await client.fetchRelayList(event.pubkey) + const events = await client.fetchEvents(relayList.read, { '#e': [event.id], kinds: [kinds.Reaction], limit: 500 @@ -72,7 +73,8 @@ export function NoteStatsProvider({ children }: { children: React.ReactNode }) { } const fetchNoteRepostCount = async (event: Event) => { - const events = await client.fetchEvents({ + const relayList = await client.fetchRelayList(event.pubkey) + const events = await client.fetchEvents(relayList.read, { '#e': [event.id], kinds: [kinds.Repost], limit: 100 @@ -92,7 +94,8 @@ export function NoteStatsProvider({ children }: { children: React.ReactNode }) { const fetchNoteLikedStatus = async (event: Event) => { if (!pubkey) return false - const events = await client.fetchEvents({ + const relayList = await client.fetchRelayList(pubkey) + const events = await client.fetchEvents(relayList.write, { '#e': [event.id], authors: [pubkey], kinds: [kinds.Reaction] @@ -119,7 +122,8 @@ export function NoteStatsProvider({ children }: { children: React.ReactNode }) { const fetchNoteRepostedStatus = async (event: Event) => { if (!pubkey) return false - const events = await client.fetchEvents({ + const relayList = await client.fetchRelayList(pubkey) + const events = await client.fetchEvents(relayList.write, { '#e': [event.id], authors: [pubkey], kinds: [kinds.Repost] diff --git a/src/renderer/src/services/client.service.ts b/src/renderer/src/services/client.service.ts index 1a47acff..056c3943 100644 --- a/src/renderer/src/services/client.service.ts +++ b/src/renderer/src/services/client.service.ts @@ -1,11 +1,13 @@ import { TRelayGroup } from '@common/types' import { formatPubkey } from '@renderer/lib/pubkey' -import { TProfile } from '@renderer/types' +import { tagNameEquals } from '@renderer/lib/tag' +import { TProfile, TRelayList } from '@renderer/types' import DataLoader from 'dataloader' import { LRUCache } from 'lru-cache' import { Filter, kinds, Event as NEvent, SimplePool } from 'nostr-tools' import { EVENT_TYPES, eventBus } from './event-bus.service' import storage from './storage.service' +import { isWebsocketUrl, normalizeUrl } from '@renderer/lib/url' const BIG_RELAY_URLS = [ 'wss://relay.damus.io/', @@ -24,24 +26,31 @@ class ClientService { private eventCache = new LRUCache>({ max: 10000, fetchMethod: async (filterStr) => { - const [event] = await this.fetchEvents(JSON.parse(filterStr)) + const [event] = await this.fetchEvents( + BIG_RELAY_URLS.concat(this.relayUrls), + JSON.parse(filterStr) + ) return event } }) - private eventDataloader = new DataLoader( this.eventBatchLoadFn.bind(this), { cacheMap: new LRUCache>({ max: 10000 }) } ) - private profileDataloader = new DataLoader( this.profileBatchLoadFn.bind(this), { cacheMap: new LRUCache>({ max: 10000 }) } ) + private relayListDataLoader = new DataLoader( + this.relayListBatchLoadFn.bind(this), + { + cacheMap: new LRUCache>({ max: 10000 }) + } + ) constructor() { if (!ClientService.instance) { @@ -68,9 +77,8 @@ class ClientService { return this.pool.listConnectionStatus() } - async publishEvent(event: NEvent) { - // TODO: outbox - return await Promise.any(this.pool.publish(this.relayUrls, event)) + async publishEvent(relayUrls: string[], event: NEvent) { + return await Promise.any(this.pool.publish(this.relayUrls.concat(relayUrls), event)) } subscribeEvents( @@ -103,9 +111,10 @@ class ClientService { }) } - async fetchEvents(filter: Filter, relayUrls: string[] = this.relayUrls) { + async fetchEvents(relayUrls: string[], filter: Filter) { await this.initPromise - return await this.pool.querySync(relayUrls, filter) + // If relayUrls is empty, use this.relayUrls + return await this.pool.querySync(relayUrls.length > 0 ? relayUrls : this.relayUrls, filter) } async fetchEventByFilter(filter: Filter) { @@ -120,8 +129,12 @@ class ClientService { return this.profileDataloader.load(pubkey) } + async fetchRelayList(pubkey: string): Promise { + return this.relayListDataLoader.load(pubkey) + } + private async eventBatchLoadFn(ids: readonly string[]) { - const events = await this.fetchEvents({ + const events = await this.fetchEvents(this.relayUrls, { ids: ids as string[], limit: ids.length }) @@ -133,11 +146,11 @@ class ClientService { const missingIds = ids.filter((id) => !eventsMap.has(id)) if (missingIds.length > 0) { const missingEvents = await this.fetchEvents( + BIG_RELAY_URLS.filter((url) => !this.relayUrls.includes(url)), { ids: missingIds, limit: missingIds.length - }, - BIG_RELAY_URLS.filter((url) => !this.relayUrls.includes(url)) + } ) for (const event of missingEvents) { eventsMap.set(event.id, event) @@ -148,7 +161,7 @@ class ClientService { } private async profileBatchLoadFn(pubkeys: readonly string[]) { - const events = await this.fetchEvents({ + const events = await this.fetchEvents(this.relayUrls, { authors: pubkeys as string[], kinds: [kinds.Metadata], limit: pubkeys.length @@ -165,12 +178,12 @@ class ClientService { const missingPubkeys = pubkeys.filter((pubkey) => !eventsMap.has(pubkey)) if (missingPubkeys.length > 0) { const missingEvents = await this.fetchEvents( + BIG_RELAY_URLS.filter((url) => !this.relayUrls.includes(url)), { authors: missingPubkeys, kinds: [kinds.Metadata], limit: missingPubkeys.length - }, - BIG_RELAY_URLS.filter((url) => !this.relayUrls.includes(url)) + } ) for (const event of missingEvents) { const pubkey = event.pubkey @@ -187,6 +200,49 @@ class ClientService { }) } + private async relayListBatchLoadFn(pubkeys: readonly string[]) { + const events = await this.fetchEvents(BIG_RELAY_URLS.concat(this.relayUrls), { + authors: pubkeys as string[], + kinds: [kinds.RelayList], + limit: pubkeys.length + }) + const eventsMap = new Map() + for (const event of events) { + const pubkey = event.pubkey + const existing = eventsMap.get(pubkey) + if (!existing || existing.created_at < event.created_at) { + eventsMap.set(pubkey, event) + } + } + + return pubkeys.map((pubkey) => { + const event = eventsMap.get(pubkey) + const relayList = { write: [], read: [] } as TRelayList + if (!event) return relayList + + event.tags.filter(tagNameEquals('r')).forEach(([, url, type]) => { + if (!url || !isWebsocketUrl(url)) return + + const normalizedUrl = normalizeUrl(url) + switch (type) { + case 'w': + relayList.write.push(normalizedUrl) + break + case 'r': + relayList.read.push(normalizedUrl) + break + default: + relayList.write.push(normalizedUrl) + relayList.read.push(normalizedUrl) + } + }) + return { + write: relayList.write.slice(0, 3), + read: relayList.read.slice(0, 3) + } + }) + } + private parseProfileFromEvent(event: NEvent): TProfile { try { const profileObj = JSON.parse(event.content) diff --git a/src/renderer/src/types.ts b/src/renderer/src/types.ts index 352357a3..330abe74 100644 --- a/src/renderer/src/types.ts +++ b/src/renderer/src/types.ts @@ -6,3 +6,8 @@ export type TProfile = { nip05?: string about?: string } + +export type TRelayList = { + write: string[] + read: string[] +}