diff --git a/src/screens/Onboarding/StepProfile/AvatarCircle.tsx b/src/screens/Onboarding/StepProfile/AvatarCircle.tsx
new file mode 100644
index 0000000000..b4478e600c
--- /dev/null
+++ b/src/screens/Onboarding/StepProfile/AvatarCircle.tsx
@@ -0,0 +1,135 @@
+import React from 'react'
+import {
+ LayoutAnimation,
+ Pressable,
+ PressableProps,
+ StyleSheet,
+ View,
+} from 'react-native'
+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'
+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'
+
+export function AvatarBottomButton({...props}: PressableProps) {
+ const t = useTheme()
+
+ return (
+
+ {props.children}
+
+ )
+}
+
+export function AvatarCircle() {
+ const t = useTheme()
+ const avatar = useAvatar()
+ const setAvatar = useSetAvatar()
+ const Icon = avatar.placeholder.component
+ const {requestPhotoAccessIfNeeded} = usePhotoLibraryPermission()
+
+ 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 (
+
+
+
+
+
+
+ )
+ }
+
+ return (
+
+
+
+
+
+
+
+
+ )
+}
+
+const styles = StyleSheet.create({
+ imageContainer: {
+ borderRadius: 100,
+ height: 150,
+ width: 150,
+ overflow: 'hidden',
+ borderWidth: 2,
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+})
diff --git a/src/screens/Onboarding/StepProfile/AvatarCreatorItems.tsx b/src/screens/Onboarding/StepProfile/AvatarCreatorItems.tsx
new file mode 100644
index 0000000000..8eb3df9bc7
--- /dev/null
+++ b/src/screens/Onboarding/StepProfile/AvatarCreatorItems.tsx
@@ -0,0 +1,187 @@
+import React from 'react'
+import {atoms as a, useTheme} from '#/alf'
+import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
+import Animated, {
+ useAnimatedStyle,
+ useSharedValue,
+ withTiming,
+} from 'react-native-reanimated'
+import {
+ AvatarColor,
+ avatarColors,
+ emojiItems,
+ EmojiName,
+ emojiNames,
+} from '#/screens/Onboarding/StepProfile/types'
+import {
+ FlatList,
+ FlatListProps,
+ ListRenderItemInfo,
+ Pressable,
+ StyleSheet,
+ View,
+} from 'react-native'
+import {useAvatar, useSetAvatar} from '#/screens/Onboarding/StepProfile/index'
+
+const WITH_TIMING_CONFIG = {duration: 150}
+
+function AnimatedCircle({
+ selected,
+ children,
+}: React.PropsWithChildren<{selected: boolean}>) {
+ const t = useTheme()
+ const {isTabletOrDesktop} = useWebMediaQueries()
+ const styles = useStyles()
+ const size = useSharedValue(selected ? 1.2 : 1)
+
+ 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 (
+
+ {children}
+
+ )
+}
+
+function ColorItem({color}: {color: AvatarColor}) {
+ const avatar = useAvatar()
+ const setAvatar = useSetAvatar()
+
+ const onPress = React.useCallback(() => {
+ setAvatar(prev => ({
+ ...prev,
+ backgroundColor: color,
+ }))
+ }, [color, setAvatar])
+
+ return (
+
+
+
+ )
+}
+
+function EmojiItem({emojiName}: {emojiName: EmojiName}) {
+ const t = useTheme()
+ const avatar = useAvatar()
+ const setAvatar = useSetAvatar()
+ const Icon = React.useMemo(() => emojiItems[emojiName].component, [emojiName])
+
+ const onPress = React.useCallback(() => {
+ setAvatar(prev => ({
+ ...prev,
+ placeholder: emojiItems[emojiName],
+ }))
+ }, [emojiName, setAvatar])
+
+ return (
+
+
+
+
+
+ )
+}
+
+function colorRenderItem({item}: ListRenderItemInfo) {
+ return
+}
+function emojiRenderItem({item}: ListRenderItemInfo) {
+ return
+}
+
+export function AvatarCreatorItems({type}: {type: 'emojis' | 'colors'}) {
+ const {isTabletOrDesktop} = useWebMediaQueries()
+ const styles = useStyles()
+
+ return (
+
+
+
+ )
+}
+
+const useStyles = () => {
+ const {isTabletOrDesktop} = useWebMediaQueries()
+
+ return StyleSheet.create({
+ imageContainer: {
+ borderRadius: 100,
+ height: 150,
+ width: 150,
+ overflow: 'hidden',
+ borderWidth: 2,
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ paletteContainer: {
+ height: 70,
+ width: 70,
+ margin: isTabletOrDesktop ? 8 : 2,
+ },
+ flatListOuter: isTabletOrDesktop
+ ? {
+ height: 435,
+ }
+ : {
+ flexDirection: 'row',
+ alignItems: 'center',
+ height: 100,
+ },
+ flatListContainer: {
+ paddingHorizontal: 40,
+ },
+ })
+}
+
+const commonFlatListProps: Partial> = {}
diff --git a/src/screens/Onboarding/StepProfile/SelectImageButton.tsx b/src/screens/Onboarding/StepProfile/SelectImageButton.tsx
deleted file mode 100644
index 8923242123..0000000000
--- a/src/screens/Onboarding/StepProfile/SelectImageButton.tsx
+++ /dev/null
@@ -1,91 +0,0 @@
-import React from 'react'
-import {LayoutAnimation, Pressable, StyleSheet} from 'react-native'
-import {Image} from 'expo-image'
-import {Camera_Stroke2_Corner0_Rounded as Camera} from '#/components/icons/Camera'
-import {useSetAvatar} from '#/screens/Onboarding/StepProfile/index'
-import {atoms as a, useTheme} from '#/alf'
-import {usePhotoLibraryPermission} from 'lib/hooks/usePermissions'
-import {openPicker} from 'lib/media/picker.shared'
-import {openCropper} from 'lib/media/picker'
-import {compressIfNeeded} from 'lib/media/manip'
-import {isNative, isWeb} from 'platform/detection'
-import Animated from 'react-native-reanimated'
-import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
-
-export function SelectImageButton() {
- const t = useTheme()
- const {isTabletOrDesktop} = useWebMediaQueries()
- const setAvatar = useSetAvatar()
- const {requestPhotoAccessIfNeeded} = usePhotoLibraryPermission()
-
- 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])
-
- return (
-
-
-
-
-
- )
-}
-
-const styles = StyleSheet.create({
- imageContainer: {
- borderRadius: 100,
- borderWidth: 2,
- height: 150,
- width: 150,
- overflow: 'hidden',
- alignItems: 'center',
- justifyContent: 'center',
- },
- paletteContainer: {
- height: 70,
- width: 70,
- },
-})
diff --git a/src/screens/Onboarding/StepProfile/index.tsx b/src/screens/Onboarding/StepProfile/index.tsx
index b3875a245d..14deab35c9 100644
--- a/src/screens/Onboarding/StepProfile/index.tsx
+++ b/src/screens/Onboarding/StepProfile/index.tsx
@@ -1,13 +1,5 @@
import React from 'react'
-import {
- FlatList,
- FlatListProps,
- LayoutAnimation,
- ListRenderItemInfo,
- Pressable,
- StyleSheet,
- View,
-} from 'react-native'
+import {View} from 'react-native'
import {useLingui} from '@lingui/react'
import {msg, Trans} from '@lingui/macro'
@@ -23,27 +15,15 @@ import {
OnboardingControls,
} from '#/screens/Onboarding/Layout'
import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron'
-import {TimesLarge_Stroke2_Corner0_Rounded as Times} from '#/components/icons/Times'
import {IconCircle} from '#/components/IconCircle'
-import {Image} from 'expo-image'
-import {
- Emoji,
- EmojiName,
- emojiItems,
- emojiNames,
- AvatarColor,
- avatarColors,
-} from './types'
+import {Emoji, emojiItems, AvatarColor, avatarColors} from './types'
import {
PlaceholderCanvas,
PlaceholderCanvasRef,
} from '#/screens/Onboarding/StepProfile/PlaceholderCanvas'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
-import Animated, {
- useAnimatedStyle,
- useSharedValue,
- withTiming,
-} from 'react-native-reanimated'
+import {AvatarCircle} from '#/screens/Onboarding/StepProfile/AvatarCircle'
+import {AvatarCreatorItems} from '#/screens/Onboarding/StepProfile/AvatarCreatorItems'
interface Avatar {
image?: {
@@ -57,8 +37,6 @@ interface Avatar {
placeholder: Emoji
}
-const WITH_TIMING_CONFIG = {duration: 100}
-
const AvatarContext = React.createContext({} as Avatar)
const SetAvatarContext = React.createContext<
React.Dispatch>
@@ -132,8 +110,8 @@ export function StepProfile() {
? [a.flex_row, a.justify_between]
: undefined
}>
-
-
+
+
)}
@@ -159,220 +137,3 @@ export function StepProfile() {
)
}
-
-function AvatarCircle() {
- const t = useTheme()
- const styles = useStyles()
- const avatar = useAvatar()
- const setAvatar = useSetAvatar()
- const Icon = avatar.placeholder.component
-
- const onPressRemoveAvatar = React.useCallback(() => {
- LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
- setAvatar(prev => ({
- ...prev,
- image: undefined,
- }))
- }, [setAvatar])
-
- if (avatar.image) {
- return (
-
-
-
-
-
-
- )
- }
-
- return (
-
-
-
- )
-}
-
-function AnimatedCircle({
- selected,
- children,
-}: React.PropsWithChildren<{selected: boolean}>) {
- const t = useTheme()
- const {isTabletOrDesktop} = useWebMediaQueries()
- const styles = useStyles()
- const size = useSharedValue(selected ? 1.2 : 1)
-
- 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 (
-
- {children}
-
- )
-}
-
-function ColorItem({color}: {color: AvatarColor}) {
- const avatar = useAvatar()
- const setAvatar = useSetAvatar()
-
- const onPress = React.useCallback(() => {
- setAvatar(prev => ({
- ...prev,
- backgroundColor: color,
- }))
- }, [color, setAvatar])
-
- return (
-
-
-
- )
-}
-function colorRenderItem({item}: ListRenderItemInfo) {
- return
-}
-
-function EmojiItem({emojiName}: {emojiName: EmojiName}) {
- const t = useTheme()
- const avatar = useAvatar()
- const setAvatar = useSetAvatar()
- const Icon = React.useMemo(() => emojiItems[emojiName].component, [emojiName])
-
- const onPress = React.useCallback(() => {
- setAvatar(prev => ({
- ...prev,
- placeholder: emojiItems[emojiName],
- }))
- }, [emojiName, setAvatar])
-
- return (
-
-
-
-
-
- )
-}
-function emojiRenderItem({item}: ListRenderItemInfo) {
- return
-}
-
-function Items({type}: {type: 'emojis' | 'colors'}) {
- const {isTabletOrDesktop} = useWebMediaQueries()
- const styles = useStyles()
-
- return (
-
-
-
- )
-}
-
-const useStyles = () => {
- const {isTabletOrDesktop} = useWebMediaQueries()
-
- return StyleSheet.create({
- imageContainer: {
- borderRadius: 100,
- height: 150,
- width: 150,
- overflow: 'hidden',
- borderWidth: 2,
- alignItems: 'center',
- justifyContent: 'center',
- },
- paletteContainer: {
- height: 70,
- width: 70,
- margin: isTabletOrDesktop ? 8 : 2,
- },
- flatListOuter: isTabletOrDesktop
- ? {
- height: 435,
- }
- : {
- flexDirection: 'row',
- alignItems: 'center',
- height: 100,
- },
- flatListContainer: {
- paddingHorizontal: 40,
- },
- })
-}
-
-const commonFlatListProps: Partial> = {}