Improved notifications cleaning UI with set operations (#109)
* added notification cleaning drawer * bugfix * fully implemented set operations for notif cleaning * i18n for notif cleaning drawer & improved logic slightly. Also added a confirm dialog * - notif dismiss "overlay" now shoves the notif aside to avoid overlap - added focus ring to header buttons - removed notif overlay entirely from DOM if mode is disabled * removed comment * CSS tuning - inconsistent division lines fix
This commit is contained in:
@@ -10,6 +10,7 @@ export const NOTIFICATIONS_UPDATE = 'NOTIFICATIONS_UPDATE';
|
||||
export const NOTIFICATIONS_DELETE_MARKED_REQUEST = 'NOTIFICATIONS_DELETE_MARKED_REQUEST';
|
||||
export const NOTIFICATIONS_DELETE_MARKED_SUCCESS = 'NOTIFICATIONS_DELETE_MARKED_SUCCESS';
|
||||
export const NOTIFICATIONS_DELETE_MARKED_FAIL = 'NOTIFICATIONS_DELETE_MARKED_FAIL';
|
||||
export const NOTIFICATIONS_MARK_ALL_FOR_DELETE = 'NOTIFICATIONS_MARK_ALL_FOR_DELETE';
|
||||
export const NOTIFICATIONS_ENTER_CLEARING_MODE = 'NOTIFICATIONS_ENTER_CLEARING_MODE'; // arg: yes
|
||||
// Unmark notifications (when the cleaning mode is left)
|
||||
export const NOTIFICATIONS_UNMARK_ALL_FOR_DELETE = 'NOTIFICATIONS_UNMARK_ALL_FOR_DELETE';
|
||||
@@ -210,13 +211,11 @@ export function deleteMarkedNotifications() {
|
||||
});
|
||||
|
||||
if (ids.length === 0) {
|
||||
dispatch(enterNotificationClearingMode(false));
|
||||
return;
|
||||
}
|
||||
|
||||
api(getState).delete(`/api/v1/notifications/destroy_multiple?ids[]=${ids.join('&ids[]=')}`).then(() => {
|
||||
dispatch(deleteMarkedNotificationsSuccess());
|
||||
dispatch(expandNotifications()); // Load more (to fill the empty space)
|
||||
}).catch(error => {
|
||||
console.error(error);
|
||||
dispatch(deleteMarkedNotificationsFail(error));
|
||||
@@ -231,6 +230,13 @@ export function enterNotificationClearingMode(yes) {
|
||||
};
|
||||
};
|
||||
|
||||
export function markAllNotifications(yes) {
|
||||
return {
|
||||
type: NOTIFICATIONS_MARK_ALL_FOR_DELETE,
|
||||
yes: yes, // true, false or null. null = invert
|
||||
};
|
||||
};
|
||||
|
||||
export function deleteMarkedNotificationsRequest() {
|
||||
return {
|
||||
type: NOTIFICATIONS_DELETE_MARKED_REQUEST,
|
||||
|
||||
@@ -7,6 +7,7 @@ export default class Column extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
children: PropTypes.node,
|
||||
extraClasses: PropTypes.string,
|
||||
};
|
||||
|
||||
scrollTop () {
|
||||
@@ -40,10 +41,10 @@ export default class Column extends React.PureComponent {
|
||||
}
|
||||
|
||||
render () {
|
||||
const { children } = this.props;
|
||||
const { children, extraClasses } = this.props;
|
||||
|
||||
return (
|
||||
<div role='region' className='column' ref={this.setRef}>
|
||||
<div role='region' className={`column ${extraClasses || ''}`} ref={this.setRef}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -8,8 +8,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import NotificationPurgeButtonsContainer from '../../glitch/components/column/notif_cleaning_widget/container';
|
||||
|
||||
const messages = defineMessages({
|
||||
titleNotifClearing: { id: 'column.notifications_clearing', defaultMessage: 'Dismiss selected notifications:' },
|
||||
titleNotifClearingShort: { id: 'column.notifications_clearing_short', defaultMessage: 'Dismiss selected:' },
|
||||
enterNotifCleaning : { id: 'notification_purge.start', defaultMessage: 'Enter notification cleaning mode' },
|
||||
});
|
||||
|
||||
@injectIntl
|
||||
@@ -28,6 +27,7 @@ export default class ColumnHeader extends React.PureComponent {
|
||||
showBackButton: PropTypes.bool,
|
||||
notifCleaning: PropTypes.bool, // true only for the notification column
|
||||
notifCleaningActive: PropTypes.bool,
|
||||
onEnterCleaningMode: PropTypes.func,
|
||||
children: PropTypes.node,
|
||||
pinned: PropTypes.bool,
|
||||
onPin: PropTypes.func,
|
||||
@@ -39,6 +39,7 @@ export default class ColumnHeader extends React.PureComponent {
|
||||
state = {
|
||||
collapsed: true,
|
||||
animating: false,
|
||||
animatingNCD: false,
|
||||
};
|
||||
|
||||
handleToggleClick = (e) => {
|
||||
@@ -71,16 +72,21 @@ export default class ColumnHeader extends React.PureComponent {
|
||||
this.setState({ animating: false });
|
||||
}
|
||||
|
||||
handleTransitionEndNCD = () => {
|
||||
this.setState({ animatingNCD: false });
|
||||
}
|
||||
|
||||
onEnterCleaningMode = () => {
|
||||
this.setState({ animatingNCD: true });
|
||||
this.props.onEnterCleaningMode(!this.props.notifCleaningActive);
|
||||
}
|
||||
|
||||
render () {
|
||||
const { intl, icon, active, children, pinned, onPin, multiColumn, showBackButton, notifCleaning, localSettings } = this.props;
|
||||
const { collapsed, animating } = this.state;
|
||||
const { intl, icon, active, children, pinned, onPin, multiColumn, showBackButton, notifCleaning, notifCleaningActive } = this.props;
|
||||
const { collapsed, animating, animatingNCD } = this.state;
|
||||
|
||||
|
||||
let title = this.props.title;
|
||||
if (notifCleaning && this.props.notifCleaningActive) {
|
||||
title = intl.formatMessage(localSettings.getIn(['stretch']) ?
|
||||
messages.titleNotifClearing :
|
||||
messages.titleNotifClearingShort);
|
||||
}
|
||||
|
||||
const wrapperClassName = classNames('column-header__wrapper', {
|
||||
'active': active,
|
||||
@@ -99,8 +105,20 @@ export default class ColumnHeader extends React.PureComponent {
|
||||
'active': !collapsed,
|
||||
});
|
||||
|
||||
const notifCleaningButtonClassName = classNames('column-header__button', {
|
||||
'active': notifCleaningActive,
|
||||
});
|
||||
|
||||
const notifCleaningDrawerClassName = classNames('ncd column-header__collapsible', {
|
||||
'collapsed': !notifCleaningActive,
|
||||
'animating': animatingNCD,
|
||||
});
|
||||
|
||||
let extraContent, pinButton, moveButtons, backButton, collapseButton;
|
||||
|
||||
//*glitch
|
||||
const msgEnterNotifCleaning = intl.formatMessage(messages.enterNotifCleaning);
|
||||
|
||||
if (children) {
|
||||
extraContent = (
|
||||
<div key='extra-content' className='column-header__collapsible__extra'>
|
||||
@@ -149,14 +167,30 @@ export default class ColumnHeader extends React.PureComponent {
|
||||
<div role='button heading' tabIndex='0' className={buttonClassName} onClick={this.handleTitleClick}>
|
||||
<i className={`fa fa-fw fa-${icon} column-header__icon`} />
|
||||
{title}
|
||||
|
||||
<div className='column-header__buttons'>
|
||||
{notifCleaning ? (<NotificationPurgeButtonsContainer />) : null}
|
||||
{backButton}
|
||||
{ notifCleaning ? (
|
||||
<button
|
||||
aria-label={msgEnterNotifCleaning}
|
||||
title={msgEnterNotifCleaning}
|
||||
onClick={this.onEnterCleaningMode}
|
||||
className={notifCleaningButtonClassName}
|
||||
>
|
||||
<i className='fa fa-eraser' />
|
||||
</button>
|
||||
) : null}
|
||||
{collapseButton}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{ notifCleaning ? (
|
||||
<div className={notifCleaningDrawerClassName} onTransitionEnd={this.handleTransitionEndNCD}>
|
||||
<div className='column-header__collapsible-inner nopad-drawer'>
|
||||
{(notifCleaningActive || animatingNCD) ? (<NotificationPurgeButtonsContainer />) : null }
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className={collapsibleClassName} onTransitionEnd={this.handleTransitionEnd}>
|
||||
<div className='column-header__collapsible-inner'>
|
||||
{(!collapsed || animating) && collapsedContent}
|
||||
|
||||
@@ -5,6 +5,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import Column from '../../components/column';
|
||||
import ColumnHeader from '../../components/column_header';
|
||||
import {
|
||||
enterNotificationClearingMode,
|
||||
expandNotifications,
|
||||
scrollTopNotifications,
|
||||
} from '../../actions/notifications';
|
||||
@@ -36,7 +37,15 @@ const mapStateToProps = state => ({
|
||||
notifCleaningActive: state.getIn(['notifications', 'cleaningMode']),
|
||||
});
|
||||
|
||||
@connect(mapStateToProps)
|
||||
/* glitch */
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
onEnterCleaningMode(yes) {
|
||||
dispatch(enterNotificationClearingMode(yes));
|
||||
},
|
||||
dispatch,
|
||||
});
|
||||
|
||||
@connect(mapStateToProps, mapDispatchToProps)
|
||||
@injectIntl
|
||||
export default class Notifications extends React.PureComponent {
|
||||
|
||||
@@ -52,6 +61,7 @@ export default class Notifications extends React.PureComponent {
|
||||
hasMore: PropTypes.bool,
|
||||
localSettings: ImmutablePropTypes.map,
|
||||
notifCleaningActive: PropTypes.bool,
|
||||
onEnterCleaningMode: PropTypes.func,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
@@ -173,6 +183,7 @@ export default class Notifications extends React.PureComponent {
|
||||
return (
|
||||
<Column
|
||||
ref={this.setColumnRef}
|
||||
extraClasses={this.props.notifCleaningActive ? 'notif-cleaning' : null}
|
||||
>
|
||||
<ColumnHeader
|
||||
icon='bell'
|
||||
@@ -186,6 +197,7 @@ export default class Notifications extends React.PureComponent {
|
||||
localSettings={this.props.localSettings}
|
||||
notifCleaning
|
||||
notifCleaningActive={this.props.notifCleaningActive} // this is used to toggle the header text
|
||||
onEnterCleaningMode={this.props.onEnterCleaningMode}
|
||||
>
|
||||
<ColumnSettingsContainer />
|
||||
</ColumnHeader>
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
NOTIFICATION_MARK_FOR_DELETE,
|
||||
NOTIFICATIONS_DELETE_MARKED_FAIL,
|
||||
NOTIFICATIONS_ENTER_CLEARING_MODE,
|
||||
NOTIFICATIONS_MARK_ALL_FOR_DELETE,
|
||||
} from '../actions/notifications';
|
||||
import { ACCOUNT_BLOCK_SUCCESS } from '../actions/accounts';
|
||||
import { TIMELINE_DELETE } from '../actions/timelines';
|
||||
@@ -26,13 +27,15 @@ const initialState = ImmutableMap({
|
||||
loaded: false,
|
||||
isLoading: true,
|
||||
cleaningMode: false,
|
||||
// notification removal mark of new notifs loaded whilst cleaningMode is true.
|
||||
markNewForDelete: false,
|
||||
});
|
||||
|
||||
const notificationToMap = notification => ImmutableMap({
|
||||
const notificationToMap = (state, notification) => ImmutableMap({
|
||||
id: notification.id,
|
||||
type: notification.type,
|
||||
account: notification.account.id,
|
||||
markedForDelete: false,
|
||||
markedForDelete: state.get('markNewForDelete'),
|
||||
status: notification.status ? notification.status.id : null,
|
||||
});
|
||||
|
||||
@@ -48,7 +51,7 @@ const normalizeNotification = (state, notification) => {
|
||||
list = list.take(20);
|
||||
}
|
||||
|
||||
return list.unshift(notificationToMap(notification));
|
||||
return list.unshift(notificationToMap(state, notification));
|
||||
});
|
||||
};
|
||||
|
||||
@@ -57,7 +60,7 @@ const normalizeNotifications = (state, notifications, next) => {
|
||||
const loaded = state.get('loaded');
|
||||
|
||||
notifications.forEach((n, i) => {
|
||||
items = items.set(i, notificationToMap(n));
|
||||
items = items.set(i, notificationToMap(state, n));
|
||||
});
|
||||
|
||||
if (state.get('next') === null) {
|
||||
@@ -74,7 +77,7 @@ const appendNormalizedNotifications = (state, notifications, next) => {
|
||||
let items = ImmutableList();
|
||||
|
||||
notifications.forEach((n, i) => {
|
||||
items = items.set(i, notificationToMap(n));
|
||||
items = items.set(i, notificationToMap(state, n));
|
||||
});
|
||||
|
||||
return state
|
||||
@@ -109,6 +112,16 @@ const markForDelete = (state, notificationId, yes) => {
|
||||
}));
|
||||
};
|
||||
|
||||
const markAllForDelete = (state, yes) => {
|
||||
return state.update('items', list => list.map(item => {
|
||||
if(yes !== null) {
|
||||
return item.set('markedForDelete', yes);
|
||||
} else {
|
||||
return item.set('markedForDelete', !item.get('markedForDelete'));
|
||||
}
|
||||
}));
|
||||
};
|
||||
|
||||
const unmarkAllForDelete = (state) => {
|
||||
return state.update('items', list => list.map(item => item.set('markedForDelete', false)));
|
||||
};
|
||||
@@ -118,6 +131,8 @@ const deleteMarkedNotifs = (state) => {
|
||||
};
|
||||
|
||||
export default function notifications(state = initialState, action) {
|
||||
let st;
|
||||
|
||||
switch(action.type) {
|
||||
case NOTIFICATIONS_REFRESH_REQUEST:
|
||||
case NOTIFICATIONS_EXPAND_REQUEST:
|
||||
@@ -141,15 +156,31 @@ export default function notifications(state = initialState, action) {
|
||||
return state.set('items', ImmutableList()).set('next', null);
|
||||
case TIMELINE_DELETE:
|
||||
return deleteByStatus(state, action.id);
|
||||
|
||||
case NOTIFICATION_MARK_FOR_DELETE:
|
||||
return markForDelete(state, action.id, action.yes);
|
||||
|
||||
case NOTIFICATIONS_DELETE_MARKED_SUCCESS:
|
||||
return deleteMarkedNotifs(state).set('isLoading', false).set('cleaningMode', false);
|
||||
return deleteMarkedNotifs(state).set('isLoading', false);
|
||||
|
||||
case NOTIFICATIONS_ENTER_CLEARING_MODE:
|
||||
const st = state.set('cleaningMode', action.yes);
|
||||
if (!action.yes)
|
||||
return unmarkAllForDelete(st);
|
||||
else return st;
|
||||
st = state.set('cleaningMode', action.yes);
|
||||
if (!action.yes) {
|
||||
return unmarkAllForDelete(st).set('markNewForDelete', false);
|
||||
} else {
|
||||
return st;
|
||||
}
|
||||
|
||||
case NOTIFICATIONS_MARK_ALL_FOR_DELETE:
|
||||
st = state;
|
||||
if (action.yes === null) {
|
||||
// Toggle - this is a bit confusing, as it toggles the all-none mode
|
||||
//st = st.set('markNewForDelete', !st.get('markNewForDelete'));
|
||||
} else {
|
||||
st = st.set('markNewForDelete', action.yes);
|
||||
}
|
||||
return markAllForDelete(st, action.yes);
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user