diff options
author | Ondřej Hruška <ondra@ondrovo.com> | 2017-07-21 20:33:16 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2017-07-21 20:33:16 +0200 |
commit | 604654ccb417ffdc9b48d876bea76c8bec14f360 (patch) | |
tree | 1fe2c98677aa5328c8366a37114325b625399ace /app/javascript/glitch | |
parent | 0efd7e740602dd684712563b7ad0b41c23d86d69 (diff) |
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
Diffstat (limited to 'app/javascript/glitch')
12 files changed, 295 insertions, 123 deletions
diff --git a/app/javascript/glitch/components/column/notif_cleaning_widget/container.js b/app/javascript/glitch/components/column/notif_cleaning_widget/container.js new file mode 100644 index 000000000..bf079e3c4 --- /dev/null +++ b/app/javascript/glitch/components/column/notif_cleaning_widget/container.js @@ -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); 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 new file mode 100644 index 000000000..e41572256 --- /dev/null +++ b/app/javascript/glitch/components/column/notif_cleaning_widget/notification_purge_buttons.js @@ -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> + ); + } + +} diff --git a/app/javascript/glitch/components/notification/container.js b/app/javascript/glitch/components/notification/container.js index bed086172..7d2590684 100644 --- a/app/javascript/glitch/components/notification/container.js +++ b/app/javascript/glitch/components/notification/container.js @@ -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); diff --git a/app/javascript/glitch/components/notification/follow.js b/app/javascript/glitch/components/notification/follow.js index 26396478b..0e0065eb1 100644 --- a/app/javascript/glitch/components/notification/follow.js +++ b/app/javascript/glitch/components/notification/follow.js @@ -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,22 +45,10 @@ import emojify from '../../../mastodon/emoji'; import Permalink from '../../../mastodon/components/permalink'; import AccountContainer from '../../../mastodon/containers/account_container'; -// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - -/* - -Inital setup: -------------- - -The `messages` constant is used to define any messages that we need -from inside props. - -*/ +// Our imports // +import NotificationOverlayContainer from '../notification/overlay/container'; -const messages = defineMessages({ - deleteNotification : - { id: 'status.dismiss_notification', defaultMessage: 'Dismiss notification' }, -}); +// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * /* @@ -69,31 +57,16 @@ 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} /> diff --git a/app/javascript/glitch/components/notification/index.js b/app/javascript/glitch/components/notification/index.js index 556d5aea8..b2e55aad5 100644 --- a/app/javascript/glitch/components/notification/index.js +++ b/app/javascript/glitch/components/notification/index.js @@ -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 /> ); diff --git a/app/javascript/glitch/components/notification/overlay/container.js b/app/javascript/glitch/components/notification/overlay/container.js new file mode 100644 index 000000000..019b78d0b --- /dev/null +++ b/app/javascript/glitch/components/notification/overlay/container.js @@ -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); diff --git a/app/javascript/glitch/components/notification/overlay/notification_overlay.js b/app/javascript/glitch/components/notification/overlay/notification_overlay.js new file mode 100644 index 000000000..73eda718f --- /dev/null +++ b/app/javascript/glitch/components/notification/overlay/notification_overlay.js @@ -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> + ); + } + +} diff --git a/app/javascript/glitch/components/status/action_bar.js b/app/javascript/glitch/components/status/action_bar.js index df0904a7c..7c73002c1 100644 --- a/app/javascript/glitch/components/status/action_bar.js +++ b/app/javascript/glitch/components/status/action_bar.js @@ -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); } diff --git a/app/javascript/glitch/components/status/container.js b/app/javascript/glitch/components/status/container.js index c45b2e0ec..1d572e0e7 100644 --- a/app/javascript/glitch/components/status/container.js +++ b/app/javascript/glitch/components/status/container.js @@ -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( diff --git a/app/javascript/glitch/components/status/index.js b/app/javascript/glitch/components/status/index.js index 4a91b5aa3..dc06250ec 100644 --- a/app/javascript/glitch/components/status/index.js +++ b/app/javascript/glitch/components/status/index.js @@ -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 diff --git a/app/javascript/glitch/components/status/prepend.js b/app/javascript/glitch/components/status/prepend.js index d9b04b5ec..6213e4c8d 100644 --- a/app/javascript/glitch/components/status/prepend.js +++ b/app/javascript/glitch/components/status/prepend.js @@ -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> ); } diff --git a/app/javascript/glitch/locales/en.json b/app/javascript/glitch/locales/en.json index 80fdc3a39..d202d9c33 100644 --- a/app/javascript/glitch/locales/en.json +++ b/app/javascript/glitch/locales/en.json @@ -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" } |