New notification cleaning mode (#89)
This PR adds a new notification cleaning mode, super perfectly tuned for accessibility, and removes the previous notification cleaning functionality as it's now redundant. * w.i.p. notif clearing mode * Better CSS for selected notification and shorter text if Stretch is off * wip for rebase ~ * all working in notif clearing mode, except the actual removal * bulk delete route for piggo * cleaning + refactor. endpoint gives 422 for some reason * formatting * use the right route * fix broken destroy_multiple * load more notifs after succ cleaning * satisfy eslint * Removed CSS for the old notif delete button * Tabindex=0 is mandatory In order to make it possible to tab to this element you must have tab index = 0. Removing this violates WCAG and makes it impossible to use the interface without good eyesight and a mouse. So nobody with certain mobility impairments, vision impairments, or brain injuries would be able to use this feature if you don't have tabindex=0 * Corrected aria-label Previous label implied a different behavior from what actually happens * aria role localization & made the overlay behave like a checkbox * checkboxes css and better contrast * color tuning for the notif overlay * fanceh checkboxes etc and nice backgrounds * SHUT UP TRAVIS
This commit is contained in:
		
							parent
							
								
									0efd7e7406
								
							
						
					
					
						commit
						604654ccb4
					
				
					 20 changed files with 514 additions and 157 deletions
				
			
		|  | @ -33,6 +33,11 @@ class Api::V1::NotificationsController < Api::BaseController | |||
|     render_empty | ||||
|   end | ||||
| 
 | ||||
|   def destroy_multiple | ||||
|     current_account.notifications.where(id: params[:ids]).destroy_all | ||||
|     render_empty | ||||
|   end | ||||
| 
 | ||||
|   private | ||||
| 
 | ||||
|   def load_notifications | ||||
|  |  | |||
|  | @ -0,0 +1,56 @@ | |||
| /* | ||||
| 
 | ||||
| `<NotificationPurgeButtonsContainer>` | ||||
| ========================= | ||||
| 
 | ||||
| This container connects `<NotificationPurgeButtons>`s to the Redux store. | ||||
| 
 | ||||
| */ | ||||
| 
 | ||||
| //  * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
 | ||||
| 
 | ||||
| /* | ||||
| 
 | ||||
| Imports: | ||||
| -------- | ||||
| 
 | ||||
| */ | ||||
| 
 | ||||
| //  Package imports  //
 | ||||
| import { connect } from 'react-redux'; | ||||
| 
 | ||||
| //  Our imports  //
 | ||||
| import NotificationPurgeButtons from './notification_purge_buttons'; | ||||
| import { | ||||
|   deleteMarkedNotifications, | ||||
|   enterNotificationClearingMode, | ||||
| } from '../../../../mastodon/actions/notifications'; | ||||
| 
 | ||||
| //  * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
 | ||||
| 
 | ||||
| /* | ||||
| 
 | ||||
| Dispatch mapping: | ||||
| ----------------- | ||||
| 
 | ||||
| The `mapDispatchToProps()` function maps dispatches to our store to the | ||||
| various props of our component. We only need to provide a dispatch for | ||||
| deleting notifications. | ||||
| 
 | ||||
| */ | ||||
| 
 | ||||
| const mapDispatchToProps = dispatch => ({ | ||||
|   onEnterCleaningMode(yes) { | ||||
|     dispatch(enterNotificationClearingMode(yes)); | ||||
|   }, | ||||
| 
 | ||||
|   onDeleteMarkedNotifications() { | ||||
|     dispatch(deleteMarkedNotifications()); | ||||
|   }, | ||||
| }); | ||||
| 
 | ||||
| const mapStateToProps = state => ({ | ||||
|   active: state.getIn(['notifications', 'cleaningMode']), | ||||
| }); | ||||
| 
 | ||||
| export default connect(mapStateToProps, mapDispatchToProps)(NotificationPurgeButtons); | ||||
|  | @ -0,0 +1,100 @@ | |||
| /** | ||||
|  * Buttons widget for controlling the notification clearing mode. | ||||
|  * In idle state, the cleaning mode button is shown. When the mode is active, | ||||
|  * a Confirm and Abort buttons are shown in its place. | ||||
|  */ | ||||
| 
 | ||||
| 
 | ||||
| //  Package imports  //
 | ||||
| import React from 'react'; | ||||
| import PropTypes from 'prop-types'; | ||||
| import { defineMessages, injectIntl } from 'react-intl'; | ||||
| import ImmutablePureComponent from 'react-immutable-pure-component'; | ||||
| 
 | ||||
| //  Mastodon imports  //
 | ||||
| 
 | ||||
| //  * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
 | ||||
| 
 | ||||
| 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' }, | ||||
| }); | ||||
| 
 | ||||
| @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
 | ||||
|     intl: PropTypes.object.isRequired, | ||||
|   }; | ||||
| 
 | ||||
|   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> | ||||
|       ); | ||||
|     } | ||||
| 
 | ||||
|     return ( | ||||
|       <div className='column-header__notif-cleaning-buttons'> | ||||
|         {acceptButton}{abortButton}{enterButton} | ||||
|       </div> | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
| } | ||||
|  | @ -24,7 +24,6 @@ import { makeGetNotification } from '../../../mastodon/selectors'; | |||
| 
 | ||||
| //  Our imports  //
 | ||||
| import Notification from '.'; | ||||
| import { deleteNotification } from '../../../mastodon/actions/notifications'; | ||||
| 
 | ||||
| //  * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
 | ||||
| 
 | ||||
|  | @ -53,21 +52,4 @@ const makeMapStateToProps = () => { | |||
| 
 | ||||
| //  * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
 | ||||
| 
 | ||||
| /* | ||||
| 
 | ||||
| Dispatch mapping: | ||||
| ----------------- | ||||
| 
 | ||||
| The `mapDispatchToProps()` function maps dispatches to our store to the | ||||
| various props of our component. We only need to provide a dispatch for | ||||
| deleting notifications. | ||||
| 
 | ||||
| */ | ||||
| 
 | ||||
| const mapDispatchToProps = dispatch => ({ | ||||
|   onDeleteNotification (id) { | ||||
|     dispatch(deleteNotification(id)); | ||||
|   }, | ||||
| }); | ||||
| 
 | ||||
| export default connect(makeMapStateToProps, mapDispatchToProps)(Notification); | ||||
| export default connect(makeMapStateToProps)(Notification); | ||||
|  |  | |||
|  | @ -36,7 +36,7 @@ Imports: | |||
| import React from 'react'; | ||||
| import ImmutablePropTypes from 'react-immutable-proptypes'; | ||||
| import PropTypes from 'prop-types'; | ||||
| import { defineMessages, FormattedMessage, injectIntl } from 'react-intl'; | ||||
| import { FormattedMessage } from 'react-intl'; | ||||
| import escapeTextContentForBrowser from 'escape-html'; | ||||
| import ImmutablePureComponent from 'react-immutable-pure-component'; | ||||
| 
 | ||||
|  | @ -45,55 +45,28 @@ import emojify from '../../../mastodon/emoji'; | |||
| import Permalink from '../../../mastodon/components/permalink'; | ||||
| import AccountContainer from '../../../mastodon/containers/account_container'; | ||||
| 
 | ||||
| // Our imports //
 | ||||
| import NotificationOverlayContainer from '../notification/overlay/container'; | ||||
| 
 | ||||
| //  * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
 | ||||
| 
 | ||||
| /* | ||||
| 
 | ||||
| Inital setup: | ||||
| ------------- | ||||
| 
 | ||||
| The `messages` constant is used to define any messages that we need | ||||
| from inside props. | ||||
| 
 | ||||
| */ | ||||
| 
 | ||||
| const messages = defineMessages({ | ||||
|   deleteNotification : | ||||
|     { id: 'status.dismiss_notification', defaultMessage: 'Dismiss notification' }, | ||||
| }); | ||||
| 
 | ||||
| /* | ||||
| 
 | ||||
| Implementation: | ||||
| --------------- | ||||
| 
 | ||||
| */ | ||||
| 
 | ||||
| @injectIntl | ||||
| export default class NotificationFollow extends ImmutablePureComponent { | ||||
| 
 | ||||
|   static propTypes = { | ||||
|     id                   : PropTypes.number.isRequired, | ||||
|     onDeleteNotification : PropTypes.func.isRequired, | ||||
|     account              : ImmutablePropTypes.map.isRequired, | ||||
|     intl                 : PropTypes.object.isRequired, | ||||
|     notification         : ImmutablePropTypes.map.isRequired, | ||||
|   }; | ||||
| 
 | ||||
| /* | ||||
| 
 | ||||
| ###  `handleNotificationDeleteClick()` | ||||
| 
 | ||||
| This function just calls our `onDeleteNotification()` prop with the | ||||
| notification's `id`. | ||||
| 
 | ||||
| */ | ||||
| 
 | ||||
|   handleNotificationDeleteClick = () => { | ||||
|     this.props.onDeleteNotification(this.props.id); | ||||
|   } | ||||
| 
 | ||||
| /* | ||||
| 
 | ||||
| ###  `render()` | ||||
| 
 | ||||
| This actually renders the component. | ||||
|  | @ -101,26 +74,7 @@ This actually renders the component. | |||
| */ | ||||
| 
 | ||||
|   render () { | ||||
|     const { account, intl } = this.props; | ||||
| 
 | ||||
| /* | ||||
| 
 | ||||
| `dismiss` creates the notification dismissal button. Its title is given | ||||
| by `dismissTitle`. | ||||
| 
 | ||||
| */ | ||||
| 
 | ||||
|     const dismissTitle = intl.formatMessage(messages.deleteNotification); | ||||
|     const dismiss = ( | ||||
|       <button | ||||
|         aria-label={dismissTitle} | ||||
|         title={dismissTitle} | ||||
|         onClick={this.handleNotificationDeleteClick} | ||||
|         className='status__prepend-dismiss-button' | ||||
|       > | ||||
|         <i className='fa fa-eraser' /> | ||||
|       </button> | ||||
|     ); | ||||
|     const { account, notification } = this.props; | ||||
| 
 | ||||
| /* | ||||
| 
 | ||||
|  | @ -149,6 +103,7 @@ We can now render our component. | |||
| 
 | ||||
|     return ( | ||||
|       <div className='notification notification-follow'> | ||||
|         <NotificationOverlayContainer notification={notification} /> | ||||
|         <div className='notification__message'> | ||||
|           <div className='notification__favourite-icon-wrapper'> | ||||
|             <i className='fa fa-fw fa-user-plus' /> | ||||
|  | @ -159,8 +114,6 @@ We can now render our component. | |||
|             defaultMessage='{name} followed you' | ||||
|             values={{ name: link }} | ||||
|           /> | ||||
| 
 | ||||
|           {dismiss} | ||||
|         </div> | ||||
| 
 | ||||
|         <AccountContainer id={account.get('id')} withNote={false} /> | ||||
|  |  | |||
|  | @ -2,7 +2,6 @@ | |||
| import React from 'react'; | ||||
| import ImmutablePropTypes from 'react-immutable-proptypes'; | ||||
| import ImmutablePureComponent from 'react-immutable-pure-component'; | ||||
| import PropTypes from 'prop-types'; | ||||
| 
 | ||||
| //  Mastodon imports  //
 | ||||
| 
 | ||||
|  | @ -15,7 +14,6 @@ export default class Notification extends ImmutablePureComponent { | |||
|   static propTypes = { | ||||
|     notification: ImmutablePropTypes.map.isRequired, | ||||
|     settings: ImmutablePropTypes.map.isRequired, | ||||
|     onDeleteNotification: PropTypes.func.isRequired, | ||||
|   }; | ||||
| 
 | ||||
|   renderFollow (notification) { | ||||
|  | @ -23,7 +21,7 @@ export default class Notification extends ImmutablePureComponent { | |||
|       <NotificationFollow | ||||
|         id={notification.get('id')} | ||||
|         account={notification.get('account')} | ||||
|         onDeleteNotification={this.props.onDeleteNotification} | ||||
|         notification={notification} | ||||
|       /> | ||||
|     ); | ||||
|   } | ||||
|  | @ -32,7 +30,7 @@ export default class Notification extends ImmutablePureComponent { | |||
|     return ( | ||||
|       <StatusContainer | ||||
|         id={notification.get('status')} | ||||
|         notificationId={notification.get('id')} | ||||
|         notification={notification} | ||||
|         withDismiss | ||||
|       /> | ||||
|     ); | ||||
|  | @ -45,7 +43,7 @@ export default class Notification extends ImmutablePureComponent { | |||
|         account={notification.get('account')} | ||||
|         prepend='favourite' | ||||
|         muted | ||||
|         notificationId={notification.get('id')} | ||||
|         notification={notification} | ||||
|         withDismiss | ||||
|       /> | ||||
|     ); | ||||
|  | @ -58,7 +56,7 @@ export default class Notification extends ImmutablePureComponent { | |||
|         account={notification.get('account')} | ||||
|         prepend='reblog' | ||||
|         muted | ||||
|         notificationId={notification.get('id')} | ||||
|         notification={notification} | ||||
|         withDismiss | ||||
|       /> | ||||
|     ); | ||||
|  |  | |||
|  | @ -0,0 +1,49 @@ | |||
| /* | ||||
| 
 | ||||
| `<NotificationOverlayContainer>` | ||||
| ========================= | ||||
| 
 | ||||
| This container connects `<NotificationOverlay>`s to the Redux store. | ||||
| 
 | ||||
| */ | ||||
| 
 | ||||
| //  * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
 | ||||
| 
 | ||||
| /* | ||||
| 
 | ||||
| Imports: | ||||
| -------- | ||||
| 
 | ||||
| */ | ||||
| 
 | ||||
| //  Package imports  //
 | ||||
| import { connect } from 'react-redux'; | ||||
| 
 | ||||
| //  Our imports  //
 | ||||
| import NotificationOverlay from './notification_overlay'; | ||||
| import { markNotificationForDelete } from '../../../../mastodon/actions/notifications'; | ||||
| 
 | ||||
| //  * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
 | ||||
| 
 | ||||
| /* | ||||
| 
 | ||||
| Dispatch mapping: | ||||
| ----------------- | ||||
| 
 | ||||
| The `mapDispatchToProps()` function maps dispatches to our store to the | ||||
| various props of our component. We only need to provide a dispatch for | ||||
| deleting notifications. | ||||
| 
 | ||||
| */ | ||||
| 
 | ||||
| const mapDispatchToProps = dispatch => ({ | ||||
|   onMarkForDelete(id, yes) { | ||||
|     dispatch(markNotificationForDelete(id, yes)); | ||||
|   }, | ||||
| }); | ||||
| 
 | ||||
| const mapStateToProps = state => ({ | ||||
|   revealed: state.getIn(['notifications', 'cleaningMode']), | ||||
| }); | ||||
| 
 | ||||
| export default connect(mapStateToProps, mapDispatchToProps)(NotificationOverlay); | ||||
|  | @ -0,0 +1,59 @@ | |||
| /** | ||||
|  * Notification overlay | ||||
|  */ | ||||
| 
 | ||||
| 
 | ||||
| //  Package imports  //
 | ||||
| import React from 'react'; | ||||
| import ImmutablePropTypes from 'react-immutable-proptypes'; | ||||
| import PropTypes from 'prop-types'; | ||||
| import ImmutablePureComponent from 'react-immutable-pure-component'; | ||||
| import { defineMessages, injectIntl } from 'react-intl'; | ||||
| 
 | ||||
| //  Mastodon imports  //
 | ||||
| 
 | ||||
| //  * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
 | ||||
| 
 | ||||
| const messages = defineMessages({ | ||||
|   markForDeletion: { id: 'notification.markForDeletion', defaultMessage: 'Mark for deletion' }, | ||||
| }); | ||||
| 
 | ||||
| @injectIntl | ||||
| export default class NotificationOverlay extends ImmutablePureComponent { | ||||
| 
 | ||||
|   static propTypes = { | ||||
|     notification    : ImmutablePropTypes.map.isRequired, | ||||
|     onMarkForDelete : PropTypes.func.isRequired, | ||||
|     revealed        : PropTypes.bool.isRequired, | ||||
|     intl            : PropTypes.object.isRequired, | ||||
|   }; | ||||
| 
 | ||||
|   onToggleMark = () => { | ||||
|     const mark = !this.props.notification.get('markedForDelete'); | ||||
|     const id = this.props.notification.get('id'); | ||||
|     this.props.onMarkForDelete(id, mark); | ||||
|   } | ||||
| 
 | ||||
|   render () { | ||||
|     const { notification, revealed, intl } = this.props; | ||||
| 
 | ||||
|     const active = notification.get('markedForDelete'); | ||||
|     const label = intl.formatMessage(messages.markForDeletion); | ||||
| 
 | ||||
|     return ( | ||||
|       <div | ||||
|         aria-label={label} | ||||
|         role='checkbox' | ||||
|         aria-checked={active} | ||||
|         tabIndex={0} | ||||
|         className={`notification__dismiss-overlay ${active ? 'active' : ''} ${revealed ? 'show' : ''}`} | ||||
|         onClick={this.onToggleMark} | ||||
|       > | ||||
|         <div className='notification__dismiss-overlay__ckbox' aria-hidden='true' title={label}> | ||||
|           {active ? (<i className='fa fa-check' />) : ''} | ||||
|         </div> | ||||
|       </div> | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
| } | ||||
|  | @ -24,7 +24,6 @@ const messages = defineMessages({ | |||
|   report: { id: 'status.report', defaultMessage: 'Report @{name}' }, | ||||
|   muteConversation: { id: 'status.mute_conversation', defaultMessage: 'Mute conversation' }, | ||||
|   unmuteConversation: { id: 'status.unmute_conversation', defaultMessage: 'Unmute conversation' }, | ||||
|   deleteNotification: { id: 'status.dismiss_notification', defaultMessage: 'Dismiss notification' }, | ||||
| }); | ||||
| 
 | ||||
| @injectIntl | ||||
|  | @ -36,7 +35,6 @@ export default class StatusActionBar extends ImmutablePureComponent { | |||
| 
 | ||||
|   static propTypes = { | ||||
|     status: ImmutablePropTypes.map.isRequired, | ||||
|     notificationId: PropTypes.number, | ||||
|     onReply: PropTypes.func, | ||||
|     onFavourite: PropTypes.func, | ||||
|     onReblog: PropTypes.func, | ||||
|  | @ -46,7 +44,6 @@ export default class StatusActionBar extends ImmutablePureComponent { | |||
|     onBlock: PropTypes.func, | ||||
|     onReport: PropTypes.func, | ||||
|     onMuteConversation: PropTypes.func, | ||||
|     onDeleteNotification: PropTypes.func, | ||||
|     me: PropTypes.number, | ||||
|     withDismiss: PropTypes.bool, | ||||
|     intl: PropTypes.object.isRequired, | ||||
|  | @ -100,10 +97,6 @@ export default class StatusActionBar extends ImmutablePureComponent { | |||
|     this.props.onMuteConversation(this.props.status); | ||||
|   } | ||||
| 
 | ||||
|   handleNotificationDeleteClick = () => { | ||||
|     this.props.onDeleteNotification(this.props.notificationId); | ||||
|   } | ||||
| 
 | ||||
|   render () { | ||||
|     const { status, me, intl, withDismiss } = this.props; | ||||
|     const reblogDisabled = status.get('visibility') === 'private' || status.get('visibility') === 'direct'; | ||||
|  | @ -120,7 +113,6 @@ export default class StatusActionBar extends ImmutablePureComponent { | |||
| 
 | ||||
|     if (withDismiss) { | ||||
|       menu.push({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick }); | ||||
|       menu.push({ text: intl.formatMessage(messages.deleteNotification), action: this.handleNotificationDeleteClick }); | ||||
|       menu.push(null); | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
|  | @ -50,7 +50,6 @@ import { | |||
| } from '../../../mastodon/actions/statuses'; | ||||
| import { initReport } from '../../../mastodon/actions/reports'; | ||||
| import { openModal } from '../../../mastodon/actions/modal'; | ||||
| import { deleteNotification } from '../../../mastodon/actions/notifications'; | ||||
| 
 | ||||
| //  Our imports  //
 | ||||
| import Status from '.'; | ||||
|  | @ -245,10 +244,6 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ | |||
|       dispatch(muteStatus(status.get('id'))); | ||||
|     } | ||||
|   }, | ||||
| 
 | ||||
|   onDeleteNotification (id) { | ||||
|     dispatch(deleteNotification(id)); | ||||
|   }, | ||||
| }); | ||||
| 
 | ||||
| export default injectIntl( | ||||
|  |  | |||
|  | @ -47,6 +47,7 @@ import StatusContent from './content'; | |||
| import StatusActionBar from './action_bar'; | ||||
| import StatusGallery from './gallery'; | ||||
| import StatusPlayer from './player'; | ||||
| import NotificationOverlayContainer from '../notification/overlay/container'; | ||||
| 
 | ||||
|                             /* * * * */ | ||||
| 
 | ||||
|  | @ -158,6 +159,7 @@ export default class Status extends ImmutablePureComponent { | |||
|     status                      : ImmutablePropTypes.map, | ||||
|     account                     : ImmutablePropTypes.map, | ||||
|     settings                    : ImmutablePropTypes.map, | ||||
|     notification                : ImmutablePropTypes.map, | ||||
|     me                          : PropTypes.number, | ||||
|     onFavourite                 : PropTypes.func, | ||||
|     onReblog                    : PropTypes.func, | ||||
|  | @ -170,7 +172,6 @@ export default class Status extends ImmutablePureComponent { | |||
|     onReport                    : PropTypes.func, | ||||
|     onOpenMedia                 : PropTypes.func, | ||||
|     onOpenVideo                 : PropTypes.func, | ||||
|     onDeleteNotification        : PropTypes.func, | ||||
|     reblogModal                 : PropTypes.bool, | ||||
|     deleteModal                 : PropTypes.bool, | ||||
|     autoPlayGif                 : PropTypes.bool, | ||||
|  | @ -178,7 +179,6 @@ export default class Status extends ImmutablePureComponent { | |||
|     collapse                    : PropTypes.bool, | ||||
|     prepend                     : PropTypes.string, | ||||
|     withDismiss                 : PropTypes.bool, | ||||
|     notificationId              : PropTypes.number, | ||||
|     intersectionObserverWrapper : PropTypes.object, | ||||
|   }; | ||||
| 
 | ||||
|  | @ -186,6 +186,7 @@ export default class Status extends ImmutablePureComponent { | |||
|     isExpanded                  : null, | ||||
|     isIntersecting              : true, | ||||
|     isHidden                    : false, | ||||
|     markedForDelete             : false, | ||||
|   } | ||||
| 
 | ||||
| /* | ||||
|  | @ -212,10 +213,12 @@ to remember to specify it here. | |||
|     'autoPlayGif', | ||||
|     'muted', | ||||
|     'collapse', | ||||
|     'notification', | ||||
|   ] | ||||
| 
 | ||||
|   updateOnStates = [ | ||||
|     'isExpanded', | ||||
|     'markedForDelete', | ||||
|   ] | ||||
| 
 | ||||
| /* | ||||
|  | @ -523,6 +526,10 @@ applicable. | |||
|     } | ||||
|   } | ||||
| 
 | ||||
|   markNotifForDelete = () => { | ||||
|     this.setState({ 'markedForDelete' : !this.state.markedForDelete }); | ||||
|   } | ||||
| 
 | ||||
| /* | ||||
| 
 | ||||
| ####  `render()`. | ||||
|  | @ -551,6 +558,7 @@ this operation are further explained in the code below. | |||
|       onOpenVideo, | ||||
|       onOpenMedia, | ||||
|       autoPlayGif, | ||||
|       notification, | ||||
|       ...other | ||||
|     } = this.props; | ||||
|     const { isExpanded, isIntersecting, isHidden } = this.state; | ||||
|  | @ -678,6 +686,8 @@ collapsed. | |||
|             isExpanded === false ? ' collapsed' : '' | ||||
|           }${ | ||||
|             isExpanded === false && background ? ' has-background' : '' | ||||
|           }${ | ||||
|             this.state.markedForDelete ? ' marked-for-delete' : '' | ||||
|           }` | ||||
|         } | ||||
|         style={{ | ||||
|  | @ -689,13 +699,17 @@ collapsed. | |||
|         }} | ||||
|         ref={handleRef} | ||||
|       > | ||||
|         {notification ? ( | ||||
|           <NotificationOverlayContainer | ||||
|             notification={notification} | ||||
|           /> | ||||
|         ) : null} | ||||
|         {prepend && account ? ( | ||||
|           <StatusPrepend | ||||
|             type={prepend} | ||||
|             account={account} | ||||
|             parseClick={parseClick} | ||||
|             notificationId={this.props.notificationId} | ||||
|             onDeleteNotification={this.props.onDeleteNotification} | ||||
|           /> | ||||
|         ) : null} | ||||
|         <StatusHeader | ||||
|  |  | |||
|  | @ -23,17 +23,11 @@ import React from 'react'; | |||
| import PropTypes from 'prop-types'; | ||||
| import ImmutablePropTypes from 'react-immutable-proptypes'; | ||||
| import escapeTextContentForBrowser from 'escape-html'; | ||||
| import { defineMessages, injectIntl } from 'react-intl'; | ||||
| import { FormattedMessage } from 'react-intl'; | ||||
| 
 | ||||
| //  Mastodon imports  //
 | ||||
| import emojify from '../../../mastodon/emoji'; | ||||
| 
 | ||||
| 
 | ||||
| const messages = defineMessages({ | ||||
|   deleteNotification: { id: 'status.dismiss_notification', defaultMessage: 'Dismiss notification' }, | ||||
| }); | ||||
| 
 | ||||
|                             /* * * * */ | ||||
| 
 | ||||
| /* | ||||
|  | @ -59,7 +53,6 @@ element. | |||
| 
 | ||||
| */ | ||||
| 
 | ||||
| @injectIntl | ||||
| export default class StatusPrepend extends React.PureComponent { | ||||
| 
 | ||||
|   static propTypes = { | ||||
|  | @ -67,8 +60,6 @@ export default class StatusPrepend extends React.PureComponent { | |||
|     account: ImmutablePropTypes.map.isRequired, | ||||
|     parseClick: PropTypes.func.isRequired, | ||||
|     notificationId: PropTypes.number, | ||||
|     onDeleteNotification: PropTypes.func, | ||||
|     intl: PropTypes.object.isRequired, | ||||
|   }; | ||||
| 
 | ||||
| /* | ||||
|  | @ -87,10 +78,6 @@ an account link is clicked. | |||
|     parseClick(e, `/accounts/${+account.get('id')}`); | ||||
|   } | ||||
| 
 | ||||
|   handleNotificationDeleteClick = () => { | ||||
|     this.props.onDeleteNotification(this.props.notificationId); | ||||
|   } | ||||
| 
 | ||||
| /* | ||||
| 
 | ||||
| ####  `<Message>`. | ||||
|  | @ -159,19 +146,7 @@ the `<Message>` inside of an <aside>. | |||
| 
 | ||||
|   render () { | ||||
|     const { Message } = this; | ||||
|     const { type, intl } = this.props; | ||||
| 
 | ||||
|     const dismissTitle = intl.formatMessage(messages.deleteNotification); | ||||
|     const dismiss = this.props.notificationId ? ( | ||||
|       <button | ||||
|         aria-label={dismissTitle} | ||||
|         title={dismissTitle} | ||||
|         onClick={this.handleNotificationDeleteClick} | ||||
|         className='status__prepend-dismiss-button' | ||||
|       > | ||||
|         <i className='fa fa-eraser' /> | ||||
|       </button> | ||||
|     ) : null; | ||||
|     const { type } = this.props; | ||||
| 
 | ||||
|     return !type ? null : ( | ||||
|       <aside className={type === 'reblogged_by' ? 'status__prepend' : 'notification__message'}> | ||||
|  | @ -183,7 +158,6 @@ the `<Message>` inside of an <aside>. | |||
|           /> | ||||
|         </div> | ||||
|         <Message /> | ||||
|         {dismiss} | ||||
|       </aside> | ||||
|     ); | ||||
|   } | ||||
|  |  | |||
|  | @ -28,5 +28,5 @@ | |||
|   "settings.wide_view": "Wide view (Desktop mode only)", | ||||
|   "status.collapse": "Collapse", | ||||
|   "status.uncollapse": "Uncollapse", | ||||
|   "status.dismiss_notification": "Dismiss notification" | ||||
|   "notification.markForDeletion": "Mark for deletion" | ||||
| } | ||||
|  |  | |||
|  | @ -6,7 +6,15 @@ import { defineMessages } from 'react-intl'; | |||
| 
 | ||||
| export const NOTIFICATIONS_UPDATE = 'NOTIFICATIONS_UPDATE'; | ||||
| 
 | ||||
| export const NOTIFICATION_DELETE_SUCCESS = 'NOTIFICATION_DELETE_SUCCESS'; | ||||
| // tracking the notif cleaning 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_FAIL = 'NOTIFICATIONS_DELETE_MARKED_FAIL'; | ||||
| 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'; | ||||
| // Mark one for delete
 | ||||
| export const NOTIFICATION_MARK_FOR_DELETE = 'NOTIFICATION_MARK_FOR_DELETE'; | ||||
| 
 | ||||
| export const NOTIFICATIONS_REFRESH_REQUEST = 'NOTIFICATIONS_REFRESH_REQUEST'; | ||||
| export const NOTIFICATIONS_REFRESH_SUCCESS = 'NOTIFICATIONS_REFRESH_SUCCESS'; | ||||
|  | @ -190,17 +198,61 @@ export function scrollTopNotifications(top) { | |||
|   }; | ||||
| }; | ||||
| 
 | ||||
| export function deleteNotification(id) { | ||||
| export function deleteMarkedNotifications() { | ||||
|   return (dispatch, getState) => { | ||||
|     api(getState).delete(`/api/v1/notifications/${id}`).then(() => { | ||||
|       dispatch(deleteNotificationSuccess(id)); | ||||
|     dispatch(deleteMarkedNotificationsRequest()); | ||||
| 
 | ||||
|     let ids = []; | ||||
|     getState().getIn(['notifications', 'items']).forEach((n) => { | ||||
|       if (n.get('markedForDelete')) { | ||||
|         ids.push(n.get('id')); | ||||
|       } | ||||
|     }); | ||||
| 
 | ||||
|     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)); | ||||
|     }); | ||||
|   }; | ||||
| }; | ||||
| 
 | ||||
| export function deleteNotificationSuccess(id) { | ||||
| export function enterNotificationClearingMode(yes) { | ||||
|   return { | ||||
|     type: NOTIFICATION_DELETE_SUCCESS, | ||||
|     id: id, | ||||
|     type: NOTIFICATIONS_ENTER_CLEARING_MODE, | ||||
|     yes: yes, | ||||
|   }; | ||||
| }; | ||||
| 
 | ||||
| export function deleteMarkedNotificationsRequest() { | ||||
|   return { | ||||
|     type: NOTIFICATIONS_DELETE_MARKED_REQUEST, | ||||
|   }; | ||||
| }; | ||||
| 
 | ||||
| export function deleteMarkedNotificationsFail() { | ||||
|   return { | ||||
|     type: NOTIFICATIONS_DELETE_MARKED_FAIL, | ||||
|   }; | ||||
| }; | ||||
| 
 | ||||
| export function markNotificationForDelete(id, yes) { | ||||
|   return { | ||||
|     type: NOTIFICATION_MARK_FOR_DELETE, | ||||
|     id: id, | ||||
|     yes: yes, | ||||
|   }; | ||||
| }; | ||||
| 
 | ||||
| export function deleteMarkedNotificationsSuccess() { | ||||
|   return { | ||||
|     type: NOTIFICATIONS_DELETE_MARKED_SUCCESS, | ||||
|   }; | ||||
| }; | ||||
|  |  | |||
|  | @ -34,7 +34,12 @@ export default class Column extends React.PureComponent { | |||
|     const { children } = this.props; | ||||
| 
 | ||||
|     return ( | ||||
|       <div role='region' className='column' ref={this.setRef} onWheel={this.handleWheel}> | ||||
|       <div | ||||
|         role='region' | ||||
|         className='column' | ||||
|         ref={this.setRef} | ||||
|         onWheel={this.handleWheel} | ||||
|       > | ||||
|         {children} | ||||
|       </div> | ||||
|     ); | ||||
|  |  | |||
|  | @ -1,8 +1,18 @@ | |||
| import React from 'react'; | ||||
| import PropTypes from 'prop-types'; | ||||
| import classNames from 'classnames'; | ||||
| import { FormattedMessage } from 'react-intl'; | ||||
| import { defineMessages, FormattedMessage, injectIntl } from 'react-intl'; | ||||
| import ImmutablePropTypes from 'react-immutable-proptypes'; | ||||
| 
 | ||||
| // Glitch imports
 | ||||
| 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:' }, | ||||
| }); | ||||
| 
 | ||||
| @injectIntl | ||||
| export default class ColumnHeader extends React.PureComponent { | ||||
| 
 | ||||
|   static contextTypes = { | ||||
|  | @ -13,13 +23,17 @@ export default class ColumnHeader extends React.PureComponent { | |||
|     title: PropTypes.node.isRequired, | ||||
|     icon: PropTypes.string.isRequired, | ||||
|     active: PropTypes.bool, | ||||
|     localSettings : ImmutablePropTypes.map, | ||||
|     multiColumn: PropTypes.bool, | ||||
|     showBackButton: PropTypes.bool, | ||||
|     notifCleaning: PropTypes.bool, // true only for the notification column
 | ||||
|     notifCleaningActive: PropTypes.bool, | ||||
|     children: PropTypes.node, | ||||
|     pinned: PropTypes.bool, | ||||
|     onPin: PropTypes.func, | ||||
|     onMove: PropTypes.func, | ||||
|     onClick: PropTypes.func, | ||||
|     intl: PropTypes.object.isRequired, | ||||
|   }; | ||||
| 
 | ||||
|   state = { | ||||
|  | @ -58,9 +72,16 @@ export default class ColumnHeader extends React.PureComponent { | |||
|   } | ||||
| 
 | ||||
|   render () { | ||||
|     const { title, icon, active, children, pinned, onPin, multiColumn, showBackButton } = this.props; | ||||
|     const { intl, icon, active, children, pinned, onPin, multiColumn, showBackButton, notifCleaning, localSettings } = this.props; | ||||
|     const { collapsed, animating } = 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, | ||||
|     }); | ||||
|  | @ -130,6 +151,7 @@ export default class ColumnHeader extends React.PureComponent { | |||
|           {title} | ||||
| 
 | ||||
|           <div className='column-header__buttons'> | ||||
|             {notifCleaning ? (<NotificationPurgeButtonsContainer />) : null} | ||||
|             {backButton} | ||||
|             {collapseButton} | ||||
|           </div> | ||||
|  |  | |||
|  | @ -4,7 +4,10 @@ import PropTypes from 'prop-types'; | |||
| import ImmutablePropTypes from 'react-immutable-proptypes'; | ||||
| import Column from '../../components/column'; | ||||
| import ColumnHeader from '../../components/column_header'; | ||||
| import { expandNotifications, scrollTopNotifications } from '../../actions/notifications'; | ||||
| import { | ||||
|   expandNotifications, | ||||
|   scrollTopNotifications, | ||||
| } from '../../actions/notifications'; | ||||
| import { addColumn, removeColumn, moveColumn } from '../../actions/columns'; | ||||
| import NotificationContainer from '../../../glitch/components/notification/container'; | ||||
| import { ScrollContainer } from 'react-router-scroll'; | ||||
|  | @ -26,9 +29,11 @@ const getNotifications = createSelector([ | |||
| 
 | ||||
| const mapStateToProps = state => ({ | ||||
|   notifications: getNotifications(state), | ||||
|   localSettings:  state.get('local_settings'), | ||||
|   isLoading: state.getIn(['notifications', 'isLoading'], true), | ||||
|   isUnread: state.getIn(['notifications', 'unread']) > 0, | ||||
|   hasMore: !!state.getIn(['notifications', 'next']), | ||||
|   notifCleaningActive: state.getIn(['notifications', 'cleaningMode']), | ||||
| }); | ||||
| 
 | ||||
| @connect(mapStateToProps) | ||||
|  | @ -45,6 +50,8 @@ export default class Notifications extends React.PureComponent { | |||
|     isUnread: PropTypes.bool, | ||||
|     multiColumn: PropTypes.bool, | ||||
|     hasMore: PropTypes.bool, | ||||
|     localSettings: ImmutablePropTypes.map, | ||||
|     notifCleaningActive: PropTypes.bool, | ||||
|   }; | ||||
| 
 | ||||
|   static defaultProps = { | ||||
|  | @ -164,7 +171,9 @@ export default class Notifications extends React.PureComponent { | |||
|     this.scrollableArea = scrollableArea; | ||||
| 
 | ||||
|     return ( | ||||
|       <Column ref={this.setColumnRef}> | ||||
|       <Column | ||||
|         ref={this.setColumnRef} | ||||
|       > | ||||
|         <ColumnHeader | ||||
|           icon='bell' | ||||
|           active={isUnread} | ||||
|  | @ -174,6 +183,9 @@ export default class Notifications extends React.PureComponent { | |||
|           onClick={this.handleHeaderClick} | ||||
|           pinned={pinned} | ||||
|           multiColumn={multiColumn} | ||||
|           localSettings={this.props.localSettings} | ||||
|           notifCleaning | ||||
|           notifCleaningActive={this.props.notifCleaningActive} // this is used to toggle the header text
 | ||||
|         > | ||||
|           <ColumnSettingsContainer /> | ||||
|         </ColumnHeader> | ||||
|  |  | |||
|  | @ -8,7 +8,11 @@ import { | |||
|   NOTIFICATIONS_EXPAND_FAIL, | ||||
|   NOTIFICATIONS_CLEAR, | ||||
|   NOTIFICATIONS_SCROLL_TOP, | ||||
|   NOTIFICATION_DELETE_SUCCESS, | ||||
|   NOTIFICATIONS_DELETE_MARKED_REQUEST, | ||||
|   NOTIFICATIONS_DELETE_MARKED_SUCCESS, | ||||
|   NOTIFICATION_MARK_FOR_DELETE, | ||||
|   NOTIFICATIONS_DELETE_MARKED_FAIL, | ||||
|   NOTIFICATIONS_ENTER_CLEARING_MODE, | ||||
| } from '../actions/notifications'; | ||||
| import { ACCOUNT_BLOCK_SUCCESS } from '../actions/accounts'; | ||||
| import { TIMELINE_DELETE } from '../actions/timelines'; | ||||
|  | @ -21,12 +25,14 @@ const initialState = ImmutableMap({ | |||
|   unread: 0, | ||||
|   loaded: false, | ||||
|   isLoading: true, | ||||
|   cleaningMode: false, | ||||
| }); | ||||
| 
 | ||||
| const notificationToMap = notification => ImmutableMap({ | ||||
|   id: notification.id, | ||||
|   type: notification.type, | ||||
|   account: notification.account.id, | ||||
|   markedForDelete: false, | ||||
|   status: notification.status ? notification.status.id : null, | ||||
| }); | ||||
| 
 | ||||
|  | @ -93,17 +99,34 @@ const deleteByStatus = (state, statusId) => { | |||
|   return state.update('items', list => list.filterNot(item => item.get('status') === statusId)); | ||||
| }; | ||||
| 
 | ||||
| const deleteById = (state, notificationId) => { | ||||
|   return state.update('items', list => list.filterNot(item => item.get('id') === notificationId)); | ||||
| const markForDelete = (state, notificationId, yes) => { | ||||
|   return state.update('items', list => list.map(item => { | ||||
|     if(item.get('id') === notificationId) { | ||||
|       return item.set('markedForDelete', yes); | ||||
|     } else { | ||||
|       return item; | ||||
|     } | ||||
|   })); | ||||
| }; | ||||
| 
 | ||||
| const unmarkAllForDelete = (state) => { | ||||
|   return state.update('items', list => list.map(item => item.set('markedForDelete', false))); | ||||
| }; | ||||
| 
 | ||||
| const deleteMarkedNotifs = (state) => { | ||||
|   return state.update('items', list => list.filterNot(item => item.get('markedForDelete'))); | ||||
| }; | ||||
| 
 | ||||
| export default function notifications(state = initialState, action) { | ||||
|   switch(action.type) { | ||||
|   case NOTIFICATIONS_REFRESH_REQUEST: | ||||
|   case NOTIFICATIONS_EXPAND_REQUEST: | ||||
|   case NOTIFICATIONS_DELETE_MARKED_REQUEST: | ||||
|     return state.set('isLoading', true); | ||||
|   case NOTIFICATIONS_DELETE_MARKED_FAIL: | ||||
|   case NOTIFICATIONS_REFRESH_FAIL: | ||||
|   case NOTIFICATIONS_EXPAND_FAIL: | ||||
|     return state.set('isLoading', true); | ||||
|     return state.set('isLoading', false); | ||||
|   case NOTIFICATIONS_SCROLL_TOP: | ||||
|     return updateTop(state, action.top); | ||||
|   case NOTIFICATIONS_UPDATE: | ||||
|  | @ -118,8 +141,15 @@ 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_DELETE_SUCCESS: | ||||
|     return deleteById(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); | ||||
|   case NOTIFICATIONS_ENTER_CLEARING_MODE: | ||||
|     const st = state.set('cleaningMode', action.yes); | ||||
|     if (!action.yes) | ||||
|       return unmarkAllForDelete(st); | ||||
|     else return st; | ||||
|   default: | ||||
|     return state; | ||||
|   } | ||||
|  |  | |||
|  | @ -451,6 +451,63 @@ | |||
|   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 & { | ||||
|     outline: rgb(77, 144, 254) auto 10px; | ||||
|     outline: -webkit-focus-ring-color auto 10px; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| // --- Extra clickable area in the status gutter --- | ||||
| .ui.wide { | ||||
|   @mixin xtraspaces-full { | ||||
|  | @ -627,24 +684,14 @@ | |||
|   position: absolute; | ||||
| } | ||||
| 
 | ||||
| .status__prepend-dismiss-button { | ||||
|   border: 0; | ||||
|   background: transparent; | ||||
|   position: absolute; | ||||
|   right: -3px; | ||||
|   opacity: 0; | ||||
|   transition: opacity 0.1s ease-in-out; | ||||
| .notification-follow { | ||||
|   position: relative; | ||||
| 
 | ||||
|   i.fa { | ||||
|     color: crimson; | ||||
|   } | ||||
|   // same like Status | ||||
|   border-bottom: 1px solid lighten($ui-base-color, 8%); | ||||
| 
 | ||||
|   .notification__message:hover & { | ||||
|     opacity: 1; | ||||
|   } | ||||
| 
 | ||||
|   .notification-follow & { | ||||
|     right: 6px; | ||||
|   .account { | ||||
|     border-bottom: 0 none; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
|  | @ -2408,6 +2455,17 @@ button.icon-button.active i.fa-retweet { | |||
|   } | ||||
| } | ||||
| 
 | ||||
| .column-header__notif-cleaning-buttons { | ||||
|   display: flex; | ||||
|   align-items: stretch; | ||||
| 
 | ||||
|   button { | ||||
|     @extend .column-header__button; | ||||
|     padding-left: 12px; | ||||
|     padding-right: 12px; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .column-header__collapsible { | ||||
|   max-height: 70vh; | ||||
|   overflow: hidden; | ||||
|  |  | |||
|  | @ -182,6 +182,7 @@ Rails.application.routes.draw do | |||
|         collection do | ||||
|           post :clear | ||||
|           post :dismiss | ||||
|           delete :destroy_multiple | ||||
|         end | ||||
|       end | ||||
| 
 | ||||
|  |  | |||
		Loading…
	
		Reference in a new issue