diff options
29 files changed, 1034 insertions, 218 deletions
diff --git a/src/Navigation.tsx b/src/Navigation.tsx index df601d0cd..2422491e2 100644 --- a/src/Navigation.tsx +++ b/src/Navigation.tsx @@ -260,6 +260,7 @@ function TabsNavigator() { function HomeTabNavigator() { const contentStyle = useColorSchemeStyle(styles.bgLight, styles.bgDark) + return ( <HomeTab.Navigator screenOptions={{ diff --git a/src/lib/analytics/types.ts b/src/lib/analytics/types.ts index f876c6d53..5f9437319 100644 --- a/src/lib/analytics/types.ts +++ b/src/lib/analytics/types.ts @@ -122,6 +122,8 @@ interface TrackPropertiesMap { // ONBOARDING events 'Onboarding:Begin': {} 'Onboarding:Complete': {} + 'Onboarding:Skipped': {} + 'Onboarding:Reset': {} } interface ScreenPropertiesMap { diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 001cdf8c3..94551e6ef 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -148,3 +148,110 @@ export const HITSLOP_10 = createHitslop(10) export const HITSLOP_20 = createHitslop(20) export const HITSLOP_30 = createHitslop(30) export const BACK_HITSLOP = HITSLOP_30 + +export const RECOMMENDED_FEEDS = [ + { + did: 'did:plc:hsqwcidfez66lwm3gxhfv5in', + rkey: 'aaaf2pqeodmpy', + }, + { + did: 'did:plc:gekdk2nd47gkk3utfz2xf7cn', + rkey: 'aaap4tbjcfe5y', + }, + { + did: 'did:plc:5rw2on4i56btlcajojaxwcat', + rkey: 'aaao6g552b33o', + }, + { + did: 'did:plc:jfhpnnst6flqway4eaeqzj2a', + rkey: 'for-science', + }, + { + did: 'did:plc:7q4nnnxawajbfaq7to5dpbsy', + rkey: 'bsky-news', + }, + { + did: 'did:plc:jcoy7v3a2t4rcfdh6i4kza25', + rkey: 'astro', + }, + { + did: 'did:plc:tenurhgjptubkk5zf5qhi3og', + rkey: 'h-nba', + }, + { + did: 'did:plc:vpkhqolt662uhesyj6nxm7ys', + rkey: 'devfeed', + }, + { + did: 'did:plc:cndfx4udwgvpjaakvxvh7wm5', + rkey: 'flipboard-tech', + }, + { + did: 'did:plc:w4xbfzo7kqfes5zb7r6qv3rw', + rkey: 'blacksky', + }, + { + did: 'did:plc:lptjvw6ut224kwrj7ub3sqbe', + rkey: 'aaaotfjzjplna', + }, + { + did: 'did:plc:gkvpokm7ec5j5yxls6xk4e3z', + rkey: 'formula-one', + }, + { + did: 'did:plc:q6gjnaw2blty4crticxkmujt', + rkey: 'positivifeed', + }, + { + did: 'did:plc:l72uci4styb4jucsgcrrj5ap', + rkey: 'aaao5dzfm36u4', + }, + { + did: 'did:plc:k3jkadxv5kkjgs6boyon7m6n', + rkey: 'aaaavlyvqzst2', + }, + { + did: 'did:plc:nkahctfdi6bxk72umytfwghw', + rkey: 'aaado2uvfsc6w', + }, + { + did: 'did:plc:epihigio3d7un7u3gpqiy5gv', + rkey: 'aaaekwsc7zsvs', + }, + { + did: 'did:plc:qiknc4t5rq7yngvz7g4aezq7', + rkey: 'aaaejxlobe474', + }, + { + did: 'did:plc:mlq4aycufcuolr7ax6sezpc4', + rkey: 'aaaoudweck6uy', + }, + { + did: 'did:plc:rcez5hcvq3vzlu5x7xrjyccg', + rkey: 'aaadzjxbcddzi', + }, + { + did: 'did:plc:lnxbuzaenlwjrncx6sc4cfdr', + rkey: 'aaab2vesjtszc', + }, + { + did: 'did:plc:x3cya3wkt4n6u4ihmvpsc5if', + rkey: 'aaacynbxwimok', + }, + { + did: 'did:plc:abv47bjgzjgoh3yrygwoi36x', + rkey: 'aaagt6amuur5e', + }, + { + did: 'did:plc:ffkgesg3jsv2j7aagkzrtcvt', + rkey: 'aaacjerk7gwek', + }, + { + did: 'did:plc:geoqe3qls5mwezckxxsewys2', + rkey: 'aaai43yetqshu', + }, + { + did: 'did:plc:2wqomm3tjqbgktbrfwgvrw34', + rkey: 'authors', + }, +] diff --git a/src/lib/hooks/useWebMediaQueries.tsx b/src/lib/hooks/useWebMediaQueries.tsx index 441585442..fd7e383f0 100644 --- a/src/lib/hooks/useWebMediaQueries.tsx +++ b/src/lib/hooks/useWebMediaQueries.tsx @@ -1,8 +1,14 @@ import {useMediaQuery} from 'react-responsive' +import {isNative} from 'platform/detection' export function useWebMediaQueries() { const isDesktop = useMediaQuery({ - query: '(min-width: 1230px)', + query: '(min-width: 1224px)', }) - return {isDesktop} + const isTabletOrMobile = useMediaQuery({query: '(max-width: 1224px)'}) + const isMobile = useMediaQuery({query: '(max-width: 800px)'}) + if (isNative) { + return {isMobile: true, isTabletOrMobile: true, isDesktop: false} + } + return {isMobile, isTabletOrMobile, isDesktop} } diff --git a/src/state/models/discovery/onboarding.ts b/src/state/models/discovery/onboarding.ts new file mode 100644 index 000000000..09c9eac04 --- /dev/null +++ b/src/state/models/discovery/onboarding.ts @@ -0,0 +1,94 @@ +import {makeAutoObservable} from 'mobx' +import {RootStoreModel} from '../root-store' +import {hasProp} from 'lib/type-guards' +import {track} from 'lib/analytics/analytics' + +export const OnboardingScreenSteps = { + Welcome: 'Welcome', + RecommendedFeeds: 'RecommendedFeeds', + Home: 'Home', +} as const + +type OnboardingStep = + (typeof OnboardingScreenSteps)[keyof typeof OnboardingScreenSteps] +const OnboardingStepsArray = Object.values(OnboardingScreenSteps) +export class OnboardingModel { + // state + step: OnboardingStep = 'Home' // default state to skip onboarding, only enabled for new users by calling start() + + constructor(public rootStore: RootStoreModel) { + makeAutoObservable(this, { + rootStore: false, + hydrate: false, + serialize: false, + }) + } + + serialize(): unknown { + return { + step: this.step, + } + } + + hydrate(v: unknown) { + if (typeof v === 'object' && v !== null) { + if ( + hasProp(v, 'step') && + typeof v.step === 'string' && + OnboardingStepsArray.includes(v.step as OnboardingStep) + ) { + this.step = v.step as OnboardingStep + } + } else { + // if there is no valid state, we'll just reset + this.reset() + } + } + + /** + * Returns the name of the next screen in the onboarding process based on the current step or screen name provided. + * @param {OnboardingStep} [currentScreenName] + * @returns name of next screen in the onboarding process + */ + next(currentScreenName?: OnboardingStep) { + currentScreenName = currentScreenName || this.step + if (currentScreenName === 'Welcome') { + this.step = 'RecommendedFeeds' + return this.step + } else if (this.step === 'RecommendedFeeds') { + this.finish() + return this.step + } else { + // if we get here, we're in an invalid state, let's just go Home + return 'Home' + } + } + + start() { + this.step = 'Welcome' + track('Onboarding:Begin') + } + + finish() { + this.step = 'Home' + track('Onboarding:Complete') + } + + reset() { + this.step = 'Welcome' + track('Onboarding:Reset') + } + + skip() { + this.step = 'Home' + track('Onboarding:Skipped') + } + + get isComplete() { + return this.step === 'Home' + } + + get isActive() { + return !this.isComplete + } +} diff --git a/src/state/models/feeds/custom-feed.ts b/src/state/models/feeds/custom-feed.ts index 3c6d52755..2de4534e7 100644 --- a/src/state/models/feeds/custom-feed.ts +++ b/src/state/models/feeds/custom-feed.ts @@ -67,6 +67,19 @@ export class CustomFeedModel { } } + async pin() { + try { + await this.rootStore.preferences.addPinnedFeed(this.uri) + } catch (error) { + this.rootStore.log.error('Failed to pin feed', error) + } finally { + track('CustomFeed:Pin', { + name: this.data.displayName, + uri: this.uri, + }) + } + } + async unsave() { try { await this.rootStore.preferences.removeSavedFeed(this.uri) diff --git a/src/state/models/root-store.ts b/src/state/models/root-store.ts index 1d6d3a0d0..6204e0d10 100644 --- a/src/state/models/root-store.ts +++ b/src/state/models/root-store.ts @@ -27,6 +27,7 @@ import {reset as resetNavigation} from '../../Navigation' // remove after backend testing finishes // -prf import {applyDebugHeader} from 'lib/api/debug-appview-proxy-header' +import {OnboardingModel} from './discovery/onboarding' export const appInfo = z.object({ build: z.string(), @@ -44,6 +45,7 @@ export class RootStoreModel { shell = new ShellUiModel(this) preferences = new PreferencesModel(this) me = new MeModel(this) + onboarding = new OnboardingModel(this) invitedUsers = new InvitedUsers(this) handleResolutions = new HandleResolutionsCache() profiles = new ProfilesCache(this) @@ -70,6 +72,7 @@ export class RootStoreModel { appInfo: this.appInfo, session: this.session.serialize(), me: this.me.serialize(), + onboarding: this.onboarding.serialize(), shell: this.shell.serialize(), preferences: this.preferences.serialize(), invitedUsers: this.invitedUsers.serialize(), @@ -88,6 +91,9 @@ export class RootStoreModel { if (hasProp(v, 'me')) { this.me.hydrate(v.me) } + if (hasProp(v, 'onboarding')) { + this.onboarding.hydrate(v.onboarding) + } if (hasProp(v, 'session')) { this.session.hydrate(v.session) } diff --git a/src/state/models/ui/create-account.ts b/src/state/models/ui/create-account.ts index 04e1554c6..d9d4f51b9 100644 --- a/src/state/models/ui/create-account.ts +++ b/src/state/models/ui/create-account.ts @@ -109,10 +109,10 @@ export class CreateAccountModel { this.setError('') this.setIsProcessing(true) - // open the onboarding modal after the session is created + // open the onboarding screens after the session is created const sessionReadySub = this.rootStore.onSessionReady(() => { sessionReadySub.remove() - this.rootStore.shell.openModal({name: 'onboarding'}) + this.rootStore.onboarding.start() }) try { diff --git a/src/state/models/ui/shell.ts b/src/state/models/ui/shell.ts index d19de4b96..33fdd5710 100644 --- a/src/state/models/ui/shell.ts +++ b/src/state/models/ui/shell.ts @@ -136,10 +136,6 @@ export interface PostLanguagesSettingsModal { name: 'post-languages-settings' } -export interface OnboardingModal { - name: 'onboarding' -} - export type Modal = // Account | AddAppPasswordModal @@ -171,9 +167,6 @@ export type Modal = | WaitlistModal | InviteCodesModal - // Onboarding - | OnboardingModal - // Generic | ConfirmModal diff --git a/src/view/com/auth/Onboarding.tsx b/src/view/com/auth/Onboarding.tsx new file mode 100644 index 000000000..065d4d244 --- /dev/null +++ b/src/view/com/auth/Onboarding.tsx @@ -0,0 +1,34 @@ +import React from 'react' +import {SafeAreaView} from 'react-native' +import {observer} from 'mobx-react-lite' +import {ErrorBoundary} from 'view/com/util/ErrorBoundary' +import {s} from 'lib/styles' +import {usePalette} from 'lib/hooks/usePalette' +import {useStores} from 'state/index' +import {Welcome} from './onboarding/Welcome' +import {RecommendedFeeds} from './onboarding/RecommendedFeeds' + +export const Onboarding = observer(() => { + const pal = usePalette('default') + const store = useStores() + + React.useEffect(() => { + store.shell.setMinimalShellMode(true) + }, [store]) + + const next = () => store.onboarding.next() + const skip = () => store.onboarding.skip() + + return ( + <SafeAreaView testID="onboardingView" style={[s.hContentRegion, pal.view]}> + <ErrorBoundary> + {store.onboarding.step === 'Welcome' && ( + <Welcome skip={skip} next={next} /> + )} + {store.onboarding.step === 'RecommendedFeeds' && ( + <RecommendedFeeds next={next} /> + )} + </ErrorBoundary> + </SafeAreaView> + ) +}) diff --git a/src/view/com/auth/onboarding/Onboarding.tsx b/src/view/com/auth/onboarding/Onboarding.tsx deleted file mode 100644 index 28e4419d7..000000000 --- a/src/view/com/auth/onboarding/Onboarding.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import React from 'react' -import {StyleSheet, View} from 'react-native' -import {usePalette} from 'lib/hooks/usePalette' -import {Welcome} from './Welcome' -import {useStores} from 'state/index' -import {track} from 'lib/analytics/analytics' - -enum OnboardingStep { - WELCOME = 'WELCOME', - // SELECT_INTERESTS = 'SELECT_INTERESTS', - COMPLETE = 'COMPLETE', -} -type OnboardingState = { - currentStep: OnboardingStep -} -type Action = {type: 'NEXT_STEP'} -const initialState: OnboardingState = { - currentStep: OnboardingStep.WELCOME, -} -const reducer = (state: OnboardingState, action: Action): OnboardingState => { - switch (action.type) { - case 'NEXT_STEP': - switch (state.currentStep) { - case OnboardingStep.WELCOME: - track('Onboarding:Begin') - return {...state, currentStep: OnboardingStep.COMPLETE} - case OnboardingStep.COMPLETE: - track('Onboarding:Complete') - return state - default: - return state - } - default: - return state - } -} - -export const Onboarding = () => { - const pal = usePalette('default') - const rootStore = useStores() - const [state, dispatch] = React.useReducer(reducer, initialState) - const next = React.useCallback( - () => dispatch({type: 'NEXT_STEP'}), - [dispatch], - ) - - React.useEffect(() => { - if (state.currentStep === OnboardingStep.COMPLETE) { - // navigate to home - rootStore.shell.closeModal() - } - }, [state.currentStep, rootStore.shell]) - - return ( - <View style={[pal.view, styles.container]}> - {state.currentStep === OnboardingStep.WELCOME && <Welcome next={next} />} - </View> - ) -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - paddingHorizontal: 20, - }, -}) diff --git a/src/view/com/auth/onboarding/RecommendedFeeds.tsx b/src/view/com/auth/onboarding/RecommendedFeeds.tsx new file mode 100644 index 000000000..28dc2cdd0 --- /dev/null +++ b/src/view/com/auth/onboarding/RecommendedFeeds.tsx @@ -0,0 +1,176 @@ +import React from 'react' +import {FlatList, StyleSheet, View} from 'react-native' +import {observer} from 'mobx-react-lite' +import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' +import {TabletOrDesktop, Mobile} from 'view/com/util/layouts/Breakpoints' +import {Text} from 'view/com/util/text/Text' +import {ViewHeader} from 'view/com/util/ViewHeader' +import {TitleColumnLayout} from 'view/com/util/layouts/TitleColumnLayout' +import {Button} from 'view/com/util/forms/Button' +import {RecommendedFeedsItem} from './RecommendedFeedsItem' +import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' +import {usePalette} from 'lib/hooks/usePalette' +import {RECOMMENDED_FEEDS} from 'lib/constants' + +type Props = { + next: () => void +} +export const RecommendedFeeds = observer(({next}: Props) => { + const pal = usePalette('default') + const {isTabletOrMobile} = useWebMediaQueries() + + const title = ( + <> + <Text + style={[ + pal.textLight, + tdStyles.title1, + isTabletOrMobile && tdStyles.title1Small, + ]}> + Choose your + </Text> + <Text + style={[ + pal.link, + tdStyles.title2, + isTabletOrMobile && tdStyles.title2Small, + ]}> + Recomended + </Text> + <Text + style={[ + pal.link, + tdStyles.title2, + isTabletOrMobile && tdStyles.title2Small, + ]}> + Feeds + </Text> + <Text type="2xl-medium" style={[pal.textLight, tdStyles.description]}> + Feeds are created by users to curate content. Choose some feeds that you + find interesting. + </Text> + <View + style={{ + flexDirection: 'row', + justifyContent: 'flex-end', + marginTop: 20, + }}> + <Button onPress={next} testID="continueBtn"> + <View + style={{ + flexDirection: 'row', + alignItems: 'center', + paddingLeft: 2, + gap: 6, + }}> + <Text + type="2xl-medium" + style={{color: '#fff', position: 'relative', top: -1}}> + Done + </Text> + <FontAwesomeIcon icon="angle-right" color="#fff" size={14} /> + </View> + </Button> + </View> + </> + ) + + return ( + <> + <TabletOrDesktop> + <TitleColumnLayout + testID="recommendedFeedsScreen" + title={title} + horizontal + titleStyle={isTabletOrMobile ? undefined : {minWidth: 470}} + contentStyle={{paddingHorizontal: 0}}> + <FlatList + data={RECOMMENDED_FEEDS} + renderItem={({item}) => <RecommendedFeedsItem {...item} />} + keyExtractor={item => item.did + item.rkey} + style={{flex: 1}} + /> + </TitleColumnLayout> + </TabletOrDesktop> + <Mobile> + <View style={[mStyles.container]} testID="recommendedFeedsScreen"> + <ViewHeader + title="Recommended Feeds" + showBackButton={false} + showOnDesktop + /> + <Text type="lg-medium" style={[pal.text, mStyles.header]}> + Check out some recommended feeds. Tap + to add them to your list of + pinned feeds. + </Text> + + <FlatList + data={RECOMMENDED_FEEDS} + renderItem={({item}) => <RecommendedFeedsItem {...item} />} + keyExtractor={item => item.did + item.rkey} + style={{flex: 1}} + /> + + <Button + onPress={next} + label="Continue" + testID="continueBtn" + style={mStyles.button} + labelStyle={mStyles.buttonText} + /> + </View> + </Mobile> + </> + ) +}) + +const tdStyles = StyleSheet.create({ + container: { + flex: 1, + marginHorizontal: 16, + justifyContent: 'space-between', + }, + title1: { + fontSize: 36, + fontWeight: '800', + textAlign: 'right', + }, + title1Small: { + fontSize: 24, + }, + title2: { + fontSize: 58, + fontWeight: '800', + textAlign: 'right', + }, + title2Small: { + fontSize: 36, + }, + description: { + maxWidth: 400, + marginTop: 10, + marginLeft: 'auto', + textAlign: 'right', + }, +}) + +const mStyles = StyleSheet.create({ + container: { + flex: 1, + justifyContent: 'space-between', + }, + header: { + marginBottom: 16, + marginHorizontal: 16, + }, + button: { + marginBottom: 16, + marginHorizontal: 16, + marginTop: 16, + }, + buttonText: { + textAlign: 'center', + fontSize: 18, + paddingVertical: 4, + }, +}) diff --git a/src/view/com/auth/onboarding/RecommendedFeedsItem.tsx b/src/view/com/auth/onboarding/RecommendedFeedsItem.tsx new file mode 100644 index 000000000..d16b3213e --- /dev/null +++ b/src/view/com/auth/onboarding/RecommendedFeedsItem.tsx @@ -0,0 +1,142 @@ +import React from 'react' +import {View} from 'react-native' +import {observer} from 'mobx-react-lite' +import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' +import {Text} from 'view/com/util/text/Text' +import {Button} from 'view/com/util/forms/Button' +import {UserAvatar} from 'view/com/util/UserAvatar' +import * as Toast from 'view/com/util/Toast' +import {HeartIcon} from 'lib/icons' +import {usePalette} from 'lib/hooks/usePalette' +import {useCustomFeed} from 'lib/hooks/useCustomFeed' +import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' +import {makeRecordUri} from 'lib/strings/url-helpers' +import {sanitizeHandle} from 'lib/strings/handles' + +export const RecommendedFeedsItem = observer( + ({did, rkey}: {did: string; rkey: string}) => { + const {isMobile} = useWebMediaQueries() + const pal = usePalette('default') + const uri = makeRecordUri(did, 'app.bsky.feed.generator', rkey) + const item = useCustomFeed(uri) + if (!item) return null + const onToggle = async () => { + if (item.isSaved) { + try { + await item.unsave() + } catch (e) { + Toast.show('There was an issue contacting your server') + console.error('Failed to unsave feed', {e}) + } + } else { + try { + await item.save() + await item.pin() + } catch (e) { + Toast.show('There was an issue contacting your server') + console.error('Failed to pin feed', {e}) + } + } + } + return ( + <View testID={`feed-${item.displayName}`}> + <View + style={[ + pal.border, + { + flex: isMobile ? 1 : undefined, + flexDirection: 'row', + gap: 18, + maxWidth: isMobile ? undefined : 670, + borderRightWidth: isMobile ? undefined : 1, + paddingHorizontal: 24, + paddingVertical: isMobile ? 12 : 24, + borderTopWidth: 1, + }, + ]}> + <View style={{marginTop: 2}}> + <UserAvatar type="algo" size={42} avatar={item.data.avatar} /> + </View> + <View style={{flex: isMobile ? 1 : undefined}}> + <Text + type="2xl-bold" + numberOfLines={1} + style={[pal.text, {fontSize: 19}]}> + {item.displayName} + </Text> + + <Text style={[pal.textLight, {marginBottom: 8}]} numberOfLines={1}> + by {sanitizeHandle(item.data.creator.handle, '@')} + </Text> + + {item.data.description ? ( + <Text + type="xl" + style={[ + pal.text, + { + flex: isMobile ? 1 : undefined, + maxWidth: 550, + marginBottom: 18, + }, + ]} + numberOfLines={6}> + {item.data.description} + </Text> + ) : null} + + <View style={{flexDirection: 'row', alignItems: 'center', gap: 12}}> + <Button + type="inverted" + style={{paddingVertical: 6}} + onPress={onToggle}> + <View + style={{ + flexDirection: 'row', + alignItems: 'center', + paddingRight: 2, + gap: 6, + }}> + {item.isSaved ? ( + <> + <FontAwesomeIcon + icon="check" + size={16} + color={pal.colors.textInverted} + /> + <Text type="lg-medium" style={pal.textInverted}> + Added + </Text> + </> + ) : ( + <> + <FontAwesomeIcon + icon="plus" + size={16} + color={pal.colors.textInverted} + /> + <Text type="lg-medium" style={pal.textInverted}> + Add + </Text> + </> + )} + </View> + </Button> + + <View style={{flexDirection: 'row', gap: 4}}> + <HeartIcon + size={16} + strokeWidth={2.5} + style={[pal.textLight, {position: 'relative', top: 2}]} + /> + <Text type="lg-medium" style={[pal.text, pal.textLight]}> + {item.data.likeCount || 0} + </Text> + </View> + </View> + </View> + </View> + </View> + ) + }, +) diff --git a/src/view/com/auth/onboarding/Welcome.tsx b/src/view/com/auth/onboarding/Welcome.tsx index 87435c88a..b44b58f84 100644 --- a/src/view/com/auth/onboarding/Welcome.tsx +++ b/src/view/com/auth/onboarding/Welcome.tsx @@ -1,92 +1,10 @@ -import React from 'react' -import {StyleSheet, View} from 'react-native' -import {Text} from 'view/com/util/text/Text' -import {s} from 'lib/styles' -import {usePalette} from 'lib/hooks/usePalette' -import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' -import {Button} from 'view/com/util/forms/Button' - -export const Welcome = ({next}: {next: () => void}) => { - const pal = usePalette('default') - return ( - <View style={[styles.container]}> - <View testID="welcomeScreen"> - <Text style={[pal.text, styles.title]}>Welcome to </Text> - <Text style={[pal.text, pal.link, styles.title]}>Bluesky</Text> - - <View style={styles.spacer} /> - - <View style={[styles.row]}> - <FontAwesomeIcon icon={'globe'} size={36} color={pal.colors.link} /> - <View style={[styles.rowText]}> - <Text type="lg-bold" style={[pal.text]}> - Bluesky is public. - </Text> - <Text type="lg-thin" style={[pal.text, s.pt2]}> - Your posts, likes, and blocks are public. Mutes are private. - </Text> - </View> - </View> - <View style={[styles.row]}> - <FontAwesomeIcon icon={'at'} size={36} color={pal.colors.link} /> - <View style={[styles.rowText]}> - <Text type="lg-bold" style={[pal.text]}> - Bluesky is open. - </Text> - <Text type="lg-thin" style={[pal.text, s.pt2]}> - Never lose access to your followers and data. - </Text> - </View> - </View> - <View style={[styles.row]}> - <FontAwesomeIcon icon={'gear'} size={36} color={pal.colors.link} /> - <View style={[styles.rowText]}> - <Text type="lg-bold" style={[pal.text]}> - Bluesky is flexible. - </Text> - <Text type="lg-thin" style={[pal.text, s.pt2]}> - Choose the algorithms that power your experience with custom - feeds. - </Text> - </View> - </View> - </View> - - <Button - onPress={next} - label="Continue" - testID="continueBtn" - labelStyle={styles.buttonText} - /> - </View> - ) -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - marginVertical: 60, - justifyContent: 'space-between', - }, - title: { - fontSize: 48, - fontWeight: '800', - }, - row: { - flexDirection: 'row', - columnGap: 20, - alignItems: 'center', - marginVertical: 20, - }, - rowText: { - flex: 1, - }, - spacer: { - height: 20, - }, - buttonText: { - textAlign: 'center', - fontSize: 18, - marginVertical: 4, - }, -}) +import 'react' +import {withBreakpoints} from 'view/com/util/layouts/withBreakpoints' +import {WelcomeDesktop} from './WelcomeDesktop' +import {WelcomeMobile} from './WelcomeMobile' + +export const Welcome = withBreakpoints( + WelcomeMobile, + WelcomeDesktop, + WelcomeDesktop, +) diff --git a/src/view/com/auth/onboarding/WelcomeDesktop.tsx b/src/view/com/auth/onboarding/WelcomeDesktop.tsx new file mode 100644 index 000000000..e63693443 --- /dev/null +++ b/src/view/com/auth/onboarding/WelcomeDesktop.tsx @@ -0,0 +1,123 @@ +import React from 'react' +import {StyleSheet, View} from 'react-native' +import {useMediaQuery} from 'react-responsive' +import {Text} from 'view/com/util/text/Text' +import {s} from 'lib/styles' +import {usePalette} from 'lib/hooks/usePalette' +import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' +import {TitleColumnLayout} from 'view/com/util/layouts/TitleColumnLayout' +import {Button} from 'view/com/util/forms/Button' +import {observer} from 'mobx-react-lite' + +type Props = { + next: () => void + skip: () => void +} + +export const WelcomeDesktop = observer(({next}: Props) => { + const pal = usePalette('default') + const horizontal = useMediaQuery({ + query: '(min-width: 1230px)', + }) + const title = ( + <> + <Text + style={[ + pal.textLight, + { + fontSize: 36, + fontWeight: '800', + textAlign: horizontal ? 'right' : 'left', + }, + ]}> + Welcome to + </Text> + <Text + style={[ + pal.link, + { + fontSize: 72, + fontWeight: '800', + textAlign: horizontal ? 'right' : 'left', + }, + ]}> + Bluesky + </Text> + </> + ) + return ( + <TitleColumnLayout + testID="welcomeOnboarding" + title={title} + horizontal={horizontal} + titleStyle={horizontal ? {paddingBottom: 160} : undefined}> + <View style={[styles.row]}> + <FontAwesomeIcon icon={'globe'} size={36} color={pal.colors.link} /> + <View style={[styles.rowText]}> + <Text type="xl-bold" style={[pal.text]}> + Bluesky is public. + </Text> + <Text type="xl" style={[pal.text, s.pt2]}> + Your posts, likes, and blocks are public. Mutes are private. + </Text> + </View> + </View> + <View style={[styles.row]}> + <FontAwesomeIcon icon={'at'} size={36} color={pal.colors.link} /> + <View style={[styles.rowText]}> + <Text type="xl-bold" style={[pal.text]}> + Bluesky is open. + </Text> + <Text type="xl" style={[pal.text, s.pt2]}> + Never lose access to your followers and data. + </Text> + </View> + </View> + <View style={[styles.row]}> + <FontAwesomeIcon icon={'gear'} size={36} color={pal.colors.link} /> + <View style={[styles.rowText]}> + <Text type="xl-bold" style={[pal.text]}> + Bluesky is flexible. + </Text> + <Text type="xl" style={[pal.text, s.pt2]}> + Choose the algorithms that power your experience with custom feeds. + </Text> + </View> + </View> + <View style={styles.spacer} /> + <View style={{flexDirection: 'row'}}> + <Button onPress={next} testID="continueBtn"> + <View + style={{ + flexDirection: 'row', + alignItems: 'center', + paddingLeft: 2, + gap: 6, + }}> + <Text + type="2xl-medium" + style={{color: '#fff', position: 'relative', top: -1}}> + Next + </Text> + <FontAwesomeIcon icon="angle-right" color="#fff" size={14} /> + </View> + </Button> + </View> + </TitleColumnLayout> + ) +}) + +const styles = StyleSheet.create({ + row: { + flexDirection: 'row', + columnGap: 20, + alignItems: 'center', + marginVertical: 20, + }, + rowText: { + flex: 1, + }, + spacer: { + height: 20, + }, +}) diff --git a/src/view/com/auth/onboarding/WelcomeMobile.tsx b/src/view/com/auth/onboarding/WelcomeMobile.tsx new file mode 100644 index 000000000..eb72de836 --- /dev/null +++ b/src/view/com/auth/onboarding/WelcomeMobile.tsx @@ -0,0 +1,123 @@ +import React from 'react' +import {Pressable, StyleSheet, View} from 'react-native' +import {Text} from 'view/com/util/text/Text' +import {s} from 'lib/styles' +import {usePalette} from 'lib/hooks/usePalette' +import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' +import {Button} from 'view/com/util/forms/Button' +import {observer} from 'mobx-react-lite' +import {ViewHeader} from 'view/com/util/ViewHeader' +import {isDesktopWeb} from 'platform/detection' + +type Props = { + next: () => void + skip: () => void +} + +export const WelcomeMobile = observer(({next, skip}: Props) => { + const pal = usePalette('default') + + return ( + <View style={[styles.container]} testID="welcomeOnboarding"> + <ViewHeader + showOnDesktop + showBorder={false} + showBackButton={false} + title="" + renderButton={() => { + return ( + <Pressable + accessibilityRole="button" + style={[s.flexRow, s.alignCenter]} + onPress={skip}> + <Text style={[pal.link]}>Skip</Text> + <FontAwesomeIcon + icon={'chevron-right'} + size={14} + color={pal.colors.link} + /> + </Pressable> + ) + }} + /> + <View> + <Text style={[pal.text, styles.title]}> + Welcome to{' '} + <Text style={[pal.text, pal.link, styles.title]}>Bluesky</Text> + </Text> + <View style={styles.spacer} /> + <View style={[styles.row]}> + <FontAwesomeIcon icon={'globe'} size={36} color={pal.colors.link} /> + <View style={[styles.rowText]}> + <Text type="lg-bold" style={[pal.text]}> + Bluesky is public. + </Text> + <Text type="lg-thin" style={[pal.text, s.pt2]}> + Your posts, likes, and blocks are public. Mutes are private. + </Text> + </View> + </View> + <View style={[styles.row]}> + <FontAwesomeIcon icon={'at'} size={36} color={pal.colors.link} /> + <View style={[styles.rowText]}> + <Text type="lg-bold" style={[pal.text]}> + Bluesky is open. + </Text> + <Text type="lg-thin" style={[pal.text, s.pt2]}> + Never lose access to your followers and data. + </Text> + </View> + </View> + <View style={[styles.row]}> + <FontAwesomeIcon icon={'gear'} size={36} color={pal.colors.link} /> + <View style={[styles.rowText]}> + <Text type="lg-bold" style={[pal.text]}> + Bluesky is flexible. + </Text> + <Text type="lg-thin" style={[pal.text, s.pt2]}> + Choose the algorithms that power your experience with custom + feeds. + </Text> + </View> + </View> + </View> + + <Button + onPress={next} + label="Continue" + testID="continueBtn" + labelStyle={styles.buttonText} + /> + </View> + ) +}) + +const styles = StyleSheet.create({ + container: { + flex: 1, + marginBottom: isDesktopWeb ? 30 : 60, + marginHorizontal: 16, + justifyContent: 'space-between', + }, + title: { + fontSize: 42, + fontWeight: '800', + }, + row: { + flexDirection: 'row', + columnGap: 20, + alignItems: 'center', + marginVertical: 20, + }, + rowText: { + flex: 1, + }, + spacer: { + height: 20, + }, + buttonText: { + textAlign: 'center', + fontSize: 18, + marginVertical: 4, + }, +}) diff --git a/src/view/com/auth/withAuthRequired.tsx b/src/view/com/auth/withAuthRequired.tsx index 8e57669be..c81c2d5df 100644 --- a/src/view/com/auth/withAuthRequired.tsx +++ b/src/view/com/auth/withAuthRequired.tsx @@ -9,6 +9,7 @@ import {observer} from 'mobx-react-lite' import {useStores} from 'state/index' import {CenteredView} from '../util/Views' import {LoggedOut} from './LoggedOut' +import {Onboarding} from './Onboarding' import {Text} from '../util/text/Text' import {usePalette} from 'lib/hooks/usePalette' import {STATUS_PAGE_URL} from 'lib/constants' @@ -24,6 +25,9 @@ export const withAuthRequired = <P extends object>( if (!store.session.hasSession) { return <LoggedOut /> } + if (store.onboarding.isActive) { + return <Onboarding /> + } return <Component {...props} /> }) diff --git a/src/view/com/modals/Modal.tsx b/src/view/com/modals/Modal.tsx index d6d1e212d..4a5a7c504 100644 --- a/src/view/com/modals/Modal.tsx +++ b/src/view/com/modals/Modal.tsx @@ -28,7 +28,6 @@ import * as AddAppPassword from './AddAppPasswords' import * as ContentFilteringSettingsModal from './ContentFilteringSettings' import * as ContentLanguagesSettingsModal from './lang-settings/ContentLanguagesSettings' import * as PostLanguagesSettingsModal from './lang-settings/PostLanguagesSettings' -import * as OnboardingModal from './OnboardingModal' import * as ModerationDetailsModal from './ModerationDetails' const DEFAULT_SNAPPOINTS = ['90%'] @@ -130,9 +129,6 @@ export const ModalsContainer = observer(function ModalsContainer() { } else if (activeModal?.name === 'post-languages-settings') { snapPoints = PostLanguagesSettingsModal.snapPoints element = <PostLanguagesSettingsModal.Component /> - } else if (activeModal?.name === 'onboarding') { - snapPoints = OnboardingModal.snapPoints - element = <OnboardingModal.Component /> } else if (activeModal?.name === 'moderation-details') { snapPoints = ModerationDetailsModal.snapPoints element = <ModerationDetailsModal.Component {...activeModal} /> diff --git a/src/view/com/modals/Modal.web.tsx b/src/view/com/modals/Modal.web.tsx index 05bb7161f..5cfdd6bb3 100644 --- a/src/view/com/modals/Modal.web.tsx +++ b/src/view/com/modals/Modal.web.tsx @@ -26,7 +26,6 @@ import * as AddAppPassword from './AddAppPasswords' import * as ContentFilteringSettingsModal from './ContentFilteringSettings' import * as ContentLanguagesSettingsModal from './lang-settings/ContentLanguagesSettings' import * as PostLanguagesSettingsModal from './lang-settings/PostLanguagesSettings' -import * as OnboardingModal from './OnboardingModal' import * as ModerationDetailsModal from './ModerationDetails' export const ModalsContainer = observer(function ModalsContainer() { @@ -105,8 +104,6 @@ function Modal({modal}: {modal: ModalIface}) { element = <AltTextImageModal.Component {...modal} /> } else if (modal.name === 'edit-image') { element = <EditImageModal.Component {...modal} /> - } else if (modal.name === 'onboarding') { - element = <OnboardingModal.Component /> } else if (modal.name === 'moderation-details') { element = <ModerationDetailsModal.Component {...modal} /> } else { diff --git a/src/view/com/modals/OnboardingModal.tsx b/src/view/com/modals/OnboardingModal.tsx deleted file mode 100644 index c70f4fd62..000000000 --- a/src/view/com/modals/OnboardingModal.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' -import {Onboarding} from '../auth/onboarding/Onboarding' - -export const snapPoints = ['90%'] - -export function Component() { - return <Onboarding /> -} diff --git a/src/view/com/util/ViewHeader.tsx b/src/view/com/util/ViewHeader.tsx index f5a921ac0..7482db8eb 100644 --- a/src/view/com/util/ViewHeader.tsx +++ b/src/view/com/util/ViewHeader.tsx @@ -17,6 +17,7 @@ const BACK_HITSLOP = {left: 20, top: 20, right: 50, bottom: 20} export const ViewHeader = observer(function ({ title, canGoBack, + showBackButton = true, hideOnScroll, showOnDesktop, showBorder, @@ -24,6 +25,7 @@ export const ViewHeader = observer(function ({ }: { title: string canGoBack?: boolean + showBackButton?: boolean hideOnScroll?: boolean showOnDesktop?: boolean showBorder?: boolean @@ -49,7 +51,13 @@ export const ViewHeader = observer(function ({ if (isDesktopWeb) { if (showOnDesktop) { - return <DesktopWebHeader title={title} renderButton={renderButton} /> + return ( + <DesktopWebHeader + title={title} + renderButton={renderButton} + showBorder={showBorder} + /> + ) } return null } else { @@ -59,30 +67,32 @@ export const ViewHeader = observer(function ({ return ( <Container hideOnScroll={hideOnScroll || false} showBorder={showBorder}> - <TouchableOpacity - testID="viewHeaderDrawerBtn" - onPress={canGoBack ? onPressBack : onPressMenu} - hitSlop={BACK_HITSLOP} - style={canGoBack ? styles.backBtn : styles.backBtnWide} - accessibilityRole="button" - accessibilityLabel={canGoBack ? 'Back' : 'Menu'} - accessibilityHint={ - canGoBack ? '' : 'Access navigation links and settings' - }> - {canGoBack ? ( - <FontAwesomeIcon - size={18} - icon="angle-left" - style={[styles.backIcon, pal.text]} - /> - ) : ( - <FontAwesomeIcon - size={18} - icon="bars" - style={[styles.backIcon, pal.textLight]} - /> - )} - </TouchableOpacity> + {showBackButton ? ( + <TouchableOpacity + testID="viewHeaderDrawerBtn" + onPress={canGoBack ? onPressBack : onPressMenu} + hitSlop={BACK_HITSLOP} + style={canGoBack ? styles.backBtn : styles.backBtnWide} + accessibilityRole="button" + accessibilityLabel={canGoBack ? 'Back' : 'Menu'} + accessibilityHint={ + canGoBack ? '' : 'Access navigation links and settings' + }> + {canGoBack ? ( + <FontAwesomeIcon + size={18} + icon="angle-left" + style={[styles.backIcon, pal.text]} + /> + ) : ( + <FontAwesomeIcon + size={18} + icon="bars" + style={[styles.backIcon, pal.textLight]} + /> + )} + </TouchableOpacity> + ) : null} <View style={styles.titleContainer} pointerEvents="none"> <Text type="title" style={[pal.text, styles.title]}> {title} @@ -90,9 +100,9 @@ export const ViewHeader = observer(function ({ </View> {renderButton ? ( renderButton() - ) : ( + ) : showBackButton ? ( <View style={canGoBack ? styles.backBtn : styles.backBtnWide} /> - )} + ) : null} </Container> ) } @@ -101,13 +111,23 @@ export const ViewHeader = observer(function ({ function DesktopWebHeader({ title, renderButton, + showBorder = true, }: { title: string renderButton?: () => JSX.Element + showBorder?: boolean }) { const pal = usePalette('default') return ( - <CenteredView style={[styles.header, styles.desktopHeader, pal.border]}> + <CenteredView + style={[ + styles.header, + styles.desktopHeader, + pal.border, + { + borderBottomWidth: showBorder ? 1 : 0, + }, + ]}> <View style={styles.titleContainer} pointerEvents="none"> <Text type="title-lg" style={[pal.text, styles.title]}> {title} @@ -195,13 +215,11 @@ const styles = StyleSheet.create({ width: '100%', }, desktopHeader: { - borderBottomWidth: 1, paddingVertical: 12, }, border: { borderBottomWidth: 1, }, - titleContainer: { marginLeft: 'auto', marginRight: 'auto', diff --git a/src/view/com/util/layouts/Breakpoints.tsx b/src/view/com/util/layouts/Breakpoints.tsx new file mode 100644 index 000000000..51c3ccd5a --- /dev/null +++ b/src/view/com/util/layouts/Breakpoints.tsx @@ -0,0 +1,8 @@ +import React from 'react' + +export const Desktop = ({}: React.PropsWithChildren<{}>) => null +export const TabletOrDesktop = ({}: React.PropsWithChildren<{}>) => null +export const Tablet = ({}: React.PropsWithChildren<{}>) => null +export const TabletOrMobile = ({children}: React.PropsWithChildren<{}>) => + children +export const Mobile = ({children}: React.PropsWithChildren<{}>) => children diff --git a/src/view/com/util/layouts/Breakpoints.web.tsx b/src/view/com/util/layouts/Breakpoints.web.tsx new file mode 100644 index 000000000..7031a1735 --- /dev/null +++ b/src/view/com/util/layouts/Breakpoints.web.tsx @@ -0,0 +1,20 @@ +import React from 'react' +import MediaQuery from 'react-responsive' + +export const Desktop = ({children}: React.PropsWithChildren<{}>) => ( + <MediaQuery minWidth={1224}>{children}</MediaQuery> +) +export const TabletOrDesktop = ({children}: React.PropsWithChildren<{}>) => ( + <MediaQuery minWidth={800}>{children}</MediaQuery> +) +export const Tablet = ({children}: React.PropsWithChildren<{}>) => ( + <MediaQuery minWidth={800} maxWidth={1224}> + {children} + </MediaQuery> +) +export const TabletOrMobile = ({children}: React.PropsWithChildren<{}>) => ( + <MediaQuery maxWidth={1224}>{children}</MediaQuery> +) +export const Mobile = ({children}: React.PropsWithChildren<{}>) => ( + <MediaQuery maxWidth={800}>{children}</MediaQuery> +) diff --git a/src/view/com/util/layouts/TitleColumnLayout.tsx b/src/view/com/util/layouts/TitleColumnLayout.tsx new file mode 100644 index 000000000..49ad9fcdb --- /dev/null +++ b/src/view/com/util/layouts/TitleColumnLayout.tsx @@ -0,0 +1,69 @@ +import React from 'react' +import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native' +import {usePalette} from 'lib/hooks/usePalette' +import {useColorSchemeStyle} from 'lib/hooks/useColorSchemeStyle' + +interface Props { + testID?: string + title: JSX.Element + horizontal: boolean + titleStyle?: StyleProp<ViewStyle> + contentStyle?: StyleProp<ViewStyle> +} + +export function TitleColumnLayout({ + testID, + title, + horizontal, + children, + titleStyle, + contentStyle, +}: React.PropsWithChildren<Props>) { + const pal = usePalette('default') + const titleBg = useColorSchemeStyle(pal.viewLight, pal.view) + const contentBg = useColorSchemeStyle(pal.view, { + backgroundColor: pal.colors.background, + borderColor: pal.colors.border, + borderLeftWidth: 1, + }) + + const layoutStyles = horizontal ? styles2Column : styles1Column + return ( + <View testID={testID} style={layoutStyles.container}> + <View style={[layoutStyles.title, titleBg, titleStyle]}>{title}</View> + <View style={[layoutStyles.content, contentBg, contentStyle]}> + {children} + </View> + </View> + ) +} + +const styles2Column = StyleSheet.create({ + container: { + flexDirection: 'row', + height: '100%', + }, + title: { + flex: 1, + paddingHorizontal: 40, + paddingBottom: 80, + justifyContent: 'center', + }, + content: { + flex: 2, + paddingHorizontal: 40, + justifyContent: 'center', + }, +}) + +const styles1Column = StyleSheet.create({ + container: {}, + title: { + paddingHorizontal: 40, + paddingVertical: 40, + }, + content: { + paddingHorizontal: 40, + paddingVertical: 40, + }, +}) diff --git a/src/view/com/util/layouts/withBreakpoints.tsx b/src/view/com/util/layouts/withBreakpoints.tsx new file mode 100644 index 000000000..dc3f50dc9 --- /dev/null +++ b/src/view/com/util/layouts/withBreakpoints.tsx @@ -0,0 +1,21 @@ +import React from 'react' +import {isNative} from 'platform/detection' +import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' + +export const withBreakpoints = + <P extends object>( + Mobile: React.ComponentType<P>, + Tablet: React.ComponentType<P>, + Desktop: React.ComponentType<P>, + ): React.FC<P> => + (props: P) => { + const {isMobile, isTabletOrMobile} = useWebMediaQueries() + + if (isMobile || isNative) { + return <Mobile {...props} /> + } + if (isTabletOrMobile) { + return <Tablet {...props} /> + } + return <Desktop {...props} /> + } diff --git a/src/view/index.ts b/src/view/index.ts index 1c3dc3937..2e4c08ec7 100644 --- a/src/view/index.ts +++ b/src/view/index.ts @@ -92,6 +92,7 @@ import {faPlay} from '@fortawesome/free-solid-svg-icons/faPlay' import {faPause} from '@fortawesome/free-solid-svg-icons/faPause' import {faThumbtack} from '@fortawesome/free-solid-svg-icons/faThumbtack' import {faList} from '@fortawesome/free-solid-svg-icons/faList' +import {faChevronRight} from '@fortawesome/free-solid-svg-icons/faChevronRight' export function setup() { library.add( @@ -187,5 +188,6 @@ export function setup() { faPlay, faPause, faList, + faChevronRight, ) } diff --git a/src/view/screens/Home.tsx b/src/view/screens/Home.tsx index 9259d4bea..7262756d3 100644 --- a/src/view/screens/Home.tsx +++ b/src/view/screens/Home.tsx @@ -31,7 +31,7 @@ const POLL_FREQ = 30e3 // 30sec type Props = NativeStackScreenProps<HomeTabNavigatorParams, 'Home'> export const HomeScreen = withAuthRequired( - observer((_opts: Props) => { + observer(({}: Props) => { const store = useStores() const pagerRef = React.useRef<PagerRef>(null) const [selectedPage, setSelectedPage] = React.useState(0) diff --git a/src/view/screens/Settings.tsx b/src/view/screens/Settings.tsx index b20d36310..481d77086 100644 --- a/src/view/screens/Settings.tsx +++ b/src/view/screens/Settings.tsx @@ -162,6 +162,11 @@ export const SettingsScreen = withAuthRequired( Toast.show('Preferences reset') }, [store]) + const onPressResetOnboarding = React.useCallback(async () => { + store.onboarding.reset() + Toast.show('Onboarding reset') + }, [store]) + const onPressBuildInfo = React.useCallback(() => { Clipboard.setString( `Build version: ${AppInfo.appVersion}; Platform: ${Platform.OS}`, @@ -533,6 +538,16 @@ export const SettingsScreen = withAuthRequired( Reset preferences state </Text> </TouchableOpacity> + <TouchableOpacity + style={[pal.view, styles.linkCardNoIcon]} + onPress={onPressResetOnboarding} + accessibilityRole="button" + accessibilityHint="Reset onboarding" + accessibilityLabel="Resets the onboarding state"> + <Text type="lg" style={pal.text}> + Reset onboarding state + </Text> + </TouchableOpacity> </> ) : null} <View style={[styles.footer]}> diff --git a/src/view/shell/index.web.tsx b/src/view/shell/index.web.tsx index 16ed17a5b..e36439c82 100644 --- a/src/view/shell/index.web.tsx +++ b/src/view/shell/index.web.tsx @@ -20,7 +20,6 @@ import {NavigationProp} from 'lib/routes/types' const ShellInner = observer(() => { const store = useStores() const {isDesktop} = useWebMediaQueries() - const navigator = useNavigation<NavigationProp>() useEffect(() => { @@ -29,6 +28,9 @@ const ShellInner = observer(() => { }) }, [navigator, store.shell]) + const showBottomBar = !isDesktop && !store.onboarding.isActive + const showSideNavs = + isDesktop && store.session.hasSession && !store.onboarding.isActive return ( <> <View style={s.hContentRegion}> @@ -36,7 +38,7 @@ const ShellInner = observer(() => { <FlatNavigator /> </ErrorBoundary> </View> - {isDesktop && store.session.hasSession && ( + {showSideNavs && ( <> <DesktopLeftNav /> <DesktopRightNav /> @@ -51,7 +53,7 @@ const ShellInner = observer(() => { onPost={store.shell.composerOpts?.onPost} mention={store.shell.composerOpts?.mention} /> - {!isDesktop && <BottomBarWeb />} + {showBottomBar && <BottomBarWeb />} <ModalsContainer /> <Lightbox /> {!isDesktop && store.shell.isDrawerOpen && ( |