Merge commit 'd702a03a0c35fc631a0fa456532946e6751cbbfd' into glitch-soc/merge-upstream

This commit is contained in:
Claire 2024-03-15 22:48:04 +01:00
commit b8f476256e
30 changed files with 679 additions and 348 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

@ -11,6 +11,7 @@ import { connect } from 'react-redux';
import AlternateEmailIcon from '@/material-icons/400-24px/alternate_email.svg?react';
import BookmarksIcon from '@/material-icons/400-24px/bookmarks-fill.svg?react';
import ExploreIcon from '@/material-icons/400-24px/explore.svg?react';
import PeopleIcon from '@/material-icons/400-24px/group.svg?react';
import HomeIcon from '@/material-icons/400-24px/home-fill.svg?react';
import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react';
@ -19,7 +20,6 @@ import PersonAddIcon from '@/material-icons/400-24px/person_add.svg?react';
import PublicIcon from '@/material-icons/400-24px/public.svg?react';
import SettingsIcon from '@/material-icons/400-24px/settings-fill.svg?react';
import StarIcon from '@/material-icons/400-24px/star.svg?react';
import TagIcon from '@/material-icons/400-24px/tag.svg?react';
import { fetchFollowRequests } from 'mastodon/actions/accounts';
import Column from 'mastodon/components/column';
import ColumnHeader from 'mastodon/components/column_header';
@ -112,7 +112,7 @@ class GettingStarted extends ImmutablePureComponent {
if (showTrends) {
navItems.push(
<ColumnLink key='explore' icon='explore' iconComponent={TagIcon} text={intl.formatMessage(messages.explore)} to='/explore' />,
<ColumnLink key='explore' icon='explore' iconComponent={ExploreIcon} text={intl.formatMessage(messages.explore)} to='/explore' />,
);
}

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,116 @@
import PropTypes from 'prop-types';
import { PureComponent } from 'react';
import { useCallback, useState } from 'react';
import { injectIntl, FormattedMessage } from 'react-intl';
import { FormattedMessage } from 'react-intl';
import { connect } from 'react-redux';
import classNames from 'classnames';
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 { useDispatch } from 'react-redux';
const makeMapStateToProps = () => {
const getAccount = makeGetAccount();
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 mapStateToProps = state => ({
account: getAccount(state, state.getIn(['blocks', 'new', 'account_id'])),
});
export const BlockModal = ({ accountId, acct }) => {
const dispatch = useDispatch();
const [expanded, setExpanded] = useState(false);
return mapStateToProps;
};
const domain = acct.split('@')[1];
const mapDispatchToProps = dispatch => {
return {
onConfirm(account) {
dispatch(blockAccount(account.get('id')));
},
const handleClick = useCallback(() => {
dispatch(closeModal({ modalType: undefined, ignoreFocus: false }));
dispatch(blockAccount(accountId));
}, [dispatch, accountId]);
onBlockAndReport(account) {
dispatch(blockAccount(account.get('id')));
dispatch(initReport(account));
},
const handleCancel = useCallback(() => {
dispatch(closeModal({ modalType: undefined, ignoreFocus: false }));
}, [dispatch]);
onClose() {
dispatch(closeModal({
modalType: undefined,
ignoreFocus: false,
}));
},
};
};
const handleToggleLearnMore = useCallback(() => {
setExpanded(!expanded);
}, [expanded, setExpanded]);
class BlockModal extends PureComponent {
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>
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={classNames('safety-action-modal__bottom', { active: expanded })}>
{domain && (
<div className='safety-action-modal__bottom__collapsible'>
<div className='safety-action-modal__caveats'>
<FormattedMessage
id='block_modal.remote_users_caveat'
defaultMessage='We will ask the server {domain} to respect your decision. However, compliance is not guaranteed since some servers may handle blocks differently. Public posts may still be visible to non-logged-in users.'
values={{ domain: <strong>{domain}</strong> }}
/>
</div>
</div>
)}
<div className='safety-action-modal__actions'>
{domain && (
<button onClick={handleToggleLearnMore} className='link-button'>
{expanded ? <FormattedMessage id='block_modal.show_less' defaultMessage='Show less' /> : <FormattedMessage id='block_modal.show_more' defaultMessage='Show more' />}
</button>
)}
<div className='spacer' />
<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,14 @@
"announcement.announcement": "Announcement",
"attachments_list.unprocessed": "(unprocessed)",
"audio.hide": "Hide audio",
"block_modal.remote_users_caveat": "We will ask the server {domain} to respect your decision. However, compliance is not guaranteed since some servers may handle blocks differently. Public posts may still be visible to non-logged-in users.",
"block_modal.show_less": "Show less",
"block_modal.show_more": "Show more",
"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 +168,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 +177,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 +209,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 +427,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,154 @@ 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;
}
}
&__caveats {
font-size: 14px;
padding: 0 12px;
strong {
font-weight: 500;
}
}
&__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 +7265,8 @@ a.status-card {
display: flex;
}
.radio-button {
.radio-button,
.check-box {
font-size: 14px;
position: relative;
display: inline-flex;
@ -7132,17 +7285,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 +7306,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 +8809,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 +8859,9 @@ noscript {
&__icon {
color: $highlight-text-color;
margin: 0 5px;
display: flex;
align-items: center;
justify-content: center;
}
&__lead {
@ -8701,6 +8894,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%);

View file

@ -100,6 +100,14 @@ module Mastodon::CLI
progress.finish
say("Indexed #{added} records, de-indexed #{removed}", :green, true)
rescue Elasticsearch::Transport::Transport::ServerError => e
fail_with_message <<~ERROR
There was an issue connecting to the search server. Make sure the
server is configured and running correctly, and that the environment
variable settings match what the server is expecting.
#{e.message}
ERROR
end
private

View file

@ -33,6 +33,17 @@ describe Mastodon::CLI::Search do
end
end
context 'when server communication raises an error' do
let(:options) { { reset_chewy: true } }
before { allow(Chewy::Stash::Specification).to receive(:reset!).and_raise(Elasticsearch::Transport::Transport::Errors::InternalServerError) }
it 'Exits with error message' do
expect { subject }
.to raise_error(Thor::Error, /issue connecting to the search/)
end
end
context 'without options' do
before { stub_search_indexes }