Browse Source

bug-fixes

imwald
Silberengel 1 month ago
parent
commit
a58224c55a
  1. 16
      src/components/FavoriteRelaysSetting/FavoriteRelayList.tsx
  2. 13
      src/components/OthersRelayList/index.tsx
  3. 67
      src/components/ProfileOptions/index.tsx
  4. 78
      src/hooks/useFetchRelayList.tsx
  5. 6
      src/pages/secondary/OthersRelaySettingsPage/index.tsx
  6. 9
      src/pages/secondary/RelaySettingsPage/index.tsx
  7. 2
      src/providers/FavoriteRelaysProvider.tsx
  8. 19
      src/providers/NostrProvider/index.tsx
  9. 2
      src/providers/favorite-relays-context.tsx

16
src/components/FavoriteRelaysSetting/FavoriteRelayList.tsx

@ -1,4 +1,5 @@ @@ -1,4 +1,5 @@
import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider'
import { useNostr } from '@/providers/NostrProvider'
import {
closestCenter,
DndContext,
@ -20,7 +21,9 @@ import RelayItem from './RelayItem' @@ -20,7 +21,9 @@ import RelayItem from './RelayItem'
export default function FavoriteRelayList() {
const { t } = useTranslation()
const { favoriteRelays, blockedRelays, reorderFavoriteRelays } = useFavoriteRelays()
const { pubkey } = useNostr()
const { favoriteRelays, blockedRelays, reorderFavoriteRelays, favoriteRelaysFromPublishedList } =
useFavoriteRelays()
// Show all relays including blocked ones (they'll be marked visually)
@ -46,6 +49,17 @@ export default function FavoriteRelayList() { @@ -46,6 +49,17 @@ export default function FavoriteRelayList() {
return (
<div className="space-y-2">
<div className="text-muted-foreground font-semibold select-none">{t('Relays')}</div>
{!!pubkey && !favoriteRelaysFromPublishedList && (
<p
className="rounded-md border border-amber-500/35 bg-amber-500/10 px-3 py-2 text-sm text-foreground"
role="status"
>
{t('favoriteRelaysDefaultsBanner', {
defaultValue:
'No favorite-relays list (kind 10012) is loaded for this account yet. The relays below are app defaults and local relay sets, not a published list from your relays.'
})}
</p>
)}
<DndContext
sensors={sensors}
collisionDetection={closestCenter}

13
src/components/OthersRelayList/index.tsx

@ -11,7 +11,7 @@ import RelaySimpleInfo from '../RelaySimpleInfo' @@ -11,7 +11,7 @@ import RelaySimpleInfo from '../RelaySimpleInfo'
export default function OthersRelayList({ userId }: { userId: string }) {
const { t } = useTranslation()
const pubkey = useMemo(() => userIdToPubkey(userId), [userId])
const { relayList, isFetching } = useFetchRelayList(pubkey)
const { relayList, isFetching, showingRelayListFallback } = useFetchRelayList(pubkey)
if (isFetching) {
return <div className="text-center text-sm text-muted-foreground">{t('loading...')}</div>
@ -19,6 +19,17 @@ export default function OthersRelayList({ userId }: { userId: string }) { @@ -19,6 +19,17 @@ export default function OthersRelayList({ userId }: { userId: string }) {
return (
<div className="space-y-4">
{showingRelayListFallback && (
<p
className="rounded-md border border-amber-500/35 bg-amber-500/10 px-3 py-2 text-sm text-foreground"
role="status"
>
{t('othersRelayListKind10002Fallback', {
defaultValue:
'No NIP-65 relay list (kind 10002) was found for this user in local storage yet. The addresses below are default discovery relays, not this user’s published read/write list.'
})}
</p>
)}
{relayList.originalRelays.map((relay, index) => (
<RelayItem key={`read-${relay.url}-${index}`} relay={relay} />
))}

67
src/components/ProfileOptions/index.tsx

@ -8,28 +8,27 @@ import { @@ -8,28 +8,27 @@ import {
} from '@/components/ui/dropdown-menu'
import { buildHiveTalkJoinUrl, roomIdForPubkeys } from '@/lib/hivetalk'
import { formatPubkey, pubkeyToNpub } from '@/lib/pubkey'
import { normalizeAnyRelayUrl } from '@/lib/url'
import { useMuteList } from '@/contexts/mute-list-context'
import { muteSetHas } from '@/lib/mute-set'
import { normalizeAnyRelayUrl } from '@/lib/url'
import { useNostr } from '@/providers/NostrProvider'
import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider'
import { useCurrentRelays } from '@/providers/CurrentRelaysProvider'
import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider'
import { FAST_READ_RELAY_URLS, FAST_WRITE_RELAY_URLS } from '@/constants'
import client from '@/services/client.service'
import { replaceableEventService } from '@/services/client.service'
import client, { replaceableEventService } from '@/services/client.service'
import { nip66Service } from '@/services/nip66.service'
import RawEventDialog from '@/components/NoteOptions/RawEventDialog'
import {
Bell,
BellOff,
Code,
Copy,
Ellipsis,
ThumbsUp,
MessageCircle,
Send,
Video,
SatelliteDish,
Code,
Video,
LayoutGrid
} from 'lucide-react'
import { useMemo, useState, useEffect } from 'react'
@ -40,7 +39,7 @@ import { useTranslation } from 'react-i18next' @@ -40,7 +39,7 @@ import { useTranslation } from 'react-i18next'
import { useSmartProfileInteractionsNavigation } from '@/PageManager'
import { toProfileInteractionMap } from '@/lib/link'
import { toast } from 'sonner'
import { Event } from 'nostr-tools'
import { Event, kinds } from 'nostr-tools'
export default function ProfileOptions({
pubkey,
@ -49,7 +48,7 @@ export default function ProfileOptions({ @@ -49,7 +48,7 @@ export default function ProfileOptions({
onSendCallInvite
}: {
pubkey: string
/** Optional profile event (kind 0) for republishing and viewing JSON */
/** Optional profile event (kind 0): reply / like, republish to relays, view JSON */
profileEvent?: Event
/** Opens the post editor in public message mode with this profile's pubkey in the mention list. */
onSendPublicMessage?: () => void
@ -84,7 +83,7 @@ export default function ProfileOptions({ @@ -84,7 +83,7 @@ export default function ProfileOptions({
setLocalProfileEvent(event)
}
} catch (error) {
// Silently fail - menu items just won't show
// Silently fail: reply/like stay hidden until the event loads
}
}
@ -97,28 +96,32 @@ export default function ProfileOptions({ @@ -97,28 +96,32 @@ export default function ProfileOptions({
/** All available relays: current feed, favorites, relay sets, defaults (FAST_READ, FAST_WRITE). */
const allAvailableRelayUrls = useMemo(() => {
const urls = [
...currentBrowsingRelayUrls.map(url => normalizeAnyRelayUrl(url) || url),
...favoriteRelays.map(url => normalizeAnyRelayUrl(url) || url),
...relaySets.flatMap(set => set.relayUrls.map(url => normalizeAnyRelayUrl(url) || url)),
...FAST_READ_RELAY_URLS.map(url => normalizeAnyRelayUrl(url) || url),
...FAST_WRITE_RELAY_URLS.map(url => normalizeAnyRelayUrl(url) || url)
...currentBrowsingRelayUrls.map((url) => normalizeAnyRelayUrl(url) || url),
...favoriteRelays.map((url) => normalizeAnyRelayUrl(url) || url),
...relaySets.flatMap((set) => set.relayUrls.map((url) => normalizeAnyRelayUrl(url) || url)),
...FAST_READ_RELAY_URLS.map((url) => normalizeAnyRelayUrl(url) || url),
...FAST_WRITE_RELAY_URLS.map((url) => normalizeAnyRelayUrl(url) || url)
].filter(Boolean) as string[]
return Array.from(new Set(urls))
}, [currentBrowsingRelayUrls, favoriteRelays, relaySets])
useEffect(() => {
nip66Service.getPublicLivelyRelayUrls().then((urls) => {
void nip66Service.getPublicLivelyRelayUrls().then((urls) => {
setMonitoringListRelayCount(urls?.length ?? 0)
})
}, [])
const eventToUse = localProfileEvent || profileEvent
/** Kind 0 only; coerce `kind` in case deserialization yields a string. */
const kind0ForRelay =
eventToUse != null && Number(eventToUse.kind) === kinds.Metadata ? eventToUse : undefined
const handleRepublishToAllAvailable = async () => {
const eventToPublish = localProfileEvent || profileEvent
if (!eventToPublish) {
if (!kind0ForRelay) {
toast.error(t('Profile event not available'))
return
}
const promise = client.publishEvent(allAvailableRelayUrls, eventToPublish).then((result) => {
const promise = client.publishEvent(allAvailableRelayUrls, kind0ForRelay).then((result) => {
if (result.successCount < 1) {
throw new Error(t('No relay accepted the event'))
}
@ -132,8 +135,7 @@ export default function ProfileOptions({ @@ -132,8 +135,7 @@ export default function ProfileOptions({
}
const handleRepublishToAllActive = async () => {
const eventToPublish = localProfileEvent || profileEvent
if (!eventToPublish) {
if (!kind0ForRelay) {
toast.error(t('Profile event not available'))
return
}
@ -146,12 +148,14 @@ export default function ProfileOptions({ @@ -146,12 +148,14 @@ export default function ProfileOptions({
if (!relays?.length) {
throw new Error(t('No relays available'))
}
const result = await client.publishEvent(relays, eventToPublish)
const result = await client.publishEvent(relays, kind0ForRelay)
const minRequired = usedMonitoringList ? 5 : 1
if (result.successCount < minRequired) {
throw new Error(
usedMonitoringList
? t('Only {{count}} relay(s) accepted the event; at least 5 required for "all active relays".', { count: result.successCount })
? t('Only {{count}} relay(s) accepted the event; at least 5 required for "all active relays".', {
count: result.successCount
})
: t('No relay accepted the event')
)
}
@ -164,8 +168,6 @@ export default function ProfileOptions({ @@ -164,8 +168,6 @@ export default function ProfileOptions({
})
}
const eventToUse = localProfileEvent || profileEvent
const handleLike = () => {
if (!eventToUse) return
checkLogin(async () => {
@ -258,7 +260,7 @@ export default function ProfileOptions({ @@ -258,7 +260,7 @@ export default function ProfileOptions({
<Copy />
{t('Copy user ID')}
</DropdownMenuItem>
{(localProfileEvent || profileEvent) && (
{kind0ForRelay && (
<>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={handleRepublishToAllAvailable}>
@ -268,7 +270,8 @@ export default function ProfileOptions({ @@ -268,7 +270,8 @@ export default function ProfileOptions({
<DropdownMenuItem onClick={handleRepublishToAllActive}>
<SatelliteDish />
{t('Republish to all active relays')}
{monitoringListRelayCount !== null && ` (${monitoringListRelayCount > 0 ? monitoringListRelayCount : allAvailableRelayUrls.length})`}
{monitoringListRelayCount !== null &&
` (${monitoringListRelayCount > 0 ? monitoringListRelayCount : allAvailableRelayUrls.length})`}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setIsRawEventDialogOpen(true)}>
<Code />
@ -310,13 +313,13 @@ export default function ProfileOptions({ @@ -310,13 +313,13 @@ export default function ProfileOptions({
setOpen={setOpenReply}
/>
)}
{(localProfileEvent || profileEvent) && (
{kind0ForRelay && (
<RawEventDialog
event={(localProfileEvent || profileEvent)!}
isOpen={isRawEventDialogOpen}
onClose={() => setIsRawEventDialogOpen(false)}
/>
)}
event={kind0ForRelay}
isOpen={isRawEventDialogOpen}
onClose={() => setIsRawEventDialogOpen(false)}
/>
)}
</DropdownMenu>
)
}

78
src/hooks/useFetchRelayList.tsx

@ -1,45 +1,87 @@ @@ -1,45 +1,87 @@
import logger from '@/lib/logger'
import client from '@/services/client.service'
import indexedDb from '@/services/indexed-db.service'
import { TRelayList } from '@/types'
import { kinds } from 'nostr-tools'
import { useEffect, useState } from 'react'
const emptyRelayList = (): TRelayList => ({
write: [],
read: [],
originalRelays: [],
httpRead: [],
httpWrite: [],
httpOriginalRelays: []
})
export function useFetchRelayList(pubkey?: string | null) {
const [relayList, setRelayList] = useState<TRelayList>({
write: [],
read: [],
originalRelays: [],
httpRead: [],
httpWrite: [],
httpOriginalRelays: []
})
const [relayList, setRelayList] = useState<TRelayList>(emptyRelayList)
const [isFetching, setIsFetching] = useState(true)
/** True when IndexedDB has this author's kind 10002 (even if `originalRelays` is empty after merge). */
const [hasKind10002InStorage, setHasKind10002InStorage] = useState(false)
useEffect(() => {
let cancelled = false
const targetPk = pubkey?.trim() || null
const fetchRelayList = async () => {
setIsFetching(true)
if (!pubkey) {
setHasKind10002InStorage(false)
if (!targetPk) {
setRelayList(emptyRelayList())
setIsFetching(false)
return
}
setRelayList(emptyRelayList())
try {
const fromStorage = await client.peekRelayListFromStorage(pubkey)
const [fromStorage, k10002] = await Promise.all([
client.peekRelayListFromStorage(targetPk),
indexedDb.getReplaceableEvent(targetPk, kinds.RelayList).catch(() => null)
])
if (cancelled) return
setHasKind10002InStorage(!!k10002)
setRelayList(fromStorage)
const relayList = await client.fetchRelayList(pubkey)
setRelayList(relayList)
const merged = await client.fetchRelayList(targetPk)
if (cancelled) return
setRelayList(merged)
const k10002After = await indexedDb.getReplaceableEvent(targetPk, kinds.RelayList).catch(() => null)
if (!cancelled) {
setHasKind10002InStorage(!!k10002After)
}
} catch (err) {
logger.error('Failed to fetch relay list', { error: err, pubkey })
logger.error('Failed to fetch relay list', { error: err, pubkey: targetPk })
try {
setRelayList(await client.peekRelayListFromStorage(pubkey))
const fallback = await client.peekRelayListFromStorage(targetPk)
const k10002 = await indexedDb.getReplaceableEvent(targetPk, kinds.RelayList).catch(() => null)
if (!cancelled) {
setRelayList(fallback)
setHasKind10002InStorage(!!k10002)
}
} catch {
/* keep last good state */
if (!cancelled) {
setRelayList(emptyRelayList())
}
}
} finally {
setIsFetching(false)
if (!cancelled) {
setIsFetching(false)
}
}
}
fetchRelayList()
void fetchRelayList()
return () => {
cancelled = true
}
}, [pubkey])
return { relayList, isFetching }
const showingRelayListFallback =
!isFetching &&
!hasKind10002InStorage &&
relayList.originalRelays.length === 0
return { relayList, isFetching, hasKind10002InStorage, showingRelayListFallback }
}

6
src/pages/secondary/OthersRelaySettingsPage/index.tsx

@ -53,6 +53,12 @@ const RelaySettingsPage = forwardRef(({ id, index, hideTitlebar = false }: { id? @@ -53,6 +53,12 @@ const RelaySettingsPage = forwardRef(({ id, index, hideTitlebar = false }: { id?
setJsonOpen(true)
}, [profile?.pubkey, relayList])
useEffect(() => {
if (profile?.pubkey) {
setListKey((k) => k + 1)
}
}, [profile?.pubkey])
useEffect(() => {
if (!hideTitlebar) {
registerPrimaryPanelRefresh(null)

9
src/pages/secondary/RelaySettingsPage/index.tsx

@ -29,6 +29,13 @@ const RelaySettingsPage = forwardRef(({ index, hideTitlebar = false }: { index?: @@ -29,6 +29,13 @@ const RelaySettingsPage = forwardRef(({ index, hideTitlebar = false }: { index?:
const { account, relayList } = useNostr()
const [contentKey, setContentKey] = useState(0)
const bump = useCallback(() => setContentKey((k) => k + 1), [])
useEffect(() => {
if (account?.pubkey) {
setContentKey((k) => k + 1)
}
}, [account?.pubkey])
const [tabValue, setTabValue] = useState('favorite-relays')
const [jsonOpen, setJsonOpen] = useState(false)
const [jsonPayload, setJsonPayload] = useState<unknown>(null)
@ -75,7 +82,7 @@ const RelaySettingsPage = forwardRef(({ index, hideTitlebar = false }: { index?: @@ -75,7 +82,7 @@ const RelaySettingsPage = forwardRef(({ index, hideTitlebar = false }: { index?:
setTabValue('cache-relays')
break
}
}, [])
}, [account?.pubkey])
useEffect(() => {
if (!hideTitlebar) {

2
src/providers/FavoriteRelaysProvider.tsx

@ -310,6 +310,7 @@ export function FavoriteRelaysProvider({ children }: { children: React.ReactNode @@ -310,6 +310,7 @@ export function FavoriteRelaysProvider({ children }: { children: React.ReactNode
const contextValue = useMemo(
() => ({
favoriteRelaysFromPublishedList: !!favoriteRelaysEvent,
favoriteRelays,
addFavoriteRelays,
deleteFavoriteRelays,
@ -325,6 +326,7 @@ export function FavoriteRelaysProvider({ children }: { children: React.ReactNode @@ -325,6 +326,7 @@ export function FavoriteRelaysProvider({ children }: { children: React.ReactNode
reorderRelaySets
}),
[
favoriteRelaysEvent,
favoriteRelays,
blockedRelays,
relaySets,

19
src/providers/NostrProvider/index.tsx

@ -462,6 +462,9 @@ export function NostrProvider({ children }: { children: React.ReactNode }) { @@ -462,6 +462,9 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
limit: 1
})
])
if (hydrationGenForThisRun !== accountHydrationGenerationRef.current) {
return controller
}
const relayListEvent = getLatestEvent(relayListEvents) ?? storedRelayListEvent
const cacheRelayListEvent = getLatestEvent(cacheRelayListEvents) ?? storedCacheRelayListEvent
const httpRelayListEventFetched = getLatestEvent(httpRelayListEvents) ?? storedHttpRelayListEvent ?? null
@ -487,6 +490,9 @@ export function NostrProvider({ children }: { children: React.ReactNode }) { @@ -487,6 +490,9 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
}
// Fetch updated relay list (merges 10002, 10432, 10243)
const mergedRelayList = await client.fetchRelayList(account.pubkey) // Keep using client for relay list merging
if (hydrationGenForThisRun !== accountHydrationGenerationRef.current) {
return controller
}
setRelayList(mergedRelayList)
const normalizedRelays = [
@ -512,6 +518,9 @@ export function NostrProvider({ children }: { children: React.ReactNode }) { @@ -512,6 +518,9 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
authors: [account.pubkey]
}
])
if (hydrationGenForThisRun !== accountHydrationGenerationRef.current) {
return controller
}
const sortedEvents = events.sort((a, b) => b.created_at - a.created_at)
const profileEvent = sortedEvents.find((e) => e.kind === kinds.Metadata)
const followListEvent = sortedEvents.find((e) => e.kind === kinds.Contacts)
@ -554,6 +563,10 @@ export function NostrProvider({ children }: { children: React.ReactNode }) { @@ -554,6 +563,10 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
safePutReplaceable(userEmojiListEvent)
])
if (hydrationGenForThisRun !== accountHydrationGenerationRef.current) {
return controller
}
if (profileEvent) {
const resolvedProfileEvent = resolvedProfilePut ?? profileEvent
try {
@ -639,7 +652,11 @@ export function NostrProvider({ children }: { children: React.ReactNode }) { @@ -639,7 +652,11 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
}
}
if (favoriteRelaysEvent) {
if (resolvedFavoritePut && resolvedFavoritePut.id === favoriteRelaysEvent.id) {
if (
hydrationGenForThisRun === accountHydrationGenerationRef.current &&
resolvedFavoritePut &&
resolvedFavoritePut.id === favoriteRelaysEvent.id
) {
setFavoriteRelaysEvent(favoriteRelaysEvent)
}
}

2
src/providers/favorite-relays-context.tsx

@ -8,6 +8,8 @@ import { Event } from 'nostr-tools' @@ -8,6 +8,8 @@ import { Event } from 'nostr-tools'
import { createContext, useContext } from 'react'
export type TFavoriteRelaysContext = {
/** True when rows come from a published kind 10012 (favorite relays) event, not app defaults. */
favoriteRelaysFromPublishedList: boolean
favoriteRelays: string[]
addFavoriteRelays: (relayUrls: string[]) => Promise<void>
deleteFavoriteRelays: (relayUrls: string[]) => Promise<void>

Loading…
Cancel
Save