feat: 💨

This commit is contained in:
codytseng
2025-02-07 22:56:00 +08:00
parent 5d21172017
commit ec19b9cbfe
6 changed files with 130 additions and 72 deletions

View File

@@ -35,7 +35,7 @@ export default function NoteList({
}) { }) {
const { t } = useTranslation() const { t } = useTranslation()
const { isLargeScreen } = useScreenSize() const { isLargeScreen } = useScreenSize()
const { signEvent, checkLogin } = useNostr() const { startLogin } = useNostr()
const { mutePubkeys } = useMuteList() const { mutePubkeys } = useMuteList()
const [refreshCount, setRefreshCount] = useState(0) const [refreshCount, setRefreshCount] = useState(0)
const [timelineKey, setTimelineKey] = useState<string | undefined>(undefined) const [timelineKey, setTimelineKey] = useState<string | undefined>(undefined)
@@ -102,10 +102,7 @@ export default function NoteList({
} }
}, },
{ {
signer: async (evt) => { startLogin,
const signedEvt = await checkLogin(() => signEvent(evt))
return signedEvt ?? null
},
needSort: !areAlgoRelays needSort: !areAlgoRelays
} }
) )

View File

@@ -65,11 +65,12 @@ export function getEventCoordinate(event: Event) {
} }
export function getSharableEventId(event: Event) { export function getSharableEventId(event: Event) {
const hints = client.getEventHints(event.id).slice(0, 3)
if (isReplaceable(event.kind)) { if (isReplaceable(event.kind)) {
const identifier = event.tags.find(tagNameEquals('d'))?.[1] ?? '' 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) { export function getUsingClient(event: Event) {

View File

@@ -39,9 +39,10 @@ type TNostrContext = {
options?: { additionalRelayUrls?: string[]; specifiedRelayUrls?: string[] } options?: { additionalRelayUrls?: string[]; specifiedRelayUrls?: string[] }
) => Promise<Event> ) => Promise<Event>
signHttpAuth: (url: string, method: string) => Promise<string> signHttpAuth: (url: string, method: string) => Promise<string>
signEvent: (draftEvent: TDraftEvent) => Promise<Event> signEvent: (draftEvent: TDraftEvent) => Promise<VerifiedEvent>
nip04Encrypt: (pubkey: string, plainText: string) => Promise<string> nip04Encrypt: (pubkey: string, plainText: string) => Promise<string>
nip04Decrypt: (pubkey: string, cipherText: string) => Promise<string> nip04Decrypt: (pubkey: string, cipherText: string) => Promise<string>
startLogin: () => void
checkLogin: <T>(cb?: () => T) => Promise<T | void> checkLogin: <T>(cb?: () => T) => Promise<T | void>
getRelayList: (pubkey: string) => Promise<TRelayList> getRelayList: (pubkey: string) => Promise<TRelayList>
updateRelayListEvent: (relayListEvent: Event) => void updateRelayListEvent: (relayListEvent: Event) => void
@@ -158,6 +159,14 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
client.initUserIndexFromFollowings(account.pubkey) client.initUserIndexFromFollowings(account.pubkey)
}, [account]) }, [account])
useEffect(() => {
if (signer) {
client.signer = signer.signEvent.bind(signer)
} else {
client.signer = undefined
}
}, [signer])
const hasNostrLoginHash = () => { const hasNostrLoginHash = () => {
return window.location.hash && window.location.hash.startsWith('#nostr-login') return window.location.hash && window.location.hash.startsWith('#nostr-login')
} }
@@ -336,8 +345,7 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
: (relayList?.write ?? []) : (relayList?.write ?? [])
.concat(additionalRelayUrls ?? []) .concat(additionalRelayUrls ?? [])
.concat(client.getDefaultRelayUrls()), .concat(client.getDefaultRelayUrls()),
event, event
{ signer: signEvent }
) )
return event return event
} }
@@ -415,6 +423,7 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
signHttpAuth, signHttpAuth,
nip04Encrypt, nip04Encrypt,
nip04Decrypt, nip04Decrypt,
startLogin: () => setOpenLoginDialog(true),
checkLogin, checkLogin,
signEvent, signEvent,
getRelayList, getRelayList,

View File

