diff options
Diffstat (limited to 'src/view/com/auth/onboarding')
-rw-r--r-- | src/view/com/auth/onboarding/Onboarding.tsx | 66 | ||||
-rw-r--r-- | src/view/com/auth/onboarding/RecommendedFeeds.tsx | 176 | ||||
-rw-r--r-- | src/view/com/auth/onboarding/RecommendedFeedsItem.tsx | 142 | ||||
-rw-r--r-- | src/view/com/auth/onboarding/Welcome.tsx | 102 | ||||
-rw-r--r-- | src/view/com/auth/onboarding/WelcomeDesktop.tsx | 123 | ||||
-rw-r--r-- | src/view/com/auth/onboarding/WelcomeMobile.tsx | 123 |
6 files changed, 574 insertions, 158 deletions
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, + }, +}) |