Professional Documents
Culture Documents
js
let timer;
try {
const result = await createUserWithEmailAndPassword(
auth,
email,
password
);
const { uid, stsTokenManager } = result.user;
} catch (error) {
console.log(error);
const errorCode = error.code;
try {
const result = await signInWithEmailAndPassword(
auth,
email,
password
);
const { uid, stsTokenManager } = result.user;
} catch (error) {
const errorCode = error.code;
//Store userId, auth token and expiry date as firebase auth tokens expire after 1 hr
const saveDataToStorage = (token, userId, expiryDate) => {
AsyncStorage.setItem("userData", JSON.stringify({
token,
userId,
expiryDate: expiryDate.toISOString()
}));
}
chatActions.js
import { child, get, getDatabase, push, ref, remove, set, update } from
"firebase/database";
import { getFirebaseApp } from "../firebaseHelper";
import { addUserChat, deleteUserChat, getUserChats } from "./userActions";
await update(chatRef, {
...chatData,
updatedAt: new Date().toISOString(),
updatedBy: userId})
}
if (type) {
messageData.type = type;
}
//Push the message data to the messages node for the particular chat
await push(messagesRef, messageData);
if (snapshot.exists()) {
//Favourited item exists - un-favourites the message
await remove(childRef);
}
else {
//Favourited item does not exist - favourite it
const starredMessageData = {
messageId,
chatId,
starredAt: new Date().toISOString()
}
//Adds favourited data in firebase
await set(childRef, starredMessageData);
}
} catch (error) {
console.log(error);
}
}
//Loop over the person's chats and removes the correct one
for (const key in userChats) {
const currentChatId = userChats[key];
newUsers.push(userToAddId);
if (newUsers.length === 0) {
return;
}
//Updates chatData in firebase
await updateChatData(chatData.key, userLoggedInData.userId,{ users:
existingUsers.concat(newUsers)})
//Displays a message showing who added whom to a new chat
const multipleUsersMessage = newUsers.length > 1 ? `and ${newUsers.length - 1}
others ` : "";
const messageText = `${userLoggedInData.firstName} ${userLoggedInData.lastName}
added ${userAddedName} ${multipleUsersMessage}to the chat`;
await sendInfoMessage(chatData.key, userLoggedInData.userId, messageText);
}
formAction.js
import {
validateEmail,
validateLength,
validatePassword,
validateString,
} from "../validationConstraints";
userActions.js
import { child, endAt, get, getDatabase, orderByChild, push, query, ref, remove,
startAt } from "firebase/database"
import { getFirebaseApp } from "../firebaseHelper";
await remove(chatRef);
} catch (error) {
console.log(error);
throw error;
}
};
await push(chatRef,chatId);
} catch (error) {
console.log(error);
throw error;
}
};
if(snapshot.exists()) {
return snapshot.val();
return {}
} catch (error) {
console.log(error)
throw error;
}
formReducer:
export const reducer = (state, action) => {
const { validationResult, inputId, inputValue } = action;
const updatedValues = {
...state.inputValues,
[inputId]: inputValue,
};
const updatedValidities = {
...state.inputValidities,
[inputId]: validationResult,
};
return {
inputValues: updatedValues,
inputValidities: updatedValidities,
formIsValid: updatedFormIsValid,
};
};
firebaseHelper.js
// Import the functions you need from the SDKs you need
import { initializeApp } from "firebase/app";
// Initialize Firebase
return initializeApp(firebaseConfig);
}
imagePickerHelper.js
import * as ImagePicker from "expo-image-picker";
import { Platform } from "react-native";
import { getFirebaseApp } from "./firebaseHelper";
import uuid from "react-native-uuid";
import {
getDownloadURL,
getStorage,
ref,
uploadBytesResumable,
} from "firebase/storage";
//Allows picking of image from media library and sets image settings
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.Images,
allowsEditing: true,
aspect: [1, 1],
quality: 1,
});
if (!result.canceled) {
return result.uri;
}
};
if (!result.canceled) {
return result.uri;
}
};
xhr.responseType = "blob";
xhr.open("GET", uri, true);
xhr.send();
});
return Promise.resolve();
};
validationConstraints.js
import { validate } from "validate.js";
};
if (minLength != null) {
constraints.length.minimum = minLength;
}
if (maxLength != null) {
constraints.length.maximum = maxLength;
}
}
{
/* Returns the error value in the array*/
}
return validationResult && validationResult[id];
};
Bubble.js
import React, { startTransition, useRef } from "react";
import { Image, StyleSheet, Text, TouchableWithoutFeedback, View } from
"react-native";
import { Menu, MenuTrigger, MenuOptions, MenuOption } from "react-native-popup-menu";
import colors from "../constants/colors";
import uuid from 'react-native-uuid';
import * as Clipboard from 'expo-clipboard';
import { Entypo, Feather, FontAwesome } from "@expo/vector-icons";
import { starMessage } from "./utils/actions/chatActions";
import { useSelector } from "react-redux";
case "error":
textStyle.color = "white";
bubbleStyle.backgroundColor = colors.red;
bubbleStyle.alignItems = "center";
bubbleStyle.marginTop = 10;
break;
case "myMessage":
wrapperStyle.justifyContent= "flex-end"
bubbleStyle.backgroundColor = "#E7FED5"
bubbleStyle.maxWidth = "90%";
Container = TouchableWithoutFeedback;
isUserMessage = true;
break;
case "theirMessage":
wrapperStyle.justifyContent = 'flex-start';
bubbleStyle.maxWidth = "90%"
Container = TouchableWithoutFeedback;
isUserMessage = true;
break;
case "info":
bubbleStyle.backgroundColor = "white";
bubbleStyle.alignItems = "center";
textStyle.color = colors.textColor;
break;
case "reply":
bubbleStyle.backgroundColor = "#F2F2F2";
break;
default:
break;
}
{replyingToUser && (
<Bubble
type="reply"
text={replyingTo.text}
name={`${replyingToUser.firstName} ${replyingToUser.lastName}`}
/>
)}
{!imageUrl &&
<Text style={textStyle}>{text}</Text>}
{imageUrl && (
<Image source={{ uri: imageUrl }} style={styles.image} />
)}
{
//Renders the time
dateString && type!== 'info' &&(
<View style={styles.timeContainer}>
{isStarred && (
<FontAwesome
name="star"
size={14}
color={colors.grey}
style={{ marginRight: 5 }}
/>
)}
<Text style={styles.time}>{dateString}</Text>
</View>
)
}
<MenuOptions>
<MenuItem
text="Copy this message"
icon={"copy"}
onSelect={() => copyToClipboard(text)}
/>
<MenuItem
//Boolean menu item that unfavourites and favourites messages
text={`${isStarred ? "Un-favourite" : "Favourite"} message`}
icon={isStarred ? "star" : "star-o"}
iconPack={FontAwesome}
onSelect={() => starMessage(messageId, chatId, userId)}
/>
<MenuItem
//Boolean menu item that unfavourites and favourites messages
text="Reply"
icon={"reply"}
iconPack={Entypo}
onSelect={setReply}
/>
</MenuOptions>
</Menu>
</View>
</Container>
</View>
);
}
CustomHeaderButton.js
import { Ionicons } from "@expo/vector-icons";
import React from "react";
import { HeaderButton } from "react-navigation-header-buttons";
import colors from "../constants/colors";
/>
};
DataItem.js
// import React from "react";
// import { StyleSheet, Text, TouchableWithoutFeedback, View } from "react-native";
// import colors from "../constants/colors";
// import ProfileImage from "./ProfileImage";
// import { Feather, Ionicons } from "@expo/vector-icons";
// const imgSize = 45
// return (
// <TouchableWithoutFeedback onPress={props.onPress}>
// <View style={styles.container}>
// {!icon && !hideImage && <ProfileImage uri={image} size={imgSize} />}
// {icon &&
// <View style={styles.leftIconContainer}>
// <Feather name= {icon} size={18} color= {colors.blue} />
// </View>
// }
// <View style={styles.textContainer}>
// <Text numberOfLines={1}
// style={{...styles.title ,...{color: type === 'button'?
colors.blue : colors.textColor}}}
// >
// {title}
// </Text>
// {subTitle &&
// <Text numberOfLines={1} style={styles.subTitle}>
// {subTitle}
// </Text>
// }
// </View>
// {type === "link" && //Handles styling of chevron to indicate its a link
// <View>
// <Feather name="chevron-right" size={18} color={colors.grey} />
// </View>
// }
// </View>
// </TouchableWithoutFeedback>
// );
// }
return (
<TouchableWithoutFeedback onPress={props.onPress}>
<View style={styles.container}>
{!icon && !hideImage && <ProfileImage uri={image} size={imageSize} />}
{icon &&
<View style={styles.leftIconContainer}>
<AntDesign name={icon} size={20} color={colors.blue} />
</View>
}
<View style={styles.textContainer}>
<Text
numberOfLines={1}
style={{
...styles.title,
...{ color: type === "button" ? colors.blue : colors.textColor },
}}
>
{title}
</Text>
{subTitle &&
<Text numberOfLines={1} style={styles.subTitle}>
{subTitle}
</Text>
}
</View>
input.Js
import { useState } from "react";
import { StyleSheet, Text, TextInput, View } from "react-native";
return (
<View style={styles.container}>
<Text style={styles.label}>{props.label}</Text>
<View style={styles.inputContainer}>
{props.icon && (
<props.iconPack
name={props.icon}
size={props.iconSize || 15}
style={styles.icon}
/>
)}
<TextInput
{ ...props }
style={styles.input}
onChangeText={onChangeText}
value = {value}/>
</View>
{
props.errorText &&
<View style={styles.errorContainer}>
<Text style={styles.errorText}> {props.errorText}</Text>
</View>
}
</View>
);
};
}
});
PageContainer.js
import { StyleSheet, View } from "react-native"
PageTitle.js
ProfileImage.js
import React, { useState } from "react";
import { ActivityIndicator, Image, StyleSheet, Text, TouchableOpacity, View } from
"react-native";
import { Feather } from "@expo/vector-icons";
//checking that if we have passed uri property to component, use that. if not, use
userImage
const source = props.uri ? { uri: props.uri } : userImage
if (!tempUri) return;
if (!uploadUrl){
throw new Error("The upload failed")
}
if (chatId) {
await updateChatData(chatId, userId, {chatImage: uploadUrl})
} else {
const newData = { profilePicture: uploadUrl };
return (
<Container style= {props.style} onPress={props.onPress || pickImage}>
{isLoading ? (
<View
height={props.size}
width={props.size}
style={styles.loadingContainer}
>
<ActivityIndicator size={"small"} color={colors.primary} />
</View>
) : (
<Image
style={{
...styles.image,
...{ width: props.size, height: props.size },
}}
source={image}
/>
)}
ReplyTo.js
import React from "react";
import { StyleSheet, Text, TouchableOpacity, View } from "react-native";
import colors from "../constants/colors";
import { AntDesign } from "@expo/vector-icons";
},
name: {
color: colors.blue,
fontFamily: 'semibold',
letterSpacing: 0.3
}
});
SignInForm.js
import React, { useCallback, useEffect, useReducer, useState } from 'react';
import { Feather } from "@expo/vector-icons";
import Input from "../components/Input";
import SubmitButton from "../components/SubmitButton";
const initialState = {
inputValues: {
email: isTestMode ? "fredleeyw@gmail.com" : "",
password: isTestMode ? "password" : "",
},
inputValidities: {
email: isTestMode,
password: isTestMode,
},
formIsValid: isTestMode,
};
useEffect(() => {
if (error) {
Alert.alert("Oh no! An error!", error, [{ text: "Try Again" }]);
}
}, [error]);
setError(null);
await dispatch(action);
} catch (error) {
setError(error.message);
setIsLoading(false);
}
}, [dispatch, formState]);
return (
<>
<Input
id="email"
label="Email"
icon="mail"
iconPack={Feather}
iconSize={"20"}
keyboardType="email-address"
autoCapitalize="none"
onInputChanged={inputChangedHandler}
initialValue={formState.inputValues.email}
errorText={formState.inputValidities["email"]}
/>
<Input
id="password"
label="Password"
icon="lock"
iconPack={Feather}
iconSize={"20"}
autoCapitalize="none"
secureTextEntry
onInputChanged={inputChangedHandler}
initialValue={formState.inputValues.password}
errorText={formState.inputValidities["password"]}
/>
{isLoading ? (
<ActivityIndicator
size={"small"}
color={colors.primary}
style={{ marginTop: 10 }}
/>
) : (
<SubmitButton
title="Sign in"
onPress={authHandler}
style={{ marginTop: 20 }}
disabled={!formState.formIsValid}
/>
)}
</>
);
};
SignUpForm.js
import React, { useCallback, useEffect, useReducer, useState } from 'react';
import { Feather } from "@expo/vector-icons";
const initialState = {
inputValues: {
firstName: '',
lastName: '',
email: '',
password: '',
},
inputValidities: {
firstName: false,
lastName: false,
email: false,
password: false,
},
formIsValid: false,
};
useEffect(() => {
if (error){
Alert.alert('Oh no! An error!', error, [{ text: "Try Again"}]);
}
}, [error])
return (
<>
< Input
id="firstName"
label="First Name"
icon="user"
iconPack={Feather}
iconSize={"20"}
onInputChanged={inputChangedHandler}
errorText={formState.inputValidities["firstName"]}
/>
<Input
id="lastName"
label="Last Name"
icon="user"
iconPack={Feather}
iconSize={"20"}
onInputChanged={inputChangedHandler}
errorText={formState.inputValidities["lastName"]}
/>
<Input
id="email"
label="Email"
icon="mail"
iconPack={Feather}
iconSize={"20"}
onInputChanged={inputChangedHandler}
keyboardType="email-address"
autoCapitalize="none"
errorText={formState.inputValidities["email"]}
/>
<Input
id="password"
label="Password"
icon="lock"
autoCapitalize="none"
secureTextEntry
iconPack={Feather}
iconSize={"20"}
onInputChanged={inputChangedHandler}
errorText={formState.inputValidities["password"]}
/>
{
isLoading ?
<ActivityIndicator size ={"small"} color = {colors.primary} style = {{ marginTop:
10}}/>:
<SubmitButton
title="Sign up"
onPress={authHandler}
style={{ marginTop: 20 }}
disabled={!formState.formIsValid}/>
}
</>
);
};
SubmitButton.js
import React from 'react';
import { StyleSheet, TouchableOpacity, Text } from 'react-native';
return <TouchableOpacity
onPress={props.disabled ? () => {} : props.onPress}
style={{
...styles.button,
...props.style,
...{backgroundColor: bgColor}}}>
<Text style={{ color: props.disabled ? colors.grey: 'white'}}>
{props.title}
</Text>
</TouchableOpacity>
};
Colors.js
export default {
blue: "#3497db",
lightGrey: "#bdc3c6",
extraLightGrey: '#ededec',
nearlyWhite: "#F4F8F7",
grey: "#7f8c7d",
textColor: "1c1e21",
primary: "#32d47e",
red: '#e74c3b',
beige: '#FEF5C2'
}
commonStyles.js
import { StyleSheet } from "react-native";
AppNavigator.js
import React from "react";
import { NavigationContainer } from "@react-navigation/native";
MainNavigator.js
//Imports expo notifications modules
import * as Device from "expo-device";
import * as Notifications from "expo-notifications";
//Creates the main navigator componenet that fetches chat data and renders the stack
navigator
const MainNavigator = (props) => {
const dispatch = useDispatch();
//Runs only on the first render, registers the app for push notifications
useEffect(() => {
//If theres token, set it in setExpoPushToken
registerForPushNotificationsAsync().then((token) =>
setExpoPushToken(token)
);
//Event handler that listens for notifications, if received
notificationListener.current =
Notifications.addNotificationReceivedListener((notification) => {
//Handle received notification CAN REMOVE
});
useEffect(() => {
console.log("subscribing to firebase listeners");
//When the app loads, we make a call to firebase to retrieve the chats the user's a
part of
const app = getFirebaseApp();
const dbRef = ref(getDatabase(app));
//Firebase listener
const userChatsRef = child(dbRef, `userChats/${userData.userId}`);
//Aggregates various ref listeners to group unsubscribe
const refs = [userChatsRef];
//Loop over all chats users are part of and get data for each chat
for (let i = 0; i < chatIds.length; i++) {
const chatId = chatIds[i];
const chatRef = child(dbRef, `chats/${chatId}`);
refs.push(chatRef);
if (data) {
if (!data.users.includes(userData.userId)) {
return;
}
data.key = chatSnapshot.key;
//Iterate over the users that are part of the chat, and retrieve the data
data.users.forEach((userId) => {
if (storedUsers[userId]) return;
get(userRef).then((userSnapshot) => {
const userSnapshotData = userSnapshot.val();
dispatch(setStoredUsers({ newUsers: { userSnapshotData } }));
});
refs.push(userRef);
});
if (chatsFoundCount === 0) {
setIsLoading(false);
}
}
});
return (
<KeyboardAvoidingView
style={{ flex: 1 }}
behavior={Platform.OS === "ios" ? "padding" : undefined}
>
<StackNavigator />
</KeyboardAvoidingView>
);
};
return token;
}
AuthScreen.js
import React, { useState } from "react";
import { View, Text, StyleSheet, TouchableOpacity, Image, ScrollView,
KeyboardAvoidingView, Platform } from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
<TouchableOpacity
onPress={() => setIsSignUp((prevState) => !prevState)}
style={styles.linkContainer}
>
<Text style={styles.link}>{`Switch to ${
isSignUp ? "sign in" : "sign up"
}`}</Text>
</TouchableOpacity>
</KeyboardAvoidingView>
</ScrollView>
</PageContainer>
</SafeAreaView>
);
};
ChatListScreen.js
import React, { useEffect } from "react";
import { View, Text, StyleSheet, Button, FlatList, TouchableOpacity } from
"react-native";
import { HeaderButtons, Item } from "react-navigation-header-buttons";
import { useSelector } from "react-redux";
import CustomHeaderButton from "../components/CustomHeaderButton";
import DataItem from "../components/DataItem";
import PageContainer from "../components/PageContainer";
import PageTitle from "../components/PageTitle";
import colors from "../constants/colors";
let chatData;
let navigationProps;
//Searches user chats, find one which includes selectedUser, and only if it isnt a
group chat
if (selectedUser) {
chatData = userChats.find(cd => !cd.isGroupChat &&
cd.users.includes(selectedUser))
}
//If chatData is set, set chatId as the only thing
if (chatData) {
navigationProps = {chatId: chatData.key}
}
//Otherwise, its a new chat, continue
else {
const chatUsers = selectedUserList || [selectedUser];
if (!chatUsers.includes(userData.userId)) {
chatUsers.push(userData.userId);
}
navigationProps = {
newChatData: {
users: chatUsers,
isGroupChat: selectedUserList !== undefined,
//chatName
},
};
}
if (chatName) {
navigationProps.chatName = chatName;
}
props.navigation.navigate("ChatScreen", navigationProps);
}, [props.route?.params]);
return (
<PageContainer>
<PageTitle text="Chats" />
if (isGroupChat) {
title =chatData.chatName;
image =chatData.chatImage;
}
else {
//Find the first user that isnt the user we are logged in as
const otherUserId = chatData.users.find(
(uid) => uid !== userData.userId
);
//accesses the stored user data
const otherUser = storedUsers[otherUserId];
if (!otherUser) return;
return (
<DataItem
title={title}
subTitle={subTitle}
image={image}
onPress={() =>
props.navigation.navigate("ChatScreen", { chatId })
}
/>
);
}}
/>
</PageContainer>
);
};
ChatScreen.js
import React, { useCallback, useEffect, useRef, useState } from "react";
import {
View,
Text,
StyleSheet,
Button,
ImageBackground,
TextInput,
TouchableOpacity,
Image,
ActivityIndicator,
} from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
import { Feather } from "@expo/vector-icons";
//Retrieves chat messages for specific chat through an array with the Id and message
inside
const chatMessages = useSelector((state) => {
//Handles brand new chat if there is nothing in it
if (!chatId) return [];
messageList.push({
key,
...message,
});
}
return messageList;
});
return (
otherUserData && `${otherUserData.firstName} ${otherUserData.lastName}`
);
};
props.navigation.setOptions({
//If chatname has value, use that, if not get title from name
headerTitle: chatData.chatName ?? getChatTitleFromName(),
//Handles chat settings UI
headerRight: () => {
return (
<HeaderButtons HeaderButtonComponent={CustomHeaderButton}>
{chatId &&
<Item
title="Chat settings"
iconName="settings-outline"
//Navigates to chatsettings page or contact page
onPress={() =>
chatData.isGroupChat ?
props.navigation.navigate("ChatSettings", { chatId }) :
props.navigation.navigate("Contact", {
uid: chatUsers.find(uid => uid !== userData.userId),
})
}
/>
}
</HeaderButtons>
);
},
});
setChatUsers(chatData.users);
}, [chatUsers]);
{
/* Function used to manage state changes and handle message sending*/
}
const sendMessage = useCallback(async () => {
try {
let id = chatId;
if (!id) {
//If there is no chatId, create the chat
id = await createChat(userData.userId, props.route.params.newChatData);
setChatId(id);
}
//Sends text message
await sendTextMessage(
id,
userData.userId,
messageText,
replyingTo && replyingTo.key
);
//Only clears the text box if the message sends
setMessageText("");
setReplyingTo(null);
} catch (error) {
console.log(error);
setErrorBannerText("Message has failed to send");
setTimeout(() => setErrorBannerText(""), 5000);
}
}, [messageText, chatId]);
setTempImageUri(tempUri);
} catch (error) {
console.log(error);
}
}, [tempImageUri]);
setTempImageUri(tempUri);
} catch (error) {
console.log(error);
}
}, [tempImageUri]);
try {
let id = chatId;
if (!id) {
//If there is no chatId, create the chat
id = await createChat(userData.userId, props.route.params.newChatData);
setChatId(id);
}
const uploadUrl = await uploadImageAsync(tempImageUri, true);
setIsLoading(false);
//Send image
await sendImage(
id,
userData.userId,
uploadUrl,
replyingTo && replyingTo.key
);
setReplyingTo(null);
return (
<SafeAreaView edges={["right", "left", "bottom"]} style={styles.container}>
<ImageBackground
source={backgroundImage}
style={styles.backgroundImage}
>
{/* all chats appear here- UI*/}
<PageContainer style={{ backgroundColor: "transparent" }}>
{!chatId && (
<Bubble text="This is a new chat. Say hello!" type="system" />
)}
return (
<Bubble
type={messageType}
text={message.text}
messageId={message.key}
userId={userData.userId}
chatId={chatId}
//If not groupchat, dont use name, if own message dont use
name,if groupchat use name
name={!chatData.isGroupChat || isOwnMessage ? undefined : name}
date={message.sentAt}
setReply={() => setReplyingTo(message)}
//Search the messages in state, and find one that equals the id
we are replying to
replyingTo={
message.replyTo &&
chatMessages.find((i) => i.key === message.replyTo)
}
imageUrl={message.imageUrl}
/>
);
}}
/>
)}
</PageContainer>
{/*Handles UI functions for replying of messages*/}
{replyingTo && (
<ReplyTo
text={replyingTo.text}
user={storedUsers[replyingTo.sentBy]}
onCancel={() => setReplyingTo(null)}
/>
)}
</ImageBackground>
<View style={styles.inputContainer}>
{/* Allows us to add an image from our photos*/}
<TouchableOpacity style={styles.mediaButton} onPress={pickImage}>
<Feather name="plus" size={24} color={colors.blue} />
</TouchableOpacity>
<TextInput
style={styles.textbox}
value={messageText}
onChangeText={(text) => setMessageText(text)}
onSubmitEditing={sendMessage}
/>
ChatSettingsScreen.js
import React, { useCallback, useEffect, useReducer, useState } from "react";
import { View, Text, StyleSheet, ScrollView, ActivityIndicator } from "react-native";
import { useSelector } from "react-redux";
import DataItem from "../components/DataItem";
import Input from "../components/Input";
import PageContainer from "../components/PageContainer";
import PageTitle from "../components/PageTitle";
import ProfileImage from "../components/ProfileImage";
import SubmitButton from "../components/SubmitButton";
import { addUsersToChat, removeUserFromChat, updateChatData } from
"../components/utils/actions/chatActions";
import { validateInput } from "../components/utils/actions/formActions";
import { reducer } from "../components/utils/reducers/formReducer";
import colors from "../constants/colors";
const initialState = {
inputValues: { chatName: chatData.chatName },
inputValidities: { chatName: undefined },
formIsValid: false,
};
if (!storedUsers[uid]) {
console.log('No user data found in the servers');
return;
}
//Pushes users id to the screen
selectedUserData.push(storedUsers[uid]);
});
}, [selectedUsers])
//Handles save button press, updates user data, and shows success message
const saveHandler = useCallback(async () => {
const updatedValues = formState.inputValues;
try {
setIsLoading(true);
await updateChatData(chatId, userData.userId, updatedValues);
setShowSuccessMessage(true);
setTimeout(() => {
setShowSuccessMessage(false);
}, 1000)(false);
} catch (error) {
console.log(error);
} finally {
setIsLoading(false);
}
}, [formState]);
props.navigation.popToTop();
} catch (error) {
console.log(error);
} finally {
setIsLoading(false);
}
}, [props.navigation, isLoading]);
return (
<PageContainer>
<PageTitle text="Chat Settings" />
{/*Allows scrolling for long content*/}
<ScrollView contentContainerStyle={styles.scrollView}>
{/*Component for displaying group chat images*/}
<ProfileImage
showEditButton={true}
size={80}
chatId={chatId}
userId={userData.userId}
uri={chatData.chatImage}
/>
<DataItem
title="Add users"
icon="plus"
type="button"
onPress={() =>
props.navigation.navigate("NewChat", {
isGroupChat: true,
existingUsers: chatData.users,
chatId,
})
}
/>
{/*Maps over all users in the chat and displays their profile info
Also slices to ensure that number of users in a groups doesn't overflow*/}
{chatData.users.slice(0, 4).map((uid) => {
const currentUser = storedUsers[uid];
return (
<DataItem
key={uid}
image={currentUser.profilePicture}
title={`${currentUser.firstName} ${currentUser.lastName}`}
subTitle={currentUser.about}
type={uid !== userData.userId && "link"}
onPress={() =>
uid !== userData.userId &&
props.navigation.navigate("Contact", { uid, chatId })
}
/>
);
})}
{
//Links to a new page with all the users, if no. of users in chat > 4
chatData.users.length > 4 && (
<DataItem
type={"link"}
title="View all"
hideImage={true}
//Passes through properties needed for setting the data list screen
onPress={() =>
props.navigation.navigate("DataList", {
title: "Chat Participants",
data: chatData.users,
type: "users",
chatId,
})
}
/>
)
}
</View>
{
<SubmitButton
title="Leave the Chat"
color={colors.red}
onPress={() => leaveChat()}
style={{ marginBottom: 20 }}
/>
}
</PageContainer>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: "center",
alignItems: "center",
},
scrollView: {
justifyContent: "center",
alignItems: "center",
},
sectionContainer: {
width: "100%",
marginTop: 10,
},
heading: {
color: colors.textColor,
marginVertical: 8,
fontFamily: "semibold",
letterSpacing: 0.3,
},
});
ContactScreen.js
import React, { useCallback, useEffect, useState } from "react";
import { ActivityIndicator, StyleSheet, Text, View } from "react-native";
import { useSelector } from "react-redux";
import DataItem from "../components/DataItem";
import PageContainer from "../components/PageContainer";
import ProfileImage from "../components/ProfileImage";
import SubmitButton from "../components/SubmitButton";
import { removeUserFromChat } from "../components/utils/actions/chatActions";
import { getUserChats } from "../components/utils/actions/userActions";
import colors from "../constants/colors";
getCommonUserChats();
}, [ ])
props.navigation.goBack();
} catch (error) {
console.log(error)
}
finally {
setIsLoading(false);
}
}, [props.navigation, isLoading])
{
chatData && chatData.isGroupChat &&
(
isLoading ?
<ActivityIndicator size= "small" color= {colors.primary} /> :
<SubmitButton
title= 'Remove from chat'
color = {colors.red}
onPress={removeFromChat}
/>
)
}
</PageContainer>
}
DataListScreen.js
import React, { useEffect } from 'react';
import { FlatList } from 'react-native-gesture-handler';
import { useSelector } from 'react-redux';
import DataItem from '../components/DataItem';
import PageContainer from '../components/PageContainer';
return <PageContainer>
<FlatList
//Renders list of groupchats
data= {data}
keyExtractor= {item=>item.messageId || item}
//Renders individual items in the list
renderItem ={(itemData) => {
//Initializes variables for items being rendered
let key, onPress, image, title, subTitle, itemType;
//Sets variables for users list
if (type === 'users') {
const uid=itemData.item;
const currentUser = storedUsers[uid];
if (!currentUser) return;
if (!messagesForChat){
return;
}
const messageData = messagesForChat[messageId];
//Retrieving the user data for the user that sent a given msg
const sender = messageData.sentBy &&
storedUsers[messageData.sentBy];
//Retrieving sender name
const name = sender && `${sender.firstName}
${sender.lastName}`;
return <DataItem
key = {key}
onPress = {onPress}
image = {image}
title = {title}
subTitle = {subTitle}
type = {itemType}
/>
}}
/>
</PageContainer>
};
NewChatScreen.js
import React, { useEffect, useRef, useState } from "react";
import { View, Text, StyleSheet, Button, TextInput, ActivityIndicator, FlatList } from
"react-native";
import { HeaderButtons, Item } from "react-navigation-header-buttons";
import CustomHeaderButton from "../components/CustomHeaderButton";
import PageContainer from "../components/PageContainer";
import { Feather } from "@expo/vector-icons";
import colors from "../constants/colors";
import commonStyles from "../constants/commonStyles";
import { searchUsers } from "../components/utils/actions/userActions";
import DataItem from "../components/DataItem";
import { useDispatch, useSelector } from "react-redux";
import { setStoredUsers } from "../store/userSlice";
import ProfileImage from "../components/ProfileImage";
//this will be the button that allows us to close the page or create group chat
useEffect(() => {
props.navigation.setOptions({
headerLeft: () => {
return <HeaderButtons HeaderButtonComponent={CustomHeaderButton}>
<Item title="Close" onPress={() => props.navigation.goBack()} />
</HeaderButtons>
},
headerRight: () => {
return <HeaderButtons HeaderButtonComponent={CustomHeaderButton}>
{
//Only renders if it is a group chat
//Handles the create button depending if user keys in chat name
//If not new chat, title will be Add instead of create
isGroupChat &&
<Item
title={isNewChat ? "Create" : "Add"}
disabled={isGroupChatDisabled}
color={isGroupChatDisabled ? colors.lightGrey : undefined}
onPress={() => {
const screenName = isNewChat ? "ChatList" : "ChatSettings";
props.navigation.navigate(screenName, {
selectedUsers,
chatName,
chatId,
});
}}
/>
}
</HeaderButtons>
},
headerTitle: isGroupChat ? "Add participants" : "New chat",
});
}, [chatName, selectedUsers]);
if (Object.keys(usersResult).length === 0) {
setNoResultsFound(true);
} else {
setNoResultsFound(false);
setSelectedUsers(newSelectedUsers);
} else {
props.navigation.navigate("ChatList", {
selectedUserId: userId,
});
}
};
{/* Handles the bar which adds selected user's profiles */}
{isGroupChat &&
<View style={styles.selectedUsersContainer}>
<FlatList
style={styles.selectedUsersList}
data={selectedUsers}
horizontal={true}
keyExtractor={(item) => item}
contentContainerStyle={{ alignItems: "center" }}
//Handles scrolling of the selected users' bar
ref={(ref) => (selectedUsersFlatList.current = ref)}
onContentSizeChange={() =>
selectedUsersFlatList.current.scrollToEnd()
}
//Renders the profile images
renderItem={(itemData) => {
const userId = itemData.item;
const userData = storedUsers[userId];
return (
<ProfileImage
style={styles.selectedUserStyle}
size={40}
uri={userData.profilePicture}
//Using the selected users function to remove users
onPress={() => userPressed(userId)}
showRemoveButton={true}
/>
);
}}
/>
</View>
}
<TextInput
placeholder="Search"
style={styles.searchBox}
onChangeText={(text) => setSearchTerm(text)}
/>
</View>
return (
//Returns the results of a search
<DataItem
title={`${userData.firstName} ${userData.lastName}`}
subTitle={userData.about}
image={userData.profilePicture}
onPress={() => userPressed(userId)}
//If groupchat, pass the checkbox type, if not, pass empty string
type={isGroupChat ? "checkbox" : ""}
isChecked={selectedUsers.includes(userId)}
/>
);
}}
/>
}
SettingsScreen.js
import { Feather } from "@expo/vector-icons";
import React, { useCallback, useMemo, useReducer, useState } from "react";
import { View, Text, StyleSheet, ActivityIndicator, ScrollView } from "react-native";
import { useDispatch, useSelector } from "react-redux";
import DataItem from "../components/DataItem";
import Input from "../components/Input";
import PageContainer from "../components/PageContainer";
import PageTitle from "../components/PageTitle";
import ProfileImage from "../components/ProfileImage";
import SubmitButton from "../components/SubmitButton";
import { updateSignedInUserData, userLogout } from
"../components/utils/actions/authActions";
import { validateInput } from "../components/utils/actions/formActions";
import { reducer } from "../components/utils/reducers/formReducer";
import colors from "../constants/colors";
import { updateLoggedInUserData } from "../store/authSlice";
//Destructures user data into separate variables, and sets default empty strings if
they don't exist
const firstName = userData.firstName || "";
const lastName = userData.lastName || "";
const email = userData.email || "";
const about = userData.about || "";
// Initializes form state with user data, and empty validities
const initialState = {
inputValues: {
firstName,
lastName,
email,
about,
},
inputValidities: {
firstName: undefined,
lastName: undefined,
email: undefined,
about: undefined,
},
formIsValid: false,
};
// Handles save button press, updates user data, and shows success message
const saveHandler = useCallback(async () => {
const updatedValues = formState.inputValues;
try {
setIsLoading(true);
await updateSignedInUserData(userData.userId, updatedValues);
dispatch(updateLoggedInUserData({ newData: updatedValues }));
setShowSuccessMessage(true);
setTimeout(() => {
setShowSuccessMessage(false);
}, 3000)(false);
} catch (error) {
console.log(error);
} finally {
setIsLoading(false);
}
}, [formState, dispatch]);
return (
currentValues.firstName != firstName ||
currentValues.lastName != lastName ||
currentValues.email != email ||
currentValues.about != about
);
};
return (
<PageContainer>
<PageTitle text="Settings" />
<ScrollView contentContainerStyle={styles.formContainer}>
<ProfileImage
size={80}
userId={userData.userId}
uri={userData.profilePicture}
showEditButton={true}
/>
<Input
id="firstName"
label="First name"
icon="user"
iconPack={Feather}
iconSize={"20"}
onInputChanged={inputChangedHandler}
errorText={formState.inputValidities["firstName"]}
initialValue={userData.firstName}
/>
<Input
id="lastName"
label="Last name"
icon="user"
iconPack={Feather}
iconSize={"20"}
onInputChanged={inputChangedHandler}
errorText={formState.inputValidities["lastName"]}
initialValue={userData.lastName}
/>
<Input
id="email"
label="Email"
icon="mail"
iconPack={Feather}
iconSize={"20"}
onInputChanged={inputChangedHandler}
keyboardType="email-address"
autoCapitalize="none"
errorText={formState.inputValidities["email"]}
initialValue={userData.email}
/>
<Input
id="about"
label="About"
icon="user"
iconPack={Feather}
iconSize={"20"}
onInputChanged={inputChangedHandler}
errorText={formState.inputValidities["about"]}
initialValue={userData.about}
/>
<View style={{ marginTop: 20 }}>
{showSuccessMessage && <Text> Saved!</Text>}
{isLoading ? (
<ActivityIndicator
size={"small"}
color={colors.primary}
style={{ marginTop: 10 }}
/>
) : (
hasChanges() && (
<SubmitButton
title="Save"
onPress={saveHandler}
style={{ marginTop: 20 }}
disabled={!formState.formIsValid}
/>
)
)}
</View>
<SubmitButton
title="Logout"
onPress={() => dispatch(userLogout())}
style={{ marginTop: 20 }}
color={colors.red}
/>
</ScrollView>
</PageContainer>
);
};
StartUpScreen.js
//Screen to check the storage and if data is there see if we can sign in
useEffect(()=>{
const tryLogin = async () => {
const storedAuthInfo = await AsyncStorage.getItem("userData");
if (!storedAuthInfo){
dispatch(setDidTryAutoLogin());
return;
}
tryLogin();
}, [dispatch])
authSlice.js
import { createSlice } from "@reduxjs/toolkit";
chatSlice.js
import { createSlice } from "@reduxjs/toolkit";
messageSlice.js
import { createSlice } from "@reduxjs/toolkit";
existingMessages[chatId] = messagesData;
//Updates the state
state.messagesData = existingMessages;
},
addStarredMessage: (state, action) => {
//Retrieves starredMessageData from action.payload;
const { starredMessageData } = action.payload;
//Updates starredMessages object with messageId and values to be
starredMessageData
state.starredMessages[starredMessageData.messageId] = starredMessageData;
},
removeStarredMessage: (state, action) => {
const { messageId } = action.payload;
//Deletes starredMessages item with this specific messageId
delete state.starredMessages[messageId];
},
setStarredMessages: (state, action) => {
//Retrieves all past starred messages
const { starredMessages } = action.payload;
//Sets initial value of preexisting starred messages
state.starredMessages = {...starredMessages};
},
},
});
export const
{setChatMessages,setStarredMessages,addStarredMessage,removeStarredMessage} =
messagesSlice.actions;
userSlice.js
import { createSlice } from "@reduxjs/toolkit";
state.storedUsers = existingUsers;
},
},
});
export const setStoredUsers = userSlice.actions.setStoredUsers;
export default userSlice.reducer;
App.js
import "react-native-gesture-handler";
import { LogBox, StyleSheet, Text } from "react-native";
import { SafeAreaProvider, SafeAreaView } from "react-native-safe-area-context";
import * as SplashScreen from "expo-splash-screen";
import { useEffect, useCallback, useState } from "react";
import * as Font from "expo-font";
import AppNavigator from "./navigation/AppNavigator";
import { Provider } from "react-redux";
import { store } from "./store/store";
import AsyncStorage from "@react-native-async-storage/async-storage";
import { MenuProvider } from "react-native-popup-menu";
LogBox.ignoreLogs([
"AsyncStorage has been extracted from react-native core and will be removed in a
future release.",
]);
//AsyncStorage.clear();
prepare();
}, []);
return (
<Provider store={store}>
<SafeAreaProvider style={styles.container} onLayout={onLayout}>
<MenuProvider>
<AppNavigator />
</MenuProvider>
</SafeAreaProvider>
</Provider>
);
}
Package.json
{
"name": "chatfinals",
"version": "1.0.0",
"main": "node_modules/expo/AppEntry.js",
"scripts": {
"start": "expo start",
"android": "expo start --android",
"ios": "expo start --ios",
"web": "expo start --web"
},
"dependencies": {
"@expo/vector-icons": "^13.0.0",
"@react-native-async-storage/async-storage": "^1.17.11",
"@react-navigation/bottom-tabs": "^6.5.5",
"@react-navigation/native-stack": "^6.9.12",
"@react-navigation/stack": "^6.3.14",
"@reduxjs/toolkit": "^1.9.3",
"expo": "~47.0.12",
"expo-clipboard": "~4.0.1",
"expo-image-picker": "^14.1.1",
"expo-splash-screen": "~0.17.5",
"expo-status-bar": "~1.4.2",
"firebase": "^9.17.1",
"react": "18.1.0",
"react-native": "0.70.5",
"react-native-awesome-alerts": "^2.0.0",
"react-native-gesture-handler": "~2.8.0",
"react-native-popup-menu": "^0.16.1",
"react-native-safe-area-context": "4.4.1",
"react-native-screens": "~3.18.0",
"react-native-uuid": "^2.0.1",
"react-navigation-header-buttons": "^10.0.0",
"react-redux": "^8.0.5",
"validate.js": "^0.13.1",
"expo-notifications": "~0.17.0",
"expo-device": "~5.0.0"
},
"devDependencies": {
"@babel/core": "^7.12.9"
},
"private": true
}