Bluesky app fork with some witchin' additions 💫
0
fork

Configure Feed

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

Splash: reduce motion + dark mode (#2448)

* Don't use mask for android at all

* Handle reduced motion

* Add dark splash

* Add dark config

* Fix android version code

---------

Co-authored-by: Paul Frazee <pfrazee@gmail.com>

authored by

Eric Bailey
Paul Frazee
and committed by
GitHub
9eeff2cc fcfebda4

+119 -57
+12
app.config.js
··· 66 66 'Used for profile pictures, posts, and other kinds of content', 67 67 }, 68 68 associatedDomains: ['applinks:bsky.app', 'applinks:staging.bsky.app'], 69 + splash: { 70 + dark: { 71 + image: './assets/splash-dark.png', 72 + backgroundColor: '#001429', 73 + }, 74 + }, 69 75 }, 70 76 androidStatusBar: { 71 77 barStyle: 'dark-content', ··· 95 101 category: ['BROWSABLE', 'DEFAULT'], 96 102 }, 97 103 ], 104 + splash: { 105 + dark: { 106 + image: './assets/splash-dark.png', 107 + backgroundColor: '#001429', 108 + }, 109 + }, 98 110 }, 99 111 web: { 100 112 favicon: './assets/favicon.png',
assets/splash-dark.png

This is a binary file and will not be displayed.

+107 -57
src/Splash.tsx
··· 1 1 import React, {useCallback, useEffect} from 'react' 2 - import {View, StyleSheet, Image as RNImage} from 'react-native' 2 + import { 3 + View, 4 + StyleSheet, 5 + Image as RNImage, 6 + AccessibilityInfo, 7 + useColorScheme, 8 + } from 'react-native' 3 9 import * as SplashScreen from 'expo-splash-screen' 4 10 import {Image} from 'expo-image' 5 11 import Animated, { ··· 15 21 import Svg, {Path, SvgProps} from 'react-native-svg' 16 22 17 23 import {isAndroid} from '#/platform/detection' 24 + import {useColorMode} from 'state/shell' 25 + import {colors} from '#/lib/styles' 18 26 19 27 // @ts-ignore 20 28 import splashImagePointer from '../assets/splash.png' 29 + // @ts-ignore 30 + import darkSplashImagePointer from '../assets/splash-dark.png' 21 31 const splashImageUri = RNImage.resolveAssetSource(splashImagePointer).uri 32 + const darkSplashImageUri = RNImage.resolveAssetSource( 33 + darkSplashImagePointer, 34 + ).uri 22 35 23 36 export const Logo = React.forwardRef(function LogoImpl(props: SvgProps, ref) { 24 37 const width = 1000 ··· 29 42 // @ts-ignore it's fiiiiine 30 43 ref={ref} 31 44 viewBox="0 0 64 66" 32 - style={{width, height}}> 45 + style={[{width, height}, props.style]}> 33 46 <Path 34 - fill="#fff" 47 + fill={props.fill || '#fff'} 35 48 d="M13.873 3.77C21.21 9.243 29.103 20.342 32 26.3v15.732c0-.335-.13.043-.41.858-1.512 4.414-7.418 21.642-20.923 7.87-7.111-7.252-3.819-14.503 9.125-16.692-7.405 1.252-15.73-.817-18.014-8.93C1.12 22.804 0 8.431 0 6.488 0-3.237 8.579-.18 13.873 3.77ZM50.127 3.77C42.79 9.243 34.897 20.342 32 26.3v15.732c0-.335.13.043.41.858 1.512 4.414 7.418 21.642 20.923 7.87 7.111-7.252 3.819-14.503-9.125-16.692 7.405 1.252 15.73-.817 18.014-8.93C62.88 22.804 64 8.431 64 6.488 64-3.237 55.422-.18 50.127 3.77Z" 36 49 /> 37 50 </Svg> ··· 53 66 const [isAnimationComplete, setIsAnimationComplete] = React.useState(false) 54 67 const [isImageLoaded, setIsImageLoaded] = React.useState(false) 55 68 const [isLayoutReady, setIsLayoutReady] = React.useState(false) 56 - const isReady = props.isReady && isImageLoaded && isLayoutReady 69 + const [reduceMotion, setReduceMotion] = React.useState<boolean | undefined>( 70 + false, 71 + ) 72 + const isReady = 73 + props.isReady && 74 + isImageLoaded && 75 + isLayoutReady && 76 + reduceMotion !== undefined 77 + 78 + const colorMode = useColorMode() 79 + const colorScheme = useColorScheme() 80 + const themeName = colorMode === 'system' ? colorScheme : colorMode 81 + const isDarkMode = themeName === 'dark' 57 82 58 83 const logoAnimation = useAnimatedStyle(() => { 59 84 return { ··· 73 98 opacity: interpolate(intro.value, [0, 1], [0, 1], 'clamp'), 74 99 } 75 100 }) 101 + const reducedLogoAnimation = useAnimatedStyle(() => { 102 + return { 103 + transform: [ 104 + { 105 + scale: interpolate(intro.value, [0, 1], [0.8, 1], 'clamp'), 106 + }, 107 + ], 108 + opacity: interpolate(intro.value, [0, 1], [0, 1], 'clamp'), 109 + } 110 + }) 111 + 76 112 const logoWrapperAnimation = useAnimatedStyle(() => { 77 113 return { 78 114 opacity: interpolate( ··· 137 173 } 138 174 }, [onFinish, intro, outroLogo, outroApp, outroAppOpacity, isReady]) 139 175 176 + useEffect(() => { 177 + AccessibilityInfo.isReduceMotionEnabled().then(setReduceMotion) 178 + }, []) 179 + 180 + const logoAnimations = 181 + reduceMotion === true ? reducedLogoAnimation : logoAnimation 182 + 140 183 return ( 141 184 <View style={{flex: 1}} onLayout={onLayout}> 142 185 {!isAnimationComplete && ( 143 186 <Image 144 187 accessibilityIgnoresInvertColors 145 188 onLoadEnd={onLoadEnd} 146 - source={{uri: splashImageUri}} 189 + source={{uri: isDarkMode ? darkSplashImageUri : splashImageUri}} 147 190 style={StyleSheet.absoluteFillObject} 148 191 /> 149 192 )} 150 193 151 - {isAndroid ? ( 152 - // Use a simple fade on older versions of android (work around a bug) 153 - <> 154 - <Animated.View style={[{flex: 1}, appAnimation]}> 155 - {props.children} 156 - </Animated.View> 157 - 158 - {!isAnimationComplete && ( 159 - <Animated.View 160 - style={[ 161 - StyleSheet.absoluteFillObject, 162 - logoWrapperAnimation, 163 - { 164 - flex: 1, 165 - justifyContent: 'center', 166 - alignItems: 'center', 167 - transform: [{translateY: -(insets.top / 2)}, {scale: 0.1}], // scale from 1000px to 100px 168 - }, 169 - ]}> 170 - <AnimatedLogo style={[{opacity: 0}, logoAnimation]} /> 194 + {isReady && 195 + (isAndroid || reduceMotion === true ? ( 196 + // Use a simple fade on older versions of android (work around a bug) 197 + <> 198 + <Animated.View style={[{flex: 1}, appAnimation]}> 199 + {props.children} 171 200 </Animated.View> 172 - )} 173 - </> 174 - ) : ( 175 - <MaskedView 176 - style={[StyleSheet.absoluteFillObject]} 177 - maskElement={ 178 - <Animated.View 179 - style={[ 180 - { 181 - // Transparent background because mask is based off alpha channel. 182 - backgroundColor: 'transparent', 183 - flex: 1, 184 - justifyContent: 'center', 185 - alignItems: 'center', 186 - transform: [{translateY: -(insets.top / 2)}, {scale: 0.1}], // scale from 1000px to 100px 187 - }, 188 - ]}> 189 - <AnimatedLogo style={[logoAnimation]} /> 201 + 202 + {!isAnimationComplete && ( 203 + <Animated.View 204 + style={[ 205 + StyleSheet.absoluteFillObject, 206 + logoWrapperAnimation, 207 + { 208 + flex: 1, 209 + justifyContent: 'center', 210 + alignItems: 'center', 211 + transform: [{translateY: -(insets.top / 2)}, {scale: 0.1}], // scale from 1000px to 100px 212 + }, 213 + ]}> 214 + <AnimatedLogo 215 + fill={isDarkMode ? colors.blue3 : '#fff'} 216 + style={[{opacity: 0}, logoAnimations]} 217 + /> 218 + </Animated.View> 219 + )} 220 + </> 221 + ) : ( 222 + <MaskedView 223 + style={[StyleSheet.absoluteFillObject]} 224 + maskElement={ 225 + <Animated.View 226 + style={[ 227 + { 228 + // Transparent background because mask is based off alpha channel. 229 + backgroundColor: 'transparent', 230 + flex: 1, 231 + justifyContent: 'center', 232 + alignItems: 'center', 233 + transform: [{translateY: -(insets.top / 2)}, {scale: 0.1}], // scale from 1000px to 100px 234 + }, 235 + ]}> 236 + <AnimatedLogo 237 + fill={isDarkMode ? colors.blue3 : '#fff'} 238 + style={[logoAnimations]} 239 + /> 240 + </Animated.View> 241 + }> 242 + {!isAnimationComplete && ( 243 + <View 244 + style={[ 245 + StyleSheet.absoluteFillObject, 246 + {backgroundColor: isDarkMode ? colors.blue3 : '#fff'}, 247 + ]} 248 + /> 249 + )} 250 + <Animated.View style={[{flex: 1}, appAnimation]}> 251 + {props.children} 190 252 </Animated.View> 191 - }> 192 - {!isAnimationComplete && ( 193 - <View 194 - style={[ 195 - StyleSheet.absoluteFillObject, 196 - {backgroundColor: 'white'}, 197 - ]} 198 - /> 199 - )} 200 - <Animated.View style={[{flex: 1}, appAnimation]}> 201 - {props.children} 202 - </Animated.View> 203 - </MaskedView> 204 - )} 253 + </MaskedView> 254 + ))} 205 255 </View> 206 256 ) 207 257 }