feat: adapt for algo relay
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import { Image } from '@nextui-org/image'
|
import { Image } from '@nextui-org/image'
|
||||||
|
import { ScrollArea, ScrollBar } from '@renderer/components/ui/scroll-area'
|
||||||
import { cn } from '@renderer/lib/utils'
|
import { cn } from '@renderer/lib/utils'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import Lightbox from 'yet-another-react-lightbox'
|
import Lightbox from 'yet-another-react-lightbox'
|
||||||
@@ -23,21 +24,25 @@ export default function ImageGallery({
|
|||||||
setIndex(current)
|
setIndex(current)
|
||||||
}
|
}
|
||||||
|
|
||||||
const maxHight = size === 'small' ? 'h-[15vh]' : images.length < 3 ? 'h-[30vh]' : 'h-[20vh]'
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={className} onClick={(e) => e.stopPropagation()}>
|
<div className={cn('relative', className)} onClick={(e) => e.stopPropagation()}>
|
||||||
<div className="flex flex-wrap gap-2">
|
<ScrollArea className="w-full">
|
||||||
|
<div className="flex space-x-2">
|
||||||
{images.map((src, index) => (
|
{images.map((src, index) => (
|
||||||
<ImageWithNsfwOverlay
|
<Image
|
||||||
key={index}
|
key={index}
|
||||||
|
className={cn(
|
||||||
|
'rounded-lg cursor-pointer z-0',
|
||||||
|
size === 'small' ? 'h-[15vh]' : 'h-[30vh]'
|
||||||
|
)}
|
||||||
src={src}
|
src={src}
|
||||||
isNsfw={isNsfw}
|
|
||||||
className={maxHight}
|
|
||||||
onClick={(e) => handlePhotoClick(e, index)}
|
onClick={(e) => handlePhotoClick(e, index)}
|
||||||
|
removeWrapper
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
<ScrollBar orientation="horizontal" />
|
||||||
|
</ScrollArea>
|
||||||
<Lightbox
|
<Lightbox
|
||||||
index={index}
|
index={index}
|
||||||
slides={images.map((src) => ({ src }))}
|
slides={images.map((src) => ({ src }))}
|
||||||
@@ -51,28 +56,6 @@ export default function ImageGallery({
|
|||||||
}}
|
}}
|
||||||
styles={{ toolbar: { paddingTop: '2.25rem' } }}
|
styles={{ toolbar: { paddingTop: '2.25rem' } }}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function ImageWithNsfwOverlay({
|
|
||||||
src,
|
|
||||||
isNsfw = false,
|
|
||||||
className,
|
|
||||||
onClick
|
|
||||||
}: {
|
|
||||||
src: string
|
|
||||||
isNsfw: boolean
|
|
||||||
className?: string
|
|
||||||
onClick?: (e: React.MouseEvent) => void
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<div className="relative" onClick={onClick}>
|
|
||||||
<Image
|
|
||||||
className={cn('rounded-lg object-cover aspect-square', className)}
|
|
||||||
src={src}
|
|
||||||
removeWrapper
|
|
||||||
/>
|
|
||||||
{isNsfw && <NsfwOverlay className="rounded-lg" />}
|
{isNsfw && <NsfwOverlay className="rounded-lg" />}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Button } from '@renderer/components/ui/button'
|
import { Button } from '@renderer/components/ui/button'
|
||||||
import { isReplyNoteEvent } from '@renderer/lib/event'
|
import { isReplyNoteEvent } from '@renderer/lib/event'
|
||||||
import { cn } from '@renderer/lib/utils'
|
import { cn } from '@renderer/lib/utils'
|
||||||
|
import { useNostr } from '@renderer/providers/NostrProvider'
|
||||||
import client from '@renderer/services/client.service'
|
import client from '@renderer/services/client.service'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { Event, Filter, kinds } from 'nostr-tools'
|
import { Event, Filter, kinds } from 'nostr-tools'
|
||||||
@@ -16,6 +17,7 @@ export default function NoteList({
|
|||||||
filter?: Filter
|
filter?: Filter
|
||||||
className?: string
|
className?: string
|
||||||
}) {
|
}) {
|
||||||
|
const { isReady, singEvent } = useNostr()
|
||||||
const [events, setEvents] = useState<Event[]>([])
|
const [events, setEvents] = useState<Event[]>([])
|
||||||
const [newEvents, setNewEvents] = useState<Event[]>([])
|
const [newEvents, setNewEvents] = useState<Event[]>([])
|
||||||
const [until, setUntil] = useState<number>(() => dayjs().unix())
|
const [until, setUntil] = useState<number>(() => dayjs().unix())
|
||||||
@@ -26,18 +28,23 @@ export default function NoteList({
|
|||||||
const noteFilter = useMemo(() => {
|
const noteFilter = useMemo(() => {
|
||||||
return {
|
return {
|
||||||
kinds: [kinds.ShortTextNote, kinds.Repost],
|
kinds: [kinds.ShortTextNote, kinds.Repost],
|
||||||
limit: 100,
|
limit: 200,
|
||||||
...filter
|
...filter
|
||||||
}
|
}
|
||||||
}, [JSON.stringify(filter)])
|
}, [JSON.stringify(filter)])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!isReady) return
|
||||||
|
|
||||||
setInitialized(false)
|
setInitialized(false)
|
||||||
setEvents([])
|
setEvents([])
|
||||||
setNewEvents([])
|
setNewEvents([])
|
||||||
setHasMore(true)
|
setHasMore(true)
|
||||||
|
|
||||||
const sub = client.subscribeEvents(relayUrls, noteFilter, {
|
const subCloser = client.subscribeEventsWithAuth(
|
||||||
|
relayUrls,
|
||||||
|
noteFilter,
|
||||||
|
{
|
||||||
onEose: (events) => {
|
onEose: (events) => {
|
||||||
const processedEvents = events.filter((e) => !isReplyNoteEvent(e))
|
const processedEvents = events.filter((e) => !isReplyNoteEvent(e))
|
||||||
if (processedEvents.length > 0) {
|
if (processedEvents.length > 0) {
|
||||||
@@ -56,12 +63,14 @@ export default function NoteList({
|
|||||||
setNewEvents((oldEvents) => [event, ...oldEvents])
|
setNewEvents((oldEvents) => [event, ...oldEvents])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
},
|
||||||
|
singEvent
|
||||||
|
)
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
sub.close()
|
subCloser()
|
||||||
}
|
}
|
||||||
}, [JSON.stringify(relayUrls), JSON.stringify(noteFilter)])
|
}, [JSON.stringify(relayUrls), JSON.stringify(noteFilter), isReady])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!initialized) return
|
if (!initialized) return
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { Event, kinds } from 'nostr-tools'
|
|||||||
import { createContext, useContext, useEffect, useState } from 'react'
|
import { createContext, useContext, useEffect, useState } from 'react'
|
||||||
|
|
||||||
type TNostrContext = {
|
type TNostrContext = {
|
||||||
|
isReady: boolean
|
||||||
pubkey: string | null
|
pubkey: string | null
|
||||||
canLogin: boolean
|
canLogin: boolean
|
||||||
login: (nsec: string) => Promise<string>
|
login: (nsec: string) => Promise<string>
|
||||||
@@ -19,6 +20,7 @@ type TNostrContext = {
|
|||||||
*/
|
*/
|
||||||
publish: (draftEvent: TDraftEvent, additionalRelayUrls?: string[]) => Promise<Event>
|
publish: (draftEvent: TDraftEvent, additionalRelayUrls?: string[]) => Promise<Event>
|
||||||
signHttpAuth: (url: string, method: string) => Promise<string>
|
signHttpAuth: (url: string, method: string) => Promise<string>
|
||||||
|
singEvent: (draftEvent: TDraftEvent) => Promise<Event>
|
||||||
checkLogin: (cb?: () => void | Promise<void>) => void
|
checkLogin: (cb?: () => void | Promise<void>) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,17 +36,23 @@ export const useNostr = () => {
|
|||||||
|
|
||||||
export function NostrProvider({ children }: { children: React.ReactNode }) {
|
export function NostrProvider({ children }: { children: React.ReactNode }) {
|
||||||
const { toast } = useToast()
|
const { toast } = useToast()
|
||||||
|
const [isReady, setIsReady] = useState(false)
|
||||||
const [pubkey, setPubkey] = useState<string | null>(null)
|
const [pubkey, setPubkey] = useState<string | null>(null)
|
||||||
const [canLogin, setCanLogin] = useState(false)
|
const [canLogin, setCanLogin] = useState(false)
|
||||||
const [openLoginDialog, setOpenLoginDialog] = useState(false)
|
const [openLoginDialog, setOpenLoginDialog] = useState(false)
|
||||||
const relayList = useFetchRelayList(pubkey)
|
const relayList = useFetchRelayList(pubkey)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
window.nostr?.getPublicKey().then((pubkey) => {
|
if (window.nostr) {
|
||||||
|
window.nostr.getPublicKey().then((pubkey) => {
|
||||||
if (pubkey) {
|
if (pubkey) {
|
||||||
setPubkey(pubkey)
|
setPubkey(pubkey)
|
||||||
}
|
}
|
||||||
|
setIsReady(true)
|
||||||
})
|
})
|
||||||
|
} else {
|
||||||
|
setIsReady(true)
|
||||||
|
}
|
||||||
if (isElectron(window)) {
|
if (isElectron(window)) {
|
||||||
window.api?.system.isEncryptionAvailable().then((isEncryptionAvailable) => {
|
window.api?.system.isEncryptionAvailable().then((isEncryptionAvailable) => {
|
||||||
setCanLogin(isEncryptionAvailable)
|
setCanLogin(isEncryptionAvailable)
|
||||||
@@ -104,6 +112,14 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
|
|||||||
return event
|
return event
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const singEvent = async (draftEvent: TDraftEvent) => {
|
||||||
|
const event = await window.nostr?.signEvent(draftEvent)
|
||||||
|
if (!event) {
|
||||||
|
throw new Error('sign event failed')
|
||||||
|
}
|
||||||
|
return event
|
||||||
|
}
|
||||||
|
|
||||||
const signHttpAuth = async (url: string, method: string) => {
|
const signHttpAuth = async (url: string, method: string) => {
|
||||||
const event = await window.nostr?.signEvent({
|
const event = await window.nostr?.signEvent({
|
||||||
content: '',
|
content: '',
|
||||||
@@ -142,7 +158,18 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<NostrContext.Provider
|
<NostrContext.Provider
|
||||||
value={{ pubkey, canLogin, login, nip07Login, logout, publish, signHttpAuth, checkLogin }}
|
value={{
|
||||||
|
isReady,
|
||||||
|
pubkey,
|
||||||
|
canLogin,
|
||||||
|
login,
|
||||||
|
nip07Login,
|
||||||
|
logout,
|
||||||
|
publish,
|
||||||
|
signHttpAuth,
|
||||||
|
checkLogin,
|
||||||
|
singEvent
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
<LoginDialog open={openLoginDialog} setOpen={setOpenLoginDialog} />
|
<LoginDialog open={openLoginDialog} setOpen={setOpenLoginDialog} />
|
||||||
|
|||||||
@@ -1,13 +1,20 @@
|
|||||||
import { TRelayGroup } from '@common/types'
|
import { TDraftEvent, TRelayGroup } from '@common/types'
|
||||||
import { formatPubkey } from '@renderer/lib/pubkey'
|
import { formatPubkey } from '@renderer/lib/pubkey'
|
||||||
import { tagNameEquals } from '@renderer/lib/tag'
|
import { tagNameEquals } from '@renderer/lib/tag'
|
||||||
|
import { isWebsocketUrl, normalizeUrl } from '@renderer/lib/url'
|
||||||
import { TProfile, TRelayList } from '@renderer/types'
|
import { TProfile, TRelayList } from '@renderer/types'
|
||||||
import DataLoader from 'dataloader'
|
import DataLoader from 'dataloader'
|
||||||
import { LRUCache } from 'lru-cache'
|
import { LRUCache } from 'lru-cache'
|
||||||
import { Filter, kinds, Event as NEvent, SimplePool } from 'nostr-tools'
|
import {
|
||||||
|
EventTemplate,
|
||||||
|
Filter,
|
||||||
|
kinds,
|
||||||
|
Event as NEvent,
|
||||||
|
SimplePool,
|
||||||
|
VerifiedEvent
|
||||||
|
} from 'nostr-tools'
|
||||||
import { EVENT_TYPES, eventBus } from './event-bus.service'
|
import { EVENT_TYPES, eventBus } from './event-bus.service'
|
||||||
import storage from './storage.service'
|
import storage from './storage.service'
|
||||||
import { isWebsocketUrl, normalizeUrl } from '@renderer/lib/url'
|
|
||||||
|
|
||||||
const BIG_RELAY_URLS = [
|
const BIG_RELAY_URLS = [
|
||||||
'wss://relay.damus.io/',
|
'wss://relay.damus.io/',
|
||||||
@@ -26,10 +33,7 @@ class ClientService {
|
|||||||
private eventByFilterCache = new LRUCache<string, Promise<NEvent | undefined>>({
|
private eventByFilterCache = new LRUCache<string, Promise<NEvent | undefined>>({
|
||||||
max: 10000,
|
max: 10000,
|
||||||
fetchMethod: async (filterStr) => {
|
fetchMethod: async (filterStr) => {
|
||||||
const events = await this.fetchEvents(
|
const events = await this.fetchEvents(BIG_RELAY_URLS, JSON.parse(filterStr))
|
||||||
BIG_RELAY_URLS.concat(this.relayUrls),
|
|
||||||
JSON.parse(filterStr)
|
|
||||||
)
|
|
||||||
events.forEach((event) => this.addEventToCache(event))
|
events.forEach((event) => this.addEventToCache(event))
|
||||||
return events.sort((a, b) => b.created_at - a.created_at)[0]
|
return events.sort((a, b) => b.created_at - a.created_at)[0]
|
||||||
}
|
}
|
||||||
@@ -85,44 +89,86 @@ class ClientService {
|
|||||||
return await Promise.any(this.pool.publish(this.relayUrls.concat(relayUrls), event))
|
return await Promise.any(this.pool.publish(this.relayUrls.concat(relayUrls), event))
|
||||||
}
|
}
|
||||||
|
|
||||||
subscribeEvents(
|
subscribeEventsWithAuth(
|
||||||
urls: string[],
|
urls: string[],
|
||||||
filter: Filter,
|
filter: Filter,
|
||||||
opts: {
|
{
|
||||||
|
onEose,
|
||||||
|
onNew
|
||||||
|
}: {
|
||||||
onEose: (events: NEvent[]) => void
|
onEose: (events: NEvent[]) => void
|
||||||
onNew: (evt: NEvent) => void
|
onNew: (evt: NEvent) => void
|
||||||
}
|
},
|
||||||
|
signer?: (evt: TDraftEvent) => Promise<NEvent>
|
||||||
) {
|
) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
||||||
|
const that = this
|
||||||
|
const _knownIds = new Set<string>()
|
||||||
const events: NEvent[] = []
|
const events: NEvent[] = []
|
||||||
let eose = false
|
let started = 0
|
||||||
return this.pool.subscribeMany(
|
let eosed = 0
|
||||||
urls.length > 0 ? urls : this.relayUrls.concat(BIG_RELAY_URLS),
|
const subPromises = urls.map(async (url) => {
|
||||||
[filter],
|
const relay = await this.pool.ensureRelay(url)
|
||||||
{
|
let hasAuthed = false
|
||||||
onevent: (evt) => {
|
|
||||||
if (eose) {
|
return startSub()
|
||||||
opts.onNew(evt)
|
|
||||||
|
function startSub() {
|
||||||
|
started++
|
||||||
|
return relay.subscribe([filter], {
|
||||||
|
alreadyHaveEvent: (id: string) => {
|
||||||
|
const have = _knownIds.has(id)
|
||||||
|
_knownIds.add(id)
|
||||||
|
return have
|
||||||
|
},
|
||||||
|
onevent(evt: NEvent) {
|
||||||
|
if (eosed === started) {
|
||||||
|
onNew(evt)
|
||||||
} else {
|
} else {
|
||||||
events.push(evt)
|
events.push(evt)
|
||||||
}
|
}
|
||||||
|
that.eventByIdCache.set(evt.id, Promise.resolve(evt))
|
||||||
},
|
},
|
||||||
oneose: () => {
|
onclose(reason: string) {
|
||||||
eose = true
|
if (reason.startsWith('auth-required:')) {
|
||||||
opts.onEose(events.sort((a, b) => b.created_at - a.created_at))
|
if (!hasAuthed && signer) {
|
||||||
|
relay
|
||||||
|
.auth((authEvt: EventTemplate) => {
|
||||||
|
return signer(authEvt) as Promise<VerifiedEvent>
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
hasAuthed = true
|
||||||
|
startSub()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onclose: () => {
|
oneose() {
|
||||||
if (!eose) {
|
eosed++
|
||||||
opts.onEose(events.sort((a, b) => b.created_at - a.created_at))
|
if (eosed === started) {
|
||||||
|
events.sort((a, b) => b.created_at - a.created_at)
|
||||||
|
onEose(events)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
onEose = () => {}
|
||||||
|
onNew = () => {}
|
||||||
|
subPromises.forEach((subPromise) => {
|
||||||
|
subPromise.then((sub) => {
|
||||||
|
sub.close()
|
||||||
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetchEvents(relayUrls: string[], filter: Filter) {
|
async fetchEvents(relayUrls: string[], filter: Filter) {
|
||||||
await this.initPromise
|
await this.initPromise
|
||||||
// If relayUrls is empty, use this.relayUrls
|
// If relayUrls is empty, use this.relayUrls
|
||||||
return await this.pool.querySync(relayUrls.length > 0 ? relayUrls : this.relayUrls, filter)
|
return await this.pool.querySync(relayUrls.length > 0 ? relayUrls : BIG_RELAY_URLS, filter)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetchEventByFilter(filter: Filter) {
|
async fetchEventByFilter(filter: Filter) {
|
||||||
@@ -154,7 +200,7 @@ class ClientService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async eventBatchLoadFn(ids: readonly string[]) {
|
private async eventBatchLoadFn(ids: readonly string[]) {
|
||||||
const events = await this.fetchEvents(this.relayUrls, {
|
const events = await this.fetchEvents(BIG_RELAY_URLS, {
|
||||||
ids: ids as string[],
|
ids: ids as string[],
|
||||||
limit: ids.length
|
limit: ids.length
|
||||||
})
|
})
|
||||||
@@ -163,25 +209,11 @@ class ClientService {
|
|||||||
eventsMap.set(event.id, event)
|
eventsMap.set(event.id, event)
|
||||||
}
|
}
|
||||||
|
|
||||||
const missingIds = ids.filter((id) => !eventsMap.has(id))
|
|
||||||
if (missingIds.length > 0) {
|
|
||||||
const missingEvents = await this.fetchEvents(
|
|
||||||
BIG_RELAY_URLS.filter((url) => !this.relayUrls.includes(url)),
|
|
||||||
{
|
|
||||||
ids: missingIds,
|
|
||||||
limit: missingIds.length
|
|
||||||
}
|
|
||||||
)
|
|
||||||
for (const event of missingEvents) {
|
|
||||||
eventsMap.set(event.id, event)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ids.map((id) => eventsMap.get(id))
|
return ids.map((id) => eventsMap.get(id))
|
||||||
}
|
}
|
||||||
|
|
||||||
private async profileBatchLoadFn(pubkeys: readonly string[]) {
|
private async profileBatchLoadFn(pubkeys: readonly string[]) {
|
||||||
const events = await this.fetchEvents(this.relayUrls, {
|
const events = await this.fetchEvents(BIG_RELAY_URLS, {
|
||||||
authors: pubkeys as string[],
|
authors: pubkeys as string[],
|
||||||
kinds: [kinds.Metadata],
|
kinds: [kinds.Metadata],
|
||||||
limit: pubkeys.length
|
limit: pubkeys.length
|
||||||
@@ -195,25 +227,6 @@ class ClientService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const missingPubkeys = pubkeys.filter((pubkey) => !eventsMap.has(pubkey))
|
|
||||||
if (missingPubkeys.length > 0) {
|
|
||||||
const missingEvents = await this.fetchEvents(
|
|
||||||
BIG_RELAY_URLS.filter((url) => !this.relayUrls.includes(url)),
|
|
||||||
{
|
|
||||||
authors: missingPubkeys,
|
|
||||||
kinds: [kinds.Metadata],
|
|
||||||
limit: missingPubkeys.length
|
|
||||||
}
|
|
||||||
)
|
|
||||||
for (const event of missingEvents) {
|
|
||||||
const pubkey = event.pubkey
|
|
||||||
const existing = eventsMap.get(pubkey)
|
|
||||||
if (!existing || existing.created_at < event.created_at) {
|
|
||||||
eventsMap.set(pubkey, event)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return pubkeys.map((pubkey) => {
|
return pubkeys.map((pubkey) => {
|
||||||
const event = eventsMap.get(pubkey)
|
const event = eventsMap.get(pubkey)
|
||||||
return event ? this.parseProfileFromEvent(event) : undefined
|
return event ? this.parseProfileFromEvent(event) : undefined
|
||||||
@@ -221,7 +234,7 @@ class ClientService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async relayListBatchLoadFn(pubkeys: readonly string[]) {
|
private async relayListBatchLoadFn(pubkeys: readonly string[]) {
|
||||||
const events = await this.fetchEvents(BIG_RELAY_URLS.concat(this.relayUrls), {
|
const events = await this.fetchEvents(BIG_RELAY_URLS, {
|
||||||
authors: pubkeys as string[],
|
authors: pubkeys as string[],
|
||||||
kinds: [kinds.RelayList],
|
kinds: [kinds.RelayList],
|
||||||
limit: pubkeys.length
|
limit: pubkeys.length
|
||||||
|
|||||||
Reference in New Issue
Block a user