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