Fix notification autorisation request process

This commit is contained in:
Jeremie Pardou-Piquemal 2020-06-13 13:47:52 +02:00 committed by Jérémie Pardou-Piquemal
parent f01f995d9f
commit d88fcbdc33
15 changed files with 567 additions and 156 deletions

View File

@ -19,6 +19,10 @@ export const toggleNotificationEnabled = payload => async (dispatch) => {
dispatch({ type: 'TOGGLE_NOTIFICATION_ENABLED', payload }) dispatch({ type: 'TOGGLE_NOTIFICATION_ENABLED', payload })
} }
export const toggleNotificationAllowed = payload => async (dispatch) => {
dispatch({ type: 'TOGGLE_NOTIFICATION_ALLOWED', payload })
}
export const toggleSocketConnected = payload => async (dispatch) => { export const toggleSocketConnected = payload => async (dispatch) => {
dispatch({ type: 'TOGGLE_SOCKET_CONNECTED', payload }) dispatch({ type: 'TOGGLE_SOCKET_CONNECTED', payload })
} }

View File

@ -1,47 +1,46 @@
import React, { Component } from 'react' import React, { Component } from 'react';
import PropTypes from 'prop-types' import PropTypes from 'prop-types';
import Crypto from 'utils/crypto' import Crypto from 'utils/crypto';
import { connect as connectSocket } from 'utils/socket' import { connect as connectSocket } from 'utils/socket';
import Nav from 'components/Nav' import Nav from 'components/Nav';
import shortId from 'shortid' import shortId from 'shortid';
import Connecting from 'components/Connecting' import Connecting from 'components/Connecting';
import Modal from 'react-modal' import Modal from 'react-modal';
import About from 'components/About' import About from 'components/About';
import Settings from 'components/Settings' import Settings from 'components/Settings';
import Welcome from 'components/Welcome' import Welcome from 'components/Welcome';
import RoomLocked from 'components/RoomLocked' import RoomLocked from 'components/RoomLocked';
import { X, AlertCircle } from 'react-feather' import { X, AlertCircle } from 'react-feather';
import classNames from 'classnames' import classNames from 'classnames';
import ActivityList from './ActivityList' import ActivityList from './ActivityList';
import styles from './styles.module.scss' import styles from './styles.module.scss';
const crypto = new Crypto() const crypto = new Crypto();
Modal.setAppElement('#root'); Modal.setAppElement('#root');
class Home extends Component { class Home extends Component {
async componentWillMount() { async componentWillMount() {
const roomId = encodeURI(this.props.match.params.roomId) const roomId = encodeURI(this.props.match.params.roomId);
const user = await this.createUser() const user = await this.createUser();
const socket = connectSocket(roomId) const socket = connectSocket(roomId);
this.socket = socket; this.socket = socket;
socket.on('disconnect', () => { socket.on('disconnect', () => {
this.props.toggleSocketConnected(false) this.props.toggleSocketConnected(false);
}) });
socket.on('connect', () => { socket.on('connect', () => {
this.initApp(user) this.initApp(user);
this.props.toggleSocketConnected(true) this.props.toggleSocketConnected(true);
}) });
socket.on('USER_ENTER', (payload) => { socket.on('USER_ENTER', payload => {
this.props.receiveUnencryptedMessage('USER_ENTER', payload) this.props.receiveUnencryptedMessage('USER_ENTER', payload);
this.props.sendEncryptedMessage({ this.props.sendEncryptedMessage({
type: 'ADD_USER', type: 'ADD_USER',
payload: { payload: {
@ -50,35 +49,35 @@ class Home extends Component {
isOwner: this.props.iAmOwner, isOwner: this.props.iAmOwner,
id: this.props.userId, id: this.props.userId,
}, },
}) });
if (payload.users.length === 1) { if (payload.users.length === 1) {
this.props.openModal('Welcome'); this.props.openModal('Welcome');
} }
})
socket.on('USER_EXIT', (payload) => {
this.props.receiveUnencryptedMessage('USER_EXIT', payload)
})
socket.on('ENCRYPTED_MESSAGE', (payload) => {
this.props.receiveEncryptedMessage(payload)
})
socket.on('TOGGLE_LOCK_ROOM', (payload) => {
this.props.receiveUnencryptedMessage('TOGGLE_LOCK_ROOM', payload)
})
socket.on('ROOM_LOCKED', (payload) => {
this.props.openModal('Room Locked')
}); });
window.addEventListener('beforeunload', (evt) => { socket.on('USER_EXIT', payload => {
socket.emit('USER_DISCONNECT') this.props.receiveUnencryptedMessage('USER_EXIT', payload);
});
socket.on('ENCRYPTED_MESSAGE', payload => {
this.props.receiveEncryptedMessage(payload);
});
socket.on('TOGGLE_LOCK_ROOM', payload => {
this.props.receiveUnencryptedMessage('TOGGLE_LOCK_ROOM', payload);
});
socket.on('ROOM_LOCKED', payload => {
this.props.openModal('Room Locked');
});
window.addEventListener('beforeunload', evt => {
socket.emit('USER_DISCONNECT');
}); });
} }
componentDidMount() { componentDidMount() {
this.bindEvents() this.bindEvents();
} }
getModal() { getModal() {
@ -88,91 +87,100 @@ class Home extends Component {
component: <Connecting />, component: <Connecting />,
title: 'Connecting...', title: 'Connecting...',
preventClose: true, preventClose: true,
} };
case 'About': case 'About':
return { return {
component: <About roomId={this.props.roomId} />, component: <About roomId={this.props.roomId} />,
title: this.props.translations.aboutHeader, title: this.props.translations.aboutHeader,
} };
case 'Settings': case 'Settings':
return { return {
component: <Settings component: (
roomId={this.props.roomId} <Settings
toggleSoundEnabled={this.props.toggleSoundEnabled} roomId={this.props.roomId}
soundIsEnabled={this.props.soundIsEnabled} toggleSoundEnabled={this.props.toggleSoundEnabled}
toggleNotificationEnabled={this.props.toggleNotificationEnabled} soundIsEnabled={this.props.soundIsEnabled}
notificationIsEnabled={this.props.notificationIsEnabled} toggleNotificationEnabled={this.props.toggleNotificationEnabled}
setLanguage={this.props.setLanguage} toggleNotificationAllowed={this.props.toggleNotificationAllowed}
language={this.props.language} notificationIsEnabled={this.props.notificationIsEnabled}
translations={this.props.translations} notificationIsAllowed={this.props.notificationIsAllowed}
/>, setLanguage={this.props.setLanguage}
language={this.props.language}
translations={this.props.translations}
/>
),
title: this.props.translations.settingsHeader, title: this.props.translations.settingsHeader,
} };
case 'Welcome': case 'Welcome':
return { return {
component: <Welcome roomId={this.props.roomId} close={this.props.closeModal} translations={this.props.translations} />, component: (
<Welcome roomId={this.props.roomId} close={this.props.closeModal} translations={this.props.translations} />
),
title: this.props.translations.welcomeHeader, title: this.props.translations.welcomeHeader,
} };
case 'Room Locked': case 'Room Locked':
return { return {
component: <RoomLocked modalContent={this.props.translations.lockedRoomHeader} />, component: <RoomLocked modalContent={this.props.translations.lockedRoomHeader} />,
title: this.props.translations.lockedRoomHeader, title: this.props.translations.lockedRoomHeader,
preventClose: true, preventClose: true,
} };
default: default:
return { return {
component: null, component: null,
title: null, title: null,
} };
} }
} }
initApp(user) { initApp(user) {
this.socket.emit('USER_ENTER', { this.socket.emit('USER_ENTER', {
publicKey: user.publicKey, publicKey: user.publicKey,
}) });
} }
bindEvents() { bindEvents() {
window.onfocus = () => { window.onfocus = () => {
this.props.toggleWindowFocus(true) this.props.toggleWindowFocus(true);
} };
window.onblur = () => { window.onblur = () => {
this.props.toggleWindowFocus(false) this.props.toggleWindowFocus(false);
} };
} }
createUser() { createUser() {
return new Promise(async (resolve) => { return new Promise(async resolve => {
const username = shortId.generate() const username = shortId.generate();
const encryptDecryptKeys = await crypto.createEncryptDecryptKeys() const encryptDecryptKeys = await crypto.createEncryptDecryptKeys();
const exportedEncryptDecryptPrivateKey = await crypto.exportKey(encryptDecryptKeys.privateKey) const exportedEncryptDecryptPrivateKey = await crypto.exportKey(encryptDecryptKeys.privateKey);
const exportedEncryptDecryptPublicKey = await crypto.exportKey(encryptDecryptKeys.publicKey) const exportedEncryptDecryptPublicKey = await crypto.exportKey(encryptDecryptKeys.publicKey);
this.props.createUser({ this.props.createUser({
username, username,
publicKey: exportedEncryptDecryptPublicKey, publicKey: exportedEncryptDecryptPublicKey,
privateKey: exportedEncryptDecryptPrivateKey, privateKey: exportedEncryptDecryptPrivateKey,
}) });
resolve({ resolve({
publicKey: exportedEncryptDecryptPublicKey, publicKey: exportedEncryptDecryptPublicKey,
}) });
}) });
} }
render() { render() {
const modalOpts = this.getModal() const modalOpts = this.getModal();
return ( return (
<div className={classNames(styles.styles, 'h-100')}> <div className={classNames(styles.styles, 'h-100')}>
<div className="nav-container"> <div className="nav-container">
{!this.props.socketConnected && {!this.props.socketConnected && (
<div className="alert-banner"> <div className="alert-banner">
<span className="icon"><AlertCircle size="15" /></span> <span>Disconnected</span> <span className="icon">
<AlertCircle size="15" />
</span>{' '}
<span>Disconnected</span>
</div> </div>
} )}
<Nav <Nav
members={this.props.members} members={this.props.members}
roomId={this.props.roomId} roomId={this.props.roomId}
@ -184,10 +192,7 @@ class Home extends Component {
translations={this.props.translations} translations={this.props.translations}
/> />
</div> </div>
<ActivityList <ActivityList openModal={this.props.openModal} activities={this.props.activities} />
openModal={this.props.openModal}
activities={this.props.activities}
/>
<Modal <Modal
isOpen={Boolean(this.props.modalComponent)} isOpen={Boolean(this.props.modalComponent)}
contentLabel="Modal" contentLabel="Modal"
@ -206,27 +211,23 @@ class Home extends Component {
onRequestClose={this.props.closeModal} onRequestClose={this.props.closeModal}
> >
<div className="react-modal-header"> <div className="react-modal-header">
{!modalOpts.preventClose && {!modalOpts.preventClose && (
<button onClick={this.props.closeModal} className="btn btn-link btn-plain close-modal"> <button onClick={this.props.closeModal} className="btn btn-link btn-plain close-modal">
<X /> <X />
</button> </button>
} )}
<h3 className="react-modal-title"> <h3 className="react-modal-title">{modalOpts.title}</h3>
{modalOpts.title}
</h3>
</div>
<div className="react-modal-component">
{modalOpts.component}
</div> </div>
<div className="react-modal-component">{modalOpts.component}</div>
</Modal> </Modal>
</div> </div>
) );
} }
} }
Home.defaultProps = { Home.defaultProps = {
modalComponent: null, modalComponent: null,
} };
Home.propTypes = { Home.propTypes = {
receiveEncryptedMessage: PropTypes.func.isRequired, receiveEncryptedMessage: PropTypes.func.isRequired,
@ -249,11 +250,13 @@ Home.propTypes = {
soundIsEnabled: PropTypes.bool.isRequired, soundIsEnabled: PropTypes.bool.isRequired,
toggleSoundEnabled: PropTypes.func.isRequired, toggleSoundEnabled: PropTypes.func.isRequired,
notificationIsEnabled: PropTypes.bool.isRequired, notificationIsEnabled: PropTypes.bool.isRequired,
notificationIsAllowed: PropTypes.bool,
toggleNotificationEnabled: PropTypes.func.isRequired, toggleNotificationEnabled: PropTypes.func.isRequired,
toggleNotificationAllowed: PropTypes.func.isRequired,
toggleSocketConnected: PropTypes.func.isRequired, toggleSocketConnected: PropTypes.func.isRequired,
socketConnected: PropTypes.bool.isRequired, socketConnected: PropTypes.bool.isRequired,
sendUnencryptedMessage: PropTypes.func.isRequired, sendUnencryptedMessage: PropTypes.func.isRequired,
sendEncryptedMessage: PropTypes.func.isRequired sendEncryptedMessage: PropTypes.func.isRequired,
} };
export default Home; export default Home;

View File

@ -62,6 +62,7 @@ test('Home component is displaying', async () => {
toggleSoundEnabled={() => {}} toggleSoundEnabled={() => {}}
soundIsEnabled={false} soundIsEnabled={false}
toggleNotificationEnabled={() => {}} toggleNotificationEnabled={() => {}}
toggleNotificationAllowed={() => {}}
notificationIsEnabled={false} notificationIsEnabled={false}
faviconCount={0} faviconCount={0}
toggleWindowFocus={() => {}} toggleWindowFocus={() => {}}

View File

@ -2,6 +2,7 @@ import React, { Component } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { notify, beep } from 'utils/notifications'; import { notify, beep } from 'utils/notifications';
import Tinycon from 'tinycon'; import Tinycon from 'tinycon';
import { toggleNotificationAllowed, toggleNotificationEnabled } from 'actions';
const mapStateToProps = state => { const mapStateToProps = state => {
return { return {
@ -10,12 +11,21 @@ const mapStateToProps = state => {
windowIsFocused: state.app.windowIsFocused, windowIsFocused: state.app.windowIsFocused,
soundIsEnabled: state.app.soundIsEnabled, soundIsEnabled: state.app.soundIsEnabled,
notificationIsEnabled: state.app.notificationIsEnabled, notificationIsEnabled: state.app.notificationIsEnabled,
notificationIsAllowed: state.app.notificationIsAllowed,
room: state.room, room: state.room,
}; };
}; };
const mapDispatchToProps = {
toggleNotificationAllowed,
toggleNotificationEnabled
};
const WithNewMessageNotification = WrappedComponent => { const WithNewMessageNotification = WrappedComponent => {
return connect(mapStateToProps)( return connect(
mapStateToProps,
mapDispatchToProps,
)(
class WithNotificationHOC extends Component { class WithNotificationHOC extends Component {
state = { lastMessage: null, unreadMessageCount: 0 }; state = { lastMessage: null, unreadMessageCount: 0 };
@ -24,6 +34,7 @@ const WithNewMessageNotification = WrappedComponent => {
room: { id: roomId }, room: { id: roomId },
activities, activities,
notificationIsEnabled, notificationIsEnabled,
notificationIsAllowed,
soundIsEnabled, soundIsEnabled,
unreadMessageCount, unreadMessageCount,
windowIsFocused, windowIsFocused,
@ -38,7 +49,7 @@ const WithNewMessageNotification = WrappedComponent => {
if (lastMessage !== prevState.lastMessage && !windowIsFocused) { if (lastMessage !== prevState.lastMessage && !windowIsFocused) {
const title = `Message from ${username} (${roomId})`; const title = `Message from ${username} (${roomId})`;
if (notificationIsEnabled) notify(title, text); if (notificationIsAllowed && notificationIsEnabled) notify(title, text);
if (soundIsEnabled) beep.play(); if (soundIsEnabled) beep.play();
} }
@ -49,15 +60,32 @@ const WithNewMessageNotification = WrappedComponent => {
return { lastMessage, unreadMessageCount }; return { lastMessage, unreadMessageCount };
} }
componentDidMount() {
switch (Notification.permission) {
case 'granted':
this.props.toggleNotificationAllowed(true);
this.props.toggleNotificationEnabled(true);
break;
case 'denied':
this.props.toggleNotificationAllowed(false);
break;
default:
this.props.toggleNotificationAllowed(null);
}
}
render() { render() {
// Filter props // Filter props
const { const {
room, room,
activities, activities,
notificationIsEnabled, notificationIsEnabled,
motificationIsAllowed,
soundIsEnabled, soundIsEnabled,
unreadMessageCount, unreadMessageCount,
windowIsFocused, windowIsFocused,
toggleNotificationAllowed,
toggleNotificationnEnabled,
...rest ...rest
} = this.props; } = this.props;
return <WrappedComponent {...rest} />; return <WrappedComponent {...rest} />;

View File

@ -8,6 +8,7 @@ import {
toggleWindowFocus, toggleWindowFocus,
toggleSoundEnabled, toggleSoundEnabled,
toggleNotificationEnabled, toggleNotificationEnabled,
toggleNotificationAllowed,
toggleSocketConnected, toggleSocketConnected,
receiveUnencryptedMessage, receiveUnencryptedMessage,
sendUnencryptedMessage, sendUnencryptedMessage,
@ -33,6 +34,7 @@ const mapStateToProps = (state) => {
faviconCount: state.app.unreadMessageCount, faviconCount: state.app.unreadMessageCount,
soundIsEnabled: state.app.soundIsEnabled, soundIsEnabled: state.app.soundIsEnabled,
notificationIsEnabled: state.app.notificationIsEnabled, notificationIsEnabled: state.app.notificationIsEnabled,
notificationIsAllowed: state.app.notificationIsAllowed,
socketConnected: state.app.socketConnected, socketConnected: state.app.socketConnected,
language: state.app.language, language: state.app.language,
translations: state.app.translations, translations: state.app.translations,
@ -47,6 +49,7 @@ const mapDispatchToProps = {
toggleWindowFocus, toggleWindowFocus,
toggleSoundEnabled, toggleSoundEnabled,
toggleNotificationEnabled, toggleNotificationEnabled,
toggleNotificationAllowed,
toggleSocketConnected, toggleSocketConnected,
receiveUnencryptedMessage, receiveUnencryptedMessage,
sendUnencryptedMessage, sendUnencryptedMessage,

View File

@ -87,6 +87,16 @@ jest.mock('tinycon', () => {
}); });
describe('Connected Home component', () => { describe('Connected Home component', () => {
beforeEach(()=>{
global.Notification = {
permission: 'granted'
}
})
afterEach(()=>{
delete global.Notification
})
it('should display', () => { it('should display', () => {
const { asFragment } = render( const { asFragment } = render(
<Provider store={store}> <Provider store={store}>
@ -97,6 +107,43 @@ describe('Connected Home component', () => {
expect(asFragment()).toMatchSnapshot(); expect(asFragment()).toMatchSnapshot();
}); });
it('should set notification', () => {
render(
<Provider store={store}>
<ConnectedHome match={{ params: { roomId: 'roomTest' } }} userId="testUserId" />
</Provider>,
);
expect(store.getState().app.notificationIsAllowed).toBe(true);
expect(store.getState().app.notificationIsEnabled).toBe(true);
global.Notification = {
permission: 'denied'
}
render(
<Provider store={store}>
<ConnectedHome match={{ params: { roomId: 'roomTest' } }} userId="testUserId" />
</Provider>,
);
expect(store.getState().app.notificationIsAllowed).toBe(false);
global.Notification = {
permission: 'default'
}
render(
<Provider store={store}>
<ConnectedHome match={{ params: { roomId: 'roomTest' } }} userId="testUserId" />
</Provider>,
);
expect(store.getState().app.notificationIsAllowed).toBe(null);
});
it('should send notifications', async () => { it('should send notifications', async () => {
Modal.prototype.getSnapshotBeforeUpdate = jest.fn().mockReturnValue(null); Modal.prototype.getSnapshotBeforeUpdate = jest.fn().mockReturnValue(null);
const { rerender } = render( const { rerender } = render(

View File

@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import { render, fireEvent } from '@testing-library/react'; import { render, fireEvent, waitFor } from '@testing-library/react';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import configureStore from 'store'; import configureStore from 'store';
@ -11,17 +11,20 @@ const mockTranslations = {
sound: 'soundCheck', sound: 'soundCheck',
}; };
jest.useFakeTimers();
jest.mock('components/RoomLink'); jest.mock('components/RoomLink');
describe('Settings component', () => { describe('Settings component', () => {
it('should display', async () => { it('should display', async () => {
const { asFragment } = render( const { asFragment, rerender } = render(
<Provider store={store}> <Provider store={store}>
<Settings <Settings
soundIsEnabled={true} soundIsEnabled={true}
toggleSoundEnabled={() => {}} toggleSoundEnabled={() => {}}
notificationIsEnabled={true} notificationIsEnabled={true}
toggleNotificationEnabled={() => {}} toggleNotificationEnabled={() => {}}
toggleNotificationAllowed={jest.fn()}
roomId="roomId" roomId="roomId"
setLanguage={() => {}} setLanguage={() => {}}
translations={{}} translations={{}}
@ -30,6 +33,25 @@ describe('Settings component', () => {
); );
expect(asFragment()).toMatchSnapshot(); expect(asFragment()).toMatchSnapshot();
rerender(
<Provider store={store}>
<Settings
soundIsEnabled={true}
toggleSoundEnabled={() => {}}
notificationIsEnabled={true}
notificationIsAllowed={false}
toggleNotificationEnabled={() => {}}
toggleNotificationAllowed={jest.fn()}
roomId="roomId"
setLanguage={() => {}}
translations={{}}
/>
</Provider>,
);
expect(asFragment()).toMatchSnapshot();
}); });
it('should toggle sound', async () => { it('should toggle sound', async () => {
@ -40,7 +62,9 @@ describe('Settings component', () => {
soundIsEnabled={true} soundIsEnabled={true}
toggleSoundEnabled={toggleSound} toggleSoundEnabled={toggleSound}
notificationIsEnabled={true} notificationIsEnabled={true}
notificationIsAllowed={true}
toggleNotificationEnabled={() => {}} toggleNotificationEnabled={() => {}}
toggleNotificationAllowed={jest.fn()}
roomId="roomId" roomId="roomId"
setLanguage={() => {}} setLanguage={() => {}}
translations={{}} translations={{}}
@ -55,6 +79,10 @@ describe('Settings component', () => {
}); });
it('should toggle notifications', async () => { it('should toggle notifications', async () => {
global.Notification = {
requestPermission: jest.fn().mockResolvedValue('granted'),
};
const toggleNotifications = jest.fn(); const toggleNotifications = jest.fn();
const { getByText } = render( const { getByText } = render(
<Provider store={store}> <Provider store={store}>
@ -62,7 +90,9 @@ describe('Settings component', () => {
soundIsEnabled={true} soundIsEnabled={true}
toggleSoundEnabled={() => {}} toggleSoundEnabled={() => {}}
notificationIsEnabled={true} notificationIsEnabled={true}
notificationIsAllowed={true}
toggleNotificationEnabled={toggleNotifications} toggleNotificationEnabled={toggleNotifications}
toggleNotificationAllowed={jest.fn()}
roomId="roomId" roomId="roomId"
setLanguage={() => {}} setLanguage={() => {}}
translations={{}} translations={{}}
@ -72,9 +102,48 @@ describe('Settings component', () => {
fireEvent.click(getByText('Desktop Notification')); fireEvent.click(getByText('Desktop Notification'));
expect(toggleNotifications).toHaveBeenCalledWith(false); jest.runAllTimers();
delete global.Notification;
waitFor(() =>expect(toggleNotifications).toHaveBeenCalledWith(false));
}); });
it('should not toggle notifications', async () => {
global.Notification = {
requestPermission: jest.fn().mockResolvedValue('denied'),
};
const toggleNotifications = jest.fn();
const toggleAllowed = jest.fn();
const { getByText } = render(
<Provider store={store}>
<Settings
soundIsEnabled={true}
toggleSoundEnabled={() => {}}
notificationIsEnabled={true}
notificationIsAllowed={true}
toggleNotificationEnabled={toggleNotifications}
toggleNotificationAllowed={toggleAllowed}
roomId="roomId"
setLanguage={() => {}}
translations={{}}
/>
</Provider>,
);
fireEvent.click(getByText('Desktop Notification'));
jest.runAllTimers();
delete global.Notification;
waitFor(() =>expect(toggleAllowed).toHaveBeenCalledWith(false));
waitFor(() =>expect(toggleNotifications).not.toHaveBeenCalled());
});
it('should change lang', async () => { it('should change lang', async () => {
const changeLang = jest.fn(); const changeLang = jest.fn();
@ -85,6 +154,7 @@ describe('Settings component', () => {
toggleSoundEnabled={() => {}} toggleSoundEnabled={() => {}}
notificationIsEnabled={true} notificationIsEnabled={true}
toggleNotificationEnabled={() => {}} toggleNotificationEnabled={() => {}}
toggleNotificationAllowed={jest.fn()}
roomId="roomId" roomId="roomId"
setLanguage={changeLang} setLanguage={changeLang}
translations={{}} translations={{}}

View File

@ -171,3 +171,169 @@ exports[`Settings component should display 1`] = `
</div> </div>
</DocumentFragment> </DocumentFragment>
`; `;
exports[`Settings component should display 2`] = `
<DocumentFragment>
<div
class="styles"
>
<section>
<h4>
New message notification
</h4>
<form>
<div
class="form-check"
>
<label
class="form-check-label"
for="sound-control"
>
<input
checked=""
class="form-check-input"
id="sound-control"
type="checkbox"
/>
Sound
</label>
</div>
<div
class="form-check"
>
<label
class="form-check-label"
for="notif-control"
>
Desktop notifications have been denied
</label>
</div>
</form>
</section>
<section>
<h4
class="mb-3"
>
This room
</h4>
</section>
<section>
<h4
class="mb-3"
>
Language
</h4>
<p>
<a
href="https://github.com/darkwire/darkwire.io/blob/master/client/README.md#translations"
rel="noopener noreferrer"
target="_blank"
>
Help us translate Darkwire!
</a>
</p>
<div
class="form-group"
>
<select
class="form-control"
>
<option
value="en"
>
English
</option>
<option
value="fr"
>
Français
</option>
<option
value="oc"
>
Occitan
</option>
<option
value="de"
>
Deutsch
</option>
<option
value="nl"
>
Nederlands
</option>
<option
value="it"
>
Italiano
</option>
<option
value="zhCN"
>
中文
</option>
</select>
</div>
</section>
<section>
<h4>
Room Ownership
</h4>
<p>
The person who created the room is the room owner and has special privileges, like the ability to lock and unlock the room. If the owner leaves the room, the second person to join assumes ownership. If they leave, the third person becomes owner, and so on. The room owner has a star icon next to their username in the participants dropdown.
</p>
</section>
<section>
<h4>
Lock Room
</h4>
<p>
If you are the room owner, you can lock and unlock the room by clicking the lock icon in the nav bar. When a room is locked, no other participants will be able to join.
</p>
</section>
<section>
<h4>
Slash Commands
</h4>
<p>
The following slash commands are available:
</p>
<ul>
<li>
/nick [username]
<span
class="text-muted"
>
changes username
</span>
</li>
<li>
/me [action]
<span
class="text-muted"
>
performs an action
</span>
</li>
<li>
/clear
<span
class="text-muted"
>
clears your message history
</span>
</li>
<li>
/help
<span
class="text-muted"
>
lists all commands
</span>
</li>
</ul>
</section>
</div>
</DocumentFragment>
`;

View File

@ -1,18 +1,26 @@
import React, { Component } from 'react' import React, { Component } from 'react';
import PropTypes from 'prop-types' import PropTypes from 'prop-types';
import RoomLink from 'components/RoomLink' import RoomLink from 'components/RoomLink';
import {styles} from './styles.module.scss' import { styles } from './styles.module.scss';
import Cookie from 'js-cookie'; import Cookie from 'js-cookie';
import T from 'components/T' import T from 'components/T';
class Settings extends Component { class Settings extends Component {
handleSoundToggle() { handleSoundToggle() {
this.props.toggleSoundEnabled(!this.props.soundIsEnabled) this.props.toggleSoundEnabled(!this.props.soundIsEnabled);
} }
handleNotificationToggle() { handleNotificationToggle() {
this.props.toggleNotificationEnabled(!this.props.notificationIsEnabled) Notification.requestPermission().then(permission => {
this.props.toggleNotificationEnabled(true);
if (permission === 'granted') {
this.props.toggleNotificationEnabled(!this.props.notificationIsEnabled);
this.props.toggleNotificationAllowed(true);
}
if (permission === 'denied') {
this.props.toggleNotificationAllowed(false);
}
});
} }
handleLanguageChange(evt) { handleLanguageChange(evt) {
@ -25,33 +33,69 @@ class Settings extends Component {
return ( return (
<div className={styles}> <div className={styles}>
<section> <section>
<h4><T path='newMessageNotification'/></h4> <h4>
<T path="newMessageNotification" />
</h4>
<form> <form>
<div className="form-check"> <div className="form-check">
<label className="form-check-label" htmlFor="sound-control"> <label className="form-check-label" htmlFor="sound-control">
<input id="sound-control" onChange={this.handleSoundToggle.bind(this)} className="form-check-input" type="checkbox" checked={this.props.soundIsEnabled} /> <input
<T path='sound'/> id="sound-control"
onChange={this.handleSoundToggle.bind(this)}
className="form-check-input"
type="checkbox"
checked={this.props.soundIsEnabled}
/>
<T path="sound" />
</label> </label>
</div> </div>
<div className="form-check"> <div className="form-check">
<label className="form-check-label" htmlFor="notif-control"> <label className="form-check-label" htmlFor="notif-control">
<input id="notif-control" onChange={this.handleNotificationToggle.bind(this)} className="form-check-input" type="checkbox" checked={this.props.notificationIsEnabled} /> {this.props.notificationIsAllowed !== false &&
<T path='desktopNotification'/> <>
<input
id="notif-control"
onChange={this.handleNotificationToggle.bind(this)}
className="form-check-input"
type="checkbox"
checked={this.props.notificationIsEnabled}
disabled={this.props.notificationIsAllowed === false} // Important to keep '=== false' here
/>
<T path="desktopNotification" />
</>
}
{this.props.notificationIsAllowed === false && <T path="desktopNotificationBlocked" />}
</label> </label>
</div> </div>
</form> </form>
</section> </section>
<section> <section>
<h4 className='mb-3'><T path='copyRoomHeader'/></h4> <h4 className="mb-3">
<T path="copyRoomHeader" />
</h4>
<RoomLink roomId={this.props.roomId} translations={this.props.translations} /> <RoomLink roomId={this.props.roomId} translations={this.props.translations} />
</section> </section>
<section> <section>
<h4 className='mb-3'><T path='languageDropdownHeader'/></h4> <h4 className="mb-3">
<p><a href="https://github.com/darkwire/darkwire.io/blob/master/client/README.md#translations" rel="noopener noreferrer" target="_blank"><T path='helpTranslate'/></a></p> <T path="languageDropdownHeader" />
</h4>
<p>
<a
href="https://github.com/darkwire/darkwire.io/blob/master/client/README.md#translations"
rel="noopener noreferrer"
target="_blank"
>
<T path="helpTranslate" />
</a>
</p>
<div className="form-group"> <div className="form-group">
<select value={this.props.language} className="form-control" onChange={this.handleLanguageChange.bind(this)}> <select
value={this.props.language}
className="form-control"
onChange={this.handleLanguageChange.bind(this)}
>
<option value="en">English</option> <option value="en">English</option>
<option value="fr">Français</option> <option value="fr">Français</option>
<option value="oc">Occitan</option> <option value="oc">Occitan</option>
@ -64,25 +108,57 @@ class Settings extends Component {
</section> </section>
<section> <section>
<h4><T path='roomOwnerHeader'/></h4> <h4>
<p><T path='roomOwnerText'/></p> <T path="roomOwnerHeader" />
</h4>
<p>
<T path="roomOwnerText" />
</p>
</section> </section>
<section> <section>
<h4><T path='lockRoomHeader'/></h4> <h4>
<p><T path='lockRoomText'/></p> <T path="lockRoomHeader" />
</h4>
<p>
<T path="lockRoomText" />
</p>
</section> </section>
<section> <section>
<h4><T path='slashCommandsHeader'/></h4> <h4>
<p><T path='slashCommandsText'/></p> <T path="slashCommandsHeader" />
</h4>
<p>
<T path="slashCommandsText" />
</p>
<ul> <ul>
<li>/nick [username] <span className="text-muted"><T path='slashCommandsBullets.0'/></span></li> <li>
<li>/me [action] <span className="text-muted"><T path='slashCommandsBullets.1'/></span></li> /nick [username]{' '}
<li>/clear <span className="text-muted"><T path='slashCommandsBullets.2'/></span></li> <span className="text-muted">
<li>/help <span className="text-muted"><T path='slashCommandsBullets.3'/></span></li> <T path="slashCommandsBullets.0" />
</span>
</li>
<li>
/me [action]{' '}
<span className="text-muted">
<T path="slashCommandsBullets.1" />
</span>
</li>
<li>
/clear{' '}
<span className="text-muted">
<T path="slashCommandsBullets.2" />
</span>
</li>
<li>
/help{' '}
<span className="text-muted">
<T path="slashCommandsBullets.3" />
</span>
</li>
</ul> </ul>
</section> </section>
</div> </div>
) );
} }
} }
@ -90,10 +166,12 @@ Settings.propTypes = {
soundIsEnabled: PropTypes.bool.isRequired, soundIsEnabled: PropTypes.bool.isRequired,
toggleSoundEnabled: PropTypes.func.isRequired, toggleSoundEnabled: PropTypes.func.isRequired,
notificationIsEnabled: PropTypes.bool.isRequired, notificationIsEnabled: PropTypes.bool.isRequired,
notificationIsAllowed: PropTypes.bool,
toggleNotificationEnabled: PropTypes.func.isRequired, toggleNotificationEnabled: PropTypes.func.isRequired,
toggleNotificationAllowed: PropTypes.func.isRequired,
roomId: PropTypes.string.isRequired, roomId: PropTypes.string.isRequired,
setLanguage: PropTypes.func.isRequired, setLanguage: PropTypes.func.isRequired,
translations: PropTypes.object.isRequired, translations: PropTypes.object.isRequired,
} };
export default Settings export default Settings;

View File

@ -29,6 +29,7 @@
"sound": "Sound", "sound": "Sound",
"newMessageNotification": "New message notification", "newMessageNotification": "New message notification",
"desktopNotification": "Desktop Notification", "desktopNotification": "Desktop Notification",
"desktopNotificationBlocked": "Desktop notifications have been denied",
"welcomeModalCTA": "Ok", "welcomeModalCTA": "Ok",
"lockedRoomHeader": "This room is locked", "lockedRoomHeader": "This room is locked",
"helpTranslate": "Help us translate Darkwire!" "helpTranslate": "Help us translate Darkwire!"

View File

@ -29,6 +29,7 @@
"sound": "Son", "sound": "Son",
"newMessageNotification": "Notification lors d'un nouveau message", "newMessageNotification": "Notification lors d'un nouveau message",
"desktopNotification": "Notification Système", "desktopNotification": "Notification Système",
"desktopNotificationBlocked": "Les notifications systèmes ont été refusée",
"welcomeModalCTA": "Ok", "welcomeModalCTA": "Ok",
"lockedRoomHeader": "Ce salon est verrouillé", "lockedRoomHeader": "Ce salon est verrouillé",
"helpTranslate": "Aidez-nous à traduire Darkwire!" "helpTranslate": "Aidez-nous à traduire Darkwire!"

View File

@ -9,11 +9,12 @@ const initialState = {
windowIsFocused: true, windowIsFocused: true,
unreadMessageCount: 0, unreadMessageCount: 0,
soundIsEnabled: true, soundIsEnabled: true,
notificationIsEnabled: true, notificationIsEnabled: false,
notificationIsAllowed: null,
socketConnected: false, socketConnected: false,
language, language,
translations: getTranslations(language) translations: getTranslations(language),
} };
const app = (state = initialState, action) => { const app = (state = initialState, action) => {
switch (action.type) { switch (action.type) {
@ -21,52 +22,57 @@ const app = (state = initialState, action) => {
return { return {
...state, ...state,
modalComponent: action.payload, modalComponent: action.payload,
} };
case 'CLOSE_MODAL': case 'CLOSE_MODAL':
return { return {
...state, ...state,
modalComponent: null, modalComponent: null,
} };
case 'SET_SCROLLED_TO_BOTTOM': case 'SET_SCROLLED_TO_BOTTOM':
return { return {
...state, ...state,
scrolledToBottom: action.payload, scrolledToBottom: action.payload,
} };
case 'TOGGLE_WINDOW_FOCUS': case 'TOGGLE_WINDOW_FOCUS':
return { return {
...state, ...state,
windowIsFocused: action.payload, windowIsFocused: action.payload,
unreadMessageCount: 0, unreadMessageCount: 0,
} };
case 'RECEIVE_ENCRYPTED_MESSAGE_TEXT_MESSAGE': case 'RECEIVE_ENCRYPTED_MESSAGE_TEXT_MESSAGE':
return { return {
...state, ...state,
unreadMessageCount: state.windowIsFocused ? 0 : state.unreadMessageCount + 1, unreadMessageCount: state.windowIsFocused ? 0 : state.unreadMessageCount + 1,
} };
case 'TOGGLE_SOUND_ENABLED': case 'TOGGLE_SOUND_ENABLED':
return { return {
...state, ...state,
soundIsEnabled: action.payload, soundIsEnabled: action.payload,
} };
case 'TOGGLE_NOTIFICATION_ENABLED': case 'TOGGLE_NOTIFICATION_ENABLED':
return { return {
...state, ...state,
notificationIsEnabled: action.payload, notificationIsEnabled: action.payload,
} };
case 'TOGGLE_NOTIFICATION_ALLOWED':
return {
...state,
notificationIsAllowed: action.payload,
};
case 'TOGGLE_SOCKET_CONNECTED': case 'TOGGLE_SOCKET_CONNECTED':
return { return {
...state, ...state,
socketConnected: action.payload, socketConnected: action.payload,
} };
case 'CHANGE_LANGUAGE': case 'CHANGE_LANGUAGE':
return { return {
...state, ...state,
language: action.payload, language: action.payload,
translations: getTranslations(action.payload) translations: getTranslations(action.payload),
} };
default: default:
return state return state;
} }
} };
export default app export default app;

View File

@ -14,7 +14,8 @@ describe('App reducer', () => {
modalComponent: null, modalComponent: null,
scrolledToBottom: true, scrolledToBottom: true,
socketConnected: false, socketConnected: false,
notificationIsEnabled: true, notificationIsEnabled: false,
notificationIsAllowed: null,
soundIsEnabled: true, soundIsEnabled: true,
translations: { path: 'test' }, translations: { path: 'test' },
unreadMessageCount: 0, unreadMessageCount: 0,

View File

@ -21,10 +21,13 @@ const room = (state = initialState, action) => {
.filter(member => memberPubKeys.includes(member.publicKey.n)) .filter(member => memberPubKeys.includes(member.publicKey.n))
.map(member => { .map(member => {
const thisMember = action.payload.members.find(mem => mem.publicKey.n === member.id) const thisMember = action.payload.members.find(mem => mem.publicKey.n === member.id)
return { if (thisMember){
...member, return {
isOwner: thisMember.isOwner ...member,
isOwner: thisMember.isOwner
}
} }
return {...member}
}) })
} }
case 'RECEIVE_ENCRYPTED_MESSAGE_ADD_USER': case 'RECEIVE_ENCRYPTED_MESSAGE_ADD_USER':

View File

@ -1,5 +1,4 @@
import beepFile from 'audio/beep.mp3' import beepFile from 'audio/beep.mp3';
const showNotification = (title, message, avatarUrl) => { const showNotification = (title, message, avatarUrl) => {
const notifBody = { const notifBody = {
@ -45,6 +44,6 @@ export const notify = (title, content) => {
} }
}; };
export const beep = (window.Audio && new window.Audio(beepFile)) || { play: () => { }} export const beep = (window.Audio && new window.Audio(beepFile)) || { play: () => {} };
export default { notify, beep }; export default { notify, beep };