diff options
Diffstat (limited to 'src/screens')
-rw-r--r-- | src/screens/Onboarding/Layout.tsx | 2 | ||||
-rw-r--r-- | src/screens/Onboarding/StepFinished.tsx | 23 | ||||
-rw-r--r-- | src/screens/Onboarding/StepInterests/index.tsx | 2 | ||||
-rw-r--r-- | src/screens/Onboarding/StepProfile/AvatarCircle.tsx | 77 | ||||
-rw-r--r-- | src/screens/Onboarding/StepProfile/AvatarCreatorCircle.tsx | 43 | ||||
-rw-r--r-- | src/screens/Onboarding/StepProfile/AvatarCreatorItems.tsx | 145 | ||||
-rw-r--r-- | src/screens/Onboarding/StepProfile/PlaceholderCanvas.tsx | 67 | ||||
-rw-r--r-- | src/screens/Onboarding/StepProfile/index.tsx | 326 | ||||
-rw-r--r-- | src/screens/Onboarding/StepProfile/types.ts | 148 | ||||
-rw-r--r-- | src/screens/Onboarding/StepSuggestedAccounts/index.tsx | 2 | ||||
-rw-r--r-- | src/screens/Onboarding/state.ts | 33 |
11 files changed, 831 insertions, 37 deletions
diff --git a/src/screens/Onboarding/Layout.tsx b/src/screens/Onboarding/Layout.tsx index d48234cca..75e2f99fc 100644 --- a/src/screens/Onboarding/Layout.tsx +++ b/src/screens/Onboarding/Layout.tsx @@ -23,7 +23,7 @@ import {createPortalGroup} from '#/components/Portal' import {leading, P, Text} from '#/components/Typography' import {IS_DEV} from '#/env' -const COL_WIDTH = 500 +const COL_WIDTH = 420 export const OnboardingControls = createPortalGroup() diff --git a/src/screens/Onboarding/StepFinished.tsx b/src/screens/Onboarding/StepFinished.tsx index 7d0bfa422..148069621 100644 --- a/src/screens/Onboarding/StepFinished.tsx +++ b/src/screens/Onboarding/StepFinished.tsx @@ -12,6 +12,7 @@ import {logger} from '#/logger' import {useOverwriteSavedFeedsMutation} from '#/state/queries/preferences' import {useAgent} from '#/state/session' import {useOnboardingDispatch} from '#/state/shell' +import {uploadBlob} from 'lib/api' import { DescriptionText, OnboardingControls, @@ -46,11 +47,13 @@ export function StepFinished() { const finishOnboarding = React.useCallback(async () => { setSaving(true) + // TODO uncomment const { interestsStepResults, suggestedAccountsStepResults, algoFeedsStepResults, topicalFeedsStepResults, + profileStepResults, } = state const {selectedInterests} = interestsStepResults const selectedFeeds = [ @@ -110,6 +113,26 @@ export function StepFinished() { } })(), ]) + + if (gate('reduced_onboarding_and_home_algo')) { + await getAgent().upsertProfile(async existing => { + existing = existing ?? {} + + if (profileStepResults.imageUri && profileStepResults.imageMime) { + const res = await uploadBlob( + getAgent(), + profileStepResults.imageUri, + profileStepResults.imageMime, + ) + + if (res.data.blob) { + existing.avatar = res.data.blob + } + } + + return existing + }) + } } catch (e: any) { logger.info(`onboarding: bulk save failed`) logger.error(e) diff --git a/src/screens/Onboarding/StepInterests/index.tsx b/src/screens/Onboarding/StepInterests/index.tsx index 174488a34..d6678f4b0 100644 --- a/src/screens/Onboarding/StepInterests/index.tsx +++ b/src/screens/Onboarding/StepInterests/index.tsx @@ -31,8 +31,8 @@ import {Text} from '#/components/Typography' export function StepInterests() { const {_} = useLingui() const t = useTheme() - const {track} = useAnalytics() const {gtMobile} = useBreakpoints() + const {track} = useAnalytics() const {state, dispatch, interestsDisplayNames} = React.useContext(Context) const [saving, setSaving] = React.useState(false) const [interests, setInterests] = React.useState<string[]>( diff --git a/src/screens/Onboarding/StepProfile/AvatarCircle.tsx b/src/screens/Onboarding/StepProfile/AvatarCircle.tsx new file mode 100644 index 000000000..1be38b0d5 --- /dev/null +++ b/src/screens/Onboarding/StepProfile/AvatarCircle.tsx @@ -0,0 +1,77 @@ +import React from 'react' +import {View} from 'react-native' +import {Image as ExpoImage} from 'expo-image' +import {msg} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {AvatarCreatorCircle} from '#/screens/Onboarding/StepProfile/AvatarCreatorCircle' +import {useAvatar} from '#/screens/Onboarding/StepProfile/index' +import {atoms as a, useTheme} from '#/alf' +import {Button, ButtonIcon} from '#/components/Button' +import {Pencil_Stroke2_Corner0_Rounded as Pencil} from '#/components/icons/Pencil' +import {StreamingLive_Stroke2_Corner0_Rounded as StreamingLive} from '#/components/icons/StreamingLive' + +export function AvatarCircle({ + openLibrary, + openCreator, +}: { + openLibrary: () => unknown + openCreator: () => unknown +}) { + const {_} = useLingui() + const t = useTheme() + const {avatar} = useAvatar() + + const styles = React.useMemo( + () => ({ + imageContainer: [ + a.rounded_full, + a.overflow_hidden, + a.align_center, + a.justify_center, + a.border, + t.atoms.border_contrast_low, + t.atoms.bg_contrast_25, + { + height: 200, + width: 200, + }, + ], + }), + [t.atoms.bg_contrast_25, t.atoms.border_contrast_low], + ) + + return ( + <View> + {avatar.useCreatedAvatar ? ( + <AvatarCreatorCircle avatar={avatar} size={200} /> + ) : avatar.image ? ( + <ExpoImage + source={avatar.image.path} + style={styles.imageContainer} + accessibilityIgnoresInvertColors + transition={{duration: 300, effect: 'cross-dissolve'}} + /> + ) : ( + <View style={styles.imageContainer}> + <StreamingLive + height={100} + width={100} + style={{color: t.palette.contrast_200}} + /> + </View> + )} + <View style={[a.absolute, {bottom: 2, right: 2}]}> + <Button + label={_(msg`Select an avatar`)} + size="large" + shape="round" + variant="solid" + color="primary" + onPress={avatar.useCreatedAvatar ? openCreator : openLibrary}> + <ButtonIcon icon={Pencil} /> + </Button> + </View> + </View> + ) +} diff --git a/src/screens/Onboarding/StepProfile/AvatarCreatorCircle.tsx b/src/screens/Onboarding/StepProfile/AvatarCreatorCircle.tsx new file mode 100644 index 000000000..1cd68eb61 --- /dev/null +++ b/src/screens/Onboarding/StepProfile/AvatarCreatorCircle.tsx @@ -0,0 +1,43 @@ +import React from 'react' +import {View} from 'react-native' + +import {Avatar} from '#/screens/Onboarding/StepProfile/index' +import {atoms as a, useTheme} from '#/alf' + +export function AvatarCreatorCircle({ + avatar, + size = 125, +}: { + avatar: Avatar + size?: number +}) { + const t = useTheme() + const Icon = avatar.placeholder.component + + const styles = React.useMemo( + () => ({ + imageContainer: [ + a.rounded_full, + a.overflow_hidden, + a.align_center, + a.justify_center, + a.border, + t.atoms.border_contrast_high, + { + height: size, + width: size, + backgroundColor: avatar.backgroundColor, + }, + ], + }), + [avatar.backgroundColor, size, t.atoms.border_contrast_high], + ) + + return ( + <View> + <View style={styles.imageContainer}> + <Icon height={85} width={85} style={{color: t.palette.white}} /> + </View> + </View> + ) +} diff --git a/src/screens/Onboarding/StepProfile/AvatarCreatorItems.tsx b/src/screens/Onboarding/StepProfile/AvatarCreatorItems.tsx new file mode 100644 index 000000000..98c01ce7d --- /dev/null +++ b/src/screens/Onboarding/StepProfile/AvatarCreatorItems.tsx @@ -0,0 +1,145 @@ +import React from 'react' +import {View} from 'react-native' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {Avatar} from '#/screens/Onboarding/StepProfile/index' +import { + AvatarColor, + avatarColors, + emojiItems, + EmojiName, + emojiNames, +} from '#/screens/Onboarding/StepProfile/types' +import {atoms as a, useTheme} from '#/alf' +import {Button, ButtonIcon} from '#/components/Button' +import {Text} from '#/components/Typography' + +const ACTIVE_BORDER_WIDTH = 3 +const ACTIVE_BORDER_STYLES = { + top: -ACTIVE_BORDER_WIDTH, + bottom: -ACTIVE_BORDER_WIDTH, + left: -ACTIVE_BORDER_WIDTH, + right: -ACTIVE_BORDER_WIDTH, + opacity: 0.5, + borderWidth: 3, +} + +export function AvatarCreatorItems({ + type, + avatar, + setAvatar, +}: { + type: 'emojis' | 'colors' + avatar: Avatar + setAvatar: React.Dispatch<React.SetStateAction<Avatar>> +}) { + const {_} = useLingui() + const t = useTheme() + const isEmojis = type === 'emojis' + + const onSelectEmoji = React.useCallback( + (emoji: EmojiName) => { + setAvatar(prev => ({ + ...prev, + placeholder: emojiItems[emoji], + })) + }, + [setAvatar], + ) + + const onSelectColor = React.useCallback( + (color: AvatarColor) => { + setAvatar(prev => ({ + ...prev, + backgroundColor: color, + })) + }, + [setAvatar], + ) + + return ( + <View style={[a.w_full]}> + <Text style={[a.pb_md, t.atoms.text_contrast_medium]}> + {isEmojis ? ( + <Trans>Select an emoji</Trans> + ) : ( + <Trans>Select a color</Trans> + )} + </Text> + + <View + style={[ + a.flex_row, + a.align_start, + a.justify_start, + a.flex_wrap, + a.gap_md, + ]}> + {isEmojis + ? emojiNames.map(emojiName => ( + <Button + key={emojiName} + label={_(msg`Select the ${emojiName} emoji as your avatar`)} + size="small" + shape="round" + variant="solid" + color="secondary" + onPress={() => onSelectEmoji(emojiName)}> + <ButtonIcon icon={emojiItems[emojiName].component} /> + {avatar.placeholder.name === emojiName && ( + <View + style={[ + a.absolute, + a.rounded_full, + ACTIVE_BORDER_STYLES, + { + borderColor: avatar.backgroundColor, + }, + ]} + /> + )} + </Button> + )) + : avatarColors.map(color => ( + <Button + key={color} + label={_(msg`Choose this color as your avatar`)} + size="small" + shape="round" + variant="solid" + onPress={() => onSelectColor(color)}> + {ctx => ( + <> + <View + style={[ + a.absolute, + a.inset_0, + a.rounded_full, + { + opacity: ctx.hovered || ctx.pressed ? 0.8 : 1, + backgroundColor: color, + }, + ]} + /> + + {avatar.backgroundColor === color && ( + <View + style={[ + a.absolute, + a.rounded_full, + ACTIVE_BORDER_STYLES, + { + borderColor: color, + }, + ]} + /> + )} + </> + )} + </Button> + ))} + </View> + </View> + ) +} diff --git a/src/screens/Onboarding/StepProfile/PlaceholderCanvas.tsx b/src/screens/Onboarding/StepProfile/PlaceholderCanvas.tsx new file mode 100644 index 000000000..29ba39a0b --- /dev/null +++ b/src/screens/Onboarding/StepProfile/PlaceholderCanvas.tsx @@ -0,0 +1,67 @@ +import React from 'react' +import {View} from 'react-native' +import ViewShot from 'react-native-view-shot' + +import {useAvatar} from '#/screens/Onboarding/StepProfile/index' +import {atoms as a} from '#/alf' + +const SIZE_MULTIPLIER = 1.5 + +export interface PlaceholderCanvasRef { + capture: () => Promise<string> +} + +// This component is supposed to be invisible to the user. We only need this for ViewShot to have something to +// "screenshot". +export const PlaceholderCanvas = React.forwardRef<PlaceholderCanvasRef, {}>( + function PlaceholderCanvas({}, ref) { + const {avatar} = useAvatar() + const viewshotRef = React.useRef() + const Icon = avatar.placeholder.component + + const styles = React.useMemo( + () => ({ + container: [a.absolute, {top: -2000}], + imageContainer: [ + a.align_center, + a.justify_center, + {height: 150 * SIZE_MULTIPLIER, width: 150 * SIZE_MULTIPLIER}, + ], + }), + [], + ) + + React.useImperativeHandle(ref, () => ({ + // @ts-ignore this library doesn't have types + capture: viewshotRef.current.capture, + })) + + return ( + <View style={styles.container}> + <ViewShot + // @ts-ignore this library doesn't have types + ref={viewshotRef} + options={{ + fileName: 'placeholderAvatar', + format: 'jpg', + quality: 0.8, + height: 150 * SIZE_MULTIPLIER, + width: 150 * SIZE_MULTIPLIER, + }}> + <View + style={[ + styles.imageContainer, + {backgroundColor: avatar.backgroundColor}, + ]} + collapsable={false}> + <Icon + height={85 * SIZE_MULTIPLIER} + width={85 * SIZE_MULTIPLIER} + style={{color: 'white'}} + /> + </View> + </ViewShot> + </View> + ) + }, +) diff --git a/src/screens/Onboarding/StepProfile/index.tsx b/src/screens/Onboarding/StepProfile/index.tsx index 8db3e7761..bf47bbc95 100644 --- a/src/screens/Onboarding/StepProfile/index.tsx +++ b/src/screens/Onboarding/StepProfile/index.tsx @@ -1,55 +1,319 @@ import React from 'react' import {View} from 'react-native' +import {Image as ExpoImage} from 'expo-image' +import { + ImagePickerOptions, + launchImageLibraryAsync, + MediaTypeOptions, +} from 'expo-image-picker' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' +import {useAnalytics} from '#/lib/analytics/analytics' +import {logEvent} from '#/lib/statsig/statsig' +import {usePhotoLibraryPermission} from 'lib/hooks/usePermissions' +import {compressIfNeeded} from 'lib/media/manip' +import {openCropper} from 'lib/media/picker' +import {getDataUriSize} from 'lib/media/util' +import {isNative, isWeb} from 'platform/detection' import { DescriptionText, OnboardingControls, TitleText, } from '#/screens/Onboarding/Layout' import {Context} from '#/screens/Onboarding/state' -import {atoms as a} from '#/alf' +import {AvatarCircle} from '#/screens/Onboarding/StepProfile/AvatarCircle' +import {AvatarCreatorCircle} from '#/screens/Onboarding/StepProfile/AvatarCreatorCircle' +import {AvatarCreatorItems} from '#/screens/Onboarding/StepProfile/AvatarCreatorItems' +import { + PlaceholderCanvas, + PlaceholderCanvasRef, +} from '#/screens/Onboarding/StepProfile/PlaceholderCanvas' +import {atoms as a, useBreakpoints, useTheme} from '#/alf' import {Button, ButtonIcon, ButtonText} from '#/components/Button' +import * as Dialog from '#/components/Dialog' import {IconCircle} from '#/components/IconCircle' import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron' +import {CircleInfo_Stroke2_Corner0_Rounded} from '#/components/icons/CircleInfo' import {StreamingLive_Stroke2_Corner0_Rounded as StreamingLive} from '#/components/icons/StreamingLive' +import {Text} from '#/components/Typography' +import {AvatarColor, avatarColors, Emoji, emojiItems} from './types' + +export interface Avatar { + image?: { + path: string + mime: string + size: number + width: number + height: number + } + backgroundColor: AvatarColor + placeholder: Emoji + useCreatedAvatar: boolean +} + +interface IAvatarContext { + avatar: Avatar + setAvatar: React.Dispatch<React.SetStateAction<Avatar>> +} + +const AvatarContext = React.createContext<IAvatarContext>({} as IAvatarContext) +export const useAvatar = () => React.useContext(AvatarContext) + +const randomColor = + avatarColors[Math.floor(Math.random() * avatarColors.length)] export function StepProfile() { const {_} = useLingui() - const {dispatch} = React.useContext(Context) + const t = useTheme() + const {gtMobile} = useBreakpoints() + const {track} = useAnalytics() + const {requestPhotoAccessIfNeeded} = usePhotoLibraryPermission() + const creatorControl = Dialog.useDialogControl() + const [error, setError] = React.useState('') + + const {state, dispatch} = React.useContext(Context) + const [avatar, setAvatar] = React.useState<Avatar>({ + image: state.profileStepResults?.image, + placeholder: emojiItems.at, + backgroundColor: randomColor, + useCreatedAvatar: false, + }) + + const canvasRef = React.useRef<PlaceholderCanvasRef>(null) + + React.useEffect(() => { + track('OnboardingV2:StepProfile:Start') + }, [track]) + + const openPicker = React.useCallback( + async (opts?: ImagePickerOptions) => { + const response = await launchImageLibraryAsync({ + exif: false, + mediaTypes: MediaTypeOptions.Images, + quality: 1, + ...opts, + }) + + return (response.assets ?? []) + .slice(0, 1) + .filter(asset => { + if ( + !asset.mimeType?.startsWith('image/') || + (!asset.mimeType?.endsWith('jpeg') && + !asset.mimeType?.endsWith('jpg') && + !asset.mimeType?.endsWith('png')) + ) { + setError(_(msg`Only .jpg and .png files are supported`)) + return false + } + return true + }) + .map(image => ({ + mime: 'image/jpeg', + height: image.height, + width: image.width, + path: image.uri, + size: getDataUriSize(image.uri), + })) + }, + [_, setError], + ) + + const onContinue = React.useCallback(async () => { + let imageUri = avatar?.image?.path + if (!imageUri || avatar.useCreatedAvatar) { + imageUri = await canvasRef.current?.capture() + } + + if (imageUri) { + dispatch({ + type: 'setProfileStepResults', + image: avatar.image, + imageUri, + imageMime: avatar.image?.mime ?? 'image/jpeg', + }) + } - const onContinue = React.useCallback(() => { dispatch({type: 'next'}) - }, [dispatch]) + track('OnboardingV2:StepProfile:End') + logEvent('onboarding:profile:nextPressed', {}) + }, [avatar.image, avatar.useCreatedAvatar, dispatch, track]) + + const onDoneCreating = React.useCallback(() => { + setAvatar(prev => ({ + ...prev, + useCreatedAvatar: true, + })) + creatorControl.close() + }, [creatorControl]) + + const openLibrary = React.useCallback(async () => { + if (!(await requestPhotoAccessIfNeeded())) { + return + } + + setError('') + + const items = await openPicker({ + aspect: [1, 1], + }) + let image = items[0] + if (!image) return + + if (!isWeb) { + image = await openCropper({ + mediaType: 'photo', + cropperCircleOverlay: true, + height: image.height, + width: image.width, + path: image.path, + }) + } + image = await compressIfNeeded(image, 1000000) + + // If we are on mobile, prefetching the image will load the image into memory before we try and display it, + // stopping any brief flickers. + if (isNative) { + await ExpoImage.prefetch(image.path) + } + + setAvatar(prev => ({ + ...prev, + image, + useCreatedAvatar: false, + })) + }, [requestPhotoAccessIfNeeded, setAvatar, openPicker, setError]) + + const onSecondaryPress = React.useCallback(() => { + if (avatar.useCreatedAvatar) { + openLibrary() + } else { + creatorControl.open() + } + }, [avatar.useCreatedAvatar, creatorControl, openLibrary]) + + const value = React.useMemo( + () => ({ + avatar, + setAvatar, + }), + [avatar], + ) return ( - <View style={[a.align_start]}> - <IconCircle icon={StreamingLive} style={[a.mb_2xl]} /> - - <TitleText> - <Trans>Give your profile a face</Trans> - </TitleText> - <DescriptionText> - <Trans> - Help people know you're not a bot by uploading a picture or creating - an avatar. - </Trans> - </DescriptionText> - - <OnboardingControls.Portal> - <Button - variant="gradient" - color="gradient_sky" - size="large" - label={_(msg`Continue to next step`)} - onPress={onContinue}> - <ButtonText> - <Trans>Continue</Trans> - </ButtonText> - <ButtonIcon icon={ChevronRight} position="right" /> - </Button> - </OnboardingControls.Portal> - </View> + <AvatarContext.Provider value={value}> + <View style={[a.align_start, t.atoms.bg, a.justify_between]}> + <IconCircle icon={StreamingLive} style={[a.mb_2xl]} /> + <TitleText> + <Trans>Give your profile a face</Trans> + </TitleText> + <DescriptionText> + <Trans> + Help people know you're not a bot by uploading a picture or creating + an avatar. + </Trans> + </DescriptionText> + <View + style={[a.w_full, a.align_center, {paddingTop: gtMobile ? 80 : 40}]}> + <AvatarCircle + openLibrary={openLibrary} + openCreator={creatorControl.open} + /> + + {error && ( + <View + style={[ + a.flex_row, + a.gap_sm, + a.align_center, + a.mt_xl, + a.py_md, + a.px_lg, + a.border, + a.rounded_md, + t.atoms.bg_contrast_25, + t.atoms.border_contrast_low, + ]}> + <CircleInfo_Stroke2_Corner0_Rounded size="sm" /> + <Text style={[a.leading_snug]}>{error}</Text> + </View> + )} + </View> + + <OnboardingControls.Portal> + <View style={[a.gap_md, gtMobile && {flexDirection: 'row-reverse'}]}> + <Button + variant="gradient" + color="gradient_sky" + size="large" + label={_(msg`Continue to next step`)} + onPress={onContinue}> + <ButtonText> + <Trans>Continue</Trans> + </ButtonText> + <ButtonIcon icon={ChevronRight} position="right" /> + </Button> + <Button + variant="ghost" + color="primary" + size="large" + label={_(msg`Open avatar creator`)} + onPress={onSecondaryPress}> + <ButtonText> + {avatar.useCreatedAvatar ? ( + <Trans>Upload a photo instead</Trans> + ) : ( + <Trans>Create an avatar instead</Trans> + )} + </ButtonText> + </Button> + </View> + </OnboardingControls.Portal> + </View> + + <Dialog.Outer control={creatorControl}> + <Dialog.Handle /> + <Dialog.Inner + label="Avatar creator" + style={[ + { + width: 'auto', + maxWidth: 410, + }, + ]}> + <View style={[a.align_center, {paddingTop: 20}]}> + <AvatarCreatorCircle avatar={avatar} /> + </View> + + <View style={[a.pt_3xl, a.gap_lg]}> + <AvatarCreatorItems + type="emojis" + avatar={avatar} + setAvatar={setAvatar} + /> + <AvatarCreatorItems + type="colors" + avatar={avatar} + setAvatar={setAvatar} + /> + </View> + <View style={[a.pt_4xl]}> + <Button + variant="solid" + color="primary" + size="large" + label={_(msg`Done`)} + onPress={onDoneCreating}> + <ButtonText> + <Trans>Done</Trans> + </ButtonText> + </Button> + </View> + </Dialog.Inner> + </Dialog.Outer> + + <PlaceholderCanvas ref={canvasRef} /> + </AvatarContext.Provider> ) } diff --git a/src/screens/Onboarding/StepProfile/types.ts b/src/screens/Onboarding/StepProfile/types.ts new file mode 100644 index 000000000..92a82f101 --- /dev/null +++ b/src/screens/Onboarding/StepProfile/types.ts @@ -0,0 +1,148 @@ +import {Alien_Stroke2_Corner0_Rounded as Alien} from '#/components/icons/Alien' +import {Apple_Stroke2_Corner0_Rounded as Apple} from '#/components/icons/Apple' +import {At_Stroke2_Corner0_Rounded as At} from '#/components/icons/At' +import {Atom_Stroke2_Corner0_Rounded as Atom} from '#/components/icons/Atom' +import {Celebrate_Stroke2_Corner0_Rounded as Celebrate} from '#/components/icons/Celebrate' +import {Coffee_Stroke2_Corner0_Rounded as Coffee} from '#/components/icons/Coffee' +import { + EmojiArc_Stroke2_Corner0_Rounded as EmojiArc, + EmojiHeartEyes_Stroke2_Corner0_Rounded as EmojiHeartEyes, +} from '#/components/icons/Emoji' +import {Explosion_Stroke2_Corner0_Rounded as Explosion} from '#/components/icons/Explosion' +import {GameController_Stroke2_Corner0_Rounded as GameController} from '#/components/icons/GameController' +import {Lab_Stroke2_Corner0_Rounded as Lab} from '#/components/icons/Lab' +import {Leaf_Stroke2_Corner0_Rounded as Leaf} from '#/components/icons/Leaf' +import {MusicNote_Stroke2_Corner0_Rounded as MusicNote} from '#/components/icons/MusicNote' +import {PiggyBank_Stroke2_Corner0_Rounded as PiggyBank} from '#/components/icons/PiggyBank' +import {Pizza_Stroke2_Corner0_Rounded as Pizza} from '#/components/icons/Pizza' +import {Poop_Stroke2_Corner0_Rounded as Poop} from '#/components/icons/Poop' +import {Rose_Stroke2_Corner0_Rounded as Rose} from '#/components/icons/Rose' +import {Shaka_Stroke2_Corner0_Rounded as Shaka} from '#/components/icons/Shaka' +import {UFO_Stroke2_Corner0_Rounded as UFO} from '#/components/icons/UFO' +import {Zap_Stroke2_Corner0_Rounded as Zap} from '#/components/icons/Zap' + +/** + * If you want to add or remove icons from the selection, just add the name to the `emojiNames` array and + * add the item to the `emojiItems` record.. + */ + +export const emojiNames = [ + 'at', + 'arc', + 'heartEyes', + 'alien', + 'apple', + 'atom', + 'celebrate', + 'coffee', + 'gameController', + 'leaf', + 'musicNote', + 'pizza', + 'rose', + 'shaka', + 'ufo', + 'zap', + 'explosion', + 'lab', + 'piggyBank', + 'poop', +] as const +export type EmojiName = (typeof emojiNames)[number] + +export interface Emoji { + name: EmojiName + component: typeof EmojiArc +} +export const emojiItems: Record<EmojiName, Emoji> = { + at: { + name: 'at', + component: At, + }, + arc: { + name: 'arc', + component: EmojiArc, + }, + heartEyes: { + name: 'heartEyes', + component: EmojiHeartEyes, + }, + alien: { + name: 'alien', + component: Alien, + }, + apple: { + name: 'apple', + component: Apple, + }, + atom: { + name: 'atom', + component: Atom, + }, + celebrate: { + name: 'celebrate', + component: Celebrate, + }, + coffee: { + name: 'coffee', + component: Coffee, + }, + gameController: { + name: 'gameController', + component: GameController, + }, + leaf: { + name: 'leaf', + component: Leaf, + }, + musicNote: { + name: 'musicNote', + component: MusicNote, + }, + pizza: { + name: 'pizza', + component: Pizza, + }, + rose: { + name: 'rose', + component: Rose, + }, + shaka: { + name: 'shaka', + component: Shaka, + }, + ufo: { + name: 'ufo', + component: UFO, + }, + zap: { + name: 'zap', + component: Zap, + }, + explosion: { + name: 'explosion', + component: Explosion, + }, + lab: { + name: 'lab', + component: Lab, + }, + piggyBank: { + name: 'piggyBank', + component: PiggyBank, + }, + poop: { + name: 'poop', + component: Poop, + }, +} + +export const avatarColors = [ + '#FE8311', + '#FED811', + '#73DF84', + '#1185FE', + '#EF75EA', + '#F55454', +] as const +export type AvatarColor = (typeof avatarColors)[number] diff --git a/src/screens/Onboarding/StepSuggestedAccounts/index.tsx b/src/screens/Onboarding/StepSuggestedAccounts/index.tsx index 7b2ad2b99..774f2d3b0 100644 --- a/src/screens/Onboarding/StepSuggestedAccounts/index.tsx +++ b/src/screens/Onboarding/StepSuggestedAccounts/index.tsx @@ -69,9 +69,9 @@ export function Inner({ export function StepSuggestedAccounts() { const {_} = useLingui() + const {gtMobile} = useBreakpoints() const {track} = useAnalytics() const {state, dispatch, interestsDisplayNames} = React.useContext(Context) - const {gtMobile} = useBreakpoints() const suggestedDids = React.useMemo(() => { return aggregateInterestItems( state.interestsStepResults.selectedInterests, diff --git a/src/screens/Onboarding/state.ts b/src/screens/Onboarding/state.ts index 9452fbbc7..3031dfbf4 100644 --- a/src/screens/Onboarding/state.ts +++ b/src/screens/Onboarding/state.ts @@ -13,6 +13,7 @@ export type OnboardingState = { | 'algoFeeds' | 'topicalFeeds' | 'moderation' + | 'profile' | 'finished' activeStepIndex: number @@ -30,6 +31,13 @@ export type OnboardingState = { feedUris: string[] } profileStepResults: { + image?: { + path: string + mime: string + size: number + width: number + height: number + } imageUri?: string imageMime?: string } @@ -64,6 +72,7 @@ export type OnboardingAction = } | { type: 'setProfileStepResults' + image?: OnboardingState['profileStepResults']['image'] imageUri: string imageMime: string } @@ -80,7 +89,7 @@ export type ApiResponseMap = { export const initialState: OnboardingState = { hasPrev: false, - totalSteps: 7, + totalSteps: 8, activeStep: 'interests', activeStepIndex: 1, @@ -102,6 +111,7 @@ export const initialState: OnboardingState = { feedUris: [], }, profileStepResults: { + image: undefined, imageUri: '', imageMime: '', }, @@ -168,8 +178,11 @@ export function reducer( next.activeStep = 'moderation' next.activeStepIndex = 6 } else if (s.activeStep === 'moderation') { - next.activeStep = 'finished' + next.activeStep = 'profile' next.activeStepIndex = 7 + } else if (s.activeStep === 'profile') { + next.activeStep = 'finished' + next.activeStepIndex = 8 } break } @@ -189,9 +202,12 @@ export function reducer( } else if (s.activeStep === 'moderation') { next.activeStep = 'topicalFeeds' next.activeStepIndex = 5 - } else if (s.activeStep === 'finished') { + } else if (s.activeStep === 'profile') { next.activeStep = 'moderation' next.activeStepIndex = 6 + } else if (s.activeStep === 'finished') { + next.activeStep = 'profile' + next.activeStepIndex = 7 } break } @@ -226,6 +242,14 @@ export function reducer( } break } + case 'setProfileStepResults': { + next.profileStepResults = { + image: a.image, + imageUri: a.imageUri, + imageMime: a.imageMime, + } + break + } } const state = { @@ -243,6 +267,7 @@ export function reducer( suggestedAccountsStepResults: state.suggestedAccountsStepResults, algoFeedsStepResults: state.algoFeedsStepResults, topicalFeedsStepResults: state.topicalFeedsStepResults, + profileStepResults: state.profileStepResults, }) if (s.activeStep !== state.activeStep) { @@ -276,6 +301,7 @@ export const initialStateReduced: OnboardingState = { feedUris: [], }, profileStepResults: { + image: undefined, imageUri: '', imageMime: '', }, @@ -330,6 +356,7 @@ export function reducerReduced( } case 'setProfileStepResults': { next.profileStepResults = { + image: a.image, imageUri: a.imageUri, imageMime: a.imageMime, } |