Simplify avi creator
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
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'
|
||||
import {Avatar} from '#/screens/Onboarding/StepProfile/index'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
|
||||
export function AvatarCreatorCircle({
|
||||
avatar,
|
||||
@@ -21,14 +21,13 @@ export function AvatarCreatorCircle({
|
||||
a.overflow_hidden,
|
||||
a.align_center,
|
||||
a.justify_center,
|
||||
a.border,
|
||||
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],
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import React from 'react'
|
||||
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 {View} from 'react-native'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {atoms as a, native, useBreakpoints, useTheme, web} from '#/alf'
|
||||
import {Avatar} from '#/screens/Onboarding/StepProfile/index'
|
||||
import {
|
||||
AvatarColor,
|
||||
@@ -13,100 +11,18 @@ import {
|
||||
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'
|
||||
|
||||
function Circle({children}: React.PropsWithChildren<{}>) {
|
||||
const t = useTheme()
|
||||
|
||||
const styles = React.useMemo(
|
||||
() => ({
|
||||
container: [
|
||||
a.rounded_full,
|
||||
a.overflow_hidden,
|
||||
a.align_center,
|
||||
a.justify_center,
|
||||
t.atoms.bg_contrast_25,
|
||||
web({borderWidth: 2}),
|
||||
native({borderWidth: 1}),
|
||||
{
|
||||
height: 60,
|
||||
width: 60,
|
||||
margin: 4,
|
||||
},
|
||||
],
|
||||
}),
|
||||
[t.atoms.bg_contrast_25],
|
||||
)
|
||||
|
||||
return (
|
||||
<View style={[styles.container, t.atoms.border_contrast_high]}>
|
||||
{children}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
function ColorItem({
|
||||
color,
|
||||
setAvatar,
|
||||
}: {
|
||||
color: AvatarColor
|
||||
setAvatar: React.Dispatch<React.SetStateAction<Avatar>>
|
||||
}) {
|
||||
const onPress = React.useCallback(() => {
|
||||
setAvatar(prev => ({
|
||||
...prev,
|
||||
backgroundColor: color,
|
||||
}))
|
||||
}, [color, setAvatar])
|
||||
|
||||
return (
|
||||
<Circle>
|
||||
<Pressable
|
||||
accessibilityRole="button"
|
||||
style={[a.h_full, a.w_full, {backgroundColor: color}]}
|
||||
onPress={onPress}
|
||||
/>
|
||||
</Circle>
|
||||
)
|
||||
}
|
||||
|
||||
function EmojiItem({
|
||||
emojiName,
|
||||
avatar,
|
||||
setAvatar,
|
||||
}: {
|
||||
emojiName: EmojiName
|
||||
avatar: Avatar
|
||||
setAvatar: React.Dispatch<React.SetStateAction<Avatar>>
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const Icon = React.useMemo(() => emojiItems[emojiName].component, [emojiName])
|
||||
|
||||
const onPress = React.useCallback(() => {
|
||||
setAvatar(prev => ({
|
||||
...prev,
|
||||
placeholder: emojiItems[emojiName],
|
||||
}))
|
||||
}, [emojiName, setAvatar])
|
||||
|
||||
const selected = React.useMemo(
|
||||
() => avatar.placeholder.name === emojiName,
|
||||
[avatar.placeholder.name, emojiName],
|
||||
)
|
||||
|
||||
return (
|
||||
<Circle>
|
||||
<Pressable
|
||||
accessibilityRole="button"
|
||||
style={[a.flex_1, a.justify_center, a.align_center]}
|
||||
onPress={onPress}>
|
||||
<Icon
|
||||
style={selected ? t.atoms.text : t.atoms.text_contrast_low}
|
||||
height={30}
|
||||
width={30}
|
||||
/>
|
||||
</Pressable>
|
||||
</Circle>
|
||||
)
|
||||
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({
|
||||
@@ -118,58 +34,112 @@ export function AvatarCreatorItems({
|
||||
avatar: Avatar
|
||||
setAvatar: React.Dispatch<React.SetStateAction<Avatar>>
|
||||
}) {
|
||||
const {gtMobile} = useBreakpoints()
|
||||
const {_} = useLingui()
|
||||
const t = useTheme()
|
||||
const isEmojis = type === 'emojis'
|
||||
|
||||
const styles = React.useMemo(
|
||||
() => ({
|
||||
flatListOuter: gtMobile
|
||||
? {
|
||||
height: 338,
|
||||
}
|
||||
: [a.flex_row, a.align_center, {height: 70}],
|
||||
}),
|
||||
[gtMobile],
|
||||
)
|
||||
|
||||
const colorRenderItem = React.useCallback(
|
||||
({item}: ListRenderItemInfo<AvatarColor>) => {
|
||||
return <ColorItem color={item} setAvatar={setAvatar} />
|
||||
const onSelectEmoji = React.useCallback(
|
||||
(emoji: EmojiName) => {
|
||||
setAvatar(prev => ({
|
||||
...prev,
|
||||
placeholder: emojiItems[emoji],
|
||||
}))
|
||||
},
|
||||
[setAvatar],
|
||||
)
|
||||
|
||||
const emojiRenderItem = React.useCallback(
|
||||
({item}: ListRenderItemInfo<EmojiName>) => {
|
||||
return (
|
||||
<EmojiItem emojiName={item} avatar={avatar} setAvatar={setAvatar} />
|
||||
)
|
||||
const onSelectColor = React.useCallback(
|
||||
(color: AvatarColor) => {
|
||||
setAvatar(prev => ({
|
||||
...prev,
|
||||
backgroundColor: color,
|
||||
}))
|
||||
},
|
||||
[avatar, setAvatar],
|
||||
[setAvatar],
|
||||
)
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
styles.flatListOuter,
|
||||
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={gtMobile ? 0 : 1}
|
||||
data={type === 'colors' ? avatarColors : emojiNames}
|
||||
renderItem={type === 'colors' ? colorRenderItem : emojiRenderItem}
|
||||
keyExtractor={item => item}
|
||||
style={[gtMobile && {marginHorizontal: 10}]}
|
||||
contentContainerStyle={[
|
||||
a.align_center,
|
||||
gtMobile && type === 'colors' && a.pr_xs,
|
||||
!gtMobile && {paddingHorizontal: 40},
|
||||
]}
|
||||
numColumns={gtMobile && type === 'emojis' ? 4 : undefined}
|
||||
showsHorizontalScrollIndicator={gtMobile && type === 'colors'}
|
||||
horizontal={!gtMobile}
|
||||
/>
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ export function StepProfile() {
|
||||
const {requestPhotoAccessIfNeeded} = usePhotoLibraryPermission()
|
||||
const creatorControl = Dialog.useDialogControl()
|
||||
|
||||
const {state, dispatch} = React.useContext(Context)
|
||||
const {dispatch} = React.useContext(Context)
|
||||
const [avatar, setAvatar] = React.useState<Avatar>({
|
||||
placeholder: emojiItems.at,
|
||||
backgroundColor: randomColor,
|
||||
@@ -173,18 +173,17 @@ export function StepProfile() {
|
||||
an avatar.
|
||||
</Trans>
|
||||
</DescriptionText>
|
||||
<View style={[a.w_full, a.align_center, {paddingTop: 80}]}>
|
||||
<View
|
||||
style={[a.w_full, a.align_center, {paddingTop: gtMobile ? 80 : 40}]}>
|
||||
<AvatarCircle
|
||||
openLibrary={openLibrary}
|
||||
openCreator={creatorControl.open}
|
||||
/>
|
||||
</View>
|
||||
<View style={[a.w_full, a.px_2xl, a.pt_5xl]} />
|
||||
|
||||
<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"
|
||||
@@ -196,7 +195,6 @@ export function StepProfile() {
|
||||
<ButtonIcon icon={ChevronRight} position="right" />
|
||||
</Button>
|
||||
<Button
|
||||
key={state.activeStep} // remove focus state on nav
|
||||
variant="ghost"
|
||||
color="primary"
|
||||
size="large"
|
||||
@@ -216,18 +214,19 @@ export function StepProfile() {
|
||||
|
||||
<Dialog.Outer control={creatorControl}>
|
||||
<Dialog.Handle />
|
||||
<Dialog.Inner label="Avatar creator">
|
||||
<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_2xl,
|
||||
a.align_center,
|
||||
a.justify_center,
|
||||
gtMobile && a.flex_row,
|
||||
]}>
|
||||
<View style={[a.pt_3xl, a.gap_lg]}>
|
||||
<AvatarCreatorItems
|
||||
type="emojis"
|
||||
avatar={avatar}
|
||||
@@ -239,9 +238,8 @@ export function StepProfile() {
|
||||
setAvatar={setAvatar}
|
||||
/>
|
||||
</View>
|
||||
<View style={[a.px_xl, a.pt_4xl]}>
|
||||
<View style={[a.pt_4xl]}>
|
||||
<Button
|
||||
key={state.activeStep} // remove focus state on nav
|
||||
variant="solid"
|
||||
color="primary"
|
||||
size="large"
|
||||
@@ -254,6 +252,7 @@ export function StepProfile() {
|
||||
</View>
|
||||
</Dialog.Inner>
|
||||
</Dialog.Outer>
|
||||
|
||||
<PlaceholderCanvas ref={canvasRef} />
|
||||
</AvatarContext.Provider>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user