Add phrase confirmation to domain block dialog

This commit is contained in:
noellabo 2022-09-25 19:58:11 +09:00
parent c297bf5471
commit 695f9a7d08
18 changed files with 137 additions and 29 deletions

View file

@ -96,6 +96,7 @@ class Settings::PreferencesController < Settings::BaseController
:setting_default_search_searchability,
:setting_show_reload_button,
:setting_default_column_width,
:setting_confirm_domain_block,
notification_emails: %i(follow follow_request reblog favourite emoji_reaction status_reference mention digest report pending_account trending_tag),
interactions: %i(must_be_follower must_be_following must_be_following_dm must_be_dm_to_send_email must_be_following_reference)
)

View file

@ -1,13 +1,6 @@
import React from 'react';
import { connect } from 'react-redux';
import { blockDomain, unblockDomain } from '../actions/domain_blocks';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { unblockDomain } from '../actions/domain_blocks';
import Domain from '../components/domain';
import { openModal } from '../actions/modal';
const messages = defineMessages({
blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Block entire domain' },
});
const makeMapStateToProps = () => {
const mapStateToProps = () => ({});
@ -15,18 +8,10 @@ const makeMapStateToProps = () => {
return mapStateToProps;
};
const mapDispatchToProps = (dispatch, { intl }) => ({
onBlockDomain (domain) {
dispatch(openModal('CONFIRM', {
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.' values={{ domain: <strong>{domain}</strong> }} />,
confirm: intl.formatMessage(messages.blockDomainConfirm),
onConfirm: () => dispatch(blockDomain(domain)),
}));
},
const mapDispatchToProps = (dispatch) => ({
onUnblockDomain (domain) {
dispatch(unblockDomain(domain));
},
});
export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Domain));
export default connect(makeMapStateToProps, mapDispatchToProps)(Domain);

View file

@ -52,7 +52,7 @@ import { initReport } from '../actions/reports';
import { openModal } from '../actions/modal';
import { deployPictureInPicture } from '../actions/picture_in_picture';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { boostModal, deleteModal, unfollowModal, unsubscribeModal } from '../initial_state';
import { boostModal, deleteModal, unfollowModal, unsubscribeModal, confirmDomainBlock } from '../initial_state';
import { showAlertForError } from '../actions/alerts';
import { createSelector } from 'reselect';
@ -68,6 +68,7 @@ const messages = defineMessages({
quoteConfirm: { id: 'confirmations.quote.confirm', defaultMessage: 'Quote' },
quoteMessage: { id: 'confirmations.quote.message', defaultMessage: 'Quoting now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Hide entire domain' },
blockDomainPassphrase: { id: 'confirmations.domain_block.passphrase', defaultMessage: 'block' },
unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' },
unsubscribeConfirm: { id: 'confirmations.unsubscribe.confirm', defaultMessage: 'Unsubscribe' },
});
@ -254,6 +255,8 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
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)),
passphrase: confirmDomainBlock && intl.formatMessage(messages.blockDomainPassphrase),
destructive: true,
}));
},

View file

@ -22,13 +22,14 @@ import { initReport } from '../../../actions/reports';
import { openModal } from '../../../actions/modal';
import { blockDomain, unblockDomain } from '../../../actions/domain_blocks';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { unfollowModal, unsubscribeModal } from '../../../initial_state';
import { unfollowModal, unsubscribeModal, confirmDomainBlock } from '../../../initial_state';
import { List as ImmutableList } from 'immutable';
const messages = defineMessages({
unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' },
unsubscribeConfirm: { id: 'confirmations.unsubscribe.confirm', defaultMessage: 'Unsubscribe' },
blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Hide entire domain' },
blockDomainPassphrase: { id: 'confirmations.domain_block.passphrase', defaultMessage: 'block' },
});
const makeMapStateToProps = () => {
@ -146,6 +147,8 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
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)),
passphrase: confirmDomainBlock && intl.formatMessage(messages.blockDomainPassphrase),
destructive: true,
}));
},

View file

