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 fixlolsob-rspec
parent
041e5f4b83
commit
53bad3a721
|
@ -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));
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
// glitch-soc added variables
|
||||||
|
|
||||||
|
$dismiss-overlay-width: 4rem;
|
Loading…
Reference in New Issue