diff options
author | Eugen Rochko <eugen@zeonfederated.com> | 2017-01-02 14:09:57 +0100 |
---|---|---|
committer | Eugen Rochko <eugen@zeonfederated.com> | 2017-01-02 14:09:57 +0100 |
commit | 65647a24720c1fa1dda832a8ce0f48f48f4cf358 (patch) | |
tree | 9f29be394bf72d110c773cc67610903118304718 | |
parent | 75122e162d7b933d34c6b694c6c5a0b2533f547f (diff) |
See #244 - Added notifications column settings to filter what's displayed
in the column and what appears as desktop notifications. Settings do not persist yet
7 files changed, 221 insertions, 8 deletions
diff --git a/app/assets/javascripts/components/actions/notifications.jsx b/app/assets/javascripts/components/actions/notifications.jsx index 6a8b1b05b..8bd835406 100644 --- a/app/assets/javascripts/components/actions/notifications.jsx +++ b/app/assets/javascripts/components/actions/notifications.jsx @@ -14,6 +14,8 @@ export const NOTIFICATIONS_EXPAND_REQUEST = 'NOTIFICATIONS_EXPAND_REQUEST'; export const NOTIFICATIONS_EXPAND_SUCCESS = 'NOTIFICATIONS_EXPAND_SUCCESS'; export const NOTIFICATIONS_EXPAND_FAIL = 'NOTIFICATIONS_EXPAND_FAIL'; +export const NOTIFICATIONS_SETTING_CHANGE = 'NOTIFICATIONS_SETTING_CHANGE'; + const fetchRelatedRelationships = (dispatch, notifications) => { const accountIds = notifications.filter(item => item.type === 'follow').map(item => item.account.id); @@ -23,7 +25,7 @@ const fetchRelatedRelationships = (dispatch, notifications) => { }; export function updateNotifications(notification, intlMessages, intlLocale) { - return dispatch => { + return (dispatch, getState) => { dispatch({ type: NOTIFICATIONS_UPDATE, notification, @@ -34,7 +36,7 @@ export function updateNotifications(notification, intlMessages, intlLocale) { fetchRelatedRelationships(dispatch, [notification]); // Desktop notifications - if (typeof window.Notification !== 'undefined') { + if (typeof window.Notification !== 'undefined' && getState().getIn(['notifications', 'settings', 'alerts', notification.type], false)) { const title = new IntlMessageFormat(intlMessages[`notification.${notification.type}`], intlLocale).format({ name: notification.account.display_name.length > 0 ? notification.account.display_name : notification.account.username }); const body = $('<p>').html(notification.status ? notification.status.content : '').text(); @@ -131,3 +133,11 @@ export function expandNotificationsFail(error) { error }; }; + +export function changeNotificationsSetting(key, checked) { + return { + type: NOTIFICATIONS_SETTING_CHANGE, + key, + checked + }; +}; diff --git a/app/assets/javascripts/components/features/notifications/components/column_settings.jsx b/app/assets/javascripts/components/features/notifications/components/column_settings.jsx new file mode 100644 index 000000000..b4035c20d --- /dev/null +++ b/app/assets/javascripts/components/features/notifications/components/column_settings.jsx @@ -0,0 +1,150 @@ +import PureRenderMixin from 'react-addons-pure-render-mixin'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import Toggle from 'react-toggle'; +import { Motion, spring } from 'react-motion'; +import { FormattedMessage } from 'react-intl'; + +const outerStyle = { + background: '#373b4a', + padding: '15px' +}; + +const iconStyle = { + fontSize: '16px', + padding: '15px', + position: 'absolute', + right: '0', + top: '-48px', + cursor: 'pointer' +}; + +const labelStyle = { + display: 'block', + lineHeight: '24px', + verticalAlign: 'middle' +}; + +const labelSpanStyle = { + display: 'inline-block', + verticalAlign: 'middle', + marginBottom: '14px', + marginLeft: '8px', + color: '#9baec8' +}; + +const sectionStyle = { + cursor: 'default', + display: 'block', + fontWeight: '500', + color: '#9baec8', + marginBottom: '10px' +}; + +const rowStyle = { + +}; + +const ColumnSettings = React.createClass({ + + propTypes: { + settings: ImmutablePropTypes.map.isRequired, + onChange: React.PropTypes.func.isRequired + }, + + getInitialState () { + return { + collapsed: true + }; + }, + + mixins: [PureRenderMixin], + + handleToggleCollapsed () { + this.setState({ collapsed: !this.state.collapsed }); + }, + + handleChange (key, e) { + this.props.onChange(key, e.target.checked); + }, + + render () { + const { settings } = this.props; + const { collapsed } = this.state; + + const alertStr = <FormattedMessage id='notifications.column_settings.alert' defaultMessage='Desktop notifications' />; + const showStr = <FormattedMessage id='notifications.column_settings.show' defaultMessage='Show in column' />; + + return ( + <div style={{ position: 'relative' }}> + <div style={{...iconStyle, color: collapsed ? '#9baec8' : '#fff', background: collapsed ? '#2f3441' : '#373b4a' }} onClick={this.handleToggleCollapsed}><i className='fa fa-sliders' /></div> + + <Motion defaultStyle={{ opacity: 0, height: 0 }} style={{ opacity: spring(collapsed ? 0 : 100), height: spring(collapsed ? 0 : 458) }}> + {({ opacity, height }) => + <div style={{ overflow: 'hidden', height: `${height}px`, opacity: opacity / 100 }}> + <div style={outerStyle}> + <span style={sectionStyle}><FormattedMessage id='notifications.column_settings.follow' defaultMessage='New followers:' /></span> + + <div style={rowStyle}> + <label style={labelStyle}> + <Toggle checked={settings.getIn(['alerts', 'follow'])} onChange={this.handleChange.bind(this, ['alerts', 'follow'])} /> + <span style={labelSpanStyle}>{alertStr}</span> + </label> + + <label style={labelStyle}> + <Toggle checked={settings.getIn(['shows', 'follow'])} onChange={this.handleChange.bind(this, ['shows', 'follow'])} /> + <span style={labelSpanStyle}>{showStr}</span> + </label> + </div> + + <span style={sectionStyle}><FormattedMessage id='notifications.column_settings.favourite' defaultMessage='Favourites:' /></span> + + <div style={rowStyle}> + <label style={labelStyle}> + <Toggle checked={settings.getIn(['alerts', 'favourite'])} onChange={this.handleChange.bind(this, ['alerts', 'favourite'])} /> + <span style={labelSpanStyle}>{alertStr}</span> + </label> + + <label style={labelStyle}> + <Toggle checked={settings.getIn(['shows', 'favourite'])} onChange={this.handleChange.bind(this, ['shows', 'favourite'])} /> + <span style={labelSpanStyle}>{showStr}</span> + </label> + </div> + + <span style={sectionStyle}><FormattedMessage id='notifications.column_settings.mention' defaultMessage='Mentions:' /></span> + + <div style={rowStyle}> + <label style={labelStyle}> + <Toggle checked={settings.getIn(['alerts', 'mention'])} onChange={this.handleChange.bind(this, ['alerts', 'mention'])} /> + <span style={labelSpanStyle}>{alertStr}</span> + </label> + + <label style={labelStyle}> + <Toggle checked={settings.getIn(['shows', 'mention'])} onChange={this.handleChange.bind(this, ['shows', 'mention'])} /> + <span style={labelSpanStyle}>{showStr}</span> + </label> + </div> + + <span style={sectionStyle}><FormattedMessage id='notifications.column_settings.reblog' defaultMessage='Boosts:' /></span> + + <div style={rowStyle}> + <label style={labelStyle}> + <Toggle checked={settings.getIn(['alerts', 'reblog'])} onChange={this.handleChange.bind(this, ['alerts', 'reblog'])} /> + <span style={labelSpanStyle}>{alertStr}</span> + </label> + + <label style={labelStyle}> + <Toggle checked={settings.getIn(['shows', 'reblog'])} onChange={this.handleChange.bind(this, ['shows', 'reblog'])} /> + <span style={labelSpanStyle}>{showStr}</span> + </label> + </div> + </div> + </div> + } + </Motion> + </div> + ); + } + +}); + +export default ColumnSettings; diff --git a/app/assets/javascripts/components/features/notifications/containers/column_settings_container.jsx b/app/assets/javascripts/components/features/notifications/containers/column_settings_container.jsx new file mode 100644 index 000000000..6907fd351 --- /dev/null +++ b/app/assets/javascripts/components/features/notifications/containers/column_settings_container.jsx @@ -0,0 +1,17 @@ +import { connect } from 'react-redux'; +import ColumnSettings from '../components/column_settings'; +import { changeNotificationsSetting } from '../../../actions/notifications'; + +const mapStateToProps = state => ({ + settings: state.getIn(['notifications', 'settings']) +}); + +const mapDispatchToProps = dispatch => ({ + + onChange (key, checked) { + dispatch(changeNotificationsSetting(key, checked)); + } + +}); + +export default connect(mapStateToProps, mapDispatchToProps)(ColumnSettings); diff --git a/app/assets/javascripts/components/features/notifications/index.jsx b/app/assets/javascripts/components/features/notifications/index.jsx index 218196cfd..7e706ad6a 100644 --- a/app/assets/javascripts/components/features/notifications/index.jsx +++ b/app/assets/javascripts/components/features/notifications/index.jsx @@ -9,13 +9,21 @@ import { import NotificationContainer from './containers/notification_container'; import { ScrollContainer } from 'react-router-scroll'; import { defineMessages, injectIntl } from 'react-intl'; +import ColumnSettingsContainer from './containers/column_settings_container'; +import { createSelector } from 'reselect'; +import Immutable from 'immutable'; const messages = defineMessages({ title: { id: 'column.notifications', defaultMessage: 'Notifications' } }); +const getNotifications = createSelector([ + state => Immutable.List(state.getIn(['notifications', 'settings', 'shows']).filter(item => !item).keys()), + state => state.getIn(['notifications', 'items']) +], (excludedTypes, notifications) => notifications.filterNot(item => excludedTypes.includes(item.get('type')))); + const mapStateToProps = state => ({ - notifications: state.getIn(['notifications', 'items']) + notifications: getNotifications(state) }); const Notifications = React.createClass({ @@ -23,7 +31,8 @@ const Notifications = React.createClass({ propTypes: { notifications: ImmutablePropTypes.list.isRequired, dispatch: React.PropTypes.func.isRequired, - trackScroll: React.PropTypes.bool + trackScroll: React.PropTypes.bool, + intl: React.PropTypes.object.isRequired }, getDefaultProps () { @@ -69,6 +78,7 @@ const Notifications = React.createClass({ } else { return ( <Column icon='bell' heading={intl.formatMessage(messages.title)}> + <ColumnSettingsContainer /> {scrollableArea} </Column> ); diff --git a/app/assets/javascripts/components/features/ui/components/column.jsx b/app/assets/javascripts/components/features/ui/components/column.jsx index c2060749a..c382e108d 100644 --- a/app/assets/javascripts/components/features/ui/components/column.jsx +++ b/app/assets/javascripts/components/features/ui/components/column.jsx @@ -40,7 +40,8 @@ const Column = React.createClass({ propTypes: { heading: React.PropTypes.string, - icon: React.PropTypes.string + icon: React.PropTypes.string, + children: React.PropTypes.node }, mixins: [PureRenderMixin], diff --git a/app/assets/javascripts/components/locales/en.jsx b/app/assets/javascripts/components/locales/en.jsx index 50007a7da..3d4a38919 100644 --- a/app/assets/javascripts/components/locales/en.jsx +++ b/app/assets/javascripts/components/locales/en.jsx @@ -52,7 +52,13 @@ const en = { "notification.follow": "{name} followed you", "notification.favourite": "{name} favourited your status", "notification.reblog": "{name} boosted your status", - "notification.mention": "{name} mentioned you" + "notification.mention": "{name} mentioned you", + "notifications.column_settings.alert": "Desktop notifications", + "notifications.column_settings.show": "Show in column", + "notifications.column_settings.follow": "New followers:", + "notifications.column_settings.favourite": "Favourites:", + "notifications.column_settings.mention": "Mentions:", + "notifications.column_settings.reblog": "Boosts:", }; export default en; diff --git a/app/assets/javascripts/components/reducers/notifications.jsx b/app/assets/javascripts/components/reducers/notifications.jsx index 617a833d2..e0d1ccf83 100644 --- a/app/assets/javascripts/components/reducers/notifications.jsx +++ b/app/assets/javascripts/components/reducers/notifications.jsx @@ -1,7 +1,8 @@ import { NOTIFICATIONS_UPDATE, NOTIFICATIONS_REFRESH_SUCCESS, - NOTIFICATIONS_EXPAND_SUCCESS + NOTIFICATIONS_EXPAND_SUCCESS, + NOTIFICATIONS_SETTING_CHANGE } from '../actions/notifications'; import { ACCOUNT_BLOCK_SUCCESS } from '../actions/accounts'; import Immutable from 'immutable'; @@ -9,7 +10,23 @@ import Immutable from 'immutable'; const initialState = Immutable.Map({ items: Immutable.List(), next: null, - loaded: false + loaded: false, + + settings: Immutable.Map({ + alerts: Immutable.Map({ + follow: true, + favourite: true, + reblog: true, + mention: true + }), + + shows: Immutable.Map({ + follow: true, + favourite: true, + reblog: true, + mention: true + }) + }) }); const notificationToMap = notification => Immutable.Map({ @@ -58,6 +75,8 @@ export default function notifications(state = initialState, action) { return appendNormalizedNotifications(state, action.notifications, action.next); case ACCOUNT_BLOCK_SUCCESS: return filterNotifications(state, action.relationship); + case NOTIFICATIONS_SETTING_CHANGE: + return state.setIn(['settings', ...action.key], action.checked); default: return state; } |