Bluesky app fork with some witchin' additions 💫

Multiple assorted merge fixes

Keep things more consistent with deer-social/social-app, make images higher quality in more places, re-add direct quote fetching, fix some icons, and make some more tweaks.

+179 -113
+1 -1
README.md
··· 77 77 - Stay away from PRs like... 78 78 - Changing "Post" to "Skeet." 79 79 - Refactoring the codebase, e.g., to replace React Query with Redux Toolkit or something. 80 - - Adding entirely new features without prior discussion. 80 + - Include a new toggle and preference for your feature. 81 81 82 82 If we don't merge your PR for whatever reason, you are welcome to fork and/or self host: 83 83
+1 -1
src/Navigation.tsx
··· 88 88 import {AppIconSettingsScreen} from '#/screens/Settings/AppIconSettings' 89 89 import {AppPasswordsScreen} from '#/screens/Settings/AppPasswords' 90 90 import {ContentAndMediaSettingsScreen} from '#/screens/Settings/ContentAndMediaSettings' 91 + import {DeerSettingsScreen} from '#/screens/Settings/DeerSettings' 91 92 import {ExternalMediaPreferencesScreen} from '#/screens/Settings/ExternalMediaPreferences' 92 93 import {FollowingFeedPreferencesScreen} from '#/screens/Settings/FollowingFeedPreferences' 93 94 import {InterestsSettingsScreen} from '#/screens/Settings/InterestsSettings' ··· 109 110 } from '#/components/dialogs/EmailDialog' 110 111 import {router} from '#/routes' 111 112 import {Referrer} from '../modules/expo-bluesky-swiss-army' 112 - import {DeerSettingsScreen} from './screens/Settings/DeerSettings' 113 113 import {LegacyNotificationSettingsScreen} from './screens/Settings/LegacyNotificationSettings' 114 114 import {NotificationSettingsScreen} from './screens/Settings/NotificationSettings' 115 115 import {LikeNotificationSettingsScreen} from './screens/Settings/NotificationSettings/LikeNotificationSettings'
+7 -2
src/components/Post/Embed/ImageEmbed.tsx
··· 9 9 import {Image} from 'expo-image' 10 10 11 11 import {useLightboxControls} from '#/state/lightbox' 12 + import { 13 + maybeModifyHighQualityImage, 14 + useHighQualityImages, 15 + } from '#/state/preferences/high-quality-images' 12 16 import {type Dimensions} from '#/view/com/lightbox/ImageViewing/@types' 13 17 import {AutoSizedImage} from '#/view/com/util/images/AutoSizedImage' 14 18 import {ImageLayoutGrid} from '#/view/com/util/images/ImageLayoutGrid' ··· 24 28 embed: EmbedType<'images'> 25 29 }) { 26 30 const {openLightbox} = useLightboxControls() 31 + const highQualityImages = useHighQualityImages() 27 32 const {images} = embed.view 28 33 29 34 if (images.length > 0) { 30 35 const items = images.map(img => ({ 31 - uri: img.fullsize, 32 - thumbUri: img.thumb, 36 + uri: maybeModifyHighQualityImage(img.fullsize, highQualityImages), 37 + thumbUri: maybeModifyHighQualityImage(img.thumb, highQualityImages), 33 38 alt: img.alt, 34 39 dimensions: img.aspectRatio ?? null, 35 40 }))
+13 -2
src/components/Post/Embed/PostPlaceholder.tsx
··· 4 4 import {InfoCircleIcon} from '#/lib/icons' 5 5 import {Text} from '#/view/com/util/text/Text' 6 6 import {atoms as a, useTheme} from '#/alf' 7 + import {Loader} from '#/components/Loader' 7 8 8 - export function PostPlaceholder({children}: {children: React.ReactNode}) { 9 + export function PostPlaceholder({ 10 + children, 11 + directFetchEnabled, 12 + }: { 13 + children: React.ReactNode 14 + directFetchEnabled?: boolean 15 + }) { 9 16 const t = useTheme() 10 17 const pal = usePalette('default') 11 18 return ( 12 19 <View 13 20 style={[styles.errorContainer, a.border, t.atoms.border_contrast_low]}> 14 - <InfoCircleIcon size={18} style={pal.text} /> 21 + {directFetchEnabled ? ( 22 + <Loader size={'md'} style={pal.text} /> 23 + ) : ( 24 + <InfoCircleIcon size={18} style={pal.text} /> 25 + )} 15 26 <Text type="lg" style={pal.text}> 16 27 {children} 17 28 </Text>
+111 -5
src/components/Post/Embed/index.tsx
··· 1 1 import React from 'react' 2 - import {View} from 'react-native' 2 + import {StyleSheet, View} from 'react-native' 3 3 import { 4 4 type $Typed, 5 5 type AppBskyFeedDefs, ··· 8 8 moderatePost, 9 9 RichText as RichTextAPI, 10 10 } from '@atproto/api' 11 - import {Trans} from '@lingui/macro' 11 + import {msg, Trans} from '@lingui/macro' 12 + import {useLingui} from '@lingui/react' 12 13 import {useQueryClient} from '@tanstack/react-query' 13 14 14 15 import {usePalette} from '#/lib/hooks/usePalette' 15 16 import {makeProfileLink} from '#/lib/routes/links' 17 + import {useDirectFetchRecords} from '#/state/preferences/direct-fetch-records' 16 18 import {useModerationOpts} from '#/state/preferences/moderation-opts' 19 + import {useDirectFetchEmbedRecord} from '#/state/queries/direct-fetch-record' 17 20 import {unstableCacheProfileView} from '#/state/queries/profile' 18 21 import {useSession} from '#/state/session' 19 22 import {Link} from '#/view/com/util/Link' 20 23 import {PostMeta} from '#/view/com/util/PostMeta' 24 + import {Text} from '#/view/com/util/text/Text' 21 25 import {atoms as a, useTheme} from '#/alf' 26 + import {EyeSlash_Stroke2_Corner0_Rounded as EyeSlashIcon} from '#/components/icons/EyeSlash' 22 27 import {ContentHider} from '#/components/moderation/ContentHider' 23 28 import {PostAlerts} from '#/components/moderation/PostAlerts' 24 29 import {RichText} from '#/components/RichText' ··· 122 127 }: CommonProps & { 123 128 embed: TEmbed 124 129 }) { 130 + const {_} = useLingui() 131 + const directFetchEnabled = useDirectFetchRecords() 132 + const shouldDirectFetch = 133 + (embed.type === 'post_blocked' || embed.type === 'post_detached') && 134 + directFetchEnabled 135 + 136 + const directRecord = useDirectFetchEmbedRecord({ 137 + uri: 138 + embed.type === 'post_blocked' || embed.type === 'post_detached' 139 + ? embed.view.uri 140 + : '', 141 + enabled: shouldDirectFetch, 142 + }) 143 + 125 144 switch (embed.type) { 126 145 case 'feed': { 127 146 return ( ··· 175 194 ) 176 195 } 177 196 case 'post_blocked': { 197 + const record = directRecord.data 198 + if (record !== undefined) { 199 + return ( 200 + <DirectFetchEmbed 201 + {...rest} 202 + embed={record} 203 + visibilityLabel={_(msg`Blocked`)} 204 + /> 205 + ) 206 + } 207 + 178 208 return ( 179 - <PostPlaceholderText> 209 + <PostPlaceholderText directFetchEnabled={directFetchEnabled}> 180 210 <Trans>Blocked</Trans> 181 211 </PostPlaceholderText> 182 212 ) 183 213 } 184 214 case 'post_detached': { 185 - return <PostDetachedEmbed embed={embed} /> 215 + const record = directRecord.data 216 + if (record !== undefined) { 217 + return ( 218 + <DirectFetchEmbed 219 + {...rest} 220 + embed={record} 221 + visibilityLabel={_(msg`Removed by author`)} 222 + visibilityLabelOwner={_(`Removed by you`)} 223 + /> 224 + ) 225 + } 226 + 227 + return ( 228 + <PostDetachedEmbed 229 + embed={embed} 230 + directFetchEnabled={directFetchEnabled} 231 + /> 232 + ) 186 233 } 187 234 default: { 188 235 return null ··· 190 237 } 191 238 } 192 239 240 + export function DirectFetchEmbed({ 241 + embed, 242 + visibilityLabel, 243 + visibilityLabelOwner, 244 + ...rest 245 + }: Omit<CommonProps, 'viewContext'> & { 246 + embed: EmbedType<'post'> 247 + viewContext?: PostEmbedViewContext 248 + visibilityLabel: string 249 + visibilityLabelOwner?: string 250 + }) { 251 + const {currentAccount} = useSession() 252 + const isViewerOwner = currentAccount?.did 253 + ? embed.view.uri.includes(currentAccount.did) 254 + : false 255 + 256 + return ( 257 + <View> 258 + <QuoteEmbed 259 + {...rest} 260 + embed={embed} 261 + viewContext={ 262 + rest.viewContext === PostEmbedViewContext.Feed 263 + ? QuoteEmbedViewContext.FeedEmbedRecordWithMedia 264 + : undefined 265 + } 266 + isWithinQuote={rest.isWithinQuote} 267 + allowNestedQuotes={rest.allowNestedQuotes} 268 + visibilityLabel={ 269 + isViewerOwner && visibilityLabelOwner 270 + ? visibilityLabelOwner 271 + : visibilityLabel 272 + } 273 + /> 274 + </View> 275 + ) 276 + } 277 + 193 278 export function PostDetachedEmbed({ 194 279 embed, 280 + directFetchEnabled, 195 281 }: { 196 282 embed: EmbedType<'post_detached'> 283 + directFetchEnabled?: boolean 197 284 }) { 198 285 const {currentAccount} = useSession() 199 286 const isViewerOwner = currentAccount?.did ··· 201 288 : false 202 289 203 290 return ( 204 - <PostPlaceholderText> 291 + <PostPlaceholderText directFetchEnabled={directFetchEnabled}> 205 292 {isViewerOwner ? ( 206 293 <Trans>Removed by you</Trans> 207 294 ) : ( ··· 221 308 style, 222 309 isWithinQuote: parentIsWithinQuote, 223 310 allowNestedQuotes: parentAllowNestedQuotes, 311 + visibilityLabel, 224 312 }: Omit<CommonProps, 'viewContext'> & { 225 313 embed: EmbedType<'post'> 226 314 viewContext?: QuoteEmbedViewContext 315 + visibilityLabel?: string 227 316 }) { 228 317 const moderationOpts = useModerationOpts() 229 318 const quote = React.useMemo<$Typed<AppBskyFeedDefs.PostView>>( ··· 292 381 title={itemTitle} 293 382 onBeforePress={onBeforePress}> 294 383 <View pointerEvents="none"> 384 + {visibilityLabel !== undefined ? ( 385 + <View style={[styles.blockHeader, t.atoms.border_contrast_low]}> 386 + <EyeSlashIcon size="sm" style={pal.text} /> 387 + <Text type="lg" style={pal.text}> 388 + {visibilityLabel} 389 + </Text> 390 + </View> 391 + ) : undefined} 295 392 <PostMeta 296 393 author={quote.author} 297 394 moderation={moderation} ··· 330 427 </View> 331 428 ) 332 429 } 430 + 431 + const styles = StyleSheet.create({ 432 + blockHeader: { 433 + flexDirection: 'row', 434 + alignItems: 'center', 435 + gap: 4, 436 + marginBottom: 8, 437 + }, 438 + })
-1
src/components/PostControls/PostMenu/PostMenuItems.tsx
··· 35 35 import {toShareUrl} from '#/lib/strings/url-helpers' 36 36 import {getTranslatorLink} from '#/locale/helpers' 37 37 import {logger} from '#/logger' 38 - import {isWeb} from '#/platform/detection' 39 38 import {type Shadow} from '#/state/cache/post-shadow' 40 39 import {useProfileShadow} from '#/state/cache/profile-shadow' 41 40 import {useFeedFeedbackContext} from '#/state/feed-feedback'
+2 -2
src/components/PostControls/ShareMenu/ShareMenuItems.web.tsx
··· 94 94 return ( 95 95 <> 96 96 <Menu.Outer> 97 - {!hideInPWI && copyLinkItem} 97 + {copyLinkItem} 98 98 99 99 {hasSession && ( 100 100 <Menu.Item ··· 124 124 </Menu.Item> 125 125 )} 126 126 127 - {hideInPWI && ( 127 + {false && hideInPWI && ( 128 128 <> 129 129 {hasSession && <Menu.Divider />} 130 130 {copyLinkItem}
+1 -2
src/components/verification/VerifierDialog.tsx
··· 102 102 Accounts with a scalloped blue check mark{' '} 103 103 <RNText> 104 104 <VerifierCheck width={14} /> 105 - {NON_BREAKING_SPACE} 106 - </RNText> 105 + </RNText>{' '} 107 106 can verify others. These trusted verifiers are selected by{' '} 108 107 {deerVerificationEnabled ? 'you' : 'Bluesky'}. 109 108 </Trans>
+2 -2
src/screens/Profile/Header/Handle.tsx
··· 3 3 import {msg, Trans} from '@lingui/macro' 4 4 import {useLingui} from '@lingui/react' 5 5 6 - import {isInvalidHandle} from '#/lib/strings/handles' 6 + import {isInvalidHandle, sanitizeHandle} from '#/lib/strings/handles' 7 7 import {isIOS} from '#/platform/detection' 8 8 import {type Shadow} from '#/state/cache/types' 9 9 import {useShowLinkInHandle} from '#/state/preferences/show-link-in-handle.tsx' ··· 64 64 </Text> 65 65 </InlineLinkText> 66 66 ) : ( 67 - `@${profile.handle}` 67 + sanitizeHandle(profile.handle, '@') 68 68 )} 69 69 </Text> 70 70 </View>
+1 -1
src/screens/Settings/AboutSettings.tsx
··· 30 30 const {_, i18n} = useLingui() 31 31 const [devModeEnabled, setDevModeEnabled] = useDevMode() 32 32 const [demoModeEnabled, setDemoModeEnabled] = useDemoMode() 33 - const stableID = useMemo(() => Statsig.getStableID(), []) 33 + const stableID = `DEER_SOCIAL_OOPS` 34 34 35 35 const {mutate: onClearImageCache, isPending: isClearingImageCache} = 36 36 useMutation({
+6 -66
src/state/geolocation.tsx
··· 1 1 import React from 'react' 2 2 import EventEmitter from 'eventemitter3' 3 3 4 - import {networkRetry} from '#/lib/async/retry' 5 4 import {logger} from '#/logger' 6 5 import {type Device, device} from '#/storage' 7 6 ··· 27 26 countryCode: 'US', 28 27 } 29 28 30 - async function getGeolocation(): Promise<Device['geolocation']> { 31 - const res = await fetch(`https://bsky.app/ipcc`) 32 - 33 - if (!res.ok) { 34 - throw new Error(`geolocation: lookup failed ${res.status}`) 35 - } 36 - 37 - const json = await res.json() 38 - 39 - if (json.countryCode) { 40 - return { 41 - countryCode: json.countryCode, 42 - } 43 - } else { 44 - return undefined 45 - } 46 - } 47 - 48 29 /** 49 30 * Local promise used within this file only. 50 31 */ ··· 64 45 * In dev, IP server is unavailable, so we just set the default geolocation 65 46 * and fail closed. 66 47 */ 67 - if (__DEV__) { 68 - geolocationResolution = new Promise(y => y({success: true})) 48 + geolocationResolution = new Promise(y => y({success: true})) 49 + if (device.get(['geolocation']) == undefined) { 69 50 device.set(['geolocation'], DEFAULT_GEOLOCATION) 70 51 } 71 52 } 72 53 73 - geolocationResolution = new Promise(async resolve => { 74 - let success = true 75 - 76 - try { 77 - // Try once, fail fast 78 - const geolocation = await getGeolocation() 79 - if (geolocation) { 80 - device.set(['geolocation'], geolocation) 81 - emitGeolocationUpdate(geolocation) 82 - logger.debug(`geolocation: success`, {geolocation}) 83 - } else { 84 - // endpoint should throw on all failures, this is insurance 85 - throw new Error(`geolocation: nothing returned from initial request`) 86 - } 87 - } catch (e: any) { 88 - success = false 89 - 90 - logger.debug(`geolocation: failed initial request`, { 91 - safeMessage: e.message, 92 - }) 93 - 94 - // set to default 95 - device.set(['geolocation'], DEFAULT_GEOLOCATION) 96 - 97 - // retry 3 times, but don't await, proceed with default 98 - networkRetry(3, getGeolocation) 99 - .then(geolocation => { 100 - if (geolocation) { 101 - device.set(['geolocation'], geolocation) 102 - emitGeolocationUpdate(geolocation) 103 - logger.debug(`geolocation: success`, {geolocation}) 104 - success = true 105 - } else { 106 - // endpoint should throw on all failures, this is insurance 107 - throw new Error(`geolocation: nothing returned from retries`) 108 - } 109 - }) 110 - .catch((e: any) => { 111 - // complete fail closed 112 - logger.debug(`geolocation: failed retries`, {safeMessage: e.message}) 113 - }) 114 - } finally { 115 - resolve({success}) 116 - } 117 - }) 54 + export function setGeolocation(geolocation: Device['geolocation']) { 55 + device.set(['geolocation'], geolocation) 56 + emitGeolocationUpdate(geolocation) 57 + } 118 58 119 59 /** 120 60 * Ensure that geolocation has been resolved, or at the very least attempted
+13 -9
src/state/queries/direct-fetch-record.ts
··· 12 12 import {STALE} from '#/state/queries' 13 13 import {useAgent} from '#/state/session' 14 14 import * as bsky from '#/types/bsky' 15 + import {type EmbedType} from '#/types/bsky/post' 15 16 16 17 const RQKEY_ROOT = 'direct-fetch-record' 17 18 export const RQKEY = (uri: string) => [RQKEY_ROOT, uri] ··· 62 63 export async function directFetchEmbedRecord( 63 64 agent: BskyAgent, 64 65 uri: string, 65 - ): Promise<AppBskyEmbedRecord.ViewRecord | undefined> { 66 + ): Promise<EmbedType<'post'> | undefined> { 66 67 const res = await directFetchRecordAndProfile(agent, uri) 67 68 if (res === undefined) return undefined 68 69 const {profile, record} = res 69 70 70 71 if (record && bsky.validate(record, AppBskyFeedPost.validateRecord)) { 71 72 return { 72 - $type: 'app.bsky.embed.record#viewRecord', 73 - uri, 74 - author: profile as ProfileViewBasic, 75 - cid: 'directfetch', 76 - value: record, 77 - indexedAt: new Date().toISOString(), 78 - } satisfies AppBskyEmbedRecord.ViewRecord 73 + type: 'post', 74 + view: { 75 + $type: 'app.bsky.embed.record#viewRecord', 76 + uri, 77 + author: profile as ProfileViewBasic, 78 + cid: 'directfetch', 79 + value: record, 80 + indexedAt: new Date().toISOString(), 81 + } satisfies AppBskyEmbedRecord.ViewRecord, 82 + } 79 83 } else { 80 84 return undefined 81 85 } ··· 89 93 enabled?: boolean 90 94 }) { 91 95 const agent = useAgent() 92 - return useQuery<AppBskyEmbedRecord.ViewRecord | undefined>({ 96 + return useQuery<EmbedType<'post'> | undefined>({ 93 97 staleTime: STALE.HOURS.ONE, 94 98 queryKey: RQKEY(uri || ''), 95 99 async queryFn() {
+1 -1
src/storage/schema.ts
··· 10 10 } 11 11 trendingBetaEnabled: boolean 12 12 devMode: boolean 13 + demoMode: boolean 13 14 14 15 // deer 15 16 deerGateCache: string 16 - demoMode: boolean 17 17 } 18 18 19 19 export type Account = {
+1
src/view/com/posts/PostFeed.tsx
··· 164 164 return feedRow.items.map((item, i) => ({ 165 165 item: item.items[0], 166 166 feedContext: feedRow.items[i].feedContext, 167 + reqId: feedRow.items[i].reqId, 167 168 })) 168 169 } else if (feedRow.type === 'videoGridRow') { 169 170 return feedRow.items.map((item, i) => ({
+2 -2
src/view/com/profile/ProfileMenu.tsx
··· 330 330 <Menu.ItemText> 331 331 <Trans>Remove trust</Trans> 332 332 </Menu.ItemText> 333 - <Menu.ItemIcon icon={CircleX} /> 333 + <Menu.ItemIcon icon={CircleXIcon} /> 334 334 </Menu.Item> 335 335 ) : ( 336 336 <Menu.Item ··· 340 340 <Menu.ItemText> 341 341 <Trans>Trust verifier</Trans> 342 342 </Menu.ItemText> 343 - <Menu.ItemIcon icon={CircleCheck} /> 343 + <Menu.ItemIcon icon={CircleCheckIcon} /> 344 344 </Menu.Item> 345 345 ))} 346 346 {isSelf && canGoLive && (
+1
src/view/com/util/UserAvatar.tsx
··· 431 431 }, 432 432 [onSelectNewAvatar], 433 433 ) 434 + 434 435 return ( 435 436 <> 436 437 <Menu.Root>
+16 -16
src/view/com/util/UserBanner.tsx
··· 1 - import React from 'react' 2 1 import {useCallback, useState} from 'react' 3 2 import { 4 3 Pressable, ··· 7 6 View, 8 7 } from 'react-native' 9 8 import { 9 + measure, 10 10 type MeasuredDimensions, 11 11 runOnJS, 12 12 runOnUI, 13 + useAnimatedRef, 13 14 } from 'react-native-reanimated' 14 - import {measure, useAnimatedRef} from 'react-native-reanimated' 15 15 import {Image} from 'expo-image' 16 16 import {type ModerationUI} from '@atproto/api' 17 17 import {msg, Trans} from '@lingui/macro' ··· 24 24 import {compressIfNeeded} from '#/lib/media/manip' 25 25 import {openCamera, openCropper, openPicker} from '#/lib/media/picker' 26 26 import {type PickerImage} from '#/lib/media/picker.shared' 27 - import {useTheme} from '#/lib/ThemeContext' 28 27 import {logger} from '#/logger' 29 28 import {isAndroid, isNative} from '#/platform/detection' 30 29 import { ··· 39 38 } from '#/state/preferences/high-quality-images' 40 39 import {EditImageDialog} from '#/view/com/composer/photos/EditImageDialog' 41 40 import {EventStopper} from '#/view/com/util/EventStopper' 42 - import {atoms as a, tokens, useTheme as useAlfTheme} from '#/alf' 41 + import {atoms as a, tokens, useTheme} from '#/alf' 43 42 import {useDialogControl} from '#/components/Dialog' 44 43 import {useSheetWrapper} from '#/components/Dialog/sheet-wrapper' 45 44 import { ··· 61 60 moderation?: ModerationUI 62 61 onSelectNewBanner?: (img: PickerImage | null) => void 63 62 }) { 64 - const theme = useTheme() 65 - const t = useAlfTheme() 63 + const t = useTheme() 66 64 const {_} = useLingui() 67 65 const {requestCameraAccessIfNeeded} = useCameraPermission() 68 66 const {requestPhotoAccessIfNeeded} = usePhotoLibraryPermission() 69 67 const sheetWrapper = useSheetWrapper() 68 + const [rawImage, setRawImage] = useState<ComposerImage | undefined>() 69 + const editImageDialogControl = useDialogControl() 70 70 const {openLightbox} = useLightboxControls() 71 71 const highQualityImages = useHighQualityImages() 72 72 73 73 const bannerRef = useAnimatedRef() 74 - const [rawImage, setRawImage] = useState<ComposerImage | undefined>() 75 - const editImageDialogControl = useDialogControl() 76 74 77 75 const onOpenCamera = useCallback(async () => { 78 76 if (!(await requestCameraAccessIfNeeded())) { ··· 126 124 onSelectNewBanner?.(null) 127 125 }, [onSelectNewBanner]) 128 126 129 - const _openLightbox = React.useCallback( 127 + const _openLightbox = useCallback( 130 128 (uri: string, thumbRect: MeasuredDimensions | null) => { 131 129 openLightbox({ 132 130 images: [ ··· 145 143 [openLightbox, highQualityImages], 146 144 ) 147 145 148 - const onPressBanner = React.useCallback(() => { 146 + const onPressBanner = useCallback(() => { 149 147 if (banner && !(moderation?.blur && moderation?.noOverride)) { 150 148 runOnUI(() => { 151 149 'worklet' ··· 175 173 <Image 176 174 testID="userBannerImage" 177 175 style={styles.bannerImage} 178 - source={{uri: banner}} 176 + source={{ 177 + uri: maybeModifyHighQualityImage( 178 + banner, 179 + highQualityImages, 180 + ), 181 + }} 179 182 accessible={true} 180 183 accessibilityIgnoresInvertColors 181 184 /> ··· 266 269 accessibilityHint=""> 267 270 <Image 268 271 testID="userBannerImage" 269 - style={[ 270 - styles.bannerImage, 271 - {backgroundColor: theme.palette.default.backgroundLight}, 272 - ]} 272 + style={[styles.bannerImage, t.atoms.bg_contrast_25]} 273 273 contentFit="cover" 274 - source={{uri: banner}} 274 + source={{uri: maybeModifyHighQualityImage(banner, highQualityImages)}} 275 275 blurRadius={moderation?.blur ? 100 : 0} 276 276 accessible={true} 277 277 accessibilityIgnoresInvertColors