From 2b03b5f3a7148683d0ff14cfa1347dad523ec6fb Mon Sep 17 00:00:00 2001 From: "xan.lol" Date: Fri, 17 Apr 2026 02:28:42 -0700 Subject: [PATCH] fix: pds badge, relike on autolike on repost, misc errors slightly improved favicon fetching (actually goes for favicon.ico files again after the set favicon fetcher fails. also shows the filler db icon until something else becomes available) --- __tests__/lib/pds-label.test.ts | 26 ++++++ metro.config.js | 7 ++ src/components/PdsDialog.tsx | 91 +++++++++++++++---- .../PostControls/ShareMenu/ShareMenuItems.tsx | 4 +- src/components/PostControls/index.tsx | 20 +++- src/components/ProfileBadges.tsx | 1 + src/lib/api/index.ts | 2 +- src/state/queries/pds-label.ts | 45 ++------- src/state/queries/pds-label.util.ts | 43 +++++++++ src/state/session/index.tsx | 4 +- .../notifications/NotificationFeedItem.tsx | 2 +- 11 files changed, 183 insertions(+), 62 deletions(-) create mode 100644 __tests__/lib/pds-label.test.ts create mode 100644 src/state/queries/pds-label.util.ts diff --git a/__tests__/lib/pds-label.test.ts b/__tests__/lib/pds-label.test.ts new file mode 100644 index 000000000..89639c4b5 --- /dev/null +++ b/__tests__/lib/pds-label.test.ts @@ -0,0 +1,26 @@ +import { + getPdsFallbackFaviconUrl, + isBridgedPdsUrl, + isBskyPdsUrl, +} from '#/state/queries/pds-label.util' + +describe('pds-label helpers', () => { + it('builds a favicon.ico fallback URL from the PDS origin', () => { + expect(getPdsFallbackFaviconUrl('https://pds.example')).toBe( + 'https://pds.example/favicon.ico', + ) + expect(getPdsFallbackFaviconUrl('https://pds.example/xrpc')).toBe( + 'https://pds.example/favicon.ico', + ) + }) + + it('returns undefined for an invalid PDS URL', () => { + expect(getPdsFallbackFaviconUrl('not a url')).toBeUndefined() + }) + + it('detects special-case PDS hosts', () => { + expect(isBskyPdsUrl('https://bsky.social')).toBe(true) + expect(isBskyPdsUrl('https://foo.host.bsky.network')).toBe(true) + expect(isBridgedPdsUrl('https://atproto.brid.gy')).toBe(true) + }) +}) diff --git a/metro.config.js b/metro.config.js index dcdead3f5..85c6e01c8 100644 --- a/metro.config.js +++ b/metro.config.js @@ -35,6 +35,13 @@ cfg.resolver.resolveRequest = (context, moduleName, platform) => { if (moduleName === '@ipld/dag-cbor') { return context.resolveRequest(context, '@ipld/dag-cbor/src', platform) } + if (moduleName === '@easrng/tr58') { + return context.resolveRequest( + context, + '@easrng/tr58/dist/index.js', + platform, + ) + } if (process.env.BSKY_PROFILE) { if (moduleName.endsWith('ReactNativeRenderer-prod')) { return context.resolveRequest( diff --git a/src/components/PdsDialog.tsx b/src/components/PdsDialog.tsx index 473ad3235..4d6ee7c85 100644 --- a/src/components/PdsDialog.tsx +++ b/src/components/PdsDialog.tsx @@ -1,5 +1,6 @@ import {useState} from 'react' import {Image, View} from 'react-native' +import Svg, {G, Path, Rect} from 'react-native-svg' import { FontAwesomeIcon, type FontAwesomeIconStyle, @@ -8,17 +9,19 @@ import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {Trans} from '@lingui/react/macro' -import {isBridgedPdsUrl, isBskyPdsUrl} from '#/state/queries/pds-label' - -const failedFaviconUrls = new Set() -import Svg, {G, Path, Rect} from 'react-native-svg' - +import { + getPdsFallbackFaviconUrl, + isBridgedPdsUrl, + isBskyPdsUrl, +} from '#/state/queries/pds-label.util' import {atoms as a, useBreakpoints, useTheme} from '#/alf' import {Button, ButtonText} from '#/components/Button' import * as Dialog from '#/components/Dialog' import {InlineLinkText} from '#/components/Link' import {Text} from '#/components/Typography' +const failedFaviconUrls = new Set() + function formatBskyPdsDisplayName(hostname: string): string { const match = hostname.match(/^([^.]+)\.([^.]+)\.host\.bsky\.network$/) if (match) { @@ -72,6 +75,7 @@ export function PdsDialog({ Fediverse : displayName} } - {isBsky && + {isBsky && ( Bluesky Social - } + )} @@ -244,17 +248,26 @@ function FaviconBadgeIcon({ size, borderRadius, faviconUrl, + fallbackFaviconUrl, }: { size: number borderRadius: number faviconUrl: string + fallbackFaviconUrl?: string }) { - const t = useTheme() - const [imgError, setImgError] = useState(() => - failedFaviconUrls.has(faviconUrl), + const getInitialUrl = () => { + if (!failedFaviconUrls.has(faviconUrl)) return faviconUrl + if (fallbackFaviconUrl && !failedFaviconUrls.has(fallbackFaviconUrl)) { + return fallbackFaviconUrl + } + return undefined + } + const [currentUrl, setCurrentUrl] = useState( + getInitialUrl, ) + const [imageLoaded, setImageLoaded] = useState(false) - if (imgError) { + if (!currentUrl) { return } @@ -262,22 +275,42 @@ function FaviconBadgeIcon({ + { + setImageLoaded(true) + }} onError={() => { - failedFaviconUrls.add(faviconUrl) - setImgError(true) + failedFaviconUrls.add(currentUrl) + setImageLoaded(false) + + if ( + fallbackFaviconUrl && + currentUrl !== fallbackFaviconUrl && + !failedFaviconUrls.has(fallbackFaviconUrl) + ) { + setCurrentUrl(fallbackFaviconUrl) + return + } + + setCurrentUrl(undefined) }} /> @@ -286,12 +319,14 @@ function FaviconBadgeIcon({ export function PdsBadgeIcon({ faviconUrl, + pdsUrl, isBsky, isBridged, size, borderRadius, }: { faviconUrl?: string + pdsUrl?: string isBsky: boolean isBridged: boolean size: number @@ -300,9 +335,27 @@ export function PdsBadgeIcon({ const r = borderRadius ?? size / 5 if (isBsky) return if (isBridged) return + const fallbackFaviconUrl = pdsUrl + ? getPdsFallbackFaviconUrl(pdsUrl) + : undefined if (faviconUrl) return ( - + + ) + if (fallbackFaviconUrl) + return ( + ) return } diff --git a/src/components/PostControls/ShareMenu/ShareMenuItems.tsx b/src/components/PostControls/ShareMenu/ShareMenuItems.tsx index d3ae87b5d..e38368cd7 100644 --- a/src/components/PostControls/ShareMenu/ShareMenuItems.tsx +++ b/src/components/PostControls/ShareMenu/ShareMenuItems.tsx @@ -98,7 +98,9 @@ let ShareMenuItems = ({ } else { await ExpoClipboard.setStringAsync(url) } - Toast.show(_(msg`Copied to clipboard`), 'clipboard-check') + Toast.show(_(msg`Copied to clipboard`), { + type: 'success', + }) onShareProp() } diff --git a/src/components/PostControls/index.tsx b/src/components/PostControls/index.tsx index c757e8c26..c190381ca 100644 --- a/src/components/PostControls/index.tsx +++ b/src/components/PostControls/index.tsx @@ -20,6 +20,7 @@ import {useDisableQuotesMetrics} from '#/state/preferences/disable-quotes-metric import {useDisableReplyMetrics} from '#/state/preferences/disable-reply-metrics' import {useDisableRepostsMetrics} from '#/state/preferences/disable-reposts-metrics' import { + useGetPost, usePostLikeMutationQueue, usePostRepostMutationQueue, } from '#/state/queries/post' @@ -28,6 +29,7 @@ import { ProgressGuideAction, useProgressGuideControls, } from '#/state/shell/progress-guide' +import * as userActionHistory from '#/state/userActionHistory' import {atoms as a, useBreakpoints, useTheme} from '#/alf' import {Reply as Bubble} from '#/components/icons/Reply' import {useFormatPostStatCount} from '#/components/PostControls/util' @@ -83,6 +85,7 @@ let PostControls = ({ const {t: l} = useLingui() const {openComposer} = useOpenComposer() const {feedDescriptor} = useFeedFeedbackContext() + const getPost = useGetPost() const [queueLike, queueUnlike] = usePostLikeMutationQueue( post, viaRepost, @@ -118,6 +121,21 @@ let PostControls = ({ const autoLikeOnRepost = useAutoLikeOnRepost() + const shouldAutoLikeOnRepost = async () => { + if (post.viewer?.like) return false + + if (userActionHistory.getActionHistory().likes.includes(post.uri)) { + return false + } + + try { + const latestPost = await getPost({uri: post.uri}) + return !latestPost.viewer?.like + } catch { + return false + } + } + const onPressToggleLike = async () => { if (isBlocked) { Toast.show(l`Cannot interact with a blocked user`, { @@ -167,7 +185,7 @@ let PostControls = ({ }) await queueRepost() setHasLikeIconBeenToggled(true) - if (!post.viewer?.like && autoLikeOnRepost) { + if (autoLikeOnRepost && (await shouldAutoLikeOnRepost())) { sendInteraction({ item: post.uri, event: 'app.bsky.feed.defs#interactionLike', diff --git a/src/components/ProfileBadges.tsx b/src/components/ProfileBadges.tsx index 95ac5406d..e2eef8a96 100644 --- a/src/components/ProfileBadges.tsx +++ b/src/components/ProfileBadges.tsx @@ -173,6 +173,7 @@ function PdsInlineIcon({ const icon = ( [RQKEY_ROOT, did] @@ -78,7 +49,7 @@ export function usePdsFaviconQuery(pdsUrl: string | undefined) { return useQuery({ queryKey, queryFn: () => - isEnabled ? getFaviconUrl(pdsUrl!, faviconService!) : undefined, + isEnabled ? getFaviconServiceUrl(pdsUrl!, faviconService!) : undefined, enabled: isEnabled, staleTime: 1000 * 60 * 60, // 1 hour }) diff --git a/src/state/queries/pds-label.util.ts b/src/state/queries/pds-label.util.ts new file mode 100644 index 000000000..800f84b23 --- /dev/null +++ b/src/state/queries/pds-label.util.ts @@ -0,0 +1,43 @@ +const BSKY_PDS_HOSTNAMES = ['bsky.social', 'staging.bsky.dev'] +const BSKY_PDS_SUFFIX = '.bsky.network' +const BRIDGY_FED_HOSTNAME = 'atproto.brid.gy' + +export function isBskyPdsUrl(url: string): boolean { + try { + const hostname = new URL(url).hostname + return ( + BSKY_PDS_HOSTNAMES.includes(hostname) || + hostname.endsWith(BSKY_PDS_SUFFIX) + ) + } catch { + return false + } +} + +export function isBridgedPdsUrl(url: string): boolean { + try { + return new URL(url).hostname === BRIDGY_FED_HOSTNAME + } catch { + return false + } +} + +export function getFaviconServiceUrl( + pdsUrl: string, + faviconService: string, +): string | undefined { + try { + const hostname = new URL(pdsUrl).hostname + return faviconService.replace('(pds)', hostname) + } catch { + return undefined + } +} + +export function getPdsFallbackFaviconUrl(pdsUrl: string): string | undefined { + try { + return new URL('/favicon.ico', pdsUrl).toString() + } catch { + return undefined + } +} diff --git a/src/state/session/index.tsx b/src/state/session/index.tsx index 06cb382d3..fffcba60b 100644 --- a/src/state/session/index.tsx +++ b/src/state/session/index.tsx @@ -8,7 +8,7 @@ import { useState, useSyncExternalStore, } from 'react' -import {type AtpAgent, type AtpSessionEvent} from '@atproto/api' +import {type Agent, type AtpAgent, type AtpSessionEvent} from '@atproto/api' import * as persisted from '#/state/persisted' import {useCloseAllActiveElements} from '#/state/util' @@ -482,7 +482,7 @@ export function useAgent(): AtpAgent { return agent } -export function useBlankPrefAuthedAgent(): BskyAgent { +export function useBlankPrefAuthedAgent(): Agent { const agent = useContext(AgentContext) if (!agent) { throw Error('useAgent() must be below .') diff --git a/src/view/com/notifications/NotificationFeedItem.tsx b/src/view/com/notifications/NotificationFeedItem.tsx index 5ebad1a47..6fb8ad374 100644 --- a/src/view/com/notifications/NotificationFeedItem.tsx +++ b/src/view/com/notifications/NotificationFeedItem.tsx @@ -504,7 +504,7 @@ let NotificationFeedItem = ({ ) : ( {firstAuthorLink} liked your repost ) - icon = + icon = } else if (item.type === 'repost-via-repost') { a11yLabel = hasMultipleAuthors ? _( -- 2.51.2