You are on page 1of 102

AuthActions.

js

import { getFirebaseApp } from '../firebaseHelper';


import { createUserWithEmailAndPassword, getAuth, signInWithEmailAndPassword } from
'firebase/auth';
import { child, getDatabase, ref, set, update } from 'firebase/database';
import { authenticate, logout } from '../../../store/authSlice';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { getUserData } from './userActions';

let timer;

export const signUp = (firstName, lastName, email, password) => {


return async dispatch => {
const app = getFirebaseApp();
const auth = getAuth(app);

try {
const result = await createUserWithEmailAndPassword(
auth,
email,
password
);
const { uid, stsTokenManager } = result.user;

const { accessToken, expirationTime } = stsTokenManager;

const expiryDate = new Date(expirationTime);


const timeNow = new Date();
const millisecondsUntilExpiry = expiryDate - timeNow;

const userData = await createUser(firstName, lastName, email, uid);

dispatch(authenticate({token: accessToken, userData}));


saveDataToStorage(accessToken, uid, expiryDate);

timer = setTimeout(() => {


dispatch(userLogout());
}, millisecondsUntilExpiry);

} catch (error) {
console.log(error);
const errorCode = error.code;

let message = "Something is wrong.";

if (errorCode === "auth/email-already-in-use") {


message = "This email is already in use";
}

throw new Error(message);


}
}

export const userLogout = () => {


return async dispatch => {
AsyncStorage.clear();
clearTimeout(timer);
dispatch(logout());
}
}

export const updateSignedInUserData = async (userId, newData) => {


if (newData.firstLast && newData.lastName) {
const firstLast = `${newData.firstName} ${newData.lastName}`.toLowerCase();
newData.firstLast = firstLast;
}

const dbRef = ref(getDatabase());


const childRef = child(dbRef, `users/${userId}`);
await update(childRef, newData);
}

export const signIn = (email, password) => {


return async (dispatch) => {
const app = getFirebaseApp();
const auth = getAuth(app);

try {
const result = await signInWithEmailAndPassword(
auth,
email,
password
);
const { uid, stsTokenManager } = result.user;

const { accessToken, expirationTime } = stsTokenManager;

const expiryDate = new Date(expirationTime);


const timeNow = new Date();
const millisecondsUntilExpiry = expiryDate - timeNow;

const userData = await getUserData(uid);

dispatch(authenticate({ token: accessToken, userData }));

saveDataToStorage(accessToken, uid, expiryDate);

timer = setTimeout(() => {


dispatch(userLogout());
}, millisecondsUntilExpiry);

} catch (error) {
const errorCode = error.code;

let message = "Something is wrong.";

if (errorCode === "auth/wrong-password" || errorcode === "auth/user-not-found") {


message = "The username or password is incorrect";
}

throw new Error(message);


}
};
};

//This is the user login data we want to store in the database


const createUser = async (firstName, lastName, email, userId) => {
const firstLast = `${firstName} ${lastName}`.toLowerCase();
const userData = {
firstName,
lastName,
firstLast,
email,
userId,
signUpDate: new Date().toISOString()
};
//gives us the reference to the real time database
const dbRef = ref(getDatabase());
const childRef = child(dbRef, `users/${userId}`);
await set(childRef, userData);
return userData;
}

//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";

export const createChat = async (loggedInUserId, chatData) => {


//Gets the newChat data
const newChatData = {
...chatData,
createdBy: loggedInUserId,
updatedBy: loggedInUserId,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
};

const app = getFirebaseApp();


const dbRef = ref(getDatabase(app));
//Creates a new chat and adds to firebase with push
const newChat = await push(child(dbRef, 'chats'), newChatData);
//Adds one entry for every user in the userChats node with each chatId
const chatUsers = newChatData.users;
for (let i = 0; i < chatUsers.length; i++) {
const userId = chatUsers[i];
await push(child(dbRef, `userChats/${userId}`), newChat.key);
}
//Returns the id of the newly created chat
return newChat.key;
}

//Function to send text message only


export const sendTextMessage = async (chatId, senderId, messageText, replyTo) => {
await sendMessage(chatId, senderId, messageText, null, replyTo, null);
}

//Function that notifies group that a user was removed


export const sendInfoMessage = async (chatId, senderId, messageText) => {
await sendMessage(chatId, senderId, messageText, null, null, 'info');
};

//Function to send image message only


export const sendImage = async (chatId, senderId, imageUrl, replyTo) => {
await sendMessage(chatId, senderId, 'Image', imageUrl, replyTo, null);
}

export const updateChatData = async (chatId, userId, chatData) => {


const app = getFirebaseApp();
const dbRef = ref(getDatabase(app));
const chatRef = child(dbRef, `chats/${chatId}`);

await update(chatRef, {
...chatData,
updatedAt: new Date().toISOString(),
updatedBy: userId})
}

//Refactored code to include all types of message sent - to be called above


const sendMessage = async (chatId, senderId, messageText, imageUrl, replyTo, type) =>
{
const app = getFirebaseApp();
const dbRef = ref(getDatabase());
const messagesRef = child(dbRef, `messages/${chatId}`);

//Message data to be sent to database


const messageData = {
sentBy: senderId,
sentAt: new Date().toISOString(),
text: messageText,
};
//Handles sending of reply
if (replyTo) {
messageData.replyTo = replyTo;
}
//Handles sending of image
if (imageUrl) {
messageData.imageUrl = imageUrl;
}

if (type) {
messageData.type = type;
}

//Push the message data to the messages node for the particular chat
await push(messagesRef, messageData);

//Updates chat message data that was sent


const chatRef = child(dbRef, `chats/${chatId}`);
await update(chatRef, {
updatedBy: senderId,
updatedAt: new Date().toISOString(),
latestMessageText: messageText,
});
}

//Function that favourites messages


export const starMessage = async (messageId, chatId, userId) => {
try {
const app = getFirebaseApp();
const dbRef = ref(getDatabase(app));
const childRef = child(dbRef,
`userFavouritedMessages/${userId}/${chatId}/${messageId}`);
//Check if the favourited message already exists
const snapshot = await get(childRef);

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);
}
}

export const removeUserFromChat = async (userLoggedInData, userToRemoveData, chatData)


