From 50612ed5ee7ed1d5c5eccac67618fdaa794277fb Mon Sep 17 00:00:00 2001 From: COURTES Guillaume <guillaumecourtes88@gmail.com> Date: Sat, 19 Jun 2021 18:24:25 +0200 Subject: [PATCH] add menu on item long press; add update discussion pages; fix api changes; add menu & multiselect packages; --- react-native/ChatApp/App.js | 5 +- .../components/AddUsersDiscussionScreen.js | 195 ++++++++++++++++++ .../components/AuthenticateNavigation.js | 134 +++++++++++- .../components/CreateDiscussionScreen.js | 2 +- .../ChatApp/components/DiscussionsScreen.js | 97 ++++++++- react-native/ChatApp/components/ListScreen.js | 61 +++++- .../ChatApp/components/MessagesScreen.js | 4 +- .../components/UpdateDiscussionScreen.js | 109 ++++++++++ react-native/ChatApp/package-lock.json | 10 + react-native/ChatApp/package.json | 2 + 10 files changed, 597 insertions(+), 22 deletions(-) create mode 100644 react-native/ChatApp/components/AddUsersDiscussionScreen.js create mode 100644 react-native/ChatApp/components/UpdateDiscussionScreen.js diff --git a/react-native/ChatApp/App.js b/react-native/ChatApp/App.js index 87bba2081..7455f7890 100644 --- a/react-native/ChatApp/App.js +++ b/react-native/ChatApp/App.js @@ -1,9 +1,12 @@ import React from 'react'; import AuthenticateNavigation from './components/AuthenticateNavigation' +import { MenuProvider } from 'react-native-popup-menu'; export default function App() { return ( - <AuthenticateNavigation /> + <MenuProvider> + <AuthenticateNavigation /> + </MenuProvider> ); } diff --git a/react-native/ChatApp/components/AddUsersDiscussionScreen.js b/react-native/ChatApp/components/AddUsersDiscussionScreen.js new file mode 100644 index 000000000..c079d6cec --- /dev/null +++ b/react-native/ChatApp/components/AddUsersDiscussionScreen.js @@ -0,0 +1,195 @@ +import React from 'react'; +import { + View, + StyleSheet, + KeyboardAvoidingView, + ActivityIndicator, + Dimensions +} from 'react-native'; +import { Input, Button, Text } from 'react-native-elements'; +import FontAwesome5 from 'react-native-vector-icons/FontAwesome5'; +import AsyncStorage from '@react-native-async-storage/async-storage'; +import Icon from 'react-native-vector-icons/MaterialIcons'; +import SectionedMultiSelect from 'react-native-sectioned-multi-select'; + +const windowHeight = Dimensions.get('window').height; + +class AddUsersDiscussionScreen extends React.Component { + + constructor(props) { + + super(props); + + this.state = { + users: [], + selectedUsers: [], + user: null, + isLoading: true + } + + } + + async getUsers() { + + let defaultUsers = this.props.route.params.item.users.map( + user => user._id + ); + defaultUsers = [...new Set(defaultUsers)]; + this.setState({selectedUsers: defaultUsers}); + + let status = null; + + //get users from API + try { + const token = await AsyncStorage.getItem('@token'); + //if token is stored, user is logged in + if (token !== null) { + fetch("http://163.172.178.146:3000/users", { + method: 'GET', + headers: { + Authorization: "Bearer " + token + } + }) + .then(response => { + status = response.status; + return response.json(); + }) + .then(async (json) => { + + //OK Status : we store the rooms list in list state + if (status == 200) { + this.setState({ users: json }); + console.log(this.state.users); + } + + //Unauthorized status : token has expired, we force user to login to continue + else if (status == 401) { + this.props.logoutAuthentication(); + } + + //Handled error status + else if ("error" in json) { + console.warn(json.error); + } + + //Unhandled error status + else { + console.warn("Unhandled error"); + } + + }) + .catch(async (error) => { + //Error fetching url + }); + } + //token is not stored, user is not logged in + else { + this.props.logoutAuthentication(); + } + } catch (error) { + //Error fetching data + } + } + + async componentDidMount() { + //get user from asyncStorage + try { + const user = await AsyncStorage.getItem('@user'); + if (user !== null) { + await this.setState({ user: JSON.parse(user) }); + } + else { + this.props.logoutAuthentication(); + } + } catch (error) { + console.warn(error); + } + + await this.getUsers(); + + this.setState({ isLoading: false }); + + } + + updateDiscussion() { + this.props.updateDiscussion(this.props.route.params.item._id, this.props.route.params.item.name, this.props.route.params.item.description, this.state.selectedUsers); + } + + onSelectedItemsChange = (selectedUsers) => { + this.setState({ selectedUsers }); + }; + + render() { + if (this.state.isLoading) + return ( + <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}> + <ActivityIndicator size="large" /> + </View> + ) + else + return ( + <View style={{flex: 1, justifyContent: 'center'}}> + <View style={{flex: 3, alignItems: 'center', justifyContent: 'center'}}> + <FontAwesome5 name={'users'} solid color={'lightgrey'} size={150} /> + <SectionedMultiSelect + items={this.state.users} + selectedItems={this.state.selectedUsers} + IconRenderer={Icon} + uniqueKey="_id" + displayKey="username" + primary='dodgerblue' + itemFontFamily={{ + fontFamily: Platform.OS === 'android' ? 'normal' : 'Avenir', + fontWeight: '200' + }} + confirmFontFamily={{ + fontFamily: Platform.OS === 'android' ? 'normal' : 'Avenir', + fontWeight: '200' + }} + confirmText="Confirmer" + showDropDowns={true} + onSelectedItemsChange={this.onSelectedItemsChange} + renderSelectText={() => "Utilisateurs"} + searchPlaceholderText="Rechercher" + noResultsComponent={ + <View + style={{ marginTop: 20, alignItems: 'center', justifyContent: 'center' }}> + <Text>Aucun résultat trouvé</Text> + </View> + } + noItemsComponent={ + <View + style={{ marginTop: 20, alignItems: 'center', justifyContent: 'center' }}> + <Text>Aucun éléments trouvé</Text> + </View> + } + styles={{ button: { backgroundColor: "dodgerblue" }, searchBar: {backgroundColor: 'white'}, selectToggle: {backgroundColor: 'white', padding: 10, margin: 5, maxWidth: 250, alignSelf: 'center'} }} + /> + </View> + <View style={{flex:1,alignItems: 'center', justifyContent: 'center', width: '100%' }}> + <Button + containerStyle={styles.customButton} + title="Enregistrer" + onPress={this.updateDiscussion.bind(this)} + buttonStyle={{backgroundColor: 'dodgerblue'}} + titleStyle={{ marginLeft: 5 }} + icon={ + <FontAwesome5 name={'user-plus'} solid color={'white'} size={20} /> + } + /> + </View> + </View> + ) + } +}; + +const styles = StyleSheet.create({ + customButton: { + borderRadius: 1000, + width: 200, + textAlign: 'center', + backgroundColor: 'red' + } +}) + +export default AddUsersDiscussionScreen; \ No newline at end of file diff --git a/react-native/ChatApp/components/AuthenticateNavigation.js b/react-native/ChatApp/components/AuthenticateNavigation.js index 1f902a413..fd7df274e 100644 --- a/react-native/ChatApp/components/AuthenticateNavigation.js +++ b/react-native/ChatApp/components/AuthenticateNavigation.js @@ -49,7 +49,6 @@ class AuthenticateNavigation extends React.Component { } async logoutAuthentication() { - this.setState({isAuthenticated: false}); try { await AsyncStorage.removeItem('@token'); } @@ -62,6 +61,7 @@ class AuthenticateNavigation extends React.Component { catch(exception) { console.warn(exception) } + this.setState({isAuthenticated: false}); } login(username, password) { @@ -220,6 +220,128 @@ class AuthenticateNavigation extends React.Component { } } + async updateDiscussion(id, name, description, users, format = false) { + let status = null; + + if (format) { + users = users.map( + user => user._id + ); + } + + //post room from API + try { + const token = await AsyncStorage.getItem('@token'); + //if token is stored, user is logged in + if (token !== null) { + fetch('http://163.172.178.146:3000/rooms/' + id, { + method: 'PUT', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: "Bearer " + token + }, + body: JSON.stringify({ + name: name, + description: description, + users: users + }) + }) + .then(response => { + status = response.status; + return response.json(); + }) + .then(async (json) => { + //OK Status : we store the token & the user in local storage + if (status == 200) { + console.warn("Discussion mise à jour"); + } + + //Unauthorized status : token has expired, we force user to login to continue + else if (status == 401) { + this.props.logoutAuthentication(); + } + + //Handled error status + else if ("error" in json) { + console.warn(json.error); + } + + //Unhandled error status + else { + console.warn("Unhandled error"); + } + }) + .catch(async (error) => { + //Error fetching url + }); + } + //token is not stored, user is not logged in + else { + this.props.logoutAuthentication(); + } + } catch (error) { + //Error fetching data + } + } + + async deleteDiscussion(id, user) { + let status = null; + + //post room from API + try { + const token = await AsyncStorage.getItem('@token'); + //if token is stored, user is logged in + if (token !== null) { + fetch('http://163.172.178.146:3000/rooms/' + id, { + method: 'DELETE', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: "Bearer " + token + }, + body: JSON.stringify({ + user: user + }) + }) + .then(response => { + status = response.status; + return response.json(); + }) + .then(async (json) => { + //OK Status : we store the token & the user in local storage + if (status == 200) { + console.warn("Discussion supprimée"); + } + + //Unauthorized status : token has expired, we force user to login to continue + else if (status == 401) { + this.props.logoutAuthentication(); + } + + //Handled error status + else if ("error" in json) { + console.warn(json.error); + } + + //Unhandled error status + else { + console.warn("Unhandled error"); + } + }) + .catch(async (error) => { + //Error fetching url + }); + } + //token is not stored, user is not logged in + else { + this.props.logoutAuthentication(); + } + } catch (error) { + //Error fetching data + } + } + render() { if (this.state.isLoading) { return ( @@ -261,7 +383,15 @@ class AuthenticateNavigation extends React.Component { /> <Tab.Screen name="Discussions" - children={(props) => <DiscussionsScreen {...props} addDiscussion={this.addDiscussion.bind(this)} logoutAuthentication={this.logoutAuthentication.bind(this)} />} + children={(props) => + <DiscussionsScreen + {...props} + addDiscussion={this.addDiscussion.bind(this)} + updateDiscussion={this.updateDiscussion.bind(this)} + deleteDiscussion={this.deleteDiscussion.bind(this)} + logoutAuthentication={this.logoutAuthentication.bind(this)} + /> + } options={{ tabBarLabel: 'Discussions', tabBarIcon: ({ color }) => ( diff --git a/react-native/ChatApp/components/CreateDiscussionScreen.js b/react-native/ChatApp/components/CreateDiscussionScreen.js index 176b262a9..7f29c3614 100644 --- a/react-native/ChatApp/components/CreateDiscussionScreen.js +++ b/react-native/ChatApp/components/CreateDiscussionScreen.js @@ -62,7 +62,7 @@ class CreateDiscussionScreen extends React.Component { style={{flex:1, alignItems: 'center', justifyContent: 'center' }} > - <FontAwesome5 name={'users'} solid color={'lightgrey'} size={150} /> + <FontAwesome5 name={'comment'} solid color={'lightgrey'} size={150} /> <View style={{alignItems: 'center', justifyContent: 'center', width: '100%' }}> <Input placeholder="Nom" diff --git a/react-native/ChatApp/components/DiscussionsScreen.js b/react-native/ChatApp/components/DiscussionsScreen.js index f60dab8d5..1b871e44e 100644 --- a/react-native/ChatApp/components/DiscussionsScreen.js +++ b/react-native/ChatApp/components/DiscussionsScreen.js @@ -1,10 +1,23 @@ import React from 'react'; -import { Icon, Button } from 'react-native-elements'; +import { + View, + FlatList +} from 'react-native'; +import { Icon, Button, Text } from 'react-native-elements'; import { createStackNavigator } from '@react-navigation/stack'; import ListScreen from './ListScreen' import MessagesScreen from './MessagesScreen' import CreateDiscussionScreen from './CreateDiscussionScreen' +import UpdateDiscussionScreen from './UpdateDiscussionScreen' +import AddUsersDiscussionScreen from './AddUsersDiscussionScreen' +import FontAwesome5 from 'react-native-vector-icons/FontAwesome5'; +import { + Menu, + MenuOptions, + MenuOption, + MenuTrigger +} from 'react-native-popup-menu'; const Stack = createStackNavigator(); @@ -16,16 +29,30 @@ class DiscussionsScreen extends React.Component { } + openMenu = (id) => { + this["user-list" + id].open(); + }; + + renderItem = ({ item }) => ( + <Text style={{fontSize: 15, margin: 5, marginLeft: 15}}><FontAwesome5 name={'user-alt'} solid color={'lightgrey'} size={15} /> {item.username}</Text> + ); + render() { return ( <Stack.Navigator initialRouteName="List"> <Stack.Screen name="List" - children={(props) => <ListScreen {...props} logoutAuthentication={this.props.logoutAuthentication}/>} + children={(props) => + <ListScreen + {...props} + deleteDiscussion={this.props.deleteDiscussion} + logoutAuthentication={this.props.logoutAuthentication} + /> + } options={{ title: "Discussions", headerRight: () => ( <Button - icon={<Icon solid name='plus' type='font-awesome-5' />} + icon={<Icon solid name='plus' type='font-awesome-5' size={20} />} onPress={() => this.props.navigation.navigate('CreateDiscussion')} type="clear" containerStyle={{margin: 5}} @@ -35,15 +62,73 @@ class DiscussionsScreen extends React.Component { /> <Stack.Screen name="CreateDiscussion" - children={(props) => <CreateDiscussionScreen {...props} addDiscussion={this.props.addDiscussion} logoutAuthentication={this.props.logoutAuthentication}/>} + children={(props) => + <CreateDiscussionScreen + {...props} + addDiscussion={this.props.addDiscussion} + logoutAuthentication={this.props.logoutAuthentication} + /> + } options={{ title: "Nouvelle discussion" }} /> + <Stack.Screen + name="UpdateDiscussion" + children={(props) => + <UpdateDiscussionScreen + {...props} + updateDiscussion={this.props.updateDiscussion} + logoutAuthentication={this.props.logoutAuthentication} + /> + } + options={({ route }) => ({ title: 'Modifier ' + route.params.item.name })} + /> + <Stack.Screen + name="AddUsersDiscussion" + children={(props) => + <AddUsersDiscussionScreen + {...props} + updateDiscussion={this.props.updateDiscussion} + logoutAuthentication={this.props.logoutAuthentication} + /> + } + options={({ route }) => ({ title: 'Gérer les utilisateurs' })} + /> <Stack.Screen name="Messages" - children={(props) => <MessagesScreen {...props} logoutAuthentication={this.props.logoutAuthentication}/>} - options={({ route }) => ({ title: route.params.title })} + children={(props) => + <MessagesScreen + {...props} + logoutAuthentication={this.props.logoutAuthentication} + /> + } + options={ + ({ route }) => ({ + title: route.params.item.name, headerRight: () => ( + <View> + <Button + icon={<Icon solid name='user-friends' type='font-awesome-5' size={20} />} + onPress={() => this.openMenu(route.params.item._id)} + type="clear" + containerStyle={{margin: 5}} + /> + <Menu ref={c => (this["user-list" + route.params.item._id] = c)}> + <MenuTrigger /> + <MenuOptions customStyles={{optionWrapper: { padding: 10 }}}> + <MenuOption> + <FlatList + data={route.params.item.users} + renderItem={this.renderItem} + keyExtractor={item => item._id} + /> + </MenuOption> + </MenuOptions> + </Menu> + </View> + ), + }) + } /> </Stack.Navigator> ) diff --git a/react-native/ChatApp/components/ListScreen.js b/react-native/ChatApp/components/ListScreen.js index 9415a6088..ebbd4b66f 100644 --- a/react-native/ChatApp/components/ListScreen.js +++ b/react-native/ChatApp/components/ListScreen.js @@ -1,11 +1,17 @@ import React from 'react'; import { View, - FlatList + FlatList, + Alert } from 'react-native'; import { ListItem, Icon, Text } from 'react-native-elements'; import AsyncStorage from '@react-native-async-storage/async-storage'; - +import { + Menu, + MenuOptions, + MenuOption, + MenuTrigger +} from 'react-native-popup-menu'; class ListScreen extends React.Component { constructor(props) { @@ -88,6 +94,15 @@ class ListScreen extends React.Component { keyExtractor = (item, index) => index.toString(); + openMenu = (id) => { + this["menu" + id].open(); + }; + + deleteDiscussion(id) { + this.props.deleteDiscussion(id, this.state.user); + this.getRooms(); + } + render() { if (this.state.list.length > 0) return ( @@ -96,14 +111,40 @@ class ListScreen extends React.Component { keyExtractor={this.keyExtractor} data={this.state.list} renderItem={({ item }) => ( - <ListItem bottomDivider onPress={() => this.props.navigation.navigate('Messages', { title: item.name, id: item._id })}> - <Icon solid name='comment' type='font-awesome-5' /> - <ListItem.Content> - <ListItem.Title>{item.name}</ListItem.Title> - <ListItem.Subtitle>{item.description}</ListItem.Subtitle> - </ListItem.Content> - <ListItem.Chevron /> - </ListItem> + <ListItem bottomDivider onLongPress={() => this.openMenu(item._id)} onPress={() => this.props.navigation.navigate('Messages', { item: item })}> + <Icon solid name='comment' type='font-awesome-5' /> + <ListItem.Content> + <ListItem.Title>{item.name}</ListItem.Title> + <ListItem.Subtitle>{item.description}</ListItem.Subtitle> + </ListItem.Content> + <ListItem.Chevron /> + <Menu ref={c => (this["menu" + item._id] = c)}> + <MenuTrigger /> + <MenuOptions customStyles={{optionWrapper: { padding: 10 }}}> + <MenuOption onSelect={() => this.props.navigation.navigate('UpdateDiscussion', { item: item })}> + <Text style={{ fontSize: 15 }}><Icon solid name='edit' type='font-awesome-5' size={13} style={{marginRight: 5}}/>Modifier</Text> + </MenuOption> + <MenuOption onSelect={() => this.props.navigation.navigate('AddUsersDiscussion', { item: item })}> + <Text style={{ fontSize: 15 }}><Icon solid name='user-cog' type='font-awesome-5' size={13} style={{marginRight: 5}}/>Gérer les utilisateurs</Text> + </MenuOption> + <MenuOption onSelect={() => + Alert.alert( + "", + "Voulez vous vraiment supprimer cette discussion ?", + [ + { + text: "Annuler", + style: 'cancel' + }, + { text: "Supprimer", style: 'destructive', onPress: () => this.deleteDiscussion(item._id) } + ] + ) + }> + <Text style={{ color: 'red', fontSize: 15 }}><Icon solid name='trash-alt' type='font-awesome-5' color={'red'} size={13} style={{marginRight: 5}}/>Supprimer</Text> + </MenuOption> + </MenuOptions> + </Menu> + </ListItem> )} /> </View> diff --git a/react-native/ChatApp/components/MessagesScreen.js b/react-native/ChatApp/components/MessagesScreen.js index ccba3d04e..4d4f8bd5e 100644 --- a/react-native/ChatApp/components/MessagesScreen.js +++ b/react-native/ChatApp/components/MessagesScreen.js @@ -43,7 +43,7 @@ class MessagesScreen extends React.Component { const token = await AsyncStorage.getItem('@token'); //if token is stored, user is logged in if (token !== null) { - fetch("http://163.172.178.146:3000/rooms/" + this.props.route.params.id + "/messages", { + fetch("http://163.172.178.146:3000/rooms/" + this.props.route.params.item._id + "/messages", { method: 'GET', headers: { Authorization: "Bearer " + token @@ -138,7 +138,7 @@ class MessagesScreen extends React.Component { }, body: JSON.stringify({ body: messages[0].text, - roomId: this.props.route.params.id, + roomId: this.props.route.params.item._id, }) }) .then(response => { diff --git a/react-native/ChatApp/components/UpdateDiscussionScreen.js b/react-native/ChatApp/components/UpdateDiscussionScreen.js new file mode 100644 index 000000000..ba15dde69 --- /dev/null +++ b/react-native/ChatApp/components/UpdateDiscussionScreen.js @@ -0,0 +1,109 @@ +import React from 'react'; +import { + View, + StyleSheet, + KeyboardAvoidingView, + ActivityIndicator, + Dimensions +} from 'react-native'; +import { Input, Button } from 'react-native-elements'; +import FontAwesome5 from 'react-native-vector-icons/FontAwesome5'; +import AsyncStorage from '@react-native-async-storage/async-storage'; + +const windowHeight = Dimensions.get('window').height; + +class UpdateDiscussionScreen extends React.Component { + + constructor(props) { + + super(props); + + this.state = { + name: this.props.route.params.item.name, + description: this.props.route.params.item.description, + users: this.props.route.params.item.users, + user: null, + isLoading: true + } + + } + + async componentDidMount() { + //get user from asyncStorage + try { + const user = await AsyncStorage.getItem('@user'); + if (user !== null) { + await this.setState({ user: JSON.parse(user) }); + } + else { + this.props.logoutAuthentication(); + } + } catch (error) { + console.warn(error); + } + + this.setState({ isLoading: false }); + + console.log(this.state.users); + } + + updateDiscussion() { + this.props.updateDiscussion(this.props.route.params.item._id, this.state.name, this.state.description, this.state.users, this.state.user, true); + } + + render() { + if (this.state.isLoading) + return ( + <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}> + <ActivityIndicator size="large" /> + </View> + ) + else + return ( + <KeyboardAvoidingView + behavior="height" + style={{flex:1, alignItems: 'center', justifyContent: 'center' }} + > + + <FontAwesome5 name={'comment'} solid color={'lightgrey'} size={150} /> + <View style={{alignItems: 'center', justifyContent: 'center', width: '100%' }}> + <Input + placeholder="Nom" + style={{ fontSize: 16, borderBottomColor: "grey" }} + onChangeText={value => this.setState({ name: value })} + placeholderTextColor={"black"} + defaultValue={this.state.name} + /> + <Input + placeholder="Description" + style={{ fontSize: 16, borderBottomColor: "grey" }} + onChangeText={value => this.setState({ description: value })} + placeholderTextColor={"black"} + defaultValue={this.state.description} + /> + <Button + containerStyle={styles.customButton} + title="Enregistrer" + onPress={this.updateDiscussion.bind(this)} + buttonStyle={{backgroundColor: 'dodgerblue'}} + titleStyle={{ marginLeft: 5 }} + icon={ + <FontAwesome5 name={'edit'} solid color={'white'} size={20} /> + } + /> + </View> + </KeyboardAvoidingView> + ) + } +}; + +const styles = StyleSheet.create({ + customButton: { + borderRadius: 1000, + width: 200, + textAlign: 'center', + backgroundColor: 'red' + } +}) + +export default UpdateDiscussionScreen; \ No newline at end of file diff --git a/react-native/ChatApp/package-lock.json b/react-native/ChatApp/package-lock.json index fd492981d..2c3446db3 100644 --- a/react-native/ChatApp/package-lock.json +++ b/react-native/ChatApp/package-lock.json @@ -8515,6 +8515,11 @@ "prop-types": "^15.7.x" } }, + "react-native-popup-menu": { + "version": "0.15.11", + "resolved": "https://registry.npmjs.org/react-native-popup-menu/-/react-native-popup-menu-0.15.11.tgz", + "integrity": "sha512-f5q2GoDN99bkA24wHiwasaErcdQEgyqYZ8IYuZPOrZNFr66E4rg6f4LElSVBA3EZJTSq5OddVeaOcU340bSTEg==" + }, "react-native-ratings": { "version": "7.3.0", "resolved": "https://registry.npmjs.org/react-native-ratings/-/react-native-ratings-7.3.0.tgz", @@ -8542,6 +8547,11 @@ "resolved": "https://registry.npmjs.org/react-native-screens/-/react-native-screens-2.16.1.tgz", "integrity": "sha512-WZ7m0sBDVaHbBnlHxwQnUlI6KNfQKHq+Unfw+VBuAlnSXvT+aw6Bb/K2bUlHzBgvrPjwY3Spc7ZERFuTwRLLwg==" }, + "react-native-sectioned-multi-select": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/react-native-sectioned-multi-select/-/react-native-sectioned-multi-select-0.8.1.tgz", + "integrity": "sha512-iH/5PHBo64yFhKzjd+6sCG0aEvgDnUwpn3I7R0ohcGo/+gJdw47mYmJMTwJTmR9JqBFXHx+2bkQi4ij2ghfqRQ==" + }, "react-native-size-matters": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/react-native-size-matters/-/react-native-size-matters-0.3.1.tgz", diff --git a/react-native/ChatApp/package.json b/react-native/ChatApp/package.json index 9593289a5..c101bd961 100644 --- a/react-native/ChatApp/package.json +++ b/react-native/ChatApp/package.json @@ -22,9 +22,11 @@ "react-native-gesture-handler": "^1.9.0", "react-native-gifted-chat": "^0.16.3", "react-native-paper": "^4.6.0", + "react-native-popup-menu": "^0.15.11", "react-native-reanimated": "^1.13.2", "react-native-safe-area-context": "^3.1.9", "react-native-screens": "^2.16.1", + "react-native-sectioned-multi-select": "^0.8.1", "react-native-vector-icons": "^7.1.0" }, "devDependencies": { -- GitLab