diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..247f1f0 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,11 @@ +; top-most EditorConfig file +root = true + +; Unix-style newlines +[*] +charset = utf-8 +end_of_line = lf +indent_size = 2 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true diff --git a/.gitignore b/.gitignore index 273e5ef..7fcbb2d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +.DS_Store *.swp *.swo dist/ diff --git a/package.json b/package.json index 2658cfd..f4c9a3e 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ "redux-logger": "^3.0.6", "redux-promise-middleware": "^4.2.0", "redux-thunk": "^2.2.0", + "screenfull": "^3.3.3", "seamless-immutable": "^7.1.2", "simple-peer": "^8.1.0", "socket.io": "^2.1.1", diff --git a/src/client/__mocks__/window.js b/src/client/__mocks__/window.js index e589b8f..a56378b 100644 --- a/src/client/__mocks__/window.js +++ b/src/client/__mocks__/window.js @@ -4,7 +4,18 @@ export const createObjectURL = jest.fn() .mockImplementation(object => 'blob://' + String(object)) export const revokeObjectURL = jest.fn() -export class MediaStream {} +export class MediaStream { + getVideoTracks () { + return [{ + enabled: true + }] + } + getAudioTracks () { + return [{ + enabled: true + }] + } +} export function getUserMedia () { return !getUserMedia.shouldFail ? Promise.resolve(getUserMedia.stream) diff --git a/src/client/actions/ChatActions.js b/src/client/actions/ChatActions.js new file mode 100644 index 0000000..e057a60 --- /dev/null +++ b/src/client/actions/ChatActions.js @@ -0,0 +1,16 @@ +import * as constants from '../constants.js' + +export const addMessage = ({ userId, message, timestamp, image }) => ({ + type: constants.MESSAGE_ADD, + payload: { + userId, + message, + timestamp, + image + } +}) + +export const loadHistory = messages => ({ + type: constants.MESSAGES_HISTORY, + messages +}) diff --git a/src/client/actions/SocketActions.js b/src/client/actions/SocketActions.js index 69c6429..f017d14 100644 --- a/src/client/actions/SocketActions.js +++ b/src/client/actions/SocketActions.js @@ -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) diff --git a/src/client/components/App.js b/src/client/components/App.js index 50db8bf..e742d41 100644 --- a/src/client/components/App.js +++ b/src/client/components/App.js @@ -1,7 +1,8 @@ import Alerts, { AlertPropType } from './Alerts.js' import * as constants from '../constants.js' -import Input from './Input.js' +import Toolbar from './Toolbar.js' import Notifications, { NotificationPropTypes } from './Notifications.js' +import Chat, { MessagePropTypes } from './Chat.js' import PropTypes from 'prop-types' import React from 'react' import Video, { StreamPropType } from './Video.js' @@ -15,11 +16,18 @@ 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, toggleActive: PropTypes.func.isRequired } + constructor () { + super() + this.state = { + videos: {} + } + } componentDidMount () { const { init } = this.props init() @@ -31,34 +39,54 @@ export default class App extends React.PureComponent { dismissAlert, notifications, notify, + messages, peers, sendMessage, toggleActive, streams } = this.props - return (
- - - -
-
-
) + ) } } diff --git a/src/client/components/Chat.js b/src/client/components/Chat.js new file mode 100644 index 0000000..e32f619 --- /dev/null +++ b/src/client/components/Chat.js @@ -0,0 +1,103 @@ +import PropTypes from 'prop-types' +import React from 'react' +import socket from '../socket.js' +import Input from './Input.js' + +export const MessagePropTypes = PropTypes.shape({ + userId: PropTypes.string.isRequired, + message: PropTypes.string.isRequired, + timestamp: PropTypes.string.isRequired, + image: PropTypes.string +}) + +export default class Chat extends React.PureComponent { + static propTypes = { + messages: PropTypes.arrayOf(MessagePropTypes).isRequired, + videos: PropTypes.object.isRequired, + notify: PropTypes.func.isRequired, + sendMessage: PropTypes.func.isRequired, + toolbarRef: PropTypes.object.isRequired + } + handleCloseChat = e => { + const { toolbarRef } = this.props + toolbarRef.chatButton.click() + } + scrollToBottom = () => { + this.chatScroll.scrollTop = this.chatScroll.scrollHeight + } + componentDidMount () { + this.scrollToBottom() + } + componentDidUpdate () { + this.scrollToBottom() + } + render () { + const { messages, videos, notify, sendMessage } = this.props + return ( +
+
+
+
+ +
+
+
Chat
+
+
{ this.chatScroll = div }}> + + {messages.length ? ( + messages.map((message, i) => ( +
+ {message.userId === socket.id ? ( +
+
+ + {message.userId} + + + +

{message.message}

+
+ {message.image ? ( + + ) : ( + + )} +
+ ) : ( +
+ {message.image ? ( + + ) : ( + + )} +
+ + {message.userId} + + + +

{message.message}

+
+
+ )} +
+ )) + ) : ( +
+ +
No Notifications
+
+ )} + +
+ + +
+ ) + } +} diff --git a/src/client/components/Input.js b/src/client/components/Input.js index a5eaaae..adb52df 100644 --- a/src/client/components/Input.js +++ b/src/client/components/Input.js @@ -1,8 +1,10 @@ import PropTypes from 'prop-types' import React from 'react' +import socket from '../socket.js' export default class Input extends React.PureComponent { static propTypes = { + videos: PropTypes.object.isRequired, notify: PropTypes.func.isRequired, sendMessage: PropTypes.func.isRequired } @@ -22,30 +24,85 @@ export default class Input extends React.PureComponent { this.submit() } handleKeyPress = e => { - if (e.key === 'Enter') { + if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault() this.submit() } } + handleSmileClick = e => { + this.setState({ + message: this.textArea.value + e.currentTarget.innerHTML + }) + } submit = () => { - const { notify, sendMessage } = this.props + const { videos, 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 = new Date().toLocaleString('en-US', { + hour: 'numeric', + minute: 'numeric', + hour12: false + }) + let image = null + + // take snapshoot + try { + const video = videos[userId] + if (video) { + const canvas = document.createElement('canvas') + canvas.height = video.videoHeight + canvas.width = video.videoWidth + const avatar = canvas.getContext('2d') + avatar.drawImage(video, 0, 0, canvas.width, canvas.height) + image = canvas.toDataURL() + } + } catch (e) {} + + const payload = { userId, message, timestamp, image } + socket.emit('new_message', payload) + } this.setState({ message: '' }) } render () { const { message } = this.state return ( -
- +