=> {
const userToRemoveId = userToRemoveData.userId;
const newUsers = chatData.users.filter((uid) => uid !== userToRemoveId);
await updateChatData(chatData.key, userLoggedInData.userId, {
users: newUsers,
});

//Gets userId of person to be removed


const userChats = await getUserChats(userToRemoveId);

//Loop over the person's chats and removes the correct one
for (const key in userChats) {
const currentChatId = userChats[key];

if (currentChatId === chatData.key) {


await deleteUserChat(userToRemoveId, key);
break;
}
}
//Handles infomessage sent
const messageText = userLoggedInData.userId === userToRemoveData.userId ?
`${userLoggedInData.firstName} has left the chat` :
`${userLoggedInData.firstName} removed ${userToRemoveData.firstName} from the chat`

await sendInfoMessage(chatData.key, userLoggedInData.userId, messageText)

//Adds users to group chat


export const addUsersToChat = async (userLoggedInData, usersToAddData, chatData) => {
//Gives us id for all users already in chat
const existingUsers = Object.values(chatData.users);
//Array to track the new users
const newUsers = [];

//Tracks new user's name


let userAddedName = ''

usersToAddData.forEach(async userToAdd =>{


const userToAddId = userToAdd.userId;

//Make sure we dont add users twice


if (existingUsers.includes(userToAddId)) return;

newUsers.push(userToAddId);

//Adds chat to user's chatlist


await addUserChat(userToAddId, chatData.key);

//Contains name of a given user


userAddedName = `${userToAdd.firstName} ${userToAdd.lastName}`;
})

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";

export const validateInput = (inputId, inputValue) => {


if (inputId === "firstName" || inputId === "lastName") {
return validateString(inputId, inputValue);
} else if (inputId === "email") {
return validateEmail(inputId, inputValue);
} else if (inputId === "password") {
return validatePassword(inputId, inputValue);
} else if (inputId === "about") {
return validateLength(inputId, inputValue, 0, 150, true);
} else if (inputId === "chatName") {
return validateLength(inputId, inputValue, 5, 150, false);
}
};

userActions.js
import { child, endAt, get, getDatabase, orderByChild, push, query, ref, remove,
startAt } from "firebase/database"
import { getFirebaseApp } from "../firebaseHelper";

//Gets other users' data


export const getUserData = async (userId) => {
try {
const app = getFirebaseApp();
const dbRef = ref(getDatabase(app));
const userRef = child(dbRef, `users/${userId}`);

const snapshot = await get(userRef);


return snapshot.val();
} catch (error) {
console.log(error);
}
}

//Gets current/own users data


export const getUserChats = async (userId) => {
try {
const app = getFirebaseApp();
const dbRef = ref(getDatabase(app));
const userRef = child(dbRef, `userChats/${userId}`);

const snapshot = await get(userRef);


return snapshot.val();
} catch (error) {
console.log(error);
}
};

//Deletes a user's chat


export const deleteUserChat = async (userId, key) => {
try {
const app = getFirebaseApp();
const dbRef = ref(getDatabase(app));
const chatRef = child(dbRef, `userChats/${userId}/${key}`);

await remove(chatRef);
} catch (error) {
console.log(error);
throw error;
}
};

//Adds a user to group chat


export const addUserChat = async (userId, chatId) => {
try {
const app = getFirebaseApp();
const dbRef = ref(getDatabase(app));
const chatRef = child(dbRef, `userChats/${userId}`);

await push(chatRef,chatId);
} catch (error) {
console.log(error);
throw error;
}
};

//handles search for users


export const searchUsers = async (queryText) => {
const searchTerm = queryText.toLowerCase();
try {
const app = getFirebaseApp();
const dbRef = ref(getDatabase(app));
const userRef = child(dbRef, "users");

const queryRef = query(userRef, orderByChild('firstLast'), startAt(searchTerm),


endAt(searchTerm + "\uf8ff"));

const snapshot = await get(queryRef);

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,
};

let updatedFormIsValid = true;

for (const key in updatedValidities) {


if (updatedValidities[key] !== undefined) {
updatedFormIsValid = false;
break;
}
}

return {
inputValues: updatedValues,
inputValidities: updatedValidities,
formIsValid: updatedFormIsValid,
};
};

firebaseHelper.js
// Import the functions you need from the SDKs you need
import { initializeApp } from "firebase/app";

export const getFirebaseApp = () => {


// TODO: Add SDKs for Firebase products that you want to use
// https://firebase.google.com/docs/web/setup#available-libraries

// Your web app's Firebase configuration


// For Firebase JS SDK v7.20.0 and later, measurementId is optional
const firebaseConfig = {
apiKey: "AIzaSyAtDUax68nw_gRejA_WgPmzIVSssnGtfz0",
authDomain: "chatfinals.firebaseapp.com",
databaseURL:
"https://chatfinals-default-rtdb.asia-southeast1.firebasedatabase.app",
projectId: "chatfinals",
storageBucket: "chatfinals.appspot.com",
messagingSenderId: "126597456010",
appId: "1:126597456010:web:fcfe12ec6d0d0b63b60d73",
measurementId: "G-SXPB30RS9H",
};

// 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";

export const launchImagePicker = async () => {


//Checks if access to media is granted
await checkMediaPermissions();

//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;
}
};

export const openCamera = async () => {


//Checks if permission granted to app to access camera
const permissionResult = await ImagePicker.requestCameraPermissionsAsync();

if (permissionResult.granted === false) {


console.log("Permission to access camera not granted")
return;
}
//Allows picking of image from camera and sets image settings
const result = await ImagePicker.launchCameraAsync({
mediaTypes: ImagePicker.MediaTypeOptions.Images,
allowsEditing: true,
aspect: [1, 1],
quality: 1,
});

if (!result.canceled) {
return result.uri;
}
};

//Uploads image to firebase storage and returns URL


export const uploadImageAsync = async (uri, isChatImage = false ) => {
const app = getFirebaseApp();

//Creates a blob object from the image data


const blob = await new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.onload = function () {
resolve(xhr.response);
};

xhr.onerror = function (e) {


console.log(e);
reject(new TypeError("Network request failed"));
};

xhr.responseType = "blob";
xhr.open("GET", uri, true);
xhr.send();
});

//Determines which folder to upload images to


const pathFolder = isChatImage ? "chatImages" : "profilePics";
//Storage reference variable to the file and upload to firebase
const storageRef = ref(getStorage(app), `${pathFolder}/${uuid.v4()}`);
await uploadBytesResumable(storageRef, blob);
blob.close();

//Get the download URL of the uploaded image and returns it


return await getDownloadURL(storageRef);
};

//Checks if user has granted permission to access photos


const checkMediaPermissions = async () => {
if (Platform.OS !== "web") {
const permissionResult =
await ImagePicker.requestMediaLibraryPermissionsAsync();
if (permissionResult.granted === false) {
return Promise.reject("We need permission to access your photos");
}
}

return Promise.resolve();
};

validationConstraints.js
import { validate } from "validate.js";

export const validateString = (id, value) => {


const constraints = {
presence: { allowEmpty: false },
};

if (value !== "") {


constraints.format = {
pattern: "[a-z]+",
flags: "i",
message: "values can only contain letters",
};
}

const validationResult = validate({ [id]: value }, { [id]: constraints });

{/* Returns the error value in the array*/}


return validationResult && validationResult[id];
};

export const validateEmail = (id, value) => {


const constraints = {
presence: { allowEmpty: false },
};

if (value !== "") {


constraints.email = true
};

const validationResult = validate({ [id]: value }, { [id]: constraints });

/* Returns the error value in the array*/

return validationResult && validationResult[id];


};

export const validatePassword = (id, value) => {


const constraints = {
presence: { allowEmpty: false },
};

if (value !== "") {


constraints.length = {
minimum: 6,
message: "must be at least 6 characters"
};
}

const validationResult = validate({ [id]: value }, { [id]: constraints });

/* Returns the error value in the array*/


return validationResult && validationResult[id];
};

