Bluesky app fork with some witchin' additions 💫
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Merge branch 'main' of https://github.com/bluesky-social/social-app

+86 -35
+1 -2
app.config.js
··· 182 182 icon: './assets/app-icons/android_icon_default_next.png', 183 183 adaptiveIcon: { 184 184 foregroundImage: './assets/icon-android-foreground.png', 185 - monochromeImage: './assets/icon-android-foreground.png', 186 - backgroundImage: './assets/icon-android-background.png', 185 + monochromeImage: './assets/icon-android-monochrome.png', 187 186 backgroundColor: '#8e4b9b', 188 187 }, 189 188 googleServicesFile: './google-services.json',
assets/icon-android-background.png

This is a binary file and will not be displayed.

assets/icon-android-monochrome.png

This is a binary file and will not be displayed.

+4 -1
src/components/Post/Embed/ImageEmbed.tsx
··· 69 69 } 70 70 const onPressIn = (_: number) => { 71 71 InteractionManager.runAfterInteractions(() => { 72 - Image.prefetch(items.map(i => i.uri)) 72 + Image.prefetch( 73 + items.map(i => i.uri), 74 + 'memory', 75 + ) 73 76 }) 74 77 } 75 78
+8 -1
src/lib/hooks/useOTAUpdates.ts
··· 12 12 13 13 import {isNetworkError} from '#/lib/strings/errors' 14 14 import {logger} from '#/logger' 15 - import {isIOS} from '#/platform/detection' 15 + import {isAndroid, isIOS} from '#/platform/detection' 16 16 import {IS_TESTFLIGHT} from '#/env' 17 17 18 18 const MINIMUM_MINIMIZE_TIME = 15 * 60e3 ··· 192 192 if (!isEnabled || currentChannel?.startsWith('pull-request')) { 193 193 return 194 194 } 195 + 196 + // TEMP: disable wake-from-background OTA loading on Android. 197 + // This is causing a crash when the thread view is open due to 198 + // `maintainVisibleContentPosition`. See repro repo for more details: 199 + // https://github.com/mozzius/ota-crash-repro 200 + // Old Arch only - re-enable once we're on the New Archictecture! -sfn 201 + if (isAndroid) return 195 202 196 203 const subscription = AppState.addEventListener( 197 204 'change',
+10 -3
src/lib/media/manip.ts
··· 432 432 433 433 async function downloadImage(uri: string, path: string, timeout: number) { 434 434 const dlResumable = createDownloadResumable(uri, path, {cache: true}) 435 - 436 - const to1 = setTimeout(() => dlResumable.cancelAsync(), timeout) 435 + let timedOut = false 436 + const to1 = setTimeout(() => { 437 + timedOut = true 438 + dlResumable.cancelAsync() 439 + }, timeout) 437 440 438 441 const dlRes = await dlResumable.downloadAsync() 439 442 clearTimeout(to1) 440 443 441 444 if (!dlRes?.uri) { 442 - throw new Error('Failed to download image - dlRes is undefined') 445 + if (timedOut) { 446 + throw new Error('Failed to download image - timed out') 447 + } else { 448 + throw new Error('Failed to download image - dlRes is undefined') 449 + } 443 450 } 444 451 445 452 return normalizePath(dlRes.uri)
-1
src/lib/statsig/gates.ts
··· 8 8 | 'old_postonboarding' 9 9 | 'onboarding_add_video_feed' 10 10 | 'onboarding_suggested_starterpacks' 11 - | 'post_follow_profile_suggested_accounts' 12 11 | 'remove_show_latest_button' 13 12 | 'test_gate_1' 14 13 | 'test_gate_2'
+4
src/logger/metrics.ts
··· 196 196 count: number 197 197 } 198 198 199 + 'feed:discover:emptyError': { 200 + userDid: string 201 + } 202 + 199 203 'composer:gif:open': {} 200 204 'composer:gif:select': {} 201 205
-4
src/screens/Profile/Header/SuggestedFollows.tsx
··· 1 1 import {AccordionAnimation} from '#/lib/custom-animations/AccordionAnimation' 2 - import {useGate} from '#/lib/statsig/statsig' 3 2 import {isAndroid} from '#/platform/detection' 4 3 import {useSuggestedFollowsByActorQuery} from '#/state/queries/suggested-follows' 5 4 import {ProfileGrid} from '#/components/FeedInterstitials' ··· 27 26 isExpanded: boolean 28 27 actorDid: string 29 28 }) { 30 - const gate = useGate() 31 29 const {isLoading, data, error} = useSuggestedFollowsByActorQuery({ 32 30 did: actorDid, 33 31 }) ··· 40 38 * Blocking the ability to scroll on Android is too much of a trade-off for now. 41 39 **/ 42 40 if (isAndroid) return null 43 - 44 - if (!gate('post_follow_profile_suggested_accounts')) return null 45 41 46 42 return ( 47 43 <AccordionAnimation isExpanded={isExpanded}>
+1 -1
src/view/com/composer/Composer.tsx
··· 580 580 label={_(msg`View post`)} 581 581 onPress={() => { 582 582 const {host: name, rkey} = new AtUri(postUri) 583 - navigation.navigate('PostThread', {name, rkey}) 583 + navigation.push('PostThread', {name, rkey}) 584 584 }}> 585 585 <Trans context="Action to view the post the user just created"> 586 586 View
+1
src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.ios.tsx
··· 252 252 onLoad({width: e.source.width, height: e.source.height}) 253 253 } 254 254 } 255 + cachePolicy="memory" 255 256 /> 256 257 </Animated.View> 257 258 </Animated.View>
+24 -1
src/view/com/posts/CustomFeedEmptyState.tsx
··· 1 - import React from 'react' 1 + import React, {useEffect} from 'react' 2 2 import {StyleSheet, View} from 'react-native' 3 3 import { 4 4 FontAwesomeIcon, ··· 7 7 import {Trans} from '@lingui/macro' 8 8 import {useNavigation} from '@react-navigation/native' 9 9 10 + import {DISCOVER_FEED_URI} from '#/lib/constants' 10 11 import {usePalette} from '#/lib/hooks/usePalette' 11 12 import {MagnifyingGlassIcon} from '#/lib/icons' 12 13 import {type NavigationProp} from '#/lib/routes/types' 13 14 import {s} from '#/lib/styles' 15 + import {logger} from '#/logger' 14 16 import {isWeb} from '#/platform/detection' 17 + import {useFeedFeedbackContext} from '#/state/feed-feedback' 18 + import {useSession} from '#/state/session' 15 19 import {Button} from '../util/forms/Button' 16 20 import {Text} from '../util/text/Text' 17 21 18 22 export function CustomFeedEmptyState() { 23 + const feedFeedback = useFeedFeedbackContext() 24 + const {currentAccount} = useSession() 25 + const hasLoggedDiscoverEmptyErrorRef = React.useRef(false) 26 + 27 + useEffect(() => { 28 + // Log the empty feed error event 29 + if (feedFeedback.feedSourceInfo && currentAccount?.did) { 30 + const uri = feedFeedback.feedSourceInfo.uri 31 + if ( 32 + uri === DISCOVER_FEED_URI && 33 + !hasLoggedDiscoverEmptyErrorRef.current 34 + ) { 35 + hasLoggedDiscoverEmptyErrorRef.current = true 36 + logger.metric('feed:discover:emptyError', { 37 + userDid: currentAccount.did, 38 + }) 39 + } 40 + } 41 + }, [feedFeedback.feedSourceInfo, currentAccount?.did]) 19 42 const pal = usePalette('default') 20 43 const palInverted = usePalette('inverted') 21 44 const navigation = useNavigation<NavigationProp>()
+11 -3
src/view/com/util/List.tsx
··· 39 39 sideBorders?: boolean 40 40 progressViewOffset?: number 41 41 } 42 - export type ListRef = React.MutableRefObject<FlatList_INTERNAL | null> 42 + export type ListRef = React.RefObject<FlatList_INTERNAL | null> 43 43 44 44 const SCROLLED_DOWN_LIMIT = 200 45 45 ··· 61 61 const isScrolledDown = useSharedValue(false) 62 62 const t = useTheme() 63 63 const dedupe = useDedupe(400) 64 - const {activeLightbox} = useLightbox() 64 + const scrollsToTop = useAllowScrollToTop() 65 65 66 66 function handleScrolledDownChange(didScrollDown: boolean) { 67 67 onScrolledDownChange?.(didScrollDown) ··· 168 168 contentOffset={contentOffset} 169 169 refreshControl={refreshControl} 170 170 onScroll={scrollHandler} 171 - scrollsToTop={!activeLightbox} 171 + scrollsToTop={scrollsToTop} 172 172 scrollEventThrottle={1} 173 173 style={style} 174 174 // @ts-expect-error FlatList_INTERNAL ref type is wrong -sfn ··· 181 181 182 182 List = memo(List) 183 183 export {List} 184 + 185 + // We only want to use this context value on iOS because the `scrollsToTop` prop is iOS-only 186 + // removing it saves us a re-render on Android 187 + const useAllowScrollToTop = isIOS ? useAllowScrollToTopIOS : () => undefined 188 + function useAllowScrollToTopIOS() { 189 + const {activeLightbox} = useLightbox() 190 + return !activeLightbox 191 + }
+9
src/view/com/util/images/AutoSizedImage.tsx
··· 6 6 } from 'react-native-reanimated' 7 7 import {Image} from 'expo-image' 8 8 import {type AppBskyEmbedImages} from '@atproto/api' 9 + import {utils} from '@bsky.app/alf' 9 10 import {msg} from '@lingui/macro' 10 11 import {useLingui} from '@lingui/react' 11 12 ··· 210 211 accessibilityLabel={image.alt} 211 212 accessibilityHint={_(msg`Views full image`)} 212 213 accessibilityRole="button" 214 + android_ripple={{ 215 + color: utils.alpha(t.atoms.bg.backgroundColor, 0.2), 216 + foreground: true, 217 + }} 213 218 style={[ 214 219 a.w_full, 215 220 a.rounded_md, ··· 233 238 accessibilityLabel={image.alt} 234 239 accessibilityHint={_(msg`Views full image`)} 235 240 accessibilityRole="button" 241 + android_ripple={{ 242 + color: utils.alpha(t.atoms.bg.backgroundColor, 0.2), 243 + foreground: true, 244 + }} 236 245 style={[a.h_full]}> 237 246 {contents} 238 247 </Pressable>
+5
src/view/com/util/images/Gallery.tsx
··· 2 2 import {type AnimatedRef} from 'react-native-reanimated' 3 3 import {Image, type ImageStyle} from 'expo-image' 4 4 import {type AppBskyEmbedImages} from '@atproto/api' 5 + import {utils} from '@bsky.app/alf' 5 6 import {msg} from '@lingui/macro' 6 7 import {useLingui} from '@lingui/react' 7 8 ··· 65 66 } 66 67 onPressIn={onPressIn ? () => onPressIn(index) : undefined} 67 68 onLongPress={onLongPress ? () => onLongPress(index) : undefined} 69 + android_ripple={{ 70 + color: utils.alpha(t.atoms.bg.backgroundColor, 0.2), 71 + foreground: true, 72 + }} 68 73 style={[ 69 74 a.flex_1, 70 75 a.overflow_hidden,
+3 -7
src/view/shell/desktop/Feeds.tsx
··· 27 27 28 28 if (isLoading) { 29 29 return ( 30 - <View 31 - style={[ 32 - { 33 - gap: 10, 34 - paddingVertical: 2, 35 - }, 36 - ]}> 30 + <View style={[{gap: 10}]}> 37 31 {Array(5) 38 32 .fill(0) 39 33 .map((_, i) => ( ··· 60 54 return ( 61 55 <View 62 56 style={[ 57 + a.flex_1, 63 58 web({ 64 59 gap: 10, 65 60 /* ··· 89 84 style={[ 90 85 a.text_md, 91 86 a.leading_snug, 87 + a.flex_shrink_0, 92 88 current 93 89 ? [a.font_semi_bold, t.atoms.text] 94 90 : [t.atoms.text_contrast_medium],
+1 -2
src/view/shell/desktop/RightNav.tsx
··· 79 79 * Compensate for the right padding above (2px) to retain intended width. 80 80 */ 81 81 width: width + gutters.paddingLeft + 2, 82 - maxHeight: '100%', 83 - overflowY: 'auto', 82 + maxHeight: '100vh', 84 83 }), 85 84 ]}> 86 85 {!isSearchScreen && <DesktopSearch />}
+4 -9
yarn.lock
··· 9210 9210 lodash.memoize "^4.1.2" 9211 9211 lodash.uniq "^4.5.0" 9212 9212 9213 - caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001517, caniuse-lite@^1.0.30001587, caniuse-lite@^1.0.30001669: 9214 - version "1.0.30001697" 9215 - resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001697.tgz" 9216 - integrity sha512-GwNPlWJin8E+d7Gxq96jxM6w0w+VFeyyXRsjU58emtkYqnbwHqXm5uT2uCmO0RQE9htWknOP4xtBlLmM/gWxvQ== 9217 - 9218 - caniuse-lite@^1.0.30001741: 9219 - version "1.0.30001743" 9220 - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001743.tgz#50ff91a991220a1ee2df5af00650dd5c308ea7cd" 9221 - integrity sha512-e6Ojr7RV14Un7dz6ASD0aZDmQPT/A+eZU+nuTNfjqmRrmkmQlnTNWH0SKmqagx9PeW87UVqapSurtAXifmtdmw== 9213 + caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001517, caniuse-lite@^1.0.30001587, caniuse-lite@^1.0.30001669, caniuse-lite@^1.0.30001741: 9214 + version "1.0.30001751" 9215 + resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001751.tgz" 9216 + integrity sha512-A0QJhug0Ly64Ii3eIqHu5X51ebln3k4yTUkY1j8drqpWHVreg/VLijN48cZ1bYPiqOQuqpkIKnzr/Ul8V+p6Cw== 9222 9217 9223 9218 cbor-extract@^2.1.1: 9224 9219 version "2.1.1"