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_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 { | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user