export const validateLength = (id, value, minLength, maxLength, allowEmpty) => {


const constraints = {
presence: { allowEmpty },
};
if (!allowEmpty || value !== "") {
constraints.length = {

};

if (minLength != null) {
constraints.length.minimum = minLength;
}

if (maxLength != null) {
constraints.length.maximum = maxLength;
}
}

const validationResult = validate({ [id]: value }, { [id]: constraints });

{
/* 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";

//Formats the time to X:XXampm


function formatAmPm(dateString) {
var date = new Date(dateString);
var hours = date.getHours();
var minutes = date.getMinutes();
var ampm = hours >= 12 ? "pm" : "am";
hours = hours % 12;
hours = hours ? hours : 12; // the hour '0' should be '12'
minutes = minutes < 10 ? "0" + minutes : minutes;
var strTime = hours + ":" + minutes + " " + ampm;
return hours + ':' + minutes + ' ' + ampm;
}

//Component for a single menu item


const MenuItem = props => {

const Icon = props.iconPack ?? Feather

//Styling menu item container popup


return <MenuOption onSelect={props.onSelect}>
<View style = {styles.menuItemContainer}>
<Text style = {styles.menuText}>{props.text}</Text>
<Icon name={props.icon} size = {18} />
</View>
</MenuOption>
}
//Component for a message bubble
const Bubble = props => {
//Create the respective props
const {text, type, messageId, chatId, userId, date, setReply, replyingTo, name,
imageUrl} = props;
//Gets starredMessages
const starredMessages = useSelector(state => state.messages.starredMessages[chatId]
?? {});
const storedUsers = useSelector(state => state.users.storedUsers);
//Switches bubble style according to the type of message sent
const bubbleStyle = {...styles.container };
const textStyle = {...styles.text};
const wrapperStyle = {...styles.wrapperStyle}

const menuRef = useRef(null);


const id = useRef(uuid.v4());
let Container = View;
let isUserMessage = false;
const dateString = date && formatAmPm(date);

//Handles different stylings of the bubbles for different message types


switch (type) {
case "system":
textStyle.color = "#65644B";
bubbleStyle.backgroundColor = colors.beige;
bubbleStyle.alignItems = "center";
bubbleStyle.marginTop = 10;
break;

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;
}

//Copy text to clipboard function


const copyToClipboard = async text => {
await Clipboard.setStringAsync(text);
}

//Checks if user message is starred


const isStarred = isUserMessage && starredMessages[messageId] !== undefined;
//Contains the user data of the person we are replying to
const replyingToUser = replyingTo && storedUsers[replyingTo.sentBy];
//Renders the long press menu for the message bubbles
return (
<View style={wrapperStyle}>
<Container
//Triggers the opening of the menu
onLongPress={() =>
menuRef.current.props.ctx.menuActions.openMenu(id.current)
}
style={{ width: "100%" }}
>
<View style={bubbleStyle}>
{name && type != 'info' && <Text style={styles.name}> {name} </Text>}

{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>
)
}

<Menu name={id.current} ref={menuRef}>


<MenuTrigger />

<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>
);
}

const styles = StyleSheet.create({


wrapperStyle: {
flexDirection: "row",
justifyContent: "center",
},
container: {
backgroundColor: "white",
borderRadius: 6,
padding: 5,
marginBottom: 10,
borderColor: "#E2DACB",
borderWidth: 1,
},
text: {
fontFamily: "regular",
letterSpacing: 0.3,
},
menuItemContainer: {
flexDirection: 'row',
padding: 5
},
menuText: {
flex: 1,
fontFamily: 'regular',
letterSpacing: 0.3,
fontSize: 16
},
timeContainer: {
flexDirection: 'row',
justifyContent: 'flex-end'
},
time: {
fontFamily: 'regular',
letterSpacing: 0.3,
color: colors.grey,
fontSize: 12
},
name:{
fontFamily: 'semibold',
letterSpacing: 0.3
},
image: {
width: 300,
height: 300,
marginBottom: 5
}
});

export default Bubble;

CustomHeaderButton.js
import { Ionicons } from "@expo/vector-icons";
import React from "react";
import { HeaderButton } from "react-navigation-header-buttons";
import colors from "../constants/colors";

const CustomHeaderButton = props => {


return <HeaderButton
{ ...props}
IconComponent={Ionicons}
iconSize ={22}
color={props.color ?? colors.blue}

/>
};

export default CustomHeaderButton;

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

// const DataItem = props => {

// const { title, subTitle, image, type, isChecked, icon } = props;

// const hideImage = props.hideImage && props.hideImage === true;

// 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 === "checkbox" && //Handles styling of checkbox for selecting


group users
// <View
// style={{
// ...styles.iconContainer,
// ...(isChecked && styles.checkedStyle)
// }}
// >
// <Ionicons name="checkmark" size={18} color="white" />
// </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>
// );
// }

// const styles = StyleSheet.create({


// container: {
// flexDirection: "row",
// paddingVertical: 7,
// borderBottomColor: colors.extraLightGrey,
// borderBottomWidth: 1,
// alignItems: "center",
// minHeight: 50,
// },
// textContainer: {
// marginLeft: 14,
// flex: 1,
// },
// title: {
// fontFamily: "semibold",
// fontSize: 16,
// letterSpacing: 0.3,
// },
// subTitle: {
// fontFamily: "regular",
// color: colors.grey,
// letterSpacing: 0.3,
// },
// iconContainer: {
// borderWidth: 1,
// borderRadius: 50,
// borderColor: colors.lightGrey,
// backgroundColor: "white",
// },
// checkedStyle: {
// backgroundColor: colors.primary,
// borderColor: "transparent",
// },
// leftIconContainer: {
// backgroundColor: colors.extraLightGrey,
// alignItems: "center",
// borderRadius: 50,
// justifyContent: "center",
// width: imgSize,
// height: imgSize
// },
// });

// export default DataItem

import React from "react";


import { StyleSheet, Text, TouchableWithoutFeedback, View } from "react-native";
import colors from "../constants/colors";
import ProfileImage from "./ProfileImage";
import { Ionicons, AntDesign } from "@expo/vector-icons";

const imageSize = 40;

const DataItem = (props) => {


const { title, subTitle, image, type, isChecked, icon } = props;

const hideImage = props.hideImage && props.hideImage === true;

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>

{type === "checkbox" &&


<View
style={{
...styles.iconContainer,
...(isChecked && styles.checkedStyle),
}}
>
<Ionicons name="checkmark" size={18} color="white" />
</View>
}

{type === "link" &&


<View>
<Ionicons
name="chevron-forward-outline"
size={18}
color={colors.grey}
/>
</View>
}
</View>
</TouchableWithoutFeedback>
);
};
const styles = StyleSheet.create({
container: {
flexDirection: "row",
paddingVertical: 7,
borderBottomColor: colors.extraLightGrey,
borderBottomWidth: 1,
alignItems: "center",
minHeight: 50,
},
textContainer: {
marginLeft: 14,
flex: 1,
},
title: {
fontFamily: "semibold",
fontSize: 16,
letterSpacing: 0.3,
},
subTitle: {
fontFamily: "regular",
color: colors.grey,
letterSpacing: 0.3,
},
iconContainer: {
borderWidth: 1,
borderRadius: 50,
borderColor: colors.lightGrey,
backgroundColor: "white",
},
checkedStyle: {
backgroundColor: colors.primary,
borderColor: "transparent",
},
leftIconContainer: {
backgroundColor: colors.extraLightGrey,
borderRadius: 50,
alignItems: "center",
justifyContent: "center",
width: imageSize,
height: imageSize,
},
});

export default DataItem;

input.Js
import { useState } from "react";
import { StyleSheet, Text, TextInput, View } from "react-native";

import colors from "../constants/colors";

const Input = (props) => {

const [value, setValue] = useState(props.initialValue)


//console.log(value);

const onChangeText = text => {


setValue(text);
props.onInputChanged(props.id, text)

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>
);
};

const styles = StyleSheet.create({


container: {
width: '100%'
},
label: {
marginVertical:8,
fontFamily: 'semibold',
letterSpacing: 0.3,
color: colors.textColor
},
inputContainer: {
width: '100%',
paddingHorizontal: 10,
paddingVertical: 15,
borderRadius: 2,
backgroundColor: colors.nearlyWhite,
flexDirection: 'row'
},
icon: {
marginRight: 10,
color: colors.grey,
},
input: {
color: colors.textColor,
flex: 1,
fontFamily: 'regular',
letterSpacing: 0.3,
paddingTop: 0
},
errorContainer: {
marginVertical: 5
},
errorText: {
color: 'red',
fontSize: 13,
fontFamily: 'regular',
letterSpacing: 0.3

}
});

export default Input;

PageContainer.js
import { StyleSheet, View } from "react-native"

const PageContainer = props => {


return (
<View style={{ ...styles.container, ...props.style }}>
{props.children}
</View>
);
};

const styles = StyleSheet.create({


container: {
paddingHorizontal: 20,
flex: 1,
backgroundColor: 'white'
}
})

export default PageContainer;

PageTitle.js

import { StyleSheet, Text, View } from "react-native"


import colors from "../constants/colors"

export default PageTitle = props => {


return <View style={styles.container}>
<Text style={styles.text}>{props.text} </Text>
</View>
}

const styles = StyleSheet.create({


container: {
marginBottom: 10
},
text: {
fontSize: 28,
color: colors.textColor,
fontFamily: 'semibold',
letterSpacing: 0.3
}
})

ProfileImage.js
import React, { useState } from "react";
import { ActivityIndicator, Image, StyleSheet, Text, TouchableOpacity, View } from
"react-native";
import { Feather } from "@expo/vector-icons";

import userImage from '../assets/images/profilepic.jpg';


import colors from "../constants/colors";
import { launchImagePicker, uploadImageAsync } from "./utils/imagePickerHelper";
import { updateSignedInUserData } from "./utils/actions/authActions";
import { useDispatch } from "react-redux";
import { updateLoggedInUserData } from "../store/authSlice";
import { updateChatData } from "./utils/actions/chatActions";

const ProfileImage = props => {


const dispatch = useDispatch();

//checking that if we have passed uri property to component, use that. if not, use
userImage
const source = props.uri ? { uri: props.uri } : userImage

const [image, setImage] = useState(source);


const [isLoading, setIsLoading] = useState(false);
//If there is a showEditButton passed in and the value is true, it will reflect in
var
const showEditButton = props.showEditButton && props.showEditButton === true

//True if showRemoveButton is passed, and value is true


const showRemoveButton = props.showRemoveButton && props.showRemoveButton === true;

const userId = props.userId;


const chatId = props.chatId;

const pickImage = async () => {


try{
const tempUri = await launchImagePicker();

if (!tempUri) return;

//Upload the image


setIsLoading(true);
const uploadUrl = await uploadImageAsync(tempUri , chatId !== undefined);
setIsLoading(false);

if (!uploadUrl){
throw new Error("The upload failed")
}

if (chatId) {
await updateChatData(chatId, userId, {chatImage: uploadUrl})
} else {
const newData = { profilePicture: uploadUrl };

await updateSignedInUserData(userId, newData);


dispatch(updateLoggedInUserData({ newData }));
}

//Set the image


setImage({ uri: uploadUrl});
}
catch (error) {
console.log(error);
setIsLoading(false);
}
}
//Handles the editviews
const Container = props.onPress || showEditButton ? TouchableOpacity : View;

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}
/>
)}

{/*Handles the editing of profile pictures */}


{showEditButton && !isLoading && (
<View style={styles.editIconContainer}>
<Feather name="edit-3" size={15} color="black" />
</View>
)}

{/*Handles the removal of profile pictures */}


{showRemoveButton && !isLoading && (
<View style={styles.removeIconContainer}>
<Feather name="x" size={15} color="black" />
</View>
)}
</Container>
);
};

const styles = StyleSheet.create({


image: {
borderRadius: 50,
borderColor: colors.grey,
borderWidth: 1,
},
editIconContainer: {
position: "absolute",
bottom: -5,
right: -5,
backgroundColor: colors.lightGrey,
borderRadius: 20,
padding: 8,
},
loadingContainer: {
justifyContent: "center",
alignItems: "center",
},
removeIconContainer: {
position: "absolute",
bottom: -3,
right: -3,
backgroundColor: colors.lightGrey,
borderRadius: 20,
padding: 3,
},
});

export default ProfileImage;

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";

