From d1150b947adcebf6a398456c86079353920ea414 Mon Sep 17 00:00:00 2001 From: codytseng Date: Sun, 17 Nov 2024 23:08:01 +0800 Subject: [PATCH] feat: adapt for algo relay --- .../src/components/ImageGallery/index.tsx | 55 +++---- .../src/components/NoteList/index.tsx | 51 +++--- src/renderer/src/providers/NostrProvider.tsx | 39 ++++- src/renderer/src/services/client.service.ts | 149 ++++++++++-------- 4 files changed, 163 insertions(+), 131 deletions(-) diff --git a/src/renderer/src/components/ImageGallery/index.tsx b/src/renderer/src/components/ImageGallery/index.tsx index 8c55e2c9..9b984b6c 100644 --- a/src/renderer/src/components/ImageGallery/index.tsx +++ b/src/renderer/src/components/ImageGallery/index.tsx @@ -1,4 +1,5 @@ import { Image } from '@nextui-org/image' +import { ScrollArea, ScrollBar } from '@renderer/components/ui/scroll-area' import { cn } from '@renderer/lib/utils' import { useState } from 'react' import Lightbox from 'yet-another-react-lightbox' @@ -23,21 +24,25 @@ export default function ImageGallery({ setIndex(current) } - const maxHight = size === 'small' ? 'h-[15vh]' : images.length < 3 ? 'h-[30vh]' : 'h-[20vh]' - return ( -
e.stopPropagation()}> -
- {images.map((src, index) => ( - handlePhotoClick(e, index)} - /> - ))} -
+
e.stopPropagation()}> + +
+ {images.map((src, index) => ( + handlePhotoClick(e, index)} + removeWrapper + /> + ))} +
+ +
({ src }))} @@ -51,28 +56,6 @@ export default function ImageGallery({ }} styles={{ toolbar: { paddingTop: '2.25rem' } }} /> -
- ) -} - -function ImageWithNsfwOverlay({ - src, - isNsfw = false, - className, - onClick -}: { - src: string - isNsfw: boolean - className?: string - onClick?: (e: React.MouseEvent) => void -}) { - return ( -
- {isNsfw && }
) diff --git a/src/renderer/src/components/NoteList/index.tsx b/src/renderer/src/components/NoteList/index.tsx index f16c35e0..994bdc52 100644 --- a/src/renderer/src/components/NoteList/index.tsx +++ b/src/renderer/src/components/NoteList/index.tsx @@ -1,6 +1,7 @@ import { Button } from '@renderer/components/ui/button' import { isReplyNoteEvent } from '@renderer/lib/event' import { cn } from '@renderer/lib/utils' +import { useNostr } from '@renderer/providers/NostrProvider' import client from '@renderer/services/client.service' import dayjs from 'dayjs' import { Event, Filter, kinds } from 'nostr-tools' @@ -16,6 +17,7 @@ export default function NoteList({ filter?: Filter className?: string }) { + const { isReady, singEvent } = useNostr() const [events, setEvents] = useState([]) const [newEvents, setNewEvents] = useState([]) const [until, setUntil] = useState(() => dayjs().unix()) @@ -26,42 +28,49 @@ export default function NoteList({ const noteFilter = useMemo(() => { return { kinds: [kinds.ShortTextNote, kinds.Repost], - limit: 100, + limit: 200, ...filter } }, [JSON.stringify(filter)]) useEffect(() => { + if (!isReady) return + setInitialized(false) setEvents([]) setNewEvents([]) setHasMore(true) - const sub = client.subscribeEvents(relayUrls, noteFilter, { - onEose: (events) => { - const processedEvents = events.filter((e) => !isReplyNoteEvent(e)) - if (processedEvents.length > 0) { - setEvents((pre) => [...pre, ...processedEvents]) + const subCloser = client.subscribeEventsWithAuth( + relayUrls, + noteFilter, + { + onEose: (events) => { + const processedEvents = events.filter((e) => !isReplyNoteEvent(e)) + if (processedEvents.length > 0) { + setEvents((pre) => [...pre, ...processedEvents]) + } + if (events.length > 0) { + setUntil(events[events.length - 1].created_at - 1) + } + setInitialized(true) + processedEvents.forEach((e) => { + client.addEventToCache(e) + }) + }, + onNew: (event) => { + if (!isReplyNoteEvent(event)) { + setNewEvents((oldEvents) => [event, ...oldEvents]) + } } - if (events.length > 0) { - setUntil(events[events.length - 1].created_at - 1) - } - setInitialized(true) - processedEvents.forEach((e) => { - client.addEventToCache(e) - }) }, - onNew: (event) => { - if (!isReplyNoteEvent(event)) { - setNewEvents((oldEvents) => [event, ...oldEvents]) - } - } - }) + singEvent + ) return () => { - sub.close() + subCloser() } - }, [JSON.stringify(relayUrls), JSON.stringify(noteFilter)]) + }, [JSON.stringify(relayUrls), JSON.stringify(noteFilter), isReady]) useEffect(() => { if (!initialized) return diff --git a/src/renderer/src/providers/NostrProvider.tsx b/src/renderer/src/providers/NostrProvider.tsx index bbbc21f6..e4167fcb 100644 --- a/src/renderer/src/providers/NostrProvider.tsx +++ b/src/renderer/src/providers/NostrProvider.tsx @@ -9,6 +9,7 @@ import { Event, kinds } from 'nostr-tools' import { createContext, useContext, useEffect, useState } from 'react' type TNostrContext = { + isReady: boolean pubkey: string | null canLogin: boolean login: (nsec: string) => Promise @@ -19,6 +20,7 @@ type TNostrContext = { */ publish: (draftEvent: TDraftEvent, additionalRelayUrls?: string[]) => Promise signHttpAuth: (url: string, method: string) => Promise + singEvent: (draftEvent: TDraftEvent) => Promise checkLogin: (cb?: () => void | Promise) => void } @@ -34,17 +36,23 @@ export const useNostr = () => { export function NostrProvider({ children }: { children: React.ReactNode }) { const { toast } = useToast() + const [isReady, setIsReady] = useState(false) const [pubkey, setPubkey] = useState(null) const [canLogin, setCanLogin] = useState(false) const [openLoginDialog, setOpenLoginDialog] = useState(false) const relayList = useFetchRelayList(pubkey) useEffect(() => { - window.nostr?.getPublicKey().then((pubkey) => { - if (pubkey) { - setPubkey(pubkey) - } - }) + if (window.nostr) { + window.nostr.getPublicKey().then((pubkey) => { + if (pubkey) { + setPubkey(pubkey) + } + setIsReady(true) + }) + } else { + setIsReady(true) + } if (isElectron(window)) { window.api?.system.isEncryptionAvailable().then((isEncryptionAvailable) => { setCanLogin(isEncryptionAvailable) @@ -104,6 +112,14 @@ export function NostrProvider({ children }: { children: React.ReactNode }) { 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 event = await window.nostr?.signEvent({ content: '', @@ -142,7 +158,18 @@ export function NostrProvider({ children }: { children: React.ReactNode }) { return ( {children} diff --git a/src/renderer/src/services/client.service.ts b/src/renderer/src/services/client.service.ts index 6e7eff9c..67769202 100644 --- a/src/renderer/src/services/client.service.ts +++ b/src/renderer/src/services/client.service.ts @@ -1,13 +1,20 @@ -import { TRelayGroup } from '@common/types' +import { TDraftEvent, TRelayGroup } from '@common/types' import { formatPubkey } from '@renderer/lib/pubkey' import { tagNameEquals } from '@renderer/lib/tag' +import { isWebsocketUrl, normalizeUrl } from '@renderer/lib/url' import { TProfile, TRelayList } from '@renderer/types' import DataLoader from 'dataloader' 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 storage from './storage.service' -import { isWebsocketUrl, normalizeUrl } from '@renderer/lib/url' const BIG_RELAY_URLS = [ 'wss://relay.damus.io/', @@ -26,10 +33,7 @@ class ClientService { private eventByFilterCache = new LRUCache>({ max: 10000, fetchMethod: async (filterStr) => { - const events = await this.fetchEvents( - BIG_RELAY_URLS.concat(this.relayUrls), - JSON.parse(filterStr) - ) + const events = await this.fetchEvents(BIG_RELAY_URLS, JSON.parse(filterStr)) events.forEach((event) => this.addEventToCache(event)) 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)) } - subscribeEvents( + subscribeEventsWithAuth( urls: string[], filter: Filter, - opts: { + { + onEose, + onNew + }: { onEose: (events: NEvent[]) => void onNew: (evt: NEvent) => void - } + }, + signer?: (evt: TDraftEvent) => Promise ) { + // eslint-disable-next-line @typescript-eslint/no-this-alias + const that = this + const _knownIds = new Set() const events: NEvent[] = [] - let eose = false - return this.pool.subscribeMany( - urls.length > 0 ? urls : this.relayUrls.concat(BIG_RELAY_URLS), - [filter], - { - onevent: (evt) => { - if (eose) { - opts.onNew(evt) - } else { - events.push(evt) + let started = 0 + let eosed = 0 + const subPromises = urls.map(async (url) => { + const relay = await this.pool.ensureRelay(url) + let hasAuthed = false + + return startSub() + + 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 { + events.push(evt) + } + that.eventByIdCache.set(evt.id, Promise.resolve(evt)) + }, + onclose(reason: string) { + if (reason.startsWith('auth-required:')) { + if (!hasAuthed && signer) { + relay + .auth((authEvt: EventTemplate) => { + return signer(authEvt) as Promise + }) + .then(() => { + hasAuthed = true + startSub() + }) + } + } + }, + oneose() { + eosed++ + if (eosed === started) { + events.sort((a, b) => b.created_at - a.created_at) + onEose(events) + } } - }, - oneose: () => { - eose = true - opts.onEose(events.sort((a, b) => b.created_at - a.created_at)) - }, - onclose: () => { - if (!eose) { - opts.onEose(events.sort((a, b) => b.created_at - a.created_at)) - } - } + }) } - ) + }) + + return () => { + onEose = () => {} + onNew = () => {} + subPromises.forEach((subPromise) => { + subPromise.then((sub) => { + sub.close() + }) + }) + } } async fetchEvents(relayUrls: string[], filter: Filter) { await this.initPromise // 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) { @@ -154,7 +200,7 @@ class ClientService { } 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[], limit: ids.length }) @@ -163,25 +209,11 @@ class ClientService { 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)) } 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[], kinds: [kinds.Metadata], 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) => { const event = eventsMap.get(pubkey) return event ? this.parseProfileFromEvent(event) : undefined @@ -221,7 +234,7 @@ class ClientService { } 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[], kinds: [kinds.RelayList], limit: pubkeys.length