Add option to ignore filtered notifications to the web interface (#31342)
This commit is contained in:
parent
8a5b57f668
commit
1701575704
14 changed files with 402 additions and 43 deletions
|
@ -2,8 +2,8 @@ import { apiRequestGet, apiRequestPut } from 'mastodon/api';
|
||||||
import type { NotificationPolicyJSON } from 'mastodon/api_types/notification_policies';
|
import type { NotificationPolicyJSON } from 'mastodon/api_types/notification_policies';
|
||||||
|
|
||||||
export const apiGetNotificationPolicy = () =>
|
export const apiGetNotificationPolicy = () =>
|
||||||
apiRequestGet<NotificationPolicyJSON>('/v1/notifications/policy');
|
apiRequestGet<NotificationPolicyJSON>('/v2/notifications/policy');
|
||||||
|
|
||||||
export const apiUpdateNotificationsPolicy = (
|
export const apiUpdateNotificationsPolicy = (
|
||||||
policy: Partial<NotificationPolicyJSON>,
|
policy: Partial<NotificationPolicyJSON>,
|
||||||
) => apiRequestPut<NotificationPolicyJSON>('/v1/notifications/policy', policy);
|
) => apiRequestPut<NotificationPolicyJSON>('/v2/notifications/policy', policy);
|
||||||
|
|
|
@ -1,10 +1,13 @@
|
||||||
// See app/serializers/rest/notification_policy_serializer.rb
|
// See app/serializers/rest/notification_policy_serializer.rb
|
||||||
|
|
||||||
|
export type NotificationPolicyValue = 'accept' | 'filter' | 'drop';
|
||||||
|
|
||||||
export interface NotificationPolicyJSON {
|
export interface NotificationPolicyJSON {
|
||||||
filter_not_following: boolean;
|
for_not_following: NotificationPolicyValue;
|
||||||
filter_not_followers: boolean;
|
for_not_followers: NotificationPolicyValue;
|
||||||
filter_new_accounts: boolean;
|
for_new_accounts: NotificationPolicyValue;
|
||||||
filter_private_mentions: boolean;
|
for_private_mentions: NotificationPolicyValue;
|
||||||
|
for_limited_accounts: NotificationPolicyValue;
|
||||||
summary: {
|
summary: {
|
||||||
pending_requests_count: number;
|
pending_requests_count: number;
|
||||||
pending_notifications_count: number;
|
pending_notifications_count: number;
|
||||||
|
|
|
@ -13,7 +13,7 @@ const listenerOptions = supportsPassiveEvents
|
||||||
? { passive: true, capture: true }
|
? { passive: true, capture: true }
|
||||||
: true;
|
: true;
|
||||||
|
|
||||||
interface SelectItem {
|
export interface SelectItem {
|
||||||
value: string;
|
value: string;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
iconComponent?: IconProp;
|
iconComponent?: IconProp;
|
||||||
|
|
|
@ -1,16 +1,52 @@
|
||||||
import { useCallback } from 'react';
|
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 { updateNotificationsPolicy } from 'mastodon/actions/notification_policies';
|
||||||
|
import type { AppDispatch } from 'mastodon/store';
|
||||||
import { useAppSelector, useAppDispatch } from 'mastodon/store';
|
import { useAppSelector, useAppDispatch } from 'mastodon/store';
|
||||||
|
|
||||||
import { CheckboxWithLabel } from './checkbox_with_label';
|
import { SelectWithLabel } from './select_with_label';
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
const messages = defineMessages({
|
||||||
const noop = () => {};
|
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 = () => {
|
export const PolicyControls: React.FC = () => {
|
||||||
|
const intl = useIntl();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
const notificationPolicy = useAppSelector(
|
const notificationPolicy = useAppSelector(
|
||||||
|
@ -18,56 +54,74 @@ export const PolicyControls: React.FC = () => {
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleFilterNotFollowing = useCallback(
|
const handleFilterNotFollowing = useCallback(
|
||||||
(checked: boolean) => {
|
(value: string) => {
|
||||||
void dispatch(
|
changeFilter(dispatch, 'for_not_following', value);
|
||||||
updateNotificationsPolicy({ filter_not_following: checked }),
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
[dispatch],
|
[dispatch],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleFilterNotFollowers = useCallback(
|
const handleFilterNotFollowers = useCallback(
|
||||||
(checked: boolean) => {
|
(value: string) => {
|
||||||
void dispatch(
|
changeFilter(dispatch, 'for_not_followers', value);
|
||||||
updateNotificationsPolicy({ filter_not_followers: checked }),
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
[dispatch],
|
[dispatch],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleFilterNewAccounts = useCallback(
|
const handleFilterNewAccounts = useCallback(
|
||||||
(checked: boolean) => {
|
(value: string) => {
|
||||||
void dispatch(
|
changeFilter(dispatch, 'for_new_accounts', value);
|
||||||
updateNotificationsPolicy({ filter_new_accounts: checked }),
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
[dispatch],
|
[dispatch],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleFilterPrivateMentions = useCallback(
|
const handleFilterPrivateMentions = useCallback(
|
||||||
(checked: boolean) => {
|
(value: string) => {
|
||||||
void dispatch(
|
changeFilter(dispatch, 'for_private_mentions', value);
|
||||||
updateNotificationsPolicy({ filter_private_mentions: checked }),
|
},
|
||||||
);
|
[dispatch],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleFilterLimitedAccounts = useCallback(
|
||||||
|
(value: string) => {
|
||||||
|
changeFilter(dispatch, 'for_limited_accounts', value);
|
||||||
},
|
},
|
||||||
[dispatch],
|
[dispatch],
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!notificationPolicy) return null;
|
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 (
|
return (
|
||||||
<section>
|
<section>
|
||||||
<h3>
|
<h3>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='notifications.policy.title'
|
id='notifications.policy.title'
|
||||||
defaultMessage='Filter out notifications from…'
|
defaultMessage='Manage notifications from…'
|
||||||
/>
|
/>
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<div className='column-settings__row'>
|
<div className='column-settings__row'>
|
||||||
<CheckboxWithLabel
|
<SelectWithLabel
|
||||||
checked={notificationPolicy.filter_not_following}
|
value={notificationPolicy.for_not_following}
|
||||||
onChange={handleFilterNotFollowing}
|
onChange={handleFilterNotFollowing}
|
||||||
|
options={options}
|
||||||
>
|
>
|
||||||
<strong>
|
<strong>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
|
@ -81,11 +135,12 @@ export const PolicyControls: React.FC = () => {
|
||||||
defaultMessage='Until you manually approve them'
|
defaultMessage='Until you manually approve them'
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
</CheckboxWithLabel>
|
</SelectWithLabel>
|
||||||
|
|
||||||
<CheckboxWithLabel
|
<SelectWithLabel
|
||||||
checked={notificationPolicy.filter_not_followers}
|
value={notificationPolicy.for_not_followers}
|
||||||
onChange={handleFilterNotFollowers}
|
onChange={handleFilterNotFollowers}
|
||||||
|
options={options}
|
||||||
>
|
>
|
||||||
<strong>
|
<strong>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
|
@ -100,11 +155,12 @@ export const PolicyControls: React.FC = () => {
|
||||||
values={{ days: 3 }}
|
values={{ days: 3 }}
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
</CheckboxWithLabel>
|
</SelectWithLabel>
|
||||||
|
|
||||||
<CheckboxWithLabel
|
<SelectWithLabel
|
||||||
checked={notificationPolicy.filter_new_accounts}
|
value={notificationPolicy.for_new_accounts}
|
||||||
onChange={handleFilterNewAccounts}
|
onChange={handleFilterNewAccounts}
|
||||||
|
options={options}
|
||||||
>
|
>
|
||||||
<strong>
|
<strong>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
|
@ -119,11 +175,12 @@ export const PolicyControls: React.FC = () => {
|
||||||
values={{ days: 30 }}
|
values={{ days: 30 }}
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
</CheckboxWithLabel>
|
</SelectWithLabel>
|
||||||
|
|
||||||
<CheckboxWithLabel
|
<SelectWithLabel
|
||||||
checked={notificationPolicy.filter_private_mentions}
|
value={notificationPolicy.for_private_mentions}
|
||||||
onChange={handleFilterPrivateMentions}
|
onChange={handleFilterPrivateMentions}
|
||||||
|
options={options}
|
||||||
>
|
>
|
||||||
<strong>
|
<strong>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
|
@ -137,9 +194,13 @@ export const PolicyControls: React.FC = () => {
|
||||||
defaultMessage="Filtered unless it's in reply to your own mention or if you follow the sender"
|
defaultMessage="Filtered unless it's in reply to your own mention or if you follow the sender"
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
</CheckboxWithLabel>
|
</SelectWithLabel>
|
||||||
|
|
||||||
<CheckboxWithLabel checked disabled onChange={noop}>
|
<SelectWithLabel
|
||||||
|
value={notificationPolicy.for_limited_accounts}
|
||||||
|
onChange={handleFilterLimitedAccounts}
|
||||||
|
options={options}
|
||||||
|
>
|
||||||
<strong>
|
<strong>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='notifications.policy.filter_limited_accounts_title'
|
id='notifications.policy.filter_limited_accounts_title'
|
||||||
|
@ -152,7 +213,7 @@ export const PolicyControls: React.FC = () => {
|
||||||
defaultMessage='Limited by server moderators'
|
defaultMessage='Limited by server moderators'
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
</CheckboxWithLabel>
|
</SelectWithLabel>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</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>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,108 @@
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
import { useDispatch } from 'react-redux';
|
||||||
|
|
||||||
|
import InventoryIcon from '@/material-icons/400-24px/inventory_2.svg?react';
|
||||||
|
import PersonAlertIcon from '@/material-icons/400-24px/person_alert.svg?react';
|
||||||
|
import ShieldQuestionIcon from '@/material-icons/400-24px/shield_question.svg?react';
|
||||||
|
import { closeModal } from 'mastodon/actions/modal';
|
||||||
|
import { updateNotificationsPolicy } from 'mastodon/actions/notification_policies';
|
||||||
|
import { Button } from 'mastodon/components/button';
|
||||||
|
import { Icon } from 'mastodon/components/icon';
|
||||||
|
|
||||||
|
export const IgnoreNotificationsModal = ({ filterType }) => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const handleClick = useCallback(() => {
|
||||||
|
dispatch(closeModal({ modalType: undefined, ignoreFocus: false }));
|
||||||
|
void dispatch(updateNotificationsPolicy({ [filterType]: 'drop' }));
|
||||||
|
}, [dispatch, filterType]);
|
||||||
|
|
||||||
|
const handleSecondaryClick = useCallback(() => {
|
||||||
|
dispatch(closeModal({ modalType: undefined, ignoreFocus: false }));
|
||||||
|
void dispatch(updateNotificationsPolicy({ [filterType]: 'filter' }));
|
||||||
|
}, [dispatch, filterType]);
|
||||||
|
|
||||||
|
const handleCancel = useCallback(() => {
|
||||||
|
dispatch(closeModal({ modalType: undefined, ignoreFocus: false }));
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
let title = null;
|
||||||
|
|
||||||
|
switch(filterType) {
|
||||||
|
case 'for_not_following':
|
||||||
|
title = <FormattedMessage id='ignore_notifications_modal.not_following_title' defaultMessage="Ignore notifications from people you don't follow?" />;
|
||||||
|
break;
|
||||||
|
case 'for_not_followers':
|
||||||
|
title = <FormattedMessage id='ignore_notifications_modal.not_followers_title' defaultMessage='Ignore notifications from people not following you?' />;
|
||||||
|
break;
|
||||||
|
case 'for_new_accounts':
|
||||||
|
title = <FormattedMessage id='ignore_notifications_modal.new_accounts_title' defaultMessage='Ignore notifications from new accounts?' />;
|
||||||
|
break;
|
||||||
|
case 'for_private_mentions':
|
||||||
|
title = <FormattedMessage id='ignore_notifications_modal.private_mentions_title' defaultMessage='Ignore notifications from unsolicited Private Mentions?' />;
|
||||||
|
break;
|
||||||
|
case 'for_limited_accounts':
|
||||||
|
title = <FormattedMessage id='ignore_notifications_modal.limited_accounts_title' defaultMessage='Ignore notifications from moderated accounts?' />;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='modal-root__modal safety-action-modal'>
|
||||||
|
<div className='safety-action-modal__top'>
|
||||||
|
<div className='safety-action-modal__header'>
|
||||||
|
<h1>{title}</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='safety-action-modal__bullet-points'>
|
||||||
|
<div>
|
||||||
|
<div className='safety-action-modal__bullet-points__icon'><Icon icon={InventoryIcon} /></div>
|
||||||
|
<div><FormattedMessage id='ignore_notifications_modal.filter_to_review_separately' defaultMessage='You can review filtered notifications speparately' /></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div className='safety-action-modal__bullet-points__icon'><Icon icon={PersonAlertIcon} /></div>
|
||||||
|
<div><FormattedMessage id='ignore_notifications_modal.filter_to_act_users' defaultMessage="You'll still be able to accept, reject, or report users" /></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div className='safety-action-modal__bullet-points__icon'><Icon icon={ShieldQuestionIcon} /></div>
|
||||||
|
<div><FormattedMessage id='ignore_notifications_modal.filter_to_avoid_confusion' defaultMessage='Filtering helps avoid potential confusion' /></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<FormattedMessage id='ignore_notifications_modal.disclaimer' defaultMessage="Mastodon cannot inform users that you've ignored their notifications. Ignoring notifications will not stop the messages themselves from being sent." />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div className='safety-action-modal__bottom'>
|
||||||
|
<div className='safety-action-modal__actions'>
|
||||||
|
<Button onClick={handleSecondaryClick} secondary>
|
||||||
|
<FormattedMessage id='ignore_notifications_modal.filter_instead' defaultMessage='Filter instead' />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<div className='spacer' />
|
||||||
|
|
||||||
|
<button onClick={handleCancel} className='link-button'>
|
||||||
|
<FormattedMessage id='confirmation_modal.cancel' defaultMessage='Cancel' />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button onClick={handleClick} className='link-button'>
|
||||||
|
<FormattedMessage id='ignore_notifications_modal.ignore' defaultMessage='Ignore notifications' />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
IgnoreNotificationsModal.propTypes = {
|
||||||
|
filterType: PropTypes.string.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default IgnoreNotificationsModal;
|
|
@ -17,6 +17,7 @@ import {
|
||||||
InteractionModal,
|
InteractionModal,
|
||||||
SubscribedLanguagesModal,
|
SubscribedLanguagesModal,
|
||||||
ClosedRegistrationsModal,
|
ClosedRegistrationsModal,
|
||||||
|
IgnoreNotificationsModal,
|
||||||
} from 'mastodon/features/ui/util/async-components';
|
} from 'mastodon/features/ui/util/async-components';
|
||||||
import { getScrollbarWidth } from 'mastodon/utils/scrollbar';
|
import { getScrollbarWidth } from 'mastodon/utils/scrollbar';
|
||||||
|
|
||||||
|
@ -70,6 +71,7 @@ export const MODAL_COMPONENTS = {
|
||||||
'SUBSCRIBED_LANGUAGES': SubscribedLanguagesModal,
|
'SUBSCRIBED_LANGUAGES': SubscribedLanguagesModal,
|
||||||
'INTERACTION': InteractionModal,
|
'INTERACTION': InteractionModal,
|
||||||
'CLOSED_REGISTRATIONS': ClosedRegistrationsModal,
|
'CLOSED_REGISTRATIONS': ClosedRegistrationsModal,
|
||||||
|
'IGNORE_NOTIFICATIONS': IgnoreNotificationsModal,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default class ModalRoot extends PureComponent {
|
export default class ModalRoot extends PureComponent {
|
||||||
|
|
|
@ -134,6 +134,10 @@ export function ReportModal () {
|
||||||
return import(/* webpackChunkName: "modals/report_modal" */'../components/report_modal');
|
return import(/* webpackChunkName: "modals/report_modal" */'../components/report_modal');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function IgnoreNotificationsModal () {
|
||||||
|
return import(/* webpackChunkName: "modals/domain_block_modal" */'../components/ignore_notifications_modal');
|
||||||
|
}
|
||||||
|
|
||||||
export function MediaGallery () {
|
export function MediaGallery () {
|
||||||
return import(/* webpackChunkName: "status/media_gallery" */'../../../components/media_gallery');
|
return import(/* webpackChunkName: "status/media_gallery" */'../../../components/media_gallery');
|
||||||
}
|
}
|
||||||
|
|
|
@ -356,6 +356,17 @@
|
||||||
"home.pending_critical_update.link": "See updates",
|
"home.pending_critical_update.link": "See updates",
|
||||||
"home.pending_critical_update.title": "Critical security update available!",
|
"home.pending_critical_update.title": "Critical security update available!",
|
||||||
"home.show_announcements": "Show announcements",
|
"home.show_announcements": "Show announcements",
|
||||||
|
"ignore_notifications_modal.disclaimer": "Mastodon cannot inform users that you've ignored their notifications. Ignoring notifications will not stop the messages themselves from being sent.",
|
||||||
|
"ignore_notifications_modal.filter_instead": "Filter instead",
|
||||||
|
"ignore_notifications_modal.filter_to_act_users": "Filtering helps avoid potential confusion",
|
||||||
|
"ignore_notifications_modal.filter_to_avoid_confusion": "Filtering helps avoid potential confusion",
|
||||||
|
"ignore_notifications_modal.filter_to_review_separately": "You can review filtered notifications speparately",
|
||||||
|
"ignore_notifications_modal.ignore": "Ignore notifications",
|
||||||
|
"ignore_notifications_modal.limited_accounts_title": "Ignore notifications from moderated accounts?",
|
||||||
|
"ignore_notifications_modal.new_accounts_title": "Ignore notifications from new accounts?",
|
||||||
|
"ignore_notifications_modal.not_followers_title": "Ignore notifications from people not following you?",
|
||||||
|
"ignore_notifications_modal.not_following_title": "Ignore notifications from people you don't follow?",
|
||||||
|
"ignore_notifications_modal.private_mentions_title": "Ignore notifications from unsolicited Private Mentions?",
|
||||||
"interaction_modal.description.favourite": "With an account on Mastodon, you can favorite this post to let the author know you appreciate it and save it for later.",
|
"interaction_modal.description.favourite": "With an account on Mastodon, you can favorite this post to let the author know you appreciate it and save it for later.",
|
||||||
"interaction_modal.description.follow": "With an account on Mastodon, you can follow {name} to receive their posts in your home feed.",
|
"interaction_modal.description.follow": "With an account on Mastodon, you can follow {name} to receive their posts in your home feed.",
|
||||||
"interaction_modal.description.reblog": "With an account on Mastodon, you can boost this post to share it with your own followers.",
|
"interaction_modal.description.reblog": "With an account on Mastodon, you can boost this post to share it with your own followers.",
|
||||||
|
@ -550,6 +561,12 @@
|
||||||
"notifications.permission_denied": "Desktop notifications are unavailable due to previously denied browser permissions request",
|
"notifications.permission_denied": "Desktop notifications are unavailable due to previously denied browser permissions request",
|
||||||
"notifications.permission_denied_alert": "Desktop notifications can't be enabled, as browser permission has been denied before",
|
"notifications.permission_denied_alert": "Desktop notifications can't be enabled, as browser permission has been denied before",
|
||||||
"notifications.permission_required": "Desktop notifications are unavailable because the required permission has not been granted.",
|
"notifications.permission_required": "Desktop notifications are unavailable because the required permission has not been granted.",
|
||||||
|
"notifications.policy.accept": "Accept",
|
||||||
|
"notifications.policy.accept_hint": "Show in notifications",
|
||||||
|
"notifications.policy.drop": "Ignore",
|
||||||
|
"notifications.policy.drop_hint": "Send to the void, never to be seen again",
|
||||||
|
"notifications.policy.filter": "Filter",
|
||||||
|
"notifications.policy.filter_hint": "Send to filtered notifications inbox",
|
||||||
"notifications.policy.filter_limited_accounts_hint": "Limited by server moderators",
|
"notifications.policy.filter_limited_accounts_hint": "Limited by server moderators",
|
||||||
"notifications.policy.filter_limited_accounts_title": "Moderated accounts",
|
"notifications.policy.filter_limited_accounts_title": "Moderated accounts",
|
||||||
"notifications.policy.filter_new_accounts.hint": "Created within the past {days, plural, one {one day} other {# days}}",
|
"notifications.policy.filter_new_accounts.hint": "Created within the past {days, plural, one {one day} other {# days}}",
|
||||||
|
@ -560,7 +577,7 @@
|
||||||
"notifications.policy.filter_not_following_title": "People you don't follow",
|
"notifications.policy.filter_not_following_title": "People you don't follow",
|
||||||
"notifications.policy.filter_private_mentions_hint": "Filtered unless it's in reply to your own mention or if you follow the sender",
|
"notifications.policy.filter_private_mentions_hint": "Filtered unless it's in reply to your own mention or if you follow the sender",
|
||||||
"notifications.policy.filter_private_mentions_title": "Unsolicited private mentions",
|
"notifications.policy.filter_private_mentions_title": "Unsolicited private mentions",
|
||||||
"notifications.policy.title": "Filter out notifications from…",
|
"notifications.policy.title": "Manage notifications from…",
|
||||||
"notifications_permission_banner.enable": "Enable desktop notifications",
|
"notifications_permission_banner.enable": "Enable desktop notifications",
|
||||||
"notifications_permission_banner.how_to_control": "To receive notifications when Mastodon isn't open, enable desktop notifications. You can control precisely which types of interactions generate desktop notifications through the {icon} button above once they're enabled.",
|
"notifications_permission_banner.how_to_control": "To receive notifications when Mastodon isn't open, enable desktop notifications. You can control precisely which types of interactions generate desktop notifications through the {icon} button above once they're enabled.",
|
||||||
"notifications_permission_banner.title": "Never miss a thing",
|
"notifications_permission_banner.title": "Never miss a thing",
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M800-520q-17 0-28.5-11.5T760-560q0-17 11.5-28.5T800-600q17 0 28.5 11.5T840-560q0 17-11.5 28.5T800-520Zm-40-120v-200h80v200h-80ZM360-480q-66 0-113-47t-47-113q0-66 47-113t113-47q66 0 113 47t47 113q0 66-47 113t-113 47ZM40-160v-112q0-34 17.5-62.5T104-378q62-31 126-46.5T360-440q66 0 130 15.5T616-378q29 15 46.5 43.5T680-272v112H40Z"/></svg>
|
After Width: | Height: | Size: 433 B |
1
app/javascript/material-icons/400-24px/person_alert.svg
Normal file
1
app/javascript/material-icons/400-24px/person_alert.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M800-520q-17 0-28.5-11.5T760-560q0-17 11.5-28.5T800-600q17 0 28.5 11.5T840-560q0 17-11.5 28.5T800-520Zm-40-120v-200h80v200h-80ZM360-480q-66 0-113-47t-47-113q0-66 47-113t113-47q66 0 113 47t47 113q0 66-47 113t-113 47ZM40-160v-112q0-34 17.5-62.5T104-378q62-31 126-46.5T360-440q66 0 130 15.5T616-378q29 15 46.5 43.5T680-272v112H40Zm80-80h480v-32q0-11-5.5-20T580-306q-54-27-109-40.5T360-360q-56 0-111 13.5T140-306q-9 5-14.5 14t-5.5 20v32Zm240-320q33 0 56.5-23.5T440-640q0-33-23.5-56.5T360-720q-33 0-56.5 23.5T280-640q0 33 23.5 56.5T360-560Zm0-80Zm0 400Z"/></svg>
|
After Width: | Height: | Size: 654 B |
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M480-80q-139-35-229.5-159.5T160-516v-244l320-120 320 120v244q0 152-90.5 276.5T480-80Zm0-200q17 0 29.5-12.5T522-322q0-17-12.5-29.5T480-364q-17 0-29.5 12.5T438-322q0 17 12.5 29.5T480-280Zm-29-128h60v-22q0-11 5-21 6-14 16-23.5t21-19.5q17-17 29.5-38t12.5-46q0-45-34.5-73.5T480-680q-40 0-71.5 23T366-596l54 22q6-20 22.5-34t37.5-14q22 0 38.5 13t16.5 33q0 17-10.5 31.5T501-518q-12 11-24 22.5T458-469q-7 14-7 29.5v31.5Z"/></svg>
|
After Width: | Height: | Size: 517 B |
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M480-80q-139-35-229.5-159.5T160-516v-244l320-120 320 120v244q0 152-90.5 276.5T480-80Zm0-84q104-33 172-132t68-220v-189l-240-90-240 90v189q0 121 68 220t172 132Zm0-316Zm0 200q17 0 29.5-12.5T522-322q0-17-12.5-29.5T480-364q-17 0-29.5 12.5T438-322q0 17 12.5 29.5T480-280Zm-29-128h60v-22q0-11 5-21 6-14 16-23.5t21-19.5q17-17 29.5-38t12.5-46q0-45-34.5-73.5T480-680q-40 0-71.5 23T366-596l54 22q6-20 22.5-34t37.5-14q22 0 38.5 13t16.5 33q0 17-10.5 31.5T501-518q-12 11-24 22.5T458-469q-7 14-7 29.5v31.5Z"/></svg>
|
After Width: | Height: | Size: 597 B |
|
@ -877,6 +877,13 @@ body > [data-popper-placement] {
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
|
||||||
|
&[disabled] {
|
||||||
|
cursor: default;
|
||||||
|
color: $highlight-text-color;
|
||||||
|
border-color: $highlight-text-color;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
width: 15px;
|
width: 15px;
|
||||||
height: 15px;
|
height: 15px;
|
||||||
|
|
Loading…
Reference in a new issue