about summary refs log tree commit diff
path: root/app
diff options
context:
space:
mode:
authorOndřej Hruška <ondra@ondrovo.com>2017-07-30 18:36:28 +0200
committerbeatrix <beatrix.bitrot@gmail.com>2017-07-30 12:36:28 -0400
commit6ff084dbbb06e5c2f131546829066435f2bf8f8a (patch)
tree06386f3ffadc2148b1f49be93cff1eb9ac685262 /app
parent9aaf3218d24bb8b3eb1de697243c13637398ab46 (diff)
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
Diffstat (limited to 'app')
-rw-r--r--app/javascript/glitch/components/column/notif_cleaning_widget/container.js34
-rw-r--r--app/javascript/glitch/components/column/notif_cleaning_widget/notification_purge_buttons.js86
-rw-r--r--app/javascript/glitch/components/notification/container.js1
-rw-r--r--app/javascript/glitch/components/notification/overlay/container.js2
-rw-r--r--app/javascript/glitch/components/notification/overlay/notification_overlay.js16
-rw-r--r--app/javascript/glitch/locales/en.json11
-rw-r--r--app/javascript/mastodon/actions/notifications.js10
-rw-r--r--app/javascript/mastodon/components/column.js5
-rw-r--r--app/javascript/mastodon/components/column_header.js56
-rw-r--r--app/javascript/mastodon/features/notifications/index.js14
-rw-r--r--app/javascript/mastodon/reducers/notifications.js51
-rw-r--r--app/javascript/styles/application.scss1
-rw-r--r--app/javascript/styles/components.scss147
-rw-r--r--app/javascript/styles/variables-glitch.scss3
14 files changed, 277 insertions, 160 deletions
diff --git a/app/javascript/glitch/components/column/notif_cleaning_widget/container.js b/app/javascript/glitch/components/column/notif_cleaning_widget/container.js
index bf079e3c4..d3507d752 100644
--- a/app/javascript/glitch/components/column/notif_cleaning_widget/container.js
+++ b/app/javascript/glitch/components/column/notif_cleaning_widget/container.js
@@ -24,7 +24,10 @@ import NotificationPurgeButtons from './notification_purge_buttons';
 import {
   deleteMarkedNotifications,
   enterNotificationClearingMode,
+  markAllNotifications,
 } from '../../../../mastodon/actions/notifications';
+import { defineMessages, injectIntl } from 'react-intl';
+import { openModal } from '../../../../mastodon/actions/modal';
 
 //  * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
 
@@ -39,18 +42,39 @@ deleting notifications.
 
 */
 
-const mapDispatchToProps = dispatch => ({
+const messages = defineMessages({
+  clearMessage: { id: 'notifications.marked_clear_confirmation', defaultMessage: 'Are you sure you want to permanently clear all selected notifications?' },
+  clearConfirm: { id: 'notifications.marked_clear', defaultMessage: 'Clear selected notifications' },
+});
+
+const mapDispatchToProps = (dispatch, { intl }) => ({
   onEnterCleaningMode(yes) {
     dispatch(enterNotificationClearingMode(yes));
   },
 
-  onDeleteMarkedNotifications() {
-    dispatch(deleteMarkedNotifications());
+  onDeleteMarked() {
+    dispatch(openModal('CONFIRM', {
+      message: intl.formatMessage(messages.clearMessage),
+      confirm: intl.formatMessage(messages.clearConfirm),
+      onConfirm: () => dispatch(deleteMarkedNotifications()),
+    }));
+  },
+
+  onMarkAll() {
+    dispatch(markAllNotifications(true));
+  },
+
+  onMarkNone() {
+    dispatch(markAllNotifications(false));
+  },
+
+  onInvert() {
+    dispatch(markAllNotifications(null));
   },
 });
 
 const mapStateToProps = state => ({
-  active: state.getIn(['notifications', 'cleaningMode']),
+  markNewForDelete: state.getIn(['notifications', 'markNewForDelete']),
 });
 
-export default connect(mapStateToProps, mapDispatchToProps)(NotificationPurgeButtons);
+export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(NotificationPurgeButtons));
diff --git a/app/javascript/glitch/components/column/notif_cleaning_widget/notification_purge_buttons.js b/app/javascript/glitch/components/column/notif_cleaning_widget/notification_purge_buttons.js
index e41572256..62c887fb7 100644
--- a/app/javascript/glitch/components/column/notif_cleaning_widget/notification_purge_buttons.js
+++ b/app/javascript/glitch/components/column/notif_cleaning_widget/notification_purge_buttons.js
@@ -16,83 +16,45 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
 //  * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
 
 const messages = defineMessages({
-  enter : { id: 'notification_purge.start', defaultMessage: 'Enter notification cleaning mode' },
-  accept : { id: 'notification_purge.confirm', defaultMessage: 'Dismiss selected notifications' },
-  abort : { id: 'notification_purge.abort', defaultMessage: 'Leave cleaning mode' },
+  btnAll : { id: 'notification_purge.btn_all', defaultMessage: 'Select\nall' },
+  btnNone : { id: 'notification_purge.btn_none', defaultMessage: 'Select\nnone' },
+  btnInvert : { id: 'notification_purge.btn_invert', defaultMessage: 'Invert\nselection' },
+  btnApply : { id: 'notification_purge.btn_apply', defaultMessage: 'Clear\nselected' },
 });
 
 @injectIntl
 export default class NotificationPurgeButtons extends ImmutablePureComponent {
 
   static propTypes = {
-    // Nukes all marked notifications
-    onDeleteMarkedNotifications : PropTypes.func.isRequired,
-    // Enables or disables the mode
-    // and also clears the marked status of all notifications
-    onEnterCleaningMode : PropTypes.func.isRequired,
-    // Active state, changed via onStateChange()
-    active: PropTypes.bool.isRequired,
-    // i18n
+    onDeleteMarked : PropTypes.func.isRequired,
+    onMarkAll : PropTypes.func.isRequired,
+    onMarkNone : PropTypes.func.isRequired,
+    onInvert : PropTypes.func.isRequired,
     intl: PropTypes.object.isRequired,
+    markNewForDelete: PropTypes.bool,
   };
 
-  onEnterBtnClick = () => {
-    this.props.onEnterCleaningMode(true);
-  }
-
-  onAcceptBtnClick = () => {
-    this.props.onDeleteMarkedNotifications();
-  }
-
-  onAbortBtnClick = () => {
-    this.props.onEnterCleaningMode(false);
-  }
-
   render () {
-    const { intl, active } = this.props;
-
-    const msgEnter = intl.formatMessage(messages.enter);
-    const msgAccept = intl.formatMessage(messages.accept);
-    const msgAbort = intl.formatMessage(messages.abort);
-
-    let enterButton, acceptButton, abortButton;
+    const { intl, markNewForDelete } = this.props;
 
-    if (active) {
-      acceptButton = (
-        <button
-          className='active'
-          aria-label={msgAccept}
-          title={msgAccept}
-          onClick={this.onAcceptBtnClick}
-        >
-          <i className='fa fa-check' />
+    //className='active'
+    return (
+      <div className='column-header__notif-cleaning-buttons'>
+        <button onClick={this.props.onMarkAll} className={markNewForDelete ? 'active' : ''}>
+          <b>∀</b><br />{intl.formatMessage(messages.btnAll)}
         </button>
-      );
-      abortButton = (
-        <button
-          className='active'
-          aria-label={msgAbort}
-          title={msgAbort}
-          onClick={this.onAbortBtnClick}
-        >
-          <i className='fa fa-times' />
+
+        <button onClick={this.props.onMarkNone} className={!markNewForDelete ? 'active' : ''}>
+          <b>∅</b><br />{intl.formatMessage(messages.btnNone)}
         </button>
-      );
-    } else {
-      enterButton = (
-        <button
-          aria-label={msgEnter}
-          title={msgEnter}
-          onClick={this.onEnterBtnClick}
-        >
-          <i className='fa fa-eraser' />
+
+        <button onClick={this.props.onInvert}>
+          <b>¬</b><br />{intl.formatMessage(messages.btnInvert)}
         </button>
-      );
-    }
 
-    return (
-      <div className='column-header__notif-cleaning-buttons'>
-        {acceptButton}{abortButton}{enterButton}
+        <button onClick={this.props.onDeleteMarked}>
+          <i className='fa fa-trash' /><br />{intl.formatMessage(messages.btnApply)}
+        </button>
       </div>
     );
   }
diff --git a/app/javascript/glitch/components/notification/container.js b/app/javascript/glitch/components/notification/container.js
index 7d2590684..e29d6ba60 100644
--- a/app/javascript/glitch/components/notification/container.js
+++ b/app/javascript/glitch/components/notification/container.js
@@ -45,6 +45,7 @@ const makeMapStateToProps = () => {
   const mapStateToProps = (state, props) => ({
     notification: getNotification(state, props.notification, props.accountId),
     settings: state.get('local_settings'),
+    notifCleaning: state.getIn(['notifications', 'cleaningMode']),
   });
 
   return mapStateToProps;
diff --git a/app/javascript/glitch/components/notification/overlay/container.js b/app/javascript/glitch/components/notification/overlay/container.js
index 019b78d0b..089f615f0 100644
--- a/app/javascript/glitch/components/notification/overlay/container.js
+++ b/app/javascript/glitch/components/notification/overlay/container.js
@@ -43,7 +43,7 @@ const mapDispatchToProps = dispatch => ({
 });
 
 const mapStateToProps = state => ({
-  revealed: state.getIn(['notifications', 'cleaningMode']),
+  show: state.getIn(['notifications', 'cleaningMode']),
 });
 
 export default connect(mapStateToProps, mapDispatchToProps)(NotificationOverlay);
diff --git a/app/javascript/glitch/components/notification/overlay/notification_overlay.js b/app/javascript/glitch/components/notification/overlay/notification_overlay.js
index 73eda718f..aaca95cac 100644
--- a/app/javascript/glitch/components/notification/overlay/notification_overlay.js
+++ b/app/javascript/glitch/components/notification/overlay/notification_overlay.js
@@ -24,7 +24,7 @@ export default class NotificationOverlay extends ImmutablePureComponent {
   static propTypes = {
     notification    : ImmutablePropTypes.map.isRequired,
     onMarkForDelete : PropTypes.func.isRequired,
-    revealed        : PropTypes.bool.isRequired,
+    show            : PropTypes.bool.isRequired,
     intl            : PropTypes.object.isRequired,
   };
 
@@ -35,25 +35,27 @@ export default class NotificationOverlay extends ImmutablePureComponent {
   }
 
   render () {
-    const { notification, revealed, intl } = this.props;
+    const { notification, show, intl } = this.props;
 
     const active = notification.get('markedForDelete');
     const label = intl.formatMessage(messages.markForDeletion);
 
-    return (
+    return show ? (
       <div
         aria-label={label}
         role='checkbox'
         aria-checked={active}
         tabIndex={0}
-        className={`notification__dismiss-overlay ${active ? 'active' : ''} ${revealed ? 'show' : ''}`}
+        className={`notification__dismiss-overlay ${active ? 'active' : ''}`}
         onClick={this.onToggleMark}
       >
-        <div className='notification__dismiss-overlay__ckbox' aria-hidden='true' title={label}>
-          {active ? (<i className='fa fa-check' />) : ''}
+        <div className='wrappy'>
+          <div className='ckbox' aria-hidden='true' title={label}>
+            {active ? (<i className='fa fa-check' />) : ''}
+          </div>
         </div>
       </div>
-    );
+    ) : null;
   }
 
 }
diff --git a/app/javascript/glitch/locales/en.json b/app/javascript/glitch/locales/en.json
index 21616f556..7ec381de1 100644
--- a/app/javascript/glitch/locales/en.json
+++ b/app/javascript/glitch/locales/en.json
@@ -29,5 +29,14 @@
   "settings.navbar_under": "Navbar at the bottom (Mobile only)",
   "status.collapse": "Collapse",
   "status.uncollapse": "Uncollapse",
-  "notification.markForDeletion": "Mark for deletion"
+
+  "notification.markForDeletion": "Mark for deletion",
+  "notifications.clear": "Clear all my notifications",
+  "notifications.marked_clear_confirmation": "Are you sure you want to permanently clear all selected notifications?",
+  "notifications.marked_clear": "Clear selected notifications",
+
+  "notification_purge.btn_all": "Select\nall",
+  "notification_purge.btn_none": "Select\nnone",
+  "notification_purge.btn_invert": "Invert\nselection",
+  "notification_purge.btn_apply": "Clear\nselected"
 }
diff --git a/app/javascript/mastodon/actions/notifications.js b/app/javascript/mastodon/actions/notifications.js
index fca26516a..ebdf21322 100644
--- a/app/javascript/mastodon/actions/notifications.js
+++ b/app/javascript/mastodon/actions/notifications.js
@@ -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,
diff --git a/app/javascript/mastodon/components/column.js b/app/javascript/mastodon/components/column.js
index 93f1d6260..29c8f4389 100644
--- a/app/javascript/mastodon/components/column.js
+++ b/app/javascript/mastodon/components/column.js
@@ -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>
     );
diff --git a/app/javascript/mastodon/components/column_header.js b/app/javascript/mastodon/components/column_header.js
index 045904206..9945fc209 100644
--- a/app/javascript/mastodon/components/column_header.js
+++ b/app/javascript/mastodon/components/column_header.js
@@ -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}
diff --git a/app/javascript/mastodon/features/notifications/index.js b/app/javascript/mastodon/features/notifications/index.js
index 6a262a59e..0d86d41ce 100644
--- a/app/javascript/mastodon/features/notifications/index.js
+++ b/app/javascript/mastodon/features/notifications/index.js
@@ -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>
diff --git a/app/javascript/mastodon/reducers/notifications.js b/app/javascript/mastodon/reducers/notifications.js
index dd81653d6..ecce8dcb6 100644
--- a/app/javascript/mastodon/reducers/notifications.js
+++ b/app/javascript/mastodon/reducers/notifications.js
@@ -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;
   }
diff --git a/app/javascript/styles/application.scss b/app/javascript/styles/application.scss
index f418ba699..b08b69449 100644
--- a/app/javascript/styles/application.scss
+++ b/app/javascript/styles/application.scss
@@ -1,5 +1,6 @@
 @import 'mixins';
 @import 'variables';
+@import 'variables-glitch';
 @import 'fonts/roboto';
 @import 'fonts/roboto-mono';
 @import 'fonts/montserrat';
diff --git a/app/javascript/styles/components.scss b/app/javascript/styles/components.scss
index fa44b825c..fe74bae84 100644
--- a/app/javascript/styles/components.scss
+++ b/app/javascript/styles/components.scss
@@ -1,4 +1,5 @@
 @import 'variables';
+@import 'variables-glitch';
 
 .app-body {
   -webkit-overflow-scrolling: touch;
@@ -451,62 +452,6 @@
   cursor: pointer;
 }
 
-.notification__dismiss-overlay {
-  position: absolute;
-  left: 0; top: 0; right: 0; bottom: 0;
-
-  $c1: #00000A;
-  $c2: #222228;
-  background: linear-gradient(to right,
-    rgba($c1, 0.1),
-    rgba($c1, 0.2) 60%,
-    rgba($c2, 1) 90%,
-    rgba($c2, 1));
-
-  z-index: 999;
-  align-items: center;
-  justify-content: flex-end;
-  cursor: pointer;
-
-  display: none;
-
-  &.show {
-    display: flex;
-  }
-
-  // make it brighter
-  &.active {
-    $c: #222931;
-    background: linear-gradient(to right,
-      rgba($c, 0.1),
-      rgba($c, 0.2) 60%,
-      rgba($c, 1) 90%,
-      rgba($c, 1));
-  }
-
-  &:focus {
-    outline: 0 !important;
-  }
-}
-
-.notification__dismiss-overlay__ckbox {
-  border: 2px solid #9baec8;
-  border-radius: 2px;
-  width: 30px;
-  height: 30px;
-  margin-right: 20px;
-  font-size: 20px;
-  color: #c3dcfd;
-  text-shadow: 0 0 5px black;
-  display: flex;
-  justify-content: center;
-  align-items: center;
-
-  :focus & {
-    box-shadow: 0 0 2px 2px #3e6fc1;
-  }
-}
-
 // --- Extra clickable area in the status gutter ---
 .ui.wide {
   @mixin xtraspaces-full {
@@ -683,6 +628,12 @@
   position: absolute;
 }
 
+.notif-cleaning {
+  .status, .notification-follow {
+    padding-right: ($dismiss-overlay-width + 0.5rem);
+  }
+}
+
 .notification-follow {
   position: relative;
 
@@ -2479,17 +2430,88 @@ button.icon-button.active i.fa-retweet {
       background: lighten($ui-base-color, 8%);
     }
   }
+
+  // glitch - added focus ring for keyboard navigation
+  &:focus {
+    text-shadow: 0 0 4px darken($ui-highlight-color, 5%);
+  }
+}
+
+.scrollable > div > :first-child .notification__dismiss-overlay > .wrappy {
+  border-top: 1px solid $ui-base-color;
+}
+
+.notification__dismiss-overlay {
+  overflow: hidden;
+  position: absolute;
+  top: 0;
+  right: 0;
+  bottom: -1px;
+  padding-left: 15px; // space for the box shadow to be visible
+
+  z-index: 999;
+  align-items: center;
+  justify-content: flex-end;
+  cursor: pointer;
+
+  display: flex;
+
+  .wrappy {
+    width: $dismiss-overlay-width;
+    align-self: stretch;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    background: lighten($ui-base-color, 8%);
+    border-left: 1px solid lighten($ui-base-color, 20%);
+    box-shadow: 0 0 5px black;
+    border-bottom: 1px solid $ui-base-color;
+  }
+
+  .ckbox {
+    border: 2px solid $ui-primary-color;
+    border-radius: 2px;
+    width: 30px;
+    height: 30px;
+    font-size: 20px;
+    color: $ui-primary-color;
+    text-shadow: 0 0 5px black;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+  }
+
+  &:focus {
+    outline: 0 !important;
+
+    .ckbox {
+      box-shadow: 0 0 1px 1px $ui-highlight-color;
+    }
+  }
 }
 
 .column-header__notif-cleaning-buttons {
   display: flex;
   align-items: stretch;
+  justify-content: space-around;
 
   button {
     @extend .column-header__button;
-    padding-left: 12px;
-    padding-right: 12px;
+    background: transparent;
+    text-align: center;
+    padding: 10px 0;
+    white-space: pre-wrap;
   }
+
+  b {
+    font-weight: bold;
+  }
+}
+
+// The notifs drawer with no padding to have more space for the buttons
+.column-header__collapsible-inner.nopad-drawer {
+  padding: 0;
 }
 
 .column-header__collapsible {
@@ -2508,6 +2530,15 @@ button.icon-button.active i.fa-retweet {
   &.animating {
     overflow-y: hidden;
   }
+
+  // notif cleaning drawer
+  &.ncd {
+    transition: none;
+    &.collapsed {
+      max-height: 0;
+      opacity: 0.7;
+    }
+  }
 }
 
 .column-header__collapsible-inner {
diff --git a/app/javascript/styles/variables-glitch.scss b/app/javascript/styles/variables-glitch.scss
new file mode 100644
index 000000000..44d3322f2
--- /dev/null
+++ b/app/javascript/styles/variables-glitch.scss
@@ -0,0 +1,3 @@
+// glitch-soc added variables
+
+$dismiss-overlay-width: 4rem;