const ReplyTo = props => {


//Extract relevant props
const {text,user,onCancel} = props;
//Extract name
const name = `${user.firstName} ${user.lastName}`;
//Styles and outputs the reply feature
return (
<View style={styles.container}>
<View style={styles.textContainer}>
<Text numberOfLines={1} style={styles.name}>
{name}
</Text>
<Text numberOfLines={1}>{text}</Text>
</View>
<TouchableOpacity onPress={onCancel}>
<AntDesign name="closecircleo" size={24} color={colors.blue} />
</TouchableOpacity>
</View>
);
}

const styles = StyleSheet.create({


container:{
backgroundColor: colors.extraLightGrey,
padding: 8,
flexDirection: 'row',
alignItems: 'center',
borderLeftColor: colors.blue,
borderLeftWidth: 4
},
textContainer: {
flex: 1,
marginRight: 5

},
name: {
color: colors.blue,
fontFamily: 'semibold',
letterSpacing: 0.3
}
});

export default ReplyTo;

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";

import { validateInput } from './utils/actions/formActions';


import { reducer } from './utils/reducers/formReducer';
import { ActivityIndicator, Alert } from 'react-native';
import { useDispatch } from 'react-redux';
import { signIn } from './utils/actions/authActions';
import colors from '../constants/colors';

const isTestMode = true;

const initialState = {
inputValues: {
email: isTestMode ? "fredleeyw@gmail.com" : "",
password: isTestMode ? "password" : "",
},
inputValidities: {
email: isTestMode,
password: isTestMode,
},
formIsValid: isTestMode,
};

const SignInForm = () => {


const dispatch = useDispatch();

const [error, setError] = useState();


const [isLoading, setIsLoading] = useState(false);
const [formState, dispatchFormState] = useReducer(reducer, initialState);

const inputChangedHandler = useCallback(


(inputId, inputValue) => {
const result = validateInput(inputId, inputValue);
dispatchFormState({ inputId, validationResult: result, inputValue });
},
[dispatchFormState]
);

useEffect(() => {
if (error) {
Alert.alert("Oh no! An error!", error, [{ text: "Try Again" }]);
}
}, [error]);

const authHandler = useCallback(async () => {


try {
setIsLoading(true);

const action = signIn(


formState.inputValues.email,
formState.inputValues.password,
);

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}
/>
)}
</>
);
};

export default SignInForm;

SignUpForm.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";
import { validateInput } from './utils/actions/formActions';
import { reducer } from './utils/reducers/formReducer';
import { signUp } from './utils/actions/authActions';
import { ActivityIndicator, Alert } from 'react-native';
import colors from '../constants/colors';
import { useDispatch, useSelector } from 'react-redux';

const initialState = {
inputValues: {
firstName: '',
lastName: '',
email: '',
password: '',
},
inputValidities: {
firstName: false,
lastName: false,
email: false,
password: false,
},
formIsValid: false,
};

const SignUpForm = props => {

const dispatch = useDispatch();

const [error, setError] = useState();


const [isLoading, setIsLoading] = useState(false);
const [formState, dispatchFormState] = useReducer(reducer, initialState);

const inputChangedHandler = useCallback((inputId, inputValue) => {


const result = validateInput(inputId, inputValue);
dispatchFormState({ inputId, validationResult: result, inputValue })
}, [dispatchFormState]);

useEffect(() => {
if (error){
Alert.alert('Oh no! An error!', error, [{ text: "Try Again"}]);
}
}, [error])

const authHandler = useCallback( async () => {


try {
setIsLoading(true);

const action = signUp(


formState.inputValues.firstName,
formState.inputValues.lastName,
formState.inputValues.email,
formState.inputValues.password
);
setError(null);
await dispatch(action);
} catch (error) {
setError(error.message);
setIsLoading(false);
}
}, [dispatch, formState]);

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}/>
}
</>
);

};

export default SignUpForm;

SubmitButton.js
import React from 'react';
import { StyleSheet, TouchableOpacity, Text } from 'react-native';

import colors from '../constants/colors';

const SubmitButton = props => {

const enabledBgColor = props.color || colors.primary;


const disabledBgColor = colors.lightGrey;
const bgColor = props.disabled ? disabledBgColor : enabledBgColor;

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>
};

const styles = StyleSheet.create({


button: {
paddingHorizontal: 30,
paddingVertical: 10,
borderRadius: 30,
justifyContent: 'center',
alignItems: 'center',
}
})

export default SubmitButton

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";

export default StyleSheet.create({


center: {
flex: 1,
justifyContent: 'center',
alignItems: 'center'
}
})

AppNavigator.js
​import React from "react";
import { NavigationContainer } from "@react-navigation/native";

import MainNavigator from "./MainNavigator";


import AuthScreen from "../screens/AuthScreen";
import { useSelector } from "react-redux";
import StartUpScreen from "../screens/StartUpScreen";

const AppNavigator = (props) => {

const isAuth = useSelector(state => state.auth.token !== null &&


state.auth.token !=='');
const didTryAutoLogin = useSelector(state => state.auth.didTryAutoLogin);
return (
<NavigationContainer>
{isAuth && <MainNavigator />}
{!isAuth && didTryAutoLogin && <AuthScreen />}
{!isAuth && !didTryAutoLogin && <StartUpScreen />}
</NavigationContainer>
);
};
//We will have multiple navigators - so each navigator you have different conditional
sequences - if signed in, if not, if need to reset account etc
export default AppNavigator;

MainNavigator.js
//Imports expo notifications modules
import * as Device from "expo-device";
import * as Notifications from "expo-notifications";

import React, { useEffect, useRef, useState } from "react";


import { createBottomTabNavigator } from "@react-navigation/bottom-tabs";
import { Ionicons } from "@expo/vector-icons";
//Imports screens to be used in navigator
import ChatListScreen from "../screens/ChatListScreen";
import ChatSettingsScreen from "../screens/ChatSettingsScreen";
import SettingsScreen from "../screens/SettingsScreen";
import ChatScreen from "../screens/ChatScreen";
import NewChatScreen from "../screens/NewChatScreen";
//Imports necessary components from react-navigation and redux
import { createNativeStackNavigator } from "@react-navigation/native-stack";
import { useDispatch, useSelector } from "react-redux";
//Import firebase helper functions
import { getFirebaseApp } from "../components/utils/firebaseHelper";
import { child, get, getDatabase, off, onValue, ref } from "firebase/database";
//Import necessary redux actions
import { setChatsData } from "../store/chatSlice";
import {
ActivityIndicator,
KeyboardAvoidingView,
Platform,
View,
} from "react-native";
import colors from "../constants/colors";
import commonStyles from "../constants/commonStyles";
import { setStoredUsers } from "../store/userSlice";
import { setChatMessages, setStarredMessages } from "../store/messagesSlice";
import ContactScreen from "../screens/ContactScreen";
import DataListScreen from "../screens/DataListScreen";

//Create navigator stacks


const Stack = createNativeStackNavigator();
const Tab = createBottomTabNavigator();

//Creates TabNavigator to handle navigation via tabs


const TabNavigator = () => {
return (
<Tab.Navigator
screenOptions={{
headerTitle: "",
headerShadowVisible: false,
}}
>
<Tab.Screen
name="ChatList"
component={ChatListScreen}
options={{
tabBarLabel: "Chats",
tabBarIcon: ({ color, size }) => (
<Ionicons name="chatbubbles-outline" size={size} color={color} />
),
}}
/>
<Tab.Screen
name="Settings"
component={SettingsScreen}
options={{
tabBarLabel: "Settings",
tabBarIcon: ({ color, size }) => (
<Ionicons name="settings-outline" size={size} color={color} />
),
}}
/>
</Tab.Navigator>
);
};

//Creates stack navigator to handle navigation between screens


const StackNavigator = () => {
return (
<Stack.Navigator>
<Stack.Group>
<Stack.Screen
name="Home"
component={TabNavigator}
options={{ headerShown: false }}
/>
<Stack.Screen
name="ChatScreen"
component={ChatScreen}
options={{
headerTitle: "",
headerBackTitle: "Go back",
}}
/>
<Stack.Screen
name="ChatSettings"
component={ChatSettingsScreen}
options={{
headerTitle: "",
headerBackTitle: "Go back",
headerShadowVisible: false,
}}
/>
<Stack.Screen
name="Contact"
component={ContactScreen}
options={{
headerTitle: "Contact Information",
headerBackTitle: "Go back",
headerShadowVisible: false,
}}
/>
<Stack.Screen
name="DataList"
component={DataListScreen}
options={{
headerTitle: "",
headerBackTitle: "Go back",
headerShadowVisible: false,
}}
/>
</Stack.Group>

<Stack.Group screenOptions= {{ presentation: "containedModal" }}>


<Stack.Screen name="NewChat" component={NewChatScreen} />
</Stack.Group>
</Stack.Navigator>
);
};

//Creates the main navigator componenet that fetches chat data and renders the stack
navigator
const MainNavigator = (props) => {
const dispatch = useDispatch();

const [isLoading, setIsLoading] = useState(true);


const userData = useSelector((state) => state.auth.userData);
const storedUsers = useSelector((state) => state.users.storedUsers);

//Expo push notification state variables


const [expoPushToken, setExpoPushToken] = useState("");
//console.log(expoPushToken)
const notificationListener = useRef();
const responseListener = useRef();

//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
});

