├── 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 | 
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 |
--------------------------------------------------------------------------------