mirror of
https://github.com/mastodon/mastodon.git
synced 2024-12-23 09:35:03 +00:00
Add ability to report, block and mute from notification requests list (#31309)
Co-authored-by: Renaud Chaput <renchap@gmail.com>
This commit is contained in:
parent
eaedd52def
commit
658addcbf7
|
@ -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_SUCCESS = 'NOTIFICATION_REQUEST_DISMISS_SUCCESS';
|
||||||
export const NOTIFICATION_REQUEST_DISMISS_FAIL = 'NOTIFICATION_REQUEST_DISMISS_FAIL';
|
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_REQUEST = 'NOTIFICATIONS_FOR_REQUEST_FETCH_REQUEST';
|
||||||
export const NOTIFICATIONS_FOR_REQUEST_FETCH_SUCCESS = 'NOTIFICATIONS_FOR_REQUEST_FETCH_SUCCESS';
|
export const NOTIFICATIONS_FOR_REQUEST_FETCH_SUCCESS = 'NOTIFICATIONS_FOR_REQUEST_FETCH_SUCCESS';
|
||||||
export const NOTIFICATIONS_FOR_REQUEST_FETCH_FAIL = 'NOTIFICATIONS_FOR_REQUEST_FETCH_FAIL';
|
export const NOTIFICATIONS_FOR_REQUEST_FETCH_FAIL = 'NOTIFICATIONS_FOR_REQUEST_FETCH_FAIL';
|
||||||
|
@ -496,6 +504,62 @@ export const dismissNotificationRequestFail = (id, error) => ({
|
||||||
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) => {
|
export const fetchNotificationsForRequest = accountId => (dispatch, getState) => {
|
||||||
const current = getState().getIn(['notificationRequests', 'current']);
|
const current = getState().getIn(['notificationRequests', 'current']);
|
||||||
const params = { account_id: accountId };
|
const params = { account_id: accountId };
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import classNames from 'classnames';
|
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 DoneIcon from '@/material-icons/400-24px/done.svg?react';
|
||||||
|
|
||||||
import { Icon } from './icon';
|
import { Icon } from './icon';
|
||||||
|
@ -7,6 +8,7 @@ import { Icon } from './icon';
|
||||||
interface Props {
|
interface Props {
|
||||||
value: string;
|
value: string;
|
||||||
checked: boolean;
|
checked: boolean;
|
||||||
|
indeterminate: boolean;
|
||||||
name: string;
|
name: string;
|
||||||
onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
||||||
label: React.ReactNode;
|
label: React.ReactNode;
|
||||||
|
@ -16,6 +18,7 @@ export const CheckBox: React.FC<Props> = ({
|
||||||
name,
|
name,
|
||||||
value,
|
value,
|
||||||
checked,
|
checked,
|
||||||
|
indeterminate,
|
||||||
onChange,
|
onChange,
|
||||||
label,
|
label,
|
||||||
}) => {
|
}) => {
|
||||||
|
@ -29,8 +32,14 @@ export const CheckBox: React.FC<Props> = ({
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<span className={classNames('check-box__input', { checked })}>
|
<span
|
||||||
{checked && <Icon id='check' icon={DoneIcon} />}
|
className={classNames('check-box__input', { checked, indeterminate })}
|
||||||
|
>
|
||||||
|
{indeterminate ? (
|
||||||
|
<Icon id='indeterminate' icon={CheckIndeterminateSmallIcon} />
|
||||||
|
) : (
|
||||||
|
checked && <Icon id='check' icon={DoneIcon} />
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span>{label}</span>
|
<span>{label}</span>
|
||||||
|
|
|
@ -3,15 +3,21 @@ import { useCallback } from 'react';
|
||||||
|
|
||||||
import { defineMessages, useIntl } from 'react-intl';
|
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 { useSelector, useDispatch } from 'react-redux';
|
||||||
|
|
||||||
import DeleteIcon from '@/material-icons/400-24px/delete.svg?react';
|
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 { acceptNotificationRequest, dismissNotificationRequest } from 'mastodon/actions/notifications';
|
||||||
|
import { initReport } from 'mastodon/actions/reports';
|
||||||
import { Avatar } from 'mastodon/components/avatar';
|
import { Avatar } from 'mastodon/components/avatar';
|
||||||
|
import { CheckBox } from 'mastodon/components/check_box';
|
||||||
import { IconButton } from 'mastodon/components/icon_button';
|
import { IconButton } from 'mastodon/components/icon_button';
|
||||||
|
import DropdownMenuContainer from 'mastodon/containers/dropdown_menu_container';
|
||||||
import { makeGetAccount } from 'mastodon/selectors';
|
import { makeGetAccount } from 'mastodon/selectors';
|
||||||
import { toCappedNumber } from 'mastodon/utils/numbers';
|
import { toCappedNumber } from 'mastodon/utils/numbers';
|
||||||
|
|
||||||
|
@ -20,12 +26,18 @@ const getAccount = makeGetAccount();
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
accept: { id: 'notification_requests.accept', defaultMessage: 'Accept' },
|
accept: { id: 'notification_requests.accept', defaultMessage: 'Accept' },
|
||||||
dismiss: { id: 'notification_requests.dismiss', defaultMessage: 'Dismiss' },
|
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 dispatch = useDispatch();
|
||||||
const account = useSelector(state => getAccount(state, accountId));
|
const account = useSelector(state => getAccount(state, accountId));
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
const { push: historyPush } = useHistory();
|
||||||
|
|
||||||
const handleDismiss = useCallback(() => {
|
const handleDismiss = useCallback(() => {
|
||||||
dispatch(dismissNotificationRequest(id));
|
dispatch(dismissNotificationRequest(id));
|
||||||
|
@ -35,9 +47,51 @@ export const NotificationRequest = ({ id, accountId, notificationsCount }) => {
|
||||||
dispatch(acceptNotificationRequest(id));
|
dispatch(acceptNotificationRequest(id));
|
||||||
}, [dispatch, 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 (
|
return (
|
||||||
<div className='notification-request'>
|
/* eslint-disable-next-line jsx-a11y/no-static-element-interactions -- this is just a minor affordance, but we will need a comprehensive accessibility pass */
|
||||||
<Link to={`/notifications/requests/${id}`} className='notification-request__link'>
|
<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)} />
|
<Avatar account={account} size={40} counter={toCappedNumber(notificationsCount)} />
|
||||||
|
|
||||||
<div className='notification-request__name'>
|
<div className='notification-request__name'>
|
||||||
|
@ -51,7 +105,13 @@ export const NotificationRequest = ({ id, accountId, notificationsCount }) => {
|
||||||
|
|
||||||
<div className='notification-request__actions'>
|
<div className='notification-request__actions'>
|
||||||
<IconButton iconComponent={DeleteIcon} onClick={handleDismiss} title={intl.formatMessage(messages.dismiss)} />
|
<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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -61,4 +121,7 @@ NotificationRequest.propTypes = {
|
||||||
id: PropTypes.string.isRequired,
|
id: PropTypes.string.isRequired,
|
||||||
accountId: PropTypes.string.isRequired,
|
accountId: PropTypes.string.isRequired,
|
||||||
notificationsCount: PropTypes.string.isRequired,
|
notificationsCount: PropTypes.string.isRequired,
|
||||||
|
checked: PropTypes.bool,
|
||||||
|
showCheckbox: PropTypes.bool,
|
||||||
|
toggleCheck: PropTypes.func,
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { useRef, useCallback, useEffect } from 'react';
|
import { useRef, useCallback, useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
|
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
@ -8,11 +8,15 @@ import { Helmet } from 'react-helmet';
|
||||||
import { useSelector, useDispatch } from 'react-redux';
|
import { useSelector, useDispatch } from 'react-redux';
|
||||||
|
|
||||||
import InventoryIcon from '@/material-icons/400-24px/inventory_2.svg?react';
|
import InventoryIcon from '@/material-icons/400-24px/inventory_2.svg?react';
|
||||||
import { fetchNotificationRequests, expandNotificationRequests } from 'mastodon/actions/notifications';
|
import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
|
||||||
|
import { openModal } from 'mastodon/actions/modal';
|
||||||
|
import { fetchNotificationRequests, expandNotificationRequests, acceptNotificationRequests, dismissNotificationRequests } from 'mastodon/actions/notifications';
|
||||||
import { changeSetting } from 'mastodon/actions/settings';
|
import { changeSetting } from 'mastodon/actions/settings';
|
||||||
|
import { CheckBox } from 'mastodon/components/check_box';
|
||||||
import Column from 'mastodon/components/column';
|
import Column from 'mastodon/components/column';
|
||||||
import ColumnHeader from 'mastodon/components/column_header';
|
import ColumnHeader from 'mastodon/components/column_header';
|
||||||
import ScrollableList from 'mastodon/components/scrollable_list';
|
import ScrollableList from 'mastodon/components/scrollable_list';
|
||||||
|
import DropdownMenuContainer from 'mastodon/containers/dropdown_menu_container';
|
||||||
|
|
||||||
import { NotificationRequest } from './components/notification_request';
|
import { NotificationRequest } from './components/notification_request';
|
||||||
import { PolicyControls } from './components/policy_controls';
|
import { PolicyControls } from './components/policy_controls';
|
||||||
|
@ -20,7 +24,18 @@ import SettingToggle from './components/setting_toggle';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
title: { id: 'notification_requests.title', defaultMessage: 'Filtered notifications' },
|
title: { id: 'notification_requests.title', defaultMessage: 'Filtered notifications' },
|
||||||
maximize: { id: 'notification_requests.maximize', defaultMessage: 'Maximize' }
|
maximize: { id: 'notification_requests.maximize', defaultMessage: 'Maximize' },
|
||||||
|
more: { id: 'status.more', defaultMessage: 'More' },
|
||||||
|
acceptAll: { id: 'notification_requests.accept_all', defaultMessage: 'Accept all' },
|
||||||
|
dismissAll: { id: 'notification_requests.dismiss_all', defaultMessage: 'Dismiss all' },
|
||||||
|
acceptMultiple: { id: 'notification_requests.accept_multiple', defaultMessage: '{count, plural, one {Accept # request} other {Accept # requests}}' },
|
||||||
|
dismissMultiple: { id: 'notification_requests.dismiss_multiple', defaultMessage: '{count, plural, one {Dismiss # request} other {Dismiss # requests}}' },
|
||||||
|
confirmAcceptAllTitle: { id: 'notification_requests.confirm_accept_all.title', defaultMessage: 'Accept notification requests?' },
|
||||||
|
confirmAcceptAllMessage: { id: 'notification_requests.confirm_accept_all.message', defaultMessage: 'You are about to accept {count, plural, one {one notification request} other {# notification requests}}. Are you sure you want to proceed?' },
|
||||||
|
confirmAcceptAllButton: { id: 'notification_requests.confirm_accept_all.button', defaultMessage: 'Accept all' },
|
||||||
|
confirmDismissAllTitle: { id: 'notification_requests.confirm_dismiss_all.title', defaultMessage: 'Dismiss notification requests?' },
|
||||||
|
confirmDismissAllMessage: { id: 'notification_requests.confirm_dismiss_all.message', defaultMessage: "You are about to dismiss {count, plural, one {one notification request} other {# notification requests}}. You won't be able to easily access {count, plural, one {it} other {them}} again. Are you sure you want to proceed?" },
|
||||||
|
confirmDismissAllButton: { id: 'notification_requests.confirm_dismiss_all.button', defaultMessage: 'Dismiss all' },
|
||||||
});
|
});
|
||||||
|
|
||||||
const ColumnSettings = () => {
|
const ColumnSettings = () => {
|
||||||
|
@ -55,6 +70,94 @@ const ColumnSettings = () => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const SelectRow = ({selectAllChecked, toggleSelectAll, selectedItems, selectionMode, setSelectionMode}) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const selectedCount = selectedItems.length;
|
||||||
|
|
||||||
|
const handleAcceptAll = useCallback(() => {
|
||||||
|
dispatch(openModal({
|
||||||
|
modalType: 'CONFIRM',
|
||||||
|
modalProps: {
|
||||||
|
title: intl.formatMessage(messages.confirmAcceptAllTitle),
|
||||||
|
message: intl.formatMessage(messages.confirmAcceptAllMessage, { count: selectedItems.length }),
|
||||||
|
confirm: intl.formatMessage(messages.confirmAcceptAllButton),
|
||||||
|
onConfirm: () =>
|
||||||
|
dispatch(acceptNotificationRequests(selectedItems)),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
}, [dispatch, intl, selectedItems]);
|
||||||
|
|
||||||
|
const handleDismissAll = useCallback(() => {
|
||||||
|
dispatch(openModal({
|
||||||
|
modalType: 'CONFIRM',
|
||||||
|
modalProps: {
|
||||||
|
title: intl.formatMessage(messages.confirmDismissAllTitle),
|
||||||
|
message: intl.formatMessage(messages.confirmDismissAllMessage, { count: selectedItems.length }),
|
||||||
|
confirm: intl.formatMessage(messages.confirmDismissAllButton),
|
||||||
|
onConfirm: () =>
|
||||||
|
dispatch(dismissNotificationRequests(selectedItems)),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
}, [dispatch, intl, selectedItems]);
|
||||||
|
|
||||||
|
const handleToggleSelectionMode = useCallback(() => {
|
||||||
|
setSelectionMode((mode) => !mode);
|
||||||
|
}, [setSelectionMode]);
|
||||||
|
|
||||||
|
const menu = selectedCount === 0 ?
|
||||||
|
[
|
||||||
|
{ text: intl.formatMessage(messages.acceptAll), action: handleAcceptAll },
|
||||||
|
{ text: intl.formatMessage(messages.dismissAll), action: handleDismissAll },
|
||||||
|
] : [
|
||||||
|
{ text: intl.formatMessage(messages.acceptMultiple, { count: selectedCount }), action: handleAcceptAll },
|
||||||
|
{ text: intl.formatMessage(messages.dismissMultiple, { count: selectedCount }), action: handleDismissAll },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='column-header__select-row'>
|
||||||
|
{selectionMode && (
|
||||||
|
<div className='column-header__select-row__checkbox'>
|
||||||
|
<CheckBox checked={selectAllChecked} indeterminate={selectedCount > 0 && !selectAllChecked} onChange={toggleSelectAll} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className='column-header__select-row__selection-mode'>
|
||||||
|
<button className='text-btn' tabIndex={0} onClick={handleToggleSelectionMode}>
|
||||||
|
{selectionMode ? (
|
||||||
|
<FormattedMessage id='notification_requests.exit_selection_mode' defaultMessage='Cancel' />
|
||||||
|
) :
|
||||||
|
(
|
||||||
|
<FormattedMessage id='notification_requests.enter_selection_mode' defaultMessage='Select' />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{selectedCount > 0 &&
|
||||||
|
<div className='column-header__select-row__selected-count'>
|
||||||
|
{selectedCount} selected
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<div className='column-header__select-row__actions'>
|
||||||
|
<DropdownMenuContainer
|
||||||
|
items={menu}
|
||||||
|
icons='ellipsis-h'
|
||||||
|
iconComponent={MoreHorizIcon}
|
||||||
|
direction='right'
|
||||||
|
title={intl.formatMessage(messages.more)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
SelectRow.propTypes = {
|
||||||
|
selectAllChecked: PropTypes.func.isRequired,
|
||||||
|
toggleSelectAll: PropTypes.func.isRequired,
|
||||||
|
selectedItems: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||||
|
selectionMode: PropTypes.bool,
|
||||||
|
setSelectionMode: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
export const NotificationRequests = ({ multiColumn }) => {
|
export const NotificationRequests = ({ multiColumn }) => {
|
||||||
const columnRef = useRef();
|
const columnRef = useRef();
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
@ -63,10 +166,40 @@ export const NotificationRequests = ({ multiColumn }) => {
|
||||||
const notificationRequests = useSelector(state => state.getIn(['notificationRequests', 'items']));
|
const notificationRequests = useSelector(state => state.getIn(['notificationRequests', 'items']));
|
||||||
const hasMore = useSelector(state => !!state.getIn(['notificationRequests', 'next']));
|
const hasMore = useSelector(state => !!state.getIn(['notificationRequests', 'next']));
|
||||||
|
|
||||||
|
const [selectionMode, setSelectionMode] = useState(false);
|
||||||
|
const [checkedRequestIds, setCheckedRequestIds] = useState([]);
|
||||||
|
const [selectAllChecked, setSelectAllChecked] = useState(false);
|
||||||
|
|
||||||
const handleHeaderClick = useCallback(() => {
|
const handleHeaderClick = useCallback(() => {
|
||||||
columnRef.current?.scrollTop();
|
columnRef.current?.scrollTop();
|
||||||
}, [columnRef]);
|
}, [columnRef]);
|
||||||
|
|
||||||
|
const handleCheck = useCallback(id => {
|
||||||
|
setCheckedRequestIds(ids => {
|
||||||
|
const position = ids.indexOf(id);
|
||||||
|
|
||||||
|
if(position > -1)
|
||||||
|
ids.splice(position, 1);
|
||||||
|
else
|
||||||
|
ids.push(id);
|
||||||
|
|
||||||
|
setSelectAllChecked(ids.length === notificationRequests.size);
|
||||||
|
|
||||||
|
return [...ids];
|
||||||
|
});
|
||||||
|
}, [setCheckedRequestIds, notificationRequests]);
|
||||||
|
|
||||||
|
const toggleSelectAll = useCallback(() => {
|
||||||
|
setSelectAllChecked(checked => {
|
||||||
|
if(checked)
|
||||||
|
setCheckedRequestIds([]);
|
||||||
|
else
|
||||||
|
setCheckedRequestIds(notificationRequests.map(request => request.get('id')).toArray());
|
||||||
|
|
||||||
|
return !checked;
|
||||||
|
});
|
||||||
|
}, [notificationRequests]);
|
||||||
|
|
||||||
const handleLoadMore = useCallback(() => {
|
const handleLoadMore = useCallback(() => {
|
||||||
dispatch(expandNotificationRequests());
|
dispatch(expandNotificationRequests());
|
||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
|
@ -84,6 +217,8 @@ export const NotificationRequests = ({ multiColumn }) => {
|
||||||
onClick={handleHeaderClick}
|
onClick={handleHeaderClick}
|
||||||
multiColumn={multiColumn}
|
multiColumn={multiColumn}
|
||||||
showBackButton
|
showBackButton
|
||||||
|
appendContent={
|
||||||
|
<SelectRow selectionMode={selectionMode} setSelectionMode={setSelectionMode} selectAllChecked={selectAllChecked} toggleSelectAll={toggleSelectAll} selectedItems={checkedRequestIds} />}
|
||||||
>
|
>
|
||||||
<ColumnSettings />
|
<ColumnSettings />
|
||||||
</ColumnHeader>
|
</ColumnHeader>
|
||||||
|
@ -104,6 +239,9 @@ export const NotificationRequests = ({ multiColumn }) => {
|
||||||
id={request.get('id')}
|
id={request.get('id')}
|
||||||
accountId={request.get('account')}
|
accountId={request.get('account')}
|
||||||
notificationsCount={request.get('notifications_count')}
|
notificationsCount={request.get('notifications_count')}
|
||||||
|
showCheckbox={selectionMode}
|
||||||
|
checked={checkedRequestIds.includes(request.get('id'))}
|
||||||
|
toggleCheck={handleCheck}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</ScrollableList>
|
</ScrollableList>
|
||||||
|
|
|
@ -518,13 +518,26 @@
|
||||||
"notification.status": "{name} just posted",
|
"notification.status": "{name} just posted",
|
||||||
"notification.update": "{name} edited a post",
|
"notification.update": "{name} edited a post",
|
||||||
"notification_requests.accept": "Accept",
|
"notification_requests.accept": "Accept",
|
||||||
|
"notification_requests.accept_all": "Accept all",
|
||||||
|
"notification_requests.accept_multiple": "{count, plural, one {Accept # request} other {Accept # requests}}",
|
||||||
|
"notification_requests.confirm_accept_all.button": "Accept all",
|
||||||
|
"notification_requests.confirm_accept_all.message": "You are about to accept {count, plural, one {one notification request} other {# notification requests}}. Are you sure you want to proceed?",
|
||||||
|
"notification_requests.confirm_accept_all.title": "Accept notification requests?",
|
||||||
|
"notification_requests.confirm_dismiss_all.button": "Dismiss all",
|
||||||
|
"notification_requests.confirm_dismiss_all.message": "You are about to dismiss {count, plural, one {one notification request} other {# notification requests}}. You won't be able to easily access {count, plural, one {it} other {them}} again. Are you sure you want to proceed?",
|
||||||
|
"notification_requests.confirm_dismiss_all.title": "Dismiss notification requests?",
|
||||||
"notification_requests.dismiss": "Dismiss",
|
"notification_requests.dismiss": "Dismiss",
|
||||||
|
"notification_requests.dismiss_all": "Dismiss all",
|
||||||
|
"notification_requests.dismiss_multiple": "{count, plural, one {Dismiss # request} other {Dismiss # requests}}",
|
||||||
|
"notification_requests.enter_selection_mode": "Select",
|
||||||
|
"notification_requests.exit_selection_mode": "Cancel",
|
||||||
"notification_requests.explainer_for_limited_account": "Notifications from this account have been filtered because the account has been limited by a moderator.",
|
"notification_requests.explainer_for_limited_account": "Notifications from this account have been filtered because the account has been limited by a moderator.",
|
||||||
"notification_requests.explainer_for_limited_remote_account": "Notifications from this account have been filtered because the account or its server has been limited by a moderator.",
|
"notification_requests.explainer_for_limited_remote_account": "Notifications from this account have been filtered because the account or its server has been limited by a moderator.",
|
||||||
"notification_requests.maximize": "Maximize",
|
"notification_requests.maximize": "Maximize",
|
||||||
"notification_requests.minimize_banner": "Minimize filtered notifications banner",
|
"notification_requests.minimize_banner": "Minimize filtered notifications banner",
|
||||||
"notification_requests.notifications_from": "Notifications from {name}",
|
"notification_requests.notifications_from": "Notifications from {name}",
|
||||||
"notification_requests.title": "Filtered notifications",
|
"notification_requests.title": "Filtered notifications",
|
||||||
|
"notification_requests.view": "View notifications",
|
||||||
"notifications.clear": "Clear notifications",
|
"notifications.clear": "Clear notifications",
|
||||||
"notifications.clear_confirmation": "Are you sure you want to permanently clear all your notifications?",
|
"notifications.clear_confirmation": "Are you sure you want to permanently clear all your notifications?",
|
||||||
"notifications.clear_title": "Clear notifications?",
|
"notifications.clear_title": "Clear notifications?",
|
||||||
|
|
|
@ -13,6 +13,8 @@ import {
|
||||||
NOTIFICATION_REQUEST_FETCH_FAIL,
|
NOTIFICATION_REQUEST_FETCH_FAIL,
|
||||||
NOTIFICATION_REQUEST_ACCEPT_REQUEST,
|
NOTIFICATION_REQUEST_ACCEPT_REQUEST,
|
||||||
NOTIFICATION_REQUEST_DISMISS_REQUEST,
|
NOTIFICATION_REQUEST_DISMISS_REQUEST,
|
||||||
|
NOTIFICATION_REQUESTS_ACCEPT_REQUEST,
|
||||||
|
NOTIFICATION_REQUESTS_DISMISS_REQUEST,
|
||||||
NOTIFICATIONS_FOR_REQUEST_FETCH_REQUEST,
|
NOTIFICATIONS_FOR_REQUEST_FETCH_REQUEST,
|
||||||
NOTIFICATIONS_FOR_REQUEST_FETCH_SUCCESS,
|
NOTIFICATIONS_FOR_REQUEST_FETCH_SUCCESS,
|
||||||
NOTIFICATIONS_FOR_REQUEST_FETCH_FAIL,
|
NOTIFICATIONS_FOR_REQUEST_FETCH_FAIL,
|
||||||
|
@ -83,6 +85,9 @@ export const notificationRequestsReducer = (state = initialState, action) => {
|
||||||
case NOTIFICATION_REQUEST_ACCEPT_REQUEST:
|
case NOTIFICATION_REQUEST_ACCEPT_REQUEST:
|
||||||
case NOTIFICATION_REQUEST_DISMISS_REQUEST:
|
case NOTIFICATION_REQUEST_DISMISS_REQUEST:
|
||||||
return removeRequest(state, action.id);
|
return removeRequest(state, action.id);
|
||||||
|
case NOTIFICATION_REQUESTS_ACCEPT_REQUEST:
|
||||||
|
case NOTIFICATION_REQUESTS_DISMISS_REQUEST:
|
||||||
|
return action.ids.reduce((state, id) => removeRequest(state, id), state);
|
||||||
case blockAccountSuccess.type:
|
case blockAccountSuccess.type:
|
||||||
return removeRequestByAccount(state, action.payload.relationship.id);
|
return removeRequestByAccount(state, action.payload.relationship.id);
|
||||||
case muteAccountSuccess.type:
|
case muteAccountSuccess.type:
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M240-440v-80h480v80H240Z"/></svg>
|
After Width: | Height: | Size: 130 B |
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M240-440v-80h480v80H240Z"/></svg>
|
After Width: | Height: | Size: 130 B |
|
@ -4320,6 +4320,36 @@ a.status-card {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.column-header__select-row {
|
||||||
|
border-width: 0 1px 1px;
|
||||||
|
border-style: solid;
|
||||||
|
border-color: var(--background-border-color);
|
||||||
|
padding: 15px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
&__checkbox .check-box {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__selection-mode {
|
||||||
|
flex-grow: 1;
|
||||||
|
|
||||||
|
.text-btn:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__actions {
|
||||||
|
.icon-button {
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid var(--background-border-color);
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.column-header {
|
.column-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
|
@ -7467,20 +7497,9 @@ a.status-card {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
|
|
||||||
&.checked {
|
&.checked,
|
||||||
|
&.indeterminate {
|
||||||
border-color: $ui-highlight-color;
|
border-color: $ui-highlight-color;
|
||||||
|
|
||||||
&::before {
|
|
||||||
position: absolute;
|
|
||||||
left: 2px;
|
|
||||||
top: 2px;
|
|
||||||
content: '';
|
|
||||||
display: block;
|
|
||||||
border-radius: 50%;
|
|
||||||
width: 12px;
|
|
||||||
height: 12px;
|
|
||||||
background: $ui-highlight-color;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
|
@ -7490,19 +7509,28 @@ a.status-card {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.radio-button.checked::before {
|
||||||
|
position: absolute;
|
||||||
|
left: 2px;
|
||||||
|
top: 2px;
|
||||||
|
content: '';
|
||||||
|
display: block;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
background: $ui-highlight-color;
|
||||||
|
}
|
||||||
|
|
||||||
.check-box {
|
.check-box {
|
||||||
&__input {
|
&__input {
|
||||||
width: 18px;
|
width: 18px;
|
||||||
height: 18px;
|
height: 18px;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
|
|
||||||
&.checked {
|
&.checked,
|
||||||
|
&.indeterminate {
|
||||||
background: $ui-highlight-color;
|
background: $ui-highlight-color;
|
||||||
color: $white;
|
color: $white;
|
||||||
|
|
||||||
&::before {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10223,12 +10251,28 @@ noscript {
|
||||||
}
|
}
|
||||||
|
|
||||||
.notification-request {
|
.notification-request {
|
||||||
|
$padding: 15px;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
padding: $padding;
|
||||||
gap: 16px;
|
gap: 8px;
|
||||||
padding: 15px;
|
position: relative;
|
||||||
border-bottom: 1px solid var(--background-border-color);
|
border-bottom: 1px solid var(--background-border-color);
|
||||||
|
|
||||||
|
&__checkbox {
|
||||||
|
position: absolute;
|
||||||
|
inset-inline-start: $padding;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
opacity: 0;
|
||||||
|
|
||||||
|
.check-box {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&__link {
|
&__link {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
@ -10286,6 +10330,31 @@ noscript {
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.notification-request__link {
|
||||||
|
transition: padding-inline-start 0.1s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--forced-checkbox {
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: lighten($ui-base-color, 1%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-request__checkbox {
|
||||||
|
opacity: 1;
|
||||||
|
width: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-request__link {
|
||||||
|
padding-inline-start: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-request__actions {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.more-from-author {
|
.more-from-author {
|
||||||
|
|
Loading…
Reference in a new issue