//If user taps the notification, this code runs


responseListener.current =
Notifications.addNotificationResponseReceivedListener((response) => {
console.log("Notification tapped:");
console.log(response)
});

//Unsubscribes from the event handlers when useEffect is disposed


return () => {
Notifications.removeNotificationSubscription(
notificationListener.current
);
Notifications.removeNotificationSubscription(responseListener.current);
};
}, []);

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];

//Listens for changes on userChatsRef


onValue(userChatsRef, (querySnapshot) => {
const chatIdsData = querySnapshot.val() || {};
const chatIds = Object.values(chatIdsData);

const chatsData = {};


let chatsFoundCount = 0;

//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);

onValue(chatRef, (chatSnapshot) => {


chatsFoundCount++;

const data = chatSnapshot.val();

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;

const userRef = child(dbRef, `users/${userId}`);

get(userRef).then((userSnapshot) => {
const userSnapshotData = userSnapshot.val();
dispatch(setStoredUsers({ newUsers: { userSnapshotData } }));
});

refs.push(userRef);
});

//contains data for all of our chats


chatsData[chatSnapshot.key] = data;
}
//checks if loaded all of our chats yet
if (chatsFoundCount >= chatIds.length) {
//dispatches the chats data in setChatsData
dispatch(setChatsData({ chatsData }));
setIsLoading(false);
}
});

//Gets messages for each one of the chats looped over


const messagesRef = child(dbRef, `messages/${chatId}`);
refs.push(messagesRef);

onValue(messagesRef, (messagesSnapshot) => {


const messagesData = messagesSnapshot.val();
//Dispatch action to messages reducer
dispatch(setChatMessages({ chatId, messagesData }));
});

if (chatsFoundCount === 0) {
setIsLoading(false);
}
}
});

//Sets initial state of the starred messages


const userStarredMessagesRef = child(
dbRef,
`userFavouritedMessages/${userData.userId}`
);
//When useEffect is disposed of, will unsubscribe listener
refs.push(userStarredMessagesRef);
onValue(userStarredMessagesRef, (querySnapshot) => {
const starredMessages = querySnapshot.val() ?? {};
dispatch(setStarredMessages({ starredMessages }));
});

//unsubscribes when useEffect terminates


return () => {
console.log("Unsubscribing to firebase listeners");
refs.forEach((ref) => off(ref));
};
}, []);

//Loading activity indicator


if (isLoading) {
<View style={commonStyles.center}>
<ActivityIndicator size={"large"} color={colors.primary} />
</View>;
}

return (
<KeyboardAvoidingView
style={{ flex: 1 }}
behavior={Platform.OS === "ios" ? "padding" : undefined}
>
<StackNavigator />
</KeyboardAvoidingView>
);
};

export default MainNavigator;

//Checks for permissions for push notifications if/else etc


async function registerForPushNotificationsAsync() {
let token;
//Checks if application is android, does setup for android devices
if (Platform.OS === "android") {
await Notifications.setNotificationChannelAsync("default", {
name: "default",
importance: Notifications.AndroidImportance.MAX,
vibrationPattern: [0, 250, 250, 250],
lightColor: "#FF231F7C",
});
}
//Checks if the device is an acutal physical device
if (Device.isDevice) {
//Gets permissions
const { status: existingStatus } = await Notifications.getPermissionsAsync();
let finalStatus = existingStatus;
//If permission is not granted, requests permission
if (existingStatus !== "granted") {
const { status } = await Notifications.requestPermissionsAsync();
finalStatus = status;
}
//If permission still not granted, we know permission has been rejected
if (finalStatus !== "granted") {
alert("Failed to get push token for push notification!");
return;
}
//If we do have permission, get the push tokens
token = (await Notifications.getExpoPushTokenAsync()).data;
} else { //Only runs if not using physical device
console.log("Must use physical device for Push Notifications");
}

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";

import PageContainer from "../components/PageContainer";


import SignInForm from "../components/SignInForm";
import SignUpForm from "../components/SignUpForm";
import colors from "../constants/colors";

import logo from '../assets/images/logo.png';

const AuthScreen = props => {;

const [isSignUp, setIsSignUp] = useState(false);


return (
<SafeAreaView style={{ flex: 1 }}>
<PageContainer>
<ScrollView>
<KeyboardAvoidingView
style={styles.keyboardAvoidingView}
behavior={Platform.OS === "ios" ? "height" : undefined}
keyboardVerticalOffset = {100}>
<View style={styles.imageContainer}>
<Image style={styles.image} source={logo} resizeMode="contain" />
</View>
{isSignUp ? <SignUpForm /> : <SignInForm />}

<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>
);
};

const styles = StyleSheet.create({


linkContainer: {
justifyContent: "center",
alignItems: "center",
marginVertical: 15,
},
link: {
color: colors.blue,
fontFamily: "regular",
letterSpacing: 0.3,
},
imageContainer: {
justifyContent: 'center',
alignItems: 'center'
},
image: {
width: '70%',
},
keyboardAvoidingView: {
flex: 1,
justifyContent: 'center'
}
});

export default AuthScreen;

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";

const ChatListScreen = (props) => {


//Accesses parameters of selected user passed through to screen
const selectedUser = props.route?.params?.selectedUserId;

//Accesses parameters of selected user list passed


const selectedUserList = props.route?.params?.selectedUsers;

//Accesses parameters of chatName passed


const chatName = props.route?.params?.chatName;
//Accesses storedUsers data
const storedUsers = useSelector((state) => state.users.storedUsers);

//Gets the logged in user's data


const userData = useSelector((state) => state.auth.userData);
//Gets chat data
const userChats = useSelector(state => {
const chatsData = state.chats.chatsData;
//Returns chat data as an object with values
return Object.values(chatsData).sort((a,b) => {
//Sorts chats displayed according to most recent
return new Date(b.updatedAt) - new Date(a.updatedAt);
});
});

//New chat button


useEffect(() => {
props.navigation.setOptions({
headerRight: () => {
return (
<HeaderButtons HeaderButtonComponent={CustomHeaderButton}>
<Item
title="New chat"
iconName="ios-create-outline"
onPress={() => props.navigation.navigate("NewChat")}
/>
</HeaderButtons>
);
},
});
}, []);

//Handles navigating to chat page


useEffect(() => {
if (!selectedUser && !selectedUserList) {
return;
}

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" />

{/*Group chat button*/}


<View>
<TouchableOpacity onPress={() => props.navigation.navigate("NewChat",
{isGroupChat: true})}>
<Text style={styles.newGroupText}>New Group</Text>
</TouchableOpacity>
</View>

{/*Returns the chat data within a flatlist*/}


<FlatList
data={userChats}
renderItem={(itemData) => {
const chatData = itemData.item;
const chatId = chatData.key;
const isGroupChat = chatData.isGroupChat;

let title = '';


const subTitle = chatData.latestMessageText || "New chat";
let image = '';

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;

title = `${otherUser.firstName} ${otherUser.lastName}`;


image = otherUser.profilePicture;
}

return (
<DataItem
title={title}
subTitle={subTitle}
image={image}
onPress={() =>
props.navigation.navigate("ChatScreen", { chatId })
}
/>
);
}}
/>
</PageContainer>
);
};

const styles = StyleSheet.create({


container: {
flex: 1,
justifyContent: "center",
alignItems: "center",
},
newGroupText: {
color: colors.blue,
fontSize: 17,
marginBottom: 5
}
});
export default ChatListScreen;

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";

import backgroundImage from "../assets/images/background.jpg";


import colors from "../constants/colors";
import { useSelector } from "react-redux";
import PageContainer from "../components/PageContainer";
import Bubble from "../components/Bubble";
import { createChat, sendImage, sendTextMessage } from
"../components/utils/actions/chatActions";
import { FlatList } from "react-native-gesture-handler";
import ReplyTo from "../components/ReplyTo";
import {launchImagePicker, openCamera, uploadImageAsync} from
"../components/utils/imagePickerHelper";
import AwesomeAlert from "react-native-awesome-alerts";
import { HeaderButtons, Item } from "react-navigation-header-buttons";
import CustomHeaderButton from "../components/CustomHeaderButton";

const ChatScreen = (props) => {


//Declares state variables using hooks
const [chatUsers, setChatUsers] = useState([]);
//messageText contains the state and data that the user has entered in textbox
const [messageText, setMessageText] = useState("");
const [chatId, setChatId] = useState(props.route?.params?.chatId);
//Error banner state manager
const [errorBannerText, setErrorBannerText] = useState("");
//Reply state manager
const [replyingTo, setReplyingTo] = useState();
//Image state manager
const [tempImageUri, setTempImageUri] = useState("");
//Loading state manager (for image uploading)
const [isLoading, setIsLoading] = useState(false);

//Handles automatic scrolling of page to bottom useRef hook


const flatList = useRef();

//Declares selectors for retrieving data from redux storage


const userData = useSelector((state) => state.auth.userData);
const storedUsers = useSelector((state) => state.users.storedUsers);
const storedChats = useSelector((state) => state.chats.chatsData);

//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 [];

//Gets all chat messages data for a particular chat


const chatMessagesData = state.messages.messagesData[chatId];

if (!chatMessagesData) return [];


const messageList = [];
for (const key in chatMessagesData) {
//Use the key to retrieve chat message data
const message = chatMessagesData[key];

messageList.push({
key,
...message,
});
}
return messageList;
});

