Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save Jalson1982/eca8468b4a1837df329f5fabc31cedc9 to your computer and use it in GitHub Desktop.
Save Jalson1982/eca8468b4a1837df329f5fabc31cedc9 to your computer and use it in GitHub Desktop.
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