Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CP-9319: K2-Avatars #2138

Merged
merged 10 commits into from
Dec 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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",
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

had to downgrade due to compatibility issue with expo

"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",
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

introduced for implementing infinite scrolling. I had to modify it slightly to allow touch events outside the item layout for implementing our hexagonal multi-row layout. I opened a pr and they merged it quickly.

"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]
}
atn4z7 marked this conversation as resolved.
Show resolved Hide resolved
}, [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
Loading