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:
		@@ -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';
 | 
			
		||||
@@ -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 };
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
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';
 | 
			
		||||
@@ -7,6 +8,7 @@ import { Icon } from './icon';
 | 
			
		||||
interface Props {
 | 
			
		||||
  value: string;
 | 
			
		||||
  checked: boolean;
 | 
			
		||||
  indeterminate: boolean;
 | 
			
		||||
  name: string;
 | 
			
		||||
  onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
 | 
			
		||||
  label: React.ReactNode;
 | 
			
		||||
@@ -16,6 +18,7 @@ export const CheckBox: React.FC<Props> = ({
 | 
			
		||||
  name,
 | 
			
		||||
  value,
 | 
			
		||||
  checked,
 | 
			
		||||
  indeterminate,
 | 
			
		||||
  onChange,
 | 
			
		||||
  label,
 | 
			
		||||
}) => {
 | 
			
		||||
@@ -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>
 | 
			
		||||
 
 | 
			
		||||
@@ -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';
 | 
			
		||||
 | 
			
		||||
@@ -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));
 | 
			
		||||
@@ -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'>
 | 
			
		||||
@@ -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>
 | 
			
		||||
  );
 | 
			
		||||
@@ -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,
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
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';
 | 
			
		||||
 | 
			
		||||
@@ -8,11 +8,15 @@ import { Helmet } from 'react-helmet';
 | 
			
		||||
import { useSelector, useDispatch } from 'react-redux';
 | 
			
		||||
 | 
			
		||||
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 { CheckBox } from 'mastodon/components/check_box';
 | 
			
		||||
import Column from 'mastodon/components/column';
 | 
			
		||||
import ColumnHeader from 'mastodon/components/column_header';
 | 
			
		||||
import ScrollableList from 'mastodon/components/scrollable_list';
 | 
			
		||||
import DropdownMenuContainer from 'mastodon/containers/dropdown_menu_container';
 | 
			
		||||
 | 
			
		||||
import { NotificationRequest } from './components/notification_request';
 | 
			
		||||
import { PolicyControls } from './components/policy_controls';
 | 
			
		||||
@@ -20,7 +24,18 @@ import SettingToggle from './components/setting_toggle';
 | 
			
		||||
 | 
			
		||||
