Change mute, block and domain block confirmations in web UI (#29576)

This commit is contained in:
Eugen Rochko 2024-03-15 18:36:41 +01:00 committed by GitHub
parent be7a68b095
commit ec19d0a14b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
27 changed files with 620 additions and 349 deletions

View file

@ -12,8 +12,6 @@ export const BLOCKS_EXPAND_REQUEST = 'BLOCKS_EXPAND_REQUEST';
export const BLOCKS_EXPAND_SUCCESS = 'BLOCKS_EXPAND_SUCCESS';
export const BLOCKS_EXPAND_FAIL = 'BLOCKS_EXPAND_FAIL';
export const BLOCKS_INIT_MODAL = 'BLOCKS_INIT_MODAL';
export function fetchBlocks() {
return (dispatch, getState) => {
dispatch(fetchBlocksRequest());
@ -90,11 +88,12 @@ export function expandBlocksFail(error) {
export function initBlockModal(account) {
return dispatch => {
dispatch({
type: BLOCKS_INIT_MODAL,
account,
});
dispatch(openModal({ modalType: 'BLOCK' }));
dispatch(openModal({
modalType: 'BLOCK',
modalProps: {
accountId: account.get('id'),
acct: account.get('acct'),
},
}));
};
}

View file

@ -1,6 +1,8 @@
import api, { getLinks } from '../api';
import { blockDomainSuccess, unblockDomainSuccess } from "./domain_blocks_typed";
import { openModal } from './modal';
export * from "./domain_blocks_typed";
@ -150,3 +152,12 @@ export function expandDomainBlocksFail(error) {
error,
};
}
export const initDomainBlockModal = account => dispatch => dispatch(openModal({
modalType: 'DOMAIN_BLOCK',
modalProps: {
domain: account.get('acct').split('@')[1],
acct: account.get('acct'),
accountId: account.get('id'),
},
}));

View file

@ -12,10 +12,6 @@ export const MUTES_EXPAND_REQUEST = 'MUTES_EXPAND_REQUEST';
export const MUTES_EXPAND_SUCCESS = 'MUTES_EXPAND_SUCCESS';
export const MUTES_EXPAND_FAIL = 'MUTES_EXPAND_FAIL';
export const MUTES_INIT_MODAL = 'MUTES_INIT_MODAL';
export const MUTES_TOGGLE_HIDE_NOTIFICATIONS = 'MUTES_TOGGLE_HIDE_NOTIFICATIONS';
export const MUTES_CHANGE_DURATION = 'MUTES_CHANGE_DURATION';
export function fetchMutes() {
return (dispatch, getState) => {
dispatch(fetchMutesRequest());
@ -92,26 +88,12 @@ export function expandMutesFail(error) {
export function initMuteModal(account) {
return dispatch => {
dispatch({
type: MUTES_INIT_MODAL,
account,
});
dispatch(openModal({ modalType: 'MUTE' }));
};
}
export function toggleHideNotifications() {
return dispatch => {
dispatch({ type: MUTES_TOGGLE_HIDE_NOTIFICATIONS });
};
}
export function changeMuteDuration(duration) {
return dispatch => {
dispatch({
type: MUTES_CHANGE_DURATION,
duration,
});
dispatch(openModal({
modalType: 'MUTE',
modalProps: {
accountId: account.get('id'),
acct: account.get('acct'),
},
}));
};
}

View file

@ -0,0 +1,39 @@
import classNames from 'classnames';
import DoneIcon from '@/material-icons/400-24px/done.svg?react';
import { Icon } from './icon';
interface Props {
value: string;
checked: boolean;
name: string;
onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
label: React.ReactNode;
}
export const CheckBox: React.FC<Props> = ({
name,
value,
checked,
onChange,
label,
}) => {
return (
<label className='check-box'>
<input
name={name}
type='checkbox'
value={value}
checked={checked}
onChange={onChange}
/>
<span className={classNames('check-box__input', { checked })}>
{checked && <Icon id='check' icon={DoneIcon} />}
</span>
<span>{label}</span>
</label>
);
};

View file

@ -209,7 +209,7 @@ class StatusActionBar extends ImmutablePureComponent {
const { status, onBlockDomain } = this.props;
const account = status.get('account');
onBlockDomain(account.get('acct').split('@')[1]);
onBlockDomain(account);
};
handleUnblockDomain = () => {

View file

@ -1,4 +1,4 @@
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { defineMessages, injectIntl } from 'react-intl';
import { connect } from 'react-redux';
@ -15,7 +15,7 @@ import {
directCompose,
} from '../actions/compose';
import {
blockDomain,
initDomainBlockModal,
unblockDomain,
} from '../actions/domain_blocks';
import {
@ -253,15 +253,8 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({
dispatch(toggleStatusCollapse(status.get('id'), isCollapsed));
},
onBlockDomain (domain) {
dispatch(openModal({
modalType: 'CONFIRM',
modalProps: {
message: <FormattedMessage id='confirmations.domain_block.message' defaultMessage='Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable. You will not see content from that domain in any public timelines or your notifications. Your followers from that domain will be removed.' values={{ domain: <strong>{domain}</strong> }} />,
confirm: intl.formatMessage(messages.blockDomainConfirm),
onConfirm: () => dispatch(blockDomain(domain)),
},
}));
onBlockDomain (account) {
dispatch(initDomainBlockModal(account));
},
onUnblockDomain (domain) {

View file

@ -72,11 +72,7 @@ class Header extends ImmutablePureComponent {
};
handleBlockDomain = () => {
const domain = this.props.account.get('acct').split('@')[1];
if (!domain) return;
this.props.onBlockDomain(domain);
this.props.onBlockDomain(this.props.account);
};
handleUnblockDomain = () => {

View file

@ -17,7 +17,7 @@ import {
mentionCompose,
directCompose,
} from '../../../actions/compose';
import { blockDomain, unblockDomain } from '../../../actions/domain_blocks';
import { initDomainBlockModal, unblockDomain } from '../../../actions/domain_blocks';
import { openModal } from '../../../actions/modal';
import { initMuteModal } from '../../../actions/mutes';
import { initReport } from '../../../actions/reports';
@ -140,15 +140,8 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
}
},
onBlockDomain (domain) {
dispatch(openModal({
modalType: 'CONFIRM',
modalProps: {
message: <FormattedMessage id='confirmations.domain_block.message' defaultMessage='Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable. You will not see content from that domain in any public timelines or your notifications. Your followers from that domain will be removed.' values={{ domain: <strong>{domain}</strong> }} />,
confirm: intl.formatMessage(messages.blockDomainConfirm),
onConfirm: () => dispatch(blockDomain(domain)),
},
}));
onBlockDomain (account) {
dispatch(initDomainBlockModal(account));
},
onUnblockDomain (domain) {

View file

@ -159,7 +159,7 @@ class ActionBar extends PureComponent {
const { status, onBlockDomain } = this.props;
const account = status.get('account');
onBlockDomain(account.get('acct').split('@')[1]);
onBlockDomain(account);
};
handleUnblockDomain = () => {

View file

@ -1,6 +1,6 @@
import PropTypes from 'prop-types';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { defineMessages, injectIntl } from 'react-intl';
import classNames from 'classnames';
import { Helmet } from 'react-helmet';
@ -34,7 +34,7 @@ import {
directCompose,
} from '../../actions/compose';
import {
blockDomain,
initDomainBlockModal,
unblockDomain,
} from '../../actions/domain_blocks';
import {
@ -463,15 +463,8 @@ class Status extends ImmutablePureComponent {
this.props.dispatch(unblockAccount(account.get('id')));
};
handleBlockDomainClick = domain => {
this.props.dispatch(openModal({
modalType: 'CONFIRM',
modalProps: {
message: <FormattedMessage id='confirmations.domain_block.message' defaultMessage='Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable. You will not see content from that domain in any public timelines or your notifications. Your followers from that domain will be removed.' values={{ domain: <strong>{domain}</strong> }} />,
confirm: this.props.intl.formatMessage(messages.blockDomainConfirm),
onConfirm: () => this.props.dispatch(blockDomain(domain)),
},
}));
handleBlockDomainClick = account => {
this.props.dispatch(initDomainBlockModal(account));
};
handleUnblockDomainClick = domain => {

View file

@ -1,100 +1,87 @@
import PropTypes from 'prop-types';
import { PureComponent } from 'react';
import { useCallback } from 'react';
import { injectIntl, FormattedMessage } from 'react-intl';
import { FormattedMessage } from 'react-intl';
import { connect } from 'react-redux';
import { useDispatch } from 'react-redux';
import { blockAccount } from '../../../actions/accounts';
import { closeModal } from '../../../actions/modal';
import { initReport } from '../../../actions/reports';
import { Button } from '../../../components/button';
import { makeGetAccount } from '../../../selectors';
import AlternateEmailIcon from '@/material-icons/400-24px/alternate_email.svg?react';
import BlockIcon from '@/material-icons/400-24px/block.svg?react';
import CampaignIcon from '@/material-icons/400-24px/campaign.svg?react';
import ReplyIcon from '@/material-icons/400-24px/reply.svg?react';
import VisibilityOffIcon from '@/material-icons/400-24px/visibility_off.svg?react';
import { blockAccount } from 'mastodon/actions/accounts';
import { closeModal } from 'mastodon/actions/modal';
import { Button } from 'mastodon/components/button';
import { Icon } from 'mastodon/components/icon';
const makeMapStateToProps = () => {
const getAccount = makeGetAccount();
export const BlockModal = ({ accountId, acct }) => {
const dispatch = useDispatch();
const mapStateToProps = state => ({
account: getAccount(state, state.getIn(['blocks', 'new', 'account_id'])),
});
const handleClick = useCallback(() => {
dispatch(closeModal({ modalType: undefined, ignoreFocus: false }));
dispatch(blockAccount(accountId));
}, [dispatch, accountId]);
return mapStateToProps;
};
const handleCancel = useCallback(() => {
dispatch(closeModal({ modalType: undefined, ignoreFocus: false }));
}, [dispatch]);
const mapDispatchToProps = dispatch => {
return {
onConfirm(account) {
dispatch(blockAccount(account.get('id')));
},
return (
<div className='modal-root__modal safety-action-modal'>
<div className='safety-action-modal__top'>
<div className='safety-action-modal__header'>
<div className='safety-action-modal__header__icon'>
<Icon icon={BlockIcon} />
</div>
onBlockAndReport(account) {
dispatch(blockAccount(account.get('id')));
dispatch(initReport(account));
},
onClose() {
dispatch(closeModal({
modalType: undefined,
ignoreFocus: false,
}));
},
};
};
class BlockModal extends PureComponent {
static propTypes = {
account: PropTypes.object.isRequired,
onClose: PropTypes.func.isRequired,
onBlockAndReport: PropTypes.func.isRequired,
onConfirm: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
};
handleClick = () => {
this.props.onClose();
this.props.onConfirm(this.props.account);
};
handleSecondary = () => {
this.props.onClose();
this.props.onBlockAndReport(this.props.account);
};
handleCancel = () => {
this.props.onClose();
};
render () {
const { account } = this.props;
return (
<div className='modal-root__modal block-modal'>
<div className='block-modal__container'>
<p>
<FormattedMessage
id='confirmations.block.message'
defaultMessage='Are you sure you want to block {name}?'
values={{ name: <strong>@{account.get('acct')}</strong> }}
/>
</p>
<div>
<h1><FormattedMessage id='block_modal.title' defaultMessage='Block user?' /></h1>
<div>@{acct}</div>
</div>
</div>
<div className='block-modal__action-bar'>
<Button onClick={this.handleCancel} className='block-modal__cancel-button'>
<div className='safety-action-modal__bullet-points'>
<div>
<div className='safety-action-modal__bullet-points__icon'><Icon icon={CampaignIcon} /></div>
<div><FormattedMessage id='block_modal.they_will_know' defaultMessage="They can see that they're blocked." /></div>
</div>
<div>
<div className='safety-action-modal__bullet-points__icon'><Icon icon={VisibilityOffIcon} /></div>
<div><FormattedMessage id='block_modal.they_cant_see_posts' defaultMessage="They can't see your posts and you won't see theirs." /></div>
</div>
<div>
<div className='safety-action-modal__bullet-points__icon'><Icon icon={AlternateEmailIcon} /></div>
<div><FormattedMessage id='block_modal.you_wont_see_mentions' defaultMessage="You won't see posts that mentions them." /></div>
</div>
<div>
<div className='safety-action-modal__bullet-points__icon'><Icon icon={ReplyIcon} /></div>
<div><FormattedMessage id='block_modal.they_cant_mention' defaultMessage="They can't mention or follow you." /></div>
</div>
</div>
</div>
<div className='safety-action-modal__bottom'>
<div className='safety-action-modal__actions'>
<button onClick={handleCancel} className='link-button'>
<FormattedMessage id='confirmation_modal.cancel' defaultMessage='Cancel' />
</Button>
<Button onClick={this.handleSecondary} className='confirmation-modal__secondary-button'>
<FormattedMessage id='confirmations.block.block_and_report' defaultMessage='Block & Report' />
</Button>
<Button onClick={this.handleClick} autoFocus>
</button>
<Button onClick={handleClick}>
<FormattedMessage id='confirmations.block.confirm' defaultMessage='Block' />
</Button>
</div>
</div>
);
}
</div>
);
};
}
BlockModal.propTypes = {
accountId: PropTypes.string.isRequired,
acct: PropTypes.string.isRequired,
};
export default connect(makeMapStateToProps, mapDispatchToProps)(injectIntl(BlockModal));
export default BlockModal;

View file

@ -0,0 +1,106 @@
import PropTypes from 'prop-types';
import { useCallback } from 'react';
import { FormattedMessage } from 'react-intl';
import { useDispatch } from 'react-redux';
import CampaignIcon from '@/material-icons/400-24px/campaign.svg?react';
import DomainDisabledIcon from '@/material-icons/400-24px/domain_disabled.svg?react';
import HistoryIcon from '@/material-icons/400-24px/history.svg?react';
import PersonRemoveIcon from '@/material-icons/400-24px/person_remove.svg?react';
import ReplyIcon from '@/material-icons/400-24px/reply.svg?react';
import VisibilityOffIcon from '@/material-icons/400-24px/visibility_off.svg?react';
import { blockAccount } from 'mastodon/actions/accounts';
import { blockDomain } from 'mastodon/actions/domain_blocks';
import { closeModal } from 'mastodon/actions/modal';
import { Button } from 'mastodon/components/button';
import { Icon } from 'mastodon/components/icon';
export const DomainBlockModal = ({ domain, accountId, acct }) => {
const dispatch = useDispatch();
const handleClick = useCallback(() => {
dispatch(closeModal({ modalType: undefined, ignoreFocus: false }));
dispatch(blockDomain(domain));
}, [dispatch, domain]);
const handleSecondaryClick = useCallback(() => {
dispatch(closeModal({ modalType: undefined, ignoreFocus: false }));
dispatch(blockAccount(accountId));
}, [dispatch, accountId]);
const handleCancel = useCallback(() => {
dispatch(closeModal({ modalType: undefined, ignoreFocus: false }));
}, [dispatch]);
return (
<div className='modal-root__modal safety-action-modal'>
<div className='safety-action-modal__top'>
<div className='safety-action-modal__header'>
<div className='safety-action-modal__header__icon'>
<Icon icon={DomainDisabledIcon} />
</div>
<div>
<h1><FormattedMessage id='domain_block_modal.title' defaultMessage='Block domain?' /></h1>
<div>{domain}</div>
</div>
</div>
<div className='safety-action-modal__bullet-points'>
<div>
<div className='safety-action-modal__bullet-points__icon'><Icon icon={CampaignIcon} /></div>
<div><FormattedMessage id='domain_block_modal.they_wont_know' defaultMessage="They won't know they've been blocked." /></div>
</div>
<div>
<div className='safety-action-modal__bullet-points__icon'><Icon icon={VisibilityOffIcon} /></div>
<div><FormattedMessage id='domain_block_modal.you_wont_see_posts' defaultMessage="You won't see posts or notifications from users on this server." /></div>
</div>
<div>
<div className='safety-action-modal__bullet-points__icon'><Icon icon={PersonRemoveIcon} /></div>
<div><FormattedMessage id='domain_block_modal.you_will_lose_followers' defaultMessage='All your followers from this server will be removed.' /></div>
</div>
<div>
<div className='safety-action-modal__bullet-points__icon'><Icon icon={ReplyIcon} /></div>
<div><FormattedMessage id='domain_block_modal.they_cant_follow' defaultMessage='Nobody from this server can follow you.' /></div>
</div>
<div>
<div className='safety-action-modal__bullet-points__icon'><Icon icon={HistoryIcon} /></div>
<div><FormattedMessage id='domain_block_modal.they_can_interact_with_old_posts' defaultMessage='People from this server can interact with your old posts.' /></div>
</div>
</div>
</div>
<div className='safety-action-modal__bottom'>
<div className='safety-action-modal__actions'>
<Button onClick={handleSecondaryClick} secondary>
<FormattedMessage id='domain_block_modal.block_account_instead' defaultMessage='Block @{name} instead' values={{ name: acct.split('@')[0] }} />
</Button>
<div className='spacer' />
<button onClick={handleCancel} className='link-button'>
<FormattedMessage id='confirmation_modal.cancel' defaultMessage='Cancel' />
</button>
<Button onClick={handleClick}>
<FormattedMessage id='domain_block_modal.block' defaultMessage='Block server' />
</Button>
</div>
</div>
</div>
);
};
DomainBlockModal.propTypes = {
domain: PropTypes.string.isRequired,
accountId: PropTypes.string.isRequired,
acct: PropTypes.string.isRequired,
};
export default DomainBlockModal;

View file

@ -7,6 +7,7 @@ import Base from 'mastodon/components/modal_root';
import {
MuteModal,
BlockModal,
DomainBlockModal,
ReportModal,
EmbedModal,
ListEditor,
@ -41,6 +42,7 @@ export const MODAL_COMPONENTS = {
'CONFIRM': () => Promise.resolve({ default: ConfirmationModal }),
'MUTE': MuteModal,
'BLOCK': BlockModal,
'DOMAIN_BLOCK': DomainBlockModal,
'REPORT': ReportModal,
'ACTIONS': () => Promise.resolve({ default: ActionsModal }),
'EMBED': EmbedModal,

View file

@ -1,138 +1,154 @@
import PropTypes from 'prop-types';
import { PureComponent } from 'react';
import { useCallback, useState } from 'react';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
import { connect } from 'react-redux';
import classNames from 'classnames';
import Toggle from 'react-toggle';
import { useDispatch } from 'react-redux';
import { muteAccount } from '../../../actions/accounts';
import { closeModal } from '../../../actions/modal';
import { toggleHideNotifications, changeMuteDuration } from '../../../actions/mutes';
import { Button } from '../../../components/button';
import AlternateEmailIcon from '@/material-icons/400-24px/alternate_email.svg?react';
import CampaignIcon from '@/material-icons/400-24px/campaign.svg?react';
import ReplyIcon from '@/material-icons/400-24px/reply.svg?react';
import VisibilityOffIcon from '@/material-icons/400-24px/visibility_off.svg?react';
import VolumeOffIcon from '@/material-icons/400-24px/volume_off.svg?react';
import { muteAccount } from 'mastodon/actions/accounts';
import { closeModal } from 'mastodon/actions/modal';
import { Button } from 'mastodon/components/button';
import { CheckBox } from 'mastodon/components/check_box';
import { Icon } from 'mastodon/components/icon';
import { RadioButton } from 'mastodon/components/radio_button';
const messages = defineMessages({
minutes: { id: 'intervals.full.minutes', defaultMessage: '{number, plural, one {# minute} other {# minutes}}' },
hours: { id: 'intervals.full.hours', defaultMessage: '{number, plural, one {# hour} other {# hours}}' },
days: { id: 'intervals.full.days', defaultMessage: '{number, plural, one {# day} other {# days}}' },
indefinite: { id: 'mute_modal.indefinite', defaultMessage: 'Indefinite' },
indefinite: { id: 'mute_modal.indefinite', defaultMessage: 'Until I unmute them' },
hideFromNotifications: { id: 'mute_modal.hide_from_notifications', defaultMessage: 'Hide from notifications' },
});
const mapStateToProps = state => {
return {
account: state.getIn(['mutes', 'new', 'account']),
notifications: state.getIn(['mutes', 'new', 'notifications']),
muteDuration: state.getIn(['mutes', 'new', 'duration']),
};
const RadioButtonLabel = ({ name, value, currentValue, onChange, label }) => (
<RadioButton
name={name}
value={value}
checked={value === currentValue}
onChange={onChange}
label={label}
/>
);
RadioButtonLabel.propTypes = {
name: PropTypes.string,
value: PropTypes.oneOf([PropTypes.string, PropTypes.number, PropTypes.bool]),
currentValue: PropTypes.oneOf([PropTypes.string, PropTypes.number, PropTypes.bool]),
checked: PropTypes.bool,
onChange: PropTypes.func,
label: PropTypes.node,
};
const mapDispatchToProps = dispatch => {
return {
onConfirm(account, notifications, muteDuration) {
dispatch(muteAccount(account.get('id'), notifications, muteDuration));
},
export const MuteModal = ({ accountId, acct }) => {
const intl = useIntl();
const dispatch = useDispatch();
const [notifications, setNotifications] = useState(true);
const [muteDuration, setMuteDuration] = useState('0');
const [expanded, setExpanded] = useState(false);
onClose() {
dispatch(closeModal({
modalType: undefined,
ignoreFocus: false,
}));
},
const handleClick = useCallback(() => {
dispatch(closeModal({ modalType: undefined, ignoreFocus: false }));
dispatch(muteAccount(accountId, notifications, muteDuration));
}, [dispatch, accountId, notifications, muteDuration]);
onToggleNotifications() {
dispatch(toggleHideNotifications());
},
const handleCancel = useCallback(() => {
dispatch(closeModal({ modalType: undefined, ignoreFocus: false }));
}, [dispatch]);
onChangeMuteDuration(e) {
dispatch(changeMuteDuration(e.target.value));
},
};
};
const handleToggleNotifications = useCallback(({ target }) => {
setNotifications(target.checked);
}, [setNotifications]);
class MuteModal extends PureComponent {
const handleChangeMuteDuration = useCallback(({ target }) => {
setMuteDuration(target.value);
}, [setMuteDuration]);
static propTypes = {
account: PropTypes.object.isRequired,
notifications: PropTypes.bool.isRequired,
onClose: PropTypes.func.isRequired,
onConfirm: PropTypes.func.isRequired,
onToggleNotifications: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
muteDuration: PropTypes.number.isRequired,
onChangeMuteDuration: PropTypes.func.isRequired,
};
const handleToggleSettings = useCallback(() => {
setExpanded(!expanded);
}, [expanded, setExpanded]);
handleClick = () => {
this.props.onClose();
this.props.onConfirm(this.props.account, this.props.notifications, this.props.muteDuration);
};
handleCancel = () => {
this.props.onClose();
};
toggleNotifications = () => {
this.props.onToggleNotifications();
};
changeMuteDuration = (e) => {
this.props.onChangeMuteDuration(e);
};
render () {
const { account, notifications, muteDuration, intl } = this.props;
return (
<div className='modal-root__modal mute-modal'>
<div className='mute-modal__container'>
<p>
<FormattedMessage
id='confirmations.mute.message'
defaultMessage='Are you sure you want to mute {name}?'
values={{ name: <strong>@{account.get('acct')}</strong> }}
/>
</p>
<p className='mute-modal__explanation'>
<FormattedMessage
id='confirmations.mute.explanation'
defaultMessage='This will hide posts from them and posts mentioning them, but it will still allow them to see your posts and follow you.'
/>
</p>
<div className='setting-toggle'>
<Toggle id='mute-modal__hide-notifications-checkbox' checked={notifications} onChange={this.toggleNotifications} />
<label className='setting-toggle__label' htmlFor='mute-modal__hide-notifications-checkbox'>
<FormattedMessage id='mute_modal.hide_notifications' defaultMessage='Hide notifications from this user?' />
</label>
return (
<div className='modal-root__modal safety-action-modal'>
<div className='safety-action-modal__top'>
<div className='safety-action-modal__header'>
<div className='safety-action-modal__header__icon'>
<Icon icon={VolumeOffIcon} />
</div>
<div>
<span><FormattedMessage id='mute_modal.duration' defaultMessage='Duration' />: </span>
<select value={muteDuration} onChange={this.changeMuteDuration}>
<option value={0}>{intl.formatMessage(messages.indefinite)}</option>
<option value={300}>{intl.formatMessage(messages.minutes, { number: 5 })}</option>
<option value={1800}>{intl.formatMessage(messages.minutes, { number: 30 })}</option>
<option value={3600}>{intl.formatMessage(messages.hours, { number: 1 })}</option>
<option value={21600}>{intl.formatMessage(messages.hours, { number: 6 })}</option>
<option value={86400}>{intl.formatMessage(messages.days, { number: 1 })}</option>
<option value={259200}>{intl.formatMessage(messages.days, { number: 3 })}</option>
<option value={604800}>{intl.formatMessage(messages.days, { number: 7 })}</option>
</select>
<div>
<h1><FormattedMessage id='mute_modal.title' defaultMessage='Mute user?' /></h1>
<div>@{acct}</div>
</div>
</div>
<div className='mute-modal__action-bar'>
<Button onClick={this.handleCancel} className='mute-modal__cancel-button'>
<div className='safety-action-modal__bullet-points'>
<div>
<div className='safety-action-modal__bullet-points__icon'><Icon icon={CampaignIcon} /></div>
<div><FormattedMessage id='mute_modal.they_wont_know' defaultMessage="They won't know they've been muted." /></div>
</div>
<div>
<div className='safety-action-modal__bullet-points__icon'><Icon icon={VisibilityOffIcon} /></div>
<div><FormattedMessage id='mute_modal.you_wont_see_posts' defaultMessage="They can still see your posts, but you won't see theirs." /></div>
</div>
<div>
<div className='safety-action-modal__bullet-points__icon'><Icon icon={AlternateEmailIcon} /></div>
<div><FormattedMessage id='mute_modal.you_wont_see_mentions' defaultMessage="You won't see posts that mention them." /></div>
</div>
<div>
<div className='safety-action-modal__bullet-points__icon'><Icon icon={ReplyIcon} /></div>
<div><FormattedMessage id='mute_modal.they_can_mention_and_follow' defaultMessage="They can mention and follow you, but you won't see them." /></div>
</div>
</div>
</div>
<div className={classNames('safety-action-modal__bottom', { active: expanded })}>
<div className='safety-action-modal__bottom__collapsible'>
<div className='safety-action-modal__field-group'>
<RadioButtonLabel name='duration' value='0' label={intl.formatMessage(messages.indefinite)} currentValue={muteDuration} onChange={handleChangeMuteDuration} />
<RadioButtonLabel name='duration' value='86400' label={intl.formatMessage(messages.hours, { number: 24 })} currentValue={muteDuration} onChange={handleChangeMuteDuration} />
<RadioButtonLabel name='duration' value='604800' label={intl.formatMessage(messages.days, { number: 7 })} currentValue={muteDuration} onChange={handleChangeMuteDuration} />
<RadioButtonLabel name='duration' value='x' label={intl.formatMessage(messages.days, { number: 30 })} currentValue={muteDuration} onChange={handleChangeMuteDuration} />
</div>
<div className='safety-action-modal__field-group'>
<CheckBox label={intl.formatMessage(messages.hideFromNotifications)} checked={notifications} onChange={handleToggleNotifications} />
</div>
</div>
<div className='safety-action-modal__actions'>
<button onClick={handleToggleSettings} className='link-button'>
{expanded ? <FormattedMessage id='mute_modal.hide_options' defaultMessage='Hide options' /> : <FormattedMessage id='mute_modal.show_options' defaultMessage='Show options' />}
</button>
<div className='spacer' />
<button onClick={handleCancel} className='link-button'>
<FormattedMessage id='confirmation_modal.cancel' defaultMessage='Cancel' />
</Button>
<Button onClick={this.handleClick} autoFocus>
</button>
<Button onClick={handleClick}>
<FormattedMessage id='confirmations.mute.confirm' defaultMessage='Mute' />
</Button>
</div>
</div>
);
}
</div>
);
};
}
MuteModal.propTypes = {
accountId: PropTypes.string.isRequired,
acct: PropTypes.string.isRequired,
};
export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(MuteModal));
export default MuteModal;

View file

@ -118,6 +118,10 @@ export function BlockModal () {
return import(/* webpackChunkName: "modals/block_modal" */'../components/block_modal');
}
export function DomainBlockModal () {
return import(/* webpackChunkName: "modals/domain_block_modal" */'../components/domain_block_modal');
}
export function ReportModal () {
return import(/* webpackChunkName: "modals/report_modal" */'../components/report_modal');
}

View file

@ -89,6 +89,11 @@
"announcement.announcement": "Announcement",
"attachments_list.unprocessed": "(unprocessed)",
"audio.hide": "Hide audio",
"block_modal.they_cant_mention": "They can't mention or follow you.",
"block_modal.they_cant_see_posts": "They can't see your posts and you won't see theirs.",
"block_modal.they_will_know": "They can see that they're blocked.",
"block_modal.title": "Block user?",
"block_modal.you_wont_see_mentions": "You won't see posts that mention them.",
"boost_modal.combo": "You can press {combo} to skip this next time",
"bundle_column_error.copy_stacktrace": "Copy error report",
"bundle_column_error.error.body": "The requested page could not be rendered. It could be due to a bug in our code, or a browser compatibility issue.",
@ -160,9 +165,7 @@
"compose_form.spoiler.unmarked": "Add content warning",
"compose_form.spoiler_placeholder": "Content warning (optional)",
"confirmation_modal.cancel": "Cancel",
"confirmations.block.block_and_report": "Block & Report",
"confirmations.block.confirm": "Block",
"confirmations.block.message": "Are you sure you want to block {name}?",
"confirmations.cancel_follow_request.confirm": "Withdraw request",
"confirmations.cancel_follow_request.message": "Are you sure you want to withdraw your request to follow {name}?",
"confirmations.delete.confirm": "Delete",
@ -171,15 +174,13 @@
"confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
"confirmations.discard_edit_media.confirm": "Discard",
"confirmations.discard_edit_media.message": "You have unsaved changes to the media description or preview, discard them anyway?",
"confirmations.domain_block.confirm": "Block entire domain",
"confirmations.domain_block.confirm": "Block server",
"confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable. You will not see content from that domain in any public timelines or your notifications. Your followers from that domain will be removed.",
"confirmations.edit.confirm": "Edit",
"confirmations.edit.message": "Editing now will overwrite the message you are currently composing. Are you sure you want to proceed?",
"confirmations.logout.confirm": "Log out",
"confirmations.logout.message": "Are you sure you want to log out?",
"confirmations.mute.confirm": "Mute",
"confirmations.mute.explanation": "This will hide posts from them and posts mentioning them, but it will still allow them to see your posts and follow you.",
"confirmations.mute.message": "Are you sure you want to mute {name}?",
"confirmations.redraft.confirm": "Delete & redraft",
"confirmations.redraft.message": "Are you sure you want to delete this post and re-draft it? Favorites and boosts will be lost, and replies to the original post will be orphaned.",
"confirmations.reply.confirm": "Reply",
@ -205,6 +206,14 @@
"dismissable_banner.explore_statuses": "These are posts from across the social web that are gaining traction today. Newer posts with more boosts and favorites are ranked higher.",
"dismissable_banner.explore_tags": "These are hashtags that are gaining traction on the social web today. Hashtags that are used by more different people are ranked higher.",
"dismissable_banner.public_timeline": "These are the most recent public posts from people on the social web that people on {domain} follow.",
"domain_block_modal.block": "Block server",
"domain_block_modal.block_account_instead": "Block @{name} instead",
"domain_block_modal.they_can_interact_with_old_posts": "People from this server can interact with your old posts.",
"domain_block_modal.they_cant_follow": "Nobody from this server can follow you.",
"domain_block_modal.they_wont_know": "They won't know they've been blocked.",
"domain_block_modal.title": "Block domain?",
"domain_block_modal.you_will_lose_followers": "All your followers from this server will be removed.",
"domain_block_modal.you_wont_see_posts": "You won't see posts or notifications from users on this server.",
"domain_pill.activitypub_lets_connect": "It lets you connect and interact with people not just on Mastodon, but across different social apps too.",
"domain_pill.activitypub_like_language": "ActivityPub is like the language Mastodon speaks with other social networks.",
"domain_pill.server": "Server",
@ -415,9 +424,15 @@
"loading_indicator.label": "Loading…",
"media_gallery.toggle_visible": "{number, plural, one {Hide image} other {Hide images}}",
"moved_to_account_banner.text": "Your account {disabledAccount} is currently disabled because you moved to {movedToAccount}.",
"mute_modal.duration": "Duration",
"mute_modal.hide_notifications": "Hide notifications from this user?",
"mute_modal.indefinite": "Indefinite",
"mute_modal.hide_from_notifications": "Hide from notifications",
"mute_modal.hide_options": "Hide options",
"mute_modal.indefinite": "Until I unmute them",
"mute_modal.show_options": "Show options",
"mute_modal.they_can_mention_and_follow": "They can mention and follow you, but you won't see them.",
"mute_modal.they_wont_know": "They won't know they've been muted.",
"mute_modal.title": "Mute user?",
"mute_modal.you_wont_see_mentions": "You won't see posts that mention them.",
"mute_modal.you_wont_see_posts": "They can still see your posts, but you won't see theirs.",
"navigation_bar.about": "About",
"navigation_bar.advanced_interface": "Open in advanced web interface",
"navigation_bar.blocks": "Blocked users",

View file

@ -1,22 +0,0 @@
import Immutable from 'immutable';
import {
BLOCKS_INIT_MODAL,
} from '../actions/blocks';
const initialState = Immutable.Map({
new: Immutable.Map({
account_id: null,
}),
});
export default function mutes(state = initialState, action) {
switch (action.type) {
case BLOCKS_INIT_MODAL:
return state.withMutations((state) => {
state.setIn(['new', 'account_id'], action.account.get('id'));
});
default:
return state;
}
}

View file

@ -7,7 +7,6 @@ import { accountsReducer } from './accounts';
import accounts_map from './accounts_map';
import alerts from './alerts';
import announcements from './announcements';
import blocks from './blocks';
import boosts from './boosts';
import compose from './compose';
import contexts from './contexts';
@ -26,7 +25,6 @@ import markers from './markers';
import media_attachments from './media_attachments';
import meta from './meta';
import { modalReducer } from './modal';
import mutes from './mutes';
import { notificationPolicyReducer } from './notification_policy';
import { notificationRequestsReducer } from './notification_requests';
import notifications from './notifications';
@ -62,8 +60,6 @@ const reducers = {
relationships: relationshipsReducer,
settings,
push_notifications,
mutes,
blocks,
boosts,
server,
contexts,

View file

@ -1,31 +0,0 @@
import Immutable from 'immutable';
import {
MUTES_INIT_MODAL,
MUTES_TOGGLE_HIDE_NOTIFICATIONS,
MUTES_CHANGE_DURATION,
} from '../actions/mutes';
const initialState = Immutable.Map({
new: Immutable.Map({
account: null,
notifications: true,
duration: 0,
}),
});
export default function mutes(state = initialState, action) {
switch (action.type) {
case MUTES_INIT_MODAL:
return state.withMutations((state) => {
state.setIn(['new', 'account'], action.account);
state.setIn(['new', 'notifications'], true);
});
case MUTES_TOGGLE_HIDE_NOTIFICATIONS:
return state.updateIn(['new', 'notifications'], (old) => !old);
case MUTES_CHANGE_DURATION:
return state.setIn(['new', 'duration'], Number(action.duration));
default:
return state;
}
}

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="m880-194-80-80v-326H474l-74-74v-86h-86l-80-80h246v160h400v486ZM820-28l-94-92H80v-648l-52-52 56-56L876-84l-56 56ZM160-200h80v-80h-80v80Zm0-160h80v-80h-80v80Zm0-160h80v-80h-80v80Zm160 320h80v-80h-80v80Zm0-160h80v-80h-80v80Zm160 160h166l-80-80h-86v80Zm240-240h-80v-80h80v80Z"/></svg>

After

Width:  |  Height:  |  Size: 377 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="m880-194-80-80v-326H474l-74-74v-86h-86l-80-80h246v160h400v486ZM820-28l-94-92H80v-648l-52-52 56-56L876-84l-56 56ZM160-200h80v-80h-80v80Zm0-160h80v-80h-80v80Zm0-160h80v-80h-80v80Zm160 320h80v-80h-80v80Zm0-160h80v-80h-80v80Zm160 160h166l-80-80h-86v80Zm240-240h-80v-80h80v80Z"/></svg>

After

Width:  |  Height:  |  Size: 377 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M480-120q-138 0-240.5-91.5T122-440h82q14 104 92.5 172T480-200q117 0 198.5-81.5T760-480q0-117-81.5-198.5T480-760q-69 0-129 32t-101 88h110v80H120v-240h80v94q51-64 124.5-99T480-840q75 0 140.5 28.5t114 77q48.5 48.5 77 114T840-480q0 75-28.5 140.5t-77 114q-48.5 48.5-114 77T480-120Zm112-192L440-464v-216h80v184l128 128-56 56Z"/></svg>

After

Width:  |  Height:  |  Size: 425 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M480-120q-138 0-240.5-91.5T122-440h82q14 104 92.5 172T480-200q117 0 198.5-81.5T760-480q0-117-81.5-198.5T480-760q-69 0-129 32t-101 88h110v80H120v-240h80v94q51-64 124.5-99T480-840q75 0 140.5 28.5t114 77q48.5 48.5 77 114T840-480q0 75-28.5 140.5t-77 114q-48.5 48.5-114 77T480-120Zm112-192L440-464v-216h80v184l128 128-56 56Z"/></svg>

After

Width:  |  Height:  |  Size: 425 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M640-520v-80h240v80H640Zm-280 40q-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: 330 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M640-520v-80h240v80H640Zm-280 40q-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: 551 B

View file

@ -106,17 +106,17 @@
}
&.button-secondary {
color: $ui-button-secondary-color;
color: $highlight-text-color;
background: transparent;
padding: 6px 17px;
border: 1px solid $ui-button-secondary-border-color;
border: 1px solid $highlight-text-color;
&:active,
&:focus,
&:hover {
border-color: $ui-button-secondary-focus-background-color;
color: $ui-button-secondary-focus-color;
background-color: $ui-button-secondary-focus-background-color;
border-color: lighten($highlight-text-color, 4%);
color: lighten($highlight-text-color, 4%);
background-color: transparent;
text-decoration: none;
}
@ -5480,6 +5480,10 @@ a.status-card {
pointer-events: auto;
user-select: text;
display: flex;
@media screen and (max-width: $no-gap-breakpoint) {
margin-top: auto;
}
}
.video-modal .video-player {
@ -5811,6 +5815,145 @@ a.status-card {
margin-inline-start: 10px;
}
.safety-action-modal {
width: 600px;
flex-direction: column;
&__top,
&__bottom {
display: flex;
gap: 8px;
padding: 24px;
flex-direction: column;
background: var(--modal-background-color);
backdrop-filter: var(--background-filter);
border: 1px solid var(--modal-border-color);
}
&__top {
border-radius: 16px 16px 0 0;
border-bottom: 0;
gap: 16px;
}
&__bottom {
border-radius: 0 0 16px 16px;
border-top: 0;
@media screen and (max-width: $no-gap-breakpoint) {
border-radius: 0;
border-bottom: 0;
padding-bottom: 32px;
}
}
&__header {
display: flex;
gap: 16px;
align-items: center;
font-size: 14px;
line-height: 20px;
color: $darker-text-color;
&__icon {
border-radius: 64px;
background: $ui-highlight-color;
color: $white;
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
flex-shrink: 0;
.icon {
width: 24px;
height: 24px;
}
}
h1 {
font-size: 22px;
line-height: 28px;
color: $primary-text-color;
}
}
&__bullet-points {
display: flex;
flex-direction: column;
gap: 8px;
font-size: 16px;
line-height: 24px;
& > div {
display: flex;
gap: 16px;
align-items: center;
}
&__icon {
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
.icon {
width: 24px;
height: 24px;
}
}
}
&__field-group {
display: flex;
flex-direction: column;
label {
display: flex;
gap: 16px;
align-items: center;
font-size: 16px;
line-height: 24px;
height: 32px;
padding: 0 12px;
}
}
&__bottom {
padding-top: 0;
&__collapsible {
display: none;
flex-direction: column;
gap: 16px;
}
&.active {
background: var(--modal-background-variant-color);
padding-top: 24px;
.safety-action-modal__bottom__collapsible {
display: flex;
}
}
}
&__actions {
display: flex;
align-items: center;
gap: 8px;
justify-content: flex-end;
.link-button {
padding: 10px 12px;
font-weight: 600;
}
}
}
.boost-modal,
.confirmation-modal,
.report-modal,
@ -7113,7 +7256,8 @@ a.status-card {
display: flex;
}
.radio-button {
.radio-button,
.check-box {
font-size: 14px;
position: relative;
display: inline-flex;
@ -7132,17 +7276,19 @@ a.status-card {
}
&__input {
display: block;
display: flex;
align-items: center;
justify-content: center;
position: relative;
border: 2px solid $secondary-text-color;
box-sizing: border-box;
width: 18px;
height: 18px;
width: 20px;
height: 20px;
flex: 0 0 auto;
border-radius: 50%;
&.checked {
border-color: $secondary-text-color;
border-color: $ui-highlight-color;
&::before {
position: absolute;
@ -7151,9 +7297,31 @@ a.status-card {
content: '';
display: block;
border-radius: 50%;
width: 10px;
height: 10px;
background: $secondary-text-color;
width: 12px;
height: 12px;
background: $ui-highlight-color;
}
}
.icon {
width: 18px;
height: 18px;
}
}
}
.check-box {
&__input {
width: 18px;
height: 18px;
border-radius: 2px;
&.checked {
background: $ui-highlight-color;
color: $white;
&::before {
display: none;
}
}
}
@ -8632,22 +8800,36 @@ noscript {
}
}
.safety-action-modal,
.interaction-modal {
max-width: 90vw;
width: 600px;
background: var(--modal-background-color);
border: 1px solid var(--modal-border-color);
border-radius: 8px;
}
.interaction-modal {
overflow: visible;
position: relative;
display: block;
padding: 40px;
border-radius: 16px;
background: var(--modal-background-color);
backdrop-filter: var(--background-filter);
border: 1px solid var(--modal-border-color);
padding: 24px;
@media screen and (max-width: $no-gap-breakpoint) {
border-radius: 16px 16px 0 0;
border-bottom: 0;
padding-bottom: 32px;
}
h3 {
font-size: 22px;
line-height: 33px;
font-weight: 700;
text-align: center;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
p {
@ -8668,7 +8850,9 @@ noscript {
&__icon {
color: $highlight-text-color;
margin: 0 5px;
display: flex;
align-items: center;
justify-content: center;
}
&__lead {
@ -8701,6 +8885,7 @@ noscript {
border: 0;
padding: 15px - 4px 15px - 6px;
flex: 1 1 auto;
min-width: 0;
&::placeholder {
color: lighten($darker-text-color, 4%);

View file

@ -46,10 +46,10 @@ $ui-button-focus-background-color: $blurple-600 !default;
$ui-button-focus-outline-color: $blurple-400 !default;
$ui-button-focus-outline: solid 2px $ui-button-focus-outline-color !default;
$ui-button-secondary-color: $grey-100 !default;
$ui-button-secondary-border-color: $grey-100 !default;
$ui-button-secondary-focus-background-color: $grey-600 !default;
$ui-button-secondary-focus-color: $white !default;
$ui-button-secondary-color: $blurple-500 !default;
$ui-button-secondary-border-color: $blurple-500 !default;
$ui-button-secondary-focus-border-color: $blurple-300 !default;
$ui-button-secondary-focus-color: $blurple-300 !default;
$ui-button-tertiary-color: $blurple-300 !default;
$ui-button-tertiary-border-color: $blurple-300 !default;
@ -98,7 +98,8 @@ $font-monospace: 'mastodon-font-monospace' !default;
--dropdown-background-color: #{rgba(darken($ui-base-color, 8%), 0.9)};
--dropdown-shadow: 0 20px 25px -5px #{rgba($base-shadow-color, 0.25)},
0 8px 10px -6px #{rgba($base-shadow-color, 0.25)};
--modal-background-color: #{darken($ui-base-color, 4%)};
--modal-background-color: #{rgba(darken($ui-base-color, 8%), 0.7)};
--modal-background-variant-color: #{rgba($ui-base-color, 0.7)};
--modal-border-color: #{lighten($ui-base-color, 4%)};
--background-border-color: #{lighten($ui-base-color, 4%)};
--background-filter: blur(10px) saturate(180%) contrast(75%) brightness(70%);