diff --git a/README.md b/README.md
index 4538075ee..584868d7c 100644
--- a/README.md
+++ b/README.md
@@ -188,6 +188,8 @@ Customization details can be found in the [Customization doc](docs/customization
Soapbox FE is based on [Gab Social](https://code.gab.com/gab/social/gab-social)'s frontend which is in turn based on [Mastodon](https://github.com/tootsuite/mastodon/)'s frontend.
+- `static/sounds/chat.mp3` and `static/sounds/chat.oga` are from [notificationsounds.com](https://notificationsounds.com/notification-sounds/intuition-561) licensed under CC BY 4.0.
+
Soapbox FE is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
diff --git a/app/soapbox/actions/settings.js b/app/soapbox/actions/settings.js
index 4432fa6e0..097265814 100644
--- a/app/soapbox/actions/settings.js
+++ b/app/soapbox/actions/settings.js
@@ -32,6 +32,7 @@ const defaultSettings = ImmutableMap({
chats: ImmutableMap({
panes: ImmutableList(),
mainWindow: 'minimized',
+ sound: true,
}),
home: ImmutableMap({
diff --git a/app/soapbox/actions/streaming.js b/app/soapbox/actions/streaming.js
index a581ae0fe..b3ca60fb3 100644
--- a/app/soapbox/actions/streaming.js
+++ b/app/soapbox/actions/streaming.js
@@ -55,7 +55,17 @@ export function connectTimelineStream(timelineId, path, pollingRefresh = null, a
dispatch(fetchFilters());
break;
case 'pleroma:chat_update':
- dispatch({ type: STREAMING_CHAT_UPDATE, chat: JSON.parse(data.payload), me: getState().get('me') });
+ dispatch((dispatch, getState) => {
+ const chat = JSON.parse(data.payload);
+ const messageOwned = !(chat.last_message && chat.last_message.account_id !== getState().get('me'));
+
+ dispatch({
+ type: STREAMING_CHAT_UPDATE,
+ chat,
+ // Only play sounds for recipient messages
+ meta: !messageOwned && getSettings(getState()).getIn(['chats', 'sound']) && { sound: 'chat' },
+ });
+ });
break;
}
},
diff --git a/app/soapbox/components/helmet.js b/app/soapbox/components/helmet.js
index f3bff8740..c9c1eb5a6 100644
--- a/app/soapbox/components/helmet.js
+++ b/app/soapbox/components/helmet.js
@@ -36,6 +36,7 @@ class SoapboxHelmet extends React.Component {
{children}
diff --git a/app/soapbox/features/chats/components/audio_toggle.js b/app/soapbox/features/chats/components/audio_toggle.js
new file mode 100644
index 000000000..9b207273a
--- /dev/null
+++ b/app/soapbox/features/chats/components/audio_toggle.js
@@ -0,0 +1,61 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { connect } from 'react-redux';
+import { injectIntl, defineMessages } from 'react-intl';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import Icon from 'soapbox/components/icon';
+import { changeSetting, getSettings } from 'soapbox/actions/settings';
+import SettingToggle from 'soapbox/features/notifications/components/setting_toggle';
+
+const messages = defineMessages({
+ switchToOn: { id: 'chats.audio_toggle_on', defaultMessage: 'Audio notification on' },
+ switchToOff: { id: 'chats.audio_toggle_off', defaultMessage: 'Audio notification off' },
+});
+
+const mapStateToProps = state => {
+ return {
+ settings: getSettings(state),
+ };
+};
+
+const mapDispatchToProps = (dispatch) => ({
+ toggleAudio(setting) {
+ dispatch(changeSetting(['chats', 'sound'], setting));
+ },
+});
+
+export default @connect(mapStateToProps, mapDispatchToProps)
+@injectIntl
+class AudioToggle extends React.PureComponent {
+
+ static propTypes = {
+ intl: PropTypes.object.isRequired,
+ settings: ImmutablePropTypes.map.isRequired,
+ toggleAudio: PropTypes.func,
+ showLabel: PropTypes.bool,
+ };
+
+ handleToggleAudio = () => {
+ this.props.toggleAudio(this.props.settings.getIn(['chats', 'sound']) === true ? false : true);
+ }
+
+ render() {
+ const { intl, settings, showLabel } = this.props;
+ let toggle = (
+ , unchecked: }} ariaLabel={settings.get('chats', 'sound') === true ? intl.formatMessage(messages.switchToOff) : intl.formatMessage(messages.switchToOn)} />
+ );
+
+ if (showLabel) {
+ toggle = (
+ , unchecked: }} label={settings.get('chats', 'sound') === true ? intl.formatMessage(messages.switchToOff) : intl.formatMessage(messages.switchToOn)} />
+ );
+ }
+
+ return (
+
+ {toggle}
+
+ );
+ }
+
+}
diff --git a/app/soapbox/features/chats/components/chat_box.js b/app/soapbox/features/chats/components/chat_box.js
index ee2b7cd20..cded7fd70 100644
--- a/app/soapbox/features/chats/components/chat_box.js
+++ b/app/soapbox/features/chats/components/chat_box.js
@@ -94,6 +94,7 @@ class ChatBox extends ImmutablePureComponent {
}
handleKeyDown = (e) => {
+ this.markRead();
if (e.key === 'Enter' && e.shiftKey) {
this.insertLine();
e.preventDefault();
@@ -122,17 +123,6 @@ class ChatBox extends ImmutablePureComponent {
onSetInputRef(el);
};
- componentDidUpdate(prevProps) {
- const markReadConditions = [
- () => this.props.chat !== undefined,
- () => document.activeElement === this.inputElem,
- () => this.props.chat.get('unread') > 0,
- ];
-
- if (markReadConditions.every(c => c() === true))
- this.markRead();
- }
-
handleRemoveFile = (e) => {
this.setState({ attachment: undefined, resetFileKey: fileKeyGen() });
}
diff --git a/app/soapbox/features/chats/components/chat_panes.js b/app/soapbox/features/chats/components/chat_panes.js
index 1bd7167bd..876ff9666 100644
--- a/app/soapbox/features/chats/components/chat_panes.js
+++ b/app/soapbox/features/chats/components/chat_panes.js
@@ -11,6 +11,7 @@ import { makeGetChat } from 'soapbox/selectors';
import { openChat, toggleMainWindow } from 'soapbox/actions/chats';
import ChatWindow from './chat_window';
import { shortNumberFormat } from 'soapbox/utils/numbers';
+import AudioToggle from 'soapbox/features/chats/components/audio_toggle';
const addChatsToPanes = (state, panesData) => {
const getChat = makeGetChat();
@@ -62,6 +63,7 @@ class ChatPanes extends ImmutablePureComponent {
+
next => action => {
diff --git a/app/styles/chats.scss b/app/styles/chats.scss
index 5a8752987..6cb6ef929 100644
--- a/app/styles/chats.scss
+++ b/app/styles/chats.scss
@@ -94,6 +94,41 @@
overflow: hidden;
}
}
+
+ .audio-toggle .react-toggle-thumb {
+ height: 14px;
+ width: 14px;
+ border: 1px solid var(--brand-color--med);
+ }
+
+ .audio-toggle .react-toggle {
+ height: 16px;
+ top: 4px;
+ }
+
+ .audio-toggle .react-toggle-track {
+ height: 16px;
+ width: 34px;
+ background-color: var(--accent-color);
+ }
+
+ .audio-toggle .react-toggle-track-check {
+ left: 4px;
+ bottom: 4px;
+ }
+
+ .react-toggle--checked .react-toggle-thumb {
+ left: 19px;
+ }
+
+ .audio-toggle .react-toggle-track-x {
+ right: 4px;
+ bottom: 4px;
+ }
+
+ .fa {
+ font-size: 14px;
+ }
}
.chat-messages {
diff --git a/static/sounds/chat.mp3 b/static/sounds/chat.mp3
new file mode 100644
index 000000000..c86114292
Binary files /dev/null and b/static/sounds/chat.mp3 differ
diff --git a/static/sounds/chat.oga b/static/sounds/chat.oga
new file mode 100644
index 000000000..76d4bda7a
Binary files /dev/null and b/static/sounds/chat.oga differ