From ec19b9cbfe050bc659a92d672edc6e434cb53974 Mon Sep 17 00:00:00 2001 From: codytseng Date: Fri, 7 Feb 2025 22:56:00 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=F0=9F=92=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/NoteList/index.tsx | 7 +- src/lib/event.ts | 5 +- src/providers/NostrProvider/index.tsx | 15 +- src/providers/NostrProvider/nsec.signer.ts | 12 +- src/services/client.service.ts | 153 ++++++++++++++------- src/types.ts | 10 +- 6 files changed, 130 insertions(+), 72 deletions(-) diff --git a/src/components/NoteList/index.tsx b/src/components/NoteList/index.tsx index da561c65..291af05e 100644 --- a/src/components/NoteList/index.tsx +++ b/src/components/NoteList/index.tsx @@ -35,7 +35,7 @@ export default function NoteList({ }) { const { t } = useTranslation() const { isLargeScreen } = useScreenSize() - const { signEvent, checkLogin } = useNostr() + const { startLogin } = useNostr() const { mutePubkeys } = useMuteList() const [refreshCount, setRefreshCount] = useState(0) const [timelineKey, setTimelineKey] = useState(undefined) @@ -102,10 +102,7 @@ export default function NoteList({ } }, { - signer: async (evt) => { - const signedEvt = await checkLogin(() => signEvent(evt)) - return signedEvt ?? null - }, + startLogin, needSort: !areAlgoRelays } ) diff --git a/src/lib/event.ts b/src/lib/event.ts index e0247c41..dfa4989e 100644 --- a/src/lib/event.ts +++ b/src/lib/event.ts @@ -65,11 +65,12 @@ export function getEventCoordinate(event: Event) { } export function getSharableEventId(event: Event) { + const hints = client.getEventHints(event.id).slice(0, 3) if (isReplaceable(event.kind)) { const identifier = event.tags.find(tagNameEquals('d'))?.[1] ?? '' - return nip19.naddrEncode({ pubkey: event.pubkey, kind: event.kind, identifier }) + return nip19.naddrEncode({ pubkey: event.pubkey, kind: event.kind, identifier, relays: hints }) } - return nip19.neventEncode({ id: event.id, author: event.pubkey, kind: event.kind }) + return nip19.neventEncode({ id: event.id, author: event.pubkey, kind: event.kind, relays: hints }) } export function getUsingClient(event: Event) { diff --git a/src/providers/NostrProvider/index.tsx b/src/providers/NostrProvider/index.tsx index b0df2deb..144adb5f 100644 --- a/src/providers/NostrProvider/index.tsx +++ b/src/providers/NostrProvider/index.tsx @@ -39,9 +39,10 @@ type TNostrContext = { options?: { additionalRelayUrls?: string[]; specifiedRelayUrls?: string[] } ) => Promise signHttpAuth: (url: string, method: string) => Promise - signEvent: (draftEvent: TDraftEvent) => Promise + signEvent: (draftEvent: TDraftEvent) => Promise nip04Encrypt: (pubkey: string, plainText: string) => Promise nip04Decrypt: (pubkey: string, cipherText: string) => Promise + startLogin: () => void checkLogin: (cb?: () => T) => Promise getRelayList: (pubkey: string) => Promise updateRelayListEvent: (relayListEvent: Event) => void @@ -158,6 +159,14 @@ export function NostrProvider({ children }: { children: React.ReactNode }) { client.initUserIndexFromFollowings(account.pubkey) }, [account]) + useEffect(() => { + if (signer) { + client.signer = signer.signEvent.bind(signer) + } else { + client.signer = undefined + } + }, [signer]) + const hasNostrLoginHash = () => { return window.location.hash && window.location.hash.startsWith('#nostr-login') } @@ -336,8 +345,7 @@ export function NostrProvider({ children }: { children: React.ReactNode }) { : (relayList?.write ?? []) .concat(additionalRelayUrls ?? []) .concat(client.getDefaultRelayUrls()), - event, - { signer: signEvent } + event ) return event } @@ -415,6 +423,7 @@ export function NostrProvider({ children }: { children: React.ReactNode }) { signHttpAuth, nip04Encrypt, nip04Decrypt, + startLogin: () => setOpenLoginDialog(true), checkLogin, signEvent, getRelayList, diff --git a/src/providers/NostrProvider/nsec.signer.ts b/src/providers/NostrProvider/nsec.signer.ts index 54cd1449..138575a6 100644 --- a/src/providers/NostrProvider/nsec.signer.ts +++ b/src/providers/NostrProvider/nsec.signer.ts @@ -23,20 +23,18 @@ export class NsecSigner implements ISigner { } async getPublicKey() { + if (!this.pubkey) { + throw new Error('Not logged in') + } return this.pubkey } async signEvent(draftEvent: TDraftEvent) { if (!this.privkey) { - return null + throw new Error('Not logged in') } - try { - return finalizeEvent(draftEvent, this.privkey) - } catch (error) { - console.error(error) - return null - } + return finalizeEvent(draftEvent, this.privkey) } async nip04Encrypt(pubkey: string, plainText: string) { diff --git a/src/services/client.service.ts b/src/services/client.service.ts index 7886a194..cb872e89 100644 --- a/src/services/client.service.ts +++ b/src/services/client.service.ts @@ -24,6 +24,7 @@ type TTimelineRef = [string, number] class ClientService extends EventTarget { static instance: ClientService + signer?: (evt: TDraftEvent) => Promise private defaultRelayUrls: string[] = BIG_RELAY_URLS private pool: SimplePool @@ -109,17 +110,11 @@ class ClientService extends EventTarget { return this.defaultRelayUrls } - async publishEvent( - relayUrls: string[], - event: NEvent, - { - signer - }: { - signer?: (evt: TDraftEvent) => Promise - } = {} - ) { + async publishEvent(relayUrls: string[], event: NEvent) { const result = await Promise.any( relayUrls.map(async (url) => { + // eslint-disable-next-line @typescript-eslint/no-this-alias + const that = this const relay = await this.pool.ensureRelay(url) return relay .publish(event) @@ -128,9 +123,13 @@ class ClientService extends EventTarget { return reason }) .catch((error) => { - if (error instanceof Error && error.message.startsWith('auth-required:') && signer) { + if ( + error instanceof Error && + error.message.startsWith('auth-required:') && + !!that.signer + ) { relay - .auth((authEvt: EventTemplate) => signer(authEvt)) + .auth((authEvt: EventTemplate) => that.signer!(authEvt)) .then(() => relay.publish(event)) } else { throw error @@ -162,10 +161,10 @@ class ClientService extends EventTarget { onNew: (evt: NEvent) => void }, { - signer, + startLogin, needSort = true }: { - signer?: (evt: TDraftEvent) => Promise + startLogin?: () => void needSort?: boolean } = {} ) { @@ -255,26 +254,29 @@ class ClientService extends EventTarget { timeline.refs.splice(idx, 0, [evt.id, evt.created_at]) }, onclose: (reason: string) => { - if (reason.startsWith('auth-required:')) { - if (!hasAuthed && signer) { - relay - .auth(async (authEvt: EventTemplate) => { - const evt = await signer(authEvt) - if (!evt) { - throw new Error('sign event failed') - } - return evt as VerifiedEvent - }) - .then(() => { - hasAuthed = true - if (!eosed) { - startSub() - } - }) - .catch(() => { - // ignore - }) - } + if (!reason.startsWith('auth-required:')) return + if (hasAuthed) return + + if (that.signer) { + relay + .auth(async (authEvt: EventTemplate) => { + const evt = await that.signer!(authEvt) + if (!evt) { + throw new Error('sign event failed') + } + return evt as VerifiedEvent + }) + .then(() => { + hasAuthed = true + if (!eosed) { + startSub() + } + }) + .catch(() => { + // ignore + }) + } else if (startLogin) { + startLogin() } }, oneose: () => { @@ -344,6 +346,55 @@ class ClientService extends EventTarget { } } + async query(urls: string[], filter: Filter) { + const _knownIds = new Set() + const events: NEvent[] = [] + await Promise.allSettled( + urls.map(async (url) => { + // eslint-disable-next-line @typescript-eslint/no-this-alias + const that = this + const relay = await this.pool.ensureRelay(url) + let hasAuthed = false + + return new Promise((resolve, reject) => { + const startQuery = () => { + relay.subscribe([filter], { + receivedEvent(relay, id) { + that.trackEventSeenOn(id, relay) + }, + onclose(reason) { + if (!reason.startsWith('auth-required:') || hasAuthed) { + resolve() + return + } + + if (that.signer) { + relay + .auth((authEvt: EventTemplate) => that.signer!(authEvt)) + .then(() => { + hasAuthed = true + startQuery() + }) + .catch(reject) + } + }, + oneose() { + resolve() + }, + onevent(evt) { + if (_knownIds.has(evt.id)) return + _knownIds.add(evt.id) + events.push(evt) + } + }) + } + startQuery() + }) + }) + ) + return events + } + async loadMoreTimeline(key: string, until: number, limit: number) { const timeline = this.timelines[key] if (!timeline) return [] @@ -362,7 +413,7 @@ class ClientService extends EventTarget { return cachedEvents } - let events = await this.pool.querySync(urls, { ...filter, until: until, limit: limit }) + let events = await this.query(urls, { ...filter, until: until, limit: limit }) events.forEach((evt) => { this.eventDataLoader.prime(evt.id, Promise.resolve(evt)) }) @@ -372,7 +423,7 @@ class ClientService extends EventTarget { } async fetchEvents(relayUrls: string[], filter: Filter, cache = false) { - const events = await this.pool.querySync( + const events = await this.query( relayUrls.length > 0 ? relayUrls : this.defaultRelayUrls, filter ) @@ -440,7 +491,7 @@ class ClientService extends EventTarget { } async fetchProfiles(relayUrls: string[], filter: Filter): Promise { - const events = await this.pool.querySync(relayUrls, { + const events = await this.query(relayUrls, { ...filter, kinds: [kinds.Metadata] }) @@ -560,9 +611,12 @@ class ClientService extends EventTarget { return this.getSeenEventRelays(eventId).map((relay) => relay.url) } + getEventHints(eventId: string) { + return this.getSeenEventRelayUrls(eventId).filter((url) => !isLocalNetworkUrl(url)) + } + getEventHint(eventId: string) { - const relayUrls = this.getSeenEventRelayUrls(eventId) - return relayUrls.find((url) => !isLocalNetworkUrl(url)) ?? '' + return this.getSeenEventRelayUrls(eventId).find((url) => !isLocalNetworkUrl(url)) ?? '' } trackEventSeenOn(eventId: string, relay: AbstractRelay) { @@ -617,7 +671,9 @@ class ClientService extends EventTarget { let event: NEvent | undefined if (filter.ids) { event = await this.fetchEventById(relays, filter.ids[0]) - } else { + } + + if (!event) { event = await this.tryHarderToFetchEvent(relays, filter) } @@ -710,12 +766,12 @@ class ClientService extends EventTarget { } if (!relayUrls.length) return - const events = await this.pool.querySync(relayUrls, filter) + const events = await this.query(relayUrls, filter) return events.sort((a, b) => b.created_at - a.created_at)[0] } private async eventBatchLoadFn(ids: readonly string[]) { - const events = await this.pool.querySync(this.defaultRelayUrls, { + const events = await this.query(this.defaultRelayUrls, { ids: Array.from(new Set(ids)), limit: ids.length }) @@ -728,7 +784,7 @@ class ClientService extends EventTarget { } private async profileEventBatchLoadFn(pubkeys: readonly string[]) { - const events = await this.pool.querySync(this.defaultRelayUrls, { + const events = await this.query(this.defaultRelayUrls, { authors: Array.from(new Set(pubkeys)), kinds: [kinds.Metadata], limit: pubkeys.length @@ -748,7 +804,7 @@ class ClientService extends EventTarget { } private async relayListEventBatchLoadFn(pubkeys: readonly string[]) { - const events = await this.pool.querySync(this.defaultRelayUrls, { + const events = await this.query(this.defaultRelayUrls, { authors: pubkeys as string[], kinds: [kinds.RelayList], limit: pubkeys.length @@ -767,13 +823,10 @@ class ClientService extends EventTarget { private async _fetchFollowListEvent(pubkey: string) { const relayList = await this.fetchRelayList(pubkey) - const followListEvents = await this.pool.querySync( - relayList.write.concat(this.defaultRelayUrls), - { - authors: [pubkey], - kinds: [kinds.Contacts] - } - ) + const followListEvents = await this.query(relayList.write.concat(this.defaultRelayUrls), { + authors: [pubkey], + kinds: [kinds.Contacts] + }) return followListEvents.sort((a, b) => b.created_at - a.created_at)[0] } diff --git a/src/types.ts b/src/types.ts index c45634ff..e2e95f75 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,4 +1,4 @@ -import { Event } from 'nostr-tools' +import { Event, VerifiedEvent } from 'nostr-tools' export type TProfile = { username: string @@ -61,8 +61,8 @@ export type TTheme = 'light' | 'dark' export type TDraftEvent = Pick export type TNip07 = { - getPublicKey: () => Promise - signEvent: (draftEvent: TDraftEvent) => Promise + getPublicKey: () => Promise + signEvent: (draftEvent: TDraftEvent) => Promise nip04?: { encrypt?: (pubkey: string, plainText: string) => Promise decrypt?: (pubkey: string, cipherText: string) => Promise @@ -70,8 +70,8 @@ export type TNip07 = { } export interface ISigner { - getPublicKey: () => Promise - signEvent: (draftEvent: TDraftEvent) => Promise + getPublicKey: () => Promise + signEvent: (draftEvent: TDraftEvent) => Promise nip04Encrypt: (pubkey: string, plainText: string) => Promise nip04Decrypt: (pubkey: string, cipherText: string) => Promise }