├── .babelrc
├── .gitignore
├── README.md
├── package.json
├── public
├── css
│ └── main.css
└── index.html
├── server
├── db
│ ├── Channel.js
│ ├── User.js
│ ├── index.js
│ └── initializeDB.js
├── getDefaultState.js
├── server.js
├── serverRenderMiddleWare.js
└── simulateActivity.js
├── src
├── App.jsx
├── App.less
├── actions
│ ├── completeChannelCreation.js
│ ├── index.js
│ ├── openContactChannel.js
│ ├── receiveMessage.js
│ ├── requestCreateChannel.js
│ ├── setActiveChannel.js
│ ├── setChannelInfo.js
│ ├── setUserInfo.js
│ ├── submitChannelInputText.js
│ ├── updateChannelInputText.js
│ ├── updateChannelLoadedStatus.js
│ ├── updateStatus.js
│ └── updateUserFetchStatus.js
├── combineReducers.js
├── components
│ ├── ChannelContent
│ │ ├── ChannelContent.js
│ │ └── ChannelContentContainer.js
│ ├── ChannelList
│ │ ├── ChannelList.js
│ │ ├── ChannelListContainer.js
│ │ └── ChannelListItem.js
│ ├── ContactList
│ │ ├── ContactList.js
│ │ ├── ContactListContainer.jsx
│ │ ├── ContactListItem.js
│ │ └── ContactListItemContainer.js
│ ├── CurrentChannelTextInput
│ │ ├── CurrentChannelTextInput.js
│ │ └── CurrentChannelTextInputContainer.js
│ ├── CurrentUser
│ │ ├── CurrentUser.js
│ │ └── CurrentUserContainer.js
│ ├── DevTools
│ │ └── DevTools.jsx
│ ├── Message
│ │ ├── Message.js
│ │ └── MessageContainer.js
│ └── index.js
├── getPreloadedState.js
├── getStore.js
├── initSagas.js
├── main.jsx
├── reducers
│ ├── activeChannel.js
│ ├── channels.js
│ ├── currentUser.js
│ ├── index.js
│ └── userInfo.js
├── sagas
│ ├── activeChannelSaga.js
│ ├── channelSaga.js
│ ├── createChannelSaga.js
│ ├── currentChannelInputSaga.js
│ ├── currentUserStatusSaga.js
│ ├── index.js
│ └── userInfoSaga.js
├── selectors
│ ├── activeChannelSelector.js
│ ├── channelSelector.js
│ ├── currentUser.js
│ ├── index.js
│ └── userSelector.js
├── socketMiddleware.js
└── utility
│ ├── chance.js
│ ├── createReducer.js
│ ├── getDebugSessionKey.js
│ ├── index.js
│ └── makeActionCreator.js
└── webpack.config.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["es2015","react"],
3 | "plugins": ["transform-regenerator","transform-object-rest-spread"]
4 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 |
8 | # Runtime data
9 | pids
10 | *.pid
11 | *.seed
12 | *.pid.lock
13 |
14 | # Directory for instrumented libs generated by jscoverage/JSCover
15 | lib-cov
16 |
17 | # Coverage directory used by tools like istanbul
18 | coverage
19 |
20 | # nyc test coverage
21 | .nyc_output
22 |
23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
24 | .grunt
25 |
26 | # Bower dependency directory (https://bower.io/)
27 | bower_components
28 |
29 | # node-waf configuration
30 | .lock-wscript
31 |
32 | # Compiled binary addons (http://nodejs.org/api/addons.html)
33 | build/Release
34 |
35 | # Dependency directories
36 | node_modules/
37 | jspm_packages/
38 |
39 | # Typescript v1 declaration files
40 | typings/
41 |
42 | # Optional npm cache directory
43 | .npm
44 |
45 | # Optional eslint cache
46 | .eslintcache
47 |
48 | # Optional REPL history
49 | .node_repl_history
50 |
51 | # Output of 'npm pack'
52 | *.tgz
53 |
54 | # Yarn Integrity file
55 | .yarn-integrity
56 |
57 | # dotenv environment variables file
58 | .env
59 |
60 | .idea
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Advanced Redux Messenger
2 | ## Redux Messenger
3 | 
4 | ### Introduction
5 | This application is a fully functional messenger application similar to Slack or HipChat. It includes the following features,
6 | - State managed with Redux
7 | - React-Redux view
8 | - Selectors with Reselect
9 | - Immutable State
10 | - Live server updates with websockets
11 | - Redux-Saga
12 | - More!
13 | ### How to Use This Application
14 | - This application is meant as a reference for students completing Advanced Redux on Pluralsight
15 | - Developers who are already familiar with the technology are welcome to copy the application and make any desired changes
16 |
17 | ### Getting started
18 | 1. Clone the application,
19 | ```bash
20 | git clone git@github.com:danielstern/advanced-redux.git
21 | ```
22 | 2. Install dependencies
23 | ```bash
24 | cd advanced-redux
25 | npm install
26 | npm install -g babel-cli
27 | ```
28 |
29 | 3. Start the application
30 | ```javascript
31 | npm start
32 | ```
33 | or
34 | ```javascript
35 | npm run dev
36 | ```
37 | * `npm run dev` is meant for development and includes file monitoring with Nodemon
38 |
39 | 4. Navigate to the application in Chrome
40 | [http://localhost:9000/#](http://localhost:9000)
41 |
42 | ### Troubleshooting the Application
43 | Refer to the following guide if the application does not seem to be working as expected
44 |
45 | #### Application not working
46 | Before trying any of the technique below, make sure your application is correctly coded.
47 | 1. Clone this repository's master branch
48 | 2. Without making any changes to it, run with `npm install` and `npm start`
49 |
50 | This should solve 90% of errors. If not, see below:
51 |
52 | #### Correct admin priveleges (Mac only)
53 | 1. If you are using Mac, make sure that you installed Mac with [Brew](https://brew.sh/) so that proper admin priveleges are configured
54 |
55 | #### Install Global packages
56 | Depending on your OS, NPM may have a hard time running locally installed packages from the command line. To resolve this, manually install the dependencies under `devDependencies` with `npm install -g` or use the following script:
57 |
58 | ```bash
59 | npm install -g babel-loader@6.2.8 babel-plugin-transform-object-rest-spread@6.19.0 babel-preset-es2015@6.18.0 babel-preset-react@6.23.0 babel-regenerator-runtime@6.5.0 nodemon@1.11.0 webpack@1.13.3 webpack-dev-server@1.16.2 webpack-hot-middleware@2.17.1 webpack-dev-middleware@1.10.1
60 | ```
61 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "redux-messenger",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1",
8 | "start": "babel-node server/server.js",
9 | "dev": "nodemon --watch server/**/* --exec babel-node --inspect server/server.js"
10 | },
11 | "devDependencies": {
12 | "babel-cli": "^6.24.1",
13 | "babel-core": "^6.18.2",
14 | "babel-loader": "^6.2.8",
15 | "babel-plugin-transform-object-rest-spread": "^6.19.0",
16 | "babel-preset-es2015": "^6.18.0",
17 | "babel-preset-react": "^6.23.0",
18 | "babel-regenerator-runtime": "^6.5.0",
19 | "nodemon": "^1.11.0",
20 | "redux-devtools": "^3.3.2",
21 | "redux-devtools-dock-monitor": "^1.1.1",
22 | "redux-devtools-log-monitor": "^1.2.0",
23 | "webpack": "^1.13.3",
24 | "webpack-dev-middleware": "^1.10.1",
25 | "webpack-dev-server": "^1.16.2",
26 | "webpack-hot-middleware": "^2.17.1"
27 | },
28 | "dependencies": {
29 | "chance": "^1.0.6",
30 | "classnames": "^2.2.5",
31 | "cors": "^2.8.1",
32 | "express": "^4.15.2",
33 | "immutable": "^3.8.1",
34 | "isomorphic-fetch": "^2.2.1",
35 | "lodash": "^4.17.4",
36 | "react": "^15.4.2",
37 | "react-dom": "^15.4.2",
38 | "react-redux": "^5.0.3",
39 | "redux": "^3.6.0",
40 | "redux-saga": "^0.14.3",
41 | "redux-thunk": "^2.2.0",
42 | "reselect": "^2.5.4",
43 | "socket.io": "^1.7.3"
44 | },
45 | "keywords": [],
46 | "author": "",
47 | "license": "ISC"
48 | }
49 |
--------------------------------------------------------------------------------
/public/css/main.css:
--------------------------------------------------------------------------------
1 | @import url("https://fonts.googleapis.com/css?family=Montserrat:400,700|DroidSans:100,300,400|Roboto:400,700");
2 |
3 | body {
4 | font-size: 16px;
5 | font-family: "Roboto";
6 | }
7 |
8 | h1,h2,h3,h4 {
9 | font-family: "Montserrat";
10 | }
11 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Redux Messaging App
5 |
6 |
7 |
8 |
9 |
10 |
11 | <%= html%>
12 |
13 |
14 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/server/db/Channel.js:
--------------------------------------------------------------------------------
1 | export const channels = [];
2 |
3 | export const Channel = (id)=>{
4 | const channel = channels.find(channel=>channel.id===id);
5 | if (!channel) {
6 | throw new Error(`Could not find channel with ID ${id}`);
7 | }
8 | return channel;
9 | }
--------------------------------------------------------------------------------
/server/db/User.js:
--------------------------------------------------------------------------------
1 | export const users = [];
2 |
3 | export const User = (id)=>{
4 | const user = users.find(user=>user.id===id);
5 | if (!user) {
6 | throw new Error(`Could not find user with ID ${id}`);
7 | }
8 | return user;
9 | }
--------------------------------------------------------------------------------
/server/db/index.js:
--------------------------------------------------------------------------------
1 | export { Channel, channels } from './Channel';
2 | export { User, users } from './User';
3 | export { getRandomMessageText } from './initializeDB';
--------------------------------------------------------------------------------
/server/db/initializeDB.js:
--------------------------------------------------------------------------------
1 | import {
2 | channels,
3 | users
4 | } from './'
5 |
6 | import {
7 | ONLINE, OFFLINE, AWAY, FETCHED
8 | } from './../../src/actions'
9 |
10 | import {
11 | template
12 | } from 'lodash';
13 |
14 | import { chance } from './../../src/utility';
15 |
16 | const messageActions = [`collate`,`port`,`merge`,`refactor`,`check`,`deploy`,`automate`,`debug`,`erase`,`cache`,`rebase`,`transpile`,`desync`,`fork`];
17 | const messageObjects = [`repo`,`source code`,`code base`,`sprint`,`workflow`,`debugger`,`module`,`version`,`transpiler`,`language`,`integer`,`router`];
18 | const templates = [
19 | `I'm going to <%= action%> the <%= object%>.`,
20 | `Could you <%= action%> the <%= object%>?`,
21 | `I've noticed a <%= object%> that you could <%= action%>.`,
22 | `Is there a <%= object%> I could <%= action%>?`,
23 | `I'm thinking of attending a conference on <%= object%> management.`,
24 | `Do you know how to <%= action%> the <%= object%>?`,
25 | `The <%= object%> is amazing!`,
26 | `Note to self: <%= action%> the <%= object%>.`,
27 | ];
28 |
29 | export const getRandomMessageText = ()=> template(chance.pick(templates))({
30 | action: chance.pick(messageActions),
31 | object: chance.pick(messageObjects)
32 | });
33 |
34 | export const getRandomMessage = (userIDs)=>({
35 | id:chance.guid(),
36 | owner: chance.pick(userIDs),
37 | content: {
38 | text: getRandomMessageText()
39 | },
40 | date:chance.date({year:2017})
41 | });
42 |
43 | export const initializeDB = ()=>{
44 |
45 | const firstNames = [`Emily`,`Chuck`,`Andy`,`Edgar`,`Stephen`,`Pablo`,`Gustav`,`Jackson`,`Leonardo`];
46 | const lastNames = [`McCartney`,`Webber`,`Combs`,`John`,`Martin`,`Starr`,`Springstein`,`Simmons`,`Harrison`];
47 |
48 | let userCount = 12;
49 | while (userCount--) {
50 | users.push({
51 | name:`${chance.pick(firstNames)} ${chance.pick(lastNames)}`,
52 | id: chance.guid(),
53 | contacts: [],
54 | channels:[],
55 | fetchStatus:FETCHED,
56 | status:chance.pick([ONLINE,OFFLINE,AWAY])
57 | })
58 | }
59 |
60 | let contactCount = 6;
61 | users.forEach(user=>{
62 | for (let i = 0; i < contactCount; i++) {
63 | user.contacts.push(chance.pick(users.filter(({id}) => id !== user.id && !user.contacts.includes(id))).id);
64 | }
65 | });
66 |
67 |
68 | const getMessages = (userIDs,count = 12)=>{
69 | const messages = [];
70 | for (let i = count; i > 0; i--) {
71 | messages.push(getRandomMessage(userIDs));
72 | };
73 | return messages;
74 | };
75 |
76 | users.forEach(user=>{
77 | channels.push({
78 | id:chance.guid(),
79 | name:`${user.name}'s Private Channel`,
80 | participants:[user.id],
81 | messages:getMessages([user.id])
82 | })
83 | });
84 |
85 | users.forEach(user=>{
86 | const user2 = chance.pick(users.filter(_user=>_user.id !== user.id));
87 | channels.push({
88 | id:chance.guid(),
89 | name:`${user.name} and ${user2.name}'s Private Chat`,
90 | participants:[user.id,user2.id],
91 | messages:getMessages([user.id,user2.id])
92 | })
93 | });
94 |
95 | channels.push({
96 | id:chance.guid(),
97 | name:`Group Chat`,
98 | participants:users.map(user=>user.id),
99 | messages:getMessages(users.map(user=>user.id),28)
100 | });
101 |
102 | users.forEach(user=>{
103 | user.activeChannel = chance.pick(channels.filter(channel=>channel.participants.includes(user.id))).id;
104 | });
105 | };
--------------------------------------------------------------------------------
/server/getDefaultState.js:
--------------------------------------------------------------------------------
1 | import { fromJS } from 'immutable'
2 |
3 | import {
4 | User,
5 | channels,
6 | Channel
7 | } from './db';
8 |
9 | import {
10 | NOT_FETCHED,
11 | FETCHED
12 | } from './../src/actions'
13 |
14 | export const getDefaultState = (currentUser)=>{
15 |
16 | const defaultState = {
17 | currentUser:{},
18 | channels:[],
19 | userInfo:[],
20 | };
21 |
22 | const userChannels = channels.filter(channel=>channel.participants.includes(currentUser.id));
23 | const activeChannel = Channel(currentUser.activeChannel);
24 | defaultState.currentUser = currentUser;
25 | defaultState.channels = userChannels.map(channel=>{
26 | if (channel.id === activeChannel.id) {
27 | return {
28 | ...channel,
29 | fetchStatus:FETCHED
30 | };
31 | } else {
32 | return {
33 | id:channel.id,
34 | name:channel.name,
35 | messages:[],
36 | fetchStatus:NOT_FETCHED,
37 | participants:channel.participants
38 | }
39 | }
40 | });
41 |
42 | defaultState.activeChannel = activeChannel.id;
43 | defaultState.userInfo = [currentUser,...activeChannel.participants.map(User), ...currentUser.contacts.map(User)].map(user=>({
44 | name:user.name,
45 | fetchStatus:FETCHED,
46 | id:user.id,
47 | status:user.status
48 | }));
49 |
50 | return fromJS(defaultState);
51 | };
--------------------------------------------------------------------------------
/server/server.js:
--------------------------------------------------------------------------------
1 | import http from 'http';
2 | import express from 'express';
3 | import cors from 'cors';
4 | import webpack from 'webpack';
5 | import webpackConfig from './../webpack.config'
6 | import webpackDevMiddleware from 'webpack-dev-middleware';
7 | import { simulateActivity } from './simulateActivity';
8 | const compiler = webpack(webpackConfig);
9 | import webpackHotMiddleware from "webpack-hot-middleware";
10 |
11 | import {
12 | channels,
13 | } from './db/Channel';
14 |
15 | import {
16 | users
17 | } from './db/User';
18 |
19 | import socketIO from 'socket.io'
20 |
21 | import {
22 | OFFLINE,
23 | ONLINE,
24 | AWAY
25 | } from './../src/actions'
26 |
27 | let app = express();
28 | const server = http.createServer(app);
29 | const io = socketIO(server);
30 |
31 | app.use(cors());
32 | app.use(webpackDevMiddleware(compiler, {
33 | noInfo: true,
34 | publicPath: webpackConfig.output.publicPath,
35 | }));
36 |
37 | app.use(webpackHotMiddleware(compiler, {
38 | 'log': false,
39 | 'path': '/__webpack_hmr',
40 | 'heartbeat': 10 * 1000
41 | }));
42 |
43 | import { getDefaultState } from './getDefaultState'
44 | import { handleRender } from './serverRenderMiddleWare';
45 | import { initializeDB } from './db/initializeDB';
46 |
47 | import {
48 | chance
49 | } from './../src/utility';
50 |
51 | initializeDB();
52 | const currentUser = chance.pick(users);
53 |
54 | // Simulate a small amount of delay to demonstrate app's async features
55 | app.use((req,res,next)=>{
56 | const delay = 297;
57 | setTimeout(next,delay);
58 | });
59 |
60 | app.use('/channel/create/:channelID/:name/:participants',({params:{channelID,name,participants}},res)=>{
61 | const channel = {
62 | id:channelID,
63 | name,
64 | participants:JSON.parse(participants),
65 | messages:[]
66 | };
67 | channels.push(channel);
68 | res.status(300).json(channel);
69 | });
70 |
71 | app.use('/channel/:id',(req,res)=>{
72 | res.json(channels.find(channel=>channel.id === req.params.id));
73 | });
74 |
75 | app.use('/user/activeChannel/:userID/:channelID',({params:{userID,channelID}},res)=>{
76 | users.find(user=>user.id === userID).activeChannel = channelID;
77 | res.status(200).send(true);
78 | });
79 |
80 | app.use('/user/:id',(req,res)=>{
81 | res.json(users
82 | .map(({name,id})=>({name,id}))
83 | .find(user=>user.id === req.params.id));
84 | });
85 |
86 | app.use('/status/:id/:status',({params:{id,status}},res)=>{
87 | if (![ONLINE,OFFLINE,AWAY].includes(status)) {
88 | return res.status(403).send();
89 | }
90 | const user = users
91 | .find(user=>user.id === id);
92 | if (user) {
93 | user.status = status;
94 | res.status(200).send();
95 | } else {
96 | res.status(404).send();
97 | }
98 | });
99 |
100 | export const createMessage = ({userID,channelID,messageID,input}) =>{
101 | const channel = channels.find(channel=>channel.id === channelID);
102 |
103 | const message = {
104 | id:messageID,
105 | content:{
106 | text:input
107 | },
108 | owner:userID
109 | };
110 |
111 | channel.messages.push(message);
112 | io.emit("NEW_MESSAGE",{channelID:channel.id, ...message});
113 | }
114 |
115 | app.use('/input/submit/:userID/:channelID/:messageID/:input',({params:{userID,channelID,messageID,input}},res)=>{
116 | const user = users.find(user=>user.id === userID);
117 |
118 | if (!user) {
119 | return res.status(404).send();
120 | }
121 |
122 | createMessage({userID,channelID,messageID,input});
123 | res.status(300).send();
124 | });
125 |
126 | app.use(express.static('public/css'));
127 | app.use('/',handleRender(()=>getDefaultState(currentUser)));
128 |
129 | const port = 9000;
130 |
131 | server.listen(port,()=>{
132 | console.info(`Redux Messenger is listening on port ${port}.`);
133 | });
134 |
135 | simulateActivity(currentUser.id);
--------------------------------------------------------------------------------
/server/serverRenderMiddleWare.js:
--------------------------------------------------------------------------------
1 | import { createStore } from 'redux';
2 | import { reducer } from './../src/reducers';
3 | import {Provider} from 'react-redux';
4 | import { App } from './../src/App'
5 | import { renderToString } from 'react-dom/server'
6 | import React from 'react';
7 | import template from 'lodash/template';
8 |
9 | import fs from 'fs';
10 |
11 | const readModuleFile = (path, callback)=>{
12 | try {
13 | const filename = require.resolve(path);
14 | fs.readFile(filename, 'utf8', callback);
15 | } catch (e) {
16 | callback(e);
17 | }
18 | }
19 |
20 | export const handleRender = (getState) => (req, res)=>{
21 | let defaultState = getState();
22 | const store = createStore(reducer,defaultState);
23 |
24 | const html = renderToString(
25 |
26 |
27 |
28 | );
29 |
30 | const preloadedState = store.getState().toJS();
31 |
32 | readModuleFile('./../public/index.html', (err, index)=>{
33 | const templated = template(index)({
34 | html,
35 | preloadedState:JSON.stringify(preloadedState).replace(/{
20 | if (count < max) {
21 | count++;
22 | const input = getRandomMessageText(users.map(user=>user));
23 | const messageID = chance.guid();
24 | const channel = chance.pick(channels.filter(channel=>channel.participants.includes(user)));
25 | const channelID = channel.id;
26 | const userID = chance.pick(channel.participants);
27 | createMessage({userID,channelID,messageID,input})
28 | }
29 | };
30 |
31 | export function simulateActivity(userID){
32 | setInterval(simulateCreateMessage,interval,userID);
33 | }
--------------------------------------------------------------------------------
/src/App.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 |
4 | import {
5 | ContactListContainer,
6 | CurrentUserContainer,
7 | ChannelListContainer,
8 | ChannelContentContainer,
9 | CurrentChannelTextInputContainer
10 | } from './components';
11 |
12 | export const App = ()=>(
13 |
14 |
21 |
22 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 | );
--------------------------------------------------------------------------------
/src/App.less:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danielstern/advanced-redux/917b137e8528c264f8160545debe76f4ae9a6a84/src/App.less
--------------------------------------------------------------------------------
/src/actions/completeChannelCreation.js:
--------------------------------------------------------------------------------
1 | import { makeActionCreator } from '../utility';
2 |
3 | export const COMPLETE_CHANNEL_CREATION = `COMPLETE_CHANNEL_CREATION`;
4 | export const completeChannelCreation = makeActionCreator(COMPLETE_CHANNEL_CREATION, `channelID`, `success`);
--------------------------------------------------------------------------------
/src/actions/index.js:
--------------------------------------------------------------------------------
1 | export { makeActionCreator } from '../utility/makeActionCreator';
2 | export { setUserInfo, SET_USER_INFO } from './setUserInfo';
3 | export { updateChannelInputText, UPDATE_CHANNEL_INPUT_TEXT } from './updateChannelInputText';
4 | export { updateStatus, UPDATE_STATUS, AWAY, OFFLINE, ONLINE } from './updateStatus';
5 | export { updateUserFetchStatus, UPDATE_USER_FETCH_STATUS } from './updateUserFetchStatus';
6 | export { setActiveChannel, SET_ACTIVE_CHANNEL } from './setActiveChannel';
7 | export { updateChannelLoadedStatus, UPDATE_CHANNEL_FETCHED_STATUS } from './updateChannelLoadedStatus';
8 | export { setChannelInfo, SET_CHANNEL_INFO } from './setChannelInfo';
9 | export { submitChannelInputText, SUBMIT_CHANNEL_INPUT_TEXT } from './submitChannelInputText'
10 | export { receiveMessage, RECEIVE_MESSAGE } from './receiveMessage';
11 | export { openContactChannel } from './openContactChannel';
12 | export { requestCreateChannel, REQUEST_CREATE_CHANNEL } from './requestCreateChannel';
13 | export { completeChannelCreation, COMPLETE_CHANNEL_CREATION} from './completeChannelCreation';
14 |
15 | export const NOT_FETCHED = `NOT_FETCHED`;
16 | export const FETCHING = `FETCHING`;
17 | export const FETCHED = `FETCHED`;
18 |
--------------------------------------------------------------------------------
/src/actions/openContactChannel.js:
--------------------------------------------------------------------------------
1 | import { chance } from './../utility'
2 |
3 | import {
4 | setActiveChannel,
5 | requestCreateChannel
6 | } from './'
7 |
8 | import {
9 | currentUserSelector,
10 | userSelector
11 | } from './../selectors'
12 |
13 | export const openContactChannel = (id)=>(dispatch, getState)=>{
14 | const state = getState();
15 | const existingChannel = state.get(`channels`).find(channel=> channel.get(`participants`).size === 2 && channel.get(`participants`).includes(id));
16 | if (existingChannel) {
17 | dispatch(setActiveChannel(existingChannel.get(`id`)));
18 | } else {
19 | const channelID = chance.guid();
20 | const currentUserID = currentUserSelector(state).get(`id`);
21 | const channelName = `${currentUserSelector(state).get(`name`)} and ${userSelector(id)(state).get(`name`)}'s Private Chat`;
22 | dispatch(requestCreateChannel(channelID, id, currentUserID, channelName));
23 | dispatch(setActiveChannel(channelID));
24 | }
25 | };
--------------------------------------------------------------------------------
/src/actions/receiveMessage.js:
--------------------------------------------------------------------------------
1 | import { makeActionCreator } from '../utility';
2 |
3 | export const RECEIVE_MESSAGE = `RECEIVE_MESSAGE`;
4 | export const receiveMessage = makeActionCreator(RECEIVE_MESSAGE, `message`);
--------------------------------------------------------------------------------
/src/actions/requestCreateChannel.js:
--------------------------------------------------------------------------------
1 | import { makeActionCreator } from '../utility';
2 |
3 | export const REQUEST_CREATE_CHANNEL = `REQUEST_CREATE_CHANNEL`;
4 | export const requestCreateChannel = makeActionCreator(REQUEST_CREATE_CHANNEL, `channelID`,`contactID`,`ownID`,`channelName`);
--------------------------------------------------------------------------------
/src/actions/setActiveChannel.js:
--------------------------------------------------------------------------------
1 | import { makeActionCreator } from '../utility';
2 |
3 | export const SET_ACTIVE_CHANNEL = `SET_ACTIVE_CHANNEL`;
4 | export const setActiveChannel = makeActionCreator(SET_ACTIVE_CHANNEL, `id`);
--------------------------------------------------------------------------------
/src/actions/setChannelInfo.js:
--------------------------------------------------------------------------------
1 | import { makeActionCreator } from '../utility';
2 |
3 | export const SET_CHANNEL_INFO = `SET_CHANNEL_INFO`;
4 | export const setChannelInfo = makeActionCreator(SET_CHANNEL_INFO, `channel`);
--------------------------------------------------------------------------------
/src/actions/setUserInfo.js:
--------------------------------------------------------------------------------
1 | import { makeActionCreator } from '../utility/makeActionCreator';
2 |
3 | export const SET_USER_INFO = `SET_USER_INFO`;
4 | export const setUserInfo = makeActionCreator(SET_USER_INFO, `user`);
--------------------------------------------------------------------------------
/src/actions/submitChannelInputText.js:
--------------------------------------------------------------------------------
1 | import { chance } from './../utility';
2 | import { currentUserSelector } from './../selectors'
3 |
4 | export const SUBMIT_CHANNEL_INPUT_TEXT = `SUBMIT_CHANNEL_INPUT_TEXT`;
5 | export const submitChannelInputText = (channel,text)=>(dispatch,getState)=>{
6 | const state = getState();
7 | dispatch({
8 | type:SUBMIT_CHANNEL_INPUT_TEXT,
9 | channel,
10 | text,
11 | owner:currentUserSelector(state).get(`id`),
12 | id:chance.guid()
13 | })
14 | };
--------------------------------------------------------------------------------
/src/actions/updateChannelInputText.js:
--------------------------------------------------------------------------------
1 | import { makeActionCreator } from './';
2 |
3 | export const UPDATE_CHANNEL_INPUT_TEXT = `UPDATE_CHANNEL_INPUT_TEXT`;
4 | export const updateChannelInputText = makeActionCreator(UPDATE_CHANNEL_INPUT_TEXT, `channel`,`text`);
--------------------------------------------------------------------------------
/src/actions/updateChannelLoadedStatus.js:
--------------------------------------------------------------------------------
1 | import { makeActionCreator } from './';
2 |
3 | export const UPDATE_CHANNEL_FETCHED_STATUS = `UPDATE_CHANNEL_FETCHED_STATUS`;
4 | export const updateChannelLoadedStatus = makeActionCreator(UPDATE_CHANNEL_FETCHED_STATUS, `channel`,`status`);
--------------------------------------------------------------------------------
/src/actions/updateStatus.js:
--------------------------------------------------------------------------------
1 | import { makeActionCreator } from './';
2 |
3 | export const UPDATE_STATUS = `UPDATE_STATUS`;
4 | export const ONLINE = `ONLINE`;
5 | export const OFFLINE = `OFFLINE`;
6 | export const AWAY = `AWAY`;
7 |
8 | export const updateStatus = makeActionCreator(UPDATE_STATUS, `status`);
9 |
--------------------------------------------------------------------------------
/src/actions/updateUserFetchStatus.js:
--------------------------------------------------------------------------------
1 | import { makeActionCreator } from '../utility/makeActionCreator';
2 |
3 | export const UPDATE_USER_FETCH_STATUS = `UPDATE_USER_FETCH_STATUS`;
4 | export const updateUserFetchStatus = makeActionCreator(UPDATE_USER_FETCH_STATUS, `id`,`status`);
--------------------------------------------------------------------------------
/src/combineReducers.js:
--------------------------------------------------------------------------------
1 | export const combineReducers = (config) =>{
2 | return (state,action)=>{
3 | return Object.keys(config).reduce((state,key)=>{
4 | const reducer = config[key];
5 |
6 | const previousState = state.get(key);
7 |
8 | const newValue = reducer(previousState,action);
9 |
10 | if (!newValue) {
11 | throw new Error(`A reducer returned undefined when reducing key::"${key}"`);
12 | }
13 | return state.set(key,newValue);
14 | },state);
15 | };
16 | }
--------------------------------------------------------------------------------
/src/components/ChannelContent/ChannelContent.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {
3 | OFFLINE,
4 | FETCHED,
5 | FETCHING
6 | } from './../../actions'
7 | import { MessageContainer } from './../Message/MessageContainer'
8 | export const ChannelContent = ({messages,channelName,status,fetchStatus})=>(
9 |
10 |
11 | Channel: {channelName}
12 |
13 | {status === OFFLINE ?
14 | Contacts in the channel will see you as offline.
15 |
: null}
16 |
17 | {fetchStatus !== FETCHED ?
Please wait... : null}
18 | {messages.size === 0 && fetchStatus === FETCHED ?
Be the first to say something. : null}
19 | {messages.map(message=>(
20 |
21 |
22 |
23 | ))}
24 |
25 |
26 | );
27 |
--------------------------------------------------------------------------------
/src/components/ChannelContent/ChannelContentContainer.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import { connect } from 'react-redux'
4 |
5 | import {
6 | ChannelContent
7 | } from './ChannelContent';
8 |
9 | import {
10 | activeChannelSelector,
11 | } from './../../selectors'
12 |
13 | const mapStateToProps = (state) => {
14 | const channels = state.get(`channels`);
15 | const activeChannel = state.get(`activeChannel`);
16 | const channel = activeChannelSelector(state);
17 |
18 | return {
19 | messages:channel.get(`messages`),
20 | channelName:channel.get(`name`),
21 | fetchStatus:channel.get(`fetchStatus`),
22 | status:state.get(`currentUser`).get(`status`)
23 | }
24 | };
25 |
26 | const mapDispatchToProps = (dispatch) => ({});
27 |
28 | export const ChannelContentContainer = connect(
29 | mapStateToProps,
30 | mapDispatchToProps
31 | )(ChannelContent);
--------------------------------------------------------------------------------
/src/components/ChannelList/ChannelList.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {ChannelListItem} from './ChannelListItem'
3 | export const ChannelList = ({channels,activeChannel,setActiveChannel})=>(
4 |
5 |
6 |
Channels
7 |
8 |
9 | {channels.map(channel=>
10 |
16 | )}
17 |
18 |
19 | );
20 |
--------------------------------------------------------------------------------
/src/components/ChannelList/ChannelListContainer.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import { connect } from 'react-redux'
4 |
5 | import {
6 | ChannelList
7 | } from './ChannelList';
8 |
9 | import {
10 | setActiveChannel
11 | } from './../../actions/setActiveChannel'
12 |
13 | const mapStateToProps = (state) => ({
14 | channels:state.get(`channels`),
15 | activeChannel: state.get(`activeChannel`)
16 | });
17 |
18 | const mapDispatchToProps = (dispatch) => ({
19 | setActiveChannel:(channel)=>{
20 | dispatch(setActiveChannel(channel));
21 | }
22 | });
23 |
24 | export const ChannelListContainer = connect(
25 | mapStateToProps,
26 | mapDispatchToProps
27 | )(ChannelList);
--------------------------------------------------------------------------------
/src/components/ChannelList/ChannelListItem.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import classnames from 'classnames';
3 |
4 |
5 | export const ChannelListItem = ({id,name,setActiveChannel,isActive})=>{
6 | const className = classnames('list-group-item',{active:isActive});
7 | return (
8 | setActiveChannel(id)}>{name}
9 | )
10 | };
--------------------------------------------------------------------------------
/src/components/ContactList/ContactList.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { ContactListItemContainer } from './ContactListItemContainer'
3 |
4 | export const ContactList = ({contacts,name,openConversation})=>(
5 |
6 |
7 |
{name}'s Contacts
8 |
9 |
10 | {contacts.map(contact=>(
11 |
12 | ))}
13 |
14 |
15 | );
--------------------------------------------------------------------------------
/src/components/ContactList/ContactListContainer.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import { connect } from 'react-redux'
4 |
5 | import {
6 | ContactList
7 | } from './ContactList';
8 |
9 | const mapStateToProps = (state) => {
10 | return {
11 | contacts:state.get(`currentUser`).get(`contacts`),
12 | name:state.get(`currentUser`).get(`name`)
13 | }
14 | };
15 |
16 | export const ContactListContainer = connect(
17 | mapStateToProps
18 | )(ContactList);
--------------------------------------------------------------------------------
/src/components/ContactList/ContactListItem.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { OFFLINE } from './../../actions'
3 | import Chance from 'chance';
4 |
5 | export const ContactListItem = ({id,name,status,openChannel})=>(
6 |
7 |
8 |
13 |
14 |
{name}
15 | {status !== OFFLINE ? openChannel(id)} disabled={!status || status === OFFLINE}>
16 | Chat
17 | : (Offline)}
18 |
19 |
20 |
21 | );
--------------------------------------------------------------------------------
/src/components/ContactList/ContactListItemContainer.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import { connect } from 'react-redux'
4 | import { ContactListItem } from './ContactListItem';
5 | import { userSelector } from './../../selectors'
6 | import { openContactChannel } from './../../actions'
7 |
8 | const mapStateToProps = (state, {id}) => {
9 | const contact = userSelector(id)(state);
10 | return {
11 | name:contact.get(`name`),
12 | id:contact.get(`id`),
13 | status:contact.get(`status`),
14 | }
15 | };
16 |
17 | const mapDispatchToProps = (dispatch) => ({
18 | openChannel(id){
19 | dispatch(openContactChannel(id));
20 | }
21 | });
22 |
23 | export const ContactListItemContainer = connect(
24 | mapStateToProps,
25 | mapDispatchToProps
26 | )(ContactListItem);
--------------------------------------------------------------------------------
/src/components/CurrentChannelTextInput/CurrentChannelTextInput.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {
3 | FETCHED,
4 | OFFLINE
5 | } from './../../actions'
6 | import classnames from 'classnames';
7 |
8 | export const CurrentChannelTextInput = ({text = "",submitMessage,updateText,activeChannel,fetchStatus,userStatus})=>{
9 | const buttonClass = classnames('btn','btn-default',{disabled:userStatus === OFFLINE});
10 | return (
11 |
29 | )};
30 |
--------------------------------------------------------------------------------
/src/components/CurrentChannelTextInput/CurrentChannelTextInputContainer.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import { connect } from 'react-redux'
4 |
5 | import {
6 | CurrentChannelTextInput
7 | } from './CurrentChannelTextInput';
8 |
9 | import {
10 | updateChannelInputText,
11 | submitChannelInputText,
12 | } from './../../actions/';
13 |
14 | import {
15 | activeChannelSelector,
16 | currentUserSelector
17 | } from './../../selectors'
18 |
19 | const mapStateToProps = (state) => {
20 | const activeChannel = activeChannelSelector(state);
21 | return {
22 | activeChannel:activeChannel.get(`id`),
23 | text:activeChannel.get(`currentUserText`),
24 | fetchStatus:activeChannel.get(`fetchStatus`),
25 | userStatus:currentUserSelector(state).get(`status`)
26 | }
27 | };
28 |
29 | const mapDispatchToProps = (dispatch) => {
30 | return {
31 | updateText: (text,channel) => {
32 | dispatch(updateChannelInputText(channel,text));
33 | },
34 | submitMessage: (text,channel) => {
35 | dispatch(submitChannelInputText(channel,text));
36 | }
37 | }
38 | };
39 |
40 | export const CurrentChannelTextInputContainer = connect(
41 | mapStateToProps,
42 | mapDispatchToProps,
43 | )(CurrentChannelTextInput);
--------------------------------------------------------------------------------
/src/components/CurrentUser/CurrentUser.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {
3 | ONLINE,
4 | OFFLINE,
5 | AWAY
6 | } from './../../actions'
7 | export const CurrentUser = ({name,status,updateStatus,id})=>(
8 |
9 |
10 |
Hi, {name}!
11 |
12 | {/*
*/}
15 |
16 |
21 |
22 |
23 | );
24 |
--------------------------------------------------------------------------------
/src/components/CurrentUser/CurrentUserContainer.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { connect } from 'react-redux'
3 | import { updateStatus } from './../../actions'
4 | import { CurrentUser } from './CurrentUser';
5 |
6 | const mapStateToProps = (state) => {
7 | const currentUser = state.get(`currentUser`);
8 | return {
9 | name:currentUser.get(`name`),
10 | status:currentUser.get(`status`),
11 | id:currentUser.get(`id`)
12 | }
13 | };
14 |
15 | const mapDispatchToProps = (dispatch) => {
16 | return {
17 | updateStatus: ({target:{value}}) => {
18 | dispatch(updateStatus(value));
19 | }
20 | }
21 | };
22 |
23 | export const CurrentUserContainer = connect(
24 | mapStateToProps,
25 | mapDispatchToProps
26 | )(CurrentUser);
--------------------------------------------------------------------------------
/src/components/DevTools/DevTools.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { createDevTools } from 'redux-devtools';
3 |
4 | import LogMonitor from 'redux-devtools-log-monitor';
5 | import DockMonitor from 'redux-devtools-dock-monitor';
6 |
7 | export const DevTools = createDevTools(
8 |
12 |
13 |
14 | );
--------------------------------------------------------------------------------
/src/components/Message/Message.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | export const Message = ({owner:{name},text})=>(
3 |
4 |
5 | {name}
6 | : {text}
7 |
8 | )
--------------------------------------------------------------------------------
/src/components/Message/MessageContainer.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import { connect } from 'react-redux'
4 |
5 | import {
6 | Message
7 | } from './Message';
8 |
9 | import { userSelector } from './../../selectors'
10 |
11 | const mapStateToProps = (state, {message}) => {
12 | const owner = userSelector(message.get(`owner`))(state);
13 | return {
14 | text:message.get(`content`).get(`text`),
15 | owner:{
16 | name:owner.get(`fetchStatus`).includes(`FETCHED`) ? owner.get(`name`) : `[...]`
17 | }
18 | }
19 | };
20 |
21 | const mapDispatchToProps = (dispatch) => ({});
22 |
23 | export const MessageContainer = connect(
24 | mapStateToProps,
25 | mapDispatchToProps
26 | )(Message);
--------------------------------------------------------------------------------
/src/components/index.js:
--------------------------------------------------------------------------------
1 | export { ChannelContentContainer } from './ChannelContent/ChannelContentContainer';
2 | export { ChannelListContainer } from './ChannelList/ChannelListContainer';
3 | export { ContactListContainer } from './ContactList/ContactListContainer';
4 | export { CurrentChannelTextInputContainer } from './CurrentChannelTextInput/CurrentChannelTextInputContainer';
5 | export { CurrentUserContainer } from './CurrentUser/CurrentUserContainer';
6 | export { DevTools } from './DevTools/DevTools';
7 |
--------------------------------------------------------------------------------
/src/getPreloadedState.js:
--------------------------------------------------------------------------------
1 | import { fromJS } from 'immutable'
2 |
3 | const preloadedState = fromJS(window.__PRELOADED_STATE__);
4 | delete window.__PRELOADED_STATE__;
5 |
6 | export const getPreloadedState = ()=>preloadedState;
7 |
8 |
--------------------------------------------------------------------------------
/src/getStore.js:
--------------------------------------------------------------------------------
1 | import {
2 | createStore,
3 | applyMiddleware,
4 | compose
5 | } from 'redux';
6 |
7 | import createSagaMiddleware from 'redux-saga';
8 |
9 | import thunk from 'redux-thunk';
10 | import { persistState } from 'redux-devtools';
11 | import { getPreloadedState } from './getPreloadedState'
12 | import { getDebugSessionKey } from './utility'
13 | import { DevTools } from './components/DevTools/DevTools'
14 | import { initSagas } from './initSagas';
15 |
16 | import {
17 | RECEIVE_MESSAGE,
18 | receiveMessage
19 | } from './actions'
20 |
21 | import { reducer } from './reducers';
22 | import { createSocketMiddleware} from './socketMiddleware'
23 |
24 | const preloadedState = getPreloadedState();
25 | const sagaMiddleware = createSagaMiddleware();
26 |
27 |
28 | const io = window.io;
29 |
30 | const socketMiddleware = createSocketMiddleware(io)({
31 | NEW_MESSAGE:(data)=>({
32 | type:RECEIVE_MESSAGE,
33 | message:data
34 | })
35 | });
36 |
37 | const enhancer = compose(
38 | applyMiddleware(
39 | sagaMiddleware,
40 | thunk,
41 | socketMiddleware
42 | ),
43 | DevTools.instrument(),
44 | persistState(getDebugSessionKey())
45 | );
46 |
47 | const store = createStore(
48 | reducer,
49 | preloadedState,
50 | enhancer
51 | );
52 |
53 | export const getStore = ()=>store;
54 |
55 | initSagas(sagaMiddleware);
56 |
57 |
--------------------------------------------------------------------------------
/src/initSagas.js:
--------------------------------------------------------------------------------
1 | import * as sagas from './sagas';
2 |
3 | export const initSagas = (sagaMiddleware)=>{
4 | Object.values(sagas).forEach(sagaMiddleware.run.bind(sagaMiddleware));
5 | };
--------------------------------------------------------------------------------
/src/main.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import reactDOM from 'react-dom'
3 | import {getStore} from './getStore';
4 | import {Provider} from 'react-redux';
5 |
6 | import { DevTools } from './components/DevTools/DevTools'
7 | import { App } from './App';
8 |
9 | const store = getStore();
10 |
11 | const Main = ()=>(
12 |
13 |
14 |
15 | )
16 |
17 | const render = (store)=>{
18 | reactDOM.render(
19 |
20 |
21 |
22 |
,
23 | document.getElementById('AppContainer'));
24 | };
25 |
26 | render(store);
--------------------------------------------------------------------------------
/src/reducers/activeChannel.js:
--------------------------------------------------------------------------------
1 | import { SET_ACTIVE_CHANNEL } from './../actions/setActiveChannel'
2 | import { createReducer } from './../utility';
3 |
4 | export const activeChannel = createReducer(null, {
5 | [SET_ACTIVE_CHANNEL](state,action) {
6 | return action.id;
7 | }
8 | });
--------------------------------------------------------------------------------
/src/reducers/channels.js:
--------------------------------------------------------------------------------
1 | import {
2 | UPDATE_CHANNEL_INPUT_TEXT,
3 | UPDATE_CHANNEL_FETCHED_STATUS,
4 | SET_CHANNEL_INFO,
5 | SUBMIT_CHANNEL_INPUT_TEXT,
6 | RECEIVE_MESSAGE,
7 | REQUEST_CREATE_CHANNEL,
8 | COMPLETE_CHANNEL_CREATION,
9 | FETCHED
10 | } from './../actions/'
11 |
12 | import { createReducer } from './../utility';
13 |
14 | import {
15 | fromJS
16 | } from 'immutable';
17 |
18 | const receiveMessageHandler = (state,{message:{id,channelID,content,owner}})=>{
19 | const index = state.findIndex(channel=>channel.get(`id`) === channelID);
20 | if (index === -1) {
21 | return state;
22 | }
23 | let channel = state.get(index);
24 | let messages = channel.get(`messages`);
25 |
26 | if (messages.map(message=>message.get(`id`)).includes(id)) {
27 | return state;
28 | }
29 |
30 | let newMessages = messages.push(fromJS({
31 | id, content, owner, date:new Date()
32 | }));
33 |
34 | return state.setIn([index,`messages`],newMessages);
35 |
36 | };
37 |
38 | const requestCreateChannelHandler = (state,{ownID,contactID, channelID, channelName})=>{
39 | return state.push(fromJS({
40 | id:channelID,
41 | participants:[ownID,contactID],
42 | fetchStatus:`FETCHING`,
43 | messages:[],
44 | name:channelName
45 | }));
46 | };
47 |
48 | const completeChannelCreationHandler = (state,{channelID,success})=>{
49 | const index = state.findIndex(channel=>channel.get(`id`) === channelID);
50 | return state.setIn([index,`fetchStatus`],FETCHED);
51 |
52 | }
53 |
54 | export const channels = createReducer(null, {
55 | [COMPLETE_CHANNEL_CREATION]:completeChannelCreationHandler,
56 | [REQUEST_CREATE_CHANNEL]: requestCreateChannelHandler,
57 | [RECEIVE_MESSAGE]: receiveMessageHandler,
58 | [UPDATE_CHANNEL_INPUT_TEXT](state,action) {
59 | const index = state.findIndex(channel=>channel.get(`id`) === action.channel);
60 | return state.setIn([index,`currentUserText`],action.text);
61 | },
62 | [SUBMIT_CHANNEL_INPUT_TEXT](state,action) {
63 | const index = state.findIndex(channel=>channel.get(`id`) === action.channel);
64 | let channel = state.get(index);
65 | let messages = channel.get(`messages`);
66 | let id = action.id;
67 |
68 | let newMessages = messages.push(fromJS({
69 | id,
70 | content:{
71 | text:action.text
72 | },
73 | owner:action.owner,
74 | date:new Date()
75 | }));
76 |
77 | let newState = state.setIn([index,`messages`],newMessages);
78 | newState = newState.setIn([index,`currentUserText`],"");
79 | return newState;
80 |
81 | },
82 | [UPDATE_CHANNEL_FETCHED_STATUS](state, action) {
83 | const index = state.findIndex(channel=>channel.get(`id`) === action.channel);
84 | return state.setIn([index,`fetchStatus`],action.status);
85 | },
86 | [SET_CHANNEL_INFO](state,action) {
87 | const index = state.findIndex(channel=>channel.get(`id`) === action.channel.get(`id`));
88 | return state.set(index,action.channel);
89 | },
90 | });
--------------------------------------------------------------------------------
/src/reducers/currentUser.js:
--------------------------------------------------------------------------------
1 | import { createReducer } from './../utility';
2 | import { UPDATE_STATUS } from './../actions/'
3 |
4 | export const currentUser = createReducer(null, {
5 | [UPDATE_STATUS](state,action) {
6 | return state.set(`status`,action.status);
7 | }
8 | });
--------------------------------------------------------------------------------
/src/reducers/index.js:
--------------------------------------------------------------------------------
1 | import { channels } from './channels'
2 | import { currentUser } from './currentUser'
3 | import { activeChannel } from './activeChannel'
4 | import { userInfo } from './userInfo';
5 | import { combineReducers } from './../combineReducers';
6 |
7 | export const reducer = combineReducers({
8 | currentUser,
9 | channels,
10 | activeChannel,
11 | userInfo,
12 | });
--------------------------------------------------------------------------------
/src/reducers/userInfo.js:
--------------------------------------------------------------------------------
1 | import { createReducer } from './../utility'
2 | import { Map } from 'immutable'
3 | import {
4 | UPDATE_USER_FETCH_STATUS,
5 | SET_USER_INFO,
6 | FETCHED
7 | } from './../actions'
8 |
9 | export const userInfo = createReducer(null, {
10 | [UPDATE_USER_FETCH_STATUS](state,action) {
11 | const index = state.findIndex(user=>user.get(`id`) === action.id);
12 | if (index === -1) {
13 | return state.push(Map({
14 | id:action.id,
15 | fetchStatus:action.status,
16 | name:`[...]`
17 | }))
18 | } else {
19 | return state.setIn([index,`fetchStatus`],action.status);
20 | }
21 | },
22 | [SET_USER_INFO](state,action) {
23 | const index = state.findIndex(user=>user.get(`id`) === action.user.get(`id`));
24 | return state.set(index,action.user.set(`fetchStatus`,FETCHED));
25 | },
26 |
27 | });
--------------------------------------------------------------------------------
/src/sagas/activeChannelSaga.js:
--------------------------------------------------------------------------------
1 | import { SET_ACTIVE_CHANNEL } from './../actions'
2 | import { currentUserSelector } from './../selectors'
3 | import { takeLatest, call, select } from 'redux-saga/effects'
4 |
5 | function* putActiveChannel({id}){
6 | const currentUser = yield select(currentUserSelector);
7 | const userID = currentUser.get(`id`);
8 | yield call(()=>fetch(`/user/activeChannel/${userID}/${id}`));
9 | }
10 |
11 | export function* activeChannelSaga() {
12 | yield takeLatest(SET_ACTIVE_CHANNEL, putActiveChannel);
13 | }
--------------------------------------------------------------------------------
/src/sagas/channelSaga.js:
--------------------------------------------------------------------------------
1 | import { fromJS } from 'immutable';
2 | import { takeEvery, select, put, call } from 'redux-saga/effects'
3 | import { channelSelector } from './../selectors'
4 | import {
5 | FETCHED,
6 | NOT_FETCHED,
7 | SET_ACTIVE_CHANNEL,
8 | setChannelInfo,
9 | } from './../actions'
10 |
11 | function* fetchChannelInfo({id}){
12 | const selector = channelSelector(id);
13 | const channel = yield select(selector);
14 |
15 | if (channel.get(`fetchStatus`) === NOT_FETCHED) {
16 | const response = yield call(()=>fetch(`/channel/${id}`));
17 | const channelInfo = yield call(()=>response.json());
18 | yield put(setChannelInfo(fromJS({fetchStatus:FETCHED,...channelInfo})));
19 | } else {
20 | yield;
21 | }
22 | }
23 |
24 | export function* channelSaga() {
25 | yield takeEvery(SET_ACTIVE_CHANNEL, fetchChannelInfo);
26 | }
--------------------------------------------------------------------------------
/src/sagas/createChannelSaga.js:
--------------------------------------------------------------------------------
1 | import { takeEvery, put, call } from 'redux-saga/effects'
2 |
3 | import {
4 | REQUEST_CREATE_CHANNEL,
5 | completeChannelCreation
6 | } from './../actions'
7 |
8 | function* requestCreateChannel({ownID,contactID,channelName,channelID}) {
9 | const participants = JSON.stringify([ownID,contactID]);
10 | yield call(()=>fetch(`/channel/create/${channelID}/${channelName}/${participants}`));
11 | yield put(completeChannelCreation(channelID,true));
12 | }
13 |
14 | export function* createChannelSaga() {
15 | yield takeEvery(REQUEST_CREATE_CHANNEL, requestCreateChannel);
16 | }
--------------------------------------------------------------------------------
/src/sagas/currentChannelInputSaga.js:
--------------------------------------------------------------------------------
1 | import { SUBMIT_CHANNEL_INPUT_TEXT } from './../actions'
2 | import { takeEvery, call } from 'redux-saga/effects'
3 |
4 | function* submitChannelInputText({channel,text,owner,id}){
5 | const channelID = channel;
6 | yield call(()=>fetch(`/input/submit/${owner}/${channelID}/${id}/${text}`));
7 | }
8 |
9 | export function* currentChannelInputSaga() {
10 | yield takeEvery(SUBMIT_CHANNEL_INPUT_TEXT, submitChannelInputText);
11 | }
--------------------------------------------------------------------------------
/src/sagas/currentUserStatusSaga.js:
--------------------------------------------------------------------------------
1 | import { UPDATE_STATUS } from './../actions'
2 | import { currentUserSelector } from './../selectors'
3 | import { takeLatest, call, select } from 'redux-saga/effects'
4 |
5 | function* putUserStatus({status}){
6 | const currentUser = yield select(currentUserSelector);
7 | const id = currentUser.get(`id`);
8 | yield call(()=>fetch(`/status/${id}/${status}`));
9 | }
10 |
11 | export function* currentUserStatusSaga() {
12 | yield takeLatest(UPDATE_STATUS, putUserStatus);
13 | }
--------------------------------------------------------------------------------
/src/sagas/index.js:
--------------------------------------------------------------------------------
1 | export { userInfoSaga } from './userInfoSaga'
2 | export { channelSaga } from './channelSaga'
3 | export { currentUserStatusSaga } from './currentUserStatusSaga'
4 | export { currentChannelInputSaga } from './currentChannelInputSaga';
5 | export { createChannelSaga } from './createChannelSaga'
6 | export { activeChannelSaga } from './activeChannelSaga'
--------------------------------------------------------------------------------
/src/sagas/userInfoSaga.js:
--------------------------------------------------------------------------------
1 | import fetch from 'isomorphic-fetch';
2 | import { fromJS } from 'immutable';
3 | import {
4 | call,
5 | put,
6 | takeEvery,
7 | select,
8 | fork
9 | } from 'redux-saga/effects'
10 |
11 | import { SET_CHANNEL_INFO } from './../actions'
12 | import { userSelector } from './../selectors'
13 | import {
14 | updateUserFetchStatus,
15 | setUserInfo,
16 | FETCHING,
17 | FETCHED,
18 | NOT_FETCHED
19 | } from './../actions'
20 |
21 | function* fetchUserInfo(id){
22 | const user = yield select(userSelector(id));
23 |
24 | if (user.get(`fetchStatus`) === NOT_FETCHED) {
25 | yield put(updateUserFetchStatus(id,FETCHING));
26 | const response = yield call(()=>fetch(`/user/${id}`));
27 | const userInfo = yield call(()=>response.json());
28 | yield put(updateUserFetchStatus(id,FETCHED));
29 | yield put(setUserInfo(fromJS(userInfo)));
30 | }
31 | }
32 |
33 | function* fetchChannelUsers({channel}){
34 | const ids = channel.get(`participants`);
35 | for (let id of ids) {
36 | yield fork(fetchUserInfo,id);
37 | }
38 | }
39 |
40 | export function* userInfoSaga() {
41 | yield takeEvery(SET_CHANNEL_INFO, fetchChannelUsers);
42 | }
--------------------------------------------------------------------------------
/src/selectors/activeChannelSelector.js:
--------------------------------------------------------------------------------
1 | import { createSelector } from 'reselect';
2 |
3 | export const activeChannelSelector = createSelector(
4 | state=>state.get(`activeChannel`),
5 | state=>state.get(`channels`),
6 | (activeChannel,channels)=>{return channels.find(channel=>channel.get(`id`)=== activeChannel)}
7 | );
--------------------------------------------------------------------------------
/src/selectors/channelSelector.js:
--------------------------------------------------------------------------------
1 | import { createSelector } from 'reselect';
2 |
3 | export const channelSelector = (id)=>createSelector(
4 | state=>state.get(`channels`),
5 | (channels)=>channels.find(channel=>channel.get(`id`) === id)
6 | );
--------------------------------------------------------------------------------
/src/selectors/currentUser.js:
--------------------------------------------------------------------------------
1 | import { createSelector } from 'reselect';
2 |
3 | export const currentUserSelector = createSelector(
4 | state=>state.get(`currentUser`),
5 | currentUser=>currentUser
6 | );
7 |
--------------------------------------------------------------------------------
/src/selectors/index.js:
--------------------------------------------------------------------------------
1 | export { channelSelector } from './channelSelector';
2 | export { userSelector } from './userSelector';
3 | export { activeChannelSelector } from './activeChannelSelector';
4 | export { currentUserSelector } from './currentUser';
--------------------------------------------------------------------------------
/src/selectors/userSelector.js:
--------------------------------------------------------------------------------
1 | import {
2 | fromJS
3 | } from 'immutable'
4 |
5 | import { createSelector } from 'reselect';
6 |
7 | export const userSelector = (id)=>createSelector(
8 | state=>state.get(`userInfo`),
9 | userInfo=>{
10 | const user = userInfo.find(user=>user.get(`id`)===id);
11 | if (user) {
12 | return user;
13 | } else {
14 | return fromJS({
15 | name:"[...]",
16 | fetchStatus:"NOT_FETCHED",
17 | id
18 | });
19 | }
20 | }
21 | );
22 |
--------------------------------------------------------------------------------
/src/socketMiddleware.js:
--------------------------------------------------------------------------------
1 | export const createSocketMiddleware = io => config => {
2 | const socket = io();
3 | return store => next => action => {
4 | for (const key in config) {
5 | socket.on(key, config[key]);
6 | }
7 | let result = next(action);
8 | return result;
9 | };
10 | }
--------------------------------------------------------------------------------
/src/utility/chance.js:
--------------------------------------------------------------------------------
1 | import Chance from 'chance';
2 | export const chance = new Chance();
--------------------------------------------------------------------------------
/src/utility/createReducer.js:
--------------------------------------------------------------------------------
1 | export const createReducer = (initialState, handlers)=>{
2 | return function reducer(state = initialState, action) {
3 | if (handlers.hasOwnProperty(action.type)) {
4 | return handlers[action.type](state, action)
5 | } else {
6 | return state
7 | }
8 | }
9 | }
--------------------------------------------------------------------------------
/src/utility/getDebugSessionKey.js:
--------------------------------------------------------------------------------
1 | export const getDebugSessionKey = ()=>{
2 | if (typeof (window) !== 'undefined') {
3 | const matches = window.location.href.match(/[?&]debug_session=([^]+)\b/);
4 | return (matches && matches.length > 0)? matches[1] : null;
5 | } else {
6 | return null;
7 | }
8 |
9 | };
--------------------------------------------------------------------------------
/src/utility/index.js:
--------------------------------------------------------------------------------
1 | export { chance } from './chance';
2 | export { makeActionCreator } from './makeActionCreator';
3 | export { createReducer } from './createReducer';
4 | export { getDebugSessionKey } from './getDebugSessionKey'
--------------------------------------------------------------------------------
/src/utility/makeActionCreator.js:
--------------------------------------------------------------------------------
1 | export const makeActionCreator = (type, ...argNames) => {
2 | return function(...args) {
3 | let action = { type };
4 | argNames.forEach((arg, index) => {
5 | action[argNames[index]] = args[index]
6 | });
7 | return action
8 | }
9 | };
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const webpack = require('webpack');
2 | const path = require('path');
3 | module.exports = {
4 | module: {
5 | loaders: [
6 | {
7 | loader: "babel-loader",
8 | exclude: [
9 | /(node_modules)/,
10 | ],
11 | query: {
12 | presets: ['es2015','react'],
13 | plugins: ['transform-object-rest-spread']
14 | }
15 | }
16 | ]
17 | },
18 | plugins: [
19 | new webpack.HotModuleReplacementPlugin(),
20 | new webpack.NoErrorsPlugin()
21 | ],
22 | entry: {
23 | "index": [
24 | 'babel-regenerator-runtime',
25 | 'webpack-hot-middleware/client?path=/__webpack_hmr&timeout=20000&reload=true',
26 | './src/main'
27 | ]
28 | },
29 | output: {
30 | path: path.resolve(__dirname, "public"),
31 | publicPath: "/assets",
32 | filename: "[name].bundle.js"
33 | },
34 | resolve: {
35 | extensions: ['', '.js', '.jsx'],
36 | },
37 | devServer: { inline: true },
38 | devtool: 'source-map'
39 | };
--------------------------------------------------------------------------------