├── README.md
├── package.json
├── public
├── favicon.ico
├── index.html
└── manifest.json
└── src
├── AppContainer.js
├── ChatList
├── chatList.js
└── styles.js
├── ChatTextBox
├── chatTextBox.js
└── styles.js
├── ChatView
├── chatView.js
└── styles.js
├── Dashboard
├── dashboard.js
└── styles.js
├── Login
├── login.js
└── styles.js
├── NewChat
├── newChat.js
└── styles.js
├── Signup
├── signup.js
└── styles.js
├── index.css
├── index.js
├── logo.svg
└── serviceWorker.js
/README.md:
--------------------------------------------------------------------------------
1 | # React-Chat-Tutorial
2 |
3 | [Link to the tutorial series here](https://www.youtube.com/playlist?list=PL2pKsGhl_rg8ojPTdkoroDgWLg0NS4qhl)
4 |
5 | In this tutorial series we will be building a fully functional instant messaging application using React and Firebase. This tutorial series is directed more towards the intermediate to advanced level developers however it is still a great way to learn the basics.
6 |
7 | Please consider becoming a Patron!
8 |
9 | If you would like to support me on Patreon: https://www.patreon.com/portexe
10 |
11 | Follow me on social media! Links on my website: https://www.portexe.com/
12 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "im",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@material-ui/core": "^3.9.3",
7 | "@material-ui/icons": "^4.0.0",
8 | "firebase": "^6.0.2",
9 | "react": "^16.8.6",
10 | "react-dom": "^16.8.6",
11 | "react-router-dom": "^5.0.0",
12 | "react-scripts": "3.0.1"
13 | },
14 | "scripts": {
15 | "start": "react-scripts start",
16 | "build": "react-scripts build",
17 | "test": "react-scripts test",
18 | "eject": "react-scripts eject"
19 | },
20 | "eslintConfig": {
21 | "extends": "react-app"
22 | },
23 | "browserslist": {
24 | "production": [
25 | ">0.2%",
26 | "not dead",
27 | "not op_mini all"
28 | ],
29 | "development": [
30 | "last 1 chrome version",
31 | "last 1 firefox version",
32 | "last 1 safari version"
33 | ]
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/portexe/React-Chat-Tutorial/2b9aa61924f0282432b26e41e31ec2e604a7414c/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
22 | React App
23 |
24 |
25 | You need to enable JavaScript to run this app.
26 |
27 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/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 | "start_url": ".",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/src/AppContainer.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import LoginComponent from './Login/login';
3 | import SignupComponent from './Signup/signup';
4 | import DashboardComponent from './Dashboard/dashboard';
5 | const firebase = require("firebase");
6 | require("firebase/firestore"); // Required for side-effects
7 |
8 | class AppContainer extends React.Component {
9 |
10 | componentWillMount() {
11 | this.checkForSavedAuth();
12 | }
13 |
14 | // constructor() {
15 | // super();
16 | // firebase.initializeApp({
17 | // apiKey: "AIzaSyAlWBvbvLv7dT6_RYnlCeZbOcotpeBU3Y8",
18 | // authDomain: "im-app-tutorial.firebaseapp.com",
19 | // databaseURL: "https://im-app-tutorial.firebaseio.com",
20 | // projectId: "im-app-tutorial",
21 | // storageBucket: "im-app-tutorial.appspot.com",
22 | // messagingSenderId: "199544684635",
23 | // appId: "1:199544684635:web:fb388e2c181f0476"
24 | // });
25 | // this.db = firebase.firestore();
26 | // this.state = {
27 | // user: null
28 | // };
29 | // }
30 |
31 | render() {
32 | if(this.state.user)
33 | return
34 | else
35 | return
36 | }
37 |
38 | checkForSavedAuth = () => {
39 | console.log(firebase.auth().currentUser);
40 | };
41 |
42 | loggedIn = (user) => this.setState({ user: user });
43 |
44 | }
45 |
46 | export default AppContainer;
--------------------------------------------------------------------------------
/src/ChatList/chatList.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { withStyles } from '@material-ui/core/styles';
3 | import List from '@material-ui/core/List';
4 | import ListItem from '@material-ui/core/ListItem';
5 | import ListItemText from '@material-ui/core/ListItemText';
6 | import ListItemAvatar from '@material-ui/core/ListItemAvatar';
7 | import Avatar from '@material-ui/core/Avatar';
8 | import Typography from '@material-ui/core/Typography';
9 | import styles from './styles';
10 | import Divider from '@material-ui/core/Divider';
11 | import Button from '@material-ui/core/Button';
12 | import ListItemIcon from '@material-ui/core/ListItemIcon';
13 | import NotificationImportant from '@material-ui/icons/NotificationImportant';
14 |
15 | class ChatListComponent extends React.Component {
16 |
17 | render() {
18 |
19 | const { classes } = this.props;
20 |
21 | if(this.props.chats.length > 0) {
22 | return(
23 |
24 |
29 | New Message
30 |
31 |
32 | {
33 | this.props.chats.map((_chat, _index) => {
34 | return (
35 |
36 |
this.selectChat(_index)}
37 | className={classes.listItem}
38 | selected={this.props.selectedChatIndex === _index}
39 | alignItems="flex-start">
40 |
41 | {_chat.users.filter(_user => _user !== this.props.userEmail)[0].split('')[0]}
42 |
43 | _user !== this.props.userEmail)[0]}
45 | secondary={
46 |
47 |
49 | {_chat.messages[_chat.messages.length - 1].message.substring(0, 30) + ' ...'}
50 |
51 |
52 | }/>
53 | {
54 | _chat.receiverHasRead === false && !this.userIsSender(_chat) ?
55 | :
56 | null
57 | }
58 |
59 |
60 |
61 | )
62 | })
63 | }
64 |
65 |
66 | );
67 | } else {
68 | return(
69 |
70 |
75 | New Message
76 |
77 |
78 |
79 | );
80 | }
81 | }
82 | userIsSender = (chat) => chat.messages[chat.messages.length - 1].sender === this.props.userEmail;
83 | newChat = () => this.props.newChatBtnFn();
84 | selectChat = (index) => this.props.selectChatFn(index);
85 | }
86 |
87 | export default withStyles(styles)(ChatListComponent);
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 | // import React from 'react';
102 | // import './chatList.css';
103 |
104 | // class ChatListComponent extends React.Component {
105 | // render() {
106 | // if(this.props.chats.length > 0) {
107 | // return(
108 | //
109 | //
New Message
110 | // {
111 | // this.props.chats.map((_chat, _index) => {
112 | // return (
113 | //
this.selectChat(_index)} key={_index} className={`individual-chat-container ${this.props.selectedChatIndex === _index ? 'selected-chat' : ''}`}>
114 | //
{_chat.users.filter(_user => _user !== this.props.userEmail)[0]}
115 | //
{_chat.messages[_chat.messages.length - 1].message.substring(0, 30) + ' ...'}
116 | //
117 | // )
118 | // })
119 | // }
120 | //
121 | // );
122 | // } else {
123 | // return(
124 | //
125 | // New Message
126 | //
127 | // );
128 | // }
129 | // }
130 | // newChat = () => this.props.newChatBtnFn();
131 | // selectChat = (index) => this.props.selectChatFn(index);
132 | // }
133 |
134 | // export default ChatListComponent;
--------------------------------------------------------------------------------
/src/ChatList/styles.js:
--------------------------------------------------------------------------------
1 | const styles = theme => ({
2 | root: {
3 | backgroundColor: theme.palette.background.paper,
4 | height: 'calc(100% - 35px)',
5 | position: 'absolute',
6 | left: '0',
7 | width: '300px',
8 | boxShadow: '0px 0px 2px black'
9 | },
10 | listItem: {
11 | cursor: 'pointer'
12 | },
13 | newChatBtn: {
14 | borderRadius: '0px'
15 | },
16 | unreadMessage: {
17 | color: 'red',
18 | position: 'absolute',
19 | top: '0',
20 | right: '5px'
21 | }
22 | });
23 |
24 | export default styles;
--------------------------------------------------------------------------------
/src/ChatTextBox/chatTextBox.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import TextField from '@material-ui/core/TextField';
3 | import Send from '@material-ui/icons/Send';
4 | import styles from './styles';
5 | import { withStyles } from '@material-ui/core/styles';
6 |
7 | class ChatTextBoxComponent extends React.Component {
8 |
9 | constructor() {
10 | super();
11 | this.state = {
12 | chatText: ''
13 | };
14 | }
15 |
16 | render() {
17 |
18 | const { classes } = this.props;
19 |
20 | return(
21 |
22 | this.userTyping(e)}
25 | id='chattextbox'
26 | className={classes.chatTextBox}
27 | onFocus={this.userClickedInput}>
28 |
29 |
30 |
31 | );
32 | }
33 | userTyping = (e) => e.keyCode === 13 ? this.submitMessage() : this.setState({ chatText: e.target.value });
34 | messageValid = (txt) => txt && txt.replace(/\s/g, '').length;
35 | userClickedInput = () => this.props.userClickedInputFn();
36 | submitMessage = () => {
37 | if(this.messageValid(this.state.chatText)) {
38 | this.props.submitMessageFn(this.state.chatText);
39 | document.getElementById('chattextbox').value = '';
40 | }
41 | }
42 | }
43 |
44 | export default withStyles(styles)(ChatTextBoxComponent);
--------------------------------------------------------------------------------
/src/ChatTextBox/styles.js:
--------------------------------------------------------------------------------
1 | const styles = theme => ({
2 |
3 | sendBtn: {
4 | color: 'blue',
5 | cursor: 'pointer',
6 | '&:hover': {
7 | color: 'gray'
8 | }
9 | },
10 |
11 | chatTextBoxContainer: {
12 | position: 'absolute',
13 | bottom: '15px',
14 | left: '315px',
15 | boxSizing: 'border-box',
16 | overflow: 'auto',
17 | width: 'calc(100% - 300px - 50px)'
18 | },
19 |
20 | chatTextBox: {
21 | width: 'calc(100% - 25px)'
22 | }
23 |
24 | });
25 |
26 | export default styles;
--------------------------------------------------------------------------------
/src/ChatView/chatView.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styles from './styles';
3 | import { withStyles } from '@material-ui/core/styles';
4 |
5 | class ChatViewComponent extends React.Component {
6 |
7 | componentDidMount = () => {
8 | const container = document.getElementById('chatview-container');
9 | if(container)
10 | container.scrollTo(0, container.scrollHeight);
11 | }
12 | componentDidUpdate = () => {
13 | const container = document.getElementById('chatview-container');
14 | if(container)
15 | container.scrollTo(0, container.scrollHeight);
16 | }
17 |
18 | render() {
19 |
20 | const { classes } = this.props;
21 |
22 | if(this.props.chat === undefined) {
23 | return( );
24 | } else if(this.props.chat !== undefined) {
25 | return(
26 |
27 |
28 | Your conversation with {this.props.chat.users.filter(_usr => _usr !== this.props.user)[0]}
29 |
30 |
31 | {
32 | this.props.chat.messages.map((_msg, _index) => {
33 | return(
34 |
35 | {_msg.message}
36 |
37 | )
38 | })
39 | }
40 |
41 |
42 | );
43 | } else {
44 | return (Loading...
);
45 | }
46 | }
47 | }
48 |
49 | export default withStyles(styles)(ChatViewComponent);
--------------------------------------------------------------------------------
/src/ChatView/styles.js:
--------------------------------------------------------------------------------
1 | const styles = theme => ({
2 |
3 | content: {
4 | height: 'calc(100vh - 100px)',
5 | overflow: 'auto',
6 | padding: '25px',
7 | marginLeft: '300px',
8 | boxSizing: 'border-box',
9 | overflowY: 'scroll',
10 | top: '50px',
11 | width: 'calc(100% - 300px)',
12 | position: 'absolute'
13 | },
14 |
15 | userSent: {
16 | float: 'left',
17 | clear: 'both',
18 | padding: '20px',
19 | boxSizing: 'border-box',
20 | wordWrap: 'break-word',
21 | marginTop: '10px',
22 | backgroundColor: '#707BC4',
23 | color: 'white',
24 | width: '300px',
25 | borderRadius: '10px'
26 | },
27 |
28 | friendSent: {
29 | float: 'right',
30 | clear: 'both',
31 | padding: '20px',
32 | boxSizing: 'border-box',
33 | wordWrap: 'break-word',
34 | marginTop: '10px',
35 | backgroundColor: '#707BC4',
36 | color: 'white',
37 | width: '300px',
38 | borderRadius: '10px'
39 | },
40 |
41 | chatHeader: {
42 | width: 'calc(100% - 301px)',
43 | height: '50px',
44 | backgroundColor: '#344195',
45 | position: 'fixed',
46 | marginLeft: '301px',
47 | fontSize: '18px',
48 | textAlign: 'center',
49 | color: 'white',
50 | paddingTop: '10px',
51 | boxSizing: 'border-box'
52 | }
53 |
54 | });
55 |
56 | export default styles;
--------------------------------------------------------------------------------
/src/Dashboard/dashboard.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import NewChatComponent from '../NewChat/newChat';
3 | import ChatListComponent from '../ChatList/chatList';
4 | import ChatViewComponent from '../ChatView/chatView';
5 | import ChatTextBoxComponent from '../ChatTextBox/chatTextBox';
6 | import styles from './styles';
7 | import { Button, withStyles } from '@material-ui/core';
8 | const firebase = require("firebase");
9 |
10 | // I need to investigate why sometimes
11 | // two messages will send instead of just
12 | // one. I dont know if there are two instances
13 | // of the chat box component or what...
14 |
15 | // I will be using both .then and async/await
16 | // in this tutorial to give a feel of both.
17 |
18 | class DashboardComponent extends React.Component {
19 |
20 | constructor() {
21 | super();
22 | this.state = {
23 | selectedChat: null,
24 | newChatFormVisible: false,
25 | email: null,
26 | friends: [],
27 | chats: []
28 | };
29 | }
30 |
31 | render() {
32 |
33 | const { classes } = this.props;
34 |
35 | if(this.state.email) {
36 | return(
37 |
38 |
44 |
45 | {
46 | this.state.newChatFormVisible ? null :
49 |
50 | }
51 | {
52 | this.state.selectedChat !== null && !this.state.newChatFormVisible ? : null
53 | }
54 | {
55 | this.state.newChatFormVisible ? : null
56 | }
57 | Sign Out
58 |
59 | );
60 | } else {
61 | return(LOADING....
);
62 | }
63 | }
64 |
65 | signOut = () => firebase.auth().signOut();
66 |
67 | submitMessage = (msg) => {
68 | const docKey = this.buildDocKey(this.state.chats[this.state.selectedChat]
69 | .users
70 | .filter(_usr => _usr !== this.state.email)[0])
71 | firebase
72 | .firestore()
73 | .collection('chats')
74 | .doc(docKey)
75 | .update({
76 | messages: firebase.firestore.FieldValue.arrayUnion({
77 | sender: this.state.email,
78 | message: msg,
79 | timestamp: Date.now()
80 | }),
81 | receiverHasRead: false
82 | });
83 | }
84 |
85 | // Always in alphabetical order:
86 | // 'user1:user2'
87 | buildDocKey = (friend) => [this.state.email, friend].sort().join(':');
88 |
89 | newChatBtnClicked = () => this.setState({ newChatFormVisible: true, selectedChat: null });
90 |
91 | newChatSubmit = async (chatObj) => {
92 | const docKey = this.buildDocKey(chatObj.sendTo);
93 | await
94 | firebase
95 | .firestore()
96 | .collection('chats')
97 | .doc(docKey)
98 | .set({
99 | messages: [{
100 | message: chatObj.message,
101 | sender: this.state.email
102 | }],
103 | users: [this.state.email, chatObj.sendTo],
104 | receiverHasRead: false
105 | })
106 | this.setState({ newChatFormVisible: false });
107 | this.selectChat(this.state.chats.length - 1);
108 | }
109 |
110 | selectChat = async (chatIndex) => {
111 | await this.setState({ selectedChat: chatIndex, newChatFormVisible: false });
112 | this.messageRead();
113 | }
114 |
115 | goToChat = async (docKey, msg) => {
116 | const usersInChat = docKey.split(':');
117 | const chat = this.state.chats.find(_chat => usersInChat.every(_user => _chat.users.includes(_user)));
118 | this.setState({ newChatFormVisible: false });
119 | await this.selectChat(this.state.chats.indexOf(chat));
120 | this.submitMessage(msg);
121 | }
122 |
123 | // Chat index could be different than the one we are currently on in the case
124 | // that we are calling this function from within a loop such as the chatList.
125 | // So we will set a default value and can overwrite it when necessary.
126 | messageRead = () => {
127 | const chatIndex = this.state.selectedChat;
128 | const docKey = this.buildDocKey(this.state.chats[chatIndex].users.filter(_usr => _usr !== this.state.email)[0]);
129 | if(this.clickedMessageWhereNotSender(chatIndex)) {
130 | firebase
131 | .firestore()
132 | .collection('chats')
133 | .doc(docKey)
134 | .update({ receiverHasRead: true });
135 | } else {
136 | console.log('Clicked message where the user was the sender');
137 | }
138 | }
139 |
140 | clickedMessageWhereNotSender = (chatIndex) => this.state.chats[chatIndex].messages[this.state.chats[chatIndex].messages.length - 1].sender !== this.state.email;
141 |
142 | componentWillMount = () => {
143 | firebase.auth().onAuthStateChanged(async _usr => {
144 | if(!_usr)
145 | this.props.history.push('/login');
146 | else {
147 | await firebase
148 | .firestore()
149 | .collection('chats')
150 | .where('users', 'array-contains', _usr.email)
151 | .onSnapshot(async res => {
152 | const chats = res.docs.map(_doc => _doc.data());
153 | await this.setState({
154 | email: _usr.email,
155 | chats: chats,
156 | friends: []
157 | });
158 | })
159 | }
160 | });
161 | }
162 | }
163 |
164 | export default withStyles(styles)(DashboardComponent);
--------------------------------------------------------------------------------
/src/Dashboard/styles.js:
--------------------------------------------------------------------------------
1 | const styles = theme => ({
2 | signOutBtn: {
3 | position: 'absolute',
4 | bottom: '0px',
5 | left: '0px',
6 | width: '300px',
7 | borderRadius: '0px',
8 | backgroundColor: '#227092',
9 | height: '35px',
10 | boxShadow: '0px 0px 2px black',
11 | color: 'white'
12 | }
13 | });
14 |
15 | export default styles;
--------------------------------------------------------------------------------
/src/Login/login.js:
--------------------------------------------------------------------------------
1 | import { Link } from 'react-router-dom';
2 | import React from 'react';
3 | import styles from './styles';
4 | import FormControl from '@material-ui/core/FormControl';
5 | import InputLabel from '@material-ui/core/InputLabel';
6 | import Input from '@material-ui/core/Input';
7 | import Paper from '@material-ui/core/Paper';
8 | import withStyles from '@material-ui/core/styles/withStyles';
9 | import CssBaseline from '@material-ui/core/CssBaseline';
10 | import Typography from '@material-ui/core/Typography';
11 | import Button from '@material-ui/core/Button';
12 |
13 | const firebase = require("firebase");
14 |
15 | class LoginComponent extends React.Component {
16 |
17 | constructor() {
18 | super();
19 | this.state = {
20 | email: null,
21 | password: null,
22 | serverError: false
23 | };
24 | }
25 |
26 | render() {
27 |
28 | const { classes } = this.props;
29 |
30 | return (
31 |
32 |
33 |
34 |
35 | Log In!
36 |
37 |
48 | { this.state.serverError ?
49 |
50 | Incorrect Login Information
51 | :
52 | null
53 | }
54 | Don't Have An Account?
55 | Sign Up!
56 |
57 |
58 | );
59 | }
60 |
61 | userTyping = (whichInput, event) => {
62 | switch (whichInput) {
63 | case 'email':
64 | this.setState({ email: event.target.value });
65 | break;
66 |
67 | case 'password':
68 | this.setState({ password: event.target.value });
69 | break;
70 |
71 | default:
72 | break;
73 | }
74 | }
75 |
76 | submitLogin = async (e) => {
77 | e.preventDefault(); // This is to prevent the automatic refreshing of the page on submit.
78 |
79 | await firebase
80 | .auth()
81 | .signInWithEmailAndPassword(this.state.email, this.state.password)
82 | .then(() => {
83 | this.props.history.push('/dashboard');
84 | }, err => {
85 | this.setState({ serverError: true });
86 | console.log('Error logging in: ', err);
87 | });
88 | };
89 |
90 | }
91 |
92 | export default withStyles(styles)(LoginComponent);
--------------------------------------------------------------------------------
/src/Login/styles.js:
--------------------------------------------------------------------------------
1 | const styles = theme => ({
2 | main: {
3 | width: 'auto',
4 | display: 'block', // Fix IE 11 issue.
5 | marginLeft: theme.spacing.unit * 3,
6 | marginRight: theme.spacing.unit * 3,
7 | [theme.breakpoints.up(400 + theme.spacing.unit * 3 * 2)]: {
8 | width: 400,
9 | marginLeft: 'auto',
10 | marginRight: 'auto',
11 | },
12 | },
13 | paper: {
14 | marginTop: theme.spacing.unit * 8,
15 | display: 'flex',
16 | flexDirection: 'column',
17 | alignItems: 'center',
18 | padding: `${theme.spacing.unit * 2}px ${theme.spacing.unit * 3}px ${theme.spacing.unit * 3}px`,
19 | },
20 | form: {
21 | width: '100%',
22 | marginTop: theme.spacing.unit,
23 | },
24 | submit: {
25 | marginTop: theme.spacing.unit * 3,
26 | },
27 | noAccountHeader: {
28 | width: '100%'
29 | },
30 | signUpLink: {
31 | width: '100%',
32 | textDecoration: 'none',
33 | color: '#303f9f',
34 | fontWeight: 'bolder'
35 | },
36 | errorText: {
37 | color: 'red',
38 | textAlign: 'center'
39 | }
40 | });
41 |
42 | export default styles;
--------------------------------------------------------------------------------
/src/NewChat/newChat.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { FormControl, InputLabel, Input, Button, Paper, withStyles, CssBaseline, Typography } from '@material-ui/core';
3 | import styles from './styles';
4 | const firebase = require("firebase");
5 |
6 | class NewChatComponent extends React.Component {
7 |
8 | constructor() {
9 | super();
10 | this.state = {
11 | username: null,
12 | message: null
13 | };
14 | }
15 |
16 | render() {
17 |
18 | const { classes } = this.props;
19 |
20 | return(
21 |
22 |
23 |
24 | Send A Message!
25 |
49 | {
50 | this.state.serverError ?
51 |
52 | Unable to locate the user
53 | :
54 | null
55 | }
56 |
57 |
58 | );
59 | }
60 |
61 | componentWillMount() {
62 | if(!firebase.auth().currentUser)
63 | this.props.history.push('/login');
64 | }
65 |
66 | userTyping = (inputType, e) => {
67 | switch (inputType) {
68 | case 'username':
69 | this.setState({ username: e.target.value });
70 | break;
71 |
72 | case 'message':
73 | this.setState({ message: e.target.value });
74 | break;
75 |
76 | default:
77 | break;
78 | }
79 | }
80 |
81 | submitNewChat = async (e) => {
82 | e.preventDefault();
83 | const userExists = await this.userExists();
84 | if(userExists) {
85 | const chatExists = await this.chatExists();
86 | chatExists ? this.goToChat() : this.createChat();
87 | }
88 | }
89 |
90 | buildDocKey = () => [firebase.auth().currentUser.email, this.state.username].sort().join(':');
91 |
92 | createChat = () => {
93 | this.props.newChatSubmitFn({
94 | sendTo: this.state.username,
95 | message: this.state.message
96 | });
97 | }
98 |
99 | goToChat = () => this.props.goToChatFn(this.buildDocKey(), this.state.message);
100 |
101 | chatExists = async () => {
102 | const docKey = this.buildDocKey();
103 | const chat = await
104 | firebase
105 | .firestore()
106 | .collection('chats')
107 | .doc(docKey)
108 | .get();
109 | console.log(chat.exists);
110 | return chat.exists;
111 | }
112 | userExists = async () => {
113 | const usersSnapshot = await
114 | firebase
115 | .firestore()
116 | .collection('users')
117 | .get();
118 | const exists = usersSnapshot
119 | .docs
120 | .map(_doc => _doc.data().email)
121 | .includes(this.state.username);
122 | this.setState({ serverError: !exists });
123 | return exists;
124 | }
125 | }
126 |
127 | export default withStyles(styles)(NewChatComponent);
--------------------------------------------------------------------------------
/src/NewChat/styles.js:
--------------------------------------------------------------------------------
1 | const styles = theme => ({
2 | main: {
3 | width: 'auto',
4 | display: 'block', // Fix IE 11 issue.
5 | marginLeft: theme.spacing.unit * 3,
6 | marginRight: theme.spacing.unit * 3,
7 | [theme.breakpoints.up(400 + theme.spacing.unit * 3 * 2)]: {
8 | width: 400,
9 | marginLeft: 'auto',
10 | marginRight: 'auto',
11 | },
12 | },
13 | paper: {
14 | padding: `${theme.spacing.unit * 2}px ${theme.spacing.unit * 3}px ${theme.spacing.unit * 3}px`,
15 | position: 'absolute',
16 | width: '350px',
17 | top: '50px',
18 | left: 'calc(50% + 150px - 175px)'
19 | },
20 | input: {
21 | },
22 | form: {
23 | width: '100%',
24 | marginTop: theme.spacing.unit,
25 | },
26 | submit: {
27 | marginTop: theme.spacing.unit * 3
28 | },
29 | errorText: {
30 | color: 'red',
31 | textAlign: 'center'
32 | }
33 | });
34 |
35 | export default styles;
--------------------------------------------------------------------------------
/src/Signup/signup.js:
--------------------------------------------------------------------------------
1 | import { Link } from 'react-router-dom';
2 | import React from 'react';
3 | import styles from './styles';
4 | import FormControl from '@material-ui/core/FormControl';
5 | import InputLabel from '@material-ui/core/InputLabel';
6 | import Input from '@material-ui/core/Input';
7 | import Paper from '@material-ui/core/Paper';
8 | import withStyles from '@material-ui/core/styles/withStyles';
9 | import CssBaseline from '@material-ui/core/CssBaseline';
10 | import Typography from '@material-ui/core/Typography';
11 | import Button from '@material-ui/core/Button';
12 | const firebase = require("firebase");
13 |
14 | class SignupComponent extends React.Component {
15 |
16 | constructor() {
17 | super();
18 | this.state = {
19 | email: null,
20 | password: null,
21 | passwordConfirmation: null,
22 | signupError: ''
23 | };
24 | }
25 |
26 | render() {
27 |
28 | const { classes } = this.props;
29 |
30 | return (
31 |
32 |
33 |
34 |
35 | Sign Up!
36 |
37 |
52 | {
53 | this.state.signupError ?
54 |
55 | {this.state.signupError}
56 | :
57 | null
58 | }
59 | Already Have An Account?
60 | Log In!
61 |
62 |
63 | );
64 | }
65 |
66 | userTyping = (whichInput, event) => {
67 | switch (whichInput) {
68 | case 'email':
69 | this.setState({ email: event.target.value });
70 | break;
71 |
72 | case 'password':
73 | this.setState({ password: event.target.value });
74 | break;
75 |
76 | case 'passwordConfirmation':
77 | this.setState({ passwordConfirmation: event.target.value });
78 | break;
79 |
80 | default:
81 | break;
82 | }
83 | }
84 |
85 | formIsValid = () => this.state.password === this.state.passwordConfirmation;
86 |
87 | submitSignup = (e) => {
88 | e.preventDefault(); // This is to prevent the automatic refreshing of the page on submit.
89 |
90 | if(!this.formIsValid()) {
91 | this.setState({ signupError: 'Passwords do not match' });
92 | return;
93 | }
94 |
95 | firebase
96 | .auth()
97 | .createUserWithEmailAndPassword(this.state.email, this.state.password)
98 | .then(authRes => {
99 | const userObj = {
100 | email: authRes.user.email,
101 | friends: [],
102 | messages: []
103 | };
104 | firebase
105 | .firestore()
106 | .collection('users')
107 | .doc(this.state.email)
108 | .set(userObj)
109 | .then(() => {
110 | this.props.history.push('/dashboard');
111 | }, dbErr => {
112 | console.log('Failed to add user to the database: ', dbErr);
113 | this.setState({ signupError: 'Failed to add user' });
114 | });
115 | }, authErr => {
116 | console.log('Failed to create user: ', authErr);
117 | this.setState({ signupError: 'Failed to add user' });
118 | });
119 | };
120 | }
121 |
122 | export default withStyles(styles)(SignupComponent);
--------------------------------------------------------------------------------
/src/Signup/styles.js:
--------------------------------------------------------------------------------
1 | const styles = theme => ({
2 | main: {
3 | width: 'auto',
4 | display: 'block', // Fix IE 11 issue.
5 | marginLeft: theme.spacing.unit * 3,
6 | marginRight: theme.spacing.unit * 3,
7 | [theme.breakpoints.up(400 + theme.spacing.unit * 3 * 2)]: {
8 | width: 400,
9 | marginLeft: 'auto',
10 | marginRight: 'auto',
11 | },
12 | },
13 | paper: {
14 | marginTop: theme.spacing.unit * 8,
15 | display: 'flex',
16 | flexDirection: 'column',
17 | alignItems: 'center',
18 | padding: `${theme.spacing.unit * 2}px ${theme.spacing.unit * 3}px ${theme.spacing.unit * 3}px`,
19 | },
20 | form: {
21 | width: '100%',
22 | marginTop: theme.spacing.unit,
23 | },
24 | submit: {
25 | marginTop: theme.spacing.unit * 3,
26 | },
27 | hasAccountHeader: {
28 | width: '100%'
29 | },
30 | logInLink: {
31 | width: '100%',
32 | textDecoration: 'none',
33 | color: '#303f9f',
34 | fontWeight: 'bolder'
35 | },
36 | errorText: {
37 | color: 'red',
38 | textAlign: 'center'
39 | }
40 | });
41 |
42 | export default styles;
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | padding: 0;
4 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
5 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
6 | sans-serif;
7 | -webkit-font-smoothing: antialiased;
8 | -moz-osx-font-smoothing: grayscale;
9 | overflow: hidden;
10 | background-color: rgb(240, 240, 240);
11 | }
12 |
13 | code {
14 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
15 | monospace;
16 | }
17 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import './index.css';
4 | import { Route, BrowserRouter as Router } from 'react-router-dom';
5 | import * as serviceWorker from './serviceWorker';
6 | import LoginComponent from './Login/login';
7 | import SignupComponent from './Signup/signup';
8 | import DashboardComponent from './Dashboard/dashboard';
9 |
10 | const firebase = require("firebase");
11 | require("firebase/firestore"); // Required for side-effects?????
12 |
13 | firebase.initializeApp({
14 | apiKey: "AIzaSyAlWBvbvLv7dT6_RYnlCeZbOcotpeBU3Y8",
15 | authDomain: "im-app-tutorial.firebaseapp.com",
16 | databaseURL: "https://im-app-tutorial.firebaseio.com",
17 | projectId: "im-app-tutorial",
18 | storageBucket: "im-app-tutorial.appspot.com",
19 | messagingSenderId: "199544684635",
20 | appId: "1:199544684635:web:fb388e2c181f0476"
21 | });
22 |
23 | const routing = (
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | );
32 |
33 | ReactDOM.render(routing, document.getElementById('root'));
34 |
35 | // If you want your app to work offline and load faster, you can change
36 | // unregister() to register() below. Note this comes with some pitfalls.
37 | // Learn more about service workers: https://bit.ly/CRA-PWA
38 | serviceWorker.unregister();
39 |
--------------------------------------------------------------------------------
/src/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/src/serviceWorker.js:
--------------------------------------------------------------------------------
1 | // This optional code is used to register a service worker.
2 | // register() is not called by default.
3 |
4 | // This lets the app load faster on subsequent visits in production, and gives
5 | // it offline capabilities. However, it also means that developers (and users)
6 | // will only see deployed updates on subsequent visits to a page, after all the
7 | // existing tabs open on the page have been closed, since previously cached
8 | // resources are updated in the background.
9 |
10 | // To learn more about the benefits of this model and instructions on how to
11 | // opt-in, read https://bit.ly/CRA-PWA
12 |
13 | const isLocalhost = Boolean(
14 | window.location.hostname === 'localhost' ||
15 | // [::1] is the IPv6 localhost address.
16 | window.location.hostname === '[::1]' ||
17 | // 127.0.0.1/8 is considered localhost for IPv4.
18 | window.location.hostname.match(
19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
20 | )
21 | );
22 |
23 | export function register(config) {
24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
25 | // The URL constructor is available in all browsers that support SW.
26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
27 | if (publicUrl.origin !== window.location.origin) {
28 | // Our service worker won't work if PUBLIC_URL is on a different origin
29 | // from what our page is served on. This might happen if a CDN is used to
30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374
31 | return;
32 | }
33 |
34 | window.addEventListener('load', () => {
35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
36 |
37 | if (isLocalhost) {
38 | // This is running on localhost. Let's check if a service worker still exists or not.
39 | checkValidServiceWorker(swUrl, config);
40 |
41 | // Add some additional logging to localhost, pointing developers to the
42 | // service worker/PWA documentation.
43 | navigator.serviceWorker.ready.then(() => {
44 | console.log(
45 | 'This web app is being served cache-first by a service ' +
46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA'
47 | );
48 | });
49 | } else {
50 | // Is not localhost. Just register service worker
51 | registerValidSW(swUrl, config);
52 | }
53 | });
54 | }
55 | }
56 |
57 | function registerValidSW(swUrl, config) {
58 | navigator.serviceWorker
59 | .register(swUrl)
60 | .then(registration => {
61 | registration.onupdatefound = () => {
62 | const installingWorker = registration.installing;
63 | if (installingWorker == null) {
64 | return;
65 | }
66 | installingWorker.onstatechange = () => {
67 | if (installingWorker.state === 'installed') {
68 | if (navigator.serviceWorker.controller) {
69 | // At this point, the updated precached content has been fetched,
70 | // but the previous service worker will still serve the older
71 | // content until all client tabs are closed.
72 | console.log(
73 | 'New content is available and will be used when all ' +
74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
75 | );
76 |
77 | // Execute callback
78 | if (config && config.onUpdate) {
79 | config.onUpdate(registration);
80 | }
81 | } else {
82 | // At this point, everything has been precached.
83 | // It's the perfect time to display a
84 | // "Content is cached for offline use." message.
85 | console.log('Content is cached for offline use.');
86 |
87 | // Execute callback
88 | if (config && config.onSuccess) {
89 | config.onSuccess(registration);
90 | }
91 | }
92 | }
93 | };
94 | };
95 | })
96 | .catch(error => {
97 | console.error('Error during service worker registration:', error);
98 | });
99 | }
100 |
101 | function checkValidServiceWorker(swUrl, config) {
102 | // Check if the service worker can be found. If it can't reload the page.
103 | fetch(swUrl)
104 | .then(response => {
105 | // Ensure service worker exists, and that we really are getting a JS file.
106 | const contentType = response.headers.get('content-type');
107 | if (
108 | response.status === 404 ||
109 | (contentType != null && contentType.indexOf('javascript') === -1)
110 | ) {
111 | // No service worker found. Probably a different app. Reload the page.
112 | navigator.serviceWorker.ready.then(registration => {
113 | registration.unregister().then(() => {
114 | window.location.reload();
115 | });
116 | });
117 | } else {
118 | // Service worker found. Proceed as normal.
119 | registerValidSW(swUrl, config);
120 | }
121 | })
122 | .catch(() => {
123 | console.log(
124 | 'No internet connection found. App is running in offline mode.'
125 | );
126 | });
127 | }
128 |
129 | export function unregister() {
130 | if ('serviceWorker' in navigator) {
131 | navigator.serviceWorker.ready.then(registration => {
132 | registration.unregister();
133 | });
134 | }
135 | }
136 |
--------------------------------------------------------------------------------