change layout on tablet/desktop
This commit is contained in:
@@ -9,9 +9,12 @@ import {openPicker} from 'lib/media/picker.shared'
|
|||||||
import {openCropper} from 'lib/media/picker'
|
import {openCropper} from 'lib/media/picker'
|
||||||
import {compressIfNeeded} from 'lib/media/manip'
|
import {compressIfNeeded} from 'lib/media/manip'
|
||||||
import {isNative, isWeb} from 'platform/detection'
|
import {isNative, isWeb} from 'platform/detection'
|
||||||
|
import Animated from 'react-native-reanimated'
|
||||||
|
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||||
|
|
||||||
export function SelectImageButton() {
|
export function SelectImageButton() {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
|
const {isTabletOrDesktop} = useWebMediaQueries()
|
||||||
const setAvatar = useSetAvatar()
|
const setAvatar = useSetAvatar()
|
||||||
const {requestPhotoAccessIfNeeded} = usePhotoLibraryPermission()
|
const {requestPhotoAccessIfNeeded} = usePhotoLibraryPermission()
|
||||||
|
|
||||||
@@ -53,17 +56,21 @@ export function SelectImageButton() {
|
|||||||
}, [requestPhotoAccessIfNeeded, setAvatar])
|
}, [requestPhotoAccessIfNeeded, setAvatar])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Pressable
|
<Animated.View
|
||||||
accessibilityRole="button"
|
|
||||||
style={[
|
style={[
|
||||||
styles.imageContainer,
|
styles.imageContainer,
|
||||||
styles.paletteContainer,
|
styles.paletteContainer,
|
||||||
a.mr_2xl,
|
a.mr_2xl,
|
||||||
t.atoms.border_contrast_high,
|
t.atoms.border_contrast_high,
|
||||||
]}
|
{height: 70, width: 70, marginRight: isTabletOrDesktop ? 0 : 5},
|
||||||
onPress={onCameraPress}>
|
]}>
|
||||||
<Camera height={35} width={35} style={[t.atoms.text_contrast_medium]} />
|
<Pressable
|
||||||
</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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,6 +87,5 @@ const styles = StyleSheet.create({
|
|||||||
paletteContainer: {
|
paletteContainer: {
|
||||||
height: 70,
|
height: 70,
|
||||||
width: 70,
|
width: 70,
|
||||||
marginHorizontal: 5,
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import {
|
|||||||
FlatList,
|
FlatList,
|
||||||
FlatListProps,
|
FlatListProps,
|
||||||
LayoutAnimation,
|
LayoutAnimation,
|
||||||
LayoutChangeEvent,
|
|
||||||
ListRenderItemInfo,
|
ListRenderItemInfo,
|
||||||
Pressable,
|
Pressable,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
@@ -23,10 +22,7 @@ import {
|
|||||||
Description,
|
Description,
|
||||||
OnboardingControls,
|
OnboardingControls,
|
||||||
} from '#/screens/Onboarding/Layout'
|
} from '#/screens/Onboarding/Layout'
|
||||||
import {
|
import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron'
|
||||||
ChevronLeft_Stroke2_Corner0_Rounded,
|
|
||||||
ChevronRight_Stroke2_Corner0_Rounded as ChevronRight,
|
|
||||||
} from '#/components/icons/Chevron'
|
|
||||||
import {TimesLarge_Stroke2_Corner0_Rounded as Times} from '#/components/icons/Times'
|
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 {Image} from 'expo-image'
|
||||||
@@ -62,6 +58,8 @@ 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>>
|
||||||
@@ -72,6 +70,7 @@ export const useSetAvatar = () => React.useContext(SetAvatarContext)
|
|||||||
export function StepProfile() {
|
export function StepProfile() {
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
|
const {isTabletOrDesktop} = useWebMediaQueries()
|
||||||
const {gtMobile} = useBreakpoints()
|
const {gtMobile} = useBreakpoints()
|
||||||
const {track} = useAnalytics()
|
const {track} = useAnalytics()
|
||||||
const {state, dispatch} = React.useContext(Context)
|
const {state, dispatch} = React.useContext(Context)
|
||||||
@@ -128,10 +127,15 @@ export function StepProfile() {
|
|||||||
<AvatarCircle />
|
<AvatarCircle />
|
||||||
</View>
|
</View>
|
||||||
{!avatar.image && (
|
{!avatar.image && (
|
||||||
<>
|
<View
|
||||||
|
style={
|
||||||
|
isTabletOrDesktop
|
||||||
|
? [a.flex_row, a.justify_between]
|
||||||
|
: undefined
|
||||||
|
}>
|
||||||
<Items type="emojis" />
|
<Items type="emojis" />
|
||||||
<Items type="colors" />
|
<Items type="colors" />
|
||||||
</>
|
</View>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
@@ -159,6 +163,7 @@ export function StepProfile() {
|
|||||||
|
|
||||||
function AvatarCircle() {
|
function AvatarCircle() {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
|
const styles = useStyles()
|
||||||
const avatar = useAvatar()
|
const avatar = useAvatar()
|
||||||
const setAvatar = useSetAvatar()
|
const setAvatar = useSetAvatar()
|
||||||
const Icon = avatar.placeholder.component
|
const Icon = avatar.placeholder.component
|
||||||
@@ -218,13 +223,14 @@ function AnimatedCircle({
|
|||||||
children,
|
children,
|
||||||
}: React.PropsWithChildren<{selected: boolean}>) {
|
}: React.PropsWithChildren<{selected: boolean}>) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
|
const styles = useStyles()
|
||||||
const size = useSharedValue(selected ? 80 : 70)
|
const size = useSharedValue(selected ? 80 : 70)
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (selected && size.value !== 80) {
|
if (selected && size.value !== 80) {
|
||||||
size.value = withTiming(80)
|
size.value = withTiming(80, WITH_TIMING_CONFIG)
|
||||||
} else if (!selected && size.value !== 70) {
|
} else if (!selected && size.value !== 70) {
|
||||||
size.value = withTiming(70)
|
size.value = withTiming(70, WITH_TIMING_CONFIG)
|
||||||
}
|
}
|
||||||
}, [selected, size])
|
}, [selected, size])
|
||||||
|
|
||||||
@@ -275,7 +281,7 @@ function EmojiItem({emojiName}: {emojiName: EmojiName}) {
|
|||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const avatar = useAvatar()
|
const avatar = useAvatar()
|
||||||
const setAvatar = useSetAvatar()
|
const setAvatar = useSetAvatar()
|
||||||
const Icon = emojiItems[emojiName].component
|
const Icon = React.useMemo(() => emojiItems[emojiName].component, [emojiName])
|
||||||
|
|
||||||
const onPress = React.useCallback(() => {
|
const onPress = React.useCallback(() => {
|
||||||
setAvatar(prev => ({
|
setAvatar(prev => ({
|
||||||
@@ -294,7 +300,7 @@ function EmojiItem({emojiName}: {emojiName: EmojiName}) {
|
|||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
style={[a.flex_1, a.justify_center, a.align_center]}
|
style={[a.flex_1, a.justify_center, a.align_center]}
|
||||||
onPress={onPress}>
|
onPress={onPress}>
|
||||||
<Icon height={35} width={35} style={[t.atoms.text_contrast_medium]} />
|
<Icon style={[t.atoms.text_contrast_medium]} height={40} width={40} />
|
||||||
</Pressable>
|
</Pressable>
|
||||||
</AnimatedCircle>
|
</AnimatedCircle>
|
||||||
)
|
)
|
||||||
@@ -304,112 +310,63 @@ function emojiRenderItem({item}: ListRenderItemInfo<EmojiName>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function Items({type}: {type: 'emojis' | 'colors'}) {
|
function Items({type}: {type: 'emojis' | 'colors'}) {
|
||||||
const t = useTheme()
|
|
||||||
const {isTabletOrDesktop} = useWebMediaQueries()
|
const {isTabletOrDesktop} = useWebMediaQueries()
|
||||||
|
const styles = useStyles()
|
||||||
const maxWidth = React.useRef(0)
|
|
||||||
const width = React.useRef(0)
|
|
||||||
const page = React.useRef(0)
|
|
||||||
const ref = React.useRef<FlatList>(null)
|
|
||||||
|
|
||||||
const onLayout = React.useCallback((e: LayoutChangeEvent) => {
|
|
||||||
width.current = e.nativeEvent.layout.width
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const onContentSizeChanged = React.useCallback((w: number, _: number) => {
|
|
||||||
maxWidth.current = w
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const onLeftPress = React.useCallback(() => {
|
|
||||||
if (page.current === 0) return
|
|
||||||
page.current -= 1
|
|
||||||
ref.current?.scrollToOffset({
|
|
||||||
offset: width.current * page.current - width.current + 50,
|
|
||||||
})
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const onRightPress = React.useCallback(() => {
|
|
||||||
const newOffset = width.current * (page.current + 1) - 50
|
|
||||||
if (newOffset >= maxWidth.current + width.current) return
|
|
||||||
|
|
||||||
page.current += 1
|
|
||||||
ref.current?.scrollToOffset({
|
|
||||||
offset: newOffset,
|
|
||||||
})
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.flatListOuter}>
|
<View style={styles.flatListOuter}>
|
||||||
{isTabletOrDesktop && (
|
|
||||||
<Pressable
|
|
||||||
onPress={onLeftPress}
|
|
||||||
style={[
|
|
||||||
a.align_center,
|
|
||||||
a.justify_center,
|
|
||||||
t.atoms.bg_contrast_100,
|
|
||||||
{height: 40, width: 40, borderRadius: 100},
|
|
||||||
]}
|
|
||||||
accessibilityRole="button">
|
|
||||||
<ChevronLeft_Stroke2_Corner0_Rounded style={t.atoms.text} />
|
|
||||||
</Pressable>
|
|
||||||
)}
|
|
||||||
<FlatList
|
<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}
|
data={type === 'colors' ? avatarColors : emojiNames}
|
||||||
renderItem={type === 'colors' ? colorRenderItem : emojiRenderItem}
|
renderItem={type === 'colors' ? colorRenderItem : emojiRenderItem}
|
||||||
ref={ref}
|
|
||||||
onLayout={onLayout}
|
|
||||||
onContentSizeChange={onContentSizeChanged}
|
|
||||||
style={[isTabletOrDesktop && {marginHorizontal: 10}]}
|
style={[isTabletOrDesktop && {marginHorizontal: 10}]}
|
||||||
contentContainerStyle={
|
contentContainerStyle={[
|
||||||
!isTabletOrDesktop ? styles.flatListContainer : undefined
|
a.align_center,
|
||||||
}
|
!isTabletOrDesktop && styles.flatListContainer,
|
||||||
|
]}
|
||||||
|
numColumns={isTabletOrDesktop && type === 'emojis' ? 4 : undefined}
|
||||||
|
showsHorizontalScrollIndicator={isTabletOrDesktop && type === 'emojis'}
|
||||||
|
horizontal={!isTabletOrDesktop}
|
||||||
{...commonFlatListProps}
|
{...commonFlatListProps}
|
||||||
/>
|
/>
|
||||||
{isTabletOrDesktop && (
|
|
||||||
<Pressable
|
|
||||||
onPress={onRightPress}
|
|
||||||
style={[
|
|
||||||
a.align_center,
|
|
||||||
a.justify_center,
|
|
||||||
t.atoms.bg_contrast_100,
|
|
||||||
{height: 40, width: 40, borderRadius: 100},
|
|
||||||
]}
|
|
||||||
accessibilityRole="button">
|
|
||||||
<ChevronRight style={t.atoms.text} />
|
|
||||||
</Pressable>
|
|
||||||
)}
|
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const useStyles = () => {
|
||||||
imageContainer: {
|
const {isTabletOrDesktop} = useWebMediaQueries()
|
||||||
borderRadius: 100,
|
|
||||||
height: 150,
|
|
||||||
width: 150,
|
|
||||||
overflow: 'hidden',
|
|
||||||
borderWidth: 2,
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
},
|
|
||||||
paletteContainer: {
|
|
||||||
height: 70,
|
|
||||||
width: 70,
|
|
||||||
marginHorizontal: 5,
|
|
||||||
},
|
|
||||||
flatListOuter: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
height: 100,
|
|
||||||
},
|
|
||||||
flatListContainer: {
|
|
||||||
paddingLeft: 40,
|
|
||||||
paddingRight: 40,
|
|
||||||
alignItems: 'center',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const commonFlatListProps: Partial<FlatListProps<any>> = {
|
return StyleSheet.create({
|
||||||
horizontal: true,
|
imageContainer: {
|
||||||
showsHorizontalScrollIndicator: false,
|
borderRadius: 100,
|
||||||
|
height: 150,
|
||||||
|
width: 150,
|
||||||
|
overflow: 'hidden',
|
||||||
|
borderWidth: 2,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
},
|
||||||
|
paletteContainer: {
|
||||||
|
height: 70,
|
||||||
|
width: 70,
|
||||||
|
marginHorizontal: 5,
|
||||||
|
marginVertical: 5,
|
||||||
|
},
|
||||||
|
flatListOuter: isTabletOrDesktop
|
||||||
|
? {
|
||||||
|
height: 450,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
height: 100,
|
||||||
|
},
|
||||||
|
flatListContainer: {
|
||||||
|
paddingHorizontal: 40,
|
||||||
|
},
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const commonFlatListProps: Partial<FlatListProps<any>> = {}
|
||||||
|
|||||||
Reference in New Issue
Block a user