Browse Source

bug-fixes

imwald
Silberengel 1 month ago
parent
commit
1a2adb87f8
  1. 2
      src/PageManager.tsx
  2. 16
      src/components/LatestFromFollowsSection/index.tsx
  3. 16
      src/components/NoteBoostBadges/index.tsx
  4. 1
      src/components/ReplyNote/index.tsx
  5. 1
      src/i18n/locales/de.ts
  6. 1
      src/i18n/locales/en.ts
  7. 13
      src/providers/FavoriteRelaysActivityProvider.tsx
  8. 103
      src/providers/NostrProvider/index.tsx
  9. 29
      src/services/client-replaceable-events.service.ts
  10. 6
      src/services/client.service.ts

2
src/PageManager.tsx

@ -203,7 +203,7 @@ function mergePrimaryPageEntry( @@ -203,7 +203,7 @@ function mergePrimaryPageEntry(
export { PrimaryPageContext, usePrimaryPage }
export { useSecondaryPage }
export { useSecondaryPage, useSecondaryPageOptional }
// Helper function to build contextual note URL
function buildNoteUrl(noteId: string, currentPage: TPrimaryPageName | null): string {

16
src/components/LatestFromFollowsSection/index.tsx

@ -88,7 +88,7 @@ function recommendedCuratorHexPubkey(): string | null { @@ -88,7 +88,7 @@ function recommendedCuratorHexPubkey(): string | null {
export default function LatestFromFollowsSection({ defaultOpen = false }: { defaultOpen?: boolean } = {}) {
const { t } = useTranslation()
const { push } = useSecondaryPage()
const { pubkey, followListEvent, isInitialized } = useNostr()
const { pubkey, followListEvent, isInitialized, isAccountSessionHydrating } = useNostr()
const { favoriteRelays, blockedRelays } = useFavoriteRelays()
const { mutePubkeySet } = useMuteList()
const { isEventDeleted } = useDeletedEvent()
@ -110,7 +110,19 @@ export default function LatestFromFollowsSection({ defaultOpen = false }: { defa @@ -110,7 +110,19 @@ export default function LatestFromFollowsSection({ defaultOpen = false }: { defa
const followPubkeys = pubkey ? (loggedInFollowPubkeys ?? []) : guestFollowPubkeys
const followsLabel: 'self' | 'recommended' = pubkey ? 'self' : 'recommended'
const loadingFollowList = !pubkey && isInitialized && !guestListReady
const [followListGraceExpired, setFollowListGraceExpired] = useState(false)
useEffect(() => {
if (!pubkey || followListEvent) {
setFollowListGraceExpired(false)
return
}
const t = setTimeout(() => setFollowListGraceExpired(true), 4000)
return () => clearTimeout(t)
}, [pubkey, followListEvent])
const loadingFollowList =
(!pubkey && isInitialized && !guestListReady) ||
(!!pubkey && !followListEvent && (isAccountSessionHydrating || !followListGraceExpired))
const [aggregateRelayUrls, setAggregateRelayUrls] = useState<string[]>([])
const [aggregateRelaysReady, setAggregateRelaysReady] = useState(false)

16
src/components/NoteBoostBadges/index.tsx

@ -34,23 +34,21 @@ export default function NoteBoostBadges({ event, className }: { event: Event; cl @@ -34,23 +34,21 @@ export default function NoteBoostBadges({ event, className }: { event: Event; cl
return (
<div
className={cn('flex flex-wrap items-center gap-x-0 gap-y-1', className)}
className={cn('flex flex-wrap items-center gap-x-1 gap-y-1', className)}
role="list"
aria-label={t('Boosts')}
>
{visible.map((r, i) => (
<div
key={r.id}
role="listitem"
className={cn(i > 0 && '-ml-2')}
style={{ zIndex: visible.length - i }}
>
<span className="text-muted-foreground text-sm shrink-0 mr-1">
{t('Boosted by:')}
</span>
{visible.map((r) => (
<div key={r.id} role="listitem">
<UserAvatar userId={r.pubkey} size="small" className="ring-2 ring-background" />
</div>
))}
{overflow > 0 ? (
<span
className="-ml-1 rounded-full bg-muted px-2 py-0.5 text-xs font-medium text-muted-foreground ring-2 ring-background"
className="rounded-full bg-muted px-2 py-0.5 text-xs font-medium text-muted-foreground ring-2 ring-background"
title={t('n more boosts', { count: overflow })}
>
+{overflow}

1
src/components/ReplyNote/index.tsx

@ -3,7 +3,6 @@ import { Button } from '@/components/ui/button' @@ -3,7 +3,6 @@ import { Button } from '@/components/ui/button'
import { Skeleton } from '@/components/ui/skeleton'
import { isMentioningMutedUsers } from '@/lib/event'
import { toNote } from '@/lib/link'
import client from '@/services/client.service'
import { useContentPolicy } from '@/providers/ContentPolicyProvider'
import { useMuteList } from '@/contexts/mute-list-context'
import { useScreenSize } from '@/providers/ScreenSizeProvider'

1
src/i18n/locales/de.ts

@ -33,6 +33,7 @@ export default { @@ -33,6 +33,7 @@ export default {
Following: 'Folgende',
followings: 'Folgekonten',
boosted: 'geboostet',
'Boosted by:': 'Geboostet von:',
'just now': 'gerade eben',
'n minutes ago': 'vor {{n}} Minuten',
'n m': 'vor {{n}}m',

1
src/i18n/locales/en.ts

@ -31,6 +31,7 @@ export default { @@ -31,6 +31,7 @@ export default {
Following: 'Following',
followings: 'followings',
boosted: 'boosted',
'Boosted by:': 'Boosted by:',
'just now': 'just now',
'n minutes ago': '{{n}} minutes ago',
'n m': '{{n}}m',

13
src/providers/FavoriteRelaysActivityProvider.tsx

@ -116,6 +116,13 @@ export function FavoriteRelaysActivityProvider({ children }: { children: React.R @@ -116,6 +116,13 @@ export function FavoriteRelaysActivityProvider({ children }: { children: React.R
const fetchRef = useRef(fetchActive)
fetchRef.current = fetchActive
/** Reset pulse state when account or relay set changes so we show loading until fresh data. */
const resetForRefetch = useCallback(() => {
setRelayActivityReady(false)
setOrderedPubkeys([])
setProfileKind0ByPubkey({})
}, [])
/** Initial fetch on mount and when relay set changes (refresh snapshot, not hourly cadence). */
const prevRelayKeyRef = useRef<string | undefined>(undefined)
useEffect(() => {
@ -126,17 +133,19 @@ export function FavoriteRelaysActivityProvider({ children }: { children: React.R @@ -126,17 +133,19 @@ export function FavoriteRelaysActivityProvider({ children }: { children: React.R
}
if (prevRelayKeyRef.current === relayKey) return
prevRelayKeyRef.current = relayKey
resetForRefetch()
void fetchRef.current()
}, [relayKey])
}, [relayKey, resetForRefetch])
/** Logged-in user changed — refetch for the new account. Follow list changes update partition via useMemo. */
const prevViewerRef = useRef<string | undefined>(undefined)
useEffect(() => {
if (prevViewerRef.current !== undefined && prevViewerRef.current !== viewerPubkey) {
resetForRefetch()
void fetchRef.current()
}
prevViewerRef.current = viewerPubkey ?? undefined
}, [viewerPubkey])
}, [viewerPubkey, resetForRefetch])
/** While the document is visible: poll once per hour; when returning after a long background, catch up if due. */
useEffect(() => {

103
src/providers/NostrProvider/index.tsx

@ -3,10 +3,11 @@ import { @@ -3,10 +3,11 @@ import {
ACCOUNT_SESSION_NETWORK_HYDRATE_MIN_INTERVAL_MS,
DEFAULT_FAVORITE_RELAYS,
FAST_READ_RELAY_URLS,
ExtendedKind,
FAST_WRITE_RELAY_URLS,
ExtendedKind,
PROFILE_FETCH_RELAY_URLS,
PROFILE_RELAY_URLS
PROFILE_RELAY_URLS,
SEARCHABLE_RELAY_URLS
} from '@/constants'
import {
buildAltTag,
@ -458,9 +459,10 @@ export function NostrProvider({ children }: { children: React.ReactNode }) { @@ -458,9 +459,10 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
const normalizedRelays = [
...relayList.write.map((url: string) => normalizeUrl(url) || url),
...FAST_WRITE_RELAY_URLS.map((url: string) => normalizeUrl(url) || url),
...PROFILE_FETCH_RELAY_URLS.map((url: string) => normalizeUrl(url) || url)
]
const fetchRelays = Array.from(new Set(normalizedRelays)).slice(0, 8)
const fetchRelays = Array.from(new Set(normalizedRelays)).slice(0, 16)
const events = await queryService.fetchEvents(fetchRelays, [
{
kinds: [
@ -509,6 +511,51 @@ export function NostrProvider({ children }: { children: React.ReactNode }) { @@ -509,6 +511,51 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
if (updatedFollowListEvent.id === followListEvent.id) {
setFollowListEvent(followListEvent)
}
} else {
// Hydrate batch uses limited relays; fallback fetches from broader set (author relays, etc.)
const trySetFollowList = (evt: Event) => {
if (hydrationGenForThisRun !== accountHydrationGenerationRef.current) return
indexedDb
.putReplaceableEvent(evt)
.then(() => {
if (hydrationGenForThisRun === accountHydrationGenerationRef.current) {
setFollowListEvent(evt)
logger.info('[NostrProvider] Follow list loaded via fallback fetch')
}
})
.catch(() => {
if (hydrationGenForThisRun === accountHydrationGenerationRef.current) {
setFollowListEvent(evt)
}
})
}
const followListRelays = Array.from(
new Set([
...mergedRelayList.write.map((u) => normalizeUrl(u) || u),
...SEARCHABLE_RELAY_URLS.map((u) => normalizeUrl(u) || u)
])
).filter(Boolean)
queryService
.fetchEvents(followListRelays, {
authors: [account.pubkey],
kinds: [kinds.Contacts],
limit: 1
})
.then((evts) => {
const evt = evts.sort((a, b) => b.created_at - a.created_at)[0]
if (evt && hydrationGenForThisRun === accountHydrationGenerationRef.current) {
trySetFollowList(evt)
return
}
client.fetchFollowListEvent(account.pubkey, followListRelays).then((f) => {
if (f) trySetFollowList(f)
})
})
.catch(() => {
client.fetchFollowListEvent(account.pubkey, followListRelays).then((f) => {
if (f) trySetFollowList(f)
})
})
}
if (muteListEvent) {
const updatedMuteListEvent = await indexedDb.putReplaceableEvent(muteListEvent)
@ -581,6 +628,36 @@ export function NostrProvider({ children }: { children: React.ReactNode }) { @@ -581,6 +628,36 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
if (storedRelayListEvent) {
client.updateRelayListCache(storedRelayListEvent)
}
if (!storedFollowListEvent) {
const trySetFollowListSkip = (evt: Event) => {
if (hydrationGenForThisRun !== accountHydrationGenerationRef.current) return
indexedDb
.putReplaceableEvent(evt)
.then(() => {
if (hydrationGenForThisRun === accountHydrationGenerationRef.current) {
setFollowListEvent(evt)
logger.info('[NostrProvider] Follow list loaded via fallback (skip-network path)')
}
})
.catch(() => {
if (hydrationGenForThisRun === accountHydrationGenerationRef.current) {
setFollowListEvent(evt)
}
})
}
const getFollowListRelays = async () => {
const rl = storedRelayListEvent
? getRelayListFromEvent(storedRelayListEvent, blockedRelays)
: { write: [] as string[], read: [] as string[] }
const writes = rl.write.map((u) => normalizeUrl(u) || u).filter(Boolean)
return Array.from(new Set([...writes, ...SEARCHABLE_RELAY_URLS.map((u) => normalizeUrl(u) || u)])).filter(Boolean)
}
getFollowListRelays().then((relays) =>
client.fetchFollowListEvent(account.pubkey, relays.length > 0 ? relays : undefined).then((fallback) => {
if (fallback) trySetFollowListSkip(fallback)
})
)
}
}
return controller
}
@ -611,6 +688,26 @@ export function NostrProvider({ children }: { children: React.ReactNode }) { @@ -611,6 +688,26 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
}
}, [account, accountNetworkHydrateBump])
/** Recovery: if hydrate finished but follow list is still null, fetch using user write + search relays. */
useEffect(() => {
if (!account || followListEvent !== null || isAccountSessionHydrating) return
let cancelled = false
client
.fetchRelayList(account.pubkey)
.then((rl) => {
const writes = rl.write.map((u) => normalizeUrl(u) || u).filter(Boolean)
const relays = Array.from(new Set([...writes, ...SEARCHABLE_RELAY_URLS.map((u) => normalizeUrl(u) || u)])).filter(Boolean)
return client.fetchFollowListEvent(account.pubkey, relays.length > 0 ? relays : undefined)
})
.then((evt) => {
if (!cancelled && evt) setFollowListEvent(evt)
})
.catch(() => {})
return () => {
cancelled = true
}
}, [account, followListEvent, isAccountSessionHydrating])
useEffect(() => {
if (!account) return

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

@ -1,6 +1,7 @@ @@ -1,6 +1,7 @@
import {
ExtendedKind,
FAST_READ_RELAY_URLS,
FAST_WRITE_RELAY_URLS,
MAX_CONCURRENT_RELAY_CONNECTIONS,
METADATA_BATCH_QUERY_EOSE_TIMEOUT_MS,
METADATA_BATCH_QUERY_GLOBAL_TIMEOUT_MS,
@ -499,6 +500,15 @@ export class ReplaceableEventService { @@ -499,6 +500,15 @@ export class ReplaceableEventService {
)
)
).filter(Boolean)
} else if (kind === kinds.Contacts) {
// Contacts (follow list) are published to user's write relays; use write + read + profile relays
relayUrls = Array.from(
new Set(
[...FAST_WRITE_RELAY_URLS, ...PROFILE_FETCH_RELAY_URLS, ...FAST_READ_RELAY_URLS].map(
(u) => normalizeUrl(u) || u
)
)
).filter(Boolean)
} else {
relayUrls = [...FAST_READ_RELAY_URLS]
}
@ -1001,9 +1011,24 @@ export class ReplaceableEventService { @@ -1001,9 +1011,24 @@ export class ReplaceableEventService {
*/
/**
* Fetch follow list event
* Fetch follow list event.
* When relayUrls are provided (e.g. user write + search relays), queries those directly.
* Otherwise uses the default relay set (FAST_WRITE + PROFILE_FETCH + FAST_READ).
*/
async fetchFollowListEvent(pubkey: string): Promise<NEvent | undefined> {
async fetchFollowListEvent(pubkey: string, relayUrls?: string[]): Promise<NEvent | undefined> {
if (relayUrls && relayUrls.length > 0) {
const normalized = Array.from(
new Set(relayUrls.map((u) => normalizeUrl(u) || u).filter(Boolean))
)
const events = await this.queryService.query(
normalized,
{ authors: [pubkey], kinds: [kinds.Contacts], limit: 1 },
undefined,
{ replaceableRace: true, eoseTimeout: 1500, globalTimeout: 8000 }
)
const latest = events.sort((a, b) => b.created_at - a.created_at)[0]
return latest
}
return await this.fetchReplaceableEvent(pubkey, kinds.Contacts)
}

6
src/services/client.service.ts

@ -2579,9 +2579,9 @@ class ClientService extends EventTarget { @@ -2579,9 +2579,9 @@ class ClientService extends EventTarget {
/** =========== Replaceable event =========== */
// Delegate to ReplaceableEventService
async fetchFollowListEvent(pubkey: string) {
return this.replaceableEventService.fetchFollowListEvent(pubkey)
// Delegate to ReplaceableEventService. Pass relayUrls for fallback (user write + search relays).
async fetchFollowListEvent(pubkey: string, relayUrls?: string[]) {
return this.replaceableEventService.fetchFollowListEvent(pubkey, relayUrls)
}
async fetchFollowings(pubkey: string): Promise<string[]> {

Loading…
Cancel
Save