From ad50fe2b72bfcea7a5dfc4b561b6cf2b660191c0 Mon Sep 17 00:00:00 2001 From: Hailey Date: Wed, 14 Feb 2024 23:03:37 -0800 Subject: [PATCH] organize --- .../Onboarding/StepProfile/AvatarCircle.tsx | 135 ++++++++++ .../StepProfile/AvatarCreatorItems.tsx | 187 +++++++++++++ .../StepProfile/SelectImageButton.tsx | 91 ------- src/screens/Onboarding/StepProfile/index.tsx | 251 +----------------- 4 files changed, 328 insertions(+), 336 deletions(-) create mode 100644 src/screens/Onboarding/StepProfile/AvatarCircle.tsx create mode 100644 src/screens/Onboarding/StepProfile/AvatarCreatorItems.tsx delete mode 100644 src/screens/Onboarding/StepProfile/SelectImageButton.tsx 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> = {}