Browse Source

bug-fixes

imwald
Silberengel 2 weeks ago
parent
commit
69cf5a5031
  1. 44
      src/PageManager.tsx
  2. 17
      src/components/HelpAndAccountMenu.tsx
  3. 3
      src/components/LogoutDialog/index.tsx
  4. 8
      src/components/Profile/index.tsx
  5. 16
      src/components/ui/avatar.tsx
  6. 3
      src/constants.ts
  7. 70
      src/lib/new-user-template-broadcast.ts
  8. 31
      src/lib/new-user-template.test.ts
  9. 13
      src/lib/new-user-template.ts
  10. 9
      src/lib/relay-strikes.test.ts
  11. 10
      src/lib/relay-strikes.ts
  12. 8
      src/pages/secondary/ProfileEditorPage/index.tsx
  13. 8
      src/providers/NostrProvider/index.tsx
  14. 8
      src/services/client.service.ts

44
src/PageManager.tsx

@ -19,7 +19,7 @@ import { NavigationService } from '@/services/navigation.service' @@ -19,7 +19,7 @@ import { NavigationService } from '@/services/navigation.service'
import { ImwaldBrandBar } from '@/assets/Logo'
import LiveActivitiesStrip from '@/components/LiveActivitiesStrip'
import NoteDrawer from '@/components/NoteDrawer'
import { PROFILE_SECONDARY_PANEL_DEFER_MS } from '@/constants'
import { APP_RESET_TO_LANDING_EVENT, PROFILE_SECONDARY_PANEL_DEFER_MS } from '@/constants'
import { extendProfileNetworkDeferral } from '@/lib/profile-batch-coordinator'
import client from '@/services/client.service'
import noteStatsService from '@/services/note-stats.service'
@ -2160,6 +2160,48 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) { @@ -2160,6 +2160,48 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) {
restorePrimaryTabAfterSecondaryClose()
}
/** Logout / session clear: drop note overlays and replace the current URL (e.g. `/feed/notes/…`) with `/`. */
const resetToLandingPage = () => {
ignorePopStateRef.current = true
pendingDrawerCloseUrlRef.current = '/'
setSavedPrimaryPage(null)
savedPrimaryPagePropsRef.current = undefined
setPrimaryNoteViewState(null)
setPrimaryViewType(null)
noteStatsService.setBackgroundStatsPaused(false)
if (drawerOpenRef.current) {
setDrawerOpen(false)
}
setSinglePaneSheetOpen(false)
secondaryStackRef.current = []
setSecondaryStack([])
setPrimaryPages((prev) => {
if (prev.some((p) => p.name === 'feed')) return prev
return [...prev, { name: 'feed', element: getPrimaryPageMap().feed }]
})
setCurrentPrimaryPage('feed')
window.history.replaceState(null, '', '/')
window.setTimeout(() => {
setDrawerNoteId(null)
setDrawerInitialEvent(null)
pendingDrawerCloseUrlRef.current = null
}, 400)
}
const resetToLandingPageStable = useEventCallback(resetToLandingPage)
useEffect(() => {
const onReset = () => resetToLandingPageStable()
window.addEventListener(APP_RESET_TO_LANDING_EVENT, onReset)
return () => window.removeEventListener(APP_RESET_TO_LANDING_EVENT, onReset)
}, [resetToLandingPageStable])
let lastPopSecondaryPageAt = 0
const POP_SECONDARY_PAGE_DEBOUNCE_MS = 400

17
src/components/HelpAndAccountMenu.tsx