//Handles if there is chatId, set data to be storedChats, else set newChatData


const chatData =
(chatId && storedChats[chatId]) || props.route?.params?.newChatData || {};

//Get chat title based on chat name or users name


const getChatTitleFromName = () => {
//Selects any element that does not have the same id as my logged in user
const otherUserId = chatUsers.find((uid) => uid !== userData.userId);
const otherUserData = storedUsers[otherUserId];

return (
otherUserData && `${otherUserData.firstName} ${otherUserData.lastName}`
);
};

//Set header options using chat title (group vs individual)


useEffect(() => {
if(!chatData) return;

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]);

//Image picker function


const pickImage = useCallback(async () => {
try {
const tempUri = await launchImagePicker();
if (!tempUri) return;

setTempImageUri(tempUri);
} catch (error) {
console.log(error);
}
}, [tempImageUri]);

//Takes the photo


const takePhoto = useCallback(async () => {
try {
const tempUri = await openCamera();
if (!tempUri) return;

setTempImageUri(tempUri);
} catch (error) {
console.log(error);
}
}, [tempImageUri]);

//Image uploader function


const uploadImage = useCallback(async () => {
setIsLoading(true);

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);

//Timesout in 0.5 seconds to close the popup


setTimeout(() => setTempImageUri(""), 500);
} catch (error) {
console.log(error);
//setIsLoading(false);
}
}, [isLoading, tempImageUri, chatId]);

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" />
)}

{/* Displays error banner text */}


{errorBannerText !== "" && (
<Bubble text={errorBannerText} type="error" />
)}

{/* Styling and displaying chat in appropriate bubbles */}


{chatId && (
<FlatList
ref={(ref) => (flatList.current = ref)}
//Sets flatlist to current point
onContentSizeChange={() =>
flatList.current.scrollToEnd({ animated: false })
}
onLayout={() =>
flatList.current.scrollToEnd({ animated: false })
}
data={chatMessages}
renderItem={(itemData) => {
const message = itemData.item;
//Checks that message is own
const isOwnMessage = message.sentBy === userData.userId;
//Handles message type
let messageType;
if(message.type && message.type === 'info') {
messageType = 'info';
}
else if (isOwnMessage) {
messageType = 'myMessage';
}
else {
messageType = 'theirMessage';
}

//Retrieves sender name


const sender = message.sentBy && storedUsers[message.sentBy]
const name = sender && `${sender.firstName} ${sender.lastName}`;

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}
/>

{/* Allows us to take an image from our camera*/}


{messageText === "" && (
<TouchableOpacity style={styles.mediaButton} onPress={takePhoto}>
<Feather name="camera" size={24} color={colors.blue} />
</TouchableOpacity>
)}
{/* If not an empty string display button */}
{messageText !== "" && (
<TouchableOpacity
style={{ ...styles.mediaButton, ...styles.sendButton }}
onPress={sendMessage}
>
<Feather name="send" size={20} color={"white"} />
</TouchableOpacity>
)}

<AwesomeAlert //Formats the image sending popup


//If not empty means a path to an image is set
show={tempImageUri !== ""}
title="Send image?"
closeOnTouchOutside={true}
closeOnHardwareBackPress={true}
showCancelButton={true}
showConfirmButton={true}
cancelText="Cancel"
confirmText="Send Image"
confirmButtonColor={colors.primary}
cancelButtonColor={colors.red}
titleStyle={styles.popupTitleStyle}
onCancelPressed={() => setTempImageUri("")}
onConfirmPressed={uploadImage}
onDismiss={() => setTempImageUri("")}
customView={
<View>
{isLoading && (
<ActivityIndicator size="small" color={colors.primary} />
)}

{!isLoading && tempImageUri !== "" && (


<Image
source={{ uri: tempImageUri }}
style={{ width: 200, height: 200 }}
/>
)}
</View>
}
/>
</View>
</SafeAreaView>
);
};

const styles = StyleSheet.create({


container: {
flex: 1,
flexDirection: "column",
},
screen: {
flex: 1,
},
backgroundImage: {
flex: 1,
},
inputContainer: {
flexDirection: "row",
paddingVertical: 8,
paddingHorizontal: 10,
height: 50,
},
textbox: {
flex: 1,
borderWidth: 1,
borderRadius: 50,
borderColor: colors.lightGrey,
marginHorizontal: 15,
paddingHorizontal: 12,
},
mediaButton: {
alignItems: 'center',
justifyContent: 'center',
width: 35,
},
sendButton: {
backgroundColor: colors.blue,
borderRadius: 50,
padding: 8,
width: 35
},
popupTitleStyle: {
fontFamily: 'semibold',
letterSpacing: 0.3,
color: colors.textColor
}
});
export default ChatScreen;

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";

//Function for handling chat settings UI


const ChatSettingsScreen = (props) => {
const [isLoading, setIsLoading] = useState(false);
const [showSuccessMessage, setShowSuccessMessage] = useState(false);

//Gets respective slices from redux store


const chatId = props.route.params.chatId;
const chatData = useSelector((state) => state.chats.chatsData[chatId] || {});
const userData = useSelector((state) => state.auth.userData);
const storedUsers = useSelector((state) => state.users.storedUsers);
const starredMessages = useSelector((state) => state.messages.starredMessages[chatId]
?? {});

const initialState = {
inputValues: { chatName: chatData.chatName },
inputValidities: { chatName: undefined },
formIsValid: false,
};

//Manages state of input values and validity


const [formState, dispatchFormState] = useReducer(reducer, initialState);

//Gets props of selected users for adding group chat


const selectedUsers = props.route.params && props.route.params.selectedUsers
useEffect(() => {
if (!selectedUsers) {
return;
}
//Stores userIds in an array
const selectedUserData = [];
selectedUsers.forEach(uid => {
if (uid === userData.userId) return;

if (!storedUsers[uid]) {
console.log('No user data found in the servers');
return;
}
//Pushes users id to the screen
selectedUserData.push(storedUsers[uid]);
});

addUsersToChat(userData, selectedUserData, chatData);

}, [selectedUsers])

//Handles input changes, and validates input using a helper function


const inputChangedHandler = useCallback(
(inputId, inputValue) => {
const result = validateInput(inputId, inputValue);
dispatchFormState({ inputId, validationResult: result, inputValue });
},
[dispatchFormState]
);

//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]);

//Checks if changes have been made


const hasChanges = () => {
const currentValues = formState.inputValues;
return currentValues.chatName != chatData.chatName;
};

//Handles leaving of user from chat


const leaveChat = useCallback(async () => {
try {
setIsLoading(true);
//Remove user
await removeUserFromChat(userData, userData, chatData);

props.navigation.popToTop();
} catch (error) {
console.log(error);
} finally {
setIsLoading(false);
}
}, [props.navigation, isLoading]);

if (!chatData.users) return null;

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}
/>

{/*Renders an input field for the chat name */}


<Input
id="chatName"
label="Chat name"
autoCapitalize="none"
initialValue={chatData.chatName}
allowEmpty={false}
onInputChanged={inputChangedHandler}
errorText={formState.inputValidities["chatName"]}
/>

{/*Renders the list of chat members*/}


<View style={styles.sectionContainer}>
<Text style={styles.heading}> {chatData.users.length} Members </Text>

<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>

{/*Displays success message */}


{showSuccessMessage && <Text>Saved!!</Text>}

{/*Conditionally renders activity indicator and submit button*/}


{isLoading ? (
<ActivityIndicator size={"small"} color={colors.primary} />
) : (
hasChanges() && (
<SubmitButton
title="Save changes"
color={colors.primary}
onPress={saveHandler}
//If formState invalid, disable save button
disabled={!formState.formIsValid}
/>
)
)}

{/*Takes us to starred messages*/}


<DataItem
type={"link"}
title="Starred messages"
hideImage={true}
//Passes through properties needed for setting the starredMessages screen
onPress={() =>
props.navigation.navigate("DataList", {
title: "Starred Messages",
data: Object.values(starredMessages),
type: "messages"
})
}
/>
</ScrollView>

{
<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,
},
});

export default ChatSettingsScreen;

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";

