Skip to content

Commit

Permalink
CP-9319: K2-Avatars (#2138)
Browse files Browse the repository at this point in the history
  • Loading branch information
onghwan authored Dec 3, 2024
1 parent 172b0a0 commit aa0b157
Show file tree
Hide file tree
Showing 17 changed files with 566 additions and 8 deletions.
4 changes: 2 additions & 2 deletions packages/core-mobile/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1288,7 +1288,7 @@ PODS:
- React-Core
- RNFS (2.20.0):
- React-Core
- RNGestureHandler (2.20.0):
- RNGestureHandler (2.14.1):
- glog
- RCT-Folly (= 2022.05.16.00)
- React-Core
Expand Down Expand Up @@ -1843,7 +1843,7 @@ SPEC CHECKSUMS:
RNFBMessaging: 2dd7ef3e3eca8ec62599f0f11b739e6f75c57cd2
RNFlashList: 83a272ae1c35b08a02490f4d1503631fb64b3dd8
RNFS: 4ac0f0ea233904cb798630b3c077808c06931688
RNGestureHandler: bb81850add626ddd265294323310fec6e861c96b
RNGestureHandler: 15c6ef51acba34c49ff03003806cf5dd6098f383
RNGoogleSignin: 9e68b9bcc3888219357924e32ee563624745647d
RNInAppBrowser: e36d6935517101ccba0e875bac8ad7b0cb655364
RNKeychain: ff836453cba46938e0e9e4c22e43d43fa2c90333
Expand Down
2 changes: 1 addition & 1 deletion packages/core-mobile/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@
"react-native-device-info": "13.0.0",
"react-native-fast-image": "8.6.3",
"react-native-fs": "2.20.0",
"react-native-gesture-handler": "2.20.0",
"react-native-gesture-handler": "2.14.1",
"react-native-graph": "1.1.0",
"react-native-haptic-feedback": "2.0.3",
"react-native-inappbrowser-reborn": "3.7.0",
Expand Down
4 changes: 4 additions & 0 deletions packages/k2-alpine/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,21 @@
"postinstall": "node_modules/.bin/patch-package"
},
"dependencies": {
"@react-native-masked-view/masked-view": "0.3.0",
"dripsy": "4.3.7",
"expo": "50.0.21",
"expo-blur": "12.9.2",
"expo-font": "11.10.3",
"expo-image": "1.10.6",
"expo-linear-gradient": "12.7.2",
"expo-splash-screen": "0.27.6",
"expo-status-bar": "1.12.1",
"react": "18.3.1",
"react-native": "0.73.7",
"react-native-dialog": "9.3.0",
"react-native-gesture-handler": "2.14.1",
"react-native-reanimated": "3.6.2",
"react-native-reanimated-carousel": "v4.0.0-canary.22",
"react-native-svg": "15.7.1"
},
"peerDependencies": {
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
92 changes: 92 additions & 0 deletions packages/k2-alpine/src/components/Avatar/Avatar.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import React, { useState } from 'react'
import { Switch } from 'react-native'
import { ScrollView, Text, View } from '../Primitives'
import { useTheme } from '../..'
import AvatarSelector from './AvatarSelector'
import { Avatar } from './Avatar'

export default {
title: 'Avatar'
}

export const All = (): JSX.Element => {
const { theme } = useTheme()

const [hasBlur, setHasBlur] = useState(true)

const AVATARS = [
require('../../assets/avatars/avatar-1.jpeg'),
require('../../assets/avatars/avatar-2.jpeg'),
require('../../assets/avatars/avatar-3.jpeg'),
require('../../assets/avatars/avatar-4.jpeg'),
require('../../assets/avatars/avatar-5.jpeg'),
require('../../assets/avatars/avatar-6.png'),
require('../../assets/avatars/avatar-7.png'),
require('../../assets/avatars/avatar-8.png'),
require('../../assets/avatars/avatar-9.jpeg'),
{
uri: 'https://miro.medium.com/v2/resize:fit:1256/format:webp/1*xm2-adeU3YD4MsZikpc5UQ.png'
},
{
uri: 'https://www.cnet.com/a/img/resize/7589227193923c006f9a7fd904b77bc898e105ff/hub/2021/11/29/f566750f-79b6-4be9-9c32-8402f58ba0ef/richerd.png?auto=webp&width=768'
},
{
uri: 'https://i.seadn.io/s/raw/files/a9cb8c2298a64819a3036083818d0447.jpg?auto=format&dpr=1&w=1000'
},
{
uri: 'https://i.seadn.io/gcs/files/441e674e79460fc975d976465bb3634d.png?auto=format&dpr=1&w=1000'
},
{
uri: 'https://www.svgrepo.com/show/19461/url-link.svg'
}
].map((avatar, index) => {
return { id: index.toString(), source: avatar }
})

const [selectedAvatarId, setSelectedAvatarId] = useState<string | undefined>(
AVATARS[0]?.id
)

const handleSelect = (id: string): void => {
setSelectedAvatarId(id)
}

const backgroundColor = theme.colors.$surfacePrimary

return (
<ScrollView
style={{
width: '100%',
backgroundColor
}}>
<View sx={{ alignItems: 'flex-end', padding: 12 }}>
<View
sx={{
gap: 8,
flexDirection: 'row',
alignItems: 'center'
}}>
<Text>Blur on</Text>
<Switch value={hasBlur} onValueChange={setHasBlur} />
</View>
</View>
<View sx={{ alignItems: 'center', padding: 100 }}>
<Avatar
backgroundColor={backgroundColor}
source={
AVATARS.find(avatar => avatar.id === selectedAvatarId)?.source
}
size="large"
hasBlur={hasBlur}
/>
</View>
<AvatarSelector
backgroundColor={backgroundColor}
selectedId={selectedAvatarId}
avatars={AVATARS}
onSelect={handleSelect}
/>
<View sx={{ height: 200 }} />
</ScrollView>
)
}
115 changes: 115 additions & 0 deletions packages/k2-alpine/src/components/Avatar/Avatar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import React, { useEffect } from 'react'
import { ImageSourcePropType, Platform, ViewStyle } from 'react-native'
import Animated, {
Easing,
useAnimatedStyle,
useSharedValue,
withTiming
} from 'react-native-reanimated'
import { BlurView } from 'expo-blur'
import { View } from '../Primitives'
import { useTheme } from '../..'
import { HexagonImageView, HexagonBorder } from './HexagonImageView'

export const Avatar = ({
source,
size,
isSelected,
isPressed,
hasBlur,
style,
backgroundColor
}: {
source: ImageSourcePropType
size: number | 'small' | 'large'
backgroundColor: string
isSelected?: boolean
isPressed?: boolean
hasBlur?: boolean
style?: ViewStyle
}): JSX.Element => {
const { theme } = useTheme()

const height = typeof size === 'number' ? size : size === 'small' ? 90 : 150

const pressedAnimation = useSharedValue(1)
const pressedAnimatedStyle = useAnimatedStyle(() => ({
transform: [{ scale: pressedAnimation.value }]
}))
// to cancel out the blur effect on the backgroundColor, we need to use a darker background color for the blur view
const surfacePrimaryBlurBgMap = theme.isDark
? {
[theme.colors.$surfacePrimary]:
Platform.OS === 'ios' ? '#050506' : '#0a0a0b',
[theme.colors.$surfaceSecondary]:
Platform.OS === 'ios' ? '#37373f' : '#373743',
[theme.colors.$surfaceTertiary]:
Platform.OS === 'ios' ? '#1A1A1C' : '#1C1C1F'
}
: {
[theme.colors.$surfacePrimary]: undefined,
[theme.colors.$surfaceSecondary]: undefined,
[theme.colors.$surfaceTertiary]:
Platform.OS === 'ios' ? '#8b8b8c' : '#79797c'
}

useEffect(() => {
pressedAnimation.value = withTiming(isPressed ? 0.95 : 1, {
duration: 150,
easing: Easing.inOut(Easing.ease)
})
}, [isPressed, pressedAnimation])

return (
<Animated.View
style={[
{ width: height, height: height, overflow: 'visible' },
pressedAnimatedStyle,
style
]}>
{hasBlur === true && (
<View
sx={{
backgroundColor: surfacePrimaryBlurBgMap[backgroundColor],
position: 'absolute',
top: -BLURAREA_INSET + 10,
left: -BLURAREA_INSET,
right: 0,
bottom: 0,
width: height + BLURAREA_INSET * 2,
height: height + BLURAREA_INSET * 2,
alignItems: 'center',
justifyContent: 'center'
}}>
<HexagonImageView
backgroundColor={backgroundColor}
source={source}
height={height}
/>
<BlurView
style={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0
}}
tint={theme.isDark ? 'dark' : undefined}
intensity={75}
experimentalBlurMethod="dimezisBlurView"
/>
</View>
)}
<HexagonImageView
source={source}
height={height}
backgroundColor={backgroundColor}
isSelected={isSelected}
hasLoading={true}
/>
<HexagonBorder height={height} />
</Animated.View>
)
}

