├── api ├── .gitignore ├── src │ └── main │ │ ├── resources │ │ └── application.properties │ │ └── java │ │ └── example │ │ └── chat │ │ ├── Application.java │ │ ├── RegisterPayload.java │ │ ├── UserLeftMessage.java │ │ ├── UserEnterMessage.java │ │ ├── UserListenerConfig.java │ │ ├── Message.java │ │ ├── ChatRoomController.java │ │ ├── DisconnectionWatcher.java │ │ ├── UserListService.java │ │ ├── RegisterController.java │ │ ├── RegisterResponse.java │ │ └── WebSocketConfig.java ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── build.gradle ├── gradlew.bat └── gradlew ├── front ├── .gitignore ├── src │ ├── containers │ │ ├── index.jsx │ │ ├── Welcome │ │ │ ├── components │ │ │ │ ├── index.js │ │ │ │ ├── errorMessage.jsx │ │ │ │ ├── tests │ │ │ │ │ ├── nameInput.test.jsx │ │ │ │ │ ├── enterButton.test.jsx │ │ │ │ │ ├── welcomePage.test.jsx │ │ │ │ │ └── enterForm.test.jsx │ │ │ │ ├── nameInput.jsx │ │ │ │ ├── enterButton.jsx │ │ │ │ ├── welcomePage.jsx │ │ │ │ └── enterForm.jsx │ │ │ ├── actionTypes.js │ │ │ ├── actions.js │ │ │ ├── index.jsx │ │ │ ├── tests │ │ │ │ ├── connectedComponent.test.js │ │ │ │ ├── actions.test.js │ │ │ │ └── reducer.test.js │ │ │ ├── reducer.js │ │ │ └── saga.js │ │ └── Chat │ │ │ ├── components │ │ │ ├── index.js │ │ │ ├── sendMessageButton.jsx │ │ │ ├── tests │ │ │ │ ├── userList.test.js │ │ │ │ ├── sendMessageButton.test.js │ │ │ │ ├── messageInput.test.js │ │ │ │ ├── message.test.js │ │ │ │ ├── messageArea.test.js │ │ │ │ ├── chatPage.test.js │ │ │ │ └── messageForm.test.js │ │ │ ├── messageInput.jsx │ │ │ ├── messageArea.jsx │ │ │ ├── message.jsx │ │ │ ├── userList.jsx │ │ │ ├── messageForm.jsx │ │ │ └── chatPage.jsx │ │ │ ├── actionTypes.js │ │ │ ├── index.jsx │ │ │ ├── tests │ │ │ ├── connectedComponent.test.js │ │ │ ├── actions.test.js │ │ │ └── reducer.test.js │ │ │ ├── actions.js │ │ │ ├── reducer.js │ │ │ └── saga.js │ ├── index.jsx │ ├── elements │ │ ├── index.js │ │ ├── page.jsx │ │ ├── title.jsx │ │ ├── card.jsx │ │ ├── input.jsx │ │ └── button.jsx │ ├── template.jsx │ ├── utils │ │ ├── checkEnter.js │ │ ├── getUserColorByName.js │ │ └── socketConnection.js │ ├── getReducer.js │ ├── cssVars.js │ └── app.jsx ├── .babelrc ├── .eslintrc.js ├── internals │ ├── webpackConfigs │ │ ├── js.js │ │ ├── devServer.js │ │ ├── html.js │ │ └── base.js │ └── webpack.config.babel.js └── package.json ├── demo.gif ├── front_Dockerfile ├── api_dev_Dockerfile ├── api_Dockerfile ├── docker-compose.yml ├── docker-compose-dev.yml └── README.md /api/.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | .gradle 3 | -------------------------------------------------------------------------------- /api/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | server.port = 3000 2 | -------------------------------------------------------------------------------- /front/.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | node_modules 3 | package-lock.json 4 | jest_* 5 | -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drFabio/react_springboot_simple_chat/master/demo.gif -------------------------------------------------------------------------------- /front/src/containers/index.jsx: -------------------------------------------------------------------------------- 1 | export {Welcome} from './Welcome' 2 | export {Chat} from './Chat' 3 | -------------------------------------------------------------------------------- /front_Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:8.0.0 2 | RUN mkdir /etc/api 3 | WORKDIR /etc/front 4 | COPY front /etc/front 5 | RUN npm install 6 | -------------------------------------------------------------------------------- /api/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drFabio/react_springboot_simple_chat/master/api/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /front/src/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import {App} from './app.jsx' 4 | 5 | ReactDOM.render(, document.getElementById('app')) 6 | -------------------------------------------------------------------------------- /front/src/elements/index.js: -------------------------------------------------------------------------------- 1 | export {Page} from './page' 2 | export {Title} from './title' 3 | export {Input} from './input' 4 | export {Button} from './button' 5 | export {Card} from './card' 6 | -------------------------------------------------------------------------------- /front/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "es2015", 4 | "es2016", 5 | "es2017", 6 | "react", 7 | "stage-0" 8 | ], 9 | "env": { 10 | "start": { 11 | "presets": [ 12 | "react-hmre" 13 | ] 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /front/src/containers/Welcome/components/index.js: -------------------------------------------------------------------------------- 1 | export {EnterForm} from './enterForm' 2 | export {EnterButton} from './enterButton' 3 | export {NameInput} from './nameInput' 4 | export {WelcomePage} from './welcomePage' 5 | export {ErrorMessage} from './errorMessage' 6 | -------------------------------------------------------------------------------- /front/src/template.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react' 2 | import {Page} from 'elements' 3 | export class Template extends Component { 4 | render () { 5 | return ( 6 | 7 | {this.props.children} 8 | 9 | ) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /front/src/utils/checkEnter.js: -------------------------------------------------------------------------------- 1 | export function checkEnter (e) { 2 | const event = e || window.event 3 | const charCode = event.which || event.keyCode 4 | if (parseInt(charCode, 10) === 13) { 5 | return true 6 | } 7 | return false 8 | } 9 | export default checkEnter 10 | -------------------------------------------------------------------------------- /api/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sun Jun 04 14:57:21 UTC 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-3.5-bin.zip 7 | -------------------------------------------------------------------------------- /api_dev_Dockerfile: -------------------------------------------------------------------------------- 1 | FROM openjdk:8-jdk 2 | ENV SDKMAN_DIR /opt/sdkman 3 | RUN apt-get update -y 4 | RUN apt-get install zip unzip curl -y 5 | RUN curl -s "https://get.sdkman.io" | bash 6 | RUN chmod +x "$SDKMAN_DIR/bin/sdkman-init.sh" 7 | RUN /bin/bash -c "source $SDKMAN_DIR/bin/sdkman-init.sh && sdk install gradle 3.5 && sdk install springboot" 8 | -------------------------------------------------------------------------------- /front/src/containers/Chat/components/index.js: -------------------------------------------------------------------------------- 1 | export {ChatPage} from './chatPage' 2 | export {MessageInput} from './messageInput' 3 | export {MessageForm} from './messageForm' 4 | export {SendMessageButton} from './sendMessageButton' 5 | export {Message} from './message' 6 | export {MessageArea} from './messageArea' 7 | export {UserList} from './userList' 8 | -------------------------------------------------------------------------------- /front/src/containers/Welcome/actionTypes.js: -------------------------------------------------------------------------------- 1 | const prefix = 'welcome' 2 | export const ENTER_CHAT = `${prefix}_ENTER_CHAT` 3 | export const USER_ALREADY_EXISTS = `${prefix}_USER_ALREADY_EXISTS` 4 | export const EMPTY_USER_NAME = `${prefix}_EMPTY_USER_NAME` 5 | export default { 6 | ENTER_CHAT, 7 | USER_ALREADY_EXISTS, 8 | EMPTY_USER_NAME 9 | } 10 | -------------------------------------------------------------------------------- /front/src/getReducer.js: -------------------------------------------------------------------------------- 1 | import { routerReducer } from 'react-router-redux' 2 | import { combineReducers } from 'redux' 3 | 4 | import welcome from 'containers/Welcome/reducer' 5 | import chat from 'containers/Chat/reducer' 6 | export default () => { 7 | return combineReducers({ 8 | welcome, 9 | chat, 10 | routing: routerReducer 11 | }) 12 | } 13 | -------------------------------------------------------------------------------- /api/src/main/java/example/chat/Application.java: -------------------------------------------------------------------------------- 1 | package example.chat; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class Application { 8 | public static void main(String[] args) { 9 | SpringApplication.run(Application.class, args); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /api_Dockerfile: -------------------------------------------------------------------------------- 1 | FROM openjdk:8-jdk 2 | ENV SDKMAN_DIR /opt/sdkman 3 | RUN apt-get update -y 4 | RUN apt-get install zip unzip curl -y 5 | RUN curl -s "https://get.sdkman.io" | bash 6 | RUN chmod +x "$SDKMAN_DIR/bin/sdkman-init.sh" 7 | RUN /bin/bash -c "source $SDKMAN_DIR/bin/sdkman-init.sh && sdk install gradle 3.5 && sdk install springboot" 8 | RUN mkdir /etc/api 9 | WORKDIR /etc/api 10 | COPY api /etc/api 11 | RUN ./gradlew build 12 | -------------------------------------------------------------------------------- /front/src/containers/Chat/components/sendMessageButton.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react' 2 | import {Button} from 'elements' 3 | export class SendMessageButton extends Component { 4 | render () { 5 | return ( 6 | 12 | ) 13 | } 14 | } 15 | export default SendMessageButton 16 | -------------------------------------------------------------------------------- /api/src/main/java/example/chat/RegisterPayload.java: -------------------------------------------------------------------------------- 1 | package example.chat; 2 | 3 | public class RegisterPayload { 4 | 5 | private String name; 6 | 7 | public RegisterPayload() { 8 | } 9 | public RegisterPayload(String name) { 10 | this.name = name; 11 | } 12 | public String getName () { 13 | return name; 14 | } 15 | public void setName (String name) { 16 | this.name = name; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | --- 2 | services: 3 | front: 4 | build: 5 | context: ./ 6 | dockerfile: front_Dockerfile 7 | hostname: front 8 | ports: 9 | - "8080:8080" 10 | working_dir: /etc/front 11 | command: npm run start 12 | api: 13 | ports: 14 | - "3000:3000" 15 | build: 16 | context: ./ 17 | dockerfile: api_Dockerfile 18 | working_dir: /etc/api 19 | command: ./gradlew bootRun 20 | version: "3" 21 | -------------------------------------------------------------------------------- /front/src/containers/Chat/components/tests/userList.test.js: -------------------------------------------------------------------------------- 1 | import { mount } from 'enzyme' 2 | import React from 'react' 3 | import {UserList} from '../userList' 4 | 5 | describe('', () => { 6 | it('renders the number of users', () => { 7 | const mockUsers = ['foo', 'bar', 'baz'] 8 | const renderedComponent = mount() 9 | expect(renderedComponent.find('li').length).toEqual(mockUsers.length) 10 | }) 11 | }) 12 | -------------------------------------------------------------------------------- /api/src/main/java/example/chat/UserLeftMessage.java: -------------------------------------------------------------------------------- 1 | package example.chat; 2 | 3 | public class UserLeftMessage { 4 | private String userName; 5 | public UserLeftMessage() { 6 | } 7 | public UserLeftMessage(String userName) { 8 | this.userName = userName; 9 | } 10 | public String getUserName() { 11 | return userName; 12 | } 13 | public void setUserName(String userName) { 14 | this.userName = userName; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /front/src/containers/Chat/components/tests/sendMessageButton.test.js: -------------------------------------------------------------------------------- 1 | import { mount } from 'enzyme' 2 | import React from 'react' 3 | import {SendMessageButton} from '../sendMessageButton' 4 | 5 | describe('', () => { 6 | const mockClick = () => {} 7 | it('renders a button', () => { 8 | const renderedComponent = mount() 9 | expect(renderedComponent.find('button').length).toEqual(1) 10 | }) 11 | }) 12 | -------------------------------------------------------------------------------- /api/src/main/java/example/chat/UserEnterMessage.java: -------------------------------------------------------------------------------- 1 | package example.chat; 2 | 3 | public class UserEnterMessage { 4 | 5 | private String userName; 6 | public UserEnterMessage() { 7 | } 8 | public UserEnterMessage(String userName) { 9 | this.userName = userName; 10 | } 11 | public String getUserName() { 12 | return userName; 13 | } 14 | 15 | public void setUserName(String userName) { 16 | this.userName = userName; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /front/src/containers/Chat/components/messageInput.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react' 2 | import {Input} from 'elements' 3 | import styled from 'styled-components' 4 | const StyledInput = styled(Input)` 5 | margin-bottom: 0; 6 | ` 7 | export class MessageInput extends Component { 8 | render () { 9 | return ( 10 | 14 | ) 15 | } 16 | } 17 | export default MessageInput 18 | -------------------------------------------------------------------------------- /front/src/containers/Welcome/components/errorMessage.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react' 2 | import styled from 'styled-components' 3 | import cssVars from 'cssVars' 4 | 5 | const Error = styled.p` 6 | color: ${cssVars.errorColor}; 7 | font-weight: bold; 8 | ` 9 | export class ErrorMessage extends Component { 10 | render () { 11 | return ( 12 | 13 | {this.props.children} 14 | 15 | ) 16 | } 17 | } 18 | export default ErrorMessage 19 | -------------------------------------------------------------------------------- /front/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 'extends': ['standard', 'standard-jsx'], 3 | 'parser': 'babel-eslint', 4 | 'plugins': [ 5 | 'react' 6 | ], 7 | 'parserOptions': { 8 | 'ecmaVersion': 2017, 9 | 'ecmaFeatures': { 10 | 'impliedStrict': true, 11 | 'jsx': true 12 | } 13 | }, 14 | 'env': { 15 | 'browser': true, 16 | 'node': true, 17 | 'jest': true, 18 | 'es6': true 19 | }, 20 | 'rules': { 21 | 'prefer-const': 2 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /front/src/containers/Chat/actionTypes.js: -------------------------------------------------------------------------------- 1 | const prefix = 'chat' 2 | export const SOMEONE_ENTERED = `${prefix}_SOMEONE_ENTERED` 3 | export const SOMEONE_LEFT = `${prefix}_SOMEONE_LEFT` 4 | export const SEND_MESSAGE = `${prefix}_SEND_MESSAGE` 5 | export const RECEIVED_MESSAGE = `${prefix}_RECEIVED_MESSAGE` 6 | export const ENTER_CHAT_ROOM = `${prefix}_ENTER_CHAT_ROOM` 7 | 8 | export default { 9 | SOMEONE_ENTERED, 10 | RECEIVED_MESSAGE, 11 | SOMEONE_LEFT, 12 | SEND_MESSAGE, 13 | ENTER_CHAT_ROOM 14 | } 15 | -------------------------------------------------------------------------------- /docker-compose-dev.yml: -------------------------------------------------------------------------------- 1 | --- 2 | services: 3 | front: 4 | image: node:8.0.0 5 | hostname: front 6 | ports: 7 | - "8080:8080" 8 | volumes: 9 | - ./front:/etc/front 10 | working_dir: /etc/front 11 | command: "sleep infinity" 12 | api: 13 | ports: 14 | - "3000:3000" 15 | build: 16 | context: ./ 17 | dockerfile: api_dev_Dockerfile 18 | volumes: 19 | - ./api:/etc/api 20 | working_dir: /etc/api 21 | command: "sleep infinity" 22 | version: "3" 23 | -------------------------------------------------------------------------------- /front/src/elements/page.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react' 2 | import styled from 'styled-components' 3 | 4 | const PageContainer = styled.div` 5 | width: 80%; 6 | margin: 0 auto; 7 | padding: 2rem; 8 | position: relative; 9 | height: 100%; 10 | ` 11 | export class Page extends Component { 12 | render () { 13 | const {children, ...other} = this.props 14 | return ( 15 | 16 | {children} 17 | 18 | ) 19 | } 20 | } 21 | export default Page 22 | -------------------------------------------------------------------------------- /front/src/containers/Welcome/actions.js: -------------------------------------------------------------------------------- 1 | import types from './actionTypes' 2 | export function enterChat (userName) { 3 | if (userName === '') { 4 | return { 5 | type: types.EMPTY_USER_NAME, 6 | error: 'Please, pick an user name' 7 | } 8 | } 9 | return { 10 | type: types.ENTER_CHAT, 11 | userName 12 | } 13 | } 14 | export function userAlreadyExists (error) { 15 | return { 16 | type: types.USER_ALREADY_EXISTS, 17 | error 18 | } 19 | } 20 | export default { 21 | enterChat, 22 | userAlreadyExists 23 | } 24 | -------------------------------------------------------------------------------- /front/internals/webpackConfigs/js.js: -------------------------------------------------------------------------------- 1 | export const js = (paths) => { 2 | const cacheDir = (process.env.CACHE_DIRECTORY ? process.env.CACHE_DIRECTORY : 'true') 3 | return { 4 | module: { 5 | rules: [ 6 | { 7 | test: /\.jsx?$/, 8 | exclude: /(node_modules|bower_components)/, 9 | use: [ 10 | { 11 | loader: `babel-loader?cacheDirectory=${cacheDir}` 12 | } 13 | ], 14 | include: paths 15 | } 16 | ] 17 | } 18 | } 19 | } 20 | export default js 21 | -------------------------------------------------------------------------------- /front/internals/webpackConfigs/devServer.js: -------------------------------------------------------------------------------- 1 | import webpack from 'webpack' 2 | const devServer = (options) => { 3 | return { 4 | devServer: { 5 | hot: true, 6 | inline: true, 7 | stats: 'errors-only', 8 | host: options.host, 9 | port: options.port, 10 | historyApiFallback: true 11 | }, 12 | plugins: [ 13 | new webpack.HotModuleReplacementPlugin({ 14 | multiStep: true 15 | }), 16 | new webpack.NoEmitOnErrorsPlugin() 17 | ] 18 | } 19 | } 20 | export default devServer 21 | export {devServer} 22 | -------------------------------------------------------------------------------- /front/src/elements/title.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react' 2 | import styled from 'styled-components' 3 | import cssVars from 'cssVars' 4 | const TitleContainer = styled.h1` 5 | text-align: center; 6 | color: ${cssVars['titleColor']}; 7 | font-size: 2rem; 8 | margin: 0.3rem 0; 9 | ` 10 | export class Title extends Component { 11 | render () { 12 | const {children, ...other} = this.props 13 | return ( 14 | 15 | {children} 16 | 17 | ) 18 | } 19 | } 20 | export default Title 21 | -------------------------------------------------------------------------------- /front/src/containers/Welcome/components/tests/nameInput.test.jsx: -------------------------------------------------------------------------------- 1 | import { shallow, mount } from 'enzyme' 2 | import React from 'react' 3 | import {NameInput} from '../nameInput' 4 | 5 | describe('', () => { 6 | it('renders an input', () => { 7 | const renderedComponent = mount() 8 | expect(renderedComponent.find('input').length).toEqual(1) 9 | }) 10 | it('Disables if entering prop is passed ', () => { 11 | const renderedComponent = shallow() 12 | expect(renderedComponent.prop('disabled')).toEqual(true) 13 | }) 14 | }) 15 | -------------------------------------------------------------------------------- /api/src/main/java/example/chat/UserListenerConfig.java: -------------------------------------------------------------------------------- 1 | package example.chat; 2 | 3 | import org.springframework.context.annotation.Configuration; 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.messaging.simp.SimpMessagingTemplate; 7 | 8 | @Configuration 9 | class UserListenerConfig { 10 | @Bean 11 | public DisconnectionWatcher disconnectionWatcher(UserListService userList, SimpMessagingTemplate messagingTemplate) { 12 | return new DisconnectionWatcher(userList, messagingTemplate); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /front/src/containers/Welcome/components/nameInput.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react' 2 | import PropTypes from 'prop-types' 3 | import {Input} from 'elements' 4 | export class NameInput extends Component { 5 | static propTypes = { 6 | entering: PropTypes.bool 7 | } 8 | render () { 9 | const {entering, value, onChange, ...other} = this.props 10 | return ( 11 | 18 | ) 19 | } 20 | } 21 | export default NameInput 22 | -------------------------------------------------------------------------------- /front/src/containers/Welcome/index.jsx: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | import actions from './actions' 3 | import {WelcomePage} from './components' 4 | 5 | function mapStateToProps (state) { 6 | const myState = state.welcome 7 | return { 8 | entering: myState.get('entering'), 9 | error: myState.get('error') 10 | } 11 | } 12 | function mapDispatchToProps (dispatch, ownProps) { 13 | return { 14 | onEnterChat (name) { 15 | dispatch(actions.enterChat(name)) 16 | } 17 | } 18 | } 19 | export const Welcome = connect(mapStateToProps, mapDispatchToProps)(WelcomePage) 20 | export default Welcome 21 | -------------------------------------------------------------------------------- /front/src/containers/Chat/index.jsx: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | import {ChatPage} from './components' 3 | import actions from './actions' 4 | function mapStateToProps (state) { 5 | const myState = state.chat 6 | return { 7 | messages: myState.get('messagesList').toJS(), 8 | users: myState.get('userList').toJS() 9 | } 10 | } 11 | function mapDispatchToProps (dispatch, ownProps) { 12 | return { 13 | sendMessage (message) { 14 | dispatch(actions.sendMessage(message)) 15 | } 16 | } 17 | } 18 | export const Chat = connect(mapStateToProps, mapDispatchToProps)(ChatPage) 19 | export default Chat 20 | -------------------------------------------------------------------------------- /front/src/containers/Chat/components/tests/messageInput.test.js: -------------------------------------------------------------------------------- 1 | import { shallow, mount } from 'enzyme' 2 | import React from 'react' 3 | import {MessageInput} from '../messageInput' 4 | 5 | describe('', () => { 6 | it('renders an input', () => { 7 | const renderedComponent = mount() 8 | expect(renderedComponent.find('input').length).toEqual(1) 9 | }) 10 | it('rendered input displays the prop value', () => { 11 | const mockValue = 'mockValue' 12 | const renderedComponent = shallow() 13 | expect(renderedComponent.prop('value')).toEqual(mockValue) 14 | }) 15 | }) 16 | -------------------------------------------------------------------------------- /front/src/elements/card.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react' 2 | import styled from 'styled-components' 3 | import cssVars from 'cssVars' 4 | const Container = styled.div` 5 | width: 100%; 6 | text-align: center; 7 | height: auto; 8 | position: relative; 9 | padding: 1rem 0.5rem; 10 | box-shadow: 0 2px 4px -2px ${cssVars.shadowColor}; 11 | background-color: ${cssVars.cardBgColor} 12 | ` 13 | export class Card extends Component { 14 | render () { 15 | const {children, ...other} = this.props 16 | return ( 17 | 18 | {children} 19 | 20 | ) 21 | } 22 | } 23 | export default Card 24 | -------------------------------------------------------------------------------- /api/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | mavenCentral() 4 | } 5 | dependencies { 6 | classpath("org.springframework.boot:spring-boot-gradle-plugin:1.5.2.RELEASE") 7 | } 8 | } 9 | 10 | apply plugin: 'java' 11 | apply plugin: 'org.springframework.boot' 12 | 13 | jar { 14 | baseName = 'chat_example' 15 | version = '0.1.0' 16 | } 17 | sourceCompatibility = 1.8 18 | targetCompatibility = 1.8 19 | 20 | repositories { 21 | mavenCentral() 22 | } 23 | 24 | dependencies { 25 | compile("org.springframework.boot:spring-boot-starter-websocket") 26 | compile("org.springframework.boot:spring-boot-starter-integration") 27 | } 28 | -------------------------------------------------------------------------------- /front/src/utils/getUserColorByName.js: -------------------------------------------------------------------------------- 1 | import cssVars from 'cssVars' 2 | const colorMap = {} 3 | const totalColors = cssVars.userColors.length 4 | export function getUserColorByName (userName) { 5 | if (colorMap.hasOwnProperty(userName)) { 6 | console.log('Has own property!') 7 | return cssVars.userColors[colorMap[userName]] 8 | } 9 | const charQuantity = userName.length 10 | let colorIndex = userName.charCodeAt(0) * charQuantity 11 | for (let i = 0; i < Math.min(4, charQuantity - 1); i++) { 12 | colorIndex = userName.charCodeAt(i) 13 | } 14 | colorIndex = colorIndex % totalColors 15 | const color = cssVars.userColors[colorIndex] 16 | colorMap[userName] = colorIndex 17 | return color 18 | } 19 | export default getUserColorByName 20 | -------------------------------------------------------------------------------- /front/src/containers/Chat/components/tests/message.test.js: -------------------------------------------------------------------------------- 1 | import { mount } from 'enzyme' 2 | import React from 'react' 3 | import {Message} from '../message' 4 | 5 | describe('', () => { 6 | it('renders an div with the internal content', () => { 7 | const mockContent = 'mockContent' 8 | const renderedComponent = mount({mockContent}) 9 | expect(renderedComponent.text()).toEqual(mockContent) 10 | }) 11 | it('renders the username on a span if present', () => { 12 | const mockContent = 'mockContent' 13 | const mockName = 'mockName' 14 | const renderedComponent = mount({mockContent}) 15 | expect(renderedComponent.find('span').first().text()).toEqual(mockName) 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /api/src/main/java/example/chat/Message.java: -------------------------------------------------------------------------------- 1 | package example.chat; 2 | 3 | public class Message { 4 | 5 | private String senderName; 6 | private String message; 7 | public Message() { 8 | } 9 | public Message(String message) { 10 | this.message = message; 11 | } 12 | 13 | public Message(String senderName, String message) { 14 | this.senderName = senderName; 15 | this.message = message; 16 | } 17 | public String getSenderName() { 18 | return senderName; 19 | } 20 | 21 | public void setSenderName(String senderName) { 22 | this.senderName = senderName; 23 | } 24 | public String getMessage () { 25 | return message; 26 | } 27 | public void setMessage (String message) { 28 | this.message = message; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /front/src/containers/Chat/tests/connectedComponent.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import configureStore from 'redux-mock-store' 3 | import { shallow } from 'enzyme' 4 | import {Chat} from '../index' 5 | import {Map, List, Set} from 'immutable' 6 | describe(' - Connected page', () => { 7 | const mockStore = configureStore() 8 | it('Renders and pass down the appropriate properties', () => { 9 | const mockMessages = ['foo', 'bar', 'baz'] 10 | const initialState = { 11 | chat: new Map({ 12 | messagesList: new List(mockMessages), 13 | userList: new Set() 14 | }) 15 | } 16 | const store = mockStore(initialState) 17 | const renderedComponent = shallow() 18 | expect(renderedComponent.prop('messages')).toEqual(mockMessages) 19 | }) 20 | }) 21 | -------------------------------------------------------------------------------- /front/src/containers/Welcome/components/enterButton.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react' 2 | import PropTypes from 'prop-types' 3 | import {Button} from 'elements' 4 | import styled from 'styled-components' 5 | const StyledButton = styled(Button)` 6 | padding-left:2em; 7 | padding-right:2em; 8 | font-size: 1.5rem; 9 | margin-top: 2rem; 10 | ` 11 | export class EnterButton extends Component { 12 | static propTypes = { 13 | entering: PropTypes.bool, 14 | onClick: PropTypes.func.isRequired 15 | } 16 | render () { 17 | const {entering} = this.props 18 | const label = (entering ? 'Entering ...' : 'Enter') 19 | return ( 20 | 21 | {label} 22 | 23 | ) 24 | } 25 | } 26 | export default EnterButton 27 | -------------------------------------------------------------------------------- /front/src/containers/Welcome/tests/connectedComponent.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import configureStore from 'redux-mock-store' 3 | import { shallow } from 'enzyme' 4 | import {Welcome} from '../index' 5 | import {Map} from 'immutable' 6 | describe(' - Connected page', () => { 7 | const mockStore = configureStore() 8 | it('Renders and pass down the appropriate properties', () => { 9 | const initialState = { 10 | welcome: new Map({ 11 | entering: true, 12 | error: 'mockError' 13 | }) 14 | } 15 | const store = mockStore(initialState) 16 | const renderedComponent = shallow() 17 | expect(renderedComponent.prop('entering')).toEqual(initialState.welcome.get('entering')) 18 | expect(renderedComponent.prop('error')).toEqual(initialState.welcome.get('error')) 19 | }) 20 | }) 21 | -------------------------------------------------------------------------------- /front/src/cssVars.js: -------------------------------------------------------------------------------- 1 | export default { 2 | bodyBgColor: '#F0F0F0', 3 | cardBgColor: 'white', 4 | inputBorderColor: '#90CAF9', 5 | inputBorderColorFocusColor: '#2196F3', 6 | inputBorderDisabledColor: '#757575', 7 | buttonBgColor: '#00BCD4', 8 | buttonColor: '#FFF', 9 | buttonBgColorHover: '#4DD0E1', 10 | buttonBgColorPressed: '#006064', 11 | buttonBgColorDisabled: '#9E9E9E', 12 | buttonColorHover: '#FAFAFA', 13 | shadowColor: '#424242', 14 | titleColor: '#40C4FF', 15 | borderColor: '#757575', 16 | systemMessagesColor: '#FFF', 17 | systemMessagesBgColor: '#009688', 18 | errorColor: '#D50000', 19 | userColors: [ 20 | '#FFCDD2', 21 | '#F8BBD0', 22 | '#E1BEE7', 23 | '#B39DDB', 24 | '#BBDEFB', 25 | '#4DD0E1', 26 | '#C8E6C9', 27 | '#9CCC65', 28 | '#FFCC80', 29 | '#B0BEC5', 30 | '#FFAB91' 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /api/src/main/java/example/chat/ChatRoomController.java: -------------------------------------------------------------------------------- 1 | package example.chat; 2 | import org.springframework.beans.factory.annotation.Autowired; 3 | 4 | import org.springframework.messaging.handler.annotation.MessageMapping; 5 | import org.springframework.messaging.handler.annotation.SendTo; 6 | import org.springframework.stereotype.Controller; 7 | import org.springframework.messaging.simp.stomp.StompHeaderAccessor; 8 | 9 | @Controller 10 | public class ChatRoomController { 11 | @Autowired UserListService userList; 12 | 13 | @MessageMapping("/chat") 14 | @SendTo("/topic/messages") 15 | public Message chat(Message message, StompHeaderAccessor accessor) throws Exception { 16 | String sessionId = accessor.getSessionId(); 17 | String senderName = userList.getUserName(sessionId); 18 | message.setSenderName(senderName); 19 | return message; 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /front/src/containers/Chat/actions.js: -------------------------------------------------------------------------------- 1 | import types from './actionTypes' 2 | 3 | export function someoneEntered (userName) { 4 | return { 5 | type: types.SOMEONE_ENTERED, 6 | userName 7 | } 8 | } 9 | export function someoneLeft (userName) { 10 | return { 11 | type: types.SOMEONE_LEFT, 12 | userName 13 | } 14 | } 15 | export function receivedMessage (message, senderName) { 16 | return { 17 | type: types.RECEIVED_MESSAGE, 18 | message, 19 | senderName 20 | } 21 | } 22 | export function sendMessage (message) { 23 | return { 24 | type: types.SEND_MESSAGE, 25 | message 26 | } 27 | } 28 | export function enterChatRoom (welcomeMessage, usersList) { 29 | return { 30 | type: types.ENTER_CHAT_ROOM, 31 | welcomeMessage, 32 | usersList 33 | } 34 | } 35 | export default { 36 | someoneEntered, 37 | someoneLeft, 38 | receivedMessage, 39 | sendMessage 40 | } 41 | -------------------------------------------------------------------------------- /front/internals/webpackConfigs/html.js: -------------------------------------------------------------------------------- 1 | import HtmlWebpackTemplate from 'html-webpack-template' 2 | import HtmlWebpackPlugin from 'html-webpack-plugin' 3 | 4 | const html = (options) => { 5 | const {title, appMountId = 'app'} = options 6 | let devServer = null 7 | if (options.isDevelopment) { 8 | devServer = options.port 9 | } 10 | return { 11 | plugins: [ 12 | new HtmlWebpackPlugin({ 13 | title, 14 | template: HtmlWebpackTemplate, 15 | appMountId, 16 | inject: false, 17 | devServer, 18 | publicPath: '/', 19 | link: [ 20 | 'https://fonts.googleapis.com/css?family=Montserrat' 21 | ], 22 | scripts: [ 23 | 'https://cdnjs.cloudflare.com/ajax/libs/stomp.js/2.3.3/stomp.min.js', 24 | 'https://fonts.googleapis.com/css?family=Julius+Sans+One' 25 | ] 26 | }) 27 | ] 28 | } 29 | } 30 | export default html 31 | export {html} 32 | -------------------------------------------------------------------------------- /front/src/containers/Welcome/components/tests/enterButton.test.jsx: -------------------------------------------------------------------------------- 1 | import { mount } from 'enzyme' 2 | import React from 'react' 3 | import {EnterButton} from '../enterButton' 4 | 5 | describe('', () => { 6 | const mockClick = () => {} 7 | it('renders a button', () => { 8 | const renderedComponent = mount() 9 | expect(renderedComponent.find('button').length).toEqual(1) 10 | }) 11 | it('Displays enter if no props is passed ', () => { 12 | const renderedComponent = mount() 13 | expect(renderedComponent.text()).toEqual('Enter') 14 | }) 15 | it('Displays "Entering ..." and disables if entering prop is passed ', () => { 16 | const renderedComponent = mount() 17 | expect(renderedComponent.text()).toEqual('Entering ...') 18 | expect(renderedComponent.find('button').prop('disabled')).toEqual(true) 19 | }) 20 | }) 21 | -------------------------------------------------------------------------------- /front/src/containers/Welcome/reducer.js: -------------------------------------------------------------------------------- 1 | import { Map } from 'immutable' 2 | import types from './actionTypes' 3 | import {LOCATION_CHANGE} from 'react-router-redux' 4 | 5 | export const initial = new Map({ 6 | entering: false, 7 | success: null 8 | }) 9 | function welcomeReducer (state = initial, action) { 10 | switch (action.type) { 11 | case types.ENTER_CHAT: 12 | return state.withMutations((state) => { 13 | return state.set('userName', action.userName) 14 | .set('entering', true) 15 | .delete('error') 16 | }) 17 | case types.USER_ALREADY_EXISTS: 18 | return state.withMutations((state) => { 19 | return state.set('entering', false) 20 | .set('error', action.error) 21 | }) 22 | case types.EMPTY_USER_NAME: 23 | return state.set('error', action.error) 24 | case LOCATION_CHANGE: 25 | return initial 26 | default: 27 | return state 28 | } 29 | } 30 | export default welcomeReducer 31 | -------------------------------------------------------------------------------- /front/src/containers/Chat/components/messageArea.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react' 2 | import PropTypes from 'prop-types' 3 | import {Message} from './message' 4 | import styled from 'styled-components' 5 | const Container = styled.div` 6 | font-family: 'Julius Sans One', sans-serif; 7 | height: 100%; 8 | overflow-y: auto; 9 | padding-bottom: 2rem; 10 | 11 | ` 12 | export class MessageArea extends Component { 13 | static propTypes = { 14 | messages: PropTypes.array 15 | } 16 | static defaultProps = { 17 | messages: [] 18 | } 19 | render () { 20 | let index = 0 21 | return ( 22 | 23 | {this.props.messages.map((message) => { 24 | const key = `message_${index++}` 25 | const {body, ...other} = message 26 | return ( 27 | 28 | {body} 29 | 30 | ) 31 | })} 32 | 33 | ) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /front/src/containers/Welcome/components/welcomePage.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react' 2 | import {EnterForm} from './enterForm' 3 | import PropTypes from 'prop-types' 4 | import {ErrorMessage} from './errorMessage' 5 | import {Title, Card} from 'elements' 6 | 7 | export class WelcomePage extends Component { 8 | static propTypes = { 9 | onEnterChat: PropTypes.func.isRequired, 10 | entering: PropTypes.bool, 11 | error: PropTypes.string 12 | } 13 | render () { 14 | let errorElement = null 15 | if (this.props.error) { 16 | errorElement = ( 17 | 18 | {this.props.error} 19 | 20 | ) 21 | } 22 | return ( 23 | 24 | Welcome to the chat example 25 | {errorElement} 26 | 30 | 31 | ) 32 | } 33 | } 34 | export default WelcomePage 35 | -------------------------------------------------------------------------------- /front/src/elements/input.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react' 2 | import styled from 'styled-components' 3 | import cssVars from 'cssVars' 4 | 5 | const StyledInput = styled.input` 6 | background-color: inherit; 7 | display: block; 8 | width: 100%; 9 | border: 0; 10 | font-size: 1.3rem; 11 | color: black; 12 | border-bottom: 2px solid ${cssVars.inputBorderColor}; 13 | margin-bottom: 0.5em; 14 | padding-bottom: 0.5em; 15 | font-family: 'Julius Sans One', sans-serif; 16 | &:focus { 17 | outline: none; 18 | border-bottom: 2px solid ${cssVars.inputBorderColorFocusColor}; 19 | } 20 | &:disabled { 21 | border-bottom: 2px solid ${cssVars.inputBorderDisabledColor}; 22 | background-color: inherit; 23 | } 24 | ` 25 | export class Input extends Component { 26 | render () { 27 | const {children, ...other} = this.props 28 | return ( 29 | 30 | {children} 31 | 32 | ) 33 | } 34 | } 35 | export default Input 36 | -------------------------------------------------------------------------------- /api/src/main/java/example/chat/DisconnectionWatcher.java: -------------------------------------------------------------------------------- 1 | package example.chat; 2 | import org.springframework.stereotype.Service; 3 | import org.springframework.messaging.simp.SimpMessagingTemplate; 4 | import org.springframework.context.event.EventListener; 5 | import org.springframework.web.socket.messaging.SessionDisconnectEvent; 6 | 7 | public class DisconnectionWatcher { 8 | private SimpMessagingTemplate messagingTemplate; 9 | 10 | private UserListService userList; 11 | DisconnectionWatcher(UserListService userList, SimpMessagingTemplate messagingTemplate) { 12 | this.userList = userList; 13 | this.messagingTemplate = messagingTemplate; 14 | } 15 | @EventListener 16 | private void handleSessionDisconnect(SessionDisconnectEvent event) { 17 | String sessionId = event.getSessionId(); 18 | String userName = this.userList.getUserName(sessionId); 19 | userList.removeFromlist(sessionId); 20 | this.messagingTemplate.convertAndSend("/topic/userLeft", new UserLeftMessage(userName)); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /front/src/containers/Welcome/tests/actions.test.js: -------------------------------------------------------------------------------- 1 | import types from '../actionTypes' 2 | import actions from '../actions' 3 | describe('Welcome container Actions', () => { 4 | it('enterChat returns ENTER_CHAT with userName data', () => { 5 | const mockName = 'mockName' 6 | const expectedResult = { 7 | type: types.ENTER_CHAT, 8 | userName: mockName 9 | } 10 | expect(actions.enterChat(mockName)) 11 | .toEqual(expectedResult) 12 | }) 13 | it('user already exists returns USER_ALREADY_EXISTS', () => { 14 | const mockError = 'The user already exists' 15 | const expectedResult = { 16 | type: types.USER_ALREADY_EXISTS, 17 | error: mockError 18 | } 19 | expect(actions.userAlreadyExists(mockError)) 20 | .toEqual(expectedResult) 21 | }) 22 | it('Returns EMPTY_USER_NAME with error if userName is empty on enterChat', () => { 23 | const result = actions.enterChat('') 24 | expect(result.type).toEqual(types.EMPTY_USER_NAME) 25 | expect(result.error).toBeDefined() 26 | }) 27 | }) 28 | -------------------------------------------------------------------------------- /front/src/elements/button.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react' 2 | import styled from 'styled-components' 3 | import cssVars from 'cssVars' 4 | 5 | const StyledButton = styled.button` 6 | border: 0; 7 | border-radius: 5px; 8 | background-color: ${cssVars['buttonBgColor']}; 9 | color: ${cssVars['buttonColor']}; 10 | font-size: 1rem; 11 | padding: 0.5em; 12 | font-weight: bold; 13 | outline: 0; 14 | &:hover { 15 | color: ${cssVars['buttonColorHover']}; 16 | cursor: pointer; 17 | background-color: ${cssVars['buttonBgColorHover']}; 18 | } 19 | &:active,&:hover:active{ 20 | background-color: ${cssVars['buttonBgColorPressed']}; 21 | } 22 | &:disabled,&:hover:disabled{ 23 | cursor: auto; 24 | background-color: ${cssVars['buttonBgColorDisabled']}; 25 | } 26 | ` 27 | export class Button extends Component { 28 | render () { 29 | const {children, ...other} = this.props 30 | return ( 31 | 32 | {children} 33 | 34 | ) 35 | } 36 | } 37 | export default Button 38 | -------------------------------------------------------------------------------- /front/src/containers/Chat/components/tests/messageArea.test.js: -------------------------------------------------------------------------------- 1 | import { shallow } from 'enzyme' 2 | import React from 'react' 3 | import {MessageArea} from '../messageArea' 4 | import {Message} from '../message' 5 | 6 | describe('', () => { 7 | it('renders an the number of passed messages', () => { 8 | const mockMessages = [{body: 'foo'}, {body: 'bar'}, {body: 'baz'}] 9 | const renderedComponent = shallow() 10 | expect(renderedComponent.find(Message).length).toEqual(mockMessages.length) 11 | }) 12 | it('Pass the message body as the inner value and the rest as props', () => { 13 | const mockMessages = [{body: 'foo', senderName: 'baz'}] 14 | const renderedComponent = shallow() 15 | const renderedMessage = renderedComponent.find(Message) 16 | const {body, ...other} = mockMessages[0] 17 | expect(renderedMessage.prop('children')).toEqual(body) 18 | for (const propName in other) { 19 | expect(renderedMessage.prop(propName)).toEqual(other[propName]) 20 | } 21 | }) 22 | }) 23 | -------------------------------------------------------------------------------- /front/src/containers/Welcome/saga.js: -------------------------------------------------------------------------------- 1 | import { call, put, select, takeLatest } from 'redux-saga/effects' 2 | import * as types from './actionTypes' 3 | import * as actions from './actions' 4 | import {socketConnection} from 'utils/socketConnection' 5 | import { push } from 'react-router-redux' 6 | import * as chatActions from '../Chat/actions' 7 | 8 | const makeSelectWelcome = (state) => state.welcome 9 | export function * handleChatEnter () { 10 | const state = yield select(makeSelectWelcome) 11 | const userName = state.get('userName') 12 | try { 13 | const response = yield call(socketConnection.connectWithName.bind(socketConnection), userName) 14 | if (response.entered) { 15 | yield put(chatActions.enterChatRoom(response.successMessage, response.userList)) 16 | yield put(push('/chat')) 17 | } else { 18 | yield put(actions.userAlreadyExists(response.errorMessage)) 19 | } 20 | } catch (err) { 21 | console.error('Something went wrong', err) 22 | } 23 | } 24 | export function * welcomeSaga () { 25 | yield takeLatest(types.ENTER_CHAT, handleChatEnter) 26 | } 27 | export default welcomeSaga 28 | -------------------------------------------------------------------------------- /api/src/main/java/example/chat/UserListService.java: -------------------------------------------------------------------------------- 1 | package example.chat; 2 | import java.util.Map; 3 | import java.util.HashMap; 4 | import java.util.ArrayList; 5 | import org.springframework.stereotype.Service; 6 | 7 | @Service 8 | public class UserListService { 9 | private final Map sessionIdNameMap = new HashMap(); 10 | 11 | public boolean addToList(String sessionId, RegisterPayload payload) { 12 | String name = payload.getName(); 13 | if (sessionIdNameMap.containsValue(name)) { 14 | if (sessionIdNameMap.containsKey(sessionId)){ 15 | if (getUserName(sessionId) != name) { 16 | return false; 17 | } 18 | } else { 19 | return false; 20 | } 21 | } 22 | sessionIdNameMap.put(sessionId, name); 23 | return true; 24 | } 25 | public void removeFromlist(String sessionId) { 26 | sessionIdNameMap.remove(sessionId); 27 | } 28 | public String getUserName (String sessionId) { 29 | return sessionIdNameMap.get(sessionId); 30 | } 31 | public ArrayList getUserList() { 32 | return new ArrayList(sessionIdNameMap.values()); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /api/src/main/java/example/chat/RegisterController.java: -------------------------------------------------------------------------------- 1 | package example.chat; 2 | import org.springframework.beans.factory.annotation.Autowired; 3 | 4 | import org.springframework.messaging.handler.annotation.MessageMapping; 5 | import org.springframework.messaging.simp.annotation.SendToUser; 6 | import org.springframework.stereotype.Controller; 7 | import org.springframework.messaging.simp.stomp.StompHeaderAccessor; 8 | import org.springframework.messaging.simp.SimpMessagingTemplate; 9 | 10 | @Controller 11 | public class RegisterController { 12 | @Autowired UserListService userList; 13 | @Autowired private SimpMessagingTemplate webSocket; 14 | @MessageMapping("/register") 15 | @SendToUser("/topic/registerResponse") 16 | public RegisterResponse register(RegisterPayload payload, StompHeaderAccessor accessor) throws Exception { 17 | String sessionId = accessor.getSessionId(); 18 | if (userList.addToList(sessionId, payload)) { 19 | webSocket.convertAndSend("/topic/userEnter", new UserEnterMessage(payload.getName())); 20 | return new RegisterResponse(true, payload, userList.getUserList()); 21 | } 22 | return new RegisterResponse(false, payload); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /front/src/containers/Welcome/components/enterForm.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react' 2 | import PropTypes from 'prop-types' 3 | import {NameInput} from './nameInput' 4 | import {EnterButton} from './enterButton' 5 | import {checkEnter} from 'utils/checkEnter' 6 | export class EnterForm extends Component { 7 | static propTypes = { 8 | entering: PropTypes.bool, 9 | onEnterChat: PropTypes.func 10 | } 11 | state = { 12 | chatName: '' 13 | } 14 | onEnterButton = () => { 15 | this.props.onEnterChat(this.state.chatName.trim()) 16 | } 17 | handleChange = (ev) => { 18 | const chatName = ev.target.value 19 | this.setState({chatName}) 20 | } 21 | onCheckKey = (e) => { 22 | if (checkEnter(e)) { 23 | this.onEnterButton() 24 | } 25 | } 26 | render () { 27 | return ( 28 |
29 | 35 | 36 |
37 | ) 38 | } 39 | } 40 | export default EnterForm 41 | -------------------------------------------------------------------------------- /api/src/main/java/example/chat/RegisterResponse.java: -------------------------------------------------------------------------------- 1 | package example.chat; 2 | import java.util.ArrayList; 3 | public class RegisterResponse { 4 | 5 | private String userName; 6 | private boolean entered; 7 | private ArrayList userList; 8 | 9 | public RegisterResponse(boolean entered, RegisterPayload payload) { 10 | this.entered = entered; 11 | this.userName = payload.getName(); 12 | } 13 | public RegisterResponse(boolean entered, RegisterPayload payload, ArrayList userList) { 14 | this.entered = entered; 15 | this.userName = payload.getName(); 16 | this.userList = userList; 17 | } 18 | public String getSuccessMessage() { 19 | if (!this.entered) { 20 | return ""; 21 | } 22 | return "Hello " + userName + "! Glad you are here"; 23 | } 24 | public String getErrorMessage() { 25 | if (this.entered) { 26 | return ""; 27 | } 28 | return "The userName " + userName + " is already taken, choose another"; 29 | } 30 | public boolean getEntered() { 31 | return this.entered; 32 | } 33 | public ArrayList getUserList() { 34 | return this.userList; 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /api/src/main/java/example/chat/WebSocketConfig.java: -------------------------------------------------------------------------------- 1 | package example.chat; 2 | 3 | import org.springframework.context.annotation.Configuration; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.messaging.simp.config.MessageBrokerRegistry; 6 | import org.springframework.web.socket.config.annotation.AbstractWebSocketMessageBrokerConfigurer; 7 | import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; 8 | import org.springframework.web.socket.config.annotation.StompEndpointRegistry; 9 | import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry; 10 | import org.springframework.messaging.simp.config.ChannelRegistration; 11 | 12 | @Configuration 13 | @EnableWebSocketMessageBroker 14 | public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer { 15 | @Autowired UserListService userList; 16 | 17 | @Override 18 | public void configureMessageBroker(MessageBrokerRegistry config) { 19 | config.enableSimpleBroker("/topic"); 20 | config.setApplicationDestinationPrefixes("/api"); 21 | config.setUserDestinationPrefix("/user"); 22 | } 23 | 24 | @Override 25 | public void registerStompEndpoints(StompEndpointRegistry registry) { 26 | registry.addEndpoint("/chatExample").setAllowedOrigins("*").withSockJS(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /front/src/containers/Welcome/components/tests/welcomePage.test.jsx: -------------------------------------------------------------------------------- 1 | import { shallow, mount } from 'enzyme' 2 | import React from 'react' 3 | import {WelcomePage} from '../welcomePage' 4 | import {EnterForm} from '../enterForm' 5 | import {ErrorMessage} from '../errorMessage' 6 | 7 | describe('', () => { 8 | const mockFunc = () => {} 9 | it('renders a form', () => { 10 | const renderedComponent = shallow() 11 | expect(renderedComponent.first(EnterForm)).toBeDefined() 12 | }) 13 | it('renders a Form passing down the onEnterChat and entering props', () => { 14 | const renderedComponent = shallow() 15 | const form = renderedComponent.find(EnterForm) 16 | expect(form.prop('entering')).toEqual(true) 17 | expect(form.prop('onEnterChat')).toEqual(mockFunc) 18 | }) 19 | it('Diplays an error if error is passed while still displaying the enterForm', () => { 20 | const mockError = 'mockError' 21 | const renderedComponent = mount() 22 | expect(renderedComponent.first(EnterForm)).toBeDefined() 23 | const errorMessage = renderedComponent.find(ErrorMessage) 24 | expect(errorMessage).toBeDefined() 25 | expect(errorMessage.first().text()).toEqual(mockError) 26 | }) 27 | }) 28 | -------------------------------------------------------------------------------- /front/internals/webpackConfigs/base.js: -------------------------------------------------------------------------------- 1 | import htmlPartial from './webpackConfigs/html' 2 | import jsPartial from './webpackConfigs/js' 3 | import devServerPartial from './webpackConfigs/devServer' 4 | import merge from 'webpack-merge' 5 | import path from 'path' 6 | const PATHS = { 7 | app: path.join(__dirname, '../src'), 8 | build: path.join(__dirname, '../build') 9 | } 10 | const common = { 11 | entry: { 12 | app: path.resolve(PATHS.app, '../src/index.jsx') 13 | }, 14 | output: { 15 | path: PATHS.build, 16 | filename: 'app.js', 17 | publicPath: '/' 18 | }, 19 | resolve: { 20 | modules: ['src', 'node_modules'], 21 | extensions: ['.js', '.jsx', '.json'] 22 | } 23 | } 24 | const htmlOptions = {title: 'chat'} 25 | let envConfig = {} 26 | switch (process.env.npm_lifecycle_event) { 27 | case 'start': 28 | const port = process.env.PORT || '8080' 29 | const host = process.env.HOST || '0.0.0.0' 30 | htmlOptions.devServer = process.env.PORT 31 | htmlOptions.isDevelopment = true 32 | envConfig = merge( 33 | devServerPartial({ 34 | host, 35 | port 36 | }), 37 | { devtool: 'eval-cheap-module-source-map' } 38 | ) 39 | break 40 | } 41 | const config = merge( 42 | htmlPartial(htmlOptions), 43 | jsPartial(PATHS.app), 44 | envConfig, 45 | common 46 | ) 47 | export default config 48 | -------------------------------------------------------------------------------- /front/internals/webpack.config.babel.js: -------------------------------------------------------------------------------- 1 | import htmlPartial from './webpackConfigs/html' 2 | import jsPartial from './webpackConfigs/js' 3 | import devServerPartial from './webpackConfigs/devServer' 4 | import merge from 'webpack-merge' 5 | import path from 'path' 6 | 7 | const PATHS = { 8 | app: path.join(__dirname, '../src'), 9 | build: path.join(__dirname, '../build') 10 | } 11 | const common = { 12 | entry: { 13 | app: ['babel-polyfill', path.resolve(PATHS.app, '../src/index.jsx')] 14 | }, 15 | output: { 16 | path: PATHS.build, 17 | filename: 'app.js', 18 | publicPath: '/' 19 | }, 20 | resolve: { 21 | modules: ['src', 'node_modules'], 22 | extensions: ['.js', '.jsx', '.json'] 23 | } 24 | } 25 | const htmlOptions = {title: 'chat example'} 26 | let envConfig = {} 27 | switch (process.env.npm_lifecycle_event) { 28 | case 'start': 29 | const port = process.env.PORT || '8080' 30 | const host = process.env.HOST || '0.0.0.0' 31 | htmlOptions.devServer = process.env.PORT 32 | htmlOptions.isDevelopment = true 33 | envConfig = merge( 34 | devServerPartial({ 35 | host, 36 | port 37 | }), 38 | { devtool: 'eval-cheap-module-source-map' } 39 | ) 40 | break 41 | } 42 | const config = merge( 43 | htmlPartial(htmlOptions), 44 | jsPartial(PATHS.app), 45 | envConfig, 46 | common 47 | ) 48 | export default config 49 | -------------------------------------------------------------------------------- /front/src/containers/Chat/components/tests/chatPage.test.js: -------------------------------------------------------------------------------- 1 | 2 | import { shallow, mount } from 'enzyme' 3 | import React from 'react' 4 | import { 5 | ChatPage, 6 | MessageForm, 7 | MessageArea, 8 | UserList 9 | } from '../' 10 | import sinon from 'sinon' 11 | 12 | describe('', () => { 13 | it('renders the messageForm, UserList and messageArea', () => { 14 | const renderedComponent = mount() 15 | expect(renderedComponent.find(MessageArea).length).toEqual(1) 16 | expect(renderedComponent.find(MessageForm).length).toEqual(1) 17 | expect(renderedComponent.find(UserList).length).toEqual(1) 18 | }) 19 | it('Passs down the messaged to MessageArea', () => { 20 | const mockMessages = ['foo', 'bar', 'baz'] 21 | const renderedComponent = shallow() 22 | expect(renderedComponent.find(MessageArea).prop('messages')).toEqual(mockMessages) 23 | }) 24 | it('Passs down sendMessage to MessageForm', () => { 25 | const sendMessageSpy = sinon.spy() 26 | const renderedComponent = mount() 27 | expect(renderedComponent.find(MessageForm).prop('sendMessage')).toEqual(sendMessageSpy) 28 | }) 29 | it('Passs down users to userList', () => { 30 | const mockUsers = ['foo', 'bar', 'baz'] 31 | const renderedComponent = mount() 32 | expect(renderedComponent.find(UserList).prop('users')).toEqual(mockUsers) 33 | }) 34 | }) 35 | -------------------------------------------------------------------------------- /front/src/containers/Chat/tests/actions.test.js: -------------------------------------------------------------------------------- 1 | import types from '../actionTypes' 2 | import actions from '../actions' 3 | describe('Chat container Actions', () => { 4 | it('someoneEntered returns who entered', () => { 5 | const mockUsername = 'mockUsername' 6 | const expectedResult = { 7 | type: types.SOMEONE_ENTERED, 8 | userName: mockUsername 9 | } 10 | expect(actions.someoneEntered(mockUsername)) 11 | .toEqual(expectedResult) 12 | }) 13 | it('someoneLeft returns who left', () => { 14 | const mockUsername = 'mockUsername' 15 | const expectedResult = { 16 | type: types.SOMEONE_LEFT, 17 | userName: mockUsername 18 | } 19 | expect(actions.someoneLeft(mockUsername)) 20 | .toEqual(expectedResult) 21 | }) 22 | it('receivedMessage returns what message and who is from', () => { 23 | const mockUsername = 'mockUsername' 24 | const mockMessage = 'mockMessage' 25 | const expectedResult = { 26 | type: types.RECEIVED_MESSAGE, 27 | senderName: mockUsername, 28 | message: mockMessage 29 | } 30 | expect(actions.receivedMessage(mockMessage, mockUsername)) 31 | .toEqual(expectedResult) 32 | }) 33 | it('sendMessage returns what message', () => { 34 | const mockMessage = 'mockMessage' 35 | const expectedResult = { 36 | type: types.SEND_MESSAGE, 37 | message: mockMessage 38 | } 39 | expect(actions.sendMessage(mockMessage)) 40 | .toEqual(expectedResult) 41 | }) 42 | }) 43 | -------------------------------------------------------------------------------- /front/src/containers/Chat/components/message.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react' 2 | import PropTypes from 'prop-types' 3 | import styled from 'styled-components' 4 | import getUserColorByName from 'utils/getUserColorByName' 5 | import cssVars from 'cssVars' 6 | const SenderName = styled.span` 7 | font-weight: bold; 8 | margin-right: 1rem; 9 | &::after { 10 | content: " - "; 11 | } 12 | ` 13 | const Container = styled.div` 14 | border-radius: 10px; 15 | padding: 0.2rem 0.4rem; 16 | font-size: 1.2rem; 17 | border-color: currentColor; 18 | border-width: 2px; 19 | border-style: solid; 20 | margin-bottom: 0.2rem; 21 | ` 22 | export class Message extends Component { 23 | static propTypes = { 24 | senderName: PropTypes.string, 25 | isJoin: PropTypes.bool, 26 | isLeft: PropTypes.bool 27 | } 28 | render () { 29 | let senderElement = null 30 | if (this.props.senderName) { 31 | senderElement = ( 32 | 33 | {this.props.senderName} 34 | 35 | ) 36 | } 37 | const style = {} 38 | let color = 'teal' 39 | if (this.props.senderName) { 40 | color = getUserColorByName(this.props.senderName) 41 | style.color = color 42 | } else { 43 | style.color = cssVars.systemMessagesColor 44 | style.backgroundColor = cssVars.systemMessagesBgColor 45 | } 46 | return ( 47 | 48 | {senderElement} 49 | {this.props.children} 50 | 51 | ) 52 | } 53 | } 54 | export default Message 55 | -------------------------------------------------------------------------------- /front/src/containers/Chat/components/userList.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react' 2 | import styled from 'styled-components' 3 | import cssVars from 'cssVars' 4 | import PropTypes from 'prop-types' 5 | import getUserColorByName from 'utils/getUserColorByName' 6 | 7 | const Container = styled.section` 8 | width: 100%; 9 | height: 100%; 10 | margin: 0; 11 | border-left: 2px solid ${cssVars['borderColor']}; 12 | position: relative; 13 | overflow-y: auto; 14 | padding: 1rem; 15 | ` 16 | const Item = styled.li` 17 | padding: 1rem; 18 | margin: 0; 19 | margin-bottom: 0.3rem; 20 | list-style: none; 21 | text-align: left; 22 | color: #FFF; 23 | border-radius: 10px; 24 | font-weight: bold; 25 | text-overflow: ellipsis; 26 | overflow: hidden; 27 | white-space: nowrap; 28 | ` 29 | const StyledOl = styled.ol` 30 | margin: 0; 31 | padding: 0; 32 | ` 33 | const Title = styled.h4` 34 | margin-top: 0; 35 | text-align: center; 36 | ` 37 | export class UserList extends Component { 38 | static propTypes = { 39 | users: PropTypes.array 40 | } 41 | static defaultProps = { 42 | users: [] 43 | } 44 | render () { 45 | return ( 46 | 47 | Users 48 | 49 | {this.props.users.map((userName) => { 50 | const backgroundColor = getUserColorByName(userName) 51 | return ( 52 | 53 | {userName} 54 | 55 | ) 56 | })} 57 | 58 | 59 | ) 60 | } 61 | } 62 | export default UserList 63 | -------------------------------------------------------------------------------- /front/src/containers/Chat/reducer.js: -------------------------------------------------------------------------------- 1 | import { Map, List, Set } from 'immutable' 2 | import types from './actionTypes' 3 | // import {LOCATION_CHANGE} from 'react-router-redux' 4 | 5 | export const initial = new Map({ 6 | messagesList: new List(), 7 | userList: new Set() 8 | }) 9 | function chatReducer (state = initial, action) { 10 | switch (action.type) { 11 | case types.SOMEONE_ENTERED: 12 | return state.withMutations((mutableState) => { 13 | return mutableState 14 | .update( 15 | 'messagesList', 16 | messages => messages.push({body: `${action.userName} joined the room`, isJoin: true}) 17 | ) 18 | .update( 19 | 'userList', 20 | userList => userList.add(action.userName).sort() 21 | ) 22 | }) 23 | 24 | case types.SOMEONE_LEFT: 25 | return state.withMutations((mutableState) => { 26 | return mutableState 27 | .update( 28 | 'messagesList', 29 | messages => messages.push({body: `${action.userName} left the room`, isLeft: true}) 30 | ) 31 | .update( 32 | 'userList', 33 | userList => userList.delete(action.userName) 34 | ) 35 | }) 36 | case types.ENTER_CHAT_ROOM: 37 | const newState = state.withMutations((mutableState) => { 38 | return mutableState 39 | .update( 40 | 'messagesList', 41 | messages => new List([{body: action.welcomeMessage}]) 42 | ) 43 | .update( 44 | 'userList', 45 | users => new Set(action.usersList).sort() 46 | ) 47 | }) 48 | return newState 49 | case types.RECEIVED_MESSAGE: 50 | return state.update('messagesList', messages => messages.push({body: `${action.message}`, senderName: action.senderName})) 51 | case types.SEND_MESSAGE: 52 | return state.set('outgoingMessage', action.message) 53 | default: 54 | return state 55 | } 56 | } 57 | export default chatReducer 58 | -------------------------------------------------------------------------------- /front/src/containers/Chat/components/messageForm.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react' 2 | import {MessageInput} from './messageInput' 3 | import {SendMessageButton} from './sendMessageButton' 4 | import styled from 'styled-components' 5 | import {checkEnter} from 'utils/checkEnter' 6 | 7 | const Container = styled.div` 8 | display: table; 9 | width: 100%; 10 | border-collapse: collapse; 11 | border-spacing: 0; 12 | position: relative; 13 | ` 14 | const InputContainer = styled.div` 15 | display: table-cell; 16 | width: 100%; 17 | white-space:nowrap; 18 | ` 19 | const ButtonContainer = styled.div` 20 | display: table-cell; 21 | white-space:nowrap; 22 | ` 23 | const StyledRow = styled.div` 24 | display: table-row; 25 | ` 26 | const StyledButton = styled(SendMessageButton)` 27 | border-top-left-radius: 0; 28 | border-bottom-left-radius: 0; 29 | height: 100%; 30 | ` 31 | export class MessageForm extends Component { 32 | state = { 33 | message: '' 34 | } 35 | onSendMessage = () => { 36 | const message = this.state.message 37 | this.props.sendMessage(message) 38 | this.setState({message: ''}) 39 | } 40 | handleChange = (ev) => { 41 | const message = ev.target.value 42 | this.setState({message}) 43 | } 44 | onCheckKey = (e) => { 45 | if (checkEnter(e)) { 46 | this.onSendMessage() 47 | } 48 | } 49 | render () { 50 | return ( 51 | 52 | 53 | 54 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | ) 67 | } 68 | } 69 | export default MessageForm 70 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Demo](demo.gif "How it looks like?") 2 | 3 | # react_springboot_simple_chat 4 | 5 | A simple react chat with spring boot as a backend using STOMP to communicate trhough sockets. 6 | 7 | It's build on top of docker containers for convenience. 8 | 9 | ## File structure 10 | 11 | On the root there are 2 folders **api** and **front** the first is the spring boot socket api, the second a react webpack based app. 12 | 13 | The Dockerfiles on the root are used for the build and development inside docker containers. 14 | 15 | ## How to run. 16 | 17 | ### The easy way 18 | 19 | If you want to run using the docker infrastructure you can simply do 20 | 21 | ```sh 22 | docker-compose up 23 | ``` 24 | This will download the api dependencies and the node dependencies, after that is done the frontend will be on localhost:8080 and the backend on localhost:3000 25 | 26 | ### The local way 27 | 28 | If you want to run on your local infrastructure and you have Java and Node you can open 2 terminalls and do the following: 29 | 30 | ```sh 31 | cd front 32 | npm install 33 | npm start ## or npm test 34 | ``` 35 | 36 | And 37 | ```sh 38 | cd api 39 | ./gradlew bootRun 40 | ``` 41 | 42 | ### The "I want a terminal but I don't want to install all your dependencies locally way". 43 | This way will run the images but will run then on a shared volume on front an api, this way the file dependencies will be installed on a shared folder but the dependencies programs will not. 44 | 45 | For this to work we start the 2 containers and let them sleep, then we xec to use the bash on them so we can run any command as if Node, Java, Gradle and so on were installed locally. 46 | 47 | ```sh 48 | docker-compose -f docker-compose-dev.yml up 49 | ``` 50 | 51 | On a terminal to use the front: 52 | 53 | ```sh 54 | docker-compose exec front bash 55 | # You will be on the bash terminal of the front (node) image 56 | npm install # needed since it was not installed before 57 | npm start # Or npm test 58 | ``` 59 | 60 | On another terminal to run the api 61 | ```sh 62 | docker-compose exec api bash 63 | gradle bootRun 64 | ``` 65 | -------------------------------------------------------------------------------- /front/src/utils/socketConnection.js: -------------------------------------------------------------------------------- 1 | /* global Stomp */ 2 | 3 | import SockJS from 'sockjs-client' 4 | const SOCKET_URL = 'http://localhost:3000/chatExample' 5 | 6 | export class SocketConnector { 7 | _ensureConnection () { 8 | if (this._connected) { 9 | return Promise.resolve() 10 | } 11 | return new Promise((resolve, reject) => { 12 | const socket = new SockJS(SOCKET_URL) 13 | this._client = Stomp.over(socket) 14 | this._client.connect({}, (frame) => { 15 | this._connected = true 16 | resolve() 17 | }, 18 | () => { 19 | this._connected = false 20 | }) 21 | }) 22 | } 23 | isConnected () { 24 | return this._connected 25 | } 26 | subscribe (messageCallback, userEnterCallback, userLeftCallback) { 27 | this._messageCallback = messageCallback 28 | this._userEnterCallback = userEnterCallback 29 | this._userLeftCallback = userLeftCallback 30 | } 31 | connectWithName (name) { 32 | return this._ensureConnection().then(() => { 33 | return new Promise((resolve) => { 34 | this._client.subscribe('/user/topic/registerResponse', (response) => { 35 | return resolve(JSON.parse(response.body)) 36 | }) 37 | this._client.subscribe('/topic/messages', (response) => { 38 | if (this._messageCallback) { 39 | this._messageCallback(JSON.parse(response.body)) 40 | } 41 | }) 42 | this._client.subscribe('/topic/userEnter', (response) => { 43 | if (this._userEnterCallback) { 44 | this._userEnterCallback(JSON.parse(response.body)) 45 | } 46 | }) 47 | this._client.subscribe('/topic/userLeft', (response) => { 48 | if (this._userEnterCallback) { 49 | this._userLeftCallback(JSON.parse(response.body)) 50 | } 51 | }) 52 | this._client.send('/api/register', {}, JSON.stringify({name})) 53 | }) 54 | }) 55 | } 56 | sendMessage (message) { 57 | this._client.send('/api/chat', {}, JSON.stringify({message})) 58 | } 59 | } 60 | export const socketConnection = new SocketConnector() 61 | export default socketConnection 62 | -------------------------------------------------------------------------------- /front/src/app.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react' 2 | import {Provider} from 'react-redux' 3 | import { createStore, applyMiddleware } from 'redux' 4 | import { Router, Route, browserHistory, IndexRoute } from 'react-router' 5 | import getReducer from './getReducer' 6 | import {Welcome, Chat} from 'containers' 7 | import {Template} from './template' 8 | import { syncHistoryWithStore, routerMiddleware } from 'react-router-redux' 9 | import createSagaMiddleware from 'redux-saga' 10 | import {welcomeSaga} from './containers/Welcome/saga' 11 | import {chatSaga} from './containers/Chat/saga' 12 | import socketConnection from 'utils/socketConnection' 13 | import {injectGlobal} from 'styled-components' 14 | import cssVars from 'cssVars' 15 | 16 | injectGlobal` 17 | html { 18 | height: 100%; 19 | } 20 | body { 21 | font-family: 'Montserrat', sans-serif; 22 | height: 100%; 23 | margin: 0; 24 | overflow-y: hidden; 25 | background-color: ${cssVars['bodyBgColor']} 26 | } 27 | #app { 28 | height: 100%; 29 | } 30 | * { 31 | box-sizing: border-box; 32 | } 33 | ` 34 | const routeMiddleware = routerMiddleware(browserHistory) 35 | const sagaMiddleware = createSagaMiddleware() 36 | const createStoreWithMiddleware = applyMiddleware( 37 | routeMiddleware, 38 | sagaMiddleware 39 | )(createStore) 40 | const store = createStoreWithMiddleware(getReducer()) 41 | const history = syncHistoryWithStore(browserHistory, store) 42 | 43 | sagaMiddleware.run(welcomeSaga) 44 | sagaMiddleware.run(chatSaga) 45 | 46 | const requireConnection = (nextState, replace, callback) => { 47 | if (socketConnection.isConnected()) { 48 | callback() 49 | } 50 | replace({pathname: '/'}) 51 | callback() 52 | } 53 | export class App extends Component { 54 | render () { 55 | return ( 56 | 57 | window.scrollTo(0, 0)} history={history}> 58 | 59 | 60 | 61 | 62 | 63 | 64 | ) 65 | } 66 | } 67 | export default App 68 | -------------------------------------------------------------------------------- /front/src/containers/Chat/components/tests/messageForm.test.js: -------------------------------------------------------------------------------- 1 | import { mount } from 'enzyme' 2 | import React from 'react' 3 | import {MessageInput, MessageForm, SendMessageButton} from '../' 4 | import sinon from 'sinon' 5 | 6 | describe('', () => { 7 | it('renders one SendMessageButton and one MessageInput', () => { 8 | const renderedComponent = mount() 9 | expect(renderedComponent.find(SendMessageButton).length).toEqual(1) 10 | expect(renderedComponent.find(MessageInput).length).toEqual(1) 11 | }) 12 | it('Fills the input value based on the state', () => { 13 | const renderedComponent = mount() 14 | const mockValue = 'mockValue' 15 | renderedComponent.setState({'message': mockValue}) 16 | expect(renderedComponent.first(MessageInput).find('input').prop('value')).toEqual(mockValue) 17 | }) 18 | it('Save the value of name on state when it changes', () => { 19 | const renderedComponent = mount() 20 | const mockValue = 'mockValue' 21 | const mockEvent = {target: {value: mockValue}} 22 | renderedComponent.first(MessageInput).find('input').simulate('change', mockEvent) 23 | expect(renderedComponent.state('message')).toEqual(mockValue) 24 | }) 25 | it('When the button is send sendMessage prop is invoked and message state is cleared', () => { 26 | const sendMessageSpy = sinon.spy() 27 | const renderedComponent = mount() 28 | const mockValue = 'mockValue' 29 | renderedComponent.setState({'message': mockValue}) 30 | renderedComponent.first(SendMessageButton).find('button').simulate('click') 31 | expect(renderedComponent.state('message')).toEqual('') 32 | }) 33 | it('Sends the message if enterKey is pressed', () => { 34 | const sendMessageSpy = sinon.spy() 35 | const renderedComponent = mount() 36 | const mockValue = 'mockValue' 37 | renderedComponent.setState({'message': mockValue}) 38 | const mockEvent = {keyCode: 13} 39 | renderedComponent.first(MessageInput).find('input').simulate('keyUp', mockEvent) 40 | expect(sendMessageSpy.calledWith(mockValue)).toBe(true) 41 | }) 42 | }) 43 | -------------------------------------------------------------------------------- /front/src/containers/Welcome/components/tests/enterForm.test.jsx: -------------------------------------------------------------------------------- 1 | import { shallow, mount } from 'enzyme' 2 | import React from 'react' 3 | import {EnterForm, EnterButton, NameInput} from '../' 4 | import sinon from 'sinon' 5 | 6 | describe('', () => { 7 | it('renders one EnterButton and one NameInput', () => { 8 | const renderedComponent = shallow() 9 | expect(renderedComponent.find(EnterButton).length).toEqual(1) 10 | expect(renderedComponent.find(NameInput).length).toEqual(1) 11 | }) 12 | it('Pass entering property down', () => { 13 | const renderedComponent = mount() 14 | expect(renderedComponent.first(EnterButton).prop('entering')).toEqual(true) 15 | expect(renderedComponent.first(NameInput).prop('entering')).toEqual(true) 16 | }) 17 | it('Save the value of name on state when it changes', () => { 18 | const renderedComponent = mount() 19 | const mockValue = 'mockValue' 20 | const mockEvent = {target: {value: mockValue}} 21 | renderedComponent.first(NameInput).find('input').simulate('change', mockEvent) 22 | expect(renderedComponent.state('chatName')).toEqual(mockValue) 23 | }) 24 | it('Fills the input value based on the state', () => { 25 | const renderedComponent = mount() 26 | const mockValue = 'mockValue' 27 | renderedComponent.setState({'chatName': mockValue}) 28 | expect(renderedComponent.first(NameInput).find('input').prop('value')).toEqual(mockValue) 29 | }) 30 | it('Invokes the onEnterChat with the trimmed chatName value when button is clicked', () => { 31 | const onEnterChatSpy = sinon.spy() 32 | const renderedComponent = mount() 33 | const mockValue = ' mockValue ' 34 | renderedComponent.setState({'chatName': mockValue}) 35 | renderedComponent.find(EnterButton).simulate('click') 36 | expect(onEnterChatSpy.calledWith(mockValue.trim())).toBe(true) 37 | }) 38 | it('Submits the form if enter key is pressed', () => { 39 | const onEnterChatSpy = sinon.spy() 40 | const renderedComponent = mount() 41 | const mockValue = ' mockValue ' 42 | renderedComponent.setState({'chatName': mockValue}) 43 | const mockEvent = {keyCode: 13} 44 | renderedComponent.first(NameInput).find('input').simulate('keyUp', mockEvent) 45 | expect(onEnterChatSpy.calledWith(mockValue.trim())).toBe(true) 46 | }) 47 | }) 48 | -------------------------------------------------------------------------------- /api/gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /front/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chat_example", 3 | "version": "1.0.0", 4 | "description": "A chat react APP", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "cross-env NODE_ENV=test jest", 8 | "build": "webpack --config internals/webpack.config.babel.js", 9 | "build:production": "webpack -p --config internals/webpack.config.babel.js", 10 | "start": "webpack-dev-server --config internals/webpack.config.babel.js", 11 | "lint:js": "eslint --ext .js --ext .jsx --fix src/", 12 | "startWithInstall": "npm start", 13 | "prestartWithInstall": "npm install" 14 | }, 15 | "author": "fabio oliveira costa ", 16 | "license": "MIT", 17 | "devDependencies": { 18 | "babel-core": "^6.24.1", 19 | "babel-eslint": "^7.2.3", 20 | "babel-loader": "^7.0.0", 21 | "babel-plugin-transform-class-properties": "^6.24.1", 22 | "babel-plugin-transform-es2015-modules-commonjs": "^6.24.1", 23 | "babel-plugin-transform-object-rest-spread": "^6.23.0", 24 | "babel-polyfill": "^6.23.0", 25 | "babel-preset-es2015": "^6.24.1", 26 | "babel-preset-es2016": "^6.24.1", 27 | "babel-preset-es2017": "^6.24.1", 28 | "babel-preset-react": "^6.24.1", 29 | "babel-preset-react-hmre": "^1.1.1", 30 | "babel-preset-stage-0": "^6.24.1", 31 | "babel-preset-stage-2": "^6.24.1", 32 | "babel-register": "^6.24.1", 33 | "cross-env": "^5.0.0", 34 | "enzyme": "^2.8.2", 35 | "eslint": "^3.19.0", 36 | "eslint-config-standard": "^10.2.1", 37 | "eslint-config-standard-jsx": "^4.0.1", 38 | "eslint-plugin-import": "^2.3.0", 39 | "eslint-plugin-node": "^4.2.2", 40 | "eslint-plugin-promise": "^3.5.0", 41 | "eslint-plugin-react": "^7.0.1", 42 | "eslint-plugin-standard": "^3.0.1", 43 | "html-webpack-plugin": "^2.28.0", 44 | "html-webpack-template": "^6.0.1", 45 | "jest-cli": "^20.0.4", 46 | "react-test-renderer": "^15.5.4", 47 | "redux-mock-store": "^1.2.3", 48 | "sinon": "^2.3.2", 49 | "webpack": "^2.6.1", 50 | "webpack-dev-server": "^2.4.5", 51 | "webpack-merge": "^4.1.0" 52 | }, 53 | "dependencies": { 54 | "immutable": "^3.8.1", 55 | "prop-types": "^15.5.10", 56 | "react": "^15.5.4", 57 | "react-dom": "^15.5.4", 58 | "react-redux": "^5.0.5", 59 | "react-router": "^3.0.5", 60 | "react-router-redux": "^4.0.8", 61 | "redux": "^3.6.0", 62 | "redux-saga": "^0.15.3", 63 | "reselect": "^3.0.1", 64 | "sockjs-client": "^1.1.4", 65 | "styled-components": "^2.0.0", 66 | "whatwg-fetch": "^2.0.3" 67 | }, 68 | "jest": { 69 | "moduleDirectories": [ 70 | "src", 71 | "node_modules" 72 | ] 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /front/src/containers/Welcome/tests/reducer.test.js: -------------------------------------------------------------------------------- 1 | import reducer, {initial as initialState} from '../reducer' 2 | import types from '../actionTypes' 3 | import {LOCATION_CHANGE} from 'react-router-redux' 4 | 5 | describe('welcome reducer', () => { 6 | it('Flags entering and save userName on ENTER_CHAT', () => { 7 | const mockName = 'mockName' 8 | const mockAction = { 9 | type: types.ENTER_CHAT, 10 | userName: mockName 11 | } 12 | const expectedResult = initialState.withMutations((state) => { 13 | return state.set('userName', mockName) 14 | .set('entering', true) 15 | }) 16 | const result = reducer(initialState, mockAction) 17 | expect(result).toEqual(expectedResult) 18 | }) 19 | it('Remove error on ENTER_CHAT', () => { 20 | const mockName = 'mockName' 21 | const mockAction = { 22 | type: types.ENTER_CHAT, 23 | userName: mockName, 24 | error: {message: 'error'} 25 | } 26 | const expectedResult = initialState.withMutations((state) => { 27 | return state.set('userName', mockName) 28 | .set('entering', true) 29 | }) 30 | const result = reducer(initialState, mockAction) 31 | expect(result).toEqual(expectedResult) 32 | }) 33 | it('Unflags entering and set error on USER_ALREADY_EXISTS', () => { 34 | const mockName = 'mockName' 35 | const reducerState = initialState.withMutations((state) => { 36 | return state.set('userName', mockName) 37 | .set('entering', true) 38 | }) 39 | const mockError = 'The user already exists' 40 | const mockAction = { 41 | type: types.USER_ALREADY_EXISTS, 42 | error: mockError 43 | } 44 | const expectedResult = initialState.withMutations((state) => { 45 | return state.set('userName', mockName) 46 | .set('entering', false) 47 | .set('error', mockError) 48 | }) 49 | const result = reducer(reducerState, mockAction) 50 | expect(result).toEqual(expectedResult) 51 | }) 52 | it('Sets the error is EMPTY_USER_NAME', () => { 53 | const mockError = 'mockError' 54 | const mockAction = { 55 | type: types.EMPTY_USER_NAME, 56 | error: mockError 57 | } 58 | const expectedResult = initialState.set('error', mockError) 59 | const result = reducer(initialState, mockAction) 60 | expect(result).toEqual(expectedResult) 61 | }) 62 | it('Resets the state to the initial state on LOCATION_CHANGE', () => { 63 | const reducerState = initialState.withMutations((state) => { 64 | return state.set('userName', 'mockName') 65 | .set('entering', true) 66 | }) 67 | const mockAction = { 68 | type: LOCATION_CHANGE 69 | } 70 | const result = reducer(reducerState, mockAction) 71 | expect(result).toEqual(initialState) 72 | }) 73 | }) 74 | -------------------------------------------------------------------------------- /front/src/containers/Chat/components/chatPage.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react' 2 | import {MessageForm} from './messageForm' 3 | import {MessageArea} from './messageArea' 4 | import {UserList} from './userList' 5 | import ReactDOM from 'react-dom' 6 | import styled from 'styled-components' 7 | import {Title, Card} from 'elements' 8 | 9 | const Container = styled(Card)` 10 | height: 100%; 11 | position: relative; 12 | overflow: hidden; 13 | text-align: left; 14 | ` 15 | const FormContainer = styled.div` 16 | position: absolute; 17 | bottom: 0; 18 | height: 3rem; 19 | width: 100%; 20 | ` 21 | const InnerContainer = styled.div` 22 | padding: 0.5rem; 23 | position: relative; 24 | height: 100%; 25 | ` 26 | const InnerFormContainer = styled.div` 27 | padding: 0 0.5rem; 28 | position relative; 29 | height: 100%; 30 | ` 31 | const MainContainer = styled.section` 32 | position: absolute; 33 | top: 4.5rem; 34 | bottom: 3.5rem; 35 | width: 100%; 36 | ` 37 | const InnerMainContainer = styled.section` 38 | border-collapse: collapse; 39 | border-spacing: 0; 40 | display: table; 41 | height: 100%; 42 | position: relative; 43 | ` 44 | const InnerRow = styled.section` 45 | display: table-row; 46 | height: 100%; 47 | ` 48 | const InnerOuterMainContainer = styled.section` 49 | width: 100%; 50 | height: 100%; 51 | position: relative; 52 | overflow:hidden; 53 | ` 54 | const MessageAreaContainer = styled.section` 55 | display: table-cell; 56 | width: 100%; 57 | height: 0; 58 | padding-right: 1rem; 59 | white-space: nowrap; 60 | vertical-align: top; 61 | ` 62 | const UserListContainer = styled.section` 63 | display: table-cell; 64 | max-width: 25%; 65 | white-space: nowrap; 66 | ` 67 | export class ChatPage extends Component { 68 | scrollToBottom = () => { 69 | const node = ReactDOM.findDOMNode(this.chatArea) 70 | node.scrollTop = node.scrollHeight 71 | } 72 | componentDidUpdate () { 73 | this.scrollToBottom() 74 | } 75 | render () { 76 | return ( 77 | 78 | 79 | You are on the Chat, Talk alway! 80 | { this.chatArea = c }}> 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | ) 102 | } 103 | } 104 | export default ChatPage 105 | -------------------------------------------------------------------------------- /front/src/containers/Chat/saga.js: -------------------------------------------------------------------------------- 1 | import { call, select, takeEvery, fork, put } from 'redux-saga/effects' 2 | import * as types from './actionTypes' 3 | import * as actions from './actions' 4 | import {socketConnection} from 'utils/socketConnection' 5 | 6 | const makeSelectChat = (state) => state.chat 7 | 8 | function createMessageSource () { 9 | let deferradMessage = null 10 | const onMessage = (message) => { 11 | if (deferradMessage) { 12 | deferradMessage.resolve(message) 13 | deferradMessage = null 14 | } 15 | } 16 | let deferredEnter = null 17 | const onUserEnter = (message) => { 18 | if (deferredEnter) { 19 | deferredEnter.resolve(message) 20 | deferredEnter = null 21 | } 22 | } 23 | let deferredLeft = null 24 | const onUserLeft = (message) => { 25 | if (deferredLeft) { 26 | deferredLeft.resolve(message) 27 | deferredLeft = null 28 | } 29 | } 30 | socketConnection.subscribe(onMessage, onUserEnter, onUserLeft) 31 | return { 32 | getMessage () { 33 | if (!deferradMessage) { 34 | deferradMessage = {} 35 | deferradMessage.promise = new Promise((resolve) => { 36 | deferradMessage.resolve = resolve 37 | }) 38 | } 39 | return deferradMessage.promise 40 | }, 41 | getUserEnter () { 42 | if (!deferredEnter) { 43 | deferredEnter = {} 44 | deferredEnter.promise = new Promise((resolve) => { 45 | deferredEnter.resolve = resolve 46 | }) 47 | } 48 | return deferredEnter.promise 49 | }, 50 | getUserLeft () { 51 | if (!deferredLeft) { 52 | deferredLeft = {} 53 | deferredLeft.promise = new Promise((resolve) => { 54 | deferredLeft.resolve = resolve 55 | }) 56 | } 57 | return deferredLeft.promise 58 | } 59 | } 60 | } 61 | export function * handleMessage () { 62 | const state = yield select(makeSelectChat) 63 | const message = state.get('outgoingMessage') 64 | yield call(socketConnection.sendMessage.bind(socketConnection), message) 65 | } 66 | export function * listenToMessages (messageSource) { 67 | while (true) { 68 | const message = yield call(messageSource.getMessage) 69 | console.log('received message', message) 70 | yield put(actions.receivedMessage(message.message, message.senderName)) 71 | } 72 | } 73 | export function * listenToEnter (messageSource) { 74 | while (true) { 75 | const message = yield call(messageSource.getUserEnter) 76 | console.log('received enter', message) 77 | yield put(actions.someoneEntered(message.userName)) 78 | } 79 | } 80 | export function * listenToUserLeave (messageSource) { 81 | while (true) { 82 | const message = yield call(messageSource.getUserLeft) 83 | console.log('received Left', message) 84 | yield put(actions.someoneLeft(message.userName)) 85 | } 86 | } 87 | export function * chatSaga () { 88 | const messageSource = createMessageSource() 89 | yield [ 90 | takeEvery(types.SEND_MESSAGE, handleMessage), 91 | fork(listenToMessages, messageSource), 92 | fork(listenToEnter, messageSource), 93 | fork(listenToUserLeave, messageSource) 94 | 95 | ] 96 | } 97 | export default chatSaga 98 | -------------------------------------------------------------------------------- /front/src/containers/Chat/tests/reducer.test.js: -------------------------------------------------------------------------------- 1 | import reducer, {initial as initialState} from '../reducer' 2 | import types from '../actionTypes' 3 | import {LOCATION_CHANGE} from 'react-router-redux' 4 | 5 | describe('chat reducer', () => { 6 | it('Adds a a user to the user list sorted when someone enters', () => { 7 | const mockName = 'mockName' 8 | const mockAction = { 9 | type: types.SOMEONE_ENTERED, 10 | userName: mockName 11 | } 12 | const testState = initialState.update( 13 | 'userList', 14 | userList => userList.withMutations((set) => { 15 | return set.add('foo') 16 | .add('zap') 17 | .add('abc') 18 | .add('bar') 19 | .sort() 20 | }) 21 | ) 22 | const expectedResult = testState.update( 23 | 'userList', 24 | userList => userList.add(mockName).sort() 25 | ) 26 | const result = reducer(testState, mockAction) 27 | expect(result.get('userList')).toEqual(expectedResult.get('userList')) 28 | }) 29 | it('Removes a user from the user list when someone leaves', () => { 30 | const mockName = 'mockName' 31 | const mockAction = { 32 | type: types.SOMEONE_LEFT, 33 | userName: mockName 34 | } 35 | const testState = initialState.update( 36 | 'userList', 37 | userList => userList.withMutations((set) => { 38 | return set.add('foo') 39 | .add('zap') 40 | .add('abc') 41 | .add('bar') 42 | .add('mockName') 43 | .sort() 44 | }) 45 | ) 46 | const expectedResult = testState.update( 47 | 'userList', 48 | userList => userList.withMutations((set) => { 49 | return set.delete(mockName) 50 | }) 51 | ) 52 | const result = reducer(testState, mockAction) 53 | expect(result.get('userList')).toEqual(expectedResult.get('userList')) 54 | }) 55 | it('Adds a message to the list when someone enters', () => { 56 | const mockName = 'mockName' 57 | const mockAction = { 58 | type: types.SOMEONE_ENTERED, 59 | userName: mockName 60 | } 61 | const expectedResult = initialState.update('messagesList', messages => messages.push({body: `${mockName} joined the room`, isJoin: true})) 62 | const result = reducer(initialState, mockAction) 63 | expect(result.get('messagesList')).toEqual(expectedResult.get('messagesList')) 64 | }) 65 | it('Adds a message to the list when someone leaves', () => { 66 | const mockName = 'mockName' 67 | const mockAction = { 68 | type: types.SOMEONE_LEFT, 69 | userName: mockName 70 | } 71 | const expectedResult = initialState.update('messagesList', messages => messages.push({body: `${mockName} left the room`, isLeft: true})) 72 | const result = reducer(initialState, mockAction) 73 | expect(result.get('messagesList')).toEqual(expectedResult.get('messagesList')) 74 | }) 75 | it('Adds a message to the list when a message is received', () => { 76 | const mockName = 'mockName' 77 | const mockMessage = 'mockMessage' 78 | const mockAction = { 79 | type: types.RECEIVED_MESSAGE, 80 | senderName: mockName, 81 | message: mockMessage 82 | } 83 | const expectedResult = initialState.update('messagesList', messages => messages.push({body: `${mockMessage}`, senderName: mockName})) 84 | const result = reducer(initialState, mockAction) 85 | expect(result.get('messagesList')).toEqual(expectedResult.get('messagesList')) 86 | }) 87 | it('Adds a message to the outgoingMessage when a message is sent', () => { 88 | const mockMessage = 'mockMessage' 89 | const mockAction = { 90 | type: types.SEND_MESSAGE, 91 | message: mockMessage 92 | } 93 | const expectedResult = initialState.set('outgoingMessage', mockMessage) 94 | const result = reducer(initialState, mockAction) 95 | expect(result).toEqual(expectedResult) 96 | }) 97 | }) 98 | -------------------------------------------------------------------------------- /api/gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn ( ) { 37 | echo "$*" 38 | } 39 | 40 | die ( ) { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save ( ) { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | --------------------------------------------------------------------------------