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. 18
      src/components/RelayIcon/index.tsx
  5. 6
      src/components/UserAvatar/index.tsx
  6. 9
      src/lib/error-suppression.ts
  7. 35
      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 @@ @@ -23,6 +23,7 @@
<meta name="apple-mobile-web-app-title" content="Imwald" />
<link rel="manifest" href="/manifest.webmanifest" />
<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.svg" type="image/svg+xml" />
<meta name="theme-color" content="#121e18" media="(prefers-color-scheme: dark)" />

2
package.json

@ -1,6 +1,6 @@ @@ -1,6 +1,6 @@
{
"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",
"private": true,
"type": "module",

6
src/components/ContentPreview/FollowPackPreview.tsx

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

18
src/components/RelayIcon/index.tsx

@ -8,8 +8,11 @@ import { useMemo } from 'react' @@ -8,8 +8,11 @@ import { useMemo } from 'react'
/**
* Resolve an image URL from NIP-11. Handles:
* - Absolute HTTP(S) URLs used as-is
* - Relative paths (e.g. "/favicon.ico") resolved against the relay's base HTTP URL
* - ws(s):// URLs some relays mistakenly return → ignored, fall through to favicon
* - Relative paths (e.g. "/logo.png") resolved against the relay's base HTTP URL
* - 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 {
if (!raw) return undefined
@ -47,16 +50,7 @@ export default function RelayIcon({ @@ -47,16 +50,7 @@ export default function RelayIcon({
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])
return (

6
src/components/UserAvatar/index.tsx

@ -216,7 +216,8 @@ export default function UserAvatar({ @@ -216,7 +216,8 @@ export default function UserAvatar({
const [imgError, setImgError] = useState(false)
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
useEffect(() => {
@ -356,7 +357,8 @@ export function SimpleUserAvatar({ @@ -356,7 +357,8 @@ export function SimpleUserAvatar({
const [imgError, setImgError] = useState(false)
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
useEffect(() => {

9
src/lib/error-suppression.ts

@ -285,6 +285,15 @@ function suppressExpectedErrors() { @@ -285,6 +285,15 @@ function suppressExpectedErrors() {
console.log = (...args: any[]) => {
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)
if (message.includes('Download the React DevTools')) {
return

35
src/providers/FollowListProvider.tsx

@ -32,9 +32,8 @@ export function FollowListProvider({ children }: { children: React.ReactNode }) @@ -32,9 +32,8 @@ export function FollowListProvider({ children }: { children: React.ReactNode })
})
}, [accountPubkey, favoriteRelays, blockedRelays])
const follow = async (pubkey: string) => {
if (!accountPubkey) return
const mergeLatestFollowTags = async (): Promise<{ tags: string[][]; content: string | undefined } | null> => {
if (!accountPubkey) return null
const relays = await buildMergeRelays()
let latest =
(await fetchLatestReplaceableListEvent(accountPubkey, kinds.Contacts, relays)) ?? null
@ -43,13 +42,32 @@ export function FollowListProvider({ children }: { children: React.ReactNode }) @@ -43,13 +42,32 @@ export function FollowListProvider({ children }: { children: React.ReactNode })
}
if (!latest) {
const result = confirm(t('FollowListNotFoundConfirmation'))
if (!result) return null
}
return { tags: latest?.tags ?? [], content: latest?.content }
}
const follow = async (pubkey: string) => {
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)
}
if (!result) {
return
}
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, latest?.content)
const newFollowListDraftEvent = createFollowListDraftEvent(mergedTags, base.content)
const newFollowListEvent = await publish(newFollowListDraftEvent)
await updateFollowListEvent(newFollowListEvent)
}
@ -78,6 +96,7 @@ export function FollowListProvider({ children }: { children: React.ReactNode }) @@ -78,6 +96,7 @@ export function FollowListProvider({ children }: { children: React.ReactNode })
value={{
followings,
follow,
followMany,
unfollow
}}
>

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

@ -3,6 +3,8 @@ import { createContext, useContext } from 'react' @@ -3,6 +3,8 @@ import { createContext, useContext } from 'react'
export type TFollowListContext = {
followings: string[]
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>
}

15
src/services/client.service.ts

@ -227,6 +227,10 @@ async function mapPoolWithConcurrency<T, R>( @@ -227,6 +227,10 @@ async function mapPoolWithConcurrency<T, R>(
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 {
static instance: ClientService
@ -3569,9 +3573,14 @@ class ClientService extends EventTarget { @@ -3569,9 +3573,14 @@ class ClientService extends EventTarget {
)
}
logger.warn('[FetchRelayLists] Network relay-list fetch exceeded budget; using IndexedDB / empty network layer only', {
pubkeyCount: pubkeys.length
})
const now = Date.now()
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)
return this.mergeRelayListsBundle(
pubkeys,

Loading…
Cancel
Save