Add alerts and notifications about connection status
This commit is contained in:
parent
be14b016de
commit
b8108226cd
@ -22,6 +22,7 @@
|
|||||||
"flux": "^2.1.1",
|
"flux": "^2.1.1",
|
||||||
"jade": "^1.11.0",
|
"jade": "^1.11.0",
|
||||||
"react": "^0.14.8",
|
"react": "^0.14.8",
|
||||||
|
"react-addons-css-transition-group": "^0.14.8",
|
||||||
"react-dom": "^0.14.8",
|
"react-dom": "^0.14.8",
|
||||||
"simple-peer": "^6.0.3",
|
"simple-peer": "^6.0.3",
|
||||||
"socket.io": "^1.3.7",
|
"socket.io": "^1.3.7",
|
||||||
|
|||||||
78
src/js/action/__tests__/notify-test.js
Normal file
78
src/js/action/__tests__/notify-test.js
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
jest.unmock('../notify.js');
|
||||||
|
|
||||||
|
const dispatcher = require('../../dispatcher/dispatcher.js');
|
||||||
|
const notify = require('../notify.js');
|
||||||
|
|
||||||
|
describe('notify', () => {
|
||||||
|
|
||||||
|
beforeEach(() => dispatcher.dispatch.mockClear());
|
||||||
|
|
||||||
|
describe('info', () => {
|
||||||
|
|
||||||
|
it('should dispatch info notification', () => {
|
||||||
|
notify.info('test: {0} {1}', 'arg1', 'arg2');
|
||||||
|
|
||||||
|
expect(dispatcher.dispatch.mock.calls).toEqual([[{
|
||||||
|
type: 'notify',
|
||||||
|
notification: {
|
||||||
|
message: 'test: arg1 arg2',
|
||||||
|
type: 'info'
|
||||||
|
}
|
||||||
|
}]]);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('warn', () => {
|
||||||
|
|
||||||
|
it('should dispatch warning notification', () => {
|
||||||
|
notify.warn('test: {0} {1}', 'arg1', 'arg2');
|
||||||
|
|
||||||
|
expect(dispatcher.dispatch.mock.calls).toEqual([[{
|
||||||
|
type: 'notify',
|
||||||
|
notification: {
|
||||||
|
message: 'test: arg1 arg2',
|
||||||
|
type: 'warning'
|
||||||
|
}
|
||||||
|
}]]);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('error', () => {
|
||||||
|
|
||||||
|
it('should dispatch error notification', () => {
|
||||||
|
notify.error('test: {0} {1}', 'arg1', 'arg2');
|
||||||
|
|
||||||
|
expect(dispatcher.dispatch.mock.calls).toEqual([[{
|
||||||
|
type: 'notify',
|
||||||
|
notification: {
|
||||||
|
message: 'test: arg1 arg2',
|
||||||
|
type: 'error'
|
||||||
|
}
|
||||||
|
}]]);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('alert', () => {
|
||||||
|
|
||||||
|
it('should dispatch an alert', () => {
|
||||||
|
|
||||||
|
notify.alert('alert!', true);
|
||||||
|
|
||||||
|
expect(dispatcher.dispatch.mock.calls).toEqual([[{
|
||||||
|
type: 'alert',
|
||||||
|
alert: {
|
||||||
|
action: 'Dismiss',
|
||||||
|
dismissable: true,
|
||||||
|
message: 'alert!',
|
||||||
|
type: 'warning'
|
||||||
|
}
|
||||||
|
}]]);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
42
src/js/action/notify.js
Normal file
42
src/js/action/notify.js
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
const dispatcher = require('../dispatcher/dispatcher.js');
|
||||||
|
|
||||||
|
function format(string, args) {
|
||||||
|
string = args
|
||||||
|
.reduce((string, arg, i) => string.replace('{' + i + '}', arg), string);
|
||||||
|
return string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _notify(type, args) {
|
||||||
|
let string = args[0] || '';
|
||||||
|
let message = format(string, Array.prototype.slice.call(args, 1));
|
||||||
|
dispatcher.dispatch({
|
||||||
|
type: 'notify',
|
||||||
|
notification: { type, message }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function info() {
|
||||||
|
_notify('info', arguments);
|
||||||
|
}
|
||||||
|
|
||||||
|
function warn() {
|
||||||
|
_notify('warning', arguments);
|
||||||
|
}
|
||||||
|
|
||||||
|
function error() {
|
||||||
|
_notify('error', arguments);
|
||||||
|
}
|
||||||
|
|
||||||
|
function alert(message, dismissable) {
|
||||||
|
dispatcher.dispatch({
|
||||||
|
type: 'alert',
|
||||||
|
alert: {
|
||||||
|
action: dismissable ? 'Dismiss' : '',
|
||||||
|
dismissable: !!dismissable,
|
||||||
|
message,
|
||||||
|
type: 'warning'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { alert, info, warn, error };
|
||||||
@ -9,6 +9,7 @@ function getUserMedia(constraints) {
|
|||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const getMedia = navigator.getUserMedia || navigator.webkitGetUserMedia;
|
const getMedia = navigator.getUserMedia || navigator.webkitGetUserMedia;
|
||||||
|
if (!getMedia) reject(new Error('Browser unsupported'));
|
||||||
getMedia.call(navigator, constraints, resolve, reject);
|
getMedia.call(navigator, constraints, resolve, reject);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
74
src/js/components/__tests__/alert-test.js
Normal file
74
src/js/components/__tests__/alert-test.js
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
jest.unmock('../alert.js');
|
||||||
|
|
||||||
|
const React = require('react');
|
||||||
|
const ReactDOM = require('react-dom');
|
||||||
|
const TestUtils = require('react-addons-test-utils');
|
||||||
|
|
||||||
|
const Alert = require('../alert.js');
|
||||||
|
const dispatcher = require('../../dispatcher/dispatcher.js');
|
||||||
|
const alertStore = require('../../store/alertStore.js');
|
||||||
|
|
||||||
|
describe('alert', () => {
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
alertStore.getAlert.mockClear();
|
||||||
|
});
|
||||||
|
|
||||||
|
function render() {
|
||||||
|
let component = TestUtils.renderIntoDocument(<div><Alert /></div>);
|
||||||
|
return ReactDOM.findDOMNode(component);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('render', () => {
|
||||||
|
|
||||||
|
it('should do nothing when no alert', () => {
|
||||||
|
let node = render();
|
||||||
|
expect(node.querySelector('.alert.hidden')).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render alert', () => {
|
||||||
|
alertStore.getAlert.mockReturnValue({
|
||||||
|
message: 'example',
|
||||||
|
type: 'warning'
|
||||||
|
});
|
||||||
|
|
||||||
|
let node = render();
|
||||||
|
|
||||||
|
expect(node.querySelector('.alert.warning')).toBeTruthy();
|
||||||
|
expect(node.querySelector('.alert span').textContent).toMatch(/example/);
|
||||||
|
expect(node.querySelector('.alert button')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render dismissable alert', () => {
|
||||||
|
alertStore.getAlert.mockReturnValue({
|
||||||
|
message: 'example',
|
||||||
|
type: 'warning',
|
||||||
|
dismissable: true
|
||||||
|
});
|
||||||
|
|
||||||
|
let node = render();
|
||||||
|
|
||||||
|
expect(node.querySelector('.alert.warning')).toBeTruthy();
|
||||||
|
expect(node.querySelector('.alert span').textContent).toMatch(/example/);
|
||||||
|
expect(node.querySelector('.alert button')).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should dispatch dismiss alert on dismiss clicked', () => {
|
||||||
|
let alert = {
|
||||||
|
message: 'example',
|
||||||
|
type: 'warning',
|
||||||
|
dismissable: true
|
||||||
|
};
|
||||||
|
alertStore.getAlert.mockReturnValue(alert);
|
||||||
|
|
||||||
|
let node = render();
|
||||||
|
TestUtils.Simulate.click(node.querySelector('.alert button'));
|
||||||
|
|
||||||
|
expect(dispatcher.dispatch.mock.calls).toEqual([[{
|
||||||
|
type: 'alert-dismiss',
|
||||||
|
alert
|
||||||
|
}]]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
@ -5,6 +5,8 @@ const React = require('react');
|
|||||||
const ReactDOM = require('react-dom');
|
const ReactDOM = require('react-dom');
|
||||||
const TestUtils = require('react-addons-test-utils');
|
const TestUtils = require('react-addons-test-utils');
|
||||||
|
|
||||||
|
require('../alert.js').mockImplementation(() => <div />);
|
||||||
|
require('../notifications.js').mockImplementation(() => <div />);
|
||||||
const App = require('../app.js');
|
const App = require('../app.js');
|
||||||
const activeStore = require('../../store/activeStore.js');
|
const activeStore = require('../../store/activeStore.js');
|
||||||
const dispatcher = require('../../dispatcher/dispatcher.js');
|
const dispatcher = require('../../dispatcher/dispatcher.js');
|
||||||
|
|||||||
59
src/js/components/__tests__/notifications-test.js
Normal file
59
src/js/components/__tests__/notifications-test.js
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
jest.unmock('../notifications.js');
|
||||||
|
|
||||||
|
const React = require('react');
|
||||||
|
const ReactDOM = require('react-dom');
|
||||||
|
const TestUtils = require('react-addons-test-utils');
|
||||||
|
|
||||||
|
const Notifications = require('../notifications.js');
|
||||||
|
const notificationsStore = require('../../store/notificationsStore.js');
|
||||||
|
|
||||||
|
describe('alert', () => {
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
notificationsStore.getNotifications.mockClear();
|
||||||
|
notificationsStore.getNotifications.mockReturnValue([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
function render(component) {
|
||||||
|
let rendered = TestUtils.renderIntoDocument(<div>{component}</div>);
|
||||||
|
return ReactDOM.findDOMNode(rendered);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('render', () => {
|
||||||
|
|
||||||
|
it('should render notifications placeholder', () => {
|
||||||
|
let node = render(<Notifications />);
|
||||||
|
expect(node.querySelector('.notifications')).toBeTruthy();
|
||||||
|
expect(node.querySelector('.notifications .notification')).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render notifications', () => {
|
||||||
|
notificationsStore.getNotifications.mockReturnValue([{
|
||||||
|
_id: 1,
|
||||||
|
message: 'message 1',
|
||||||
|
type: 'warning'
|
||||||
|
}, {
|
||||||
|
_id: 2,
|
||||||
|
message: 'message 2',
|
||||||
|
type: 'error'
|
||||||
|
}]);
|
||||||
|
|
||||||
|
let node = render(<Notifications />);
|
||||||
|
expect(notificationsStore.getNotifications.mock.calls).toEqual([[ 10 ]]);
|
||||||
|
|
||||||
|
let c = node.querySelector('.notifications');
|
||||||
|
expect(c).toBeTruthy();
|
||||||
|
expect(c.querySelectorAll('.notification').length).toBe(2);
|
||||||
|
expect(c.querySelector('.notification.warning').textContent)
|
||||||
|
.toEqual('message 1');
|
||||||
|
expect(c.querySelector('.notification.error').textContent)
|
||||||
|
.toEqual('message 2');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render max X notifications', () => {
|
||||||
|
render(<Notifications max={1} />);
|
||||||
|
expect(notificationsStore.getNotifications.mock.calls).toEqual([[ 1 ]]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
30
src/js/components/alert.js
Normal file
30
src/js/components/alert.js
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
const React = require('react');
|
||||||
|
const alertStore = require('../store/alertStore.js');
|
||||||
|
const dispatcher = require('../dispatcher/dispatcher.js');
|
||||||
|
|
||||||
|
function alert() {
|
||||||
|
let alert = alertStore.getAlert();
|
||||||
|
if (!alert) return <div className="alert hidden"><span> </span></div>;
|
||||||
|
let button;
|
||||||
|
|
||||||
|
function dismiss() {
|
||||||
|
dispatcher.dispatch({
|
||||||
|
type: 'alert-dismiss',
|
||||||
|
alert
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (alert.dismissable) {
|
||||||
|
button = <button onClick={dismiss}>{alert.action}</button>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={alert.type + ' alert'}>
|
||||||
|
<span>{alert.message}</span>
|
||||||
|
{button}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = alert;
|
||||||
@ -1,3 +1,5 @@
|
|||||||
|
const Alert = require('./alert.js');
|
||||||
|
const Notifications = require('./notifications.js');
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
const _ = require('underscore');
|
const _ = require('underscore');
|
||||||
const activeStore = require('../store/activeStore.js');
|
const activeStore = require('../store/activeStore.js');
|
||||||
@ -38,6 +40,8 @@ function app() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (<div className="app">
|
return (<div className="app">
|
||||||
|
<Alert />
|
||||||
|
<Notifications />
|
||||||
<div className="videos">
|
<div className="videos">
|
||||||
{videos}
|
{videos}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
30
src/js/components/notifications.js
Normal file
30
src/js/components/notifications.js
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
const React = require('react');
|
||||||
|
const Transition = require('react-addons-css-transition-group');
|
||||||
|
const notificationsStore = require('../store/notificationsStore.js');
|
||||||
|
|
||||||
|
function notifications(props) {
|
||||||
|
let notifs = notificationsStore.getNotifications(props.max || 10);
|
||||||
|
|
||||||
|
let notificationElements = notifs.map(notif => {
|
||||||
|
return (
|
||||||
|
<div className={notif.type + ' notification'} key={notif._id}>
|
||||||
|
{notif.message}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="notifications">
|
||||||
|
<Transition
|
||||||
|
transitionEnterTimeout={200}
|
||||||
|
transitionLeaveTimeout={100}
|
||||||
|
transitionName="fade"
|
||||||
|
>
|
||||||
|
{notificationElements}
|
||||||
|
</Transition>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = notifications;
|
||||||
@ -8,10 +8,13 @@ const App = require('./components/app.js');
|
|||||||
const React = require('react');
|
const React = require('react');
|
||||||
const ReactDom = require('react-dom');
|
const ReactDom = require('react-dom');
|
||||||
const activeStore = require('./store/activeStore.js');
|
const activeStore = require('./store/activeStore.js');
|
||||||
|
const alertStore = require('./store/alertStore.js');
|
||||||
const debug = require('debug')('peer-calls:index');
|
const debug = require('debug')('peer-calls:index');
|
||||||
const dispatcher = require('./dispatcher/dispatcher.js');
|
const dispatcher = require('./dispatcher/dispatcher.js');
|
||||||
const getUserMedia = require('./browser/getUserMedia.js');
|
const getUserMedia = require('./browser/getUserMedia.js');
|
||||||
const handshake = require('./peer/handshake.js');
|
const handshake = require('./peer/handshake.js');
|
||||||
|
const notificationsStore = require('./store/notificationsStore.js');
|
||||||
|
const notify = require('./action/notify.js');
|
||||||
const socket = require('./socket.js');
|
const socket = require('./socket.js');
|
||||||
const streamStore = require('./store/streamStore.js');
|
const streamStore = require('./store/streamStore.js');
|
||||||
|
|
||||||
@ -36,12 +39,10 @@ dispatcher.register(action => {
|
|||||||
if (action.type === 'play') play();
|
if (action.type === 'play') play();
|
||||||
});
|
});
|
||||||
|
|
||||||
streamStore.addListener(() => () => {
|
|
||||||
debug('streamStore - change');
|
|
||||||
debug(streamStore.getStreams());
|
|
||||||
});
|
|
||||||
streamStore.addListener(render);
|
|
||||||
activeStore.addListener(render);
|
activeStore.addListener(render);
|
||||||
|
alertStore.addListener(render);
|
||||||
|
notificationsStore.addListener(render);
|
||||||
|
streamStore.addListener(render);
|
||||||
|
|
||||||
render();
|
render();
|
||||||
|
|
||||||
@ -54,9 +55,13 @@ getUserMedia({ video: true, audio: false })
|
|||||||
userId: '_me_',
|
userId: '_me_',
|
||||||
stream
|
stream
|
||||||
});
|
});
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
notify.alert('Could not get access to microphone & camera');
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.once('connect', () => {
|
socket.once('connect', () => {
|
||||||
|
notify.warn('Connected to server socket');
|
||||||
debug('socket connected');
|
debug('socket connected');
|
||||||
getUserMedia({ video: true, audio: true })
|
getUserMedia({ video: true, audio: true })
|
||||||
.then(stream => {
|
.then(stream => {
|
||||||
@ -64,6 +69,11 @@ socket.once('connect', () => {
|
|||||||
handshake.init(socket, callId, stream);
|
handshake.init(socket, callId, stream);
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
|
notify.alert('Could not get access to camera!', true);
|
||||||
debug('error getting media: %s %s', err.name, err.message);
|
debug('error getting media: %s %s', err.name, err.message);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
socket.on('disconnect', () => {
|
||||||
|
notify.error('Server socket disconnected');
|
||||||
|
});
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
const Peer = require('./Peer.js');
|
const Peer = require('./Peer.js');
|
||||||
const debug = require('debug')('peer-calls:peer');
|
const debug = require('debug')('peer-calls:peer');
|
||||||
const dispatcher = require('../dispatcher/dispatcher.js');
|
const dispatcher = require('../dispatcher/dispatcher.js');
|
||||||
|
const notify = require('../action/notify.js');
|
||||||
const _ = require('underscore');
|
const _ = require('underscore');
|
||||||
|
|
||||||
function init(socket, roomName, stream) {
|
function init(socket, roomName, stream) {
|
||||||
@ -9,6 +10,7 @@ function init(socket, roomName, stream) {
|
|||||||
|
|
||||||
function createPeer(user, initiator) {
|
function createPeer(user, initiator) {
|
||||||
debug('create peer: %s', user.id);
|
debug('create peer: %s', user.id);
|
||||||
|
notify.warn('Initializing new peer connection');
|
||||||
|
|
||||||
let peer = peers[user.id] = Peer.init({
|
let peer = peers[user.id] = Peer.init({
|
||||||
initiator: '/#' + socket.id === initiator,
|
initiator: '/#' + socket.id === initiator,
|
||||||
@ -27,6 +29,7 @@ function init(socket, roomName, stream) {
|
|||||||
|
|
||||||
peer.once('error', err => {
|
peer.once('error', err => {
|
||||||
debug('peer: %s, error %s', user.id, err.stack);
|
debug('peer: %s, error %s', user.id, err.stack);
|
||||||
|
notify.error('A peer connection error occurred');
|
||||||
destroyPeer(user.id);
|
destroyPeer(user.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -39,6 +42,7 @@ function init(socket, roomName, stream) {
|
|||||||
|
|
||||||
peer.once('connect', () => {
|
peer.once('connect', () => {
|
||||||
debug('peer: %s, connect', user.id);
|
debug('peer: %s, connect', user.id);
|
||||||
|
notify.warn('Peer connection established');
|
||||||
dispatcher.dispatch({ type: 'play' });
|
dispatcher.dispatch({ type: 'play' });
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -53,6 +57,7 @@ function init(socket, roomName, stream) {
|
|||||||
|
|
||||||
peer.once('close', () => {
|
peer.once('close', () => {
|
||||||
debug('peer: %s, close', user.id);
|
debug('peer: %s, close', user.id);
|
||||||
|
notify.error('Peer connection closed');
|
||||||
dispatcher.dispatch({
|
dispatcher.dispatch({
|
||||||
type: 'remove-stream',
|
type: 'remove-stream',
|
||||||
userId: user.id
|
userId: user.id
|
||||||
@ -82,6 +87,7 @@ function init(socket, roomName, stream) {
|
|||||||
socket.on('users', payload => {
|
socket.on('users', payload => {
|
||||||
let { initiator, users } = payload;
|
let { initiator, users } = payload;
|
||||||
debug('socket users: %o', users);
|
debug('socket users: %o', users);
|
||||||
|
notify.info('Connected users: {0}', users.length);
|
||||||
|
|
||||||
users
|
users
|
||||||
.filter(user => !peers[user.id] && user.id !== '/#' + socket.id)
|
.filter(user => !peers[user.id] && user.id !== '/#' + socket.id)
|
||||||
@ -96,6 +102,7 @@ function init(socket, roomName, stream) {
|
|||||||
|
|
||||||
debug('socket.id: %s', socket.id);
|
debug('socket.id: %s', socket.id);
|
||||||
debug('emit ready for room: %s', roomName);
|
debug('emit ready for room: %s', roomName);
|
||||||
|
notify.info('Ready for connections');
|
||||||
socket.emit('ready', roomName);
|
socket.emit('ready', roomName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
70
src/js/store/__tests__/alertStore-test.js
Normal file
70
src/js/store/__tests__/alertStore-test.js
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
jest.unmock('../alertStore.js');
|
||||||
|
|
||||||
|
const dispatcher = require('../../dispatcher/dispatcher.js');
|
||||||
|
const alertStore = require('../alertStore.js');
|
||||||
|
|
||||||
|
describe('alertStore', () => {
|
||||||
|
|
||||||
|
let handleAction, onChange;
|
||||||
|
beforeEach(() => {
|
||||||
|
handleAction = dispatcher.register.mock.calls[0][0];
|
||||||
|
handleAction({ type: 'alert-clear' });
|
||||||
|
|
||||||
|
onChange = jest.genMockFunction();
|
||||||
|
alertStore.addListener(onChange);
|
||||||
|
});
|
||||||
|
afterEach(() => {
|
||||||
|
alertStore.removeListener(onChange);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('alert', () => {
|
||||||
|
|
||||||
|
it('should add alerts to end of queue and dispatch change', () => {
|
||||||
|
let alert1 = { message: 'example alert 1' };
|
||||||
|
let alert2 = { message: 'example alert 2' };
|
||||||
|
|
||||||
|
handleAction({ type: 'alert', alert: alert1 });
|
||||||
|
handleAction({ type: 'alert', alert: alert2 });
|
||||||
|
|
||||||
|
expect(onChange.mock.calls.length).toBe(2);
|
||||||
|
expect(alertStore.getAlerts()).toEqual([ alert1, alert2 ]);
|
||||||
|
expect(alertStore.getAlert()).toBe(alert1);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('alert-dismiss', () => {
|
||||||
|
|
||||||
|
it('should remove alert and dispatch change', () => {
|
||||||
|
let alert1 = { message: 'example alert 1' };
|
||||||
|
let alert2 = { message: 'example alert 2' };
|
||||||
|
|
||||||
|
handleAction({ type: 'alert', alert: alert1 });
|
||||||
|
handleAction({ type: 'alert', alert: alert2 });
|
||||||
|
handleAction({ type: 'alert-dismiss', alert: alert1 });
|
||||||
|
|
||||||
|
expect(onChange.mock.calls.length).toBe(3);
|
||||||
|
expect(alertStore.getAlerts()).toEqual([ alert2 ]);
|
||||||
|
expect(alertStore.getAlert()).toBe(alert2);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('alert-clear', () => {
|
||||||
|
|
||||||
|
it('should remove all alerts', () => {
|
||||||
|
let alert1 = { message: 'example alert 1' };
|
||||||
|
let alert2 = { message: 'example alert 2' };
|
||||||
|
|
||||||
|
handleAction({ type: 'alert', alert: alert1 });
|
||||||
|
handleAction({ type: 'alert', alert: alert2 });
|
||||||
|
handleAction({ type: 'alert-clear' });
|
||||||
|
|
||||||
|
expect(onChange.mock.calls.length).toBe(3);
|
||||||
|
expect(alertStore.getAlerts()).toEqual([]);
|
||||||
|
expect(alertStore.getAlert()).not.toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
67
src/js/store/__tests__/notificationsStore-test.js
Normal file
67
src/js/store/__tests__/notificationsStore-test.js
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
jest.unmock('../notificationsStore.js');
|
||||||
|
|
||||||
|
const dispatcher = require('../../dispatcher/dispatcher.js');
|
||||||
|
const store = require('../notificationsStore.js');
|
||||||
|
|
||||||
|
describe('store', () => {
|
||||||
|
|
||||||
|
let handleAction, onChange;
|
||||||
|
beforeEach(() => {
|
||||||
|
dispatcher.dispatch.mockClear();
|
||||||
|
handleAction = dispatcher.register.mock.calls[0][0];
|
||||||
|
|
||||||
|
handleAction({ type: 'notify-clear' });
|
||||||
|
|
||||||
|
onChange = jest.genMockFunction();
|
||||||
|
store.addListener(onChange);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('notify', () => {
|
||||||
|
|
||||||
|
it('should add notification and dispatch change', () => {
|
||||||
|
let notif1 = { message: 'example notif 1' };
|
||||||
|
let notif2 = { message: 'example notif 2' };
|
||||||
|
|
||||||
|
handleAction({ type: 'notify', notification: notif1 });
|
||||||
|
handleAction({ type: 'notify', notification: notif2 });
|
||||||
|
|
||||||
|
expect(onChange.mock.calls.length).toBe(2);
|
||||||
|
expect(store.getNotifications()).toEqual([ notif1, notif2 ]);
|
||||||
|
expect(store.getNotifications(1)).toEqual([ notif2 ]);
|
||||||
|
expect(store.getNotifications(3)).toEqual([ notif1, notif2 ]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add timeout for autoremoval', () => {
|
||||||
|
let notif1 = { message: 'example notif 1' };
|
||||||
|
|
||||||
|
handleAction({ type: 'notify', notification: notif1 });
|
||||||
|
|
||||||
|
expect(onChange.mock.calls.length).toBe(1);
|
||||||
|
expect(store.getNotifications()).toEqual([ notif1 ]);
|
||||||
|
|
||||||
|
jest.runAllTimers();
|
||||||
|
|
||||||
|
expect(onChange.mock.calls.length).toBe(2);
|
||||||
|
expect(store.getNotifications()).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('notify-dismiss', () => {
|
||||||
|
|
||||||
|
it('should remove notif and dispatch change', () => {
|
||||||
|
let notif1 = { message: 'example notif 1' };
|
||||||
|
let notif2 = { message: 'example notif 2' };
|
||||||
|
|
||||||
|
handleAction({ type: 'notify', notification: notif1 });
|
||||||
|
handleAction({ type: 'notify', notification: notif2 });
|
||||||
|
handleAction({ type: 'notify-dismiss', notification: notif1 });
|
||||||
|
|
||||||
|
expect(onChange.mock.calls.length).toBe(3);
|
||||||
|
expect(store.getNotifications()).toEqual([ notif2 ]);
|
||||||
|
expect(store.getNotifications(2)).toEqual([ notif2 ]);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
50
src/js/store/alertStore.js
Normal file
50
src/js/store/alertStore.js
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
const EventEmitter = require('events');
|
||||||
|
const debug = require('debug')('peer-calls:alertStore');
|
||||||
|
const dispatcher = require('../dispatcher/dispatcher.js');
|
||||||
|
|
||||||
|
const emitter = new EventEmitter();
|
||||||
|
const addListener = cb => emitter.on('change', cb);
|
||||||
|
const removeListener = cb => emitter.removeListener('change', cb);
|
||||||
|
|
||||||
|
let alerts = [];
|
||||||
|
|
||||||
|
const handlers = {
|
||||||
|
alert: ({ alert }) => {
|
||||||
|
debug('alert: %s', alert.message);
|
||||||
|
alerts.push(alert);
|
||||||
|
},
|
||||||
|
'alert-dismiss': ({ alert }) => {
|
||||||
|
debug('alert-dismiss: %s', alert.message);
|
||||||
|
let index = alerts.indexOf(alert);
|
||||||
|
debug('index: %s', index);
|
||||||
|
if (index < 0) return;
|
||||||
|
alerts.splice(index, 1);
|
||||||
|
},
|
||||||
|
'alert-clear': () => {
|
||||||
|
debug('alert-clear');
|
||||||
|
alerts = [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const dispatcherIndex = dispatcher.register(action => {
|
||||||
|
let handle = handlers[action.type];
|
||||||
|
if (!handle) return;
|
||||||
|
handle(action);
|
||||||
|
emitter.emit('change');
|
||||||
|
});
|
||||||
|
|
||||||
|
function getAlert() {
|
||||||
|
return alerts[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAlerts() {
|
||||||
|
return alerts;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
dispatcherIndex,
|
||||||
|
addListener,
|
||||||
|
removeListener,
|
||||||
|
getAlert,
|
||||||
|
getAlerts,
|
||||||
|
};
|
||||||
65
src/js/store/notificationsStore.js
Normal file
65
src/js/store/notificationsStore.js
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
const EventEmitter = require('events');
|
||||||
|
const debug = require('debug')('peer-calls:alertStore');
|
||||||
|
const dispatcher = require('../dispatcher/dispatcher.js');
|
||||||
|
|
||||||
|
const emitter = new EventEmitter();
|
||||||
|
const addListener = cb => emitter.on('change', cb);
|
||||||
|
const removeListener = cb => emitter.removeListener('change', cb);
|
||||||
|
|
||||||
|
let index = 0;
|
||||||
|
let notifications = [];
|
||||||
|
|
||||||
|
function dismiss(notification) {
|
||||||
|
let index = notifications.indexOf(notification);
|
||||||
|
if (index < 0) return;
|
||||||
|
notifications.splice(index, 1);
|
||||||
|
clearTimeout(notification._timeout);
|
||||||
|
delete notification._timeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
function emitChange() {
|
||||||
|
emitter.emit('change');
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlers = {
|
||||||
|
notify: ({ notification }) => {
|
||||||
|
index++;
|
||||||
|
debug('notify', notification.message);
|
||||||
|
notification._id = index;
|
||||||
|
notifications.push(notification);
|
||||||
|
notification._timeout = setTimeout(() => {
|
||||||
|
debug('notify-dismiss timeout: %s', notification.message);
|
||||||
|
dismiss(notification);
|
||||||
|
emitChange();
|
||||||
|
}, 10000);
|
||||||
|
},
|
||||||
|
'notify-dismiss': ({ notification }) => {
|
||||||
|
debug('notify-dismiss: %s', notification.message);
|
||||||
|
dismiss(notification);
|
||||||
|
},
|
||||||
|
'notify-clear': () => {
|
||||||
|
debug('notify-clear');
|
||||||
|
notifications = [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const dispatcherIndex = dispatcher.register(action => {
|
||||||
|
let handle = handlers[action.type];
|
||||||
|
if (!handle) return;
|
||||||
|
handle(action);
|
||||||
|
emitChange();
|
||||||
|
});
|
||||||
|
|
||||||
|
function getNotifications(max) {
|
||||||
|
if (!max) max = notifications.length;
|
||||||
|
let start = notifications.length - max;
|
||||||
|
if (start < 0) start = 0;
|
||||||
|
return notifications.slice(start, notifications.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
dispatcherIndex,
|
||||||
|
addListener,
|
||||||
|
removeListener,
|
||||||
|
getNotifications
|
||||||
|
};
|
||||||
@ -1,5 +1,8 @@
|
|||||||
@import "fonts.less";
|
@import "fonts.less";
|
||||||
|
|
||||||
|
@font-sans-serif: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||||
|
@font-monospace: Menlo, Monaco, Consolas, "Courier New", monospace;
|
||||||
|
|
||||||
@color-bg: #086788;
|
@color-bg: #086788;
|
||||||
@color-fg: #07A0C3;
|
@color-fg: #07A0C3;
|
||||||
// @color-btn: #F0C808;
|
// @color-btn: #F0C808;
|
||||||
@ -7,6 +10,10 @@
|
|||||||
@color-active: #F0C808;
|
@color-active: #F0C808;
|
||||||
@icon-size: 48px;
|
@icon-size: 48px;
|
||||||
|
|
||||||
|
@color-info: white;
|
||||||
|
@color-warning: @color-active;
|
||||||
|
@color-error: #FF4400;
|
||||||
|
|
||||||
* {
|
* {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
@ -19,13 +26,16 @@ html, body {
|
|||||||
|
|
||||||
body {
|
body {
|
||||||
background-color: @color-bg;
|
background-color: @color-bg;
|
||||||
|
color: @color-fg;
|
||||||
|
margin: 0 0;
|
||||||
|
font-family: @font-sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.call {
|
||||||
background-image: url('/res/peer-calls.svg');
|
background-image: url('/res/peer-calls.svg');
|
||||||
background-size: 200px;
|
background-size: 200px;
|
||||||
background-position: 50% 50%;
|
background-position: 50% 50%;
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
color: @color-fg;
|
|
||||||
margin: 0 0;
|
|
||||||
font-family: sans-serif;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#form {
|
#form {
|
||||||
@ -87,8 +97,80 @@ body {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.warning {
|
||||||
|
color: @color-warning;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
color: @color-error;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info {
|
||||||
|
color: @color-info;
|
||||||
|
}
|
||||||
|
|
||||||
.app {
|
.app {
|
||||||
|
|
||||||
|
.alert {
|
||||||
|
background-color: #000;
|
||||||
|
background-color: rgba(0, 0, 0, 0.3);
|
||||||
|
left: 0;
|
||||||
|
opacity: 1;
|
||||||
|
position: fixed;
|
||||||
|
right: 0;
|
||||||
|
text-align: center;
|
||||||
|
top: 0;
|
||||||
|
transition: visibility 100ms ease-in, opacity 100ms ease-in;
|
||||||
|
z-index: 4;
|
||||||
|
|
||||||
|
span {
|
||||||
|
display: inline-block;
|
||||||
|
margin: 1rem 0;
|
||||||
|
padding: 0 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
line-height: 1.4rem;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0.3rem;
|
||||||
|
color: @color-info;
|
||||||
|
background-color: @color-fg;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert.hidden {
|
||||||
|
opacity: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notifications {
|
||||||
|
font-family: @font-monospace;
|
||||||
|
font-size: 10px;
|
||||||
|
left: 1rem;
|
||||||
|
position: fixed;
|
||||||
|
right: 1rem;
|
||||||
|
text-align: right;
|
||||||
|
top: 1rem;
|
||||||
|
z-index: 3;
|
||||||
|
|
||||||
|
.notification {
|
||||||
|
color: @color-info;
|
||||||
|
text-shadow: 0 0 0.1rem @color-info;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification.error {
|
||||||
|
color: @color-error;
|
||||||
|
text-shadow: 0 0 0.1rem @color-error;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification.warning {
|
||||||
|
color: @color-warning;
|
||||||
|
text-shadow: 0 0 0.1rem @color-warning;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
.videos {
|
.videos {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
height: 100px;
|
height: 100px;
|
||||||
@ -134,3 +216,22 @@ body {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.fade-enter {
|
||||||
|
opacity: 0.01;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-enter.fade-enter-active {
|
||||||
|
opacity: 1;
|
||||||
|
transition: opacity 200ms ease-in;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-leave {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-leave.fade-leave-active {
|
||||||
|
opacity: 0.01;
|
||||||
|
transition: opacity 100ms ease-in;
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@ -10,7 +10,7 @@ html
|
|||||||
link(rel="icon" sizes="256x256" href="../res/icon.png")
|
link(rel="icon" sizes="256x256" href="../res/icon.png")
|
||||||
link(rel="stylesheet" type="text/css" href="../less/main.css")
|
link(rel="stylesheet" type="text/css" href="../less/main.css")
|
||||||
|
|
||||||
body
|
body.call
|
||||||
input#callId(type="hidden" value="#{callId}")
|
input#callId(type="hidden" value="#{callId}")
|
||||||
|
|
||||||
div#container
|
div#container
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user