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
lolsob-rspec
Ondřej Hruška 2017-07-30 18:36:28 +02:00 committed by beatrix
parent 041e5f4b83
commit 53bad3a721
14 changed files with 279 additions and 162 deletions

View File

@ -24,7 +24,10 @@ import NotificationPurgeButtons from './notification_purge_buttons';
import { import {
deleteMarkedNotifications, deleteMarkedNotifications,
enterNotificationClearingMode, enterNotificationClearingMode,
markAllNotifications,
} from '../../../../mastodon/actions/notifications'; } 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) { onEnterCleaningMode(yes) {
dispatch(enterNotificationClearingMode(yes)); dispatch(enterNotificationClearingMode(yes));
}, },
onDeleteMarkedNotifications() { onDeleteMarked() {
dispatch(deleteMarkedNotifications()); 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 => ({ 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));

View File

@ -16,83 +16,45 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
const messages = defineMessages({ const messages = defineMessages({
enter : { id: 'notification_purge.start', defaultMessage: 'Enter notification cleaning mode' }, btnAll : { id: 'notification_purge.btn_all', defaultMessage: 'Select\nall' },
accept : { id: 'notification_purge.confirm', defaultMessage: 'Dismiss selected notifications' }, btnNone : { id: 'notification_purge.btn_none', defaultMessage: 'Select\nnone' },
abort : { id: 'notification_purge.abort', defaultMessage: 'Leave cleaning mode' }, btnInvert : { id: 'notification_purge.btn_invert', defaultMessage: 'Invert\nselection' },
btnApply : { id: 'notification_purge.btn_apply', defaultMessage: 'Clear\nselected' },
}); });
@injectIntl @injectIntl
export default class NotificationPurgeButtons extends ImmutablePureComponent { export default class NotificationPurgeButtons extends ImmutablePureComponent {
static propTypes = { static propTypes = {
// Nukes all marked notifications onDeleteMarked : PropTypes.func.isRequired,
onDeleteMarkedNotifications : PropTypes.func.isRequired, onMarkAll : PropTypes.func.isRequired,
// Enables or disables the mode onMarkNone : PropTypes.func.isRequired,
// and also clears the marked status of all notifications onInvert : PropTypes.func.isRequired,
onEnterCleaningMode : PropTypes.func.isRequired,
// Active state, changed via onStateChange()
active: PropTypes.bool.isRequired,
// i18n
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
markNewForDelete: PropTypes.bool,
}; };
onEnterBtnClick = () => {
this.props.onEnterCleaningMode(true);
}
onAcceptBtnClick = () => {
this.props.onDeleteMarkedNotifications();
}
onAbortBtnClick = () => {
this.props.onEnterCleaningMode(false);
}
render () { render () {
const { intl, active } = this.props; const { intl, markNewForDelete } = this.props;
const msgEnter = intl.formatMessage(messages.enter);
const msgAccept = intl.formatMessage(messages.accept);
const msgAbort = intl.formatMessage(messages.abort);
let enterButton, acceptButton, abortButton;
if (active) {
acceptButton = (
<button
className='active'
aria-label={msgAccept}
title={msgAccept}
onClick={this.onAcceptBtnClick}
>
<i className='fa fa-check' />
</button>
);
abortButton = (
<button
className='active'
aria-label={msgAbort}
title={msgAbort}
onClick={this.onAbortBtnClick}
>
<i className='fa fa-times' />
</button>
);
} else {
enterButton = (
<button
aria-label={msgEnter}
title={msgEnter}
onClick={this.onEnterBtnClick}
>
<i className='fa fa-eraser' />
</button>
);
}
//className='active'
return ( return (
<div className='column-header__notif-cleaning-buttons'> <div className='column-header__notif-cleaning-buttons'>
{acceptButton}{abortButton}{enterButton} <button onClick={this.props.onMarkAll} className={markNewForDelete ? 'active' : ''}>
<b></b><br />{intl.formatMessage(messages.btnAll)}
</button>
<button onClick={this.props.onMarkNone} className={!markNewForDelete ? 'active' : ''}>
<b></b><br />{intl.formatMessage(messages.btnNone)}
</button>
<button onClick={this.props.onInvert}>
<b>¬</b><br />{intl.formatMessage(messages.btnInvert)}
</button>
<button onClick={this.props.onDeleteMarked}>
<i className='fa fa-trash' /><br />{intl.formatMessage(messages.btnApply)}
</button>
</div> </div>
); );
} }

View File

@ -45,6 +45,7 @@ const makeMapStateToProps = () => {
const mapStateToProps = (state, props) => ({ const mapStateToProps = (state, props) => ({
notification: getNotification(state, props.notification, props.accountId), notification: getNotification(state, props.notification, props.accountId),
settings: state.get('local_settings'), settings: state.get('local_settings'),
notifCleaning: state.getIn(['notifications', 'cleaningMode']),
}); });
return mapStateToProps; return mapStateToProps;

View File

@ -43,7 +43,7 @@ const mapDispatchToProps = dispatch => ({
}); });
const mapStateToProps = state => ({ const mapStateToProps = state => ({
revealed: state.getIn(['notifications', 'cleaningMode']), show: state.getIn(['notifications', 'cleaningMode']),
}); });
export default connect(mapStateToProps, mapDispatchToProps)(NotificationOverlay); export default connect(mapStateToProps, mapDispatchToProps)(NotificationOverlay);

View File

@ -24,7 +24,7 @@ export default class NotificationOverlay extends ImmutablePureComponent {
static propTypes = { static propTypes = {
notification : ImmutablePropTypes.map.isRequired, notification : ImmutablePropTypes.map.isRequired,
onMarkForDelete : PropTypes.func.isRequired, onMarkForDelete : PropTypes.func.isRequired,
revealed : PropTypes.bool.isRequired, show : PropTypes.bool.isRequired,
intl : PropTypes.object.isRequired, intl : PropTypes.object.isRequired,
}; };
@ -35,25 +35,27 @@ export default class NotificationOverlay extends ImmutablePureComponent {
} }
render () { render () {
const { notification, revealed, intl } = this.props; const { notification, show, intl } = this.props;
const active = notification.get('markedForDelete'); const active = notification.get('markedForDelete');
const label = intl.formatMessage(messages.markForDeletion); const label = intl.formatMessage(messages.markForDeletion);
return ( return show ? (
<div <div
aria-label={label} aria-label={label}
role='checkbox' role='checkbox'
aria-checked={active} aria-checked={active}
tabIndex={0} tabIndex={0}
className={`notification__dismiss-overlay ${active ? 'active' : ''} ${revealed ? 'show' : ''}`} className={`notification__dismiss-overlay ${active ? 'active' : ''}`}
onClick={this.onToggleMark} onClick={this.onToggleMark}
> >
<div className='notification__dismiss-overlay__ckbox' aria-hidden='true' title={label}> <div className='wrappy'>
{active ? (<i className='fa fa-check' />) : ''} <div className='ckbox' aria-hidden='true' title={label}>
{active ? (<i className='fa fa-check' />) : ''}
</div>
</div> </div>
</div> </div>
); ) : null;
} }
} }

View File

@ -29,5 +29,14 @@
"settings.navbar_under": "Navbar at the bottom (Mobile only)", "settings.navbar_under": "Navbar at the bottom (Mobile only)",
"status.collapse": "Collapse", "status.collapse": "Collapse",
"status.uncollapse": "Uncollapse", "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"
} }

