implement design changes

use row for buttons on web

use RNGH FlatList

random color at start

improve logic

update dialog for web

update dialog style on mobile

some more progress

create dialog

simplify context

start implementing design
This commit is contained in:
Hailey
2024-03-09 00:10:37 -08:00
parent 3d80b8befe
commit bcd519a4ad
11 changed files with 390 additions and 246 deletions
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M3 5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5Zm16 0H5v7.213l1.246-.932.044-.03a3 3 0 0 1 3.863.454c1.468 1.58 2.941 2.749 4.847 2.749 1.703 0 2.855-.555 4-1.618V5Zm0 10.357c-1.112.697-2.386 1.097-4 1.097-2.81 0-4.796-1.755-6.313-3.388a1 1 0 0 0-1.269-.164L5 14.712V19h14v-3.643ZM15 8a1 1 0 1 0 0 2 1 1 0 0 0 0-2Zm-3 1a3 3 0 1 1 6 0 3 3 0 0 1-6 0Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 513 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 21 21"><path fill="#000" fill-rule="evenodd" d="M13.586 1.5a2 2 0 0 1 2.828 0L19.5 4.586a2 2 0 0 1 0 2.828l-13 13A2 2 0 0 1 5.086 21H1a1 1 0 0 1-1-1v-4.086A2 2 0 0 1 .586 14.5l13-13ZM15 2.914l-13 13V19h3.086l13-13L15 2.914ZM11 20a1 1 0 0 1 1-1h7a1 1 0 1 1 0 2h-7a1 1 0 0 1-1-1Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 371 B