@ -60,7 +60,7 @@ import { openModal } from '../../actions/modal';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { HotKeys } from 'react-hotkeys';
import { boostModal, deleteModal, enableStatusReference } from '../../initial_state';
import { boostModal, deleteModal, confirmDomainBlock, enableStatusReference } from '../../initial_state';
import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from '../ui/util/fullscreen';
import { textForScreenReader, defaultMediaVisibility } from '../../components/status';
import Icon from 'mastodon/components/icon';
@ -82,6 +82,7 @@ const messages = defineMessages({
quoteConfirm: { id: 'confirmations.quote.confirm', defaultMessage: 'Quote' },
quoteMessage: { id: 'confirmations.quote.message', defaultMessage: 'Quoting now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Hide entire domain' },
blockDomainPassphrase: { id: 'confirmations.domain_block.passphrase', defaultMessage: 'block' },
});
const makeMapStateToProps = () => {
@ -430,6 +431,8 @@ class Status extends ImmutablePureComponent {
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)),
passphrase: confirmDomainBlock && this.props.intl.formatMessage(messages.blockDomainPassphrase),
destructive: true,
}));
}

View file

@ -2,6 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import { injectIntl, FormattedMessage } from 'react-intl';
import Button from '../../../components/button';
import classNames from 'classnames';
export default @injectIntl
class ConfirmationModal extends React.PureComponent {
@ -14,6 +15,8 @@ class ConfirmationModal extends React.PureComponent {
secondary: PropTypes.string,
onSecondary: PropTypes.func,
closeWhenConfirm: PropTypes.bool,
destructive: PropTypes.bool,
passphrase: PropTypes.string,
intl: PropTypes.object.isRequired,
};
@ -21,15 +24,30 @@ class ConfirmationModal extends React.PureComponent {
closeWhenConfirm: true,
};
state = {
passphrase: '',
};
componentDidMount() {
if (this.props.passphrase) {
this.passphraseInput.focus();
} else {
this.button.focus();
}
}
handleClick = () => {
if (this.props.closeWhenConfirm) {
this.props.onClose();
const { passphrase, closeWhenConfirm, onClose, onConfirm } = this.props;
if (passphrase && this.state.passphrase !== passphrase) {
this.passphraseInput.focus();
return;
}
this.props.onConfirm();
if (closeWhenConfirm) {
onClose();
}
onConfirm();
}
handleSecondary = () => {
@ -41,12 +59,20 @@ class ConfirmationModal extends React.PureComponent {
this.props.onClose();
}
handleChange = (e) => {
this.setState({ passphrase: e.target.value });
}
setRef = (c) => {
this.button = c;
}
setPassphraseRef = (c) => {
this.passphraseInput = c;
}
render () {
const { message, confirm, secondary } = this.props;
const { message, confirm, secondary, destructive, passphrase } = this.props;
return (
<div className='modal-root__modal confirmation-modal'>
@ -54,6 +80,15 @@ class ConfirmationModal extends React.PureComponent {
{message}
</div>
{passphrase && (
<div className='confirmation-modal__passphrase'>
<div className='passphrase__label'>
<FormattedMessage id='confirmations.passphrase' defaultMessage='Please type "{passphrase}" to confirm' values={{ passphrase: <strong>{passphrase}</strong> }} />,
</div>
<input type='text' className={classNames('passphrase__input', { invalid: this.state.passphrase !== passphrase })} onChange={this.handleChange} ref={this.setPassphraseRef} />
</div>
)}
<div className='confirmation-modal__action-bar'>
<Button onClick={this.handleCancel} className='confirmation-modal__cancel-button'>
<FormattedMessage id='confirmation_modal.cancel' defaultMessage='Cancel' />
@ -61,7 +96,7 @@ class ConfirmationModal extends React.PureComponent {
{secondary !== undefined && (
<Button text={secondary} onClick={this.handleSecondary} className='confirmation-modal__secondary-button' />
)}
<Button text={confirm} onClick={this.handleClick} ref={this.setRef} />
<Button text={confirm} className={classNames({ 'always-destructive': destructive })} onClick={this.handleClick} disabled={passphrase && this.state.passphrase !== passphrase} ref={this.setRef} />
</div>
</div>
);

View file

@ -31,6 +31,7 @@ export const showTrends = getMeta('trends');
export const title = getMeta('title');
export const cropImages = getMeta('crop_images');
export const disableSwiping = getMeta('disable_swiping');
export const confirmDomainBlock = getMeta('confirm_domain_block');
export const show_follow_button_on_timeline = getMeta('show_follow_button_on_timeline');
export const show_subscribe_button_on_timeline = getMeta('show_subscribe_button_on_timeline');
export const show_followed_by = getMeta('show_followed_by');

View file

@ -185,11 +185,13 @@
"confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
"confirmations.domain_block.confirm": "Block entire domain",
"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.domain_block.passphrase": "block",
"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.passphrase": "Please type \"{passphrase}\" to confirm",
"confirmations.post_reference.confirm": "Post",
"confirmations.post_reference.message": "It contains references, do you want to post it?",
"confirmations.quote.confirm": "Quote",

View file

@ -185,11 +185,13 @@
"confirmations.delete_list.message": "本当にこのリストを完全に削除しますか?",
"confirmations.domain_block.confirm": "ドメイン全体をブロック",
"confirmations.domain_block.message": "本当に{domain}全体を非表示にしますか? 多くの場合は個別にブロックやミュートするだけで充分であり、また好ましいです。公開タイムラインにそのドメインのコンテンツが表示されなくなり、通知も届かなくなります。そのドメインのフォロワーはアンフォローされます。",
"confirmations.domain_block.passphrase": "ブロック",
"confirmations.logout.confirm": "ログアウト",
"confirmations.logout.message": "本当にログアウトしますか?",
"confirmations.mute.confirm": "ミュート",
"confirmations.mute.explanation": "これにより相手の投稿と返信は見えなくなりますが、相手はあなたをフォローし続け投稿を見ることができます。",
"confirmations.mute.message": "本当に{name}さんをミュートしますか?",
"confirmations.passphrase": "確認のため \"{passphrase}\" と入力してください",
"confirmations.post_reference.confirm": "投稿",
"confirmations.post_reference.message": "参照を含んでいますが、投稿しますか?",
"confirmations.quote.confirm": "引用",

View file

@ -382,6 +382,14 @@ html {
border: 1px solid lighten($ui-base-color, 8%);
}
.confirmation-modal__passphrase .passphrase__input {
border: 1px solid lighten($ui-base-color, 8%);
&.invalid {
background-color: darken($error-value-color, 35%);
}
}
.reactions-bar__item {
&:hover:enabled,
&:focus:enabled,

View file

@ -84,6 +84,16 @@
}
}
&.always-destructive {
background-color: $error-red;
&:disabled,
&.disabled {
background-color: $ui-primary-color;
cursor: default;
}
}
&:disabled,
&.disabled {
background-color: $ui-primary-color;
@ -5747,6 +5757,51 @@ a.status-card.compact:hover {
text-align: center;
}
.confirmation-modal__passphrase {
text-align: center;
padding: 0 30px 30px;
font-size: 16px;
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 8px;
.passphrase__label {
flex: 1 1 auto;
strong {
font-weight: 500;
@each $lang in $cjk-langs {
&:lang(#{$lang}) {
font-weight: 700;
}
}
}
}
.passphrase__input {
flex: 1 1 auto;
min-width: 50%;
display: block;
box-sizing: border-box;
padding: 10px;
margin: 0;
color: $inverted-text-color;
background: $simple-background-color;
font-family: inherit;
font-size: 16px;
resize: vertical;
border: 0;
outline: 0;
border-radius: 4px;
&.invalid {
background-color: lighten($error-value-color, 35%);
}
}
}
.block-modal,
.mute-modal {
&__explanation {

View file

@ -57,6 +57,7 @@ class UserSettingsDecorator
user.settings['use_pending_items'] = use_pending_items_preference if change?('setting_use_pending_items')
user.settings['trends'] = trends_preference if change?('setting_trends')
user.settings['crop_images'] = crop_images_preference if change?('setting_crop_images')
user.settings['confirm_domain_block'] = confirm_domain_block_preference if change?('setting_confirm_domain_block')
user.settings['show_follow_button_on_timeline'] = show_follow_button_on_timeline_preference if change?('setting_show_follow_button_on_timeline')
user.settings['show_subscribe_button_on_timeline'] = show_subscribe_button_on_timeline_preference if change?('setting_show_subscribe_button_on_timeline')
user.settings['show_followed_by'] = show_followed_by_preference if change?('setting_show_followed_by')
@ -90,7 +91,7 @@ class UserSettingsDecorator
user.settings['default_search_searchability'] = default_search_searchability_preference if change?('setting_default_search_searchability')
user.settings['show_reload_button'] = show_reload_button_preference if change?('setting_show_reload_button')
user.settings['default_column_width'] = default_column_width_preference if change?('setting_default_column_width')
end
end
def merged_notification_emails
user.settings['notification_emails'].merge coerced_settings('notification_emails').to_h
@ -204,6 +205,10 @@ end
boolean_cast_setting 'setting_crop_images'
end
def confirm_domain_block_preference
boolean_cast_setting 'setting_confirm_domain_block'
end
def show_follow_button_on_timeline_preference
boolean_cast_setting 'setting_show_follow_button_on_timeline'
end

View file

@ -125,7 +125,7 @@ class User < ApplicationRecord
:reduce_motion, :system_font_ui, :noindex, :theme, :display_media, :hide_network,
:expand_spoilers, :default_language, :aggregate_reblogs, :show_application,
:advanced_layout, :use_blurhash, :use_pending_items, :trends, :crop_images,
:disable_swiping,
:disable_swiping, :confirm_domain_block,
:show_follow_button_on_timeline, :show_subscribe_button_on_timeline, :show_target,
:show_follow_button_on_timeline, :show_subscribe_button_on_timeline, :show_followed_by, :show_target,
:follow_button_to_list_adder, :show_navigation_panel, :show_quote_button, :show_bookmark_button,

View file

@ -42,6 +42,7 @@ class InitialStateSerializer < ActiveModel::Serializer
store[:is_staff] = object.current_account.user.staff?
store[:trends] = Setting.trends && object.current_account.user.setting_trends
store[:crop_images] = object.current_account.user.setting_crop_images
store[:confirm_domain_block] = object.current_account.user.setting_confirm_domain_block
store[:show_follow_button_on_timeline] = object.current_account.user.setting_show_follow_button_on_timeline
store[:show_subscribe_button_on_timeline] = object.current_account.user.setting_show_subscribe_button_on_timeline
store[:show_followed_by] = object.current_account.user.setting_show_followed_by

View file

@ -98,6 +98,7 @@
= f.input :setting_unsubscribe_modal, as: :boolean, wrapper: :with_label, fedibird_features: true
= f.input :setting_boost_modal, as: :boolean, wrapper: :with_label
= f.input :setting_delete_modal, as: :boolean, wrapper: :with_label
= f.input :setting_confirm_domain_block, as: :boolean, wrapper: :with_label
= f.input :setting_post_reference_modal, as: :boolean, wrapper: :with_label, fedibird_features: true
= f.input :setting_add_reference_modal, as: :boolean, wrapper: :with_label, fedibird_features: true
= f.input :setting_unselect_reference_modal, as: :boolean, wrapper: :with_label, fedibird_features: true

View file

@ -220,6 +220,7 @@ en:
setting_auto_play_gif: Auto-play animated GIFs
setting_boost_modal: Show confirmation dialog before boosting
setting_compact_reaction: Compact display of reaction
setting_confirm_domain_block: Require domain input for domain block
setting_confirm_follow_from_bot: Require follow requests from bot
setting_content_emoji_reaction_size: Emoji reaction size
setting_content_font_size: Content font size

View file

@ -220,6 +220,7 @@ ja:
setting_auto_play_gif: アニメーションGIFを自動再生する
setting_boost_modal: ブーストする前に確認ダイアログを表示する
setting_compact_reaction: リアクションをコンパクトに表示
setting_confirm_domain_block: ドメインブロックにドメイン入力を要求する
setting_confirm_follow_from_bot: Bot承認制アカウントにする
setting_content_emoji_reaction_size: 投稿の絵文字リアクションのサイズ
setting_content_font_size: 投稿のフォントサイズ

View file

@ -42,6 +42,7 @@ defaults: &defaults
trends: true
trendable_by_default: false
crop_images: true
confirm_domain_block: true
show_follow_button_on_timeline: false
show_subscribe_button_on_timeline: false
show_followed_by: false