Browse Source

bug-fixes

imwald
Silberengel 1 month ago
parent
commit
3f050bb18a
  1. 24
      src/components/Profile/ProfileInteractionsAccordion.tsx
  2. 45
      src/hooks/useProfileBadges.tsx
  3. 44
      src/hooks/useProfileFollowPacks.tsx
  4. 55
      src/hooks/useProfileInteractions.tsx
  5. 66
      src/hooks/useProfileRelayUrls.tsx
  6. 38
      src/hooks/useProfileReports.tsx
  7. 161
      src/lib/profile-accordion-session-cache.ts
  8. 12
      src/services/indexed-db.service.ts

24
src/components/Profile/ProfileInteractionsAccordion.tsx

@ -19,9 +19,15 @@ type Props = { @@ -19,9 +19,15 @@ type Props = {
onRefreshReady?: (refresh: (() => void) | null) => void
}
function ProfileInteractionsContent({ pubkey, relayUrls, onRefreshReady }: {
function ProfileInteractionsContent({
pubkey,
relayUrls,
refreshRelayUrls,
onRefreshReady
}: {
pubkey: string
relayUrls: string[] | undefined
refreshRelayUrls: () => void | Promise<void>
onRefreshReady?: (refresh: (() => void) | null) => void
}) {
const { pubkey: viewerPubkey } = useNostr()
@ -32,14 +38,17 @@ function ProfileInteractionsContent({ pubkey, relayUrls, onRefreshReady }: { @@ -32,14 +38,17 @@ function ProfileInteractionsContent({ pubkey, relayUrls, onRefreshReady }: {
useEffect(() => {
const doRefresh = () => {
refresh()
refreshBadges()
refreshFollowPacks()
refreshReports()
void (async () => {
await refreshRelayUrls()
refresh()
refreshBadges()
refreshFollowPacks()
refreshReports()
})()
}
onRefreshReady?.(doRefresh)
return () => { onRefreshReady?.(null) }
}, [refresh, refreshBadges, refreshFollowPacks, refreshReports, onRefreshReady])
}, [refreshRelayUrls, refresh, refreshBadges, refreshFollowPacks, refreshReports, onRefreshReady])
return (
<ProfileHeaderInteractions
@ -93,7 +102,7 @@ export default function ProfileInteractionsAccordion({ @@ -93,7 +102,7 @@ export default function ProfileInteractionsAccordion({
onRefreshReady
}: Props) {
const { t } = useTranslation()
const { relayUrls, loading: relayUrlsLoading } = useProfileRelayUrls(pubkey, isExpanded)
const { relayUrls, loading: relayUrlsLoading, refresh: refreshRelayUrls } = useProfileRelayUrls(pubkey, isExpanded)
const relaysReady = !relayUrlsLoading
const hasContent = isExpanded && pubkey
@ -118,6 +127,7 @@ export default function ProfileInteractionsAccordion({ @@ -118,6 +127,7 @@ export default function ProfileInteractionsAccordion({
<ProfileInteractionsContent
pubkey={pubkey}
relayUrls={relayUrls.length > 0 ? relayUrls : undefined}
refreshRelayUrls={refreshRelayUrls}
onRefreshReady={onRefreshReady}
/>
</div>

45
src/hooks/useProfileBadges.tsx

@ -1,5 +1,11 @@ @@ -1,5 +1,11 @@
import { ExtendedKind } from '@/constants'
import { extractBadgeDefinitionMedia } from '@/lib/badge-definition-media'
import {
profileAccordionGetCachedBadges,
profileAccordionInvalidate,
profileAccordionRelayUrlsKey,
profileAccordionSetBadges
} from '@/lib/profile-accordion-session-cache'
import { queryService, replaceableEventService } from '@/services/client.service'
import { useCallback, useEffect, useRef, useState } from 'react'
import { tagNameEquals } from '@/lib/tag'
@ -38,18 +44,37 @@ export function useProfileBadges(pubkey: string | undefined, relayUrls?: string[ @@ -38,18 +44,37 @@ export function useProfileBadges(pubkey: string | undefined, relayUrls?: string[
const [loading, setLoading] = useState(false)
const fetchIdRef = useRef(0)
const fetchBadges = useCallback(async () => {
const fetchBadges = useCallback(async (force = false) => {
const myFetchId = (fetchIdRef.current += 1)
if (!pubkey) {
setBadges([])
if (myFetchId === fetchIdRef.current) {
setBadges([])
setLoading(false)
}
return
}
const myFetchId = (fetchIdRef.current += 1)
const urls =
force || !(relayUrls && relayUrls.length > 0)
? await buildProfileRelayUrls(pubkey, blockedRelays)
: relayUrls
const relayKey = profileAccordionRelayUrlsKey(urls)
if (!force) {
const cached = profileAccordionGetCachedBadges(pubkey, relayKey)
if (cached) {
if (myFetchId !== fetchIdRef.current) return
setBadges(cached)
setLoading(false)
return
}
}
if (myFetchId !== fetchIdRef.current) return
setLoading(true)
try {
const urls = relayUrls ?? (await buildProfileRelayUrls(pubkey, blockedRelays))
const events = await queryService.fetchEvents(
urls,
{ authors: [pubkey], kinds: [ExtendedKind.PROFILE_BADGES], '#d': ['profile_badges'] },
@ -112,6 +137,7 @@ export function useProfileBadges(pubkey: string | undefined, relayUrls?: string[ @@ -112,6 +137,7 @@ export function useProfileBadges(pubkey: string | undefined, relayUrls?: string[
if (myFetchId !== fetchIdRef.current) return
setBadges(result)
profileAccordionSetBadges(pubkey, relayKey, result)
} catch {
if (myFetchId !== fetchIdRef.current) return
setBadges([])
@ -120,9 +146,14 @@ export function useProfileBadges(pubkey: string | undefined, relayUrls?: string[ @@ -120,9 +146,14 @@ export function useProfileBadges(pubkey: string | undefined, relayUrls?: string[
}
}, [pubkey, blockedRelays, relayUrls])
const refresh = useCallback(() => {
if (pubkey) profileAccordionInvalidate(pubkey, 'badges')
void fetchBadges(true)
}, [pubkey, fetchBadges])
useEffect(() => {
fetchBadges()
void fetchBadges(false)
}, [fetchBadges])
return { badges, loading, refresh: fetchBadges }
return { badges, loading, refresh }
}

44
src/hooks/useProfileFollowPacks.tsx

@ -1,4 +1,10 @@ @@ -1,4 +1,10 @@
import { ExtendedKind } from '@/constants'
import {
profileAccordionGetCachedFollowPacks,
profileAccordionInvalidate,
profileAccordionRelayUrlsKey,
profileAccordionSetFollowPacks
} from '@/lib/profile-accordion-session-cache'
import { queryService } from '@/services/client.service'
import { Event } from 'nostr-tools'
import { useCallback, useEffect, useRef, useState } from 'react'
@ -25,17 +31,37 @@ export function useProfileFollowPacks( @@ -25,17 +31,37 @@ export function useProfileFollowPacks(
const [loading, setLoading] = useState(false)
const fetchIdRef = useRef(0)
const fetchPacks = useCallback(async () => {
const fetchPacks = useCallback(async (force = false) => {
const myFetchId = (fetchIdRef.current += 1)
if (!pubkey) {
setPacks([])
if (myFetchId === fetchIdRef.current) {
setPacks([])
setLoading(false)
}
return
}
const myFetchId = (fetchIdRef.current += 1)
const urls =
force || !(relayUrls && relayUrls.length > 0)
? await buildProfileRelayUrls(pubkey, blockedRelays)
: relayUrls
const relayKey = profileAccordionRelayUrlsKey(urls)
if (!force && urls.length > 0) {
const cached = profileAccordionGetCachedFollowPacks(pubkey, relayKey)
if (cached) {
if (myFetchId !== fetchIdRef.current) return
setPacks(cached)
setLoading(false)
return
}
}
if (myFetchId !== fetchIdRef.current) return
setLoading(true)
try {
const urls = relayUrls ?? (await buildProfileRelayUrls(pubkey, blockedRelays))
if (urls.length === 0) {
if (myFetchId === fetchIdRef.current) setPacks([])
return
@ -54,6 +80,7 @@ export function useProfileFollowPacks( @@ -54,6 +80,7 @@ export function useProfileFollowPacks(
title: getPackTitle(evt)
}))
setPacks(result)
profileAccordionSetFollowPacks(pubkey, relayKey, result)
} catch {
if (myFetchId !== fetchIdRef.current) return
setPacks([])
@ -62,9 +89,14 @@ export function useProfileFollowPacks( @@ -62,9 +89,14 @@ export function useProfileFollowPacks(
}
}, [pubkey, blockedRelays, relayUrls])
const refresh = useCallback(() => {
if (pubkey) profileAccordionInvalidate(pubkey, 'followPacks')
void fetchPacks(true)
}, [pubkey, fetchPacks])
useEffect(() => {
fetchPacks()
void fetchPacks(false)
}, [fetchPacks])
return { packs, loading, refresh: fetchPacks }
return { packs, loading, refresh }
}

55
src/hooks/useProfileInteractions.tsx

@ -4,6 +4,12 @@ import { queryService, replaceableEventService } from '@/services/client.service @@ -4,6 +4,12 @@ import { queryService, replaceableEventService } from '@/services/client.service
import { hexPubkeysEqual } from '@/lib/pubkey'
import { Event, Filter, kinds } from 'nostr-tools'
import { useCallback, useEffect, useRef, useState } from 'react'
import {
profileAccordionGetCachedInteractions,
profileAccordionInvalidate,
profileAccordionRelayUrlsKey,
profileAccordionSetInteractions
} from '@/lib/profile-accordion-session-cache'
import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider'
import { buildProfileRelayUrls } from '@/lib/profile-relay-urls'
@ -27,20 +33,41 @@ export function useProfileInteractions(pubkey: string | undefined, relayUrls?: s @@ -27,20 +33,41 @@ export function useProfileInteractions(pubkey: string | undefined, relayUrls?: s
const [loading, setLoading] = useState(false)
const fetchIdRef = useRef(0)
const fetchAll = useCallback(async () => {
const fetchAll = useCallback(async (force = false) => {
const myFetchId = (fetchIdRef.current += 1)
if (!pubkey) {
setZaps([])
setReactions([])
setComments([])
if (myFetchId === fetchIdRef.current) {
setZaps([])
setReactions([])
setComments([])
setLoading(false)
}
return
}
const myFetchId = (fetchIdRef.current += 1)
const urls =
force || !(relayUrls && relayUrls.length > 0)
? await buildProfileRelayUrls(pubkey, blockedRelays)
: relayUrls
const relayKey = profileAccordionRelayUrlsKey(urls)
if (!force) {
const cached = profileAccordionGetCachedInteractions(pubkey, relayKey)
if (cached) {
if (myFetchId !== fetchIdRef.current) return
setZaps(cached.zaps)
setReactions(cached.reactions)
setComments(cached.comments)
setLoading(false)
return
}
}
if (myFetchId !== fetchIdRef.current) return
setLoading(true)
try {
const urls = relayUrls ?? (await buildProfileRelayUrls(pubkey, blockedRelays))
const profileMetaPromise = replaceableEventService.fetchReplaceableEvent(
pubkey,
kinds.Metadata,
@ -189,6 +216,11 @@ export function useProfileInteractions(pubkey: string | undefined, relayUrls?: s @@ -189,6 +216,11 @@ export function useProfileInteractions(pubkey: string | undefined, relayUrls?: s
setZaps(collectedZaps)
setReactions(collectedReactions)
setComments(collectedComments)
profileAccordionSetInteractions(pubkey, relayKey, {
zaps: collectedZaps,
reactions: collectedReactions,
comments: collectedComments
})
} catch {
if (myFetchId !== fetchIdRef.current) return
} finally {
@ -196,11 +228,16 @@ export function useProfileInteractions(pubkey: string | undefined, relayUrls?: s @@ -196,11 +228,16 @@ export function useProfileInteractions(pubkey: string | undefined, relayUrls?: s
}
}, [pubkey, blockedRelays, relayUrls])
const refresh = useCallback(() => {
if (pubkey) profileAccordionInvalidate(pubkey, 'interactions')
void fetchAll(true)
}, [pubkey, fetchAll])
useEffect(() => {
fetchAll()
void fetchAll(false)
}, [fetchAll])
return { zaps, reactions, comments, loading, refresh: fetchAll }
return { zaps, reactions, comments, loading, refresh }
}
/** @deprecated Use useProfileInteractions instead. Returns zaps only for compatibility. */

66
src/hooks/useProfileRelayUrls.tsx

@ -1,3 +1,8 @@ @@ -1,3 +1,8 @@
import {
profileAccordionGetCachedRelayUrls,
profileAccordionInvalidate,
profileAccordionSetRelayUrls
} from '@/lib/profile-accordion-session-cache'
import { buildProfileRelayUrls } from '@/lib/profile-relay-urls'
import { useCallback, useEffect, useState } from 'react'
import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider'
@ -8,26 +13,57 @@ export function useProfileRelayUrls(pubkey: string | undefined, enabled: boolean @@ -8,26 +13,57 @@ export function useProfileRelayUrls(pubkey: string | undefined, enabled: boolean
const [relayUrls, setRelayUrls] = useState<string[]>([])
const [loading, setLoading] = useState(false)
const fetch = useCallback(async () => {
if (!pubkey || !enabled) {
const fetch = useCallback(
async (force = false) => {
if (!pubkey) {
setRelayUrls([])
setLoading(false)
return
}
if (!force) {
const cached = profileAccordionGetCachedRelayUrls(pubkey)
if (cached?.length) {
setRelayUrls(cached)
setLoading(false)
return
}
}
setLoading(true)
try {
const urls = await buildProfileRelayUrls(pubkey, blockedRelays)
profileAccordionSetRelayUrls(pubkey, urls)
setRelayUrls(urls)
} catch {
setRelayUrls([])
} finally {
setLoading(false)
}
},
[pubkey, blockedRelays]
)
const refresh = useCallback(() => {
if (pubkey) profileAccordionInvalidate(pubkey, 'relayUrls')
if (!pubkey) return Promise.resolve()
return fetch(true)
}, [pubkey, fetch])
useEffect(() => {
if (!pubkey) {
setRelayUrls([])
setLoading(false)
return
}
setLoading(true)
try {
const urls = await buildProfileRelayUrls(pubkey, blockedRelays)
setRelayUrls(urls)
} catch {
setRelayUrls([])
} finally {
if (!enabled) {
const cached = profileAccordionGetCachedRelayUrls(pubkey)
setRelayUrls(cached ?? [])
setLoading(false)
return
}
}, [pubkey, enabled, blockedRelays])
useEffect(() => {
fetch()
}, [fetch])
void fetch(false)
}, [pubkey, enabled, fetch])
return { relayUrls, loading, refresh: fetch }
return { relayUrls, loading, refresh }
}

38
src/hooks/useProfileReports.tsx

@ -1,5 +1,10 @@ @@ -1,5 +1,10 @@
import { ExtendedKind } from '@/constants'
import { buildProfileReportRelayUrls } from '@/lib/profile-report-relay-urls'
import {
profileAccordionGetCachedReports,
profileAccordionInvalidate,
profileAccordionSetReports
} from '@/lib/profile-accordion-session-cache'
import { queryService } from '@/services/client.service'
import { Event } from 'nostr-tools'
import { useCallback, useEffect, useRef, useState } from 'react'
@ -17,15 +22,29 @@ export function useProfileReports( @@ -17,15 +22,29 @@ export function useProfileReports(
const [loading, setLoading] = useState(false)
const fetchIdRef = useRef(0)
const fetchReports = useCallback(async () => {
const fetchReports = useCallback(async (force = false) => {
const viewer = viewerPubkey?.trim()
const myFetchId = (fetchIdRef.current += 1)
if (!profilePubkey || !viewer) {
setReports([])
setLoading(false)
if (myFetchId === fetchIdRef.current) {
setReports([])
setLoading(false)
}
return
}
const myFetchId = (fetchIdRef.current += 1)
if (!force) {
const cached = profileAccordionGetCachedReports(profilePubkey, viewer)
if (cached) {
if (myFetchId !== fetchIdRef.current) return
setReports(cached)
setLoading(false)
return
}
}
if (myFetchId !== fetchIdRef.current) return
setLoading(true)
try {
@ -56,6 +75,7 @@ export function useProfileReports( @@ -56,6 +75,7 @@ export function useProfileReports(
}
deduped.sort((a, b) => b.created_at - a.created_at)
setReports(deduped)
profileAccordionSetReports(profilePubkey, viewer, deduped)
} catch {
if (myFetchId !== fetchIdRef.current) return
setReports([])
@ -64,9 +84,15 @@ export function useProfileReports( @@ -64,9 +84,15 @@ export function useProfileReports(
}
}, [profilePubkey, viewerPubkey, favoriteRelays, blockedRelays])
const refresh = useCallback(() => {
const v = viewerPubkey?.trim()
if (profilePubkey && v) profileAccordionInvalidate(profilePubkey, 'reports')
void fetchReports(true)
}, [profilePubkey, viewerPubkey, fetchReports])
useEffect(() => {
fetchReports()
void fetchReports(false)
}, [fetchReports])
return { reports, loading, refresh: fetchReports }
return { reports, loading, refresh }
}

161
src/lib/profile-accordion-session-cache.ts

@ -0,0 +1,161 @@ @@ -0,0 +1,161 @@
/**
* In-memory session cache for profile accordion fetches (per viewed profile pubkey).
* Survives collapsing/reopening the accordion; cleared on full page reload.
*/
import type { TProfileZap } from '@/hooks/useProfileInteractions'
import type { TProfileBadge } from '@/hooks/useProfileBadges'
import type { TProfileFollowPack } from '@/hooks/useProfileFollowPacks'
import type { Event } from 'nostr-tools'
export type ProfileAccordionInteractionsSnapshot = {
zaps: TProfileZap[]
reactions: Event[]
comments: Event[]
}
type Entry = {
relayUrls?: string[]
/** Fingerprint of relays used for interaction/badge/pack slices */
relayUrlsKey?: string
interactions?: ProfileAccordionInteractionsSnapshot
badges?: TProfileBadge[]
followPacks?: TProfileFollowPack[]
/** viewer hex pubkey → reports */
reportsByViewer?: Record<string, Event[]>
}
const store = new Map<string, Entry>()
export function profileAccordionRelayUrlsKey(urls: string[]): string {
if (urls.length === 0) return ''
return [...urls].sort().join('|')
}
function getEntry(pubkey: string): Entry {
let e = store.get(pubkey)
if (!e) {
e = {}
store.set(pubkey, e)
}
return e
}
export function profileAccordionGetCachedRelayUrls(pubkey: string): string[] | undefined {
const urls = getEntry(pubkey).relayUrls
return urls?.length ? urls : undefined
}
export function profileAccordionSetRelayUrls(pubkey: string, urls: string[]): void {
const e = getEntry(pubkey)
const key = profileAccordionRelayUrlsKey(urls)
if (e.relayUrlsKey && e.relayUrlsKey !== key) {
delete e.interactions
delete e.badges
delete e.followPacks
}
e.relayUrls = urls
e.relayUrlsKey = key
}
export function profileAccordionGetCachedInteractions(
pubkey: string,
relayKey: string
): ProfileAccordionInteractionsSnapshot | undefined {
const e = store.get(pubkey)
if (!e?.interactions || e.relayUrlsKey !== relayKey) return undefined
return e.interactions
}
export function profileAccordionSetInteractions(
pubkey: string,
relayKey: string,
data: ProfileAccordionInteractionsSnapshot
): void {
const e = getEntry(pubkey)
e.relayUrlsKey = relayKey
e.interactions = data
}
export function profileAccordionGetCachedBadges(pubkey: string, relayKey: string): TProfileBadge[] | undefined {
const e = store.get(pubkey)
if (!e?.badges || e.relayUrlsKey !== relayKey) return undefined
return e.badges
}
export function profileAccordionSetBadges(pubkey: string, relayKey: string, badges: TProfileBadge[]): void {
const e = getEntry(pubkey)
e.relayUrlsKey = relayKey
e.badges = badges
}
export function profileAccordionGetCachedFollowPacks(
pubkey: string,
relayKey: string
): TProfileFollowPack[] | undefined {
const e = store.get(pubkey)
if (!e?.followPacks || e.relayUrlsKey !== relayKey) return undefined
return e.followPacks
}
export function profileAccordionSetFollowPacks(
pubkey: string,
relayKey: string,
packs: TProfileFollowPack[]
): void {
const e = getEntry(pubkey)
e.relayUrlsKey = relayKey
e.followPacks = packs
}
export function profileAccordionGetCachedReports(profilePubkey: string, viewerPubkey: string): Event[] | undefined {
return getEntry(profilePubkey).reportsByViewer?.[viewerPubkey]
}
export function profileAccordionSetReports(
profilePubkey: string,
viewerPubkey: string,
reports: Event[]
): void {
const e = getEntry(profilePubkey)
if (!e.reportsByViewer) e.reportsByViewer = {}
e.reportsByViewer[viewerPubkey] = reports
}
export type ProfileAccordionCacheSlice =
| 'relayUrls'
| 'interactions'
| 'badges'
| 'followPacks'
| 'reports'
| 'all'
export function profileAccordionInvalidate(pubkey: string, slice: ProfileAccordionCacheSlice = 'all'): void {
if (slice === 'all') {
store.delete(pubkey)
return
}
const e = store.get(pubkey)
if (!e) return
switch (slice) {
case 'relayUrls':
delete e.relayUrls
delete e.relayUrlsKey
delete e.interactions
delete e.badges
delete e.followPacks
break
case 'interactions':
delete e.interactions
break
case 'badges':
delete e.badges
break
case 'followPacks':
delete e.followPacks
break
case 'reports':
delete e.reportsByViewer
break
}
}

12
src/services/indexed-db.service.ts

@ -48,11 +48,13 @@ export const StoreNames = { @@ -48,11 +48,13 @@ export const StoreNames = {
/** NIP-A7 spell events (kind 777). Key: event id. */
SPELL_EVENTS: 'spellEvents',
/** Tombstone list for deleted events (kind 5). Key: event id or replaceable coordinate. */
TOMBSTONE_LIST: 'tombstoneList'
TOMBSTONE_LIST: 'tombstoneList',
/** NIP-58 badge definitions (kind 30009). Key: pubkey:d */
BADGE_DEFINITION_EVENTS: 'badgeDefinitionEvents'
}
/** Schema version we expect. When adding stores or migrations, bump this. */
const DB_VERSION = 27
const DB_VERSION = 28
/** Max age for profile and payment info cache before we refetch (5 min). */
const PROFILE_AND_PAYMENT_CACHE_MAX_AGE_MS = 5 * 60 * 1000
@ -230,6 +232,9 @@ class IndexedDbService { @@ -230,6 +232,9 @@ class IndexedDbService {
if (!db.objectStoreNames.contains(StoreNames.TOMBSTONE_LIST)) {
db.createObjectStore(StoreNames.TOMBSTONE_LIST, { keyPath: 'key' })
}
if (!db.objectStoreNames.contains(StoreNames.BADGE_DEFINITION_EVENTS)) {
db.createObjectStore(StoreNames.BADGE_DEFINITION_EVENTS, { keyPath: 'key' })
}
}
}
);
@ -841,6 +846,8 @@ class IndexedDbService { @@ -841,6 +846,8 @@ class IndexedDbService {
case ExtendedKind.WIKI_ARTICLE:
case kinds.LongFormArticle:
return StoreNames.PUBLICATION_EVENTS
case ExtendedKind.BADGE_DEFINITION:
return StoreNames.BADGE_DEFINITION_EVENTS
default:
return undefined
}
@ -1458,6 +1465,7 @@ class IndexedDbService { @@ -1458,6 +1465,7 @@ class IndexedDbService {
if (storeName === StoreNames.USER_EMOJI_LIST_EVENTS) return kinds.UserEmojiList
if (storeName === StoreNames.EMOJI_SET_EVENTS) return kinds.Emojisets
if (storeName === StoreNames.PAYMENT_INFO_EVENTS) return ExtendedKind.PAYMENT_INFO
if (storeName === StoreNames.BADGE_DEFINITION_EVENTS) return ExtendedKind.BADGE_DEFINITION
// PUBLICATION_EVENTS is not replaceable, so we don't handle it here
return undefined
}

Loading…
Cancel
Save