const ContactScreen = props => {


const [isLoading, setIsLoading] = useState(false);

//Pre-loads and references data from state


const storedUsers = useSelector(state => state.users.storedUsers);
const userData = useSelector(state => state.auth.userData);
const currentUser = storedUsers[props.route.params.uid];

//Retrieve stored chats


const storedChats = useSelector(state => state.chats.chatsData);

const chatId = props.route.params.chatId;


const chatData = chatId && storedChats[chatId];

//State variable for common chats


const [commonChats, setCommonChats] = useState([]);

//Filters the respective common chats


useEffect(() => {
const getCommonUserChats = async () =>{
const currentUserChats = await getUserChats(currentUser.userId);
setCommonChats(
//Retrieves and filters an array of chatids, and only filtering group chats
Object.values(currentUserChats).filter(
(cid) => storedChats[cid] && storedChats[cid].isGroupChat
)
);
}

getCommonUserChats();
}, [ ])

//Handles removing of user from chat


const removeFromChat = useCallback(async () => {
try {
setIsLoading(true);
//Remove user
await removeUserFromChat(userData, currentUser, chatData);

props.navigation.goBack();
} catch (error) {
console.log(error)
}
finally {
setIsLoading(false);
}
}, [props.navigation, isLoading])

//Returns styling and content for the contact page


return <PageContainer>
<View style={styles.topContainer}>
<ProfileImage
uri={currentUser.profilePicture}
size={80}
style={{marginBottom: 20}}
/>
<PageTitle text={`${currentUser.firstName} ${currentUser.lastName}`}/>
{
currentUser.about &&
<Text style={styles.about} numberOfLines={3}>{currentUser.about}</Text>
}
</View>
{ //Outputs conditionally the common chats we have (or not)
commonChats.length > 0 &&
<>
<Text style= {styles.heading}>{commonChats.length} {commonChats.length
=== 1 ? 'Group' : 'Groups'} in common</Text>
{//Calls function on every element and returns array of results of
common chats
commonChats.map(cid=> {
const chatData = storedChats[cid];
return <DataItem
key = {cid}
title = {chatData.chatName}
subTitle = {chatData.latestMessageText}
type='link'
//Handles navigating to group chat from contacts page
onPress={() => props.navigation.push('ChatScreen',
{chatId:cid})}
image={chatData.chatImage}
/>
})
}
</>
}

{
chatData && chatData.isGroupChat &&
(
isLoading ?
<ActivityIndicator size= "small" color= {colors.primary} /> :
<SubmitButton
title= 'Remove from chat'
color = {colors.red}
onPress={removeFromChat}
/>
)
}
</PageContainer>
}

const styles = StyleSheet.create({


topContainer: {
alignItems: 'center',
justifyContent: 'center',
marginVertical: 20
},
about: {
fontFamily: 'semibold',
fontSize: 16,
letterSpacing: 0.3,
color: colors.grey
},
heading: {
fontFamily: 'semibold',
letterSpacing: 0.3,
color: colors.textColor,
marginVertical: 10
}
});

export default ContactScreen;

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';

