Skip to content

Instantly share code, notes, and snippets.

@bradleyboy
Last active April 17, 2016 22:00
Show Gist options
  • Save bradleyboy/982aeee733bafde4012c to your computer and use it in GitHub Desktop.
Save bradleyboy/982aeee733bafde4012c to your computer and use it in GitHub Desktop.
Chatpen

Chatpen

Multiuser chat built with React and Firebase. Note that this is on the Firebase free plan, so if more than 50 users are on it things may go south.

A Pen by Brad Daily on CodePen.

License.

<div id="app"></div>
<!--
This demo shows just how fast you can get up and running with React and Firebase. We have a nicely equipped chat in ~ 300 lines of generousely spaced and commented ES6 JavaScript.
Dependencies:
React
Firebase
moment
markdown-it
-->
/**
* Build up Firebase references
*/
const _fbBase = new Firebase('https://amber-heat-810.firebaseio.com');
const _fbMessages = _fbBase.child('messages');
const _fbUsers = _fbBase.child('users');
/**
* Local cache of users in a Map
* Used to track who is here.
*/
const _users = new Map();
/**
* mardown-it instance to parse chat messages
*/
const _md = window.markdownit({
linkify: true,
});
/**
* Message Component
* Renders an individual message or event in the list
*/
class Message extends React.Component {
render() {
/*
* Run message through markdown-it.
* dangerouslySetInnerHTML is necessary b/c React escapes HTML by defaukt
*/
const messageText = <div dangerouslySetInnerHTML={{__html: _md.render(this.props.message.text)}} />;
/**
* Event messages
* User has joined/left room
*/
if (this.props.message.type === 'event') {
// Buggy, removing for now.
return null;
// return <div className="message message-event">
// {messageText}
// </div>;
}
return <div className="message">
<img width="48" height="48" src={this.props.message.user_avatar} />
<div className="message-text">
<div className="message-username">
<a target="_blank" href={'https://twitter.com/' + this.props.message.username}>{'@' + this.props.message.username}</a>
{' '}
<span className="message-time">
{moment(this.props.message.timestamp).format('h:mma')}
</span>
</div>
{messageText}
</div>
</div>;
}
}
/**
* MessageList Component
* Renders the list of chat messages
*/
class MessageList extends React.Component {
/**
* Anytime new data comes in, scroll to bottom
* TODO: Could be more intelligent here and only
* scroll when user is not browsing back up the list.
*/
componentDidUpdate() {
let list = this.refs.messageList.getDOMNode();
list.scrollTop = list.scrollHeight;
}
render() {
return <div ref="messageList" className="message-list">
{this.props.messages.map((message, index) => <Message
key={index}
message={message}
currentUser={message.userID === this.props.currentUserId} />)}
</div>;
}
}
class UserList extends React.Component {
render() {
return <div className="user-list-container">
<h2>{this.props.users.length ? this.props.users.length : 'No'} user{this.props.users.length !== 1 ? 's' : ''} chatting</h2>
<div className="user-list">
{this.props.users.map((user) => <UserListItem user={user} />)}
</div>
</div>;
}
}
class UserListItem extends React.Component {
render() {
return <div className="user-list-item">
<img width="24" height="24" src={this.props.user.twitter.cachedUserProfile.profile_image_url_https} />
{this.props.user._isCurrentUser ? 'You' : '@' + this.props.user.twitter.username}
</div>;
}
}
/**
* MessageForm Component
* Form input for adding new messages
*/
class MessageForm extends React.Component {
componentDidMount() {
this.refs.messageText.getDOMNode().focus();
}
/**
* Submit the new Message
* @param {object} event - onsubmit event
*/
_onSubmit(event) {
event.preventDefault();
const user = _users.get(this.props.userId);
const message = this.refs.messageText.getDOMNode().value.trim();
if (!message.length) {
return;
}
_fbMessages.push({
type: "userMessage",
user_avatar: user.twitter.cachedUserProfile.profile_image_url_https,
username: user.twitter.username,
text: this.refs.messageText.getDOMNode().value,
timestamp: +new Date,
});
event.target.reset();
}
render() {
return <form onSubmit={this._onSubmit.bind(this)}>
<input ref="messageText" type="text" placeholder="Type your message. Be nice!" />
</form>;
}
}
/**
* RegisterForm Component
* Renders the registration form when a user enters the chat window
*/
class RegisterForm extends React.Component {
render() {
return <form onSubmit={this.props.onSubmit}>
<button type="submit">Login with Twitter</button>
</form>;
}
}
/**
* Application Component
* Main component
*/
class Application extends React.Component {
constructor() {
this._messages = [];
this._username = null;
this.state = {
messages: [],
user: null,
users: [],
userId: null,
joinError: null,
};
if (location.href.indexOf('fullpage') !== -1) {
this.state.joinError = 'View this pen in the Editor in order to join the chat. Codepen\'s fullpage view blocks popups, so Twitter authentication does not work.';
}
this._join = this._join.bind(this);
this._createUsersArray = this._createUsersArray.bind(this);
}
componentWillMount() {
/**
* onAuth fires anytime the auth status of this user changes
*/
_fbBase.onAuth((authData) => {
if (!authData) {
return;
}
this._username = authData.twitter.username;
_fbUsers.push(authData);
this.setState({
user: this._username,
joinError: null,
});
});
/**
* Faster to call once.value, then listen to
* new children after that
*/
_fbMessages.orderByKey().once('value', (dataSnapshot) => {
dataSnapshot.forEach((snapshot) => {
this._messages.push(snapshot.val());
});
this.setState({messages: this._messages});
const last = this._messages[this._messages.length - 1];
_fbMessages
.orderByChild('timestamp')
.startAt(last.timestamp + 1)
.on('child_added', (snapshot) => {
this._messages.push(snapshot.val());
this.setState({messages: this._messages});
});
});
_fbUsers.on('child_added', (snapshot) => {
const key = snapshot.key();
const userObj = snapshot.val();
_users.set(key, snapshot.val());
this._createUsersArray();
if (userObj.twitter.username === this._username) {
this.setState({userId: key});
const disconnectListener = _fbUsers.child(key);
disconnectListener.onDisconnect().remove();
}
});
_fbUsers.on('child_removed', (snapshot) => {
_users.delete(snapshot.key());
this._createUsersArray();
});
}
_createUsersArray() {
let users = [];
_users.forEach((user, key) => {
user._isCurrentUser = user.twitter.username === this._username;
users.push(user)
});
this.setState({users});
}
/**
* Add the user to the chat
* @param {object} event - the onsubmit event
*/
_join(event) {
event.preventDefault();
_fbBase.authWithOAuthPopup("twitter", (error, authData) => {
if (error) {
this.setState({joinError: `Error authenticating with Twitter: ${error.message}`});
} else {
_fbMessages.push({
type: "event",
text: `@${authData.twitter.username} has joined the room`,
});
}
}, {
remember: "sessionOnly"
});
}
render() {
return <div className="wrapper">
<h1>
CHATPEN
<div className="header-info">
{this.state.user ?
`Chatting as @${this.state.user}` :
<RegisterForm
ref="registerForm"
onSubmit={this._join} />}
</div>
</h1>
<div className="main-window">
<div className="message-list-container">
<MessageList
messages={this.state.messages}
currentUserId={this.state.userId} />
{this.state.userId ?
<MessageForm userId={this.state.userId} /> : null}
</div>
<UserList users={this.state.users} />
</div>
{this.state.joinError ?
<p className="register-error">{this.state.joinError}</p> : null}
</div>;
}
}
/**
* Get this party started!
*/
React.render(<Application />, document.getElementById('app'));
*
box-sizing: border-box
html, body, #app
height: 100%
overflow: hidden
body
font-family: "Open Sans"
div.wrapper
display: flex
flex-flow: column
height: 100%
div.main-window
display: flex
flex: 1
div.user-list-container
flex: 1
display: flex
flex-flow: column
border-left: 1px solid #ddd
h2
padding: 8px
font-size: 12px
text-align: center
color: darken(white, 50%)
border-bottom: 1px solid darken(white, 4%)
.user-list
flex: 1
overflow-y: auto
.user-list-item
display: flex
align-items: center
font-size: 12px
img
margin: 5px
border-radius: 50%
div.message-list-container
background: darken(white, 4%)
font-size: 13px
flex: 2
display: flex
flex-flow: column
.message-list
flex: 1
overflow-y: auto
.message
display: flex
align-items: center
padding: 0 10px
img
border-radius: 50%
.message-text
display: flex
flex-flow: column
flex: 3
line-height: 1.3em
padding: 10px
strong
font-weight: bold
em
font-style: italic
a
color: #336633
.message-username
font-weight: bold
font-size: 14px
margin-bottom: 4px
a
text-decoration: none
color: black
span.message-time
font-weight: normal
font-size: 12px
color: #999
.message-event
display: flex
justify-content: center
text-align: center
color: #aaa
width: 100%
font-size: 12px
padding: 10px
.register-error
background: red
padding: 10px 10px 10px 15px
font-size: 12px
color: white
form
display: flex
justify-content: center
input
flex: 4
border: 0
padding: 10px
border-top: 1px solid #ddd
&:focus
outline: 0
button
border: 0
background: #55acee
color: white
font-size: 12px
padding: 10px 10px 10px 34px
font-family: 'Helvetica Neue'
border-radius: 5px
cursor: pointer
font-weight: bold
background-image: url(//g.twimg.com/Twitter_logo_white.png)
background-position: 10px 50%
background-size: auto 14px
background-repeat: no-repeat
.wrapper > h1
padding: 20px
background: #222
color: #eee
display: flex
justify-content: space-between
align-items: center
font-weight: bold
.header-info
font-size: 11px
color: #ddd
font-weight: normal
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment