Bluesky app fork with some witchin' additions 💫

[APP-1801] New splash screen (#9780)

* MVP splash

* MVP of login screen

* Hack a footer into the logged out view

* revert unneeded

(cherry picked from commit 296420aa0df433b101389c44b05a40696e7389ae)

* Update config

(cherry picked from commit eac421966dfdee99d4c818ac07835a2a97e7590f)

* Bump size slightly

authored by

Eric Bailey and committed by
GitHub
0ac5b07b d13df6c7

+104 -226
+14 -16
app.config.js
··· 310 310 [ 311 311 'expo-splash-screen', 312 312 { 313 - ios: { 314 - enableFullScreenImage_legacy: true, 315 - backgroundColor: '#ffffff', 316 - image: './assets/splash.png', 313 + enableFullScreenImage_legacy: true, // iOS only 314 + backgroundColor: '#EEF5FF', // based on illustration 315 + image: './assets/splash/splash-mobile.png', 316 + resizeMode: 'cover', 317 + dark: { 318 + enableFullScreenImage_legacy: true, // iOS only 319 + backgroundColor: '#446DA9', // based on illustration 320 + image: './assets/splash/splash-mobile-dark.png', 317 321 resizeMode: 'cover', 318 - dark: { 319 - enableFullScreenImage_legacy: true, 320 - backgroundColor: '#001429', 321 - image: './assets/splash-dark.png', 322 - resizeMode: 'cover', 323 - }, 324 322 }, 325 323 android: { 326 - backgroundColor: '#0c7cff', 327 - image: './assets/splash-android-icon.png', 328 - imageWidth: 150, 324 + backgroundColor: '#A8CCFF', // primary_200 325 + image: './assets/splash/android-splash-logo-white.png', 326 + imageWidth: 105, 329 327 dark: { 330 - backgroundColor: '#0c2a49', 331 - image: './assets/splash-android-icon-dark.png', 332 - imageWidth: 150, 328 + backgroundColor: '#00398A', // primary_800 329 + image: './assets/splash/android-splash-logo-white.png', 330 + imageWidth: 105, 333 331 }, 334 332 }, 335 333 },
assets/splash-android-icon-dark.png

This is a binary file and will not be displayed.

assets/splash-android-icon.png

This is a binary file and will not be displayed.

assets/splash-dark.png

This is a binary file and will not be displayed.

assets/splash.png

This is a binary file and will not be displayed.

assets/splash/android-splash-logo-white.png

This is a binary file and will not be displayed.

assets/splash/splash-mobile-dark.png

This is a binary file and will not be displayed.

assets/splash/splash-mobile.png

This is a binary file and will not be displayed.

+9 -149
src/Splash.tsx
··· 9 9 import Animated, { 10 10 Easing, 11 11 interpolate, 12 - runOnJS, 13 12 useAnimatedStyle, 14 13 useSharedValue, 15 14 withTiming, 16 15 } from 'react-native-reanimated' 17 - import {useSafeAreaInsets} from 'react-native-safe-area-context' 18 - import Svg, {Path, type SvgProps} from 'react-native-svg' 19 16 import {Image} from 'expo-image' 20 17 import * as SplashScreen from 'expo-splash-screen' 21 18 22 - import {Logotype} from '#/view/icons/Logotype' 23 19 // @ts-ignore 24 - import splashImagePointer from '../assets/splash.png' 20 + import splashImagePointer from '../assets/splash/splash-mobile.png' 25 21 // @ts-ignore 26 - import darkSplashImagePointer from '../assets/splash-dark.png' 22 + import darkSplashImagePointer from '../assets/splash/splash-mobile-dark.png' 27 23 const splashImageUri = RNImage.resolveAssetSource(splashImagePointer).uri 28 24 const darkSplashImageUri = RNImage.resolveAssetSource( 29 25 darkSplashImagePointer, 30 26 ).uri 31 27 32 - export const Logo = React.forwardRef(function LogoImpl(props: SvgProps, ref) { 33 - const width = 1000 34 - const height = width * (67 / 64) 35 - return ( 36 - <Svg 37 - fill="none" 38 - // @ts-ignore it's fiiiiine 39 - ref={ref} 40 - viewBox="0 0 64 66" 41 - style={[{width, height}, props.style]}> 42 - <Path 43 - fill={props.fill || '#fff'} 44 - 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" 45 - /> 46 - </Svg> 47 - ) 48 - }) 49 - 50 28 type Props = { 51 29 isReady: boolean 52 30 } 53 31 54 32 export function Splash(props: React.PropsWithChildren<Props>) { 55 33 'use no memo' 56 - const insets = useSafeAreaInsets() 57 - const intro = useSharedValue(0) 58 - const outroLogo = useSharedValue(0) 59 - const outroApp = useSharedValue(0) 60 34 const outroAppOpacity = useSharedValue(0) 35 + const colorScheme = useColorScheme() 61 36 const [isAnimationComplete, setIsAnimationComplete] = React.useState(false) 62 37 const [isImageLoaded, setIsImageLoaded] = React.useState(false) 63 38 const [isLayoutReady, setIsLayoutReady] = React.useState(false) ··· 69 44 isImageLoaded && 70 45 isLayoutReady && 71 46 reduceMotion !== undefined 72 - 73 - const colorScheme = useColorScheme() 74 47 const isDarkMode = colorScheme === 'dark' 75 48 76 - const logoAnimation = useAnimatedStyle(() => { 77 - return { 78 - transform: [ 79 - { 80 - scale: interpolate(intro.get(), [0, 1], [0.8, 1], 'clamp'), 81 - }, 82 - { 83 - scale: interpolate( 84 - outroLogo.get(), 85 - [0, 0.08, 1], 86 - [1, 0.8, 500], 87 - 'clamp', 88 - ), 89 - }, 90 - ], 91 - opacity: interpolate(intro.get(), [0, 1], [0, 1], 'clamp'), 92 - } 93 - }) 94 - const bottomLogoAnimation = useAnimatedStyle(() => { 95 - return { 96 - opacity: interpolate(intro.get(), [0, 1], [0, 1], 'clamp'), 97 - } 98 - }) 99 - const reducedLogoAnimation = useAnimatedStyle(() => { 100 - return { 101 - transform: [ 102 - { 103 - scale: interpolate(intro.get(), [0, 1], [0.8, 1], 'clamp'), 104 - }, 105 - ], 106 - opacity: interpolate(intro.get(), [0, 1], [0, 1], 'clamp'), 107 - } 108 - }) 109 - 110 - const logoWrapperAnimation = useAnimatedStyle(() => { 111 - return { 112 - opacity: interpolate( 113 - outroAppOpacity.get(), 114 - [0, 0.1, 0.2, 1], 115 - [1, 1, 0, 0], 116 - 'clamp', 117 - ), 118 - } 119 - }) 120 - 121 49 const appAnimation = useAnimatedStyle(() => { 122 50 return { 123 - transform: [ 124 - { 125 - scale: interpolate(outroApp.get(), [0, 1], [1.1, 1], 'clamp'), 126 - }, 127 - ], 128 51 opacity: interpolate( 129 52 outroAppOpacity.get(), 130 53 [0, 0.1, 0.2, 1], ··· 142 65 if (isReady) { 143 66 SplashScreen.hideAsync() 144 67 .then(() => { 145 - intro.set(() => 146 - withTiming( 147 - 1, 148 - {duration: 400, easing: Easing.out(Easing.cubic)}, 149 - async () => { 150 - // set these values to check animation at specific point 151 - outroLogo.set(() => 152 - withTiming( 153 - 1, 154 - {duration: 1200, easing: Easing.in(Easing.cubic)}, 155 - () => { 156 - runOnJS(onFinish)() 157 - }, 158 - ), 159 - ) 160 - outroApp.set(() => 161 - withTiming(1, { 162 - duration: 1200, 163 - easing: Easing.inOut(Easing.cubic), 164 - }), 165 - ) 166 - outroAppOpacity.set(() => 167 - withTiming(1, { 168 - duration: 1200, 169 - easing: Easing.in(Easing.cubic), 170 - }), 171 - ) 172 - }, 173 - ), 68 + outroAppOpacity.set(() => 69 + withTiming(1, { 70 + duration: 1200, 71 + easing: Easing.in(Easing.cubic), 72 + }), 174 73 ) 175 74 }) 176 75 .catch(() => {}) 177 76 } 178 - }, [onFinish, intro, outroLogo, outroApp, outroAppOpacity, isReady]) 77 + }, [onFinish, outroAppOpacity, isReady]) 179 78 180 79 useEffect(() => { 181 80 AccessibilityInfo.isReduceMotionEnabled().then(setReduceMotion) 182 81 }, []) 183 82 184 - const logoAnimations = 185 - reduceMotion === true ? reducedLogoAnimation : logoAnimation 186 - // special off-spec color for dark mode 187 - const logoBg = isDarkMode ? '#0F1824' : '#fff' 188 - 189 83 return ( 190 84 <View style={{flex: 1}} onLayout={onLayout}> 191 85 {!isAnimationComplete && ( ··· 196 90 source={{uri: isDarkMode ? darkSplashImageUri : splashImageUri}} 197 91 style={StyleSheet.absoluteFillObject} 198 92 /> 199 - 200 - <Animated.View 201 - style={[ 202 - bottomLogoAnimation, 203 - { 204 - position: 'absolute', 205 - bottom: insets.bottom + 40, 206 - left: 0, 207 - right: 0, 208 - alignItems: 'center', 209 - justifyContent: 'center', 210 - opacity: 0, 211 - }, 212 - ]}> 213 - <Logotype fill="#fff" width={90} /> 214 - </Animated.View> 215 93 </View> 216 94 )} 217 95 ··· 220 98 <Animated.View style={[{flex: 1}, appAnimation]}> 221 99 {props.children} 222 100 </Animated.View> 223 - 224 - {!isAnimationComplete && ( 225 - <Animated.View 226 - style={[ 227 - StyleSheet.absoluteFillObject, 228 - logoWrapperAnimation, 229 - { 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 - <Animated.View style={[logoAnimations]}> 237 - <Logo fill={logoBg} /> 238 - </Animated.View> 239 - </Animated.View> 240 - )} 241 101 </> 242 102 )} 243 103 </View>
+81 -61
src/view/com/auth/SplashScreen.tsx
··· 1 - import {View} from 'react-native' 1 + import {useMemo} from 'react' 2 + import {Image as RNImage, View} from 'react-native' 2 3 import Animated, {FadeIn, FadeOut} from 'react-native-reanimated' 3 - import {useSafeAreaInsets} from 'react-native-safe-area-context' 4 + import {Image} from 'expo-image' 4 5 import {msg, Trans} from '@lingui/macro' 5 6 import {useLingui} from '@lingui/react' 6 7 7 8 import {useHaptics} from '#/lib/haptics' 8 - import {ErrorBoundary} from '#/view/com/util/ErrorBoundary' 9 - import {CenteredView} from '#/view/com/util/Views' 10 9 import {Logo} from '#/view/icons/Logo' 11 10 import {Logotype} from '#/view/icons/Logotype' 12 11 import {atoms as a, useTheme} from '#/alf' 13 - import {AppLanguageDropdown} from '#/components/AppLanguageDropdown' 14 12 import {Button, ButtonText} from '#/components/Button' 15 - import {Text} from '#/components/Typography' 13 + // @ts-ignore 14 + import splashImagePointer from '../../../../assets/splash/splash-mobile.png' 15 + // @ts-ignore 16 + import darkSplashImagePointer from '../../../../assets/splash/splash-mobile-dark.png' 17 + const splashImageUri = RNImage.resolveAssetSource(splashImagePointer).uri 18 + const darkSplashImageUri = RNImage.resolveAssetSource( 19 + darkSplashImagePointer, 20 + ).uri 16 21 17 22 export const SplashScreen = ({ 18 23 onPressSignin, ··· 23 28 }) => { 24 29 const t = useTheme() 25 30 const {_} = useLingui() 31 + const isDarkMode = t.name !== 'light' 26 32 27 33 const playHaptic = useHaptics() 28 - const insets = useSafeAreaInsets() 34 + 35 + const styles = useMemo(() => { 36 + const logoFill = isDarkMode ? 'white' : t.palette.primary_500 37 + return { 38 + logoFill, 39 + logoShadow: isDarkMode 40 + ? [ 41 + t.atoms.shadow_md, 42 + { 43 + shadowColor: logoFill, 44 + shadowOpacity: 0.5, 45 + shadowOffset: { 46 + width: 0, 47 + height: 0, 48 + }, 49 + }, 50 + ] 51 + : [], 52 + } 53 + }, [t, isDarkMode]) 29 54 30 55 return ( 31 - <CenteredView style={[a.h_full, a.flex_1]}> 56 + <> 57 + <Image 58 + accessibilityIgnoresInvertColors 59 + source={{uri: isDarkMode ? darkSplashImageUri : splashImageUri}} 60 + style={[a.absolute, a.inset_0]} 61 + /> 62 + 32 63 <Animated.View 33 64 entering={FadeIn.duration(90)} 34 65 exiting={FadeOut.duration(90)} 35 66 style={[a.flex_1]}> 36 - <ErrorBoundary> 37 - <View style={[a.flex_1, a.justify_center, a.align_center]}> 38 - <Logo width={92} fill="sky" /> 39 - 40 - <View style={[a.pb_sm, a.pt_5xl]}> 41 - <Logotype width={161} fill={t.atoms.text.color} /> 42 - </View> 67 + <View 68 + style={[a.justify_center, a.align_center, {gap: 6, paddingTop: 46}]}> 69 + <Logo width={76} fill={styles.logoFill} style={styles.logoShadow} /> 70 + <Logotype 71 + width={91} 72 + fill={styles.logoFill} 73 + style={styles.logoShadow} 74 + /> 75 + </View> 43 76 44 - <Text 45 - style={[ 46 - a.text_md, 47 - a.font_semi_bold, 48 - t.atoms.text_contrast_medium, 49 - a.text_center, 50 - ]}> 51 - <Trans>What's up?</Trans> 52 - </Text> 53 - </View> 77 + <View style={[a.flex_1]} /> 54 78 79 + <View 80 + testID="signinOrCreateAccount" 81 + style={[a.px_xl, a.gap_md, a.pb_sm]}> 55 82 <View 56 - testID="signinOrCreateAccount" 57 - style={[a.px_xl, a.gap_md, a.pb_2xl]}> 83 + style={[ 84 + t.atoms.shadow_md, 85 + { 86 + shadowOpacity: 0.1, 87 + shadowOffset: { 88 + width: 0, 89 + height: 5, 90 + }, 91 + }, 92 + ]}> 58 93 <Button 59 94 testID="createAccountButton" 60 95 onPress={() => { ··· 66 101 msg`Opens flow to create a new Bluesky account`, 67 102 )} 68 103 size="large" 69 - variant="solid" 70 - color="primary"> 104 + color={isDarkMode ? 'secondary_inverted' : 'secondary'}> 71 105 <ButtonText> 72 106 <Trans>Create account</Trans> 73 107 </ButtonText> 74 108 </Button> 75 - <Button 76 - testID="signInButton" 77 - onPress={() => { 78 - onPressSignin() 79 - playHaptic('Light') 80 - }} 81 - label={_(msg`Sign in`)} 82 - accessibilityHint={_( 83 - msg`Opens flow to sign in to your existing Bluesky account`, 84 - )} 85 - size="large" 86 - variant="solid" 87 - color="secondary"> 88 - <ButtonText> 89 - <Trans>Sign in</Trans> 90 - </ButtonText> 91 - </Button> 92 109 </View> 93 - <View 94 - style={[ 95 - a.px_lg, 96 - a.pt_md, 97 - a.pb_2xl, 98 - a.justify_center, 99 - a.align_center, 100 - ]}> 101 - <View> 102 - <AppLanguageDropdown /> 103 - </View> 104 - </View> 105 - <View style={{height: insets.bottom}} /> 106 - </ErrorBoundary> 110 + 111 + <Button 112 + testID="signInButton" 113 + onPress={() => { 114 + onPressSignin() 115 + playHaptic('Light') 116 + }} 117 + label={_(msg`Sign in`)} 118 + accessibilityHint={_( 119 + msg`Opens flow to sign in to your existing Bluesky account`, 120 + )} 121 + size="large"> 122 + <ButtonText style={{color: 'white'}}> 123 + <Trans>Sign in</Trans> 124 + </ButtonText> 125 + </Button> 126 + </View> 107 127 </Animated.View> 108 - </CenteredView> 128 + </> 109 129 ) 110 130 }