const DataListScreen = props => {


//Retrieves stored users from the store
const storedUsers = useSelector(state => state.users.storedUsers);
//Retrieves chat messages data from the store
const messagesData = useSelector(state => state.messages.messagesData);
//Used to compare elements for flatlist
const userData = useSelector(state => state.auth.userData);

//Extracts property values passed through from ChatSettingsScreen


const {title, data, type, chatId} = props.route.params

//Runs whenever the title changes


useEffect(() => {
//Sets new header whenever title prop changes
props.navigation.setOptions({headerTitle: title})
}, [title])

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;

const isLoggedInUser = uid === userData.userId;


//Sets variables for user items
key = uid;
image = currentUser.profilePicture;
title = `${currentUser.firstName} ${currentUser.lastName}`;
subTitle = currentUser.about;
itemType = isLoggedInUser ? undefined : 'link';
onPress = isLoggedInUser ? undefined : () =>
props.navigation.navigate('Contact', {uid, chatId})
}
//Sets variables for favourited messages
else if (type === 'messages') {
const starData=itemData.item;
//Extract chatId and messageId
const {chatId, messageId} = starData;
//Extracts messages that are in a given chat
const messagesForChat = messagesData[chatId];

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}`;

//Sets variables for the fav message items


key = messageId;
title = name;
subTitle = messageData.text;
itemType = ''
//When clicked on star message, takes us to the chat
onPress = () => {}
}

return <DataItem
key = {key}
onPress = {onPress}
image = {image}
title = {title}
subTitle = {subTitle}
type = {itemType}
/>
}}
/>
</PageContainer>
};

export default DataListScreen;

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";

const NewChatScreen = (props) => {


const dispatch = useDispatch();

const [isLoading, setIsLoading] = useState(false);


const [users, setUsers] = useState();
const [noResultsFound, setNoResultsFound] = useState(false);
const [searchTerm, setSearchTerm] = useState("");
const [chatName, setChatName] = useState("");
const [selectedUsers, setSelectedUsers] = useState([]);

//Gets the logged in user's data


const userData = useSelector((state) => state.auth.userData);

//Gets storedUsers data


const storedUsers = useSelector((state) => state.users.storedUsers);

//Checks if it is a group chat


const isGroupChat = props.route.params && props.route.params.isGroupChat;
//Disables group chat creation button
const isGroupChatDisabled = selectedUsers.length === 0 || (isNewChat && chatName ===
"");

//Used to handle scrolling of selected users' bar


const selectedUsersFlatList = useRef();

//Gets the chatId


const chatId = props.route.params && props.route.params.chatId;

//If there is no chatId, we assume its a new chat


//const isNewChat = chatId ? true : false;
const isNewChat = !chatId;

//Extracts list of existing users in a given group chat


const existingUsers = props.route.params && props.route.params.existingUsers;

//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]);

//auto searching for users, to make it less expensive


useEffect(() => {
const delaySearch = setTimeout(async () => {
if (!searchTerm || searchTerm === "") {
setUsers();
setNoResultsFound(false);
return;
}

//Toggles the no users found page


setIsLoading(true);

const usersResult = await searchUsers(searchTerm);


delete usersResult[userData.userId];
setUsers(usersResult);

if (Object.keys(usersResult).length === 0) {
setNoResultsFound(true);
} else {
setNoResultsFound(false);

dispatch(setStoredUsers({ newUsers: usersResult }));


}
setIsLoading(false);
}, 500);

//Each time use effect renders, clear the timeout


return () => clearTimeout(delaySearch);
}, [searchTerm]);

//Handles pressing of users


const userPressed = (userId) => {
if (isGroupChat) {
//Check is selectedUsers contains userId, if so, removes it.
const newSelectedUsers = selectedUsers.includes(userId)
? selectedUsers.filter((id) => id !== userId)
: //Adds the selected users
selectedUsers.concat(userId);

setSelectedUsers(newSelectedUsers);
} else {
props.navigation.navigate("ChatList", {
selectedUserId: userId,
});
}
};

//This will return the searchbox for chats


return (
//if GroupChat, renders this
<PageContainer>
{isNewChat && isGroupChat &&
<View style={styles.chatNameContainer}>
<View style={styles.inputContainer}>
<TextInput
style={styles.textbox}
placeholder="Enter a chat name"
autoCorrect={false}
autoComplete={false}
onChangeText={(text) => setChatName(text)}
/>
</View>
</View>
}

{/* 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>
}

{/*Sets the view for the search bar*/}


<View style={styles.searchContainer}>
<Feather name="search" size={15} color={colors.lightGrey} />

<TextInput
placeholder="Search"
style={styles.searchBox}
onChangeText={(text) => setSearchTerm(text)}
/>
</View>

{/*Activity indicator when loading*/}


{isLoading &&
<View style={commonStyles.center}>
<ActivityIndicator size={"large"} color={colors.primary} />
</View>
}
{/*Passes the search data through and displays it in the search tab */}
{!isLoading && !noResultsFound && users &&
<FlatList
data={Object.keys(users)}
renderItem={(itemData) => {
const userId = itemData.item;
const userData = users[userId];

//Excludes existing users in a given group chat


if (existingUsers && existingUsers.includes(userId)) {
return;
}

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)}
/>
);
}}
/>
}

{!isLoading && noResultsFound &&


<View style={commonStyles.center}>
<Feather
name="help-circle"
size={55}
color={colors.lightGrey}
style={styles.noResultsIcon}
/>
<Text style={styles.noResultsText}>No users found...</Text>
</View>
}

{!isLoading && !users &&


<View style={commonStyles.center}>
<Feather
name="users"
size={55}
color={colors.lightGrey}
style={styles.noResultsIcon}
/>
<Text style={styles.noResultsText}>
Enter a name to search for a user
</Text>
</View>
}
</PageContainer>
);
};

const styles = StyleSheet.create({


searchContainer: {
flexDirection: "row",
alignItems: "center",
backgroundColor: colors.extraLightGrey,
height: 30,
marginVertical: 8,
paddingHorizontal: 8,
paddingVertical: 5,
borderRadius: 5,
},
searchBox: {
marginleft: 8,
fontSize: 15,
width: "100%",
},
noResultsIcon: {
marginBottom: 20,
},
noResultsText: {
color: colors.textColor,
fontFamily: "regular",
letterSpacing: 0.3,
},
chatNameContainer: {
paddingVertical: 10,
},
inputContainer: {
width: "100%",
paddingHorizontal: 15,
paddingVertical: 15,
backgroundColor: colors.nearlyWhite,
flexDirection: "row",
borderRadius: 2,
},
textbox: {
color: colors.textColor,
width: "100%",
fontFamily: "regular",
letterSpacing: 0.3,
},
selectedUsersContainer: {
height: 50,
justifyContent: "center",
},
selectedUsersList: {
height: "100%",
paddingTop: 10,
},
selectedUserStyle: {
marginRight: 10,
marginBottom: 10
},
});

export default NewChatScreen;

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";

const SettingsScreen = (props) => {


const dispatch = useDispatch();

const [isLoading, setIsLoading] = useState(false);


const [showSuccessMessage, setShowSuccessMessage] = useState(false);

//Retrieves user data from redux store


const userData = useSelector((state) => state.auth.userData);

//Gets starred messages data


const starredMessages = useSelector((state) => state.messages.starredMessages ?? {});

//Gets starred messages as an array


const sortedStarredMessages = useMemo(() => {
let result = [];

const chats = Object.values(starredMessages);


chats.forEach(chat => {
const chatMessages = Object.values(chat);
result = result.concat(chatMessages);
})
}, [starredMessages])

//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,
};

// Uses a reducer to update form state based on input changes


const [formState, dispatchFormState] = useReducer(reducer, initialState);

// Handles input changes, and validates input using a helper function


const inputChangedHandler = useCallback(
(inputId, inputValue) => {
const result = validateInput(inputId, inputValue);
dispatchFormState({ inputId, validationResult: result, inputValue });
},
[dispatchFormState]
);

// 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]);

//Hides save button if there are no changes


const hasChanges = () => {
const currentValues = formState.inputValues;

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>

{/*Takes us to starred messages*/}


<DataItem
type={"link"}
title="Starred messages"
hideImage={true}
//Passes through properties needed for setting the starredMessages screen
onPress={() =>
props.navigation.navigate("DataList", {title: "Starred Messages", data:
sortedStarredMessages,type: "messages"
})
}
/>

<SubmitButton
title="Logout"
onPress={() => dispatch(userLogout())}
style={{ marginTop: 20 }}
color={colors.red}
/>
</ScrollView>
</PageContainer>
);
};

const styles = StyleSheet.create({


container: {
flex: 1,
},
formContainer: {
alignItems: 'center'
}
});

export default SettingsScreen;

StartUpScreen.js
//Screen to check the storage and if data is there see if we can sign in

import AsyncStorage from "@react-native-async-storage/async-storage";


import React, { useEffect } from "react";
import { ActivityIndicator , View } from "react-native";
import { useDispatch } from "react-redux";
import { getUserData } from "../components/utils/actions/userActions";
import colors from "../constants/colors";
import commonStyles from "../constants/commonStyles";
import { authenticate, setDidTryAutoLogin } from "../store/authSlice";

const StartUpScreen = () => {

const dispatch = useDispatch();

useEffect(()=>{
const tryLogin = async () => {
const storedAuthInfo = await AsyncStorage.getItem("userData");

if (!storedAuthInfo){
dispatch(setDidTryAutoLogin());
return;
}

const parsedData = JSON.parse(storedAuthInfo);


const { token, userId, expiryDate: expiryDateString } = parsedData;

//Handles case where token is expired


const expiryDate = new Date(expiryDateString);
if (expiryDate <= new Date() || !token || !userId) {
dispatch(setDidTryAutoLogin());
return;
}
//Handles case where token is not expired
const userData = await getUserData(userId);
dispatch(authenticate({ token: token, userData}));
};

tryLogin();
}, [dispatch])

return <View style={commonStyles.center}>


<ActivityIndicator size="large" color={colors.primary} />
</View>
}

export default StartUpScreen;

authSlice.js
import { createSlice } from "@reduxjs/toolkit";

const authSlice = createSlice({


name: "auth",
initialState: {
token: null,
userData: null,
didTryAutoLogin: false
},
reducers: {
authenticate: (state, action) => {
const { payload } = action;
state.token = payload.token;
state.userData = payload.userData;
state.didTryAutoLogin = true;
},
setDidTryAutoLogin: (state, action) => {
state.didTryAutoLogin = true;
},
logout: (state, action) => {
state.token = null;
state.userData = null;
state.didTryAutoLogin = false;
},
updateLoggedInUserData: (state, action) => {
state.userData = { ...state.userData, ...action.payload.newData }
}
}
});
export const setDidTryAutoLogin = authSlice.actions.setDidTryAutoLogin;
export const authenticate = authSlice.actions.authenticate;
export const updateLoggedInUserData = authSlice.actions.updateLoggedInUserData;
export const logout = authSlice.actions.logout;
export default authSlice.reducer;

chatSlice.js
import { createSlice } from "@reduxjs/toolkit";

const chatSlice = createSlice({


name: "chats",
initialState: {
chatsData: {},
},
reducers: {
setChatsData: (state, action) => {
state.chatsData = {...action.payload.chatsData}
},
}
});
export const setChatsData = chatSlice.actions.setChatsData;

export default chatSlice.reducer;

messageSlice.js
import { createSlice } from "@reduxjs/toolkit";

const messagesSlice = createSlice({


name: "messages",
initialState: {
//Add stored messages
messagesData: {},
starredMessages: {}
},
reducers: {
//Retrieves existing chat messages from the state
setChatMessages: (state, action) => {
const existingMessages = state.messagesData;
//Extract chatId and messagesData properties from action payload using
destructuring
const { chatId, messagesData } = action.payload;

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;

export default messagesSlice.reducer;


Store.js
import { configureStore } from "@reduxjs/toolkit";
import authSlice from "./authSlice";
import chatSlice from "./chatSlice";
import messagesSlice from "./messagesSlice";
import userSlice from "./userSlice";

//Creates the redux store with the respective reducers


export const store = configureStore({
reducer: {
auth: authSlice,
users: userSlice,
chats: chatSlice,
messages: messagesSlice,
}
});

userSlice.js
import { createSlice } from "@reduxjs/toolkit";

const userSlice = createSlice({


name: "users",
initialState: {
storedUsers: {},
},
reducers: {
setStoredUsers: (state, action) => {
const newUsers = action.payload.newUsers;
const existingUsers = state.storedUsers;

const usersArray = Object.values(newUsers);


for (let i = 0; i < usersArray.length; i++) {
const userData = usersArray[i];
existingUsers[userData.userId] = userData
}

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();

//Prevent app from auto-hiding the splash screen


SplashScreen.preventAutoHideAsync();

export default function App() {


const [appIsLoaded, setAppIsLoaded] = useState(false);

//Loads fonts when app starts up


useEffect(() => {
//Exception handling for errors in case font loading doesnt work
const prepare = async () => {
try {
await Font.loadAsync({
regular: require("./assets/fonts/IbarraRealNova-VariableFont_wght.ttf"),
semibold: require("./assets/fonts/IbarraRealNova-SemiboldFont_wght.ttf"),
});
} catch (error) {
console.log(error);
} finally {
setAppIsLoaded(true);
}
};

prepare();
}, []);

//Hide the splash screen when app has finished loading


const onLayout = useCallback(async () => {
if (appIsLoaded) {
await SplashScreen.hideAsync();
}
}, [appIsLoaded]);

//Returns the app UI once app is loaded


if (!appIsLoaded) {
return null;
}

return (
<Provider store={store}>
<SafeAreaProvider style={styles.container} onLayout={onLayout}>
<MenuProvider>
<AppNavigator />
</MenuProvider>
</SafeAreaProvider>
</Provider>
);
}

const styles = StyleSheet.create({


container: {
flex: 1,
backgroundColor: "#fff",
},
label: {
color: "black",
fontsize: 18,
fontFamily: "regular",
},
});
App.json
{
"expo": {
"name": "ChatFinals",
"slug": "ChatFinals",
"version": "1.0.0",
"orientation": "portrait",
"icon": "./assets/icon.png",
"userInterfaceStyle": "light",
"splash": {
"image": "./assets/SplashScreen.png",
"resizeMode": "contain",
"backgroundColor": "#ffffff"
},
"updates": {
"fallbackToCacheTimeout": 0
},
"assetBundlePatterns": ["**/*"],
"ios": {
"supportsTablet": true
},
"android": {
"adaptiveIcon": {
"foregroundImage": "./assets/adaptive-icon.png",
"backgroundColor": "#FFFFFF"
}
},
"web": {
"favicon": "./assets/favicon.png"
},
"plugins": [
[
"expo-notifications",
{
"icon": "./local/path/to/myNotificationIcon.png",
"color": "#ffffff",
"sounds": [
"./local/path/to/mySound.wav",
"./local/path/to/myOtherSound.wav"
],
"mode": "production"
}
]
]
}
}

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
}

You might also like