From 9db979c31d2b10ffad338633988cee25f20aefa7 Mon Sep 17 00:00:00 2001 From: codytseng Date: Mon, 20 Jan 2025 22:56:00 +0800 Subject: [PATCH] feat: calculate optimal read relays --- .../CalculateOptimalReadRelaysButton.tsx | 248 ++++++++++++++++++ src/components/MailboxSetting/index.tsx | 9 + src/i18n/en.ts | 8 +- src/i18n/zh.ts | 8 +- src/services/client.service.ts | 55 +++- 5 files changed, 325 insertions(+), 3 deletions(-) create mode 100644 src/components/MailboxSetting/CalculateOptimalReadRelaysButton.tsx diff --git a/src/components/MailboxSetting/CalculateOptimalReadRelaysButton.tsx b/src/components/MailboxSetting/CalculateOptimalReadRelaysButton.tsx new file mode 100644 index 00000000..639d304c --- /dev/null +++ b/src/components/MailboxSetting/CalculateOptimalReadRelaysButton.tsx @@ -0,0 +1,248 @@ +import { Button } from '@/components/ui/button' +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger +} from '@/components/ui/dialog' +import { + Drawer, + DrawerContent, + DrawerDescription, + DrawerHeader, + DrawerTitle, + DrawerTrigger +} from '@/components/ui/drawer' +import { toProfile } from '@/lib/link' +import { useSecondaryPage } from '@/PageManager' +import { useNostr } from '@/providers/NostrProvider' +import { useScreenSize } from '@/providers/ScreenSizeProvider' +import client from '@/services/client.service' +import { TMailboxRelay } from '@/types' +import { ChevronDown, Circle, CircleCheck, ScanSearch } from 'lucide-react' +import { Dispatch, SetStateAction, useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' +import RelayIcon from '../RelayIcon' +import { SimpleUserAvatar } from '../UserAvatar' +import { SimpleUsername } from '../Username' + +export default function CalculateOptimalReadRelaysButton({ + mergeRelays +}: { + mergeRelays: (newRelays: TMailboxRelay[]) => void +}) { + const { t } = useTranslation() + const { isSmallScreen } = useScreenSize() + const { pubkey } = useNostr() + const [open, setOpen] = useState(false) + + const trigger = ( + + ) + + if (isSmallScreen) { + return ( + + {trigger} + +
+ + {t('Select relays to append')} + + + setOpen(false)} mergeRelays={mergeRelays} /> +
+
+
+ ) + } + + return ( + + {trigger} + + + {t('Select relays to append')} + + + setOpen(false)} mergeRelays={mergeRelays} /> + + + ) +} + +function OptimalReadRelays({ + close, + mergeRelays +}: { + close: () => void + mergeRelays: (newRelays: TMailboxRelay[]) => void +}) { + const { t } = useTranslation() + const { pubkey } = useNostr() + const [isCalculating, setIsCalculating] = useState(false) + const [optimalReadRelays, setOptimalReadRelays] = useState<{ url: string; pubkeys: string[] }[]>( + [] + ) + const [selectedRelayUrls, setSelectedRelayUrls] = useState([]) + + useEffect(() => { + if (!pubkey) return + + const init = async () => { + setIsCalculating(true) + const relays = await client.calculateOptimalReadRelays(pubkey) + console.log(relays) + setOptimalReadRelays(relays) + setIsCalculating(false) + } + init() + }, []) + + if (isCalculating) { + return
{t('calculating...')}
+ } + + return ( +
+
+ {optimalReadRelays.map((relay) => ( + + ))} +
+ +
+ ) +} + +function RelayItem({ + relay, + close, + selectedRelayUrls, + setSelectedRelayUrls +}: { + relay: { url: string; pubkeys: string[] } + close: () => void + selectedRelayUrls: string[] + setSelectedRelayUrls: Dispatch> +}) { + const { t } = useTranslation() + const { push } = useSecondaryPage() + const [expanded, setExpanded] = useState(false) + + const selected = selectedRelayUrls.includes(relay.url) + + return ( +
+ setSelectedRelayUrls((pre) => + pre.includes(relay.url) ? pre.filter((url) => url !== relay.url) : [...pre, relay.url] + ) + } + > +
+
+ { + setSelectedRelayUrls((prev) => + select ? [...prev, relay.url] : prev.filter((url) => url !== relay.url) + ) + }} + /> + +
{relay.url}
+
+
{ + e.stopPropagation() + setExpanded((prev) => !prev) + }} + > +
+ {relay.pubkeys.length} {t('followings')} +
+ +
+
+ {expanded && ( +
+ {relay.pubkeys.map((pubkey) => ( +
{ + e.stopPropagation() + close() + push(toProfile(pubkey)) + }} + > + + +
+ ))} +
+ )} +
+ ) +} + +function SelectToggle({ + select, + setSelect +}: { + select: boolean + setSelect: (select: boolean) => void +}) { + return select ? ( + { + e.stopPropagation() + setSelect(false) + }} + /> + ) : ( + { + e.stopPropagation() + setSelect(true) + }} + /> + ) +} diff --git a/src/components/MailboxSetting/index.tsx b/src/components/MailboxSetting/index.tsx index 5f844351..78974db2 100644 --- a/src/components/MailboxSetting/index.tsx +++ b/src/components/MailboxSetting/index.tsx @@ -4,6 +4,7 @@ import { useNostr } from '@/providers/NostrProvider' import { TMailboxRelay, TMailboxRelayScope } from '@/types' import { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' +import CalculateOptimalReadRelaysButton from './CalculateOptimalReadRelaysButton' import MailboxRelay from './MailboxRelay' import NewMailboxRelayInput from './NewMailboxRelayInput' import SaveButton from './SaveButton' @@ -49,6 +50,13 @@ export default function MailboxSetting() { return null } + const mergeRelays = (newRelays: TMailboxRelay[]) => { + setRelays((pre) => { + return [...pre, ...newRelays.filter((r) => !pre.some((pr) => pr.url === r.url))] + }) + setHasChange(true) + } + return (
@@ -56,6 +64,7 @@ export default function MailboxSetting() {
{t('write relays description')}
{t('read & write relays notice')}
+
{relays.map((relay) => ( diff --git a/src/i18n/en.ts b/src/i18n/en.ts index 3480c63e..68ea7968 100644 --- a/src/i18n/en.ts +++ b/src/i18n/en.ts @@ -12,6 +12,7 @@ export default { Profile: 'Profile', Logout: 'Logout', Following: 'Following', + followings: 'followings', reposted: 'reposted', 'just now': 'just now', 'n minutes ago': '{{n}} minutes ago', @@ -153,6 +154,11 @@ export default { Unmute: 'Unmute', 'mute author': 'mute author', 'mute user': 'mute user', - 'unmute user': 'unmute user' + 'unmute user': 'unmute user', + 'Append n relays': 'Append {{n}} relays', + Append: 'Append', + 'Select relays to append': 'Select relays to append', + 'calculating...': 'calculating...', + 'Calculate optimal read relays': 'Calculate optimal read relays' } } diff --git a/src/i18n/zh.ts b/src/i18n/zh.ts index 387f51bb..bb732c51 100644 --- a/src/i18n/zh.ts +++ b/src/i18n/zh.ts @@ -12,6 +12,7 @@ export default { Profile: '个人资料', Logout: '退出登录', Following: '关注', + followings: '关注', reposted: '转发', 'just now': '刚刚', 'n minutes ago': '{{n}} 分钟前', @@ -154,6 +155,11 @@ export default { Unmute: '取消屏蔽', 'mute author': '屏蔽作者', 'mute user': '屏蔽用户', - 'unmute user': '取消屏蔽用户' + 'unmute user': '取消屏蔽用户', + 'Append n relays': '追加 {{n}} 个服务器', + Append: '追加', + 'Select relays to append': '选择要追加的服务器', + 'calculating...': '计算中...', + 'Calculate optimal read relays': '计算最佳读服务器' } } diff --git a/src/services/client.service.ts b/src/services/client.service.ts index 9d4befdb..dbc36a88 100644 --- a/src/services/client.service.ts +++ b/src/services/client.service.ts @@ -54,7 +54,8 @@ class ClientService extends EventTarget { private relayListEventDataLoader = new DataLoader( this.relayListEventBatchLoadFn.bind(this), { - cacheMap: new LRUCache>({ max: 10000 }) + cacheMap: new LRUCache>({ max: 10000 }), + maxBatchSize: 10 } ) private relayInfoDataLoader = new DataLoader(async (urls) => { @@ -446,6 +447,58 @@ class ClientService extends EventTarget { return infos.map((info) => (info ? (info instanceof Error ? undefined : info) : undefined)) } + async calculateOptimalReadRelays(pubkey: string) { + const followings = await this.fetchFollowings(pubkey) + const [selfRelayListEvent, ...relayListEvents] = await this.relayListEventDataLoader.loadMany([ + pubkey, + ...followings + ]) + const selfReadRelays = + selfRelayListEvent && !(selfRelayListEvent instanceof Error) + ? getRelayListFromRelayListEvent(selfRelayListEvent).read + : [] + const pubkeyRelayListMap = new Map() + relayListEvents.forEach((evt) => { + if (evt && !(evt instanceof Error)) { + pubkeyRelayListMap.set(evt.pubkey, getRelayListFromRelayListEvent(evt).write) + } + }) + + let uncoveredPubkeys = [...followings] + const readRelays: { url: string; pubkeys: string[] }[] = [] + while (uncoveredPubkeys.length) { + const relayMap = new Map() + uncoveredPubkeys.forEach((pubkey) => { + const relays = pubkeyRelayListMap.get(pubkey) + if (relays) { + relays.forEach((url) => { + relayMap.set(url, (relayMap.get(url) || []).concat(pubkey)) + }) + } + }) + let maxCoveredRelay: { url: string; pubkeys: string[] } | undefined + for (const [url, pubkeys] of relayMap.entries()) { + if (!maxCoveredRelay) { + maxCoveredRelay = { url, pubkeys } + } else if (pubkeys.length > maxCoveredRelay.pubkeys.length) { + maxCoveredRelay = { url, pubkeys } + } else if ( + pubkeys.length === maxCoveredRelay.pubkeys.length && + selfReadRelays.includes(url) + ) { + maxCoveredRelay = { url, pubkeys } + } + } + if (!maxCoveredRelay) break + + readRelays.push(maxCoveredRelay) + uncoveredPubkeys = uncoveredPubkeys.filter( + (pubkey) => !maxCoveredRelay!.pubkeys.includes(pubkey) + ) + } + return readRelays + } + private async fetchEventById(relayUrls: string[], id: string): Promise { const event = await this.fetchEventFromDefaultRelaysDataloader.load(id) if (event) {