+7 -2
View File
@@ -184,12 +184,17 @@ export function Outer({
)
}
export function Inner({children, style}: DialogInnerProps) {
export function Inner({
children,
style,
noHorizontalPadding,
}: DialogInnerProps) {
const insets = useSafeAreaInsets()
return (
<BottomSheetView
style={[
a.p_xl,
a.py_xl,
!noHorizontalPadding && a.px_xl,
{
paddingTop: 40,
borderTopLeftRadius: 40,
+2
View File
@@ -54,9 +54,11 @@ export type DialogInnerProps =
label?: undefined
accessibilityLabelledBy: A11yProps['aria-labelledby']
accessibilityDescribedBy: string
noHorizontalPadding?: boolean
}>
| DialogInnerPropsBase<{
label: string
accessibilityLabelledBy?: undefined
accessibilityDescribedBy?: undefined
noHorizontalPadding?: boolean
}>
+5
View File
@@ -0,0 +1,5 @@
import {createSinglePathSVG} from './TEMPLATE'
export const Image_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M3 5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5Zm16 0H5v7.213l1.246-.932.044-.03a3 3 0 0 1 3.863.454c1.468 1.58 2.941 2.749 4.847 2.749 1.703 0 2.855-.555 4-1.618V5Zm0 10.357c-1.112.697-2.386 1.097-4 1.097-2.81 0-4.796-1.755-6.313-3.388a1 1 0 0 0-1.269-.164L5 14.712V19h14v-3.643ZM15 8a1 1 0 1 0 0 2 1 1 0 0 0 0-2Zm-3 1a3 3 0 1 1 6 0 3 3 0 0 1-6 0Z',
})
+5
View File
@@ -0,0 +1,5 @@
import {createSinglePathSVG} from './TEMPLATE'
export const Pencil_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M13.586 1.5a2 2 0 0 1 2.828 0L19.5 4.586a2 2 0 0 1 0 2.828l-13 13A2 2 0 0 1 5.086 21H1a1 1 0 0 1-1-1v-4.086A2 2 0 0 1 .586 14.5l13-13ZM15 2.914l-13 13V19h3.086l13-13L15 2.914ZM11 20a1 1 0 0 1 1-1h7a1 1 0 1 1 0 2h-7a1 1 0 0 1-1-1Z',
})
@@ -1,42 +1,40 @@
import React from 'react'
import {LayoutAnimation, Pressable, PressableProps, View} from 'react-native'
import {Image} from 'expo-image'
import {TimesLarge_Stroke2_Corner0_Rounded as Times} from '#/components/icons/Times'
import {Camera_Stroke2_Corner0_Rounded as Camera} from '#/components/icons/Camera'
import {useAvatar, useSetAvatar} from '#/screens/Onboarding/StepProfile/index'
import {atoms as a, native, useTheme, web} from '#/alf'
import {useAvatar} from '#/screens/Onboarding/StepProfile/index'
import {TouchableOpacity, TouchableOpacityProps, View} from 'react-native'
import {Pencil_Stroke2_Corner0_Rounded as Pencil} from '#/components/icons/Pencil'
import {Image_Stroke2_Corner0_Rounded as ImageIcon} from '#/components/icons/Image'
import {Image as ExpoImage} from 'expo-image'
import {AvatarCreatorCircle} from '#/screens/Onboarding/StepProfile/AvatarCreatorCircle'
import {atoms as a, useTheme} from '#/alf'
import {usePhotoLibraryPermission} from 'lib/hooks/usePermissions'
import {openPicker} from 'lib/media/picker.shared'
import {isNative, isWeb} from 'platform/detection'
import {openCropper} from 'lib/media/picker'
import {compressIfNeeded} from 'lib/media/manip'
export function AvatarBottomButton({...props}: PressableProps) {
function AvatarBottomButton({...props}: TouchableOpacityProps) {
const t = useTheme()
return (
<Pressable
<TouchableOpacity
{...props}
style={[
a.absolute,
a.rounded_full,
a.align_center,
a.justify_center,
t.name === 'light' ? t.atoms.bg_contrast_800 : t.atoms.bg_contrast_200,
{backgroundColor: t.palette.primary_500},
{height: 48, width: 48, bottom: 2, right: 2},
]}>
{props.children}
</Pressable>
</TouchableOpacity>
)
}
export function AvatarCircle() {
export function AvatarCircle({
openLibrary,
openCreator,
}: {
openLibrary: () => unknown
openCreator: () => unknown
}) {
const t = useTheme()
const avatar = useAvatar()
const setAvatar = useSetAvatar()
const Icon = avatar.placeholder.component
const {requestPhotoAccessIfNeeded} = usePhotoLibraryPermission()
const {avatar} = useAvatar()
const styles = React.useMemo(
() => ({
@@ -45,84 +43,42 @@ export function AvatarCircle() {
a.overflow_hidden,
a.align_center,
a.justify_center,
t.atoms.border_contrast_high,
t.atoms.border_contrast_low,
t.atoms.bg_contrast_25,
{
height: 150,
width: 150,
borderWidth: 2,
backgroundColor: avatar.backgroundColor,
height: 200,
width: 200,
},
web({borderWidth: 2}),
native({borderWidth: 1}),
],
}),
[avatar.backgroundColor, t.atoms.border_contrast_high],
[t.atoms.bg_contrast_25, t.atoms.border_contrast_low],
)
const onCameraPress = React.useCallback(async () => {
if (!(await requestPhotoAccessIfNeeded())) {
return
}
const items = await openPicker({
aspect: [1, 1],
})
let image = items[0]
if (!image) return
// TODO we need an alf modal for the cropper
if (!isWeb) {
image = await openCropper({
mediaType: 'photo',
cropperCircleOverlay: true,
height: image.height,
width: image.width,
path: image.path,
})
}
image = await compressIfNeeded(image, 1000000)
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
// 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 Image.prefetch(image.path)
}
setAvatar(prev => ({
...prev,
image,
}))
}, [requestPhotoAccessIfNeeded, setAvatar])
const onPressRemoveAvatar = React.useCallback(() => {
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
setAvatar(prev => ({
...prev,
image: undefined,
}))
}, [setAvatar])
if (avatar.image) {
return (
<View>
<Image
source={avatar.image.path}
style={styles.imageContainer}
accessibilityIgnoresInvertColors
/>
<AvatarBottomButton onPress={onPressRemoveAvatar}>
<Times size="lg" style={{color: t.palette.white}} />
</AvatarBottomButton>
</View>
)
}
return (
<View>
<View style={styles.imageContainer}>
<Icon height={85} width={85} style={{color: t.palette.white}} />
</View>
<AvatarBottomButton onPress={onCameraPress}>
<Camera size="xl" style={{color: t.palette.white}} />
{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}>
<ImageIcon
height={100}
width={100}
style={{color: t.palette.contrast_200}}
/>
</View>
)}
<AvatarBottomButton
onPress={avatar.useCreatedAvatar ? openCreator : openLibrary}>
<Pencil size="md" style={{color: t.palette.white}} />
</AvatarBottomButton>
</View>
)
@@ -0,0 +1,44 @@
import React from 'react'
import {View} from 'react-native'
import {Avatar} from '#/screens/Onboarding/StepProfile/index'
import {atoms as a, native, useTheme, web} 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,
t.atoms.border_contrast_high,
{
height: size,
width: size,
backgroundColor: avatar.backgroundColor,
},
web({borderWidth: 2}),
native({borderWidth: 1}),
],
}),
[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>
)
}
@@ -1,13 +1,11 @@
import React from 'react'
import {FlatList, ListRenderItemInfo, Pressable, View} from 'react-native'
import Animated, {
useAnimatedStyle,
useSharedValue,
withTiming,
} from 'react-native-reanimated'
import {ListRenderItemInfo, Pressable, View} from 'react-native'
// Using the FlatList from RNGH allows us to nest the scroll views on Android. The default FlatList won't allow
// scrolling inside of the vertical ScrollView
import {FlatList} from 'react-native-gesture-handler'
import {atoms as a, useTheme} from '#/alf'
import {useAvatar, useSetAvatar} from '#/screens/Onboarding/StepProfile/index'
import {atoms as a, native, useBreakpoints, useTheme, web} from '#/alf'
import {Avatar} from '#/screens/Onboarding/StepProfile/index'
import {
AvatarColor,
avatarColors,
@@ -15,75 +13,44 @@ import {
EmojiName,
emojiNames,
} from '#/screens/Onboarding/StepProfile/types'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
const WITH_TIMING_CONFIG = {duration: 150}
function AnimatedCircle({
selected,
children,
}: React.PropsWithChildren<{selected: boolean}>) {
function Circle({children}: React.PropsWithChildren<{}>) {
const t = useTheme()
const {isTabletOrDesktop} = useWebMediaQueries()
const size = useSharedValue(selected ? 1.2 : 1)
const styles = React.useMemo(
() => ({
imageContainer: [
container: [
a.rounded_full,
a.overflow_hidden,
a.align_center,
a.justify_center,
{height: 150, width: 150, borderWidth: 2},
t.atoms.bg_contrast_25,
web({borderWidth: 2}),
native({borderWidth: 1}),
{
height: 60,
width: 60,
margin: 4,
},
],
paletteContainer: {
height: 70,
width: 70,
margin: isTabletOrDesktop ? 8 : 2,
},
}),
[isTabletOrDesktop],
[t.atoms.bg_contrast_25],
)
React.useEffect(() => {
if (selected && size.value !== 1.2) {
size.value = withTiming(1.2, WITH_TIMING_CONFIG)
} else if (!selected && size.value !== 1) {
size.value = withTiming(1, WITH_TIMING_CONFIG)
}
}, [selected, size])
// On mobile we want to expand the height/width of the container so the items around it get moved as well. On
// desktop, we don't want anything around the item to move so we just increase the scale.
const animatedStyle = useAnimatedStyle(() => {
if (isTabletOrDesktop) {
return {
transform: [{scale: size.value}],
}
}
return {
height: 70 * size.value,
width: 70 * size.value,
}
})
return (
<Animated.View
style={[
styles.imageContainer,
styles.paletteContainer,
t.atoms.border_contrast_high,
animatedStyle,
]}>
<View style={[styles.container, t.atoms.border_contrast_high]}>
{children}
</Animated.View>
</View>
)
}
function ColorItem({color}: {color: AvatarColor}) {
const avatar = useAvatar()
const setAvatar = useSetAvatar()
function ColorItem({
color,
setAvatar,
}: {
color: AvatarColor
setAvatar: React.Dispatch<React.SetStateAction<Avatar>>
}) {
const onPress = React.useCallback(() => {
setAvatar(prev => ({
...prev,
@@ -92,20 +59,26 @@ function ColorItem({color}: {color: AvatarColor}) {
}, [color, setAvatar])
return (
<AnimatedCircle selected={avatar.backgroundColor === color}>
<Circle>
<Pressable
accessibilityRole="button"
style={[a.h_full, a.w_full, {backgroundColor: color}]}
onPress={onPress}
/>
</AnimatedCircle>
</Circle>
)
}
function EmojiItem({emojiName}: {emojiName: EmojiName}) {
function EmojiItem({
emojiName,
avatar,
setAvatar,
}: {
emojiName: EmojiName
avatar: Avatar
setAvatar: React.Dispatch<React.SetStateAction<Avatar>>
}) {
const t = useTheme()
const avatar = useAvatar()
const setAvatar = useSetAvatar()
const Icon = React.useMemo(() => emojiItems[emojiName].component, [emojiName])
const onPress = React.useCallback(() => {
@@ -115,60 +88,87 @@ function EmojiItem({emojiName}: {emojiName: EmojiName}) {
}))
}, [emojiName, setAvatar])
const selected = React.useMemo(
() => avatar.placeholder.name === emojiName,
[avatar.placeholder.name, emojiName],
)
return (
<AnimatedCircle selected={avatar.placeholder.name === emojiName}>
<Circle>
<Pressable
accessibilityRole="button"
style={[a.flex_1, a.justify_center, a.align_center]}
onPress={onPress}>
<Icon style={[t.atoms.text_contrast_medium]} height={40} width={40} />
<Icon
style={selected ? t.atoms.text : t.atoms.text_contrast_low}
height={30}
width={30}
/>
</Pressable>
</AnimatedCircle>
</Circle>
)
}
function colorRenderItem({item}: ListRenderItemInfo<AvatarColor>) {
return <ColorItem color={item} />
}
function emojiRenderItem({item}: ListRenderItemInfo<EmojiName>) {
return <EmojiItem emojiName={item} />
}
export function AvatarCreatorItems({type}: {type: 'emojis' | 'colors'}) {
const {isTabletOrDesktop} = useWebMediaQueries()
export function AvatarCreatorItems({
type,
avatar,
setAvatar,
}: {
type: 'emojis' | 'colors'
avatar: Avatar
setAvatar: React.Dispatch<React.SetStateAction<Avatar>>
}) {
const {gtMobile} = useBreakpoints()
const styles = React.useMemo(
() => ({
flatListOuter: isTabletOrDesktop
flatListOuter: gtMobile
? {
height: 435,
height: 338,
}
: [a.flex_row, a.align_center, {height: 100}],
: [a.flex_row, a.align_center, {height: 70}],
}),
[isTabletOrDesktop],
[gtMobile],
)
const colorRenderItem = React.useCallback(
({item}: ListRenderItemInfo<AvatarColor>) => {
return <ColorItem color={item} setAvatar={setAvatar} />
},
[setAvatar],
)
const emojiRenderItem = React.useCallback(
({item}: ListRenderItemInfo<EmojiName>) => {
return (
<EmojiItem emojiName={item} avatar={avatar} setAvatar={setAvatar} />
)
},
[avatar, setAvatar],
)
return (
<View
style={[
styles.flatListOuter,
isTabletOrDesktop && type === 'colors' && {width: 125},
gtMobile && type === 'colors' && {width: 125},
]}>
<FlatList<any>
// Changing the value of numColumns on the fly isn't supported, so we want the flatlist to re-render whenever
// the size of the screen changes. Should only happen when `isTabletOrDesktop` changes.
key={isTabletOrDesktop ? 0 : 1}
key={gtMobile ? 0 : 1}
data={type === 'colors' ? avatarColors : emojiNames}
renderItem={type === 'colors' ? colorRenderItem : emojiRenderItem}
style={[isTabletOrDesktop && {marginHorizontal: 10}]}
keyExtractor={item => item}
style={[gtMobile && {marginHorizontal: 10}]}
contentContainerStyle={[
a.align_center,
isTabletOrDesktop && type === 'colors' && a.pr_xs,
!isTabletOrDesktop && {paddingHorizontal: 40},
gtMobile && type === 'colors' && a.pr_xs,
!gtMobile && {paddingHorizontal: 40},
]}
numColumns={isTabletOrDesktop && type === 'emojis' ? 4 : undefined}
showsHorizontalScrollIndicator={isTabletOrDesktop && type === 'colors'}
horizontal={!isTabletOrDesktop}
numColumns={gtMobile && type === 'emojis' ? 4 : undefined}
showsHorizontalScrollIndicator={gtMobile && type === 'colors'}
horizontal={!gtMobile}
/>
</View>
)
@@ -15,7 +15,7 @@ export interface PlaceholderCanvasRef {
// "screenshot".
export const PlaceholderCanvas = React.forwardRef<PlaceholderCanvasRef, {}>(
function PlaceholderCanvas({}, ref) {
const avatar = useAvatar()
const {avatar} = useAvatar()
const viewshotRef = React.useRef()
const Icon = avatar.placeholder.component
+189 -64
View File
@@ -11,8 +11,6 @@ import {
Description,
OnboardingControls,
} from '#/screens/Onboarding/Layout'
import {AvatarCreatorItems} from '#/screens/Onboarding/StepProfile/AvatarCreatorItems'
import {AvatarCircle} from '#/screens/Onboarding/StepProfile/AvatarCircle'
import {Emoji, emojiItems, AvatarColor, avatarColors} from './types'
import {
PlaceholderCanvas,
@@ -21,10 +19,19 @@ import {
import {Button, ButtonText, ButtonIcon} from '#/components/Button'
import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron'
import {IconCircle} from '#/components/IconCircle'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {useAnalytics} from '#/lib/analytics/analytics'
import {AvatarCircle} from '#/screens/Onboarding/StepProfile/AvatarCircle'
import * as Dialog from '#/components/Dialog'
import {AvatarCreatorItems} from '#/screens/Onboarding/StepProfile/AvatarCreatorItems'
import {AvatarCreatorCircle} from '#/screens/Onboarding/StepProfile/AvatarCreatorCircle'
import {openPicker} from 'lib/media/picker.shared'
import {isNative, isWeb} from 'platform/detection'
import {openCropper} from 'lib/media/picker'
import {compressIfNeeded} from 'lib/media/manip'
import {Image as ExpoImage} from 'expo-image/build/Image'
import {usePhotoLibraryPermission} from 'lib/hooks/usePermissions'
interface Avatar {
export interface Avatar {
image?: {
path: string
mime: string
@@ -34,25 +41,33 @@ interface Avatar {
}
backgroundColor: AvatarColor
placeholder: Emoji
useCreatedAvatar: boolean
}
const AvatarContext = React.createContext<Avatar>({} as Avatar)
const SetAvatarContext = React.createContext<
React.Dispatch<React.SetStateAction<Avatar>>
>({} as React.Dispatch<React.SetStateAction<Avatar>>)
interface IAvatarContext {
avatar: Avatar
setAvatar: React.Dispatch<React.SetStateAction<Avatar>>
}
const AvatarContext = React.createContext<IAvatarContext>({} as IAvatarContext)
export const useAvatar = () => React.useContext(AvatarContext)
export const useSetAvatar = () => React.useContext(SetAvatarContext)
const randomColor =
avatarColors[Math.floor(Math.random() * avatarColors.length)]
export function StepProfile() {
const {_} = useLingui()
const t = useTheme()
const {isTabletOrDesktop} = useWebMediaQueries()
const {gtMobile} = useBreakpoints()
const {track} = useAnalytics()
const {requestPhotoAccessIfNeeded} = usePhotoLibraryPermission()
const creatorControl = Dialog.useDialogControl()
const {state, dispatch} = React.useContext(Context)
const [avatar, setAvatar] = React.useState<Avatar>({
placeholder: emojiItems.at,
backgroundColor: avatarColors[0],
backgroundColor: randomColor,
useCreatedAvatar: false,
})
const canvasRef = React.useRef<PlaceholderCanvasRef>(null)
@@ -63,7 +78,7 @@ export function StepProfile() {
const onContinue = React.useCallback(async () => {
let imageUri = avatar?.image?.path
if (!imageUri) {
if (!imageUri || avatar.useCreatedAvatar) {
imageUri = await canvasRef.current?.capture()
}
@@ -77,61 +92,171 @@ export function StepProfile() {
dispatch({type: 'next'})
track('OnboardingV2:StepProfile:End')
}, [avatar?.image, dispatch, track])
}, [
avatar.image?.mime,
avatar.image?.path,
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
}
const items = await openPicker({
aspect: [1, 1],
})
let image = items[0]
if (!image) return
// TODO we need an alf modal for the cropper
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])
const onSecondaryPress = React.useCallback(() => {
if (avatar.useCreatedAvatar) {
openLibrary()
} else {
creatorControl.open()
}
}, [avatar.useCreatedAvatar, creatorControl, openLibrary])
const value = React.useMemo(
() => ({
avatar,
setAvatar,
}),
[avatar],
)
return (
<SetAvatarContext.Provider value={setAvatar}>
<AvatarContext.Provider value={avatar}>
<>
<View style={[a.align_start, t.atoms.bg]}>
<View style={[gtMobile ? a.px_5xl : a.px_xl]}>
<IconCircle icon={StreamingLive} style={[a.mb_2xl]} />
<AvatarContext.Provider value={value}>
<View style={[a.align_start, t.atoms.bg, a.justify_between]}>
<View style={[gtMobile ? a.px_5xl : a.px_xl]}>
<IconCircle icon={StreamingLive} style={[a.mb_2xl]} />
<Title>
<Trans>Give your profile a face</Trans>
</Title>
<Description>
<Trans>
Help people know you're not a bot by uploading a picture or
creating an avatar.
</Trans>
</Description>
</View>
<View style={[a.w_full, a.align_center, {paddingTop: 80}]}>
<AvatarCircle
openLibrary={openLibrary}
openCreator={creatorControl.open}
/>
</View>
<View style={[a.w_full, a.px_2xl, a.pt_5xl]} />
<Title>
<Trans>Set your profile picture</Trans>
</Title>
<Description>
<Trans>
Help people know you're not a bot by uploading a picture or
creating an avatar!
</Trans>
</Description>
</View>
<View style={[a.w_full, a.pt_5xl]}>
<View style={[a.align_center, a.pb_5xl]}>
<AvatarCircle />
</View>
{!avatar.image && (
<View
style={
isTabletOrDesktop
? [a.flex_row, a.justify_between]
: undefined
}>
<AvatarCreatorItems type="emojis" />
<AvatarCreatorItems type="colors" />
</View>
)}
</View>
<OnboardingControls.Portal>
<Button
key={state.activeStep} // remove focus state on nav
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>
<OnboardingControls.Portal>
<View style={[a.gap_md, gtMobile && {flexDirection: 'row-reverse'}]}>
<Button
key={state.activeStep} // remove focus state on nav
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
key={state.activeStep} // remove focus state on nav
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>
<PlaceholderCanvas ref={canvasRef} />
</>
</AvatarContext.Provider>
</SetAvatarContext.Provider>
</OnboardingControls.Portal>
</View>
<Dialog.Outer control={creatorControl}>
<Dialog.Handle />
<Dialog.Inner label="Avatar creator" noHorizontalPadding>
<View style={[a.align_center, {paddingTop: 20}]}>
<AvatarCreatorCircle avatar={avatar} />
</View>
<View
style={[
a.pt_2xl,
a.align_center,
a.justify_center,
gtMobile && a.flex_row,
]}>
<AvatarCreatorItems
type="emojis"
avatar={avatar}
setAvatar={setAvatar}
/>
<AvatarCreatorItems
type="colors"
avatar={avatar}
setAvatar={setAvatar}
/>
</View>
<View style={[a.px_xl, a.pt_4xl]}>
<Button
key={state.activeStep} // remove focus state on nav
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>
)
}