mirror of https://github.com/tootsuite/mastodon
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 fixpull/12504/head
parent
9aaf3218d2
commit
6ff084dbbb
|
@ -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));
|
||||
|
|
|
@ -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;
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
const { intl, markNewForDelete } = this.props;
|
||||
|
||||
//className='active'
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
@import 'mixins';
|
||||
@import 'variables';
|
||||
@import 'variables-glitch';
|
||||
@import 'fonts/roboto';
|
||||
@import 'fonts/roboto-mono';
|
||||
@import 'fonts/montserrat';
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
// glitch-soc added variables
|
||||
|
||||
$dismiss-overlay-width: 4rem;
|
Loading…
Reference in New Issue