diff --git a/src/components/FollowingFavoriteRelayList/index.tsx b/src/components/FollowingFavoriteRelayList/index.tsx index 30b8f620..012ae9f5 100644 --- a/src/components/FollowingFavoriteRelayList/index.tsx +++ b/src/components/FollowingFavoriteRelayList/index.tsx @@ -1,9 +1,8 @@ +import { useFetchRelayInfo } from '@/hooks' import { toRelay } from '@/lib/link' import { useSecondaryPage } from '@/PageManager' import { useNostr } from '@/providers/NostrProvider' import client from '@/services/client.service' -import relayInfoService from '@/services/relay-info.service' -import { TNip66RelayInfo } from '@/types' import { useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import RelaySimpleInfo, { RelaySimpleInfoSkeleton } from '../RelaySimpleInfo' @@ -12,10 +11,9 @@ const SHOW_COUNT = 10 export default function FollowingFavoriteRelayList() { const { t } = useTranslation() - const { push } = useSecondaryPage() const { pubkey } = useNostr() const [loading, setLoading] = useState(true) - const [relays, setRelays] = useState<(TNip66RelayInfo & { users: string[] })[]>([]) + const [relays, setRelays] = useState<[string, string[]][]>([]) const [showCount, setShowCount] = useState(SHOW_COUNT) const bottomRef = useRef(null) @@ -25,21 +23,8 @@ export default function FollowingFavoriteRelayList() { const init = async () => { if (!pubkey) return - const relayMap = - (await client.fetchFollowingFavoriteRelays(pubkey)) ?? new Map>() - const relayUrls = Array.from(relayMap.keys()) - const relayInfos = await relayInfoService.getRelayInfos(relayUrls ?? []) - setRelays( - (relayInfos.filter(Boolean) as TNip66RelayInfo[]) - .map((relayInfo) => { - const users = Array.from(relayMap.get(relayInfo.url) ?? []) - return { - ...relayInfo, - users - } - }) - .sort((a, b) => b.users.length - a.users.length) - ) + const relays = (await client.fetchFollowingFavoriteRelays(pubkey)) ?? [] + setRelays(relays) } init().finally(() => { setLoading(false) @@ -73,16 +58,8 @@ export default function FollowingFavoriteRelayList() { return (
- {relays.slice(0, showCount).map((relay) => ( - { - e.stopPropagation() - push(toRelay(relay.url)) - }} - /> + {relays.slice(0, showCount).map(([url, users]) => ( + ))} {showCount < relays.length &&
} {loading && } @@ -94,3 +71,21 @@ export default function FollowingFavoriteRelayList() {
) } + +function RelayItem({ url, users }: { url: string; users: string[] }) { + const { push } = useSecondaryPage() + const { relayInfo } = useFetchRelayInfo(url) + + return ( + { + e.stopPropagation() + push(toRelay(url)) + }} + /> + ) +} diff --git a/src/components/RelaySimpleInfo/index.tsx b/src/components/RelaySimpleInfo/index.tsx index 3e30d368..b6eee351 100644 --- a/src/components/RelaySimpleInfo/index.tsx +++ b/src/components/RelaySimpleInfo/index.tsx @@ -10,11 +10,13 @@ import { SimpleUserAvatar } from '../UserAvatar' export default function RelaySimpleInfo({ relayInfo, + users, hideBadge = false, className, ...props }: HTMLProps & { - relayInfo?: TNip66RelayInfo & { users?: string[] } + relayInfo?: TNip66RelayInfo + users?: string[] hideBadge?: boolean }) { const { t } = useTranslation() @@ -35,16 +37,16 @@ export default function RelaySimpleInfo({
{!hideBadge && relayInfo && } {!!relayInfo?.description &&
{relayInfo.description}
} - {!!relayInfo?.users?.length && ( + {!!users?.length && (
{t('Favorited by')}
- {relayInfo.users.slice(0, 10).map((user) => ( + {users.slice(0, 10).map((user) => ( ))} - {relayInfo.users.length > 10 && ( + {users.length > 10 && (
- +{relayInfo.users.length - 10} + +{users.length - 10}
)}
diff --git a/src/services/client.service.ts b/src/services/client.service.ts index 50b91c23..e72517ef 100644 --- a/src/services/client.service.ts +++ b/src/services/client.service.ts @@ -74,10 +74,7 @@ class ClientService extends EventTarget { max: 2000, fetchMethod: this._fetchFollowListEvent.bind(this) }) - private fetchFollowingFavoriteRelaysCache = new LRUCache< - string, - Promise>> - >({ + private fetchFollowingFavoriteRelaysCache = new LRUCache>({ max: 10, fetchMethod: this._fetchFollowingFavoriteRelays.bind(this) }) @@ -823,40 +820,54 @@ class ClientService extends EventTarget { } private async _fetchFollowingFavoriteRelays(pubkey: string) { - const followings = await this.fetchFollowings(pubkey) - const events = await this.fetchEvents(BIG_RELAY_URLS, { - authors: followings, - kinds: [ExtendedKind.FAVORITE_RELAYS, kinds.Relaysets], - limit: 1000 - }) - const alreadyExistsFavoriteRelaysPubkeySet = new Set() - const alreadyExistsRelaySetsPubkeySet = new Set() - const uniqueEvents: NEvent[] = [] - events - .sort((a, b) => b.created_at - a.created_at) - .forEach((event) => { - if (event.kind === ExtendedKind.FAVORITE_RELAYS) { - if (alreadyExistsFavoriteRelaysPubkeySet.has(event.pubkey)) return - alreadyExistsFavoriteRelaysPubkeySet.add(event.pubkey) - } else if (event.kind === kinds.Relaysets) { - if (alreadyExistsRelaySetsPubkeySet.has(event.pubkey)) return - alreadyExistsRelaySetsPubkeySet.add(event.pubkey) - } else { - return - } - uniqueEvents.push(event) + const fetchNewData = async () => { + const followings = await this.fetchFollowings(pubkey) + const events = await this.fetchEvents(BIG_RELAY_URLS, { + authors: followings, + kinds: [ExtendedKind.FAVORITE_RELAYS, kinds.Relaysets], + limit: 1000 }) + const alreadyExistsFavoriteRelaysPubkeySet = new Set() + const alreadyExistsRelaySetsPubkeySet = new Set() + const uniqueEvents: NEvent[] = [] + events + .sort((a, b) => b.created_at - a.created_at) + .forEach((event) => { + if (event.kind === ExtendedKind.FAVORITE_RELAYS) { + if (alreadyExistsFavoriteRelaysPubkeySet.has(event.pubkey)) return + alreadyExistsFavoriteRelaysPubkeySet.add(event.pubkey) + } else if (event.kind === kinds.Relaysets) { + if (alreadyExistsRelaySetsPubkeySet.has(event.pubkey)) return + alreadyExistsRelaySetsPubkeySet.add(event.pubkey) + } else { + return + } + uniqueEvents.push(event) + }) - const relayMap = new Map>() - uniqueEvents.forEach((event) => { - event.tags.forEach(([tagName, tagValue]) => { - if (tagName === 'relay' && tagValue && isWebsocketUrl(tagValue)) { - const url = normalizeUrl(tagValue) - relayMap.set(url, (relayMap.get(url) || new Set()).add(event.pubkey)) - } + const relayMap = new Map>() + uniqueEvents.forEach((event) => { + event.tags.forEach(([tagName, tagValue]) => { + if (tagName === 'relay' && tagValue && isWebsocketUrl(tagValue)) { + const url = normalizeUrl(tagValue) + relayMap.set(url, (relayMap.get(url) || new Set()).add(event.pubkey)) + } + }) }) - }) - return relayMap + const relayMapEntries = Array.from(relayMap.entries()) + .sort((a, b) => b[1].size - a[1].size) + .map(([url, pubkeys]) => [url, Array.from(pubkeys)]) as [string, string[]][] + + indexedDb.putFollowingFavoriteRelays(pubkey, relayMapEntries) + return relayMapEntries + } + + const cached = await indexedDb.getFollowingFavoriteRelays(pubkey) + if (cached) { + fetchNewData() + return cached + } + return fetchNewData() } updateFollowListCache(event: NEvent) { diff --git a/src/services/indexed-db.service.ts b/src/services/indexed-db.service.ts index 13f50d0c..085cd2a7 100644 --- a/src/services/indexed-db.service.ts +++ b/src/services/indexed-db.service.ts @@ -17,7 +17,8 @@ const StoreNames = { MUTE_DECRYPTED_TAGS: 'muteDecryptedTags', RELAY_INFO_EVENTS: 'relayInfoEvents', FAVORITE_RELAYS: 'favoriteRelays', - RELAY_SETS: 'relaySets' + RELAY_SETS: 'relaySets', + FOLLOWING_FAVORITE_RELAYS: 'followingFavoriteRelays' } class IndexedDbService { @@ -36,7 +37,7 @@ class IndexedDbService { init(): Promise { if (!this.initPromise) { this.initPromise = new Promise((resolve, reject) => { - const request = window.indexedDB.open('jumble', 4) + const request = window.indexedDB.open('jumble', 5) request.onerror = (event) => { reject(event) @@ -76,6 +77,9 @@ class IndexedDbService { if (!db.objectStoreNames.contains(StoreNames.RELAY_SETS)) { db.createObjectStore(StoreNames.RELAY_SETS, { keyPath: 'key' }) } + if (!db.objectStoreNames.contains(StoreNames.FOLLOWING_FAVORITE_RELAYS)) { + db.createObjectStore(StoreNames.FOLLOWING_FAVORITE_RELAYS, { keyPath: 'key' }) + } this.db = db } }) @@ -363,6 +367,50 @@ class IndexedDbService { }) } + async putFollowingFavoriteRelays(pubkey: string, relays: [string, string[]][]): Promise { + await this.initPromise + return new Promise((resolve, reject) => { + if (!this.db) { + return reject('database not initialized') + } + const transaction = this.db.transaction(StoreNames.FOLLOWING_FAVORITE_RELAYS, 'readwrite') + const store = transaction.objectStore(StoreNames.FOLLOWING_FAVORITE_RELAYS) + + const putRequest = store.put(this.formatValue(pubkey, relays)) + putRequest.onsuccess = () => { + transaction.commit() + resolve() + } + + putRequest.onerror = (event) => { + transaction.commit() + reject(event) + } + }) + } + + async getFollowingFavoriteRelays(pubkey: string): Promise<[string, string[]][] | null> { + await this.initPromise + return new Promise((resolve, reject) => { + if (!this.db) { + return reject('database not initialized') + } + const transaction = this.db.transaction(StoreNames.FOLLOWING_FAVORITE_RELAYS, 'readonly') + const store = transaction.objectStore(StoreNames.FOLLOWING_FAVORITE_RELAYS) + const request = store.get(pubkey) + + request.onsuccess = () => { + transaction.commit() + resolve((request.result as TValue<[string, string[]][]>)?.value) + } + + request.onerror = (event) => { + transaction.commit() + reject(event) + } + }) + } + private getReplaceableEventKey(event: Event): string { if ( [kinds.Metadata, kinds.Contacts].includes(event.kind) ||