Bluesky app fork with some witchin' additions 💫

Remove old old onboarding (#3674)

authored by

Eric Bailey and committed by
GitHub
05beb1bb 24da3a8f

+8 -1196
-1
src/lib/build-flags.ts
··· 1 1 export const LOGIN_INCLUDE_DEV_SERVERS = true 2 2 export const PWI_ENABLED = true 3 - export const NEW_ONBOARDING_ENABLED = true
-51
src/view/com/auth/Onboarding.tsx
··· 1 - import React from 'react' 2 - import {SafeAreaView, Platform} from 'react-native' 3 - import {ErrorBoundary} from 'view/com/util/ErrorBoundary' 4 - import {s} from 'lib/styles' 5 - import {usePalette} from 'lib/hooks/usePalette' 6 - import {Welcome} from './onboarding/Welcome' 7 - import {RecommendedFeeds} from './onboarding/RecommendedFeeds' 8 - import {RecommendedFollows} from './onboarding/RecommendedFollows' 9 - import {useSetMinimalShellMode} from '#/state/shell/minimal-mode' 10 - import {useOnboardingState, useOnboardingDispatch} from '#/state/shell' 11 - 12 - export function Onboarding() { 13 - const pal = usePalette('default') 14 - const setMinimalShellMode = useSetMinimalShellMode() 15 - const onboardingState = useOnboardingState() 16 - const onboardingDispatch = useOnboardingDispatch() 17 - 18 - React.useEffect(() => { 19 - setMinimalShellMode(true) 20 - }, [setMinimalShellMode]) 21 - 22 - const next = () => onboardingDispatch({type: 'next'}) 23 - const skip = () => onboardingDispatch({type: 'skip'}) 24 - 25 - return ( 26 - <SafeAreaView 27 - testID="onboardingView" 28 - style={[ 29 - s.hContentRegion, 30 - pal.view, 31 - // @ts-ignore web only -esb 32 - Platform.select({ 33 - web: { 34 - height: '100vh', 35 - }, 36 - }), 37 - ]}> 38 - <ErrorBoundary> 39 - {onboardingState.step === 'Welcome' && ( 40 - <Welcome skip={skip} next={next} /> 41 - )} 42 - {onboardingState.step === 'RecommendedFeeds' && ( 43 - <RecommendedFeeds next={next} /> 44 - )} 45 - {onboardingState.step === 'RecommendedFollows' && ( 46 - <RecommendedFollows next={next} /> 47 - )} 48 - </ErrorBoundary> 49 - </SafeAreaView> 50 - ) 51 - }
-211
src/view/com/auth/onboarding/RecommendedFeeds.tsx
··· 1 - import React from 'react' 2 - import {ActivityIndicator, FlatList, StyleSheet, View} from 'react-native' 3 - import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 4 - import {msg, Trans} from '@lingui/macro' 5 - import {useLingui} from '@lingui/react' 6 - 7 - import {useSuggestedFeedsQuery} from '#/state/queries/suggested-feeds' 8 - import {usePalette} from 'lib/hooks/usePalette' 9 - import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' 10 - import {ErrorMessage} from 'view/com/util/error/ErrorMessage' 11 - import {Button} from 'view/com/util/forms/Button' 12 - import {Mobile, TabletOrDesktop} from 'view/com/util/layouts/Breakpoints' 13 - import {TitleColumnLayout} from 'view/com/util/layouts/TitleColumnLayout' 14 - import {Text} from 'view/com/util/text/Text' 15 - import {ViewHeader} from 'view/com/util/ViewHeader' 16 - import {RecommendedFeedsItem} from './RecommendedFeedsItem' 17 - 18 - type Props = { 19 - next: () => void 20 - } 21 - export function RecommendedFeeds({next}: Props) { 22 - const pal = usePalette('default') 23 - const {_} = useLingui() 24 - const {isTabletOrMobile} = useWebMediaQueries() 25 - const {isLoading, data} = useSuggestedFeedsQuery() 26 - 27 - const hasFeeds = data && data.pages[0].feeds.length 28 - 29 - const title = ( 30 - <> 31 - <Trans> 32 - <Text 33 - style={[ 34 - pal.textLight, 35 - tdStyles.title1, 36 - isTabletOrMobile && tdStyles.title1Small, 37 - ]}> 38 - Choose your 39 - </Text> 40 - <Text 41 - style={[ 42 - pal.link, 43 - tdStyles.title2, 44 - isTabletOrMobile && tdStyles.title2Small, 45 - ]}> 46 - Recommended 47 - </Text> 48 - <Text 49 - style={[ 50 - pal.link, 51 - tdStyles.title2, 52 - isTabletOrMobile && tdStyles.title2Small, 53 - ]}> 54 - Feeds 55 - </Text> 56 - </Trans> 57 - <Text type="2xl-medium" style={[pal.textLight, tdStyles.description]}> 58 - <Trans> 59 - Feeds are created by users to curate content. Choose some feeds that 60 - you find interesting. 61 - </Trans> 62 - </Text> 63 - <View 64 - style={{ 65 - flexDirection: 'row', 66 - justifyContent: 'flex-end', 67 - marginTop: 20, 68 - }}> 69 - <Button onPress={next} testID="continueBtn"> 70 - <View 71 - style={{ 72 - flexDirection: 'row', 73 - alignItems: 'center', 74 - paddingLeft: 2, 75 - gap: 6, 76 - }}> 77 - <Text 78 - type="2xl-medium" 79 - style={{color: '#fff', position: 'relative', top: -1}}> 80 - <Trans>Next</Trans> 81 - </Text> 82 - <FontAwesomeIcon icon="angle-right" color="#fff" size={14} /> 83 - </View> 84 - </Button> 85 - </View> 86 - </> 87 - ) 88 - 89 - return ( 90 - <> 91 - <TabletOrDesktop> 92 - <TitleColumnLayout 93 - testID="recommendedFeedsOnboarding" 94 - title={title} 95 - horizontal 96 - titleStyle={isTabletOrMobile ? undefined : {minWidth: 470}} 97 - contentStyle={{paddingHorizontal: 0}}> 98 - {hasFeeds ? ( 99 - <FlatList 100 - data={data.pages[0].feeds} 101 - renderItem={({item}) => <RecommendedFeedsItem item={item} />} 102 - keyExtractor={item => item.uri} 103 - style={{flex: 1}} 104 - /> 105 - ) : isLoading ? ( 106 - <View> 107 - <ActivityIndicator size="large" /> 108 - </View> 109 - ) : ( 110 - <ErrorMessage message={_(msg`Failed to load recommended feeds`)} /> 111 - )} 112 - </TitleColumnLayout> 113 - </TabletOrDesktop> 114 - <Mobile> 115 - <View style={[mStyles.container]} testID="recommendedFeedsOnboarding"> 116 - <ViewHeader 117 - title={_(msg`Recommended Feeds`)} 118 - showBackButton={false} 119 - showOnDesktop 120 - /> 121 - <Text type="lg-medium" style={[pal.text, mStyles.header]}> 122 - <Trans> 123 - Check out some recommended feeds. Tap + to add them to your list 124 - of pinned feeds. 125 - </Trans> 126 - </Text> 127 - 128 - {hasFeeds ? ( 129 - <FlatList 130 - data={data.pages[0].feeds} 131 - renderItem={({item}) => <RecommendedFeedsItem item={item} />} 132 - keyExtractor={item => item.uri} 133 - style={{flex: 1}} 134 - showsVerticalScrollIndicator={false} 135 - /> 136 - ) : isLoading ? ( 137 - <View style={{flex: 1}}> 138 - <ActivityIndicator size="large" /> 139 - </View> 140 - ) : ( 141 - <View style={{flex: 1}}> 142 - <ErrorMessage 143 - message={_(msg`Failed to load recommended feeds`)} 144 - /> 145 - </View> 146 - )} 147 - 148 - <Button 149 - onPress={next} 150 - label={_(msg`Continue`)} 151 - testID="continueBtn" 152 - style={mStyles.button} 153 - labelStyle={mStyles.buttonText} 154 - /> 155 - </View> 156 - </Mobile> 157 - </> 158 - ) 159 - } 160 - 161 - const tdStyles = StyleSheet.create({ 162 - container: { 163 - flex: 1, 164 - marginHorizontal: 16, 165 - justifyContent: 'space-between', 166 - }, 167 - title1: { 168 - fontSize: 36, 169 - fontWeight: '800', 170 - textAlign: 'right', 171 - }, 172 - title1Small: { 173 - fontSize: 24, 174 - }, 175 - title2: { 176 - fontSize: 58, 177 - fontWeight: '800', 178 - textAlign: 'right', 179 - }, 180 - title2Small: { 181 - fontSize: 36, 182 - }, 183 - description: { 184 - maxWidth: 400, 185 - marginTop: 10, 186 - marginLeft: 'auto', 187 - textAlign: 'right', 188 - }, 189 - }) 190 - 191 - const mStyles = StyleSheet.create({ 192 - container: { 193 - flex: 1, 194 - justifyContent: 'space-between', 195 - }, 196 - header: { 197 - marginBottom: 16, 198 - marginHorizontal: 16, 199 - }, 200 - button: { 201 - marginBottom: 16, 202 - marginHorizontal: 16, 203 - marginTop: 16, 204 - alignItems: 'center', 205 - }, 206 - buttonText: { 207 - textAlign: 'center', 208 - fontSize: 18, 209 - paddingVertical: 4, 210 - }, 211 - })
-172
src/view/com/auth/onboarding/RecommendedFeedsItem.tsx
··· 1 - import React from 'react' 2 - import {View} from 'react-native' 3 - import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 4 - import {AppBskyFeedDefs, RichText as BskRichText} from '@atproto/api' 5 - import {Text} from 'view/com/util/text/Text' 6 - import {RichText} from 'view/com/util/text/RichText' 7 - import {Button} from 'view/com/util/forms/Button' 8 - import {UserAvatar} from 'view/com/util/UserAvatar' 9 - import * as Toast from 'view/com/util/Toast' 10 - import {HeartIcon} from 'lib/icons' 11 - import {usePalette} from 'lib/hooks/usePalette' 12 - import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' 13 - import {sanitizeHandle} from 'lib/strings/handles' 14 - import { 15 - usePreferencesQuery, 16 - usePinFeedMutation, 17 - useRemoveFeedMutation, 18 - } from '#/state/queries/preferences' 19 - import {logger} from '#/logger' 20 - import {useAnalytics} from '#/lib/analytics/analytics' 21 - import {Trans, msg} from '@lingui/macro' 22 - import {useLingui} from '@lingui/react' 23 - 24 - export function RecommendedFeedsItem({ 25 - item, 26 - }: { 27 - item: AppBskyFeedDefs.GeneratorView 28 - }) { 29 - const {isMobile} = useWebMediaQueries() 30 - const pal = usePalette('default') 31 - const {_} = useLingui() 32 - const {data: preferences} = usePreferencesQuery() 33 - const { 34 - mutateAsync: pinFeed, 35 - variables: pinnedFeed, 36 - reset: resetPinFeed, 37 - } = usePinFeedMutation() 38 - const { 39 - mutateAsync: removeFeed, 40 - variables: removedFeed, 41 - reset: resetRemoveFeed, 42 - } = useRemoveFeedMutation() 43 - const {track} = useAnalytics() 44 - 45 - if (!item || !preferences) return null 46 - 47 - const isPinned = 48 - !removedFeed?.uri && 49 - (pinnedFeed?.uri || preferences.feeds.saved.includes(item.uri)) 50 - 51 - const onToggle = async () => { 52 - if (isPinned) { 53 - try { 54 - await removeFeed({uri: item.uri}) 55 - resetRemoveFeed() 56 - } catch (e) { 57 - Toast.show(_(msg`There was an issue contacting your server`)) 58 - logger.error('Failed to unsave feed', {message: e}) 59 - } 60 - } else { 61 - try { 62 - await pinFeed({uri: item.uri}) 63 - resetPinFeed() 64 - track('Onboarding:CustomFeedAdded') 65 - } catch (e) { 66 - Toast.show(_(msg`There was an issue contacting your server`)) 67 - logger.error('Failed to pin feed', {message: e}) 68 - } 69 - } 70 - } 71 - 72 - return ( 73 - <View testID={`feed-${item.displayName}`}> 74 - <View 75 - style={[ 76 - pal.border, 77 - { 78 - flex: isMobile ? 1 : undefined, 79 - flexDirection: 'row', 80 - gap: 18, 81 - maxWidth: isMobile ? undefined : 670, 82 - borderRightWidth: isMobile ? undefined : 1, 83 - paddingHorizontal: 24, 84 - paddingVertical: isMobile ? 12 : 24, 85 - borderTopWidth: 1, 86 - }, 87 - ]}> 88 - <View style={{marginTop: 2}}> 89 - <UserAvatar type="algo" size={42} avatar={item.avatar} /> 90 - </View> 91 - <View style={{flex: isMobile ? 1 : undefined}}> 92 - <Text 93 - type="2xl-bold" 94 - numberOfLines={1} 95 - style={[pal.text, {fontSize: 19}]}> 96 - {item.displayName} 97 - </Text> 98 - 99 - <Text style={[pal.textLight, {marginBottom: 8}]} numberOfLines={1}> 100 - <Trans>by {sanitizeHandle(item.creator.handle, '@')}</Trans> 101 - </Text> 102 - 103 - {item.description ? ( 104 - <RichText 105 - type="xl" 106 - style={[ 107 - pal.text, 108 - { 109 - flex: isMobile ? 1 : undefined, 110 - maxWidth: 550, 111 - marginBottom: 18, 112 - }, 113 - ]} 114 - richText={new BskRichText({text: item.description || ''})} 115 - numberOfLines={6} 116 - /> 117 - ) : null} 118 - 119 - <View style={{flexDirection: 'row', alignItems: 'center', gap: 12}}> 120 - <Button 121 - type="inverted" 122 - style={{paddingVertical: 6}} 123 - onPress={onToggle}> 124 - <View 125 - style={{ 126 - flexDirection: 'row', 127 - alignItems: 'center', 128 - paddingRight: 2, 129 - gap: 6, 130 - }}> 131 - {isPinned ? ( 132 - <> 133 - <FontAwesomeIcon 134 - icon="check" 135 - size={16} 136 - color={pal.colors.textInverted} 137 - /> 138 - <Text type="lg-medium" style={pal.textInverted}> 139 - <Trans>Added</Trans> 140 - </Text> 141 - </> 142 - ) : ( 143 - <> 144 - <FontAwesomeIcon 145 - icon="plus" 146 - size={16} 147 - color={pal.colors.textInverted} 148 - /> 149 - <Text type="lg-medium" style={pal.textInverted}> 150 - <Trans>Add</Trans> 151 - </Text> 152 - </> 153 - )} 154 - </View> 155 - </Button> 156 - 157 - <View style={{flexDirection: 'row', gap: 4}}> 158 - <HeartIcon 159 - size={16} 160 - strokeWidth={2.5} 161 - style={[pal.textLight, {position: 'relative', top: 2}]} 162 - /> 163 - <Text type="lg-medium" style={[pal.text, pal.textLight]}> 164 - {item.likeCount || 0} 165 - </Text> 166 - </View> 167 - </View> 168 - </View> 169 - </View> 170 - </View> 171 - ) 172 - }
-272
src/view/com/auth/onboarding/RecommendedFollows.tsx
··· 1 - import React from 'react' 2 - import {ActivityIndicator, FlatList, StyleSheet, View} from 'react-native' 3 - import {AppBskyActorDefs, moderateProfile} from '@atproto/api' 4 - import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 5 - import {msg, Trans} from '@lingui/macro' 6 - import {useLingui} from '@lingui/react' 7 - 8 - import {logger} from '#/logger' 9 - import {useModerationOpts} from '#/state/queries/preferences' 10 - import {useSuggestedFollowsQuery} from '#/state/queries/suggested-follows' 11 - import {useGetSuggestedFollowersByActor} from '#/state/queries/suggested-follows' 12 - import {usePalette} from 'lib/hooks/usePalette' 13 - import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' 14 - import {Button} from 'view/com/util/forms/Button' 15 - import {Mobile, TabletOrDesktop} from 'view/com/util/layouts/Breakpoints' 16 - import {TitleColumnLayout} from 'view/com/util/layouts/TitleColumnLayout' 17 - import {Text} from 'view/com/util/text/Text' 18 - import {ViewHeader} from 'view/com/util/ViewHeader' 19 - import {RecommendedFollowsItem} from './RecommendedFollowsItem' 20 - 21 - type Props = { 22 - next: () => void 23 - } 24 - export function RecommendedFollows({next}: Props) { 25 - const pal = usePalette('default') 26 - const {_} = useLingui() 27 - const {isTabletOrMobile} = useWebMediaQueries() 28 - const {data: suggestedFollows} = useSuggestedFollowsQuery() 29 - const getSuggestedFollowsByActor = useGetSuggestedFollowersByActor() 30 - const [additionalSuggestions, setAdditionalSuggestions] = React.useState<{ 31 - [did: string]: AppBskyActorDefs.ProfileView[] 32 - }>({}) 33 - const existingDids = React.useRef<string[]>([]) 34 - const moderationOpts = useModerationOpts() 35 - 36 - const title = ( 37 - <> 38 - <Trans> 39 - <Text 40 - style={[ 41 - pal.textLight, 42 - tdStyles.title1, 43 - isTabletOrMobile && tdStyles.title1Small, 44 - ]}> 45 - Follow some 46 - </Text> 47 - <Text 48 - style={[ 49 - pal.link, 50 - tdStyles.title2, 51 - isTabletOrMobile && tdStyles.title2Small, 52 - ]}> 53 - Recommended 54 - </Text> 55 - <Text 56 - style={[ 57 - pal.link, 58 - tdStyles.title2, 59 - isTabletOrMobile && tdStyles.title2Small, 60 - ]}> 61 - Users 62 - </Text> 63 - </Trans> 64 - <Text type="2xl-medium" style={[pal.textLight, tdStyles.description]}> 65 - <Trans> 66 - Follow some users to get started. We can recommend you more users 67 - based on who you find interesting. 68 - </Trans> 69 - </Text> 70 - <View 71 - style={{ 72 - flexDirection: 'row', 73 - justifyContent: 'flex-end', 74 - marginTop: 20, 75 - }}> 76 - <Button onPress={next} testID="continueBtn"> 77 - <View 78 - style={{ 79 - flexDirection: 'row', 80 - alignItems: 'center', 81 - paddingLeft: 2, 82 - gap: 6, 83 - }}> 84 - <Text 85 - type="2xl-medium" 86 - style={{color: '#fff', position: 'relative', top: -1}}> 87 - <Trans context="action">Done</Trans> 88 - </Text> 89 - <FontAwesomeIcon icon="angle-right" color="#fff" size={14} /> 90 - </View> 91 - </Button> 92 - </View> 93 - </> 94 - ) 95 - 96 - const suggestions = React.useMemo(() => { 97 - if (!suggestedFollows) return [] 98 - 99 - const additional = Object.entries(additionalSuggestions) 100 - const items = suggestedFollows.pages.flatMap(page => page.actors) 101 - 102 - outer: while (additional.length) { 103 - const additionalAccount = additional.shift() 104 - 105 - if (!additionalAccount) break 106 - 107 - const [followedUser, relatedAccounts] = additionalAccount 108 - 109 - for (let i = 0; i < items.length; i++) { 110 - if (items[i].did === followedUser) { 111 - items.splice(i + 1, 0, ...relatedAccounts) 112 - continue outer 113 - } 114 - } 115 - } 116 - 117 - existingDids.current = items.map(i => i.did) 118 - 119 - return items 120 - }, [suggestedFollows, additionalSuggestions]) 121 - 122 - const onFollowStateChange = React.useCallback( 123 - async ({following, did}: {following: boolean; did: string}) => { 124 - if (following) { 125 - try { 126 - const {suggestions: results} = await getSuggestedFollowsByActor(did) 127 - 128 - if (results.length) { 129 - const deduped = results.filter( 130 - r => !existingDids.current.find(did => did === r.did), 131 - ) 132 - setAdditionalSuggestions(s => ({ 133 - ...s, 134 - [did]: deduped.slice(0, 3), 135 - })) 136 - } 137 - } catch (e) { 138 - logger.error('RecommendedFollows: failed to get suggestions', { 139 - message: e, 140 - }) 141 - } 142 - } 143 - 144 - // not handling the unfollow case 145 - }, 146 - [existingDids, getSuggestedFollowsByActor, setAdditionalSuggestions], 147 - ) 148 - 149 - return ( 150 - <> 151 - <TabletOrDesktop> 152 - <TitleColumnLayout 153 - testID="recommendedFollowsOnboarding" 154 - title={title} 155 - horizontal 156 - titleStyle={isTabletOrMobile ? undefined : {minWidth: 470}} 157 - contentStyle={{paddingHorizontal: 0}}> 158 - {!suggestedFollows || !moderationOpts ? ( 159 - <ActivityIndicator size="large" /> 160 - ) : ( 161 - <FlatList 162 - data={suggestions} 163 - renderItem={({item}) => ( 164 - <RecommendedFollowsItem 165 - profile={item} 166 - onFollowStateChange={onFollowStateChange} 167 - moderation={moderateProfile(item, moderationOpts)} 168 - /> 169 - )} 170 - keyExtractor={item => item.did} 171 - style={{flex: 1}} 172 - /> 173 - )} 174 - </TitleColumnLayout> 175 - </TabletOrDesktop> 176 - 177 - <Mobile> 178 - <View style={[mStyles.container]} testID="recommendedFollowsOnboarding"> 179 - <View> 180 - <ViewHeader 181 - title={_(msg`Recommended Users`)} 182 - showBackButton={false} 183 - showOnDesktop 184 - /> 185 - <Text type="lg-medium" style={[pal.text, mStyles.header]}> 186 - <Trans> 187 - Check out some recommended users. Follow them to see similar 188 - users. 189 - </Trans> 190 - </Text> 191 - </View> 192 - {!suggestedFollows || !moderationOpts ? ( 193 - <ActivityIndicator size="large" /> 194 - ) : ( 195 - <FlatList 196 - data={suggestions} 197 - renderItem={({item}) => ( 198 - <RecommendedFollowsItem 199 - profile={item} 200 - onFollowStateChange={onFollowStateChange} 201 - moderation={moderateProfile(item, moderationOpts)} 202 - /> 203 - )} 204 - keyExtractor={item => item.did} 205 - style={{flex: 1}} 206 - showsVerticalScrollIndicator={false} 207 - /> 208 - )} 209 - <Button 210 - onPress={next} 211 - label={_(msg`Continue`)} 212 - testID="continueBtn" 213 - style={mStyles.button} 214 - labelStyle={mStyles.buttonText} 215 - /> 216 - </View> 217 - </Mobile> 218 - </> 219 - ) 220 - } 221 - 222 - const tdStyles = StyleSheet.create({ 223 - container: { 224 - flex: 1, 225 - marginHorizontal: 16, 226 - justifyContent: 'space-between', 227 - }, 228 - title1: { 229 - fontSize: 36, 230 - fontWeight: '800', 231 - textAlign: 'right', 232 - }, 233 - title1Small: { 234 - fontSize: 24, 235 - }, 236 - title2: { 237 - fontSize: 58, 238 - fontWeight: '800', 239 - textAlign: 'right', 240 - }, 241 - title2Small: { 242 - fontSize: 36, 243 - }, 244 - description: { 245 - maxWidth: 400, 246 - marginTop: 10, 247 - marginLeft: 'auto', 248 - textAlign: 'right', 249 - }, 250 - }) 251 - 252 - const mStyles = StyleSheet.create({ 253 - container: { 254 - flex: 1, 255 - justifyContent: 'space-between', 256 - }, 257 - header: { 258 - marginBottom: 16, 259 - marginHorizontal: 16, 260 - }, 261 - button: { 262 - marginBottom: 16, 263 - marginHorizontal: 16, 264 - marginTop: 16, 265 - alignItems: 'center', 266 - }, 267 - buttonText: { 268 - textAlign: 'center', 269 - fontSize: 18, 270 - paddingVertical: 4, 271 - }, 272 - })
-202
src/view/com/auth/onboarding/RecommendedFollowsItem.tsx
··· 1 - import React from 'react' 2 - import {View, StyleSheet, ActivityIndicator} from 'react-native' 3 - import {ModerationDecision, AppBskyActorDefs} from '@atproto/api' 4 - import {Button} from '#/view/com/util/forms/Button' 5 - import {usePalette} from 'lib/hooks/usePalette' 6 - import {sanitizeDisplayName} from 'lib/strings/display-names' 7 - import {sanitizeHandle} from 'lib/strings/handles' 8 - import {s} from 'lib/styles' 9 - import {UserAvatar} from 'view/com/util/UserAvatar' 10 - import {Text} from 'view/com/util/text/Text' 11 - import Animated, {FadeInRight} from 'react-native-reanimated' 12 - import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' 13 - import {useAnalytics} from 'lib/analytics/analytics' 14 - import {useLingui} from '@lingui/react' 15 - import {Trans, msg} from '@lingui/macro' 16 - import {Shadow, useProfileShadow} from '#/state/cache/profile-shadow' 17 - import {useProfileFollowMutationQueue} from '#/state/queries/profile' 18 - import {logger} from '#/logger' 19 - 20 - type Props = { 21 - profile: AppBskyActorDefs.ProfileViewBasic 22 - moderation: ModerationDecision 23 - onFollowStateChange: (props: { 24 - did: string 25 - following: boolean 26 - }) => Promise<void> 27 - } 28 - 29 - export function RecommendedFollowsItem({ 30 - profile, 31 - moderation, 32 - onFollowStateChange, 33 - }: React.PropsWithChildren<Props>) { 34 - const pal = usePalette('default') 35 - const {isMobile} = useWebMediaQueries() 36 - const shadowedProfile = useProfileShadow(profile) 37 - 38 - return ( 39 - <Animated.View 40 - entering={FadeInRight} 41 - style={[ 42 - styles.cardContainer, 43 - pal.view, 44 - pal.border, 45 - { 46 - maxWidth: isMobile ? undefined : 670, 47 - borderRightWidth: isMobile ? undefined : 1, 48 - }, 49 - ]}> 50 - <ProfileCard 51 - key={profile.did} 52 - profile={shadowedProfile} 53 - onFollowStateChange={onFollowStateChange} 54 - moderation={moderation} 55 - /> 56 - </Animated.View> 57 - ) 58 - } 59 - 60 - function ProfileCard({ 61 - profile, 62 - onFollowStateChange, 63 - moderation, 64 - }: { 65 - profile: Shadow<AppBskyActorDefs.ProfileViewBasic> 66 - moderation: ModerationDecision 67 - onFollowStateChange: (props: { 68 - did: string 69 - following: boolean 70 - }) => Promise<void> 71 - }) { 72 - const {track} = useAnalytics() 73 - const pal = usePalette('default') 74 - const {_} = useLingui() 75 - const [addingMoreSuggestions, setAddingMoreSuggestions] = 76 - React.useState(false) 77 - const [queueFollow, queueUnfollow] = useProfileFollowMutationQueue( 78 - profile, 79 - 'RecommendedFollowsItem', 80 - ) 81 - 82 - const onToggleFollow = React.useCallback(async () => { 83 - try { 84 - if (profile.viewer?.following) { 85 - await queueUnfollow() 86 - } else { 87 - setAddingMoreSuggestions(true) 88 - await queueFollow() 89 - await onFollowStateChange({did: profile.did, following: true}) 90 - setAddingMoreSuggestions(false) 91 - track('Onboarding:SuggestedFollowFollowed') 92 - } 93 - } catch (e: any) { 94 - if (e?.name !== 'AbortError') { 95 - logger.error('RecommendedFollows: failed to toggle following', { 96 - message: e, 97 - }) 98 - } 99 - } finally { 100 - setAddingMoreSuggestions(false) 101 - } 102 - }, [ 103 - profile, 104 - queueFollow, 105 - queueUnfollow, 106 - setAddingMoreSuggestions, 107 - track, 108 - onFollowStateChange, 109 - ]) 110 - 111 - return ( 112 - <View style={styles.card}> 113 - <View style={styles.layout}> 114 - <View style={styles.layoutAvi}> 115 - <UserAvatar 116 - size={40} 117 - avatar={profile.avatar} 118 - moderation={moderation.ui('avatar')} 119 - /> 120 - </View> 121 - <View style={styles.layoutContent}> 122 - <Text 123 - type="2xl-bold" 124 - style={[s.bold, pal.text]} 125 - numberOfLines={1} 126 - lineHeight={1.2}> 127 - {sanitizeDisplayName( 128 - profile.displayName || sanitizeHandle(profile.handle), 129 - moderation.ui('displayName'), 130 - )} 131 - </Text> 132 - <Text type="xl" style={[pal.textLight]} numberOfLines={1}> 133 - {sanitizeHandle(profile.handle, '@')} 134 - </Text> 135 - </View> 136 - 137 - <Button 138 - type={profile.viewer?.following ? 'default' : 'inverted'} 139 - labelStyle={styles.followButton} 140 - onPress={onToggleFollow} 141 - label={profile.viewer?.following ? _(msg`Unfollow`) : _(msg`Follow`)} 142 - /> 143 - </View> 144 - {profile.description ? ( 145 - <View style={styles.details}> 146 - <Text type="lg" style={pal.text} numberOfLines={4}> 147 - {profile.description as string} 148 - </Text> 149 - </View> 150 - ) : undefined} 151 - {addingMoreSuggestions ? ( 152 - <View style={styles.addingMoreContainer}> 153 - <ActivityIndicator size="small" color={pal.colors.text} /> 154 - <Text style={[pal.text]}> 155 - <Trans>Finding similar accounts...</Trans> 156 - </Text> 157 - </View> 158 - ) : null} 159 - </View> 160 - ) 161 - } 162 - 163 - const styles = StyleSheet.create({ 164 - cardContainer: { 165 - borderTopWidth: 1, 166 - }, 167 - card: { 168 - paddingHorizontal: 10, 169 - }, 170 - layout: { 171 - flexDirection: 'row', 172 - alignItems: 'center', 173 - }, 174 - layoutAvi: { 175 - width: 54, 176 - paddingLeft: 4, 177 - paddingTop: 8, 178 - paddingBottom: 10, 179 - }, 180 - layoutContent: { 181 - flex: 1, 182 - paddingRight: 10, 183 - paddingTop: 10, 184 - paddingBottom: 10, 185 - }, 186 - details: { 187 - paddingLeft: 54, 188 - paddingRight: 10, 189 - paddingBottom: 10, 190 - }, 191 - addingMoreContainer: { 192 - flexDirection: 'row', 193 - alignItems: 'center', 194 - paddingLeft: 54, 195 - paddingTop: 4, 196 - paddingBottom: 12, 197 - gap: 4, 198 - }, 199 - followButton: { 200 - fontSize: 16, 201 - }, 202 - })
-10
src/view/com/auth/onboarding/Welcome.tsx
··· 1 - import 'react' 2 - import {withBreakpoints} from 'view/com/util/layouts/withBreakpoints' 3 - import {WelcomeDesktop} from './WelcomeDesktop' 4 - import {WelcomeMobile} from './WelcomeMobile' 5 - 6 - export const Welcome = withBreakpoints( 7 - WelcomeMobile, 8 - WelcomeDesktop, 9 - WelcomeDesktop, 10 - )
-126
src/view/com/auth/onboarding/WelcomeDesktop.tsx
··· 1 - import React from 'react' 2 - import {StyleSheet, View} from 'react-native' 3 - import {useMediaQuery} from 'react-responsive' 4 - import {Text} from 'view/com/util/text/Text' 5 - import {s} from 'lib/styles' 6 - import {usePalette} from 'lib/hooks/usePalette' 7 - import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 8 - import {TitleColumnLayout} from 'view/com/util/layouts/TitleColumnLayout' 9 - import {Button} from 'view/com/util/forms/Button' 10 - import {Trans} from '@lingui/macro' 11 - 12 - type Props = { 13 - next: () => void 14 - skip: () => void 15 - } 16 - 17 - export function WelcomeDesktop({next}: Props) { 18 - const pal = usePalette('default') 19 - const horizontal = useMediaQuery({minWidth: 1300}) 20 - const title = ( 21 - <Trans> 22 - <Text 23 - style={[ 24 - pal.textLight, 25 - { 26 - fontSize: 36, 27 - fontWeight: '800', 28 - textAlign: horizontal ? 'right' : 'left', 29 - }, 30 - ]}> 31 - Welcome to 32 - </Text> 33 - <Text 34 - style={[ 35 - pal.link, 36 - { 37 - fontSize: 72, 38 - fontWeight: '800', 39 - textAlign: horizontal ? 'right' : 'left', 40 - }, 41 - ]}> 42 - Bluesky 43 - </Text> 44 - </Trans> 45 - ) 46 - return ( 47 - <TitleColumnLayout 48 - testID="welcomeOnboarding" 49 - title={title} 50 - horizontal={horizontal} 51 - titleStyle={horizontal ? {paddingBottom: 160} : undefined}> 52 - <View style={[styles.row]}> 53 - <FontAwesomeIcon icon={'globe'} size={36} color={pal.colors.link} /> 54 - <View style={[styles.rowText]}> 55 - <Text type="xl-bold" style={[pal.text]}> 56 - <Trans>Bluesky is public.</Trans> 57 - </Text> 58 - <Text type="xl" style={[pal.text, s.pt2]}> 59 - <Trans> 60 - Your posts, likes, and blocks are public. Mutes are private. 61 - </Trans> 62 - </Text> 63 - </View> 64 - </View> 65 - <View style={[styles.row]}> 66 - <FontAwesomeIcon icon={'at'} size={36} color={pal.colors.link} /> 67 - <View style={[styles.rowText]}> 68 - <Text type="xl-bold" style={[pal.text]}> 69 - <Trans>Bluesky is open.</Trans> 70 - </Text> 71 - <Text type="xl" style={[pal.text, s.pt2]}> 72 - <Trans>Never lose access to your followers and data.</Trans> 73 - </Text> 74 - </View> 75 - </View> 76 - <View style={[styles.row]}> 77 - <FontAwesomeIcon icon={'gear'} size={36} color={pal.colors.link} /> 78 - <View style={[styles.rowText]}> 79 - <Text type="xl-bold" style={[pal.text]}> 80 - <Trans>Bluesky is flexible.</Trans> 81 - </Text> 82 - <Text type="xl" style={[pal.text, s.pt2]}> 83 - <Trans> 84 - Choose the algorithms that power your experience with custom 85 - feeds. 86 - </Trans> 87 - </Text> 88 - </View> 89 - </View> 90 - <View style={styles.spacer} /> 91 - <View style={{flexDirection: 'row'}}> 92 - <Button onPress={next} testID="continueBtn"> 93 - <View 94 - style={{ 95 - flexDirection: 'row', 96 - alignItems: 'center', 97 - paddingLeft: 2, 98 - gap: 6, 99 - }}> 100 - <Text 101 - type="2xl-medium" 102 - style={{color: '#fff', position: 'relative', top: -1}}> 103 - <Trans context="action">Next</Trans> 104 - </Text> 105 - <FontAwesomeIcon icon="angle-right" color="#fff" size={14} /> 106 - </View> 107 - </Button> 108 - </View> 109 - </TitleColumnLayout> 110 - ) 111 - } 112 - 113 - const styles = StyleSheet.create({ 114 - row: { 115 - flexDirection: 'row', 116 - columnGap: 20, 117 - alignItems: 'center', 118 - marginVertical: 20, 119 - }, 120 - rowText: { 121 - flex: 1, 122 - }, 123 - spacer: { 124 - height: 20, 125 - }, 126 - })
-136
src/view/com/auth/onboarding/WelcomeMobile.tsx
··· 1 - import React from 'react' 2 - import {Pressable, StyleSheet, View} from 'react-native' 3 - import {Text} from 'view/com/util/text/Text' 4 - import {s} from 'lib/styles' 5 - import {usePalette} from 'lib/hooks/usePalette' 6 - import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 7 - import {Button} from 'view/com/util/forms/Button' 8 - import {ViewHeader} from 'view/com/util/ViewHeader' 9 - import {useLingui} from '@lingui/react' 10 - import {Trans, msg} from '@lingui/macro' 11 - 12 - type Props = { 13 - next: () => void 14 - skip: () => void 15 - } 16 - 17 - export function WelcomeMobile({next, skip}: Props) { 18 - const pal = usePalette('default') 19 - const {_} = useLingui() 20 - 21 - return ( 22 - <View style={[styles.container]} testID="welcomeOnboarding"> 23 - <ViewHeader 24 - showOnDesktop 25 - showBorder={false} 26 - showBackButton={false} 27 - title="" 28 - renderButton={() => { 29 - return ( 30 - <Pressable 31 - accessibilityRole="button" 32 - style={[s.flexRow, s.alignCenter]} 33 - onPress={skip}> 34 - <Text style={[pal.link]}> 35 - <Trans>Skip</Trans> 36 - </Text> 37 - <FontAwesomeIcon 38 - icon={'chevron-right'} 39 - size={14} 40 - color={pal.colors.link} 41 - /> 42 - </Pressable> 43 - ) 44 - }} 45 - /> 46 - <View> 47 - <Text style={[pal.text, styles.title]}> 48 - <Trans> 49 - Welcome to{' '} 50 - <Text style={[pal.text, pal.link, styles.title]}>Bluesky</Text> 51 - </Trans> 52 - </Text> 53 - <View style={styles.spacer} /> 54 - <View style={[styles.row]}> 55 - <FontAwesomeIcon icon={'globe'} size={36} color={pal.colors.link} /> 56 - <View style={[styles.rowText]}> 57 - <Text type="lg-bold" style={[pal.text]}> 58 - <Trans>Bluesky is public.</Trans> 59 - </Text> 60 - <Text type="lg-thin" style={[pal.text, s.pt2]}> 61 - <Trans> 62 - Your posts, likes, and blocks are public. Mutes are private. 63 - </Trans> 64 - </Text> 65 - </View> 66 - </View> 67 - <View style={[styles.row]}> 68 - <FontAwesomeIcon icon={'at'} size={36} color={pal.colors.link} /> 69 - <View style={[styles.rowText]}> 70 - <Text type="lg-bold" style={[pal.text]}> 71 - <Trans>Bluesky is open.</Trans> 72 - </Text> 73 - <Text type="lg-thin" style={[pal.text, s.pt2]}> 74 - <Trans>Never lose access to your followers and data.</Trans> 75 - </Text> 76 - </View> 77 - </View> 78 - <View style={[styles.row]}> 79 - <FontAwesomeIcon icon={'gear'} size={36} color={pal.colors.link} /> 80 - <View style={[styles.rowText]}> 81 - <Text type="lg-bold" style={[pal.text]}> 82 - <Trans>Bluesky is flexible.</Trans> 83 - </Text> 84 - <Text type="lg-thin" style={[pal.text, s.pt2]}> 85 - <Trans> 86 - Choose the algorithms that power your experience with custom 87 - feeds. 88 - </Trans> 89 - </Text> 90 - </View> 91 - </View> 92 - </View> 93 - 94 - <Button 95 - onPress={next} 96 - label={_(msg`Continue`)} 97 - testID="continueBtn" 98 - style={[styles.buttonContainer]} 99 - labelStyle={styles.buttonText} 100 - /> 101 - </View> 102 - ) 103 - } 104 - 105 - const styles = StyleSheet.create({ 106 - container: { 107 - flex: 1, 108 - marginBottom: 60, 109 - marginHorizontal: 16, 110 - justifyContent: 'space-between', 111 - }, 112 - title: { 113 - fontSize: 42, 114 - fontWeight: '800', 115 - }, 116 - row: { 117 - flexDirection: 'row', 118 - columnGap: 20, 119 - alignItems: 'center', 120 - marginVertical: 20, 121 - }, 122 - rowText: { 123 - flex: 1, 124 - }, 125 - spacer: { 126 - height: 20, 127 - }, 128 - buttonContainer: { 129 - alignItems: 'center', 130 - }, 131 - buttonText: { 132 - textAlign: 'center', 133 - fontSize: 18, 134 - marginVertical: 4, 135 - }, 136 - })
+8 -15
src/view/shell/createNativeStackNavigatorWithAuth.tsx
··· 1 1 import * as React from 'react' 2 2 import {View} from 'react-native' 3 - import {PWI_ENABLED, NEW_ONBOARDING_ENABLED} from '#/lib/build-flags' 4 - 5 3 // Based on @react-navigation/native-stack/src/createNativeStackNavigator.ts 6 4 // MIT License 7 5 // Copyright (c) 2017 React Navigation Contributors 8 - 9 6 import { 10 7 createNavigatorFactory, 11 8 EventArg, ··· 21 18 NativeStackNavigationEventMap, 22 19 NativeStackNavigationOptions, 23 20 } from '@react-navigation/native-stack' 24 - import type {NativeStackNavigatorProps} from '@react-navigation/native-stack/src/types' 25 21 import {NativeStackView} from '@react-navigation/native-stack' 22 + import type {NativeStackNavigatorProps} from '@react-navigation/native-stack/src/types' 26 23 27 - import {BottomBarWeb} from './bottom-bar/BottomBarWeb' 28 - import {DesktopLeftNav} from './desktop/LeftNav' 29 - import {DesktopRightNav} from './desktop/RightNav' 24 + import {PWI_ENABLED} from '#/lib/build-flags' 30 25 import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' 26 + import {useSession} from '#/state/session' 31 27 import {useOnboardingState} from '#/state/shell' 32 28 import { 33 29 useLoggedOutView, 34 30 useLoggedOutViewControls, 35 31 } from '#/state/shell/logged-out' 36 - import {useSession} from '#/state/session' 37 32 import {isWeb} from 'platform/detection' 38 33 import {Deactivated} from '#/screens/Deactivated' 34 + import {Onboarding} from '#/screens/Onboarding' 39 35 import {LoggedOut} from '../com/auth/LoggedOut' 40 - import {Onboarding} from '../com/auth/Onboarding' 41 - import {Onboarding as NewOnboarding} from '#/screens/Onboarding' 36 + import {BottomBarWeb} from './bottom-bar/BottomBarWeb' 37 + import {DesktopLeftNav} from './desktop/LeftNav' 38 + import {DesktopRightNav} from './desktop/RightNav' 42 39 43 40 type NativeStackNavigationOptionsWithAuth = NativeStackNavigationOptions & { 44 41 requireAuth?: boolean ··· 112 109 return <LoggedOut onDismiss={() => setShowLoggedOut(false)} /> 113 110 } 114 111 if (onboardingState.isActive) { 115 - if (NEW_ONBOARDING_ENABLED) { 116 - return <NewOnboarding /> 117 - } else { 118 - return <Onboarding /> 119 - } 112 + return <Onboarding /> 120 113 } 121 114 const newDescriptors: typeof descriptors = {} 122 115 for (let key in descriptors) {