Merge remote-tracking branch 'parent/main' into upstream-20240813
This commit is contained in:
commit
e7ccc0539f
358 changed files with 4653 additions and 4261 deletions
|
@ -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,13 +1,52 @@
|
|||
import { useCallback } from 'react';
|
||||
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { useIntl, defineMessages, FormattedMessage } from 'react-intl';
|
||||
|
||||
import { openModal } from 'mastodon/actions/modal';
|
||||
import { updateNotificationsPolicy } from 'mastodon/actions/notification_policies';
|
||||
import type { AppDispatch } from 'mastodon/store';
|
||||
import { useAppSelector, useAppDispatch } from 'mastodon/store';
|
||||
|
||||
import { CheckboxWithLabel } from './checkbox_with_label';
|
||||
import { SelectWithLabel } from './select_with_label';
|
||||
|
||||
const messages = defineMessages({
|
||||
accept: { id: 'notifications.policy.accept', defaultMessage: 'Accept' },
|
||||
accept_hint: {
|
||||
id: 'notifications.policy.accept_hint',
|
||||
defaultMessage: 'Show in notifications',
|
||||
},
|
||||
filter: { id: 'notifications.policy.filter', defaultMessage: 'Filter' },
|
||||
filter_hint: {
|
||||
id: 'notifications.policy.filter_hint',
|
||||
defaultMessage: 'Send to filtered notifications inbox',
|
||||
},
|
||||
drop: { id: 'notifications.policy.drop', defaultMessage: 'Ignore' },
|
||||
drop_hint: {
|
||||
id: 'notifications.policy.drop_hint',
|
||||
defaultMessage: 'Send to the void, never to be seen again',
|
||||
},
|
||||
});
|
||||
|
||||
// TODO: change the following when we change the API
|
||||
const changeFilter = (
|
||||
dispatch: AppDispatch,
|
||||
filterType: string,
|
||||
value: string,
|
||||
) => {
|
||||
if (value === 'drop') {
|
||||
dispatch(
|
||||
openModal({
|
||||
modalType: 'IGNORE_NOTIFICATIONS',
|
||||
modalProps: { filterType },
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
void dispatch(updateNotificationsPolicy({ [filterType]: value }));
|
||||
}
|
||||
};
|
||||
|
||||
export const PolicyControls: React.FC = () => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const notificationPolicy = useAppSelector(
|
||||
|
@ -15,56 +54,74 @@ export const PolicyControls: React.FC = () => {
|
|||
);
|
||||
|
||||
const handleFilterNotFollowing = useCallback(
|
||||
(checked: boolean) => {
|
||||
void dispatch(
|
||||
updateNotificationsPolicy({ filter_not_following: checked }),
|
||||
);
|
||||
(value: string) => {
|
||||
changeFilter(dispatch, 'for_not_following', value);
|
||||
},
|
||||
[dispatch],
|
||||
);
|
||||
|
||||
const handleFilterNotFollowers = useCallback(
|
||||
(checked: boolean) => {
|
||||
void dispatch(
|
||||
updateNotificationsPolicy({ filter_not_followers: checked }),
|
||||
);
|
||||
(value: string) => {
|
||||
changeFilter(dispatch, 'for_not_followers', value);
|
||||
},
|
||||
[dispatch],
|
||||
);
|
||||
|
||||
const handleFilterNewAccounts = useCallback(
|
||||
(checked: boolean) => {
|
||||
void dispatch(
|
||||
updateNotificationsPolicy({ filter_new_accounts: checked }),
|
||||
);
|
||||
(value: string) => {
|
||||
changeFilter(dispatch, 'for_new_accounts', value);
|
||||
},
|
||||
[dispatch],
|
||||
);
|
||||
|
||||
const handleFilterPrivateMentions = useCallback(
|
||||
(checked: boolean) => {
|
||||
void dispatch(
|
||||
updateNotificationsPolicy({ filter_private_mentions: checked }),
|
||||
);
|
||||
(value: string) => {
|
||||
changeFilter(dispatch, 'for_private_mentions', value);
|
||||
},
|
||||
[dispatch],
|
||||
);
|
||||
|
||||
const handleFilterLimitedAccounts = useCallback(
|
||||
(value: string) => {
|
||||
changeFilter(dispatch, 'for_limited_accounts', value);
|
||||
},
|
||||
[dispatch],
|
||||
);
|
||||
|
||||
if (!notificationPolicy) return null;
|
||||
|
||||
const options = [
|
||||
{
|
||||
value: 'accept',
|
||||
text: intl.formatMessage(messages.accept),
|
||||
meta: intl.formatMessage(messages.accept_hint),
|
||||
},
|
||||
{
|
||||
value: 'filter',
|
||||
text: intl.formatMessage(messages.filter),
|
||||
meta: intl.formatMessage(messages.filter_hint),
|
||||
},
|
||||
{
|
||||
value: 'drop',
|
||||
text: intl.formatMessage(messages.drop),
|
||||
meta: intl.formatMessage(messages.drop_hint),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<section>
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id='notifications.policy.title'
|
||||
defaultMessage='Filter out notifications from…'
|
||||
defaultMessage='Manage notifications from…'
|
||||
/>
|
||||
</h3>
|
||||
|
||||
<div className='column-settings__row'>
|
||||
<CheckboxWithLabel
|
||||
checked={notificationPolicy.filter_not_following}
|
||||
<SelectWithLabel
|
||||
value={notificationPolicy.for_not_following}
|
||||
onChange={handleFilterNotFollowing}
|
||||
options={options}
|
||||
>
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
|
@ -78,11 +135,12 @@ export const PolicyControls: React.FC = () => {
|
|||
defaultMessage='Until you manually approve them'
|
||||
/>
|
||||
</span>
|
||||
</CheckboxWithLabel>
|
||||
</SelectWithLabel>
|
||||
|
||||
<CheckboxWithLabel
|
||||
checked={notificationPolicy.filter_not_followers}
|
||||
<SelectWithLabel
|
||||
value={notificationPolicy.for_not_followers}
|
||||
onChange={handleFilterNotFollowers}
|
||||
options={options}
|
||||
>
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
|
@ -97,11 +155,12 @@ export const PolicyControls: React.FC = () => {
|
|||
values={{ days: 3 }}
|
||||
/>
|
||||
</span>
|
||||
</CheckboxWithLabel>
|
||||
</SelectWithLabel>
|
||||
|
||||
<CheckboxWithLabel
|
||||
checked={notificationPolicy.filter_new_accounts}
|
||||
<SelectWithLabel
|
||||
value={notificationPolicy.for_new_accounts}
|
||||
onChange={handleFilterNewAccounts}
|
||||
options={options}
|
||||
>
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
|
@ -116,11 +175,12 @@ export const PolicyControls: React.FC = () => {
|
|||
values={{ days: 30 }}
|
||||
/>
|
||||
</span>
|
||||
</CheckboxWithLabel>
|
||||
</SelectWithLabel>
|
||||
|
||||
<CheckboxWithLabel
|
||||
checked={notificationPolicy.filter_private_mentions}
|
||||
<SelectWithLabel
|
||||
value={notificationPolicy.for_private_mentions}
|
||||
onChange={handleFilterPrivateMentions}
|
||||
options={options}
|
||||
>
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
|
@ -134,7 +194,26 @@ export const PolicyControls: React.FC = () => {
|
|||
defaultMessage="Filtered unless it's in reply to your own mention or if you follow the sender"
|
||||
/>
|
||||
</span>
|
||||
</CheckboxWithLabel>
|
||||
</SelectWithLabel>
|
||||
|
||||
<SelectWithLabel
|
||||
value={notificationPolicy.for_limited_accounts}
|
||||
onChange={handleFilterLimitedAccounts}
|
||||
options={options}
|
||||
>
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
id='notifications.policy.filter_limited_accounts_title'
|
||||
defaultMessage='Moderated accounts'
|
||||
/>
|
||||
</strong>
|
||||
<span className='hint'>
|
||||
<FormattedMessage
|
||||
id='notifications.policy.filter_limited_accounts_hint'
|
||||
defaultMessage='Limited by server moderators'
|
||||
/>
|
||||
</span>
|
||||
</SelectWithLabel>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
|
|
@ -0,0 +1,153 @@
|
|||
import type { PropsWithChildren } from 'react';
|
||||
import { useCallback, useState, useRef } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import type { Placement, State as PopperState } from '@popperjs/core';
|
||||
import Overlay from 'react-overlays/Overlay';
|
||||
|
||||
import ArrowDropDownIcon from '@/material-icons/400-24px/arrow_drop_down.svg?react';
|
||||
import type { SelectItem } from 'mastodon/components/dropdown_selector';
|
||||
import { DropdownSelector } from 'mastodon/components/dropdown_selector';
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
|
||||
interface DropdownProps {
|
||||
value: string;
|
||||
options: SelectItem[];
|
||||
disabled?: boolean;
|
||||
onChange: (value: string) => void;
|
||||
placement?: Placement;
|
||||
}
|
||||
|
||||
const Dropdown: React.FC<DropdownProps> = ({
|
||||
value,
|
||||
options,
|
||||
disabled,
|
||||
onChange,
|
||||
placement: initialPlacement = 'bottom-end',
|
||||
}) => {
|
||||
const activeElementRef = useRef<Element | null>(null);
|
||||
const containerRef = useRef(null);
|
||||
const [isOpen, setOpen] = useState<boolean>(false);
|
||||
const [placement, setPlacement] = useState<Placement>(initialPlacement);
|
||||
|
||||
const handleToggle = useCallback(() => {
|
||||
if (
|
||||
isOpen &&
|
||||
activeElementRef.current &&
|
||||
activeElementRef.current instanceof HTMLElement
|
||||
) {
|
||||
activeElementRef.current.focus({ preventScroll: true });
|
||||
}
|
||||
|
||||
setOpen(!isOpen);
|
||||
}, [isOpen, setOpen]);
|
||||
|
||||
const handleMouseDown = useCallback(() => {
|
||||
if (!isOpen) activeElementRef.current = document.activeElement;
|
||||
}, [isOpen]);
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(e: React.KeyboardEvent) => {
|
||||
switch (e.key) {
|
||||
case ' ':
|
||||
case 'Enter':
|
||||
if (!isOpen) activeElementRef.current = document.activeElement;
|
||||
break;
|
||||
}
|
||||
},
|
||||
[isOpen],
|
||||
);
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
if (
|
||||
isOpen &&
|
||||
activeElementRef.current &&
|
||||
activeElementRef.current instanceof HTMLElement
|
||||
)
|
||||
activeElementRef.current.focus({ preventScroll: true });
|
||||
setOpen(false);
|
||||
}, [isOpen]);
|
||||
|
||||
const handleOverlayEnter = useCallback(
|
||||
(state: Partial<PopperState>) => {
|
||||
if (state.placement) setPlacement(state.placement);
|
||||
},
|
||||
[setPlacement],
|
||||
);
|
||||
|
||||
const valueOption = options.find((item) => item.value === value);
|
||||
|
||||
return (
|
||||
<div ref={containerRef}>
|
||||
<button
|
||||
type='button'
|
||||
onClick={handleToggle}
|
||||
onMouseDown={handleMouseDown}
|
||||
onKeyDown={handleKeyDown}
|
||||
disabled={disabled}
|
||||
className={classNames('dropdown-button', { active: isOpen })}
|
||||
>
|
||||
<span className='dropdown-button__label'>{valueOption?.text}</span>
|
||||
<Icon id='down' icon={ArrowDropDownIcon} />
|
||||
</button>
|
||||
|
||||
<Overlay
|
||||
show={isOpen}
|
||||
offset={[5, 5]}
|
||||
placement={placement}
|
||||
flip
|
||||
target={containerRef}
|
||||
popperConfig={{ strategy: 'fixed', onFirstUpdate: handleOverlayEnter }}
|
||||
>
|
||||
{({ props, placement }) => (
|
||||
<div {...props}>
|
||||
<div
|
||||
className={`dropdown-animation privacy-dropdown__dropdown ${placement}`}
|
||||
>
|
||||
<DropdownSelector
|
||||
items={options}
|
||||
value={value}
|
||||
onClose={handleClose}
|
||||
onChange={onChange}
|
||||
classNamePrefix='privacy-dropdown'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Overlay>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface Props {
|
||||
value: string;
|
||||
options: SelectItem[];
|
||||
disabled?: boolean;
|
||||
onChange: (value: string) => void;
|
||||
}
|
||||
|
||||
export const SelectWithLabel: React.FC<PropsWithChildren<Props>> = ({
|
||||
value,
|
||||
options,
|
||||
disabled,
|
||||
children,
|
||||
onChange,
|
||||
}) => {
|
||||
return (
|
||||
<label className='app-form__toggle'>
|
||||
<div className='app-form__toggle__label'>{children}</div>
|
||||
|
||||
<div className='app-form__toggle__toggle'>
|
||||
<div>
|
||||
<Dropdown
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
disabled={disabled}
|
||||
options={options}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
);
|
||||
};
|
|
@ -1,7 +1,7 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import { useRef, useCallback, useEffect } from 'react';
|
||||
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
|
||||
|
||||
import { Helmet } from 'react-helmet';
|
||||
|
||||
|
@ -90,6 +90,23 @@ export const NotificationRequest = ({ multiColumn, params: { id } }) => {
|
|||
|
||||
const columnTitle = intl.formatMessage(messages.title, { name: account?.get('display_name') || account?.get('username') });
|
||||
|
||||
let explainer = null;
|
||||
|
||||
if (account?.limited) {
|
||||
const isLocal = account.acct.indexOf('@') === -1;
|
||||
explainer = (
|
||||
<div className='dismissable-banner'>
|
||||
<div className='dismissable-banner__message'>
|
||||
{isLocal ? (
|
||||
<FormattedMessage id='notification_requests.explainer_for_limited_account' defaultMessage='Notifications from this account have been filtered because the account has been limited by a moderator.' />
|
||||
) : (
|
||||
<FormattedMessage id='notification_requests.explainer_for_limited_remote_account' defaultMessage='Notifications from this account have been filtered because the account or its server has been limited by a moderator.' />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Column bindToDocument={!multiColumn} ref={columnRef} label={columnTitle}>
|
||||
<ColumnHeader
|
||||
|
@ -109,6 +126,7 @@ export const NotificationRequest = ({ multiColumn, params: { id } }) => {
|
|||
|
||||
<SensitiveMediaContextProvider hideMediaByDefault>
|
||||
<ScrollableList
|
||||
prepend={explainer}
|
||||
scrollKey={`notification_requests/${id}`}
|
||||
trackScroll={!multiColumn}
|
||||
bindToDocument={!multiColumn}
|
||||
|
|
|
@ -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,124 @@ const ColumnSettings = () => {
|
|||
);
|
||||
};
|
||||
|
||||
const SelectRow = ({selectAllChecked, toggleSelectAll, selectedItems, selectionMode, setSelectionMode}) => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const notificationRequests = useSelector(state => state.getIn(['notificationRequests', 'items']));
|
||||
|
||||
const selectedCount = selectedItems.length;
|
||||
|
||||
const handleAcceptAll = useCallback(() => {
|
||||
const items = notificationRequests.map(request => request.get('id')).toArray();
|
||||
dispatch(openModal({
|
||||
modalType: 'CONFIRM',
|
||||
modalProps: {
|
||||
title: intl.formatMessage(messages.confirmAcceptAllTitle),
|
||||
message: intl.formatMessage(messages.confirmAcceptAllMessage, { count: items.length }),
|
||||
confirm: intl.formatMessage(messages.confirmAcceptAllButton),
|
||||
onConfirm: () =>
|
||||
dispatch(acceptNotificationRequests(items)),
|
||||
},
|
||||
}));
|
||||
}, [dispatch, intl, notificationRequests]);
|
||||
|
||||
const handleDismissAll = useCallback(() => {
|
||||
const items = notificationRequests.map(request => request.get('id')).toArray();
|
||||
dispatch(openModal({
|
||||
modalType: 'CONFIRM',
|
||||
modalProps: {
|
||||
title: intl.formatMessage(messages.confirmDismissAllTitle),
|
||||
message: intl.formatMessage(messages.confirmDismissAllMessage, { count: items.length }),
|
||||
confirm: intl.formatMessage(messages.confirmDismissAllButton),
|
||||
onConfirm: () =>
|
||||
dispatch(dismissNotificationRequests(items)),
|
||||
},
|
||||
}));
|
||||
}, [dispatch, intl, notificationRequests]);
|
||||
|
||||
const handleAcceptMultiple = 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 handleDismissMultiple = 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: handleAcceptMultiple },
|
||||
{ text: intl.formatMessage(messages.dismissMultiple, { count: selectedCount }), action: handleDismissMultiple },
|
||||
];
|
||||
|
||||
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 +196,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 +247,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 +269,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>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue