mirror of
https://github.com/darkwire/darkwire.io.git
synced 2025-07-18 10:49:02 +00:00
Add optionnal desktop notification
This commit is contained in:
parent
d475a148b9
commit
f01f995d9f
@ -15,6 +15,10 @@ export const toggleSoundEnabled = payload => async (dispatch) => {
|
|||||||
dispatch({ type: 'TOGGLE_SOUND_ENABLED', payload })
|
dispatch({ type: 'TOGGLE_SOUND_ENABLED', payload })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const toggleNotificationEnabled = payload => async (dispatch) => {
|
||||||
|
dispatch({ type: 'TOGGLE_NOTIFICATION_ENABLED', 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 })
|
||||||
}
|
}
|
||||||
|
@ -11,8 +11,6 @@ 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 Tinycon from 'tinycon'
|
|
||||||
import beepFile from 'audio/beep.mp3'
|
|
||||||
import classNames from 'classnames'
|
import classNames from 'classnames'
|
||||||
import ActivityList from './ActivityList'
|
import ActivityList from './ActivityList'
|
||||||
|
|
||||||
@ -81,16 +79,6 @@ class Home extends Component {
|
|||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.bindEvents()
|
this.bindEvents()
|
||||||
|
|
||||||
this.beep = window.Audio && new window.Audio(beepFile)
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillReceiveProps(nextProps) {
|
|
||||||
Tinycon.setBubble(nextProps.faviconCount)
|
|
||||||
|
|
||||||
if (nextProps.faviconCount !== 0 && nextProps.faviconCount !== this.props.faviconCount && this.props.soundIsEnabled) {
|
|
||||||
this.beep.play()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getModal() {
|
getModal() {
|
||||||
@ -108,7 +96,16 @@ class Home extends Component {
|
|||||||
}
|
}
|
||||||
case 'Settings':
|
case 'Settings':
|
||||||
return {
|
return {
|
||||||
component: <Settings roomId={this.props.roomId} toggleSoundEnabled={this.props.toggleSoundEnabled} soundIsEnabled={this.props.soundIsEnabled} 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}
|
||||||
|
notificationIsEnabled={this.props.notificationIsEnabled}
|
||||||
|
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':
|
||||||
@ -251,6 +248,8 @@ Home.propTypes = {
|
|||||||
faviconCount: PropTypes.number.isRequired,
|
faviconCount: PropTypes.number.isRequired,
|
||||||
soundIsEnabled: PropTypes.bool.isRequired,
|
soundIsEnabled: PropTypes.bool.isRequired,
|
||||||
toggleSoundEnabled: PropTypes.func.isRequired,
|
toggleSoundEnabled: PropTypes.func.isRequired,
|
||||||
|
notificationIsEnabled: PropTypes.bool.isRequired,
|
||||||
|
toggleNotificationEnabled: 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,
|
||||||
|
@ -36,6 +36,7 @@ jest.mock('utils/crypto', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
test('Home component is displaying', async () => {
|
test('Home component is displaying', async () => {
|
||||||
const { asFragment } = render(
|
const { asFragment } = render(
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
@ -60,6 +61,8 @@ test('Home component is displaying', async () => {
|
|||||||
socketConnected={false}
|
socketConnected={false}
|
||||||
toggleSoundEnabled={() => {}}
|
toggleSoundEnabled={() => {}}
|
||||||
soundIsEnabled={false}
|
soundIsEnabled={false}
|
||||||
|
toggleNotificationEnabled={() => {}}
|
||||||
|
notificationIsEnabled={false}
|
||||||
faviconCount={0}
|
faviconCount={0}
|
||||||
toggleWindowFocus={() => {}}
|
toggleWindowFocus={() => {}}
|
||||||
closeModal={() => {}}
|
closeModal={() => {}}
|
||||||
|
69
client/src/components/Home/WithNewMessageNotification.js
Normal file
69
client/src/components/Home/WithNewMessageNotification.js
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import React, { Component } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { notify, beep } from 'utils/notifications';
|
||||||
|
import Tinycon from 'tinycon';
|
||||||
|
|
||||||
|
const mapStateToProps = state => {
|
||||||
|
return {
|
||||||
|
activities: state.activities.items,
|
||||||
|
unreadMessageCount: state.app.unreadMessageCount,
|
||||||
|
windowIsFocused: state.app.windowIsFocused,
|
||||||
|
soundIsEnabled: state.app.soundIsEnabled,
|
||||||
|
notificationIsEnabled: state.app.notificationIsEnabled,
|
||||||
|
room: state.room,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const WithNewMessageNotification = WrappedComponent => {
|
||||||
|
return connect(mapStateToProps)(
|
||||||
|
class WithNotificationHOC extends Component {
|
||||||
|
state = { lastMessage: null, unreadMessageCount: 0 };
|
||||||
|
|
||||||
|
static getDerivedStateFromProps(nextProps, prevState) {
|
||||||
|
const {
|
||||||
|
room: { id: roomId },
|
||||||
|
activities,
|
||||||
|
notificationIsEnabled,
|
||||||
|
soundIsEnabled,
|
||||||
|
unreadMessageCount,
|
||||||
|
windowIsFocused,
|
||||||
|
} = nextProps;
|
||||||
|
|
||||||
|
if (activities.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastMessage = activities[activities.length - 1];
|
||||||
|
const { username, text } = lastMessage;
|
||||||
|
|
||||||
|
if (lastMessage !== prevState.lastMessage && !windowIsFocused) {
|
||||||
|
const title = `Message from ${username} (${roomId})`;
|
||||||
|
if (notificationIsEnabled) notify(title, text);
|
||||||
|
if (soundIsEnabled) beep.play();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (unreadMessageCount !== prevState.unreadMessageCount) {
|
||||||
|
Tinycon.setBubble(unreadMessageCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { lastMessage, unreadMessageCount };
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
// Filter props
|
||||||
|
const {
|
||||||
|
room,
|
||||||
|
activities,
|
||||||
|
notificationIsEnabled,
|
||||||
|
soundIsEnabled,
|
||||||
|
unreadMessageCount,
|
||||||
|
windowIsFocused,
|
||||||
|
...rest
|
||||||
|
} = this.props;
|
||||||
|
return <WrappedComponent {...rest} />;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default WithNewMessageNotification;
|
299
client/src/components/Home/__snapshots__/index.test.js.snap
Normal file
299
client/src/components/Home/__snapshots__/index.test.js.snap
Normal file
@ -0,0 +1,299 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`Connected Home component should display 1`] = `
|
||||||
|
<DocumentFragment>
|
||||||
|
<div
|
||||||
|
class="styles h-100"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="nav-container"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="alert-banner"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="icon"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
fill="none"
|
||||||
|
height="15"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="15"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<circle
|
||||||
|
cx="12"
|
||||||
|
cy="12"
|
||||||
|
r="10"
|
||||||
|
/>
|
||||||
|
<line
|
||||||
|
x1="12"
|
||||||
|
x2="12"
|
||||||
|
y1="8"
|
||||||
|
y2="12"
|
||||||
|
/>
|
||||||
|
<line
|
||||||
|
x1="12"
|
||||||
|
x2="12"
|
||||||
|
y1="16"
|
||||||
|
y2="16"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span>
|
||||||
|
Disconnected
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<nav
|
||||||
|
class="navbar navbar-expand-md navbar-dark"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="meta"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
alt="Darkwire"
|
||||||
|
class="logo"
|
||||||
|
src="logo.png"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
class="btn btn-plain btn-link clipboard-trigger room-id ellipsis"
|
||||||
|
data-clipboard-text="http://localhost/"
|
||||||
|
data-placement="bottom"
|
||||||
|
data-toggle="tooltip"
|
||||||
|
title="Copied"
|
||||||
|
>
|
||||||
|
/
|
||||||
|
</button>
|
||||||
|
<span
|
||||||
|
class="lock-room-container"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="lock-room btn btn-link btn-plain"
|
||||||
|
data-placement="bottom"
|
||||||
|
data-toggle="tooltip"
|
||||||
|
title="You must be the owner to lock or unlock the room"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="muted"
|
||||||
|
fill="none"
|
||||||
|
height="24"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<rect
|
||||||
|
height="11"
|
||||||
|
rx="2"
|
||||||
|
ry="2"
|
||||||
|
width="18"
|
||||||
|
x="3"
|
||||||
|
y="11"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M7 11V7a5 5 0 0 1 9.9-1"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
<div
|
||||||
|
class="dropdown members-dropdown"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
class="dropdown__trigger "
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="btn btn-link btn-plain members-action"
|
||||||
|
title="Users"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="users-icon"
|
||||||
|
fill="none"
|
||||||
|
height="24"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"
|
||||||
|
/>
|
||||||
|
<circle
|
||||||
|
cx="9"
|
||||||
|
cy="7"
|
||||||
|
r="4"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M23 21v-2a4 4 0 0 0-3-3.87"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M16 3.13a4 4 0 0 1 0 7.75"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<span>
|
||||||
|
0
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
<div
|
||||||
|
class="dropdown__content "
|
||||||
|
>
|
||||||
|
<ul
|
||||||
|
class="plain"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
aria-controls="navbarSupportedContent"
|
||||||
|
aria-expanded="false"
|
||||||
|
aria-label="Toggle navigation"
|
||||||
|
class="navbar-toggler"
|
||||||
|
data-target="#navbarSupportedContent"
|
||||||
|
data-toggle="collapse"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="navbar-toggler-icon"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<div
|
||||||
|
class="collapse navbar-collapse"
|
||||||
|
id="navbarSupportedContent"
|
||||||
|
>
|
||||||
|
<ul
|
||||||
|
class="navbar-nav ml-auto"
|
||||||
|
>
|
||||||
|
<li
|
||||||
|
class="nav-item"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="btn btn-plain nav-link"
|
||||||
|
target="blank"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
fill="none"
|
||||||
|
height="24"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<circle
|
||||||
|
cx="12"
|
||||||
|
cy="12"
|
||||||
|
r="10"
|
||||||
|
/>
|
||||||
|
<line
|
||||||
|
x1="12"
|
||||||
|
x2="12"
|
||||||
|
y1="8"
|
||||||
|
y2="16"
|
||||||
|
/>
|
||||||
|
<line
|
||||||
|
x1="8"
|
||||||
|
x2="16"
|
||||||
|
y1="12"
|
||||||
|
y2="12"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<span>
|
||||||
|
New Room
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li
|
||||||
|
class=" nav-item"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="btn btn-plain nav-link"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
fill="none"
|
||||||
|
height="24"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<circle
|
||||||
|
cx="12"
|
||||||
|
cy="12"
|
||||||
|
r="3"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<span>
|
||||||
|
Settings
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li
|
||||||
|
class="nav-item"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="btn btn-plain nav-link"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
fill="none"
|
||||||
|
height="24"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<circle
|
||||||
|
cx="12"
|
||||||
|
cy="12"
|
||||||
|
r="10"
|
||||||
|
/>
|
||||||
|
<line
|
||||||
|
x1="12"
|
||||||
|
x2="12"
|
||||||
|
y1="16"
|
||||||
|
y2="12"
|
||||||
|
/>
|
||||||
|
<line
|
||||||
|
x1="12"
|
||||||
|
x2="12"
|
||||||
|
y1="8"
|
||||||
|
y2="8"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<span>
|
||||||
|
About
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DocumentFragment>
|
||||||
|
`;
|
@ -7,12 +7,14 @@ import {
|
|||||||
closeModal,
|
closeModal,
|
||||||
toggleWindowFocus,
|
toggleWindowFocus,
|
||||||
toggleSoundEnabled,
|
toggleSoundEnabled,
|
||||||
|
toggleNotificationEnabled,
|
||||||
toggleSocketConnected,
|
toggleSocketConnected,
|
||||||
receiveUnencryptedMessage,
|
receiveUnencryptedMessage,
|
||||||
sendUnencryptedMessage,
|
sendUnencryptedMessage,
|
||||||
sendEncryptedMessage,
|
sendEncryptedMessage,
|
||||||
setLanguage
|
setLanguage
|
||||||
} from 'actions'
|
} from 'actions'
|
||||||
|
import WithNewMessageNotification from './WithNewMessageNotification'
|
||||||
|
|
||||||
const mapStateToProps = (state) => {
|
const mapStateToProps = (state) => {
|
||||||
const me = state.room.members.find(m => m.id === state.user.id)
|
const me = state.room.members.find(m => m.id === state.user.id)
|
||||||
@ -30,6 +32,7 @@ const mapStateToProps = (state) => {
|
|||||||
iAmOwner: Boolean(me && me.isOwner),
|
iAmOwner: Boolean(me && me.isOwner),
|
||||||
faviconCount: state.app.unreadMessageCount,
|
faviconCount: state.app.unreadMessageCount,
|
||||||
soundIsEnabled: state.app.soundIsEnabled,
|
soundIsEnabled: state.app.soundIsEnabled,
|
||||||
|
notificationIsEnabled: state.app.notificationIsEnabled,
|
||||||
socketConnected: state.app.socketConnected,
|
socketConnected: state.app.socketConnected,
|
||||||
language: state.app.language,
|
language: state.app.language,
|
||||||
translations: state.app.translations,
|
translations: state.app.translations,
|
||||||
@ -43,6 +46,7 @@ const mapDispatchToProps = {
|
|||||||
closeModal,
|
closeModal,
|
||||||
toggleWindowFocus,
|
toggleWindowFocus,
|
||||||
toggleSoundEnabled,
|
toggleSoundEnabled,
|
||||||
|
toggleNotificationEnabled,
|
||||||
toggleSocketConnected,
|
toggleSocketConnected,
|
||||||
receiveUnencryptedMessage,
|
receiveUnencryptedMessage,
|
||||||
sendUnencryptedMessage,
|
sendUnencryptedMessage,
|
||||||
@ -50,7 +54,7 @@ const mapDispatchToProps = {
|
|||||||
setLanguage
|
setLanguage
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect(
|
export default WithNewMessageNotification(connect(
|
||||||
mapStateToProps,
|
mapStateToProps,
|
||||||
mapDispatchToProps
|
mapDispatchToProps
|
||||||
)(Home)
|
)(Home))
|
||||||
|
161
client/src/components/Home/index.test.js
Normal file
161
client/src/components/Home/index.test.js
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { render } from '@testing-library/react';
|
||||||
|
import ConnectedHome from '.';
|
||||||
|
import { Provider } from 'react-redux';
|
||||||
|
import configureStore from 'store';
|
||||||
|
import { toggleWindowFocus, toggleNotificationEnabled, toggleSoundEnabled } from 'actions/app';
|
||||||
|
import { receiveEncryptedMessage } from 'actions/encrypted_messages';
|
||||||
|
import { notify, beep } from 'utils/notifications';
|
||||||
|
import Tinycon from 'tinycon';
|
||||||
|
import Modal from 'react-modal';
|
||||||
|
|
||||||
|
const store = configureStore();
|
||||||
|
|
||||||
|
jest.useFakeTimers();
|
||||||
|
|
||||||
|
// We don't test activity list here
|
||||||
|
jest.mock('./ActivityList', () => {
|
||||||
|
return jest.fn().mockReturnValue(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
jest.mock('react-modal'); // Cant load modal without root app element
|
||||||
|
|
||||||
|
jest.mock('utils/socket', () => {
|
||||||
|
// Avoid exception
|
||||||
|
return {
|
||||||
|
connect: jest.fn().mockImplementation(() => {
|
||||||
|
return {
|
||||||
|
on: jest.fn(),
|
||||||
|
emit: jest.fn(),
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
getSocket: jest.fn().mockImplementation(() => {
|
||||||
|
return {
|
||||||
|
on: jest.fn(),
|
||||||
|
emit: jest.fn(),
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
jest.mock('shortid', () => {
|
||||||
|
// Avoid exception
|
||||||
|
return {
|
||||||
|
generate: jest.fn().mockImplementation(() => {
|
||||||
|
return 'shortidgenerated';
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
jest.mock('utils/crypto', () => {
|
||||||
|
// Need window.crytpo.subtle
|
||||||
|
return jest.fn().mockImplementation(() => {
|
||||||
|
return {
|
||||||
|
createEncryptDecryptKeys: () => {
|
||||||
|
return {
|
||||||
|
privateKey: { n: 'private' },
|
||||||
|
publicKey: { n: 'public' },
|
||||||
|
};
|
||||||
|
},
|
||||||
|
exportKey: () => {
|
||||||
|
return { n: 'exportedKey' };
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
jest.mock('utils/message', () => {
|
||||||
|
return {
|
||||||
|
process: jest.fn(async (payload, state) => ({
|
||||||
|
...payload,
|
||||||
|
payload: { payload: 'text', username: 'sender', text: 'new message' },
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
jest.mock('utils/notifications', () => {
|
||||||
|
return {
|
||||||
|
notify: jest.fn(),
|
||||||
|
beep: { play: jest.fn() },
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
jest.mock('tinycon', () => {
|
||||||
|
return {
|
||||||
|
setBubble: jest.fn(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Connected Home component', () => {
|
||||||
|
it('should display', () => {
|
||||||
|
const { asFragment } = render(
|
||||||
|
<Provider store={store}>
|
||||||
|
<ConnectedHome match={{ params: { roomId: 'roomTest' } }} userId="testUserId" />
|
||||||
|
</Provider>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(asFragment()).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should send notifications', async () => {
|
||||||
|
Modal.prototype.getSnapshotBeforeUpdate = jest.fn().mockReturnValue(null);
|
||||||
|
const { rerender } = render(
|
||||||
|
<Provider store={store}>
|
||||||
|
<ConnectedHome match={{ params: { roomId: 'roomTest' } }} userId="testUserId" roomId={'testId'} />
|
||||||
|
</Provider>,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test with window focused
|
||||||
|
await receiveEncryptedMessage({
|
||||||
|
type: 'TEXT_MESSAGE',
|
||||||
|
payload: {},
|
||||||
|
})(store.dispatch, store.getState);
|
||||||
|
|
||||||
|
rerender(
|
||||||
|
<Provider store={store}>
|
||||||
|
<ConnectedHome match={{ params: { roomId: 'roomTest' } }} userId="testUserId" roomId={'testId'} />
|
||||||
|
</Provider>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(store.getState().app.unreadMessageCount).toBe(0);
|
||||||
|
expect(notify).not.toHaveBeenCalled();
|
||||||
|
expect(beep.play).not.toHaveBeenCalled();
|
||||||
|
expect(Tinycon.setBubble).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
// Test with window unfocused
|
||||||
|
await toggleWindowFocus(false)(store.dispatch);
|
||||||
|
await receiveEncryptedMessage({
|
||||||
|
type: 'TEXT_MESSAGE',
|
||||||
|
payload: {},
|
||||||
|
})(store.dispatch, store.getState);
|
||||||
|
|
||||||
|
rerender(
|
||||||
|
<Provider store={store}>
|
||||||
|
<ConnectedHome match={{ params: { roomId: 'roomTest' } }} userId="testUserId" roomId={'testId'} />
|
||||||
|
</Provider>,
|
||||||
|
);
|
||||||
|
expect(store.getState().app.unreadMessageCount).toBe(1);
|
||||||
|
expect(notify).toHaveBeenLastCalledWith('Message from sender ()', 'new message');
|
||||||
|
expect(beep.play).toHaveBeenLastCalledWith();
|
||||||
|
expect(Tinycon.setBubble).toHaveBeenLastCalledWith(1);
|
||||||
|
|
||||||
|
// Test with sound and notification disabled
|
||||||
|
await toggleNotificationEnabled(false)(store.dispatch);
|
||||||
|
await toggleSoundEnabled(false)(store.dispatch);
|
||||||
|
await receiveEncryptedMessage({
|
||||||
|
type: 'TEXT_MESSAGE',
|
||||||
|
payload: {},
|
||||||
|
})(store.dispatch, store.getState);
|
||||||
|
|
||||||
|
rerender(
|
||||||
|
<Provider store={store}>
|
||||||
|
<ConnectedHome match={{ params: { roomId: 'roomTest' } }} userId="testUserId" roomId={'testId'} />
|
||||||
|
</Provider>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(store.getState().app.unreadMessageCount).toBe(2);
|
||||||
|
expect(notify).toHaveBeenCalledTimes(1);
|
||||||
|
expect(beep.play).toHaveBeenCalledTimes(1);
|
||||||
|
expect(Tinycon.setBubble).toHaveBeenLastCalledWith(2);
|
||||||
|
});
|
||||||
|
});
|
@ -1,32 +1,32 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { render, fireEvent } from '@testing-library/react';
|
import { render, fireEvent } from '@testing-library/react';
|
||||||
|
import { Provider } from 'react-redux';
|
||||||
|
import configureStore from 'store';
|
||||||
|
|
||||||
import Settings from '.';
|
import Settings from '.';
|
||||||
|
|
||||||
|
const store = configureStore();
|
||||||
|
|
||||||
const mockTranslations = {
|
const mockTranslations = {
|
||||||
sound: 'soundCheck',
|
sound: 'soundCheck',
|
||||||
};
|
};
|
||||||
|
|
||||||
jest.mock('components/T', () => {
|
|
||||||
return jest.fn().mockImplementation(({ path }) => {
|
|
||||||
return mockTranslations[path] || 'default';
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// To avoid missing provider
|
|
||||||
jest.mock('components/T');
|
|
||||||
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 } = render(
|
||||||
<Settings
|
<Provider store={store}>
|
||||||
soundIsEnabled={true}
|
<Settings
|
||||||
toggleSoundEnabled={() => {}}
|
soundIsEnabled={true}
|
||||||
roomId="roomId"
|
toggleSoundEnabled={() => {}}
|
||||||
setLanguage={() => {}}
|
notificationIsEnabled={true}
|
||||||
translations={mockTranslations}
|
toggleNotificationEnabled={() => {}}
|
||||||
/>,
|
roomId="roomId"
|
||||||
|
setLanguage={() => {}}
|
||||||
|
translations={{}}
|
||||||
|
/>
|
||||||
|
</Provider>,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(asFragment()).toMatchSnapshot();
|
expect(asFragment()).toMatchSnapshot();
|
||||||
@ -34,33 +34,62 @@ describe('Settings component', () => {
|
|||||||
|
|
||||||
it('should toggle sound', async () => {
|
it('should toggle sound', async () => {
|
||||||
const toggleSound = jest.fn();
|
const toggleSound = jest.fn();
|
||||||
const { getAllByText } = render(
|
const { getByText } = render(
|
||||||
<Settings
|
<Provider store={store}>
|
||||||
soundIsEnabled={true}
|
<Settings
|
||||||
toggleSoundEnabled={toggleSound}
|
soundIsEnabled={true}
|
||||||
roomId="roomId"
|
toggleSoundEnabled={toggleSound}
|
||||||
setLanguage={() => {}}
|
notificationIsEnabled={true}
|
||||||
translations={mockTranslations}
|
toggleNotificationEnabled={() => {}}
|
||||||
/>,
|
roomId="roomId"
|
||||||
|
setLanguage={() => {}}
|
||||||
|
translations={{}}
|
||||||
|
/>
|
||||||
|
</Provider>,
|
||||||
);
|
);
|
||||||
|
|
||||||
//console.log(getAllByText(mockTranslations.sound)[1]);
|
//console.log(getAllByText(mockTranslations.sound)[1]);
|
||||||
fireEvent.click(getAllByText(mockTranslations.sound)[1]);
|
fireEvent.click(getByText('Sound'));
|
||||||
|
|
||||||
expect(toggleSound).toHaveBeenCalledWith(false);
|
expect(toggleSound).toHaveBeenCalledWith(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should toggle notifications', async () => {
|
||||||
|
const toggleNotifications = jest.fn();
|
||||||
|
const { getByText } = render(
|
||||||
|
<Provider store={store}>
|
||||||
|
<Settings
|
||||||
|
soundIsEnabled={true}
|
||||||
|
toggleSoundEnabled={() => {}}
|
||||||
|
notificationIsEnabled={true}
|
||||||
|
toggleNotificationEnabled={toggleNotifications}
|
||||||
|
roomId="roomId"
|
||||||
|
setLanguage={() => {}}
|
||||||
|
translations={{}}
|
||||||
|
/>
|
||||||
|
</Provider>,
|
||||||
|
);
|
||||||
|
|
||||||
|
fireEvent.click(getByText('Desktop Notification'));
|
||||||
|
|
||||||
|
expect(toggleNotifications).toHaveBeenCalledWith(false);
|
||||||
|
});
|
||||||
|
|
||||||
it('should change lang', async () => {
|
it('should change lang', async () => {
|
||||||
const changeLang = jest.fn();
|
const changeLang = jest.fn();
|
||||||
|
|
||||||
const { getByDisplayValue } = render(
|
const { getByDisplayValue } = render(
|
||||||
<Settings
|
<Provider store={store}>
|
||||||
soundIsEnabled={true}
|
<Settings
|
||||||
toggleSoundEnabled={() => {}}
|
soundIsEnabled={true}
|
||||||
roomId="roomId"
|
toggleSoundEnabled={() => {}}
|
||||||
setLanguage={changeLang}
|
notificationIsEnabled={true}
|
||||||
translations={{}}
|
toggleNotificationEnabled={() => {}}
|
||||||
/>,
|
roomId="roomId"
|
||||||
|
setLanguage={changeLang}
|
||||||
|
translations={{}}
|
||||||
|
/>
|
||||||
|
</Provider>,
|
||||||
);
|
);
|
||||||
|
|
||||||
fireEvent.change(getByDisplayValue('English'), { target: { value: 'de' } });
|
fireEvent.change(getByDisplayValue('English'), { target: { value: 'de' } });
|
||||||
|
@ -7,7 +7,7 @@ exports[`Settings component should display 1`] = `
|
|||||||
>
|
>
|
||||||
<section>
|
<section>
|
||||||
<h4>
|
<h4>
|
||||||
soundCheck
|
New message notification
|
||||||
</h4>
|
</h4>
|
||||||
<form>
|
<form>
|
||||||
<div
|
<div
|
||||||
@ -23,7 +23,23 @@ exports[`Settings component should display 1`] = `
|
|||||||
id="sound-control"
|
id="sound-control"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
/>
|
/>
|
||||||
soundCheck
|
Sound
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="form-check"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
class="form-check-label"
|
||||||
|
for="notif-control"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
checked=""
|
||||||
|
class="form-check-input"
|
||||||
|
id="notif-control"
|
||||||
|
type="checkbox"
|
||||||
|
/>
|
||||||
|
Desktop Notification
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
@ -32,14 +48,14 @@ exports[`Settings component should display 1`] = `
|
|||||||
<h4
|
<h4
|
||||||
class="mb-3"
|
class="mb-3"
|
||||||
>
|
>
|
||||||
default
|
This room
|
||||||
</h4>
|
</h4>
|
||||||
</section>
|
</section>
|
||||||
<section>
|
<section>
|
||||||
<h4
|
<h4
|
||||||
class="mb-3"
|
class="mb-3"
|
||||||
>
|
>
|
||||||
default
|
Language
|
||||||
</h4>
|
</h4>
|
||||||
<p>
|
<p>
|
||||||
<a
|
<a
|
||||||
@ -47,7 +63,7 @@ exports[`Settings component should display 1`] = `
|
|||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
default
|
Help us translate Darkwire!
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
<div
|
<div
|
||||||
@ -96,26 +112,26 @@ exports[`Settings component should display 1`] = `
|
|||||||
</section>
|
</section>
|
||||||
<section>
|
<section>
|
||||||
<h4>
|
<h4>
|
||||||
default
|
Room Ownership
|
||||||
</h4>
|
</h4>
|
||||||
<p>
|
<p>
|
||||||
default
|
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>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
<section>
|
<section>
|
||||||
<h4>
|
<h4>
|
||||||
default
|
Lock Room
|
||||||
</h4>
|
</h4>
|
||||||
<p>
|
<p>
|
||||||
default
|
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>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
<section>
|
<section>
|
||||||
<h4>
|
<h4>
|
||||||
default
|
Slash Commands
|
||||||
</h4>
|
</h4>
|
||||||
<p>
|
<p>
|
||||||
default
|
The following slash commands are available:
|
||||||
</p>
|
</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
@ -123,7 +139,7 @@ exports[`Settings component should display 1`] = `
|
|||||||
<span
|
<span
|
||||||
class="text-muted"
|
class="text-muted"
|
||||||
>
|
>
|
||||||
default
|
changes username
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
@ -131,7 +147,7 @@ exports[`Settings component should display 1`] = `
|
|||||||
<span
|
<span
|
||||||
class="text-muted"
|
class="text-muted"
|
||||||
>
|
>
|
||||||
default
|
performs an action
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
@ -139,7 +155,7 @@ exports[`Settings component should display 1`] = `
|
|||||||
<span
|
<span
|
||||||
class="text-muted"
|
class="text-muted"
|
||||||
>
|
>
|
||||||
default
|
clears your message history
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
@ -147,7 +163,7 @@ exports[`Settings component should display 1`] = `
|
|||||||
<span
|
<span
|
||||||
class="text-muted"
|
class="text-muted"
|
||||||
>
|
>
|
||||||
default
|
lists all commands
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -11,6 +11,10 @@ class Settings extends Component {
|
|||||||
this.props.toggleSoundEnabled(!this.props.soundIsEnabled)
|
this.props.toggleSoundEnabled(!this.props.soundIsEnabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleNotificationToggle() {
|
||||||
|
this.props.toggleNotificationEnabled(!this.props.notificationIsEnabled)
|
||||||
|
}
|
||||||
|
|
||||||
handleLanguageChange(evt) {
|
handleLanguageChange(evt) {
|
||||||
const language = evt.target.value;
|
const language = evt.target.value;
|
||||||
Cookie.set('language', language);
|
Cookie.set('language', language);
|
||||||
@ -21,7 +25,7 @@ class Settings extends Component {
|
|||||||
return (
|
return (
|
||||||
<div className={styles}>
|
<div className={styles}>
|
||||||
<section>
|
<section>
|
||||||
<h4><T path='sound'/></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">
|
||||||
@ -29,8 +33,15 @@ class Settings extends Component {
|
|||||||
<T path='sound'/>
|
<T path='sound'/>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</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'/>
|
||||||
|
</label>
|
||||||
|
</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} />
|
||||||
@ -78,6 +89,8 @@ class Settings extends Component {
|
|||||||
Settings.propTypes = {
|
Settings.propTypes = {
|
||||||
soundIsEnabled: PropTypes.bool.isRequired,
|
soundIsEnabled: PropTypes.bool.isRequired,
|
||||||
toggleSoundEnabled: PropTypes.func.isRequired,
|
toggleSoundEnabled: PropTypes.func.isRequired,
|
||||||
|
notificationIsEnabled: PropTypes.bool.isRequired,
|
||||||
|
toggleNotificationEnabled: 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,
|
||||||
|
@ -22,6 +22,8 @@
|
|||||||
"slashCommandsHeader": "Slash-Befehle",
|
"slashCommandsHeader": "Slash-Befehle",
|
||||||
"slashCommandsText": "Die folgenden Schrägstrichbefehle sind verfügbar:",
|
"slashCommandsText": "Die folgenden Schrägstrichbefehle sind verfügbar:",
|
||||||
"sound": "Klingen",
|
"sound": "Klingen",
|
||||||
|
"newMessageNotification": "New message notification",
|
||||||
|
"desktopNotification": "Desktop Notification",
|
||||||
"typePlaceholder": "Tippen Sie hier",
|
"typePlaceholder": "Tippen Sie hier",
|
||||||
"unlockedRoom": "{username} hat den Raum freigeschaltet",
|
"unlockedRoom": "{username} hat den Raum freigeschaltet",
|
||||||
"userJoined": "{username} ist beigetreten",
|
"userJoined": "{username} ist beigetreten",
|
||||||
|
@ -25,13 +25,10 @@
|
|||||||
"lockRoomText": "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.",
|
"lockRoomText": "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.",
|
||||||
"slashCommandsHeader": "Slash Commands",
|
"slashCommandsHeader": "Slash Commands",
|
||||||
"slashCommandsText": "The following slash commands are available:",
|
"slashCommandsText": "The following slash commands are available:",
|
||||||
"slashCommandsBullets": [
|
"slashCommandsBullets": ["changes username", "performs an action", "clears your message history", "lists all commands"],
|
||||||
"changes username",
|
|
||||||
"performs an action",
|
|
||||||
"clears your message history",
|
|
||||||
"lists all commands"
|
|
||||||
],
|
|
||||||
"sound": "Sound",
|
"sound": "Sound",
|
||||||
|
"newMessageNotification": "New message notification",
|
||||||
|
"desktopNotification": "Desktop Notification",
|
||||||
"welcomeModalCTA": "Ok",
|
"welcomeModalCTA": "Ok",
|
||||||
"lockedRoomHeader": "This room is locked",
|
"lockedRoomHeader": "This room is locked",
|
||||||
"helpTranslate": "Help us translate Darkwire!"
|
"helpTranslate": "Help us translate Darkwire!"
|
||||||
|
@ -25,13 +25,10 @@
|
|||||||
"lockRoomText": "Si vous êtes le propriétaire du salon, vous pouvez le verrouiller et le déverrouiller en cliquant sur l'icône de cadenas située dans la barre de navigation. Quand un salon est verrouillé, aucun autre participant ne peut rejoindre.",
|
"lockRoomText": "Si vous êtes le propriétaire du salon, vous pouvez le verrouiller et le déverrouiller en cliquant sur l'icône de cadenas située dans la barre de navigation. Quand un salon est verrouillé, aucun autre participant ne peut rejoindre.",
|
||||||
"slashCommandsHeader": "Commandes",
|
"slashCommandsHeader": "Commandes",
|
||||||
"slashCommandsText": "Les commandes suivantes sont disponibles :",
|
"slashCommandsText": "Les commandes suivantes sont disponibles :",
|
||||||
"slashCommandsBullets": [
|
"slashCommandsBullets": ["changer de pseudo", "effectuer une action", "effacer votre historique de messages", "lister toutes les commandes"],
|
||||||
"changer de pseudo",
|
|
||||||
"effectuer une action",
|
|
||||||
"effacer votre historique de messages",
|
|
||||||
"lister toutes les commandes"
|
|
||||||
],
|
|
||||||
"sound": "Son",
|
"sound": "Son",
|
||||||
|
"newMessageNotification": "Notification lors d'un nouveau message",
|
||||||
|
"desktopNotification": "Notification Système",
|
||||||
"welcomeModalCTA": "Ok",
|
"welcomeModalCTA": "Ok",
|
||||||
"lockedRoomHeader": "Ce salon est verrouillé",
|
"lockedRoomHeader": "Ce salon est verrouillé",
|
||||||
"helpTranslate": "Aidez-nous à traduire Darkwire!"
|
"helpTranslate": "Aidez-nous à traduire Darkwire!"
|
||||||
|
@ -22,6 +22,8 @@
|
|||||||
"slashCommandsHeader": "Comandi",
|
"slashCommandsHeader": "Comandi",
|
||||||
"slashCommandsText": "Sono disponibili i seguenti comandi:",
|
"slashCommandsText": "Sono disponibili i seguenti comandi:",
|
||||||
"sound": "Suono",
|
"sound": "Suono",
|
||||||
|
"newMessageNotification": "New message notification",
|
||||||
|
"desktopNotification": "Desktop Notification",
|
||||||
"typePlaceholder": "Digitare qui",
|
"typePlaceholder": "Digitare qui",
|
||||||
"unlockedRoom": "{username} ha aperto la stanza",
|
"unlockedRoom": "{username} ha aperto la stanza",
|
||||||
"userJoined": "{username} si è unito",
|
"userJoined": "{username} si è unito",
|
||||||
|
@ -22,6 +22,8 @@
|
|||||||
"slashCommandsHeader": "Slash-opdrachten",
|
"slashCommandsHeader": "Slash-opdrachten",
|
||||||
"slashCommandsText": "De volgende slash-opdrachten zijn beschikbaar:",
|
"slashCommandsText": "De volgende slash-opdrachten zijn beschikbaar:",
|
||||||
"sound": "Geluid",
|
"sound": "Geluid",
|
||||||
|
"newMessageNotification": "New message notification",
|
||||||
|
"desktopNotification": "Desktop Notification",
|
||||||
"typePlaceholder": "Typ hier",
|
"typePlaceholder": "Typ hier",
|
||||||
"unlockedRoom": "{username} heeft de kamer ontgrendeld",
|
"unlockedRoom": "{username} heeft de kamer ontgrendeld",
|
||||||
"userJoined": "{username} is lid geworden",
|
"userJoined": "{username} is lid geworden",
|
||||||
|
@ -25,13 +25,10 @@
|
|||||||
"lockRoomText": "Se sètz lo proprietari de la sala, podètz clavar e desclavar en clicar l’icòna del cadenat de la barra de navigacion. Quand una sala es clavada, cap de participant pòt pas la rejónher .",
|
"lockRoomText": "Se sètz lo proprietari de la sala, podètz clavar e desclavar en clicar l’icòna del cadenat de la barra de navigacion. Quand una sala es clavada, cap de participant pòt pas la rejónher .",
|
||||||
"slashCommandsHeader": "Comandas",
|
"slashCommandsHeader": "Comandas",
|
||||||
"slashCommandsText": "Las comandas seguentas son disponiblas :",
|
"slashCommandsText": "Las comandas seguentas son disponiblas :",
|
||||||
"slashCommandsBullets": [
|
"slashCommandsBullets": ["càmbia d’escais-nom", "realiza una accion", "escafa l’istoric de conversacion", "lista totas las comandas"],
|
||||||
"càmbia d’escais-nom",
|
|
||||||
"realiza una accion",
|
|
||||||
"escafa l’istoric de conversacion",
|
|
||||||
"lista totas las comandas"
|
|
||||||
],
|
|
||||||
"sound": "Son",
|
"sound": "Son",
|
||||||
|
"newMessageNotification": "New message notification",
|
||||||
|
"desktopNotification": "Desktop Notification",
|
||||||
"welcomeModalCTA": "D’acòrdi",
|
"welcomeModalCTA": "D’acòrdi",
|
||||||
"lockedRoomHeader": "Aquesta sala es clavada",
|
"lockedRoomHeader": "Aquesta sala es clavada",
|
||||||
"helpTranslate": "Ajudatz-nos a traduire Darkwire !"
|
"helpTranslate": "Ajudatz-nos a traduire Darkwire !"
|
||||||
|
@ -22,6 +22,8 @@
|
|||||||
"slashCommandsHeader": "斜线命令",
|
"slashCommandsHeader": "斜线命令",
|
||||||
"slashCommandsText": "可以使用以下斜杠命令:",
|
"slashCommandsText": "可以使用以下斜杠命令:",
|
||||||
"sound": "声音",
|
"sound": "声音",
|
||||||
|
"newMessageNotification": "New message notification",
|
||||||
|
"desktopNotification": "Desktop Notification",
|
||||||
"typePlaceholder": "在此输入",
|
"typePlaceholder": "在此输入",
|
||||||
"unlockedRoom": "{username} 解锁了房间",
|
"unlockedRoom": "{username} 解锁了房间",
|
||||||
"userJoined": "{username} 已加入",
|
"userJoined": "{username} 已加入",
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import Cookie from 'js-cookie';
|
import Cookie from 'js-cookie';
|
||||||
import {getTranslations} from 'i18n';
|
import { getTranslations } from 'i18n';
|
||||||
|
|
||||||
const language = Cookie.get('language') || navigator.language || 'en';
|
const language = Cookie.get('language') || navigator.language || 'en';
|
||||||
|
|
||||||
@ -9,6 +9,7 @@ const initialState = {
|
|||||||
windowIsFocused: true,
|
windowIsFocused: true,
|
||||||
unreadMessageCount: 0,
|
unreadMessageCount: 0,
|
||||||
soundIsEnabled: true,
|
soundIsEnabled: true,
|
||||||
|
notificationIsEnabled: true,
|
||||||
socketConnected: false,
|
socketConnected: false,
|
||||||
language,
|
language,
|
||||||
translations: getTranslations(language)
|
translations: getTranslations(language)
|
||||||
@ -47,6 +48,11 @@ const app = (state = initialState, action) => {
|
|||||||
...state,
|
...state,
|
||||||
soundIsEnabled: action.payload,
|
soundIsEnabled: action.payload,
|
||||||
}
|
}
|
||||||
|
case 'TOGGLE_NOTIFICATION_ENABLED':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
notificationIsEnabled: action.payload,
|
||||||
|
}
|
||||||
case 'TOGGLE_SOCKET_CONNECTED':
|
case 'TOGGLE_SOCKET_CONNECTED':
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
|
@ -14,6 +14,7 @@ describe('App reducer', () => {
|
|||||||
modalComponent: null,
|
modalComponent: null,
|
||||||
scrolledToBottom: true,
|
scrolledToBottom: true,
|
||||||
socketConnected: false,
|
socketConnected: false,
|
||||||
|
notificationIsEnabled: true,
|
||||||
soundIsEnabled: true,
|
soundIsEnabled: true,
|
||||||
translations: { path: 'test' },
|
translations: { path: 'test' },
|
||||||
unreadMessageCount: 0,
|
unreadMessageCount: 0,
|
||||||
|
50
client/src/utils/notifications.js
Normal file
50
client/src/utils/notifications.js
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import beepFile from 'audio/beep.mp3'
|
||||||
|
|
||||||
|
|
||||||
|
const showNotification = (title, message, avatarUrl) => {
|
||||||
|
const notifBody = {
|
||||||
|
body: message,
|
||||||
|
tag: 'darkwire',
|
||||||
|
silent: true, // we play our own sounds
|
||||||
|
};
|
||||||
|
|
||||||
|
const notification = new Notification(title, notifBody);
|
||||||
|
const handleVisibilityChange = () => {
|
||||||
|
if (document.visibilityState === 'visible') {
|
||||||
|
// The tab has become visible so clear the now-stale Notification.
|
||||||
|
notification.close();
|
||||||
|
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// Focus window on click
|
||||||
|
notification.onclick = function () {
|
||||||
|
window.focus();
|
||||||
|
notification.close();
|
||||||
|
};
|
||||||
|
document.addEventListener('visibilitychange', handleVisibilityChange);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const notify = (title, content) => {
|
||||||
|
if (!('Notification' in window)) {
|
||||||
|
alert('This browser does not support desktop notification');
|
||||||
|
}
|
||||||
|
// Let's check whether notification permissions have already been granted
|
||||||
|
else if (Notification.permission === 'granted') {
|
||||||
|
// If it's okay let's create a notification
|
||||||
|
showNotification(title, content);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, we need to ask the user for permission
|
||||||
|
else if (Notification.permission !== 'denied') {
|
||||||
|
Notification.requestPermission().then(function (permission) {
|
||||||
|
// If the user accepts, let's create a notification
|
||||||
|
if (permission === 'granted') {
|
||||||
|
showNotification(title, content);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const beep = (window.Audio && new window.Audio(beepFile)) || { play: () => { }}
|
||||||
|
|
||||||
|
export default { notify, beep };
|
Loading…
x
Reference in New Issue
Block a user