diff --git a/src/App.tsx b/src/App.tsx
index 22eb285b..d461dfe5 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -12,7 +12,6 @@ import { FollowListProvider } from './providers/FollowListProvider'
import { MediaUploadServiceProvider } from './providers/MediaUploadServiceProvider'
import { MuteListProvider } from './providers/MuteListProvider'
import { NostrProvider } from './providers/NostrProvider'
-import { NoteStatsProvider } from './providers/NoteStatsProvider'
import { ReplyProvider } from './providers/ReplyProvider'
import { ScreenSizeProvider } from './providers/ScreenSizeProvider'
import { TranslationServiceProvider } from './providers/TranslationServiceProvider'
@@ -34,12 +33,10 @@ export default function App(): JSX.Element {
-
-
-
-
-
-
+
+
+
+
diff --git a/src/components/NoteStats/LikeButton.tsx b/src/components/NoteStats/LikeButton.tsx
index 8d1dec24..291a046d 100644
--- a/src/components/NoteStats/LikeButton.tsx
+++ b/src/components/NoteStats/LikeButton.tsx
@@ -4,10 +4,11 @@ import {
DropdownMenuContent,
DropdownMenuTrigger
} from '@/components/ui/dropdown-menu'
+import { useNoteStatsById } from '@/hooks/useNoteStatsById'
import { createReactionDraftEvent } from '@/lib/draft-event'
import { useNostr } from '@/providers/NostrProvider'
-import { useNoteStats } from '@/providers/NoteStatsProvider'
import { useScreenSize } from '@/providers/ScreenSizeProvider'
+import noteStatsService from '@/services/note-stats.service'
import { Loader, SmilePlus } from 'lucide-react'
import { Event } from 'nostr-tools'
import { useMemo, useState } from 'react'
@@ -21,15 +22,15 @@ export default function LikeButton({ event }: { event: Event }) {
const { t } = useTranslation()
const { isSmallScreen } = useScreenSize()
const { pubkey, publish, checkLogin } = useNostr()
- const { noteStatsMap, updateNoteStatsByEvents, fetchNoteStats } = useNoteStats()
const [liking, setLiking] = useState(false)
const [isEmojiReactionsOpen, setIsEmojiReactionsOpen] = useState(false)
const [isPickerOpen, setIsPickerOpen] = useState(false)
+ const noteStats = useNoteStatsById(event.id)
const { myLastEmoji, likeCount } = useMemo(() => {
- const stats = noteStatsMap.get(event.id) || {}
+ const stats = noteStats || {}
const like = stats.likes?.find((like) => like.pubkey === pubkey)
return { myLastEmoji: like?.emoji, likeCount: stats.likes?.length }
- }, [noteStatsMap, event, pubkey])
+ }, [noteStats, pubkey])
const like = async (emoji: string) => {
checkLogin(async () => {
@@ -39,14 +40,13 @@ export default function LikeButton({ event }: { event: Event }) {
const timer = setTimeout(() => setLiking(false), 10_000)
try {
- const noteStats = noteStatsMap.get(event.id)
if (!noteStats?.updatedAt) {
- await fetchNoteStats(event)
+ await noteStatsService.fetchNoteStats(event, pubkey)
}
const reaction = createReactionDraftEvent(event, emoji)
const evt = await publish(reaction)
- updateNoteStatsByEvents([evt])
+ noteStatsService.updateNoteStatsByEvents([evt])
} catch (error) {
console.error('like failed', error)
} finally {
diff --git a/src/components/NoteStats/Likes.tsx b/src/components/NoteStats/Likes.tsx
index 621ce3b8..4b62875a 100644
--- a/src/components/NoteStats/Likes.tsx
+++ b/src/components/NoteStats/Likes.tsx
@@ -1,8 +1,9 @@
import { ScrollArea, ScrollBar } from '@/components/ui/scroll-area'
+import { useNoteStatsById } from '@/hooks/useNoteStatsById'
import { createReactionDraftEvent } from '@/lib/draft-event'
import { cn } from '@/lib/utils'
import { useNostr } from '@/providers/NostrProvider'
-import { useNoteStats } from '@/providers/NoteStatsProvider'
+import noteStatsService from '@/services/note-stats.service'
import { TEmoji } from '@/types'
import { Loader } from 'lucide-react'
import { Event } from 'nostr-tools'
@@ -11,10 +12,10 @@ import Emoji from '../Emoji'
export default function Likes({ event }: { event: Event }) {
const { pubkey, checkLogin, publish } = useNostr()
- const { noteStatsMap, updateNoteStatsByEvents } = useNoteStats()
+ const noteStats = useNoteStatsById(event.id)
const [liking, setLiking] = useState(null)
const likes = useMemo(() => {
- const _likes = noteStatsMap.get(event.id)?.likes
+ const _likes = noteStats?.likes
if (!_likes) return []
const stats = new Map }>()
@@ -26,7 +27,7 @@ export default function Likes({ event }: { event: Event }) {
stats.get(key)?.pubkeys.add(item.pubkey)
})
return Array.from(stats.values()).sort((a, b) => b.pubkeys.size - a.pubkeys.size)
- }, [noteStatsMap, event])
+ }, [noteStats, event])
if (!likes.length) return null
@@ -40,7 +41,7 @@ export default function Likes({ event }: { event: Event }) {
try {
const reaction = createReactionDraftEvent(event, emoji)
const evt = await publish(reaction)
- updateNoteStatsByEvents([evt])
+ noteStatsService.updateNoteStatsByEvents([evt])
} catch (error) {
console.error('like failed', error)
} finally {
diff --git a/src/components/NoteStats/RepostButton.tsx b/src/components/NoteStats/RepostButton.tsx
index f27c8789..10a41757 100644
--- a/src/components/NoteStats/RepostButton.tsx
+++ b/src/components/NoteStats/RepostButton.tsx
@@ -6,12 +6,13 @@ import {
DropdownMenuItem,
DropdownMenuTrigger
} from '@/components/ui/dropdown-menu'
+import { useNoteStatsById } from '@/hooks/useNoteStatsById'
import { createRepostDraftEvent } from '@/lib/draft-event'
import { getSharableEventId } from '@/lib/event'
import { cn } from '@/lib/utils'
import { useNostr } from '@/providers/NostrProvider'
-import { useNoteStats } from '@/providers/NoteStatsProvider'
import { useScreenSize } from '@/providers/ScreenSizeProvider'
+import noteStatsService from '@/services/note-stats.service'
import { Loader, PencilLine, Repeat } from 'lucide-react'
import { Event, kinds } from 'nostr-tools'
import { useMemo, useState } from 'react'
@@ -23,17 +24,16 @@ export default function RepostButton({ event }: { event: Event }) {
const { t } = useTranslation()
const { isSmallScreen } = useScreenSize()
const { publish, checkLogin, pubkey } = useNostr()
- const { noteStatsMap, updateNoteStatsByEvents, fetchNoteStats } = useNoteStats()
+ const noteStats = useNoteStatsById(event.id)
const [reposting, setReposting] = useState(false)
const [isPostDialogOpen, setIsPostDialogOpen] = useState(false)
const [isDrawerOpen, setIsDrawerOpen] = useState(false)
const { repostCount, hasReposted } = useMemo(() => {
- const stats = noteStatsMap.get(event.id) || {}
return {
- repostCount: stats.reposts?.size,
- hasReposted: pubkey ? stats.reposts?.has(pubkey) : false
+ repostCount: noteStats?.reposts?.size,
+ hasReposted: pubkey ? noteStats?.reposts?.has(pubkey) : false
}
- }, [noteStatsMap, event.id])
+ }, [noteStats, event.id])
const canRepost = !hasReposted && !reposting
const repost = async () => {
@@ -44,11 +44,10 @@ export default function RepostButton({ event }: { event: Event }) {
const timer = setTimeout(() => setReposting(false), 5000)
try {
- const noteStats = noteStatsMap.get(event.id)
const hasReposted = noteStats?.reposts?.has(pubkey)
if (hasReposted) return
if (!noteStats?.updatedAt) {
- const events = await fetchNoteStats(event)
+ const events = await noteStatsService.fetchNoteStats(event, pubkey)
if (events.some((e) => e.kind === kinds.Repost && e.pubkey === pubkey)) {
return
}
@@ -56,7 +55,7 @@ export default function RepostButton({ event }: { event: Event }) {
const repost = createRepostDraftEvent(event)
const evt = await publish(repost)
- updateNoteStatsByEvents([evt])
+ noteStatsService.updateNoteStatsByEvents([evt])
} catch (error) {
console.error('repost failed', error)
} finally {
diff --git a/src/components/NoteStats/TopZaps.tsx b/src/components/NoteStats/TopZaps.tsx
index 560b08fe..6378fead 100644
--- a/src/components/NoteStats/TopZaps.tsx
+++ b/src/components/NoteStats/TopZaps.tsx
@@ -1,6 +1,6 @@
import { ScrollArea, ScrollBar } from '@/components/ui/scroll-area'
+import { useNoteStatsById } from '@/hooks/useNoteStatsById'
import { formatAmount } from '@/lib/lightning'
-import { useNoteStats } from '@/providers/NoteStatsProvider'
import { Zap } from 'lucide-react'
import { Event } from 'nostr-tools'
import { useMemo, useState } from 'react'
@@ -8,12 +8,11 @@ import { SimpleUserAvatar } from '../UserAvatar'
import ZapDialog from '../ZapDialog'
export default function TopZaps({ event }: { event: Event }) {
- const { noteStatsMap } = useNoteStats()
+ const noteStats = useNoteStatsById(event.id)
const [zapIndex, setZapIndex] = useState(-1)
const topZaps = useMemo(() => {
- const stats = noteStatsMap.get(event.id) || {}
- return stats.zaps?.slice(0, 10) || []
- }, [noteStatsMap, event])
+ return noteStats?.zaps?.sort((a, b) => b.amount - a.amount).slice(0, 10) || []
+ }, [noteStats])
if (!topZaps.length) return null
diff --git a/src/components/NoteStats/ZapButton.tsx b/src/components/NoteStats/ZapButton.tsx
index 4faed6c7..6889fee2 100644
--- a/src/components/NoteStats/ZapButton.tsx
+++ b/src/components/NoteStats/ZapButton.tsx
@@ -1,10 +1,11 @@
+import { useNoteStatsById } from '@/hooks/useNoteStatsById'
import { getLightningAddressFromProfile } from '@/lib/lightning'
import { cn } from '@/lib/utils'
import { useNostr } from '@/providers/NostrProvider'
-import { useNoteStats } from '@/providers/NoteStatsProvider'
import { useZap } from '@/providers/ZapProvider'
import client from '@/services/client.service'
import lightning from '@/services/lightning.service'
+import noteStatsService from '@/services/note-stats.service'
import { Loader, Zap } from 'lucide-react'
import { Event } from 'nostr-tools'
import { MouseEvent, TouchEvent, useEffect, useMemo, useRef, useState } from 'react'
@@ -15,18 +16,17 @@ import ZapDialog from '../ZapDialog'
export default function ZapButton({ event }: { event: Event }) {
const { t } = useTranslation()
const { checkLogin, pubkey } = useNostr()
- const { noteStatsMap, addZap } = useNoteStats()
+ const noteStats = useNoteStatsById(event.id)
const { defaultZapSats, defaultZapComment, quickZap } = useZap()
const [touchStart, setTouchStart] = useState<{ x: number; y: number } | null>(null)
const [openZapDialog, setOpenZapDialog] = useState(false)
const [zapping, setZapping] = useState(false)
const { zapAmount, hasZapped } = useMemo(() => {
- const stats = noteStatsMap.get(event.id) || {}
return {
- zapAmount: stats.zaps?.reduce((acc, zap) => acc + zap.amount, 0),
- hasZapped: pubkey ? stats.zaps?.some((zap) => zap.pubkey === pubkey) : false
+ zapAmount: noteStats?.zaps?.reduce((acc, zap) => acc + zap.amount, 0),
+ hasZapped: pubkey ? noteStats?.zaps?.some((zap) => zap.pubkey === pubkey) : false
}
- }, [noteStatsMap, event, pubkey])
+ }, [noteStats, pubkey])
const [disable, setDisable] = useState(true)
const timerRef = useRef | null>(null)
const isLongPressRef = useRef(false)
@@ -57,7 +57,13 @@ export default function ZapButton({ event }: { event: Event }) {
if (!zapResult) {
return
}
- addZap(event.id, zapResult.invoice, defaultZapSats, defaultZapComment)
+ noteStatsService.addZap(
+ pubkey,
+ event.id,
+ zapResult.invoice,
+ defaultZapSats,
+ defaultZapComment
+ )
} catch (error) {
toast.error(`${t('Zap failed')}: ${(error as Error).message}`)
} finally {
diff --git a/src/components/NoteStats/index.tsx b/src/components/NoteStats/index.tsx
index f9017962..bb27cb1a 100644
--- a/src/components/NoteStats/index.tsx
+++ b/src/components/NoteStats/index.tsx
@@ -1,6 +1,7 @@
import { cn } from '@/lib/utils'
-import { useNoteStats } from '@/providers/NoteStatsProvider'
+import { useNostr } from '@/providers/NostrProvider'
import { useScreenSize } from '@/providers/ScreenSizeProvider'
+import noteStatsService from '@/services/note-stats.service'
import { Event } from 'nostr-tools'
import { useEffect, useState } from 'react'
import BookmarkButton from '../BookmarkButton'
@@ -28,13 +29,13 @@ export default function NoteStats({
displayTopZapsAndLikes?: boolean
}) {
const { isSmallScreen } = useScreenSize()
- const { fetchNoteStats } = useNoteStats()
+ const { pubkey } = useNostr()
const [loading, setLoading] = useState(false)
useEffect(() => {
if (!fetchIfNotExisting) return
setLoading(true)
- fetchNoteStats(event).finally(() => setLoading(false))
+ noteStatsService.fetchNoteStats(event, pubkey).finally(() => setLoading(false))
}, [event, fetchIfNotExisting])
if (isSmallScreen) {
diff --git a/src/components/NotificationList/index.tsx b/src/components/NotificationList/index.tsx
index ec6ae072..151e31fe 100644
--- a/src/components/NotificationList/index.tsx
+++ b/src/components/NotificationList/index.tsx
@@ -3,10 +3,10 @@ import { Skeleton } from '@/components/ui/skeleton'
import { BIG_RELAY_URLS, ExtendedKind } from '@/constants'
import { usePrimaryPage } from '@/PageManager'
import { useNostr } from '@/providers/NostrProvider'
-import { useNoteStats } from '@/providers/NoteStatsProvider'
import { useNotification } from '@/providers/NotificationProvider'
import { useUserTrust } from '@/providers/UserTrustProvider'
import client from '@/services/client.service'
+import noteStatsService from '@/services/note-stats.service'
import { TNotificationType } from '@/types'
import dayjs from 'dayjs'
import { Event, kinds } from 'nostr-tools'
@@ -25,7 +25,6 @@ const NotificationList = forwardRef((_, ref) => {
const { pubkey } = useNostr()
const { hideUntrustedNotifications, isUserTrusted } = useUserTrust()
const { clearNewNotifications, getNotificationsSeenAt } = useNotification()
- const { updateNoteStatsByEvents } = useNoteStats()
const [notificationType, setNotificationType] = useState('all')
const [lastReadTime, setLastReadTime] = useState(0)
const [refreshCount, setRefreshCount] = useState(0)
@@ -95,7 +94,7 @@ const NotificationList = forwardRef((_, ref) => {
if (eosed) {
setLoading(false)
setUntil(events.length > 0 ? events[events.length - 1].created_at - 1 : undefined)
- updateNoteStatsByEvents(events)
+ noteStatsService.updateNoteStatsByEvents(events)
}
},
onNew: (event) => {
@@ -109,7 +108,7 @@ const NotificationList = forwardRef((_, ref) => {
}
return [...oldEvents.slice(0, index), event, ...oldEvents.slice(index)]
})
- updateNoteStatsByEvents([event])
+ noteStatsService.updateNoteStatsByEvents([event])
}
}
)
diff --git a/src/components/ZapDialog/index.tsx b/src/components/ZapDialog/index.tsx
index e7e1d253..33fc6225 100644
--- a/src/components/ZapDialog/index.tsx
+++ b/src/components/ZapDialog/index.tsx
@@ -16,10 +16,10 @@ import {
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { useNostr } from '@/providers/NostrProvider'
-import { useNoteStats } from '@/providers/NoteStatsProvider'
import { useScreenSize } from '@/providers/ScreenSizeProvider'
import { useZap } from '@/providers/ZapProvider'
import lightning from '@/services/lightning.service'
+import noteStatsService from '@/services/note-stats.service'
import { Loader } from 'lucide-react'
import { Dispatch, SetStateAction, useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
@@ -136,7 +136,6 @@ function ZapDialogContent({
const { t } = useTranslation()
const { pubkey } = useNostr()
const { defaultZapSats, defaultZapComment } = useZap()
- const { addZap } = useNoteStats()
const [sats, setSats] = useState(defaultAmount ?? defaultZapSats)
const [comment, setComment] = useState(defaultComment ?? defaultZapComment)
const [zapping, setZapping] = useState(false)
@@ -155,7 +154,7 @@ function ZapDialogContent({
return
}
if (eventId) {
- addZap(eventId, zapResult.invoice, sats, comment)
+ noteStatsService.addZap(pubkey, eventId, zapResult.invoice, sats, comment)
}
} catch (error) {
toast.error(`${t('Zap failed')}: ${(error as Error).message}`)
diff --git a/src/hooks/useNoteStatsById.tsx b/src/hooks/useNoteStatsById.tsx
new file mode 100644
index 00000000..4d4ef74b
--- /dev/null
+++ b/src/hooks/useNoteStatsById.tsx
@@ -0,0 +1,9 @@
+import noteStats from '@/services/note-stats.service'
+import { useSyncExternalStore } from 'react'
+
+export function useNoteStatsById(noteId: string) {
+ return useSyncExternalStore(
+ (cb) => noteStats.subscribeNoteStats(noteId, cb),
+ () => noteStats.getNoteStats(noteId)
+ )
+}
diff --git a/src/providers/NostrProvider/index.tsx b/src/providers/NostrProvider/index.tsx
index 75061c42..88376927 100644
--- a/src/providers/NostrProvider/index.tsx
+++ b/src/providers/NostrProvider/index.tsx
@@ -11,6 +11,7 @@ import { formatPubkey, isValidPubkey, pubkeyToNpub } from '@/lib/pubkey'
import client from '@/services/client.service'
import indexedDb from '@/services/indexed-db.service'
import storage from '@/services/local-storage.service'
+import noteStatsService from '@/services/note-stats.service'
import { ISigner, TAccount, TAccountPointer, TDraftEvent, TProfile, TRelayList } from '@/types'
import { hexToBytes } from '@noble/hashes/utils'
import dayjs from 'dayjs'
@@ -276,6 +277,29 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
}
}, [account])
+ useEffect(() => {
+ if (!account) return
+
+ const initInteractions = async () => {
+ const pubkey = account.pubkey
+ const relayList = await client.fetchRelayList(pubkey)
+ const events = await client.fetchEvents(relayList.write.slice(0, 4), [
+ {
+ authors: [pubkey],
+ kinds: [kinds.Reaction, kinds.Repost],
+ limit: 100
+ },
+ {
+ '#P': [pubkey],
+ kinds: [kinds.Zap],
+ limit: 100
+ }
+ ])
+ noteStatsService.updateNoteStatsByEvents(events)
+ }
+ initInteractions()
+ }, [account])
+
useEffect(() => {
if (signer) {
client.signer = signer
diff --git a/src/providers/NoteStatsProvider.tsx b/src/providers/NoteStatsProvider.tsx
deleted file mode 100644
index 6f33b6c4..00000000
--- a/src/providers/NoteStatsProvider.tsx
+++ /dev/null
@@ -1,243 +0,0 @@
-import { extractEmojiInfosFromTags, extractZapInfoFromReceipt } from '@/lib/event'
-import { tagNameEquals } from '@/lib/tag'
-import client from '@/services/client.service'
-import { TEmoji } from '@/types'
-import dayjs from 'dayjs'
-import { Event, Filter, kinds } from 'nostr-tools'
-import { createContext, useContext, useEffect, useState } from 'react'
-import { useNostr } from './NostrProvider'
-
-export type TNoteStats = {
- likes: { id: string; pubkey: string; created_at: number; emoji: TEmoji | string }[]
- reposts: Set
- zaps: { pr: string; pubkey: string; amount: number; comment?: string }[]
- updatedAt?: number
-}
-
-type TNoteStatsContext = {
- noteStatsMap: Map>
- addZap: (eventId: string, pr: string, amount: number, comment?: string) => void
- updateNoteStatsByEvents: (events: Event[]) => void
- fetchNoteStats: (event: Event) => Promise
-}
-
-const NoteStatsContext = createContext(undefined)
-
-export const useNoteStats = () => {
- const context = useContext(NoteStatsContext)
- if (!context) {
- throw new Error('useNoteStats must be used within a NoteStatsProvider')
- }
- return context
-}
-
-export function NoteStatsProvider({ children }: { children: React.ReactNode }) {
- const [noteStatsMap, setNoteStatsMap] = useState