├── .eslintrc.json
├── .gitignore
├── README.md
├── package.json
├── public
├── favicon.ico
├── index.html
└── manifest.json
├── result.gif
├── server
└── app.js
├── src
├── App.css
├── App.js
├── App.test.js
├── actions
│ ├── index.js
│ └── index.test.js
├── components
│ ├── AddMessage.js
│ ├── AddMessage.test.js
│ ├── Message.js
│ ├── Message.test.js
│ ├── MessagesList.js
│ ├── MessagesList.test.js
│ ├── Sidebar.js
│ └── Sidebar.test.js
├── constants
│ └── ActionTypes.js
├── containers
│ ├── AddMessage.js
│ ├── MessagesList.js
│ └── Sidebar.js
├── index.css
├── index.js
├── logo.svg
├── reducers
│ ├── index.js
│ ├── messages.js
│ ├── messages.test.js
│ ├── users.js
│ └── users.test.js
├── registerServiceWorker.js
├── sagas
│ └── index.js
├── sockets
│ └── index.js
└── utils
│ └── name.js
└── yarn.lock
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "node": true,
5 | "es6": true,
6 | "mocha": true
7 | },
8 | "extends": [
9 | "airbnb"
10 | ],
11 | "rules": {
12 | "semi": ["error", "never"],
13 | "no-unused-vars": "error",
14 | "comma-dangle": ["error", "never"],
15 | "no-implicit-globals": "error",
16 | "quotes": ["error", "single"],
17 | "react/jsx-filename-extension": [
18 | 1,
19 | {
20 | "extensions": [
21 | ".js",
22 | ".jsx"
23 | ]
24 | }
25 | ]
26 | // "prefer-promise-reject-errors": "error"
27 | },
28 | "parserOptions": {
29 | "ecmaVersion": 6,
30 | "sourceType": "module",
31 | "ecmaFeatures": {
32 | "arrowFunctions": true,
33 | "blockBindings": true,
34 | "classes": true,
35 | "defaultParams": true,
36 | "destructuring": true,
37 | "forOf": true,
38 | "generators": false,
39 | "modules": true,
40 | "objectLiteralComputedProperties": true,
41 | "objectLiteralDuplicateProperties": false,
42 | "objectLiteralShorthandMethods": true,
43 | "objectLiteralShorthandProperties": true,
44 | "spread": true,
45 | "superInFunctions": true,
46 | "templateStrings": true,
47 | "restParams": true,
48 | "jsx": true
49 | }
50 | }
51 | }
--------------------------------------------------------------------------------
/.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 | .vscode/settings.json
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Build a Chat Application using React, Redux, Redux-Saga and Web Sockets
2 |
3 | 
4 |
5 | This project was bootstrapped with [Create React App](https://github.com/facebookincubator/create-react-app).
6 |
7 | License: MIT
8 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "chat-1",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "chance": "^1.0.11",
7 | "eslint": "^4.9.0",
8 | "eslint-config-airbnb": "^16.0.0",
9 | "eslint-plugin-import": "^2.7.0",
10 | "eslint-plugin-jsx-a11y": "^6.0.2",
11 | "eslint-plugin-react": "^7.4.0",
12 | "react": "^16.0.0",
13 | "react-dom": "^16.0.0",
14 | "react-redux": "^5.0.6",
15 | "react-scripts": "1.0.14",
16 | "redux": "^3.7.2",
17 | "redux-saga": "^0.15.6",
18 | "ws": "^3.2.0",
19 | "prop-types": "*"
20 | },
21 | "scripts": {
22 | "start": "react-scripts start",
23 | "build": "react-scripts build",
24 | "test": "react-scripts test --env=jsdom",
25 | "eject": "react-scripts eject"
26 | },
27 | "devDependencies": {
28 | "babel-jest": "^21.2.0",
29 | "enzyme": "^3.1.0",
30 | "enzyme-adapter-react-16": "^1.0.1",
31 | "jest": "21.3.0-beta.2",
32 | "jest-environment-jsdom": "^21.2.1",
33 | "react-test-renderer": "^16.0.0"
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flaviocopes/chat-app-react-redux-saga-websockets/6d5f17312870c2fa1e9aab527c3c2445d30b2c28/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | Chat App
10 |
11 |
12 |
13 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/result.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flaviocopes/chat-app-react-redux-saga-websockets/6d5f17312870c2fa1e9aab527c3c2445d30b2c28/result.gif
--------------------------------------------------------------------------------
/server/app.js:
--------------------------------------------------------------------------------
1 | const WebSocket = require('ws')
2 |
3 | const wss = new WebSocket.Server({ port: 8989 })
4 |
5 | const users = []
6 |
7 | const broadcast = (data, ws) => {
8 | wss.clients.forEach((client) => {
9 | if (client.readyState === WebSocket.OPEN && client !== ws) {
10 | client.send(JSON.stringify(data))
11 | }
12 | })
13 | }
14 |
15 | wss.on('connection', (ws) => {
16 | let index
17 | ws.on('message', (message) => {
18 | const data = JSON.parse(message)
19 | console.log(message)
20 | switch (data.type) {
21 | case 'ADD_USER': {
22 | index = users.length
23 | users.push({ name: data.name, id: index + 1 })
24 | ws.send(JSON.stringify({
25 | type: 'USERS_LIST',
26 | users
27 | }))
28 | broadcast({
29 | type: 'USERS_LIST',
30 | users
31 | }, ws)
32 | break
33 | }
34 | case 'ADD_MESSAGE':
35 | broadcast({
36 | type: 'ADD_MESSAGE',
37 | message: data.message,
38 | author: data.author
39 | }, ws)
40 | break
41 | default:
42 | break
43 | }
44 | })
45 |
46 | ws.on('close', () => {
47 | users.splice(index, 1)
48 | broadcast({
49 | type: 'USERS_LIST',
50 | users
51 | }, ws)
52 | })
53 | })
54 |
--------------------------------------------------------------------------------
/src/App.css:
--------------------------------------------------------------------------------
1 | * {
2 | box-sizing: border-box;
3 | padding: 0;
4 | margin: 0;
5 | }
6 | body {
7 | font-family: "Press Start 2P";
8 | }
9 | #container {
10 | display: grid;
11 | grid-template-columns: 1fr 3fr;
12 | grid-template-areas: "sidebar main";
13 | width: 100vw;
14 | height: 100vh;
15 | }
16 | #main {
17 | grid-area: main;
18 | }
19 | #new-message {
20 | position: fixed;
21 | bottom: 0;
22 | width: 100%;
23 | padding: 5px;
24 | margin-left: 0px;
25 | border-top: 1px solid #3f3f3f;
26 | }
27 | #messages-list {
28 | padding: 5px 0 0 5px;
29 | }
30 | #sidebar {
31 | grid-area: sidebar;
32 | padding: 5px 0 0 5px;
33 | border-right: 1px solid #3f3f3f;
34 | height: 100%;
35 | }
36 |
37 | /* new */
38 |
39 | #new-message {
40 | padding: 0px;
41 | height: 50px;
42 | }
43 |
44 | #new-message input {
45 | font-family: "Press Start 2P";
46 | width: 100%;
47 | height: 100%;
48 | margin: 0;
49 | height: 50px;
50 | padding: 5px;
51 | }
--------------------------------------------------------------------------------
/src/App.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import './App.css'
3 | import { Sidebar } from './containers/Sidebar'
4 | import { MessagesList } from './containers/MessagesList'
5 | import { AddMessage } from './containers/AddMessage'
6 |
7 | const App = () => (
8 |
15 | )
16 |
17 | export default App
18 |
--------------------------------------------------------------------------------
/src/App.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import App from './App';
4 | import { Provider } from "react-redux";
5 | import { createStore } from "redux";
6 | import chat from "./reducers";
7 | let store = createStore(chat);
8 |
9 | it('renders without crashing', () => {
10 | const div = document.createElement('div');
11 | ReactDOM.render(
12 |
13 |
14 |
15 | , div);
16 | });
17 |
--------------------------------------------------------------------------------
/src/actions/index.js:
--------------------------------------------------------------------------------
1 | import * as types from '../constants/ActionTypes'
2 |
3 | let nextMessageId = 0
4 | const nextUserId = 0
5 |
6 | export const addMessage = (message, author) => ({
7 | type: types.ADD_MESSAGE,
8 | id: nextMessageId++,
9 | message,
10 | author
11 | })
12 |
13 | export const messageReceived = (message, author) => ({
14 | type: types.MESSAGE_RECEIVED,
15 | id: nextMessageId++,
16 | message,
17 | author
18 | })
19 |
20 | export const populateUsersList = users => ({
21 | type: types.USERS_LIST,
22 | users
23 | })
24 |
--------------------------------------------------------------------------------
/src/actions/index.test.js:
--------------------------------------------------------------------------------
1 | import { addMessage, addUser } from '../actions'
2 | import * as types from '../constants/ActionTypes'
3 |
4 | describe('adding a message', () => {
5 | it('should create an action to add a message with id 0', () => {
6 | const message = 'Something'
7 | const action = {
8 | type: types.ADD_MESSAGE,
9 | message,
10 | author: 'Me',
11 | id: 0
12 | }
13 | expect(addMessage(message)).toEqual(action)
14 | })
15 | })
16 |
17 | describe('adding a second message', () => {
18 | it('should create an action to add a message with id 1', () => {
19 | const message = 'Something'
20 | const action = {
21 | type: types.ADD_MESSAGE,
22 | message,
23 | author: 'Me',
24 | id: 1
25 | }
26 | expect(addMessage(message)).toEqual(action)
27 | })
28 | })
29 |
30 | describe('adding a user', () => {
31 | it('should create an action to add a user with id 0', () => {
32 | const user = 'Mark'
33 | const action = {
34 | type: types.ADD_USER,
35 | name: user,
36 | id: 0
37 | }
38 | expect(addUser(user)).toEqual(action)
39 | })
40 | })
41 |
42 | describe('adding a second user', () => {
43 | it('should create an action to add a message with id 1', () => {
44 | const user = 'Tony'
45 | const action = {
46 | type: types.ADD_USER,
47 | name: user,
48 | id: 1
49 | }
50 | expect(addUser(user)).toEqual(action)
51 | })
52 | })
53 |
--------------------------------------------------------------------------------
/src/components/AddMessage.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 |
4 | const AddMessage = (props) => {
5 | let input
6 |
7 | return (
8 |
22 | )
23 | }
24 |
25 | AddMessage.propTypes = {
26 | dispatch: PropTypes.func.isRequired
27 | }
28 |
29 | export default AddMessage
30 |
--------------------------------------------------------------------------------
/src/components/AddMessage.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Enzyme from 'enzyme'
3 | import { mount } from 'enzyme'
4 | import Adapter from 'enzyme-adapter-react-16';
5 | import AddMessage from './AddMessage'
6 |
7 | const setup = () => {
8 | const props = {
9 | users: [],
10 | addUser: jest.fn()
11 | }
12 | Enzyme.configure({ adapter: new Adapter() })
13 | const enzymeWrapper = mount()
14 |
15 | return {
16 | props,
17 | enzymeWrapper
18 | }
19 | }
20 |
21 | describe('AddMessage', () => {
22 | it('should render self', () => {
23 | const { enzymeWrapper } = setup()
24 | expect(enzymeWrapper.find('section#new-message').length).toBe(1)
25 | })
26 | })
27 |
--------------------------------------------------------------------------------
/src/components/Message.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 |
4 | const Message = ({ message, author }) => (
5 |
6 | {author}: {message}
7 |
8 | )
9 |
10 | Message.propTypes = {
11 | message: PropTypes.string.isRequired,
12 | author: PropTypes.string.isRequired
13 | }
14 |
15 | export default Message
16 |
--------------------------------------------------------------------------------
/src/components/Message.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Enzyme from 'enzyme'
3 | import { mount } from 'enzyme'
4 | import Adapter from 'enzyme-adapter-react-16';
5 | import Message from './Message'
6 |
7 | const setup = () => {
8 | const props = {
9 | author: 'Tony',
10 | message: 'Yeah'
11 | }
12 | Enzyme.configure({ adapter: new Adapter() })
13 | const enzymeWrapper = mount()
14 |
15 | return {
16 | props,
17 | enzymeWrapper
18 | }
19 | }
20 |
21 | describe('Message', () => {
22 | it('should render self', () => {
23 | const { enzymeWrapper } = setup()
24 | expect(enzymeWrapper.find('p').html()).toBe('Tony: Yeah
')
25 | })
26 | })
27 |
28 |
--------------------------------------------------------------------------------
/src/components/MessagesList.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import Message from './Message'
4 |
5 | const MessagesList = ({ messages }) => (
6 |
7 |
8 | {messages.map(message => (
9 |
13 | ))}
14 |
15 |
16 | )
17 |
18 | MessagesList.propTypes = {
19 | messages: PropTypes.arrayOf(PropTypes.shape({
20 | id: PropTypes.number.isRequired,
21 | message: PropTypes.string.isRequired,
22 | author: PropTypes.string.isRequired
23 | }).isRequired).isRequired
24 | }
25 |
26 | export default MessagesList
27 |
--------------------------------------------------------------------------------
/src/components/MessagesList.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Enzyme, { mount } from 'enzyme'
3 | import Adapter from 'enzyme-adapter-react-16'
4 | import MessagesList from './MessagesList'
5 |
6 | const setup = () => {
7 | const props = {
8 | messages: []
9 | }
10 | Enzyme.configure({ adapter: new Adapter() })
11 | const enzymeWrapper = mount()
12 |
13 | return {
14 | props,
15 | enzymeWrapper
16 | }
17 | }
18 |
19 | describe('MessagesList', () => {
20 | it('should render self', () => {
21 | const { enzymeWrapper } = setup()
22 | expect(enzymeWrapper.find('section#messages-list').length).toBe(1)
23 | })
24 | })
25 |
--------------------------------------------------------------------------------
/src/components/Sidebar.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 |
4 | const Sidebar = ({ users }) => (
5 |
12 | )
13 |
14 | Sidebar.propTypes = {
15 | users: PropTypes.arrayOf(PropTypes.shape({
16 | id: PropTypes.number.isRequired,
17 | name: PropTypes.string.isRequired
18 | }).isRequired).isRequired
19 | }
20 |
21 | export default Sidebar
22 |
--------------------------------------------------------------------------------
/src/components/Sidebar.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Enzyme from 'enzyme'
3 | import { mount } from 'enzyme'
4 | import Adapter from 'enzyme-adapter-react-16';
5 | import Sidebar from './Sidebar'
6 |
7 | const setup = () => {
8 | const props = {
9 | users: [],
10 | addUser: jest.fn()
11 | }
12 | Enzyme.configure({ adapter: new Adapter() })
13 | const enzymeWrapper = mount()
14 |
15 | return {
16 | props,
17 | enzymeWrapper
18 | }
19 | }
20 |
21 | describe('Sidebar', () => {
22 | it('should render self', () => {
23 | const { enzymeWrapper } = setup()
24 | expect(enzymeWrapper.find('aside').length).toBe(1)
25 | })
26 | })
27 |
--------------------------------------------------------------------------------
/src/constants/ActionTypes.js:
--------------------------------------------------------------------------------
1 | export const ADD_MESSAGE = 'ADD_MESSAGE'
2 | export const MESSAGE_RECEIVED = 'MESSAGE_RECEIVED'
3 | export const ADD_USER = 'ADD_USER'
4 | export const USERS_LIST = 'USERS_LIST'
5 |
--------------------------------------------------------------------------------
/src/containers/AddMessage.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux'
2 | import AddMessageComponent from '../components/AddMessage'
3 | import { addMessage } from '../actions'
4 |
5 | const mapDispatchToProps = dispatch => ({
6 | dispatch: (message, author) => {
7 | dispatch(addMessage(message, author))
8 | }
9 | })
10 |
11 | export const AddMessage = connect(() => ({}), mapDispatchToProps)(AddMessageComponent)
12 |
--------------------------------------------------------------------------------
/src/containers/MessagesList.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux'
2 | import MessagesListComponent from '../components/MessagesList'
3 |
4 | export const MessagesList = connect(state => ({
5 | messages: state.messages
6 | }), {})(MessagesListComponent)
7 |
--------------------------------------------------------------------------------
/src/containers/Sidebar.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux'
2 | import SidebarComponent from '../components/Sidebar'
3 |
4 | export const Sidebar = connect(state => ({
5 | users: state.users
6 | }), {})(SidebarComponent)
7 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | padding: 0;
4 | font-family: sans-serif;
5 | }
6 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom'
3 | import { Provider } from 'react-redux'
4 | import { createStore, applyMiddleware } from 'redux'
5 | import createSagaMiddleware from 'redux-saga'
6 |
7 | import './index.css'
8 | import App from './App'
9 | import registerServiceWorker from './registerServiceWorker'
10 | import reducers from './reducers'
11 | import handleNewMessage from './sagas'
12 | import setupSocket from './sockets'
13 | import username from './utils/name'
14 |
15 | const sagaMiddleware = createSagaMiddleware()
16 |
17 | const store = createStore(
18 | reducers,
19 | applyMiddleware(sagaMiddleware)
20 | )
21 |
22 | const socket = setupSocket(store.dispatch, username)
23 |
24 | sagaMiddleware.run(handleNewMessage, { socket, username })
25 |
26 | ReactDOM.render(
27 |
28 |
29 | ,
30 | document.getElementById('root')
31 | )
32 | registerServiceWorker()
33 |
--------------------------------------------------------------------------------
/src/logo.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/src/reducers/index.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux'
2 | import messages from './messages'
3 | import users from './users'
4 |
5 | const chat = combineReducers({
6 | messages,
7 | users
8 | })
9 |
10 | export default chat
11 |
--------------------------------------------------------------------------------
/src/reducers/messages.js:
--------------------------------------------------------------------------------
1 | const messages = (state = [], action) => {
2 | switch (action.type) {
3 | case 'ADD_MESSAGE':
4 | case 'MESSAGE_RECEIVED':
5 | return state.concat([
6 | {
7 | message: action.message,
8 | author: action.author,
9 | id: action.id
10 | }
11 | ])
12 | default:
13 | return state
14 | }
15 | }
16 |
17 | export default messages
18 |
--------------------------------------------------------------------------------
/src/reducers/messages.test.js:
--------------------------------------------------------------------------------
1 | import messages from './messages'
2 | import * as types from '../constants/ActionTypes'
3 |
4 | describe('Messages reducer', () => {
5 | it('should return the initial state, empty', () => {
6 | expect(messages(undefined, {})).toEqual([])
7 | })
8 |
9 | it('should handle ADD_MESSAGE and store every message', () => {
10 | expect(messages([], {
11 | type: types.ADD_MESSAGE,
12 | message: 'Hey',
13 | author: 'Me'
14 | })).toEqual([
15 | {
16 | message: 'Hey',
17 | author: 'Me'
18 | }
19 | ])
20 |
21 | expect(messages(
22 | [
23 | {
24 | message: 'Hey',
25 | author: 'Me'
26 | }
27 | ],
28 | {
29 | type: types.ADD_MESSAGE,
30 | message: 'Hey again',
31 | author: 'Me again'
32 | }
33 | )).toEqual([
34 | {
35 | message: 'Hey',
36 | author: 'Me'
37 | },
38 | {
39 | message: 'Hey again',
40 | author: 'Me again'
41 | }
42 | ])
43 | })
44 | })
45 |
--------------------------------------------------------------------------------
/src/reducers/users.js:
--------------------------------------------------------------------------------
1 | import * as types from '../constants/ActionTypes'
2 |
3 | const users = (state = [], action) => {
4 | switch (action.type) {
5 | case types.USERS_LIST:
6 | return action.users
7 | default:
8 | return state
9 | }
10 | }
11 |
12 | export default users
13 |
--------------------------------------------------------------------------------
/src/reducers/users.test.js:
--------------------------------------------------------------------------------
1 | import users from './users'
2 | import * as types from '../constants/ActionTypes'
3 |
4 | describe('Users reducer', () => {
5 | it('should return the initial state, empty', () => {
6 | expect(users(undefined, {})).toEqual([])
7 | })
8 |
9 | it('should handle ADD_USER and store every user', () => {
10 | expect(
11 | users([], {
12 | type: types.ADD_USER,
13 | name: 'Tony'
14 | })
15 | ).toEqual([
16 | {
17 | name: 'Tony'
18 | }
19 | ])
20 |
21 | expect(
22 | users(
23 | [
24 | {
25 | name: 'Mark',
26 | }
27 | ],
28 | {
29 | type: types.ADD_USER,
30 | name: 'Tony'
31 | }
32 | )
33 | ).toEqual([
34 | {
35 | name: 'Mark',
36 | },
37 | {
38 | name: 'Tony'
39 | }
40 | ])
41 | })
42 | })
43 |
--------------------------------------------------------------------------------
/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/sagas/index.js:
--------------------------------------------------------------------------------
1 | import { takeEvery } from 'redux-saga/effects'
2 | import * as types from '../constants/ActionTypes'
3 |
4 | const handleNewMessage = function* handleNewMessage(params) {
5 | yield takeEvery(types.ADD_MESSAGE, (action) => {
6 | action.author = params.username
7 | params.socket.send(JSON.stringify(action))
8 | })
9 | }
10 |
11 | export default handleNewMessage
12 |
--------------------------------------------------------------------------------
/src/sockets/index.js:
--------------------------------------------------------------------------------
1 | import * as types from '../constants/ActionTypes'
2 | import { messageReceived, populateUsersList } from '../actions'
3 |
4 | const setupSocket = (dispatch, username) => {
5 | const socket = new WebSocket('ws://localhost:8989')
6 |
7 | socket.onopen = () => {
8 | socket.send(JSON.stringify({
9 | type: types.ADD_USER,
10 | name: username
11 | }))
12 | }
13 | socket.onmessage = (event) => {
14 | const data = JSON.parse(event.data)
15 | switch (data.type) {
16 | case types.ADD_MESSAGE:
17 | dispatch(messageReceived(data.message, data.author))
18 | break
19 | case types.USERS_LIST:
20 | dispatch(populateUsersList(data.users))
21 | break
22 | default:
23 | break
24 | }
25 | }
26 |
27 | return socket
28 | }
29 |
30 | export default setupSocket
31 |
--------------------------------------------------------------------------------
/src/utils/name.js:
--------------------------------------------------------------------------------
1 | import Chance from 'chance'
2 |
3 | const chance = new Chance()
4 | export default chance.first()
5 |
6 |
--------------------------------------------------------------------------------