Skip to content

Commit

Permalink
Add notification quick-filter bar in the frontend app (mastodon#9399)
Browse files Browse the repository at this point in the history
* create FilterBar componer and its container, unstyled

* introduce basic styling for FilterBar

* add selection css

* allow FilterBar to display active CSS with js

* connect the FilterBar to the Redux state

* change getNotifications to use filter

* remove temporary comments

* add an option to turn the FilterBar off in settings

* fix showFilterBar data type to boolean

* fix eslint errors

* add English and Polish translations

* allowed filter bar overflow to accomodate for longer languages

* fix mispelled translation key

* add unified CSS look

* replace text in FilterBar with icons

* add tooltips

* replace text @ with an icon

* introduce simple and advanced filtering view

* add ability to toggle the advanced view

* add Polish translations

* change Advanced View description to be more clear

* make each filter flush notifications and load new ones, fixing pagination

* simplify getNotifications once frontend filtering is not needed for FilterBar

* add a semicolon

* Revert "simplify getNotifications once frontend filtering is not needed for FilterBar"

This reverts commit 9f4be78.

* reset filter to 'all' when turning off FilterBar
  • Loading branch information
pawelngei authored and hiyuki2578 committed Oct 2, 2019
1 parent 356ed3c commit 00b9600
Show file tree
Hide file tree
Showing 11 changed files with 244 additions and 7 deletions.
24 changes: 23 additions & 1 deletion app/javascript/mastodon/actions/notifications.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
importFetchedStatuses,
} from './importer';
import { defineMessages } from 'react-intl';
import { List as ImmutableList } from 'immutable';
import { unescapeHTML } from '../utils/html';
import { getFilters, regexFromFilters } from '../selectors';

Expand All @@ -18,6 +19,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_FILTER_SET = 'NOTIFICATIONS_FILTER_SET';

export const NOTIFICATIONS_CLEAR = 'NOTIFICATIONS_CLEAR';
export const NOTIFICATIONS_SCROLL_TOP = 'NOTIFICATIONS_SCROLL_TOP';

