Skip to content

Commit

Permalink
Add ability to report, block and mute from notification requests list (
Browse files Browse the repository at this point in the history
…mastodon#31309)

Co-authored-by: Renaud Chaput <[email protected]>
  • Loading branch information
ClearlyClaire and renchap authored Aug 9, 2024
1 parent eaedd52 commit 658addc
Show file tree
Hide file tree
Showing 9 changed files with 395 additions and 32 deletions.
64 changes: 64 additions & 0 deletions app/javascript/mastodon/actions/notifications.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,14 @@ export const NOTIFICATION_REQUEST_DISMISS_REQUEST = 'NOTIFICATION_REQUEST_DISMIS
export const NOTIFICATION_REQUEST_DISMISS_SUCCESS = 'NOTIFICATION_REQUEST_DISMISS_SUCCESS';
export const NOTIFICATION_REQUEST_DISMISS_FAIL = 'NOTIFICATION_REQUEST_DISMISS_FAIL';

export const NOTIFICATION_REQUESTS_ACCEPT_REQUEST = 'NOTIFICATION_REQUESTS_ACCEPT_REQUEST';
export const NOTIFICATION_REQUESTS_ACCEPT_SUCCESS = 'NOTIFICATION_REQUESTS_ACCEPT_SUCCESS';
export const NOTIFICATION_REQUESTS_ACCEPT_FAIL = 'NOTIFICATION_REQUESTS_ACCEPT_FAIL';

export const NOTIFICATION_REQUESTS_DISMISS_REQUEST = 'NOTIFICATION_REQUESTS_DISMISS_REQUEST';
export const NOTIFICATION_REQUESTS_DISMISS_SUCCESS = 'NOTIFICATION_REQUESTS_DISMISS_SUCCESS';
export const NOTIFICATION_REQUESTS_DISMISS_FAIL = 'NOTIFICATION_REQUESTS_DISMISS_FAIL';

export const NOTIFICATIONS_FOR_REQUEST_FETCH_REQUEST = 'NOTIFICATIONS_FOR_REQUEST_FETCH_REQUEST';
export const NOTIFICATIONS_FOR_REQUEST_FETCH_SUCCESS = 'NOTIFICATIONS_FOR_REQUEST_FETCH_SUCCESS';
export const NOTIFICATIONS_FOR_REQUEST_FETCH_FAIL = 'NOTIFICATIONS_FOR_REQUEST_FETCH_FAIL';
Expand Down Expand Up @@ -496,6 +504,62 @@ export const dismissNotificationRequestFail = (id, error) => ({
error,
});

export const acceptNotificationRequests = (ids) => (dispatch, getState) => {
const count = ids.reduce((count, id) => count + selectNotificationCountForRequest(getState(), id), 0);
dispatch(acceptNotificationRequestsRequest(ids));

api().post(`/api/v1/notifications/requests/accept`, { id: ids }).then(() => {
dispatch(acceptNotificationRequestsSuccess(ids));
dispatch(decreasePendingNotificationsCount(count));
}).catch(err => {
dispatch(acceptNotificationRequestFail(ids, err));
});
};

export const acceptNotificationRequestsRequest = ids => ({
type: NOTIFICATION_REQUESTS_ACCEPT_REQUEST,
ids,
});

export const acceptNotificationRequestsSuccess = ids => ({
type: NOTIFICATION_REQUESTS_ACCEPT_SUCCESS,
ids,
});

export const acceptNotificationRequestsFail = (ids, error) => ({
type: NOTIFICATION_REQUESTS_ACCEPT_FAIL,
ids,
error,
});

export const dismissNotificationRequests = (ids) => (dispatch, getState) => {
const count = ids.reduce((count, id) => count + selectNotificationCountForRequest(getState(), id), 0);
dispatch(acceptNotificationRequestsRequest(ids));

api().post(`/api/v1/notifications/requests/dismiss`, { id: ids }).then(() => {
dispatch(dismissNotificationRequestsSuccess(ids));
dispatch(decreasePendingNotificationsCount(count));
}).catch(err => {
dispatch(dismissNotificationRequestFail(ids, err));
});
};

export const dismissNotificationRequestsRequest = ids => ({
type: NOTIFICATION_REQUESTS_DISMISS_REQUEST,
ids,
});

export const dismissNotificationRequestsSuccess = ids => ({
type: NOTIFICATION_REQUESTS_DISMISS_SUCCESS,
ids,
});

export const dismissNotificationRequestsFail = (ids, error) => ({
type: NOTIFICATION_REQUESTS_DISMISS_FAIL,
ids,
error,
});

export const fetchNotificationsForRequest = accountId => (dispatch, getState) => {
const current = getState().getIn(['notificationRequests', 'current']);
const params = { account_id: accountId };
Expand Down
13 changes: 11 additions & 2 deletions app/javascript/mastodon/components/check_box.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import classNames from 'classnames';

import CheckIndeterminateSmallIcon from '@/material-icons/400-24px/check_indeterminate_small.svg?react';
import DoneIcon from '@/material-icons/400-24px/done.svg?react';

import { Icon } from './icon';

interface Props {
value: string;
checked: boolean;
indeterminate: boolean;
name: string;
onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
label: React.ReactNode;
Expand All @@ -16,6 +18,7 @@ export const CheckBox: React.FC<Props> = ({
name,
value,
checked,
indeterminate,
onChange,
label,
}) => {
Expand All @@ -29,8 +32,14 @@ export const CheckBox: React.FC<Props> = ({
onChange={onChange}
/>

<span className={classNames('check-box__input', { checked })}>
{checked && <Icon id='check' icon={DoneIcon} />}
<span
className={classNames('check-box__input', { checked, indeterminate })}
>
{indeterminate ? (
<Icon id='indeterminate' icon={CheckIndeterminateSmallIcon} />
) : (
checked && <Icon id='check' icon={DoneIcon} />
)}
</span>

<span>{label}</span>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,21 @@ import { useCallback } from 'react';

import { defineMessages, useIntl } from 'react-intl';

import { Link } from 'react-router-dom';
import classNames from 'classnames';
import { Link, useHistory } from 'react-router-dom';

import { useSelector, useDispatch } from 'react-redux';

import DeleteIcon from '@/material-icons/400-24px/delete.svg?react';
import DoneIcon from '@/material-icons/400-24px/done.svg?react';
import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
import { initBlockModal } from 'mastodon/actions/blocks';
import { initMuteModal } from 'mastodon/actions/mutes';
import { acceptNotificationRequest, dismissNotificationRequest } from 'mastodon/actions/notifications';
import { initReport } from 'mastodon/actions/reports';
import { Avatar } from 'mastodon/components/avatar';
import { CheckBox } from 'mastodon/components/check_box';
import { IconButton } from 'mastodon/components/icon_button';
import DropdownMenuContainer from 'mastodon/containers/dropdown_menu_container';
import { makeGetAccount } from 'mastodon/selectors';
import { toCappedNumber } from 'mastodon/utils/numbers';

Expand All @@ -20,12 +26,18 @@ const getAccount = makeGetAccount();
const messages = defineMessages({
accept: { id: 'notification_requests.accept', defaultMessage: 'Accept' },
dismiss: { id: 'notification_requests.dismiss', defaultMessage: 'Dismiss' },
view: { id: 'notification_requests.view', defaultMessage: 'View notifications' },
mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' },
block: { id: 'account.block', defaultMessage: 'Block @{name}' },
report: { id: 'status.report', defaultMessage: 'Report @{name}' },
more: { id: 'status.more', defaultMessage: 'More' },
});

export const NotificationRequest = ({ id, accountId, notificationsCount }) => {
export const NotificationRequest = ({ id, accountId, notificationsCount, checked, showCheckbox, toggleCheck }) => {
const dispatch = useDispatch();
const account = useSelector(state => getAccount(state, accountId));
const intl = useIntl();
const { push: historyPush } = useHistory();

const handleDismiss = useCallback(() => {
dispatch(dismissNotificationRequest(id));
Expand All @@ -35,9 +47,51 @@ export const NotificationRequest = ({ id, accountId, notificationsCount }) => {
dispatch(acceptNotificationRequest(id));
}, [dispatch, id]);

const handleMute = useCallback(() => {
dispatch(initMuteModal(account));
}, [dispatch, account]);

const handleBlock = useCallback(() => {
dispatch(initBlockModal(account));
}, [dispatch, account]);

const handleReport = useCallback(() => {
dispatch(initReport(account));
}, [dispatch, account]);

const handleView = useCallback(() => {
historyPush(`/notifications/requests/${id}`);
}, [historyPush, id]);

const menu = [
{ text: intl.formatMessage(messages.view), action: handleView },
null,
{ text: intl.formatMessage(messages.accept), action: handleAccept },
null,
{ text: intl.formatMessage(messages.mute, { name: account.username }), action: handleMute, dangerous: true },
{ text: intl.formatMessage(messages.block, { name: account.username }), action: handleBlock, dangerous: true },
{ text: intl.formatMessage(messages.report, { name: account.username }), action: handleReport, dangerous: true },
];

const handleCheck = useCallback(() => {
toggleCheck(id);
}, [toggleCheck, id]);

const handleClick = useCallback((e) => {
if (showCheckbox) {
toggleCheck(id);
e.preventDefault();
e.stopPropagation();
}
}, [toggleCheck, id, showCheckbox]);

return (
<div className='notification-request'>
<Link to={`/notifications/requests/${id}`} className='notification-request__link'>
/* eslint-disable-next-line jsx-a11y/no-static-element-interactions -- this is just a minor affordance, but we will need a comprehensive accessibility pass */
<div className={classNames('notification-request', showCheckbox && 'notification-request--forced-checkbox')} onClick={handleClick}>
<div className='notification-request__checkbox' aria-hidden={!showCheckbox}>
<CheckBox checked={checked} onChange={handleCheck} />
</div>
<Link to={`/notifications/requests/${id}`} className='notification-request__link' onClick={handleClick} title={account?.acct}>
<Avatar account={account} size={40} counter={toCappedNumber(notificationsCount)} />

<div className='notification-request__name'>
Expand All @@ -51,7 +105,13 @@ export const NotificationRequest = ({ id, accountId, notificationsCount }) => {

<div className='notification-request__actions'>
<IconButton iconComponent={DeleteIcon} onClick={handleDismiss} title={intl.formatMessage(messages.dismiss)} />
<IconButton iconComponent={DoneIcon} onClick={handleAccept} title={intl.formatMessage(messages.accept)} />
<DropdownMenuContainer
items={menu}
icons='ellipsis-h'
iconComponent={MoreHorizIcon}
direction='right'
title={intl.formatMessage(messages.more)}
/>
</div>
</div>
);
Expand All @@ -61,4 +121,7 @@ NotificationRequest.propTypes = {
id: PropTypes.string.isRequired,
accountId: PropTypes.string.isRequired,
notificationsCount: PropTypes.string.isRequired,
checked: PropTypes.bool,
showCheckbox: PropTypes.bool,
toggleCheck: PropTypes.func,
};
Loading

0 comments on commit 658addc

Please sign in to comment.