View File

@ -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_REQUEST = 'NOTIFICATIONS_DELETE_MARKED_REQUEST';
export const NOTIFICATIONS_DELETE_MARKED_SUCCESS = 'NOTIFICATIONS_DELETE_MARKED_SUCCESS'; export const NOTIFICATIONS_DELETE_MARKED_SUCCESS = 'NOTIFICATIONS_DELETE_MARKED_SUCCESS';
export const NOTIFICATIONS_DELETE_MARKED_FAIL = 'NOTIFICATIONS_DELETE_MARKED_FAIL'; 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 export const NOTIFICATIONS_ENTER_CLEARING_MODE = 'NOTIFICATIONS_ENTER_CLEARING_MODE'; // arg: yes
// Unmark notifications (when the cleaning mode is left) // Unmark notifications (when the cleaning mode is left)
export const NOTIFICATIONS_UNMARK_ALL_FOR_DELETE = 'NOTIFICATIONS_UNMARK_ALL_FOR_DELETE'; export const NOTIFICATIONS_UNMARK_ALL_FOR_DELETE = 'NOTIFICATIONS_UNMARK_ALL_FOR_DELETE';
@ -210,13 +211,11 @@ export function deleteMarkedNotifications() {
}); });
if (ids.length === 0) { if (ids.length === 0) {
dispatch(enterNotificationClearingMode(false));
return; return;
} }
api(getState).delete(`/api/v1/notifications/destroy_multiple?ids[]=${ids.join('&ids[]=')}`).then(() => { api(getState).delete(`/api/v1/notifications/destroy_multiple?ids[]=${ids.join('&ids[]=')}`).then(() => {
dispatch(deleteMarkedNotificationsSuccess()); dispatch(deleteMarkedNotificationsSuccess());
dispatch(expandNotifications()); // Load more (to fill the empty space)
}).catch(error => { }).catch(error => {
console.error(error); console.error(error);
dispatch(deleteMarkedNotificationsFail(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() { export function deleteMarkedNotificationsRequest() {
return { return {
type: NOTIFICATIONS_DELETE_MARKED_REQUEST, type: NOTIFICATIONS_DELETE_MARKED_REQUEST,

View File

@ -7,6 +7,7 @@ export default class Column extends React.PureComponent {
static propTypes = { static propTypes = {
children: PropTypes.node, children: PropTypes.node,
extraClasses: PropTypes.string,
}; };
scrollTop () { scrollTop () {
@ -40,10 +41,10 @@ export default class Column extends React.PureComponent {
} }
render () { render () {
const { children } = this.props; const { children, extraClasses } = this.props;
return ( return (
<div role='region' className='column' ref={this.setRef}> <div role='region' className={`column ${extraClasses || ''}`} ref={this.setRef}>
{children} {children}
</div> </div>
); );

View File

@ -8,8 +8,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import NotificationPurgeButtonsContainer from '../../glitch/components/column/notif_cleaning_widget/container'; import NotificationPurgeButtonsContainer from '../../glitch/components/column/notif_cleaning_widget/container';
const messages = defineMessages({ const messages = defineMessages({
titleNotifClearing: { id: 'column.notifications_clearing', defaultMessage: 'Dismiss selected notifications:' }, enterNotifCleaning : { id: 'notification_purge.start', defaultMessage: 'Enter notification cleaning mode' },
titleNotifClearingShort: { id: 'column.notifications_clearing_short', defaultMessage: 'Dismiss selected:' },
}); });
@injectIntl @injectIntl
@ -28,6 +27,7 @@ export default class ColumnHeader extends React.PureComponent {
showBackButton: PropTypes.bool, showBackButton: PropTypes.bool,
notifCleaning: PropTypes.bool, // true only for the notification column notifCleaning: PropTypes.bool, // true only for the notification column
notifCleaningActive: PropTypes.bool, notifCleaningActive: PropTypes.bool,
onEnterCleaningMode: PropTypes.func,
children: PropTypes.node, children: PropTypes.node,
pinned: PropTypes.bool, pinned: PropTypes.bool,
onPin: PropTypes.func, onPin: PropTypes.func,
@ -39,6 +39,7 @@ export default class ColumnHeader extends React.PureComponent {
state = { state = {
collapsed: true, collapsed: true,
animating: false, animating: false,
animatingNCD: false,
}; };
handleToggleClick = (e) => { handleToggleClick = (e) => {
@ -71,16 +72,21 @@ export default class ColumnHeader extends React.PureComponent {
this.setState({ animating: false }); this.setState({ animating: false });
} }
handleTransitionEndNCD = () => {
this.setState({ animatingNCD: false });
}
onEnterCleaningMode = () => {
this.setState({ animatingNCD: true });
this.props.onEnterCleaningMode(!this.props.notifCleaningActive);
}
render () { render () {
const { intl, icon, active, children, pinned, onPin, multiColumn, showBackButton, notifCleaning, localSettings } = this.props; const { intl, icon, active, children, pinned, onPin, multiColumn, showBackButton, notifCleaning, notifCleaningActive } = this.props;
const { collapsed, animating } = this.state; const { collapsed, animating, animatingNCD } = this.state;
let title = this.props.title; 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', { const wrapperClassName = classNames('column-header__wrapper', {
'active': active, 'active': active,
@ -99,8 +105,20 @@ export default class ColumnHeader extends React.PureComponent {
'active': !collapsed, '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; let extraContent, pinButton, moveButtons, backButton, collapseButton;
//*glitch
const msgEnterNotifCleaning = intl.formatMessage(messages.enterNotifCleaning);
if (children) { if (children) {
extraContent = ( extraContent = (
<div key='extra-content' className='column-header__collapsible__extra'> <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}> <div role='button heading' tabIndex='0' className={buttonClassName} onClick={this.handleTitleClick}>
<i className={`fa fa-fw fa-${icon} column-header__icon`} /> <i className={`fa fa-fw fa-${icon} column-header__icon`} />
{title} {title}
<div className='column-header__buttons'> <div className='column-header__buttons'>
{notifCleaning ? (<NotificationPurgeButtonsContainer />) : null}
{backButton} {backButton}
{ notifCleaning ? (
<button
aria-label={msgEnterNotifCleaning}
title={msgEnterNotifCleaning}
onClick={this.onEnterCleaningMode}
className={notifCleaningButtonClassName}
>
<i className='fa fa-eraser' />
</button>
) : null}
{collapseButton} {collapseButton}
</div> </div>
</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={collapsibleClassName} onTransitionEnd={this.handleTransitionEnd}>
<div className='column-header__collapsible-inner'> <div className='column-header__collapsible-inner'>
{(!collapsed || animating) && collapsedContent} {(!collapsed || animating) && collapsedContent}

View File

@ -5,6 +5,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import Column from '../../components/column'; import Column from '../../components/column';
import ColumnHeader from '../../components/column_header'; import ColumnHeader from '../../components/column_header';
import { import {
enterNotificationClearingMode,
expandNotifications, expandNotifications,
scrollTopNotifications, scrollTopNotifications,
} from '../../actions/notifications'; } from '../../actions/notifications';
@ -36,7 +37,15 @@ const mapStateToProps = state => ({
notifCleaningActive: state.getIn(['notifications', 'cleaningMode']), notifCleaningActive: state.getIn(['notifications', 'cleaningMode']),
}); });
@connect(mapStateToProps) /* glitch */
const mapDispatchToProps = dispatch => ({
onEnterCleaningMode(yes) {
dispatch(enterNotificationClearingMode(yes));
},
dispatch,
});
@connect(mapStateToProps, mapDispatchToProps)
@injectIntl @injectIntl
export default class Notifications extends React.PureComponent { export default class Notifications extends React.PureComponent {
@ -52,6 +61,7 @@ export default class Notifications extends React.PureComponent {
hasMore: PropTypes.bool, hasMore: PropTypes.bool,
localSettings: ImmutablePropTypes.map, localSettings: ImmutablePropTypes.map,
notifCleaningActive: PropTypes.bool, notifCleaningActive: PropTypes.bool,
onEnterCleaningMode: PropTypes.func,
}; };
static defaultProps = { static defaultProps = {
@ -173,6 +183,7 @@ export default class Notifications extends React.PureComponent {
return ( return (
<Column <Column
ref={this.setColumnRef} ref={this.setColumnRef}
extraClasses={this.props.notifCleaningActive ? 'notif-cleaning' : null}
> >
<ColumnHeader <ColumnHeader
icon='bell' icon='bell'
@ -186,6 +197,7 @@ export default class Notifications extends React.PureComponent {
localSettings={this.props.localSettings} localSettings={this.props.localSettings}
notifCleaning notifCleaning
notifCleaningActive={this.props.notifCleaningActive} // this is used to toggle the header text notifCleaningActive={this.props.notifCleaningActive} // this is used to toggle the header text
onEnterCleaningMode={this.props.onEnterCleaningMode}
> >
<ColumnSettingsContainer /> <ColumnSettingsContainer />
</ColumnHeader> </ColumnHeader>

View File

@ -13,6 +13,7 @@ import {
NOTIFICATION_MARK_FOR_DELETE, NOTIFICATION_MARK_FOR_DELETE,
NOTIFICATIONS_DELETE_MARKED_FAIL, NOTIFICATIONS_DELETE_MARKED_FAIL,
NOTIFICATIONS_ENTER_CLEARING_MODE, NOTIFICATIONS_ENTER_CLEARING_MODE,
NOTIFICATIONS_MARK_ALL_FOR_DELETE,
} from '../actions/notifications'; } from '../actions/notifications';
import { ACCOUNT_BLOCK_SUCCESS } from '../actions/accounts'; import { ACCOUNT_BLOCK_SUCCESS } from '../actions/accounts';
import { TIMELINE_DELETE } from '../actions/timelines'; import { TIMELINE_DELETE } from '../actions/timelines';
@ -26,13 +27,15 @@ const initialState = ImmutableMap({
loaded: false, loaded: false,
isLoading: true, isLoading: true,
cleaningMode: false, 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, id: notification.id,
type: notification.type, type: notification.type,
account: notification.account.id, account: notification.account.id,
markedForDelete: false, markedForDelete: state.get('markNewForDelete'),
status: notification.status ? notification.status.id : null, status: notification.status ? notification.status.id : null,
}); });
@ -48,7 +51,7 @@ const normalizeNotification = (state, notification) => {
list = list.take(20); 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'); const loaded = state.get('loaded');
notifications.forEach((n, i) => { notifications.forEach((n, i) => {
items = items.set(i, notificationToMap(n)); items = items.set(i, notificationToMap(state, n));
}); });
if (state.get('next') === null) { if (state.get('next') === null) {
@ -74,7 +77,7 @@ const appendNormalizedNotifications = (state, notifications, next) => {
let items = ImmutableList(); let items = ImmutableList();
notifications.forEach((n, i) => { notifications.forEach((n, i) => {
items = items.set(i, notificationToMap(n)); items = items.set(i, notificationToMap(state, n));
}); });
return state 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) => { const unmarkAllForDelete = (state) => {
return state.update('items', list => list.map(item => item.set('markedForDelete', false))); 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) { export default function notifications(state = initialState, action) {
let st;
switch(action.type) { switch(action.type) {
case NOTIFICATIONS_REFRESH_REQUEST: case NOTIFICATIONS_REFRESH_REQUEST:
case NOTIFICATIONS_EXPAND_REQUEST: case NOTIFICATIONS_EXPAND_REQUEST:
@ -141,15 +156,31 @@ export default function notifications(state = initialState, action) {
return state.set('items', ImmutableList()).set('next', null); return state.set('items', ImmutableList()).set('next', null);
case TIMELINE_DELETE: case TIMELINE_DELETE:
return deleteByStatus(state, action.id); return deleteByStatus(state, action.id);
case NOTIFICATION_MARK_FOR_DELETE: case NOTIFICATION_MARK_FOR_DELETE:
return markForDelete(state, action.id, action.yes); return markForDelete(state, action.id, action.yes);
case NOTIFICATIONS_DELETE_MARKED_SUCCESS: 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: case NOTIFICATIONS_ENTER_CLEARING_MODE:
const st = state.set('cleaningMode', action.yes); st = state.set('cleaningMode', action.yes);
if (!action.yes) if (!action.yes) {
return unmarkAllForDelete(st); return unmarkAllForDelete(st).set('markNewForDelete', false);
else return st; } 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: default:
return state; return state;
} }

View File

@ -1,5 +1,6 @@
@import 'mixins'; @import 'mixins';
@import 'variables'; @import 'variables';
@import 'variables-glitch';
@import 'fonts/roboto'; @import 'fonts/roboto';
@import 'fonts/roboto-mono'; @import 'fonts/roboto-mono';
@import 'fonts/montserrat'; @import 'fonts/montserrat';

View File

@ -1,4 +1,5 @@
@import 'variables'; @import 'variables';
@import 'variables-glitch';
.app-body { .app-body {
-webkit-overflow-scrolling: touch; -webkit-overflow-scrolling: touch;
@ -451,62 +452,6 @@
cursor: pointer; 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 --- // --- Extra clickable area in the status gutter ---
.ui.wide { .ui.wide {
@mixin xtraspaces-full { @mixin xtraspaces-full {
@ -683,6 +628,12 @@
position: absolute; position: absolute;
} }
.notif-cleaning {
.status, .notification-follow {
padding-right: ($dismiss-overlay-width + 0.5rem);
}
}
.notification-follow { .notification-follow {
position: relative; position: relative;
@ -2479,17 +2430,88 @@ button.icon-button.active i.fa-retweet {
background: lighten($ui-base-color, 8%); 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 { .column-header__notif-cleaning-buttons {
display: flex; display: flex;
align-items: stretch; align-items: stretch;
justify-content: space-around;
button { button {
@extend .column-header__button; @extend .column-header__button;
padding-left: 12px; background: transparent;
padding-right: 12px; 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 { .column-header__collapsible {
@ -2508,6 +2530,15 @@ button.icon-button.active i.fa-retweet {
&.animating { &.animating {
overflow-y: hidden; overflow-y: hidden;
} }
// notif cleaning drawer
&.ncd {
transition: none;
&.collapsed {
max-height: 0;
opacity: 0.7;
}
}
} }
.column-header__collapsible-inner { .column-header__collapsible-inner {

View File

@ -0,0 +1,3 @@
// glitch-soc added variables
$dismiss-overlay-width: 4rem;