feat: outbox model (#4)
This commit is contained in:
@@ -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 }) {
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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<IntersectionObserver | null>(null)
|
||||
const bottomRef = useRef<HTMLDivElement | null>(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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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({
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// 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()
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
23
src/renderer/src/hooks/useFetchRelayList.tsx
Normal file
23
src/renderer/src/hooks/useFetchRelayList.tsx
Normal file
@@ -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<TRelayList>({ 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
|
||||
}
|
||||
@@ -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'
|
||||
|
||||
7
src/renderer/src/lib/link.ts
Normal file
7
src/renderer/src/lib/link.ts
Normal file
@@ -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 } })
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<PrimaryPageLayout>
|
||||
<NoteList />
|
||||
<NoteList relayUrls={relayUrls} />
|
||||
</PrimaryPageLayout>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<SecondaryPageLayout titlebarContent={`# ${normalizedHashtag}`}>
|
||||
<NoteList key={normalizedHashtag} filter={{ '#t': [normalizedHashtag] }} />
|
||||
<NoteList
|
||||
key={normalizedHashtag}
|
||||
filter={{ '#t': [normalizedHashtag] }}
|
||||
relayUrls={relayUrls}
|
||||
/>
|
||||
</SecondaryPageLayout>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 }) {
|
||||
</div>
|
||||
</div>
|
||||
<Separator className="my-4" />
|
||||
<NoteList key={pubkey} filter={{ authors: [pubkey] }} />
|
||||
<NoteList key={pubkey} filter={{ authors: [pubkey] }} relayUrls={relayList.write} />
|
||||
</SecondaryPageLayout>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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<string>
|
||||
logout: () => Promise<void>
|
||||
publish: (draftEvent: TDraftEvent) => Promise<void>
|
||||
/**
|
||||
* Default publish the event to current relays, user's write relays and additional relays
|
||||
*/
|
||||
publish: (draftEvent: TDraftEvent, additionalRelayUrls?: string[]) => Promise<void>
|
||||
}
|
||||
|
||||
const NostrContext = createContext<TNostrContext | undefined>(undefined)
|
||||
@@ -23,6 +27,7 @@ export const useNostr = () => {
|
||||
export function NostrProvider({ children }: { children: React.ReactNode }) {
|
||||
const [pubkey, setPubkey] = useState<string | null>(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 (
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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<string, Promise<NEvent | undefined>>({
|
||||
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<string, NEvent | undefined>(
|
||||
this.eventBatchLoadFn.bind(this),
|
||||
{
|
||||
cacheMap: new LRUCache<string, Promise<NEvent | undefined>>({ max: 10000 })
|
||||
}
|
||||
)
|
||||
|
||||
private profileDataloader = new DataLoader<string, TProfile | undefined>(
|
||||
this.profileBatchLoadFn.bind(this),
|
||||
{
|
||||
cacheMap: new LRUCache<string, Promise<TProfile | undefined>>({ max: 10000 })
|
||||
}
|
||||
)
|
||||
private relayListDataLoader = new DataLoader<string, TRelayList>(
|
||||
this.relayListBatchLoadFn.bind(this),
|
||||
{
|
||||
cacheMap: new LRUCache<string, Promise<TRelayList>>({ 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<TRelayList> {
|
||||
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<string, NEvent>()
|
||||
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)
|
||||
|
||||
@@ -6,3 +6,8 @@ export type TProfile = {
|
||||
nip05?: string
|
||||
about?: string
|
||||
}
|
||||
|
||||
export type TRelayList = {
|
||||
write: string[]
|
||||
read: string[]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user