Browse Source

bug-fixes

imwald
Silberengel 3 weeks ago
parent
commit
60f8f585b1
  1. 1
      index.html
  2. 2
      package.json
  3. 6
      src/components/ContentPreview/FollowPackPreview.tsx
  4. 16
      src/components/RelayIcon/index.tsx
  5. 6
      src/components/UserAvatar/index.tsx
  6. 9
      src/lib/error-suppression.ts
  7. 33
      src/providers/FollowListProvider.tsx
  8. 2
      src/providers/follow-list-context.tsx
  9. 15
      src/services/client.service.ts

1
index.html

@ -23,6 +23,7 @@
<meta name="apple-mobile-web-app-title" content="Imwald" /> <meta name="apple-mobile-web-app-title" content="Imwald" />
<link rel="manifest" href="/manifest.webmanifest" /> <link rel="manifest" href="/manifest.webmanifest" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" /> <link rel="apple-touch-icon" href="/apple-touch-icon.png" />
<link rel="icon" href="/favicon.ico" sizes="any" />
<link rel="icon" href="/favicon.png" type="image/png" sizes="216x215" /> <link rel="icon" href="/favicon.png" type="image/png" sizes="216x215" />
<link rel="icon" href="/favicon.svg" type="image/svg+xml" /> <link rel="icon" href="/favicon.svg" type="image/svg+xml" />
<meta name="theme-color" content="#121e18" media="(prefers-color-scheme: dark)" /> <meta name="theme-color" content="#121e18" media="(prefers-color-scheme: dark)" />

2
package.json

