├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt └── src ├── App.css ├── App.js ├── components ├── Loading.js ├── Message.js ├── MessageForm.js ├── Navbar.js ├── PrivateRoute.js ├── User.js └── svg │ ├── Attachment.js │ ├── Camera.js │ └── Delete.js ├── context └── auth.js ├── firebase.js ├── image1.jpg ├── index.css ├── index.js └── pages ├── Home.js ├── Login.js ├── Profile.js └── Register.js /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env 17 | .env.local 18 | .env.development.local 19 | .env.test.local 20 | .env.production.local 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Private Chat App With React and Firebase version 9 2 | 3 | This project is built with React and latest Firebase SDK version 9. 4 | 5 | ## Run the app 6 | 7 | - Clone this repo 8 | - Run **npm install** 9 | - Create **.env** file at root level of project and add REACT_APP_API_KEY, REACT_APP_AUTH_DOMAIN, REACT_APP_DATABASE_URL, REACT_APP_PROJECT_ID, REACT_APP_STORAGE_BUCKET, REACT_APP_MESSAGING_SENDER_ID, REACT_APP_APP_ID 10 | - Value of REACT_APP_DATABASE_URL will be https://YOUR-FIREBASE-PROJECT-NAME.firebaseio.com simply replace YOUR-FIREBASE-PROJECT-NAME with your project name 11 | - Run **npm start** 12 | 13 | [YouTube video tutorial](https://youtu.be/fdcruaIiQxc) 14 | 15 | [Text based and video tutorials](https://farhanfarooq.com/tutorials) 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-messenger", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^5.14.1", 7 | "@testing-library/react": "^11.2.7", 8 | "@testing-library/user-event": "^12.8.3", 9 | "firebase": "^9.0.2", 10 | "moment": "^2.29.1", 11 | "react": "^17.0.2", 12 | "react-dom": "^17.0.2", 13 | "react-moment": "^1.1.1", 14 | "react-router-dom": "^5.3.0", 15 | "react-scripts": "4.0.3", 16 | "web-vitals": "^1.1.2" 17 | }, 18 | "scripts": { 19 | "start": "react-scripts start", 20 | "build": "react-scripts build", 21 | "test": "react-scripts test", 22 | "eject": "react-scripts eject" 23 | }, 24 | "eslintConfig": { 25 | "extends": [ 26 | "react-app", 27 | "react-app/jest" 28 | ] 29 | }, 30 | "browserslist": { 31 | "production": [ 32 | ">0.2%", 33 | "not dead", 34 | "not op_mini all" 35 | ], 36 | "development": [ 37 | "last 1 chrome version", 38 | "last 1 firefox version", 39 | "last 1 safari version" 40 | ] 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itsmefarhan/react-firebase-chat/6810f607c3822d46cc96d4a276adbef653619f1f/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itsmefarhan/react-firebase-chat/6810f607c3822d46cc96d4a276adbef653619f1f/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itsmefarhan/react-firebase-chat/6810f607c3822d46cc96d4a276adbef653619f1f/public/logo512.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | 5 | :root { 6 | --color-1: #242526; 7 | --color-2: white; 8 | --color-3: #0084ff; 9 | --color-4: grey; 10 | --color-5: #eb4034; 11 | --color-6: #333; 12 | } 13 | 14 | /* Navbar */ 15 | nav { 16 | display: flex; 17 | align-items: center; 18 | justify-content: space-between; 19 | height: 70px; 20 | padding: 0px 20px; 21 | background-color: var(--color-1); 22 | border-bottom: 1px solid var(--color-6); 23 | } 24 | nav a { 25 | color: var(--color-2); 26 | text-decoration: none; 27 | } 28 | nav div a { 29 | margin-right: 20px; 30 | } 31 | 32 | section { 33 | max-width: 500px; 34 | margin: 0 auto; 35 | margin-top: 100px; 36 | box-shadow: 1px 2px 10px var(--color-4); 37 | padding: 10px 20px; 38 | border-radius: 5px; 39 | } 40 | section h3 { 41 | text-align: center; 42 | font-size: 20px; 43 | color: var(--color-4); 44 | } 45 | /* Register */ 46 | section .form { 47 | margin-top: 30px; 48 | padding: 0px 20px; 49 | } 50 | .input_container { 51 | margin-top: 20px; 52 | } 53 | .input_container input { 54 | width: 100%; 55 | padding: 10px; 56 | outline: none; 57 | margin-top: 10px; 58 | border: 1px solid var(--color-6); 59 | border-radius: 5px; 60 | } 61 | .btn_container { 62 | margin: 10px 0px; 63 | text-align: center; 64 | } 65 | .btn { 66 | padding: 10px; 67 | border-radius: 5px; 68 | outline: none; 69 | border: 1px solid var(--color-4); 70 | background: var(--color-1); 71 | color: var(--color-2); 72 | cursor: pointer; 73 | transition: 0.3s ease-in-out all; 74 | font-size: 16px; 75 | } 76 | .btn:hover { 77 | transform: scale(1.05); 78 | } 79 | .error { 80 | text-align: center; 81 | color: var(--color-5); 82 | } 83 | 84 | /* Profile */ 85 | .profile_container { 86 | display: flex; 87 | align-items: center; 88 | } 89 | .img_container { 90 | position: relative; 91 | margin-right: 20px; 92 | } 93 | .img_container img { 94 | width: 100px; 95 | height: 100px; 96 | border-radius: 50%; 97 | border: 1px solid var(--color-4); 98 | transition: 0.5s ease-in-out all; 99 | } 100 | .img_container:hover img { 101 | opacity: 0.4; 102 | } 103 | .img_container:hover .overlay { 104 | opacity: 1; 105 | } 106 | .overlay { 107 | transition: 0.5s ease; 108 | opacity: 0; 109 | position: absolute; 110 | top: 50%; 111 | left: 50%; 112 | transform: translate(-50%, -50%); 113 | text-align: center; 114 | } 115 | .text_container { 116 | flex-grow: 1; 117 | } 118 | .text_container h3 { 119 | text-align: left; 120 | } 121 | 122 | .home_container { 123 | position: relative; 124 | display: grid; 125 | grid-template-columns: 1fr 3fr; 126 | overflow: hidden; 127 | height: calc(100vh - 70px); 128 | width: 100vw; 129 | } 130 | .users_container { 131 | margin-top: 10px; 132 | border-right: 2px solid var(--color-6); 133 | overflow-y: auto; 134 | } 135 | .user_wrapper { 136 | margin-bottom: 10px; 137 | padding: 10px; 138 | cursor: pointer; 139 | } 140 | .user_info { 141 | display: flex; 142 | justify-content: space-between; 143 | align-items: center; 144 | } 145 | .user_detail { 146 | display: flex; 147 | align-items: center; 148 | } 149 | .user_detail h4 { 150 | margin-left: 10px; 151 | } 152 | .avatar { 153 | width: 50px; 154 | height: 50px; 155 | border-radius: 50%; 156 | border: 1px solid var(--color-4); 157 | } 158 | .user_status { 159 | width: 10px; 160 | height: 10px; 161 | border-radius: 50%; 162 | } 163 | .online { 164 | background: #34eb52; 165 | } 166 | .offline { 167 | background: var(--color-5); 168 | } 169 | .messages_container { 170 | position: relative; 171 | width: 100%; 172 | } 173 | .messages_user { 174 | padding: 10px; 175 | text-align: center; 176 | border-bottom: 2px solid var(--color-6); 177 | } 178 | .no_conv { 179 | font-size: 20px; 180 | color: var(--color-4); 181 | text-align: center; 182 | } 183 | .message_form { 184 | position: absolute; 185 | bottom: 0; 186 | left: 20%; 187 | width: 100%; 188 | height: 30px; 189 | display: flex; 190 | align-items: center; 191 | } 192 | .message_form input { 193 | width: 40vw; 194 | margin: 0px 10px 10px; 195 | padding: 10px; 196 | border-radius: 5px; 197 | outline: none; 198 | border: none; 199 | } 200 | .message_form .btn { 201 | margin-top: -10px; 202 | } 203 | .messages { 204 | height: calc(100vh - 200px); 205 | overflow-y: auto; 206 | border-bottom: 1px solid var(--color-6); 207 | } 208 | .message_wrapper { 209 | margin-top: 5px; 210 | padding: 0px 5px; 211 | } 212 | .message_wrapper img { 213 | width: 100%; 214 | border-radius: 5px; 215 | } 216 | .message_wrapper p { 217 | padding: 10px; 218 | display: inline-block; 219 | max-width: 50%; 220 | text-align: left; 221 | border-radius: 5px; 222 | } 223 | .message_wrapper small { 224 | display: inline-block; 225 | margin-top: 15px; 226 | opacity: 0.8; 227 | } 228 | .message_wrapper.own { 229 | text-align: right; 230 | } 231 | .me { 232 | background: var(--color-3); 233 | color: white; 234 | } 235 | .friend { 236 | background: var(--color-6); 237 | } 238 | .selected_user { 239 | background: var(--color-6); 240 | } 241 | .truncate { 242 | font-size: 14px; 243 | white-space: nowrap; 244 | width: 90%; 245 | overflow: hidden; 246 | text-overflow: ellipsis; 247 | } 248 | .truncate strong { 249 | margin-right: 10px; 250 | } 251 | .unread { 252 | margin-left: 10px; 253 | background: var(--color-3); 254 | color: white; 255 | padding: 2px 4px; 256 | border-radius: 10px; 257 | } 258 | .sm_screen { 259 | display: none; 260 | } 261 | @media screen and (max-width: 767px) { 262 | .home_container { 263 | grid-template-columns: 2fr 3fr; 264 | } 265 | .message_form { 266 | left: 3%; 267 | right: 0; 268 | bottom: 5px; 269 | } 270 | .message_wrapper p { 271 | max-width: 75%; 272 | } 273 | } 274 | 275 | @media screen and (max-width: 576px) { 276 | .home_container { 277 | grid-template-columns: 1fr 5fr; 278 | } 279 | .user_wrapper { 280 | display: none; 281 | } 282 | .sm_container { 283 | padding: 10px 0px; 284 | text-align: center; 285 | cursor: pointer; 286 | } 287 | .sm_screen { 288 | display: inline-block; 289 | } 290 | .message_form input { 291 | width: 50vw; 292 | margin: 0px 10px; 293 | } 294 | .message_form .btn { 295 | margin: 0px; 296 | } 297 | 298 | .message_wrapper p { 299 | max-width: 100%; 300 | } 301 | } 302 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import "./App.css"; 2 | import { BrowserRouter, Switch, Route } from "react-router-dom"; 3 | import Home from "./pages/Home"; 4 | import Navbar from "./components/Navbar"; 5 | import Register from "./pages/Register"; 6 | import Login from "./pages/Login"; 7 | import Profile from './pages/Profile' 8 | import AuthProvider from "./context/auth"; 9 | import PrivateRoute from "./components/PrivateRoute"; 10 | 11 | function App() { 12 | return ( 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | ); 25 | } 26 | 27 | export default App; 28 | -------------------------------------------------------------------------------- /src/components/Loading.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const Loading = () => { 4 | return ( 5 |
6 |

