Add safety and privacy features

This commit is contained in:
noellabo 2022-10-03 16:13:07 +09:00
parent 1885016a4c
commit 206b5dbf04
51 changed files with 492 additions and 477 deletions

View file

@ -38,6 +38,8 @@ class Api::V1::AccountsController < Api::BaseController
end
def follow
raise Mastodon::NotPermittedError if current_user.setting_disable_follow && !current_user.account.following?(@account)
follow = FollowService.new.call(current_user.account, @account, reblogs: params.key?(:reblogs) ? truthy_param?(:reblogs) : nil, notify: params.key?(:notify) ? truthy_param?(:notify) : nil, delivery: params.key?(:delivery) ? truthy_param?(:delivery) : nil, with_rate_limit: true)
options = @account.locked? || current_user.account.silenced? ? {} : {
following_map: { @account.id => true },
@ -56,6 +58,8 @@ class Api::V1::AccountsController < Api::BaseController
end
def block
raise Mastodon::NotPermittedError if current_user.setting_disable_block
BlockService.new.call(current_user.account, @account)
render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships
end
@ -66,6 +70,8 @@ class Api::V1::AccountsController < Api::BaseController
end
def unfollow
raise Mastodon::NotPermittedError if current_user.setting_disable_unfollow && (current_user.account.following?(@account) || !current_user.account.requested?(@account))
UnfollowService.new.call(current_user.account, @account)
render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships
end

View file

@ -14,6 +14,8 @@ class Api::V1::DomainBlocksController < Api::BaseController
end
def create
raise Mastodon::NotPermittedError if current_user.setting_disable_domain_block
current_account.block_domain!(domain_block_params[:domain])
AfterAccountDomainBlockWorker.perform_async(current_account.id, domain_block_params[:domain])
render_empty

View file

@ -19,6 +19,8 @@ class Api::V1::NotificationsController < Api::BaseController
end
def clear
raise Mastodon::NotPermittedError if current_user.setting_disable_clear_all_notifications
current_account.notifications.delete_all
render_empty
end

View file

@ -8,6 +8,8 @@ class Api::V1::Statuses::EmojiReactionsController < Api::BaseController
before_action :set_status
def update
raise Mastodon::NotPermittedError if current_user.setting_disable_reactions
if EmojiReactionService.new.call(current_account, @status, params[:id]).present?
@status = Status.include_expired.find(params[:status_id])
end

View file

@ -8,6 +8,8 @@ class Api::V1::Statuses::FavouritesController < Api::BaseController
before_action :set_status, only: [:create]
def create
raise Mastodon::NotPermittedError if current_user.setting_disable_reactions
FavouriteService.new.call(current_account, @status)
render json: @status, serializer: REST::StatusSerializer
end

View file

@ -11,6 +11,8 @@ class Api::V1::Statuses::ReblogsController < Api::BaseController
override_rate_limit_headers :create, family: :statuses
def create
raise Mastodon::NotPermittedError if current_user.setting_disable_reactions
@status = ReblogService.new.call(current_account, @reblog, reblog_params)
render json: @status, serializer: REST::StatusSerializer

View file

@ -54,6 +54,8 @@ class Api::V1::StatusesController < Api::BaseController
end
def create
raise Mastodon::NotPermittedError if current_user.setting_disable_post
@status = PostStatusService.new.call(current_user.account,
text: status_params[:status],
thread: @thread,

View file

@ -0,0 +1,9 @@
# frozen_string_literal: true
class Settings::Preferences::SafetyController < Settings::PreferencesController
private
def after_update_redirect_path
settings_preferences_safety_path
end
end

View file

@ -101,6 +101,16 @@ class Settings::PreferencesController < Settings::BaseController
:setting_confirm_domain_block,
:setting_default_expires_in,
:setting_default_expires_action,
:setting_disable_post,
:setting_disable_reactions,
:setting_disable_follow,
:setting_disable_unfollow,
:setting_disable_block,
:setting_disable_domain_block,
:setting_disable_clear_all_notifications,
:setting_disable_account_delete,
:setting_prohibited_words,
setting_prohibited_visibilities: [],
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

@ -204,7 +204,7 @@ export function followAccount(id, options = { reblogs: true, delivery: true }) {
api(getState).post(`/api/v1/accounts/${id}/follow`, options).then(response => {
dispatch(followAccountSuccess(response.data, alreadyFollowing));
}).catch(error => {
dispatch(followAccountFail(error, locked));
dispatch(followAccountFail(id, error, locked));
});
};
};
@ -216,7 +216,7 @@ export function unfollowAccount(id) {
api(getState).post(`/api/v1/accounts/${id}/unfollow`).then(response => {
dispatch(unfollowAccountSuccess(response.data, getState().get('statuses')));
}).catch(error => {
dispatch(unfollowAccountFail(error));
dispatch(unfollowAccountFail(id, error));
});
};
};
@ -239,9 +239,10 @@ export function followAccountSuccess(relationship, alreadyFollowing) {
};
};
export function followAccountFail(error, locked) {
export function followAccountFail(id, error, locked) {
return {
type: ACCOUNT_FOLLOW_FAIL,
id,
error,
locked,
skipLoading: true,
@ -265,9 +266,10 @@ export function unfollowAccountSuccess(relationship, statuses) {
};
};
export function unfollowAccountFail(error) {
export function unfollowAccountFail(id, error) {
return {
type: ACCOUNT_UNFOLLOW_FAIL,
id,
error,
skipLoading: true,
};
@ -283,7 +285,7 @@ export function subscribeAccount(id, reblogs = true, list_id = null) {
api(getState).post(`/api/v1/accounts/${id}/subscribe`, { reblogs, list_id }).then(response => {
dispatch(subscribeAccountSuccess(response.data, alreadySubscribe));
}).catch(error => {
dispatch(subscribeAccountFail(error, locked));
dispatch(subscribeAccountFail(id, error, locked));
});
};
};
@ -295,7 +297,7 @@ export function unsubscribeAccount(id, list_id = null) {
api(getState).post(`/api/v1/accounts/${id}/unsubscribe`, { list_id }).then(response => {
dispatch(unsubscribeAccountSuccess(response.data, getState().get('statuses')));
}).catch(error => {
dispatch(unsubscribeAccountFail(error));
dispatch(unsubscribeAccountFail(id, error));
});
};
};
@ -318,9 +320,10 @@ export function subscribeAccountSuccess(relationship, alreadySubscribe) {
};
};
export function subscribeAccountFail(error, locked) {
export function subscribeAccountFail(id, error, locked) {
return {
type: ACCOUNT_SUBSCRIBE_FAIL,
id,
error,
locked,
skipLoading: true,
@ -344,9 +347,10 @@ export function unsubscribeAccountSuccess(relationship, statuses) {
};
};
export function unsubscribeAccountFail(error) {
export function unsubscribeAccountFail(id, error) {
return {
type: ACCOUNT_UNSUBSCRIBE_FAIL,
id,
error,
skipLoading: true,
};
@ -392,9 +396,10 @@ export function blockAccountSuccess(relationship, statuses) {
};
};
export function blockAccountFail(error) {
export function blockAccountFail(id, error) {
return {
type: ACCOUNT_BLOCK_FAIL,
id,
error,
};
};
@ -413,9 +418,10 @@ export function unblockAccountSuccess(relationship) {
};
};
export function unblockAccountFail(error) {
export function unblockAccountFail(id, error) {
return {
type: ACCOUNT_UNBLOCK_FAIL,
id,
error,
};
};

View file

@ -7,7 +7,7 @@ import Permalink from './permalink';
import IconButton from './icon_button';
import { defineMessages, injectIntl } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { me, show_followed_by, follow_button_to_list_adder } from '../initial_state';
import { me, show_followed_by, follow_button_to_list_adder, disableFollow, disableUnfollow } from '../initial_state';
import RelativeTimestamp from './relative_timestamp';
const messages = defineMessages({
@ -132,9 +132,7 @@ class Account extends ImmutablePureComponent {
subscribing_buttons = (
<IconButton
icon='rss-square'
title={intl.formatMessage(
subscribing ? messages.unsubscribe : messages.subscribe
)}
title={intl.formatMessage(subscribing ? messages.unsubscribe : messages.subscribe)}
onClick={this.handleSubscribe}
active={subscribing}
no_delivery={subscribing && !subscribing_home}
@ -144,10 +142,9 @@ class Account extends ImmutablePureComponent {
if (!account.get('moved') || following) {
following_buttons = (
<IconButton
disabled={following ? disableUnfollow : disableFollow}
icon={following ? 'user-times' : 'user-plus'}
title={intl.formatMessage(
following ? messages.unfollow : messages.follow
)}
title={intl.formatMessage(following ? messages.unfollow : messages.follow)}
onClick={this.handleFollow}
active={following}
passive={followed_by}

View file

@ -10,6 +10,8 @@ import {
show_subscribe_button_on_timeline,
show_followed_by,
follow_button_to_list_adder,
disableFollow,
disableUnfollow,
} from '../initial_state';
const messages = defineMessages({
@ -75,10 +77,10 @@ class AccountActionBar extends ImmutablePureComponent {
if (requested) {
following_buttons = <IconButton disabled icon='hourglass' title={intl.formatMessage(messages.requested)} active={followed_by} />;
} else {
following_buttons = <IconButton icon={following ? 'user-times' : 'user-plus'} title={intl.formatMessage(following ? messages.unfollow : messages.follow)} onClick={this.handleFollow} active={following} passive={followed_by} no_delivery={following && !delivery} />;
following_buttons = <IconButton disabled={following ? disableUnfollow : disableFollow} icon={following ? 'user-times' : 'user-plus'} title={intl.formatMessage(following ? messages.unfollow : messages.follow)} onClick={this.handleFollow} active={following} passive={followed_by} no_delivery={following && !delivery} />;
}
}
buttons = <span>{subscribing_buttons}{following_buttons}</span>
buttons = <span>{subscribing_buttons}{following_buttons}</span>;
}
return (

View file

@ -9,7 +9,7 @@ import Emoji from './emoji';
import unicodeMapping from 'mastodon/features/emoji/emoji_unicode_mapping_light';
import TransitionMotion from 'react-motion/lib/TransitionMotion';
import AnimatedNumber from 'mastodon/components/animated_number';
import { reduceMotion, me } from 'mastodon/initial_state';
import { reduceMotion, me, disableReactions } from 'mastodon/initial_state';
import spring from 'react-motion/lib/spring';
import Overlay from 'react-overlays/lib/Overlay';
import { isUserTouching } from 'mastodon/is_mobile';
@ -110,7 +110,7 @@ class EmojiReaction extends ImmutablePureComponent {
const { emojiReaction, status, myReaction } = this.props;
if (!emojiReaction) {
return <Fragment></Fragment>;
return <Fragment />;
}
let shortCode = emojiReaction.get('name');
@ -122,7 +122,7 @@ class EmojiReaction extends ImmutablePureComponent {
return (
<Fragment>
<div className='reactions-bar__item-wrapper' ref={this.setTargetRef}>
<button className={classNames('reactions-bar__item', { active: myReaction })} disabled={status.get('emoji_reactioned') && !myReaction} onClick={this.handleClick} title={`:${shortCode}:`} style={this.props.style}>
<button className={classNames('reactions-bar__item', { active: myReaction })} disabled={disableReactions || status.get('emoji_reactioned') && !myReaction} onClick={this.handleClick} title={`:${shortCode}:`} style={this.props.style}>
<span className='reactions-bar__item__emoji'><Emoji hovered={this.state.hovered} emoji={emojiReaction.get('name')} emojiMap={this.props.emojiMap} url={emojiReaction.get('url')} static_url={emojiReaction.get('static_url')} /></span>
<span className='reactions-bar__item__count'><AnimatedNumber value={emojiReaction.get('count')} /></span>
</button>
@ -157,11 +157,11 @@ export default class EmojiReactionsBar extends ImmutablePureComponent {
render () {
const { status } = this.props;
const emoji_reactions = status.get("emoji_reactions");
const emoji_reactions = status.get('emoji_reactions');
const visibleReactions = emoji_reactions.filter(x => x.get('count') > 0);
if (visibleReactions.isEmpty() ) {
return <Fragment></Fragment>;
return <Fragment />;
}
const styles = visibleReactions.map(emoji_reaction => {

View file

@ -6,7 +6,7 @@ import IconButton from './icon_button';
import DropdownMenuContainer from '../containers/dropdown_menu_container';
import { defineMessages, injectIntl } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { me, isStaff, show_bookmark_button, show_quote_button, enableReaction, compactReaction, enableStatusReference, maxReferences, matchVisibilityOfReferences, addReferenceModal } from '../initial_state';
import { me, isStaff, show_bookmark_button, show_quote_button, enableReaction, compactReaction, enableStatusReference, maxReferences, matchVisibilityOfReferences, addReferenceModal, disablePost, disableReactions, disableBlock, disableDomainBlock } from '../initial_state';
import classNames from 'classnames';
import { openModal } from '../actions/modal';
@ -404,11 +404,15 @@ class StatusActionBar extends ImmutablePureComponent {
if (writtenByMe) {
menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick });
menu.push({ text: intl.formatMessage(messages.redraft), action: this.handleRedraftClick });
if (!disablePost) {
menu.push({ text: intl.formatMessage(messages.redraft), action: this.handleRedraftClick });
}
} else {
menu.push({ text: intl.formatMessage(messages.mention, { name: account.get('username') }), action: this.handleMentionClick });
menu.push({ text: intl.formatMessage(messages.direct, { name: account.get('username') }), action: this.handleDirectClick });
menu.push(null);
if (!disablePost) {
menu.push({ text: intl.formatMessage(messages.mention, { name: account.get('username') }), action: this.handleMentionClick });
menu.push({ text: intl.formatMessage(messages.direct, { name: account.get('username') }), action: this.handleDirectClick });
menu.push(null);
}
if (relationship && relationship.get('muting')) {
menu.push({ text: intl.formatMessage(messages.unmute, { name: account.get('username') }), action: this.handleMuteClick });
@ -418,18 +422,18 @@ class StatusActionBar extends ImmutablePureComponent {
if (relationship && relationship.get('blocking')) {
menu.push({ text: intl.formatMessage(messages.unblock, { name: account.get('username') }), action: this.handleBlockClick });
} else {
} else if (!disableBlock) {
menu.push({ text: intl.formatMessage(messages.block, { name: account.get('username') }), action: this.handleBlockClick });
}
menu.push({ text: intl.formatMessage(messages.report, { name: account.get('username') }), action: this.handleReport });
if (domain) {
menu.push(null);
if (relationship && relationship.get('domain_blocking')) {
menu.push(null);
menu.push({ text: intl.formatMessage(messages.unblockDomain, { domain }), action: this.handleUnblockDomain });
} else {
} else if (!disableDomainBlock) {
menu.push(null);
menu.push({ text: intl.formatMessage(messages.blockDomain, { domain }), action: this.handleBlockDomain });
}
}
@ -474,22 +478,21 @@ class StatusActionBar extends ImmutablePureComponent {
return (
<div className='status__action-bar'>
<IconButton className='status__action-bar-button' disabled={expired} title={replyTitle} icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'reply' : replyIcon} onClick={this.handleReplyClick} counter={status.get('replies_count')} obfuscateCount />
{enableStatusReference && me && <IconButton className={classNames('status__action-bar-button link-icon', {referenced, 'context-referenced': contextReferenced})} animate disabled={referenceDisabled} active={referenced} pressed={referenced} title={intl.formatMessage(messages.reference)} icon='link' onClick={this.handleReferenceClick} />}
<IconButton className={classNames('status__action-bar-button', { reblogPrivate })} disabled={!publicStatus && !reblogPrivate || expired} active={reblogged} pressed={reblogged} title={reblogTitle} icon='retweet' onClick={this.handleReblogClick} />
<IconButton className='status__action-bar-button star-icon' animate disabled={!favourited && expired} active={favourited} pressed={favourited} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} />
{show_quote_button && <IconButton className='status__action-bar-button' disabled={anonymousAccess || !publicStatus || expired} title={!publicStatus ? intl.formatMessage(messages.cannot_quote) : intl.formatMessage(messages.quote)} icon='quote-right' onClick={this.handleQuoteClick} />}
<IconButton className='status__action-bar-button' disabled={disablePost || expired} title={replyTitle} icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'reply' : replyIcon} onClick={this.handleReplyClick} counter={status.get('replies_count')} obfuscateCount />
{enableStatusReference && me && <IconButton className={classNames('status__action-bar-button link-icon', {referenced, 'context-referenced': contextReferenced})} animate disabled={disablePost || referenceDisabled} active={referenced} pressed={referenced} title={intl.formatMessage(messages.reference)} icon='link' onClick={this.handleReferenceClick} />}
<IconButton className={classNames('status__action-bar-button', { reblogPrivate })} disabled={disableReactions || !publicStatus && !reblogPrivate || expired} active={reblogged} pressed={reblogged} title={reblogTitle} icon='retweet' onClick={this.handleReblogClick} />
<IconButton className='status__action-bar-button star-icon' animate disabled={disableReactions || !favourited && expired} active={favourited} pressed={favourited} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} />
{show_quote_button && <IconButton className='status__action-bar-button' disabled={disablePost || anonymousAccess || !publicStatus || expired} title={!publicStatus ? intl.formatMessage(messages.cannot_quote) : intl.formatMessage(messages.quote)} icon='quote-right' onClick={this.handleQuoteClick} />}
{shareButton}
{show_bookmark_button && <IconButton className='status__action-bar-button bookmark-icon' disabled={!bookmarked && expired} active={bookmarked} pressed={bookmarked} title={intl.formatMessage(bookmarked ? messages.removeBookmark : messages.bookmark)} icon='bookmark' onClick={this.handleBookmarkClick} />}
{enableReaction && <div className={classNames('status__action-bar-dropdown', {'icon-button--with-counter': reactionsCounter})}>
{enableReaction && <div className={classNames('status__action-bar-dropdown', { 'icon-button--with-counter': reactionsCounter })}>
<ReactionPickerDropdownContainer
scrollKey={scrollKey}
disabled={expired}
disabled={disableReactions || expired || anonymousAccess}
active={emoji_reactioned}
pressed={emoji_reactioned}
className='status__action-bar-button'
disabled={anonymousAccess}
status={status}
title={intl.formatMessage(messages.emoji_reaction)}
icon='smile-o'

View file

@ -6,7 +6,7 @@ import Permalink from './permalink';
import classnames from 'classnames';
import PollContainer from 'mastodon/containers/poll_container';
import Icon from 'mastodon/components/icon';
import { autoPlayGif } from 'mastodon/initial_state';
import { autoPlayGif, disableReactions } from 'mastodon/initial_state';
const messages = defineMessages({
linkToAcct: { id: 'status.link_to_acct', defaultMessage: 'Link to @{acct}' },
@ -268,7 +268,7 @@ export default class StatusContent extends React.PureComponent {
);
const pollContainer = (
<PollContainer pollId={status.get('poll')} />
<PollContainer pollId={status.get('poll')} disabled={disableReactions} />
);
if (status.get('spoiler_text').length > 0) {

View file

@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
import { defineMessages, injectIntl, FormattedMessage, FormattedDate } from 'react-intl';
import Button from 'mastodon/components/button';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { autoPlayGif, me, isStaff, show_followed_by, follow_button_to_list_adder } from 'mastodon/initial_state';
import { autoPlayGif, me, isStaff, show_followed_by, follow_button_to_list_adder, disablePost, disableBlock, disableDomainBlock, disableFollow, disableUnfollow } from 'mastodon/initial_state';
import classNames from 'classnames';
import Icon from 'mastodon/components/icon';
import IconButton from 'mastodon/components/icon_button';
@ -191,7 +191,11 @@ class Header extends ImmutablePureComponent {
} else if (account.getIn(['relationship', 'requested'])) {
actionBtn = <Button className={classNames('logo-button', { 'button--with-bell': bellBtn !== '' })} text={intl.formatMessage(messages.cancel_follow_request)} title={intl.formatMessage(messages.requested)} onClick={this.props.onFollow} />;
} else if (!account.getIn(['relationship', 'blocking'])) {
actionBtn = <Button disabled={account.getIn(['relationship', 'blocked_by'])} className={classNames('logo-button', { 'button--destructive': account.getIn(['relationship', 'following']), 'button--with-bell': bellBtn !== '' })} text={intl.formatMessage(account.getIn(['relationship', 'following']) ? messages.unfollow : messages.follow)} onClick={this.props.onFollow} />;
if (account.getIn(['relationship', 'following'])) {
actionBtn = <Button disabled={disableUnfollow || account.getIn(['relationship', 'blocked_by'])} className={classNames('logo-button', { 'button--destructive': !disableUnfollow, 'button--with-bell': bellBtn !== '' })} text={intl.formatMessage(messages.unfollow)} onClick={this.props.onFollow} />;
} else {
actionBtn = <Button disabled={disableFollow || account.getIn(['relationship', 'blocked_by'])} className={classNames('logo-button', { 'button--with-bell': bellBtn !== '' })} text={intl.formatMessage(messages.follow)} onClick={this.props.onFollow} />;
}
} else if (account.getIn(['relationship', 'blocking'])) {
actionBtn = <Button className='logo-button' text={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.props.onBlock} />;
}
@ -208,9 +212,11 @@ class Header extends ImmutablePureComponent {
}
if (account.get('id') !== me) {
menu.push({ text: intl.formatMessage(messages.mention, { name: account.get('username') }), action: this.props.onMention });
menu.push({ text: intl.formatMessage(messages.direct, { name: account.get('username') }), action: this.props.onDirect });
menu.push(null);
if (!disablePost) {
menu.push({ text: intl.formatMessage(messages.mention, { name: account.get('username') }), action: this.props.onMention });
menu.push({ text: intl.formatMessage(messages.direct, { name: account.get('username') }), action: this.props.onDirect });
menu.push(null);
}
menu.push({ text: intl.formatMessage(messages.conversations, { name: account.get('username') }), action: this.props.onConversations });
} else {
@ -265,7 +271,7 @@ class Header extends ImmutablePureComponent {
if (account.getIn(['relationship', 'blocking'])) {
menu.push({ text: intl.formatMessage(messages.unblock, { name: account.get('username') }), action: this.props.onBlock });
} else {
} else if (!disableBlock) {
menu.push({ text: intl.formatMessage(messages.block, { name: account.get('username') }), action: this.props.onBlock });
}
@ -278,8 +284,10 @@ class Header extends ImmutablePureComponent {
menu.push(null);
if (account.getIn(['relationship', 'domain_blocking'])) {
menu.push(null);
menu.push({ text: intl.formatMessage(messages.unblockDomain, { domain }), action: this.props.onUnblockDomain });
} else {
} else if (!disableDomainBlock) {
menu.push(null);
menu.push({ text: intl.formatMessage(messages.blockDomain, { domain }), action: this.props.onBlockDomain });
}
}
@ -318,9 +326,7 @@ class Header extends ImmutablePureComponent {
subscribing_buttons = (
<IconButton
icon='rss-square'
title={intl.formatMessage(
subscribing ? messages.unsubscribe : messages.subscribe
)}
title={intl.formatMessage(subscribing ? messages.unsubscribe : messages.subscribe)}
onClick={this.handleSubscribe}
active={subscribing}
no_delivery={subscribing && !subscribing_home}
@ -330,10 +336,9 @@ class Header extends ImmutablePureComponent {
if(!account.get('moved') || following) {
following_buttons = (
<IconButton
disabled={following ? disableUnfollow : disableFollow}
icon={following ? 'user-times' : 'user-plus'}
title={intl.formatMessage(
following ? messages.unfollow : messages.follow
)}
title={intl.formatMessage(following ? messages.unfollow : messages.follow)}
onClick={this.handleFollow}
active={following}
passive={followed_by}

View file

@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import Button from 'mastodon/components/button';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { autoPlayGif, me, isStaff, show_followed_by, follow_button_to_list_adder } from 'mastodon/initial_state';
import { autoPlayGif, me, isStaff, show_followed_by, follow_button_to_list_adder, disablePost, disableBlock, disableDomainBlock, disableFollow, disableUnfollow } from 'mastodon/initial_state';
import classNames from 'classnames';
import Icon from 'mastodon/components/icon';
import IconButton from 'mastodon/components/icon_button';
@ -22,6 +22,7 @@ const messages = defineMessages({
edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' },
account_locked: { id: 'account.locked_info', defaultMessage: 'This account privacy status is set to locked. The owner manually reviews who can follow them.' },
conversations: { id: 'account.conversations', defaultMessage: 'Show conversations with @{name}' },
conversations_all: { id: 'account.conversations_all', defaultMessage: 'Show all conversations' },
mention: { id: 'account.mention', defaultMessage: 'Mention @{name}' },
direct: { id: 'account.direct', defaultMessage: 'Direct message @{name}' },
unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' },
@ -172,7 +173,11 @@ class HeaderCommon extends ImmutablePureComponent {
} else if (account.getIn(['relationship', 'requested'])) {
actionBtn = <Button className={classNames('logo-button', { 'button--with-bell': bellBtn !== '' })} text={intl.formatMessage(messages.cancel_follow_request)} title={intl.formatMessage(messages.requested)} onClick={this.props.onFollow} />;
} else if (!account.getIn(['relationship', 'blocking'])) {
actionBtn = <Button disabled={account.getIn(['relationship', 'blocked_by'])} className={classNames('logo-button', { 'button--destructive': account.getIn(['relationship', 'following']), 'button--with-bell': bellBtn !== '' })} text={intl.formatMessage(account.getIn(['relationship', 'following']) ? messages.unfollow : messages.follow)} onClick={this.props.onFollow} />;
if (account.getIn(['relationship', 'following'])) {
actionBtn = <Button disabled={disableUnfollow || account.getIn(['relationship', 'blocked_by'])} className={classNames('logo-button', { 'button--destructive': !disableUnfollow, 'button--with-bell': bellBtn !== '' })} text={intl.formatMessage(messages.unfollow)} onClick={this.props.onFollow} />;
} else {
actionBtn = <Button disabled={disableFollow || account.getIn(['relationship', 'blocked_by'])} className={classNames('logo-button', { 'button--with-bell': bellBtn !== '' })} text={intl.formatMessage(messages.follow)} onClick={this.props.onFollow} />;
}
} else if (account.getIn(['relationship', 'blocking'])) {
actionBtn = <Button className='logo-button' text={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.props.onBlock} />;
}
@ -189,12 +194,16 @@ class HeaderCommon extends ImmutablePureComponent {
}
if (account.get('id') !== me) {
menu.push({ text: intl.formatMessage(messages.mention, { name: account.get('username') }), action: this.props.onMention });
menu.push({ text: intl.formatMessage(messages.direct, { name: account.get('username') }), action: this.props.onDirect });
menu.push(null);
}
if (!disablePost) {
menu.push({ text: intl.formatMessage(messages.mention, { name: account.get('username') }), action: this.props.onMention });
menu.push({ text: intl.formatMessage(messages.direct, { name: account.get('username') }), action: this.props.onDirect });
menu.push(null);
}
menu.push({ text: intl.formatMessage(messages.conversations, { name: account.get('username') }), action: this.props.onConversations });
menu.push({ text: intl.formatMessage(messages.conversations, { name: account.get('username') }), action: this.props.onConversations });
} else {
menu.push({ text: intl.formatMessage(messages.conversations_all), action: this.props.onConversations });
}
menu.push(null);
if ('share' in navigator) {
@ -244,7 +253,7 @@ class HeaderCommon extends ImmutablePureComponent {
if (account.getIn(['relationship', 'blocking'])) {
menu.push({ text: intl.formatMessage(messages.unblock, { name: account.get('username') }), action: this.props.onBlock });
} else {
} else if (!disableBlock) {
menu.push({ text: intl.formatMessage(messages.block, { name: account.get('username') }), action: this.props.onBlock });
}
@ -254,11 +263,11 @@ class HeaderCommon extends ImmutablePureComponent {
if (account.get('acct') !== account.get('username')) {
const domain = account.get('acct').split('@')[1];
menu.push(null);
if (account.getIn(['relationship', 'domain_blocking'])) {
menu.push(null);
menu.push({ text: intl.formatMessage(messages.unblockDomain, { domain }), action: this.props.onUnblockDomain });
} else {
} else if (!disableDomainBlock) {
menu.push(null);
menu.push({ text: intl.formatMessage(messages.blockDomain, { domain }), action: this.props.onBlockDomain });
}
}
@ -295,9 +304,7 @@ class HeaderCommon extends ImmutablePureComponent {
subscribing_buttons = (
<IconButton
icon='rss-square'
title={intl.formatMessage(
subscribing ? messages.unsubscribe : messages.subscribe
)}
title={intl.formatMessage(subscribing ? messages.unsubscribe : messages.subscribe)}
onClick={this.handleSubscribe}
active={subscribing}
no_delivery={subscribing && !subscribing_home}
@ -307,10 +314,9 @@ class HeaderCommon extends ImmutablePureComponent {
if(!account.get('moved') || following) {
following_buttons = (
<IconButton
disabled={following ? disableUnfollow : disableFollow}
icon={following ? 'user-times' : 'user-plus'}
title={intl.formatMessage(
following ? messages.unfollow : messages.follow
)}
title={intl.formatMessage(following ? messages.unfollow : messages.follow)}
onClick={this.handleFollow}
active={following}
passive={followed_by}

View file

@ -26,6 +26,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
import { length } from 'stringz';
import { countableText } from '../util/counter';
import Icon from 'mastodon/components/icon';
import { disablePost } from '../../../initial_state';
const allowedAroundShortCode = '><\u0085\u0020\u00a0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029\u0009\u000a\u000b\u000c\u000d';
@ -57,6 +58,8 @@ class ComposeForm extends ImmutablePureComponent {
isChangingUpload: PropTypes.bool,
isUploading: PropTypes.bool,
isCircleUnselected: PropTypes.bool,
prohibitedVisibilities: ImmutablePropTypes.set,
prohibitedWords: ImmutablePropTypes.set,
onChange: PropTypes.func.isRequired,
onSubmit: PropTypes.func.isRequired,
onClearSuggestions: PropTypes.func.isRequired,
@ -89,11 +92,13 @@ class ComposeForm extends ImmutablePureComponent {
}
canSubmit = () => {
const { isSubmitting, isChangingUpload, isUploading, isCircleUnselected, anyMedia } = this.props;
const { isSubmitting, isChangingUpload, isUploading, isCircleUnselected, anyMedia, prohibitedVisibilities, privacy, prohibitedWords, text, spoilerText } = this.props;
const fulltext = this.getFulltextForCharacterCounting();
const isOnlyWhitespace = fulltext.length !== 0 && fulltext.trim().length === 0;
const noVisibility = prohibitedVisibilities?.includes(privacy);
const ngWords = prohibitedWords.some( word => text.includes(word) || spoilerText?.includes(word) );
return !(isSubmitting || isUploading || isChangingUpload || isCircleUnselected || length(fulltext) > 500 || (isOnlyWhitespace && !anyMedia));
return !(isSubmitting || isUploading || isChangingUpload || isCircleUnselected || length(fulltext) > 500 || (isOnlyWhitespace && !anyMedia) || noVisibility || ngWords);
}
handleSubmit = () => {
@ -274,7 +279,7 @@ class ComposeForm extends ImmutablePureComponent {
<CircleDropdownContainer />
<div className='compose-form__publish'>
<div className='compose-form__publish-button-wrapper'><Button text={publishText} onClick={this.handleSubmit} disabled={!this.canSubmit()} block /></div>
<div className='compose-form__publish-button-wrapper'><Button text={publishText} onClick={this.handleSubmit} disabled={disablePost || !this.canSubmit()} block /></div>
</div>
<ReferenceStack />

View file

@ -1,5 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { injectIntl, defineMessages } from 'react-intl';
import IconButton from '../../../components/icon_button';
import Overlay from 'react-overlays/lib/Overlay';
@ -22,6 +23,8 @@ const messages = defineMessages({
direct_long: { id: 'privacy.direct.long', defaultMessage: 'Visible for mentioned users only' },
limited_short: { id: 'privacy.limited.short', defaultMessage: 'Circle' },
limited_long: { id: 'privacy.limited.long', defaultMessage: 'Visible for circle users only' },
none_short: { id: 'privacy.none.short', defaultMessage: 'None' },
none_long: { id: 'privacy.none.long', defaultMessage: 'No visibility allowed' },
change_privacy: { id: 'privacy.change', defaultMessage: 'Adjust status privacy' },
});
@ -160,6 +163,7 @@ class PrivacyDropdown extends React.PureComponent {
onModalOpen: PropTypes.func,
onModalClose: PropTypes.func,
value: PropTypes.string.isRequired,
prohibitedVisibilities: ImmutablePropTypes.set,
onChange: PropTypes.func.isRequired,
noDirect: PropTypes.bool,
container: PropTypes.func,
@ -235,28 +239,25 @@ class PrivacyDropdown extends React.PureComponent {
}
componentWillMount () {
const { intl: { formatMessage } } = this.props;
const { intl: { formatMessage }, prohibitedVisibilities } = this.props;
this.options = [
{ icon: 'globe', value: 'public', text: formatMessage(messages.public_short), meta: formatMessage(messages.public_long) },
{ icon: 'unlock', value: 'unlisted', text: formatMessage(messages.unlisted_short), meta: formatMessage(messages.unlisted_long) },
{ icon: 'lock', value: 'private', text: formatMessage(messages.private_short), meta: formatMessage(messages.private_long) },
{ icon: 'exchange', value: 'mutual', text: formatMessage(messages.mutual_short), meta: formatMessage(messages.mutual_long) },
];
if (!this.props.noDirect) {
this.options.push(
...!this.props.noDirect && [
{ icon: 'user-circle', value: 'limited', text: formatMessage(messages.limited_short), meta: formatMessage(messages.limited_long) },
{ icon: 'envelope', value: 'direct', text: formatMessage(messages.direct_short), meta: formatMessage(messages.direct_long) },
);
}
],
].filter(option => !prohibitedVisibilities?.includes(option.value));
}
render () {
const { value, container, intl } = this.props;
const { open, placement } = this.state;
const valueOption = this.options.find(item => item.value === value);
const valueOption = this.options.find(item => item.value === value) || { icon: 'ban', value: 'none', text: intl.formatMessage(messages.none_short), meta: intl.formatMessage(messages.none_long) };
return (
<div className={classNames('privacy-dropdown', placement, { active: open })} onKeyDown={this.handleKeyDown}>

View file

@ -1,5 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { injectIntl, defineMessages } from 'react-intl';
import IconButton from '../../../components/icon_button';
import Overlay from 'react-overlays/lib/Overlay';
@ -155,6 +156,7 @@ class SearchabilityDropdown extends React.PureComponent {
onModalOpen: PropTypes.func,
onModalClose: PropTypes.func,
value: PropTypes.string.isRequired,
prohibitedVisibilities: ImmutablePropTypes.set,
onChange: PropTypes.func.isRequired,
noDirect: PropTypes.bool,
container: PropTypes.func,
@ -230,13 +232,13 @@ class SearchabilityDropdown extends React.PureComponent {
}
componentWillMount () {
const { intl: { formatMessage } } = this.props;
const { intl: { formatMessage }, prohibitedVisibilities } = this.props;
this.options = [
{ icon: 'globe', value: 'public', text: formatMessage(messages.public_short), meta: formatMessage(messages.public_long) },
{ icon: 'lock', value: 'private', text: formatMessage(messages.private_short), meta: formatMessage(messages.private_long) },
{ icon: 'at', value: 'direct', text: formatMessage(messages.direct_short), meta: formatMessage(messages.direct_long) },
];
].filter(option => !prohibitedVisibilities?.includes(option.value));
}
render () {

View file

@ -27,6 +27,8 @@ const mapStateToProps = state => ({
isCircleUnselected: state.getIn(['compose', 'privacy']) === 'limited' && state.getIn(['compose', 'reply_status', 'visibility']) !== 'limited' && !state.getIn(['compose', 'circle_id']),
showSearch: state.getIn(['search', 'submitted']) && !state.getIn(['search', 'hidden']),
anyMedia: state.getIn(['compose', 'media_attachments']).size > 0,
prohibitedVisibilities: state.getIn(['compose', 'prohibited_visibilities']),
prohibitedWords: state.getIn(['compose', 'prohibited_words']),
});
const mapDispatchToProps = (dispatch, { intl }) => ({

View file

@ -6,6 +6,7 @@ import { isUserTouching } from '../../../is_mobile';
const mapStateToProps = state => ({
value: state.getIn(['compose', 'privacy']),
prohibitedVisibilities: state.getIn(['compose', 'prohibited_visibilities']),
});
const mapDispatchToProps = dispatch => ({

View file

@ -6,6 +6,7 @@ import { isUserTouching } from '../../../is_mobile';
const mapStateToProps = state => ({
value: state.getIn(['compose', 'searchability']),
prohibitedVisibilities: state.getIn(['compose', 'prohibited_visibilities']),
});
const mapDispatchToProps = dispatch => ({

View file

@ -17,6 +17,8 @@ import {
unsubscribeModal,
show_followed_by,
follow_button_to_list_adder,
disableFollow,
disableUnfollow,
} from 'mastodon/initial_state';
import ShortNumber from 'mastodon/components/short_number';
import {
@ -245,9 +247,7 @@ class AccountCard extends ImmutablePureComponent {
subscribing_buttons = (
<IconButton
icon='rss-square'
title={intl.formatMessage(
subscribing ? messages.unsubscribe : messages.subscribe
)}
title={intl.formatMessage(subscribing ? messages.unsubscribe : messages.subscribe)}
onClick={this.handleSubscribe}
active={subscribing}
no_delivery={subscribing && !subscribing_home}
@ -257,10 +257,9 @@ class AccountCard extends ImmutablePureComponent {
if(!account.get('moved') || following) {
following_buttons = (
<IconButton
disabled={following ? disableUnfollow : disableFollow}
icon={following ? 'user-times' : 'user-plus'}
title={intl.formatMessage(
following ? messages.unfollow : messages.follow
)}
title={intl.formatMessage(following ? messages.unfollow : messages.follow)}
onClick={this.handleFollow}
active={following}
passive={followed_by}

View file

@ -17,6 +17,8 @@ import {
unsubscribeModal,
show_followed_by,
follow_button_to_list_adder,
disableFollow,
disableUnfollow,
} from 'mastodon/initial_state';
import ShortNumber from 'mastodon/components/short_number';
import {
@ -260,9 +262,7 @@ class AccountCard extends ImmutablePureComponent {
subscribing_buttons = (
<IconButton
icon='rss-square'
title={intl.formatMessage(
subscribing ? messages.unsubscribe : messages.subscribe
)}
title={intl.formatMessage(subscribing ? messages.unsubscribe : messages.subscribe)}
onClick={this.handleSubscribe}
active={subscribing}
no_delivery={subscribing && !subscribing_home}
@ -272,10 +272,9 @@ class AccountCard extends ImmutablePureComponent {
if(!account.get('moved') || following) {
following_buttons = (
<IconButton
disabled={following ? disableUnfollow : disableFollow}
icon={following ? 'user-times' : 'user-plus'}
title={intl.formatMessage(
following ? messages.unfollow : messages.follow
)}
title={intl.formatMessage(following ? messages.unfollow : messages.follow)}
onClick={this.handleFollow}
active={following}
passive={followed_by}

View file

@ -16,6 +16,8 @@ import {
unsubscribeModal,
show_followed_by,
follow_button_to_list_adder,
disableFollow,
disableUnfollow,
} from 'mastodon/initial_state';
import ShortNumber from 'mastodon/components/short_number';
import {
@ -260,9 +262,7 @@ class GroupDetail extends ImmutablePureComponent {
subscribing_buttons = (
<IconButton
icon='rss-square'
title={intl.formatMessage(
subscribing ? messages.unsubscribe : messages.subscribe
)}
title={intl.formatMessage(subscribing ? messages.unsubscribe : messages.subscribe)}
onClick={this.handleSubscribe}
active={subscribing}
no_delivery={subscribing && !subscribing_home}
@ -272,10 +272,9 @@ class GroupDetail extends ImmutablePureComponent {
if(!account.get('moved') || following) {
following_buttons = (
<IconButton
disabled={following ? disableUnfollow : disableFollow}
icon={following ? 'user-times' : 'user-plus'}
title={intl.formatMessage(
following ? messages.unfollow : messages.follow
)}
title={intl.formatMessage(following ? messages.unfollow : messages.follow)}
onClick={this.handleFollow}
active={following}
passive={followed_by}

View file

@ -7,7 +7,7 @@ import Avatar from '../../../components/avatar';
import DisplayName from '../../../components/display_name';
import IconButton from '../../../components/icon_button';
import { unfollowAccount, followAccount } from '../../../actions/accounts';
import { me, show_followed_by, unfollowModal } from '../../../initial_state';
import { me, show_followed_by, unfollowModal, disableFollow, disableUnfollow } from '../../../initial_state';
import { openModal } from '../../../actions/modal';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
@ -18,7 +18,7 @@ const messages = defineMessages({
unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' },
});
const MapStateToProps = (state) => ({
const MapStateToProps = () => ({
});
const mapDispatchToProps = (dispatch, { intl }) => ({
@ -68,7 +68,7 @@ class Account extends ImmutablePureComponent {
if (requested) {
buttons = <IconButton icon='hourglass' title={intl.formatMessage(messages.requested)} active={followed_by} onClick={this.handleFollow} />;
} else {
buttons = <IconButton icon={following ? 'user-times' : 'user-plus'} title={intl.formatMessage(following ? messages.unfollow : messages.follow)} onClick={this.handleFollow} active={following} passive={followed_by} no_delivery={following && !delivery} />;
buttons = <IconButton disabled={following ? disableUnfollow : disableFollow} icon={following ? 'user-times' : 'user-plus'} title={intl.formatMessage(following ? messages.unfollow : messages.follow)} onClick={this.handleFollow} active={following} passive={followed_by} no_delivery={following && !delivery} />;
}
}
}

View file

@ -3,7 +3,6 @@ import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { connect } from 'react-redux';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { injectIntl } from 'react-intl';
import { setupListAdder, resetListAdder } from '../../actions/lists';
import { createSelector } from 'reselect';
import { makeGetAccount } from '../../selectors';
@ -34,7 +33,6 @@ const mapDispatchToProps = dispatch => ({
});
export default @connect(mapStateToProps, mapDispatchToProps)
@injectIntl
class ListAdder extends ImmutablePureComponent {
static propTypes = {
@ -57,21 +55,21 @@ class ListAdder extends ImmutablePureComponent {
}
render () {
const { account, listIds, intl } = this.props;
const { account, listIds } = this.props;
const following = account.getIn(['relationship', 'following']);
return (
<div className='modal-root__modal list-adder'>
<div className='list-adder__account'>
<Account account={account} intl={intl} />
<Account account={account} />
</div>
<NewListForm />
<div className='list-adder__lists'>
<Home account={account} disabled={following} intl={intl} />
{listIds.map(ListId => <List key={ListId} account={account} listId={ListId} disabled={following} intl={intl} />)}
<Home account={account} disabled={following} />
{listIds.map(ListId => <List key={ListId} account={account} listId={ListId} disabled={following} />)}
</div>
</div>
);

View file

@ -5,7 +5,7 @@ import { FormattedMessage } from 'react-intl';
import ClearColumnButton from './clear_column_button';
import GrantPermissionButton from './grant_permission_button';
import SettingToggle from './setting_toggle';
import { enableReaction, enableStatusReference } from 'mastodon/initial_state';
import { enableReaction, enableStatusReference, disableClearAllNotifications } from 'mastodon/initial_state';
export default class ColumnSettings extends React.PureComponent {
@ -52,9 +52,9 @@ export default class ColumnSettings extends React.PureComponent {
</div>
)}
<div className='column-settings__row'>
{!disableClearAllNotifications && <div className='column-settings__row'>
<ClearColumnButton onClick={onClear} />
</div>
</div>}
<div role='group' aria-labelledby='notifications-unread-markers'>
<span id='notifications-unread-markers' className='column-settings__section'>

View file

@ -5,7 +5,7 @@ import IconButton from '../../../components/icon_button';
import ImmutablePropTypes from 'react-immutable-proptypes';
import DropdownMenuContainer from '../../../containers/dropdown_menu_container';
import { defineMessages, injectIntl } from 'react-intl';
import { me, isStaff, show_quote_button, enableReaction, enableStatusReference, maxReferences, matchVisibilityOfReferences, addReferenceModal } from '../../../initial_state';
import { me, isStaff, show_quote_button, enableReaction, enableStatusReference, maxReferences, matchVisibilityOfReferences, addReferenceModal, disablePost, disableReactions, disableBlock, disableDomainBlock } from '../../../initial_state';
import classNames from 'classnames';
import ReactionPickerDropdownContainer from 'mastodon/containers/reaction_picker_dropdown_container';
import { openModal } from '../../../actions/modal';
@ -295,11 +295,11 @@ class ActionBar extends React.PureComponent {
const reblogsCount = status.get('reblogs_count');
const referredByCount = status.get('status_referred_by_count');
const favouritesCount = status.get('favourites_count');
const [ _, domain ] = account.get('acct').split('@');
const [ , domain ] = account.get('acct').split('@');
const expires_at = status.get('expires_at')
const expires_date = expires_at && new Date(expires_at)
const expired = expires_date && expires_date.getTime() < intl.now()
const expires_at = status.get('expires_at');
const expires_date = expires_at && new Date(expires_at);
const expired = expires_date && expires_date.getTime() < intl.now();
let menu = [];
@ -350,12 +350,18 @@ class ActionBar extends React.PureComponent {
menu.push({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick });
menu.push(null);
}
menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick });
menu.push({ text: intl.formatMessage(messages.redraft), action: this.handleRedraftClick });
if (!disablePost) {
menu.push({ text: intl.formatMessage(messages.redraft), action: this.handleRedraftClick });
}
} else {
menu.push({ text: intl.formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }), action: this.handleMentionClick });
menu.push({ text: intl.formatMessage(messages.direct, { name: status.getIn(['account', 'username']) }), action: this.handleDirectClick });
menu.push(null);
if (!disablePost) {
menu.push({ text: intl.formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }), action: this.handleMentionClick });
menu.push({ text: intl.formatMessage(messages.direct, { name: status.getIn(['account', 'username']) }), action: this.handleDirectClick });
menu.push(null);
}
if (relationship && relationship.get('muting')) {
menu.push({ text: intl.formatMessage(messages.unmute, { name: account.get('username') }), action: this.handleMuteClick });
@ -365,18 +371,18 @@ class ActionBar extends React.PureComponent {
if (relationship && relationship.get('blocking')) {
menu.push({ text: intl.formatMessage(messages.unblock, { name: account.get('username') }), action: this.handleBlockClick });
} else {
} else if (!disableBlock) {
menu.push({ text: intl.formatMessage(messages.block, { name: account.get('username') }), action: this.handleBlockClick });
}
menu.push({ text: intl.formatMessage(messages.report, { name: status.getIn(['account', 'username']) }), action: this.handleReport });
if (domain) {
menu.push(null);
if (relationship && relationship.get('domain_blocking')) {
menu.push(null);
menu.push({ text: intl.formatMessage(messages.unblockDomain, { domain }), action: this.handleUnblockDomain });
} else {
} else if (!disableDomainBlock) {
menu.push(null);
menu.push({ text: intl.formatMessage(messages.blockDomain, { domain }), action: this.handleBlockDomain });
}
}
@ -416,17 +422,17 @@ class ActionBar extends React.PureComponent {
return (
<div className='detailed-status__action-bar'>
<div className='detailed-status__button'><IconButton disabled={expired} title={intl.formatMessage(messages.reply)} icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'reply' : replyIcon} onClick={this.handleReplyClick} /></div>
{enableStatusReference && me && <div className='detailed-status__button'><IconButton className={classNames('link-icon', {referenced, 'context-referenced': contextReferenced})} animate disabled={referenceDisabled} active={referenced} pressed={referenced} title={intl.formatMessage(messages.reference)} icon='link' onClick={this.handleReferenceClick} /></div>}
<div className='detailed-status__button'><IconButton className={classNames({ reblogPrivate })} disabled={!publicStatus && !reblogPrivate || expired} active={reblogged} title={reblogTitle} icon='retweet' onClick={this.handleReblogClick} /></div>
<div className='detailed-status__button'><IconButton className='star-icon' animate active={favourited} disabled={!favourited && expired} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} /></div>
{show_quote_button && <div className='detailed-status__button'><IconButton disabled={!publicStatus || expired} title={!publicStatus ? intl.formatMessage(messages.cannot_quote) : intl.formatMessage(messages.quote)} icon='quote-right' onClick={this.handleQuoteClick} /></div>}
<div className='detailed-status__button'><IconButton disabled={disablePost || expired} title={intl.formatMessage(messages.reply)} icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'reply' : replyIcon} onClick={this.handleReplyClick} /></div>
{enableStatusReference && me && <div className='detailed-status__button'><IconButton className={classNames('link-icon', {referenced, 'context-referenced': contextReferenced})} animate disabled={disablePost || referenceDisabled} active={referenced} pressed={referenced} title={intl.formatMessage(messages.reference)} icon='link' onClick={this.handleReferenceClick} /></div>}
<div className='detailed-status__button'><IconButton className={classNames({ reblogPrivate })} disabled={disableReactions || !publicStatus && !reblogPrivate || expired} active={reblogged} title={reblogTitle} icon='retweet' onClick={this.handleReblogClick} /></div>
<div className='detailed-status__button'><IconButton className='star-icon' animate active={favourited} disabled={disableReactions || !favourited && expired} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} /></div>
{show_quote_button && <div className='detailed-status__button'><IconButton disabled={disablePost || !publicStatus || expired} title={!publicStatus ? intl.formatMessage(messages.cannot_quote) : intl.formatMessage(messages.quote)} icon='quote-right' onClick={this.handleQuoteClick} /></div>}
{shareButton}
<div className='detailed-status__button'><IconButton className='bookmark-icon' active={bookmarked} disabled={!bookmarked && expired} title={intl.formatMessage(messages.bookmark)} icon='bookmark' onClick={this.handleBookmarkClick} /></div>
{enableReaction && <div className='detailed-status__action-bar-dropdown'>
<ReactionPickerDropdownContainer
disabled={expired}
disabled={disableReactions || expired}
active={emoji_reactioned}
pressed={emoji_reactioned}
className='status__action-bar-button'

View file

@ -29,6 +29,7 @@ const messages = defineMessages({
const mapStateToProps = state => {
return {
privacy: state.getIn(['boosts', 'new', 'privacy']),
prohibitedVisibilities: state.getIn(['compose', 'prohibited_visibilities']),
};
};
@ -40,7 +41,7 @@ const mapDispatchToProps = dispatch => {
};
};
export default @connect(mapStateToProps, mapDispatchToProps)
export default @connect(mapStateToProps, mapDispatchToProps, null, { forwardRef: true })
@injectIntl
class BoostModal extends ImmutablePureComponent {
@ -88,7 +89,7 @@ class BoostModal extends ImmutablePureComponent {
}
render () {
const { status, privacy, intl } = this.props;
const { status, privacy, prohibitedVisibilities, intl } = this.props;
const buttonText = status.get('reblogged') ? messages.cancel_reblog : messages.reblog;
const visibilityIconInfo = {
@ -138,11 +139,12 @@ class BoostModal extends ImmutablePureComponent {
<PrivacyDropdown
noDirect
value={privacy}
prohibitedVisibilities={prohibitedVisibilities}
container={this._findContainer}
onChange={this.props.onChangeBoostPrivacy}
/>
)}
<Button text={intl.formatMessage(buttonText)} onClick={this.handleReblog} ref={this.setRef} />
<Button disabled={prohibitedVisibilities?.includes(privacy)} text={intl.formatMessage(buttonText)} onClick={this.handleReblog} ref={this.setRef} />
</div>
</div>
);

View file

@ -55,5 +55,13 @@ export const enableEmptyColumn = getMeta('enable_empty_column');
export const showReloadButton = getMeta('show_reload_button');
export const defaultColumnWidth = getMeta('default_column_width');
export const pickerEmojiSize = getMeta('picker_emoji_size');
export const disablePost = getMeta('disable_post');
export const disableReactions = getMeta('disable_reactions');
export const disableFollow = getMeta('disable_follow');
export const disableUnfollow = getMeta('disable_unfollow');
export const disableBlock = getMeta('disable_block');
export const disableDomainBlock = getMeta('disable_domain_block');
export const disableClearAllNotifications = getMeta('disable_clear_all_notifications');
export const disableAccountDelete = getMeta('disable_account_delete');
export default initialState;

View file

@ -495,6 +495,8 @@
"privacy.limited.short": "Circle",
"privacy.mutual.long": "Visible for mutual followers only (Supported servers only)",
"privacy.mutual.short": "Mutual-followers-only",
"privacy.none.long": "No visibility allowed",
"privacy.none.short": "None",
"privacy.private.long": "Visible for followers only",
"privacy.private.short": "Followers-only",
"privacy.public.long": "Visible for all, shown in public timelines",

View file

@ -495,6 +495,8 @@
"privacy.limited.short": "サークル",
"privacy.mutual.long": "相互フォローのみ閲覧可(対応サーバのみ)",
"privacy.mutual.short": "相互フォロー限定",
"privacy.none.long": "許可された公開範囲なし",
"privacy.none.short": "なし",
"privacy.private.long": "フォロワーのみ閲覧可",
"privacy.private.short": "フォロワー限定",
"privacy.public.long": "誰でも閲覧可、公開TLに表示",

View file

@ -111,6 +111,8 @@ const initialState = ImmutableMap({
expires_action: 'mark',
references: ImmutableSet(),
context_references: ImmutableSet(),
prohibited_visibilities: ImmutableSet(),
prohibited_words: ImmutableSet(),
});
const initialPoll = ImmutableMap({
@ -266,6 +268,14 @@ const hydrate = (state, hydratedState) => {
state = state.set('text', hydratedState.get('text'));
}
if (hydratedState.has('prohibited_visibilities')) {
state = state.set('prohibited_visibilities', hydratedState.get('prohibited_visibilities').toSet());
}
if (hydratedState.has('prohibited_words')) {
state = state.set('prohibited_words', hydratedState.get('prohibited_words').toSet());
}
return state;
};

View file

@ -134,6 +134,11 @@ a.table-action-link {
&:first-child {
padding-left: 0;
}
&:disabled, &:disabled:hover {
color: darken($ui-primary-color, 30%);
cursor: default;
}
}
.batch-table {

View file

@ -24,341 +24,107 @@ class UserSettingsDecorator
setting_enable_reaction
).freeze
NESTED_KEYS = %w(
notification_emails
interactions
).freeze
BOOLEAN_KEYS = %w(
default_sensitive
unfollow_modal
unsubscribe_modal
boost_modal
delete_modal
post_reference_modal
add_reference_modal
unselect_reference_modal
auto_play_gif
expand_spoiers
reduce_motion
disable_swiping
system_font_ui
noindex
hide_network
aggregate_reblogs
show_application
advanced_layout
use_blurhash
use_pending_items
trends
crop_images
confirm_domain_block
show_follow_button_on_timeline
show_subscribe_button_on_timeline
show_followed_by
follow_button_to_list_adder
show_navigation_panel
show_quote_button
show_bookmark_button
show_target
place_tab_bar_at_bottom
show_tab_bar_label
enable_limited_timeline
enable_reaction
compact_reaction
show_reply_tree_button
hide_statuses_count
hide_following_count
hide_followers_count
disable_joke_appearance
theme_public
enable_status_reference
match_visibility_of_references
hexagon_avatar
enable_empty_column
hide_bot_on_public_timeline
confirm_follow_from_bot
show_reload_button
disable_post
disable_reactions
disable_follow
disable_unfollow
disable_block
disable_domain_block
disable_clear_all_notifications
disable_account_delete
).freeze
STRING_KEYS = %w(
default_privacy
default_language
theme
display_media
new_features_policy
theme_instance_ticker
content_font_size
info_font_size
content_emoji_reaction_size
emoji_scale
picker_emoji_size
default_search_searchability
default_column_width
default_expires_in
default_expires_action
prohibited_visibilities
prohibited_words
).freeze
def profile_change?
settings.keys.intersection(PROFILE_KEYS).any?
end
def process_update
user.settings['notification_emails'] = merged_notification_emails if change?('notification_emails')
user.settings['interactions'] = merged_interactions if change?('interactions')
user.settings['default_privacy'] = default_privacy_preference if change?('setting_default_privacy')
user.settings['default_sensitive'] = default_sensitive_preference if change?('setting_default_sensitive')
user.settings['default_language'] = default_language_preference if change?('setting_default_language')
user.settings['unfollow_modal'] = unfollow_modal_preference if change?('setting_unfollow_modal')
user.settings['unsubscribe_modal'] = unsubscribe_modal_preference if change?('setting_unsubscribe_modal')
user.settings['boost_modal'] = boost_modal_preference if change?('setting_boost_modal')
user.settings['delete_modal'] = delete_modal_preference if change?('setting_delete_modal')
user.settings['post_reference_modal'] = post_reference_modal_preference if change?('setting_post_reference_modal')
user.settings['add_reference_modal'] = add_reference_modal_preference if change?('setting_add_reference_modal')
user.settings['unselect_reference_modal'] = unselect_reference_modal_preference if change?('setting_unselect_reference_modal')
user.settings['auto_play_gif'] = auto_play_gif_preference if change?('setting_auto_play_gif')
user.settings['display_media'] = display_media_preference if change?('setting_display_media')
user.settings['expand_spoilers'] = expand_spoilers_preference if change?('setting_expand_spoilers')
user.settings['reduce_motion'] = reduce_motion_preference if change?('setting_reduce_motion')
user.settings['disable_swiping'] = disable_swiping_preference if change?('setting_disable_swiping')
user.settings['system_font_ui'] = system_font_ui_preference if change?('setting_system_font_ui')
user.settings['noindex'] = noindex_preference if change?('setting_noindex')
user.settings['theme'] = theme_preference if change?('setting_theme')
user.settings['hide_network'] = hide_network_preference if change?('setting_hide_network')
user.settings['aggregate_reblogs'] = aggregate_reblogs_preference if change?('setting_aggregate_reblogs')
user.settings['show_application'] = show_application_preference if change?('setting_show_application')
user.settings['advanced_layout'] = advanced_layout_preference if change?('setting_advanced_layout')
user.settings['use_blurhash'] = use_blurhash_preference if change?('setting_use_blurhash')
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')
user.settings['follow_button_to_list_adder'] = follow_button_to_list_adder_preference if change?('setting_follow_button_to_list_adder')
user.settings['show_navigation_panel'] = show_navigation_panel_preference if change?('setting_show_navigation_panel')
user.settings['show_quote_button'] = show_quote_button_preference if change?('setting_show_quote_button')
user.settings['show_bookmark_button'] = show_bookmark_button_preference if change?('setting_show_bookmark_button')
user.settings['show_target'] = show_target_preference if change?('setting_show_target')
user.settings['place_tab_bar_at_bottom'] = place_tab_bar_at_bottom_preference if change?('setting_place_tab_bar_at_bottom')
user.settings['show_tab_bar_label'] = show_tab_bar_label_preference if change?('setting_show_tab_bar_label')
user.settings['enable_limited_timeline'] = enable_limited_timeline_preference if change?('setting_enable_limited_timeline')
user.settings['enable_reaction'] = enable_reaction_preference if change?('setting_enable_reaction')
user.settings['compact_reaction'] = compact_reaction_preference if change?('setting_compact_reaction')
user.settings['show_reply_tree_button'] = show_reply_tree_button_preference if change?('setting_show_reply_tree_button')
user.settings['hide_statuses_count'] = hide_statuses_count_preference if change?('setting_hide_statuses_count')
user.settings['hide_following_count'] = hide_following_count_preference if change?('setting_hide_following_count')
user.settings['hide_followers_count'] = hide_followers_count_preference if change?('setting_hide_followers_count')
user.settings['disable_joke_appearance'] = disable_joke_appearance_preference if change?('setting_disable_joke_appearance')
user.settings['new_features_policy'] = new_features_policy if change?('setting_new_features_policy')
user.settings['theme_instance_ticker'] = theme_instance_ticker if change?('setting_theme_instance_ticker')
user.settings['theme_public'] = theme_public if change?('setting_theme_public')
user.settings['enable_status_reference'] = enable_status_reference_preference if change?('setting_enable_status_reference')
user.settings['match_visibility_of_references'] = match_visibility_of_references_preference if change?('setting_match_visibility_of_references')
user.settings['hexagon_avatar'] = hexagon_avatar_preference if change?('setting_hexagon_avatar')
user.settings['enable_empty_column'] = enable_empty_column_preference if change?('setting_enable_empty_column')
user.settings['content_font_size'] = content_font_size_preference if change?('setting_content_font_size')
user.settings['info_font_size'] = info_font_size_preference if change?('setting_info_font_size')
user.settings['content_emoji_reaction_size'] = content_emoji_reaction_size_preference if change?('setting_content_emoji_reaction_size')
user.settings['emoji_scale'] = emoji_scale_preference if change?('setting_emoji_scale')
user.settings['picker_emoji_size'] = picker_emoji_size_preference if change?('setting_picker_emoji_size')
user.settings['hide_bot_on_public_timeline'] = hide_bot_on_public_timeline_preference if change?('setting_hide_bot_on_public_timeline')
user.settings['confirm_follow_from_bot'] = confirm_follow_from_bot_preference if change?('setting_confirm_follow_from_bot')
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')
user.settings['default_expires_in'] = default_expires_in_preference if change?('setting_default_expires_in')
user.settings['default_expires_action'] = default_expires_action_preference if change?('setting_default_expires_action')
end
NESTED_KEYS.each do |key|
user.settings[key] = user.settings[key].merge coerced_settings(key) if change?(key)
end
def merged_notification_emails
user.settings['notification_emails'].merge coerced_settings('notification_emails').to_h
end
STRING_KEYS.each do |key|
user.settings[key] = settings["setting_#{key}"] if change?("setting_#{key}")
end
def merged_interactions
user.settings['interactions'].merge coerced_settings('interactions').to_h
end
def default_privacy_preference
settings['setting_default_privacy']
end
def default_sensitive_preference
boolean_cast_setting 'setting_default_sensitive'
end
def unfollow_modal_preference
boolean_cast_setting 'setting_unfollow_modal'
end
def unsubscribe_modal_preference
boolean_cast_setting 'setting_unsubscribe_modal'
end
def boost_modal_preference
boolean_cast_setting 'setting_boost_modal'
end
def delete_modal_preference
boolean_cast_setting 'setting_delete_modal'
end
def post_reference_modal_preference
boolean_cast_setting 'setting_post_reference_modal'
end
def add_reference_modal_preference
boolean_cast_setting 'setting_add_reference_modal'
end
def unselect_reference_modal_preference
boolean_cast_setting 'setting_unselect_reference_modal'
end
def system_font_ui_preference
boolean_cast_setting 'setting_system_font_ui'
end
def auto_play_gif_preference
boolean_cast_setting 'setting_auto_play_gif'
end
def display_media_preference
settings['setting_display_media']
end
def expand_spoilers_preference
boolean_cast_setting 'setting_expand_spoilers'
end
def reduce_motion_preference
boolean_cast_setting 'setting_reduce_motion'
end
def disable_swiping_preference
boolean_cast_setting 'setting_disable_swiping'
end
def noindex_preference
boolean_cast_setting 'setting_noindex'
end
def hide_network_preference
boolean_cast_setting 'setting_hide_network'
end
def show_application_preference
boolean_cast_setting 'setting_show_application'
end
def theme_preference
settings['setting_theme']
end
def default_language_preference
settings['setting_default_language']
end
def aggregate_reblogs_preference
boolean_cast_setting 'setting_aggregate_reblogs'
end
def advanced_layout_preference
boolean_cast_setting 'setting_advanced_layout'
end
def use_blurhash_preference
boolean_cast_setting 'setting_use_blurhash'
end
def use_pending_items_preference
boolean_cast_setting 'setting_use_pending_items'
end
def trends_preference
boolean_cast_setting 'setting_trends'
end
def crop_images_preference
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
def show_subscribe_button_on_timeline_preference
boolean_cast_setting 'setting_show_subscribe_button_on_timeline'
end
def show_followed_by_preference
boolean_cast_setting 'setting_show_followed_by'
end
def follow_button_to_list_adder_preference
boolean_cast_setting 'setting_follow_button_to_list_adder'
end
def show_navigation_panel_preference
boolean_cast_setting 'setting_show_navigation_panel'
end
def show_quote_button_preference
boolean_cast_setting 'setting_show_quote_button'
end
def show_bookmark_button_preference
boolean_cast_setting 'setting_show_bookmark_button'
end
def show_target_preference
boolean_cast_setting 'setting_show_target'
end
def place_tab_bar_at_bottom_preference
boolean_cast_setting 'setting_place_tab_bar_at_bottom'
end
def show_tab_bar_label_preference
boolean_cast_setting 'setting_show_tab_bar_label'
end
def enable_limited_timeline_preference
boolean_cast_setting 'setting_enable_limited_timeline'
end
def enable_reaction_preference
boolean_cast_setting 'setting_enable_reaction'
end
def compact_reaction_preference
boolean_cast_setting 'setting_compact_reaction'
end
def show_reply_tree_button_preference
boolean_cast_setting 'setting_show_reply_tree_button'
end
def hide_statuses_count_preference
boolean_cast_setting 'setting_hide_statuses_count'
end
def hide_following_count_preference
boolean_cast_setting 'setting_hide_following_count'
end
def hide_followers_count_preference
boolean_cast_setting 'setting_hide_followers_count'
end
def disable_joke_appearance_preference
boolean_cast_setting 'setting_disable_joke_appearance'
end
def new_features_policy
settings['setting_new_features_policy']
end
def theme_instance_ticker
settings['setting_theme_instance_ticker']
end
def theme_public
boolean_cast_setting 'setting_theme_public'
end
def hexagon_avatar_preference
boolean_cast_setting 'setting_hexagon_avatar'
end
def enable_status_reference_preference
boolean_cast_setting 'setting_enable_status_reference'
end
def match_visibility_of_references_preference
boolean_cast_setting 'setting_match_visibility_of_references'
end
def enable_empty_column_preference
boolean_cast_setting 'setting_enable_empty_column'
end
def content_font_size_preference
settings['setting_content_font_size']
end
def info_font_size_preference
settings['setting_info_font_size']
end
def content_emoji_reaction_size_preference
settings['setting_content_emoji_reaction_size']
end
def emoji_scale_preference
settings['setting_emoji_scale']
end
def picker_emoji_size_preference
settings['setting_picker_emoji_size']
end
def hide_bot_on_public_timeline_preference
boolean_cast_setting 'setting_hide_bot_on_public_timeline'
end
def confirm_follow_from_bot_preference
boolean_cast_setting 'setting_confirm_follow_from_bot'
end
def default_search_searchability_preference
settings['setting_default_search_searchability']
end
def show_reload_button_preference
boolean_cast_setting 'setting_show_reload_button'
end
def default_column_width_preference
settings['setting_default_column_width']
end
def default_expires_in_preference
settings['setting_default_expires_in']
end
def default_expires_action_preference
settings['setting_default_expires_action']
BOOLEAN_KEYS.each do |key|
user.settings[key] = boolean_cast_setting "setting_#{key}" if change?("setting_#{key}")
end
end
def boolean_cast_setting(key)

View file

@ -141,6 +141,8 @@ class User < ApplicationRecord
:hide_bot_on_public_timeline, :confirm_follow_from_bot,
:default_search_searchability, :default_expires_in, :default_expires_action,
:show_reload_button, :default_column_width,
:disable_post, :disable_reactions, :disable_follow, :disable_unfollow, :disable_block, :disable_domain_block, :disable_clear_all_notifications, :disable_account_delete,
:prohibited_visibilities, :prohibited_words,
to: :settings, prefix: :setting, allow_nil: false

View file

@ -76,6 +76,14 @@ class InitialStateSerializer < ActiveModel::Serializer
store[:confirm_follow_from_bot] = object.current_account.user.setting_confirm_follow_from_bot
store[:show_reload_button] = object.current_account.user.setting_show_reload_button
store[:default_column_width] = object.current_account.user.setting_default_column_width
store[:disable_post] = object.current_account.user.setting_disable_post
store[:disable_reactions] = object.current_account.user.setting_disable_reactions
store[:disable_follow] = object.current_account.user.setting_disable_follow
store[:disable_unfollow] = object.current_account.user.setting_disable_unfollow
store[:disable_block] = object.current_account.user.setting_disable_block
store[:disable_domain_block] = object.current_account.user.setting_disable_domain_block
store[:disable_clear_all_notifications] = object.current_account.user.setting_disable_clear_all_notifications
store[:disable_account_delete] = object.current_account.user.setting_disable_account_delete
else
store[:auto_play_gif] = Setting.auto_play_gif
store[:display_media] = Setting.display_media
@ -91,12 +99,14 @@ class InitialStateSerializer < ActiveModel::Serializer
store = {}
if object.current_account
store[:me] = object.current_account.id.to_s
store[:default_privacy] = object.visibility || object.current_account.user.setting_default_privacy
store[:default_searchability] = object.current_account.searchability
store[:default_sensitive] = object.current_account.user.setting_default_sensitive
store[:default_expires_in] = object.current_account.user.setting_default_expires_in
store[:default_expires_action] = object.current_account.user.setting_default_expires_action
store[:me] = object.current_account.id.to_s
store[:default_privacy] = object.visibility || object.current_account.user.setting_default_privacy
store[:default_searchability] = object.current_account.searchability
store[:default_sensitive] = object.current_account.user.setting_default_sensitive
store[:default_expires_in] = object.current_account.user.setting_default_expires_in
store[:default_expires_action] = object.current_account.user.setting_default_expires_action
store[:prohibited_visibilities] = object.current_account.user.setting_prohibited_visibilities.filter(&:present?)
store[:prohibited_words] = (object.current_account.user.setting_prohibited_words || '').split(',').map(&:strip).filter(&:present?)
end
store[:text] = object.text if object.text

View file

@ -38,7 +38,9 @@ class PostStatusService < BaseService
validate_media!
validate_expires!
validate_prohibited_words!
preprocess_attributes!
validate_prohibited_visibilities!
preprocess_quote!
if scheduled?
@ -152,6 +154,19 @@ class PostStatusService < BaseService
expires_at.present? && expires_at <= Time.now.utc + MIN_SCHEDULE_OFFSET
end
def validate_prohibited_words!
return if @options[:spoiler_text].blank? && @options[:text].blank?
text = [@options[:spoiler_text], @options[:text]].join(' ')
words = (@account&.user&.setting_prohibited_words || '').split(',').map(&:strip).filter(&:present?)
raise Mastodon::ValidationError, I18n.t('status_prohibit.validations.prohibited_words') if words.any? { |word| text.include? word }
end
def validate_prohibited_visibilities!
raise Mastodon::ValidationError, I18n.t('status_prohibit.validations.prohibited_visibilities') if @account.user&.setting_prohibited_visibilities&.filter(&:present?)&.include?(@visibility.to_s)
end
def validate_media!
return if @options[:media_ids].blank? || !@options[:media_ids].is_a?(Enumerable)

View file

@ -37,6 +37,8 @@ class ReblogService < BaseService
end
end
raise Mastodon::ValidationError, I18n.t('status_prohibit.validations.prohibited_visibilities') if account.user&.setting_prohibited_visibilities&.filter(&:present?)&.include?(visibility)
ApplicationRecord.transaction do
reblog = account.statuses.create!(reblog: reblogged_status, text: '', visibility: visibility, rate_limit: options[:with_rate_limit])
reblog.capability_tokens.create! if reblog.limited_visibility?

View file

@ -1,3 +1,7 @@
:ruby
disable_follow = current_user.setting_disable_follow
disable_unfollow = current_user.setting_disable_unfollow
- content_for :page_title do
= t('settings.relationships')
@ -48,13 +52,13 @@
%label.batch-table__toolbar__select.batch-checkbox-all
= check_box_tag :batch_checkbox_all, nil, false
.batch-table__toolbar__actions
= f.button safe_join([fa_icon('user-plus'), t('relationships.follow_selected_followers')]), name: :follow, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') } if followed_by_relationship? && !mutual_relationship?
= f.button safe_join([fa_icon('user-plus'), t('relationships.follow_selected_followers')]), name: :follow, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') }, disabled: disable_follow if followed_by_relationship? && !mutual_relationship?
= f.button safe_join([fa_icon('user-times'), t('relationships.remove_selected_follows')]), name: :unfollow, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') } unless followed_by_relationship?
= f.button safe_join([fa_icon('user-times'), t('relationships.remove_selected_follows')]), name: :unfollow, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') }, disabled: disable_unfollow unless followed_by_relationship?
= f.button safe_join([fa_icon('trash'), t('relationships.remove_selected_followers')]), name: :remove_from_followers, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') } unless following_relationship?
= f.button safe_join([fa_icon('trash'), t('relationships.remove_selected_followers')]), name: :remove_from_followers, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') }, disabled: disable_unfollow unless following_relationship?
= f.button safe_join([fa_icon('trash'), t('relationships.remove_selected_domains')]), name: :block_domains, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') } if followed_by_relationship?
= f.button safe_join([fa_icon('trash'), t('relationships.remove_selected_domains')]), name: :block_domains, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') }, disabled: disable_unfollow if followed_by_relationship?
.batch-table__body
- if @accounts.empty?
= nothing_here 'nothing-here--under-tabs'

View file

@ -1,3 +1,6 @@
:ruby
disable = current_user.setting_disable_account_delete
- content_for :page_title do
= t('settings.delete')
@ -26,4 +29,4 @@
= f.input :username, wrapper: :with_block_label, input_html: { :autocomplete => 'off' }, hint: t('deletes.confirm_username')
.actions
= f.button :button, t('deletes.proceed'), type: :submit, class: 'negative'
= f.button :button, t('deletes.proceed'), type: :submit, class: disable ? 'button disabled' : 'negative', disabled: disable

View file

@ -0,0 +1,45 @@
- content_for :page_title do
= t('settings.safety')
- content_for :heading_actions do
= button_tag t('generic.save_changes'), class: 'button', form: 'edit_preferences'
= simple_form_for current_user, url: settings_preferences_safety_path, html: { method: :put, id: 'edit_preferences' } do |f|
= render 'shared/error_messages', object: current_user
%h4= t 'preferences.disable_actions'
%p.hint= t 'preferences.disable_actions_hint'
.fields-group
= f.input :setting_disable_reactions, as: :boolean, wrapper: :with_label, fedibird_features: true
.fields-group
= f.input :setting_disable_follow, as: :boolean, wrapper: :with_label, fedibird_features: true
.fields-group
= f.input :setting_disable_unfollow, as: :boolean, wrapper: :with_label, fedibird_features: true
.fields-group
= f.input :setting_disable_block, as: :boolean, wrapper: :with_label, fedibird_features: true
.fields-group
= f.input :setting_disable_domain_block, as: :boolean, wrapper: :with_label, fedibird_features: true
.fields-group
= f.input :setting_disable_clear_all_notifications, as: :boolean, wrapper: :with_label, fedibird_features: true
.fields-group
= f.input :setting_disable_account_delete, as: :boolean, wrapper: :with_label, fedibird_features: true
.fields-group
= f.input :setting_disable_post, as: :boolean, wrapper: :with_label, fedibird_features: true
%h4= t 'preferences.post_prohibite'
.fields-group
= f.input :setting_prohibited_visibilities, collection: Status.visibilities.keys, wrapper: :with_label, include_blank: false, label_method: lambda { |visibility| t("statuses.visibilities.#{visibility}") }, required: false, as: :check_boxes, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li', fedibird_features: true
.fields-group
= f.input :setting_prohibited_words, wrapper: :with_label, fedibird_features: true

View file

@ -1245,10 +1245,14 @@ en:
too_many_options: can't contain more than %{max} items
preferences:
beta_features: Beta features
disable_actions: Disable actions
disable_actions_hint: This setting is used to prevent accidents due to mishandling or to disable certain actions in accounts with limited use. It also applies to client applications.
fedibird_features: Fedibird features
other: Other
post_prohibite: Post prohibite
posting_defaults: Posting defaults
public_timelines: Public timelines
safety: Safety & Privacy
searching_defaults: Searching default
reactions:
errors:
@ -1371,6 +1375,7 @@ en:
preferences: Preferences
profile: Profile
relationships: Follows and followers
safety: Safety & Privacy
statuses_cleanup: Automated post deletion
two_factor_authentication: Two-factor Auth
webauthn_authentication: Security keys
@ -1487,6 +1492,10 @@ en:
expire_in_the_past: It is already past expires. You cannot specify expires before the posting time.
invalid_expire_at: Invalid expires are specified.
invalid_expire_action: Invalid expires_action are specified.
status_prohibit:
validations:
prohibited_visibilities: Prohibited visibility is specified.
prohibited_words: Prohibited words is specified.
status_references:
errors:
limit: You have already reached your reference limit

View file

@ -1191,10 +1191,14 @@ ja:
too_many_options: は%{max}個までです
preferences:
beta_features: ベータ機能
disable_actions: 操作の無効化
disable_actions_hint: 誤操作による事故を防いだり、用途を限定したアカウント向けに、特定の操作を無効化するための設定です。クライアントアプリに対しても有効です。
fedibird_features: Fedibirdの機能
other: その他
post_prohibite: 投稿の禁止
posting_defaults: デフォルトの投稿設定
public_timelines: 公開タイムライン
safety: 安全とプライバシー
searching_defaults: デフォルトの検索設定
reactions:
errors:
@ -1316,6 +1320,7 @@ ja:
preferences: ユーザー設定
profile: プロフィール
relationships: フォロー・フォロワー
safety: 安全とプライバシー
two_factor_authentication: 二段階認証
webauthn_authentication: セキュリティキー
statuses:
@ -1385,6 +1390,10 @@ ja:
public_long: 誰でも見ることができ、かつ公開タイムラインに表示されます
unlisted: 未収載
unlisted_long: 誰でも見ることができますが、公開タイムラインには表示されません
status_prohibit:
validations:
prohibited_visibilities: 禁止された公開範囲を指定しています
prohibited_words: 禁止された単語を含んでいます
status_expire:
validations:
expire_in_the_past: 既に公開期限を過ぎています。投稿時間より前の公開期限は指定できません

View file

@ -70,7 +70,15 @@ en:
The format is 1y2mo3d4h5m (1 year, 2 months, 3 days, 4 hours, 5 minutes later).
setting_default_search_searchability: For clients that do not support advanced range settings, switch the settings here. Mastodon's standard behavior is "Reacted-users-only". Targeting "Public" makes it easier to discover unknown information, but if the results are noisy, narrowing the search range is effective.
setting_default_sensitive: Sensitive media is hidden by default and can be revealed with a click
setting_disable_account_delete: Restrict account deletion due to temporary hesitation
setting_disable_block: Restricts you from accidentally blocking
setting_disable_clear_all_notifications: Restrict clearing all notifications
setting_disable_domain_block: Restrict domain blocking
setting_disable_follow: Restricts you from accidentally following
setting_disable_joke_appearance: Disable April Fools' Day and other joke functions
setting_disable_post: Restricts you from accidentally posting
setting_disable_reactions: Restrict reactions for favorites, boosts ,emoji reactions and polls
setting_disable_unfollow: Restricts you from accidentally unfollowing
setting_display_media_default: Hide media marked as sensitive
setting_display_media_hide_all: Always hide media
setting_display_media_show_all: Always show media
@ -89,6 +97,8 @@ en:
setting_new_features_policy: Set the acceptance policy when new features are added to Fedibird. The recommended setting will enable many new features, so set it to disabled if it is not desirable
setting_noindex: Affects your public profile and post pages
setting_place_tab_bar_at_bottom: When using a touch device, you can operate tabs within the reach of your fingers.
setting_prohibited_visibilities: Prohibited visibilities
setting_prohibited_words: Prohibited words
setting_show_application: The application you use to post will be displayed in the detailed view of your posts
setting_show_bookmark_button: When turned off, the bookmark call will be in Mastodon's standard position (in the menu on the action bar)
setting_show_follow_button_on_timeline: You can easily check the follow status and build a follow list quickly
@ -241,8 +251,16 @@ en:
setting_default_search_searchability: Search range
setting_default_sensitive: Always mark media as sensitive
setting_delete_modal: Show confirmation dialog before deleting a post
setting_disable_account_delete: Disable account delete
setting_disable_block: Disable block
setting_disable_clear_all_notifications: Disable clear all notifications
setting_disable_domain_block: Disable domain block
setting_disable_follow: Disable follow
setting_disable_joke_appearance: Disable joke feature to change appearance
setting_disable_post: Disable post
setting_disable_reactions: Disable reactions
setting_disable_swiping: Disable swiping motions
setting_disable_unfollow: Disable unfollow
setting_display_media: Media display
setting_display_media_default: Default
setting_display_media_hide_all: Hide all
@ -270,6 +288,8 @@ en:
setting_picker_emoji_size: Emoji size in picker
setting_place_tab_bar_at_bottom: Place the tab bar at the bottom
setting_post_reference_modal: Show a confirmation dialog before making a post containing references
setting_prohibited_visibilities: Prohibited visibility
setting_prohibited_words: Prohibited words
setting_reduce_motion: Reduce motion in animations
setting_unselect_reference_modal: Show confirmation dialog before removing a reference
setting_show_application: Disclose application used to send posts

View file

@ -66,7 +66,15 @@ ja:
setting_default_expires_in: 投稿日時を起点とする終了日時を指定します。書式は1y2mo3d4h5m1年2ヶ月3日4時間5分後です。
setting_default_search_searchability: 範囲の詳細設定に対応していないクライアントでは、ここで設定を切り替えてください。Mastodonの標準動作は『リアクション限定』です。『公開』を対象にすると未知の情報を発見しやすくなりますが、結果にイズが多い場合は検索範囲を狭めると効果的です。
setting_default_sensitive: 閲覧注意状態のメディアはデフォルトでは内容が伏せられ、クリックして初めて閲覧できるようになります
setting_disable_account_delete: 一時的な迷いでアカウント削除することを防ぎます
setting_disable_block: 誤ってブロックすることを防ぎます
setting_disable_clear_all_notifications: 通知の全消去を防ぎます
setting_disable_domain_block: 誤ってドメインブロックを実行することを防ぎます
setting_disable_follow: 誤ってフォローすることを防ぎます
setting_disable_joke_appearance: エイプリルフール等のジョーク機能を無効にします
setting_disable_post: 誤って投稿することを防ぎます
setting_disable_reactions: 誤ってお気に入り・ブースト・絵文字リアクション・投票することを防ぎます
setting_disable_unfollow: 誤ってフォロー解除することを防ぎます
setting_display_media_default: 閲覧注意としてマークされたメディアは隠す
setting_display_media_hide_all: メディアを常に隠す
setting_display_media_show_all: メディアを常に表示する
@ -85,6 +93,8 @@ ja:
setting_new_features_policy: Fedibirdに新しい機能が追加された時の受け入れポリシーを設定します。推奨設定は多くの新機能を有効にするので、望ましくない場合は無効に設定してください
setting_noindex: 公開プロフィールおよび各投稿ページに影響します
setting_place_tab_bar_at_bottom: タッチデバイス使用時に、タブの操作を指の届く範囲で行えます
setting_prohibited_visibilities: 指定した公開範囲で投稿することを禁止します
setting_prohibited_words: 投稿で使用禁止する単語をカンマ区切りで指定します
setting_show_application: 投稿するのに使用したアプリが投稿の詳細ビューに表示されるようになります
setting_show_bookmark_button: オフにした場合、ブックマークの呼び出しはMastodon標準の位置となりますアクションバーのメニューの中
setting_show_follow_button_on_timeline: フォロー状態を確認し易くなり、素早くフォローリストを構築できます
@ -237,8 +247,16 @@ ja:
setting_default_search_searchability: 検索の対象とする範囲
setting_default_sensitive: メディアを常に閲覧注意としてマークする
setting_delete_modal: 投稿を削除する前に確認ダイアログを表示する
setting_disable_account_delete: アカウント削除を無効にする
setting_disable_block: ブロックを無効にする
setting_disable_clear_all_notifications: 通知の全消去を無効にする
setting_disable_domain_block: ドメインブロックを無効にする
setting_disable_follow: フォローを無効にする
setting_disable_joke_appearance: ジョーク機能による見た目の変更を無効にする
setting_disable_post: 投稿を無効にする
setting_disable_reactions: リアクションを無効にする
setting_disable_swiping: スワイプでの切り替えを無効にする
setting_disable_unfollow: フォロー解除を無効にする
setting_display_media: メディアの表示
setting_display_media_default: 標準
setting_display_media_hide_all: 非表示
@ -266,6 +284,8 @@ ja:
setting_picker_emoji_size: 絵文字ピッカーの表示サイズ
setting_place_tab_bar_at_bottom: タブバーを下に配置する
setting_post_reference_modal: 参照を含む投稿をする前に確認ダイアログを表示する
setting_prohibited_visibilities: 投稿禁止する公開範囲
setting_prohibited_words: 投稿禁止する単語
setting_reduce_motion: アニメーションの動きを減らす
setting_unselect_reference_modal: 参照を解除する前に確認ダイアログを表示する
setting_show_application: 送信したアプリを開示する

View file

@ -15,6 +15,7 @@ SimpleNavigation::Configuration.run do |navigation|
s.item :notifications, safe_join([fa_icon('bell fw'), t('settings.notifications')]), settings_preferences_notifications_url
s.item :favourite_domains, safe_join([fa_icon('users fw'), t('settings.favourite_domains')]), settings_favourite_domains_url
s.item :favourite_tags, safe_join([fa_icon('hashtag fw'), t('settings.favourite_tags')]), settings_favourite_tags_url
s.item :safety, safe_join([fa_icon('shield fw'), t('preferences.safety')]), settings_preferences_safety_url
s.item :other, safe_join([fa_icon('cog fw'), t('preferences.other')]), settings_preferences_other_url
end

View file

@ -114,6 +114,7 @@ Rails.application.routes.draw do
namespace :preferences do
resource :appearance, only: [:show, :update], controller: :appearance
resource :notifications, only: [:show, :update]
resource :safety, only: [:show, :update], controller: :safety
resource :other, only: [:show, :update], controller: :other
end

View file

@ -116,6 +116,16 @@ defaults: &defaults
default_column_width: 'x100'
default_expires_in: ''
default_expires_action: 'mark'
disable_post: false
disable_reactions: false
disable_follow: false
disable_unfollow: false
disable_block: false
disable_domain_block: false
disable_clear_all_notifications: false
disable_account_delete: false
prohibited_visibilities: []
prohibited_words: ''
development:
<<: *defaults