Add alerts and notifications about connection status
This commit is contained in:
parent
be14b016de
commit
b8108226cd
@ -22,6 +22,7 @@
|
||||
"flux": "^2.1.1",
|
||||
"jade": "^1.11.0",
|
||||
"react": "^0.14.8",
|
||||
"react-addons-css-transition-group": "^0.14.8",
|
||||
"react-dom": "^0.14.8",
|
||||
"simple-peer": "^6.0.3",
|
||||
"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) => {
|
||||
const getMedia = navigator.getUserMedia || navigator.webkitGetUserMedia;
|
||||
if (!getMedia) reject(new Error('Browser unsupported'));
|
||||
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 TestUtils = require('react-addons-test-utils');
|
||||
|
||||
require('../alert.js').mockImplementation(() => <div />);
|
||||
require('../notifications.js').mockImplementation(() => <div />);
|
||||
const App = require('../app.js');
|
||||
const activeStore = require('../../store/activeStore.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 _ = require('underscore');
|
||||
const activeStore = require('../store/activeStore.js');
|
||||
@ -38,6 +40,8 @@ function app() {
|
||||
});
|
||||
|
||||
return (<div className="app">
|
||||
<Alert />
|
||||
<Notifications />
|
||||
<div className="videos">
|
||||
{videos}
|
||||
</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 ReactDom = require('react-dom');
|
||||
const activeStore = require('./store/activeStore.js');
|
||||
const alertStore = require('./store/alertStore.js');
|
||||
const debug = require('debug')('peer-calls:index');
|
||||
const dispatcher = require('./dispatcher/dispatcher.js');
|
||||
const getUserMedia = require('./browser/getUserMedia.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 streamStore = require('./store/streamStore.js');
|
||||
|
||||
@ -36,12 +39,10 @@ dispatcher.register(action => {
|
||||
if (action.type === 'play') play();
|
||||
});
|
||||
|
||||
streamStore.addListener(() => () => {
|
||||
debug('streamStore - change');
|
||||
debug(streamStore.getStreams());
|
||||
});
|
||||
streamStore.addListener(render);
|
||||
activeStore.addListener(render);
|
||||
alertStore.addListener(render);
|
||||
notificationsStore.addListener(render);
|
||||
streamStore.addListener(render);
|
||||
|
||||
render();
|
||||
|
||||
@ -54,9 +55,13 @@ getUserMedia({ video: true, audio: false })
|
||||
userId: '_me_',
|
||||
stream
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
notify.alert('Could not get access to microphone & camera');
|
||||
});
|
||||
|
||||
socket.once('connect', () => {
|
||||
notify.warn('Connected to server socket');
|
||||
debug('socket connected');
|
||||
getUserMedia({ video: true, audio: true })
|
||||
.then(stream => {
|
||||
@ -64,6 +69,11 @@ socket.once('connect', () => {
|
||||
handshake.init(socket, callId, stream);
|
||||
})
|
||||
.catch(err => {
|
||||
notify.alert('Could not get access to camera!', true);
|
||||
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 debug = require('debug')('peer-calls:peer');
|
||||
const dispatcher = require('../dispatcher/dispatcher.js');
|
||||
const notify = require('../action/notify.js');
|
||||
const _ = require('underscore');
|
||||
|
||||
function init(socket, roomName, stream) {
|
||||
@ -9,6 +10,7 @@ function init(socket, roomName, stream) {
|
||||
|
||||
function createPeer(user, initiator) {
|
||||
debug('create peer: %s', user.id);
|
||||
notify.warn('Initializing new peer connection');
|
||||
|
||||
let peer = peers[user.id] = Peer.init({
|
||||
initiator: '/#' + socket.id === initiator,
|
||||
@ -27,6 +29,7 @@ function init(socket, roomName, stream) {
|
||||
|
||||
peer.once('error', err => {
|
||||
debug('peer: %s, error %s', user.id, err.stack);
|
||||
notify.error('A peer connection error occurred');
|
||||
destroyPeer(user.id);
|
||||
});
|
||||
|
||||
@ -39,6 +42,7 @@ function init(socket, roomName, stream) {
|
||||
|
||||
peer.once('connect', () => {
|
||||
debug('peer: %s, connect', user.id);
|
||||
notify.warn('Peer connection established');
|
||||
dispatcher.dispatch({ type: 'play' });
|
||||
});
|
||||
|
||||
@ -53,6 +57,7 @@ function init(socket, roomName, stream) {
|
||||
|
||||
peer.once('close', () => {
|
||||
debug('peer: %s, close', user.id);
|
||||
notify.error('Peer connection closed');
|
||||
dispatcher.dispatch({
|
||||
type: 'remove-stream',
|
||||
userId: user.id
|
||||
@ -82,6 +87,7 @@ function init(socket, roomName, stream) {
|
||||
socket.on('users', payload => {
|
||||
let { initiator, users } = payload;
|
||||
debug('socket users: %o', users);
|
||||
notify.info('Connected users: {0}', users.length);
|
||||
|
||||
users
|
||||
.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('emit ready for room: %s', roomName);
|
||||
notify.info('Ready for connections');
|
||||
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";
|
||||
|
||||
@font-sans-serif: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
@font-monospace: Menlo, Monaco, Consolas, "Courier New", monospace;
|
||||
|
||||
@color-bg: #086788;
|
||||
@color-fg: #07A0C3;
|
||||
// @color-btn: #F0C808;
|
||||
@ -7,6 +10,10 @@
|
||||
@color-active: #F0C808;
|
||||
@icon-size: 48px;
|
||||
|
||||
@color-info: white;
|
||||
@color-warning: @color-active;
|
||||
@color-error: #FF4400;
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
@ -19,13 +26,16 @@ html, body {
|
||||
|
||||
body {
|
||||
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-size: 200px;
|
||||
background-position: 50% 50%;
|
||||
background-repeat: no-repeat;
|
||||
color: @color-fg;
|
||||
margin: 0 0;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
#form {
|
||||
@ -87,8 +97,80 @@ body {
|
||||
}
|
||||
}
|
||||
|
||||
.warning {
|
||||
color: @color-warning;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: @color-error;
|
||||
}
|
||||
|
||||
.info {
|
||||
color: @color-info;
|
||||
}
|
||||
|
||||
.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 {
|
||||
position: fixed;
|
||||
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="stylesheet" type="text/css" href="../less/main.css")
|
||||
|
||||
body
|
||||
body.call
|
||||
input#callId(type="hidden" value="#{callId}")
|
||||
|
||||
div#container
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user