@@ -23,20 +23,18 @@ export class NsecSigner implements ISigner {
} }
async getPublicKey() { async getPublicKey() {
if (!this.pubkey) {
throw new Error('Not logged in')
}
return this.pubkey return this.pubkey
} }
async signEvent(draftEvent: TDraftEvent) { async signEvent(draftEvent: TDraftEvent) {
if (!this.privkey) { if (!this.privkey) {
return null throw new Error('Not logged in')
} }
try {
return finalizeEvent(draftEvent, this.privkey) return finalizeEvent(draftEvent, this.privkey)
} catch (error) {
console.error(error)
return null
}
} }
async nip04Encrypt(pubkey: string, plainText: string) { async nip04Encrypt(pubkey: string, plainText: string) {

View File

@@ -24,6 +24,7 @@ type TTimelineRef = [string, number]
class ClientService extends EventTarget { class ClientService extends EventTarget {
static instance: ClientService static instance: ClientService
signer?: (evt: TDraftEvent) => Promise<VerifiedEvent>
private defaultRelayUrls: string[] = BIG_RELAY_URLS private defaultRelayUrls: string[] = BIG_RELAY_URLS
private pool: SimplePool private pool: SimplePool
@@ -109,17 +110,11 @@ class ClientService extends EventTarget {
return this.defaultRelayUrls return this.defaultRelayUrls
} }
async publishEvent( async publishEvent(relayUrls: string[], event: NEvent) {
relayUrls: string[],
event: NEvent,
{
signer
}: {
signer?: (evt: TDraftEvent) => Promise<VerifiedEvent>
} = {}
) {
const result = await Promise.any( const result = await Promise.any(
relayUrls.map(async (url) => { relayUrls.map(async (url) => {
// eslint-disable-next-line @typescript-eslint/no-this-alias
const that = this
const relay = await this.pool.ensureRelay(url) const relay = await this.pool.ensureRelay(url)
return relay return relay
.publish(event) .publish(event)
@@ -128,9 +123,13 @@ class ClientService extends EventTarget {
return reason return reason
}) })
.catch((error) => { .catch((error) => {
if (error instanceof Error && error.message.startsWith('auth-required:') && signer) { if (
error instanceof Error &&
error.message.startsWith('auth-required:') &&
!!that.signer
) {
relay relay
.auth((authEvt: EventTemplate) => signer(authEvt)) .auth((authEvt: EventTemplate) => that.signer!(authEvt))
.then(() => relay.publish(event)) .then(() => relay.publish(event))
} else { } else {
throw error throw error
@@ -162,10 +161,10 @@ class ClientService extends EventTarget {
onNew: (evt: NEvent) => void onNew: (evt: NEvent) => void
}, },
{ {
signer, startLogin,
needSort = true needSort = true
}: { }: {
signer?: (evt: TDraftEvent) => Promise<NEvent | null> startLogin?: () => void
needSort?: boolean needSort?: boolean
} = {} } = {}
) { ) {
@@ -255,11 +254,13 @@ class ClientService extends EventTarget {
timeline.refs.splice(idx, 0, [evt.id, evt.created_at]) timeline.refs.splice(idx, 0, [evt.id, evt.created_at])
}, },
onclose: (reason: string) => { onclose: (reason: string) => {
if (reason.startsWith('auth-required:')) { if (!reason.startsWith('auth-required:')) return
if (!hasAuthed && signer) { if (hasAuthed) return
if (that.signer) {
relay relay
.auth(async (authEvt: EventTemplate) => { .auth(async (authEvt: EventTemplate) => {
const evt = await signer(authEvt) const evt = await that.signer!(authEvt)
if (!evt) { if (!evt) {
throw new Error('sign event failed') throw new Error('sign event failed')
} }
@@ -274,7 +275,8 @@ class ClientService extends EventTarget {
.catch(() => { .catch(() => {
// ignore // ignore
}) })
} } else if (startLogin) {
startLogin()
} }
}, },
oneose: () => { oneose: () => {
@@ -344,6 +346,55 @@ class ClientService extends EventTarget {
} }
} }
async query(urls: string[], filter: Filter) {
const _knownIds = new Set<string>()
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<void>((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) { async loadMoreTimeline(key: string, until: number, limit: number) {
const timeline = this.timelines[key] const timeline = this.timelines[key]
if (!timeline) return [] if (!timeline) return []
@@ -362,7 +413,7 @@ class ClientService extends EventTarget {
return cachedEvents 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) => { events.forEach((evt) => {
this.eventDataLoader.prime(evt.id, Promise.resolve(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) { 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, relayUrls.length > 0 ? relayUrls : this.defaultRelayUrls,
filter filter
) )
@@ -440,7 +491,7 @@ class ClientService extends EventTarget {
} }
async fetchProfiles(relayUrls: string[], filter: Filter): Promise<TProfile[]> { async fetchProfiles(relayUrls: string[], filter: Filter): Promise<TProfile[]> {
const events = await this.pool.querySync(relayUrls, { const events = await this.query(relayUrls, {
...filter, ...filter,
kinds: [kinds.Metadata] kinds: [kinds.Metadata]
}) })
@@ -560,9 +611,12 @@ class ClientService extends EventTarget {
return this.getSeenEventRelays(eventId).map((relay) => relay.url) return this.getSeenEventRelays(eventId).map((relay) => relay.url)
} }
getEventHints(eventId: string) {
return this.getSeenEventRelayUrls(eventId).filter((url) => !isLocalNetworkUrl(url))
}
getEventHint(eventId: string) { getEventHint(eventId: string) {
const relayUrls = this.getSeenEventRelayUrls(eventId) return this.getSeenEventRelayUrls(eventId).find((url) => !isLocalNetworkUrl(url)) ?? ''
return relayUrls.find((url) => !isLocalNetworkUrl(url)) ?? ''
} }
trackEventSeenOn(eventId: string, relay: AbstractRelay) { trackEventSeenOn(eventId: string, relay: AbstractRelay) {
@@ -617,7 +671,9 @@ class ClientService extends EventTarget {
let event: NEvent | undefined let event: NEvent | undefined
if (filter.ids) { if (filter.ids) {
event = await this.fetchEventById(relays, filter.ids[0]) event = await this.fetchEventById(relays, filter.ids[0])
} else { }
if (!event) {
event = await this.tryHarderToFetchEvent(relays, filter) event = await this.tryHarderToFetchEvent(relays, filter)
} }
@@ -710,12 +766,12 @@ class ClientService extends EventTarget {
} }
if (!relayUrls.length) return 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] return events.sort((a, b) => b.created_at - a.created_at)[0]
} }
private async eventBatchLoadFn(ids: readonly string[]) { 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)), ids: Array.from(new Set(ids)),
limit: ids.length limit: ids.length
}) })
@@ -728,7 +784,7 @@ class ClientService extends EventTarget {
} }
private async profileEventBatchLoadFn(pubkeys: readonly string[]) { 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)), authors: Array.from(new Set(pubkeys)),
kinds: [kinds.Metadata], kinds: [kinds.Metadata],
limit: pubkeys.length limit: pubkeys.length
@@ -748,7 +804,7 @@ class ClientService extends EventTarget {
} }
private async relayListEventBatchLoadFn(pubkeys: readonly string[]) { 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[], authors: pubkeys as string[],
kinds: [kinds.RelayList], kinds: [kinds.RelayList],
limit: pubkeys.length limit: pubkeys.length
@@ -767,13 +823,10 @@ class ClientService extends EventTarget {
private async _fetchFollowListEvent(pubkey: string) { private async _fetchFollowListEvent(pubkey: string) {
const relayList = await this.fetchRelayList(pubkey) const relayList = await this.fetchRelayList(pubkey)
const followListEvents = await this.pool.querySync( const followListEvents = await this.query(relayList.write.concat(this.defaultRelayUrls), {
relayList.write.concat(this.defaultRelayUrls),
{
authors: [pubkey], authors: [pubkey],
kinds: [kinds.Contacts] kinds: [kinds.Contacts]
} })
)
return followListEvents.sort((a, b) => b.created_at - a.created_at)[0] return followListEvents.sort((a, b) => b.created_at - a.created_at)[0]
} }

View File

@@ -1,4 +1,4 @@
import { Event } from 'nostr-tools' import { Event, VerifiedEvent } from 'nostr-tools'
export type TProfile = { export type TProfile = {
username: string username: string
@@ -61,8 +61,8 @@ export type TTheme = 'light' | 'dark'
export type TDraftEvent = Pick<Event, 'content' | 'created_at' | 'kind' | 'tags'> export type TDraftEvent = Pick<Event, 'content' | 'created_at' | 'kind' | 'tags'>
export type TNip07 = { export type TNip07 = {
getPublicKey: () => Promise<string | null> getPublicKey: () => Promise<string>
signEvent: (draftEvent: TDraftEvent) => Promise<Event | null> signEvent: (draftEvent: TDraftEvent) => Promise<VerifiedEvent>
nip04?: { nip04?: {
encrypt?: (pubkey: string, plainText: string) => Promise<string> encrypt?: (pubkey: string, plainText: string) => Promise<string>
decrypt?: (pubkey: string, cipherText: string) => Promise<string> decrypt?: (pubkey: string, cipherText: string) => Promise<string>
@@ -70,8 +70,8 @@ export type TNip07 = {
} }
export interface ISigner { export interface ISigner {
getPublicKey: () => Promise<string | null> getPublicKey: () => Promise<string>
signEvent: (draftEvent: TDraftEvent) => Promise<Event | null> signEvent: (draftEvent: TDraftEvent) => Promise<VerifiedEvent>
nip04Encrypt: (pubkey: string, plainText: string) => Promise<string> nip04Encrypt: (pubkey: string, plainText: string) => Promise<string>
nip04Decrypt: (pubkey: string, cipherText: string) => Promise<string> nip04Decrypt: (pubkey: string, cipherText: string) => Promise<string>
} }