Browse Source

fix bugs

imwald
Silberengel 4 weeks ago
parent
commit
ec4539a664
  1. 21
      src/components/Nip05/index.tsx
  2. 54
      src/components/Profile/FollowedBy.tsx
  3. 36
      src/components/Profile/index.tsx
  4. 3
      src/constants.ts
  5. 2
      src/hooks/useFetchProfile.tsx
  6. 34
      src/hooks/useProfileAuthorFeedSubRequests.ts
  7. 7
      src/services/client-replaceable-events.service.ts

21
src/components/Nip05/index.tsx

@ -6,12 +6,19 @@ import { SecondaryPageLink } from '@/PageManager'
import { BadgeAlert, BadgeCheck } from 'lucide-react' import { BadgeAlert, BadgeCheck } from 'lucide-react'
import { Favicon } from '../Favicon' import { Favicon } from '../Favicon'
export default function Nip05({ pubkey, append }: { pubkey: string; append?: string }) { export default function Nip05({
const { profile } = useFetchProfile(pubkey) pubkey,
const { nip05IsVerified, nip05Name, nip05Domain, isFetching } = useFetchNip05( nip05: nip05Prop,
profile?.nip05, append
pubkey }: {
) pubkey: string
/** When set (e.g. profile page), skip a second {@link useFetchProfile} network pass. */
nip05?: string
append?: string
}) {
const { profile } = useFetchProfile(nip05Prop === undefined ? pubkey : undefined)
const resolvedNip05 = nip05Prop ?? profile?.nip05
const { nip05IsVerified, nip05Name, nip05Domain, isFetching } = useFetchNip05(resolvedNip05, pubkey)
if (isFetching) { if (isFetching) {
return ( return (
@ -21,7 +28,7 @@ export default function Nip05({ pubkey, append }: { pubkey: string; append?: str
) )
} }
if (!profile?.nip05 || !nip05Name || !nip05Domain) return null if (!resolvedNip05 || !nip05Name || !nip05Domain) return null
return ( return (
<div <div

54
src/components/Profile/FollowedBy.tsx

@ -1,54 +0,0 @@
import UserAvatar from '@/components/UserAvatar'
import { useNostr } from '@/providers/NostrProvider'
import { useScreenSize } from '@/providers/ScreenSizeProvider'
import { replaceableEventService } from '@/services/client.service'
import { getPubkeysFromPTags } from '@/lib/tag'
import { kinds } from 'nostr-tools'
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
export default function FollowedBy({ pubkey }: { pubkey: string }) {
const { t } = useTranslation()
const { isSmallScreen } = useScreenSize()
const [followedBy, setFollowedBy] = useState<string[]>([])
const { pubkey: accountPubkey } = useNostr()
useEffect(() => {
if (!pubkey || !accountPubkey) return
const init = async () => {
const followListEvent = await replaceableEventService.fetchReplaceableEvent(accountPubkey, kinds.Contacts)
const followings = followListEvent ? getPubkeysFromPTags(followListEvent.tags).reverse() : []
const followingsOfFollowings = await Promise.all(
followings.map(async (following) => {
const followListEvent = await replaceableEventService.fetchReplaceableEvent(following, kinds.Contacts)
return followListEvent ? getPubkeysFromPTags(followListEvent.tags) : []
})
)
const _followedBy: string[] = []
const limit = isSmallScreen ? 3 : 5
for (const [index, following] of followings.entries()) {
if (following === pubkey) continue
if (followingsOfFollowings[index].includes(pubkey)) {
_followedBy.push(following)
}
if (_followedBy.length >= limit) {
break
}
}
setFollowedBy(_followedBy)
}
init()
}, [pubkey, accountPubkey])
if (followedBy.length === 0) return null
return (
<div className="flex items-center gap-1">
<div className="text-muted-foreground">{t('Followed by')}</div>
{followedBy.map((p) => (
<UserAvatar userId={p} key={p} size="xSmall" />
))}
</div>
)
}

36
src/components/Profile/index.tsx

@ -62,7 +62,6 @@ import { useTranslation } from 'react-i18next'
import logger from '@/lib/logger' import logger from '@/lib/logger'
import { AlexandriaEventsSearchEmptyCta } from '@/components/AlexandriaEventsSearchEmptyCta' import { AlexandriaEventsSearchEmptyCta } from '@/components/AlexandriaEventsSearchEmptyCta'
import NotFound from '../NotFound' import NotFound from '../NotFound'
import FollowedBy from './FollowedBy'
import ProfileBadges from './ProfileBadges' import ProfileBadges from './ProfileBadges'
import ProfileFeed from './ProfileFeed' import ProfileFeed from './ProfileFeed'
import ProfileReportsDialog from './ProfileReportsDialog' import ProfileReportsDialog from './ProfileReportsDialog'
@ -195,9 +194,14 @@ export default function Profile({
}, [syncAuthorReplaceablesFromCache]) }, [syncAuthorReplaceablesFromCache])
useEffect(() => { useEffect(() => {
if (!profile?.pubkey) return if (!profile?.pubkey || profile.batchPlaceholder) return
void client.refreshAuthorPublishedReplaceablesOnProfileView(profile.pubkey) const pk = profile.pubkey
}, [profile?.pubkey]) // Defer wide replaceable refresh so initial kind-0 / feed relay setup can finish first.
const timer = window.setTimeout(() => {
void client.refreshAuthorPublishedReplaceablesOnProfileView(pk)
}, 2_000)
return () => clearTimeout(timer)
}, [profile?.pubkey, profile?.batchPlaceholder])
useEffect(() => { useEffect(() => {
if (!isSelf || !profile?.pubkey || !accountProfileEvent) return if (!isSelf || !profile?.pubkey || !accountProfileEvent) return
@ -213,7 +217,7 @@ export default function Profile({
const onAuthorReplaceablesRefreshed: EventListener = (domEvt) => { const onAuthorReplaceablesRefreshed: EventListener = (domEvt) => {
const detailPk = (domEvt as CustomEvent<{ pubkey?: string }>).detail?.pubkey?.toLowerCase() const detailPk = (domEvt as CustomEvent<{ pubkey?: string }>).detail?.pubkey?.toLowerCase()
if (detailPk !== pk) return if (detailPk !== pk) return
void syncAuthorReplaceablesFromCache(profile.pubkey, { bustCache: true }) void syncAuthorReplaceablesFromCache(profile.pubkey)
} }
window.addEventListener( window.addEventListener(
ReplaceableEventService.AUTHOR_REPLACEABLES_REFRESHED_EVENT, ReplaceableEventService.AUTHOR_REPLACEABLES_REFRESHED_EVENT,
@ -226,10 +230,6 @@ export default function Profile({
) )
}, [profile?.pubkey, syncAuthorReplaceablesFromCache]) }, [profile?.pubkey, syncAuthorReplaceablesFromCache])
const isFollowingYou = useMemo(() => {
// This will be handled by the FollowedBy component
return false
}, [profile, accountPubkey])
const defaultImage = useMemo( const defaultImage = useMemo(
() => (profile?.pubkey ? generateImageByPubkey(profile?.pubkey) : ''), () => (profile?.pubkey ? generateImageByPubkey(profile?.pubkey) : ''),
[profile] [profile]
@ -524,13 +524,8 @@ export default function Profile({
<div className="pt-2 pb-4 md:pl-56"> <div className="pt-2 pb-4 md:pl-56">
<div className="flex flex-wrap gap-2 items-center min-w-0"> <div className="flex flex-wrap gap-2 items-center min-w-0">
<div className="text-xl font-semibold truncate select-text max-w-full">{username}</div> <div className="text-xl font-semibold truncate select-text max-w-full">{username}</div>
{isFollowingYou && (
<div className="text-muted-foreground rounded-full bg-muted text-xs h-fit px-2 shrink-0">
{t('Follows you')}
</div>
)}
</div> </div>
<Nip05 pubkey={pubkey} /> <Nip05 pubkey={pubkey} nip05={profile.nip05} />
{/* Display multiple NIP-05 values if available, with verification */} {/* Display multiple NIP-05 values if available, with verification */}
{nip05List && nip05List.length > 1 && ( {nip05List && nip05List.length > 1 && (
<Nip05List nip05List={nip05List.slice(1)} pubkey={pubkey} /> <Nip05List nip05List={nip05List.slice(1)} pubkey={pubkey} />
@ -608,13 +603,10 @@ export default function Profile({
defaultLightningAddress={zapLightningDefault} defaultLightningAddress={zapLightningDefault}
prefetchedPayment={prefetchedZapPayment} prefetchedPayment={prefetchedZapPayment}
/> />
<div className="flex flex-wrap justify-between items-center gap-x-4 gap-y-2 mt-2 text-sm min-w-0"> <div className="flex flex-wrap gap-4 items-center gap-x-4 gap-y-2 mt-2 text-sm min-w-0">
<div className="flex flex-wrap gap-4 items-center min-w-0"> <SmartFollowings pubkey={pubkey} />
<SmartFollowings pubkey={pubkey} /> <SmartRelays pubkey={pubkey} />
<SmartRelays pubkey={pubkey} /> {isSelf && <SmartMuteLink />}
{isSelf && <SmartMuteLink />}
</div>
{!isSelf && <FollowedBy pubkey={pubkey} />}
</div> </div>
<ProfileBadges pubkey={pubkey} profileEventId={effectiveProfileEvent?.id} /> <ProfileBadges pubkey={pubkey} profileEventId={effectiveProfileEvent?.id} />
</div> </div>

3
src/constants.ts

@ -406,7 +406,7 @@ export const NIP66_DISCOVERY_RELAY_URLS = [
// Relay with bookstr composite index support // Relay with bookstr composite index support
export const BOOKSTR_RELAY_URLS = [ export const BOOKSTR_RELAY_URLS = [
'wss://orly-relay.imwald.eu' 'wss://thecitadel.nostr1.com'
] ]
/** /**
@ -469,7 +469,6 @@ export const FAST_READ_RELAY_URLS = [
'wss://theforest.nostr1.com', 'wss://theforest.nostr1.com',
'wss://nostr.land', 'wss://nostr.land',
'wss://nostr.wine', 'wss://nostr.wine',
'wss://orly-relay.imwald.eu',
'wss://nostr21.com' 'wss://nostr21.com'
] ]

2
src/hooks/useFetchProfile.tsx

@ -691,6 +691,8 @@ export function useFetchProfile(id?: string, skipCache = false) {
const onAuthorReplaceablesRefreshed: EventListener = (domEvt) => { const onAuthorReplaceablesRefreshed: EventListener = (domEvt) => {
const detailPk = (domEvt as CustomEvent<{ pubkey?: string }>).detail?.pubkey?.toLowerCase() const detailPk = (domEvt as CustomEvent<{ pubkey?: string }>).detail?.pubkey?.toLowerCase()
if (detailPk !== pkLowerResolved) return if (detailPk !== pkLowerResolved) return
// Background profile-view refresh already persisted kind 0 — avoid a second full fetchProfileEvent pass.
if (initializedPubkeysRef.current.has(pkLowerResolved)) return
void checkProfile(pkLowerResolved, { current: profileRefreshCancelledRef.current }) void checkProfile(pkLowerResolved, { current: profileRefreshCancelledRef.current })
} }
window.addEventListener( window.addEventListener(

34
src/hooks/useProfileAuthorFeedSubRequests.ts

@ -88,21 +88,35 @@ export function useProfileAuthorFeedSubRequests({
let cancelled = false let cancelled = false
const socialKinds = kinds.some(isSocialKindBlockedKind) const socialKinds = kinds.some(isSocialKindBlockedKind)
const applyRelayList = (authorRl: typeof emptyAuthor) => {
const urls = buildProfilePageReadRelayUrls(
favoriteRelays,
blockedRelays,
authorRl,
socialKinds,
includeAuthorLocalRelays,
kinds,
useGlobalRelayBootstrap
)
if (urls.length > 0) {
setRelayUrls(urls)
}
}
void client
.peekRelayListFromStorage(pubkey)
.then((cached) => {
if (cancelled) return
applyRelayList(cached)
})
.catch(() => {})
void client void client
.fetchRelayList(pubkey) .fetchRelayList(pubkey)
.catch(() => emptyAuthor) .catch(() => emptyAuthor)
.then((authorRl) => { .then((authorRl) => {
if (cancelled) return if (cancelled) return
const urls = buildProfilePageReadRelayUrls( applyRelayList(authorRl)
favoriteRelays,
blockedRelays,
authorRl,
socialKinds,
includeAuthorLocalRelays,
kinds,
useGlobalRelayBootstrap
)
setRelayUrls(urls)
}) })
return () => { return () => {

7
src/services/client-replaceable-events.service.ts

@ -87,6 +87,8 @@ export class ReplaceableEventService {
}) })
/** One in-flight profile replaceables pull per author (avoids stacked REQs when profile UI remounts). */ /** One in-flight profile replaceables pull per author (avoids stacked REQs when profile UI remounts). */
private authorReplaceablesRefreshByPubkey = new Map<string, Promise<void>>() private authorReplaceablesRefreshByPubkey = new Map<string, Promise<void>>()
/** Per-author cooldown after a successful profile-view replaceable sweep (avoids reopen loops). */
private authorProfileViewRefreshNotBeforeMs = new Map<string, number>()
private replaceableEventFromBigRelaysDataloader: DataLoader< private replaceableEventFromBigRelaysDataloader: DataLoader<
{ pubkey: string; kind: number }, { pubkey: string; kind: number },
NEvent | null, NEvent | null,
@ -1409,6 +1411,9 @@ export class ReplaceableEventService {
const pk = pubkey.trim().toLowerCase() const pk = pubkey.trim().toLowerCase()
if (!/^[0-9a-f]{64}$/.test(pk)) return if (!/^[0-9a-f]{64}$/.test(pk)) return
const notBefore = this.authorProfileViewRefreshNotBeforeMs.get(pk) ?? 0
if (Date.now() < notBefore) return
const inFlight = this.authorReplaceablesRefreshByPubkey.get(pk) const inFlight = this.authorReplaceablesRefreshByPubkey.get(pk)
if (inFlight) return inFlight if (inFlight) return inFlight
@ -1502,6 +1507,8 @@ export class ReplaceableEventService {
}) })
) )
this.authorProfileViewRefreshNotBeforeMs.set(pk, Date.now() + 90_000)
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
window.dispatchEvent( window.dispatchEvent(
new CustomEvent(ReplaceableEventService.AUTHOR_REPLACEABLES_REFRESHED_EVENT, { detail: { pubkey: pk } }) new CustomEvent(ReplaceableEventService.AUTHOR_REPLACEABLES_REFRESHED_EVENT, { detail: { pubkey: pk } })

Loading…
Cancel
Save