Created
June 12, 2023 20:01
-
-
Save Jalson1982/eca8468b4a1837df329f5fabc31cedc9 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import * as React from 'react'; | |
import { | |
Text, | |
View, | |
StyleSheet, | |
StatusBar, | |
TouchableOpacity, | |
} from 'react-native'; | |
import PagerView from 'react-native-pager-view'; | |
import Animated, { | |
Extrapolate, | |
useSharedValue, | |
useAnimatedStyle, | |
interpolate, | |
} from 'react-native-reanimated'; | |
import {faker} from '@faker-js/faker'; | |
faker.seed(10); | |
const _allUsers = [...Array(8).keys()].map(i => ({ | |
key: `user-${i}`, | |
avatar: `https://i.pravatar.cc/80?img=${i}`, | |
name: faker.name.firstName().slice(0, 3), | |
index: i, | |
online: faker.helpers.arrayElement([true, false]), | |
})); | |
const _data = [...Array(10).keys()].map(sectionIndex => ({ | |
// Assign Random users inside chats (this should be part of the API) | |
users: faker.helpers.arrayElements( | |
_allUsers, | |
faker.number.int(Math.floor(_allUsers.length / 2)) + 2, | |
), | |
key: `section-${sectionIndex}`, | |
index: sectionIndex, | |
bg: faker.color.human(), | |
})); | |
const _avatarSize = 40; | |
const AnimatedAvatar = ({offset, avatar, users, onPress}) => { | |
// Returns true/false -> Checking if the avatar is part of each chat. | |
const selection = React.useMemo( | |
() => | |
users.map(a => | |
a.users.findIndex(u => u.key === avatar.key) === -1 ? false : true, | |
), | |
[users, avatar], | |
); | |
// Create ahead of time the inputRange (for all the existing chats) | |
const inputRange = React.useMemo( | |
() => selection.map((_, i) => i), | |
[selection], | |
); | |
// Create ahead of time the outputRange (for all the existing chats, we check the user is part of the chat or not) | |
// 0 -> is in the chat | |
// _avatarSize - 10 -> it is NOT in the chat <--- this is where we animate down the avatar | |
const outputRange = React.useMemo( | |
() => selection.map(v => (v ? 0 : _avatarSize - 10)), | |
[selection], | |
); | |
const stylez = useAnimatedStyle(() => { | |
return { | |
transform: [ | |
{ | |
// E.g for user A (chat1, chat2, chat3, chat4, chat5) | |
// in chats [true, false, false, true, true] | |
// output -> [VISIBLE, HIDDEN, HIDDEN, VISIBLE, VISIBLE] | |
// HIDDEN => TranslateY -> x amount (in our example is _avatarSize - 10) | |
// VISIBLE => TranslateY -> 0 (we keep it at the top position) | |
// linear interpolation (aka lerp) will create all the scenarios for our | |
// avatar movement. | |
translateY: interpolate( | |
offset.value, | |
inputRange, | |
outputRange, | |
Extrapolate.CLAMP, | |
), | |
}, | |
], | |
}; | |
}); | |
return ( | |
<TouchableOpacity | |
onPress={onPress} | |
style={{alignItems: 'center', marginRight: 8}}> | |
<View | |
style={{overflow: 'hidden', width: _avatarSize, height: _avatarSize}}> | |
<Animated.Image | |
source={{uri: avatar.avatar}} | |
style={[ | |
{ | |
width: _avatarSize, | |
height: _avatarSize, | |
borderRadius: _avatarSize / 2, | |
}, | |
stylez, | |
]} | |
/> | |
<View | |
style={{ | |
position: 'absolute', | |
bottom: 4, | |
left: 4, | |
width: _avatarSize / 6, | |
height: _avatarSize / 6, | |
borderRadius: _avatarSize / 6, | |
backgroundColor: avatar.online ? 'green' : 'red', | |
}} | |
/> | |
</View> | |
<Text>{avatar.name}</Text> | |
</TouchableOpacity> | |
); | |
}; | |
export default function App() { | |
const offset = useSharedValue(0); | |
const pagerRef = React.useRef(null); | |
const [data, setData] = React.useState(_data); | |
const [activeIndex, setActiveIndex] = React.useState(0); | |
const navigateToPage = user => { | |
const users = data[activeIndex].users; | |
const isUserInArray = users.some(u => u.key === user.key); | |
let newUsersArray; | |
if (isUserInArray) { | |
newUsersArray = users.filter(u => u.key !== user.key); | |
} else { | |
newUsersArray = [...users, user]; | |
} | |
const matchedIndex = data.findIndex(obj => { | |
if (obj.users.length !== newUsersArray.length) { | |
return false; | |
} | |
const objUsersKeys = obj.users.map(u => u.key).sort(); | |
const newUsersKeys = newUsersArray.map(u => u.key).sort(); | |
return objUsersKeys.every((key, i) => key === newUsersKeys[i]); | |
}); | |
if (matchedIndex !== -1) { | |
pagerRef.current.setPage(matchedIndex); | |
setActiveIndex(matchedIndex); | |
} else { | |
const newEntry = { | |
users: newUsersArray, | |
key: `section-${Date.now()}`, // Using timestamp for unique key | |
index: data.length, // Assuming the new index should be the next available index | |
bg: faker.color.human(), | |
}; | |
const newData = [newEntry, ...data]; | |
setData(newData); | |
setActiveIndex(0); | |
setTimeout(() => { | |
pagerRef.current.setPage(0); | |
}, 0); | |
} | |
}; | |
return ( | |
<View style={styles.container}> | |
<View style={{flexDirection: 'row'}}> | |
{_allUsers.map(avatar => { | |
return ( | |
<AnimatedAvatar | |
onPress={() => navigateToPage(avatar)} | |
offset={offset} | |
avatar={avatar} | |
users={data} | |
key={avatar.key} | |
/> | |
); | |
})} | |
</View> | |
<PagerView | |
ref={pagerRef} | |
style={{flex: 1}} | |
onPageSelected={ev => { | |
setActiveIndex(ev.nativeEvent.position); | |
}} | |
initialPage={activeIndex} | |
onPageScroll={ev => { | |
// Change the offset (this is the float value of the position) | |
// Based on which we are going to interpolate the avatars position | |
console.log(ev.nativeEvent.offset + ev.nativeEvent.position); | |
offset.value = ev.nativeEvent.offset + ev.nativeEvent.position; | |
}}> | |
{data.map((slide, index) => ( | |
<View | |
key={index.toString()} | |
style={{ | |
backgroundColor: slide.bg, | |
flex: 1, | |
justifyContent: 'center', | |
alignItems: 'center', | |
}}> | |
<Text>Content for ${slide.index}</Text> | |
<Text>Active users in this Chat:</Text> | |
<View | |
style={{ | |
flexDirection: 'row', | |
marginVertical: 20, | |
flexWrap: 'wrap', | |
alignSelf: 'center', | |
paddingHorizontal: 12, | |
}}> | |
{slide.users.map(avatar => { | |
return ( | |
<View style={{alignItems: 'center', padding: 2}}> | |
<Animated.Image | |
source={{uri: avatar.avatar}} | |
style={[ | |
{ | |
width: _avatarSize * 2, | |
height: _avatarSize * 2, | |
borderRadius: _avatarSize * 2, | |
}, | |
]} | |
/> | |
<Text>{avatar.name}</Text> | |
</View> | |
); | |
})} | |
</View> | |
</View> | |
))} | |
</PagerView> | |
<StatusBar hidden /> | |
</View> | |
); | |
} | |
const styles = StyleSheet.create({ | |
container: { | |
flex: 1, | |
justifyContent: 'center', | |
paddingTop: 50, | |
}, | |
paragraph: { | |
margin: 24, | |
fontSize: 18, | |
fontWeight: 'bold', | |
textAlign: 'center', | |
}, | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment