diff --git a/app/javascript/glitch/components/column/notif_cleaning_widget/container.js b/app/javascript/glitch/components/column/notif_cleaning_widget/container.js index bf079e3c44..d3507d7524 100644 --- a/app/javascript/glitch/components/column/notif_cleaning_widget/container.js +++ b/app/javascript/glitch/components/column/notif_cleaning_widget/container.js @@ -24,7 +24,10 @@ import NotificationPurgeButtons from './notification_purge_buttons'; import { deleteMarkedNotifications, enterNotificationClearingMode, + markAllNotifications, } from '../../../../mastodon/actions/notifications'; +import { defineMessages, injectIntl } from 'react-intl'; +import { openModal } from '../../../../mastodon/actions/modal'; // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * @@ -39,18 +42,39 @@ deleting notifications. */ -const mapDispatchToProps = dispatch => ({ +const messages = defineMessages({ + clearMessage: { id: 'notifications.marked_clear_confirmation', defaultMessage: 'Are you sure you want to permanently clear all selected notifications?' }, + clearConfirm: { id: 'notifications.marked_clear', defaultMessage: 'Clear selected notifications' }, +}); + +const mapDispatchToProps = (dispatch, { intl }) => ({ onEnterCleaningMode(yes) { dispatch(enterNotificationClearingMode(yes)); }, - onDeleteMarkedNotifications() { - dispatch(deleteMarkedNotifications()); + onDeleteMarked() { + dispatch(openModal('CONFIRM', { + message: intl.formatMessage(messages.clearMessage), + confirm: intl.formatMessage(messages.clearConfirm), + onConfirm: () => dispatch(deleteMarkedNotifications()), + })); + }, + + onMarkAll() { + dispatch(markAllNotifications(true)); + }, + + onMarkNone() { + dispatch(markAllNotifications(false)); + }, + + onInvert() { + dispatch(markAllNotifications(null)); }, }); const mapStateToProps = state => ({ - active: state.getIn(['notifications', 'cleaningMode']), + markNewForDelete: state.getIn(['notifications', 'markNewForDelete']), }); -export default connect(mapStateToProps, mapDispatchToProps)(NotificationPurgeButtons); +export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(NotificationPurgeButtons)); diff --git a/app/javascript/glitch/components/column/notif_cleaning_widget/notification_purge_buttons.js b/app/javascript/glitch/components/column/notif_cleaning_widget/notification_purge_buttons.js index e41572256f..62c887fb75 100644 --- a/app/javascript/glitch/components/column/notif_cleaning_widget/notification_purge_buttons.js +++ b/app/javascript/glitch/components/column/notif_cleaning_widget/notification_purge_buttons.js @@ -16,83 +16,45 @@ import ImmutablePureComponent from 'react-immutable-pure-component'; // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * const messages = defineMessages({ - enter : { id: 'notification_purge.start', defaultMessage: 'Enter notification cleaning mode' }, - accept : { id: 'notification_purge.confirm', defaultMessage: 'Dismiss selected notifications' }, - abort : { id: 'notification_purge.abort', defaultMessage: 'Leave cleaning mode' }, + btnAll : { id: 'notification_purge.btn_all', defaultMessage: 'Select\nall' }, + btnNone : { id: 'notification_purge.btn_none', defaultMessage: 'Select\nnone' }, + btnInvert : { id: 'notification_purge.btn_invert', defaultMessage: 'Invert\nselection' }, + btnApply : { id: 'notification_purge.btn_apply', defaultMessage: 'Clear\nselected' }, }); @injectIntl export default class NotificationPurgeButtons extends ImmutablePureComponent { static propTypes = { - // Nukes all marked notifications - onDeleteMarkedNotifications : PropTypes.func.isRequired, - // Enables or disables the mode - // and also clears the marked status of all notifications - onEnterCleaningMode : PropTypes.func.isRequired, - // Active state, changed via onStateChange() - active: PropTypes.bool.isRequired, - // i18n + onDeleteMarked : PropTypes.func.isRequired, + onMarkAll : PropTypes.func.isRequired, + onMarkNone : PropTypes.func.isRequired, + onInvert : PropTypes.func.isRequired, intl: PropTypes.object.isRequired, + markNewForDelete: PropTypes.bool, }; - onEnterBtnClick = () => { - this.props.onEnterCleaningMode(true); - } - - onAcceptBtnClick = () => { - this.props.onDeleteMarkedNotifications(); - } - - onAbortBtnClick = () => { - this.props.onEnterCleaningMode(false); - } - render () { - const { intl, active } = this.props; - - const msgEnter = intl.formatMessage(messages.enter); - const msgAccept = intl.formatMessage(messages.accept); - const msgAbort = intl.formatMessage(messages.abort); - - let enterButton, acceptButton, abortButton; + const { intl, markNewForDelete } = this.props; - if (active) { - acceptButton = ( - - ); - abortButton = ( - - ); - } else { - enterButton = ( - - ); - } - return ( -
- {acceptButton}{abortButton}{enterButton} +
); } diff --git a/app/javascript/glitch/components/notification/container.js b/app/javascript/glitch/components/notification/container.js index 7d2590684c..e29d6ba60e 100644 --- a/app/javascript/glitch/components/notification/container.js +++ b/app/javascript/glitch/components/notification/container.js @@ -45,6 +45,7 @@ const makeMapStateToProps = () => { const mapStateToProps = (state, props) => ({ notification: getNotification(state, props.notification, props.accountId), settings: state.get('local_settings'), + notifCleaning: state.getIn(['notifications', 'cleaningMode']), }); return mapStateToProps; diff --git a/app/javascript/glitch/components/notification/overlay/container.js b/app/javascript/glitch/components/notification/overlay/container.js index 019b78d0b2..089f615f08 100644 --- a/app/javascript/glitch/components/notification/overlay/container.js +++ b/app/javascript/glitch/components/notification/overlay/container.js @@ -43,7 +43,7 @@ const mapDispatchToProps = dispatch => ({ }); const mapStateToProps = state => ({ - revealed: state.getIn(['notifications', 'cleaningMode']), + show: state.getIn(['notifications', 'cleaningMode']), }); export default connect(mapStateToProps, mapDispatchToProps)(NotificationOverlay); diff --git a/app/javascript/glitch/components/notification/overlay/notification_overlay.js b/app/javascript/glitch/components/notification/overlay/notification_overlay.js index 73eda718f9..aaca95cacb 100644 --- a/app/javascript/glitch/components/notification/overlay/notification_overlay.js +++ b/app/javascript/glitch/components/notification/overlay/notification_overlay.js @@ -24,7 +24,7 @@ export default class NotificationOverlay extends ImmutablePureComponent { static propTypes = { notification : ImmutablePropTypes.map.isRequired, onMarkForDelete : PropTypes.func.isRequired, - revealed : PropTypes.bool.isRequired, + show : PropTypes.bool.isRequired, intl : PropTypes.object.isRequired, }; @@ -35,25 +35,27 @@ export default class NotificationOverlay extends ImmutablePureComponent { } render () { - const { notification, revealed, intl } = this.props; + const { notification, show, intl } = this.props; const active = notification.get('markedForDelete'); const label = intl.formatMessage(messages.markForDeletion); - return ( + return show ? (
- - ); + ) : null; } } diff --git a/app/javascript/glitch/locales/en.json b/app/javascript/glitch/locales/en.json index 21616f556b..7ec381de12 100644 --- a/app/javascript/glitch/locales/en.json +++ b/app/javascript/glitch/locales/en.json @@ -29,5 +29,14 @@ "settings.navbar_under": "Navbar at the bottom (Mobile only)", "status.collapse": "Collapse", "status.uncollapse": "Uncollapse", - "notification.markForDeletion": "Mark for deletion" + + "notification.markForDeletion": "Mark for deletion", + "notifications.clear": "Clear all my notifications", + "notifications.marked_clear_confirmation": "Are you sure you want to permanently clear all selected notifications?", + "notifications.marked_clear": "Clear selected notifications", + + "notification_purge.btn_all": "Select\nall", + "notification_purge.btn_none": "Select\nnone", + "notification_purge.btn_invert": "Invert\nselection", + "notification_purge.btn_apply": "Clear\nselected" } diff --git a/app/javascript/mastodon/actions/notifications.js b/app/javascript/mastodon/actions/notifications.js index fca26516a2..ebdf213223 100644 --- a/app/javascript/mastodon/actions/notifications.js +++ b/app/javascript/mastodon/actions/notifications.js @@ -10,6 +10,7 @@ export const NOTIFICATIONS_UPDATE = 'NOTIFICATIONS_UPDATE'; export const NOTIFICATIONS_DELETE_MARKED_REQUEST = 'NOTIFICATIONS_DELETE_MARKED_REQUEST'; export const NOTIFICATIONS_DELETE_MARKED_SUCCESS = 'NOTIFICATIONS_DELETE_MARKED_SUCCESS'; export const NOTIFICATIONS_DELETE_MARKED_FAIL = 'NOTIFICATIONS_DELETE_MARKED_FAIL'; +export const NOTIFICATIONS_MARK_ALL_FOR_DELETE = 'NOTIFICATIONS_MARK_ALL_FOR_DELETE'; export const NOTIFICATIONS_ENTER_CLEARING_MODE = 'NOTIFICATIONS_ENTER_CLEARING_MODE'; // arg: yes // Unmark notifications (when the cleaning mode is left) export const NOTIFICATIONS_UNMARK_ALL_FOR_DELETE = 'NOTIFICATIONS_UNMARK_ALL_FOR_DELETE'; @@ -210,13 +211,11 @@ export function deleteMarkedNotifications() { }); if (ids.length === 0) { - dispatch(enterNotificationClearingMode(false)); return; } api(getState).delete(`/api/v1/notifications/destroy_multiple?ids[]=${ids.join('&ids[]=')}`).then(() => { dispatch(deleteMarkedNotificationsSuccess()); - dispatch(expandNotifications()); // Load more (to fill the empty space) }).catch(error => { console.error(error); dispatch(deleteMarkedNotificationsFail(error)); @@ -231,6 +230,13 @@ export function enterNotificationClearingMode(yes) { }; }; +export function markAllNotifications(yes) { + return { + type: NOTIFICATIONS_MARK_ALL_FOR_DELETE, + yes: yes, // true, false or null. null = invert + }; +}; + export function deleteMarkedNotificationsRequest() { return { type: NOTIFICATIONS_DELETE_MARKED_REQUEST, diff --git a/app/javascript/mastodon/components/column.js b/app/javascript/mastodon/components/column.js index 93f1d6260b..29c8f43899 100644 --- a/app/javascript/mastodon/components/column.js +++ b/app/javascript/mastodon/components/column.js @@ -7,6 +7,7 @@ export default class Column extends React.PureComponent { static propTypes = { children: PropTypes.node, + extraClasses: PropTypes.string, }; scrollTop () { @@ -40,10 +41,10 @@ export default class Column extends React.PureComponent { } render () { - const { children } = this.props; + const { children, extraClasses } = this.props; return ( -
+
{children}
); diff --git a/app/javascript/mastodon/components/column_header.js b/app/javascript/mastodon/components/column_header.js index 0459042061..9945fc2092 100644 --- a/app/javascript/mastodon/components/column_header.js +++ b/app/javascript/mastodon/components/column_header.js @@ -8,8 +8,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes'; import NotificationPurgeButtonsContainer from '../../glitch/components/column/notif_cleaning_widget/container'; const messages = defineMessages({ - titleNotifClearing: { id: 'column.notifications_clearing', defaultMessage: 'Dismiss selected notifications:' }, - titleNotifClearingShort: { id: 'column.notifications_clearing_short', defaultMessage: 'Dismiss selected:' }, + enterNotifCleaning : { id: 'notification_purge.start', defaultMessage: 'Enter notification cleaning mode' }, }); @injectIntl @@ -28,6 +27,7 @@ export default class ColumnHeader extends React.PureComponent { showBackButton: PropTypes.bool, notifCleaning: PropTypes.bool, // true only for the notification column notifCleaningActive: PropTypes.bool, + onEnterCleaningMode: PropTypes.func, children: PropTypes.node, pinned: PropTypes.bool, onPin: PropTypes.func, @@ -39,6 +39,7 @@ export default class ColumnHeader extends React.PureComponent { state = { collapsed: true, animating: false, + animatingNCD: false, }; handleToggleClick = (e) => { @@ -71,16 +72,21 @@ export default class ColumnHeader extends React.PureComponent { this.setState({ animating: false }); } + handleTransitionEndNCD = () => { + this.setState({ animatingNCD: false }); + } + + onEnterCleaningMode = () => { + this.setState({ animatingNCD: true }); + this.props.onEnterCleaningMode(!this.props.notifCleaningActive); + } + render () { - const { intl, icon, active, children, pinned, onPin, multiColumn, showBackButton, notifCleaning, localSettings } = this.props; - const { collapsed, animating } = this.state; + const { intl, icon, active, children, pinned, onPin, multiColumn, showBackButton, notifCleaning, notifCleaningActive } = this.props; + const { collapsed, animating, animatingNCD } = this.state; + let title = this.props.title; - if (notifCleaning && this.props.notifCleaningActive) { - title = intl.formatMessage(localSettings.getIn(['stretch']) ? - messages.titleNotifClearing : - messages.titleNotifClearingShort); - } const wrapperClassName = classNames('column-header__wrapper', { 'active': active, @@ -99,8 +105,20 @@ export default class ColumnHeader extends React.PureComponent { 'active': !collapsed, }); + const notifCleaningButtonClassName = classNames('column-header__button', { + 'active': notifCleaningActive, + }); + + const notifCleaningDrawerClassName = classNames('ncd column-header__collapsible', { + 'collapsed': !notifCleaningActive, + 'animating': animatingNCD, + }); + let extraContent, pinButton, moveButtons, backButton, collapseButton; + //*glitch + const msgEnterNotifCleaning = intl.formatMessage(messages.enterNotifCleaning); + if (children) { extraContent = (
@@ -149,14 +167,30 @@ export default class ColumnHeader extends React.PureComponent {
{title} -
- {notifCleaning ? () : null} {backButton} + { notifCleaning ? ( + + ) : null} {collapseButton}
+ { notifCleaning ? ( +
+
+ {(notifCleaningActive || animatingNCD) ? () : null } +
+
+ ) : null} +
{(!collapsed || animating) && collapsedContent} diff --git a/app/javascript/mastodon/features/notifications/index.js b/app/javascript/mastodon/features/notifications/index.js index 6a262a59e0..0d86d41cee 100644 --- a/app/javascript/mastodon/features/notifications/index.js +++ b/app/javascript/mastodon/features/notifications/index.js @@ -5,6 +5,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes'; import Column from '../../components/column'; import ColumnHeader from '../../components/column_header'; import { + enterNotificationClearingMode, expandNotifications, scrollTopNotifications, } from '../../actions/notifications'; @@ -36,7 +37,15 @@ const mapStateToProps = state => ({ notifCleaningActive: state.getIn(['notifications', 'cleaningMode']), }); -@connect(mapStateToProps) +/* glitch */ +const mapDispatchToProps = dispatch => ({ + onEnterCleaningMode(yes) { + dispatch(enterNotificationClearingMode(yes)); + }, + dispatch, +}); + +@connect(mapStateToProps, mapDispatchToProps) @injectIntl export default class Notifications extends React.PureComponent { @@ -52,6 +61,7 @@ export default class Notifications extends React.PureComponent { hasMore: PropTypes.bool, localSettings: ImmutablePropTypes.map, notifCleaningActive: PropTypes.bool, + onEnterCleaningMode: PropTypes.func, }; static defaultProps = { @@ -173,6 +183,7 @@ export default class Notifications extends React.PureComponent { return ( diff --git a/app/javascript/mastodon/reducers/notifications.js b/app/javascript/mastodon/reducers/notifications.js index dd81653d6a..ecce8dcb6e 100644 --- a/app/javascript/mastodon/reducers/notifications.js +++ b/app/javascript/mastodon/reducers/notifications.js @@ -13,6 +13,7 @@ import { NOTIFICATION_MARK_FOR_DELETE, NOTIFICATIONS_DELETE_MARKED_FAIL, NOTIFICATIONS_ENTER_CLEARING_MODE, + NOTIFICATIONS_MARK_ALL_FOR_DELETE, } from '../actions/notifications'; import { ACCOUNT_BLOCK_SUCCESS } from '../actions/accounts'; import { TIMELINE_DELETE } from '../actions/timelines'; @@ -26,13 +27,15 @@ const initialState = ImmutableMap({ loaded: false, isLoading: true, cleaningMode: false, + // notification removal mark of new notifs loaded whilst cleaningMode is true. + markNewForDelete: false, }); -const notificationToMap = notification => ImmutableMap({ +const notificationToMap = (state, notification) => ImmutableMap({ id: notification.id, type: notification.type, account: notification.account.id, - markedForDelete: false, + markedForDelete: state.get('markNewForDelete'), status: notification.status ? notification.status.id : null, }); @@ -48,7 +51,7 @@ const normalizeNotification = (state, notification) => { list = list.take(20); } - return list.unshift(notificationToMap(notification)); + return list.unshift(notificationToMap(state, notification)); }); }; @@ -57,7 +60,7 @@ const normalizeNotifications = (state, notifications, next) => { const loaded = state.get('loaded'); notifications.forEach((n, i) => { - items = items.set(i, notificationToMap(n)); + items = items.set(i, notificationToMap(state, n)); }); if (state.get('next') === null) { @@ -74,7 +77,7 @@ const appendNormalizedNotifications = (state, notifications, next) => { let items = ImmutableList(); notifications.forEach((n, i) => { - items = items.set(i, notificationToMap(n)); + items = items.set(i, notificationToMap(state, n)); }); return state @@ -109,6 +112,16 @@ const markForDelete = (state, notificationId, yes) => { })); }; +const markAllForDelete = (state, yes) => { + return state.update('items', list => list.map(item => { + if(yes !== null) { + return item.set('markedForDelete', yes); + } else { + return item.set('markedForDelete', !item.get('markedForDelete')); + } + })); +}; + const unmarkAllForDelete = (state) => { return state.update('items', list => list.map(item => item.set('markedForDelete', false))); }; @@ -118,6 +131,8 @@ const deleteMarkedNotifs = (state) => { }; export default function notifications(state = initialState, action) { + let st; + switch(action.type) { case NOTIFICATIONS_REFRESH_REQUEST: case NOTIFICATIONS_EXPAND_REQUEST: @@ -141,15 +156,31 @@ export default function notifications(state = initialState, action) { return state.set('items', ImmutableList()).set('next', null); case TIMELINE_DELETE: return deleteByStatus(state, action.id); + case NOTIFICATION_MARK_FOR_DELETE: return markForDelete(state, action.id, action.yes); + case NOTIFICATIONS_DELETE_MARKED_SUCCESS: - return deleteMarkedNotifs(state).set('isLoading', false).set('cleaningMode', false); + return deleteMarkedNotifs(state).set('isLoading', false); + case NOTIFICATIONS_ENTER_CLEARING_MODE: - const st = state.set('cleaningMode', action.yes); - if (!action.yes) - return unmarkAllForDelete(st); - else return st; + st = state.set('cleaningMode', action.yes); + if (!action.yes) { + return unmarkAllForDelete(st).set('markNewForDelete', false); + } else { + return st; + } + + case NOTIFICATIONS_MARK_ALL_FOR_DELETE: + st = state; + if (action.yes === null) { + // Toggle - this is a bit confusing, as it toggles the all-none mode + //st = st.set('markNewForDelete', !st.get('markNewForDelete')); + } else { + st = st.set('markNewForDelete', action.yes); + } + return markAllForDelete(st, action.yes); + default: return state; } diff --git a/app/javascript/styles/application.scss b/app/javascript/styles/application.scss index f418ba6993..b08b694498 100644 --- a/app/javascript/styles/application.scss +++ b/app/javascript/styles/application.scss @@ -1,5 +1,6 @@ @import 'mixins'; @import 'variables'; +@import 'variables-glitch'; @import 'fonts/roboto'; @import 'fonts/roboto-mono'; @import 'fonts/montserrat'; diff --git a/app/javascript/styles/components.scss b/app/javascript/styles/components.scss index fa44b825c2..fe74bae84f 100644 --- a/app/javascript/styles/components.scss +++ b/app/javascript/styles/components.scss @@ -1,4 +1,5 @@ @import 'variables'; +@import 'variables-glitch'; .app-body { -webkit-overflow-scrolling: touch; @@ -451,62 +452,6 @@ cursor: pointer; } -.notification__dismiss-overlay { - position: absolute; - left: 0; top: 0; right: 0; bottom: 0; - - $c1: #00000A; - $c2: #222228; - background: linear-gradient(to right, - rgba($c1, 0.1), - rgba($c1, 0.2) 60%, - rgba($c2, 1) 90%, - rgba($c2, 1)); - - z-index: 999; - align-items: center; - justify-content: flex-end; - cursor: pointer; - - display: none; - - &.show { - display: flex; - } - - // make it brighter - &.active { - $c: #222931; - background: linear-gradient(to right, - rgba($c, 0.1), - rgba($c, 0.2) 60%, - rgba($c, 1) 90%, - rgba($c, 1)); - } - - &:focus { - outline: 0 !important; - } -} - -.notification__dismiss-overlay__ckbox { - border: 2px solid #9baec8; - border-radius: 2px; - width: 30px; - height: 30px; - margin-right: 20px; - font-size: 20px; - color: #c3dcfd; - text-shadow: 0 0 5px black; - display: flex; - justify-content: center; - align-items: center; - - :focus & { - box-shadow: 0 0 2px 2px #3e6fc1; - } -} - // --- Extra clickable area in the status gutter --- .ui.wide { @mixin xtraspaces-full { @@ -683,6 +628,12 @@ position: absolute; } +.notif-cleaning { + .status, .notification-follow { + padding-right: ($dismiss-overlay-width + 0.5rem); + } +} + .notification-follow { position: relative; @@ -2479,17 +2430,88 @@ button.icon-button.active i.fa-retweet { background: lighten($ui-base-color, 8%); } } + + // glitch - added focus ring for keyboard navigation + &:focus { + text-shadow: 0 0 4px darken($ui-highlight-color, 5%); + } +} + +.scrollable > div > :first-child .notification__dismiss-overlay > .wrappy { + border-top: 1px solid $ui-base-color; +} + +.notification__dismiss-overlay { + overflow: hidden; + position: absolute; + top: 0; + right: 0; + bottom: -1px; + padding-left: 15px; // space for the box shadow to be visible + + z-index: 999; + align-items: center; + justify-content: flex-end; + cursor: pointer; + + display: flex; + + .wrappy { + width: $dismiss-overlay-width; + align-self: stretch; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + background: lighten($ui-base-color, 8%); + border-left: 1px solid lighten($ui-base-color, 20%); + box-shadow: 0 0 5px black; + border-bottom: 1px solid $ui-base-color; + } + + .ckbox { + border: 2px solid $ui-primary-color; + border-radius: 2px; + width: 30px; + height: 30px; + font-size: 20px; + color: $ui-primary-color; + text-shadow: 0 0 5px black; + display: flex; + justify-content: center; + align-items: center; + } + + &:focus { + outline: 0 !important; + + .ckbox { + box-shadow: 0 0 1px 1px $ui-highlight-color; + } + } } .column-header__notif-cleaning-buttons { display: flex; align-items: stretch; + justify-content: space-around; button { @extend .column-header__button; - padding-left: 12px; - padding-right: 12px; + background: transparent; + text-align: center; + padding: 10px 0; + white-space: pre-wrap; } + + b { + font-weight: bold; + } +} + +// The notifs drawer with no padding to have more space for the buttons +.column-header__collapsible-inner.nopad-drawer { + padding: 0; } .column-header__collapsible { @@ -2508,6 +2530,15 @@ button.icon-button.active i.fa-retweet { &.animating { overflow-y: hidden; } + + // notif cleaning drawer + &.ncd { + transition: none; + &.collapsed { + max-height: 0; + opacity: 0.7; + } + } } .column-header__collapsible-inner { diff --git a/app/javascript/styles/variables-glitch.scss b/app/javascript/styles/variables-glitch.scss new file mode 100644 index 0000000000..44d3322f2c --- /dev/null +++ b/app/javascript/styles/variables-glitch.scss @@ -0,0 +1,3 @@ +// glitch-soc added variables + +$dismiss-overlay-width: 4rem;