Implemented socket chat

This commit is contained in:
Michael H. Arieli 2018-11-23 16:55:48 -08:00
parent 352a10ab89
commit 0d8d3fbb33
21 changed files with 8067 additions and 244 deletions

1
.gitignore vendored
View File

@ -1,5 +1,4 @@
.DS_Store
yarn.lock
*.swp
*.swo
dist/

View File

@ -43,6 +43,7 @@
"classnames": "^2.2.5",
"config": "^1.26.1",
"express": "^4.13.3",
"moment": "^2.22.2",
"prop-types": "^15.5.10",
"pug": "^2.0.0-rc.2",
"react": "^15.5.4",

View File

@ -0,0 +1,21 @@
import * as constants from '../constants.js'
import _ from 'underscore'
export function addMessage ({ userId, message, timestamp }) {
return {
type: constants.MESSAGE_ADD,
payload: {
id: _.uniqueId('chat'),
userId,
message,
timestamp
}
}
}
export function loadHistory (messages) {
return {
type: constants.MESSAGES_HISTORY,
messages
}
}

View File

@ -1,3 +1,4 @@
import * as ChatActions from '../actions/ChatActions.js'
import * as NotifyActions from '../actions/NotifyActions.js'
import * as PeerActions from '../actions/PeerActions.js'
import * as constants from '../constants.js'
@ -41,6 +42,16 @@ class SocketHandler {
.filter(id => !newUsersMap[id])
.forEach(id => peers[id].destroy())
}
handleMessages = ({ messages }) => {
const { dispatch } = this
debug('socket messages: %o', messages)
dispatch(ChatActions.loadHistory(messages))
}
handleNewMessage = (payload) => {
const { dispatch } = this
debug('socket message: %o', payload)
dispatch(ChatActions.addMessage(payload))
}
}
export function handshake ({ socket, roomName, stream }) {
@ -55,6 +66,8 @@ export function handshake ({ socket, roomName, stream }) {
socket.on(constants.SOCKET_EVENT_SIGNAL, handler.handleSignal)
socket.on(constants.SOCKET_EVENT_USERS, handler.handleUsers)
socket.on(constants.SOCKET_EVENT_MESSAGES, handler.handleMessages)
socket.on(constants.SOCKET_EVENT_NEW_MESSAGE, handler.handleNewMessage)
debug('socket.id: %s', socket.id)
debug('emit ready for room: %s', roomName)

View File

@ -1,8 +1,9 @@
import Alerts, { AlertPropType } from './Alerts.js'
import * as constants from '../constants.js'
import Toolbar from './Toolbar.js'
import Input from './Input.js'
import Notifications, { NotificationPropTypes } from './Notifications.js'
import Chat, { MessagePropTypes } from './Chat.js'
import Input from './Input.js'
import PropTypes from 'prop-types'
import React from 'react'
import Video, { StreamPropType } from './Video.js'
@ -16,6 +17,7 @@ export default class App extends React.PureComponent {
init: PropTypes.func.isRequired,
notifications: PropTypes.objectOf(NotificationPropTypes).isRequired,
notify: PropTypes.func.isRequired,
messages: PropTypes.arrayOf(MessagePropTypes).isRequired,
peers: PropTypes.object.isRequired,
sendMessage: PropTypes.func.isRequired,
streams: PropTypes.objectOf(StreamPropType).isRequired,
@ -32,35 +34,41 @@ export default class App extends React.PureComponent {
dismissAlert,
notifications,
notify,
messages,
peers,
sendMessage,
toggleActive,
streams
} = this.props
return (<div className="app">
<Toolbar stream={streams[constants.ME]} />
<Alerts alerts={alerts} dismiss={dismissAlert} />
<Notifications notifications={notifications} />
<Input notify={notify} sendMessage={sendMessage} />
<div className="videos">
<Video
active={active === constants.ME}
onClick={toggleActive}
stream={streams[constants.ME]}
userId={constants.ME}
/>
{_.map(peers, (_, userId) => (
return (
<div className="app">
<Toolbar stream={streams[constants.ME]} />
<Alerts alerts={alerts} dismiss={dismissAlert} />
<Notifications notifications={notifications} />
<div id="chat">
<Chat messages={messages} />
<Input notify={notify} sendMessage={sendMessage} />
</div>
<div className="videos">
<Video
active={userId === active}
key={userId}
active={active === constants.ME}
onClick={toggleActive}
stream={streams[userId]}
userId={userId}
stream={streams[constants.ME]}
userId={constants.ME}
/>
))}
{_.map(peers, (_, userId) => (
<Video
active={userId === active}
key={userId}
onClick={toggleActive}
stream={streams[userId]}
userId={userId}
/>
))}
</div>
</div>
</div>)
)
}
}

View File

@ -0,0 +1,55 @@
import PropTypes from 'prop-types'
import React from 'react'
import socket from '../socket.js'
export const MessagePropTypes = PropTypes.shape({
userId: PropTypes.string.isRequired,
message: PropTypes.string.isRequired,
timestamp: PropTypes.string.isRequired
})
export default class Chat extends React.PureComponent {
static propTypes = {
messages: PropTypes.arrayOf(MessagePropTypes).isRequired
}
hideChat = e => {
document.getElementById('chat').classList.remove('show')
document.querySelector('.toolbar .chat').classList.remove('on')
}
render () {
const { messages } = this.props
return (
<div>
<div className="chat-header">
<div className="chat-close" onClick={this.hideChat}>
<div className="button button-icon">
<span className="material-icons">arrow_back</span>
</div>
</div>
<div className="chat-title">Chat</div>
</div>
<div className="chat-content">
{messages.length ? (
messages.map((message, i) => (
<div className={message.userId === socket.id ? 'chat-bubble alt' : 'chat-bubble'} key={i}>
<div className="txt">
<p className="name">{message.userId}</p>
<p className="message">{message.message}</p>
<span className="timestamp">{message.timestamp}</span>
</div>
<div className="arrow"></div>
</div>
))
) : (
<div className="chat-empty">
<div className="chat-empty-icon material-icons">chat</div>
<div className="chat-empty-message">No Notifications</div>
</div>
)}
</div>
</div>
)
}
}

View File

@ -1,5 +1,7 @@
import PropTypes from 'prop-types'
import React from 'react'
import moment from 'moment'
import socket from '../socket.js'
export default class Input extends React.PureComponent {
static propTypes = {
@ -30,8 +32,15 @@ export default class Input extends React.PureComponent {
submit = () => {
const { notify, sendMessage } = this.props
const { message } = this.state
notify('You: ' + message)
sendMessage(message)
if (message) {
notify('You: ' + message)
sendMessage(message)
const userId = socket.id;
const timestamp = moment().format('ddd, D MMM HH:mm a');
const payload = { userId, message, timestamp }
socket.emit('new_message', payload)
}
this.setState({ message: '' })
}
render () {

View File

@ -5,6 +5,10 @@ export default class Toolbar extends React.PureComponent {
static propTypes = {
stream: StreamPropType
}
handleChatClick = e => {
document.getElementById('chat').classList.toggle('show')
e.currentTarget.classList.toggle('on')
}
handleMicClick = e => {
const { stream } = this.props
stream.mediaStream.getAudioTracks().forEach(track => {
@ -58,6 +62,12 @@ export default class Toolbar extends React.PureComponent {
return (
<div className="toolbar active">
<div onClick={this.handleChatClick}
className="button chat"
title="Chat"
>
<span className="material-icons">chat</span>
</div>
{stream && (
<div onClick={this.handleMicClick}

View File

@ -17,6 +17,9 @@ export const NOTIFY = 'NOTIFY'
export const NOTIFY_DISMISS = 'NOTIFY_DISMISS'
export const NOTIFY_CLEAR = 'NOTIFY_CLEAR'
export const MESSAGE_ADD = 'MESSAGE_ADD'
export const MESSAGES_HISTORY = 'MESSAGES_HISTORY'
export const PEER_ADD = 'PEER_ADD'
export const PEER_REMOVE = 'PEER_REMOVE'
export const PEERS_DESTROY = 'PEERS_DESTROY'
@ -30,6 +33,8 @@ export const PEER_EVENT_DATA = 'data'
export const SOCKET_EVENT_SIGNAL = 'signal'
export const SOCKET_EVENT_USERS = 'users'
export const SOCKET_EVENT_MESSAGES = 'messages'
export const SOCKET_EVENT_NEW_MESSAGE = 'new_message'
export const STREAM_ADD = 'PEER_STREAM_ADD'
export const STREAM_REMOVE = 'PEER_STREAM_REMOVE'

View File

@ -12,6 +12,7 @@ function mapStateToProps (state) {
peers: state.peers,
alerts: state.alerts,
notifications: state.notifications,
messages: state.messages,
active: state.active
}
}

View File

@ -1,6 +1,7 @@
import active from './active.js'
import alerts from './alerts.js'
import notifications from './notifications.js'
import messages from './messages.js'
import peers from './peers.js'
import streams from './streams.js'
import { combineReducers } from 'redux'
@ -9,6 +10,7 @@ export default combineReducers({
active,
alerts,
notifications,
messages,
peers,
streams
})

View File

@ -0,0 +1,17 @@
import * as constants from '../constants.js'
import Immutable from 'seamless-immutable'
const defaultState = Immutable([])
export default function messages (state = defaultState, action) {
switch (action && action.type) {
case constants.MESSAGE_ADD:
const messages = state.asMutable()
messages.push(action.payload)
return Immutable(messages)
case constants.MESSAGES_HISTORY:
return Immutable(action.messages);
default:
return state
}
}

32
src/scss/_alert.scss Normal file
View File

@ -0,0 +1,32 @@
.alert {
background-color: black;
background-color: rgba(0, 0, 0, 0.3);
left: 0;
opacity: 1;
position: fixed;
right: 0;
text-align: center;
top: 0;
transition: visibility 100ms ease-in, opacity 100ms ease-in;
z-index: 4;
span {
display: inline-block;
margin: 1rem 0;
padding: 0 1rem;
}
button {
line-height: 1.4rem;
border: none;
border-radius: 0.3rem;
color: $color-info;
background-color: $color-fg;
vertical-align: middle;
}
}
.alert.hidden {
opacity: 0;
visibility: hidden;
}

197
src/scss/_chat.scss Normal file
View File

@ -0,0 +1,197 @@
.input {
position: absolute;
right: 10px;
bottom: 30px;
z-index: 2;
width: calc(100% - 30px);
input {
box-shadow: 0px 0px 5px black;
background-color: #333;
border: none;
color: #ccc;
padding: 0.5rem;
font-family: $font-monospace;
}
input[type="text"] {
width: 85%;
}
}
#chat {
background-color: #f2f2f2;
position: fixed;
top: 0;
bottom: 0;
right: 0;
-ms-transform: translateX(100%);
-webkit-transform: translateX(100%);
transform: translateX(100%);
-webkit-transition: -webkit-transform 0.5s cubic-bezier(0.55, 0, 0, 1), box-shadow 0.5s cubic-bezier(0.55, 0, 0, 1);
transition: transform 0.5s cubic-bezier(0.55, 0, 0, 1), box-shadow 0.5s cubic-bezier(0.55, 0, 0, 1);
width: 360px;
z-index: 2;
&.show {
-ms-transform: none;
-webkit-transform: none;
transform: none;
box-shadow: 0 5px 5px 5px rgba(0, 0, 0, 0.19), 0 1px 6px rgba(0, 0, 0, 0.12);
}
@media (max-width: 575.98px) {
.chat {
width: 100% !important;
}
}
.chat-header {
background-color: #333;
color: #fff;
height: 52px;
line-height: 52px;
}
.chat-close {
font-size: 24px;
height: 100%;
line-height: 24px;
padding: 12px;
text-align: center;
float: left;
width: 64px;
cursor: pointer;
}
.chat-button {
float: right;
a {
color: #fff;
}
}
.chat-title {
font-size: 18px;
font-weight: 500;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.chat-content {
background-color: #999;
bottom: 0;
left: 0;
overflow-y: auto;
position: absolute;
right: 0;
top: 52px;
padding: 20px;
}
.chat-empty {
color: #eee;
left: 0;
position: absolute;
right: 0;
text-align: center;
top: 50%;
-ms-transform: translateY(-50%);
-webkit-transform: translateY(-50%);
transform: translateY(-50%);
}
.chat-empty-icon {
font-size: 88px;
}
.chat-empty-message {
font-size: 15px;
font-weight: bold;
}
.chat-bubble {
max-width: 240px;
height: auto;
display: block;
background: #f5f5f5;
border-radius: 4px;
box-shadow: 0px 0px 5px black;
position: relative;
margin: 0 0 25px;
&.alt {
margin: 0 0 25px 60px;
}
&:last-child {
margin-bottom: 80px;
}
.txt {
padding: 8px 55px 8px 14px;
.name {
font-weight: 600;
font-size: 12px;
margin: 0 0 4px;
color: #3498db;
span {
font-weight: normal;
color: #b3b3b3;
}
&.alt {
color: #2ecc71;
}
}
.message {
font-size: 12px;
margin: 0;
color: #2b2b2b;
}
.timestamp {
font-size: 11px;
position: absolute;
bottom: 8px;
right: 10px;
text-transform: uppercase; color: #999
}
}
.arrow {
position: absolute;
width: 0;
bottom:42px;
left: -16px;
height: 0;
&:after {
content: "";
position: absolute;
border: 0 solid transparent;
border-top: 9px solid #f5f5f5;
border-radius: 0 20px 0;
width: 15px;
height: 30px;
transform: rotate(145deg);
}
}
&.alt {
.arrow {
right: -2px;
bottom: 40px;
left: auto;
&:after {
transform: rotate(45deg) scaleY(-1);
}
}
}
}
}

View File

@ -0,0 +1,24 @@
.notifications {
font-family: $font-monospace;
font-size: 10px;
left: 1rem;
position: fixed;
right: 1rem;
text-align: right;
top: 1rem;
z-index: 3;
.notification {
color: $color-info;
padding: 0.25rem;
background-color: rgba(0, 0, 0, 0.2);
}
.notification.error {
color: $color-error;
}
.notification.warning {
color: $color-warning;
}
}

85
src/scss/_toolbar.scss Normal file
View File

@ -0,0 +1,85 @@
.toolbar {
bottom: 20px;
left: 6vw;
position: absolute;
z-index: 3;
/* on icons are hidden by default */
.material-icons {
&.on {
display: none;
}
&.off {
display: block;
}
color: #fff;
position: absolute;
top: 12px;
left: 12px;
}
/* off icons are displayed by default */
/* on icons are displayed when parent svg has class 'on' */
.button {
&.on .material-icons {
&.on {
display: block;
}
&.off {
display: none;
}
}
width: 48px;
height: 48px;
border-radius: 48px;
box-shadow: 2px 2px 24px #444;
display: block;
margin: 0 0 3vh 0;
transform: translateX(calc(-6vw - 96px));
transition: all .1s;
transition-timing-function: ease-in-out;
&:hover {
box-shadow: 4px 4px 48px #666;
cursor: pointer;
}
}
/* off icons are hidden when parent svg has class 'on' */
&.active .button {
transform: translateX(0);
}
.chat {
&:hover, &.on {
background: #407cf7;
}
}
.mute-audio {
&:hover, &.on {
background: #407cf7;
}
}
.mute-video {
&:hover, &.on {
background: #407cf7;
}
}
.fullscreen {
&:hover, &.on {
background: #407cf7;
}
}
.hangup {
&:hover {
background: #dd2c00;
}
}
}

12
src/scss/_variables.scss Normal file
View File

@ -0,0 +1,12 @@
$font-sans-serif: "Helvetica Neue", Helvetica, Arial, sans-serif;
$font-monospace: Menlo, Monaco, Consolas, "Ubuntu Mono", monospace;
$color-bg: #086788;
$color-fg: #07A0C3;
$color-btn: #FFF1D0;
$icon-size: 48px;
$color-primary: white;
$color-info: #31EF40;
$color-warning: #F0C808;
$color-error: #EE7600;

46
src/scss/_video.scss Normal file
View File

@ -0,0 +1,46 @@
.videos {
position: fixed;
height: 100px;
bottom: 15px;
right: 0px;
text-align: right;
$video-size: 100px;
.video-container {
background-color: black;
box-shadow: 0px 0px 5px black;
border-radius: 10px;
display: inline-block;
margin-right: 10px;
width: $video-size;
height: 100%;
z-index: 2;
video {
border-radius: 10px;
cursor: pointer;
object-fit: cover;
width: 100%;
height: 100%;
}
}
.video-container.active {
background-color: transparent;
box-shadow: none;
border-radius: 0;
position: fixed;
width: 100%;
left: 0;
right: 0;
top: 0;
bottom: 0;
z-index: -1;
video {
border-radius: 0;
cursor: inherit;
}
}
}

View File

@ -1,18 +1,6 @@
@import './variables';
@import url("https://fonts.googleapis.com/icon?family=Material+Icons");
$font-sans-serif: "Helvetica Neue", Helvetica, Arial, sans-serif;
$font-monospace: Menlo, Monaco, Consolas, "Ubuntu Mono", monospace;
$color-bg: #086788;
$color-fg: #07A0C3;
$color-btn: #FFF1D0;
$icon-size: 48px;
$color-primary: white;
$color-info: #31EF40;
$color-warning: #F0C808;
$color-error: #EE7600;
*,
*:before,
*:after {
@ -150,212 +138,11 @@ body.call {
}
.app {
.alert {
background-color: black;
background-color: rgba(0, 0, 0, 0.3);
left: 0;
opacity: 1;
position: fixed;
right: 0;
text-align: center;
top: 0;
transition: visibility 100ms ease-in, opacity 100ms ease-in;
z-index: 4;
span {
display: inline-block;
margin: 1rem 0;
padding: 0 1rem;
}
button {
line-height: 1.4rem;
border: none;
border-radius: 0.3rem;
color: $color-info;
background-color: $color-fg;
vertical-align: middle;
}
}
.alert.hidden {
opacity: 0;
visibility: hidden;
}
.notifications {
font-family: $font-monospace;
font-size: 10px;
left: 1rem;
position: fixed;
right: 1rem;
text-align: right;
top: 1rem;
z-index: 3;
.notification {
color: $color-info;
padding: 0.25rem;
background-color: rgba(0, 0, 0, 0.2);
}
.notification.error {
color: $color-error;
}
.notification.warning {
color: $color-warning;
}
}
.videos {
position: fixed;
height: 100px;
bottom: 15px;
right: 0px;
text-align: right;
$video-size: 100px;
.video-container {
background-color: black;
box-shadow: 0px 0px 5px black;
border-radius: 10px;
display: inline-block;
margin-right: 10px;
width: $video-size;
height: 100%;
z-index: 2;
video {
border-radius: 10px;
cursor: pointer;
object-fit: cover;
width: 100%;
height: 100%;
}
}
.video-container.active {
background-color: transparent;
box-shadow: none;
border-radius: 0;
position: fixed;
width: 100%;
left: 0;
right: 0;
top: 0;
bottom: 0;
z-index: -1;
video {
border-radius: 0;
cursor: inherit;
}
}
}
.input {
position: fixed;
left: 10pxpx;
bottom: 15px;
z-index: 3;
input {
box-shadow: 0px 0px 5px black;
// background-color: black;
background-color: #333;
border: none;
color: #ccc;
padding: 0.5rem;
font-family: $font-monospace;
}
input[type="submit"] {
}
}
.toolbar {
bottom: 80px;
left: 6vw;
position: absolute;
z-index: 1;
/* on icons are hidden by default */
.material-icons {
&.on {
display: none;
}
&.off {
display: block;
}
color: #fff;
position: absolute;
top: 12px;
left: 12px;
}
/* off icons are displayed by default */
/* on icons are displayed when parent svg has class 'on' */
.button {
&.on .material-icons {
&.on {
display: block;
}
&.off {
display: none;
}
}
width: 48px;
height: 48px;
border-radius: 48px;
box-shadow: 2px 2px 24px #444;
display: block;
margin: 0 0 3vh 0;
transform: translateX(calc(-6vw - 96px));
transition: all .1s;
transition-timing-function: ease-in-out;
&:hover {
box-shadow: 4px 4px 48px #666;
}
}
/* off icons are hidden when parent svg has class 'on' */
&.active .button {
transform: translateX(0);
}
.mute-audio {
&:hover, &.on {
background: #407cf7;
}
}
.mute-video {
&:hover, &.on {
background: #407cf7;
}
}
.fullscreen {
&:hover, &.on {
background: #407cf7;
}
}
.hangup {
&:hover {
background: #dd2c00;
}
}
}
@import './alert';
@import './notification';
@import './video';
@import './chat';
@import './toolbar';
}
.fade-enter {

View File

@ -2,6 +2,8 @@
const debug = require('debug')('peer-calls:socket')
const _ = require('underscore')
const messages = {};
module.exports = function (socket, io) {
socket.on('signal', payload => {
// debug('signal: %s, payload: %o', socket.id, payload);
@ -11,6 +13,11 @@ module.exports = function (socket, io) {
})
})
socket.on('new_message', payload => {
addMesssage(socket.room, payload);
io.to(socket.room).emit('new_message', payload)
})
socket.on('ready', roomName => {
debug('ready: %s, room: %s', socket.id, roomName)
if (socket.room) socket.leave(socket.room)
@ -19,11 +26,13 @@ module.exports = function (socket, io) {
socket.room = roomName
let users = getUsers(roomName)
debug('ready: %s, room: %s, users: %o', socket.id, roomName, users)
let messages = getMesssages(roomName)
debug('ready: %s, room: %s, users: %o, messages: %o', socket.id, roomName, users, messages)
io.to(roomName).emit('users', {
initiator: socket.id,
users
})
io.to(roomName).emit('messages', { messages })
})
function getUsers (roomName) {
@ -31,4 +40,15 @@ module.exports = function (socket, io) {
return { id }
})
}
function getMesssages (roomName) {
if (_.isUndefined(messages[roomName])) {
messages[roomName] = [];
}
return messages[roomName]
}
function addMesssage (roomName, payload) {
getMesssages(roomName).push(payload)
}
}

7479
yarn.lock Normal file

File diff suppressed because it is too large Load Diff