Refactor actions/reducers to TS
This commit is contained in:
parent
1eaca46a16
commit
e9926e3484
149
package-lock.json
generated
149
package-lock.json
generated
@ -1933,6 +1933,12 @@
|
||||
"@types/babel-types": "*"
|
||||
}
|
||||
},
|
||||
"@types/bluebird": {
|
||||
"version": "3.5.29",
|
||||
"resolved": "https://registry.npmjs.org/@types/bluebird/-/bluebird-3.5.29.tgz",
|
||||
"integrity": "sha512-kmVtnxTuUuhCET669irqQmPAez4KFnFVKvpleVRyfC3g+SHD1hIkFZcWLim9BVcwUBLO59o8VZE4yGCmTif8Yw==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/body-parser": {
|
||||
"version": "1.17.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.17.1.tgz",
|
||||
@ -1997,6 +2003,33 @@
|
||||
"@types/range-parser": "*"
|
||||
}
|
||||
},
|
||||
"@types/hoist-non-react-statics": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz",
|
||||
"integrity": "sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/react": "*",
|
||||
"hoist-non-react-statics": "^3.3.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"hoist-non-react-statics": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.0.tgz",
|
||||
"integrity": "sha512-0XsbTXxgiaCDYDIWFcwkmerZPSwywfUqYmwT4jzewKTQSWoE6FCMoUVOeBJWK3E/CrWbxRG3m5GzY4lnIwGRBA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"react-is": "^16.7.0"
|
||||
}
|
||||
},
|
||||
"react-is": {
|
||||
"version": "16.11.0",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.11.0.tgz",
|
||||
"integrity": "sha512-gbBVYR2p8mnriqAwWx9LbuUrShnAuSCNnuPGyc7GJrMVQtPDAh8iLpv7FRuMPFb56KkaVZIYSz1PrjI9q0QPCw==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"@types/istanbul-lib-coverage": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.1.tgz",
|
||||
@ -2049,12 +2082,107 @@
|
||||
"integrity": "sha512-E6Zn0rffhgd130zbCbAr/JdXfXkoOUFAKNs/rF8qnafSJ8KYaA/j3oz7dcwal+lYjLA7xvdd5J4wdYpCTlP8+w==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/prop-types": {
|
||||
"version": "15.7.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.3.tgz",
|
||||
"integrity": "sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/range-parser": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.3.tgz",
|
||||
"integrity": "sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/react": {
|
||||
"version": "16.9.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-16.9.11.tgz",
|
||||
"integrity": "sha512-UBT4GZ3PokTXSWmdgC/GeCGEJXE5ofWyibCcecRLUVN2ZBpXQGVgQGtG2foS7CrTKFKlQVVswLvf7Js6XA/CVQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/prop-types": "*",
|
||||
"csstype": "^2.2.0"
|
||||
}
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"version": "16.9.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-16.9.4.tgz",
|
||||
"integrity": "sha512-fya9xteU/n90tda0s+FtN5Ym4tbgxpq/hb/Af24dvs6uYnYn+fspaxw5USlw0R8apDNwxsqumdRoCoKitckQqw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"@types/react-redux": {
|
||||
"version": "7.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.5.tgz",
|
||||
"integrity": "sha512-ZoNGQMDxh5ENY7PzU7MVonxDzS1l/EWiy8nUhDqxFqUZn4ovboCyvk4Djf68x6COb7vhGTKjyjxHxtFdAA5sUA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/hoist-non-react-statics": "^3.3.0",
|
||||
"@types/react": "*",
|
||||
"hoist-non-react-statics": "^3.3.0",
|
||||
"redux": "^4.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"hoist-non-react-statics": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.0.tgz",
|
||||
"integrity": "sha512-0XsbTXxgiaCDYDIWFcwkmerZPSwywfUqYmwT4jzewKTQSWoE6FCMoUVOeBJWK3E/CrWbxRG3m5GzY4lnIwGRBA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"react-is": "^16.7.0"
|
||||
}
|
||||
},
|
||||
"react-is": {
|
||||
"version": "16.11.0",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.11.0.tgz",
|
||||
"integrity": "sha512-gbBVYR2p8mnriqAwWx9LbuUrShnAuSCNnuPGyc7GJrMVQtPDAh8iLpv7FRuMPFb56KkaVZIYSz1PrjI9q0QPCw==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"@types/redux": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/redux/-/redux-3.6.0.tgz",
|
||||
"integrity": "sha1-8evh5UEVGAcuT9/KXHbhbnTBOZo=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"redux": "*"
|
||||
}
|
||||
},
|
||||
"@types/redux-logger": {
|
||||
"version": "3.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/redux-logger/-/redux-logger-3.0.7.tgz",
|
||||
"integrity": "sha512-oV9qiCuowhVR/ehqUobWWkXJjohontbDGLV88Be/7T4bqMQ3kjXwkFNL7doIIqlbg3X2PC5WPziZ8/j/QHNQ4A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"redux": "^3.6.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"redux": {
|
||||
"version": "3.7.2",
|
||||
"resolved": "https://registry.npmjs.org/redux/-/redux-3.7.2.tgz",
|
||||
"integrity": "sha512-pNqnf9q1hI5HHZRBkj3bAngGZW/JMCmexDlOxw4XagXY2o1327nHH54LoTjiPJ0gizoqPDRqWyX/00g0hD6w+A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"lodash": "^4.2.1",
|
||||
"lodash-es": "^4.2.1",
|
||||
"loose-envify": "^1.1.0",
|
||||
"symbol-observable": "^1.0.3"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@types/redux-mock-store": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/redux-mock-store/-/redux-mock-store-1.0.1.tgz",
|
||||
"integrity": "sha512-1egEnh2/+sRRKImnCo5EMVm0Uxu4fBHeLHk/inhSp/VpE93It8lk3gYeNfehUgXd6OzqP5LLA9kzO9x7o3WfwA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"redux": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"@types/serve-static": {
|
||||
"version": "1.13.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.3.tgz",
|
||||
@ -2065,6 +2193,15 @@
|
||||
"@types/mime": "*"
|
||||
}
|
||||
},
|
||||
"@types/simple-peer": {
|
||||
"version": "6.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/simple-peer/-/simple-peer-6.1.6.tgz",
|
||||
"integrity": "sha512-jnktVe4nDkNAIf/5brRQ4VqMP845dtRCrFMRagOxiTdC9gZ3rpnidt8EGR89b55Wtol+otYEJveFkEy3IpxA0Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"@types/socket.io": {
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/socket.io/-/socket.io-2.1.4.tgz",
|
||||
@ -4330,6 +4467,12 @@
|
||||
"cssom": "~0.3.6"
|
||||
}
|
||||
},
|
||||
"csstype": {
|
||||
"version": "2.6.7",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.7.tgz",
|
||||
"integrity": "sha512-9Mcn9sFbGBAdmimWb2gLVDtFJzeKtDGIr76TUqmjZrw9LFXBMSU70lcs+C0/7fyCd6iBDqmksUcCOUIkisPHsQ==",
|
||||
"dev": true
|
||||
},
|
||||
"currently-unhandled": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz",
|
||||
@ -9956,6 +10099,12 @@
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
|
||||
"integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A=="
|
||||
},
|
||||
"lodash-es": {
|
||||
"version": "4.17.15",
|
||||
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.15.tgz",
|
||||
"integrity": "sha512-rlrc3yU3+JNOpZ9zj5pQtxnx2THmvRykwL4Xlxoa8I9lHBlVbbyPhgyPMioxVZ4NqyxaVVtaJnzsyOidQIhyyQ==",
|
||||
"dev": true
|
||||
},
|
||||
"lodash.debounce": {
|
||||
"version": "4.0.8",
|
||||
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
|
||||
|
||||
@ -80,11 +80,19 @@
|
||||
"@babel/polyfill": "^7.4.4",
|
||||
"@babel/preset-env": "^7.5.0",
|
||||
"@babel/preset-react": "^7.0.0",
|
||||
"@types/bluebird": "^3.5.29",
|
||||
"@types/config": "0.0.36",
|
||||
"@types/debug": "^4.1.5",
|
||||
"@types/express": "^4.17.2",
|
||||
"@types/jest": "^24.0.23",
|
||||
"@types/node": "^12.12.7",
|
||||
"@types/react": "^16.9.11",
|
||||
"@types/react-dom": "^16.9.4",
|
||||
"@types/react-redux": "^7.1.5",
|
||||
"@types/redux": "^3.6.0",
|
||||
"@types/redux-logger": "^3.0.7",
|
||||
"@types/redux-mock-store": "^1.0.1",
|
||||
"@types/simple-peer": "^6.1.6",
|
||||
"@types/socket.io": "^2.1.4",
|
||||
"@types/socket.io-client": "^1.4.32",
|
||||
"@types/supertest": "^2.0.8",
|
||||
|
||||
@ -7,21 +7,21 @@ export const revokeObjectURL = jest.fn()
|
||||
export class MediaStream {
|
||||
getVideoTracks () {
|
||||
return [{
|
||||
enabled: true
|
||||
enabled: true,
|
||||
}]
|
||||
}
|
||||
getAudioTracks () {
|
||||
return [{
|
||||
enabled: true
|
||||
enabled: true,
|
||||
}]
|
||||
}
|
||||
}
|
||||
export function getUserMedia () {
|
||||
return !getUserMedia.shouldFail
|
||||
return !(getUserMedia as any).shouldFail
|
||||
? Promise.resolve(getUserMedia.stream)
|
||||
: Promise.reject(new Error('test'))
|
||||
}
|
||||
getUserMedia.fail = shouldFail => getUserMedia.shouldFail = shouldFail
|
||||
getUserMedia.fail = (shouldFail: boolean) => (getUserMedia as any).shouldFail = shouldFail
|
||||
getUserMedia.stream = new MediaStream()
|
||||
|
||||
export const navigator = window.navigator
|
||||
@ -1,50 +0,0 @@
|
||||
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 Promise from 'bluebird'
|
||||
import socket from '../socket.js'
|
||||
import { callId, getUserMedia } from '../window.js'
|
||||
|
||||
export const init = () => dispatch => {
|
||||
return dispatch({
|
||||
type: constants.INIT,
|
||||
payload: Promise.all([
|
||||
connect()(dispatch),
|
||||
getCameraStream()(dispatch)
|
||||
])
|
||||
.spread((socket, stream) => {
|
||||
dispatch(SocketActions.handshake({
|
||||
socket,
|
||||
roomName: callId,
|
||||
stream
|
||||
}))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export const connect = () => dispatch => {
|
||||
return new Promise(resolve => {
|
||||
socket.once('connect', () => {
|
||||
resolve(socket)
|
||||
})
|
||||
socket.on('connect', () => {
|
||||
dispatch(NotifyActions.warning('Connected to server socket'))
|
||||
})
|
||||
socket.on('disconnect', () => {
|
||||
dispatch(NotifyActions.error('Server socket disconnected'))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export const getCameraStream = () => dispatch => {
|
||||
return getUserMedia({ video: { facingMode: 'user' }, audio: true })
|
||||
.then(stream => {
|
||||
dispatch(StreamActions.addStream({ stream, userId: constants.ME }))
|
||||
return stream
|
||||
})
|
||||
.catch(() => {
|
||||
dispatch(NotifyActions.alert('Could not get access to microphone & camera'))
|
||||
return null
|
||||
})
|
||||
}
|
||||
62
src/client/actions/CallActions.ts
Normal file
62
src/client/actions/CallActions.ts
Normal file
@ -0,0 +1,62 @@
|
||||
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 { Dispatch } from 'redux'
|
||||
import { GetState } from '../store.js'
|
||||
|
||||
export interface InitAction {
|
||||
type: 'INIT'
|
||||
payload: Promise<void>
|
||||
}
|
||||
|
||||
interface InitializeAction {
|
||||
type: 'INIT'
|
||||
}
|
||||
|
||||
const initialize = (): InitializeAction => ({
|
||||
type: 'INIT',
|
||||
})
|
||||
|
||||
export const init = async (dispatch: Dispatch, getState: GetState) => {
|
||||
dispatch(initialize())
|
||||
|
||||
const socket = await connect(dispatch)
|
||||
const stream = await getCameraStream(dispatch)
|
||||
|
||||
SocketActions.handshake({
|
||||
socket,
|
||||
roomName: callId,
|
||||
stream,
|
||||
})(dispatch, getState)
|
||||
}
|
||||
|
||||
export async function connect (dispatch: Dispatch) {
|
||||
return new Promise<SocketIOClient.Socket>(resolve => {
|
||||
socket.once('connect', () => {
|
||||
resolve(socket)
|
||||
})
|
||||
socket.on('connect', () => {
|
||||
NotifyActions.warning('Connected to server socket')(dispatch)
|
||||
})
|
||||
socket.on('disconnect', () => {
|
||||
NotifyActions.error('Server socket disconnected')(dispatch)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export async function getCameraStream (dispatch: Dispatch) {
|
||||
try {
|
||||
const stream = await getUserMedia({
|
||||
video: { facingMode: 'user' },
|
||||
audio: true,
|
||||
})
|
||||
dispatch(StreamActions.addStream({ stream, userId: constants.ME }))
|
||||
return stream
|
||||
} catch (err) {
|
||||
dispatch(NotifyActions.alert('Could not get access to microphone & camera'))
|
||||
return
|
||||
}
|
||||
}
|
||||
@ -1,11 +0,0 @@
|
||||
import * as constants from '../constants.js'
|
||||
|
||||
export const addMessage = ({ userId, message, timestamp, image }) => ({
|
||||
type: constants.MESSAGE_ADD,
|
||||
payload: {
|
||||
userId,
|
||||
message,
|
||||
timestamp,
|
||||
image
|
||||
}
|
||||
})
|
||||
18
src/client/actions/ChatActions.ts
Normal file
18
src/client/actions/ChatActions.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { MESSAGE_ADD } from '../constants.js'
|
||||
|
||||
export interface MessageAddAction {
|
||||
type: 'MESSAGE_ADD'
|
||||
payload: Message
|
||||
}
|
||||
|
||||
export interface Message {
|
||||
userId: string
|
||||
message: string
|
||||
timestamp: string
|
||||
image?: string
|
||||
}
|
||||
|
||||
export const addMessage = (message: Message): MessageAddAction => ({
|
||||
type: MESSAGE_ADD,
|
||||
payload: message,
|
||||
})
|
||||
@ -1,75 +0,0 @@
|
||||
import * as constants from '../constants.js'
|
||||
import * as ChatActions from './ChatActions.js'
|
||||
import _ from 'underscore'
|
||||
|
||||
const TIMEOUT = 5000
|
||||
|
||||
function format (string, args) {
|
||||
string = args
|
||||
.reduce((string, arg, i) => string.replace('{' + i + '}', arg), string)
|
||||
return string
|
||||
}
|
||||
|
||||
const _notify = (type, args) => dispatch => {
|
||||
let string = args[0] || ''
|
||||
let message = format(string, Array.prototype.slice.call(args, 1))
|
||||
const id = _.uniqueId('notification')
|
||||
const payload = { id, type, message }
|
||||
dispatch({
|
||||
type: constants.NOTIFY,
|
||||
payload
|
||||
})
|
||||
dispatch(ChatActions.addMessage({
|
||||
userId: '[PeerCalls]',
|
||||
message,
|
||||
timestamp: new Date().toLocaleString(),
|
||||
image: null
|
||||
}))
|
||||
setTimeout(() => {
|
||||
dispatch({
|
||||
type: constants.NOTIFY_DISMISS,
|
||||
payload: { id }
|
||||
})
|
||||
}, TIMEOUT)
|
||||
}
|
||||
|
||||
export const info = function () {
|
||||
return dispatch => _notify('info', arguments)(dispatch)
|
||||
}
|
||||
|
||||
export const warning = function () {
|
||||
return dispatch => _notify('warning', arguments)(dispatch)
|
||||
}
|
||||
|
||||
export const error = function () {
|
||||
return dispatch => _notify('error', arguments)(dispatch)
|
||||
}
|
||||
|
||||
export const clear = () => ({
|
||||
type: constants.NOTIFY_CLEAR
|
||||
})
|
||||
|
||||
export function alert (message, dismissable) {
|
||||
return {
|
||||
type: constants.ALERT,
|
||||
payload: {
|
||||
action: dismissable ? 'Dismiss' : '',
|
||||
dismissable: !!dismissable,
|
||||
message,
|
||||
type: 'warning'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const dismissAlert = alert => {
|
||||
return {
|
||||
type: constants.ALERT_DISMISS,
|
||||
payload: alert
|
||||
}
|
||||
}
|
||||
|
||||
export const clearAlerts = () => {
|
||||
return {
|
||||
type: constants.ALERT_CLEAR
|
||||
}
|
||||
}
|
||||
137
src/client/actions/NotifyActions.ts
Normal file
137
src/client/actions/NotifyActions.ts
Normal file
@ -0,0 +1,137 @@
|
||||
import * as constants from '../constants.js'
|
||||
import * as ChatActions from './ChatActions.js'
|
||||
import { Dispatch } from 'redux'
|
||||
import _ from 'underscore'
|
||||
|
||||
const TIMEOUT = 5000
|
||||
|
||||
function format (string: string, args: string[]) {
|
||||
string = args
|
||||
.reduce((string, arg, i) => string.replace('{' + i + '}', arg), string)
|
||||
return string
|
||||
}
|
||||
|
||||
export type NotifyType = 'info' | 'warning' | 'error'
|
||||
|
||||
const _notify = (type: NotifyType, args: string[]) => (dispatch: Dispatch) => {
|
||||
const string = args[0] || ''
|
||||
const message = format(string, Array.prototype.slice.call(args, 1))
|
||||
const id = _.uniqueId('notification')
|
||||
const payload: Notification = { id, type, message }
|
||||
dispatch(addNotification(payload))
|
||||
dispatch(ChatActions.addMessage({
|
||||
userId: '[PeerCalls]',
|
||||
message,
|
||||
timestamp: new Date().toLocaleString(),
|
||||
image: undefined,
|
||||
}))
|
||||
setTimeout(() => {
|
||||
dispatch(dismissNotification(id))
|
||||
}, TIMEOUT)
|
||||
}
|
||||
|
||||
function addNotification(payload: Notification): NotificationAddAction {
|
||||
return {
|
||||
type: constants.NOTIFY,
|
||||
payload,
|
||||
}
|
||||
}
|
||||
|
||||
function dismissNotification(id: string): NotificationDismissAction {
|
||||
return {
|
||||
type: constants.NOTIFY_DISMISS,
|
||||
payload: { id },
|
||||
}
|
||||
}
|
||||
|
||||
export interface Notification {
|
||||
id: string
|
||||
type: NotifyType
|
||||
message: string
|
||||
}
|
||||
|
||||
export interface NotificationAddAction {
|
||||
type: 'NOTIFY'
|
||||
payload: Notification
|
||||
}
|
||||
|
||||
export interface NotificationDismissAction {
|
||||
type: 'NOTIFY_DISMISS'
|
||||
payload: { id: string }
|
||||
}
|
||||
|
||||
export function info (...args: any[]) {
|
||||
return (dispatch: Dispatch) => _notify('info', args)(dispatch)
|
||||
}
|
||||
|
||||
export function warning (...args: any[]) {
|
||||
return (dispatch: Dispatch) => _notify('warning', args)(dispatch)
|
||||
}
|
||||
|
||||
export function error (...args: any[]) {
|
||||
return (dispatch: Dispatch) => _notify('error', args)(dispatch)
|
||||
}
|
||||
|
||||
export interface NotificationClearAction {
|
||||
type: 'NOTIFY_CLEAR'
|
||||
}
|
||||
|
||||
export const clear = (): NotificationClearAction => ({
|
||||
type: constants.NOTIFY_CLEAR,
|
||||
})
|
||||
|
||||
export interface Alert {
|
||||
action?: string
|
||||
dismissable: boolean
|
||||
message: string
|
||||
type: NotifyType
|
||||
}
|
||||
|
||||
export interface AlertAddAction {
|
||||
type: 'ALERT'
|
||||
payload: Alert
|
||||
}
|
||||
|
||||
export function alert (message: string, dismissable = false): AlertAddAction {
|
||||
return {
|
||||
type: constants.ALERT,
|
||||
payload: {
|
||||
action: dismissable ? 'Dismiss' : '',
|
||||
dismissable: !!dismissable,
|
||||
message,
|
||||
type: 'warning',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export interface AlertDismissAction {
|
||||
type: 'ALERT_DISMISS'
|
||||
payload: Alert
|
||||
}
|
||||
|
||||
export const dismissAlert = (alert: Alert): AlertDismissAction => {
|
||||
return {
|
||||
type: constants.ALERT_DISMISS,
|
||||
payload: alert,
|
||||
}
|
||||
}
|
||||
|
||||
export interface AlertClearAction {
|
||||
type: 'ALERT_CLEAR'
|
||||
}
|
||||
|
||||
export const clearAlerts = (): AlertClearAction => {
|
||||
return {
|
||||
type: constants.ALERT_CLEAR,
|
||||
}
|
||||
}
|
||||
|
||||
export type AlertActionType =
|
||||
AlertAddAction |
|
||||
AlertDismissAction |
|
||||
AlertClearAction
|
||||
|
||||
export type NotificationActionType =
|
||||
NotificationAddAction |
|
||||
NotificationDismissAction |
|
||||
NotificationClearAction
|
||||
@ -1,169 +0,0 @@
|
||||
jest.mock('../window.js')
|
||||
jest.mock('simple-peer')
|
||||
|
||||
import * as PeerActions from './PeerActions.js'
|
||||
import Peer from 'simple-peer'
|
||||
import { EventEmitter } from 'events'
|
||||
import { createStore } from '../store.js'
|
||||
import { play } from '../window.js'
|
||||
|
||||
describe('PeerActions', () => {
|
||||
function createSocket () {
|
||||
const socket = new EventEmitter()
|
||||
socket.id = 'user1'
|
||||
return socket
|
||||
}
|
||||
|
||||
let socket, stream, user, store
|
||||
beforeEach(() => {
|
||||
store = createStore()
|
||||
|
||||
user = { id: 'user2' }
|
||||
socket = createSocket()
|
||||
Peer.instances = []
|
||||
Peer.mockClear()
|
||||
play.mockClear()
|
||||
stream = { stream: true }
|
||||
})
|
||||
|
||||
describe('create', () => {
|
||||
it('creates a new peer', () => {
|
||||
store.dispatch(
|
||||
PeerActions.createPeer({ socket, user, initiator: 'user2', stream })
|
||||
)
|
||||
|
||||
expect(Peer.instances.length).toBe(1)
|
||||
expect(Peer.mock.calls.length).toBe(1)
|
||||
expect(Peer.mock.calls[0][0].initiator).toBe(false)
|
||||
expect(Peer.mock.calls[0][0].stream).toBe(stream)
|
||||
})
|
||||
|
||||
it('sets initiator correctly', () => {
|
||||
store.dispatch(
|
||||
PeerActions.createPeer({ socket, user, initiator: 'user1', stream })
|
||||
)
|
||||
|
||||
expect(Peer.instances.length).toBe(1)
|
||||
expect(Peer.mock.calls.length).toBe(1)
|
||||
expect(Peer.mock.calls[0][0].initiator).toBe(true)
|
||||
expect(Peer.mock.calls[0][0].stream).toBe(stream)
|
||||
})
|
||||
|
||||
it('destroys old peer before creating new one', () => {
|
||||
store.dispatch(
|
||||
PeerActions.createPeer({ socket, user, initiator: 'user2', stream })
|
||||
)
|
||||
store.dispatch(
|
||||
PeerActions.createPeer({ socket, user, initiator: 'user2', stream })
|
||||
)
|
||||
|
||||
expect(Peer.instances.length).toBe(2)
|
||||
expect(Peer.mock.calls.length).toBe(2)
|
||||
expect(Peer.instances[0].destroy.mock.calls.length).toBe(1)
|
||||
expect(Peer.instances[1].destroy.mock.calls.length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('events', () => {
|
||||
let peer
|
||||
|
||||
beforeEach(() => {
|
||||
store.dispatch(
|
||||
PeerActions.createPeer({ socket, user, initiator: 'user1', stream })
|
||||
)
|
||||
peer = Peer.instances[0]
|
||||
})
|
||||
|
||||
describe('connect', () => {
|
||||
beforeEach(() => peer.emit('connect'))
|
||||
|
||||
it('dispatches "play" action', () => {
|
||||
expect(play.mock.calls.length).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('data', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
window.TextDecoder = class TextDecoder {
|
||||
constructor (encoding) {
|
||||
this.encoding = encoding
|
||||
}
|
||||
decode (object) {
|
||||
return object.toString(this.encoding)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it('decodes a message', () => {
|
||||
const payload = 'test'
|
||||
const object = JSON.stringify({ payload })
|
||||
peer.emit('data', Buffer.from(object, 'utf-8'))
|
||||
const { messages } = store.getState()
|
||||
expect(messages[messages.length - 1]).toEqual({
|
||||
userId: 'user2',
|
||||
timestamp: jasmine.any(String),
|
||||
image: null,
|
||||
message: 'test'
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('get', () => {
|
||||
it('returns undefined when not found', () => {
|
||||
const { peers } = store.getState()
|
||||
expect(peers[user.id]).not.toBeDefined()
|
||||
})
|
||||
|
||||
it('returns Peer instance when found', () => {
|
||||
store.dispatch(
|
||||
PeerActions.createPeer({ socket, user, initiator: 'user2', stream })
|
||||
)
|
||||
|
||||
const { peers } = store.getState()
|
||||
expect(peers[user.id]).toBe(Peer.instances[0])
|
||||
})
|
||||
})
|
||||
|
||||
describe('destroyPeers', () => {
|
||||
it('destroys all peers and removes them', () => {
|
||||
store.dispatch(PeerActions.createPeer({
|
||||
socket, user: { id: 'user2' }, initiator: 'user2', stream
|
||||
}))
|
||||
store.dispatch(PeerActions.createPeer({
|
||||
socket, user: { id: 'user3' }, initiator: 'user3', stream
|
||||
}))
|
||||
|
||||
store.dispatch(PeerActions.destroyPeers())
|
||||
|
||||
expect(Peer.instances[0].destroy.mock.calls.length).toEqual(1)
|
||||
expect(Peer.instances[1].destroy.mock.calls.length).toEqual(1)
|
||||
|
||||
const { peers } = store.getState()
|
||||
expect(Object.keys(peers)).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('sendMessage', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
store.dispatch(PeerActions.createPeer({
|
||||
socket, user: { id: 'user2' }, initiator: 'user2', stream
|
||||
}))
|
||||
store.dispatch(PeerActions.createPeer({
|
||||
socket, user: { id: 'user3' }, initiator: 'user3', stream
|
||||
}))
|
||||
})
|
||||
|
||||
it('sends a message to all peers', () => {
|
||||
store.dispatch(PeerActions.sendMessage({ payload: 'test', type: 'text' }))
|
||||
const { peers } = store.getState()
|
||||
expect(peers['user2'].send.mock.calls)
|
||||
.toEqual([[ '{"payload":"test","type":"text"}' ]])
|
||||
expect(peers['user3'].send.mock.calls)
|
||||
.toEqual([[ '{"payload":"test","type":"text"}' ]])
|
||||
})
|
||||
|
||||
})
|
||||
})
|
||||
177
src/client/actions/PeerActions.test.ts
Normal file
177
src/client/actions/PeerActions.test.ts
Normal file
@ -0,0 +1,177 @@
|
||||
jest.mock('../window.js')
|
||||
jest.mock('simple-peer')
|
||||
|
||||
import * as PeerActions from './PeerActions.js'
|
||||
import Peer from 'simple-peer'
|
||||
import { EventEmitter } from 'events'
|
||||
import { createStore, Store, GetState } from '../store.js'
|
||||
import { play } from '../window.js'
|
||||
import { Dispatch } from 'redux'
|
||||
|
||||
describe('PeerActions', () => {
|
||||
function createSocket () {
|
||||
const socket = new EventEmitter() as unknown as SocketIOClient.Socket
|
||||
socket.id = 'user1'
|
||||
return socket
|
||||
}
|
||||
|
||||
let socket: SocketIOClient.Socket
|
||||
let stream: MediaStream
|
||||
let user: { id: string }
|
||||
let store: Store
|
||||
let instances: Peer.Instance[]
|
||||
let dispatch: Dispatch
|
||||
let getState: GetState
|
||||
let PeerMock: jest.Mock<Peer.Instance>
|
||||
|
||||
beforeEach(() => {
|
||||
store = createStore()
|
||||
dispatch = store.dispatch
|
||||
getState = store.getState
|
||||
|
||||
user = { id: 'user2' }
|
||||
socket = createSocket()
|
||||
instances = (Peer as any).instances = [];
|
||||
(Peer as unknown as jest.Mock).mockClear();
|
||||
(play as jest.Mock).mockClear()
|
||||
stream = { stream: true } as unknown as MediaStream
|
||||
PeerMock = Peer as unknown as jest.Mock<Peer.Instance>
|
||||
})
|
||||
|
||||
describe('create', () => {
|
||||
it('creates a new peer', () => {
|
||||
PeerActions.createPeer({ socket, user, initiator: 'user2', stream })(
|
||||
dispatch, getState)
|
||||
|
||||
expect(instances.length).toBe(1)
|
||||
expect(PeerMock.mock.calls.length).toBe(1)
|
||||
expect(PeerMock.mock.calls[0][0].initiator).toBe(false)
|
||||
expect(PeerMock.mock.calls[0][0].stream).toBe(stream)
|
||||
})
|
||||
|
||||
it('sets initiator correctly', () => {
|
||||
PeerActions
|
||||
.createPeer({
|
||||
socket, user, initiator: 'user1', stream,
|
||||
})(dispatch, getState)
|
||||
|
||||
expect(instances.length).toBe(1)
|
||||
expect(PeerMock.mock.calls.length).toBe(1)
|
||||
expect(PeerMock.mock.calls[0][0].initiator).toBe(true)
|
||||
expect(PeerMock.mock.calls[0][0].stream).toBe(stream)
|
||||
})
|
||||
|
||||
it('destroys old peer before creating new one', () => {
|
||||
PeerActions.createPeer({ socket, user, initiator: 'user2', stream })(
|
||||
dispatch, getState)
|
||||
PeerActions.createPeer({ socket, user, initiator: 'user2', stream })(
|
||||
dispatch, getState)
|
||||
|
||||
expect(instances.length).toBe(2)
|
||||
expect(PeerMock.mock.calls.length).toBe(2)
|
||||
expect((instances[0].destroy as jest.Mock).mock.calls.length).toBe(1)
|
||||
expect((instances[1].destroy as jest.Mock).mock.calls.length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('events', () => {
|
||||
let peer: Peer.Instance
|
||||
|
||||
beforeEach(() => {
|
||||
PeerActions.createPeer({ socket, user, initiator: 'user1', stream })(
|
||||
dispatch, getState)
|
||||
peer = instances[0]
|
||||
})
|
||||
|
||||
describe('connect', () => {
|
||||
beforeEach(() => peer.emit('connect'))
|
||||
|
||||
it('dispatches "play" action', () => {
|
||||
expect((play as jest.Mock).mock.calls.length).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('data', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
(window as any).TextDecoder = class TextDecoder {
|
||||
constructor (readonly encoding: string) {
|
||||
}
|
||||
decode (object: any) {
|
||||
return object.toString(this.encoding)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it('decodes a message', () => {
|
||||
const payload = 'test'
|
||||
const object = JSON.stringify({ payload })
|
||||
peer.emit('data', Buffer.from(object, 'utf-8'))
|
||||
const { messages } = store.getState()
|
||||
expect(messages[messages.length - 1]).toEqual({
|
||||
userId: 'user2',
|
||||
timestamp: jasmine.any(String),
|
||||
image: null,
|
||||
message: 'test',
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('get', () => {
|
||||
it('returns undefined when not found', () => {
|
||||
const { peers } = store.getState()
|
||||
expect(peers[user.id]).not.toBeDefined()
|
||||
})
|
||||
|
||||
it('returns Peer instance when found', () => {
|
||||
PeerActions.createPeer({ socket, user, initiator: 'user2', stream })(
|
||||
dispatch, getState)
|
||||
|
||||
const { peers } = store.getState()
|
||||
expect(peers[user.id]).toBe(instances[0])
|
||||
})
|
||||
})
|
||||
|
||||
describe('destroyPeers', () => {
|
||||
it('destroys all peers and removes them', () => {
|
||||
PeerActions.createPeer({
|
||||
socket, user: { id: 'user2' }, initiator: 'user2', stream,
|
||||
})(dispatch, getState)
|
||||
PeerActions.createPeer({
|
||||
socket, user: { id: 'user3' }, initiator: 'user3', stream,
|
||||
})(dispatch, getState)
|
||||
|
||||
store.dispatch(PeerActions.destroyPeers())
|
||||
|
||||
expect((instances[0].destroy as jest.Mock).mock.calls.length).toEqual(1)
|
||||
expect((instances[1].destroy as jest.Mock).mock.calls.length).toEqual(1)
|
||||
|
||||
const { peers } = store.getState()
|
||||
expect(Object.keys(peers)).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('sendMessage', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
PeerActions.createPeer({
|
||||
socket, user: { id: 'user2' }, initiator: 'user2', stream,
|
||||
})(dispatch, getState)
|
||||
PeerActions.createPeer({
|
||||
socket, user: { id: 'user3' }, initiator: 'user3', stream,
|
||||
})(dispatch, getState)
|
||||
})
|
||||
|
||||
it('sends a message to all peers', () => {
|
||||
PeerActions.sendMessage({ payload: 'test', type: 'text' })(
|
||||
dispatch, getState)
|
||||
const { peers } = store.getState()
|
||||
expect(peers['user2'].send.mock.calls)
|
||||
.toEqual([[ '{"payload":"test","type":"text"}' ]])
|
||||
expect(peers['user3'].send.mock.calls)
|
||||
.toEqual([[ '{"payload":"test","type":"text"}' ]])
|
||||
})
|
||||
|
||||
})
|
||||
})
|
||||
@ -6,25 +6,44 @@ import Peer from 'simple-peer'
|
||||
import _ from 'underscore'
|
||||
import _debug from 'debug'
|
||||
import { play, iceServers } from '../window.js'
|
||||
import { Dispatch } from 'redux'
|
||||
|
||||
const debug = _debug('peercalls')
|
||||
|
||||
export interface Peers {
|
||||
[id: string]: Peer.Instance
|
||||
}
|
||||
|
||||
export type GetState = () => { peers: Peers }
|
||||
|
||||
export interface PeerHandlerOptions {
|
||||
socket: SocketIOClient.Socket
|
||||
user: { id: string }
|
||||
dispatch: Dispatch
|
||||
getState: GetState
|
||||
}
|
||||
|
||||
class PeerHandler {
|
||||
constructor ({ socket, user, dispatch, getState }) {
|
||||
this.socket = socket
|
||||
this.user = user
|
||||
this.dispatch = dispatch
|
||||
this.getState = getState
|
||||
socket: SocketIOClient.Socket
|
||||
user: { id: string }
|
||||
dispatch: Dispatch
|
||||
getState: GetState
|
||||
|
||||
constructor (readonly options: PeerHandlerOptions) {
|
||||
this.socket = options.socket
|
||||
this.user = options.user
|
||||
this.dispatch = options.dispatch
|
||||
this.getState = options.getState
|
||||
}
|
||||
handleError = err => {
|
||||
handleError = (err: Error) => {
|
||||
const { dispatch, getState, user } = this
|
||||
debug('peer: %s, error %s', user.id, err.stack)
|
||||
dispatch(NotifyActions.error('A peer connection error occurred'))
|
||||
NotifyActions.error('A peer connection error occurred')(dispatch)
|
||||
const peer = getState().peers[user.id]
|
||||
peer && peer.destroy()
|
||||
dispatch(removePeer(user.id))
|
||||
}
|
||||
handleSignal = signal => {
|
||||
handleSignal = (signal: unknown) => {
|
||||
const { socket, user } = this
|
||||
debug('peer: %s, signal: %o', user.id, signal)
|
||||
|
||||
@ -34,18 +53,18 @@ class PeerHandler {
|
||||
handleConnect = () => {
|
||||
const { dispatch, user } = this
|
||||
debug('peer: %s, connect', user.id)
|
||||
dispatch(NotifyActions.warning('Peer connection established'))
|
||||
NotifyActions.warning('Peer connection established')(dispatch)
|
||||
play()
|
||||
}
|
||||
handleStream = stream => {
|
||||
handleStream = (stream: MediaStream) => {
|
||||
const { user, dispatch } = this
|
||||
debug('peer: %s, stream', user.id)
|
||||
dispatch(StreamActions.addStream({
|
||||
userId: user.id,
|
||||
stream
|
||||
stream,
|
||||
}))
|
||||
}
|
||||
handleData = object => {
|
||||
handleData = (object: any) => {
|
||||
const { dispatch, user } = this
|
||||
const message = JSON.parse(new window.TextDecoder('utf-8').decode(object))
|
||||
debug('peer: %s, message: %o', user.id, object)
|
||||
@ -55,7 +74,7 @@ class PeerHandler {
|
||||
userId: user.id,
|
||||
message: message.payload.name,
|
||||
timestamp: new Date().toLocaleString(),
|
||||
image: message.payload.data
|
||||
image: message.payload.data,
|
||||
}))
|
||||
break
|
||||
default:
|
||||
@ -63,19 +82,26 @@ class PeerHandler {
|
||||
userId: user.id,
|
||||
message: message.payload,
|
||||
timestamp: new Date().toLocaleString(),
|
||||
image: null
|
||||
image: undefined,
|
||||
}))
|
||||
}
|
||||
}
|
||||
handleClose = () => {
|
||||
const { dispatch, user } = this
|
||||
debug('peer: %s, close', user.id)
|
||||
dispatch(NotifyActions.error('Peer connection closed'))
|
||||
NotifyActions.error('Peer connection closed')(dispatch)
|
||||
dispatch(StreamActions.removeStream(user.id))
|
||||
dispatch(removePeer(user.id))
|
||||
}
|
||||
}
|
||||
|
||||
export interface CreatePeerOptions {
|
||||
socket: SocketIOClient.Socket
|
||||
user: { id: string }
|
||||
initiator: string
|
||||
stream?: MediaStream
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} options
|
||||
* @param {Socket} options.socket
|
||||
@ -84,15 +110,17 @@ class PeerHandler {
|
||||
* @param {Boolean} [options.initiator=false]
|
||||
* @param {MediaStream} [options.stream]
|
||||
*/
|
||||
export function createPeer ({ socket, user, initiator, stream }) {
|
||||
return (dispatch, getState) => {
|
||||
export function createPeer (options: CreatePeerOptions) {
|
||||
const { socket, user, initiator, stream } = options
|
||||
|
||||
return (dispatch: Dispatch, getState: GetState) => {
|
||||
const userId = user.id
|
||||
debug('create peer: %s, stream:', userId, stream)
|
||||
dispatch(NotifyActions.warning('Connecting to peer...'))
|
||||
NotifyActions.warning('Connecting to peer...')(dispatch)
|
||||
|
||||
const oldPeer = getState().peers[userId]
|
||||
if (oldPeer) {
|
||||
dispatch(NotifyActions.info('Cleaning up old connection...'))
|
||||
NotifyActions.info('Cleaning up old connection...')(dispatch)
|
||||
oldPeer.destroy()
|
||||
dispatch(removePeer(userId))
|
||||
}
|
||||
@ -104,16 +132,16 @@ export function createPeer ({ socket, user, initiator, stream }) {
|
||||
// https://github.com/feross/simple-peer/issues/95
|
||||
offerConstraints: {
|
||||
offerToReceiveAudio: true,
|
||||
offerToReceiveVideo: true
|
||||
offerToReceiveVideo: true,
|
||||
},
|
||||
stream
|
||||
stream,
|
||||
})
|
||||
|
||||
const handler = new PeerHandler({
|
||||
socket,
|
||||
user,
|
||||
dispatch,
|
||||
getState
|
||||
getState,
|
||||
})
|
||||
|
||||
peer.once(constants.PEER_EVENT_ERROR, handler.handleError)
|
||||
@ -127,21 +155,65 @@ export function createPeer ({ socket, user, initiator, stream }) {
|
||||
}
|
||||
}
|
||||
|
||||
export const addPeer = ({ peer, userId }) => ({
|
||||
export interface AddPeerParams {
|
||||
peer: Peer.Instance
|
||||
userId: string
|
||||
}
|
||||
|
||||
export interface AddPeerAction {
|
||||
type: 'PEER_ADD'
|
||||
payload: AddPeerParams
|
||||
}
|
||||
|
||||
export const addPeer = (payload: AddPeerParams): AddPeerAction => ({
|
||||
type: constants.PEER_ADD,
|
||||
payload: { peer, userId }
|
||||
payload,
|
||||
})
|
||||
|
||||
export const removePeer = userId => ({
|
||||
export interface RemovePeerAction {
|
||||
type: 'PEER_REMOVE'
|
||||
payload: { userId: string }
|
||||
}
|
||||
|
||||
export const removePeer = (userId: string): RemovePeerAction => ({
|
||||
type: constants.PEER_REMOVE,
|
||||
payload: { userId }
|
||||
payload: { userId },
|
||||
})
|
||||
|
||||
export const destroyPeers = () => ({
|
||||
type: constants.PEERS_DESTROY
|
||||
export interface DestroyPeersAction {
|
||||
type: 'PEERS_DESTROY'
|
||||
}
|
||||
|
||||
export const destroyPeers = (): DestroyPeersAction => ({
|
||||
type: constants.PEERS_DESTROY,
|
||||
})
|
||||
|
||||
export const sendMessage = message => (dispatch, getState) => {
|
||||
export type PeerAction =
|
||||
AddPeerAction |
|
||||
RemovePeerAction |
|
||||
DestroyPeersAction
|
||||
|
||||
export interface TextMessage {
|
||||
type: 'text'
|
||||
payload: string
|
||||
}
|
||||
|
||||
export interface Base64File {
|
||||
name: string
|
||||
size: number
|
||||
type: string
|
||||
data: string
|
||||
}
|
||||
|
||||
export interface FileMessage {
|
||||
type: 'file'
|
||||
payload: Base64File
|
||||
}
|
||||
|
||||
export type Message = TextMessage | FileMessage
|
||||
|
||||
export const sendMessage = (message: Message) =>
|
||||
(dispatch: Dispatch, getState: GetState) => {
|
||||
const { peers } = getState()
|
||||
debug('Sending message type: %s to %s peers.',
|
||||
message.type, Object.keys(peers).length)
|
||||
@ -153,7 +225,7 @@ export const sendMessage = message => (dispatch, getState) => {
|
||||
message: 'Send file: "' +
|
||||
message.payload.name + '" to peer: ' + userId,
|
||||
timestamp: new Date().toLocaleString(),
|
||||
image: message.payload.data
|
||||
image: message.payload.data,
|
||||
}))
|
||||
break
|
||||
default:
|
||||
@ -161,27 +233,28 @@ export const sendMessage = message => (dispatch, getState) => {
|
||||
userId: 'You',
|
||||
message: message.payload,
|
||||
timestamp: new Date().toLocaleString(),
|
||||
image: null
|
||||
image: undefined,
|
||||
}))
|
||||
}
|
||||
peer.send(JSON.stringify(message))
|
||||
})
|
||||
}
|
||||
|
||||
export const sendFile = file => async (dispatch, getState) => {
|
||||
export const sendFile = (file: File) =>
|
||||
async (dispatch: Dispatch, getState: GetState) => {
|
||||
const { name, size, type } = file
|
||||
if (!window.FileReader) {
|
||||
dispatch(NotifyActions.error('File API is not supported by your browser'))
|
||||
NotifyActions.error('File API is not supported by your browser')(dispatch)
|
||||
return
|
||||
}
|
||||
const reader = new window.FileReader()
|
||||
const base64File = await new Promise(resolve => {
|
||||
const base64File = await new Promise<Base64File>(resolve => {
|
||||
reader.addEventListener('load', () => {
|
||||
resolve({
|
||||
name,
|
||||
size,
|
||||
type,
|
||||
data: reader.result
|
||||
data: reader.result as string,
|
||||
})
|
||||
})
|
||||
reader.readAsDataURL(file)
|
||||
@ -1,64 +0,0 @@
|
||||
import * as NotifyActions from '../actions/NotifyActions.js'
|
||||
import * as PeerActions from '../actions/PeerActions.js'
|
||||
import * as constants from '../constants.js'
|
||||
import _ from 'underscore'
|
||||
import _debug from 'debug'
|
||||
|
||||
const debug = _debug('peercalls')
|
||||
|
||||
class SocketHandler {
|
||||
constructor ({ socket, roomName, stream, dispatch, getState }) {
|
||||
this.socket = socket
|
||||
this.roomName = roomName
|
||||
this.stream = stream
|
||||
this.dispatch = dispatch
|
||||
this.getState = getState
|
||||
}
|
||||
handleSignal = ({ userId, signal }) => {
|
||||
const { getState } = this
|
||||
const peer = getState().peers[userId]
|
||||
// debug('socket signal, userId: %s, signal: %o', userId, signal);
|
||||
if (!peer) return debug('user: %s, no peer found', userId)
|
||||
peer.signal(signal)
|
||||
}
|
||||
handleUsers = ({ initiator, users }) => {
|
||||
const { socket, stream, dispatch, getState } = this
|
||||
debug('socket users: %o', users)
|
||||
dispatch(NotifyActions.info('Connected users: {0}', users.length))
|
||||
const { peers } = getState()
|
||||
|
||||
users
|
||||
.filter(user => !peers[user.id] && user.id !== socket.id)
|
||||
.forEach(user => dispatch(PeerActions.createPeer({
|
||||
socket,
|
||||
user,
|
||||
initiator,
|
||||
stream
|
||||
})))
|
||||
|
||||
let newUsersMap = _.indexBy(users, 'id')
|
||||
_.keys(peers)
|
||||
.filter(id => !newUsersMap[id])
|
||||
.forEach(id => peers[id].destroy())
|
||||
}
|
||||
}
|
||||
|
||||
export function handshake ({ socket, roomName, stream }) {
|
||||
return (dispatch, getState) => {
|
||||
const handler = new SocketHandler({
|
||||
socket,
|
||||
roomName,
|
||||
stream,
|
||||
dispatch,
|
||||
getState
|
||||
})
|
||||
|
||||
socket.on(constants.SOCKET_EVENT_SIGNAL, handler.handleSignal)
|
||||
socket.on(constants.SOCKET_EVENT_USERS, handler.handleUsers)
|
||||
|
||||
debug('socket.id: %s', socket.id)
|
||||
debug('emit ready for room: %s', roomName)
|
||||
dispatch(NotifyActions.info('Ready for connections'))
|
||||
socket.emit('ready', roomName)
|
||||
}
|
||||
}
|
||||
@ -5,94 +5,101 @@ import * as SocketActions from './SocketActions.js'
|
||||
import * as constants from '../constants.js'
|
||||
import Peer from 'simple-peer'
|
||||
import { EventEmitter } from 'events'
|
||||
import { createStore } from '../store.js'
|
||||
import { createStore, Store, GetState } from '../store.js'
|
||||
import { Dispatch } from 'redux'
|
||||
|
||||
describe('SocketActions', () => {
|
||||
const roomName = 'bla'
|
||||
|
||||
let socket, store
|
||||
let socket: SocketIOClient.Socket
|
||||
let store: Store
|
||||
let dispatch: Dispatch
|
||||
let getState: GetState
|
||||
let instances: Peer.Instance[]
|
||||
beforeEach(() => {
|
||||
socket = new EventEmitter()
|
||||
socket.id = 'a'
|
||||
socket = new EventEmitter() as any;
|
||||
(socket as any).id = 'a'
|
||||
|
||||
store = createStore()
|
||||
getState = store.getState
|
||||
dispatch = store.dispatch
|
||||
|
||||
Peer.instances = []
|
||||
instances = (Peer as any).instances = []
|
||||
})
|
||||
|
||||
describe('handshake', () => {
|
||||
describe('users', () => {
|
||||
beforeEach(() => {
|
||||
store.dispatch(SocketActions.handshake({ socket, roomName }))
|
||||
SocketActions.handshake({ socket, roomName })(dispatch, getState)
|
||||
const payload = {
|
||||
users: [{ id: 'a' }, { id: 'b' }],
|
||||
initiator: 'a'
|
||||
initiator: 'a',
|
||||
}
|
||||
socket.emit('users', payload)
|
||||
expect(Peer.instances.length).toBe(1)
|
||||
expect(instances.length).toBe(1)
|
||||
})
|
||||
|
||||
it('adds a peer for each new user and destroys peers for missing', () => {
|
||||
const payload = {
|
||||
users: [{ id: 'a' }, { id: 'c' }],
|
||||
initiator: 'c'
|
||||
initiator: 'c',
|
||||
}
|
||||
socket.emit(constants.SOCKET_EVENT_USERS, payload)
|
||||
|
||||
// then
|
||||
expect(Peer.instances.length).toBe(2)
|
||||
expect(Peer.instances[0].destroy.mock.calls.length).toBe(1)
|
||||
expect(Peer.instances[1].destroy.mock.calls.length).toBe(0)
|
||||
expect(instances.length).toBe(2)
|
||||
expect((instances[0].destroy as jest.Mock).mock.calls.length).toBe(1)
|
||||
expect((instances[1].destroy as jest.Mock).mock.calls.length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('signal', () => {
|
||||
let data
|
||||
let data: Peer.SignalData
|
||||
beforeEach(() => {
|
||||
data = {}
|
||||
store.dispatch(SocketActions.handshake({ socket, roomName }))
|
||||
data = {} as any
|
||||
SocketActions.handshake({ socket, roomName })(dispatch, getState)
|
||||
socket.emit('users', {
|
||||
initiator: 'a',
|
||||
users: [{ id: 'a' }, { id: 'b' }]
|
||||
users: [{ id: 'a' }, { id: 'b' }],
|
||||
})
|
||||
})
|
||||
|
||||
it('should forward signal to peer', () => {
|
||||
socket.emit('signal', {
|
||||
userId: 'b',
|
||||
data
|
||||
data,
|
||||
})
|
||||
|
||||
expect(Peer.instances.length).toBe(1)
|
||||
expect(Peer.instances[0].signal.mock.calls.length).toBe(1)
|
||||
expect(instances.length).toBe(1)
|
||||
expect((instances[0].signal as jest.Mock).mock.calls.length).toBe(1)
|
||||
})
|
||||
|
||||
it('does nothing if no peer', () => {
|
||||
socket.emit('signal', {
|
||||
userId: 'a',
|
||||
data
|
||||
data,
|
||||
})
|
||||
|
||||
expect(Peer.instances.length).toBe(1)
|
||||
expect(Peer.instances[0].signal.mock.calls.length).toBe(0)
|
||||
expect(instances.length).toBe(1)
|
||||
expect((instances[0].signal as jest.Mock).mock.calls.length).toBe(0)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('peer events', () => {
|
||||
let peer
|
||||
let peer: Peer.Instance
|
||||
beforeEach(() => {
|
||||
let ready = false
|
||||
socket.once('ready', () => { ready = true })
|
||||
|
||||
store.dispatch(SocketActions.handshake({ socket, roomName }))
|
||||
SocketActions.handshake({ socket, roomName })(dispatch, getState)
|
||||
|
||||
socket.emit('users', {
|
||||
initiator: 'a',
|
||||
users: [{ id: 'a' }, { id: 'b' }]
|
||||
users: [{ id: 'a' }, { id: 'b' }],
|
||||
})
|
||||
expect(Peer.instances.length).toBe(1)
|
||||
peer = Peer.instances[0]
|
||||
expect(instances.length).toBe(1)
|
||||
peer = instances[0]
|
||||
|
||||
expect(ready).toBeDefined()
|
||||
})
|
||||
@ -100,15 +107,15 @@ describe('SocketActions', () => {
|
||||
describe('error', () => {
|
||||
it('destroys peer', () => {
|
||||
peer.emit(constants.PEER_EVENT_ERROR, new Error('bla'))
|
||||
expect(peer.destroy.mock.calls.length).toBe(1)
|
||||
expect((peer.destroy as jest.Mock).mock.calls.length).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('signal', () => {
|
||||
it('emits socket signal with user id', done => {
|
||||
let signal = { bla: 'bla' }
|
||||
const signal = { bla: 'bla' }
|
||||
|
||||
socket.once('signal', payload => {
|
||||
socket.once('signal', (payload: SocketActions.SignalOptions) => {
|
||||
expect(payload.userId).toEqual('b')
|
||||
expect(payload.signal).toBe(signal)
|
||||
done()
|
||||
@ -126,8 +133,8 @@ describe('SocketActions', () => {
|
||||
expect(store.getState().streams).toEqual({
|
||||
b: {
|
||||
mediaStream: stream,
|
||||
url: jasmine.any(String)
|
||||
}
|
||||
url: jasmine.any(String),
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -139,8 +146,8 @@ describe('SocketActions', () => {
|
||||
expect(store.getState().streams).toEqual({
|
||||
b: {
|
||||
mediaStream: stream,
|
||||
url: jasmine.any(String)
|
||||
}
|
||||
url: jasmine.any(String),
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
98
src/client/actions/SocketActions.ts
Normal file
98
src/client/actions/SocketActions.ts
Normal file
@ -0,0 +1,98 @@
|
||||
import * as NotifyActions from '../actions/NotifyActions.js'
|
||||
import * as PeerActions from '../actions/PeerActions.js'
|
||||
import * as constants from '../constants.js'
|
||||
import _ from 'underscore'
|
||||
import _debug from 'debug'
|
||||
import { Dispatch } from 'redux'
|
||||
import { SignalData } from 'simple-peer'
|
||||
|
||||
const debug = _debug('peercalls')
|
||||
|
||||
export interface SocketHandlerOptions {
|
||||
socket: SocketIOClient.Socket
|
||||
roomName: string
|
||||
stream?: MediaStream
|
||||
dispatch: Dispatch
|
||||
getState: PeerActions.GetState
|
||||
}
|
||||
|
||||
export interface SignalOptions {
|
||||
signal: SignalData
|
||||
userId: string
|
||||
}
|
||||
|
||||
export interface UsersOptions {
|
||||
initiator: string
|
||||
users: Array<{ id: string }>
|
||||
}
|
||||
|
||||
class SocketHandler {
|
||||
socket: SocketIOClient.Socket
|
||||
roomName: string
|
||||
stream?: MediaStream
|
||||
dispatch: Dispatch
|
||||
getState: PeerActions.GetState
|
||||
|
||||
constructor (options: SocketHandlerOptions) {
|
||||
this.socket = options.socket
|
||||
this.roomName = options.roomName
|
||||
this.stream = options.stream
|
||||
this.dispatch = options.dispatch
|
||||
this.getState = options.getState
|
||||
}
|
||||
handleSignal = ({ userId, signal }: SignalOptions) => {
|
||||
const { getState } = this
|
||||
const peer = getState().peers[userId]
|
||||
// debug('socket signal, userId: %s, signal: %o', userId, signal);
|
||||
if (!peer) return debug('user: %s, no peer found', userId)
|
||||
peer.signal(signal)
|
||||
}
|
||||
handleUsers = ({ initiator, users }: UsersOptions) => {
|
||||
const { socket, stream, dispatch, getState } = this
|
||||
debug('socket users: %o', users)
|
||||
NotifyActions.info('Connected users: {0}', users.length)(dispatch)
|
||||
const { peers } = getState()
|
||||
|
||||
users
|
||||
.filter(user => !peers[user.id] && user.id !== socket.id)
|
||||
.forEach(user => PeerActions.createPeer({
|
||||
socket,
|
||||
user,
|
||||
initiator,
|
||||
stream,
|
||||
})(dispatch, getState))
|
||||
|
||||
const newUsersMap = _.indexBy(users, 'id')
|
||||
_.keys(peers)
|
||||
.filter(id => !newUsersMap[id])
|
||||
.forEach(id => peers[id].destroy())
|
||||
}
|
||||
}
|
||||
|
||||
export interface HandshakeOptions {
|
||||
socket: SocketIOClient.Socket
|
||||
roomName: string
|
||||
stream?: MediaStream
|
||||
}
|
||||
|
||||
export function handshake (options: HandshakeOptions) {
|
||||
const { socket, roomName, stream } = options
|
||||
|
||||
return (dispatch: Dispatch, getState: PeerActions.GetState) => {
|
||||
const handler = new SocketHandler({
|
||||
socket,
|
||||
roomName,
|
||||
stream,
|
||||
dispatch,
|
||||
getState,
|
||||
})
|
||||
|
||||
socket.on(constants.SOCKET_EVENT_SIGNAL, handler.handleSignal)
|
||||
socket.on(constants.SOCKET_EVENT_USERS, handler.handleUsers)
|
||||
|
||||
debug('socket.id: %s', socket.id)
|
||||
debug('emit ready for room: %s', roomName)
|
||||
NotifyActions.info('Ready for connections')(dispatch)
|
||||
socket.emit('ready', roomName)
|
||||
}
|
||||
}
|
||||
@ -1,24 +0,0 @@
|
||||
import * as constants from '../constants.js'
|
||||
|
||||
export const addStream = ({ stream, userId }) => ({
|
||||
type: constants.STREAM_ADD,
|
||||
payload: {
|
||||
userId,
|
||||
stream
|
||||
}
|
||||
})
|
||||
|
||||
export const removeStream = userId => ({
|
||||
type: constants.STREAM_REMOVE,
|
||||
payload: { userId }
|
||||
})
|
||||
|
||||
export const setActive = userId => ({
|
||||
type: constants.ACTIVE_SET,
|
||||
payload: { userId }
|
||||
})
|
||||
|
||||
export const toggleActive = userId => ({
|
||||
type: constants.ACTIVE_TOGGLE,
|
||||
payload: { userId }
|
||||
})
|
||||
61
src/client/actions/StreamActions.ts
Normal file
61
src/client/actions/StreamActions.ts
Normal file
@ -0,0 +1,61 @@
|
||||
import * as constants from '../constants.js'
|
||||
|
||||
export interface AddStreamPayload {
|
||||
userId: string
|
||||
stream: MediaStream
|
||||
url?: string
|
||||
}
|
||||
|
||||
export interface AddStreamAction {
|
||||
type: 'PEER_STREAM_ADD'
|
||||
payload: AddStreamPayload
|
||||
}
|
||||
|
||||
export interface RemoveStreamAction {
|
||||
type: 'PEER_STREAM_REMOVE'
|
||||
payload: RemoveStreamPayload
|
||||
}
|
||||
|
||||
export interface RemoveStreamPayload {
|
||||
userId: string
|
||||
}
|
||||
|
||||
export interface SetActiveStreamAction {
|
||||
type: 'ACTIVE_SET'
|
||||
payload: RemoveStreamPayload
|
||||
}
|
||||
|
||||
export interface ToggleActiveStreamAction {
|
||||
type: 'ACTIVE_TOGGLE'
|
||||
payload: UserIdPayload
|
||||
}
|
||||
|
||||
export interface UserIdPayload {
|
||||
userId: string
|
||||
}
|
||||
|
||||
export const addStream = (payload: AddStreamPayload): AddStreamAction => ({
|
||||
type: constants.STREAM_ADD,
|
||||
payload,
|
||||
})
|
||||
|
||||
export const removeStream = (userId: string): RemoveStreamAction => ({
|
||||
type: constants.STREAM_REMOVE,
|
||||
payload: { userId },
|
||||
})
|
||||
|
||||
export const setActive = (userId: string): SetActiveStreamAction => ({
|
||||
type: constants.ACTIVE_SET,
|
||||
payload: { userId },
|
||||
})
|
||||
|
||||
export const toggleActive = (userId: string): ToggleActiveStreamAction => ({
|
||||
type: constants.ACTIVE_TOGGLE,
|
||||
payload: { userId },
|
||||
})
|
||||
|
||||
export type StreamAction =
|
||||
AddStreamAction |
|
||||
RemoveStreamAction
|
||||
// SetActiveStreamAction |
|
||||
// ToggleActiveStreamAction
|
||||
@ -1,4 +1,3 @@
|
||||
|
||||
export const ACTIVE_SET = 'ACTIVE_SET'
|
||||
export const ACTIVE_TOGGLE = 'ACTIVE_TOGGLE'
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
'use strict'
|
||||
import '@babel/polyfill'
|
||||
import App from './containers/App.js'
|
||||
import App from './containers/App'
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import store from './store.js'
|
||||
@ -3,7 +3,7 @@ import promiseMiddleware from 'redux-promise-middleware'
|
||||
import thunk from 'redux-thunk'
|
||||
|
||||
export const middlewares = [thunk, promiseMiddleware()]
|
||||
export const create = log => {
|
||||
export const create = (log = false) => {
|
||||
const m = middlewares.slice()
|
||||
log && m.push(logger)
|
||||
return m
|
||||
@ -1,6 +1,6 @@
|
||||
import * as constants from '../constants.js'
|
||||
|
||||
export default function active (state = null, action) {
|
||||
export default function active (state = null, action: Action) {
|
||||
switch (action && action.type) {
|
||||
case constants.ACTIVE_SET:
|
||||
case constants.STREAM_ADD:
|
||||
@ -1,15 +0,0 @@
|
||||
import * as constants from '../constants.js'
|
||||
import Immutable from 'seamless-immutable'
|
||||
|
||||
const defaultState = Immutable([])
|
||||
|
||||
export default function messages (state = defaultState, action) {
|
||||
switch (action && action.type) {
|
||||
case constants.MESSAGE_ADD:
|
||||
const messages = state.asMutable()
|
||||
messages.push(action.payload)
|
||||
return Immutable(messages)
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
||||
17
src/client/reducers/messages.ts
Normal file
17
src/client/reducers/messages.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import * as constants from '../constants.js'
|
||||
import { Message, MessageAddAction } from '../actions/ChatActions.js'
|
||||
|
||||
export type MessagesState = Message[]
|
||||
|
||||
const defaultState: MessagesState = []
|
||||
|
||||
export default function messages (
|
||||
state = defaultState, action: MessageAddAction,
|
||||
) {
|
||||
switch (action && action.type) {
|
||||
case constants.MESSAGE_ADD:
|
||||
return [...state, action.payload]
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
||||
@ -1,14 +1,20 @@
|
||||
import * as constants from '../constants.js'
|
||||
import _ from 'underscore'
|
||||
import Peer from 'simple-peer'
|
||||
import { PeerAction } from '../actions/PeerActions'
|
||||
|
||||
const defaultState = {}
|
||||
export interface PeersState {
|
||||
[userId: string]: Peer.Instance
|
||||
}
|
||||
|
||||
export default function peers (state = defaultState, action) {
|
||||
switch (action && action.type) {
|
||||
const defaultState: PeersState = {}
|
||||
|
||||
export default function peers (state = defaultState, action: PeerAction) {
|
||||
switch (action.type) {
|
||||
case constants.PEER_ADD:
|
||||
return {
|
||||
...state,
|
||||
[action.payload.userId]: action.payload.peer
|
||||
[action.payload.userId]: action.payload.peer,
|
||||
}
|
||||
case constants.PEER_REMOVE:
|
||||
return _.omit(state, [action.payload.userId])
|
||||
@ -3,23 +3,23 @@ jest.mock('../window.js')
|
||||
import * as StreamActions from '../actions/StreamActions.js'
|
||||
import reducers from './index.js'
|
||||
import { createObjectURL, MediaStream } from '../window.js'
|
||||
import { applyMiddleware, createStore } from 'redux'
|
||||
import { applyMiddleware, createStore, Store } from 'redux'
|
||||
import { create } from '../middlewares.js'
|
||||
|
||||
describe('reducers/alerts', () => {
|
||||
|
||||
let store, stream, userId
|
||||
let store: Store, stream: MediaStream, userId: string
|
||||
beforeEach(() => {
|
||||
store = createStore(
|
||||
reducers,
|
||||
applyMiddleware.apply(null, create())
|
||||
applyMiddleware(...create()),
|
||||
)
|
||||
userId = 'test id'
|
||||
stream = new MediaStream()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
createObjectURL
|
||||
(createObjectURL as jest.Mock)
|
||||
.mockImplementation(object => 'blob://' + String(object))
|
||||
})
|
||||
|
||||
@ -35,19 +35,19 @@ describe('reducers/alerts', () => {
|
||||
expect(store.getState().streams).toEqual({
|
||||
[userId]: {
|
||||
mediaStream: stream,
|
||||
url: jasmine.any(String)
|
||||
}
|
||||
url: jasmine.any(String),
|
||||
},
|
||||
})
|
||||
})
|
||||
it('does not fail when createObjectURL fails', () => {
|
||||
createObjectURL
|
||||
(createObjectURL as jest.Mock)
|
||||
.mockImplementation(() => { throw new Error('test') })
|
||||
store.dispatch(StreamActions.addStream({ userId, stream }))
|
||||
expect(store.getState().streams).toEqual({
|
||||
[userId]: {
|
||||
mediaStream: stream,
|
||||
url: null
|
||||
}
|
||||
url: null,
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -1,12 +1,13 @@
|
||||
import * as constants from '../constants.js'
|
||||
import _ from 'underscore'
|
||||
import { createObjectURL, revokeObjectURL } from '../window.js'
|
||||
import _debug from 'debug'
|
||||
import { AddStreamPayload, AddStreamAction, RemoveStreamAction, StreamAction } from '../actions/StreamActions.js'
|
||||
import { STREAM_ADD, STREAM_REMOVE } from '../constants.js'
|
||||
|
||||
const debug = _debug('peercalls')
|
||||
const defaultState = Object.freeze({})
|
||||
|
||||
function safeCreateObjectURL (stream) {
|
||||
function safeCreateObjectURL (stream: MediaStream) {
|
||||
try {
|
||||
return createObjectURL(stream)
|
||||
} catch (err) {
|
||||
@ -15,18 +16,22 @@ function safeCreateObjectURL (stream) {
|
||||
}
|
||||
}
|
||||
|
||||
function addStream (state, action) {
|
||||
export interface StreamsState {
|
||||
[userId: string]: AddStreamPayload
|
||||
}
|
||||
|
||||
function addStream (state: StreamsState, action: AddStreamAction) {
|
||||
const { userId, stream } = action.payload
|
||||
return Object.freeze({
|
||||
...state,
|
||||
[userId]: Object.freeze({
|
||||
mediaStream: stream,
|
||||
url: safeCreateObjectURL(stream)
|
||||
})
|
||||
url: safeCreateObjectURL(stream),
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
function removeStream (state, action) {
|
||||
function removeStream (state: StreamsState, action: RemoveStreamAction) {
|
||||
const { userId } = action.payload
|
||||
const stream = state[userId]
|
||||
if (stream && stream.url) {
|
||||
@ -35,11 +40,11 @@ function removeStream (state, action) {
|
||||
return Object.freeze(_.omit(state, [userId]))
|
||||
}
|
||||
|
||||
export default function streams (state = defaultState, action) {
|
||||
switch (action && action.type) {
|
||||
case constants.STREAM_ADD:
|
||||
export default function streams (state = defaultState, action: StreamAction) {
|
||||
switch (action.type) {
|
||||
case STREAM_ADD:
|
||||
return addStream(state, action)
|
||||
case constants.STREAM_REMOVE:
|
||||
case STREAM_REMOVE:
|
||||
return removeStream(state, action)
|
||||
default:
|
||||
return state
|
||||
@ -1,3 +1,3 @@
|
||||
import SocketIOClient from 'socket.io-client'
|
||||
import { baseUrl } from './window.js'
|
||||
export default new SocketIOClient('', { path: baseUrl + '/ws' })
|
||||
export default SocketIOClient('', { path: baseUrl + '/ws' })
|
||||
@ -1,13 +0,0 @@
|
||||
import { create } from './middlewares.js'
|
||||
import reducers from './reducers'
|
||||
import { applyMiddleware, createStore as _createStore } from 'redux'
|
||||
export const middlewares = create(
|
||||
window.localStorage && window.localStorage.log
|
||||
)
|
||||
|
||||
export const createStore = () => _createStore(
|
||||
reducers,
|
||||
applyMiddleware.apply(null, middlewares)
|
||||
)
|
||||
|
||||
export default createStore()
|
||||
19
src/client/store.ts
Normal file
19
src/client/store.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { create } from './middlewares.js'
|
||||
import reducers from './reducers'
|
||||
import { applyMiddleware, createStore as _createStore, Store as ReduxStore } from 'redux'
|
||||
export const middlewares = create(
|
||||
window.localStorage && window.localStorage.log,
|
||||
)
|
||||
|
||||
export const createStore = () => _createStore(
|
||||
reducers,
|
||||
applyMiddleware(...middlewares),
|
||||
)
|
||||
|
||||
export default createStore()
|
||||
|
||||
export type Store = ReturnType<typeof createStore>
|
||||
|
||||
type TGetState<T> = T extends ReduxStore<infer State> ? State : never
|
||||
export type State = TGetState<Store>
|
||||
export type GetState = () => State
|
||||
@ -1,12 +1,10 @@
|
||||
import Promise from 'bluebird'
|
||||
|
||||
import {
|
||||
createObjectURL,
|
||||
revokeObjectURL,
|
||||
getUserMedia,
|
||||
navigator,
|
||||
play,
|
||||
valueOf
|
||||
valueOf,
|
||||
} from './window.js'
|
||||
|
||||
describe('window', () => {
|
||||
@ -18,47 +16,50 @@ describe('window', () => {
|
||||
const constraints = { video: true }
|
||||
|
||||
afterEach(() => {
|
||||
delete navigator.mediaDevices
|
||||
delete (navigator as any).mediaDevices
|
||||
delete navigator.getUserMedia
|
||||
delete navigator.webkitGetUserMedia
|
||||
delete (navigator as any).webkitGetUserMedia
|
||||
})
|
||||
|
||||
it('calls navigator.mediaDevices.getUserMedia', () => {
|
||||
const promise = Promise.resolve(stream)
|
||||
navigator.mediaDevices = {
|
||||
getUserMedia: jest.fn().mockReturnValue(promise)
|
||||
const promise = Promise.resolve(stream);
|
||||
(navigator as any).mediaDevices = {
|
||||
getUserMedia: jest.fn().mockReturnValue(promise),
|
||||
}
|
||||
expect(getUserMedia(constraints)).toBe(promise)
|
||||
})
|
||||
|
||||
;['getUserMedia', 'webkitGetUserMedia'].forEach((method) => {
|
||||
it(`it calls navigator.${method} as a fallback`, () => {
|
||||
navigator[method] = jest.fn()
|
||||
(navigator as any)[method] = jest.fn()
|
||||
.mockImplementation(
|
||||
(constraints, onSuccess, onError) => onSuccess(stream)
|
||||
(constraints, onSuccess, onError) => onSuccess(stream),
|
||||
)
|
||||
return getUserMedia(constraints)
|
||||
.then(s => expect(s).toBe(stream))
|
||||
})
|
||||
})
|
||||
|
||||
it('throws error when no supported method', done => {
|
||||
getUserMedia(constraints)
|
||||
.asCallback(err => {
|
||||
expect(err).toBeTruthy()
|
||||
expect(err.message).toBe('Browser unsupported')
|
||||
done()
|
||||
})
|
||||
it('throws error when no supported method', async () => {
|
||||
let error: Error
|
||||
try {
|
||||
await getUserMedia(constraints)
|
||||
} catch (err) {
|
||||
error = err
|
||||
}
|
||||
expect(error!).toBeTruthy()
|
||||
expect(error!.message).toBe('Browser unsupported')
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
describe('play', () => {
|
||||
|
||||
let v1, v2
|
||||
let v1: HTMLVideoElement & { play: jest.Mock }
|
||||
let v2: HTMLVideoElement & { play: jest.Mock }
|
||||
beforeEach(() => {
|
||||
v1 = window.document.createElement('video')
|
||||
v2 = window.document.createElement('video')
|
||||
v1 = window.document.createElement('video') as any
|
||||
v2 = window.document.createElement('video') as any
|
||||
window.document.body.appendChild(v1)
|
||||
window.document.body.appendChild(v2)
|
||||
v1.play = jest.fn()
|
||||
@ -96,7 +97,7 @@ describe('window', () => {
|
||||
|
||||
it('calls window.URL.createObjectURL', () => {
|
||||
window.URL.createObjectURL = jest.fn().mockReturnValue('test')
|
||||
expect(createObjectURL()).toBe('test')
|
||||
expect(createObjectURL('bla')).toBe('test')
|
||||
})
|
||||
|
||||
})
|
||||
@ -105,14 +106,14 @@ describe('window', () => {
|
||||
|
||||
it('calls window.URL.revokeObjectURL', () => {
|
||||
window.URL.revokeObjectURL = jest.fn()
|
||||
expect(revokeObjectURL()).toBe(undefined)
|
||||
expect(revokeObjectURL('bla')).toBe(undefined)
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
describe('valueOf', () => {
|
||||
|
||||
let input
|
||||
let input: HTMLInputElement
|
||||
beforeEach(() => {
|
||||
input = window.document.createElement('input')
|
||||
input.setAttribute('id', 'my-main-id')
|
||||
@ -1,27 +1,28 @@
|
||||
import Promise from 'bluebird'
|
||||
import _debug from 'debug'
|
||||
|
||||
const debug = _debug('peercalls')
|
||||
|
||||
export function getUserMedia (constraints) {
|
||||
export async function getUserMedia (constraints: MediaStreamConstraints) {
|
||||
if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
|
||||
return navigator.mediaDevices.getUserMedia(constraints)
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const getMedia = navigator.getUserMedia || navigator.webkitGetUserMedia
|
||||
return new Promise<MediaStream>((resolve, reject) => {
|
||||
const getMedia = navigator.getUserMedia ||
|
||||
(navigator as any).webkitGetUserMedia
|
||||
if (!getMedia) reject(new Error('Browser unsupported'))
|
||||
getMedia.call(navigator, constraints, resolve, reject)
|
||||
})
|
||||
}
|
||||
|
||||
export const createObjectURL = object => window.URL.createObjectURL(object)
|
||||
export const revokeObjectURL = url => window.URL.revokeObjectURL(url)
|
||||
export const createObjectURL = (object: unknown) =>
|
||||
window.URL.createObjectURL(object)
|
||||
export const revokeObjectURL = (url: string) => window.URL.revokeObjectURL(url)
|
||||
|
||||
export const navigator = window.navigator
|
||||
|
||||
export function play () {
|
||||
let videos = window.document.querySelectorAll('video')
|
||||
const videos = window.document.querySelectorAll('video')
|
||||
Array.prototype.forEach.call(videos, (video, index) => {
|
||||
debug('playing video: %s', index)
|
||||
try {
|
||||
@ -32,13 +33,13 @@ export function play () {
|
||||
})
|
||||
}
|
||||
|
||||
export const valueOf = id => {
|
||||
const el = window.document.getElementById(id)
|
||||
export const valueOf = (id: string) => {
|
||||
const el = window.document.getElementById(id) as HTMLInputElement
|
||||
return el && el.value
|
||||
}
|
||||
|
||||
export const baseUrl = valueOf('baseUrl')
|
||||
export const callId = valueOf('callId')
|
||||
export const iceServers = JSON.parse(valueOf('iceServers'))
|
||||
export const iceServers = JSON.parse(valueOf('iceServers')!)
|
||||
|
||||
export const MediaStream = window.MediaStream
|
||||
Loading…
x
Reference in New Issue
Block a user