mirror of
https://github.com/darkwire/darkwire.io.git
synced 2025-07-18 10:49:02 +00:00
Fix notification autorisation request process
This commit is contained in:
parent
f01f995d9f
commit
d88fcbdc33
@ -19,6 +19,10 @@ export const toggleNotificationEnabled = payload => async (dispatch) => {
|
||||
dispatch({ type: 'TOGGLE_NOTIFICATION_ENABLED', payload })
|
||||
}
|
||||
|
||||
export const toggleNotificationAllowed = payload => async (dispatch) => {
|
||||
dispatch({ type: 'TOGGLE_NOTIFICATION_ALLOWED', payload })
|
||||
}
|
||||
|
||||
export const toggleSocketConnected = payload => async (dispatch) => {
|
||||
dispatch({ type: 'TOGGLE_SOCKET_CONNECTED', payload })
|
||||
}
|
||||
|
@ -1,47 +1,46 @@
|
||||
import React, { Component } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import Crypto from 'utils/crypto'
|
||||
import { connect as connectSocket } from 'utils/socket'
|
||||
import Nav from 'components/Nav'
|
||||
import shortId from 'shortid'
|
||||
import Connecting from 'components/Connecting'
|
||||
import Modal from 'react-modal'
|
||||
import About from 'components/About'
|
||||
import Settings from 'components/Settings'
|
||||
import Welcome from 'components/Welcome'
|
||||
import RoomLocked from 'components/RoomLocked'
|
||||
import { X, AlertCircle } from 'react-feather'
|
||||
import classNames from 'classnames'
|
||||
import ActivityList from './ActivityList'
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Crypto from 'utils/crypto';
|
||||
import { connect as connectSocket } from 'utils/socket';
|
||||
import Nav from 'components/Nav';
|
||||
import shortId from 'shortid';
|
||||
import Connecting from 'components/Connecting';
|
||||
import Modal from 'react-modal';
|
||||
import About from 'components/About';
|
||||
import Settings from 'components/Settings';
|
||||
import Welcome from 'components/Welcome';
|
||||
import RoomLocked from 'components/RoomLocked';
|
||||
import { X, AlertCircle } from 'react-feather';
|
||||
import classNames from 'classnames';
|
||||
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');
|
||||
|
||||
class Home extends Component {
|
||||
|
||||
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;
|
||||
|
||||
socket.on('disconnect', () => {
|
||||
this.props.toggleSocketConnected(false)
|
||||
})
|
||||
this.props.toggleSocketConnected(false);
|
||||
});
|
||||
|
||||
socket.on('connect', () => {
|
||||
this.initApp(user)
|
||||
this.props.toggleSocketConnected(true)
|
||||
})
|
||||
this.initApp(user);
|
||||
this.props.toggleSocketConnected(true);
|
||||
});
|
||||
|
||||
socket.on('USER_ENTER', (payload) => {
|
||||
this.props.receiveUnencryptedMessage('USER_ENTER', payload)
|
||||
socket.on('USER_ENTER', payload => {
|
||||
this.props.receiveUnencryptedMessage('USER_ENTER', payload);
|
||||
this.props.sendEncryptedMessage({
|
||||
type: 'ADD_USER',
|
||||
payload: {
|
||||
@ -50,35 +49,35 @@ class Home extends Component {
|
||||
isOwner: this.props.iAmOwner,
|
||||
id: this.props.userId,
|
||||
},
|
||||
})
|
||||
});
|
||||
if (payload.users.length === 1) {
|
||||
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.emit('USER_DISCONNECT')
|
||||
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.emit('USER_DISCONNECT');
|
||||
});
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.bindEvents()
|
||||
this.bindEvents();
|
||||
}
|
||||
|
||||
getModal() {
|
||||
@ -88,91 +87,100 @@ class Home extends Component {
|
||||
component: <Connecting />,
|
||||
title: 'Connecting...',
|
||||
preventClose: true,
|
||||
}
|
||||
};
|
||||
case 'About':
|
||||
return {
|
||||
component: <About roomId={this.props.roomId} />,
|
||||
title: this.props.translations.aboutHeader,
|
||||
}
|
||||
};
|
||||
case 'Settings':
|
||||
return {
|
||||
component: <Settings
|
||||
roomId={this.props.roomId}
|
||||
toggleSoundEnabled={this.props.toggleSoundEnabled}
|
||||
soundIsEnabled={this.props.soundIsEnabled}
|
||||
toggleNotificationEnabled={this.props.toggleNotificationEnabled}
|
||||
notificationIsEnabled={this.props.notificationIsEnabled}
|
||||
setLanguage={this.props.setLanguage}
|
||||
language={this.props.language}
|
||||
translations={this.props.translations}
|
||||
/>,
|
||||
component: (
|
||||
<Settings
|
||||
roomId={this.props.roomId}
|
||||
toggleSoundEnabled={this.props.toggleSoundEnabled}
|
||||
soundIsEnabled={this.props.soundIsEnabled}
|
||||
toggleNotificationEnabled={this.props.toggleNotificationEnabled}
|
||||
toggleNotificationAllowed={this.props.toggleNotificationAllowed}
|
||||
notificationIsEnabled={this.props.notificationIsEnabled}
|
||||
notificationIsAllowed={this.props.notificationIsAllowed}
|
||||
setLanguage={this.props.setLanguage}
|
||||
language={this.props.language}
|
||||
translations={this.props.translations}
|
||||
/>
|
||||
),
|
||||
title: this.props.translations.settingsHeader,
|
||||
}
|
||||
};
|
||||
case 'Welcome':
|
||||
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,
|
||||
}
|
||||
};
|
||||
case 'Room Locked':
|
||||
return {
|
||||
component: <RoomLocked modalContent={this.props.translations.lockedRoomHeader} />,
|
||||
title: this.props.translations.lockedRoomHeader,
|
||||
preventClose: true,
|
||||
}
|
||||
};
|
||||
default:
|
||||
return {
|
||||
component: null,
|
||||
title: null,
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
initApp(user) {
|
||||
this.socket.emit('USER_ENTER', {
|
||||
publicKey: user.publicKey,
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
bindEvents() {
|
||||
window.onfocus = () => {
|
||||
this.props.toggleWindowFocus(true)
|
||||
}
|
||||
this.props.toggleWindowFocus(true);
|
||||
};
|
||||
|
||||
window.onblur = () => {
|
||||
this.props.toggleWindowFocus(false)
|
||||
}
|
||||
this.props.toggleWindowFocus(false);
|
||||
};
|
||||
}
|
||||
|
||||
createUser() {
|
||||
return new Promise(async (resolve) => {
|
||||
const username = shortId.generate()
|
||||
return new Promise(async resolve => {
|
||||
const username = shortId.generate();
|
||||
|
||||
const encryptDecryptKeys = await crypto.createEncryptDecryptKeys()
|
||||
const exportedEncryptDecryptPrivateKey = await crypto.exportKey(encryptDecryptKeys.privateKey)
|
||||
const exportedEncryptDecryptPublicKey = await crypto.exportKey(encryptDecryptKeys.publicKey)
|
||||
const encryptDecryptKeys = await crypto.createEncryptDecryptKeys();
|
||||
const exportedEncryptDecryptPrivateKey = await crypto.exportKey(encryptDecryptKeys.privateKey);
|
||||
const exportedEncryptDecryptPublicKey = await crypto.exportKey(encryptDecryptKeys.publicKey);
|
||||
|
||||
this.props.createUser({
|
||||
username,
|
||||
publicKey: exportedEncryptDecryptPublicKey,
|
||||
privateKey: exportedEncryptDecryptPrivateKey,
|
||||
})
|
||||
});
|
||||
|
||||
resolve({
|
||||
publicKey: exportedEncryptDecryptPublicKey,
|
||||
})
|
||||
})
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const modalOpts = this.getModal()
|
||||
const modalOpts = this.getModal();
|
||||
return (
|
||||
<div className={classNames(styles.styles, 'h-100')}>
|
||||
<div className="nav-container">
|
||||
{!this.props.socketConnected &&
|
||||
{!this.props.socketConnected && (
|
||||
<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>
|
||||
}
|
||||
)}
|
||||
<Nav
|
||||
members={this.props.members}
|
||||
roomId={this.props.roomId}
|
||||
@ -184,10 +192,7 @@ class Home extends Component {
|
||||
translations={this.props.translations}
|
||||
/>
|
||||
</div>
|
||||
<ActivityList
|
||||
openModal={this.props.openModal}
|
||||
activities={this.props.activities}
|
||||
/>
|
||||
<ActivityList openModal={this.props.openModal} activities={this.props.activities} />
|
||||
<Modal
|
||||
isOpen={Boolean(this.props.modalComponent)}
|
||||
contentLabel="Modal"
|
||||
@ -206,27 +211,23 @@ class Home extends Component {
|
||||
onRequestClose={this.props.closeModal}
|
||||
>
|
||||
<div className="react-modal-header">
|
||||
{!modalOpts.preventClose &&
|
||||
{!modalOpts.preventClose && (
|
||||
<button onClick={this.props.closeModal} className="btn btn-link btn-plain close-modal">
|
||||
<X />
|
||||
</button>
|
||||
}
|
||||
<h3 className="react-modal-title">
|
||||
{modalOpts.title}
|
||||
</h3>
|
||||
</div>
|
||||
<div className="react-modal-component">
|
||||
{modalOpts.component}
|
||||
)}
|
||||
<h3 className="react-modal-title">{modalOpts.title}</h3>
|
||||
</div>
|
||||
<div className="react-modal-component">{modalOpts.component}</div>
|
||||
</Modal>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Home.defaultProps = {
|
||||
modalComponent: null,
|
||||
}
|
||||
};
|
||||
|
||||
Home.propTypes = {
|
||||
receiveEncryptedMessage: PropTypes.func.isRequired,
|
||||
@ -249,11 +250,13 @@ Home.propTypes = {
|
||||
soundIsEnabled: PropTypes.bool.isRequired,
|
||||
toggleSoundEnabled: PropTypes.func.isRequired,
|
||||
notificationIsEnabled: PropTypes.bool.isRequired,
|
||||
notificationIsAllowed: PropTypes.bool,
|
||||
toggleNotificationEnabled: PropTypes.func.isRequired,
|
||||
toggleNotificationAllowed: PropTypes.func.isRequired,
|
||||
toggleSocketConnected: PropTypes.func.isRequired,
|
||||
socketConnected: PropTypes.bool.isRequired,
|
||||
sendUnencryptedMessage: PropTypes.func.isRequired,
|
||||
sendEncryptedMessage: PropTypes.func.isRequired
|
||||
}
|
||||
sendEncryptedMessage: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default Home;
|
||||
|
@ -62,6 +62,7 @@ test('Home component is displaying', async () => {
|
||||
toggleSoundEnabled={() => {}}
|
||||
soundIsEnabled={false}
|
||||
toggleNotificationEnabled={() => {}}
|
||||
toggleNotificationAllowed={() => {}}
|
||||
notificationIsEnabled={false}
|
||||
faviconCount={0}
|
||||
toggleWindowFocus={() => {}}
|
||||
|
@ -2,6 +2,7 @@ import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { notify, beep } from 'utils/notifications';
|
||||
import Tinycon from 'tinycon';
|
||||
import { toggleNotificationAllowed, toggleNotificationEnabled } from 'actions';
|
||||
|
||||
const mapStateToProps = state => {
|
||||
return {
|
||||
@ -10,12 +11,21 @@ const mapStateToProps = state => {
|
||||
windowIsFocused: state.app.windowIsFocused,
|
||||
soundIsEnabled: state.app.soundIsEnabled,
|
||||
notificationIsEnabled: state.app.notificationIsEnabled,
|
||||
notificationIsAllowed: state.app.notificationIsAllowed,
|
||||
room: state.room,
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = {
|
||||
toggleNotificationAllowed,
|
||||
toggleNotificationEnabled
|
||||
};
|
||||
|
||||
const WithNewMessageNotification = WrappedComponent => {
|
||||
return connect(mapStateToProps)(
|
||||
return connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps,
|
||||
)(
|
||||
class WithNotificationHOC extends Component {
|
||||
state = { lastMessage: null, unreadMessageCount: 0 };
|
||||
|
||||
@ -24,6 +34,7 @@ const WithNewMessageNotification = WrappedComponent => {
|
||||
room: { id: roomId },
|
||||
activities,
|
||||
notificationIsEnabled,
|
||||
notificationIsAllowed,
|
||||
soundIsEnabled,
|
||||
unreadMessageCount,
|
||||
windowIsFocused,
|
||||
@ -38,7 +49,7 @@ const WithNewMessageNotification = WrappedComponent => {
|
||||
|
||||
if (lastMessage !== prevState.lastMessage && !windowIsFocused) {
|
||||
const title = `Message from ${username} (${roomId})`;
|
||||
if (notificationIsEnabled) notify(title, text);
|
||||
if (notificationIsAllowed && notificationIsEnabled) notify(title, text);
|
||||
if (soundIsEnabled) beep.play();
|
||||
}
|
||||
|
||||
@ -49,15 +60,32 @@ const WithNewMessageNotification = WrappedComponent => {
|
||||
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() {
|
||||
// Filter props
|
||||
const {
|
||||
room,
|
||||
activities,
|
||||
notificationIsEnabled,
|
||||
motificationIsAllowed,
|
||||
soundIsEnabled,
|
||||
unreadMessageCount,
|
||||
windowIsFocused,
|
||||
toggleNotificationAllowed,
|
||||
toggleNotificationnEnabled,
|
||||
...rest
|
||||
} = this.props;
|
||||
return <WrappedComponent {...rest} />;
|
||||
|
@ -8,6 +8,7 @@ import {
|
||||
toggleWindowFocus,
|
||||
toggleSoundEnabled,
|
||||
toggleNotificationEnabled,
|
||||
toggleNotificationAllowed,
|
||||
toggleSocketConnected,
|
||||
receiveUnencryptedMessage,
|
||||
sendUnencryptedMessage,
|
||||
@ -33,6 +34,7 @@ const mapStateToProps = (state) => {
|
||||
faviconCount: state.app.unreadMessageCount,
|
||||
soundIsEnabled: state.app.soundIsEnabled,
|
||||
notificationIsEnabled: state.app.notificationIsEnabled,
|
||||
notificationIsAllowed: state.app.notificationIsAllowed,
|
||||
socketConnected: state.app.socketConnected,
|
||||
language: state.app.language,
|
||||
translations: state.app.translations,
|
||||
@ -47,6 +49,7 @@ const mapDispatchToProps = {
|
||||
toggleWindowFocus,
|
||||
toggleSoundEnabled,
|
||||
toggleNotificationEnabled,
|
||||
toggleNotificationAllowed,
|
||||
toggleSocketConnected,
|
||||
receiveUnencryptedMessage,
|
||||
sendUnencryptedMessage,
|
||||
|
@ -87,6 +87,16 @@ jest.mock('tinycon', () => {
|
||||
});
|
||||
|
||||
describe('Connected Home component', () => {
|
||||
beforeEach(()=>{
|
||||
global.Notification = {
|
||||
permission: 'granted'
|
||||
}
|
||||
})
|
||||
|
||||
afterEach(()=>{
|
||||
delete global.Notification
|
||||
})
|
||||
|
||||
it('should display', () => {
|
||||
const { asFragment } = render(
|
||||
<Provider store={store}>
|
||||
@ -97,6 +107,43 @@ describe('Connected Home component', () => {
|
||||
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 () => {
|
||||
Modal.prototype.getSnapshotBeforeUpdate = jest.fn().mockReturnValue(null);
|
||||
const { rerender } = render(
|
||||
|
@ -1,5 +1,5 @@
|
||||
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 configureStore from 'store';
|
||||
|
||||
@ -11,17 +11,20 @@ const mockTranslations = {
|
||||
sound: 'soundCheck',
|
||||
};
|
||||
|
||||
jest.useFakeTimers();
|
||||
|
||||
jest.mock('components/RoomLink');
|
||||
|
||||
describe('Settings component', () => {
|
||||
it('should display', async () => {
|
||||
const { asFragment } = render(
|
||||
const { asFragment, rerender } = render(
|
||||
<Provider store={store}>
|
||||
<Settings
|
||||
soundIsEnabled={true}
|
||||
toggleSoundEnabled={() => {}}
|
||||
notificationIsEnabled={true}
|
||||
toggleNotificationEnabled={() => {}}
|
||||
toggleNotificationAllowed={jest.fn()}
|
||||
roomId="roomId"
|
||||
setLanguage={() => {}}
|
||||
translations={{}}
|
||||
@ -30,6 +33,25 @@ describe('Settings component', () => {
|
||||
);
|
||||
|
||||
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 () => {
|
||||
@ -40,7 +62,9 @@ describe('Settings component', () => {
|
||||
soundIsEnabled={true}
|
||||
toggleSoundEnabled={toggleSound}
|
||||
notificationIsEnabled={true}
|
||||
notificationIsAllowed={true}
|
||||
toggleNotificationEnabled={() => {}}
|
||||
toggleNotificationAllowed={jest.fn()}
|
||||
roomId="roomId"
|
||||
setLanguage={() => {}}
|
||||
translations={{}}
|
||||
@ -55,6 +79,10 @@ describe('Settings component', () => {
|
||||
});
|
||||
|
||||
it('should toggle notifications', async () => {
|
||||
global.Notification = {
|
||||
requestPermission: jest.fn().mockResolvedValue('granted'),
|
||||
};
|
||||
|
||||
const toggleNotifications = jest.fn();
|
||||
const { getByText } = render(
|
||||
<Provider store={store}>
|
||||
@ -62,7 +90,9 @@ describe('Settings component', () => {
|
||||
soundIsEnabled={true}
|
||||
toggleSoundEnabled={() => {}}
|
||||
notificationIsEnabled={true}
|
||||
notificationIsAllowed={true}
|
||||
toggleNotificationEnabled={toggleNotifications}
|
||||
toggleNotificationAllowed={jest.fn()}
|
||||
roomId="roomId"
|
||||
setLanguage={() => {}}
|
||||
translations={{}}
|
||||
@ -72,9 +102,48 @@ describe('Settings component', () => {
|
||||
|
||||
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 () => {
|
||||
const changeLang = jest.fn();
|
||||
|
||||
@ -85,6 +154,7 @@ describe('Settings component', () => {
|
||||
toggleSoundEnabled={() => {}}
|
||||
notificationIsEnabled={true}
|
||||
toggleNotificationEnabled={() => {}}
|
||||
toggleNotificationAllowed={jest.fn()}
|
||||
roomId="roomId"
|
||||
setLanguage={changeLang}
|
||||
translations={{}}
|
||||
|
@ -171,3 +171,169 @@ exports[`Settings component should display 1`] = `
|
||||
</div>
|
||||
</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>
|
||||
`;
|
||||
|
@ -1,18 +1,26 @@
|
||||
import React, { Component } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import RoomLink from 'components/RoomLink'
|
||||
import {styles} from './styles.module.scss'
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import RoomLink from 'components/RoomLink';
|
||||
import { styles } from './styles.module.scss';
|
||||
import Cookie from 'js-cookie';
|
||||
import T from 'components/T'
|
||||
import T from 'components/T';
|
||||
|
||||
class Settings extends Component {
|
||||
|
||||
handleSoundToggle() {
|
||||
this.props.toggleSoundEnabled(!this.props.soundIsEnabled)
|
||||
this.props.toggleSoundEnabled(!this.props.soundIsEnabled);
|
||||
}
|
||||
|
||||
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) {
|
||||
@ -25,33 +33,69 @@ class Settings extends Component {
|
||||
return (
|
||||
<div className={styles}>
|
||||
<section>
|
||||
<h4><T path='newMessageNotification'/></h4>
|
||||
<h4>
|
||||
<T path="newMessageNotification" />
|
||||
</h4>
|
||||
<form>
|
||||
<div className="form-check">
|
||||
<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} />
|
||||
<T path='sound'/>
|
||||
<input
|
||||
id="sound-control"
|
||||
onChange={this.handleSoundToggle.bind(this)}
|
||||
className="form-check-input"
|
||||
type="checkbox"
|
||||
checked={this.props.soundIsEnabled}
|
||||
/>
|
||||
<T path="sound" />
|
||||
</label>
|
||||
</div>
|
||||
<div className="form-check">
|
||||
<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} />
|
||||
<T path='desktopNotification'/>
|
||||
{this.props.notificationIsAllowed !== false &&
|
||||
<>
|
||||
<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>
|
||||
</div>
|
||||
</form>
|
||||
</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} />
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h4 className='mb-3'><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>
|
||||
<h4 className="mb-3">
|
||||
<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">
|
||||
<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="fr">Français</option>
|
||||
<option value="oc">Occitan</option>
|
||||
@ -64,25 +108,57 @@ class Settings extends Component {
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h4><T path='roomOwnerHeader'/></h4>
|
||||
<p><T path='roomOwnerText'/></p>
|
||||
<h4>
|
||||
<T path="roomOwnerHeader" />
|
||||
</h4>
|
||||
<p>
|
||||
<T path="roomOwnerText" />
|
||||
</p>
|
||||
</section>
|
||||
<section>
|
||||
<h4><T path='lockRoomHeader'/></h4>
|
||||
<p><T path='lockRoomText'/></p>
|
||||
<h4>
|
||||
<T path="lockRoomHeader" />
|
||||
</h4>
|
||||
<p>
|
||||
<T path="lockRoomText" />
|
||||
</p>
|
||||
</section>
|
||||
<section>
|
||||
<h4><T path='slashCommandsHeader'/></h4>
|
||||
<p><T path='slashCommandsText'/></p>
|
||||
<h4>
|
||||
<T path="slashCommandsHeader" />
|
||||
</h4>
|
||||
<p>
|
||||
<T path="slashCommandsText" />
|
||||
</p>
|
||||
<ul>
|
||||
<li>/nick [username] <span className="text-muted"><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>
|
||||
<li>
|
||||
/nick [username]{' '}
|
||||
<span className="text-muted">
|
||||
<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>
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -90,10 +166,12 @@ Settings.propTypes = {
|
||||
soundIsEnabled: PropTypes.bool.isRequired,
|
||||
toggleSoundEnabled: PropTypes.func.isRequired,
|
||||
notificationIsEnabled: PropTypes.bool.isRequired,
|
||||
notificationIsAllowed: PropTypes.bool,
|
||||
toggleNotificationEnabled: PropTypes.func.isRequired,
|
||||
toggleNotificationAllowed: PropTypes.func.isRequired,
|
||||
roomId: PropTypes.string.isRequired,
|
||||
setLanguage: PropTypes.func.isRequired,
|
||||
translations: PropTypes.object.isRequired,
|
||||
}
|
||||
};
|
||||
|
||||
export default Settings
|
||||
export default Settings;
|
||||
|
@ -29,6 +29,7 @@
|
||||
"sound": "Sound",
|
||||
"newMessageNotification": "New message notification",
|
||||
"desktopNotification": "Desktop Notification",
|
||||
"desktopNotificationBlocked": "Desktop notifications have been denied",
|
||||
"welcomeModalCTA": "Ok",
|
||||
"lockedRoomHeader": "This room is locked",
|
||||
"helpTranslate": "Help us translate Darkwire!"
|
||||
|
@ -29,6 +29,7 @@
|
||||
"sound": "Son",
|
||||
"newMessageNotification": "Notification lors d'un nouveau message",
|
||||
"desktopNotification": "Notification Système",
|
||||
"desktopNotificationBlocked": "Les notifications systèmes ont été refusée",
|
||||
"welcomeModalCTA": "Ok",
|
||||
"lockedRoomHeader": "Ce salon est verrouillé",
|
||||
"helpTranslate": "Aidez-nous à traduire Darkwire!"
|
||||
|
@ -9,11 +9,12 @@ const initialState = {
|
||||
windowIsFocused: true,
|
||||
unreadMessageCount: 0,
|
||||
soundIsEnabled: true,
|
||||
notificationIsEnabled: true,
|
||||
notificationIsEnabled: false,
|
||||
notificationIsAllowed: null,
|
||||
socketConnected: false,
|
||||
language,
|
||||
translations: getTranslations(language)
|
||||
}
|
||||
translations: getTranslations(language),
|
||||
};
|
||||
|
||||
const app = (state = initialState, action) => {
|
||||
switch (action.type) {
|
||||
@ -21,52 +22,57 @@ const app = (state = initialState, action) => {
|
||||
return {
|
||||
...state,
|
||||
modalComponent: action.payload,
|
||||
}
|
||||
};
|
||||
case 'CLOSE_MODAL':
|
||||
return {
|
||||
...state,
|
||||
modalComponent: null,
|
||||
}
|
||||
};
|
||||
case 'SET_SCROLLED_TO_BOTTOM':
|
||||
return {
|
||||
...state,
|
||||
scrolledToBottom: action.payload,
|
||||
}
|
||||
};
|
||||
case 'TOGGLE_WINDOW_FOCUS':
|
||||
return {
|
||||
...state,
|
||||
windowIsFocused: action.payload,
|
||||
unreadMessageCount: 0,
|
||||
}
|
||||
};
|
||||
case 'RECEIVE_ENCRYPTED_MESSAGE_TEXT_MESSAGE':
|
||||
return {
|
||||
...state,
|
||||
unreadMessageCount: state.windowIsFocused ? 0 : state.unreadMessageCount + 1,
|
||||
}
|
||||
};
|
||||
case 'TOGGLE_SOUND_ENABLED':
|
||||
return {
|
||||
...state,
|
||||
soundIsEnabled: action.payload,
|
||||
}
|
||||
};
|
||||
case 'TOGGLE_NOTIFICATION_ENABLED':
|
||||
return {
|
||||
...state,
|
||||
notificationIsEnabled: action.payload,
|
||||
}
|
||||
};
|
||||
case 'TOGGLE_NOTIFICATION_ALLOWED':
|
||||
return {
|
||||
...state,
|
||||
notificationIsAllowed: action.payload,
|
||||
};
|
||||
case 'TOGGLE_SOCKET_CONNECTED':
|
||||
return {
|
||||
...state,
|
||||
socketConnected: action.payload,
|
||||
}
|
||||
};
|
||||
case 'CHANGE_LANGUAGE':
|
||||
return {
|
||||
...state,
|
||||
language: action.payload,
|
||||
translations: getTranslations(action.payload)
|
||||
}
|
||||
translations: getTranslations(action.payload),
|
||||
};
|
||||
default:
|
||||
return state
|
||||
return state;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default app
|
||||
export default app;
|
||||
|
@ -14,7 +14,8 @@ describe('App reducer', () => {
|
||||
modalComponent: null,
|
||||
scrolledToBottom: true,
|
||||
socketConnected: false,
|
||||
notificationIsEnabled: true,
|
||||
notificationIsEnabled: false,
|
||||
notificationIsAllowed: null,
|
||||
soundIsEnabled: true,
|
||||
translations: { path: 'test' },
|
||||
unreadMessageCount: 0,
|
||||
|
@ -21,10 +21,13 @@ const room = (state = initialState, action) => {
|
||||
.filter(member => memberPubKeys.includes(member.publicKey.n))
|
||||
.map(member => {
|
||||
const thisMember = action.payload.members.find(mem => mem.publicKey.n === member.id)
|
||||
return {
|
||||
...member,
|
||||
isOwner: thisMember.isOwner
|
||||
if (thisMember){
|
||||
return {
|
||||
...member,
|
||||
isOwner: thisMember.isOwner
|
||||
}
|
||||
}
|
||||
return {...member}
|
||||
})
|
||||
}
|
||||
case 'RECEIVE_ENCRYPTED_MESSAGE_ADD_USER':
|
||||
|
@ -1,5 +1,4 @@
|
||||
import beepFile from 'audio/beep.mp3'
|
||||
|
||||
import beepFile from 'audio/beep.mp3';
|
||||
|
||||
const showNotification = (title, message, avatarUrl) => {
|
||||
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 };
|
||||
|
Loading…
x
Reference in New Issue
Block a user