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