From 4fa6a0d17a04bfea26df87e68b4a5f874d1c5645 Mon Sep 17 00:00:00 2001 From: Jerko Steiner Date: Wed, 13 Nov 2019 18:36:31 -0300 Subject: [PATCH] Refactor all components --- package-lock.json | 63 +++++---- package.json | 7 +- src/client/__mocks__/store.ts | 2 +- src/client/__mocks__/window.ts | 2 - src/client/actions/CallActions.test.ts | 72 ++++++----- src/client/actions/CallActions.ts | 14 +- src/client/actions/ChatActions.ts | 2 +- src/client/actions/NotifyActions.ts | 4 +- src/client/actions/PeerActions.test.ts | 8 +- src/client/actions/PeerActions.ts | 10 +- src/client/actions/SocketActions.test.ts | 8 +- src/client/actions/SocketActions.ts | 6 +- src/client/actions/StreamActions.ts | 8 +- .../components/{Alerts.ts => Alerts.tsx} | 28 ++-- src/client/components/{App.ts => App.tsx} | 68 +++++----- src/client/components/{Chat.ts => Chat.tsx} | 51 +++----- .../{Input.test.ts => Input.test.tsx} | 43 ++++--- src/client/components/{Input.ts => Input.tsx} | 41 +++--- src/client/components/Notifications.ts | 41 ------ src/client/components/Notifications.tsx | 41 ++++++ .../{Toolbar.test.ts => Toolbar.test.tsx} | 80 +++++++----- .../components/{Toolbar.ts => Toolbar.tsx} | 92 +++++++++----- src/client/components/Video.test.ts | 90 ------------- src/client/components/Video.test.tsx | 120 ++++++++++++++++++ src/client/components/Video.ts | 69 ---------- src/client/components/Video.tsx | 66 ++++++++++ .../containers/{App.test.ts => App.test.tsx} | 16 +-- src/client/containers/{App.ts => App.tsx} | 21 +-- src/client/debug/index.ts | 59 +++++---- src/client/index.tsx | 4 +- src/client/middlewares.test.ts | 2 +- src/client/reducers/active.test.ts | 8 +- src/client/reducers/active.ts | 10 +- src/client/reducers/alerts.test.ts | 49 ++++--- src/client/reducers/alerts.ts | 16 +-- src/client/reducers/index.ts | 14 +- src/client/reducers/messages.test.ts | 11 +- src/client/reducers/messages.ts | 4 +- src/client/reducers/notifications.ts | 30 +++-- src/client/reducers/peers.ts | 2 +- src/client/reducers/streams.test.ts | 10 +- src/client/reducers/streams.ts | 6 +- src/client/socket.ts | 2 +- src/client/store.ts | 2 +- src/client/window.test.ts | 2 +- src/screenfull.d.ts | 10 ++ src/scss/style.scss | 4 +- 47 files changed, 718 insertions(+), 600 deletions(-) rename src/client/components/{Alerts.ts => Alerts.tsx} (58%) rename src/client/components/{App.ts => App.tsx} (60%) rename src/client/components/{Chat.ts => Chat.tsx} (74%) rename src/client/components/{Input.test.ts => Input.test.tsx} (67%) rename src/client/components/{Input.ts => Input.tsx} (73%) delete mode 100644 src/client/components/Notifications.ts create mode 100644 src/client/components/Notifications.tsx rename src/client/components/{Toolbar.test.ts => Toolbar.test.tsx} (55%) rename src/client/components/{Toolbar.ts => Toolbar.tsx} (57%) delete mode 100644 src/client/components/Video.test.ts create mode 100644 src/client/components/Video.test.tsx delete mode 100644 src/client/components/Video.ts create mode 100644 src/client/components/Video.tsx rename src/client/containers/{App.test.ts => App.test.tsx} (88%) rename src/client/containers/{App.ts => App.tsx} (57%) create mode 100644 src/screenfull.d.ts diff --git a/package-lock.json b/package-lock.json index fb0fe95..86fa0de 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index d3de070..addc991 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/client/__mocks__/store.ts b/src/client/__mocks__/store.ts index 4bf55af..62809d3 100644 --- a/src/client/__mocks__/store.ts +++ b/src/client/__mocks__/store.ts @@ -1,3 +1,3 @@ import configureStore from 'redux-mock-store' -import { middlewares } from '../middlewares.js' +import { middlewares } from '../middlewares' export default configureStore(middlewares)({}) diff --git a/src/client/__mocks__/window.ts b/src/client/__mocks__/window.ts index 342d8cf..6904fac 100644 --- a/src/client/__mocks__/window.ts +++ b/src/client/__mocks__/window.ts @@ -1,5 +1,3 @@ -import Promise from 'bluebird' - export const createObjectURL = jest.fn() .mockImplementation(object => 'blob://' + String(object)) export const revokeObjectURL = jest.fn() diff --git a/src/client/actions/CallActions.test.ts b/src/client/actions/CallActions.test.ts index bc3f6d6..9fcba4a 100644 --- a/src/client/actions/CallActions.test.ts +++ b/src/client/actions/CallActions.test.ts @@ -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 }) }) diff --git a/src/client/actions/CallActions.ts b/src/client/actions/CallActions.ts index fdafb9b..5279ebc 100644 --- a/src/client/actions/CallActions.ts +++ b/src/client/actions/CallActions.ts @@ -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' diff --git a/src/client/actions/ChatActions.ts b/src/client/actions/ChatActions.ts index 1e5b54a..0ea8351 100644 --- a/src/client/actions/ChatActions.ts +++ b/src/client/actions/ChatActions.ts @@ -1,4 +1,4 @@ -import { MESSAGE_ADD } from '../constants.js' +import { MESSAGE_ADD } from '../constants' export interface MessageAddAction { type: 'MESSAGE_ADD' diff --git a/src/client/actions/NotifyActions.ts b/src/client/actions/NotifyActions.ts index f411c4f..7ce0dad 100644 --- a/src/client/actions/NotifyActions.ts +++ b/src/client/actions/NotifyActions.ts @@ -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' diff --git a/src/client/actions/PeerActions.test.ts b/src/client/actions/PeerActions.test.ts index abc1caf..b475acc 100644 --- a/src/client/actions/PeerActions.test.ts +++ b/src/client/actions/PeerActions.test.ts @@ -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', () => { diff --git a/src/client/actions/PeerActions.ts b/src/client/actions/PeerActions.ts index 71906e0..9879b54 100644 --- a/src/client/actions/PeerActions.ts +++ b/src/client/actions/PeerActions.ts @@ -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') diff --git a/src/client/actions/SocketActions.test.ts b/src/client/actions/SocketActions.test.ts index 491d00c..855d700 100644 --- a/src/client/actions/SocketActions.test.ts +++ b/src/client/actions/SocketActions.test.ts @@ -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', () => { diff --git a/src/client/actions/SocketActions.ts b/src/client/actions/SocketActions.ts index 3bcbda2..96c8ef9 100644 --- a/src/client/actions/SocketActions.ts +++ b/src/client/actions/SocketActions.ts @@ -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' diff --git a/src/client/actions/StreamActions.ts b/src/client/actions/StreamActions.ts index 2320f95..51eb4dd 100644 --- a/src/client/actions/StreamActions.ts +++ b/src/client/actions/StreamActions.ts @@ -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 diff --git a/src/client/components/Alerts.ts b/src/client/components/Alerts.tsx similarity index 58% rename from src/client/components/Alerts.ts rename to src/client/components/Alerts.tsx index bfb95dc..168b207 100644 --- a/src/client/components/Alerts.ts +++ b/src/client/components/Alerts.tsx @@ -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 { 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 { render () { const { alerts, dismiss } = this.props return ( diff --git a/src/client/components/App.ts b/src/client/components/App.tsx similarity index 60% rename from src/client/components/App.ts rename to src/client/components/App.tsx index 79ce691..a80e7d6 100644 --- a/src/client/components/App.ts +++ b/src/client/components/App.tsx @@ -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 + messages: Message[] + peers: Record + sendMessage: (message: TextMessage) => void + streams: Record + onSendFile: (file: File) => void + toggleActive: (userId: string) => void +} + +export interface AppState { + videos: Record + chatVisible: boolean +} + +export default class App extends React.PureComponent { + 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} />
diff --git a/src/client/components/Chat.ts b/src/client/components/Chat.tsx similarity index 74% rename from src/client/components/Chat.ts rename to src/client/components/Chat.tsx index 3fdaf91..ab368d8 100644 --- a/src/client/components/Chat.ts +++ b/src/client/components/Chat.tsx @@ -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 (

@@ -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 { + chatHistoryRef = React.createRef() + 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 (

@@ -111,10 +103,7 @@ export default class Chat extends React.PureComponent {
- +
) } diff --git a/src/client/components/Input.test.ts b/src/client/components/Input.test.tsx similarity index 67% rename from src/client/components/Input.test.ts rename to src/client/components/Input.test.tsx index b0b9e40..ccf10df 100644 --- a/src/client/components/Input.test.ts +++ b/src/client/components/Input.test.tsx @@ -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( - - ) - node = ReactDOM.findDOMNode(component) + const div = document.createElement('div') + await new Promise(resolve => { + ReactDOM.render( + 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😑') }) diff --git a/src/client/components/Input.ts b/src/client/components/Input.tsx similarity index 73% rename from src/client/components/Input.ts rename to src/client/components/Input.tsx index cfa8ec1..afbda6c 100644 --- a/src/client/components/Input.ts +++ b/src/client/components/Input.tsx @@ -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 { + textArea = React.createRef() + state = { + message: '', } - constructor () { - super() - this.state = { - message: '' - } - } - handleChange = e => { + handleChange: ChangeEventHandler = event => { this.setState({ - message: e.target.value + message: event.target.value, }) } - handleSubmit = e => { + handleSubmit: ReactEventHandler = e => { e.preventDefault() this.submit() } - handleKeyPress = e => { + handleKeyPress: KeyboardEventHandler = e => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault() this.submit() } } - handleSmileClick = e => { + handleSmileClick: MouseEventHandler = 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} />
- - {Object.keys(notifications).slice(-max).map(id => ( -
- {notifications[id].message} -
- ))} -
-
- ) - } -} diff --git a/src/client/components/Notifications.tsx b/src/client/components/Notifications.tsx new file mode 100644 index 0000000..2d72f31 --- /dev/null +++ b/src/client/components/Notifications.tsx @@ -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 + max: number +} + +const transitionTimeout = { + enter: 200, + exit: 100, +} + +export default class Notifications +extends React.PureComponent { + static defaultProps = { + max: 10, + } + render () { + const { notifications, max } = this.props + return ( +
+ + {Object.keys(notifications).slice(-max).map(id => ( +
+ {notifications[id].message} +
+ ))} +
+
+ ) + } +} diff --git a/src/client/components/Toolbar.test.ts b/src/client/components/Toolbar.test.tsx similarity index 55% rename from src/client/components/Toolbar.test.ts rename to src/client/components/Toolbar.test.tsx index a01eb85..6a297e9 100644 --- a/src/client/components/Toolbar.test.ts +++ b/src/client/components/Toolbar.test.tsx @@ -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 { + state = { + stream: null, } render () { return { } } - 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( - - ) - node = ReactDOM.findDOMNode(component) + const div = document.createElement('div') + await new Promise(resolve => { + ReactDOM.render( + 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] ]]) }) diff --git a/src/client/components/Toolbar.ts b/src/client/components/Toolbar.tsx similarity index 57% rename from src/client/components/Toolbar.ts rename to src/client/components/Toolbar.tsx index 16dfe76..3cb1019 100644 --- a/src/client/components/Toolbar.ts +++ b/src/client/components/Toolbar.tsx @@ -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 { + file = React.createRef() + + 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 = 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 {
this.state.readMessages} @@ -93,17 +115,20 @@ export default class Toolbar extends React.PureComponent { {stream && (
-
{ this.mixButton = node }} - className="button mute-audio" +
{ this.camButton = node }} - className="button mute-video" + className={classnames('button mute-video', { + on: this.state.camDisabled, + })} title="Mute video" > @@ -113,8 +138,9 @@ export default class Toolbar extends React.PureComponent { )}
{ this.fullscreenButton = node }} - className="button fullscreen" + className={classnames('button fullscreen', { + on: this.state.fullScreenEnabled, + })} title="Enter fullscreen" > diff --git a/src/client/components/Video.test.ts b/src/client/components/Video.test.ts deleted file mode 100644 index 642f1b5..0000000 --- a/src/client/components/Video.test.ts +++ /dev/null @@ -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