Browse Source

fix badges and reports

imwald
Silberengel 4 weeks ago
parent
commit
1fe8c5df56
  1. 1
      nip66-cron/index.mjs
  2. 1
      src/constants.ts
  3. 38
      src/hooks/useProfileReportsEvents.tsx
  4. 45
      src/hooks/useProfileWall.tsx
  5. 24
      src/lib/nip58-profile-badges-list.ts
  6. 8
      src/lib/replaceable-list-latest.ts

1
nip66-cron/index.mjs

@ -76,7 +76,6 @@ const DEFAULT_RELAYS_TO_MONITOR = [
/** Relays to publish 30166/10166 and to REQ kind 10002 from; broad enough for Imwald + NIP-66 discovery. */ /** Relays to publish 30166/10166 and to REQ kind 10002 from; broad enough for Imwald + NIP-66 discovery. */
const DEFAULT_PUBLISH_RELAYS = [ const DEFAULT_PUBLISH_RELAYS = [
'wss://nos.lol', 'wss://nos.lol',
'wss://orly-relay.imwald.eu',
'wss://relay.damus.io', 'wss://relay.damus.io',
'wss://relay.nostr.watch', 'wss://relay.nostr.watch',
'wss://relay.primal.net', 'wss://relay.primal.net',

1
src/constants.ts

@ -493,7 +493,6 @@ export const GIF_RELAY_URLS = [
export const SEARCHABLE_RELAY_URLS = [ export const SEARCHABLE_RELAY_URLS = [
'wss://search.nos.today', 'wss://search.nos.today',
'wss://nostr.wine', 'wss://nostr.wine',
'wss://orly-relay.imwald.eu',
'wss://relay.noswhere.com', 'wss://relay.noswhere.com',
'wss://nostr-pub.wellorder.net', 'wss://nostr-pub.wellorder.net',
] ]

38
src/hooks/useProfileReportsEvents.tsx

@ -89,10 +89,20 @@ export function useProfileReportsEvents({
const receivedCached = memoryByKey.get(receivedCacheKey) const receivedCached = memoryByKey.get(receivedCacheKey)
const madeCached = memoryByKey.get(madeCacheKey) const madeCached = memoryByKey.get(madeCacheKey)
const reportsCacheHasRows =
const [received, setReceived] = useState<Event[]>(receivedCached?.events ?? []) (receivedCached?.events.length ?? 0) + (madeCached?.events.length ?? 0) > 0
const [made, setMade] = useState<Event[]>(madeCached?.events ?? []) const reportsCacheFresh =
const [isLoading, setIsLoading] = useState(!receivedCached || !madeCached) !!receivedCached &&
!!madeCached &&
Date.now() - receivedCached.lastUpdated < CACHE_DURATION &&
Date.now() - madeCached.lastUpdated < CACHE_DURATION
const hasUsefulReportsCache = reportsCacheHasRows && reportsCacheFresh
const [received, setReceived] = useState<Event[]>(
hasUsefulReportsCache ? receivedCached!.events : []
)
const [made, setMade] = useState<Event[]>(hasUsefulReportsCache ? madeCached!.events : [])
const [isLoading, setIsLoading] = useState(!hasUsefulReportsCache)
const [refreshToken, setRefreshToken] = useState(0) const [refreshToken, setRefreshToken] = useState(0)
const includeAuthorLocalRelays = useMemo(() => { const includeAuthorLocalRelays = useMemo(() => {
@ -118,6 +128,7 @@ export function useProfileReportsEvents({
blockedRelaysRef.current = blockedRelays blockedRelaysRef.current = blockedRelays
const useGlobalRelayBootstrapRef = useRef(useGlobalRelayBootstrap) const useGlobalRelayBootstrapRef = useRef(useGlobalRelayBootstrap)
useGlobalRelayBootstrapRef.current = useGlobalRelayBootstrap useGlobalRelayBootstrapRef.current = useGlobalRelayBootstrap
const runGenRef = useRef(0)
const resolveFeedUrls = useCallback( const resolveFeedUrls = useCallback(
( (
@ -174,6 +185,7 @@ export function useProfileReportsEvents({
useEffect(() => { useEffect(() => {
let cancelled = false let cancelled = false
const runGen = ++runGenRef.current
const loadMode = async ( const loadMode = async (
mode: FetchMode, mode: FetchMode,
@ -196,7 +208,11 @@ export function useProfileReportsEvents({
isEventDeletedRef.current, isEventDeletedRef.current,
postFilter(pubkey, mode) postFilter(pubkey, mode)
) )
memoryByKey.set(cacheKey, { events: processed, lastUpdated: Date.now() }) if (processed.length > 0) {
memoryByKey.set(cacheKey, { events: processed, lastUpdated: Date.now() })
} else {
memoryByKey.delete(cacheKey)
}
setEvents((prev) => (eventsEqualById(prev, processed) ? prev : processed)) setEvents((prev) => (eventsEqualById(prev, processed) ? prev : processed))
} }
@ -284,15 +300,21 @@ export function useProfileReportsEvents({
const madeMem = memoryByKey.get(madeCacheKey) const madeMem = memoryByKey.get(madeCacheKey)
const recvFresh = recvMem && Date.now() - recvMem.lastUpdated < CACHE_DURATION const recvFresh = recvMem && Date.now() - recvMem.lastUpdated < CACHE_DURATION
const madeFresh = madeMem && Date.now() - madeMem.lastUpdated < CACHE_DURATION const madeFresh = madeMem && Date.now() - madeMem.lastUpdated < CACHE_DURATION
const cachedAny =
(recvMem?.events.length ?? 0) + (madeMem?.events.length ?? 0) > 0
if (recvFresh && recvMem) { if (recvFresh && recvMem) {
setReceived(recvMem.events) setReceived(recvMem.events)
} else if (recvMem?.events.length === 0) {
memoryByKey.delete(receivedCacheKey)
} }
if (madeFresh && madeMem) { if (madeFresh && madeMem) {
setMade(madeMem.events) setMade(madeMem.events)
} else if (madeMem?.events.length === 0) {
memoryByKey.delete(madeCacheKey)
} }
if (recvFresh && madeFresh && refreshToken === 0) { if (recvFresh && madeFresh && refreshToken === 0 && cachedAny) {
setIsLoading(false) if (runGen === runGenRef.current) setIsLoading(false)
return return
} }
@ -303,7 +325,7 @@ export function useProfileReportsEvents({
loadMode('made', madeCacheKey, setMade) loadMode('made', madeCacheKey, setMade)
]) ])
} finally { } finally {
if (!cancelled) setIsLoading(false) if (!cancelled && runGen === runGenRef.current) setIsLoading(false)
} }
} }

45
src/hooks/useProfileWall.tsx

@ -92,10 +92,16 @@ export function useProfileWall(pubkey: string, profileEventId: string | undefine
const cacheKey = useMemo(() => `${pubkey}-profile-wall-v1`, [pubkey]) const cacheKey = useMemo(() => `${pubkey}-profile-wall-v1`, [pubkey])
const cached = wallCacheByKey.get(cacheKey) const cached = wallCacheByKey.get(cacheKey)
const hasUsefulWallCache =
!!cached &&
cached.badges.length > 0 &&
Date.now() - cached.lastUpdated < CACHE_DURATION
const [badges, setBadges] = useState<ResolvedProfileBadge[]>(cached?.badges ?? []) const [badges, setBadges] = useState<ResolvedProfileBadge[]>(
const [comments, setComments] = useState<Event[]>(cached?.comments ?? []) hasUsefulWallCache ? cached!.badges : []
const [isLoading, setIsLoading] = useState(!cached) )
const [comments, setComments] = useState<Event[]>(hasUsefulWallCache ? cached!.comments : [])
const [isLoading, setIsLoading] = useState(!hasUsefulWallCache)
const [refreshToken, setRefreshToken] = useState(0) const [refreshToken, setRefreshToken] = useState(0)
const relayListsKey = useMemo( const relayListsKey = useMemo(
@ -108,18 +114,29 @@ export function useProfileWall(pubkey: string, profileEventId: string | undefine
blockedRelaysRef.current = blockedRelays blockedRelaysRef.current = blockedRelays
const useGlobalRelayBootstrapRef = useRef(useGlobalRelayBootstrap) const useGlobalRelayBootstrapRef = useRef(useGlobalRelayBootstrap)
useGlobalRelayBootstrapRef.current = useGlobalRelayBootstrap useGlobalRelayBootstrapRef.current = useGlobalRelayBootstrap
const runGenRef = useRef(0)
useEffect(() => { useEffect(() => {
let cancelled = false let cancelled = false
const runGen = ++runGenRef.current
const run = async () => { const run = async () => {
const mem = wallCacheByKey.get(cacheKey) const mem = wallCacheByKey.get(cacheKey)
if (mem && Date.now() - mem.lastUpdated < CACHE_DURATION && refreshToken === 0) { // Do not reuse empty cache (transient abort when secondary panel opens used to cache [] for 5m).
if (
mem &&
mem.badges.length > 0 &&
Date.now() - mem.lastUpdated < CACHE_DURATION &&
refreshToken === 0
) {
setBadges(mem.badges) setBadges(mem.badges)
setComments(mem.comments) setComments(mem.comments)
setIsLoading(false) if (runGen === runGenRef.current) setIsLoading(false)
return return
} }
if (mem?.badges.length === 0) {
wallCacheByKey.delete(cacheKey)
}
setIsLoading(true) setIsLoading(true)
try { try {
@ -148,7 +165,7 @@ export function useProfileWall(pubkey: string, profileEventId: string | undefine
) )
// --- Badges (NIP-58): IndexedDB + profile read relays (favorites / fast-read), not inbox-only --- // --- Badges (NIP-58): IndexedDB + profile read relays (favorites / fast-read), not inbox-only ---
let listEvent = await fetchProfileBadgesListEvent(pkNorm, relayUrls) let listEvent = await fetchProfileBadgesListEvent(pkNorm, relayUrls, { foreground: true })
if (!listEvent || !isNip58ProfileBadgesListEvent(listEvent)) { if (!listEvent || !isNip58ProfileBadgesListEvent(listEvent)) {
const legacy = await fetchLegacyProfileBadgesListEvent(pkNorm, relayUrls) const legacy = await fetchLegacyProfileBadgesListEvent(pkNorm, relayUrls)
if (legacy && isNip58ProfileBadgesListEvent(legacy)) listEvent = legacy if (legacy && isNip58ProfileBadgesListEvent(legacy)) listEvent = legacy
@ -208,13 +225,17 @@ export function useProfileWall(pubkey: string, profileEventId: string | undefine
if (cancelled) return if (cancelled) return
setBadges(resolvedBadges) setBadges(resolvedBadges)
setComments(wallComments) setComments(wallComments)
wallCacheByKey.set(cacheKey, { if (resolvedBadges.length > 0 || wallComments.length > 0) {
badges: resolvedBadges, wallCacheByKey.set(cacheKey, {
comments: wallComments, badges: resolvedBadges,
lastUpdated: Date.now() comments: wallComments,
}) lastUpdated: Date.now()
})
} else {
wallCacheByKey.delete(cacheKey)
}
} finally { } finally {
if (!cancelled) setIsLoading(false) if (!cancelled && runGen === runGenRef.current) setIsLoading(false)
} }
} }

24
src/lib/nip58-profile-badges-list.ts

@ -13,6 +13,7 @@ import { normalizeHexPubkey } from '@/lib/pubkey'
import { fetchLatestReplaceableListEvent } from '@/lib/replaceable-list-latest' import { fetchLatestReplaceableListEvent } from '@/lib/replaceable-list-latest'
import { normalizeAnyRelayUrl } from '@/lib/url' import { normalizeAnyRelayUrl } from '@/lib/url'
import client, { replaceableEventService } from '@/services/client.service' import client, { replaceableEventService } from '@/services/client.service'
import indexedDb from '@/services/indexed-db.service'
import type { Event } from 'nostr-tools' import type { Event } from 'nostr-tools'
export function profileBadgeEntriesToTags(entries: ProfileBadgeEntry[]): string[][] { export function profileBadgeEntriesToTags(entries: ProfileBadgeEntry[]): string[][] {
@ -42,19 +43,31 @@ export function profileBadgeListTagsAfterRemovingEntry(
export async function fetchProfileBadgesListEvent( export async function fetchProfileBadgesListEvent(
pubkeyHex: string, pubkeyHex: string,
relayUrls: string[] relayUrls: string[],
options?: { foreground?: boolean }
): Promise<Event | undefined> { ): Promise<Event | undefined> {
const pk = normalizeHexPubkey(pubkeyHex) const pk = normalizeHexPubkey(pubkeyHex)
const foreground = options?.foreground === true
let cached: Event | undefined let cached: Event | undefined
try { try {
cached = const disk = await indexedDb.getReplaceableEvent(pk, ExtendedKind.PROFILE_BADGES_LIST)
if (disk) cached = disk
} catch {
cached = undefined
}
try {
const fromService =
(await replaceableEventService.fetchReplaceableEvent(pk, ExtendedKind.PROFILE_BADGES_LIST)) ?? (await replaceableEventService.fetchReplaceableEvent(pk, ExtendedKind.PROFILE_BADGES_LIST)) ??
undefined undefined
if (!cached) cached = fromService
else if (fromService && fromService.created_at >= cached.created_at) cached = fromService
} catch { } catch {
cached = undefined /* best-effort */
} }
const fromRelays = relayUrls.length const fromRelays = relayUrls.length
? await fetchLatestReplaceableListEvent(pk, ExtendedKind.PROFILE_BADGES_LIST, relayUrls) ? await fetchLatestReplaceableListEvent(pk, ExtendedKind.PROFILE_BADGES_LIST, relayUrls, {
foreground
})
: undefined : undefined
if (!cached) return fromRelays if (!cached) return fromRelays
if (!fromRelays) return cached if (!fromRelays) return cached
@ -93,7 +106,8 @@ export async function fetchLegacyProfileBadgesListEvent(
{ {
replaceableRace: true, replaceableRace: true,
eoseTimeout: METADATA_BATCH_QUERY_EOSE_TIMEOUT_MS, eoseTimeout: METADATA_BATCH_QUERY_EOSE_TIMEOUT_MS,
globalTimeout: METADATA_BATCH_QUERY_GLOBAL_TIMEOUT_MS globalTimeout: METADATA_BATCH_QUERY_GLOBAL_TIMEOUT_MS,
foreground: true
} }
) )

8
src/lib/replaceable-list-latest.ts

@ -41,7 +41,8 @@ function newestReplaceableEvent(candidates: Event[]): Event | undefined {
export async function fetchLatestReplaceableListEvent( export async function fetchLatestReplaceableListEvent(
pubkeyHex: string, pubkeyHex: string,
kind: number, kind: number,
relayUrls: string[] relayUrls: string[],
options?: { foreground?: boolean }
): Promise<Event | undefined> { ): Promise<Event | undefined> {
const pk = normalizeHexPubkey(pubkeyHex) const pk = normalizeHexPubkey(pubkeyHex)
const allUrls = [...new Set(relayUrls.map((u) => normalizeAnyRelayUrl(u) || u).filter(Boolean))] const allUrls = [...new Set(relayUrls.map((u) => normalizeAnyRelayUrl(u) || u).filter(Boolean))]
@ -50,7 +51,10 @@ export async function fetchLatestReplaceableListEvent(
const rows = await client.fetchEvents( const rows = await client.fetchEvents(
allUrls, allUrls,
{ authors: [pk], kinds: networkKindsForReplaceableFetch(kind), limit: 80 }, { authors: [pk], kinds: networkKindsForReplaceableFetch(kind), limit: 80 },
replaceableListFetchQueryOpts(kind) {
...replaceableListFetchQueryOpts(kind),
...(options?.foreground ? { foreground: true } : {})
}
) )
return newestReplaceableEvent(rows.filter((e) => e.kind === kind)) return newestReplaceableEvent(rows.filter((e) => e.kind === kind))

Loading…
Cancel
Save