Refactor all components
This commit is contained in:
parent
69122466b1
commit
4fa6a0d17a
63
package-lock.json
generated
63
package-lock.json
generated
@ -1949,6 +1949,12 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"@types/classnames": {
|
||||
"version": "2.2.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/classnames/-/classnames-2.2.9.tgz",
|
||||
"integrity": "sha512-MNl+rT5UmZeilaPxAVs6YaPC2m6aA8rofviZbhbxpPpl61uKodfdQVsBtgJGTqGizEf02oW3tsVe7FYB8kK14A==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/config": {
|
||||
"version": "0.0.36",
|
||||
"resolved": "https://registry.npmjs.org/@types/config/-/config-0.0.36.tgz",
|
||||
@ -2142,6 +2148,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"@types/react-transition-group": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.2.3.tgz",
|
||||
"integrity": "sha512-Hk8jiuT7iLOHrcjKP/ZVSyCNXK73wJAUz60xm0mVhiRujrdiI++j4duLiL282VGxwAgxetHQFfqA29LgEeSkFA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"@types/redux": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/redux/-/redux-3.6.0.tgz",
|
||||
@ -2183,6 +2198,15 @@
|
||||
"redux": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"@types/screenfull": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/screenfull/-/screenfull-4.1.0.tgz",
|
||||
"integrity": "sha512-TuFSOzuzGFVCdQmwp+a0/Qim+nJhclaWha18yK/xmjrwdPI7qrfeyoSyPx8MoJP8X5d2MwB4a6d2sZhUjJwz9A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"screenfull": "*"
|
||||
}
|
||||
},
|
||||
"@types/serve-static": {
|
||||
"version": "1.13.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.3.tgz",
|
||||
@ -3862,11 +3886,6 @@
|
||||
"lazy-cache": "^1.0.3"
|
||||
}
|
||||
},
|
||||
"chain-function": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/chain-function/-/chain-function-1.0.1.tgz",
|
||||
"integrity": "sha512-SxltgMwL9uCko5/ZCLiyG2B7R9fY4pDZUw7hJ4MhirdjBLosoDqkWABi3XMucddHdLiFJMb7PD2MZifZriuMTg=="
|
||||
},
|
||||
"chalk": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
|
||||
@ -4733,6 +4752,7 @@
|
||||
"version": "3.4.0",
|
||||
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-3.4.0.tgz",
|
||||
"integrity": "sha512-LnuPJ+dwqKDIyotW1VzmOZ5TONUN7CwkCR5hrgawTUbkBGYdeoNLZo6nNfGkCrjtE1nXXaj7iMMpDa8/d9WoIA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.1.2"
|
||||
}
|
||||
@ -11762,6 +11782,12 @@
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.6.3.tgz",
|
||||
"integrity": "sha512-u7FDWtthB4rWibG/+mFbVd5FvdI20yde86qKGx4lVUTWmPlSWQ4QxbBIrrs+HnXGbxOUlUzTAP/VDmvCwaP2yA=="
|
||||
},
|
||||
"react-lifecycles-compat": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz",
|
||||
"integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==",
|
||||
"dev": true
|
||||
},
|
||||
"react-redux": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-6.0.0.tgz",
|
||||
@ -11776,15 +11802,15 @@
|
||||
}
|
||||
},
|
||||
"react-transition-group": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-1.2.1.tgz",
|
||||
"integrity": "sha512-CWaL3laCmgAFdxdKbhhps+c0HRGF4c+hdM4H23+FI1QBNUyx/AMeIJGWorehPNSaKnQNOAxL7PQmqMu78CDj3Q==",
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-2.5.1.tgz",
|
||||
"integrity": "sha512-8x/CxUL9SjYFmUdzsBPTgtKeCxt7QArjNSte0wwiLtF/Ix/o1nWNJooNy5o9XbHIKS31pz7J5VF2l41TwlvbHQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"chain-function": "^1.0.0",
|
||||
"dom-helpers": "^3.2.0",
|
||||
"loose-envify": "^1.3.1",
|
||||
"prop-types": "^15.5.6",
|
||||
"warning": "^3.0.0"
|
||||
"dom-helpers": "^3.3.1",
|
||||
"loose-envify": "^1.4.0",
|
||||
"prop-types": "^15.6.2",
|
||||
"react-lifecycles-compat": "^3.0.4"
|
||||
}
|
||||
},
|
||||
"read-only-stream": {
|
||||
@ -12604,7 +12630,8 @@
|
||||
"screenfull": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/screenfull/-/screenfull-3.3.3.tgz",
|
||||
"integrity": "sha512-DzYUuXr+OV2BDvYXaYzlYgJd4WXZZ2CW5NFC7Kw6TUCpzXJAx4MwlVD6CH+Mu6fi8rfAQIQfqdFZ4jtDsEkWig=="
|
||||
"integrity": "sha512-DzYUuXr+OV2BDvYXaYzlYgJd4WXZZ2CW5NFC7Kw6TUCpzXJAx4MwlVD6CH+Mu6fi8rfAQIQfqdFZ4jtDsEkWig==",
|
||||
"dev": true
|
||||
},
|
||||
"scss-tokenizer": {
|
||||
"version": "0.2.3",
|
||||
@ -14320,14 +14347,6 @@
|
||||
"makeerror": "1.0.x"
|
||||
}
|
||||
},
|
||||
"warning": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/warning/-/warning-3.0.0.tgz",
|
||||
"integrity": "sha1-MuU3fLVy3kqwR1O9+IIcAe1gW3w=",
|
||||
"requires": {
|
||||
"loose-envify": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"watchify": {
|
||||
"version": "3.11.1",
|
||||
"resolved": "https://registry.npmjs.org/watchify/-/watchify-3.11.1.tgz",
|
||||
|
||||
@ -60,12 +60,10 @@
|
||||
"react": "^16.6.3",
|
||||
"react-dom": "^16.6.3",
|
||||
"react-redux": "^6.0.0",
|
||||
"react-transition-group": "^1.2.1",
|
||||
"redux": "^4.0.1",
|
||||
"redux-logger": "^3.0.6",
|
||||
"redux-promise-middleware": "^5.1.1",
|
||||
"redux-thunk": "^2.2.0",
|
||||
"screenfull": "^3.3.3",
|
||||
"seamless-immutable": "^7.1.2",
|
||||
"simple-peer": "^9.1.2",
|
||||
"socket.io": "^2.2.0",
|
||||
@ -81,6 +79,7 @@
|
||||
"@babel/preset-env": "^7.5.0",
|
||||
"@babel/preset-react": "^7.0.0",
|
||||
"@types/bluebird": "^3.5.29",
|
||||
"@types/classnames": "^2.2.9",
|
||||
"@types/config": "0.0.36",
|
||||
"@types/debug": "^4.1.5",
|
||||
"@types/express": "^4.17.2",
|
||||
@ -89,9 +88,11 @@
|
||||
"@types/react": "^16.9.11",
|
||||
"@types/react-dom": "^16.9.4",
|
||||
"@types/react-redux": "^7.1.5",
|
||||
"@types/react-transition-group": "^4.2.3",
|
||||
"@types/redux": "^3.6.0",
|
||||
"@types/redux-logger": "^3.0.7",
|
||||
"@types/redux-mock-store": "^1.0.1",
|
||||
"@types/screenfull": "^4.1.0",
|
||||
"@types/simple-peer": "^6.1.6",
|
||||
"@types/socket.io": "^2.1.4",
|
||||
"@types/socket.io-client": "^1.4.32",
|
||||
@ -117,7 +118,9 @@
|
||||
"jest-cli": "^24.8.0",
|
||||
"node-sass": "^4.13.0",
|
||||
"nodemon": "^1.18.8",
|
||||
"react-transition-group": "^2.5.1",
|
||||
"redux-mock-store": "^1.2.3",
|
||||
"screenfull": "^3.3.3",
|
||||
"supertest": "^3.0.0",
|
||||
"ts-jest": "^24.1.0",
|
||||
"ts-node": "^8.5.0",
|
||||
|
||||
@ -1,3 +1,3 @@
|
||||
import configureStore from 'redux-mock-store'
|
||||
import { middlewares } from '../middlewares.js'
|
||||
import { middlewares } from '../middlewares'
|
||||
export default configureStore(middlewares)({})
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
import Promise from 'bluebird'
|
||||
|
||||
export const createObjectURL = jest.fn()
|
||||
.mockImplementation(object => 'blob://' + String(object))
|
||||
export const revokeObjectURL = jest.fn()
|
||||
|
||||
@ -1,23 +1,26 @@
|
||||
jest.mock('../socket.js')
|
||||
jest.mock('../window.js')
|
||||
jest.mock('../store.js')
|
||||
jest.mock('./SocketActions.js')
|
||||
jest.mock('../socket')
|
||||
jest.mock('../window')
|
||||
jest.mock('../store')
|
||||
jest.mock('./SocketActions')
|
||||
|
||||
import * as CallActions from './CallActions.js'
|
||||
import * as SocketActions from './SocketActions.js'
|
||||
import * as constants from '../constants.js'
|
||||
import socket from '../socket.js'
|
||||
import store from '../store.js'
|
||||
import { callId, getUserMedia } from '../window.js'
|
||||
import * as CallActions from './CallActions'
|
||||
import * as SocketActions from './SocketActions'
|
||||
import * as constants from '../constants'
|
||||
import socket from '../socket'
|
||||
import storeMock from '../store'
|
||||
import { callId, getUserMedia } from '../window'
|
||||
import { MockStore } from 'redux-mock-store'
|
||||
|
||||
jest.useFakeTimers()
|
||||
|
||||
describe('reducers/alerts', () => {
|
||||
|
||||
const store: MockStore = storeMock as any
|
||||
|
||||
beforeEach(() => {
|
||||
store.clearActions()
|
||||
getUserMedia.fail(false)
|
||||
SocketActions.handshake.mockReturnValue(jest.fn())
|
||||
store.clearActions();
|
||||
(getUserMedia as any).fail(false);
|
||||
(SocketActions.handshake as jest.Mock).mockReturnValue(jest.fn())
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
@ -28,81 +31,80 @@ describe('reducers/alerts', () => {
|
||||
describe('init', () => {
|
||||
|
||||
it('calls handshake.init when connected & got camera stream', async () => {
|
||||
const promise = store.dispatch(CallActions.init())
|
||||
const promise = CallActions.init(store.dispatch, store.getState)
|
||||
socket.emit('connect')
|
||||
expect(store.getActions()).toEqual([{
|
||||
type: constants.INIT_PENDING
|
||||
type: constants.INIT_PENDING,
|
||||
}, {
|
||||
type: constants.NOTIFY,
|
||||
payload: {
|
||||
id: jasmine.any(String),
|
||||
message: 'Connected to server socket',
|
||||
type: 'warning'
|
||||
}
|
||||
type: 'warning',
|
||||
},
|
||||
}, {
|
||||
type: constants.MESSAGE_ADD,
|
||||
payload: {
|
||||
image: null,
|
||||
message: 'Connected to server socket',
|
||||
timestamp: jasmine.any(String),
|
||||
userId: '[PeerCalls]'
|
||||
}
|
||||
userId: '[PeerCalls]',
|
||||
},
|
||||
}])
|
||||
await promise
|
||||
expect(SocketActions.handshake.mock.calls).toEqual([[{
|
||||
expect((SocketActions.handshake as jest.Mock).mock.calls).toEqual([[{
|
||||
socket,
|
||||
roomName: callId,
|
||||
stream: getUserMedia.stream
|
||||
stream: (getUserMedia as any).stream,
|
||||
}]])
|
||||
})
|
||||
|
||||
it('calls dispatches disconnect message on disconnect', async () => {
|
||||
|
||||
const promise = store.dispatch(CallActions.init())
|
||||
const promise = CallActions.init(store.dispatch, store.getState)
|
||||
socket.emit('connect')
|
||||
socket.emit('disconnect')
|
||||
expect(store.getActions()).toEqual([{
|
||||
type: constants.INIT_PENDING
|
||||
type: constants.INIT_PENDING,
|
||||
}, {
|
||||
type: constants.NOTIFY,
|
||||
payload: {
|
||||
id: jasmine.any(String),
|
||||
message: 'Connected to server socket',
|
||||
type: 'warning'
|
||||
}
|
||||
type: 'warning',
|
||||
},
|
||||
}, {
|
||||
type: constants.MESSAGE_ADD,
|
||||
payload: {
|
||||
image: null,
|
||||
message: 'Connected to server socket',
|
||||
timestamp: jasmine.any(String),
|
||||
userId: '[PeerCalls]'
|
||||
}
|
||||
userId: '[PeerCalls]',
|
||||
},
|
||||
}, {
|
||||
type: constants.NOTIFY,
|
||||
payload: {
|
||||
id: jasmine.any(String),
|
||||
message: 'Server socket disconnected',
|
||||
type: 'error'
|
||||
}
|
||||
type: 'error',
|
||||
},
|
||||
}, {
|
||||
type: constants.MESSAGE_ADD,
|
||||
payload: {
|
||||
image: null,
|
||||
message: 'Server socket disconnected',
|
||||
timestamp: jasmine.any(String),
|
||||
userId: '[PeerCalls]'
|
||||
}
|
||||
userId: '[PeerCalls]',
|
||||
},
|
||||
}])
|
||||
await promise
|
||||
})
|
||||
|
||||
it('dispatches alert when failed to get media stream', async () => {
|
||||
getUserMedia.fail(true)
|
||||
const promise = store.dispatch(CallActions.init())
|
||||
(getUserMedia as any).fail(true)
|
||||
const promise = CallActions.init(store.dispatch, store.getState)
|
||||
socket.emit('connect')
|
||||
const result = await promise
|
||||
expect(result.value).toBe(null)
|
||||
await promise
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
import * as NotifyActions from './NotifyActions.js'
|
||||
import * as SocketActions from './SocketActions.js'
|
||||
import * as StreamActions from './StreamActions.js'
|
||||
import * as constants from '../constants.js'
|
||||
import socket from '../socket.js'
|
||||
import { callId, getUserMedia } from '../window.js'
|
||||
import * as NotifyActions from './NotifyActions'
|
||||
import * as SocketActions from './SocketActions'
|
||||
import * as StreamActions from './StreamActions'
|
||||
import * as constants from '../constants'
|
||||
import socket from '../socket'
|
||||
import { callId, getUserMedia } from '../window'
|
||||
import { Dispatch } from 'redux'
|
||||
import { GetState } from '../store.js'
|
||||
import { GetState } from '../store'
|
||||
|
||||
export interface InitAction {
|
||||
type: 'INIT'
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { MESSAGE_ADD } from '../constants.js'
|
||||
import { MESSAGE_ADD } from '../constants'
|
||||
|
||||
export interface MessageAddAction {
|
||||
type: 'MESSAGE_ADD'
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import * as constants from '../constants.js'
|
||||
import * as ChatActions from './ChatActions.js'
|
||||
import * as constants from '../constants'
|
||||
import * as ChatActions from './ChatActions'
|
||||
import { Dispatch } from 'redux'
|
||||
import _ from 'underscore'
|
||||
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
jest.mock('../window.js')
|
||||
jest.mock('../window')
|
||||
jest.mock('simple-peer')
|
||||
|
||||
import * as PeerActions from './PeerActions.js'
|
||||
import * as PeerActions from './PeerActions'
|
||||
import Peer from 'simple-peer'
|
||||
import { EventEmitter } from 'events'
|
||||
import { createStore, Store, GetState } from '../store.js'
|
||||
import { play } from '../window.js'
|
||||
import { createStore, Store, GetState } from '../store'
|
||||
import { play } from '../window'
|
||||
import { Dispatch } from 'redux'
|
||||
|
||||
describe('PeerActions', () => {
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
import * as ChatActions from '../actions/ChatActions.js'
|
||||
import * as NotifyActions from '../actions/NotifyActions.js'
|
||||
import * as StreamActions from '../actions/StreamActions.js'
|
||||
import * as constants from '../constants.js'
|
||||
import * as ChatActions from '../actions/ChatActions'
|
||||
import * as NotifyActions from '../actions/NotifyActions'
|
||||
import * as StreamActions from '../actions/StreamActions'
|
||||
import * as constants from '../constants'
|
||||
import Peer from 'simple-peer'
|
||||
import _ from 'underscore'
|
||||
import _debug from 'debug'
|
||||
import { play, iceServers } from '../window.js'
|
||||
import { play, iceServers } from '../window'
|
||||
import { Dispatch } from 'redux'
|
||||
|
||||
const debug = _debug('peercalls')
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
jest.mock('simple-peer')
|
||||
jest.mock('../window.js')
|
||||
jest.mock('../window')
|
||||
|
||||
import * as SocketActions from './SocketActions.js'
|
||||
import * as constants from '../constants.js'
|
||||
import * as SocketActions from './SocketActions'
|
||||
import * as constants from '../constants'
|
||||
import Peer from 'simple-peer'
|
||||
import { EventEmitter } from 'events'
|
||||
import { createStore, Store, GetState } from '../store.js'
|
||||
import { createStore, Store, GetState } from '../store'
|
||||
import { Dispatch } from 'redux'
|
||||
|
||||
describe('SocketActions', () => {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import * as NotifyActions from '../actions/NotifyActions.js'
|
||||
import * as PeerActions from '../actions/PeerActions.js'
|
||||
import * as constants from '../constants.js'
|
||||
import * as NotifyActions from '../actions/NotifyActions'
|
||||
import * as PeerActions from '../actions/PeerActions'
|
||||
import * as constants from '../constants'
|
||||
import _ from 'underscore'
|
||||
import _debug from 'debug'
|
||||
import { Dispatch } from 'redux'
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import * as constants from '../constants.js'
|
||||
import * as constants from '../constants'
|
||||
|
||||
export interface AddStreamPayload {
|
||||
userId: string
|
||||
@ -56,6 +56,6 @@ export const toggleActive = (userId: string): ToggleActiveStreamAction => ({
|
||||
|
||||
export type StreamAction =
|
||||
AddStreamAction |
|
||||
RemoveStreamAction
|
||||
// SetActiveStreamAction |
|
||||
// ToggleActiveStreamAction
|
||||
RemoveStreamAction |
|
||||
SetActiveStreamAction |
|
||||
ToggleActiveStreamAction
|
||||
|
||||
@ -1,18 +1,13 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import classnames from 'classnames'
|
||||
import { Alert as AlertType } from '../actions/NotifyActions'
|
||||
|
||||
export const AlertPropType = PropTypes.shape({
|
||||
dismissable: PropTypes.bool,
|
||||
action: PropTypes.string.isRequired,
|
||||
message: PropTypes.string.isRequired
|
||||
})
|
||||
export interface AlertProps {
|
||||
alert: AlertType
|
||||
dismiss: (alert: AlertType) => void
|
||||
}
|
||||
|
||||
export class Alert extends React.PureComponent {
|
||||
static propTypes = {
|
||||
alert: AlertPropType,
|
||||
dismiss: PropTypes.func.isRequired
|
||||
}
|
||||
export class Alert extends React.PureComponent<AlertProps> {
|
||||
dismiss = () => {
|
||||
const { alert, dismiss } = this.props
|
||||
dismiss(alert)
|
||||
@ -34,11 +29,12 @@ export class Alert extends React.PureComponent {
|
||||
}
|
||||
}
|
||||
|
||||
export default class Alerts extends React.PureComponent {
|
||||
static propTypes = {
|
||||
alerts: PropTypes.arrayOf(AlertPropType).isRequired,
|
||||
dismiss: PropTypes.func.isRequired
|
||||
}
|
||||
export interface AlertsProps {
|
||||
alerts: AlertType[]
|
||||
dismiss: (alert: AlertType) => void
|
||||
}
|
||||
|
||||
export default class Alerts extends React.PureComponent<AlertsProps> {
|
||||
render () {
|
||||
const { alerts, dismiss } = this.props
|
||||
return (
|
||||
@ -1,42 +1,49 @@
|
||||
import * as constants from '../constants.js'
|
||||
import Alerts, { AlertPropType } from './Alerts.js'
|
||||
import Chat, { MessagePropTypes } from './Chat.js'
|
||||
import Notifications, { NotificationPropTypes } from './Notifications.js'
|
||||
import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
import Toolbar from './Toolbar.js'
|
||||
import Video, { StreamPropType } from './Video.js'
|
||||
import Peer from 'simple-peer'
|
||||
import _ from 'underscore'
|
||||
import { Message } from '../actions/ChatActions'
|
||||
import { Alert, Notification } from '../actions/NotifyActions'
|
||||
import { TextMessage } from '../actions/PeerActions'
|
||||
import { AddStreamPayload } from '../actions/StreamActions'
|
||||
import * as constants from '../constants'
|
||||
import Alerts from './Alerts'
|
||||
import Chat from './Chat'
|
||||
import Notifications from './Notifications'
|
||||
import Toolbar from './Toolbar'
|
||||
import Video from './Video'
|
||||
|
||||
export default class App extends React.PureComponent {
|
||||
static propTypes = {
|
||||
active: PropTypes.string,
|
||||
alerts: PropTypes.arrayOf(AlertPropType).isRequired,
|
||||
dismissAlert: PropTypes.func.isRequired,
|
||||
init: PropTypes.func.isRequired,
|
||||
notifications: PropTypes.objectOf(NotificationPropTypes).isRequired,
|
||||
messages: PropTypes.arrayOf(MessagePropTypes).isRequired,
|
||||
peers: PropTypes.object.isRequired,
|
||||
sendMessage: PropTypes.func.isRequired,
|
||||
streams: PropTypes.objectOf(StreamPropType).isRequired,
|
||||
onSendFile: PropTypes.func.isRequired,
|
||||
toggleActive: PropTypes.func.isRequired
|
||||
}
|
||||
constructor () {
|
||||
super()
|
||||
this.state = {
|
||||
videos: {},
|
||||
chatVisible: false
|
||||
}
|
||||
export interface AppProps {
|
||||
active: string | null
|
||||
alerts: Alert[]
|
||||
dismissAlert: (alert: Alert) => void
|
||||
init: () => void
|
||||
notifications: Record<string, Notification>
|
||||
messages: Message[]
|
||||
peers: Record<string, Peer.Instance>
|
||||
sendMessage: (message: TextMessage) => void
|
||||
streams: Record<string, AddStreamPayload>
|
||||
onSendFile: (file: File) => void
|
||||
toggleActive: (userId: string) => void
|
||||
}
|
||||
|
||||
export interface AppState {
|
||||
videos: Record<string, unknown>
|
||||
chatVisible: boolean
|
||||
}
|
||||
|
||||
export default class App extends React.PureComponent<AppProps, AppState> {
|
||||
state: AppState = {
|
||||
videos: {},
|
||||
chatVisible: false,
|
||||
}
|
||||
handleShowChat = () => {
|
||||
this.setState({
|
||||
chatVisible: true
|
||||
chatVisible: true,
|
||||
})
|
||||
}
|
||||
handleHideChat = () => {
|
||||
this.setState({
|
||||
chatVisible: false
|
||||
chatVisible: false,
|
||||
})
|
||||
}
|
||||
handleToggleChat = () => {
|
||||
@ -59,7 +66,7 @@ export default class App extends React.PureComponent {
|
||||
peers,
|
||||
sendMessage,
|
||||
toggleActive,
|
||||
streams
|
||||
streams,
|
||||
} = this.props
|
||||
|
||||
const { videos } = this.state
|
||||
@ -79,7 +86,6 @@ export default class App extends React.PureComponent {
|
||||
messages={messages}
|
||||
onClose={this.handleHideChat}
|
||||
sendMessage={sendMessage}
|
||||
videos={videos}
|
||||
visible={this.state.chatVisible}
|
||||
/>
|
||||
<div className="videos">
|
||||
@ -1,16 +1,14 @@
|
||||
import Input from './Input.js'
|
||||
import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
import classnames from 'classnames'
|
||||
import React from 'react'
|
||||
import { Message as MessageType } from '../actions/ChatActions'
|
||||
import { TextMessage } from '../actions/PeerActions'
|
||||
import Input from './Input'
|
||||
|
||||
export const MessagePropTypes = PropTypes.shape({
|
||||
userId: PropTypes.string.isRequired,
|
||||
message: PropTypes.string.isRequired,
|
||||
timestamp: PropTypes.string.isRequired,
|
||||
image: PropTypes.string
|
||||
})
|
||||
export interface MessageProps {
|
||||
message: MessageType
|
||||
}
|
||||
|
||||
function Message (props) {
|
||||
function Message (props: MessageProps) {
|
||||
const { message } = props
|
||||
return (
|
||||
<p className="message-text">
|
||||
@ -22,24 +20,18 @@ function Message (props) {
|
||||
)
|
||||
}
|
||||
|
||||
Message.propTypes = {
|
||||
message: MessagePropTypes
|
||||
export interface ChatProps {
|
||||
visible: boolean
|
||||
messages: MessageType[]
|
||||
onClose: () => void
|
||||
sendMessage: (message: TextMessage) => void
|
||||
}
|
||||
|
||||
export default class Chat extends React.PureComponent {
|
||||
static propTypes = {
|
||||
visible: PropTypes.bool.isRequired,
|
||||
messages: PropTypes.arrayOf(MessagePropTypes).isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
sendMessage: PropTypes.func.isRequired,
|
||||
videos: PropTypes.object.isRequired
|
||||
}
|
||||
constructor () {
|
||||
super()
|
||||
this.chatHistoryRef = React.createRef()
|
||||
}
|
||||
export default class Chat extends React.PureComponent<ChatProps> {
|
||||
chatHistoryRef = React.createRef<HTMLDivElement>()
|
||||
|
||||
scrollToBottom = () => {
|
||||
const chatHistoryRef = this.chatHistoryRef.current
|
||||
const chatHistoryRef = this.chatHistoryRef.current!
|
||||
chatHistoryRef.scrollTop = chatHistoryRef.scrollHeight
|
||||
}
|
||||
componentDidMount () {
|
||||
@ -49,10 +41,10 @@ export default class Chat extends React.PureComponent {
|
||||
this.scrollToBottom()
|
||||
}
|
||||
render () {
|
||||
const { messages, videos, sendMessage } = this.props
|
||||
const { messages, sendMessage } = this.props
|
||||
return (
|
||||
<div className={classnames('chat-container', {
|
||||
show: this.props.visible
|
||||
show: this.props.visible,
|
||||
})}>
|
||||
<div className="chat-header">
|
||||
<div className="chat-close" onClick={this.props.onClose}>
|
||||
@ -111,10 +103,7 @@ export default class Chat extends React.PureComponent {
|
||||
|
||||
</div>
|
||||
|
||||
<Input
|
||||
videos={videos}
|
||||
sendMessage={sendMessage}
|
||||
/>
|
||||
<Input sendMessage={sendMessage} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -1,36 +1,39 @@
|
||||
import Input from './Input.js'
|
||||
import Input from './Input'
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import TestUtils from 'react-dom/test-utils'
|
||||
import { TextMessage } from '../actions/PeerActions'
|
||||
|
||||
describe('components/Input', () => {
|
||||
|
||||
let component, node, videos, notify, sendMessage
|
||||
function render () {
|
||||
videos = {}
|
||||
notify = jest.fn()
|
||||
let node: Element
|
||||
let sendMessage: jest.Mock<(message: TextMessage) => void>
|
||||
async function render () {
|
||||
sendMessage = jest.fn()
|
||||
component = TestUtils.renderIntoDocument(
|
||||
<Input
|
||||
videos={videos}
|
||||
sendMessage={sendMessage}
|
||||
notify={notify}
|
||||
/>
|
||||
)
|
||||
node = ReactDOM.findDOMNode(component)
|
||||
const div = document.createElement('div')
|
||||
await new Promise<Input>(resolve => {
|
||||
ReactDOM.render(
|
||||
<Input
|
||||
ref={input => resolve(input!)}
|
||||
sendMessage={sendMessage}
|
||||
/>,
|
||||
div,
|
||||
)
|
||||
})
|
||||
node = div.children[0]
|
||||
}
|
||||
let message = 'test message'
|
||||
const message = 'test message'
|
||||
|
||||
beforeEach(() => render())
|
||||
|
||||
describe('send message', () => {
|
||||
|
||||
let input
|
||||
let input: HTMLTextAreaElement
|
||||
beforeEach(() => {
|
||||
sendMessage.mockClear()
|
||||
input = node.querySelector('textarea')
|
||||
input = node.querySelector('textarea')!
|
||||
TestUtils.Simulate.change(input, {
|
||||
target: { value: message }
|
||||
target: { value: message } as any,
|
||||
})
|
||||
expect(input.value).toBe(message)
|
||||
})
|
||||
@ -47,7 +50,7 @@ describe('components/Input', () => {
|
||||
describe('handleKeyPress', () => {
|
||||
it('sends a message', () => {
|
||||
TestUtils.Simulate.keyPress(input, {
|
||||
key: 'Enter'
|
||||
key: 'Enter',
|
||||
})
|
||||
expect(input.value).toBe('')
|
||||
expect(sendMessage.mock.calls)
|
||||
@ -56,7 +59,7 @@ describe('components/Input', () => {
|
||||
|
||||
it('does nothing when other key pressed', () => {
|
||||
TestUtils.Simulate.keyPress(input, {
|
||||
key: 'test'
|
||||
key: 'test',
|
||||
})
|
||||
expect(sendMessage.mock.calls.length).toBe(0)
|
||||
})
|
||||
@ -64,7 +67,7 @@ describe('components/Input', () => {
|
||||
|
||||
describe('handleSmileClick', () => {
|
||||
it('adds smile to message', () => {
|
||||
const div = node.querySelector('.chat-controls-buttons-smile')
|
||||
const div = node.querySelector('.chat-controls-buttons-smile')!
|
||||
TestUtils.Simulate.click(div)
|
||||
expect(input.value).toBe('test message😑')
|
||||
})
|
||||
@ -1,34 +1,37 @@
|
||||
import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
import React, { ReactEventHandler, ChangeEventHandler, KeyboardEventHandler, MouseEventHandler } from 'react'
|
||||
import { TextMessage } from '../actions/PeerActions'
|
||||
|
||||
export default class Input extends React.PureComponent {
|
||||
static propTypes = {
|
||||
sendMessage: PropTypes.func.isRequired
|
||||
export interface InputProps {
|
||||
sendMessage: (message: TextMessage) => void
|
||||
}
|
||||
|
||||
export interface InputState {
|
||||
message: string
|
||||
}
|
||||
|
||||
export default class Input extends React.PureComponent<InputProps, InputState> {
|
||||
textArea = React.createRef<HTMLTextAreaElement>()
|
||||
state = {
|
||||
message: '',
|
||||
}
|
||||
constructor () {
|
||||
super()
|
||||
this.state = {
|
||||
message: ''
|
||||
}
|
||||
}
|
||||
handleChange = e => {
|
||||
handleChange: ChangeEventHandler<HTMLTextAreaElement> = event => {
|
||||
this.setState({
|
||||
message: e.target.value
|
||||
message: event.target.value,
|
||||
})
|
||||
}
|
||||
handleSubmit = e => {
|
||||
handleSubmit: ReactEventHandler<HTMLFormElement> = e => {
|
||||
e.preventDefault()
|
||||
this.submit()
|
||||
}
|
||||
handleKeyPress = e => {
|
||||
handleKeyPress: KeyboardEventHandler<HTMLTextAreaElement> = e => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault()
|
||||
this.submit()
|
||||
}
|
||||
}
|
||||
handleSmileClick = e => {
|
||||
handleSmileClick: MouseEventHandler<HTMLElement> = event => {
|
||||
this.setState({
|
||||
message: this.textArea.value + e.currentTarget.innerHTML
|
||||
message: this.textArea.current!.value + event.currentTarget.innerHTML,
|
||||
})
|
||||
}
|
||||
submit = () => {
|
||||
@ -37,7 +40,7 @@ export default class Input extends React.PureComponent {
|
||||
if (message) {
|
||||
sendMessage({
|
||||
payload: message,
|
||||
type: 'text'
|
||||
type: 'text',
|
||||
})
|
||||
// let image = null
|
||||
|
||||
@ -66,7 +69,7 @@ export default class Input extends React.PureComponent {
|
||||
onKeyPress={this.handleKeyPress}
|
||||
placeholder="Type a message"
|
||||
value={message}
|
||||
ref={node => { this.textArea = node }}
|
||||
ref={this.textArea}
|
||||
/>
|
||||
<div className="chat-controls-buttons">
|
||||
<input type="submit" value="Send"
|
||||
@ -1,41 +0,0 @@
|
||||
import CSSTransitionGroup from 'react-transition-group/CSSTransitionGroup'
|
||||
import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
import classnames from 'classnames'
|
||||
|
||||
export const NotificationPropTypes = PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
type: PropTypes.string.isRequired,
|
||||
message: PropTypes.string.isRequired
|
||||
})
|
||||
|
||||
export default class Notifications extends React.PureComponent {
|
||||
static propTypes = {
|
||||
notifications: PropTypes.objectOf(NotificationPropTypes).isRequired,
|
||||
max: PropTypes.number.isRequired
|
||||
}
|
||||
static defaultProps = {
|
||||
max: 10
|
||||
}
|
||||
render () {
|
||||
const { notifications, max } = this.props
|
||||
return (
|
||||
<div className="notifications">
|
||||
<CSSTransitionGroup
|
||||
transitionEnterTimeout={200}
|
||||
transitionLeaveTimeout={100}
|
||||
transitionName="fade"
|
||||
>
|
||||
{Object.keys(notifications).slice(-max).map(id => (
|
||||
<div
|
||||
className={classnames(notifications[id].type, 'notification')}
|
||||
key={id}
|
||||
>
|
||||
{notifications[id].message}
|
||||
</div>
|
||||
))}
|
||||
</CSSTransitionGroup>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
41
src/client/components/Notifications.tsx
Normal file
41
src/client/components/Notifications.tsx
Normal file
@ -0,0 +1,41 @@
|
||||
import CSSTransition from 'react-transition-group/CSSTransition'
|
||||
import React from 'react'
|
||||
import classnames from 'classnames'
|
||||
import { Notification } from '../actions/NotifyActions'
|
||||
|
||||
export interface NotificationProps {
|
||||
notifications: Record<string, Notification>
|
||||
max: number
|
||||
}
|
||||
|
||||
const transitionTimeout = {
|
||||
enter: 200,
|
||||
exit: 100,
|
||||
}
|
||||
|
||||
export default class Notifications
|
||||
extends React.PureComponent<NotificationProps> {
|
||||
static defaultProps = {
|
||||
max: 10,
|
||||
}
|
||||
render () {
|
||||
const { notifications, max } = this.props
|
||||
return (
|
||||
<div className="notifications">
|
||||
<CSSTransition
|
||||
classNames='fade'
|
||||
timeout={transitionTimeout}
|
||||
>
|
||||
{Object.keys(notifications).slice(-max).map(id => (
|
||||
<div
|
||||
className={classnames(notifications[id].type, 'notification')}
|
||||
key={id}
|
||||
>
|
||||
{notifications[id].message}
|
||||
</div>
|
||||
))}
|
||||
</CSSTransition>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -1,17 +1,20 @@
|
||||
jest.mock('../window.js')
|
||||
jest.mock('../window')
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import TestUtils from 'react-dom/test-utils'
|
||||
import Toolbar from './Toolbar.js'
|
||||
import { MediaStream } from '../window.js'
|
||||
import Toolbar, { ToolbarProps } from './Toolbar'
|
||||
import { MediaStream } from '../window'
|
||||
import { AddStreamPayload } from '../actions/StreamActions'
|
||||
|
||||
describe('components/Toolbar', () => {
|
||||
|
||||
class ToolbarWrapper extends React.PureComponent {
|
||||
static propTypes = Toolbar.propTypes
|
||||
constructor () {
|
||||
super()
|
||||
this.state = {}
|
||||
interface StreamState {
|
||||
stream: AddStreamPayload | null
|
||||
}
|
||||
|
||||
class ToolbarWrapper extends React.PureComponent<ToolbarProps, StreamState> {
|
||||
state = {
|
||||
stream: null,
|
||||
}
|
||||
render () {
|
||||
return <Toolbar
|
||||
@ -24,31 +27,40 @@ describe('components/Toolbar', () => {
|
||||
}
|
||||
}
|
||||
|
||||
let component, node, mediaStream, url, onToggleChat, onSendFile
|
||||
function render () {
|
||||
let node: Element
|
||||
let mediaStream: MediaStream
|
||||
let url: string
|
||||
let onToggleChat: jest.Mock<() => void>
|
||||
let onSendFile: jest.Mock<(file: File) => void>
|
||||
async function render () {
|
||||
mediaStream = new MediaStream()
|
||||
onToggleChat = jest.fn()
|
||||
onSendFile = jest.fn()
|
||||
component = TestUtils.renderIntoDocument(
|
||||
<ToolbarWrapper
|
||||
chatVisible
|
||||
onToggleChat={onToggleChat}
|
||||
onSendFile={onSendFile}
|
||||
messages={[]}
|
||||
stream={{ mediaStream, url }}
|
||||
/>
|
||||
)
|
||||
node = ReactDOM.findDOMNode(component)
|
||||
const div = document.createElement('div')
|
||||
await new Promise<ToolbarWrapper>(resolve => {
|
||||
ReactDOM.render(
|
||||
<ToolbarWrapper
|
||||
ref={instance => resolve(instance!)}
|
||||
chatVisible
|
||||
onToggleChat={onToggleChat}
|
||||
onSendFile={onSendFile}
|
||||
messages={[]}
|
||||
stream={{ userId: '', stream: mediaStream, url }}
|
||||
/>,
|
||||
div,
|
||||
)
|
||||
})
|
||||
node = div.children[0]
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
render()
|
||||
beforeEach(async () => {
|
||||
await render()
|
||||
})
|
||||
|
||||
describe('handleChatClick', () => {
|
||||
it('toggle chat', () => {
|
||||
expect(onToggleChat.mock.calls.length).toBe(0)
|
||||
const button = node.querySelector('.chat')
|
||||
const button = node.querySelector('.chat')!
|
||||
TestUtils.Simulate.click(button)
|
||||
expect(onToggleChat.mock.calls.length).toBe(1)
|
||||
})
|
||||
@ -56,7 +68,7 @@ describe('components/Toolbar', () => {
|
||||
|
||||
describe('handleMicClick', () => {
|
||||
it('toggle mic', () => {
|
||||
const button = node.querySelector('.mute-audio')
|
||||
const button = node.querySelector('.mute-audio')!
|
||||
TestUtils.Simulate.click(button)
|
||||
expect(button.classList.contains('on')).toBe(true)
|
||||
})
|
||||
@ -64,7 +76,7 @@ describe('components/Toolbar', () => {
|
||||
|
||||
describe('handleCamClick', () => {
|
||||
it('toggle cam', () => {
|
||||
const button = node.querySelector('.mute-video')
|
||||
const button = node.querySelector('.mute-video')!
|
||||
TestUtils.Simulate.click(button)
|
||||
expect(button.classList.contains('on')).toBe(true)
|
||||
})
|
||||
@ -72,7 +84,7 @@ describe('components/Toolbar', () => {
|
||||
|
||||
describe('handleFullscreenClick', () => {
|
||||
it('toggle fullscreen', () => {
|
||||
const button = node.querySelector('.fullscreen')
|
||||
const button = node.querySelector('.fullscreen')!
|
||||
TestUtils.Simulate.click(button)
|
||||
expect(button.classList.contains('on')).toBe(false)
|
||||
})
|
||||
@ -80,7 +92,7 @@ describe('components/Toolbar', () => {
|
||||
|
||||
describe('handleHangoutClick', () => {
|
||||
it('hangout', () => {
|
||||
const button = node.querySelector('.hangup')
|
||||
const button = node.querySelector('.hangup')!
|
||||
TestUtils.Simulate.click(button)
|
||||
expect(window.location.href).toBe('http://localhost/')
|
||||
})
|
||||
@ -88,10 +100,10 @@ describe('components/Toolbar', () => {
|
||||
|
||||
describe('handleSendFile', () => {
|
||||
it('triggers input dialog', () => {
|
||||
const sendFileButton = node.querySelector('.send-file')
|
||||
const sendFileButton = node.querySelector('.send-file')!
|
||||
const click = jest.fn()
|
||||
const file = node.querySelector('input[type=file]')
|
||||
file.click = click
|
||||
const file = node.querySelector('input[type=file]')!;
|
||||
(file as any).click = click
|
||||
TestUtils.Simulate.click(sendFileButton)
|
||||
expect(click.mock.calls.length).toBe(1)
|
||||
})
|
||||
@ -99,12 +111,12 @@ describe('components/Toolbar', () => {
|
||||
|
||||
describe('handleSelectFiles', () => {
|
||||
it('iterates through files and calls onSendFile for each', () => {
|
||||
const file = node.querySelector('input[type=file]')
|
||||
const files = [{ name: 'first' }]
|
||||
const file = node.querySelector('input[type=file]')!
|
||||
const files = [{ name: 'first' }] as any
|
||||
TestUtils.Simulate.change(file, {
|
||||
target: {
|
||||
files
|
||||
}
|
||||
files,
|
||||
} as any,
|
||||
})
|
||||
expect(onSendFile.mock.calls).toEqual([[ files[0] ]])
|
||||
})
|
||||
@ -1,63 +1,85 @@
|
||||
import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
import React, { ReactEventHandler } from 'react'
|
||||
import classnames from 'classnames'
|
||||
import screenfull from 'screenfull'
|
||||
import { MessagePropTypes } from './Chat.js'
|
||||
import { StreamPropType } from './Video.js'
|
||||
import { Message } from '../actions/ChatActions'
|
||||
import { AddStreamPayload } from '../actions/StreamActions'
|
||||
|
||||
const hidden = {
|
||||
display: 'none'
|
||||
display: 'none',
|
||||
}
|
||||
|
||||
export default class Toolbar extends React.PureComponent {
|
||||
static propTypes = {
|
||||
messages: PropTypes.arrayOf(MessagePropTypes).isRequired,
|
||||
stream: StreamPropType,
|
||||
onToggleChat: PropTypes.func.isRequired,
|
||||
onSendFile: PropTypes.func.isRequired,
|
||||
chatVisible: PropTypes.bool.isRequired
|
||||
}
|
||||
constructor (props) {
|
||||
export interface ToolbarProps {
|
||||
messages: Message[]
|
||||
stream: AddStreamPayload
|
||||
onToggleChat: () => void
|
||||
onSendFile: (file: File) => void
|
||||
chatVisible: boolean
|
||||
}
|
||||
|
||||
export interface ToolbarState {
|
||||
readMessages: number
|
||||
camDisabled: boolean
|
||||
micMuted: boolean
|
||||
fullScreenEnabled: boolean
|
||||
}
|
||||
|
||||
export default class Toolbar
|
||||
extends React.PureComponent<ToolbarProps, ToolbarState> {
|
||||
file = React.createRef<HTMLInputElement>()
|
||||
|
||||
constructor(props: ToolbarProps) {
|
||||
super(props)
|
||||
this.file = React.createRef()
|
||||
this.state = {
|
||||
readMessages: props.messages.length
|
||||
readMessages: props.messages.length,
|
||||
camDisabled: false,
|
||||
micMuted: false,
|
||||
fullScreenEnabled: false,
|
||||
}
|
||||
}
|
||||
|
||||
handleMicClick = () => {
|
||||
const { stream } = this.props
|
||||
stream.mediaStream.getAudioTracks().forEach(track => {
|
||||
stream.stream.getAudioTracks().forEach(track => {
|
||||
track.enabled = !track.enabled
|
||||
})
|
||||
this.mixButton.classList.toggle('on')
|
||||
this.setState({
|
||||
...this.state,
|
||||
micMuted: !this.state.micMuted,
|
||||
})
|
||||
}
|
||||
handleCamClick = () => {
|
||||
const { stream } = this.props
|
||||
stream.mediaStream.getVideoTracks().forEach(track => {
|
||||
stream.stream.getVideoTracks().forEach(track => {
|
||||
track.enabled = !track.enabled
|
||||
})
|
||||
this.camButton.classList.toggle('on')
|
||||
this.setState({
|
||||
...this.state,
|
||||
camDisabled: !this.state.camDisabled,
|
||||
})
|
||||
}
|
||||
handleFullscreenClick = () => {
|
||||
if (screenfull.enabled) {
|
||||
screenfull.toggle()
|
||||
this.fullscreenButton.classList.toggle('on')
|
||||
this.setState({
|
||||
...this.state,
|
||||
fullScreenEnabled: !this.state.fullScreenEnabled,
|
||||
})
|
||||
}
|
||||
}
|
||||
handleHangoutClick = () => {
|
||||
window.location.href = '/'
|
||||
}
|
||||
handleSendFile = () => {
|
||||
this.file.current.click()
|
||||
this.file.current!.click()
|
||||
}
|
||||
handleSelectFiles = (event) => {
|
||||
handleSelectFiles: ReactEventHandler<HTMLInputElement> = event => {
|
||||
Array
|
||||
.from(event.target.files)
|
||||
.from(this.file.current!.files!)
|
||||
.forEach(file => this.props.onSendFile(file))
|
||||
}
|
||||
handleToggleChat = () => {
|
||||
this.setState({
|
||||
readMessages: this.props.messages.length
|
||||
readMessages: this.props.messages.length,
|
||||
})
|
||||
this.props.onToggleChat()
|
||||
}
|
||||
@ -68,7 +90,7 @@ export default class Toolbar extends React.PureComponent {
|
||||
<div className="toolbar active">
|
||||
<div onClick={this.handleToggleChat}
|
||||
className={classnames('button chat', {
|
||||
on: this.props.chatVisible
|
||||
on: this.props.chatVisible,
|
||||
})}
|
||||
data-blink={!this.props.chatVisible &&
|
||||
messages.length > this.state.readMessages}
|
||||
@ -93,17 +115,20 @@ export default class Toolbar extends React.PureComponent {
|
||||
|
||||
{stream && (
|
||||
<div>
|
||||
<div onClick={this.handleMicClick}
|
||||
ref={node => { this.mixButton = node }}
|
||||
className="button mute-audio"
|
||||
<div
|
||||
onClick={this.handleMicClick}
|
||||
className={classnames('button mute-audio', {
|
||||
on: this.state.micMuted,
|
||||
})}
|
||||
title="Mute audio"
|
||||
>
|
||||
<span className="on icon icon-mic_off" />
|
||||
<span className="off icon icon-mic" />
|
||||
</div>
|
||||
<div onClick={this.handleCamClick}
|
||||
ref={node => { this.camButton = node }}
|
||||
className="button mute-video"
|
||||
className={classnames('button mute-video', {
|
||||
on: this.state.camDisabled,
|
||||
})}
|
||||
title="Mute video"
|
||||
>
|
||||
<span className="on icon icon-videocam_off" />
|
||||
@ -113,8 +138,9 @@ export default class Toolbar extends React.PureComponent {
|
||||
)}
|
||||
|
||||
<div onClick={this.handleFullscreenClick}
|
||||
ref={node => { this.fullscreenButton = node }}
|
||||
className="button fullscreen"
|
||||
className={classnames('button fullscreen', {
|
||||
on: this.state.fullScreenEnabled,
|
||||
})}
|
||||
title="Enter fullscreen"
|
||||
>
|
||||
<span className="on icon icon-fullscreen_exit" />
|
||||
@ -1,90 +0,0 @@
|
||||
jest.mock('../window.js')
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import TestUtils from 'react-dom/test-utils'
|
||||
import Video from './Video.js'
|
||||
import { MediaStream } from '../window.js'
|
||||
|
||||
describe('components/Video', () => {
|
||||
|
||||
class VideoWrapper extends React.PureComponent {
|
||||
static propTypes = Video.propTypes
|
||||
constructor () {
|
||||
super()
|
||||
this.state = {}
|
||||
}
|
||||
render () {
|
||||
return <Video
|
||||
videos={this.props.videos}
|
||||
active={this.props.active}
|
||||
stream={this.state.stream || this.props.stream}
|
||||
onClick={this.props.onClick}
|
||||
userId="test"
|
||||
muted={this.props.muted}
|
||||
mirrored={this.props.mirrored}
|
||||
/>
|
||||
}
|
||||
}
|
||||
|
||||
let component, videos, video, onClick, mediaStream, url, wrapper
|
||||
function render (flags = {}) {
|
||||
videos = {}
|
||||
onClick = jest.fn()
|
||||
mediaStream = new MediaStream()
|
||||
component = TestUtils.renderIntoDocument(
|
||||
<VideoWrapper
|
||||
videos={videos}
|
||||
active={flags.active || false}
|
||||
stream={{ mediaStream, url }}
|
||||
onClick={onClick}
|
||||
userId="test"
|
||||
muted={flags.muted || false}
|
||||
mirrored={flags.mirrored}
|
||||
/>
|
||||
)
|
||||
wrapper = ReactDOM.findDOMNode(component)
|
||||
video = TestUtils.findRenderedComponentWithType(component, Video)
|
||||
}
|
||||
|
||||
describe('render', () => {
|
||||
it('should not fail', () => {
|
||||
render({})
|
||||
})
|
||||
|
||||
it('Mirrored and active propogate to rendered classes', () => {
|
||||
render({ active: true, mirrored: true })
|
||||
expect(wrapper.className).toBe('video-container active mirrored')
|
||||
})
|
||||
})
|
||||
|
||||
describe('componentDidUpdate', () => {
|
||||
describe('src', () => {
|
||||
beforeEach(() => {
|
||||
render()
|
||||
delete video.refs.video.srcObject
|
||||
})
|
||||
it('updates src only when changed', () => {
|
||||
mediaStream = new MediaStream()
|
||||
component.setState({
|
||||
stream: { url: 'test', mediaStream }
|
||||
})
|
||||
expect(video.refs.video.src).toBe('http://localhost/test')
|
||||
component.setState({
|
||||
stream: { url: 'test', mediaStream }
|
||||
})
|
||||
})
|
||||
it('updates srcObject only when changed', () => {
|
||||
video.refs.video.srcObject = null
|
||||
mediaStream = new MediaStream()
|
||||
component.setState({
|
||||
stream: { url: 'test', mediaStream }
|
||||
})
|
||||
expect(video.refs.video.srcObject).toBe(mediaStream)
|
||||
component.setState({
|
||||
stream: { url: 'test', mediaStream }
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
120
src/client/components/Video.test.tsx
Normal file
120
src/client/components/Video.test.tsx
Normal file
@ -0,0 +1,120 @@
|
||||
jest.mock('../window')
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import TestUtils from 'react-dom/test-utils'
|
||||
import { AddStreamPayload } from '../actions/StreamActions'
|
||||
import Video, { VideoProps } from './Video'
|
||||
import { MediaStream } from '../window'
|
||||
|
||||
describe('components/Video', () => {
|
||||
|
||||
interface VideoState {
|
||||
stream: null | AddStreamPayload
|
||||
}
|
||||
|
||||
class VideoWrapper extends React.PureComponent<VideoProps, VideoState> {
|
||||
ref = React.createRef<Video>()
|
||||
|
||||
state = {
|
||||
stream: null,
|
||||
}
|
||||
|
||||
render () {
|
||||
return <Video
|
||||
ref={this.ref}
|
||||
videos={this.props.videos}
|
||||
active={this.props.active}
|
||||
stream={this.state.stream || this.props.stream}
|
||||
onClick={this.props.onClick}
|
||||
userId="test"
|
||||
muted={this.props.muted}
|
||||
mirrored={this.props.mirrored}
|
||||
/>
|
||||
}
|
||||
}
|
||||
|
||||
let component: VideoWrapper
|
||||
let videos: Record<string, unknown> = {}
|
||||
let video: Video
|
||||
let onClick: (userId: string) => void
|
||||
let mediaStream: MediaStream
|
||||
let url: string
|
||||
let wrapper: Element
|
||||
|
||||
interface Flags {
|
||||
active: boolean
|
||||
muted: boolean
|
||||
mirrored: boolean
|
||||
}
|
||||
const defaultFlags: Flags = {
|
||||
active: false,
|
||||
muted: false,
|
||||
mirrored: false,
|
||||
}
|
||||
async function render (args?: Partial<Flags>) {
|
||||
const flags: Flags = Object.assign({}, defaultFlags, args)
|
||||
videos = {}
|
||||
onClick = jest.fn()
|
||||
mediaStream = new MediaStream()
|
||||
const div = document.createElement('div')
|
||||
component = await new Promise<VideoWrapper>(resolve => {
|
||||
ReactDOM.render(
|
||||
<VideoWrapper
|
||||
ref={instance => resolve(instance!)}
|
||||
videos={videos}
|
||||
active={flags.active}
|
||||
stream={{ stream: mediaStream, url, userId: 'test' }}
|
||||
onClick={onClick}
|
||||
userId="test"
|
||||
muted={flags.muted}
|
||||
mirrored={flags.mirrored}
|
||||
/>,
|
||||
div,
|
||||
)
|
||||
})
|
||||
video = TestUtils.findRenderedComponentWithType(component, Video)
|
||||
wrapper = div.children[0]
|
||||
}
|
||||
|
||||
describe('render', () => {
|
||||
it('should not fail', async () => {
|
||||
await render()
|
||||
})
|
||||
|
||||
it('Mirrored and active propogate to rendered classes', () => {
|
||||
render({ active: true, mirrored: true })
|
||||
expect(wrapper.className).toBe('video-container active mirrored')
|
||||
})
|
||||
})
|
||||
|
||||
describe('componentDidUpdate', () => {
|
||||
describe('src', () => {
|
||||
beforeEach(async () => {
|
||||
await render()
|
||||
delete video.video.current!.srcObject
|
||||
})
|
||||
it('updates src only when changed', () => {
|
||||
mediaStream = new MediaStream()
|
||||
component.setState({
|
||||
stream: { url: 'test', stream: mediaStream, userId: '' },
|
||||
})
|
||||
expect(video.video.current!.src).toBe('http://localhost/test')
|
||||
component.setState({
|
||||
stream: { url: 'test', stream: mediaStream, userId: '' },
|
||||
})
|
||||
})
|
||||
it('updates srcObject only when changed', () => {
|
||||
video.video.current!.srcObject = null
|
||||
mediaStream = new MediaStream()
|
||||
component.setState({
|
||||
stream: { url: 'test', stream: mediaStream, userId: '' },
|
||||
})
|
||||
expect(video.video.current!.srcObject).toBe(mediaStream)
|
||||
component.setState({
|
||||
stream: { url: 'test', stream: mediaStream, userId: '' },
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
@ -1,69 +0,0 @@
|
||||
import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
import classnames from 'classnames'
|
||||
import { MediaStream } from '../window.js'
|
||||
import socket from '../socket.js'
|
||||
|
||||
export const StreamPropType = PropTypes.shape({
|
||||
mediaStream: PropTypes.instanceOf(MediaStream).isRequired,
|
||||
url: PropTypes.string
|
||||
})
|
||||
|
||||
export default class Video extends React.PureComponent {
|
||||
static propTypes = {
|
||||
videos: PropTypes.object.isRequired,
|
||||
onClick: PropTypes.func,
|
||||
active: PropTypes.bool.isRequired,
|
||||
stream: StreamPropType,
|
||||
userId: PropTypes.string.isRequired,
|
||||
muted: PropTypes.bool.isRequired,
|
||||
mirrored: PropTypes.bool
|
||||
}
|
||||
static defaultProps = {
|
||||
muted: false,
|
||||
mirrored: false
|
||||
}
|
||||
handleClick = e => {
|
||||
const { onClick, userId } = this.props
|
||||
this.play(e)
|
||||
onClick(userId)
|
||||
}
|
||||
play = e => {
|
||||
e.preventDefault()
|
||||
e.target.play()
|
||||
}
|
||||
componentDidMount () {
|
||||
this.componentDidUpdate()
|
||||
}
|
||||
componentDidUpdate () {
|
||||
const { videos, stream } = this.props
|
||||
const { video } = this.refs
|
||||
const mediaStream = stream && stream.mediaStream
|
||||
const url = stream && stream.url
|
||||
if ('srcObject' in video) {
|
||||
if (video.srcObject !== mediaStream) {
|
||||
this.refs.video.srcObject = mediaStream
|
||||
}
|
||||
} else if (video.src !== url) {
|
||||
video.src = url
|
||||
}
|
||||
videos[socket.id] = video
|
||||
}
|
||||
render () {
|
||||
const { active, mirrored, muted } = this.props
|
||||
const className = classnames('video-container', { active, mirrored })
|
||||
return (
|
||||
<div className={className}>
|
||||
<video
|
||||
id={`video-${socket.id}`}
|
||||
autoPlay
|
||||
onClick={this.handleClick}
|
||||
onLoadedMetadata={this.play}
|
||||
playsInline
|
||||
ref="video"
|
||||
muted={muted}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
66
src/client/components/Video.tsx
Normal file
66
src/client/components/Video.tsx
Normal file
@ -0,0 +1,66 @@
|
||||
import React, { ReactEventHandler } from 'react'
|
||||
import classnames from 'classnames'
|
||||
import socket from '../socket'
|
||||
import { AddStreamPayload } from '../actions/StreamActions'
|
||||
|
||||
export interface VideoProps {
|
||||
videos: Record<string, unknown>
|
||||
onClick: (userId: string) => void
|
||||
active: boolean
|
||||
stream?: AddStreamPayload
|
||||
userId: string
|
||||
muted: boolean
|
||||
mirrored: boolean
|
||||
}
|
||||
|
||||
export default class Video extends React.PureComponent<VideoProps> {
|
||||
video = React.createRef<HTMLVideoElement>()
|
||||
|
||||
static defaultProps = {
|
||||
muted: false,
|
||||
mirrored: false,
|
||||
}
|
||||
handleClick: ReactEventHandler<HTMLVideoElement> = e => {
|
||||
const { onClick, userId } = this.props
|
||||
this.play(e)
|
||||
onClick(userId)
|
||||
}
|
||||
play: ReactEventHandler<HTMLVideoElement> = e => {
|
||||
e.preventDefault();
|
||||
(e.target as HTMLVideoElement).play()
|
||||
}
|
||||
componentDidMount () {
|
||||
this.componentDidUpdate()
|
||||
}
|
||||
componentDidUpdate () {
|
||||
const { videos, stream } = this.props
|
||||
const video = this.video.current!
|
||||
const mediaStream = stream && stream.stream || null
|
||||
const url = stream && stream.url
|
||||
if (!('srcObject' in video as unknown)) {
|
||||
if (video.srcObject !== mediaStream) {
|
||||
video.srcObject = mediaStream
|
||||
}
|
||||
} else if (video.src !== url) {
|
||||
video.src = url || ''
|
||||
}
|
||||
videos[socket.id] = video
|
||||
}
|
||||
render () {
|
||||
const { active, mirrored, muted } = this.props
|
||||
const className = classnames('video-container', { active, mirrored })
|
||||
return (
|
||||
<div className={className}>
|
||||
<video
|
||||
id={`video-${socket.id}`}
|
||||
autoPlay
|
||||
onClick={this.handleClick}
|
||||
onLoadedMetadata={this.play}
|
||||
playsInline
|
||||
ref={this.video}
|
||||
muted={muted}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -1,18 +1,18 @@
|
||||
jest.mock('../actions/CallActions.js')
|
||||
jest.mock('../socket.js')
|
||||
jest.mock('../window.js')
|
||||
jest.mock('../actions/CallActions')
|
||||
jest.mock('../socket')
|
||||
jest.mock('../window')
|
||||
|
||||
import * as constants from '../constants.js'
|
||||
import App from './App.js'
|
||||
import * as constants from '../constants'
|
||||
import App from './App'
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import TestUtils from 'react-dom/test-utils'
|
||||
import configureStore from 'redux-mock-store'
|
||||
import reducers from '../reducers'
|
||||
import { MediaStream } from '../window.js'
|
||||
import { MediaStream } from '../window'
|
||||
import { Provider } from 'react-redux'
|
||||
import { init } from '../actions/CallActions.js'
|
||||
import { middlewares } from '../store.js'
|
||||
import { init } from '../actions/CallActions'
|
||||
import { middlewares } from '../store'
|
||||
|
||||
describe('App', () => {
|
||||
|
||||
@ -1,29 +1,30 @@
|
||||
import * as CallActions from '../actions/CallActions.js'
|
||||
import * as NotifyActions from '../actions/NotifyActions.js'
|
||||
import * as PeerActions from '../actions/PeerActions.js'
|
||||
import * as StreamActions from '../actions/StreamActions.js'
|
||||
import App from '../components/App.js'
|
||||
import { bindActionCreators } from 'redux'
|
||||
import * as CallActions from '../actions/CallActions'
|
||||
import * as NotifyActions from '../actions/NotifyActions'
|
||||
import * as PeerActions from '../actions/PeerActions'
|
||||
import * as StreamActions from '../actions/StreamActions'
|
||||
import App from '../components/App'
|
||||
import { bindActionCreators, Dispatch } from 'redux'
|
||||
import { connect } from 'react-redux'
|
||||
import { State } from '../store'
|
||||
|
||||
function mapStateToProps (state) {
|
||||
function mapStateToProps (state: State) {
|
||||
return {
|
||||
streams: state.streams,
|
||||
peers: state.peers,
|
||||
alerts: state.alerts,
|
||||
notifications: state.notifications,
|
||||
messages: state.messages,
|
||||
active: state.active
|
||||
active: state.active,
|
||||
}
|
||||
}
|
||||
|
||||
function mapDispatchToProps (dispatch) {
|
||||
function mapDispatchToProps (dispatch: Dispatch) {
|
||||
return {
|
||||
toggleActive: bindActionCreators(StreamActions.toggleActive, dispatch),
|
||||
sendMessage: bindActionCreators(PeerActions.sendMessage, dispatch),
|
||||
dismissAlert: bindActionCreators(NotifyActions.dismissAlert, dispatch),
|
||||
init: bindActionCreators(CallActions.init, dispatch),
|
||||
onSendFile: bindActionCreators(PeerActions.sendFile, dispatch)
|
||||
onSendFile: bindActionCreators(PeerActions.sendFile, dispatch),
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,49 +1,48 @@
|
||||
const iceServers = require('./iceServers.js')
|
||||
import { config, ICEServer } from '../../server/config'
|
||||
|
||||
function noop () {}
|
||||
|
||||
function checkTURNServer (turnConfig, timeout) {
|
||||
async function checkTURNServer (turnConfig: ICEServer, timeoutDuration = 5000) {
|
||||
console.log('checking turn server', turnConfig)
|
||||
|
||||
return new Promise(function (resolve, reject) {
|
||||
const timeout = new Promise<unknown>((resolve, reject) => {
|
||||
setTimeout(function () {
|
||||
if (promiseResolved) return
|
||||
resolve(false)
|
||||
promiseResolved = true
|
||||
}, timeout || 5000)
|
||||
reject(new Error('timed out'))
|
||||
}, timeoutDuration)
|
||||
})
|
||||
|
||||
let promiseResolved = false
|
||||
async function start() {
|
||||
const PeerConnection = window.RTCPeerConnection ||
|
||||
window.mozRTCPeerConnection ||
|
||||
(
|
||||
window as unknown as { mozRTCPeerConnection: RTCPeerConnection }
|
||||
).mozRTCPeerConnection||
|
||||
window.webkitRTCPeerConnection
|
||||
|
||||
const pc = new PeerConnection({ iceServers: [turnConfig] })
|
||||
|
||||
// create a bogus data channel
|
||||
pc.createDataChannel('')
|
||||
pc.createOffer(function (sdp) {
|
||||
// sometimes sdp contains the ice candidates...
|
||||
if (sdp.sdp.indexOf('typ relay') > -1) {
|
||||
promiseResolved = true
|
||||
const sdp = await pc.createOffer()
|
||||
// sometimes sdp contains the ice candidates...
|
||||
if (sdp.sdp!.indexOf('typ relay') > -1) {
|
||||
return true
|
||||
}
|
||||
pc.setLocalDescription(sdp)
|
||||
|
||||
return new Promise(resolve => {
|
||||
pc.onicecandidate = function (ice) {
|
||||
if (!ice ||
|
||||
!ice.candidate ||
|
||||
!ice.candidate.candidate ||
|
||||
!(ice.candidate.candidate.indexOf('typ relay') > -1)) {
|
||||
return
|
||||
}
|
||||
resolve(true)
|
||||
}
|
||||
pc.setLocalDescription(sdp, noop, noop)
|
||||
}, noop)
|
||||
})
|
||||
}
|
||||
|
||||
pc.onicecandidate = function (ice) {
|
||||
if (promiseResolved ||
|
||||
!ice ||
|
||||
!ice.candidate ||
|
||||
!ice.candidate.candidate ||
|
||||
!(ice.candidate.candidate.indexOf('typ relay') > -1)) {
|
||||
return
|
||||
}
|
||||
promiseResolved = true
|
||||
resolve(true)
|
||||
}
|
||||
})
|
||||
return Promise.race([ timeout, start() ])
|
||||
}
|
||||
|
||||
checkTURNServer(iceServers[0], 10000)
|
||||
checkTURNServer(config.iceServers[0], 10000)
|
||||
.then(console.log.bind(console))
|
||||
.catch(console.error.bind(console))
|
||||
|
||||
@ -2,9 +2,9 @@ import '@babel/polyfill'
|
||||
import App from './containers/App'
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import store from './store.js'
|
||||
import store from './store'
|
||||
import { Provider } from 'react-redux'
|
||||
import { play } from './window.js'
|
||||
import { play } from './window'
|
||||
|
||||
const component = (
|
||||
<Provider store={store}>
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import logger from 'redux-logger'
|
||||
import { create } from './middlewares.js'
|
||||
import { create } from './middlewares'
|
||||
|
||||
describe('store', () => {
|
||||
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
import * as StreamActions from '../actions/StreamActions.js'
|
||||
import active from './active.js'
|
||||
import * as StreamActions from '../actions/StreamActions'
|
||||
import active from './active'
|
||||
|
||||
describe('reducers/active', () => {
|
||||
|
||||
describe('setActive', () => {
|
||||
it('sets active to userId', () => {
|
||||
const userId = 'test'
|
||||
let state = active()
|
||||
let state = active(null, {type: 'test'} as any)
|
||||
state = active(state, StreamActions.setActive(userId))
|
||||
expect(state).toBe(userId)
|
||||
})
|
||||
@ -15,7 +15,7 @@ describe('reducers/active', () => {
|
||||
describe('toggleActive', () => {
|
||||
it('sets active to userId', () => {
|
||||
const userId = 'test'
|
||||
let state = active()
|
||||
let state = active(null, {type: 'test'} as any)
|
||||
state = active(state, StreamActions.toggleActive(userId))
|
||||
expect(state).toBe(userId)
|
||||
state = active(state, StreamActions.toggleActive(userId))
|
||||
|
||||
@ -1,6 +1,12 @@
|
||||
import * as constants from '../constants.js'
|
||||
import * as constants from '../constants'
|
||||
import { StreamAction } from '../actions/StreamActions'
|
||||
|
||||
export default function active (state = null, action: Action) {
|
||||
export type ActiveState = null | string
|
||||
|
||||
export default function active (
|
||||
state: ActiveState = null,
|
||||
action: StreamAction,
|
||||
): ActiveState {
|
||||
switch (action && action.type) {
|
||||
case constants.ACTIVE_SET:
|
||||
case constants.STREAM_ADD:
|
||||
|
||||
@ -1,38 +1,34 @@
|
||||
import * as NotifyActions from '../actions/NotifyActions.js'
|
||||
import * as NotifyActions from '../actions/NotifyActions'
|
||||
import _ from 'underscore'
|
||||
import { applyMiddleware, createStore } from 'redux'
|
||||
import { create } from '../middlewares.js'
|
||||
import reducers from './index.js'
|
||||
import { applyMiddleware, createStore, Store } from 'redux'
|
||||
import { create } from '../middlewares'
|
||||
import reducers from './index'
|
||||
|
||||
jest.useFakeTimers()
|
||||
|
||||
describe('reducers/alerts', () => {
|
||||
|
||||
let store
|
||||
let store: Store
|
||||
beforeEach(() => {
|
||||
store = createStore(
|
||||
reducers,
|
||||
applyMiddleware.apply(null, create())
|
||||
applyMiddleware(...create()),
|
||||
)
|
||||
})
|
||||
|
||||
describe('clearAlert', () => {
|
||||
|
||||
const actions = {
|
||||
true: 'Dismiss',
|
||||
false: ''
|
||||
}
|
||||
;[true, false].forEach(dismissable => {
|
||||
[true, false].forEach(dismissable => {
|
||||
beforeEach(() => {
|
||||
store.dispatch(NotifyActions.clearAlerts())
|
||||
})
|
||||
it('adds alert to store', () => {
|
||||
store.dispatch(NotifyActions.alert('test', dismissable))
|
||||
expect(store.getState().alerts).toEqual([{
|
||||
action: actions[dismissable],
|
||||
action: dismissable ? 'Dismiss' : undefined,
|
||||
dismissable,
|
||||
message: 'test',
|
||||
type: 'warning'
|
||||
type: 'warning',
|
||||
}])
|
||||
})
|
||||
})
|
||||
@ -51,25 +47,36 @@ describe('reducers/alerts', () => {
|
||||
it('does not remove an alert when not found', () => {
|
||||
store.dispatch(NotifyActions.alert('test', true))
|
||||
expect(store.getState().alerts.length).toBe(1)
|
||||
store.dispatch(NotifyActions.dismissAlert({}))
|
||||
store.dispatch(NotifyActions.dismissAlert({
|
||||
action: undefined,
|
||||
dismissable: false,
|
||||
message: 'bla',
|
||||
type: 'error',
|
||||
}))
|
||||
expect(store.getState().alerts.length).toBe(1)
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
;['info', 'warning', 'error'].forEach(type => {
|
||||
const methods: Array<'info' | 'warning' | 'error'> = [
|
||||
'info',
|
||||
'warning',
|
||||
'error',
|
||||
]
|
||||
|
||||
methods.forEach(type => {
|
||||
|
||||
describe(type, () => {
|
||||
|
||||
beforeEach(() => {
|
||||
store.dispatch(NotifyActions[type]('Hi {0}!', 'John'))
|
||||
NotifyActions[type]('Hi {0}!', 'John')(store.dispatch)
|
||||
})
|
||||
|
||||
it('adds a notification', () => {
|
||||
expect(_.values(store.getState().notifications)).toEqual([{
|
||||
id: jasmine.any(String),
|
||||
message: 'Hi John!',
|
||||
type
|
||||
type,
|
||||
}])
|
||||
})
|
||||
|
||||
@ -79,7 +86,7 @@ describe('reducers/alerts', () => {
|
||||
})
|
||||
|
||||
it('does not fail when no arguments', () => {
|
||||
store.dispatch(NotifyActions[type]())
|
||||
NotifyActions[type]()(store.dispatch)
|
||||
})
|
||||
|
||||
})
|
||||
@ -89,9 +96,9 @@ describe('reducers/alerts', () => {
|
||||
describe('clear', () => {
|
||||
|
||||
it('clears all alerts', () => {
|
||||
store.dispatch(NotifyActions.info('Hi {0}!', 'John'))
|
||||
store.dispatch(NotifyActions.warning('Hi {0}!', 'John'))
|
||||
store.dispatch(NotifyActions.error('Hi {0}!', 'John'))
|
||||
NotifyActions.info('Hi {0}!', 'John')(store.dispatch)
|
||||
NotifyActions.warning('Hi {0}!', 'John')(store.dispatch)
|
||||
NotifyActions.error('Hi {0}!', 'John')(store.dispatch)
|
||||
store.dispatch(NotifyActions.clear())
|
||||
expect(store.getState().notifications).toEqual({})
|
||||
})
|
||||
|
||||
@ -1,14 +1,14 @@
|
||||
import * as constants from '../constants.js'
|
||||
import Immutable from 'seamless-immutable'
|
||||
import * as constants from '../constants'
|
||||
import { AlertActionType, Alert } from '../actions/NotifyActions'
|
||||
|
||||
const defaultState = Immutable([])
|
||||
export type AlertState = Alert[]
|
||||
|
||||
export default function alerts (state = defaultState, action) {
|
||||
switch (action && action.type) {
|
||||
const defaultState: AlertState = []
|
||||
|
||||
export default function alerts (state = defaultState, action: AlertActionType) {
|
||||
switch (action.type) {
|
||||
case constants.ALERT:
|
||||
const alerts = state.asMutable()
|
||||
alerts.push(action.payload)
|
||||
return Immutable(alerts)
|
||||
return [...state, action.payload]
|
||||
case constants.ALERT_DISMISS:
|
||||
return state.filter(a => a !== action.payload)
|
||||
case constants.ALERT_CLEAR:
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
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 active from './active'
|
||||
import alerts from './alerts'
|
||||
import notifications from './notifications'
|
||||
import messages from './messages'
|
||||
import peers from './peers'
|
||||
import streams from './streams'
|
||||
import { combineReducers } from 'redux'
|
||||
|
||||
export default combineReducers({
|
||||
@ -12,5 +12,5 @@ export default combineReducers({
|
||||
notifications,
|
||||
messages,
|
||||
peers,
|
||||
streams
|
||||
streams,
|
||||
})
|
||||
|
||||
@ -1,17 +1,16 @@
|
||||
import * as ChatActions from '../actions/ChatActions.js'
|
||||
import messages from './messages.js'
|
||||
import * as ChatActions from '../actions/ChatActions'
|
||||
import messages from './messages'
|
||||
|
||||
describe('reducers/messages', () => {
|
||||
|
||||
describe('addMessage', () => {
|
||||
it('add message to chat', () => {
|
||||
const payload = {
|
||||
const payload: ChatActions.Message = {
|
||||
userId: 'test',
|
||||
message: 'hello',
|
||||
timestamp: new Date(),
|
||||
image: null
|
||||
timestamp: new Date().toLocaleString(),
|
||||
}
|
||||
let state = messages()
|
||||
let state = messages(undefined, {type: 'test'} as any)
|
||||
state = messages(state, ChatActions.addMessage(payload))
|
||||
expect(state).toEqual([payload])
|
||||
})
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import * as constants from '../constants.js'
|
||||
import { Message, MessageAddAction } from '../actions/ChatActions.js'
|
||||
import * as constants from '../constants'
|
||||
import { Message, MessageAddAction } from '../actions/ChatActions'
|
||||
|
||||
export type MessagesState = Message[]
|
||||
|
||||
|
||||
@ -1,16 +1,28 @@
|
||||
import * as constants from '../constants.js'
|
||||
import Immutable from 'seamless-immutable'
|
||||
import * as constants from '../constants'
|
||||
import { Notification, NotificationActionType } from '../actions/NotifyActions'
|
||||
|
||||
const defaultState = Immutable({})
|
||||
export type NotificationState = Record<string, Notification>
|
||||
|
||||
export default function notifications (state = defaultState, action) {
|
||||
switch (action && action.type) {
|
||||
const defaultState: NotificationState = {}
|
||||
|
||||
export default function notifications (
|
||||
state = defaultState,
|
||||
action: NotificationActionType,
|
||||
) {
|
||||
switch (action.type) {
|
||||
case constants.NOTIFY:
|
||||
return state.merge({
|
||||
[action.payload.id]: action.payload
|
||||
})
|
||||
return {
|
||||
...state,
|
||||
[action.payload.id]: action.payload,
|
||||
}
|
||||
case constants.NOTIFY_DISMISS:
|
||||
return state.without(action.payload.id)
|
||||
return Object
|
||||
.keys(state)
|
||||
.filter(key => key !== action.payload.id)
|
||||
.reduce((obj, key) => {
|
||||
obj[key] = state[key]
|
||||
return obj
|
||||
}, {} as NotificationState)
|
||||
case constants.NOTIFY_CLEAR:
|
||||
return defaultState
|
||||
default:
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import * as constants from '../constants.js'
|
||||
import * as constants from '../constants'
|
||||
import _ from 'underscore'
|
||||
import Peer from 'simple-peer'
|
||||
import { PeerAction } from '../actions/PeerActions'
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
jest.mock('../window.js')
|
||||
jest.mock('../window')
|
||||
|
||||
import * as StreamActions from '../actions/StreamActions.js'
|
||||
import reducers from './index.js'
|
||||
import { createObjectURL, MediaStream } from '../window.js'
|
||||
import * as StreamActions from '../actions/StreamActions'
|
||||
import reducers from './index'
|
||||
import { createObjectURL, MediaStream } from '../window'
|
||||
import { applyMiddleware, createStore, Store } from 'redux'
|
||||
import { create } from '../middlewares.js'
|
||||
import { create } from '../middlewares'
|
||||
|
||||
describe('reducers/alerts', () => {
|
||||
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import _ from 'underscore'
|
||||
import { createObjectURL, revokeObjectURL } from '../window.js'
|
||||
import { createObjectURL, revokeObjectURL } from '../window'
|
||||
import _debug from 'debug'
|
||||
import { AddStreamPayload, AddStreamAction, RemoveStreamAction, StreamAction } from '../actions/StreamActions.js'
|
||||
import { STREAM_ADD, STREAM_REMOVE } from '../constants.js'
|
||||
import { AddStreamPayload, AddStreamAction, RemoveStreamAction, StreamAction } from '../actions/StreamActions'
|
||||
import { STREAM_ADD, STREAM_REMOVE } from '../constants'
|
||||
|
||||
const debug = _debug('peercalls')
|
||||
const defaultState = Object.freeze({})
|
||||
|
||||
@ -1,3 +1,3 @@
|
||||
import SocketIOClient from 'socket.io-client'
|
||||
import { baseUrl } from './window.js'
|
||||
import { baseUrl } from './window'
|
||||
export default SocketIOClient('', { path: baseUrl + '/ws' })
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { create } from './middlewares.js'
|
||||
import { create } from './middlewares'
|
||||
import reducers from './reducers'
|
||||
import { applyMiddleware, createStore as _createStore, Store as ReduxStore } from 'redux'
|
||||
export const middlewares = create(
|
||||
|
||||
@ -5,7 +5,7 @@ import {
|
||||
navigator,
|
||||
play,
|
||||
valueOf,
|
||||
} from './window.js'
|
||||
} from './window'
|
||||
|
||||
describe('window', () => {
|
||||
|
||||
|
||||
10
src/screenfull.d.ts
vendored
Normal file
10
src/screenfull.d.ts
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
declare module 'screenfull' {
|
||||
interface Screenfull {
|
||||
enabled: boolean
|
||||
toggle: () => void
|
||||
}
|
||||
|
||||
declare const screenfull: Screenfull
|
||||
|
||||
export = screenfull
|
||||
}
|
||||
@ -154,11 +154,11 @@ body.call {
|
||||
transition: opacity 200ms ease-in;
|
||||
}
|
||||
|
||||
.fade-leave {
|
||||
.fade-exit {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.fade-leave.fade-leave-active {
|
||||
.fade-exit.fade-exit-active {
|
||||
opacity: 0.01;
|
||||
transition: opacity 100ms ease-in;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user