@ -1,6 +1,6 @@
{ {
"name": "imwald", "name": "imwald",
"version": "22.4.2", "version": "22.5.0",
"description": "Imwald — a user-friendly Nostr client focused on relay feed browsing, publications, and relay discovery", "description": "Imwald — a user-friendly Nostr client focused on relay feed browsing, publications, and relay discovery",
"private": true, "private": true,
"type": "module", "type": "module",

6
src/components/ContentPreview/FollowPackPreview.tsx

@ -83,7 +83,7 @@ export default function FollowPackPreview({
return return
} }
if (!followList) return if (!followList) return
const { follow } = followList const { followMany } = followList
const toFollow = packPubkeys.filter((p) => !followingSet.has(p) && !muteSetHas(mutePubkeySet, p)) const toFollow = packPubkeys.filter((p) => !followingSet.has(p) && !muteSetHas(mutePubkeySet, p))
if (toFollow.length === 0) { if (toFollow.length === 0) {
const mutedCount = packPubkeys.filter((p) => muteSetHas(mutePubkeySet, p) && !followingSet.has(p)).length const mutedCount = packPubkeys.filter((p) => muteSetHas(mutePubkeySet, p) && !followingSet.has(p)).length
@ -96,9 +96,7 @@ export default function FollowPackPreview({
} }
setBusy(true) setBusy(true)
try { try {
for (const pubkeyToFollow of toFollow) { await followMany(toFollow)
await follow(pubkeyToFollow)
}
toast.success(t('Followed {{count}} users', { count: toFollow.length })) toast.success(t('Followed {{count}} users', { count: toFollow.length }))
} catch (error) { } catch (error) {
logger.error('Failed to follow pack', { error }) logger.error('Failed to follow pack', { error })

16
src/components/RelayIcon/index.tsx

@ -8,8 +8,11 @@ import { useMemo } from 'react'
/** /**
* Resolve an image URL from NIP-11. Handles: * Resolve an image URL from NIP-11. Handles:
* - Absolute HTTP(S) URLs used as-is * - Absolute HTTP(S) URLs used as-is
* - Relative paths (e.g. "/favicon.ico") resolved against the relay's base HTTP URL * - Relative paths (e.g. "/logo.png") resolved against the relay's base HTTP URL
* - ws(s):// URLs some relays mistakenly return → ignored, fall through to favicon * - ws(s):// URLs some relays mistakenly return → ignored
*
* We do not fetch `https://host/favicon.ico` as a fallback: many relays return HTML/404 there,
* which triggers Firefox Opaque Response Blocking noise and broken `<img>` loads.
*/ */
function resolveRelayImageUrl(raw: string, relayUrl: string): string | undefined { function resolveRelayImageUrl(raw: string, relayUrl: string): string | undefined {
if (!raw) return undefined if (!raw) return undefined
@ -47,16 +50,7 @@ export default function RelayIcon({
return nip11Icon return nip11Icon
} }
// Fall back to /favicon.ico at the relay's host
try {
const u = new URL(url)
const scheme = u.protocol === 'wss:' ? 'https:' : 'http:'
const favicon = `${scheme}//${u.host}/favicon.ico`
logger.debug('[RelayIcon] using favicon fallback', { url, rawIcon, favicon })
return favicon
} catch {
return undefined return undefined
}
}, [url, relayInfo]) }, [url, relayInfo])
return ( return (

6
src/components/UserAvatar/index.tsx

@ -216,7 +216,8 @@ export default function UserAvatar({
const [imgError, setImgError] = useState(false) const [imgError, setImgError] = useState(false)
const [currentSrc, setCurrentSrc] = useState(avatarSrc) const [currentSrc, setCurrentSrc] = useState(avatarSrc)
const isVideoAvatar = useMemo(() => isVideo(profile?.avatar?.trim() ?? ''), [profile?.avatar]) /** Must match `currentSrc`: deferred / fallback identicon is SVG — never pass it to `<video>`. */
const isVideoAvatar = useMemo(() => isVideo(currentSrc), [currentSrc])
// Reset error state when src changes // Reset error state when src changes
useEffect(() => { useEffect(() => {
@ -356,7 +357,8 @@ export function SimpleUserAvatar({
const [imgError, setImgError] = useState(false) const [imgError, setImgError] = useState(false)
const [currentSrc, setCurrentSrc] = useState(avatarSrc) const [currentSrc, setCurrentSrc] = useState(avatarSrc)
const isVideoAvatar = useMemo(() => isVideo(profile?.avatar?.trim() ?? ''), [profile?.avatar]) /** Must match `currentSrc`: deferred / fallback identicon is SVG — never pass it to `<video>`. */
const isVideoAvatar = useMemo(() => isVideo(currentSrc), [currentSrc])
// Reset error state when src changes // Reset error state when src changes
useEffect(() => { useEffect(() => {

9
src/lib/error-suppression.ts

@ -285,6 +285,15 @@ function suppressExpectedErrors() {
console.log = (...args: any[]) => { console.log = (...args: any[]) => {
const message = args.join(' ') const message = args.join(' ')
// Firefox ORB: cross-origin favicon / relay icon requests often hit HTML or wrong MIME; not actionable in-app.
if (
message.includes('OpaqueResponseBlocking') ||
(message.includes('favicon.ico') &&
(message.includes('blocked') || message.includes('blockiert')))
) {
return
}
// Suppress React DevTools suggestion (only show once) // Suppress React DevTools suggestion (only show once)
if (message.includes('Download the React DevTools')) { if (message.includes('Download the React DevTools')) {
return return

33
src/providers/FollowListProvider.tsx

@ -32,9 +32,8 @@ export function FollowListProvider({ children }: { children: React.ReactNode })
}) })
}, [accountPubkey, favoriteRelays, blockedRelays]) }, [accountPubkey, favoriteRelays, blockedRelays])
const follow = async (pubkey: string) => { const mergeLatestFollowTags = async (): Promise<{ tags: string[][]; content: string | undefined } | null> => {
if (!accountPubkey) return if (!accountPubkey) return null
const relays = await buildMergeRelays() const relays = await buildMergeRelays()
let latest = let latest =
(await fetchLatestReplaceableListEvent(accountPubkey, kinds.Contacts, relays)) ?? null (await fetchLatestReplaceableListEvent(accountPubkey, kinds.Contacts, relays)) ?? null
@ -43,13 +42,32 @@ export function FollowListProvider({ children }: { children: React.ReactNode })
} }
if (!latest) { if (!latest) {
const result = confirm(t('FollowListNotFoundConfirmation')) const result = confirm(t('FollowListNotFoundConfirmation'))
if (!result) return null
}
return { tags: latest?.tags ?? [], content: latest?.content }
}
if (!result) { const follow = async (pubkey: string) => {
return if (!accountPubkey) return
const base = await mergeLatestFollowTags()
if (base === null) return
const mergedTags = dedupePTagsAppendPubkey(base.tags, pubkey)
const newFollowListDraftEvent = createFollowListDraftEvent(mergedTags, base.content)
const newFollowListEvent = await publish(newFollowListDraftEvent)
await updateFollowListEvent(newFollowListEvent)
} }
const followMany = async (pubkeys: string[]) => {
if (!accountPubkey) return
const unique = [...new Set(pubkeys.map((p) => p.trim().toLowerCase()).filter(Boolean))]
if (unique.length === 0) return
const base = await mergeLatestFollowTags()
if (base === null) return
let mergedTags = base.tags
for (const pk of unique) {
mergedTags = dedupePTagsAppendPubkey(mergedTags, pk)
} }
const mergedTags = dedupePTagsAppendPubkey(latest?.tags ?? [], pubkey) const newFollowListDraftEvent = createFollowListDraftEvent(mergedTags, base.content)
const newFollowListDraftEvent = createFollowListDraftEvent(mergedTags, latest?.content)
const newFollowListEvent = await publish(newFollowListDraftEvent) const newFollowListEvent = await publish(newFollowListDraftEvent)
await updateFollowListEvent(newFollowListEvent) await updateFollowListEvent(newFollowListEvent)
} }
@ -78,6 +96,7 @@ export function FollowListProvider({ children }: { children: React.ReactNode })
value={{ value={{
followings, followings,
follow, follow,
followMany,
unfollow unfollow
}} }}
> >

2
src/providers/follow-list-context.tsx

@ -3,6 +3,8 @@ import { createContext, useContext } from 'react'
export type TFollowListContext = { export type TFollowListContext = {
followings: string[] followings: string[]
follow: (pubkey: string) => Promise<void> follow: (pubkey: string) => Promise<void>
/** One fetch + one kind-3 publish after merging all pubkeys (use for follow packs, not per-user loops). */
followMany: (pubkeys: string[]) => Promise<void>
unfollow: (pubkey: string) => Promise<void> unfollow: (pubkey: string) => Promise<void>
} }

15
src/services/client.service.ts

@ -227,6 +227,10 @@ async function mapPoolWithConcurrency<T, R>(
return results return results
} }
/** Many features call `fetchRelayLists` in parallel; each timeout used to emit an identical WARN. */
let fetchRelayListBudgetWarnLastMs = 0
const FETCH_RELAY_LIST_BUDGET_WARN_MIN_INTERVAL_MS = 60_000
class ClientService extends EventTarget { class ClientService extends EventTarget {
static instance: ClientService static instance: ClientService
@ -3569,9 +3573,14 @@ class ClientService extends EventTarget {
) )
} }
logger.warn('[FetchRelayLists] Network relay-list fetch exceeded budget; using IndexedDB / empty network layer only', { const now = Date.now()
pubkeyCount: pubkeys.length if (now - fetchRelayListBudgetWarnLastMs >= FETCH_RELAY_LIST_BUDGET_WARN_MIN_INTERVAL_MS) {
}) fetchRelayListBudgetWarnLastMs = now
logger.warn(
'[FetchRelayLists] Network relay-list fetch exceeded budget; using IndexedDB / empty network layer only',
{ pubkeyCount: pubkeys.length }
)
}
const cacheRelayEvents = storedCacheRelayEvents.map((e) => e ?? undefined) const cacheRelayEvents = storedCacheRelayEvents.map((e) => e ?? undefined)
return this.mergeRelayListsBundle( return this.mergeRelayListsBundle(
pubkeys, pubkeys,

Loading…
Cancel
Save