From c62a82f6731a395360f395da318c7f98e9d4095c Mon Sep 17 00:00:00 2001 From: codytseng Date: Mon, 13 Jan 2025 23:22:19 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=F0=9F=90=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/FeedSwitcher/index.tsx | 3 +- src/components/MailboxSetting/SaveButton.tsx | 11 +- src/constants.ts | 6 +- src/lib/event.ts | 58 ++++++- src/providers/FeedProvider.tsx | 44 ++--- src/providers/FollowListProvider.tsx | 8 +- src/providers/NostrProvider/index.tsx | 114 ++++++------- src/services/client.service.ts | 163 +++++++------------ src/services/storage.service.ts | 120 +++++++++----- 9 files changed, 283 insertions(+), 244 deletions(-) diff --git a/src/components/FeedSwitcher/index.tsx b/src/components/FeedSwitcher/index.tsx index ec44dca6..dbd4842f 100644 --- a/src/components/FeedSwitcher/index.tsx +++ b/src/components/FeedSwitcher/index.tsx @@ -21,7 +21,8 @@ export default function FeedSwitcher({ close }: { close?: () => void }) { itemName={t('Following')} isActive={feedType === 'following'} onClick={() => { - switchFeed('following') + if (!pubkey) return + switchFeed('following', { pubkey }) close?.() }} /> diff --git a/src/components/MailboxSetting/SaveButton.tsx b/src/components/MailboxSetting/SaveButton.tsx index eab77a04..1d87c8e8 100644 --- a/src/components/MailboxSetting/SaveButton.tsx +++ b/src/components/MailboxSetting/SaveButton.tsx @@ -17,10 +17,12 @@ export default function SaveButton({ setHasChange: (hasChange: boolean) => void }) { const { toast } = useToast() - const { pubkey, publish, updateRelayList } = useNostr() + const { pubkey, publish, updateRelayListEvent } = useNostr() const [pushing, setPushing] = useState(false) const save = async () => { + if (!pubkey) return + setPushing(true) const event = { kind: kinds.RelayList, @@ -30,11 +32,8 @@ export default function SaveButton({ ), created_at: dayjs().unix() } - await publish(event) - updateRelayList({ - write: mailboxRelays.filter(({ scope }) => scope !== 'read').map(({ url }) => url), - read: mailboxRelays.filter(({ scope }) => scope !== 'write').map(({ url }) => url) - }) + const relayListEvent = await publish(event) + updateRelayListEvent(relayListEvent) toast({ title: 'Save Successful', description: 'Successfully saved mailbox relays' diff --git a/src/constants.ts b/src/constants.ts index 408e8197..af2b7c0b 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -5,9 +5,9 @@ export const StorageKey = { FEED_TYPE: 'feedType', ACCOUNTS: 'accounts', CURRENT_ACCOUNT: 'currentAccount', - ACCOUNT_RELAY_LIST_MAP: 'accountRelayListMap', - ACCOUNT_FOLLOWINGS_MAP: 'accountFollowingsMap', - ACCOUNT_PROFILE_MAP: 'accountProfileMap', + ACCOUNT_RELAY_LIST_EVENT_MAP: 'accountRelayListEventMap', + ACCOUNT_FOLLOW_LIST_EVENT_MAP: 'accountFollowListEventMap', + ACCOUNT_PROFILE_EVENT_MAP: 'accountProfileEventMap', ADD_CLIENT_TAG: 'addClientTag' } diff --git a/src/lib/event.ts b/src/lib/event.ts index c1fce642..e7fb50b4 100644 --- a/src/lib/event.ts +++ b/src/lib/event.ts @@ -1,7 +1,10 @@ -import { COMMENT_EVENT_KIND, PICTURE_EVENT_KIND } from '@/constants' +import { BIG_RELAY_URLS, COMMENT_EVENT_KIND, PICTURE_EVENT_KIND } from '@/constants' import client from '@/services/client.service' +import { TRelayList } from '@/types' import { Event, kinds, nip19 } from 'nostr-tools' import { extractImageInfoFromTag, isReplyETag, isRootETag, tagNameEquals } from './tag' +import { isWebsocketUrl, normalizeUrl } from './url' +import { formatPubkey } from './pubkey' export function isNsfwEvent(event: Event) { return event.tags.some( @@ -76,6 +79,59 @@ export function getFollowingsFromFollowListEvent(event: Event) { ) } +export function getRelayListFromRelayListEvent(event?: Event) { + if (!event) { + return { write: BIG_RELAY_URLS, read: BIG_RELAY_URLS } + } + + const relayList = { write: [], read: [] } as TRelayList + 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.length ? relayList.write.slice(0, 10) : BIG_RELAY_URLS, + read: relayList.read.length ? relayList.read.slice(0, 10) : BIG_RELAY_URLS + } +} + +export function getProfileFromProfileEvent(event: Event) { + try { + const profileObj = JSON.parse(event.content) + return { + pubkey: event.pubkey, + banner: profileObj.banner, + avatar: profileObj.picture, + username: + profileObj.display_name?.trim() || + profileObj.name?.trim() || + profileObj.nip05?.split('@')[0]?.trim() || + formatPubkey(event.pubkey), + nip05: profileObj.nip05, + about: profileObj.about, + created_at: event.created_at + } + } catch (err) { + console.error(err) + return { + pubkey: event.pubkey, + username: formatPubkey(event.pubkey) + } + } +} + export async function extractMentions(content: string, parentEvent?: Event) { const pubkeySet = new Set() const relatedEventIdSet = new Set() diff --git a/src/providers/FeedProvider.tsx b/src/providers/FeedProvider.tsx index 552d1e93..c406febd 100644 --- a/src/providers/FeedProvider.tsx +++ b/src/providers/FeedProvider.tsx @@ -14,7 +14,10 @@ type TFeedContext = { filter: Filter isReady: boolean activeRelaySetId: string | null - switchFeed: (feedType: TFeedType, options?: { activeRelaySetId?: string }) => Promise + switchFeed: ( + feedType: TFeedType, + options?: { activeRelaySetId?: string; pubkey?: string } + ) => Promise } const FeedContext = createContext(undefined) @@ -52,32 +55,28 @@ export function FeedProvider({ children }: { children: React.ReactNode }) { } if (feedType === 'following') { - return await switchFeed('following') + return await switchFeed('following', { pubkey }) + } else { + await switchFeed('relays', { activeRelaySetId }) } - await switchFeed('relays', { activeRelaySetId }) } init() }, []) useEffect(() => { - if (!isReady || feedType !== 'following') return - - switchFeed('following') - }, [pubkey, feedType, isReady]) - - useEffect(() => { - if (feedType !== 'relays') return - - const relaySet = relaySets.find((set) => set.id === activeRelaySetId) - if (!relaySet) return - - setRelayUrls(relaySet.relayUrls) - }, [relaySets]) + if (pubkey && feedType === 'following') { + switchFeed('following', { pubkey }) + } + }, [feedType, pubkey]) const switchFeed = async ( feedType: TFeedType, - options: { activeRelaySetId?: string | null; temporaryRelayUrls?: string[] | null } = {} + options: { + activeRelaySetId?: string | null + temporaryRelayUrls?: string[] | null + pubkey?: string | null + } = {} ) => { setIsReady(false) if (feedType === 'relays') { @@ -100,14 +99,19 @@ export function FeedProvider({ children }: { children: React.ReactNode }) { return setIsReady(true) } if (feedType === 'following') { - if (!pubkey) { + if (!options.pubkey) { return setIsReady(true) } setFeedType(feedType) setActiveRelaySetId(null) - const [relayList, followings] = await Promise.all([getRelayList(), getFollowings()]) + const [relayList, followings] = await Promise.all([ + getRelayList(options.pubkey), + getFollowings(options.pubkey) + ]) setRelayUrls(relayList.read.concat(BIG_RELAY_URLS).slice(0, 4)) - setFilter({ authors: followings.includes(pubkey) ? followings : [...followings, pubkey] }) + setFilter({ + authors: followings.includes(options.pubkey) ? followings : [...followings, options.pubkey] + }) storage.setFeedType(feedType) return setIsReady(true) } diff --git a/src/providers/FollowListProvider.tsx b/src/providers/FollowListProvider.tsx index eed4357f..12351d64 100644 --- a/src/providers/FollowListProvider.tsx +++ b/src/providers/FollowListProvider.tsx @@ -1,6 +1,6 @@ -import { TDraftEvent } from '@/types' import { tagNameEquals } from '@/lib/tag' import client from '@/services/client.service' +import { TDraftEvent } from '@/types' import dayjs from 'dayjs' import { Event, kinds } from 'nostr-tools' import { createContext, useContext, useEffect, useMemo, useState } from 'react' @@ -25,7 +25,7 @@ export const useFollowList = () => { } export function FollowListProvider({ children }: { children: React.ReactNode }) { - const { pubkey: accountPubkey, publish, updateFollowings } = useNostr() + const { pubkey: accountPubkey, publish, updateFollowListEvent } = useNostr() const [followListEvent, setFollowListEvent] = useState(undefined) const [isFetching, setIsFetching] = useState(true) const followings = useMemo(() => { @@ -65,7 +65,7 @@ export function FollowListProvider({ children }: { children: React.ReactNode }) } const newFollowListEvent = await publish(newFollowListDraftEvent) client.updateFollowListCache(accountPubkey, newFollowListEvent) - updateFollowings([...followings, pubkey]) + updateFollowListEvent(newFollowListEvent) setFollowListEvent(newFollowListEvent) } @@ -82,7 +82,7 @@ export function FollowListProvider({ children }: { children: React.ReactNode }) } const newFollowListEvent = await publish(newFollowListDraftEvent) client.updateFollowListCache(accountPubkey, newFollowListEvent) - updateFollowings(followings.filter((followPubkey) => followPubkey !== pubkey)) + updateFollowListEvent(newFollowListEvent) setFollowListEvent(newFollowListEvent) } diff --git a/src/providers/NostrProvider/index.tsx b/src/providers/NostrProvider/index.tsx index 96300132..ecef7607 100644 --- a/src/providers/NostrProvider/index.tsx +++ b/src/providers/NostrProvider/index.tsx @@ -1,6 +1,10 @@ import LoginDialog from '@/components/LoginDialog' -import { BIG_RELAY_URLS } from '@/constants' import { useToast } from '@/hooks' +import { + getFollowingsFromFollowListEvent, + getProfileFromProfileEvent, + getRelayListFromRelayListEvent +} from '@/lib/event' import client from '@/services/client.service' import storage from '@/services/storage.service' import { ISigner, TAccount, TAccountPointer, TDraftEvent, TProfile, TRelayList } from '@/types' @@ -30,10 +34,10 @@ type TNostrContext = { signHttpAuth: (url: string, method: string) => Promise signEvent: (draftEvent: TDraftEvent) => Promise checkLogin: (cb?: () => T) => Promise - getRelayList: () => Promise - updateRelayList: (relayList: TRelayList) => void - getFollowings: () => Promise - updateFollowings: (followings: string[]) => void + getRelayList: (pubkey: string) => Promise + updateRelayListEvent: (relayListEvent: Event) => void + getFollowings: (pubkey: string) => Promise + updateFollowListEvent: (followListEvent: Event) => void } const NostrContext = createContext(undefined) @@ -69,34 +73,42 @@ export function NostrProvider({ children }: { children: React.ReactNode }) { useEffect(() => { if (!account) { setRelayList(null) + setFollowings(null) + setProfile(null) return } - const storedRelayList = storage.getAccountRelayList(account.pubkey) - if (storedRelayList) { - setRelayList(storedRelayList) + const storedRelayListEvent = storage.getAccountRelayListEvent(account.pubkey) + if (storedRelayListEvent) { + setRelayList( + storedRelayListEvent ? getRelayListFromRelayListEvent(storedRelayListEvent) : null + ) } - const followings = storage.getAccountFollowings(account.pubkey) - if (followings) { - setFollowings(followings) + const followListEvent = storage.getAccountFollowListEvent(account.pubkey) + if (followListEvent) { + setFollowings(getFollowingsFromFollowListEvent(followListEvent)) } - const profile = storage.getAccountProfile(account.pubkey) - if (profile) { - setProfile(profile) + const profileEvent = storage.getAccountProfileEvent(account.pubkey) + if (profileEvent) { + setProfile(getProfileFromProfileEvent(profileEvent)) } - client.fetchRelayList(account.pubkey).then((relayList) => { - setRelayList(relayList) - storage.setAccountRelayList(account.pubkey, relayList) + client.fetchRelayListEvent(account.pubkey).then((relayListEvent) => { + if (!relayListEvent) return + const isNew = storage.setAccountRelayListEvent(relayListEvent) + if (!isNew) return + setRelayList(getRelayListFromRelayListEvent(relayListEvent)) }) - client.fetchFollowings(account.pubkey).then((followings) => { - setFollowings(followings) - storage.setAccountFollowings(account.pubkey, followings) + client.fetchFollowListEvent(account.pubkey).then((followListEvent) => { + if (!followListEvent) return + const isNew = storage.setAccountFollowListEvent(followListEvent) + if (!isNew) return + setFollowings(getFollowingsFromFollowListEvent(followListEvent)) }) - client.fetchProfile(account.pubkey).then((profile) => { - if (profile) { - setProfile(profile) - storage.setAccountProfile(account.pubkey, profile) - } + client.fetchProfileEvent(account.pubkey).then((profileEvent) => { + if (!profileEvent) return + const isNew = storage.setAccountProfileEvent(profileEvent) + if (!isNew) return + setProfile(getProfileFromProfileEvent(profileEvent)) }) }, [account]) @@ -240,44 +252,32 @@ export function NostrProvider({ children }: { children: React.ReactNode }) { return setOpenLoginDialog(true) } - const getRelayList = async () => { - if (!account) { - return { write: BIG_RELAY_URLS, read: BIG_RELAY_URLS } + const getRelayList = async (pubkey: string) => { + const storedRelayListEvent = storage.getAccountRelayListEvent(pubkey) + if (storedRelayListEvent) { + return getRelayListFromRelayListEvent(storedRelayListEvent) } - - const storedRelayList = storage.getAccountRelayList(account.pubkey) - if (storedRelayList) { - return storedRelayList - } - return await client.fetchRelayList(account.pubkey) + return await client.fetchRelayList(pubkey) } - const updateRelayList = (relayList: TRelayList) => { - if (!account) { - return - } - setRelayList(relayList) - storage.setAccountRelayList(account.pubkey, relayList) + const updateRelayListEvent = (relayListEvent: Event) => { + const isNew = storage.setAccountRelayListEvent(relayListEvent) + if (!isNew) return + setRelayList(getRelayListFromRelayListEvent(relayListEvent)) } - const getFollowings = async () => { - if (!account) { - return [] + const getFollowings = async (pubkey: string) => { + const followListEvent = storage.getAccountFollowListEvent(pubkey) + if (followListEvent) { + return getFollowingsFromFollowListEvent(followListEvent) } - - const followings = storage.getAccountFollowings(account.pubkey) - if (followings) { - return followings - } - return await client.fetchFollowings(account.pubkey) + return await client.fetchFollowings(pubkey) } - const updateFollowings = (followings: string[]) => { - if (!account) { - return - } - setFollowings(followings) - storage.setAccountFollowings(account.pubkey, followings) + const updateFollowListEvent = (followListEvent: Event) => { + const isNew = storage.setAccountFollowListEvent(followListEvent) + if (!isNew) return + setFollowings(getFollowingsFromFollowListEvent(followListEvent)) } return ( @@ -301,9 +301,9 @@ export function NostrProvider({ children }: { children: React.ReactNode }) { checkLogin, signEvent, getRelayList, - updateRelayList, + updateRelayListEvent, getFollowings, - updateFollowings + updateFollowListEvent }} > {children} diff --git a/src/services/client.service.ts b/src/services/client.service.ts index d512acf1..395a6732 100644 --- a/src/services/client.service.ts +++ b/src/services/client.service.ts @@ -1,8 +1,10 @@ import { BIG_RELAY_URLS } from '@/constants' -import { getFollowingsFromFollowListEvent } from '@/lib/event' -import { formatPubkey } from '@/lib/pubkey' -import { tagNameEquals } from '@/lib/tag' -import { isWebsocketUrl, normalizeUrl } from '@/lib/url' +import { + getFollowingsFromFollowListEvent, + getProfileFromProfileEvent, + getRelayListFromRelayListEvent +} from '@/lib/event' +import { userIdToPubkey } from '@/lib/pubkey' import { TDraftEvent, TProfile, TRelayInfo, TRelayList } from '@/types' import { sha256 } from '@noble/hashes/sha2' import DataLoader from 'dataloader' @@ -43,19 +45,19 @@ class ClientService extends EventTarget { this.eventBatchLoadFn.bind(this), { cache: false } ) - private profileCache = new LRUCache>({ max: 10000 }) - private profileDataloader = new DataLoader( - (ids) => Promise.all(ids.map((id) => this._fetchProfile(id))), - { cacheMap: this.profileCache } + private profileEventCache = new LRUCache>({ max: 10000 }) + private profileEventDataloader = new DataLoader( + (ids) => Promise.all(ids.map((id) => this._fetchProfileEvent(id))), + { cacheMap: this.profileEventCache } ) - private fetchProfileFromDefaultRelaysDataloader = new DataLoader( - this.profileBatchLoadFn.bind(this), + private fetchProfileEventFromDefaultRelaysDataloader = new DataLoader( + this.profileEventBatchLoadFn.bind(this), { cache: false } ) - private relayListDataLoader = new DataLoader( - this.relayListBatchLoadFn.bind(this), + private relayListEventDataLoader = new DataLoader( + this.relayListEventBatchLoadFn.bind(this), { - cacheMap: new LRUCache>({ max: 10000 }) + cacheMap: new LRUCache>({ max: 10000 }) } ) private relayInfoDataLoader = new DataLoader(async (urls) => { @@ -373,30 +375,28 @@ class ClientService extends EventTarget { this.eventDataLoader.prime(event.id, Promise.resolve(event)) } - async fetchProfile(id: string): Promise { - if (!/^[0-9a-f]{64}$/.test(id)) { - let pubkey: string | undefined - const { data, type } = nip19.decode(id) - switch (type) { - case 'npub': - pubkey = data - break - case 'nprofile': - pubkey = data.pubkey - break - } - - if (!pubkey) { - throw new Error('Invalid id') - } - - const cache = await this.profileCache.get(pubkey) - if (cache) { - return cache - } + async fetchProfileEvent(id: string): Promise { + const pubkey = userIdToPubkey(id) + const cache = await this.profileEventCache.get(pubkey) + if (cache) { + return cache } - return this.profileDataloader.load(id) + return await this.profileEventDataloader.load(id) + } + + async fetchProfile(id: string): Promise { + const profileEvent = await this.fetchProfileEvent(id) + if (profileEvent) { + return getProfileFromProfileEvent(profileEvent) + } + + try { + const pubkey = userIdToPubkey(id) + return { pubkey, username: pubkey } + } catch { + return undefined + } } async fetchProfiles(relayUrls: string[], filter: Filter): Promise { @@ -405,15 +405,21 @@ class ClientService extends EventTarget { kinds: [kinds.Metadata] }) - const profiles = events - .sort((a, b) => b.created_at - a.created_at) - .map((event) => this.parseProfileFromEvent(event)) - profiles.forEach((profile) => this.profileDataloader.prime(profile.pubkey, profile)) - return profiles + const profileEvents = events.sort((a, b) => b.created_at - a.created_at) + profileEvents.forEach((profile) => this.profileEventDataloader.prime(profile.pubkey, profile)) + return profileEvents.map((profileEvent) => getProfileFromProfileEvent(profileEvent)) + } + + async fetchRelayListEvent(pubkey: string) { + return this.relayListEventDataLoader.load(pubkey) } async fetchRelayList(pubkey: string): Promise { - return this.relayListDataLoader.load(pubkey) + const event = await this.relayListEventDataLoader.load(pubkey) + if (!event) { + return { write: BIG_RELAY_URLS, read: BIG_RELAY_URLS } + } + return getRelayListFromRelayListEvent(event) } async fetchFollowListEvent(pubkey: string) { @@ -488,7 +494,7 @@ class ClientService extends EventTarget { return event } - private async _fetchProfile(id: string): Promise { + private async _fetchProfileEvent(id: string): Promise { let pubkey: string | undefined let relays: string[] = [] if (/^[0-9a-f]{64}$/.test(id)) { @@ -509,8 +515,8 @@ class ClientService extends EventTarget { if (!pubkey) { throw new Error('Invalid id') } - - const profileFromDefaultRelays = await this.fetchProfileFromDefaultRelaysDataloader.load(pubkey) + const profileFromDefaultRelays = + await this.fetchProfileEventFromDefaultRelaysDataloader.load(pubkey) if (profileFromDefaultRelays) { return profileFromDefaultRelays } @@ -524,15 +530,11 @@ class ClientService extends EventTarget { }, true ) - const profile = profileEvent - ? this.parseProfileFromEvent(profileEvent) - : { pubkey, username: formatPubkey(pubkey) } - if (pubkey !== id) { - this.profileDataloader.prime(pubkey, Promise.resolve(profile)) + this.profileEventDataloader.prime(pubkey, Promise.resolve(profileEvent)) } - return profile + return profileEvent } private async tryHarderToFetchEvent( @@ -567,7 +569,7 @@ class ClientService extends EventTarget { return ids.map((id) => eventsMap.get(id)) } - private async profileBatchLoadFn(pubkeys: readonly string[]) { + private async profileEventBatchLoadFn(pubkeys: readonly string[]) { const events = await this.pool.querySync(this.defaultRelayUrls, { authors: Array.from(new Set(pubkeys)), kinds: [kinds.Metadata], @@ -583,12 +585,11 @@ class ClientService extends EventTarget { } return pubkeys.map((pubkey) => { - const event = eventsMap.get(pubkey) - return event ? this.parseProfileFromEvent(event) : undefined + return eventsMap.get(pubkey) }) } - private async relayListBatchLoadFn(pubkeys: readonly string[]) { + private async relayListEventBatchLoadFn(pubkeys: readonly string[]) { const events = await this.pool.querySync(this.defaultRelayUrls, { authors: pubkeys as string[], kinds: [kinds.RelayList], @@ -603,34 +604,7 @@ class ClientService extends EventTarget { } } - return pubkeys.map((pubkey) => { - const event = eventsMap.get(pubkey) - const relayList = { write: [], read: [] } as TRelayList - if (!event) { - return { write: BIG_RELAY_URLS, read: BIG_RELAY_URLS } - } - - 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.length ? relayList.write.slice(0, 10) : BIG_RELAY_URLS, - read: relayList.read.length ? relayList.read.slice(0, 10) : BIG_RELAY_URLS - } - }) + return pubkeys.map((pubkey) => eventsMap.get(pubkey)) } private async _fetchFollowListEvent(pubkey: string) { @@ -645,31 +619,6 @@ class ClientService extends EventTarget { return followListEvents.sort((a, b) => b.created_at - a.created_at)[0] } - - private parseProfileFromEvent(event: NEvent): TProfile { - try { - const profileObj = JSON.parse(event.content) - return { - pubkey: event.pubkey, - banner: profileObj.banner, - avatar: profileObj.picture, - username: - profileObj.display_name?.trim() || - profileObj.name?.trim() || - profileObj.nip05?.split('@')[0]?.trim() || - formatPubkey(event.pubkey), - nip05: profileObj.nip05, - about: profileObj.about, - created_at: event.created_at - } - } catch (err) { - console.error(err) - return { - pubkey: event.pubkey, - username: formatPubkey(event.pubkey) - } - } - } } const instance = ClientService.getInstance() diff --git a/src/services/storage.service.ts b/src/services/storage.service.ts index bf7c9961..2f322e6f 100644 --- a/src/services/storage.service.ts +++ b/src/services/storage.service.ts @@ -1,15 +1,8 @@ import { StorageKey } from '@/constants' import { isSameAccount } from '@/lib/account' import { randomString } from '@/lib/random' -import { - TAccount, - TAccountPointer, - TFeedType, - TProfile, - TRelayList, - TRelaySet, - TThemeSetting -} from '@/types' +import { TAccount, TAccountPointer, TFeedType, TRelaySet, TThemeSetting } from '@/types' +import { Event } from 'nostr-tools' const DEFAULT_RELAY_SETS: TRelaySet[] = [ { @@ -33,9 +26,9 @@ class StorageService { private themeSetting: TThemeSetting = 'system' private accounts: TAccount[] = [] private currentAccount: TAccount | null = null - private accountRelayListMap: Record = {} // pubkey -> relayList - private accountFollowingsMap: Record = {} // pubkey -> followings - private accountProfileMap: Record = {} // pubkey -> profile + private accountRelayListEventMap: Record = {} // pubkey -> relayListEvent + private accountFollowListEventMap: Record = {} // pubkey -> followListEvent + private accountProfileEventMap: Record = {} // pubkey -> profileEvent constructor() { if (!StorageService.instance) { @@ -54,12 +47,25 @@ class StorageService { this.currentAccount = currentAccountStr ? JSON.parse(currentAccountStr) : null const feedTypeStr = window.localStorage.getItem(StorageKey.FEED_TYPE) this.feedType = feedTypeStr ? JSON.parse(feedTypeStr) : 'relays' - const accountRelayListMapStr = window.localStorage.getItem(StorageKey.ACCOUNT_RELAY_LIST_MAP) - this.accountRelayListMap = accountRelayListMapStr ? JSON.parse(accountRelayListMapStr) : {} - const accountFollowingsMapStr = window.localStorage.getItem(StorageKey.ACCOUNT_FOLLOWINGS_MAP) - this.accountFollowingsMap = accountFollowingsMapStr ? JSON.parse(accountFollowingsMapStr) : {} - const accountProfileMapStr = window.localStorage.getItem(StorageKey.ACCOUNT_PROFILE_MAP) - this.accountProfileMap = accountProfileMapStr ? JSON.parse(accountProfileMapStr) : {} + + const accountRelayListEventMapStr = window.localStorage.getItem( + StorageKey.ACCOUNT_RELAY_LIST_EVENT_MAP + ) + this.accountRelayListEventMap = accountRelayListEventMapStr + ? JSON.parse(accountRelayListEventMapStr) + : {} + const accountFollowListEventMapStr = window.localStorage.getItem( + StorageKey.ACCOUNT_FOLLOW_LIST_EVENT_MAP + ) + this.accountFollowListEventMap = accountFollowListEventMapStr + ? JSON.parse(accountFollowListEventMapStr) + : {} + const accountProfileEventMapStr = window.localStorage.getItem( + StorageKey.ACCOUNT_PROFILE_EVENT_MAP + ) + this.accountProfileEventMap = accountProfileEventMapStr + ? JSON.parse(accountProfileEventMapStr) + : {} const relaySetsStr = window.localStorage.getItem(StorageKey.RELAY_SETS) if (!relaySetsStr) { @@ -155,21 +161,21 @@ class StorageService { removeAccount(account: TAccount) { this.accounts = this.accounts.filter((act) => !isSameAccount(act, account)) - delete this.accountFollowingsMap[account.pubkey] - delete this.accountRelayListMap[account.pubkey] - delete this.accountProfileMap[account.pubkey] + delete this.accountFollowListEventMap[account.pubkey] + delete this.accountRelayListEventMap[account.pubkey] + delete this.accountProfileEventMap[account.pubkey] window.localStorage.setItem(StorageKey.ACCOUNTS, JSON.stringify(this.accounts)) window.localStorage.setItem( - StorageKey.ACCOUNT_FOLLOWINGS_MAP, - JSON.stringify(this.accountFollowingsMap) + StorageKey.ACCOUNT_FOLLOW_LIST_EVENT_MAP, + JSON.stringify(this.accountFollowListEventMap) ) window.localStorage.setItem( - StorageKey.ACCOUNT_RELAY_LIST_MAP, - JSON.stringify(this.accountRelayListMap) + StorageKey.ACCOUNT_RELAY_LIST_EVENT_MAP, + JSON.stringify(this.accountRelayListEventMap) ) window.localStorage.setItem( - StorageKey.ACCOUNT_PROFILE_MAP, - JSON.stringify(this.accountProfileMap) + StorageKey.ACCOUNT_PROFILE_EVENT_MAP, + JSON.stringify(this.accountProfileEventMap) ) } @@ -185,40 +191,64 @@ class StorageService { window.localStorage.setItem(StorageKey.CURRENT_ACCOUNT, JSON.stringify(act)) } - getAccountRelayList(pubkey: string) { - return this.accountRelayListMap[pubkey] + getAccountRelayListEvent(pubkey: string) { + return this.accountRelayListEventMap[pubkey] } - setAccountRelayList(pubkey: string, relayList: TRelayList) { - this.accountRelayListMap[pubkey] = relayList + setAccountRelayListEvent(relayListEvent: Event) { + const pubkey = relayListEvent.pubkey + if ( + this.accountRelayListEventMap[pubkey] && + this.accountRelayListEventMap[pubkey].created_at > relayListEvent.created_at + ) { + return false + } + this.accountRelayListEventMap[pubkey] = relayListEvent window.localStorage.setItem( - StorageKey.ACCOUNT_RELAY_LIST_MAP, - JSON.stringify(this.accountRelayListMap) + StorageKey.ACCOUNT_RELAY_LIST_EVENT_MAP, + JSON.stringify(this.accountRelayListEventMap) ) + return true } - getAccountFollowings(pubkey: string) { - return this.accountFollowingsMap[pubkey] + getAccountFollowListEvent(pubkey: string) { + return this.accountFollowListEventMap[pubkey] } - setAccountFollowings(pubkey: string, followings: string[]) { - this.accountFollowingsMap[pubkey] = followings + setAccountFollowListEvent(followListEvent: Event) { + const pubkey = followListEvent.pubkey + if ( + this.accountFollowListEventMap[pubkey] && + this.accountFollowListEventMap[pubkey].created_at > followListEvent.created_at + ) { + return false + } + this.accountFollowListEventMap[pubkey] = followListEvent window.localStorage.setItem( - StorageKey.ACCOUNT_FOLLOWINGS_MAP, - JSON.stringify(this.accountFollowingsMap) + StorageKey.ACCOUNT_FOLLOW_LIST_EVENT_MAP, + JSON.stringify(this.accountFollowListEventMap) ) + return true } - getAccountProfile(pubkey: string) { - return this.accountProfileMap[pubkey] + getAccountProfileEvent(pubkey: string) { + return this.accountProfileEventMap[pubkey] } - setAccountProfile(pubkey: string, profile: TProfile) { - this.accountProfileMap[pubkey] = profile + setAccountProfileEvent(profileEvent: Event) { + const pubkey = profileEvent.pubkey + if ( + this.accountProfileEventMap[pubkey] && + this.accountProfileEventMap[pubkey].created_at > profileEvent.created_at + ) { + return false + } + this.accountProfileEventMap[pubkey] = profileEvent window.localStorage.setItem( - StorageKey.ACCOUNT_PROFILE_MAP, - JSON.stringify(this.accountProfileMap) + StorageKey.ACCOUNT_PROFILE_EVENT_MAP, + JSON.stringify(this.accountProfileEventMap) ) + return true } }