Browse Source

bug-fixes

imwald
Silberengel 4 weeks ago
parent
commit
47ebb0de35
  1. 6
      src/components/Profile/ProfileBadges.tsx
  2. 13
      src/components/Profile/ProfileReportsDialog.tsx
  3. 29
      src/hooks/useProfileReportsEvents.tsx
  4. 82
      src/hooks/useProfileWall.tsx

6
src/components/Profile/ProfileBadges.tsx

@ -1,3 +1,4 @@
import { RefreshButton } from '@/components/RefreshButton'
import { Skeleton } from '@/components/ui/skeleton' import { Skeleton } from '@/components/ui/skeleton'
import { useProfileWall } from '@/hooks/useProfileWall' import { useProfileWall } from '@/hooks/useProfileWall'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
@ -10,7 +11,7 @@ export default function ProfileBadges({
profileEventId?: string profileEventId?: string
}) { }) {
const { t } = useTranslation() const { t } = useTranslation()
const { badges, isLoading } = useProfileWall(pubkey, profileEventId) const { badges, isLoading, refresh } = useProfileWall(pubkey, profileEventId)
if (isLoading && badges.length === 0) { if (isLoading && badges.length === 0) {
return ( return (
@ -25,6 +26,9 @@ export default function ProfileBadges({
return ( return (
<section className="mt-3 min-w-0" aria-label={t('Badges')}> <section className="mt-3 min-w-0" aria-label={t('Badges')}>
<div className="mb-1 flex items-center justify-end gap-2">
<RefreshButton onClick={refresh} onLongPress={null} />
</div>
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
{badges.map((badge) => ( {badges.map((badge) => (
<div <div

13
src/components/Profile/ProfileReportsDialog.tsx

@ -1,4 +1,5 @@
import ReportCard from '@/components/ReportCard' import ReportCard from '@/components/ReportCard'
import { RefreshButton } from '@/components/RefreshButton'
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
@ -16,7 +17,7 @@ import { useTranslation } from 'react-i18next'
export function ProfileReportsPanel({ pubkey }: { pubkey: string }) { export function ProfileReportsPanel({ pubkey }: { pubkey: string }) {
const { t } = useTranslation() const { t } = useTranslation()
const relayUrlsBuilder = useProfileReportsRelayBuilder(pubkey) const relayUrlsBuilder = useProfileReportsRelayBuilder(pubkey)
const { received, made, isLoading } = useProfileReportsEvents({ const { received, made, isLoading, refresh } = useProfileReportsEvents({
pubkey, pubkey,
relayUrlsBuilder relayUrlsBuilder
}) })
@ -26,6 +27,11 @@ export function ProfileReportsPanel({ pubkey }: { pubkey: string }) {
if (!isLoading) setIsRefreshing(false) if (!isLoading) setIsRefreshing(false)
}, [isLoading]) }, [isLoading])
const handleRefresh = () => {
setIsRefreshing(true)
refresh()
}
if (isLoading && received.length === 0 && made.length === 0) { if (isLoading && received.length === 0 && made.length === 0) {
return ( return (
<div className="space-y-4 py-2"> <div className="space-y-4 py-2">
@ -38,6 +44,9 @@ export function ProfileReportsPanel({ pubkey }: { pubkey: string }) {
return ( return (
<div className="space-y-8"> <div className="space-y-8">
<div className="flex justify-end">
<RefreshButton onClick={handleRefresh} onLongPress={null} />
</div>
{isRefreshing && ( {isRefreshing && (
<div <div
className="flex items-center justify-center gap-2 py-2 text-center text-sm text-green-500" className="flex items-center justify-center gap-2 py-2 text-center text-sm text-green-500"
@ -104,7 +113,7 @@ export default function ProfileReportsDialog({
<DialogDescription>{t('Profile reports dialog description')}</DialogDescription> <DialogDescription>{t('Profile reports dialog description')}</DialogDescription>
</DialogHeader> </DialogHeader>
<div className="min-h-0 flex-1 overflow-y-auto pr-1"> <div className="min-h-0 flex-1 overflow-y-auto pr-1">
<ProfileReportsPanel pubkey={pubkey} /> {open ? <ProfileReportsPanel key={pubkey} pubkey={pubkey} /> : null}
</div> </div>
</DialogContent> </DialogContent>
</Dialog> </Dialog>

29
src/hooks/useProfileReportsEvents.tsx

@ -1,7 +1,7 @@
import { ExtendedKind } from '@/constants' import { ExtendedKind } from '@/constants'
import { useGlobalRelayBootstrapDefaults } from '@/hooks/use-global-relay-bootstrap-defaults' import { useGlobalRelayBootstrapDefaults } from '@/hooks/use-global-relay-bootstrap-defaults'
import type { ProfileTimelineRelayUrlsBuilder } from '@/hooks/useProfileTimeline' import type { ProfileTimelineRelayUrlsBuilder } from '@/hooks/useProfileTimeline'
import { buildProfilePageReadRelayUrls } from '@/lib/favorites-feed-relays' import { buildProfilePageReadRelayUrls, mergeRelayUrlLayers } from '@/lib/favorites-feed-relays'
import { isNip56ReportEvent } from '@/lib/event' import { isNip56ReportEvent } from '@/lib/event'
import { isReportAuthoredBy, reportTargetsPubkey } from '@/lib/nip56-reports' import { isReportAuthoredBy, reportTargetsPubkey } from '@/lib/nip56-reports'
import { normalizeHexPubkey } from '@/lib/pubkey' import { normalizeHexPubkey } from '@/lib/pubkey'
@ -123,19 +123,30 @@ export function useProfileReportsEvents({
authorRelayList: { read: string[]; write: string[]; httpRead?: string[]; httpWrite?: string[] }, authorRelayList: { read: string[]; write: string[]; httpRead?: string[]; httpWrite?: string[] },
includeAuthorLocal: boolean includeAuthorLocal: boolean
) => { ) => {
const custom = relayUrlsBuilderRef.current const blocked = blockedRelaysRef.current
if (custom) { const profileRead = buildProfilePageReadRelayUrls(
return custom(
favoriteRelaysRef.current, favoriteRelaysRef.current,
blockedRelaysRef.current, blocked,
authorRelayList, authorRelayList,
includeAuthorLocal false,
includeAuthorLocal,
[...REPORT_KINDS],
useGlobalRelayBootstrapRef.current
) )
} const custom = relayUrlsBuilderRef.current
const fromCustom = custom
? custom(favoriteRelaysRef.current, blocked, authorRelayList, includeAuthorLocal)
: []
const merged = mergeRelayUrlLayers(
custom ? [fromCustom, profileRead] : [profileRead],
blocked
)
if (merged.length > 0) return merged
// NIP-65 still loading: favorites + fast-read only (same as profile feed).
return buildProfilePageReadRelayUrls( return buildProfilePageReadRelayUrls(
favoriteRelaysRef.current, favoriteRelaysRef.current,
blockedRelaysRef.current, blocked,
authorRelayList, { read: [], write: [], httpRead: [], httpWrite: [] },
false, false,
includeAuthorLocal, includeAuthorLocal,
[...REPORT_KINDS], [...REPORT_KINDS],

82
src/hooks/useProfileWall.tsx

@ -1,10 +1,17 @@
import { ExtendedKind } from '@/constants' import {
ExtendedKind,
METADATA_BATCH_QUERY_EOSE_TIMEOUT_MS,
METADATA_BATCH_QUERY_GLOBAL_TIMEOUT_MS
} from '@/constants'
import { useGlobalRelayBootstrapDefaults } from '@/hooks/use-global-relay-bootstrap-defaults' import { useGlobalRelayBootstrapDefaults } from '@/hooks/use-global-relay-bootstrap-defaults'
import { buildProfilePageReadRelayUrls } from '@/lib/favorites-feed-relays' import { buildProfilePageReadRelayUrls } from '@/lib/favorites-feed-relays'
import { getReplaceableCoordinate } from '@/lib/event' import { getReplaceableCoordinate } from '@/lib/event'
import {
fetchLegacyProfileBadgesListEvent,
fetchProfileBadgesListEvent
} from '@/lib/nip58-profile-badges-list'
import { import {
isNip58ProfileBadgesListEvent, isNip58ProfileBadgesListEvent,
LEGACY_PROFILE_BADGES_D_TAG,
parseAddressableCoordinate, parseAddressableCoordinate,
parseProfileBadgeEntries, parseProfileBadgeEntries,
resolveBadgeDisplayFromDefinition, resolveBadgeDisplayFromDefinition,
@ -17,9 +24,56 @@ import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider'
import { useDeletedEvent } from '@/providers/DeletedEventProvider' import { useDeletedEvent } from '@/providers/DeletedEventProvider'
import client, { replaceableEventService } from '@/services/client.service' import client, { replaceableEventService } from '@/services/client.service'
import { ReplaceableEventService } from '@/services/client-replaceable-events.service' import { ReplaceableEventService } from '@/services/client-replaceable-events.service'
import indexedDb from '@/services/indexed-db.service'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { Event, kinds, type Filter } from 'nostr-tools' import { Event, kinds, type Filter } from 'nostr-tools'
async function fetchBadgeDefinitionOnRelays(
coordinate: string,
relayUrls: string[]
): Promise<Event | undefined> {
const parsed = parseAddressableCoordinate(coordinate)
if (!parsed || parsed.kind !== ExtendedKind.BADGE_DEFINITION) return undefined
try {
const disk = await indexedDb.getReplaceableEvent(parsed.pubkey, parsed.kind, parsed.d)
if (disk) return disk
} catch {
/* best-effort */
}
try {
const cached = await replaceableEventService.fetchReplaceableEvent(
parsed.pubkey,
parsed.kind,
parsed.d
)
if (cached) return cached
} catch {
/* best-effort */
}
if (!relayUrls.length) return undefined
const rows = await client.fetchEvents(
relayUrls,
{
authors: [parsed.pubkey],
kinds: [ExtendedKind.BADGE_DEFINITION],
'#d': [parsed.d],
limit: 20
},
{
replaceableRace: true,
eoseTimeout: METADATA_BATCH_QUERY_EOSE_TIMEOUT_MS,
globalTimeout: METADATA_BATCH_QUERY_GLOBAL_TIMEOUT_MS
}
)
const matches = rows.filter((e) => e.kind === ExtendedKind.BADGE_DEFINITION)
if (!matches.length) return undefined
return matches.reduce((best, e) => (e.created_at > best.created_at ? e : best))
}
const CACHE_DURATION = 5 * 60 * 1000 const CACHE_DURATION = 5 * 60 * 1000
const wallCacheByKey = new Map<string, { badges: ResolvedProfileBadge[]; comments: Event[]; lastUpdated: number }>() const wallCacheByKey = new Map<string, { badges: ResolvedProfileBadge[]; comments: Event[]; lastUpdated: number }>()
@ -90,16 +144,10 @@ export function useProfileWall(pubkey: string, profileEventId: string | undefine
useGlobalRelayBootstrapRef.current useGlobalRelayBootstrapRef.current
) )
// --- Badges (NIP-58) --- // --- Badges (NIP-58): IndexedDB + profile read relays (favorites / fast-read), not inbox-only ---
let listEvent = let listEvent = await fetchProfileBadgesListEvent(pkNorm, relayUrls)
(await replaceableEventService.fetchReplaceableEvent(pkNorm, ExtendedKind.PROFILE_BADGES_LIST)) ??
undefined
if (!listEvent || !isNip58ProfileBadgesListEvent(listEvent)) { if (!listEvent || !isNip58ProfileBadgesListEvent(listEvent)) {
const legacy = await replaceableEventService.fetchReplaceableEvent( const legacy = await fetchLegacyProfileBadgesListEvent(pkNorm, relayUrls)
pkNorm,
ExtendedKind.PROFILE_BADGES,
LEGACY_PROFILE_BADGES_D_TAG
)
if (legacy && isNip58ProfileBadgesListEvent(legacy)) listEvent = legacy if (legacy && isNip58ProfileBadgesListEvent(legacy)) listEvent = legacy
} }
@ -109,17 +157,7 @@ export function useProfileWall(pubkey: string, profileEventId: string | undefine
await Promise.all( await Promise.all(
defCoords.map(async (coord) => { defCoords.map(async (coord) => {
const parsed = parseAddressableCoordinate(coord) defByCoord.set(coord, await fetchBadgeDefinitionOnRelays(coord, relayUrls))
if (!parsed || parsed.kind !== ExtendedKind.BADGE_DEFINITION) {
defByCoord.set(coord, undefined)
return
}
const defEvent = await replaceableEventService.fetchReplaceableEvent(
parsed.pubkey,
parsed.kind,
parsed.d
)
defByCoord.set(coord, defEvent)
}) })
) )

Loading…
Cancel
Save