├── public
├── favicon.ico
├── manifest.json
└── index.html
├── src
├── index.js
├── App.js
├── Events.js
├── components
│ ├── chats
│ │ ├── ChatHeading.js
│ │ ├── SideBar.js
│ │ └── ChatContainer.js
│ ├── LoginForm.js
│ ├── messages
│ │ ├── Messages.js
│ │ └── MessageInput.js
│ └── Layout.js
├── server
│ ├── index.js
│ └── SocketManager.js
├── Factories.js
├── registerServiceWorker.js
└── index.css
├── .gitignore
├── README.md
└── package.json
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leonwatson2/Tutorial---ReactJS-and-Socket.io-Chat-App/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import App from './App';
4 | import registerServiceWorker from './registerServiceWorker';
5 |
6 | ReactDOM.render(, document.getElementById('root'));
7 | registerServiceWorker();
8 |
--------------------------------------------------------------------------------
/src/App.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import Layout from './components/Layout'
3 | import './index.css';
4 |
5 | class App extends Component {
6 | render() {
7 | return (
8 |
9 | );
10 | }
11 | }
12 |
13 | export default App;
14 |
--------------------------------------------------------------------------------
/src/Events.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | COMMUNITY_CHAT:"COMMUNITY_CHAT",
3 | USER_CONNECTED:"USER_CONNECTED",
4 | MESSAGE_RECIEVED:"MESSAGE_RECIEVED",
5 | MESSAGE_SENT:"MESSAGE_SENT",
6 | USER_DISCONNECTED:"USER_DISCONNECTED",
7 | TYPING:"TYPING",
8 | VERIFY_USER:"VERIFY_USER",
9 | LOGOUT:"LOGOUT"
10 | }
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "192x192",
8 | "type": "image/png"
9 | }
10 | ],
11 | "start_url": "./index.html",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 |
6 | # testing
7 | /coverage
8 |
9 | # production
10 | /build
11 |
12 | # misc
13 | .DS_Store
14 | .env.local
15 | .env.development.local
16 | .env.test.local
17 | .env.production.local
18 |
19 | npm-debug.log*
20 | yarn-debug.log*
21 | yarn-error.log*
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ##ReactJS and Socket.io Chat App Tutorial
2 |
3 |
4 | This is the code from a tutorial I created on youtube [here](https://www.youtube.com/playlist?list=PLfUtdEcvGHFHdOYFXj4cY6ZIFkSp6MOuY).
5 |
6 | ###Getting Started
7 |
8 | First you'll need to fork or download the respository.
9 |
10 | Then in terminal you'll install the node modules
11 |
12 | ``` npm install ```
13 |
14 | Then you can run it using
15 |
16 | ``` npm run react ``` to start React dev server.
17 | ``` npm run server ``` to start NodeJS Socket.io server.
18 |
19 |
20 |
--------------------------------------------------------------------------------
/src/components/chats/ChatHeading.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import FAVideo from 'react-icons/lib/fa/video-camera'
3 | import FAUserPlus from 'react-icons/lib/fa/user-plus'
4 | import MdEllipsisMenu from 'react-icons/lib/md/keyboard-control'
5 |
6 | export default function({name, numberOfUsers}) {
7 |
8 | return (
9 |
10 |
11 |
{name}
12 |
13 |
14 |
{numberOfUsers ? numberOfUsers : null}
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | );
24 |
25 | }
26 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "chat-app",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "cors": "^2.8.5",
7 | "react": "^15.6.1",
8 | "react-dom": "^15.6.1",
9 | "react-icons": "^2.2.5",
10 | "react-scripts": "1.0.11",
11 | "socket.io": "^2.0.3",
12 | "uuid": "^3.1.0"
13 | },
14 | "scripts": {
15 | "start": "node src/server/index.js",
16 | "dev": "concurrently \"npm run react\" \"npm run server\"",
17 | "react": "react-scripts start",
18 | "server": "nodemon src/server/index.js",
19 | "build-react": "react-scripts build",
20 | "test": "react-scripts test --env=jsdom",
21 | "eject": "react-scripts eject"
22 | },
23 | "devDependencies": {
24 | "concurrently": "^3.5.0",
25 | "nodemon": "^1.11.0"
26 | }
27 | }
--------------------------------------------------------------------------------
/src/server/index.js:
--------------------------------------------------------------------------------
1 | const http = require('http')
2 | const app = http.createServer((req, res) => {
3 |
4 | res.setHeader('Access-Control-Allow-Origin', 'http://localhost:3000, https://vlw2.com/chat');
5 | res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, PUT, PATCH, DELETE');
6 | res.setHeader('Access-Control-Allow-Headers', 'X-Requested-With,content-type');
7 |
8 | // Handle requests
9 | if (req.method === 'OPTIONS') {
10 | // This is an OPTIONS request, so just return 200 OK
11 | res.writeHead(200);
12 | res.end();
13 | } else {
14 | // This is a normal request, so handle it as usual
15 | // For example:
16 | res.writeHead(200, { 'Content-Type': 'text/plain' });
17 | res.end('Hello World!');
18 | }
19 |
20 | })
21 |
22 | const io = module.exports.io = require('socket.io')(app)
23 |
24 | const cors = require('cors');
25 |
26 | const PORT = process.env.PORT || 3231
27 |
28 | const SocketManager = require('./SocketManager')
29 |
30 |
31 | io.on('connection', SocketManager)
32 |
33 | app.listen(PORT, () => {
34 | console.log("Connected to port:" + PORT);
35 | })
36 |
--------------------------------------------------------------------------------
/src/components/LoginForm.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { VERIFY_USER } from '../Events'
3 |
4 | export default class LoginForm extends Component {
5 | constructor(props) {
6 | super(props);
7 |
8 | this.state = {
9 | nickname:"",
10 | error:""
11 | };
12 | }
13 |
14 | setUser = ({user, isUser})=>{
15 |
16 | if(isUser){
17 | this.setError("User name taken")
18 | }else{
19 | this.setError("")
20 | this.props.setUser(user)
21 | }
22 | }
23 |
24 | handleSubmit = (e)=>{
25 | e.preventDefault()
26 | const { socket } = this.props
27 | const { nickname } = this.state
28 | socket.emit(VERIFY_USER, nickname, this.setUser)
29 | }
30 |
31 | handleChange = (e)=>{
32 | this.setState({nickname:e.target.value})
33 | }
34 |
35 | setError = (error)=>{
36 | this.setState({error})
37 | }
38 |
39 | render() {
40 | const { nickname, error } = this.state
41 | return (
42 |
60 | );
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/components/messages/Messages.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 |
3 | export default class Messages extends Component {
4 | constructor(props) {
5 | super(props);
6 |
7 | this.scrollDown = this.scrollDown.bind(this)
8 | }
9 |
10 | scrollDown(){
11 | const { container } = this.refs
12 | container.scrollTop = container.scrollHeight
13 | }
14 |
15 | componentDidMount() {
16 | this.scrollDown()
17 | }
18 |
19 | componentDidUpdate(prevProps, prevState) {
20 | this.scrollDown()
21 | }
22 |
23 | render() {
24 | const { messages, user, typingUsers } = this.props
25 | return (
26 |
28 |
29 | {
30 | messages.map((mes)=>{
31 | return (
32 |
36 |
{mes.time}
37 |
38 |
{mes.message}
39 |
{mes.sender}
40 |
41 |
42 |
43 | )
44 | })
45 | }
46 | {
47 | typingUsers.map((name)=>{
48 | return (
49 |
50 | {`${name} is typing . . .`}
51 |
52 | )
53 | })
54 | }
55 |
56 |
57 |
58 |
59 | );
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/components/Layout.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import io from 'socket.io-client'
3 | import { USER_CONNECTED, LOGOUT } from '../Events'
4 | import LoginForm from './LoginForm'
5 | import ChatContainer from './chats/ChatContainer'
6 |
7 | const socketUrl = "http://localhost:3231"
8 | export default class Layout extends Component {
9 |
10 | constructor(props) {
11 | super(props);
12 |
13 | this.state = {
14 | socket:null,
15 | user:null
16 | };
17 | }
18 |
19 | componentWillMount() {
20 | this.initSocket()
21 | }
22 |
23 | /*
24 | * Connect to and initializes the socket.
25 | */
26 | initSocket = ()=>{
27 | const socket = io(socketUrl)
28 |
29 | socket.on('connect', ()=>{
30 | console.log("Connected");
31 | })
32 |
33 | this.setState({socket})
34 | }
35 |
36 | /*
37 | * Sets the user property in state
38 | * @param user {id:number, name:string}
39 | */
40 | setUser = (user)=>{
41 | const { socket } = this.state
42 | socket.emit(USER_CONNECTED, user);
43 | this.setState({user})
44 | }
45 |
46 | /*
47 | * Sets the user property in state to null.
48 | */
49 | logout = ()=>{
50 | const { socket } = this.state
51 | socket.emit(LOGOUT)
52 | this.setState({user:null})
53 |
54 | }
55 |
56 |
57 | render() {
58 | const { title } = this.props
59 | const { socket, user } = this.state
60 | return (
61 |
62 | {
63 | !user ?
64 |
65 | :
66 |
67 | }
68 |
69 | );
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/src/Factories.js:
--------------------------------------------------------------------------------
1 | const uuidv4 = require('uuid/v4')
2 |
3 | /*
4 | * createUser
5 | * Creates a user.
6 | * @prop id {string}
7 | * @prop name {string}
8 | * @param {object}
9 | * name {string}
10 | */
11 | const createUser = ({name = ""} = {})=>(
12 | {
13 | id:uuidv4(),
14 | name
15 |
16 | }
17 | )
18 |
19 | /*
20 | * createMessage
21 | * Creates a messages object.
22 | * @prop id {string}
23 | * @prop time {Date} the time in 24hr format i.e. 14:22
24 | * @prop message {string} actual string message
25 | * @prop sender {string} sender of the message
26 | * @param {object}
27 | * message {string}
28 | * sender {string}
29 | */
30 | const createMessage = ({message = "", sender = ""} = { })=>(
31 | {
32 | id:uuidv4(),
33 | time:getTime(new Date(Date.now())),
34 | message,
35 | sender
36 | }
37 |
38 | )
39 |
40 | /*
41 | * createChat
42 | * Creates a Chat object
43 | * @prop id {string}
44 | * @prop name {string}
45 | * @prop messages {Array.Message}
46 | * @prop users {Array.string}
47 | * @param {object}
48 | * messages {Array.Message}
49 | * name {string}
50 | * users {Array.string}
51 | *
52 | */
53 | const createChat = ({messages = [], name = "Community", users = []} = {})=>(
54 | {
55 | id:uuidv4(),
56 | name,
57 | messages,
58 | users,
59 | typingUsers:[]
60 | }
61 | )
62 |
63 |
64 | /*
65 | * @param date {Date}
66 | * @return a string represented in 24hr time i.e. '11:30', '19:30'
67 | */
68 | const getTime = (date)=>{
69 | return `${date.getHours()}:${("0"+date.getMinutes()).slice(-2)}`
70 | }
71 |
72 | module.exports = {
73 | createMessage,
74 | createChat,
75 | createUser
76 | }
77 |
78 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
11 |
12 |
13 |
22 | React App
23 |
24 |
25 |
28 |
29 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/src/components/chats/SideBar.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import FAChevronDown from 'react-icons/lib/md/keyboard-arrow-down'
3 | import FAMenu from 'react-icons/lib/fa/list-ul'
4 | import FASearch from 'react-icons/lib/fa/search'
5 | import MdEject from 'react-icons/lib/md/eject'
6 |
7 | export default class SideBar extends Component{
8 |
9 | render(){
10 | const { chats, activeChat, user, setActiveChat, logout} = this.props
11 | return (
12 |
13 |
14 |
Our Cool Chat
15 |
16 |
17 |
18 |
19 |
24 |
{ (e.target === this.refs.user) && setActiveChat(null) }}>
28 |
29 | {
30 | chats.map((chat)=>{
31 | if(chat.name){
32 | const lastMessage = chat.messages[chat.messages.length - 1];
33 | const user = chat.users.find(({name})=>{
34 | return name !== this.props.name
35 | }) || { name:"Community" }
36 | const classNames = (activeChat && activeChat.id === chat.id) ? 'active' : ''
37 |
38 | return(
39 |
{ setActiveChat(chat) } }
43 | >
44 |
{user.name[0].toUpperCase()}
45 |
46 |
{user.name}
47 | {lastMessage &&
{lastMessage.message}
}
48 |
49 |
50 |
51 | )
52 | }
53 |
54 | return null
55 | })
56 | }
57 |
58 |
59 |
60 |
{user.name}
61 |
{logout()}} title="Logout" className="logout">
62 |
63 |
64 |
65 |
66 | );
67 |
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/components/messages/MessageInput.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 |
3 | export default class MessageInput extends Component {
4 |
5 | constructor(props) {
6 | super(props);
7 |
8 | this.state = {
9 | message:"",
10 | isTyping:false
11 | };
12 |
13 | }
14 |
15 | handleSubmit = (e)=>{
16 | e.preventDefault()
17 | this.sendMessage()
18 | this.setState({message:""})
19 | }
20 |
21 | sendMessage = ()=>{
22 | this.props.sendMessage(this.state.message)
23 |
24 | }
25 |
26 | componentWillUnmount() {
27 | this.stopCheckingTyping()
28 | }
29 |
30 | sendTyping = ()=>{
31 | this.lastUpdateTime = Date.now()
32 | if(!this.state.isTyping){
33 | this.setState({isTyping:true})
34 | this.props.sendTyping(true)
35 | this.startCheckingTyping()
36 | }
37 | }
38 |
39 | /*
40 | * startCheckingTyping
41 | * Start an interval that checks if the user is typing.
42 | */
43 | startCheckingTyping = ()=>{
44 | console.log("Typing");
45 | this.typingInterval = setInterval(()=>{
46 | if((Date.now() - this.lastUpdateTime) > 300){
47 | this.setState({isTyping:false})
48 | this.stopCheckingTyping()
49 | }
50 | }, 300)
51 | }
52 |
53 | /*
54 | * stopCheckingTyping
55 | * Start the interval from checking if the user is typing.
56 | */
57 | stopCheckingTyping = ()=>{
58 | console.log("Stop Typing");
59 | if(this.typingInterval){
60 | clearInterval(this.typingInterval)
61 | this.props.sendTyping(false)
62 | }
63 | }
64 |
65 |
66 | render() {
67 | const { message } = this.state
68 | return (
69 |
70 |
96 |
97 |
98 | );
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/src/server/SocketManager.js:
--------------------------------------------------------------------------------
1 | const io = require('./index.js').io
2 |
3 | const { VERIFY_USER, USER_CONNECTED, USER_DISCONNECTED,
4 | LOGOUT, COMMUNITY_CHAT, MESSAGE_RECIEVED, MESSAGE_SENT,
5 | TYPING } = require('../Events')
6 |
7 | const { createUser, createMessage, createChat } = require('../Factories')
8 |
9 | let connectedUsers = { }
10 |
11 | let communityChat = createChat()
12 |
13 | module.exports = function(socket){
14 |
15 | // console.log('\x1bc'); //clears console
16 | console.log("Socket Id:" + socket.id);
17 |
18 | let sendMessageToChatFromUser;
19 |
20 | let sendTypingFromUser;
21 |
22 | //Verify Username
23 | socket.on(VERIFY_USER, (nickname, callback)=>{
24 | if(isUser(connectedUsers, nickname)){
25 | callback({ isUser:true, user:null })
26 | }else{
27 | callback({ isUser:false, user:createUser({name:nickname})})
28 | }
29 | })
30 |
31 | //User Connects with username
32 | socket.on(USER_CONNECTED, (user)=>{
33 | connectedUsers = addUser(connectedUsers, user)
34 | socket.user = user
35 |
36 | sendMessageToChatFromUser = sendMessageToChat(user.name)
37 | sendTypingFromUser = sendTypingToChat(user.name)
38 |
39 | io.emit(USER_CONNECTED, connectedUsers)
40 | console.log(connectedUsers);
41 |
42 | })
43 |
44 | //User disconnects
45 | socket.on('disconnect', ()=>{
46 | if("user" in socket){
47 | connectedUsers = removeUser(connectedUsers, socket.user.name)
48 |
49 | io.emit(USER_DISCONNECTED, connectedUsers)
50 | console.log("Disconnect", connectedUsers);
51 | }
52 | })
53 |
54 |
55 | //User logsout
56 | socket.on(LOGOUT, ()=>{
57 | connectedUsers = removeUser(connectedUsers, socket.user.name)
58 | io.emit(USER_DISCONNECTED, connectedUsers)
59 | console.log("Disconnect", connectedUsers);
60 |
61 | })
62 |
63 | //Get Community Chat
64 | socket.on(COMMUNITY_CHAT, (callback)=>{
65 | callback(communityChat)
66 | })
67 |
68 | socket.on(MESSAGE_SENT, ({chatId, message})=>{
69 | sendMessageToChatFromUser(chatId, message)
70 | })
71 |
72 | socket.on(TYPING, ({chatId, isTyping})=>{
73 | sendTypingFromUser(chatId, isTyping)
74 | })
75 |
76 | }
77 | /*
78 | * Returns a function that will take a chat id and a boolean isTyping
79 | * and then emit a broadcast to the chat id that the sender is typing
80 | * @param sender {string} username of sender
81 | * @return function(chatId, message)
82 | */
83 | function sendTypingToChat(user){
84 | return (chatId, isTyping)=>{
85 | io.emit(`${TYPING}-${chatId}`, {user, isTyping})
86 | }
87 | }
88 |
89 | /*
90 | * Returns a function that will take a chat id and message
91 | * and then emit a broadcast to the chat id.
92 | * @param sender {string} username of sender
93 | * @return function(chatId, message)
94 | */
95 | function sendMessageToChat(sender){
96 | return (chatId, message)=>{
97 | io.emit(`${MESSAGE_RECIEVED}-${chatId}`, createMessage({message, sender}))
98 | }
99 | }
100 |
101 | /*
102 | * Adds user to list passed in.
103 | * @param userList {Object} Object with key value pairs of users
104 | * @param user {User} the user to added to the list.
105 | * @return userList {Object} Object with key value pairs of Users
106 | */
107 | function addUser(userList, user){
108 | let newList = Object.assign({}, userList)
109 | newList[user.name] = user
110 | return newList
111 | }
112 |
113 | /*
114 | * Removes user from the list passed in.
115 | * @param userList {Object} Object with key value pairs of Users
116 | * @param username {string} name of user to be removed
117 | * @return userList {Object} Object with key value pairs of Users
118 | */
119 | function removeUser(userList, username){
120 | let newList = Object.assign({}, userList)
121 | delete newList[username]
122 | return newList
123 | }
124 |
125 | /*
126 | * Checks if the user is in list passed in.
127 | * @param userList {Object} Object with key value pairs of Users
128 | * @param username {String}
129 | * @return userList {Object} Object with key value pairs of Users
130 | */
131 | function isUser(userList, username){
132 | return username in userList
133 | }
--------------------------------------------------------------------------------
/src/registerServiceWorker.js:
--------------------------------------------------------------------------------
1 | // In production, we register a service worker to serve assets from local cache.
2 |
3 | // This lets the app load faster on subsequent visits in production, and gives
4 | // it offline capabilities. However, it also means that developers (and users)
5 | // will only see deployed updates on the "N+1" visit to a page, since previously
6 | // cached resources are updated in the background.
7 |
8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy.
9 | // This link also includes instructions on opting out of this behavior.
10 |
11 | const isLocalhost = Boolean(
12 | window.location.hostname === 'localhost' ||
13 | // [::1] is the IPv6 localhost address.
14 | window.location.hostname === '[::1]' ||
15 | // 127.0.0.1/8 is considered localhost for IPv4.
16 | window.location.hostname.match(
17 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
18 | )
19 | );
20 |
21 | export default function register() {
22 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
23 | // The URL constructor is available in all browsers that support SW.
24 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location);
25 | if (publicUrl.origin !== window.location.origin) {
26 | // Our service worker won't work if PUBLIC_URL is on a different origin
27 | // from what our page is served on. This might happen if a CDN is used to
28 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374
29 | return;
30 | }
31 |
32 | window.addEventListener('load', () => {
33 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
34 |
35 | if (!isLocalhost) {
36 | // Is not local host. Just register service worker
37 | registerValidSW(swUrl);
38 | } else {
39 | // This is running on localhost. Lets check if a service worker still exists or not.
40 | checkValidServiceWorker(swUrl);
41 | }
42 | });
43 | }
44 | }
45 |
46 | function registerValidSW(swUrl) {
47 | navigator.serviceWorker
48 | .register(swUrl)
49 | .then(registration => {
50 | registration.onupdatefound = () => {
51 | const installingWorker = registration.installing;
52 | installingWorker.onstatechange = () => {
53 | if (installingWorker.state === 'installed') {
54 | if (navigator.serviceWorker.controller) {
55 | // At this point, the old content will have been purged and
56 | // the fresh content will have been added to the cache.
57 | // It's the perfect time to display a "New content is
58 | // available; please refresh." message in your web app.
59 | console.log('New content is available; please refresh.');
60 | } else {
61 | // At this point, everything has been precached.
62 | // It's the perfect time to display a
63 | // "Content is cached for offline use." message.
64 | console.log('Content is cached for offline use.');
65 | }
66 | }
67 | };
68 | };
69 | })
70 | .catch(error => {
71 | console.error('Error during service worker registration:', error);
72 | });
73 | }
74 |
75 | function checkValidServiceWorker(swUrl) {
76 | // Check if the service worker can be found. If it can't reload the page.
77 | fetch(swUrl)
78 | .then(response => {
79 | // Ensure service worker exists, and that we really are getting a JS file.
80 | if (
81 | response.status === 404 ||
82 | response.headers.get('content-type').indexOf('javascript') === -1
83 | ) {
84 | // No service worker found. Probably a different app. Reload the page.
85 | navigator.serviceWorker.ready.then(registration => {
86 | registration.unregister().then(() => {
87 | window.location.reload();
88 | });
89 | });
90 | } else {
91 | // Service worker found. Proceed as normal.
92 | registerValidSW(swUrl);
93 | }
94 | })
95 | .catch(() => {
96 | console.log(
97 | 'No internet connection found. App is running in offline mode.'
98 | );
99 | });
100 | }
101 |
102 | export function unregister() {
103 | if ('serviceWorker' in navigator) {
104 | navigator.serviceWorker.ready.then(registration => {
105 | registration.unregister();
106 | });
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/src/components/chats/ChatContainer.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import SideBar from './SideBar'
3 | import { COMMUNITY_CHAT, MESSAGE_SENT, MESSAGE_RECIEVED, TYPING } from '../../Events'
4 | import ChatHeading from './ChatHeading'
5 | import Messages from '../messages/Messages'
6 | import MessageInput from '../messages/MessageInput'
7 |
8 |
9 | export default class ChatContainer extends Component {
10 | constructor(props) {
11 | super(props);
12 |
13 | this.state = {
14 | chats:[],
15 | activeChat:null
16 | };
17 | }
18 |
19 | componentDidMount() {
20 | const { socket } = this.props
21 | socket.emit(COMMUNITY_CHAT, this.resetChat)
22 | }
23 |
24 | /*
25 | * Reset the chat back to only the chat passed in.
26 | * @param chat {Chat}
27 | */
28 | resetChat = (chat)=>{
29 | return this.addChat(chat, true)
30 | }
31 |
32 | /*
33 | * Adds chat to the chat container, if reset is true removes all chats
34 | * and sets that chat to the main chat.
35 | * Sets the message and typing socket events for the chat.
36 | *
37 | * @param chat {Chat} the chat to be added.
38 | * @param reset {boolean} if true will set the chat as the only chat.
39 | */
40 | addChat = (chat, reset)=>{
41 | const { socket } = this.props
42 | const { chats } = this.state
43 |
44 | const newChats = reset ? [chat] : [...chats, chat]
45 | this.setState({chats:newChats, activeChat:reset ? chat : this.state.activeChat})
46 |
47 | const messageEvent = `${MESSAGE_RECIEVED}-${chat.id}`
48 | const typingEvent = `${TYPING}-${chat.id}`
49 |
50 | socket.on(typingEvent, this.updateTypingInChat(chat.id))
51 | socket.on(messageEvent, this.addMessageToChat(chat.id))
52 | }
53 |
54 | /*
55 | * Returns a function that will
56 | * adds message to chat with the chatId passed in.
57 | *
58 | * @param chatId {number}
59 | */
60 | addMessageToChat = (chatId)=>{
61 | return message => {
62 | const { chats } = this.state
63 | let newChats = chats.map((chat)=>{
64 | if(chat.id === chatId)
65 | chat.messages.push(message)
66 | return chat
67 | })
68 |
69 | this.setState({chats:newChats})
70 | }
71 | }
72 |
73 | /*
74 | * Updates the typing of chat with id passed in.
75 | * @param chatId {number}
76 | */
77 | updateTypingInChat = (chatId) =>{
78 | return ({isTyping, user})=>{
79 | if(user !== this.props.user.name){
80 |
81 | const { chats } = this.state
82 |
83 | let newChats = chats.map((chat)=>{
84 | if(chat.id === chatId){
85 | if(isTyping && !chat.typingUsers.includes(user)){
86 | chat.typingUsers.push(user)
87 | }else if(!isTyping && chat.typingUsers.includes(user)){
88 | chat.typingUsers = chat.typingUsers.filter(u => u !== user)
89 | }
90 | }
91 | return chat
92 | })
93 | this.setState({chats:newChats})
94 | }
95 | }
96 | }
97 |
98 | /*
99 | * Adds a message to the specified chat
100 | * @param chatId {number} The id of the chat to be added to.
101 | * @param message {string} The message to be added to the chat.
102 | */
103 | sendMessage = (chatId, message)=>{
104 | const { socket } = this.props
105 | socket.emit(MESSAGE_SENT, {chatId, message} )
106 | }
107 |
108 | /*
109 | * Sends typing status to server.
110 | * chatId {number} the id of the chat being typed in.
111 | * typing {boolean} If the user is typing still or not.
112 | */
113 | sendTyping = (chatId, isTyping)=>{
114 | const { socket } = this.props
115 | socket.emit(TYPING, {chatId, isTyping})
116 | }
117 |
118 | setActiveChat = (activeChat)=>{
119 | this.setState({activeChat})
120 | }
121 | render() {
122 | const { user, logout } = this.props
123 | const { chats, activeChat } = this.state
124 | return (
125 |
126 |
133 |
134 | {
135 | activeChat !== null ? (
136 |
137 |
138 |
139 |
144 | {
147 | this.sendMessage(activeChat.id, message)
148 | }
149 | }
150 | sendTyping={
151 | (isTyping)=>{
152 | this.sendTyping(activeChat.id, isTyping)
153 | }
154 | }
155 | />
156 |
157 |
158 | ):
159 |
160 |
Choose a chat!
161 |
162 | }
163 |
164 |
165 |
166 | );
167 | }
168 | }
169 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | html,body,#root{margin:0;padding:0;font-family:sans-serif;height:100%}input,textarea{font-family:Arial}.login{width:100%;height:100%;display:flex;justify-content:center;align-items:center}.login-form{display:flex;justify-content:center;flex-direction:column}.login-form input{max-width:100%;border-top:none;border-left:none;border-right:none;height:20px;line-height:20px;font-size:20px;border-bottom:solid 2px #b3b2ca;transition:all .23s ease-in}.login-form input:focus{border-bottom:solid 2px #5d5d8a;outline:none}.login-form .error{text-align:center;margin:5px 0;padding:5px 10px;color:#c92c43}.container{color:#cac8ee;display:flex;flex-direction:row;align-items:flex-start;height:100%;width:100%}#side-bar{box-sizing:border-box;width:33.75%;height:100%;display:flex;justify-content:space-around;flex-direction:column}#side-bar .heading{box-sizing:border-box;height:12vh;max-height:65px;padding:18px 16px;display:flex;flex-direction:row;align-items:center;justify-content:space-between;background:#2e2e4f}#side-bar .search{background:#3e3e5e;box-sizing:border-box;display:flex;flex-direction:row;justify-content:space-between;align-items:center;border-width:1px 0;border-style:solid;border-color:black;padding-left:15px;padding-right:20px;height:10vh;max-height:65px}#side-bar .search .search-icon{margin-right:15px;cursor:pointer}#side-bar .search input{width:100%;background:#3e3e5e;flex-grow:1;box-sizing:border-box;border:none;color:#cac8ee}#side-bar .search input:focus{outline:none}#side-bar .search input::placeholder{color:#cac8ee;opacity:.6}#side-bar .search .plus{display:inline-block;position:relative;cursor:pointer;width:13px;height:13px}#side-bar .search .plus::after,#side-bar .search .plus::before{content:'';position:absolute;background:#b3b2ca}#side-bar .search .plus::after{width:16px;height:2px;top:5px}#side-bar .search .plus::before{width:2px;height:16px;top:-2px;left:7px}#side-bar .users{overflow-y:scroll;background:#3e3e5e;flex-grow:1}#side-bar .users::-webkit-scrollbar-track{-webkit-box-shadow:inset 0 0 6px rgba(0,0,0,0.3);background-color:#cac8ee}#side-bar .users::-webkit-scrollbar{width:5px;background-color:green}#side-bar .users::-webkit-scrollbar-thumb{background-color:#484d79}#side-bar .users .user{align-items:center;display:flex;height:66px;justify-content:flex-start;padding:18px 16px}#side-bar .users .user.active,#side-bar .users .user:hover{background:#2e2e4f}#side-bar .users .user:hover:not(.active){cursor:pointer}#side-bar .users .user .user-info{margin-left:15px;flex-grow:1}#side-bar .users .user .user-info .last-message{font-size:12px;opacity:.56}#side-bar .users .user .new-message{height:100%;display:flex;align-items:center;justify-content:center}#side-bar .current-user{align-items:center;background:#484d79;box-sizing:border-box;display:flex;flex-direction:row;height:10vh;max-height:65px;justify-content:space-between;padding-left:16px;padding-right:16px}#side-bar .current-user .logout{align-items:center;cursor:pointer;display:flex;justify-content:center;font-size:2em}.chat-header{background:#5d5d8a;box-shadow:0px 6px 5px -2px rgba(225,225,225,0.7);box-sizing:border-box;display:flex;justify-content:space-between;align-items:center;height:12vh;max-height:65px;padding:18px 16px}.chat-header .user-info{align-items:center;display:flex}.chat-header .user-info .user-name{margin-right:10px}.chat-header .user-info .status{align-items:center;display:flex}.chat-header .options{display:flex;align-items:center;flex-direction:row;justify-content:space-around;height:100%;width:15%}.chat-header .options svg{cursor:pointer}.chat-room-container{height:100%;width:76.25%}@media screen and (max-width: 510px){#side-bar{position:absolute;left:-100%}.chat-room-container{width:100%}}.chat-room{display:flex;flex-direction:column;justify-content:space-between;height:100%;width:100%}.chat-room.choose{align-items:center;justify-content:center;font-size:2em}.thread-container{flex-grow:1;overflow-y:scroll;position:relative}.thread-container::-webkit-scrollbar-track{-webkit-box-shadow:inset 0 0 6px rgba(0,0,0,0.3);background-color:#cac8ee}.thread-container::-webkit-scrollbar{width:5px;background-color:green}.thread-container::-webkit-scrollbar-thumb{background-color:#484d79}.thread-container .thread{position:relative;width:100%;min-height:800px;display:flex;flex-direction:column;justify-content:flex-end;color:#b3b2ca;background:#dcddf5}.message-container{display:flex;justify-content:flex-start;min-height:50px;margin:10px 15px;animation:.65s ease-out 0s show}.message-container .time{order:1}.message-container .data{order:2;height:100%;margin-left:25px}.message-container .name{font-size:.65em;margin-top:5px;text-align:right}.message-container .message{background:#fff;border-radius:5px;border-top-left-radius:0;box-sizing:border-box;color:#b3b2ca;height:100%;padding:10px 15px;position:relative}@keyframes show{0%{opacity:0}100%{opacity:1}}.message-container .message::before{border-bottom-color:transparent;border-left-color:transparent;border-right-color:#fff;border-style:solid;border-top-color:#fff;border-width:4px;content:'';height:0;left:-7px;position:absolute;top:0;width:0}.message-container.right{text-align:right;justify-content:flex-end}.message-container.right .time{order:2;margin-left:25px}.message-container.right .data{margin-left:0;order:1}.message-container.right .name{display:none}.message-container.right .message{background:#89a1fc;color:#fff;border-top-right-radius:0;border-top-left-radius:5px}.message-container.right .message::before{border-top-color:#89a1fc;border-left-color:#89a1fc;border-right-color:transparent;left:auto;right:-7px}.typing-user{text-align:right;margin:10px 15px}.message-input{background:white;color:#484d79;box-sizing:border-box;height:10vh;max-height:65px}.message-input .message-form{height:100%;width:100%;display:flex;justify-content:space-between}.message-input .message-form .form-control{padding-top:24px;padding-bottom:24px;resize:none;padding-left:15px;box-sizing:border-box;width:80%;height:100%;border:none}.message-input .message-form .form-control::-webkit-scrollbar-track{-webkit-box-shadow:inset 0 0 6px rgba(0,0,0,0.3);background-color:#cac8ee}.message-input .message-form .form-control::-webkit-scrollbar{width:5px;background-color:green}.message-input .message-form .form-control::-webkit-scrollbar-thumb{background-color:#484d79}.message-input .message-form .form-control:focus{outline:none}.message-input .message-form .send{width:20%;box-sizing:border-box;font-size:1.25em;text-align:center;border:none;height:100%;color:#fff;background:#3e3e5e;transition:all .35s ease-out}.message-input .message-form .send:disabled{opacity:.2;background:#5d5d8a}
2 |
--------------------------------------------------------------------------------