Expand Down Expand Up @@ -88,10 +91,16 @@ export function updateNotifications(notification, intlMessages, intlLocale) {

const excludeTypesFromSettings = state => state.getIn(['settings', 'notifications', 'shows']).filter(enabled => !enabled).keySeq().toJS();

const excludeTypesFromFilter = filter => {
const allTypes = ImmutableList(['follow', 'favourite', 'reblog', 'mention']);
return allTypes.filterNot(item => item === filter).toJS();
};

const noOp = () => {};

export function expandNotifications({ maxId } = {}, done = noOp) {
return (dispatch, getState) => {
const activeFilter = getState().getIn(['settings', 'notifications', 'quickFilter', 'active']);
const notifications = getState().get('notifications');
const isLoadingMore = !!maxId;

Expand All @@ -102,7 +111,9 @@ export function expandNotifications({ maxId } = {}, done = noOp) {

const params = {
max_id: maxId,
exclude_types: excludeTypesFromSettings(getState()),
exclude_types: activeFilter === 'all'
? excludeTypesFromSettings(getState())
: excludeTypesFromFilter(activeFilter),
};

if (!maxId && notifications.get('items').size > 0) {
Expand Down Expand Up @@ -167,3 +178,14 @@ export function scrollTopNotifications(top) {
top,
};
};

export function setFilter (filterType) {
return dispatch => {
dispatch({
type: NOTIFICATIONS_FILTER_SET,
path: ['notifications', 'quickFilter', 'active'],
value: filterType,
});
dispatch(expandNotifications());
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,11 @@ export default class ColumnSettings extends React.PureComponent {
render () {
const { settings, pushSettings, onChange, onClear } = this.props;

const alertStr = <FormattedMessage id='notifications.column_settings.alert' defaultMessage='Desktop notifications' />;
const showStr = <FormattedMessage id='notifications.column_settings.show' defaultMessage='Show in column' />;
const soundStr = <FormattedMessage id='notifications.column_settings.sound' defaultMessage='Play sound' />;
const filterShowStr = <FormattedMessage id='notifications.column_settings.filter_bar.show' defaultMessage='Show' />;
const filterAdvancedStr = <FormattedMessage id='notifications.column_settings.filter_bar.advanced' defaultMessage='Display all categories' />;
const alertStr = <FormattedMessage id='notifications.column_settings.alert' defaultMessage='Desktop notifications' />;
const showStr = <FormattedMessage id='notifications.column_settings.show' defaultMessage='Show in column' />;
const soundStr = <FormattedMessage id='notifications.column_settings.sound' defaultMessage='Play sound' />;

const showPushSettings = pushSettings.get('browserSupport') && pushSettings.get('isSubscribed');
const pushStr = showPushSettings && <FormattedMessage id='notifications.column_settings.push' defaultMessage='Push notifications' />;
Expand All @@ -34,6 +36,16 @@ export default class ColumnSettings extends React.PureComponent {
<ClearColumnButton onClick={onClear} />
</div>

<div role='group' aria-labelledby='notifications-filter-bar'>
<span id='notifications-filter-bar' className='column-settings__section'>
<FormattedMessage id='notifications.column_settings.filter_bar.category' defaultMessage='Quick filter bar' />
</span>
<div className='column-settings__row'>
<SettingToggle id='show-filter-bar' prefix='notifications' settings={settings} settingPath={['quickFilter', 'show']} onChange={onChange} label={filterShowStr} />
<SettingToggle id='show-filter-bar' prefix='notifications' settings={settings} settingPath={['quickFilter', 'advanced']} onChange={onChange} label={filterAdvancedStr} />
</div>
</div>

<div role='group' aria-labelledby='notifications-follow'>
<span id='notifications-follow' className='column-settings__section'><FormattedMessage id='notifications.column_settings.follow' defaultMessage='New followers:' /></span>

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import React from 'react';
import PropTypes from 'prop-types';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';

const tooltips = defineMessages({
mentions: { id: 'notifications.filter.mentions', defaultMessage: 'Mentions' },
favourites: { id: 'notifications.filter.favourites', defaultMessage: 'Favourites' },
boosts: { id: 'notifications.filter.boosts', defaultMessage: 'Boosts' },
follows: { id: 'notifications.filter.follows', defaultMessage: 'Follows' },
});

export default @injectIntl
class FilterBar extends React.PureComponent {

static propTypes = {
selectFilter: PropTypes.func.isRequired,
selectedFilter: PropTypes.string.isRequired,
advancedMode: PropTypes.bool.isRequired,
intl: PropTypes.object.isRequired,
};

onClick (notificationType) {
return () => this.props.selectFilter(notificationType);
}

render () {
const { selectedFilter, advancedMode, intl } = this.props;
const renderedElement = !advancedMode ? (
<div className='notification__filter-bar'>
<button
className={selectedFilter === 'all' ? 'active' : ''}
onClick={this.onClick('all')}
>
<FormattedMessage
id='notifications.filter.all'
defaultMessage='All'
/>
</button>
<button
className={selectedFilter === 'mention' ? 'active' : ''}
onClick={this.onClick('mention')}
>
<FormattedMessage
id='notifications.filter.mentions'
defaultMessage='Mentions'
/>
</button>
</div>
) : (
<div className='notification__filter-bar'>
<button
className={selectedFilter === 'all' ? 'active' : ''}
onClick={this.onClick('all')}
>
<FormattedMessage
id='notifications.filter.all'
defaultMessage='All'
/>
</button>
<button
className={selectedFilter === 'mention' ? 'active' : ''}
onClick={this.onClick('mention')}
title={intl.formatMessage(tooltips.mentions)}
>
<i className='fa fa-fw fa-at' />
</button>
<button
className={selectedFilter === 'favourite' ? 'active' : ''}
onClick={this.onClick('favourite')}
title={intl.formatMessage(tooltips.favourites)}
>
<i className='fa fa-fw fa-star' />
</button>
<button
className={selectedFilter === 'reblog' ? 'active' : ''}
onClick={this.onClick('reblog')}
title={intl.formatMessage(tooltips.boosts)}
>
<i className='fa fa-fw fa-retweet' />
</button>
<button
className={selectedFilter === 'follow' ? 'active' : ''}
onClick={this.onClick('follow')}
title={intl.formatMessage(tooltips.follows)}
>
<i className='fa fa-fw fa-user-plus' />
</button>
</div>
);
return renderedElement;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { connect } from 'react-redux';
import { defineMessages, injectIntl } from 'react-intl';
import ColumnSettings from '../components/column_settings';
import { changeSetting } from '../../../actions/settings';
import { setFilter } from '../../../actions/notifications';
import { clearNotifications } from '../../../actions/notifications';
import { changeAlerts as changePushNotifications } from '../../../actions/push_notifications';
import { openModal } from '../../../actions/modal';
Expand All @@ -21,6 +22,9 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
onChange (path, checked) {
if (path[0] === 'push') {
dispatch(changePushNotifications(path.slice(1), checked));
} else if (path[0] === 'quickFilter') {
dispatch(changeSetting(['notifications', ...path], checked));
dispatch(setFilter('all'));
} else {
dispatch(changeSetting(['notifications', ...path], checked));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { connect } from 'react-redux';
import FilterBar from '../components/filter_bar';
import { setFilter } from '../../../actions/notifications';

const makeMapStateToProps = state => ({
selectedFilter: state.getIn(['settings', 'notifications', 'quickFilter', 'active']),
advancedMode: state.getIn(['settings', 'notifications', 'quickFilter', 'advanced']),
});

const mapDispatchToProps = (dispatch) => ({
selectFilter (newActiveFilter) {
dispatch(setFilter(newActiveFilter));
},
});

export default connect(makeMapStateToProps, mapDispatchToProps)(FilterBar);
23 changes: 20 additions & 3 deletions app/javascript/mastodon/features/notifications/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
import NotificationContainer from './containers/notification_container';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import ColumnSettingsContainer from './containers/column_settings_container';
import FilterBarContainer from './containers/filter_bar_container';
import { createSelector } from 'reselect';
import { List as ImmutableList } from 'immutable';
import { debounce } from 'lodash';
Expand All @@ -20,11 +21,22 @@ const messages = defineMessages({
});

const getNotifications = createSelector([
state => state.getIn(['settings', 'notifications', 'quickFilter', 'show']),
state => state.getIn(['settings', 'notifications', 'quickFilter', 'active']),
state => ImmutableList(state.getIn(['settings', 'notifications', 'shows']).filter(item => !item).keys()),
state => state.getIn(['notifications', 'items']),
], (excludedTypes, notifications) => notifications.filterNot(item => item !== null && excludedTypes.includes(item.get('type'))));
], (showFilterBar, allowedType, excludedTypes, notifications) => {
if (!showFilterBar || allowedType === 'all') {
// used if user changed the notification settings after loading the notifications from the server
// otherwise a list of notifications will come pre-filtered from the backend
// we need to turn it off for FilterBar in order not to block ourselves from seeing a specific category
return notifications.filterNot(item => item !== null && excludedTypes.includes(item.get('type')));
}
return notifications.filter(item => item !== null && allowedType === item.get('type'));
});

const mapStateToProps = state => ({
showFilterBar: state.getIn(['settings', 'notifications', 'quickFilter', 'show']),
notifications: getNotifications(state),
isLoading: state.getIn(['notifications', 'isLoading'], true),
isUnread: state.getIn(['notifications', 'unread']) > 0,
Expand All @@ -38,6 +50,7 @@ class Notifications extends React.PureComponent {
static propTypes = {
columnId: PropTypes.string,
notifications: ImmutablePropTypes.list.isRequired,
showFilterBar: PropTypes.bool.isRequired,
dispatch: PropTypes.func.isRequired,
shouldUpdateScroll: PropTypes.func,
intl: PropTypes.object.isRequired,
Expand Down Expand Up @@ -117,12 +130,16 @@ class Notifications extends React.PureComponent {
}

render () {
const { intl, notifications, shouldUpdateScroll, isLoading, isUnread, columnId, multiColumn, hasMore } = this.props;
const { intl, notifications, shouldUpdateScroll, isLoading, isUnread, columnId, multiColumn, hasMore, showFilterBar } = this.props;
const pinned = !!columnId;
const emptyMessage = <FormattedMessage id='empty_column.notifications' defaultMessage="You don't have any notifications yet. Interact with others to start the conversation." />;

let scrollableContent = null;

const filterBarContainer = showFilterBar
? (<FilterBarContainer />)
: null;

if (isLoading && this.scrollableContent) {
scrollableContent = this.scrollableContent;
} else if (notifications.size > 0 || hasMore) {
Expand Down Expand Up @@ -179,7 +196,7 @@ class Notifications extends React.PureComponent {
>
<ColumnSettingsContainer />
</ColumnHeader>

{filterBarContainer}
{scrollContainer}
</Column>
);
Expand Down
8 changes: 8 additions & 0 deletions app/javascript/mastodon/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,14 @@
"notification.reblog": "{name} boosted your status",
"notifications.clear": "Clear notifications",
"notifications.clear_confirmation": "Are you sure you want to permanently clear all your notifications?",
"notifications.filter.all": "All",
"notifications.filter.mentions": "Mentions",
"notifications.filter.favourites": "Favourites",
"notifications.filter.boosts": "Boosts",
"notifications.filter.follows": "Follows",
"notifications.column_settings.filter_bar.category": "Quick filter bar",
"notifications.column_settings.filter_bar.show": "Show",
"notifications.column_settings.filter_bar.advanced": "Display all categories",
"notifications.column_settings.alert": "Desktop notifications",
"notifications.column_settings.favourite": "Favourites:",
"notifications.column_settings.follow": "New followers:",
Expand Down
8 changes: 8 additions & 0 deletions app/javascript/mastodon/locales/pl.json
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,14 @@
"notification.reblog": "{name} podbił(a) Twój wpis",
"notifications.clear": "Wyczyść powiadomienia",
"notifications.clear_confirmation": "Czy na pewno chcesz bezpowrotnie usunąć wszystkie powiadomienia?",
"notifications.filter.all": "Wszystkie",
"notifications.filter.mentions": "Wspomnienia",
"notifications.filter.favourites": "Ulubione",
"notifications.filter.boosts": "Podbicia",
"notifications.filter.follows": "Śledzenia",
"notifications.column_settings.filter_bar.category": "Szybkie filtrowanie",
"notifications.column_settings.filter_bar.show": "Pokaż",
"notifications.column_settings.filter_bar.advanced": "Wyświetl wszystkie kategorie",
"notifications.column_settings.alert": "Powiadomienia na pulpicie",
"notifications.column_settings.favourite": "Dodanie do ulubionych:",
"notifications.column_settings.follow": "Nowi śledzący:",
Expand Down
3 changes: 3 additions & 0 deletions app/javascript/mastodon/reducers/notifications.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
NOTIFICATIONS_EXPAND_SUCCESS,
NOTIFICATIONS_EXPAND_REQUEST,
NOTIFICATIONS_EXPAND_FAIL,
NOTIFICATIONS_FILTER_SET,
NOTIFICATIONS_CLEAR,
NOTIFICATIONS_SCROLL_TOP,
} from '../actions/notifications';
Expand Down Expand Up @@ -98,6 +99,8 @@ export default function notifications(state = initialState, action) {
return state.set('isLoading', true);
case NOTIFICATIONS_EXPAND_FAIL:
return state.set('isLoading', false);
case NOTIFICATIONS_FILTER_SET:
return state.set('items', ImmutableList()).set('hasMore', true);
case NOTIFICATIONS_SCROLL_TOP:
return updateTop(state, action.top);
case NOTIFICATIONS_UPDATE:
Expand Down
8 changes: 8 additions & 0 deletions app/javascript/mastodon/reducers/settings.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { SETTING_CHANGE, SETTING_SAVE } from '../actions/settings';
import { NOTIFICATIONS_FILTER_SET } from '../actions/notifications';
import { COLUMN_ADD, COLUMN_REMOVE, COLUMN_MOVE, COLUMN_PARAMS_CHANGE } from '../actions/columns';
import { STORE_HYDRATE } from '../actions/store';
import { EMOJI_USE } from '../actions/emojis';
Expand Down Expand Up @@ -32,6 +33,12 @@ const initialState = ImmutableMap({
mention: true,
}),

quickFilter: ImmutableMap({
active: 'all',
show: true,
advanced: false,
}),

shows: ImmutableMap({
follow: true,
favourite: true,
Expand Down Expand Up @@ -112,6 +119,7 @@ export default function settings(state = initialState, action) {
switch(action.type) {
case STORE_HYDRATE:
return hydrate(state, action.state.get('settings'));
case NOTIFICATIONS_FILTER_SET:
case SETTING_CHANGE:
return state
.setIn(action.path, action.value)
Expand Down
Loading

0 comments on commit 00b9600

Please sign in to comment.