Implemented socket chat
This commit is contained in:
parent
352a10ab89
commit
0d8d3fbb33
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,5 +1,4 @@
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
yarn.lock
|
|
||||||
*.swp
|
*.swp
|
||||||
*.swo
|
*.swo
|
||||||
dist/
|
dist/
|
||||||
|
|||||||
@ -43,6 +43,7 @@
|
|||||||
"classnames": "^2.2.5",
|
"classnames": "^2.2.5",
|
||||||
"config": "^1.26.1",
|
"config": "^1.26.1",
|
||||||
"express": "^4.13.3",
|
"express": "^4.13.3",
|
||||||
|
"moment": "^2.22.2",
|
||||||
"prop-types": "^15.5.10",
|
"prop-types": "^15.5.10",
|
||||||
"pug": "^2.0.0-rc.2",
|
"pug": "^2.0.0-rc.2",
|
||||||
"react": "^15.5.4",
|
"react": "^15.5.4",
|
||||||
|
|||||||
21
src/client/actions/ChatActions.js
Normal file
21
src/client/actions/ChatActions.js
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,3 +1,4 @@
|
|||||||
|
import * as ChatActions from '../actions/ChatActions.js'
|
||||||
import * as NotifyActions from '../actions/NotifyActions.js'
|
import * as NotifyActions from '../actions/NotifyActions.js'
|
||||||
import * as PeerActions from '../actions/PeerActions.js'
|
import * as PeerActions from '../actions/PeerActions.js'
|
||||||
import * as constants from '../constants.js'
|
import * as constants from '../constants.js'
|
||||||
@ -41,6 +42,16 @@ class SocketHandler {
|
|||||||
.filter(id => !newUsersMap[id])
|
.filter(id => !newUsersMap[id])
|
||||||
.forEach(id => peers[id].destroy())
|
.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 }) {
|
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_SIGNAL, handler.handleSignal)
|
||||||
socket.on(constants.SOCKET_EVENT_USERS, handler.handleUsers)
|
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('socket.id: %s', socket.id)
|
||||||
debug('emit ready for room: %s', roomName)
|
debug('emit ready for room: %s', roomName)
|
||||||
|
|||||||
@ -1,8 +1,9 @@
|
|||||||
import Alerts, { AlertPropType } from './Alerts.js'
|
import Alerts, { AlertPropType } from './Alerts.js'
|
||||||
import * as constants from '../constants.js'
|
import * as constants from '../constants.js'
|
||||||
import Toolbar from './Toolbar.js'
|
import Toolbar from './Toolbar.js'
|
||||||
import Input from './Input.js'
|
|
||||||
import Notifications, { NotificationPropTypes } from './Notifications.js'
|
import Notifications, { NotificationPropTypes } from './Notifications.js'
|
||||||
|
import Chat, { MessagePropTypes } from './Chat.js'
|
||||||
|
import Input from './Input.js'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import Video, { StreamPropType } from './Video.js'
|
import Video, { StreamPropType } from './Video.js'
|
||||||
@ -16,6 +17,7 @@ export default class App extends React.PureComponent {
|
|||||||
init: PropTypes.func.isRequired,
|
init: PropTypes.func.isRequired,
|
||||||
notifications: PropTypes.objectOf(NotificationPropTypes).isRequired,
|
notifications: PropTypes.objectOf(NotificationPropTypes).isRequired,
|
||||||
notify: PropTypes.func.isRequired,
|
notify: PropTypes.func.isRequired,
|
||||||
|
messages: PropTypes.arrayOf(MessagePropTypes).isRequired,
|
||||||
peers: PropTypes.object.isRequired,
|
peers: PropTypes.object.isRequired,
|
||||||
sendMessage: PropTypes.func.isRequired,
|
sendMessage: PropTypes.func.isRequired,
|
||||||
streams: PropTypes.objectOf(StreamPropType).isRequired,
|
streams: PropTypes.objectOf(StreamPropType).isRequired,
|
||||||
@ -32,35 +34,41 @@ export default class App extends React.PureComponent {
|
|||||||
dismissAlert,
|
dismissAlert,
|
||||||
notifications,
|
notifications,
|
||||||
notify,
|
notify,
|
||||||
|
messages,
|
||||||
peers,
|
peers,
|
||||||
sendMessage,
|
sendMessage,
|
||||||
toggleActive,
|
toggleActive,
|
||||||
streams
|
streams
|
||||||
} = this.props
|
} = this.props
|
||||||
|
|
||||||
return (<div className="app">
|
return (
|
||||||
<Toolbar stream={streams[constants.ME]} />
|
<div className="app">
|
||||||
<Alerts alerts={alerts} dismiss={dismissAlert} />
|
<Toolbar stream={streams[constants.ME]} />
|
||||||
<Notifications notifications={notifications} />
|
<Alerts alerts={alerts} dismiss={dismissAlert} />
|
||||||
<Input notify={notify} sendMessage={sendMessage} />
|
<Notifications notifications={notifications} />
|
||||||
<div className="videos">
|
<div id="chat">
|
||||||
<Video
|
<Chat messages={messages} />
|
||||||
active={active === constants.ME}
|
<Input notify={notify} sendMessage={sendMessage} />
|
||||||
onClick={toggleActive}
|
</div>
|
||||||
stream={streams[constants.ME]}
|
<div className="videos">
|
||||||
userId={constants.ME}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{_.map(peers, (_, userId) => (
|
|
||||||
<Video
|
<Video
|
||||||
active={userId === active}
|
active={active === constants.ME}
|
||||||
key={userId}
|
|
||||||
onClick={toggleActive}
|
onClick={toggleActive}
|
||||||
stream={streams[userId]}
|
stream={streams[constants.ME]}
|
||||||
userId={userId}
|
userId={constants.ME}
|
||||||
/>
|
/>
|
||||||
))}
|
|
||||||
|
{_.map(peers, (_, userId) => (
|
||||||
|
<Video
|
||||||
|
active={userId === active}
|
||||||
|
key={userId}
|
||||||
|
onClick={toggleActive}
|
||||||
|
stream={streams[userId]}
|
||||||
|
userId={userId}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
55
src/client/components/Chat.js
Normal file
55
src/client/components/Chat.js
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,5 +1,7 @@
|
|||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import moment from 'moment'
|
||||||
|
import socket from '../socket.js'
|
||||||
|
|
||||||
export default class Input extends React.PureComponent {
|
export default class Input extends React.PureComponent {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
@ -30,8 +32,15 @@ export default class Input extends React.PureComponent {
|
|||||||
submit = () => {
|
submit = () => {
|
||||||
const { notify, sendMessage } = this.props
|
const { notify, sendMessage } = this.props
|
||||||
const { message } = this.state
|
const { message } = this.state
|
||||||
notify('You: ' + message)
|
if (message) {
|
||||||
sendMessage(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: '' })
|
this.setState({ message: '' })
|
||||||
}
|
}
|
||||||
render () {
|
render () {
|
||||||
|
|||||||
@ -5,6 +5,10 @@ export default class Toolbar extends React.PureComponent {
|
|||||||
static propTypes = {
|
static propTypes = {
|
||||||
stream: StreamPropType
|
stream: StreamPropType
|
||||||
}
|
}
|
||||||
|
handleChatClick = e => {
|
||||||
|
document.getElementById('chat').classList.toggle('show')
|
||||||
|
e.currentTarget.classList.toggle('on')
|
||||||
|
}
|
||||||
handleMicClick = e => {
|
handleMicClick = e => {
|
||||||
const { stream } = this.props
|
const { stream } = this.props
|
||||||
stream.mediaStream.getAudioTracks().forEach(track => {
|
stream.mediaStream.getAudioTracks().forEach(track => {
|
||||||
@ -58,6 +62,12 @@ export default class Toolbar extends React.PureComponent {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="toolbar active">
|
<div className="toolbar active">
|
||||||
|
<div onClick={this.handleChatClick}
|
||||||
|
className="button chat"
|
||||||
|
title="Chat"
|
||||||
|
>
|
||||||
|
<span className="material-icons">chat</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
{stream && (
|
{stream && (
|
||||||
<div onClick={this.handleMicClick}
|
<div onClick={this.handleMicClick}
|
||||||
|
|||||||
@ -17,6 +17,9 @@ export const NOTIFY = 'NOTIFY'
|
|||||||
export const NOTIFY_DISMISS = 'NOTIFY_DISMISS'
|
export const NOTIFY_DISMISS = 'NOTIFY_DISMISS'
|
||||||
export const NOTIFY_CLEAR = 'NOTIFY_CLEAR'
|
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_ADD = 'PEER_ADD'
|
||||||
export const PEER_REMOVE = 'PEER_REMOVE'
|
export const PEER_REMOVE = 'PEER_REMOVE'
|
||||||
export const PEERS_DESTROY = 'PEERS_DESTROY'
|
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_SIGNAL = 'signal'
|
||||||
export const SOCKET_EVENT_USERS = 'users'
|
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_ADD = 'PEER_STREAM_ADD'
|
||||||
export const STREAM_REMOVE = 'PEER_STREAM_REMOVE'
|
export const STREAM_REMOVE = 'PEER_STREAM_REMOVE'
|
||||||
|
|||||||
@ -12,6 +12,7 @@ function mapStateToProps (state) {
|
|||||||
peers: state.peers,
|
peers: state.peers,
|
||||||
alerts: state.alerts,
|
alerts: state.alerts,
|
||||||
notifications: state.notifications,
|
notifications: state.notifications,
|
||||||
|
messages: state.messages,
|
||||||
active: state.active
|
active: state.active
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import active from './active.js'
|
import active from './active.js'
|
||||||
import alerts from './alerts.js'
|
import alerts from './alerts.js'
|
||||||
import notifications from './notifications.js'
|
import notifications from './notifications.js'
|
||||||
|
import messages from './messages.js'
|
||||||
import peers from './peers.js'
|
import peers from './peers.js'
|
||||||
import streams from './streams.js'
|
import streams from './streams.js'
|
||||||
import { combineReducers } from 'redux'
|
import { combineReducers } from 'redux'
|
||||||
@ -9,6 +10,7 @@ export default combineReducers({
|
|||||||
active,
|
active,
|
||||||
alerts,
|
alerts,
|
||||||
notifications,
|
notifications,
|
||||||
|
messages,
|
||||||
peers,
|
peers,
|
||||||
streams
|
streams
|
||||||
})
|
})
|
||||||
|
|||||||
17
src/client/reducers/messages.js
Normal file
17
src/client/reducers/messages.js
Normal 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
32
src/scss/_alert.scss
Normal 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
197
src/scss/_chat.scss
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
24
src/scss/_notification.scss
Normal file
24
src/scss/_notification.scss
Normal 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
85
src/scss/_toolbar.scss
Normal 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
12
src/scss/_variables.scss
Normal 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
46
src/scss/_video.scss
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,18 +1,6 @@
|
|||||||
|
@import './variables';
|
||||||
@import url("https://fonts.googleapis.com/icon?family=Material+Icons");
|
@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,
|
*:before,
|
||||||
*:after {
|
*:after {
|
||||||
@ -150,212 +138,11 @@ body.call {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.app {
|
.app {
|
||||||
|
@import './alert';
|
||||||
.alert {
|
@import './notification';
|
||||||
background-color: black;
|
@import './video';
|
||||||
background-color: rgba(0, 0, 0, 0.3);
|
@import './chat';
|
||||||
left: 0;
|
@import './toolbar';
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.fade-enter {
|
.fade-enter {
|
||||||
|
|||||||
@ -2,6 +2,8 @@
|
|||||||
const debug = require('debug')('peer-calls:socket')
|
const debug = require('debug')('peer-calls:socket')
|
||||||
const _ = require('underscore')
|
const _ = require('underscore')
|
||||||
|
|
||||||
|
const messages = {};
|
||||||
|
|
||||||
module.exports = function (socket, io) {
|
module.exports = function (socket, io) {
|
||||||
socket.on('signal', payload => {
|
socket.on('signal', payload => {
|
||||||
// debug('signal: %s, payload: %o', socket.id, 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 => {
|
socket.on('ready', roomName => {
|
||||||
debug('ready: %s, room: %s', socket.id, roomName)
|
debug('ready: %s, room: %s', socket.id, roomName)
|
||||||
if (socket.room) socket.leave(socket.room)
|
if (socket.room) socket.leave(socket.room)
|
||||||
@ -19,11 +26,13 @@ module.exports = function (socket, io) {
|
|||||||
socket.room = roomName
|
socket.room = roomName
|
||||||
|
|
||||||
let users = getUsers(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', {
|
io.to(roomName).emit('users', {
|
||||||
initiator: socket.id,
|
initiator: socket.id,
|
||||||
users
|
users
|
||||||
})
|
})
|
||||||
|
io.to(roomName).emit('messages', { messages })
|
||||||
})
|
})
|
||||||
|
|
||||||
function getUsers (roomName) {
|
function getUsers (roomName) {
|
||||||
@ -31,4 +40,15 @@ module.exports = function (socket, io) {
|
|||||||
return { id }
|
return { id }
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getMesssages (roomName) {
|
||||||
|
if (_.isUndefined(messages[roomName])) {
|
||||||
|
messages[roomName] = [];
|
||||||
|
}
|
||||||
|
return messages[roomName]
|
||||||
|
}
|
||||||
|
|
||||||
|
function addMesssage (roomName, payload) {
|
||||||
|
getMesssages(roomName).push(payload)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user