@ -1,7 +1,7 @@ @@ -1,7 +1,7 @@
import LoginDialog from '@/components/LoginDialog'
import LogoutDialog from '@/components/LogoutDialog'
import SidebarItem from '@/components/Sidebar/SidebarItem'
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
import { Avatar, AvatarFallback, AvatarIdenticon, AvatarImage } from '@/components/ui/avatar'
import { Button } from '@/components/ui/button'
import {
DropdownMenu,
@ -115,9 +115,9 @@ function SidebarAccountMenu({ @@ -115,9 +115,9 @@ function SidebarAccountMenu({
</div>
) : (
<Avatar className="size-8 shrink-0">
<AvatarImage src={avatar} />
<AvatarFallback>
<img src={defaultAvatar} alt="" />
<AvatarImage src={avatar || defaultAvatar} className="object-cover object-center" />
<AvatarFallback delayMs={0}>
<AvatarIdenticon src={defaultAvatar} />
</AvatarFallback>
</Avatar>
)}
@ -173,9 +173,12 @@ function TitlebarAccountMenu({ @@ -173,9 +173,12 @@ function TitlebarAccountMenu({
</div>
) : (
<Avatar className={cn('w-6 h-6', active ? 'ring-primary ring-1' : '')}>
<AvatarImage src={resolvedProfile.avatar} className="object-cover object-center" />
<AvatarFallback>
<img src={defaultAvatar} alt="" />
<AvatarImage
src={resolvedProfile.avatar || defaultAvatar}
className="object-cover object-center"
/>
<AvatarFallback delayMs={0}>
<AvatarIdenticon src={defaultAvatar} />
</AvatarFallback>
</Avatar>
)

3
src/components/LogoutDialog/index.tsx

@ -17,7 +17,6 @@ import { @@ -17,7 +17,6 @@ import {
DrawerHeader,
DrawerTitle
} from '@/components/ui/drawer'
import { usePrimaryPage } from '@/contexts/primary-page-context'
import { useNostr } from '@/providers/NostrProvider'
import { useScreenSizeOptional } from '@/providers/ScreenSizeProvider'
import { useTranslation } from 'react-i18next'
@ -32,12 +31,10 @@ export default function LogoutDialog({ @@ -32,12 +31,10 @@ export default function LogoutDialog({
const { t } = useTranslation()
const { isSmallScreen = false } = useScreenSizeOptional() ?? {}
const { account, switchAccount } = useNostr()
const { navigate } = usePrimaryPage()
const handleLogout = () => {
setOpen(false)
void switchAccount(null)
navigate('feed')
}
if (isSmallScreen) {

8
src/components/Profile/index.tsx

@ -8,7 +8,7 @@ import { ProfileBotBadge } from '@/components/ProfileBotBadge' @@ -8,7 +8,7 @@ import { ProfileBotBadge } from '@/components/ProfileBotBadge'
import ProfileOptions from '@/components/ProfileOptions'
import ProfileZapButton from '@/components/ProfileZapButton'
import PubkeyCopy from '@/components/PubkeyCopy'
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
import { Avatar, AvatarFallback, AvatarIdenticon, AvatarImage } from '@/components/ui/avatar'
import { Button } from '@/components/ui/button'
import { Skeleton } from '@/components/ui/skeleton'
import { useFetchProfile } from '@/hooks'
@ -396,13 +396,13 @@ export default function Profile({ @@ -396,13 +396,13 @@ export default function Profile({
<div className="relative h-full w-full">
<Avatar className="h-full w-full border-4 border-background">
<AvatarImage
src={avatar}
src={avatar || defaultImage}
className="object-cover object-center"
fetchPriority="high"
loading="eager"
/>
<AvatarFallback>
<img src={defaultImage} alt="" />
<AvatarFallback delayMs={0}>
<AvatarIdenticon src={defaultImage} />
</AvatarFallback>
</Avatar>
{isBot ? (

16
src/components/ui/avatar.tsx

@ -38,7 +38,8 @@ const AvatarFallback = React.forwardRef< @@ -38,7 +38,8 @@ const AvatarFallback = React.forwardRef<
<AvatarPrimitive.Fallback
ref={ref}
className={cn(
'flex h-full w-full items-center justify-center rounded-full bg-muted',
'flex h-full w-full items-center justify-center overflow-hidden rounded-full bg-muted',
'[&_img]:block [&_img]:h-full [&_img]:w-full [&_img]:object-cover [&_img]:object-center',
className
)}
{...props}
@ -46,4 +47,15 @@ const AvatarFallback = React.forwardRef< @@ -46,4 +47,15 @@ const AvatarFallback = React.forwardRef<
))
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
export { Avatar, AvatarImage, AvatarFallback }
/** Pubkey identicon (or other fallback) sized to fill a circular avatar. */
function AvatarIdenticon({ src, className }: { src: string; className?: string }) {
return (
<img
src={src}
alt=""
className={cn('block h-full w-full object-cover object-center', className)}
/>
)
}
export { Avatar, AvatarImage, AvatarFallback, AvatarIdenticon }

3
src/constants.ts

@ -345,6 +345,9 @@ export const BLOSSOM_PRESET_SELECT_PREFIX = 'blossom-preset:' @@ -345,6 +345,9 @@ export const BLOSSOM_PRESET_SELECT_PREFIX = 'blossom-preset:'
/** [Lotus](https://github.com/0ceanSlim/lotus) — self-hosted Blossom (BUD) server (see GitHub for cdn_url / api_addr). */
export const LOTUS_BLOSSOM_REPO_URL = 'https://github.com/0ceanSlim/lotus'
/** Window event: session cleared — PageManager returns to `/` and closes note overlays. */
export const APP_RESET_TO_LANDING_EVENT = 'app-reset-to-landing'
export const StorageKey = {
VERSION: 'version',
THEME_SETTING: 'themeSetting',

70
src/lib/new-user-template-broadcast.ts

@ -4,6 +4,8 @@ import { filterRelaysForEventPublish } from '@/lib/relay-publish-filter' @@ -4,6 +4,8 @@ import { filterRelaysForEventPublish } from '@/lib/relay-publish-filter'
import { dedupeNormalizeRelayUrlsOrdered } from '@/lib/relay-url-priority'
import { collectWriteOutboxUrlsFromRelayList } from '@/lib/viewer-write-outboxes'
import logger from '@/lib/logger'
import { NEW_USER_HTTP_RELAY_URL } from '@/lib/new-user-template'
import { normalizeAnyRelayUrl } from '@/lib/url'
import client from '@/services/client.service'
import indexedDb from '@/services/indexed-db.service'
import type { TRelayList } from '@/types'
@ -11,21 +13,80 @@ import { Event, kinds } from 'nostr-tools' @@ -11,21 +13,80 @@ import { Event, kinds } from 'nostr-tools'
const BROADCAST_PENDING_KEY = 'imwaldNewUserTemplateBroadcastPending'
export const NEW_USER_TEMPLATE_BROADCAST_INTERVAL_MS = 5000
/** Space between replaceable template events — keeps relay publish rate limits from tripping. */
export const NEW_USER_TEMPLATE_BROADCAST_INTERVAL_MS = 20_000
/** Replaceable kinds created during one-click signup, in publish order. */
export const NEW_USER_TEMPLATE_BROADCAST_KINDS = [
kinds.RelayList,
ExtendedKind.HTTP_RELAY_LIST,
ExtendedKind.FAVORITE_RELAYS,
ExtendedKind.BLOCKED_RELAYS,
kinds.Metadata,
10015,
kinds.Contacts,
kinds.Mutelist
] as const
/** Relays that reject bursts or return HTTP 429 on connect during signup publish. */
const NEW_USER_TEMPLATE_PUBLISH_EXCLUDED = [
'wss://relay.layer.systems',
'wss://profiles.nostrver.se/'
] as const
/** Profile mirrors that only mirror kind 10002 (not kind 0 or other lists). */
const RELAY_LIST_ONLY_PROFILE_MIRRORS = ['wss://indexer.coracle.social/'] as const
const broadcastScheduledOrRunning = new Set<string>()
function templateRelayKey(url: string): string {
return (normalizeAnyRelayUrl(url) || url).toLowerCase()
}
function isExcludedFromTemplateBroadcast(url: string): boolean {
const key = templateRelayKey(url)
return NEW_USER_TEMPLATE_PUBLISH_EXCLUDED.some((u) => templateRelayKey(u) === key)
}
function relayAllowsTemplateKind(url: string, kind: number): boolean {
if (kind === kinds.RelayList) return true
const key = templateRelayKey(url)
return !RELAY_LIST_ONLY_PROFILE_MIRRORS.some((u) => templateRelayKey(u) === key)
}
function maxTemplatePublishRelays(kind: number): number {
return kind === kinds.Metadata || kind === kinds.RelayList ? 4 : 3
}
/** Prefer mercury + stable write relays; profile index when kind allows. */
function prioritizeNewUserTemplateRelays(urls: string[]): string[] {
const preferredOrder = [
NEW_USER_HTTP_RELAY_URL,
'wss://profiles.nostr1.com',
'wss://nos.lol',
'wss://relay.primal.net',
'wss://relay.damus.io',
'wss://thecitadel.nostr1.com'
]
const byKey = new Map(urls.map((u) => [templateRelayKey(u), u]))
const ordered: string[] = []
for (const pref of preferredOrder) {
const u = byKey.get(templateRelayKey(pref))
if (u) {
ordered.push(u)
byKey.delete(templateRelayKey(u))
}
}
for (const u of urls) {
const k = templateRelayKey(u)
if (byKey.has(k)) {
ordered.push(u)
byKey.delete(k)
}
}
return ordered
}
export function markNewUserTemplateBroadcastPending(pubkey: string): void {
if (typeof sessionStorage === 'undefined') return
sessionStorage.setItem(BROADCAST_PENDING_KEY, pubkey)
@ -45,7 +106,10 @@ export function newUserTemplatePublishRelays(kind: number, relayList: TRelayList @@ -45,7 +106,10 @@ export function newUserTemplatePublishRelays(kind: number, relayList: TRelayList
kind === kinds.Metadata || kind === kinds.RelayList
? dedupeNormalizeRelayUrlsOrdered([...write, ...PROFILE_RELAY_URLS])
: write
return filterRelaysForEventPublish(merged, kind)
const filtered = filterRelaysForEventPublish(merged, kind)
.filter((u) => !isExcludedFromTemplateBroadcast(u))
.filter((u) => relayAllowsTemplateKind(u, kind))
return prioritizeNewUserTemplateRelays(filtered).slice(0, maxTemplatePublishRelays(kind))
}
async function loadRelayListForPublish(pubkey: string): Promise<TRelayList> {
@ -101,7 +165,7 @@ async function broadcastNewUserTemplateFromStorage(pubkey: string): Promise<void @@ -101,7 +165,7 @@ async function broadcastNewUserTemplateFromStorage(pubkey: string): Promise<void
/**
* After the user dismisses the backup banner or leaves cache settings, broadcast locally stored
* template events to their write outboxes and profile relays (5s between events).
* template events to their write outboxes and profile relays (spaced to avoid relay rate limits).
*/
export function requestNewUserTemplateBroadcast(pubkey: string): void {
if (!pubkey || broadcastScheduledOrRunning.has(pubkey)) return

31
src/lib/new-user-template.test.ts

@ -1,7 +1,7 @@ @@ -1,7 +1,7 @@
import { describe, expect, it } from 'vitest'
import { kinds } from 'nostr-tools'
import { ExtendedKind, FAST_READ_RELAY_URLS, FAST_WRITE_RELAY_URLS, PROFILE_RELAY_URLS } from '@/constants'
import { NEW_USER_HTTP_RELAY_URL, buildNewUserTemplateDrafts, newUserProfileDisplayName, newUserProfileName, newUserProfileSuffix } from '@/lib/new-user-template'
import { ExtendedKind, FAST_READ_RELAY_URLS, FAST_WRITE_RELAY_URLS } from '@/constants'
import { NEW_USER_BLOCKED_RELAY_URLS, NEW_USER_HTTP_RELAY_URL, buildNewUserTemplateDrafts, newUserProfileDisplayName, newUserProfileName, newUserProfileSuffix } from '@/lib/new-user-template'
import { newUserTemplatePublishRelays } from '@/lib/new-user-template-broadcast'
import { normalizeAnyRelayUrl } from '@/lib/url'
import type { TRelayList } from '@/types'
@ -58,6 +58,12 @@ describe('buildNewUserTemplateDrafts', () => { @@ -58,6 +58,12 @@ describe('buildNewUserTemplateDrafts', () => {
expect(drafts.favoriteRelays.tags.filter((t) => t[0] === 'relay')).toHaveLength(2)
})
it('builds blocked relays kind 10006 with dead relays', () => {
expect(drafts.blockedRelays.kind).toBe(ExtendedKind.BLOCKED_RELAYS)
const blocked = drafts.blockedRelays.tags.filter((t) => t[0] === 'relay').map((t) => t[1])
expect(blocked).toEqual([...NEW_USER_BLOCKED_RELAY_URLS])
})
it('splits mailbox read and write relays', () => {
expect(drafts.relayList.kind).toBe(kinds.RelayList)
const readTags = drafts.relayList.tags.filter((t) => t[0] === 'r' && t[2] === 'read')
@ -97,19 +103,22 @@ describe('buildNewUserTemplateDrafts', () => { @@ -97,19 +103,22 @@ describe('buildNewUserTemplateDrafts', () => {
describe('newUserTemplatePublishRelays', () => {
const relayList = templateRelayList()
it('uses template write outboxes only for list kinds', () => {
it('caps list kinds to three stable write relays and skips flaky mirrors', () => {
const targets = newUserTemplatePublishRelays(10015, relayList)
expectRelayKeys(targets, [...FAST_WRITE_RELAY_URLS, NEW_USER_HTTP_RELAY_URL])
const profileOnlyUrls = PROFILE_RELAY_URLS.filter((u) => !FAST_WRITE_RELAY_URLS.includes(u))
for (const profileUrl of profileOnlyUrls) {
expect(targets.map(relayKey)).not.toContain(relayKey(profileUrl))
}
expect(targets.length).toBeLessThanOrEqual(3)
expect(targets.map(relayKey)).not.toContain(relayKey('wss://relay.layer.systems'))
expect(targets.map(relayKey)).not.toContain(relayKey('wss://profiles.nostrver.se/'))
expect(targets.map(relayKey)).not.toContain(relayKey('wss://indexer.coracle.social/'))
expectRelayKeys(targets, [NEW_USER_HTTP_RELAY_URL])
})
it('adds profile relays for kind 0 and 10002', () => {
it('adds profile relays for kind 0 and 10002 up to four targets', () => {
const profileTargets = newUserTemplatePublishRelays(kinds.Metadata, relayList)
expectRelayKeys(profileTargets, [...FAST_WRITE_RELAY_URLS, NEW_USER_HTTP_RELAY_URL, ...PROFILE_RELAY_URLS])
expect(profileTargets.length).toBeLessThanOrEqual(4)
expectRelayKeys(profileTargets, [NEW_USER_HTTP_RELAY_URL, 'wss://profiles.nostr1.com'])
expect(profileTargets.map(relayKey)).not.toContain(relayKey('wss://indexer.coracle.social/'))
const relayListTargets = newUserTemplatePublishRelays(kinds.RelayList, relayList)
expectRelayKeys(relayListTargets, [...FAST_WRITE_RELAY_URLS, NEW_USER_HTTP_RELAY_URL, ...PROFILE_RELAY_URLS])
expect(relayListTargets.length).toBeLessThanOrEqual(4)
expectRelayKeys(relayListTargets, [NEW_USER_HTTP_RELAY_URL, 'wss://profiles.nostr1.com'])
})
})

13
src/lib/new-user-template.ts

@ -4,6 +4,7 @@ import { @@ -4,6 +4,7 @@ import {
FAST_WRITE_RELAY_URLS
} from '@/constants'
import {
createBlockedRelaysDraftEvent,
createFavoriteRelaysDraftEvent,
createFollowListDraftEvent,
createHttpRelayListDraftEvent,
@ -16,6 +17,12 @@ import { TDraftEvent, TMailboxRelay } from '@/types' @@ -16,6 +17,12 @@ import { TDraftEvent, TMailboxRelay } from '@/types'
export const NEW_USER_HTTP_RELAY_URL = 'https://mercury-relay.imwald.eu/'
/** Dead relays seeded into kind 10006 for new accounts. */
export const NEW_USER_BLOCKED_RELAY_URLS = [
'wss://orly-relay.imwald.eu',
'wss://relay.nostr.band'
] as const
export const NEW_USER_INTEREST_TOPICS = [
'art',
'music',
@ -66,6 +73,10 @@ export function buildNewUserFavoriteRelaysDraft(): TDraftEvent { @@ -66,6 +73,10 @@ export function buildNewUserFavoriteRelaysDraft(): TDraftEvent {
return createFavoriteRelaysDraftEvent([...DEFAULT_FAVORITE_RELAYS], [])
}
export function buildNewUserBlockedRelaysDraft(): TDraftEvent {
return createBlockedRelaysDraftEvent([...NEW_USER_BLOCKED_RELAY_URLS])
}
export function buildNewUserRelayListDraft(): TDraftEvent {
return createRelayListDraftEvent(buildNewUserMailboxRelays())
}
@ -89,6 +100,7 @@ export function buildNewUserMuteListDraft(): TDraftEvent { @@ -89,6 +100,7 @@ export function buildNewUserMuteListDraft(): TDraftEvent {
export type TNewUserTemplateDrafts = {
profile: TDraftEvent
favoriteRelays: TDraftEvent
blockedRelays: TDraftEvent
relayList: TDraftEvent
httpRelayList: TDraftEvent
interestList: TDraftEvent
@ -100,6 +112,7 @@ export function buildNewUserTemplateDrafts(pubkey: string): TNewUserTemplateDraf @@ -100,6 +112,7 @@ export function buildNewUserTemplateDrafts(pubkey: string): TNewUserTemplateDraf
return {
profile: buildNewUserProfileDraft(pubkey),
favoriteRelays: buildNewUserFavoriteRelaysDraft(),
blockedRelays: buildNewUserBlockedRelaysDraft(),
relayList: buildNewUserRelayListDraft(),
httpRelayList: buildNewUserHttpRelayListDraft(),
interestList: buildNewUserInterestListDraft(),

9
src/lib/relay-strikes.test.ts

@ -126,6 +126,15 @@ describe('relaySessionStrikes publish failures', () => { @@ -126,6 +126,15 @@ describe('relaySessionStrikes publish failures', () => {
const entry = snap.entries.find((e) => e.key.includes('relay.example.com'))
expect(entry?.entry.publishFailures).toBe(1)
})
it('applies rate-limit cooldown on publish rate-limit NOTICE instead of accruing strikes', () => {
const url = 'wss://relay.damus.io/'
relaySessionStrikes.recordPublishFailure(url, 'rate-limited: you are noting too much')
expect(relaySessionStrikes.isPublishSkipped(url)).toBe(true)
const snap = relaySessionStrikes.getDebugSnapshot()
const entry = snap.entries.find((e) => e.key.includes('damus'))
expect(entry?.entry.publishFailures).toBe(0)
})
})
describe('isRelayStrikeEntryActive', () => {

10
src/lib/relay-strikes.ts

@ -30,7 +30,7 @@ const STRIKE_INCREMENT_DEBOUNCE_MS = 30 * 1000 @@ -30,7 +30,7 @@ const STRIKE_INCREMENT_DEBOUNCE_MS = 30 * 1000
export type RelayNoticeClass = 'rate_limit' | 'fetch_failed' | 'neutral'
const RATE_LIMIT_RE =
/too many concurrent|concurrent req|rate\s*limit|overloaded|429|slow down|throttl|backoff|try again later|maximum\s+subscriptions/i
/too many concurrent|concurrent req|rate[\s-]*limit|overloaded|429|slow down|throttl|backoff|try again later|maximum\s+subscriptions|noting too much/i
const FETCH_FAILED_RE = /failed to fetch events/i
@ -319,7 +319,13 @@ class RelaySessionStrikes { @@ -319,7 +319,13 @@ class RelaySessionStrikes {
}
recordPublishFailure(url: string, errorMessage?: string): void {
if (errorMessage && isRelayPublishPolicyRejection(errorMessage)) return
if (errorMessage) {
if (isRelayPublishPolicyRejection(errorMessage)) return
if (classifyRelayNotice(errorMessage) === 'rate_limit') {
this.applyRateLimitCooldownForUrl(url)
return
}
}
const key = sessionKey(url)
if (!key) return
const now = Date.now()

8
src/pages/secondary/ProfileEditorPage/index.tsx

@ -12,7 +12,7 @@ import { @@ -12,7 +12,7 @@ import {
DialogHeader,
DialogTitle
} from '@/components/ui/dialog'
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
import { Avatar, AvatarFallback, AvatarIdenticon, AvatarImage } from '@/components/ui/avatar'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
@ -529,13 +529,13 @@ const ProfileEditorPage = forwardRef(({ index }: { index?: number }, ref) => { @@ -529,13 +529,13 @@ const ProfileEditorPage = forwardRef(({ index }: { index?: number }, ref) => {
) : (
<Avatar className="h-full w-full">
<AvatarImage
src={avatar}
src={avatar || defaultImage}
className="object-cover object-center"
fetchPriority="high"
loading="eager"
/>
<AvatarFallback>
<img src={defaultImage} alt="" />
<AvatarFallback delayMs={0}>
{defaultImage ? <AvatarIdenticon src={defaultImage} /> : null}
</AvatarFallback>
</Avatar>
)}

8
src/providers/NostrProvider/index.tsx

@ -1,3 +1,4 @@ @@ -1,3 +1,4 @@
import { APP_RESET_TO_LANDING_EVENT } from '@/constants'
import storage from '@/services/local-storage.service'
import LoginDialog from '@/components/LoginDialog'
import NcryptsecPasswordPrompt from '@/components/NcryptsecPasswordPrompt'
@ -1155,6 +1156,7 @@ export function NostrProvider({ children }: { children: React.ReactNode }) { @@ -1155,6 +1156,7 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
storage.switchAccount(null)
setAccount(null)
setSigner(null)
window.dispatchEvent(new CustomEvent(APP_RESET_TO_LANDING_EVENT))
return null
}
const result = await loginWithAccountPointer(act, { userInitiatedSwitch: true })
@ -1966,6 +1968,7 @@ export function NostrProvider({ children }: { children: React.ReactNode }) { @@ -1966,6 +1968,7 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
const signed = {
profile: await signDraft(drafts.profile),
favoriteRelays: await signDraft(drafts.favoriteRelays),
blockedRelays: await signDraft(drafts.blockedRelays),
relayList: await signDraft(drafts.relayList),
httpRelayList: await signDraft(drafts.httpRelayList),
interestList: await signDraft(drafts.interestList),
@ -1976,6 +1979,7 @@ export function NostrProvider({ children }: { children: React.ReactNode }) { @@ -1976,6 +1979,7 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
await Promise.all([
indexedDb.putReplaceableEvent(signed.profile),
indexedDb.putReplaceableEvent(signed.favoriteRelays),
indexedDb.putReplaceableEvent(signed.blockedRelays),
indexedDb.putReplaceableEvent(signed.relayList),
indexedDb.putReplaceableEvent(signed.httpRelayList),
indexedDb.putReplaceableEvent(signed.interestList),
@ -1983,6 +1987,10 @@ export function NostrProvider({ children }: { children: React.ReactNode }) { @@ -1983,6 +1987,10 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
indexedDb.putReplaceableEvent(signed.muteList)
])
const blockedUrls = blockedRelayUrlsFromEvent(signed.blockedRelays)
setViewerBlockedRelayUrls(blockedUrls)
setBlockedRelaysEvent(signed.blockedRelays)
client.updateRelayListCache(signed.relayList)
void client.updateFollowListCache(signed.followList).catch(() => {})
void replaceableEventService.updateReplaceableEventCache(signed.profile).catch(() => {})

8
src/services/client.service.ts

@ -202,7 +202,7 @@ import { @@ -202,7 +202,7 @@ import {
} from '@/lib/url'
import { canonicalFeedFilter, canonicalRelayUrls } from '@/features/feed/descriptor'
import { initRelayPoolIdle, touchRelayPoolActivity, closePublishTransientRelaySockets, closeRelayPoolSocketsIfIdle } from '@/lib/relay-pool-idle'
import { relaySessionStrikes } from '@/lib/relay-strikes'
import { classifyRelayNotice, relaySessionStrikes } from '@/lib/relay-strikes'
import { isSafari } from '@/lib/utils'
import {
ISigner,
@ -2109,7 +2109,11 @@ class ClientService extends EventTarget { @@ -2109,7 +2109,11 @@ class ClientService extends EventTarget {
error: error instanceof Error ? error.message : 'Connection failed'
})
const errMsg = error instanceof Error ? error.message : 'Connection failed'
relaySessionStrikes.recordPublishFailure(url, errMsg)
if (classifyRelayNotice(errMsg) === 'rate_limit' || /\b429\b/.test(errMsg)) {
relaySessionStrikes.applyConnectionRateLimitCooldownForUrl(url)
} else {
relaySessionStrikes.recordPublishFailure(url, errMsg)
}
} finally {
clearTimeout(relayTimeout)
const currentFinished = ++finishedCount

Loading…
Cancel
Save