Refactor all components

This commit is contained in:
Jerko Steiner 2019-11-13 18:36:31 -03:00
parent 69122466b1
commit 4fa6a0d17a
47 changed files with 718 additions and 600 deletions

63
package-lock.json generated
View File

@ -1949,6 +1949,12 @@
"@types/node": "*"
}
},
"@types/classnames": {
"version": "2.2.9",
"resolved": "https://registry.npmjs.org/@types/classnames/-/classnames-2.2.9.tgz",
"integrity": "sha512-MNl+rT5UmZeilaPxAVs6YaPC2m6aA8rofviZbhbxpPpl61uKodfdQVsBtgJGTqGizEf02oW3tsVe7FYB8kK14A==",
"dev": true
},
"@types/config": {
"version": "0.0.36",
"resolved": "https://registry.npmjs.org/@types/config/-/config-0.0.36.tgz",
@ -2142,6 +2148,15 @@
}
}
},
"@types/react-transition-group": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.2.3.tgz",
"integrity": "sha512-Hk8jiuT7iLOHrcjKP/ZVSyCNXK73wJAUz60xm0mVhiRujrdiI++j4duLiL282VGxwAgxetHQFfqA29LgEeSkFA==",
"dev": true,
"requires": {
"@types/react": "*"
}
},
"@types/redux": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/@types/redux/-/redux-3.6.0.tgz",
@ -2183,6 +2198,15 @@
"redux": "^4.0.0"
}
},
"@types/screenfull": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/@types/screenfull/-/screenfull-4.1.0.tgz",
"integrity": "sha512-TuFSOzuzGFVCdQmwp+a0/Qim+nJhclaWha18yK/xmjrwdPI7qrfeyoSyPx8MoJP8X5d2MwB4a6d2sZhUjJwz9A==",
"dev": true,
"requires": {
"screenfull": "*"
}
},
"@types/serve-static": {
"version": "1.13.3",
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.3.tgz",
@ -3862,11 +3886,6 @@
"lazy-cache": "^1.0.3"
}
},
"chain-function": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/chain-function/-/chain-function-1.0.1.tgz",
"integrity": "sha512-SxltgMwL9uCko5/ZCLiyG2B7R9fY4pDZUw7hJ4MhirdjBLosoDqkWABi3XMucddHdLiFJMb7PD2MZifZriuMTg=="
},
"chalk": {
"version": "1.1.3",
"resolved": "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
@ -4733,6 +4752,7 @@
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-3.4.0.tgz",
"integrity": "sha512-LnuPJ+dwqKDIyotW1VzmOZ5TONUN7CwkCR5hrgawTUbkBGYdeoNLZo6nNfGkCrjtE1nXXaj7iMMpDa8/d9WoIA==",
"dev": true,
"requires": {
"@babel/runtime": "^7.1.2"
}
@ -11762,6 +11782,12 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.6.3.tgz",
"integrity": "sha512-u7FDWtthB4rWibG/+mFbVd5FvdI20yde86qKGx4lVUTWmPlSWQ4QxbBIrrs+HnXGbxOUlUzTAP/VDmvCwaP2yA=="
},
"react-lifecycles-compat": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz",
"integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==",
"dev": true
},
"react-redux": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-6.0.0.tgz",
@ -11776,15 +11802,15 @@
}
},
"react-transition-group": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-1.2.1.tgz",
"integrity": "sha512-CWaL3laCmgAFdxdKbhhps+c0HRGF4c+hdM4H23+FI1QBNUyx/AMeIJGWorehPNSaKnQNOAxL7PQmqMu78CDj3Q==",
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-2.5.1.tgz",
"integrity": "sha512-8x/CxUL9SjYFmUdzsBPTgtKeCxt7QArjNSte0wwiLtF/Ix/o1nWNJooNy5o9XbHIKS31pz7J5VF2l41TwlvbHQ==",
"dev": true,
"requires": {
"chain-function": "^1.0.0",
"dom-helpers": "^3.2.0",
"loose-envify": "^1.3.1",
"prop-types": "^15.5.6",
"warning": "^3.0.0"
"dom-helpers": "^3.3.1",
"loose-envify": "^1.4.0",
"prop-types": "^15.6.2",
"react-lifecycles-compat": "^3.0.4"
}
},
"read-only-stream": {
@ -12604,7 +12630,8 @@
"screenfull": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/screenfull/-/screenfull-3.3.3.tgz",
"integrity": "sha512-DzYUuXr+OV2BDvYXaYzlYgJd4WXZZ2CW5NFC7Kw6TUCpzXJAx4MwlVD6CH+Mu6fi8rfAQIQfqdFZ4jtDsEkWig=="
"integrity": "sha512-DzYUuXr+OV2BDvYXaYzlYgJd4WXZZ2CW5NFC7Kw6TUCpzXJAx4MwlVD6CH+Mu6fi8rfAQIQfqdFZ4jtDsEkWig==",
"dev": true
},
"scss-tokenizer": {
"version": "0.2.3",
@ -14320,14 +14347,6 @@
"makeerror": "1.0.x"
}
},
"warning": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/warning/-/warning-3.0.0.tgz",
"integrity": "sha1-MuU3fLVy3kqwR1O9+IIcAe1gW3w=",
"requires": {
"loose-envify": "^1.0.0"
}
},
"watchify": {
"version": "3.11.1",
"resolved": "https://registry.npmjs.org/watchify/-/watchify-3.11.1.tgz",

View File

@ -60,12 +60,10 @@
"react": "^16.6.3",
"react-dom": "^16.6.3",
"react-redux": "^6.0.0",
"react-transition-group": "^1.2.1",
"redux": "^4.0.1",
"redux-logger": "^3.0.6",
"redux-promise-middleware": "^5.1.1",
"redux-thunk": "^2.2.0",
"screenfull": "^3.3.3",
"seamless-immutable": "^7.1.2",
"simple-peer": "^9.1.2",
"socket.io": "^2.2.0",
@ -81,6 +79,7 @@
"@babel/preset-env": "^7.5.0",
"@babel/preset-react": "^7.0.0",
"@types/bluebird": "^3.5.29",
"@types/classnames": "^2.2.9",
"@types/config": "0.0.36",
"@types/debug": "^4.1.5",
"@types/express": "^4.17.2",
@ -89,9 +88,11 @@
"@types/react": "^16.9.11",
"@types/react-dom": "^16.9.4",
"@types/react-redux": "^7.1.5",
"@types/react-transition-group": "^4.2.3",
"@types/redux": "^3.6.0",
"@types/redux-logger": "^3.0.7",
"@types/redux-mock-store": "^1.0.1",
"@types/screenfull": "^4.1.0",
"@types/simple-peer": "^6.1.6",
"@types/socket.io": "^2.1.4",
"@types/socket.io-client": "^1.4.32",
@ -117,7 +118,9 @@
"jest-cli": "^24.8.0",
"node-sass": "^4.13.0",
"nodemon": "^1.18.8",
"react-transition-group": "^2.5.1",
"redux-mock-store": "^1.2.3",
"screenfull": "^3.3.3",
"supertest": "^3.0.0",
"ts-jest": "^24.1.0",
"ts-node": "^8.5.0",

View File

@ -1,3 +1,3 @@
import configureStore from 'redux-mock-store'
import { middlewares } from '../middlewares.js'
import { middlewares } from '../middlewares'
export default configureStore(middlewares)({})

View File

@ -1,5 +1,3 @@
import Promise from 'bluebird'
export const createObjectURL = jest.fn()
.mockImplementation(object => 'blob://' + String(object))
export const revokeObjectURL = jest.fn()

View File

@ -1,23 +1,26 @@
jest.mock('../socket.js')
jest.mock('../window.js')
jest.mock('../store.js')
jest.mock('./SocketActions.js')
jest.mock('../socket')
jest.mock('../window')
jest.mock('../store')
jest.mock('./SocketActions')
import * as CallActions from './CallActions.js'
import * as SocketActions from './SocketActions.js'
import * as constants from '../constants.js'
import socket from '../socket.js'
import store from '../store.js'
import { callId, getUserMedia } from '../window.js'
import * as CallActions from './CallActions'
import * as SocketActions from './SocketActions'
import * as constants from '../constants'
import socket from '../socket'
import storeMock from '../store'
import { callId, getUserMedia } from '../window'
import { MockStore } from 'redux-mock-store'
jest.useFakeTimers()
describe('reducers/alerts', () => {
const store: MockStore = storeMock as any
beforeEach(() => {
store.clearActions()
getUserMedia.fail(false)
SocketActions.handshake.mockReturnValue(jest.fn())
store.clearActions();
(getUserMedia as any).fail(false);
(SocketActions.handshake as jest.Mock).mockReturnValue(jest.fn())
})
afterEach(() => {
@ -28,81 +31,80 @@ describe('reducers/alerts', () => {
describe('init', () => {
it('calls handshake.init when connected & got camera stream', async () => {
const promise = store.dispatch(CallActions.init())
const promise = CallActions.init(store.dispatch, store.getState)
socket.emit('connect')
expect(store.getActions()).toEqual([{
type: constants.INIT_PENDING
type: constants.INIT_PENDING,
}, {
type: constants.NOTIFY,
payload: {
id: jasmine.any(String),
message: 'Connected to server socket',
type: 'warning'
}
type: 'warning',
},
}, {
type: constants.MESSAGE_ADD,
payload: {
image: null,
message: 'Connected to server socket',
timestamp: jasmine.any(String),
userId: '[PeerCalls]'
}
userId: '[PeerCalls]',
},
}])
await promise
expect(SocketActions.handshake.mock.calls).toEqual([[{
expect((SocketActions.handshake as jest.Mock).mock.calls).toEqual([[{
socket,
roomName: callId,
stream: getUserMedia.stream
stream: (getUserMedia as any).stream,
}]])
})
it('calls dispatches disconnect message on disconnect', async () => {
const promise = store.dispatch(CallActions.init())
const promise = CallActions.init(store.dispatch, store.getState)
socket.emit('connect')
socket.emit('disconnect')
expect(store.getActions()).toEqual([{
type: constants.INIT_PENDING
type: constants.INIT_PENDING,
}, {
type: constants.NOTIFY,
payload: {
id: jasmine.any(String),
message: 'Connected to server socket',
type: 'warning'
}
type: 'warning',
},
}, {
type: constants.MESSAGE_ADD,
payload: {
image: null,
message: 'Connected to server socket',
timestamp: jasmine.any(String),
userId: '[PeerCalls]'
}
userId: '[PeerCalls]',
},
}, {
type: constants.NOTIFY,
payload: {
id: jasmine.any(String),
message: 'Server socket disconnected',
type: 'error'
}
type: 'error',
},
}, {
type: constants.MESSAGE_ADD,
payload: {
image: null,
message: 'Server socket disconnected',
timestamp: jasmine.any(String),
userId: '[PeerCalls]'
}
userId: '[PeerCalls]',
},
}])
await promise
})
it('dispatches alert when failed to get media stream', async () => {
getUserMedia.fail(true)
const promise = store.dispatch(CallActions.init())
(getUserMedia as any).fail(true)
const promise = CallActions.init(store.dispatch, store.getState)
socket.emit('connect')
const result = await promise
expect(result.value).toBe(null)
await promise
})
})

View File

@ -1,11 +1,11 @@
import * as NotifyActions from './NotifyActions.js'
import * as SocketActions from './SocketActions.js'
import * as StreamActions from './StreamActions.js'
import * as constants from '../constants.js'
import socket from '../socket.js'
import { callId, getUserMedia } from '../window.js'
import * as NotifyActions from './NotifyActions'
import * as SocketActions from './SocketActions'
import * as StreamActions from './StreamActions'
import * as constants from '../constants'
import socket from '../socket'
import { callId, getUserMedia } from '../window'
import { Dispatch } from 'redux'
import { GetState } from '../store.js'
import { GetState } from '../store'
export interface InitAction {
type: 'INIT'

View File

@ -1,4 +1,4 @@
import { MESSAGE_ADD } from '../constants.js'
import { MESSAGE_ADD } from '../constants'
export interface MessageAddAction {
type: 'MESSAGE_ADD'

View File

@ -1,5 +1,5 @@
import * as constants from '../constants.js'
import * as ChatActions from './ChatActions.js'
import * as constants from '../constants'
import * as ChatActions from './ChatActions'
import { Dispatch } from 'redux'
import _ from 'underscore'

View File

@ -1,11 +1,11 @@
jest.mock('../window.js')
jest.mock('../window')
jest.mock('simple-peer')
import * as PeerActions from './PeerActions.js'
import * as PeerActions from './PeerActions'
import Peer from 'simple-peer'
import { EventEmitter } from 'events'
import { createStore, Store, GetState } from '../store.js'
import { play } from '../window.js'
import { createStore, Store, GetState } from '../store'
import { play } from '../window'
import { Dispatch } from 'redux'
describe('PeerActions', () => {

View File

@ -1,11 +1,11 @@
import * as ChatActions from '../actions/ChatActions.js'
import * as NotifyActions from '../actions/NotifyActions.js'
import * as StreamActions from '../actions/StreamActions.js'
import * as constants from '../constants.js'
import * as ChatActions from '../actions/ChatActions'
import * as NotifyActions from '../actions/NotifyActions'
import * as StreamActions from '../actions/StreamActions'
import * as constants from '../constants'
import Peer from 'simple-peer'
import _ from 'underscore'
import _debug from 'debug'
import { play, iceServers } from '../window.js'
import { play, iceServers } from '../window'
import { Dispatch } from 'redux'
const debug = _debug('peercalls')

View File

@ -1,11 +1,11 @@
jest.mock('simple-peer')
jest.mock('../window.js')
jest.mock('../window')
import * as SocketActions from './SocketActions.js'
import * as constants from '../constants.js'
import * as SocketActions from './SocketActions'
import * as constants from '../constants'
import Peer from 'simple-peer'
import { EventEmitter } from 'events'
import { createStore, Store, GetState } from '../store.js'
import { createStore, Store, GetState } from '../store'
import { Dispatch } from 'redux'
describe('SocketActions', () => {

View File

@ -1,6 +1,6 @@
import * as NotifyActions from '../actions/NotifyActions.js'
import * as PeerActions from '../actions/PeerActions.js'
import * as constants from '../constants.js'
import * as NotifyActions from '../actions/NotifyActions'
import * as PeerActions from '../actions/PeerActions'
import * as constants from '../constants'
import _ from 'underscore'
import _debug from 'debug'
import { Dispatch } from 'redux'

View File

@ -1,4 +1,4 @@
import * as constants from '../constants.js'
import * as constants from '../constants'
export interface AddStreamPayload {
userId: string
@ -56,6 +56,6 @@ export const toggleActive = (userId: string): ToggleActiveStreamAction => ({
export type StreamAction =
AddStreamAction |
RemoveStreamAction
// SetActiveStreamAction |
// ToggleActiveStreamAction
RemoveStreamAction |
SetActiveStreamAction |
ToggleActiveStreamAction

View File

@ -1,18 +1,13 @@
import React from 'react'
import PropTypes from 'prop-types'
import classnames from 'classnames'
import { Alert as AlertType } from '../actions/NotifyActions'
export const AlertPropType = PropTypes.shape({
dismissable: PropTypes.bool,
action: PropTypes.string.isRequired,
message: PropTypes.string.isRequired
})
export interface AlertProps {
alert: AlertType
dismiss: (alert: AlertType) => void
}
export class Alert extends React.PureComponent {
static propTypes = {
alert: AlertPropType,
dismiss: PropTypes.func.isRequired
}
export class Alert extends React.PureComponent<AlertProps> {
dismiss = () => {
const { alert, dismiss } = this.props
dismiss(alert)
@ -34,11 +29,12 @@ export class Alert extends React.PureComponent {
}
}
export default class Alerts extends React.PureComponent {
static propTypes = {
alerts: PropTypes.arrayOf(AlertPropType).isRequired,
dismiss: PropTypes.func.isRequired
}
export interface AlertsProps {
alerts: AlertType[]
dismiss: (alert: AlertType) => void
}
export default class Alerts extends React.PureComponent<AlertsProps> {
render () {
const { alerts, dismiss } = this.props
return (

View File

@ -1,42 +1,49 @@
import * as constants from '../constants.js'
import Alerts, { AlertPropType } from './Alerts.js'
import Chat, { MessagePropTypes } from './Chat.js'
import Notifications, { NotificationPropTypes } from './Notifications.js'
import PropTypes from 'prop-types'
import React from 'react'
import Toolbar from './Toolbar.js'
import Video, { StreamPropType } from './Video.js'
import Peer from 'simple-peer'
import _ from 'underscore'
import { Message } from '../actions/ChatActions'
import { Alert, Notification } from '../actions/NotifyActions'
import { TextMessage } from '../actions/PeerActions'
import { AddStreamPayload } from '../actions/StreamActions'
import * as constants from '../constants'
import Alerts from './Alerts'
import Chat from './Chat'
import Notifications from './Notifications'
import Toolbar from './Toolbar'
import Video from './Video'
export default class App extends React.PureComponent {
static propTypes = {
active: PropTypes.string,
alerts: PropTypes.arrayOf(AlertPropType).isRequired,
dismissAlert: PropTypes.func.isRequired,
init: PropTypes.func.isRequired,
notifications: PropTypes.objectOf(NotificationPropTypes).isRequired,
messages: PropTypes.arrayOf(MessagePropTypes).isRequired,
peers: PropTypes.object.isRequired,
sendMessage: PropTypes.func.isRequired,
streams: PropTypes.objectOf(StreamPropType).isRequired,
onSendFile: PropTypes.func.isRequired,
toggleActive: PropTypes.func.isRequired
}
constructor () {
super()
this.state = {
videos: {},
chatVisible: false
}
export interface AppProps {
active: string | null
alerts: Alert[]
dismissAlert: (alert: Alert) => void
init: () => void
notifications: Record<string, Notification>
messages: Message[]
peers: Record<string, Peer.Instance>
sendMessage: (message: TextMessage) => void
streams: Record<string, AddStreamPayload>
onSendFile: (file: File) => void
toggleActive: (userId: string) => void
}
export interface AppState {
videos: Record<string, unknown>
chatVisible: boolean
}
export default class App extends React.PureComponent<AppProps, AppState> {
state: AppState = {
videos: {},
chatVisible: false,
}
handleShowChat = () => {
this.setState({
chatVisible: true
chatVisible: true,
})
}
handleHideChat = () => {
this.setState({
chatVisible: false
chatVisible: false,
})
}
handleToggleChat = () => {
@ -59,7 +66,7 @@ export default class App extends React.PureComponent {
peers,
sendMessage,
toggleActive,
streams
streams,
} = this.props
const { videos } = this.state
@ -79,7 +86,6 @@ export default class App extends React.PureComponent {
messages={messages}
onClose={this.handleHideChat}
sendMessage={sendMessage}
videos={videos}
visible={this.state.chatVisible}
/>
<div className="videos">

View File

@ -1,16 +1,14 @@
import Input from './Input.js'
import PropTypes from 'prop-types'
import React from 'react'
import classnames from 'classnames'
import React from 'react'
import { Message as MessageType } from '../actions/ChatActions'
import { TextMessage } from '../actions/PeerActions'
import Input from './Input'
export const MessagePropTypes = PropTypes.shape({
userId: PropTypes.string.isRequired,
message: PropTypes.string.isRequired,
timestamp: PropTypes.string.isRequired,
image: PropTypes.string
})
export interface MessageProps {
message: MessageType
}
function Message (props) {
function Message (props: MessageProps) {
const { message } = props
return (
<p className="message-text">
@ -22,24 +20,18 @@ function Message (props) {
)
}
Message.propTypes = {
message: MessagePropTypes
export interface ChatProps {
visible: boolean
messages: MessageType[]
onClose: () => void
sendMessage: (message: TextMessage) => void
}
export default class Chat extends React.PureComponent {
static propTypes = {
visible: PropTypes.bool.isRequired,
messages: PropTypes.arrayOf(MessagePropTypes).isRequired,
onClose: PropTypes.func.isRequired,
sendMessage: PropTypes.func.isRequired,
videos: PropTypes.object.isRequired
}
constructor () {
super()
this.chatHistoryRef = React.createRef()
}
export default class Chat extends React.PureComponent<ChatProps> {
chatHistoryRef = React.createRef<HTMLDivElement>()
scrollToBottom = () => {
const chatHistoryRef = this.chatHistoryRef.current
const chatHistoryRef = this.chatHistoryRef.current!
chatHistoryRef.scrollTop = chatHistoryRef.scrollHeight
}
componentDidMount () {
@ -49,10 +41,10 @@ export default class Chat extends React.PureComponent {
this.scrollToBottom()
}
render () {
const { messages, videos, sendMessage } = this.props
const { messages, sendMessage } = this.props
return (
<div className={classnames('chat-container', {
show: this.props.visible
show: this.props.visible,
})}>
<div className="chat-header">
<div className="chat-close" onClick={this.props.onClose}>
@ -111,10 +103,7 @@ export default class Chat extends React.PureComponent {
</div>
<Input
videos={videos}
sendMessage={sendMessage}
/>
<Input sendMessage={sendMessage} />
</div>
)
}

View File

@ -1,36 +1,39 @@
import Input from './Input.js'
import Input from './Input'
import React from 'react'
import ReactDOM from 'react-dom'
import TestUtils from 'react-dom/test-utils'
import { TextMessage } from '../actions/PeerActions'
describe('components/Input', () => {
let component, node, videos, notify, sendMessage
function render () {
videos = {}
notify = jest.fn()
let node: Element
let sendMessage: jest.Mock<(message: TextMessage) => void>
async function render () {
sendMessage = jest.fn()
component = TestUtils.renderIntoDocument(
<Input
videos={videos}
sendMessage={sendMessage}
notify={notify}
/>
)
node = ReactDOM.findDOMNode(component)
const div = document.createElement('div')
await new Promise<Input>(resolve => {
ReactDOM.render(
<Input
ref={input => resolve(input!)}
sendMessage={sendMessage}
/>,
div,
)
})
node = div.children[0]
}
let message = 'test message'
const message = 'test message'
beforeEach(() => render())
describe('send message', () => {
let input
let input: HTMLTextAreaElement
beforeEach(() => {
sendMessage.mockClear()
input = node.querySelector('textarea')
input = node.querySelector('textarea')!
TestUtils.Simulate.change(input, {
target: { value: message }
target: { value: message } as any,
})
expect(input.value).toBe(message)
})
@ -47,7 +50,7 @@ describe('components/Input', () => {
describe('handleKeyPress', () => {
it('sends a message', () => {
TestUtils.Simulate.keyPress(input, {
key: 'Enter'
key: 'Enter',
})
expect(input.value).toBe('')
expect(sendMessage.mock.calls)
@ -56,7 +59,7 @@ describe('components/Input', () => {
it('does nothing when other key pressed', () => {
TestUtils.Simulate.keyPress(input, {
key: 'test'
key: 'test',
})
expect(sendMessage.mock.calls.length).toBe(0)
})
@ -64,7 +67,7 @@ describe('components/Input', () => {
describe('handleSmileClick', () => {
it('adds smile to message', () => {
const div = node.querySelector('.chat-controls-buttons-smile')
const div = node.querySelector('.chat-controls-buttons-smile')!
TestUtils.Simulate.click(div)
expect(input.value).toBe('test message😑')
})

View File

@ -1,34 +1,37 @@
import PropTypes from 'prop-types'
import React from 'react'
import React, { ReactEventHandler, ChangeEventHandler, KeyboardEventHandler, MouseEventHandler } from 'react'
import { TextMessage } from '../actions/PeerActions'
export default class Input extends React.PureComponent {
static propTypes = {
sendMessage: PropTypes.func.isRequired
export interface InputProps {
sendMessage: (message: TextMessage) => void
}
export interface InputState {
message: string
}
export default class Input extends React.PureComponent<InputProps, InputState> {
textArea = React.createRef<HTMLTextAreaElement>()
state = {
message: '',
}
constructor () {
super()
this.state = {
message: ''
}
}
handleChange = e => {
handleChange: ChangeEventHandler<HTMLTextAreaElement> = event => {
this.setState({
message: e.target.value
message: event.target.value,
})
}
handleSubmit = e => {
handleSubmit: ReactEventHandler<HTMLFormElement> = e => {
e.preventDefault()
this.submit()
}
handleKeyPress = e => {
handleKeyPress: KeyboardEventHandler<HTMLTextAreaElement> = e => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault()
this.submit()
}
}
handleSmileClick = e => {
handleSmileClick: MouseEventHandler<HTMLElement> = event => {
this.setState({
message: this.textArea.value + e.currentTarget.innerHTML
message: this.textArea.current!.value + event.currentTarget.innerHTML,
})
}
submit = () => {
@ -37,7 +40,7 @@ export default class Input extends React.PureComponent {
if (message) {
sendMessage({
payload: message,
type: 'text'
type: 'text',
})
// let image = null
@ -66,7 +69,7 @@ export default class Input extends React.PureComponent {
onKeyPress={this.handleKeyPress}
placeholder="Type a message"
value={message}
ref={node => { this.textArea = node }}
ref={this.textArea}
/>
<div className="chat-controls-buttons">
<input type="submit" value="Send"

View File

@ -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>
)
}
}

View 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>
)
}
}

View File

@ -1,17 +1,20 @@
jest.mock('../window.js')
jest.mock('../window')
import React from 'react'
import ReactDOM from 'react-dom'
import TestUtils from 'react-dom/test-utils'
import Toolbar from './Toolbar.js'
import { MediaStream } from '../window.js'
import Toolbar, { ToolbarProps } from './Toolbar'
import { MediaStream } from '../window'
import { AddStreamPayload } from '../actions/StreamActions'
describe('components/Toolbar', () => {
class ToolbarWrapper extends React.PureComponent {
static propTypes = Toolbar.propTypes
constructor () {
super()
this.state = {}
interface StreamState {
stream: AddStreamPayload | null
}
class ToolbarWrapper extends React.PureComponent<ToolbarProps, StreamState> {
state = {
stream: null,
}
render () {
return <Toolbar
@ -24,31 +27,40 @@ describe('components/Toolbar', () => {
}
}
let component, node, mediaStream, url, onToggleChat, onSendFile
function render () {
let node: Element
let mediaStream: MediaStream
let url: string
let onToggleChat: jest.Mock<() => void>
let onSendFile: jest.Mock<(file: File) => void>
async function render () {
mediaStream = new MediaStream()
onToggleChat = jest.fn()
onSendFile = jest.fn()
component = TestUtils.renderIntoDocument(
<ToolbarWrapper
chatVisible
onToggleChat={onToggleChat}
onSendFile={onSendFile}
messages={[]}
stream={{ mediaStream, url }}
/>
)
node = ReactDOM.findDOMNode(component)
const div = document.createElement('div')
await new Promise<ToolbarWrapper>(resolve => {
ReactDOM.render(
<ToolbarWrapper
ref={instance => resolve(instance!)}
chatVisible
onToggleChat={onToggleChat}
onSendFile={onSendFile}
messages={[]}
stream={{ userId: '', stream: mediaStream, url }}
/>,
div,
)
})
node = div.children[0]
}
beforeEach(() => {
render()
beforeEach(async () => {
await render()
})
describe('handleChatClick', () => {
it('toggle chat', () => {
expect(onToggleChat.mock.calls.length).toBe(0)
const button = node.querySelector('.chat')
const button = node.querySelector('.chat')!
TestUtils.Simulate.click(button)
expect(onToggleChat.mock.calls.length).toBe(1)
})
@ -56,7 +68,7 @@ describe('components/Toolbar', () => {
describe('handleMicClick', () => {
it('toggle mic', () => {
const button = node.querySelector('.mute-audio')
const button = node.querySelector('.mute-audio')!
TestUtils.Simulate.click(button)
expect(button.classList.contains('on')).toBe(true)
})
@ -64,7 +76,7 @@ describe('components/Toolbar', () => {
describe('handleCamClick', () => {
it('toggle cam', () => {
const button = node.querySelector('.mute-video')
const button = node.querySelector('.mute-video')!
TestUtils.Simulate.click(button)
expect(button.classList.contains('on')).toBe(true)
})
@ -72,7 +84,7 @@ describe('components/Toolbar', () => {
describe('handleFullscreenClick', () => {
it('toggle fullscreen', () => {
const button = node.querySelector('.fullscreen')
const button = node.querySelector('.fullscreen')!
TestUtils.Simulate.click(button)
expect(button.classList.contains('on')).toBe(false)
})
@ -80,7 +92,7 @@ describe('components/Toolbar', () => {
describe('handleHangoutClick', () => {
it('hangout', () => {
const button = node.querySelector('.hangup')
const button = node.querySelector('.hangup')!
TestUtils.Simulate.click(button)
expect(window.location.href).toBe('http://localhost/')
})
@ -88,10 +100,10 @@ describe('components/Toolbar', () => {
describe('handleSendFile', () => {
it('triggers input dialog', () => {
const sendFileButton = node.querySelector('.send-file')
const sendFileButton = node.querySelector('.send-file')!
const click = jest.fn()
const file = node.querySelector('input[type=file]')
file.click = click
const file = node.querySelector('input[type=file]')!;
(file as any).click = click
TestUtils.Simulate.click(sendFileButton)
expect(click.mock.calls.length).toBe(1)
})
@ -99,12 +111,12 @@ describe('components/Toolbar', () => {
describe('handleSelectFiles', () => {
it('iterates through files and calls onSendFile for each', () => {
const file = node.querySelector('input[type=file]')
const files = [{ name: 'first' }]
const file = node.querySelector('input[type=file]')!
const files = [{ name: 'first' }] as any
TestUtils.Simulate.change(file, {
target: {
files
}
files,
} as any,
})
expect(onSendFile.mock.calls).toEqual([[ files[0] ]])
})

View File

@ -1,63 +1,85 @@
import PropTypes from 'prop-types'
import React from 'react'
import React, { ReactEventHandler } from 'react'
import classnames from 'classnames'
import screenfull from 'screenfull'
import { MessagePropTypes } from './Chat.js'
import { StreamPropType } from './Video.js'
import { Message } from '../actions/ChatActions'
import { AddStreamPayload } from '../actions/StreamActions'
const hidden = {
display: 'none'
display: 'none',
}
export default class Toolbar extends React.PureComponent {
static propTypes = {
messages: PropTypes.arrayOf(MessagePropTypes).isRequired,
stream: StreamPropType,
onToggleChat: PropTypes.func.isRequired,
onSendFile: PropTypes.func.isRequired,
chatVisible: PropTypes.bool.isRequired
}
constructor (props) {
export interface ToolbarProps {
messages: Message[]
stream: AddStreamPayload
onToggleChat: () => void
onSendFile: (file: File) => void
chatVisible: boolean
}
export interface ToolbarState {
readMessages: number
camDisabled: boolean
micMuted: boolean
fullScreenEnabled: boolean
}
export default class Toolbar
extends React.PureComponent<ToolbarProps, ToolbarState> {
file = React.createRef<HTMLInputElement>()
constructor(props: ToolbarProps) {
super(props)
this.file = React.createRef()
this.state = {
readMessages: props.messages.length
readMessages: props.messages.length,
camDisabled: false,
micMuted: false,
fullScreenEnabled: false,
}
}
handleMicClick = () => {
const { stream } = this.props
stream.mediaStream.getAudioTracks().forEach(track => {
stream.stream.getAudioTracks().forEach(track => {
track.enabled = !track.enabled
})
this.mixButton.classList.toggle('on')
this.setState({
...this.state,
micMuted: !this.state.micMuted,
})
}
handleCamClick = () => {
const { stream } = this.props
stream.mediaStream.getVideoTracks().forEach(track => {
stream.stream.getVideoTracks().forEach(track => {
track.enabled = !track.enabled
})
this.camButton.classList.toggle('on')
this.setState({
...this.state,
camDisabled: !this.state.camDisabled,
})
}
handleFullscreenClick = () => {
if (screenfull.enabled) {
screenfull.toggle()
this.fullscreenButton.classList.toggle('on')
this.setState({
...this.state,
fullScreenEnabled: !this.state.fullScreenEnabled,
})
}
}
handleHangoutClick = () => {
window.location.href = '/'
}
handleSendFile = () => {
this.file.current.click()
this.file.current!.click()
}
handleSelectFiles = (event) => {
handleSelectFiles: ReactEventHandler<HTMLInputElement> = event => {
Array
.from(event.target.files)
.from(this.file.current!.files!)
.forEach(file => this.props.onSendFile(file))
}
handleToggleChat = () => {
this.setState({
readMessages: this.props.messages.length
readMessages: this.props.messages.length,
})
this.props.onToggleChat()
}
@ -68,7 +90,7 @@ export default class Toolbar extends React.PureComponent {
<div className="toolbar active">
<div onClick={this.handleToggleChat}
className={classnames('button chat', {
on: this.props.chatVisible
on: this.props.chatVisible,
})}
data-blink={!this.props.chatVisible &&
messages.length > this.state.readMessages}
@ -93,17 +115,20 @@ export default class Toolbar extends React.PureComponent {
{stream && (
<div>
<div onClick={this.handleMicClick}
ref={node => { this.mixButton = node }}
className="button mute-audio"
<div
onClick={this.handleMicClick}
className={classnames('button mute-audio', {
on: this.state.micMuted,
})}
title="Mute audio"
>
<span className="on icon icon-mic_off" />
<span className="off icon icon-mic" />
</div>
<div onClick={this.handleCamClick}
ref={node => { this.camButton = node }}
className="button mute-video"
className={classnames('button mute-video', {
on: this.state.camDisabled,
})}
title="Mute video"
>
<span className="on icon icon-videocam_off" />
@ -113,8 +138,9 @@ export default class Toolbar extends React.PureComponent {
)}
<div onClick={this.handleFullscreenClick}
ref={node => { this.fullscreenButton = node }}
className="button fullscreen"
className={classnames('button fullscreen', {
on: this.state.fullScreenEnabled,
})}
title="Enter fullscreen"
>
<span className="on icon icon-fullscreen_exit" />

View File

@ -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 }
})
})
})
})
})

View 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: '' },
})
})
})
})
})

View File

@ -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>
)
}
}

View 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>
)
}
}

View File

@ -1,18 +1,18 @@
jest.mock('../actions/CallActions.js')
jest.mock('../socket.js')
jest.mock('../window.js')
jest.mock('../actions/CallActions')
jest.mock('../socket')
jest.mock('../window')
import * as constants from '../constants.js'
import App from './App.js'
import * as constants from '../constants'
import App from './App'
import React from 'react'
import ReactDOM from 'react-dom'
import TestUtils from 'react-dom/test-utils'
import configureStore from 'redux-mock-store'
import reducers from '../reducers'
import { MediaStream } from '../window.js'
import { MediaStream } from '../window'
import { Provider } from 'react-redux'
import { init } from '../actions/CallActions.js'
import { middlewares } from '../store.js'
import { init } from '../actions/CallActions'
import { middlewares } from '../store'
describe('App', () => {

View File

@ -1,29 +1,30 @@
import * as CallActions from '../actions/CallActions.js'
import * as NotifyActions from '../actions/NotifyActions.js'
import * as PeerActions from '../actions/PeerActions.js'
import * as StreamActions from '../actions/StreamActions.js'
import App from '../components/App.js'
import { bindActionCreators } from 'redux'
import * as CallActions from '../actions/CallActions'
import * as NotifyActions from '../actions/NotifyActions'
import * as PeerActions from '../actions/PeerActions'
import * as StreamActions from '../actions/StreamActions'
import App from '../components/App'
import { bindActionCreators, Dispatch } from 'redux'
import { connect } from 'react-redux'
import { State } from '../store'
function mapStateToProps (state) {
function mapStateToProps (state: State) {
return {
streams: state.streams,
peers: state.peers,
alerts: state.alerts,
notifications: state.notifications,
messages: state.messages,
active: state.active
active: state.active,
}
}
function mapDispatchToProps (dispatch) {
function mapDispatchToProps (dispatch: Dispatch) {
return {
toggleActive: bindActionCreators(StreamActions.toggleActive, dispatch),
sendMessage: bindActionCreators(PeerActions.sendMessage, dispatch),
dismissAlert: bindActionCreators(NotifyActions.dismissAlert, dispatch),
init: bindActionCreators(CallActions.init, dispatch),
onSendFile: bindActionCreators(PeerActions.sendFile, dispatch)
onSendFile: bindActionCreators(PeerActions.sendFile, dispatch),
}
}

View File

@ -1,49 +1,48 @@
const iceServers = require('./iceServers.js')
import { config, ICEServer } from '../../server/config'
function noop () {}
function checkTURNServer (turnConfig, timeout) {
async function checkTURNServer (turnConfig: ICEServer, timeoutDuration = 5000) {
console.log('checking turn server', turnConfig)
return new Promise(function (resolve, reject) {
const timeout = new Promise<unknown>((resolve, reject) => {
setTimeout(function () {
if (promiseResolved) return
resolve(false)
promiseResolved = true
}, timeout || 5000)
reject(new Error('timed out'))
}, timeoutDuration)
})
let promiseResolved = false
async function start() {
const PeerConnection = window.RTCPeerConnection ||
window.mozRTCPeerConnection ||
(
window as unknown as { mozRTCPeerConnection: RTCPeerConnection }
).mozRTCPeerConnection||
window.webkitRTCPeerConnection
const pc = new PeerConnection({ iceServers: [turnConfig] })
// create a bogus data channel
pc.createDataChannel('')
pc.createOffer(function (sdp) {
// sometimes sdp contains the ice candidates...
if (sdp.sdp.indexOf('typ relay') > -1) {
promiseResolved = true
const sdp = await pc.createOffer()
// sometimes sdp contains the ice candidates...
if (sdp.sdp!.indexOf('typ relay') > -1) {
return true
}
pc.setLocalDescription(sdp)
return new Promise(resolve => {
pc.onicecandidate = function (ice) {
if (!ice ||
!ice.candidate ||
!ice.candidate.candidate ||
!(ice.candidate.candidate.indexOf('typ relay') > -1)) {
return
}
resolve(true)
}
pc.setLocalDescription(sdp, noop, noop)
}, noop)
})
}
pc.onicecandidate = function (ice) {
if (promiseResolved ||
!ice ||
!ice.candidate ||
!ice.candidate.candidate ||
!(ice.candidate.candidate.indexOf('typ relay') > -1)) {
return
}
promiseResolved = true
resolve(true)
}
})
return Promise.race([ timeout, start() ])
}
checkTURNServer(iceServers[0], 10000)
checkTURNServer(config.iceServers[0], 10000)
.then(console.log.bind(console))
.catch(console.error.bind(console))

View File

@ -2,9 +2,9 @@ import '@babel/polyfill'
import App from './containers/App'
import React from 'react'
import ReactDOM from 'react-dom'
import store from './store.js'
import store from './store'
import { Provider } from 'react-redux'
import { play } from './window.js'
import { play } from './window'
const component = (
<Provider store={store}>

View File

@ -1,5 +1,5 @@
import logger from 'redux-logger'
import { create } from './middlewares.js'
import { create } from './middlewares'
describe('store', () => {

View File

@ -1,12 +1,12 @@
import * as StreamActions from '../actions/StreamActions.js'
import active from './active.js'
import * as StreamActions from '../actions/StreamActions'
import active from './active'
describe('reducers/active', () => {
describe('setActive', () => {
it('sets active to userId', () => {
const userId = 'test'
let state = active()
let state = active(null, {type: 'test'} as any)
state = active(state, StreamActions.setActive(userId))
expect(state).toBe(userId)
})
@ -15,7 +15,7 @@ describe('reducers/active', () => {
describe('toggleActive', () => {
it('sets active to userId', () => {
const userId = 'test'
let state = active()
let state = active(null, {type: 'test'} as any)
state = active(state, StreamActions.toggleActive(userId))
expect(state).toBe(userId)
state = active(state, StreamActions.toggleActive(userId))

View File

@ -1,6 +1,12 @@
import * as constants from '../constants.js'
import * as constants from '../constants'
import { StreamAction } from '../actions/StreamActions'
export default function active (state = null, action: Action) {
export type ActiveState = null | string
export default function active (
state: ActiveState = null,
action: StreamAction,
): ActiveState {
switch (action && action.type) {
case constants.ACTIVE_SET:
case constants.STREAM_ADD:

View File

@ -1,38 +1,34 @@
import * as NotifyActions from '../actions/NotifyActions.js'
import * as NotifyActions from '../actions/NotifyActions'
import _ from 'underscore'
import { applyMiddleware, createStore } from 'redux'
import { create } from '../middlewares.js'
import reducers from './index.js'
import { applyMiddleware, createStore, Store } from 'redux'
import { create } from '../middlewares'
import reducers from './index'
jest.useFakeTimers()
describe('reducers/alerts', () => {
let store
let store: Store
beforeEach(() => {
store = createStore(
reducers,
applyMiddleware.apply(null, create())
applyMiddleware(...create()),
)
})
describe('clearAlert', () => {
const actions = {
true: 'Dismiss',
false: ''
}
;[true, false].forEach(dismissable => {
[true, false].forEach(dismissable => {
beforeEach(() => {
store.dispatch(NotifyActions.clearAlerts())
})
it('adds alert to store', () => {
store.dispatch(NotifyActions.alert('test', dismissable))
expect(store.getState().alerts).toEqual([{
action: actions[dismissable],
action: dismissable ? 'Dismiss' : undefined,
dismissable,
message: 'test',
type: 'warning'
type: 'warning',
}])
})
})
@ -51,25 +47,36 @@ describe('reducers/alerts', () => {
it('does not remove an alert when not found', () => {
store.dispatch(NotifyActions.alert('test', true))
expect(store.getState().alerts.length).toBe(1)
store.dispatch(NotifyActions.dismissAlert({}))
store.dispatch(NotifyActions.dismissAlert({
action: undefined,
dismissable: false,
message: 'bla',
type: 'error',
}))
expect(store.getState().alerts.length).toBe(1)
})
})
;['info', 'warning', 'error'].forEach(type => {
const methods: Array<'info' | 'warning' | 'error'> = [
'info',
'warning',
'error',
]
methods.forEach(type => {
describe(type, () => {
beforeEach(() => {
store.dispatch(NotifyActions[type]('Hi {0}!', 'John'))
NotifyActions[type]('Hi {0}!', 'John')(store.dispatch)
})
it('adds a notification', () => {
expect(_.values(store.getState().notifications)).toEqual([{
id: jasmine.any(String),
message: 'Hi John!',
type
type,
}])
})
@ -79,7 +86,7 @@ describe('reducers/alerts', () => {
})
it('does not fail when no arguments', () => {
store.dispatch(NotifyActions[type]())
NotifyActions[type]()(store.dispatch)
})
})
@ -89,9 +96,9 @@ describe('reducers/alerts', () => {
describe('clear', () => {
it('clears all alerts', () => {
store.dispatch(NotifyActions.info('Hi {0}!', 'John'))
store.dispatch(NotifyActions.warning('Hi {0}!', 'John'))
store.dispatch(NotifyActions.error('Hi {0}!', 'John'))
NotifyActions.info('Hi {0}!', 'John')(store.dispatch)
NotifyActions.warning('Hi {0}!', 'John')(store.dispatch)
NotifyActions.error('Hi {0}!', 'John')(store.dispatch)
store.dispatch(NotifyActions.clear())
expect(store.getState().notifications).toEqual({})
})

View File

@ -1,14 +1,14 @@
import * as constants from '../constants.js'
import Immutable from 'seamless-immutable'
import * as constants from '../constants'
import { AlertActionType, Alert } from '../actions/NotifyActions'
const defaultState = Immutable([])
export type AlertState = Alert[]
export default function alerts (state = defaultState, action) {
switch (action && action.type) {
const defaultState: AlertState = []
export default function alerts (state = defaultState, action: AlertActionType) {
switch (action.type) {
case constants.ALERT:
const alerts = state.asMutable()
alerts.push(action.payload)
return Immutable(alerts)
return [...state, action.payload]
case constants.ALERT_DISMISS:
return state.filter(a => a !== action.payload)
case constants.ALERT_CLEAR:

View File

@ -1,9 +1,9 @@
import active from './active.js'
import alerts from './alerts.js'
import notifications from './notifications.js'
import messages from './messages.js'
import peers from './peers.js'
import streams from './streams.js'
import active from './active'
import alerts from './alerts'
import notifications from './notifications'
import messages from './messages'
import peers from './peers'
import streams from './streams'
import { combineReducers } from 'redux'
export default combineReducers({
@ -12,5 +12,5 @@ export default combineReducers({
notifications,
messages,
peers,
streams
streams,
})

View File

@ -1,17 +1,16 @@
import * as ChatActions from '../actions/ChatActions.js'
import messages from './messages.js'
import * as ChatActions from '../actions/ChatActions'
import messages from './messages'
describe('reducers/messages', () => {
describe('addMessage', () => {
it('add message to chat', () => {
const payload = {
const payload: ChatActions.Message = {
userId: 'test',
message: 'hello',
timestamp: new Date(),
image: null
timestamp: new Date().toLocaleString(),
}
let state = messages()
let state = messages(undefined, {type: 'test'} as any)
state = messages(state, ChatActions.addMessage(payload))
expect(state).toEqual([payload])
})

View File

@ -1,5 +1,5 @@
import * as constants from '../constants.js'
import { Message, MessageAddAction } from '../actions/ChatActions.js'
import * as constants from '../constants'
import { Message, MessageAddAction } from '../actions/ChatActions'
export type MessagesState = Message[]

View File

@ -1,16 +1,28 @@
import * as constants from '../constants.js'
import Immutable from 'seamless-immutable'
import * as constants from '../constants'
import { Notification, NotificationActionType } from '../actions/NotifyActions'
const defaultState = Immutable({})
export type NotificationState = Record<string, Notification>
export default function notifications (state = defaultState, action) {
switch (action && action.type) {
const defaultState: NotificationState = {}
export default function notifications (
state = defaultState,
action: NotificationActionType,
) {
switch (action.type) {
case constants.NOTIFY:
return state.merge({
[action.payload.id]: action.payload
})
return {
...state,
[action.payload.id]: action.payload,
}
case constants.NOTIFY_DISMISS:
return state.without(action.payload.id)
return Object
.keys(state)
.filter(key => key !== action.payload.id)
.reduce((obj, key) => {
obj[key] = state[key]
return obj
}, {} as NotificationState)
case constants.NOTIFY_CLEAR:
return defaultState
default:

View File

@ -1,4 +1,4 @@
import * as constants from '../constants.js'
import * as constants from '../constants'
import _ from 'underscore'
import Peer from 'simple-peer'
import { PeerAction } from '../actions/PeerActions'

View File

@ -1,10 +1,10 @@
jest.mock('../window.js')
jest.mock('../window')
import * as StreamActions from '../actions/StreamActions.js'
import reducers from './index.js'
import { createObjectURL, MediaStream } from '../window.js'
import * as StreamActions from '../actions/StreamActions'
import reducers from './index'
import { createObjectURL, MediaStream } from '../window'
import { applyMiddleware, createStore, Store } from 'redux'
import { create } from '../middlewares.js'
import { create } from '../middlewares'
describe('reducers/alerts', () => {

View File

@ -1,8 +1,8 @@
import _ from 'underscore'
import { createObjectURL, revokeObjectURL } from '../window.js'
import { createObjectURL, revokeObjectURL } from '../window'
import _debug from 'debug'
import { AddStreamPayload, AddStreamAction, RemoveStreamAction, StreamAction } from '../actions/StreamActions.js'
import { STREAM_ADD, STREAM_REMOVE } from '../constants.js'
import { AddStreamPayload, AddStreamAction, RemoveStreamAction, StreamAction } from '../actions/StreamActions'
import { STREAM_ADD, STREAM_REMOVE } from '../constants'
const debug = _debug('peercalls')
const defaultState = Object.freeze({})

View File

@ -1,3 +1,3 @@
import SocketIOClient from 'socket.io-client'
import { baseUrl } from './window.js'
import { baseUrl } from './window'
export default SocketIOClient('', { path: baseUrl + '/ws' })

View File

@ -1,4 +1,4 @@
import { create } from './middlewares.js'
import { create } from './middlewares'
import reducers from './reducers'
import { applyMiddleware, createStore as _createStore, Store as ReduxStore } from 'redux'
export const middlewares = create(

View File

@ -5,7 +5,7 @@ import {
navigator,
play,
valueOf,
} from './window.js'
} from './window'
describe('window', () => {

10
src/screenfull.d.ts vendored Normal file
View File

@ -0,0 +1,10 @@
declare module 'screenfull' {
interface Screenfull {
enabled: boolean
toggle: () => void
}
declare const screenfull: Screenfull
export = screenfull
}

View File

@ -154,11 +154,11 @@ body.call {
transition: opacity 200ms ease-in;
}
.fade-leave {
.fade-exit {
opacity: 1;
}
.fade-leave.fade-leave-active {
.fade-exit.fade-exit-active {
opacity: 0.01;
transition: opacity 100ms ease-in;
}