feat: 💨
This commit is contained in:
@@ -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
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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,26 +254,29 @@ 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
|
||||||
relay
|
|
||||||
.auth(async (authEvt: EventTemplate) => {
|
if (that.signer) {
|
||||||
const evt = await signer(authEvt)
|
relay
|
||||||
if (!evt) {
|
.auth(async (authEvt: EventTemplate) => {
|
||||||
throw new Error('sign event failed')
|
const evt = await that.signer!(authEvt)
|
||||||
}
|
if (!evt) {
|
||||||
return evt as VerifiedEvent
|
throw new Error('sign event failed')
|
||||||
})
|
}
|
||||||
.then(() => {
|
return evt as VerifiedEvent
|
||||||
hasAuthed = true
|
})
|
||||||
if (!eosed) {
|
.then(() => {
|
||||||
startSub()
|
hasAuthed = true
|
||||||
}
|
if (!eosed) {
|
||||||
})
|
startSub()
|
||||||
.catch(() => {
|
}
|
||||||
// ignore
|
})
|
||||||
})
|
.catch(() => {
|
||||||
}
|
// 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],
|
||||||
{
|
kinds: [kinds.Contacts]
|
||||||
authors: [pubkey],
|
})
|
||||||
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]
|
||||||
}
|
}
|
||||||
|
|||||||
10
src/types.ts
10
src/types.ts
@@ -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>
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user