const messages = defineMessages({
 | 
			
		||||
  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 = () => {
 | 
			
		||||
@@ -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 }) => {
 | 
			
		||||
  const columnRef = useRef();
 | 
			
		||||
  const intl = useIntl();
 | 
			
		||||
@@ -63,10 +166,40 @@ export const NotificationRequests = ({ multiColumn }) => {
 | 
			
		||||
  const notificationRequests = useSelector(state => state.getIn(['notificationRequests', 'items']));
 | 
			
		||||
  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(() => {
 | 
			
		||||
    columnRef.current?.scrollTop();
 | 
			
		||||
  }, [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(() => {
 | 
			
		||||
    dispatch(expandNotificationRequests());
 | 
			
		||||
  }, [dispatch]);
 | 
			
		||||
@@ -84,6 +217,8 @@ export const NotificationRequests = ({ multiColumn }) => {
 | 
			
		||||
        onClick={handleHeaderClick}
 | 
			
		||||
        multiColumn={multiColumn}
 | 
			
		||||
        showBackButton
 | 
			
		||||
        appendContent={
 | 
			
		||||
          <SelectRow selectionMode={selectionMode} setSelectionMode={setSelectionMode} selectAllChecked={selectAllChecked} toggleSelectAll={toggleSelectAll} selectedItems={checkedRequestIds} />}
 | 
			
		||||
      >
 | 
			
		||||
        <ColumnSettings />
 | 
			
		||||
      </ColumnHeader>
 | 
			
		||||
@@ -104,6 +239,9 @@ export const NotificationRequests = ({ multiColumn }) => {
 | 
			
		||||
            id={request.get('id')}
 | 
			
		||||
            accountId={request.get('account')}
 | 
			
		||||
            notificationsCount={request.get('notifications_count')}
 | 
			
		||||
            showCheckbox={selectionMode}
 | 
			
		||||
            checked={checkedRequestIds.includes(request.get('id'))}
 | 
			
		||||
            toggleCheck={handleCheck}
 | 
			
		||||
          />
 | 
			
		||||
        ))}
 | 
			
		||||
      </ScrollableList>
 | 
			
		||||
 
 | 
			
		||||
@@ -518,13 +518,26 @@
 | 
			
		||||
  "notification.status": "{name} just posted",
 | 
			
		||||
  "notification.update": "{name} edited a post",
 | 
			
		||||
  "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_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_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.minimize_banner": "Minimize filtered notifications banner",
 | 
			
		||||
  "notification_requests.notifications_from": "Notifications from {name}",
 | 
			
		||||
  "notification_requests.title": "Filtered notifications",
 | 
			
		||||
  "notification_requests.view": "View notifications",
 | 
			
		||||
  "notifications.clear": "Clear notifications",
 | 
			
		||||
  "notifications.clear_confirmation": "Are you sure you want to permanently clear all your notifications?",
 | 
			
		||||
  "notifications.clear_title": "Clear notifications?",
 | 
			
		||||
 
 | 
			
		||||
@@ -13,6 +13,8 @@ import {
 | 
			
		||||
  NOTIFICATION_REQUEST_FETCH_FAIL,
 | 
			
		||||
  NOTIFICATION_REQUEST_ACCEPT_REQUEST,
 | 
			
		||||
  NOTIFICATION_REQUEST_DISMISS_REQUEST,
 | 
			
		||||
  NOTIFICATION_REQUESTS_ACCEPT_REQUEST,
 | 
			
		||||
  NOTIFICATION_REQUESTS_DISMISS_REQUEST,
 | 
			
		||||
  NOTIFICATIONS_FOR_REQUEST_FETCH_REQUEST,
 | 
			
		||||
  NOTIFICATIONS_FOR_REQUEST_FETCH_SUCCESS,
 | 
			
		||||
  NOTIFICATIONS_FOR_REQUEST_FETCH_FAIL,
 | 
			
		||||
@@ -83,6 +85,9 @@ export const notificationRequestsReducer = (state = initialState, action) => {
 | 
			
		||||
  case NOTIFICATION_REQUEST_ACCEPT_REQUEST:
 | 
			
		||||
  case NOTIFICATION_REQUEST_DISMISS_REQUEST:
 | 
			
		||||
    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:
 | 
			
		||||
    return removeRequestByAccount(state, action.payload.relationship.id);
 | 
			
		||||
  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 {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  font-size: 16px;
 | 
			
		||||
@@ -7467,20 +7497,9 @@ a.status-card {
 | 
			
		||||
    flex: 0 0 auto;
 | 
			
		||||
    border-radius: 50%;
 | 
			
		||||
 | 
			
		||||
    &.checked {
 | 
			
		||||
    &.checked,
 | 
			
		||||
    &.indeterminate {
 | 
			
		||||
      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 {
 | 
			
		||||
@@ -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 {
 | 
			
		||||
  &__input {
 | 
			
		||||
    width: 18px;
 | 
			
		||||
    height: 18px;
 | 
			
		||||
    border-radius: 2px;
 | 
			
		||||
 | 
			
		||||
    &.checked {
 | 
			
		||||
    &.checked,
 | 
			
		||||
    &.indeterminate {
 | 
			
		||||
      background: $ui-highlight-color;
 | 
			
		||||
      color: $white;
 | 
			
		||||
 | 
			
		||||
      &::before {
 | 
			
		||||
        display: none;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -10223,12 +10251,28 @@ noscript {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.notification-request {
 | 
			
		||||
  $padding: 15px;
 | 
			
		||||
 | 
			
		||||
  display: flex;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  gap: 16px;
 | 
			
		||||
  padding: 15px;
 | 
			
		||||
  padding: $padding;
 | 
			
		||||
  gap: 8px;
 | 
			
		||||
  position: relative;
 | 
			
		||||
  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 {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
@@ -10286,6 +10330,31 @@ noscript {
 | 
			
		||||
      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 {
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user