const BLURAREA_INSET = 50
96 changes: 96 additions & 0 deletions packages/k2-alpine/src/components/Avatar/AvatarSelector.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { Dimensions, ImageSourcePropType } from 'react-native'
import React, { useMemo, useState } from 'react'
import Carousel from 'react-native-reanimated-carousel'
import { Pressable } from '../Primitives'
import { Avatar } from './Avatar'

const AvatarSelector = ({
avatars,
selectedId,
onSelect,
backgroundColor
}: {
avatars: { id: string; source: ImageSourcePropType }[]
selectedId?: string
onSelect?: (id: string) => void
backgroundColor: string
}): JSX.Element => {
const data = useMemo(() => {
// we should always have an even number of avatars, due to infinite scrolling + two avatars per column
if (avatars.length % 2 === 0) {
return avatars
} else {
return [...avatars, ...avatars]
}
}, [avatars])
const [pressedIndex, setPressedIndex] = useState<number>()

const handlePressIn = (index: number): void => {
setPressedIndex(index)
}

const handlePressOut = (index: number): void => {
if (pressedIndex === index) {
setPressedIndex(undefined)
}
}

const handleSelect = (index: number): void => {
if (data[index]?.id === undefined) {
return
}

onSelect?.(data[index].id)
}

const renderItem = ({
item,
index
}: {
item: { id: string; source: ImageSourcePropType }
index: number
}): JSX.Element => {
return (
<Pressable
key={index}
style={{ marginTop: index % 2 === 0 ? configuration.avatarWidth : 0 }}
onPressIn={() => handlePressIn(index)}
onPressOut={() => handlePressOut(index)}
onPress={() => handleSelect(index)}>
<Avatar
source={item.source}
size={configuration.avatarWidth}
isSelected={data[index]?.id === selectedId}
isPressed={pressedIndex === index}
backgroundColor={backgroundColor}
/>
</Pressable>
)
}

return (
<Carousel
width={configuration.avatarWidth / 2 + configuration.spacing}
height={configuration.avatarWidth * 2}
data={data}
renderItem={renderItem}
pagingEnabled={false}
snapEnabled={false}
style={{
width: '100%',
overflow: 'visible',
paddingVertical: configuration.spacing * 2,
marginLeft: SCREEN_WIDTH / 2 - configuration.avatarWidth / 2
}}
/>
)
}

const configuration = {
avatarWidth: 90,
spacing: 6
}

const SCREEN_WIDTH = Dimensions.get('window').width

export default AvatarSelector
Loading

0 comments on commit aa0b157

Please sign in to comment.