14 | Loading... 15 |

16 |
17 | ); 18 | }; 19 | 20 | export default Loading; 21 | -------------------------------------------------------------------------------- /src/components/Message.js: -------------------------------------------------------------------------------- 1 | import React, { useRef, useEffect } from "react"; 2 | import Moment from "react-moment"; 3 | 4 | const Message = ({ msg, user1 }) => { 5 | const scrollRef = useRef(); 6 | 7 | useEffect(() => { 8 | scrollRef.current?.scrollIntoView({ behavior: "smooth" }); 9 | }, [msg]); 10 | return ( 11 |
15 |

16 | {msg.media ? {msg.text} : null} 17 | {msg.text} 18 |
19 | 20 | {msg.createdAt.toDate()} 21 | 22 |

23 |
24 | ); 25 | }; 26 | 27 | export default Message; 28 | -------------------------------------------------------------------------------- /src/components/MessageForm.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Attachment from "./svg/Attachment"; 3 | 4 | const MessageForm = ({ handleSubmit, text, setText, setImg }) => { 5 | return ( 6 |
7 | 10 | setImg(e.target.files[0])} 12 | type="file" 13 | id="img" 14 | accept="image/*" 15 | style={{ display: "none" }} 16 | /> 17 |
18 | setText(e.target.value)} 23 | /> 24 |
25 |
26 | 27 |
28 |
29 | ); 30 | }; 31 | 32 | export default MessageForm; 33 | -------------------------------------------------------------------------------- /src/components/Navbar.js: -------------------------------------------------------------------------------- 1 | import React, { useContext } from "react"; 2 | import { Link } from "react-router-dom"; 3 | import { auth, db } from "../firebase"; 4 | import { signOut } from "firebase/auth"; 5 | import { updateDoc, doc } from "firebase/firestore"; 6 | import { AuthContext } from "../context/auth"; 7 | import { useHistory } from "react-router-dom"; 8 | 9 | const Navbar = () => { 10 | const history = useHistory(); 11 | const { user } = useContext(AuthContext); 12 | 13 | const handleSignout = async () => { 14 | await updateDoc(doc(db, "users", auth.currentUser.uid), { 15 | isOnline: false, 16 | }); 17 | await signOut(auth); 18 | history.replace("/login"); 19 | }; 20 | return ( 21 | 41 | ); 42 | }; 43 | 44 | export default Navbar; 45 | -------------------------------------------------------------------------------- /src/components/PrivateRoute.js: -------------------------------------------------------------------------------- 1 | import React, { useContext } from "react"; 2 | import { AuthContext } from "../context/auth"; 3 | import { Redirect, Route } from "react-router-dom"; 4 | 5 | const PrivateRoute = ({ component: Component, ...rest }) => { 6 | const { user } = useContext(AuthContext); 7 | 8 | return ( 9 | 13 | user ? : 14 | } 15 | /> 16 | ); 17 | }; 18 | 19 | export default PrivateRoute; 20 | -------------------------------------------------------------------------------- /src/components/User.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import Img from "../image1.jpg"; 3 | import { onSnapshot, doc } from "firebase/firestore"; 4 | import { db } from "../firebase"; 5 | 6 | const User = ({ user1, user, selectUser, chat }) => { 7 | const user2 = user?.uid; 8 | const [data, setData] = useState(""); 9 | 10 | useEffect(() => { 11 | const id = user1 > user2 ? `${user1 + user2}` : `${user2 + user1}`; 12 | let unsub = onSnapshot(doc(db, "lastMsg", id), (doc) => { 13 | setData(doc.data()); 14 | }); 15 | return () => unsub(); 16 | }, []); 17 | 18 | return ( 19 | <> 20 |
selectUser(user)} 23 | > 24 |
25 |
26 | avatar 27 |

{user.name}

28 | {data?.from !== user1 && data?.unread && ( 29 | New 30 | )} 31 |
32 |
35 |
36 | {data && ( 37 |

38 | {data.from === user1 ? "Me:" : null} 39 | {data.text} 40 |

41 | )} 42 |
43 |
selectUser(user)} 45 | className={`sm_container ${chat.name === user.name && "selected_user"}`} 46 | > 47 | avatar 52 |
53 | 54 | ); 55 | }; 56 | 57 | export default User; 58 | -------------------------------------------------------------------------------- /src/components/svg/Attachment.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const Attachment = () => { 4 | return ( 5 | 16 | 21 | 22 | ); 23 | }; 24 | 25 | export default Attachment; 26 | -------------------------------------------------------------------------------- /src/components/svg/Camera.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const Camera = () => { 4 | return ( 5 | 11 | 16 | 17 | ); 18 | }; 19 | 20 | export default Camera; 21 | -------------------------------------------------------------------------------- /src/components/svg/Delete.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const Delete = ({ deleteImage }) => { 4 | return ( 5 | 17 | 22 | 23 | ); 24 | }; 25 | 26 | export default Delete; 27 | -------------------------------------------------------------------------------- /src/context/auth.js: -------------------------------------------------------------------------------- 1 | import { createContext, useEffect, useState } from "react"; 2 | import { onAuthStateChanged } from "firebase/auth"; 3 | import { auth } from "../firebase"; 4 | import Loading from "../components/Loading"; 5 | 6 | export const AuthContext = createContext(); 7 | 8 | const AuthProvider = ({ children }) => { 9 | const [user, setUser] = useState(null); 10 | const [loading, setLoading] = useState(true); 11 | 12 | useEffect(() => { 13 | onAuthStateChanged(auth, (user) => { 14 | setUser(user); 15 | setLoading(false); 16 | }); 17 | }, []); 18 | if (loading) { 19 | return ; 20 | } 21 | return ( 22 | {children} 23 | ); 24 | }; 25 | 26 | export default AuthProvider; 27 | -------------------------------------------------------------------------------- /src/firebase.js: -------------------------------------------------------------------------------- 1 | // Import the functions you need from the SDKs you need 2 | import { initializeApp } from "firebase/app"; 3 | import { getAuth } from "firebase/auth"; 4 | import { getFirestore } from "firebase/firestore"; 5 | import { getStorage } from "firebase/storage"; 6 | // TODO: Add SDKs for Firebase products that you want to use 7 | // https://firebase.google.com/docs/web/setup#available-libraries 8 | 9 | // Your web app's Firebase configuration 10 | const firebaseConfig = { 11 | apiKey: process.env.REACT_APP_API_KEY, 12 | authDomain: process.env.REACT_APP_AUTH_DOMAIN, 13 | databaseURL: process.env.REACT_APP_DATABASE_URL, 14 | projectId: process.env.REACT_APP_PROJECT_ID, 15 | storageBucket: process.env.REACT_APP_STORAGE_BUCKET, 16 | messagingSenderId: process.env.REACT_APP_MESSAGING_SENDER_ID, 17 | appId: process.env.REACT_APP_APP_ID, 18 | }; 19 | 20 | // Initialize Firebase 21 | const app = initializeApp(firebaseConfig); 22 | const auth = getAuth(app); 23 | const db = getFirestore(app); 24 | const storage = getStorage(app); 25 | 26 | export { auth, db, storage }; 27 | -------------------------------------------------------------------------------- /src/image1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itsmefarhan/react-firebase-chat/6810f607c3822d46cc96d4a276adbef653619f1f/src/image1.jpg -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", 4 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | background: #242526; 9 | color: white; 10 | } 11 | 12 | code { 13 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", 14 | monospace; 15 | } 16 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | 6 | ReactDOM.render( 7 | 8 | 9 | , 10 | document.getElementById('root') 11 | ); 12 | -------------------------------------------------------------------------------- /src/pages/Home.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import { db, auth, storage } from "../firebase"; 3 | import { 4 | collection, 5 | query, 6 | where, 7 | onSnapshot, 8 | addDoc, 9 | Timestamp, 10 | orderBy, 11 | setDoc, 12 | doc, 13 | getDoc, 14 | updateDoc, 15 | } from "firebase/firestore"; 16 | import { ref, getDownloadURL, uploadBytes } from "firebase/storage"; 17 | import User from "../components/User"; 18 | import MessageForm from "../components/MessageForm"; 19 | import Message from "../components/Message"; 20 | 21 | const Home = () => { 22 | const [users, setUsers] = useState([]); 23 | const [chat, setChat] = useState(""); 24 | const [text, setText] = useState(""); 25 | const [img, setImg] = useState(""); 26 | const [msgs, setMsgs] = useState([]); 27 | 28 | const user1 = auth.currentUser.uid; 29 | 30 | useEffect(() => { 31 | const usersRef = collection(db, "users"); 32 | // create query object 33 | const q = query(usersRef, where("uid", "not-in", [user1])); 34 | // execute query 35 | const unsub = onSnapshot(q, (querySnapshot) => { 36 | let users = []; 37 | querySnapshot.forEach((doc) => { 38 | users.push(doc.data()); 39 | }); 40 | setUsers(users); 41 | }); 42 | return () => unsub(); 43 | }, []); 44 | 45 | const selectUser = async (user) => { 46 | setChat(user); 47 | 48 | const user2 = user.uid; 49 | const id = user1 > user2 ? `${user1 + user2}` : `${user2 + user1}`; 50 | 51 | const msgsRef = collection(db, "messages", id, "chat"); 52 | const q = query(msgsRef, orderBy("createdAt", "asc")); 53 | 54 | onSnapshot(q, (querySnapshot) => { 55 | let msgs = []; 56 | querySnapshot.forEach((doc) => { 57 | msgs.push(doc.data()); 58 | }); 59 | setMsgs(msgs); 60 | }); 61 | 62 | // get last message b/w logged in user and selected user 63 | const docSnap = await getDoc(doc(db, "lastMsg", id)); 64 | // if last message exists and message is from selected user 65 | if (docSnap.data() && docSnap.data().from !== user1) { 66 | // update last message doc, set unread to false 67 | await updateDoc(doc(db, "lastMsg", id), { unread: false }); 68 | } 69 | }; 70 | 71 | const handleSubmit = async (e) => { 72 | e.preventDefault(); 73 | 74 | const user2 = chat.uid; 75 | 76 | const id = user1 > user2 ? `${user1 + user2}` : `${user2 + user1}`; 77 | 78 | let url; 79 | if (img) { 80 | const imgRef = ref( 81 | storage, 82 | `images/${new Date().getTime()} - ${img.name}` 83 | ); 84 | const snap = await uploadBytes(imgRef, img); 85 | const dlUrl = await getDownloadURL(ref(storage, snap.ref.fullPath)); 86 | url = dlUrl; 87 | } 88 | 89 | await addDoc(collection(db, "messages", id, "chat"), { 90 | text, 91 | from: user1, 92 | to: user2, 93 | createdAt: Timestamp.fromDate(new Date()), 94 | media: url || "", 95 | }); 96 | 97 | await setDoc(doc(db, "lastMsg", id), { 98 | text, 99 | from: user1, 100 | to: user2, 101 | createdAt: Timestamp.fromDate(new Date()), 102 | media: url || "", 103 | unread: true, 104 | }); 105 | 106 | setText(""); 107 | setImg(""); 108 | }; 109 | return ( 110 |
111 |
112 | {users.map((user) => ( 113 | 120 | ))} 121 |
122 |
123 | {chat ? ( 124 | <> 125 |
126 |

{chat.name}

127 |
128 |
129 | {msgs.length 130 | ? msgs.map((msg, i) => ( 131 | 132 | )) 133 | : null} 134 |
135 | 141 | 142 | ) : ( 143 |

Select a user to start conversation

144 | )} 145 |
146 |
147 | ); 148 | }; 149 | 150 | export default Home; 151 | -------------------------------------------------------------------------------- /src/pages/Login.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { signInWithEmailAndPassword } from "firebase/auth"; 3 | import { auth, db } from "../firebase"; 4 | import { updateDoc, doc } from "firebase/firestore"; 5 | import { useHistory } from "react-router-dom"; 6 | 7 | const Login = () => { 8 | const [data, setData] = useState({ 9 | email: "", 10 | password: "", 11 | error: null, 12 | loading: false, 13 | }); 14 | 15 | const history = useHistory(); 16 | 17 | const { email, password, error, loading } = data; 18 | 19 | const handleChange = (e) => { 20 | setData({ ...data, [e.target.name]: e.target.value }); 21 | }; 22 | 23 | const handleSubmit = async (e) => { 24 | e.preventDefault(); 25 | setData({ ...data, error: null, loading: true }); 26 | if (!email || !password) { 27 | setData({ ...data, error: "All fields are required" }); 28 | } 29 | try { 30 | const result = await signInWithEmailAndPassword(auth, email, password); 31 | 32 | await updateDoc(doc(db, "users", result.user.uid), { 33 | isOnline: true, 34 | }); 35 | setData({ 36 | email: "", 37 | password: "", 38 | error: null, 39 | loading: false, 40 | }); 41 | history.replace("/"); 42 | } catch (err) { 43 | setData({ ...data, error: err.message, loading: false }); 44 | } 45 | }; 46 | return ( 47 |
48 |

Log into your Account

49 |
50 |
51 | 52 | 58 |
59 |
60 | 61 | 67 |
68 | {error ?

{error}

: null} 69 |
70 | 73 |
74 |
75 |
76 | ); 77 | }; 78 | 79 | export default Login; 80 | -------------------------------------------------------------------------------- /src/pages/Profile.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import Camera from "../components/svg/Camera"; 3 | import Img from "../image1.jpg"; 4 | import { storage, db, auth } from "../firebase"; 5 | import { 6 | ref, 7 | getDownloadURL, 8 | uploadBytes, 9 | deleteObject, 10 | } from "firebase/storage"; 11 | import { getDoc, doc, updateDoc } from "firebase/firestore"; 12 | import Delete from "../components/svg/Delete"; 13 | import { useHistory } from "react-router-dom"; 14 | 15 | const Profile = () => { 16 | const [img, setImg] = useState(""); 17 | const [user, setUser] = useState(); 18 | const history = useHistory(""); 19 | 20 | useEffect(() => { 21 | getDoc(doc(db, "users", auth.currentUser.uid)).then((docSnap) => { 22 | if (docSnap.exists) { 23 | setUser(docSnap.data()); 24 | } 25 | }); 26 | 27 | if (img) { 28 | const uploadImg = async () => { 29 | const imgRef = ref( 30 | storage, 31 | `avatar/${new Date().getTime()} - ${img.name}` 32 | ); 33 | try { 34 | if (user.avatarPath) { 35 | await deleteObject(ref(storage, user.avatarPath)); 36 | } 37 | const snap = await uploadBytes(imgRef, img); 38 | const url = await getDownloadURL(ref(storage, snap.ref.fullPath)); 39 | 40 | await updateDoc(doc(db, "users", auth.currentUser.uid), { 41 | avatar: url, 42 | avatarPath: snap.ref.fullPath, 43 | }); 44 | 45 | setImg(""); 46 | } catch (err) { 47 | console.log(err.message); 48 | } 49 | }; 50 | uploadImg(); 51 | } 52 | }, [img]); 53 | 54 | const deleteImage = async () => { 55 | try { 56 | const confirm = window.confirm("Delete avatar?"); 57 | if (confirm) { 58 | await deleteObject(ref(storage, user.avatarPath)); 59 | 60 | await updateDoc(doc(db, "users", auth.currentUser.uid), { 61 | avatar: "", 62 | avatarPath: "", 63 | }); 64 | history.replace("/"); 65 | } 66 | } catch (err) { 67 | console.log(err.message); 68 | } 69 | }; 70 | return user ? ( 71 |
72 |
73 |
74 | avatar 75 |
76 |
77 | 80 | {user.avatar ? : null} 81 | setImg(e.target.files[0])} 87 | /> 88 |
89 |
90 |
91 |
92 |

{user.name}

93 |

{user.email}

94 |
95 | Joined on: {user.createdAt.toDate().toDateString()} 96 |
97 |
98 |
99 | ) : null; 100 | }; 101 | 102 | export default Profile; 103 | -------------------------------------------------------------------------------- /src/pages/Register.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { createUserWithEmailAndPassword } from "firebase/auth"; 3 | import { auth, db } from "../firebase"; 4 | import { setDoc, doc, Timestamp } from "firebase/firestore"; 5 | import { useHistory } from "react-router-dom"; 6 | 7 | const Register = () => { 8 | const [data, setData] = useState({ 9 | name: "", 10 | email: "", 11 | password: "", 12 | error: null, 13 | loading: false, 14 | }); 15 | 16 | const history = useHistory(); 17 | 18 | const { name, email, password, error, loading } = data; 19 | 20 | const handleChange = (e) => { 21 | setData({ ...data, [e.target.name]: e.target.value }); 22 | }; 23 | 24 | const handleSubmit = async (e) => { 25 | e.preventDefault(); 26 | setData({ ...data, error: null, loading: true }); 27 | if (!name || !email || !password) { 28 | setData({ ...data, error: "All fields are required" }); 29 | } 30 | try { 31 | const result = await createUserWithEmailAndPassword( 32 | auth, 33 | email, 34 | password 35 | ); 36 | await setDoc(doc(db, "users", result.user.uid), { 37 | uid: result.user.uid, 38 | name, 39 | email, 40 | createdAt: Timestamp.fromDate(new Date()), 41 | isOnline: true, 42 | }); 43 | setData({ 44 | name: "", 45 | email: "", 46 | password: "", 47 | error: null, 48 | loading: false, 49 | }); 50 | history.replace("/"); 51 | } catch (err) { 52 | setData({ ...data, error: err.message, loading: false }); 53 | } 54 | }; 55 | return ( 56 |
57 |

Create An Account

58 |
59 |
60 | 61 | 62 |
63 |
64 | 65 | 71 |
72 |
73 | 74 | 80 |
81 | {error ?

{error}

: null} 82 |
83 | 86 |
87 |
88 |
89 | ); 90 | }; 91 | 92 | export default Register; 93 | --------------------------------------------------------------------------------