Skip to content
This repository has been archived by the owner on Feb 21, 2024. It is now read-only.

Single chat room #40

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
8,191 changes: 8,191 additions & 0 deletions package-lock.json

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,14 +67,17 @@
"bootstrap": "^3.3.6",
"firebase": "^3.0.3",
"jquery": "^2.2.4",
"lodash": "^4.17.4",
"react": "^15.1.0",
"react-dom": "^15.1.0",
"react-loader": "^2.4.0",
"react-redux": "^4.4.5",
"react-router": "^2.4.1",
"react-router-redux": "^4.0.4",
"redux": "^3.5.2",
"redux-saga": "^0.16.0",
"redux-thunk": "^2.1.0",
"reselect": "^3.0.1",
"toastr": "^2.1.2"
},
"repository": {
Expand Down
10 changes: 10 additions & 0 deletions src/actions/actionTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,15 @@ export const AUTH_LOGGED_OUT_SUCCESS = 'AUTH_LOGGED_OUT_SUCCESS';
export const USER_CREATED_SUCCESS = 'USER_CREATED_SUCCESS';
export const USER_LOADED_SUCCESS = 'USER_LOADED_SUCCESS';
export const USER_IS_ADMIN_SUCCESS = 'USER_IS_ADMIN_SUCCESS';
export const UPDATE_USER = 'UPDATE_USER';

// Messages actions
export const LAUNCH_PUSH_MESSAGES = 'LAUNCH_PUSH_MESSAGES';
export const PUSH_MESSAGES_SUCCESS = 'PUSH_MESSAGES_SUCCESS';
export const PUSH_MESSAGES_ERROR = 'PUSH_MESSAGES_ERROR';

export const FETCH_MESSAGES_SUCCESS = 'FETCH_MESSAGES_SUCCESS';
export const FETCH_MESSAGES_ERROR = 'FETCH_MESSAGES_ERROR';

export const REQUEST = 'REQUEST';

29 changes: 29 additions & 0 deletions src/actions/messagesActions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import * as types from './actionTypes';

export const pushMessagesSuccess = message => ({
type: types.PUSH_MESSAGES_SUCCESS,
});

export const pushMessagesError = message => ({
type: types.PUSH_MESSAGES_ERROR,
});

export const launchPushMessages = message => dispatch =>
dispatch({
type: types.LAUNCH_PUSH_MESSAGES,
message,
});

export const fetchMessagesSuccess = messages => ({
type: types.FETCH_MESSAGES_SUCCESS,
payload: messages,
});

export const fetchMessagesError = () => ({
type: types.FETCH_MESSAGES_ERROR,
});


export const requestMessages = () => dispatch => dispatch({
type: types.REQUEST,
});
15 changes: 9 additions & 6 deletions src/api/firebase.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
import * as firebase from 'firebase/firebase-browser';
import {firebaseConfig} from '../config';


class FirebaseApi {

static initAuth() {
firebase.initializeApp(firebaseConfig);
return new Promise((resolve, reject) => {
const unsub = firebase.auth().onAuthStateChanged(
user => {
Expand Down Expand Up @@ -63,12 +59,19 @@ class FirebaseApi {
}

static databaseSet(path, value) {
return firebase.database().ref(path).set(value);
}

static unsubDatabase(path) {
return firebase.database().ref(path).off();
}

static databasePathValueLimitToLast(path, limit, handler) {
return firebase
.database()
.ref(path)
.set(value);

.limitToLast(limit)
.on('child_added', handler);
}
}

Expand Down
97 changes: 97 additions & 0 deletions src/components/chat/ChatRoom.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import React, { Component } from 'react';
import { createStructuredSelector } from 'reselect';
import { connect } from 'react-redux';

import checkAuth from '../requireAuth';
import {
launchPushMessages,
requestMessages,
} from '../../actions/messagesActions';
import MessageForm from './MessageForm';
import MessageList from './MessageList';
import { getUserEmail, getUserUid } from '../../reducers/userReducer';
import { getLoading } from '../../reducers/loadingReducer';
import { getMessages } from '../../reducers/messagesReducer';
import { isLogged } from '../../reducers/authReducer';

class ChatRoom extends Component {
constructor(props) {
super(props);

this.state = {
message: { value: '' },
};
}

componentWillMount() {
const { requestMessages, isLogged } = this.props;
if (isLogged) {
requestMessages();
}
}

componentWillReceiveProps({ loading }) {
const { loading: previousLoading } = this.props;
if (previousLoading !== loading && !loading) {
this.setState({ message: { value: '' } });
}
}

updateMessage = e => {
const { message } = this.state;
const { name, value } = e.target;
this.setState({ message: { ...message, [name]: value } });
};

sendNewMessage = e => {
e.preventDefault();

const { uid, email, launchPushMessages } = this.props;
const { message: { value } } = this.state;

const message = {
value,
from: {
uid,
email,
},
};

launchPushMessages(message);
};

render() {
const { loading, messages } = this.props;
const { message } = this.state;

return (
<div>
<h3>Chat</h3>
<MessageList messages={messages} />
<MessageForm
onChange={this.updateMessage}
onClick={this.sendNewMessage}
saving={loading}
message={message}
/>
</div>
);
}
}

const mapStateToProps = createStructuredSelector({
email: getUserEmail,
uid: getUserUid,
messages: getMessages,
loading: getLoading,
isLogged,
});

const mapDispatchToProps = {
launchPushMessages,
requestMessages,
};

export default connect(mapStateToProps, mapDispatchToProps)(
checkAuth(ChatRoom)
);
36 changes: 36 additions & 0 deletions src/components/chat/MessageForm.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import React, { PropTypes } from 'react';
import prop from 'lodash/fp/prop';

import TextInput from '../common/TextInput';

const MessageForm = ({ message, onClick, onChange, loading }) =>
<form>
<TextInput
name="value"
label="New message"
onChange={onChange}
value={prop('value')(message)}
/>
<input
type="submit"
value={loading ? 'Sending ...' : 'Send'}
className="btn btn-primary"
onClick={onClick}
/>
</form>;

MessageForm.defaultProps = {
message: {},
onClick: () => {},
onChange: () => {},
loading: false,
};

MessageForm.propTypes = {
message: PropTypes.object,
onClick: PropTypes.func,
onChange: PropTypes.func,
loading: PropTypes.bool,
};

export default MessageForm;
27 changes: 27 additions & 0 deletions src/components/chat/MessageItem.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import React, { PropTypes } from 'react';
import prop from 'lodash/fp/prop';

const MessageItem = ({ value, from }) => {
return (
<div className="message-wrapper them">
<div className="circle-wrapper animated bounceIn">
{prop('email')(from)}
</div>
<div className="text-wrapper animated fadeIn">
{value}
</div>
</div>
);
};

MessageItem.defaultProps = {
value: '',
from: {},
};

MessageItem.propTypes = {
value: PropTypes.string,
from: PropTypes.object,
};

export default MessageItem;
27 changes: 27 additions & 0 deletions src/components/chat/MessageList.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import React, { PropTypes } from 'react';
import prop from 'lodash/fp/prop';
import MessageItem from './MessageItem';

const MessageList= ({ messages }) =>
<div>
<h4>Messages</h4>
<div className="message-list">
{messages.map((message, index) =>
<MessageItem
key={index}
value={prop('value')(message)}
from={prop('from')(message)}
/>
)}
</div>
</div>;

MessageList.defaultProps = {
messages: [],
};

MessageList.propTypes = {
messages: PropTypes.array,
};

export default MessageList;
24 changes: 14 additions & 10 deletions src/components/common/Header.js
Original file line number Diff line number Diff line change
@@ -1,26 +1,30 @@
import React, {PropTypes} from 'react';
import {Link, IndexLink} from 'react-router';
import React, { PropTypes } from 'react';
import { IndexLink, Link } from 'react-router';
import LoadingDots from './LoadingDots';
import LoginLink from './LoginLink';
import LogoutLink from './LogoutLink';
import AdminLink from './AdminLink';

const Header = ({loading, signOut, auth, user}) => {

let loginLogoutLink = auth.isLogged ? <LogoutLink signOut={signOut} /> : <LoginLink />;
const Header = ({ loading, signOut, auth, user }) => {
let loginLogoutLink = auth.isLogged
? <LogoutLink signOut={signOut} />
: <LoginLink />;
let adminLink = user.isAdmin ? <AdminLink /> : null;

return (
<nav>
<IndexLink to="/" activeClassName="active">Home</IndexLink>
{" | "}
{' | '}
<Link to="/about" activeClassName="active">About</Link>
{" | "}
{' | '}
<Link to="/protected" activeClassName="active">Protected</Link>
{adminLink}
{" | "}
{' | '}
{loginLogoutLink}
{loading && <LoadingDots interval={100} dots={20}/>}
{loading && <LoadingDots interval={100} dots={20} />}
{' | '}
<Link to="/chat" activeClassName="active">Chat Room</Link>
{' | '}
</nav>
);
};
Expand All @@ -29,7 +33,7 @@ Header.propTypes = {
signOut: React.PropTypes.func.isRequired,
auth: React.PropTypes.object.isRequired,
user: React.PropTypes.object.isRequired,
loading: PropTypes.bool.isRequired
loading: PropTypes.bool.isRequired,
};

export default Header;
14 changes: 12 additions & 2 deletions src/components/common/LoadingDots.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React, {PropTypes} from 'react';
import React, { PropTypes } from 'react';
import transform from 'lodash/fp/transform';

class LoadingDots extends React.Component {
constructor(props, context) {
Expand Down Expand Up @@ -26,7 +27,16 @@ class LoadingDots extends React.Component {
text += '.';
dots--;
}
return <span {...this.props}>{text}&nbsp;</span>;

const rightProps = transform({})(
(result, value, key) => (result[key] = value)
)(
Object.keys(this.props).filter(
key => key !== 'interval' && key !== 'dots'
)
);

return <span {...rightProps}>{text}&nbsp;</span>;
}
}

Expand Down
12 changes: 10 additions & 2 deletions src/config.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
// Firebase

export const firebaseConfig = {
apiKey: "AIzaSyAB7gYmtb2DahMCAN25hxC52OWhRxxMSYo",
authDomain: "redux-saga-chat-2814e.firebaseapp.com",
databaseURL: "https://redux-saga-chat-2814e.firebaseio.com",
projectId: "redux-saga-chat-2814e",
storageBucket: "redux-saga-chat-2814e.appspot.com",
messagingSenderId: "920020285472"
};

/*export const firebaseConfig = {
apiKey: "AIzaSyCNsG4Y4pkaVxqM809bpQeZ3wsFgVlCPcg",
authDomain: "react-hot-redux-firebase-start.firebaseapp.com",
databaseURL: "https://react-hot-redux-firebase-start.firebaseio.com",
storageBucket: "react-hot-redux-firebase-start.appspot.com"
};
};*/
2 changes: 2 additions & 0 deletions src/reducers/authReducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,5 @@ export default function authReducer(state = initialState.auth, action) {
return state;
}
}

export const isLogged = state => state.auth.isLogged;
Loading