organize
This commit is contained in:
@@ -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 (
|
||||||
|
<Pressable
|
||||||
|
{...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,
|
||||||
|
{height: 48, width: 48, bottom: 2, right: 2},
|
||||||
|
]}>
|
||||||
|
{props.children}
|
||||||
|
</Pressable>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<View>
|
||||||
|
<Image
|
||||||
|
source={avatar.image.path}
|
||||||
|
style={[styles.imageContainer, t.atoms.border_contrast_high]}
|
||||||
|
accessibilityIgnoresInvertColors
|
||||||
|
/>
|
||||||
|
<AvatarBottomButton onPress={onPressRemoveAvatar}>
|
||||||
|
<Times size="lg" style={{color: t.palette.white}} />
|
||||||
|
</AvatarBottomButton>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
styles.imageContainer,
|
||||||
|
t.atoms.border_contrast_high,
|
||||||
|
{
|
||||||
|
backgroundColor: avatar.backgroundColor,
|
||||||
|
},
|
||||||
|
]}>
|
||||||
|
<Icon height={85} width={85} style={{color: t.palette.white}} />
|
||||||
|
</View>
|
||||||
|
<AvatarBottomButton onPress={onCameraPress}>
|
||||||
|
<Camera size="xl" style={{color: t.palette.white}} />
|
||||||
|
</AvatarBottomButton>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
imageContainer: {
|
||||||
|
borderRadius: 100,
|
||||||
|
height: 150,
|
||||||
|
width: 150,
|
||||||
|
overflow: 'hidden',
|
||||||
|
borderWidth: 2,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -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 (
|
||||||
|
<Animated.View
|
||||||
|
style={[
|
||||||
|
styles.imageContainer,
|
||||||
|
styles.paletteContainer,
|
||||||
|
t.atoms.border_contrast_high,
|
||||||
|
animatedStyle,
|
||||||
|
]}>
|
||||||
|
{children}
|
||||||
|
</Animated.View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ColorItem({color}: {color: AvatarColor}) {
|
||||||
|
const avatar = useAvatar()
|
||||||
|
const setAvatar = useSetAvatar()
|
||||||
|
|
||||||
|
const onPress = React.useCallback(() => {
|
||||||
|
setAvatar(prev => ({
|
||||||
|
...prev,
|
||||||
|
backgroundColor: color,
|
||||||
|
}))
|
||||||
|
}, [color, setAvatar])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AnimatedCircle selected={avatar.backgroundColor === color}>
|
||||||
|
<Pressable
|
||||||
|
accessibilityRole="button"
|
||||||
|
style={[a.h_full, a.w_full, {backgroundColor: color}]}
|
||||||
|
onPress={onPress}
|
||||||
|
/>
|
||||||
|
</AnimatedCircle>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<AnimatedCircle selected={avatar.placeholder.name === emojiName}>
|
||||||
|
<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} />
|
||||||
|
</Pressable>
|
||||||
|
</AnimatedCircle>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
const styles = useStyles()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.flatListOuter}>
|
||||||
|
<FlatList
|
||||||
|
// 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
|
||||||
|
key={isTabletOrDesktop ? 0 : 1}
|
||||||
|
data={type === 'colors' ? avatarColors : emojiNames}
|
||||||
|
renderItem={type === 'colors' ? colorRenderItem : emojiRenderItem}
|
||||||
|
style={[isTabletOrDesktop && {marginHorizontal: 10}]}
|
||||||
|
contentContainerStyle={[
|
||||||
|
a.align_center,
|
||||||
|
{height: 100},
|
||||||
|
!isTabletOrDesktop && styles.flatListContainer,
|
||||||
|
isTabletOrDesktop && type === 'colors' && a.pr_xs,
|
||||||
|
]}
|
||||||
|
numColumns={isTabletOrDesktop && type === 'emojis' ? 4 : undefined}
|
||||||
|
showsHorizontalScrollIndicator={isTabletOrDesktop && type === 'colors'}
|
||||||
|
horizontal={!isTabletOrDesktop}
|
||||||
|
{...commonFlatListProps}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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<FlatListProps<any>> = {}
|
||||||
@@ -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 (
|
|
||||||
<Animated.View
|
|
||||||
style={[
|
|
||||||
styles.imageContainer,
|
|
||||||
styles.paletteContainer,
|
|
||||||
a.mr_2xl,
|
|
||||||
t.atoms.border_contrast_high,
|
|
||||||
{height: 70, width: 70, marginRight: isTabletOrDesktop ? 0 : 5},
|
|
||||||
]}>
|
|
||||||
<Pressable
|
|
||||||
accessibilityRole="button"
|
|
||||||
onPress={onCameraPress}
|
|
||||||
style={[a.flex_1, a.align_center, a.justify_center]}>
|
|
||||||
<Camera height={40} width={40} style={[t.atoms.text_contrast_medium]} />
|
|
||||||
</Pressable>
|
|
||||||
</Animated.View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
imageContainer: {
|
|
||||||
borderRadius: 100,
|
|
||||||
borderWidth: 2,
|
|
||||||
height: 150,
|
|
||||||
width: 150,
|
|
||||||
overflow: 'hidden',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
},
|
|
||||||
paletteContainer: {
|
|
||||||
height: 70,
|
|
||||||
width: 70,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
@@ -1,13 +1,5 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {
|
import {View} from 'react-native'
|
||||||
FlatList,
|
|
||||||
FlatListProps,
|
|
||||||
LayoutAnimation,
|
|
||||||
ListRenderItemInfo,
|
|
||||||
Pressable,
|
|
||||||
StyleSheet,
|
|
||||||
View,
|
|
||||||
} from 'react-native'
|
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
import {msg, Trans} from '@lingui/macro'
|
import {msg, Trans} from '@lingui/macro'
|
||||||
|
|
||||||
@@ -23,27 +15,15 @@ import {
|
|||||||
OnboardingControls,
|
OnboardingControls,
|
||||||
} from '#/screens/Onboarding/Layout'
|
} from '#/screens/Onboarding/Layout'
|
||||||
import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron'
|
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 {IconCircle} from '#/components/IconCircle'
|
||||||
import {Image} from 'expo-image'
|
import {Emoji, emojiItems, AvatarColor, avatarColors} from './types'
|
||||||
import {
|
|
||||||
Emoji,
|
|
||||||
EmojiName,
|
|
||||||
emojiItems,
|
|
||||||
emojiNames,
|
|
||||||
AvatarColor,
|
|
||||||
avatarColors,
|
|
||||||
} from './types'
|
|
||||||
import {
|
import {
|
||||||
PlaceholderCanvas,
|
PlaceholderCanvas,
|
||||||
PlaceholderCanvasRef,
|
PlaceholderCanvasRef,
|
||||||
} from '#/screens/Onboarding/StepProfile/PlaceholderCanvas'
|
} from '#/screens/Onboarding/StepProfile/PlaceholderCanvas'
|
||||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||||
import Animated, {
|
import {AvatarCircle} from '#/screens/Onboarding/StepProfile/AvatarCircle'
|
||||||
useAnimatedStyle,
|
import {AvatarCreatorItems} from '#/screens/Onboarding/StepProfile/AvatarCreatorItems'
|
||||||
useSharedValue,
|
|
||||||
withTiming,
|
|
||||||
} from 'react-native-reanimated'
|
|
||||||
|
|
||||||
interface Avatar {
|
interface Avatar {
|
||||||
image?: {
|
image?: {
|
||||||
@@ -57,8 +37,6 @@ interface Avatar {
|
|||||||
placeholder: Emoji
|
placeholder: Emoji
|
||||||
}
|
}
|
||||||
|
|
||||||
const WITH_TIMING_CONFIG = {duration: 100}
|
|
||||||
|
|
||||||
const AvatarContext = React.createContext<Avatar>({} as Avatar)
|
const AvatarContext = React.createContext<Avatar>({} as Avatar)
|
||||||
const SetAvatarContext = React.createContext<
|
const SetAvatarContext = React.createContext<
|
||||||
React.Dispatch<React.SetStateAction<Avatar>>
|
React.Dispatch<React.SetStateAction<Avatar>>
|
||||||
@@ -132,8 +110,8 @@ export function StepProfile() {
|
|||||||
? [a.flex_row, a.justify_between]
|
? [a.flex_row, a.justify_between]
|
||||||
: undefined
|
: undefined
|
||||||
}>
|
}>
|
||||||
<Items type="emojis" />
|
<AvatarCreatorItems type="emojis" />
|
||||||
<Items type="colors" />
|
<AvatarCreatorItems type="colors" />
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
@@ -159,220 +137,3 @@ export function StepProfile() {
|
|||||||
</SetAvatarContext.Provider>
|
</SetAvatarContext.Provider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
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 (
|
|
||||||
<View>
|
|
||||||
<Image
|
|
||||||
source={avatar.image.path}
|
|
||||||
style={[styles.imageContainer, t.atoms.border_contrast_high]}
|
|
||||||
accessibilityIgnoresInvertColors
|
|
||||||
/>
|
|
||||||
<Pressable
|
|
||||||
accessibilityRole="button"
|
|
||||||
style={[
|
|
||||||
a.absolute,
|
|
||||||
a.rounded_full,
|
|
||||||
a.align_center,
|
|
||||||
a.justify_center,
|
|
||||||
a.border,
|
|
||||||
a.shadow_lg,
|
|
||||||
t.atoms.border_contrast_high,
|
|
||||||
t.atoms.bg_contrast_300,
|
|
||||||
{height: 40, width: 40, bottom: 5, right: 5},
|
|
||||||
]}
|
|
||||||
onPress={onPressRemoveAvatar}>
|
|
||||||
<Times size="lg" style={{color: t.palette.white}} />
|
|
||||||
</Pressable>
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
styles.imageContainer,
|
|
||||||
t.atoms.border_contrast_high,
|
|
||||||
{
|
|
||||||
backgroundColor: avatar.backgroundColor,
|
|
||||||
},
|
|
||||||
]}>
|
|
||||||
<Icon height={85} width={85} style={{color: t.palette.white}} />
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
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 (
|
|
||||||
<Animated.View
|
|
||||||
style={[
|
|
||||||
styles.imageContainer,
|
|
||||||
styles.paletteContainer,
|
|
||||||
t.atoms.border_contrast_high,
|
|
||||||
animatedStyle,
|
|
||||||
]}>
|
|
||||||
{children}
|
|
||||||
</Animated.View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function ColorItem({color}: {color: AvatarColor}) {
|
|
||||||
const avatar = useAvatar()
|
|
||||||
const setAvatar = useSetAvatar()
|
|
||||||
|
|
||||||
const onPress = React.useCallback(() => {
|
|
||||||
setAvatar(prev => ({
|
|
||||||
...prev,
|
|
||||||
backgroundColor: color,
|
|
||||||
}))
|
|
||||||
}, [color, setAvatar])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AnimatedCircle selected={avatar.backgroundColor === color}>
|
|
||||||
<Pressable
|
|
||||||
accessibilityRole="button"
|
|
||||||
style={[a.h_full, a.w_full, {backgroundColor: color}]}
|
|
||||||
onPress={onPress}
|
|
||||||
/>
|
|
||||||
</AnimatedCircle>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
function colorRenderItem({item}: ListRenderItemInfo<AvatarColor>) {
|
|
||||||
return <ColorItem color={item} />
|
|
||||||
}
|
|
||||||
|
|
||||||
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 (
|
|
||||||
<AnimatedCircle selected={avatar.placeholder.name === emojiName}>
|
|
||||||
<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} />
|
|
||||||
</Pressable>
|
|
||||||
</AnimatedCircle>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
function emojiRenderItem({item}: ListRenderItemInfo<EmojiName>) {
|
|
||||||
return <EmojiItem emojiName={item} />
|
|
||||||
}
|
|
||||||
|
|
||||||
function Items({type}: {type: 'emojis' | 'colors'}) {
|
|
||||||
const {isTabletOrDesktop} = useWebMediaQueries()
|
|
||||||
const styles = useStyles()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View style={styles.flatListOuter}>
|
|
||||||
<FlatList
|
|
||||||
// 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
|
|
||||||
key={isTabletOrDesktop ? 0 : 1}
|
|
||||||
data={type === 'colors' ? avatarColors : emojiNames}
|
|
||||||
renderItem={type === 'colors' ? colorRenderItem : emojiRenderItem}
|
|
||||||
style={[isTabletOrDesktop && {marginHorizontal: 10}]}
|
|
||||||
contentContainerStyle={[
|
|
||||||
a.align_center,
|
|
||||||
{height: 100},
|
|
||||||
!isTabletOrDesktop && styles.flatListContainer,
|
|
||||||
isTabletOrDesktop && type === 'colors' && a.pr_xs,
|
|
||||||
]}
|
|
||||||
numColumns={isTabletOrDesktop && type === 'emojis' ? 4 : undefined}
|
|
||||||
showsHorizontalScrollIndicator={isTabletOrDesktop && type === 'colors'}
|
|
||||||
horizontal={!isTabletOrDesktop}
|
|
||||||
{...commonFlatListProps}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
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<FlatListProps<any>> = {}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user