diff --git a/src/components/Explore/index.tsx b/src/components/Explore/index.tsx new file mode 100644 index 0000000..11fc31e --- /dev/null +++ b/src/components/Explore/index.tsx @@ -0,0 +1,85 @@ +import { Skeleton } from '@/components/ui/skeleton' +import { useFetchRelayInfo } from '@/hooks' +import { toRelay } from '@/lib/link' +import { useSecondaryPage } from '@/PageManager' +import relayInfoService from '@/services/relay-info.service' +import { TAwesomeRelayCollection } from '@/types' +import { useEffect, useState } from 'react' +import RelaySimpleInfo, { RelaySimpleInfoSkeleton } from '../RelaySimpleInfo' +import { useDeepBrowsing } from '@/providers/DeepBrowsingProvider' +import { cn } from '@/lib/utils' + +export default function Explore() { + const [collections, setCollections] = useState(null) + + useEffect(() => { + relayInfoService.getAwesomeRelayCollections().then(setCollections) + }, []) + + if (!collections) { + return ( +
+
+ +
+
+ +
+
+ ) + } + + return ( +
+ {collections.map((collection) => ( + + ))} +
+ ) +} + +function RelayCollection({ collection }: { collection: TAwesomeRelayCollection }) { + const { deepBrowsing } = useDeepBrowsing() + return ( +
+
+ {collection.name} +
+
+ {collection.relays.map((url) => ( + + ))} +
+
+ ) +} + +function RelayItem({ url }: { url: string }) { + const { push } = useSecondaryPage() + const { relayInfo, isFetching } = useFetchRelayInfo(url) + + if (isFetching) { + return + } + + if (!relayInfo) { + return null + } + + return ( + { + e.stopPropagation() + push(toRelay(relayInfo.url)) + }} + /> + ) +} diff --git a/src/components/FollowingFavoriteRelayList/index.tsx b/src/components/FollowingFavoriteRelayList/index.tsx index 012ae9f..8838267 100644 --- a/src/components/FollowingFavoriteRelayList/index.tsx +++ b/src/components/FollowingFavoriteRelayList/index.tsx @@ -62,7 +62,7 @@ export default function FollowingFavoriteRelayList() { ))} {showCount < relays.length &&
} - {loading && } + {loading && } {!loading && (
{relays.length === 0 ? t('no relays found') : t('no more relays')} diff --git a/src/components/RelayList/index.tsx b/src/components/RelayList/index.tsx index d4d52c1..731fa1c 100644 --- a/src/components/RelayList/index.tsx +++ b/src/components/RelayList/index.tsx @@ -1,6 +1,6 @@ import { usePrimaryPage } from '@/PageManager' import relayInfoService from '@/services/relay-info.service' -import { TNip66RelayInfo } from '@/types' +import { TRelayInfo } from '@/types' import { useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import RelaySimpleInfo, { RelaySimpleInfoSkeleton } from '../RelaySimpleInfo' @@ -10,7 +10,7 @@ export default function RelayList() { const { t } = useTranslation() const { navigate } = usePrimaryPage() const [loading, setLoading] = useState(true) - const [relays, setRelays] = useState([]) + const [relays, setRelays] = useState([]) const [showCount, setShowCount] = useState(20) const [input, setInput] = useState('') const [debouncedInput, setDebouncedInput] = useState(input) @@ -82,7 +82,7 @@ export default function RelayList() { /> ))} {showCount < relays.length &&
} - {loading && } + {loading && } {!loading && relays.length === 0 && (
{t('no relays found')}
)} diff --git a/src/components/RelaySimpleInfo/index.tsx b/src/components/RelaySimpleInfo/index.tsx index b6eee35..60041b2 100644 --- a/src/components/RelaySimpleInfo/index.tsx +++ b/src/components/RelaySimpleInfo/index.tsx @@ -1,6 +1,6 @@ import { Skeleton } from '@/components/ui/skeleton' import { cn } from '@/lib/utils' -import { TNip66RelayInfo } from '@/types' +import { TRelayInfo } from '@/types' import { HTMLProps } from 'react' import { useTranslation } from 'react-i18next' import RelayBadges from '../RelayBadges' @@ -15,7 +15,7 @@ export default function RelaySimpleInfo({ className, ...props }: HTMLProps & { - relayInfo?: TNip66RelayInfo + relayInfo?: TRelayInfo users?: string[] hideBadge?: boolean }) { @@ -36,7 +36,7 @@ export default function RelaySimpleInfo({ {relayInfo && }
{!hideBadge && relayInfo && } - {!!relayInfo?.description &&
{relayInfo.description}
} + {!!relayInfo?.description &&
{relayInfo.description}
} {!!users?.length && (
{t('Favorited by')}
@@ -56,9 +56,9 @@ export default function RelaySimpleInfo({ ) } -export function RelaySimpleInfoSkeleton() { +export function RelaySimpleInfoSkeleton({ className }: { className?: string }) { return ( -
+
diff --git a/src/constants.ts b/src/constants.ts index 2888b2a..adc62e9 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -111,9 +111,6 @@ export const EMOJI_REGEX = export const YOUTUBE_URL_REGEX = /https?:\/\/(?:(?:www|m)\.)?(?:youtube\.com\/(?:watch\?[^#\s]*|embed\/[\w-]+|shorts\/[\w-]+|live\/[\w-]+)|youtu\.be\/[\w-]+)(?:\?[^#\s]*)?(?:#[^\s]*)?/g -export const MONITOR = '9bbbb845e5b6c831c29789900769843ab43bb5047abe697870cb50b6fc9bf923' -export const MONITOR_RELAYS = ['wss://relay.nostr.watch/'] - export const JUMBLE_PUBKEY = 'f4eb8e62add1340b9cadcd9861e669b2e907cea534e0f7f3ac974c11c758a51a' export const CODY_PUBKEY = '8125b911ed0e94dbe3008a0be48cfe5cd0c0b05923cfff917ae7e87da8400883' diff --git a/src/hooks/useFetchRelayInfo.tsx b/src/hooks/useFetchRelayInfo.tsx index 70dc9ab..20ddb80 100644 --- a/src/hooks/useFetchRelayInfo.tsx +++ b/src/hooks/useFetchRelayInfo.tsx @@ -1,10 +1,10 @@ import relayInfoService from '@/services/relay-info.service' -import { TNip66RelayInfo } from '@/types' +import { TRelayInfo } from '@/types' import { useEffect, useState } from 'react' export function useFetchRelayInfo(url?: string) { const [isFetching, setIsFetching] = useState(true) - const [relayInfo, setRelayInfo] = useState(undefined) + const [relayInfo, setRelayInfo] = useState(undefined) useEffect(() => { if (!url) return diff --git a/src/pages/primary/ExplorePage/index.tsx b/src/pages/primary/ExplorePage/index.tsx index 1d3616a..0d27568 100644 --- a/src/pages/primary/ExplorePage/index.tsx +++ b/src/pages/primary/ExplorePage/index.tsx @@ -1,15 +1,15 @@ +import Explore from '@/components/Explore' import FollowingFavoriteRelayList from '@/components/FollowingFavoriteRelayList' -import RelayList from '@/components/RelayList' import Tabs from '@/components/Tabs' import PrimaryPageLayout from '@/layouts/PrimaryPageLayout' import { Compass } from 'lucide-react' import { forwardRef, useState } from 'react' import { useTranslation } from 'react-i18next' -type TExploreTabs = 'following' | 'all' +type TExploreTabs = 'following' | 'explore' const ExplorePage = forwardRef((_, ref) => { - const [tab, setTab] = useState('following') + const [tab, setTab] = useState('explore') return ( { setTab(tab as TExploreTabs)} /> - {tab === 'following' ? : } + {tab === 'following' ? : } ) }) diff --git a/src/pages/secondary/HomePage/index.tsx b/src/pages/secondary/HomePage/index.tsx index a6798c9..a01e0cc 100644 --- a/src/pages/secondary/HomePage/index.tsx +++ b/src/pages/secondary/HomePage/index.tsx @@ -5,7 +5,7 @@ import { RECOMMENDED_RELAYS } from '@/constants' import SecondaryPageLayout from '@/layouts/SecondaryPageLayout' import { toRelay } from '@/lib/link' import relayInfoService from '@/services/relay-info.service' -import { TNip66RelayInfo } from '@/types' +import { TRelayInfo } from '@/types' import { ArrowRight, Server } from 'lucide-react' import { forwardRef, useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' @@ -14,13 +14,13 @@ const HomePage = forwardRef(({ index }: { index?: number }, ref) => { const { t } = useTranslation() const { navigate } = usePrimaryPage() const { push } = useSecondaryPage() - const [recommendedRelayInfos, setRecommendedRelayInfos] = useState([]) + const [recommendedRelayInfos, setRecommendedRelayInfos] = useState([]) useEffect(() => { const init = async () => { try { const relays = await relayInfoService.getRelayInfos(RECOMMENDED_RELAYS) - setRecommendedRelayInfos(relays.filter(Boolean) as TNip66RelayInfo[]) + setRecommendedRelayInfos(relays.filter(Boolean) as TRelayInfo[]) } catch (error) { console.error('Failed to fetch recommended relays:', error) } @@ -56,7 +56,7 @@ const HomePage = forwardRef(({ index }: { index?: number }, ref) => { {recommendedRelayInfos.map((relayInfo) => ( { e.stopPropagation() diff --git a/src/services/indexed-db.service.ts b/src/services/indexed-db.service.ts index 784bf26..3bf4481 100644 --- a/src/services/indexed-db.service.ts +++ b/src/services/indexed-db.service.ts @@ -1,5 +1,6 @@ import { ExtendedKind } from '@/constants' import { tagNameEquals } from '@/lib/tag' +import { TRelayInfo } from '@/types' import { Event, kinds } from 'nostr-tools' type TValue = { @@ -16,12 +17,13 @@ const StoreNames = { BOOKMARK_LIST_EVENTS: 'bookmarkListEvents', BLOSSOM_SERVER_LIST_EVENTS: 'blossomServerListEvents', MUTE_DECRYPTED_TAGS: 'muteDecryptedTags', - RELAY_INFO_EVENTS: 'relayInfoEvents', USER_EMOJI_LIST_EVENTS: 'userEmojiListEvents', EMOJI_SET_EVENTS: 'emojiSetEvents', FAVORITE_RELAYS: 'favoriteRelays', RELAY_SETS: 'relaySets', - FOLLOWING_FAVORITE_RELAYS: 'followingFavoriteRelays' + FOLLOWING_FAVORITE_RELAYS: 'followingFavoriteRelays', + RELAY_INFOS: 'relayInfos', + RELAY_INFO_EVENTS: 'relayInfoEvents' // deprecated } class IndexedDbService { @@ -40,7 +42,7 @@ class IndexedDbService { init(): Promise { if (!this.initPromise) { this.initPromise = new Promise((resolve, reject) => { - const request = window.indexedDB.open('jumble', 7) + const request = window.indexedDB.open('jumble', 8) request.onerror = (event) => { reject(event) @@ -71,9 +73,6 @@ class IndexedDbService { if (!db.objectStoreNames.contains(StoreNames.MUTE_DECRYPTED_TAGS)) { db.createObjectStore(StoreNames.MUTE_DECRYPTED_TAGS, { keyPath: 'key' }) } - if (!db.objectStoreNames.contains(StoreNames.RELAY_INFO_EVENTS)) { - db.createObjectStore(StoreNames.RELAY_INFO_EVENTS, { keyPath: 'key' }) - } if (!db.objectStoreNames.contains(StoreNames.FAVORITE_RELAYS)) { db.createObjectStore(StoreNames.FAVORITE_RELAYS, { keyPath: 'key' }) } @@ -92,6 +91,12 @@ class IndexedDbService { if (!db.objectStoreNames.contains(StoreNames.EMOJI_SET_EVENTS)) { db.createObjectStore(StoreNames.EMOJI_SET_EVENTS, { keyPath: 'key' }) } + if (!db.objectStoreNames.contains(StoreNames.RELAY_INFOS)) { + db.createObjectStore(StoreNames.RELAY_INFOS, { keyPath: 'key' }) + } + if (db.objectStoreNames.contains(StoreNames.RELAY_INFO_EVENTS)) { + db.deleteObjectStore(StoreNames.RELAY_INFO_EVENTS) + } this.db = db } }) @@ -297,58 +302,6 @@ class IndexedDbService { }) } - async getAllRelayInfoEvents(): Promise { - await this.initPromise - return new Promise((resolve, reject) => { - if (!this.db) { - return reject('database not initialized') - } - const transaction = this.db.transaction(StoreNames.RELAY_INFO_EVENTS, 'readonly') - const store = transaction.objectStore(StoreNames.RELAY_INFO_EVENTS) - const request = store.getAll() - - request.onsuccess = () => { - transaction.commit() - resolve( - ((request.result as TValue[]) - ?.map((item) => item.value) - .filter(Boolean) as Event[]) ?? [] - ) - } - - request.onerror = (event) => { - transaction.commit() - reject(event) - } - }) - } - - async putRelayInfoEvent(event: Event): Promise { - await this.initPromise - return new Promise((resolve, reject) => { - if (!this.db) { - return reject('database not initialized') - } - const dValue = event.tags.find(tagNameEquals('d'))?.[1] - if (!dValue) { - return resolve() - } - const transaction = this.db.transaction(StoreNames.RELAY_INFO_EVENTS, 'readwrite') - const store = transaction.objectStore(StoreNames.RELAY_INFO_EVENTS) - - const putRequest = store.put(this.formatValue(dValue, event)) - putRequest.onsuccess = () => { - transaction.commit() - resolve() - } - - putRequest.onerror = (event) => { - transaction.commit() - reject(event) - } - }) - } - async iterateProfileEvents(callback: (event: Event) => Promise): Promise { await this.initPromise if (!this.db) { @@ -424,6 +377,50 @@ class IndexedDbService { }) } + async putRelayInfo(relayInfo: TRelayInfo): Promise { + await this.initPromise + return new Promise((resolve, reject) => { + if (!this.db) { + return reject('database not initialized') + } + const transaction = this.db.transaction(StoreNames.RELAY_INFOS, 'readwrite') + const store = transaction.objectStore(StoreNames.RELAY_INFOS) + + const putRequest = store.put(this.formatValue(relayInfo.url, relayInfo)) + putRequest.onsuccess = () => { + transaction.commit() + resolve() + } + + putRequest.onerror = (event) => { + transaction.commit() + reject(event) + } + }) + } + + async getRelayInfo(url: string): Promise { + await this.initPromise + return new Promise((resolve, reject) => { + if (!this.db) { + return reject('database not initialized') + } + const transaction = this.db.transaction(StoreNames.RELAY_INFOS, 'readonly') + const store = transaction.objectStore(StoreNames.RELAY_INFOS) + const request = store.get(url) + + request.onsuccess = () => { + transaction.commit() + resolve((request.result as TValue)?.value) + } + + request.onerror = (event) => { + transaction.commit() + reject(event) + } + }) + } + private getReplaceableEventKeyFromEvent(event: Event): string { if ( [kinds.Metadata, kinds.Contacts].includes(event.kind) || @@ -491,6 +488,10 @@ class IndexedDbService { { name: StoreNames.BLOSSOM_SERVER_LIST_EVENTS, expirationTimestamp: Date.now() - 1000 * 60 * 60 * 24 // 1 days + }, + { + name: StoreNames.RELAY_INFOS, + expirationTimestamp: Date.now() - 1000 * 60 * 60 * 24 // 1 days } ] const transaction = this.db!.transaction( diff --git a/src/services/relay-info.service.ts b/src/services/relay-info.service.ts index fa4d4ab..2a4091f 100644 --- a/src/services/relay-info.service.ts +++ b/src/services/relay-info.service.ts @@ -1,12 +1,8 @@ -import { MONITOR, MONITOR_RELAYS } from '@/constants' -import { tagNameEquals } from '@/lib/tag' -import { isWebsocketUrl, simplifyUrl } from '@/lib/url' -import { TNip66RelayInfo, TRelayInfo } from '@/types' +import { simplifyUrl } from '@/lib/url' +import indexDb from '@/services/indexed-db.service' +import { TAwesomeRelayCollection, TRelayInfo } from '@/types' import DataLoader from 'dataloader' import FlexSearch from 'flexsearch' -import { Event } from 'nostr-tools' -import client from './client.service' -import indexedDb from './indexed-db.service' class RelayInfoService { static instance: RelayInfoService @@ -14,14 +10,13 @@ class RelayInfoService { public static getInstance(): RelayInfoService { if (!RelayInfoService.instance) { RelayInfoService.instance = new RelayInfoService() - RelayInfoService.instance.init() } return RelayInfoService.instance } private initPromise: Promise | null = null - - private relayInfoMap = new Map() + private awesomeRelayCollections: Promise | null = null + private relayInfoMap = new Map() private relayInfoIndex = new FlexSearch.Index({ tokenize: 'forward', encode: (str) => @@ -32,19 +27,15 @@ class RelayInfoService { .toLocaleLowerCase() .split(/\s+/) }) - private fetchDataloader = new DataLoader( - (urls) => Promise.all(urls.map((url) => this._getRelayInfo(url))), + private fetchDataloader = new DataLoader( + async (urls) => { + const results = await Promise.allSettled(urls.map((url) => this._getRelayInfo(url))) + return results.map((res) => (res.status === 'fulfilled' ? res.value : undefined)) + }, { maxBatchSize: 1 } ) private relayUrlsForRandom: string[] = [] - async init() { - if (!this.initPromise) { - this.initPromise = this.loadRelayInfos() - } - await this.initPromise - } - async search(query: string) { if (this.initPromise) { await this.initPromise @@ -60,9 +51,7 @@ class RelayInfoService { } const result = await this.relayInfoIndex.searchAsync(query) - return result - .map((url) => this.relayInfoMap.get(url as string)) - .filter(Boolean) as TNip66RelayInfo[] + return result.map((url) => this.relayInfoMap.get(url as string)).filter(Boolean) as TRelayInfo[] } async getRelayInfos(urls: string[]) { @@ -82,7 +71,7 @@ class RelayInfoService { await this.initPromise } - const relayInfos: TNip66RelayInfo[] = [] + const relayInfos: TRelayInfo[] = [] while (relayInfos.length < count) { const randomIndex = Math.floor(Math.random() * this.relayUrlsForRandom.length) const url = this.relayUrlsForRandom[randomIndex] @@ -99,146 +88,81 @@ class RelayInfoService { return relayInfos } + async getAwesomeRelayCollections() { + if (this.awesomeRelayCollections) return this.awesomeRelayCollections + + this.awesomeRelayCollections = (async () => { + try { + const res = await fetch( + 'https://raw.githubusercontent.com/CodyTseng/awesome-nostr-relays/master/dist/collections.json' + ) + if (!res.ok) { + throw new Error('Failed to fetch awesome relay collections') + } + const data = (await res.json()) as { collections: TAwesomeRelayCollection[] } + return data.collections + } catch (error) { + console.error('Error fetching awesome relay collections:', error) + return [] + } + })() + + return this.awesomeRelayCollections + } + private async _getRelayInfo(url: string) { const exist = this.relayInfoMap.get(url) - if (exist && (exist.hasNip11 || exist.triedNip11)) { + if (exist) { return exist } - const nip11 = await this.fetchRelayInfoByNip11(url) - const relayInfo = nip11 - ? { - ...nip11, - url, - shortUrl: simplifyUrl(url), - hasNip11: Object.keys(nip11).length > 0, - triedNip11: true - } - : { - url, - shortUrl: simplifyUrl(url), - hasNip11: false, - triedNip11: true - } + const storedRelayInfo = await indexDb.getRelayInfo(url) + if (storedRelayInfo) { + return await this.addRelayInfo(storedRelayInfo) + } + + const nip11 = await this.fetchRelayNip11(url) + const relayInfo = { + ...(nip11 ?? {}), + url, + shortUrl: simplifyUrl(url) + } return await this.addRelayInfo(relayInfo) } - private async fetchRelayInfoByNip11(url: string) { + private async fetchRelayNip11(url: string) { try { + console.log('Fetching NIP-11 for', url) const res = await fetch(url.replace('ws://', 'http://').replace('wss://', 'https://'), { headers: { Accept: 'application/nostr+json' } }) - return res.json() as TRelayInfo + return res.json() as Omit } catch { return undefined } } - private async loadRelayInfos() { - const localRelayInfos = await indexedDb.getAllRelayInfoEvents() - const relayInfos = formatRelayInfoEvents(localRelayInfos) - relayInfos.forEach((relayInfo) => this.addRelayInfo(relayInfo)) - this.relayUrlsForRandom = Array.from(this.relayInfoMap.keys()) - - const loadFromInternet = async (slowFetch: boolean = true) => { - let until: number = Math.round(Date.now() / 1000) - const since = until - 60 * 60 * 48 - - while (until) { - const relayInfoEvents = await client.fetchEvents(MONITOR_RELAYS, { - authors: [MONITOR], - kinds: [30166], - since, - until, - limit: slowFetch ? 100 : 1000 - }) - const events = relayInfoEvents.sort((a, b) => b.created_at - a.created_at) - if (events.length === 0) { - break - } - for (const event of events) { - await indexedDb.putRelayInfoEvent(event) - const relayInfo = formatRelayInfoEvents([event])[0] - await this.addRelayInfo(relayInfo) - } - until = events[events.length - 1].created_at - 1 - if (slowFetch) { - await new Promise((resolve) => setTimeout(resolve, 1000)) - } - } - this.relayUrlsForRandom = Array.from(this.relayInfoMap.keys()) - } - if (localRelayInfos.length === 0) { - await loadFromInternet(false) - } else { - setTimeout(loadFromInternet, 1000 * 20) // 20 seconds - } - } - - private async addRelayInfo(relayInfo: TNip66RelayInfo) { - const oldRelayInfo = this.relayInfoMap.get(relayInfo.url) - const newRelayInfo = oldRelayInfo - ? { - ...oldRelayInfo, - ...relayInfo, - hasNip11: oldRelayInfo.hasNip11 || relayInfo.hasNip11, - triedNip11: oldRelayInfo.triedNip11 || relayInfo.triedNip11 - } - : relayInfo - - if (!Array.isArray(newRelayInfo.supported_nips)) { - newRelayInfo.supported_nips = [] + private async addRelayInfo(relayInfo: TRelayInfo) { + if (!Array.isArray(relayInfo.supported_nips)) { + relayInfo.supported_nips = [] } - this.relayInfoMap.set(newRelayInfo.url, newRelayInfo) - await this.relayInfoIndex.addAsync( - newRelayInfo.url, - [ - newRelayInfo.shortUrl, - ...newRelayInfo.shortUrl.split('.'), - newRelayInfo.name ?? '', - newRelayInfo.description ?? '' - ].join(' ') - ) - return newRelayInfo + this.relayInfoMap.set(relayInfo.url, relayInfo) + await Promise.allSettled([ + this.relayInfoIndex.addAsync( + relayInfo.url, + [ + relayInfo.shortUrl, + ...relayInfo.shortUrl.split('.'), + relayInfo.name ?? '', + relayInfo.description ?? '' + ].join(' ') + ), + indexDb.putRelayInfo(relayInfo) + ]) + return relayInfo } } const instance = RelayInfoService.getInstance() export default instance - -function formatRelayInfoEvents(relayInfoEvents: Event[]) { - const urlSet = new Set() - const relayInfos: TNip66RelayInfo[] = [] - relayInfoEvents.forEach((event) => { - try { - const url = event.tags.find(tagNameEquals('d'))?.[1] - if (!url || urlSet.has(url) || !isWebsocketUrl(url)) { - return - } - - urlSet.add(url) - const basicInfo = event.content ? (JSON.parse(event.content) as TRelayInfo) : {} - const tagInfo: Omit = { - hasNip11: Object.keys(basicInfo).length > 0, - triedNip11: false - } - event.tags.forEach((tag) => { - if (tag[0] === 'T') { - tagInfo.relayType = tag[1] - } else if (tag[0] === 'g' && tag[2] === 'countryCode') { - tagInfo.countryCode = tag[1] - } - }) - relayInfos.push({ - ...basicInfo, - ...tagInfo, - url, - shortUrl: simplifyUrl(url) - }) - } catch (error) { - console.error(error) - } - }) - return relayInfos -} diff --git a/src/types/index.d.ts b/src/types/index.d.ts index 78f5940..42814a0 100644 --- a/src/types/index.d.ts +++ b/src/types/index.d.ts @@ -35,6 +35,8 @@ export type TRelayList = { } export type TRelayInfo = { + url: string + shortUrl: string name?: string description?: string icon?: string @@ -127,15 +129,6 @@ export type TNotificationType = 'all' | 'mentions' | 'reactions' | 'zaps' export type TPageRef = { scrollToTop: (behavior?: ScrollBehavior) => void } -export type TNip66RelayInfo = TRelayInfo & { - url: string - shortUrl: string - hasNip11: boolean - triedNip11: boolean - relayType?: string - countryCode?: string -} - export type TEmoji = { shortcode: string url: string @@ -185,3 +178,10 @@ export type TSearchParams = { export type TNotificationStyle = (typeof NOTIFICATION_LIST_STYLE)[keyof typeof NOTIFICATION_LIST_STYLE] + +export type TAwesomeRelayCollection = { + id: string + name: string + description: string + relays: string[] +}