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