Add Friends / DM's V1 #68

Merged
LAX18 merged 3 commits from npease-friends-v1 into main 2024-04-06 22:08:48 -09:00
11 changed files with 566 additions and 12 deletions
-2
View File
@@ -51,8 +51,6 @@ function Home() {
window.location.href = "/onboarding";
}
});
} else {
window.location.href = "/login"
}
}, [authUser])
-2
View File
@@ -47,8 +47,6 @@ function Chat() {
window.location.href = "/onboarding";
}
});
} else {
window.location.href = "/login"
}
}, [authUser])
+17
View File
@@ -0,0 +1,17 @@
import { Inter } from "next/font/google";
import "../globals.css";
const inter = Inter({ subsets: ["latin"] });
export const metadata = {
title: "ChatMaps: DM",
description: "ChatMaps: Social Media for College Students",
};
export default function RootLayout({ children }) {
return (
<html lang="en">
<body className={inter.className}>{children}</body>
</html>
);
}
+139
View File
@@ -0,0 +1,139 @@
"use client";
// System Imports
import Drawer from '@mui/material/Drawer';
import { useState, useEffect } from "react";
// Firebase Imports
import { auth, database } from "../../../firebase-config";
import { ref, onValue, set, onDisconnect } from "firebase/database";
import { useAuthState } from "react-firebase-hooks/auth"
// Component Imports
import { Header } from "../../components/app/header";
import { DMRoom } from "../../components/app/friends/page";
import { Sidebar } from "../../components/app/sidebar/dm";
import {useWindowSize} from "../../components/app/datatypes";
/**
* DM Page
* @returns {Object} Chat Page
*/
function Chat() {
// State variables for chat page
const [user, setUser] = useState(null); // user data
const [chatRoomObj, setChatRoomObj] = useState(null); // Current chatroom object
const [doneLoading, setDoneLoading] = useState(false) // is the page done loading or not
const [authUser] = useAuthState(auth) // auth user object (used to obtain other user object)
const [drawerOpen, setDrawerOpen] = useState(true); // drawer open state
var windowSize = useWindowSize()
useEffect(() => {
if (windowSize.width < 767) {
setDrawerOpen(false)
} else {
setDrawerOpen(true)
}
}, [windowSize])
// Authentication Verification / Redirection if Profile Data not Filled out
useEffect(() => {
if (authUser) {
onValue(ref(database, `users/${authUser.uid}`), (userData) => {
userData = userData.val();
if (userData) {
setUser(userData);
} else {
window.location.href = "/onboarding";
}
});
}
}, [authUser])
// Users URL params to load proper chatroom, then logs the user into that room
useEffect(() => {
if (user) {
const searchParams = new URLSearchParams(document.location.search);
var path = searchParams.get("dm")
/*// Send entered message
var payload = {
body: "entered",
user: user.username,
isSystem: true,
timestamp: new Date().getTime(),
uid: user.uid,
};
set(
ref(
database,
`/rooms/${path}/chats/${new Date().getTime()}-${user.username}`
),
payload
);*/
// Add user to online for room
set(ref(database, `/dms/${path}/users/online/${user.uid}`), user)
// Removes user from room on disconnect (reload, window close, internet lost)
onDisconnect(ref(database, `/dms/${path}/users/online/${user.uid}`)).remove()
// Sends leaving message on disconnect (Timestamp function used due to new onDisconnect stuff)
/*someRef = ref(database, `/rooms/${path}/chats/${new Date().getTime()}-${user.username}`)
onDisconnect(someRef).set({
body: "left",
user: user.username,
isSystem: true,
timestamp: serverTimestamp(),
uid: user.uid,
})*/
onValue(ref(database, `/dms/${path}`), (roomData) => {
roomData = roomData.val();
setChatRoomObj(roomData)
if (!doneLoading) {
setDoneLoading(true)
}
})
}
}, [user]);
return (
<div>
{(authUser && doneLoading) && (
<div className="overflow-hidden h-dvh">
{/* Left Side of Page */}
<div className="overflow-hidden h-dvh md:mr-[400px]">
{/* Header */}
<Header
mainTab={"dm"}
chatRoomObj={chatRoomObj}
user={user}
sidebarControl={() => {setDrawerOpen(!drawerOpen)}}
/>
{/* Main Page Section */}
<div className="mr-2 h-[calc(100%-110px)]">
<DMRoom roomObj={chatRoomObj} user={user} />
</div>
</div>
{/* Sidebar (Right Side of Page) */}
<Drawer open={drawerOpen} anchor={"right"} variant={windowSize.width > 767? "persistent": "temporary"} onClose={() => {setDrawerOpen(false)}} sx={{
width: windowSize.width > 767? 400: "80%",
marginTop: 10,
flexShrink: 0,
'& .MuiDrawer-paper': {
width: windowSize.width > 767? 400: "80%",
borderLeft: 0,
},
}}>
<div className="shadow-2xl">
<Sidebar chatRoomObj={chatRoomObj} user={user}/>
</div>
</Drawer>
</div>
)}
</div>
);
}
export default Chat;
+23 -7
View File
@@ -15,6 +15,9 @@ import { Interest } from "../../components/app/profile/Interest";
// Header Import
import { Header } from "../../components/app/header";
// Friend Import
import { addFriend } from "../../components/app/friends/friends";
/**
* User Profile Page
* @returns {Object} - User Profile Page
@@ -26,6 +29,8 @@ function UserProfile() {
const [userInterestArray, setUserInterestArray] = useState(null); // Array of user's interests
const [userRoomsArray, setUserRoomsArray] = useState(null); // Array of user's rooms
const [isOwner, setIsOwner] = useState(false); // Determines if user is owner of profile
const [friends, setFriends] = useState(false); // is user a friend?
const [isPending, setPending] = useState(false); // is friend request pending?
// Handles Edit State in Component, shares useState with ProfileEdit
const [isEditing, setIsEditing] = useState(false);
@@ -51,9 +56,6 @@ function UserProfile() {
window.location.href = "/onboarding";
}
});
} else {
setIsAuthenticated(false);
window.location.href = "/login";
}
});
}, []);
@@ -91,6 +93,18 @@ function UserProfile() {
});
}, []);
useEffect(() => {
if (user && profileData) {
console.log(user.uid in profileData.friends.requests)
if ("friends" in user) {
profileData.uid in user.friends.friends ? setFriends(true) : setFriends(false);
}
if ("friends" in profileData) {
user.uid in profileData.friends.requests ? setPending(true) : setPending(false);
}
}
}, [user, profileData]);
return (
<div>
{isAuthenticated && (
@@ -128,11 +142,13 @@ function UserProfile() {
Edit Profile
</a>
)}
{!isOwner && (
<a className="w-[120px] p-2 cursor-pointer bg-cyan-500 text-white font-bold rounded-full text-center">
Add Friend
</a>
{(!isOwner && !friends) && (
<div>
{(!isPending ) && (<a onClick={() => {addFriend(user, profileData.uid);setPending(true)}} className="w-[120px] p-2 cursor-pointer bg-cyan-500 text-white font-bold rounded-full text-center">Add Friend</a>)}
{(isPending ) && (<a className="w-[120px] p-2 bg-cyan-500 text-white font-bold rounded-full text-center">Pending</a>)}
</div>
)}
{(!isOwner && friends ) && (<div className="font-bold text-[20px]">Friends</div>)}
</div>
</div>
)}
@@ -0,0 +1,55 @@
// Firebase Imports
import { database } from "../../../../firebase-config"
import { ref, set, get } from "firebase/database";
import ChatIcon from '@mui/icons-material/Chat';
export function openDM(user, uid) {
get(ref(database, `dms/`)).then((snapshot) => {
var dmsList = snapshot.val();
for (var dmRoom in dmsList) {
if (user.uid in dmsList[dmRoom].UIDs && uid in dmsList[dmRoom].UIDs) {
window.location.href = `/dm?dm=${dmRoom}`
}
}
createDM(user, uid)
window.location.href = `/dm?dm=${user.uid}-${uid}`
});
}
export function createDM(user, uid) {
set(ref(database, `dms/${user.uid}-${uid}`), {
initUID: user.uid,
targetUID: uid,
room: user.uid + "-" + uid,
UIDs: [user.uid, uid]
})
}
/**
*
* @param {JSON} friendObj - Friend Object (user)
* @returns DM Component
*/
export function DM({user,friendObj}) {
return (
<div className="border-[black] border-1 shadow-lg m-2 rounded-lg">
<div className='grid grid-cols-4'>
<div className='place-content-center'>
<ChatIcon className='cursor-pointer' onClick={() => {openDM(user,friendObj.uid)}}/>
</div>
<div className='col-span-3 cursor-pointer'>
<div onClick={() => {openDM(user,friendObj.uid)}}>
<div className='inline-block mr-5'><img src={friendObj.pfp} className='w-[50px]'/></div>
<div className='inline-block relative top-[-6px]'>
<div className="font-bold">{friendObj.firstName} {friendObj.lastName}</div>
<div className="">@{friendObj.username}</div>
</div>
</div>
</div>
</div>
</div>
)
}
@@ -0,0 +1,101 @@
import Link from 'next/link'
// Icons
import ChatIcon from '@mui/icons-material/Chat';
// Firebase Imports
import { database } from "../../../../firebase-config"
import { ref, set } from "firebase/database";
import { openDM } from './dm';
// Icons
import CheckIcon from '@mui/icons-material/Check';
import CloseIcon from '@mui/icons-material/Close';
/**
* Send a friend request to a user
* @param {JSON} user - User Object
* @param {JSON} uid - User ID of the user to send a friend request to
*/
export function addFriend(user, uid) {
// Add to user's friend requests
set(ref(database, `users/${uid}/friends/requests/${user.uid}`), {
uid: user.uid
})
}
/**
*
* @param {JSON} friendObj - Friend Object (user)
* @returns Friend Component
*/
export function Friend({user,friendObj}) {
return (
<div className="border-[black] border-1 shadow-lg m-2 rounded-lg">
<div className='grid grid-cols-4'>
<div className='place-content-center'>
<ChatIcon className='cursor-pointer' onClick={() => {openDM(user,friendObj.uid)}}/>
</div>
<div className='col-span-3 cursor-pointer'>
<Link href={`/user?uid=${friendObj.uid}`}>
<div className='inline-block mr-5'><img src={friendObj.pfp} className='w-[50px]'/></div>
<div className='inline-block relative top-[-6px]'>
<div className="font-bold">{friendObj.firstName} {friendObj.lastName}</div>
<div className="">@{friendObj.username}</div>
</div>
</Link>
</div>
</div>
</div>
)
}
/**
*
* @prop {JSON} user - User Object
* @prop {JSON} requestingUser - User Object of the user requesting to be friends
* @returns
*/
export function FriendRequest({user, requestingUser}) {
/**
* Accepts Friend Request
* @param {JSON} user - User Object
* @param {JSON} uid - User ID of the user who sent the friend request
*/
function acceptRequest(user, uid) {
// Add to user's friends
set(ref(database, `users/${user.uid}/friends/friends/${uid}`), {
uid: uid
})
removeRequest(user, uid)
}
/**
* Removes Friend Request
* @param {JSON} user - User Object
* @param {JSON} uid - User ID of the user who sent the friend request
*/
function removeRequest(user, uid) {
// Remove from user's friend requests
set(ref(database, `users/${user.uid}/friends/requests/${uid}`), null)
}
return (
<div className="border-[black] border-1 shadow-lg m-2 rounded-lg">
<div className='grid grid-cols-4'>
<div className='place-content-center'>
<CheckIcon className='cursor-pointer ml-5' onClick={() => {acceptRequest(user, requestingUser.uid)}}/>
<CloseIcon className='cursor-pointer ml-5' onClick={() => {removeRequest(user, requestingUser.uid)}}/>
</div>
<div className='col-span-3 cursor-pointer'>
<Link href={`/user?uid=${requestingUser.uid}`}>
<div className='inline-block mr-5'><img src={requestingUser.pfp} className='w-[50px]'/></div>
<div className='inline-block relative top-[-6px]'>
<div className="font-bold">{requestingUser.firstName} {requestingUser.lastName}</div>
<div className="">@{requestingUser.username}</div>
</div>
</Link>
</div>
</div>
</div>
)
}
@@ -0,0 +1,87 @@
// Dependency Imports
import { Form, useForm } from "react-hook-form";
// Firebase Imports
import { ref, set } from "firebase/database";
import { database } from "../../../../firebase-config";
// Component Imports
import { Chat, SystemMessage } from "../datatypes";
// Icons
import SendIcon from '@mui/icons-material/Send';
/**
* Chat Room Component
* @prop {JSON} roomObj - Room Object
* @prop {JSON} user - User Object
* @returns {Object} - Chat Room Component
*/
export function DMRoom({ roomObj, user }) {
var { register, control, reset, handleSubmit } = useForm();
// Message updater
var chatsArr = [];
var messages = roomObj.chats;
for (var message in messages) {
if (messages[message].isSystem) {
chatsArr.push(
<SystemMessage
chatObj={messages[message]}
key={messages[message].timestamp}
/>
);
} else {
chatsArr.push(
<Chat
chatObj={messages[message]}
key={messages[message].timestamp}
/>
);
}
}
var chats = chatsArr.reverse();
/**
* Send Message in Chatroom
* @param {JSON} data - Message data to send (from form)
* @returns {void}
*/
function sendMessage(data) {
reset();
var payload = {
body: data.message,
user: user.username,
uid: user.uid,
isSystem: false,
timestamp: new Date().getTime(),
};
set(
ref(
database,
`/dms/${roomObj.room}/chats/${new Date().getTime()}-${user.username}`
),
payload
);
}
if (!chats) return <div>No Chats</div>;
return (
<div className="m-1 h-[100%] rounded-lg">
<div className="h-[90%] m-4 overflow-y-auto flex flex-col-reverse">
{chats}
</div>
<div className="m-2 h-[10%] w-[100%] bg-white rounded-lg">
<Form
onSubmit={handleSubmit(sendMessage)}
control={control}
>
<div className="width-[100%] grid grid-cols-6 pr-5 pt-1">
<input type="text" {...register("message")} placeholder="Enter message..." className="col-span-5 border-[0px]" />
<button className="p-2 cursor-pointer bg-cyan-500 text-white font-bold rounded-full mr-5 w-[100%]"><SendIcon/></button>
</div>
</Form>
</div>
</div>
);
}
+1 -1
View File
@@ -119,7 +119,7 @@ export function Header({mainTab,chatRoomObj,user,sidebarControl}) {
<RemoveIcon/>
</a>
)}
{mainTab == "chat" && (
{(mainTab == "chat" || mainTab == "dm" ) && (
<Link
href="/app"
className="p-2 cursor-pointer bg-cyan-500 text-white font-bold rounded-full mr-2 flex items-center"
@@ -0,0 +1,60 @@
import { Member } from "../datatypes"
import { database } from "../../../../firebase-config"
import {ref, get, set} from "firebase/database"
import { useState, useEffect } from "react"
export function Sidebar({user, chatRoomObj}) {
const [profileData, setProfileData] = useState(null)
// Active users list
if (
chatRoomObj.hasOwnProperty("users") &&
chatRoomObj.users.hasOwnProperty("online")
) {
var activeUsers = [];
var activeUsersJSON = chatRoomObj.users.online;
for (var activeUser in activeUsersJSON)
activeUsers.push(<Member memberObj={activeUsersJSON[activeUser]} />);
var chatroomOnline = activeUsers
}
useEffect(() => {
if (user) {
// Profile Information
if (user.uid == chatRoomObj.UIDs[0]) {
var profileUID = chatRoomObj.UIDs[1]
} else {
var profileUID = chatRoomObj.UIDs[0]
}
get(ref(database, `users/${profileUID}`)).then((snapshot) => {
var profileData = snapshot.val()
setProfileData(profileData)
})
}
}, [user])
return (
<div>
{profileData && (
<div className="overflow-hidden h-dvh">
<div className="m-2 h-[98%] grid grid-cols-1">
<div className="flex place-content-center">
<div className="bg-white rounded-lg m-2 shadow-2xl pt-10">
<img src={profileData.pfp} className="w-[80%] relative mx-auto"/>
<div className="font-bold text-[24px]">{profileData.firstName} {profileData.lastName}</div>
@{profileData.username}
</div>
</div>
<div className="bg-white rounded-lg m-2 shadow-2xl">
<div>In The Chat</div>
{chatroomOnline}
</div>
</div>
</div>
)}
</div>
);
}
@@ -12,6 +12,11 @@ import { ref, set, get } from "firebase/database";
// Component Imports
import { ChatRoomSidebar } from "../datatypes";
// Friend Imports (TEMP)
import { Friend, FriendRequest } from "../friends/friends";
// DM Imports
import { DM } from "../friends/dm";
/**
* Create Room Component for /app Sidebar
@@ -85,6 +90,9 @@ function classNames(...classes) {
export function Sidebar({user,location,loadingLoc}) {
const [nearbyArr, setNearbyArr] = useState([])
const [nearbyArrReady, setNearbyArrReady] = useState(false)
const [friends, setFriends] = useState([])
const [friendRequests, setFriendRequests] = useState(null)
const [dms, setDMs] = useState(null)
// Add myRooms to Sidebar
var myRoomArr = [];
for (var room in user.rooms) {
@@ -125,6 +133,50 @@ export function Sidebar({user,location,loadingLoc}) {
}
}, [location])
useEffect(() => {
if (user && user.friends) {
get(ref(database, "/users/")).then((snapshot) => {
var users = snapshot.val();
var friends = [];
for (var friend in user.friends.friends) {
friends.push(<Friend user={user} friendObj={users[friend]} key={friend} />);
}
setFriends(friends);
});
var requestArr = [];
for (var request in user.friends.requests) {
get(ref(database, `/users/${request}`)).then((snapshot) => {
requestArr.push(<FriendRequest requestingUser={snapshot.val()} user={user} key={request} />);
});
}
setFriendRequests(requestArr);
} else {
setFriends(<div>No Friends</div>);
setFriendRequests(<div>No Friend Requests</div>);
}
get(ref(database, `/dms`)).then((snapshot) => {
var dmsList = snapshot.val();
var dmArr = [];
for(var dmRoom in dmsList) {
if (user.uid == dmsList[dmRoom].UIDs[0]) {
get(ref(database, `/users/${dmsList[dmRoom].UIDs[1]}`)).then((snapshot) => {
dmArr.push(<DM user={user} friendObj={snapshot.val()} key={dmRoom}/>);
})
} else if (user.uid == dmsList[dmRoom].UIDs[1]) {
get(ref(database, `/users/${dmsList[dmRoom].UIDs[1]}`)).then((snapshot) => {
dmArr.push(<DM user={user} friendObj={snapshot.val()} key={dmRoom}/>);
})
}
}
if (dmArr.length == 0) {
dmArr.push(<div>No DMs</div>);
}
setDMs(dmArr);
})
}, [user])
return (
<div className="h-dvh bg-[aliceblue] pt-2 pb-2 pl-2 pr-1">
<div className="bg-white rounded-lg h-[98%] mb-[10px] mt-[-18px] mr-2">
@@ -151,6 +203,28 @@ export function Sidebar({user,location,loadingLoc}) {
? 'bg-cyan-500 text-white font-bold shadow hover:bg-white/[0.6] hover:text-black'
: 'hover:bg-cyan-500/[0.6] hover:text-white hover:font-bold'
)}>Create</Tab>
<Tab className={({ selected }) =>
classNames(
'w-[30%]',
selected
? 'bg-cyan-500 text-white font-bold shadow hover:bg-white/[0.6] hover:text-black'
: 'hover:bg-cyan-500/[0.6] hover:text-white hover:font-bold'
)} defaultIndex={1}>DMs</Tab>
<Tab className={({ selected }) =>
classNames(
'w-[30%]',
selected
? 'bg-cyan-500 text-white font-bold shadow hover:bg-white/[0.6] hover:text-black'
: 'hover:bg-cyan-500/[0.6] hover:text-white hover:font-bold'
)} defaultIndex={1}>Friends</Tab>
<Tab className={({ selected }) =>
classNames(
'w-[30%]',
selected
? 'bg-cyan-500 text-white font-bold shadow hover:bg-white/[0.6] hover:text-black'
: 'hover:bg-cyan-500/[0.6] hover:text-white hover:font-bold'
)} defaultIndex={1}>Requests</Tab>
</Tab.List>
<Tab.Panels>
<Tab.Panel>
@@ -173,6 +247,15 @@ export function Sidebar({user,location,loadingLoc}) {
{!loadingLoc && <CreateRoom loc={location} />}
{loadingLoc && <div>Loading...</div>}
</Tab.Panel>
<Tab.Panel>
{dms}
</Tab.Panel>
<Tab.Panel>
{friends}
</Tab.Panel>
<Tab.Panel>
{friendRequests}
</Tab.Panel>
</Tab.Panels>
</Tab.Group>
</div>