diff options
-rw-r--r-- | app/controllers/api/v1/notifications_controller.rb | 4 | ||||
-rw-r--r-- | app/javascript/glitch/components/notification/container.js | 9 | ||||
-rw-r--r-- | app/javascript/glitch/components/notification/follow_notification.js | 78 | ||||
-rw-r--r-- | app/javascript/glitch/components/notification/index.js | 31 | ||||
-rw-r--r-- | app/javascript/glitch/components/status/action_bar.js | 8 | ||||
-rw-r--r-- | app/javascript/glitch/components/status/container.js | 4 | ||||
-rw-r--r-- | app/javascript/glitch/components/status/index.js | 4 | ||||
-rw-r--r-- | app/javascript/glitch/components/status/prepend.js | 29 | ||||
-rw-r--r-- | app/javascript/mastodon/actions/notifications.js | 17 | ||||
-rw-r--r-- | app/javascript/mastodon/locales/en.json | 1 | ||||
-rw-r--r-- | app/javascript/mastodon/reducers/notifications.js | 7 | ||||
-rw-r--r-- | app/javascript/styles/components.scss | 21 | ||||
-rw-r--r-- | config/routes.rb | 2 |
13 files changed, 192 insertions, 23 deletions
diff --git a/app/controllers/api/v1/notifications_controller.rb b/app/controllers/api/v1/notifications_controller.rb index 8910b77e9..55f35fa4b 100644 --- a/app/controllers/api/v1/notifications_controller.rb +++ b/app/controllers/api/v1/notifications_controller.rb @@ -24,6 +24,10 @@ class Api::V1::NotificationsController < Api::BaseController render_empty end + def destroy + dismiss + end + def dismiss current_account.notifications.find_by!(id: params[:id]).destroy! render_empty diff --git a/app/javascript/glitch/components/notification/container.js b/app/javascript/glitch/components/notification/container.js index c58ef4bd2..60303537d 100644 --- a/app/javascript/glitch/components/notification/container.js +++ b/app/javascript/glitch/components/notification/container.js @@ -6,6 +6,7 @@ import { makeGetNotification } from '../../../mastodon/selectors'; // Our imports // import Notification from '.'; +import { deleteNotification } from '../../../mastodon/actions/notifications'; const makeMapStateToProps = () => { const getNotification = makeGetNotification(); @@ -18,4 +19,10 @@ const makeMapStateToProps = () => { return mapStateToProps; }; -export default connect(makeMapStateToProps)(Notification); +const mapDispatchToProps = (dispatch) => ({ + onDeleteNotification (id) { + dispatch(deleteNotification(id)); + }, +}); + +export default connect(makeMapStateToProps, mapDispatchToProps)(Notification); diff --git a/app/javascript/glitch/components/notification/follow_notification.js b/app/javascript/glitch/components/notification/follow_notification.js new file mode 100644 index 000000000..7cabd91f6 --- /dev/null +++ b/app/javascript/glitch/components/notification/follow_notification.js @@ -0,0 +1,78 @@ +// Package imports // +import React from 'react'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import PropTypes from 'prop-types'; +import { defineMessages, FormattedMessage, injectIntl } from 'react-intl'; +import escapeTextContentForBrowser from 'escape-html'; +import ImmutablePureComponent from 'react-immutable-pure-component'; + +// Mastodon imports // +import emojify from '../../../mastodon/emoji'; +import Permalink from '../../../mastodon/components/permalink'; +import AccountContainer from '../../../mastodon/containers/account_container'; + +const messages = defineMessages({ + deleteNotification: { id: 'status.dismiss_notification', defaultMessage: 'Dismiss notification' }, +}); + + +@injectIntl +export default class FollowNotification extends ImmutablePureComponent { + + static contextTypes = { + router: PropTypes.object, + }; + + static propTypes = { + notificationId: PropTypes.number.isRequired, + onDeleteNotification: PropTypes.func.isRequired, + account: ImmutablePropTypes.map.isRequired, + intl: PropTypes.object.isRequired, + }; + + // Avoid checking props that are functions (and whose equality will always + // evaluate to false. See react-immutable-pure-component for usage. + updateOnProps = [ + 'account', + ] + + handleNotificationDeleteClick = () => { + this.props.onDeleteNotification(this.props.notificationId); + } + + render () { + const { account, intl } = this.props; + + 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 displayName = account.get('display_name').length > 0 ? account.get('display_name') : account.get('username'); + const displayNameHTML = { __html: emojify(escapeTextContentForBrowser(displayName)) }; + const link = <Permalink className='notification__display-name' href={account.get('url')} title={account.get('acct')} to={`/accounts/${account.get('id')}`} dangerouslySetInnerHTML={displayNameHTML} />; + return ( + <div className='notification notification-follow'> + <div className='notification__message'> + <div className='notification__favourite-icon-wrapper'> + <i className='fa fa-fw fa-user-plus' /> + </div> + + <FormattedMessage id='notification.follow' defaultMessage='{name} followed you' values={{ name: link }} /> + + {dismiss} + </div> + + <AccountContainer id={account.get('id')} withNote={false} /> + </div> + ); + } + +} diff --git a/app/javascript/glitch/components/notification/index.js b/app/javascript/glitch/components/notification/index.js index 83ac8dfc1..0cdc03cbe 100644 --- a/app/javascript/glitch/components/notification/index.js +++ b/app/javascript/glitch/components/notification/index.js @@ -1,42 +1,30 @@ // Package imports // import React from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; -import { FormattedMessage } from 'react-intl'; -import escapeTextContentForBrowser from 'escape-html'; import ImmutablePureComponent from 'react-immutable-pure-component'; +import PropTypes from 'prop-types'; // Mastodon imports // -import AccountContainer from '../../../mastodon/containers/account_container'; -import Permalink from '../../../mastodon/components/permalink'; -import emojify from '../../../mastodon/emoji'; // Our imports // import StatusContainer from '../status/container'; +import FollowNotification from './follow_notification'; export default class Notification extends ImmutablePureComponent { static propTypes = { notification: ImmutablePropTypes.map.isRequired, settings: ImmutablePropTypes.map.isRequired, + onDeleteNotification: PropTypes.func.isRequired, }; renderFollow (notification) { - const account = notification.get('account'); - const displayName = account.get('display_name').length > 0 ? account.get('display_name') : account.get('username'); - const displayNameHTML = { __html: emojify(escapeTextContentForBrowser(displayName)) }; - const link = <Permalink className='notification__display-name' href={account.get('url')} title={account.get('acct')} to={`/accounts/${account.get('id')}`} dangerouslySetInnerHTML={displayNameHTML} />; return ( - <div className='notification notification-follow'> - <div className='notification__message'> - <div className='notification__favourite-icon-wrapper'> - <i className='fa fa-fw fa-user-plus' /> - </div> - - <FormattedMessage id='notification.follow' defaultMessage='{name} followed you' values={{ name: link }} /> - </div> - - <AccountContainer id={account.get('id')} withNote={false} /> - </div> + <FollowNotification + notificationId={notification.get('id')} + account={notification.get('account')} + onDeleteNotification={this.props.onDeleteNotification} + /> ); } @@ -44,6 +32,7 @@ export default class Notification extends ImmutablePureComponent { return ( <StatusContainer id={notification.get('status')} + notificationId={notification.get('id')} withDismiss /> ); @@ -56,6 +45,7 @@ export default class Notification extends ImmutablePureComponent { account={notification.get('account')} prepend='favourite' muted + notificationId={notification.get('id')} withDismiss /> ); @@ -68,6 +58,7 @@ export default class Notification extends ImmutablePureComponent { account={notification.get('account')} prepend='reblog' muted + notificationId={notification.get('id')} withDismiss /> ); diff --git a/app/javascript/glitch/components/status/action_bar.js b/app/javascript/glitch/components/status/action_bar.js index f298dcaa8..6aa088c04 100644 --- a/app/javascript/glitch/components/status/action_bar.js +++ b/app/javascript/glitch/components/status/action_bar.js @@ -24,6 +24,7 @@ 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 @@ -35,6 +36,7 @@ export default class StatusActionBar extends ImmutablePureComponent { static propTypes = { status: ImmutablePropTypes.map.isRequired, + notificationId: PropTypes.number, onReply: PropTypes.func, onFavourite: PropTypes.func, onReblog: PropTypes.func, @@ -44,6 +46,7 @@ export default class StatusActionBar extends ImmutablePureComponent { onBlock: PropTypes.func, onReport: PropTypes.func, onMuteConversation: PropTypes.func, + onDeleteNotification: PropTypes.func, me: PropTypes.number.isRequired, withDismiss: PropTypes.bool, intl: PropTypes.object.isRequired, @@ -97,6 +100,10 @@ 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'; @@ -112,6 +119,7 @@ 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 a8aa6efe9..c45b2e0ec 100644 --- a/app/javascript/glitch/components/status/container.js +++ b/app/javascript/glitch/components/status/container.js @@ -50,6 +50,7 @@ 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,6 +246,9 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ } }, + 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 1d135754a..314e8b51c 100644 --- a/app/javascript/glitch/components/status/index.js +++ b/app/javascript/glitch/components/status/index.js @@ -170,6 +170,7 @@ 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, @@ -177,6 +178,7 @@ export default class Status extends ImmutablePureComponent { collapse : PropTypes.bool, prepend : PropTypes.string, withDismiss : PropTypes.bool, + notificationId : PropTypes.number, intersectionObserverWrapper : PropTypes.object, }; @@ -685,6 +687,8 @@ collapsed. 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 ef9209e81..d9b04b5ec 100644 --- a/app/javascript/glitch/components/status/prepend.js +++ b/app/javascript/glitch/components/status/prepend.js @@ -23,11 +23,17 @@ 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' }, +}); + /* * * * */ /* @@ -53,12 +59,16 @@ element. */ +@injectIntl export default class StatusPrepend extends React.PureComponent { static propTypes = { type: PropTypes.string.isRequired, account: ImmutablePropTypes.map.isRequired, parseClick: PropTypes.func.isRequired, + notificationId: PropTypes.number, + onDeleteNotification: PropTypes.func, + intl: PropTypes.object.isRequired, }; /* @@ -77,6 +87,10 @@ an account link is clicked. parseClick(e, `/accounts/${+account.get('id')}`); } + handleNotificationDeleteClick = () => { + this.props.onDeleteNotification(this.props.notificationId); + } + /* #### `<Message>`. @@ -145,7 +159,19 @@ the `<Message>` inside of an <aside>. render () { const { Message } = this; - const { type } = this.props; + 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; return !type ? null : ( <aside className={type === 'reblogged_by' ? 'status__prepend' : 'notification__message'}> @@ -157,6 +183,7 @@ the `<Message>` inside of an <aside>. /> </div> <Message /> + {dismiss} </aside> ); } diff --git a/app/javascript/mastodon/actions/notifications.js b/app/javascript/mastodon/actions/notifications.js index c7d248122..b2a0f7ac3 100644 --- a/app/javascript/mastodon/actions/notifications.js +++ b/app/javascript/mastodon/actions/notifications.js @@ -6,6 +6,8 @@ import { defineMessages } from 'react-intl'; export const NOTIFICATIONS_UPDATE = 'NOTIFICATIONS_UPDATE'; +export const NOTIFICATION_DELETE_SUCCESS = 'NOTIFICATION_DELETE_SUCCESS'; + export const NOTIFICATIONS_REFRESH_REQUEST = 'NOTIFICATIONS_REFRESH_REQUEST'; export const NOTIFICATIONS_REFRESH_SUCCESS = 'NOTIFICATIONS_REFRESH_SUCCESS'; export const NOTIFICATIONS_REFRESH_FAIL = 'NOTIFICATIONS_REFRESH_FAIL'; @@ -187,3 +189,18 @@ export function scrollTopNotifications(top) { top, }; }; + +export function deleteNotification(id) { + return (dispatch, getState) => { + api(getState).delete(`/api/v1/notifications/${id}`).then(() => { + dispatch(deleteNotificationSuccess(id)); + }); + }; +}; + +export function deleteNotificationSuccess(id) { + return { + type: NOTIFICATION_DELETE_SUCCESS, + id: id, + }; +}; diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index cf29e38da..d2e5f90ea 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -190,6 +190,7 @@ "status.show_more": "Show more", "status.uncollapse": "Uncollapse", "status.unmute_conversation": "Unmute conversation", + "status.dismiss_notification": "Dismiss notification", "tabs_bar.compose": "Compose", "tabs_bar.federated_timeline": "Federated", "tabs_bar.home": "Home", diff --git a/app/javascript/mastodon/reducers/notifications.js b/app/javascript/mastodon/reducers/notifications.js index 0063d24e4..da5fcde84 100644 --- a/app/javascript/mastodon/reducers/notifications.js +++ b/app/javascript/mastodon/reducers/notifications.js @@ -8,6 +8,7 @@ import { NOTIFICATIONS_EXPAND_FAIL, NOTIFICATIONS_CLEAR, NOTIFICATIONS_SCROLL_TOP, + NOTIFICATION_DELETE_SUCCESS, } from '../actions/notifications'; import { ACCOUNT_BLOCK_SUCCESS } from '../actions/accounts'; import { TIMELINE_DELETE } from '../actions/timelines'; @@ -92,6 +93,10 @@ 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)); +}; + export default function notifications(state = initialState, action) { switch(action.type) { case NOTIFICATIONS_REFRESH_REQUEST: @@ -113,6 +118,8 @@ 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); default: return state; } diff --git a/app/javascript/styles/components.scss b/app/javascript/styles/components.scss index 6cca3666a..b06c99c95 100644 --- a/app/javascript/styles/components.scss +++ b/app/javascript/styles/components.scss @@ -617,6 +617,27 @@ position: absolute; } +.status__prepend-dismiss-button { + border: 0; + background: transparent; + position: absolute; + right: -3px; + opacity: 0; + transition: opacity 0.1s ease-in-out; + + i.fa { + color: crimson; + } + + .notification__message:hover & { + opacity: 1; + } + + .notification-follow & { + right: 6px; + } +} + .status { padding: 8px 10px; padding-left: 68px; diff --git a/config/routes.rb b/config/routes.rb index 963fedcb4..a63fb3ae6 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -175,7 +175,7 @@ Rails.application.routes.draw do end end - resources :notifications, only: [:index, :show] do + resources :notifications, only: [:index, :show, :destroy] do collection do post :clear post :dismiss |