Followers-only post federation (#2111)
* Make private toots get PuSHed to subscription URLs that belong to domains where you have approved followers * Authorized followers controller, stub for bulk action * Soft block in the background * Add simple test for new controller * Rename Settings::FollowersController to Settings::FollowerDomainsController, paginate results, rename "private" post setting to "followers-only", fix pagination style, improve post privacy preferences style, improve warning style * Extract compose form warnings into own container, show warning when posting to followers-only with unlocked account
This commit is contained in:
		
							parent
							
								
									ef5937da1f
								
							
						
					
					
						commit
						501514960a
					
				
					 27 changed files with 394 additions and 134 deletions
				
			
		| 
						 | 
					@ -15,6 +15,7 @@ import SensitiveButtonContainer from '../containers/sensitive_button_container';
 | 
				
			||||||
import EmojiPickerDropdown from './emoji_picker_dropdown';
 | 
					import EmojiPickerDropdown from './emoji_picker_dropdown';
 | 
				
			||||||
import UploadFormContainer from '../containers/upload_form_container';
 | 
					import UploadFormContainer from '../containers/upload_form_container';
 | 
				
			||||||
import TextIconButton from './text_icon_button';
 | 
					import TextIconButton from './text_icon_button';
 | 
				
			||||||
 | 
					import WarningContainer from '../containers/warning_container';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const messages = defineMessages({
 | 
					const messages = defineMessages({
 | 
				
			||||||
  placeholder: { id: 'compose_form.placeholder', defaultMessage: 'What is on your mind?' },
 | 
					  placeholder: { id: 'compose_form.placeholder', defaultMessage: 'What is on your mind?' },
 | 
				
			||||||
| 
						 | 
					@ -116,26 +117,13 @@ class ComposeForm extends React.PureComponent {
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  render () {
 | 
					  render () {
 | 
				
			||||||
    const { intl, needsPrivacyWarning, mentionedDomains, onPaste } = this.props;
 | 
					    const { intl, onPaste } = this.props;
 | 
				
			||||||
    const disabled = this.props.is_submitting;
 | 
					    const disabled = this.props.is_submitting;
 | 
				
			||||||
    const text = [this.props.spoiler_text, this.props.text].join('');
 | 
					    const text = [this.props.spoiler_text, this.props.text].join('');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let publishText    = '';
 | 
					    let publishText    = '';
 | 
				
			||||||
    let privacyWarning = '';
 | 
					 | 
				
			||||||
    let reply_to_other = false;
 | 
					    let reply_to_other = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (needsPrivacyWarning) {
 | 
					 | 
				
			||||||
      privacyWarning = (
 | 
					 | 
				
			||||||
        <div className='compose-form__warning'>
 | 
					 | 
				
			||||||
          <FormattedMessage
 | 
					 | 
				
			||||||
            id='compose_form.privacy_disclaimer'
 | 
					 | 
				
			||||||
            defaultMessage='Your private status will be delivered to mentioned users on {domains}. Do you trust {domainsCount, plural, one {that server} other {those servers}} to not leak your status?'
 | 
					 | 
				
			||||||
            values={{ domains: <strong>{mentionedDomains.join(', ')}</strong>, domainsCount: mentionedDomains.length }}
 | 
					 | 
				
			||||||
          />
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (this.props.privacy === 'private' || this.props.privacy === 'direct') {
 | 
					    if (this.props.privacy === 'private' || this.props.privacy === 'direct') {
 | 
				
			||||||
      publishText = <span className='compose-form__publish-private'><i className='fa fa-lock' /> {intl.formatMessage(messages.publish)}</span>;
 | 
					      publishText = <span className='compose-form__publish-private'><i className='fa fa-lock' /> {intl.formatMessage(messages.publish)}</span>;
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
| 
						 | 
					@ -150,7 +138,7 @@ class ComposeForm extends React.PureComponent {
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
        </Collapsable>
 | 
					        </Collapsable>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        {privacyWarning}
 | 
					        <WarningContainer />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <ReplyIndicatorContainer />
 | 
					        <ReplyIndicatorContainer />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -208,8 +196,6 @@ ComposeForm.propTypes = {
 | 
				
			||||||
  is_submitting: PropTypes.bool,
 | 
					  is_submitting: PropTypes.bool,
 | 
				
			||||||
  is_uploading: PropTypes.bool,
 | 
					  is_uploading: PropTypes.bool,
 | 
				
			||||||
  me: PropTypes.number,
 | 
					  me: PropTypes.number,
 | 
				
			||||||
  needsPrivacyWarning: PropTypes.bool,
 | 
					 | 
				
			||||||
  mentionedDomains: PropTypes.array.isRequired,
 | 
					 | 
				
			||||||
  onChange: PropTypes.func.isRequired,
 | 
					  onChange: PropTypes.func.isRequired,
 | 
				
			||||||
  onSubmit: PropTypes.func.isRequired,
 | 
					  onSubmit: PropTypes.func.isRequired,
 | 
				
			||||||
  onClearSuggestions: PropTypes.func.isRequired,
 | 
					  onClearSuggestions: PropTypes.func.isRequired,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -7,7 +7,7 @@ const messages = defineMessages({
 | 
				
			||||||
  public_long: { id: 'privacy.public.long', defaultMessage: 'Post to public timelines' },
 | 
					  public_long: { id: 'privacy.public.long', defaultMessage: 'Post to public timelines' },
 | 
				
			||||||
  unlisted_short: { id: 'privacy.unlisted.short', defaultMessage: 'Unlisted' },
 | 
					  unlisted_short: { id: 'privacy.unlisted.short', defaultMessage: 'Unlisted' },
 | 
				
			||||||
  unlisted_long: { id: 'privacy.unlisted.long', defaultMessage: 'Do not show in public timelines' },
 | 
					  unlisted_long: { id: 'privacy.unlisted.long', defaultMessage: 'Do not show in public timelines' },
 | 
				
			||||||
  private_short: { id: 'privacy.private.short', defaultMessage: 'Private' },
 | 
					  private_short: { id: 'privacy.private.short', defaultMessage: 'Followers-only' },
 | 
				
			||||||
  private_long: { id: 'privacy.private.long', defaultMessage: 'Post to followers only' },
 | 
					  private_long: { id: 'privacy.private.long', defaultMessage: 'Post to followers only' },
 | 
				
			||||||
  direct_short: { id: 'privacy.direct.short', defaultMessage: 'Direct' },
 | 
					  direct_short: { id: 'privacy.direct.short', defaultMessage: 'Direct' },
 | 
				
			||||||
  direct_long: { id: 'privacy.direct.long', defaultMessage: 'Post to mentioned users only' },
 | 
					  direct_long: { id: 'privacy.direct.long', defaultMessage: 'Post to mentioned users only' },
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,25 @@
 | 
				
			||||||
 | 
					import PropTypes from 'prop-types';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Warning extends React.PureComponent {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  constructor (props) {
 | 
				
			||||||
 | 
					    super(props);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  render () {
 | 
				
			||||||
 | 
					    const { message } = this.props;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      <div className='compose-form__warning'>
 | 
				
			||||||
 | 
					        {message}
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Warning.propTypes = {
 | 
				
			||||||
 | 
					  message: PropTypes.node.isRequired
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default Warning;
 | 
				
			||||||
| 
						 | 
					@ -1,7 +1,6 @@
 | 
				
			||||||
import { connect } from 'react-redux';
 | 
					import { connect } from 'react-redux';
 | 
				
			||||||
import ComposeForm from '../components/compose_form';
 | 
					import ComposeForm from '../components/compose_form';
 | 
				
			||||||
import { uploadCompose } from '../../../actions/compose';
 | 
					import { uploadCompose } from '../../../actions/compose';
 | 
				
			||||||
import { createSelector } from 'reselect';
 | 
					 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  changeCompose,
 | 
					  changeCompose,
 | 
				
			||||||
  submitCompose,
 | 
					  submitCompose,
 | 
				
			||||||
| 
						 | 
					@ -12,17 +11,7 @@ import {
 | 
				
			||||||
  insertEmojiCompose
 | 
					  insertEmojiCompose
 | 
				
			||||||
} from '../../../actions/compose';
 | 
					} from '../../../actions/compose';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const getMentionedUsernames = createSelector(state => state.getIn(['compose', 'text']), text => text.match(/(?:^|[^\/\w])@([a-z0-9_]+@[a-z0-9\.\-]+)/ig));
 | 
					const mapStateToProps = state => ({
 | 
				
			||||||
 | 
					 | 
				
			||||||
const getMentionedDomains = createSelector(getMentionedUsernames, mentionedUsernamesWithDomains => {
 | 
					 | 
				
			||||||
  return mentionedUsernamesWithDomains !== null ? [...new Set(mentionedUsernamesWithDomains.map(item => item.split('@')[2]))] : [];
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const mapStateToProps = (state, props) => {
 | 
					 | 
				
			||||||
  const mentionedUsernames = getMentionedUsernames(state);
 | 
					 | 
				
			||||||
  const mentionedUsernamesWithDomains = getMentionedDomains(state);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  return {
 | 
					 | 
				
			||||||
  text: state.getIn(['compose', 'text']),
 | 
					  text: state.getIn(['compose', 'text']),
 | 
				
			||||||
  suggestion_token: state.getIn(['compose', 'suggestion_token']),
 | 
					  suggestion_token: state.getIn(['compose', 'suggestion_token']),
 | 
				
			||||||
  suggestions: state.getIn(['compose', 'suggestions']),
 | 
					  suggestions: state.getIn(['compose', 'suggestions']),
 | 
				
			||||||
| 
						 | 
					@ -33,11 +22,8 @@ const mapStateToProps = (state, props) => {
 | 
				
			||||||
  preselectDate: state.getIn(['compose', 'preselectDate']),
 | 
					  preselectDate: state.getIn(['compose', 'preselectDate']),
 | 
				
			||||||
  is_submitting: state.getIn(['compose', 'is_submitting']),
 | 
					  is_submitting: state.getIn(['compose', 'is_submitting']),
 | 
				
			||||||
  is_uploading: state.getIn(['compose', 'is_uploading']),
 | 
					  is_uploading: state.getIn(['compose', 'is_uploading']),
 | 
				
			||||||
    me: state.getIn(['compose', 'me']),
 | 
					  me: state.getIn(['compose', 'me'])
 | 
				
			||||||
    needsPrivacyWarning: (state.getIn(['compose', 'privacy']) === 'private' || state.getIn(['compose', 'privacy']) === 'direct') && mentionedUsernames !== null,
 | 
					});
 | 
				
			||||||
    mentionedDomains: mentionedUsernamesWithDomains
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
const mapDispatchToProps = (dispatch) => ({
 | 
					const mapDispatchToProps = (dispatch) => ({
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,48 @@
 | 
				
			||||||
 | 
					import { connect } from 'react-redux';
 | 
				
			||||||
 | 
					import Warning from '../components/warning';
 | 
				
			||||||
 | 
					import { createSelector } from 'reselect';
 | 
				
			||||||
 | 
					import PropTypes from 'prop-types';
 | 
				
			||||||
 | 
					import { FormattedMessage } from 'react-intl';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const getMentionedUsernames = createSelector(state => state.getIn(['compose', 'text']), text => text.match(/(?:^|[^\/\w])@([a-z0-9_]+@[a-z0-9\.\-]+)/ig));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const getMentionedDomains = createSelector(getMentionedUsernames, mentionedUsernamesWithDomains => {
 | 
				
			||||||
 | 
					  return mentionedUsernamesWithDomains !== null ? [...new Set(mentionedUsernamesWithDomains.map(item => item.split('@')[2]))] : [];
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const mapStateToProps = state => {
 | 
				
			||||||
 | 
					  const mentionedUsernames = getMentionedUsernames(state);
 | 
				
			||||||
 | 
					  const mentionedUsernamesWithDomains = getMentionedDomains(state);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return {
 | 
				
			||||||
 | 
					    needsLeakWarning: (state.getIn(['compose', 'privacy']) === 'private' || state.getIn(['compose', 'privacy']) === 'direct') && mentionedUsernames !== null,
 | 
				
			||||||
 | 
					    mentionedDomains: mentionedUsernamesWithDomains,
 | 
				
			||||||
 | 
					    needsLockWarning: state.getIn(['compose', 'privacy']) === 'private' && !state.getIn(['accounts', state.getIn(['meta', 'me']), 'locked'])
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const WarningWrapper = ({ needsLeakWarning, needsLockWarning, mentionedDomains }) => {
 | 
				
			||||||
 | 
					  if (needsLockWarning) {
 | 
				
			||||||
 | 
					    return <Warning message={<FormattedMessage id='compose_form.lock_disclaimer' defaultMessage='Your account is not {locked}. Anyone can follow you to view your follower-only posts.' values={{ locked: <a href='/settings/profile'><FormattedMessage id='compose_form.lock_disclaimer.lock' defaultMessage='locked' /></a> }} />} />;
 | 
				
			||||||
 | 
					  } else if (needsLeakWarning) {
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      <Warning
 | 
				
			||||||
 | 
					        message={<FormattedMessage
 | 
				
			||||||
 | 
					          id='compose_form.privacy_disclaimer'
 | 
				
			||||||
 | 
					          defaultMessage='Your private status will be delivered to mentioned users on {domains}. Do you trust {domainsCount, plural, one {that server} other {those servers}} to not leak your status?'
 | 
				
			||||||
 | 
					          values={{ domains: <strong>{mentionedDomains.join(', ')}</strong>, domainsCount: mentionedDomains.length }}
 | 
				
			||||||
 | 
					        />}
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return null;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					WarningWrapper.propTypes = {
 | 
				
			||||||
 | 
					  needsLeakWarning: PropTypes.bool,
 | 
				
			||||||
 | 
					  needsLockWarning: PropTypes.bool,
 | 
				
			||||||
 | 
					  mentionedDomains: PropTypes.array.isRequired,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default connect(mapStateToProps)(WarningWrapper);
 | 
				
			||||||
| 
						 | 
					@ -99,7 +99,7 @@ const en = {
 | 
				
			||||||
  "privacy.direct.long": "Post to mentioned users only",
 | 
					  "privacy.direct.long": "Post to mentioned users only",
 | 
				
			||||||
  "privacy.direct.short": "Direct",
 | 
					  "privacy.direct.short": "Direct",
 | 
				
			||||||
  "privacy.private.long": "Post to followers only",
 | 
					  "privacy.private.long": "Post to followers only",
 | 
				
			||||||
  "privacy.private.short": "Private",
 | 
					  "privacy.private.short": "Followers-only",
 | 
				
			||||||
  "privacy.public.long": "Post to public timelines",
 | 
					  "privacy.public.long": "Post to public timelines",
 | 
				
			||||||
  "privacy.public.short": "Public",
 | 
					  "privacy.public.short": "Public",
 | 
				
			||||||
  "privacy.unlisted.long": "Do not show in public timelines",
 | 
					  "privacy.unlisted.long": "Do not show in public timelines",
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -173,7 +173,7 @@
 | 
				
			||||||
  text-align: center;
 | 
					  text-align: center;
 | 
				
			||||||
  overflow: hidden;
 | 
					  overflow: hidden;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  a, .current, .page, .gap {
 | 
					  a, .current, .next, .prev, .page, .gap {
 | 
				
			||||||
    font-size: 14px;
 | 
					    font-size: 14px;
 | 
				
			||||||
    color: $color5;
 | 
					    color: $color5;
 | 
				
			||||||
    font-weight: 500;
 | 
					    font-weight: 500;
 | 
				
			||||||
| 
						 | 
					@ -187,6 +187,7 @@
 | 
				
			||||||
    border-radius: 100px;
 | 
					    border-radius: 100px;
 | 
				
			||||||
    color: $color1;
 | 
					    color: $color1;
 | 
				
			||||||
    cursor: default;
 | 
					    cursor: default;
 | 
				
			||||||
 | 
					    margin: 0 10px;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  .gap {
 | 
					  .gap {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,6 @@
 | 
				
			||||||
@import 'variables';
 | 
					@import 'variables';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.app-body{
 | 
					.app-body {
 | 
				
			||||||
 -webkit-overflow-scrolling: touch;
 | 
					 -webkit-overflow-scrolling: touch;
 | 
				
			||||||
 -ms-overflow-style: -ms-autohiding-scrollbar;
 | 
					 -ms-overflow-style: -ms-autohiding-scrollbar;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -203,18 +203,29 @@
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.compose-form__warning {
 | 
					.compose-form__warning {
 | 
				
			||||||
  color: $color2;
 | 
					  color: darken($color3, 33%);
 | 
				
			||||||
  margin-bottom: 15px;
 | 
					  margin-bottom: 15px;
 | 
				
			||||||
  border: 1px solid $color3;
 | 
					  background: $color3;
 | 
				
			||||||
 | 
					  box-shadow: 0 2px 6px rgba($color8, 0.3);
 | 
				
			||||||
  padding: 8px 10px;
 | 
					  padding: 8px 10px;
 | 
				
			||||||
  border-radius: 4px;
 | 
					  border-radius: 4px;
 | 
				
			||||||
  font-size: 12px;
 | 
					  font-size: 13px;
 | 
				
			||||||
  font-weight: 400;
 | 
					  font-weight: 400;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  strong {
 | 
					  strong {
 | 
				
			||||||
    color: $color5;
 | 
					    color: darken($color3, 33%);
 | 
				
			||||||
    font-weight: 500;
 | 
					    font-weight: 500;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  a {
 | 
				
			||||||
 | 
					    color: darken($color3, 33%);
 | 
				
			||||||
 | 
					    font-weight: 500;
 | 
				
			||||||
 | 
					    text-decoration: underline;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    &:hover, &:active, &:focus {
 | 
				
			||||||
 | 
					      text-decoration: none;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.compose-form__modifiers {
 | 
					.compose-form__modifiers {
 | 
				
			||||||
| 
						 | 
					@ -1766,6 +1777,7 @@ button.icon-button.active i.fa-retweet {
 | 
				
			||||||
  cursor: pointer;
 | 
					  cursor: pointer;
 | 
				
			||||||
  position: relative;
 | 
					  position: relative;
 | 
				
			||||||
  z-index: 2;
 | 
					  z-index: 2;
 | 
				
			||||||
 | 
					  outline: 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  &.active {
 | 
					  &.active {
 | 
				
			||||||
    box-shadow: 0 1px 0 rgba($color4, 0.3);
 | 
					    box-shadow: 0 1px 0 rgba($color4, 0.3);
 | 
				
			||||||
| 
						 | 
					@ -1781,6 +1793,10 @@ button.icon-button.active i.fa-retweet {
 | 
				
			||||||
      display: none;
 | 
					      display: none;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  &:focus, &:active {
 | 
				
			||||||
 | 
					    outline: 0;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.column-header__icon {
 | 
					.column-header__icon {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -269,3 +269,60 @@ code {
 | 
				
			||||||
    font-size: 14px;
 | 
					    font-size: 14px;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.table-form {
 | 
				
			||||||
 | 
					  p {
 | 
				
			||||||
 | 
					    max-width: 400px;
 | 
				
			||||||
 | 
					    margin-bottom: 15px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    strong {
 | 
				
			||||||
 | 
					      font-weight: 500;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .warning {
 | 
				
			||||||
 | 
					    max-width: 400px;
 | 
				
			||||||
 | 
					    box-sizing: border-box;
 | 
				
			||||||
 | 
					    background: rgba($color6, 0.5);
 | 
				
			||||||
 | 
					    color: $color5;
 | 
				
			||||||
 | 
					    text-shadow: 1px 1px 0 rgba($color8, 0.3);
 | 
				
			||||||
 | 
					    box-shadow: 0 2px 6px rgba($color8, 0.4);
 | 
				
			||||||
 | 
					    border-radius: 4px;
 | 
				
			||||||
 | 
					    padding: 10px;
 | 
				
			||||||
 | 
					    margin-bottom: 15px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    a {
 | 
				
			||||||
 | 
					      color: $color5;
 | 
				
			||||||
 | 
					      text-decoration: underline;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      &:hover, &:focus, &:active {
 | 
				
			||||||
 | 
					        text-decoration: none;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    strong {
 | 
				
			||||||
 | 
					      font-weight: 600;
 | 
				
			||||||
 | 
					      display: block;
 | 
				
			||||||
 | 
					      margin-bottom: 5px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      .fa {
 | 
				
			||||||
 | 
					        font-weight: 400;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.action-pagination {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .actions, .pagination {
 | 
				
			||||||
 | 
					    flex: 1 1 auto;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .actions {
 | 
				
			||||||
 | 
					    padding: 30px 0;
 | 
				
			||||||
 | 
					    padding-right: 20px;
 | 
				
			||||||
 | 
					    flex: 0 0 auto;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										28
									
								
								app/controllers/settings/follower_domains_controller.rb
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								app/controllers/settings/follower_domains_controller.rb
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,28 @@
 | 
				
			||||||
 | 
					# frozen_string_literal: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Settings::FollowerDomainsController < ApplicationController
 | 
				
			||||||
 | 
					  layout 'admin'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  before_action :authenticate_user!
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def show
 | 
				
			||||||
 | 
					    @account = current_account
 | 
				
			||||||
 | 
					    @domains = current_account.followers.reorder(nil).group('accounts.domain').select('accounts.domain, count(accounts.id) as accounts_from_domain').page(params[:page]).per(10)
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def update
 | 
				
			||||||
 | 
					    domains = bulk_params[:select] || []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    domains.each do |domain|
 | 
				
			||||||
 | 
					      SoftBlockDomainFollowersWorker.perform_async(current_account.id, domain)
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    redirect_to settings_follower_domains_path, notice: I18n.t('followers.success', count: domains.size)
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def bulk_params
 | 
				
			||||||
 | 
					    params.permit(select: [])
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
| 
						 | 
					@ -135,6 +135,10 @@ class Account < ApplicationRecord
 | 
				
			||||||
    !subscription_expires_at.blank?
 | 
					    !subscription_expires_at.blank?
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def followers_domains
 | 
				
			||||||
 | 
					    followers.reorder(nil).pluck('distinct accounts.domain')
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def favourited?(status)
 | 
					  def favourited?(status)
 | 
				
			||||||
    status.proper.favourites.where(account: self).count.positive?
 | 
					    status.proper.favourites.where(account: self).count.positive?
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										33
									
								
								app/views/settings/follower_domains/show.html.haml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								app/views/settings/follower_domains/show.html.haml
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,33 @@
 | 
				
			||||||
 | 
					- content_for :page_title do
 | 
				
			||||||
 | 
					  = t('settings.followers')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					= form_tag settings_follower_domains_path, method: :patch, class: 'table-form' do
 | 
				
			||||||
 | 
					  - unless @account.locked?
 | 
				
			||||||
 | 
					    .warning
 | 
				
			||||||
 | 
					      %strong
 | 
				
			||||||
 | 
					        = fa_icon('warning')
 | 
				
			||||||
 | 
					        = t('followers.unlocked_warning_title')
 | 
				
			||||||
 | 
					      = t('followers.unlocked_warning_html', lock_link: link_to(t('followers.lock_link'), settings_profile_url))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  %p= t('followers.explanation_html')
 | 
				
			||||||
 | 
					  %p= t('followers.true_privacy_html')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  %table.table
 | 
				
			||||||
 | 
					    %thead
 | 
				
			||||||
 | 
					      %tr
 | 
				
			||||||
 | 
					        %th
 | 
				
			||||||
 | 
					        %th= t('followers.domain')
 | 
				
			||||||
 | 
					        %th= t('followers.followers_count')
 | 
				
			||||||
 | 
					    %tbody
 | 
				
			||||||
 | 
					      - @domains.each do |domain|
 | 
				
			||||||
 | 
					        %tr
 | 
				
			||||||
 | 
					          %td
 | 
				
			||||||
 | 
					            = check_box_tag 'select[]', domain.domain, false, disabled: !@account.locked? unless domain.domain.nil?
 | 
				
			||||||
 | 
					          %td
 | 
				
			||||||
 | 
					            %samp= domain.domain.presence || Rails.configuration.x.local_domain
 | 
				
			||||||
 | 
					          %td= number_with_delimiter domain.accounts_from_domain
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .action-pagination
 | 
				
			||||||
 | 
					    .actions
 | 
				
			||||||
 | 
					      = button_tag t('followers.purge'), type: :submit, class: 'button', disabled: !@account.locked?
 | 
				
			||||||
 | 
					    = paginate @domains
 | 
				
			||||||
| 
						 | 
					@ -7,7 +7,7 @@
 | 
				
			||||||
  .fields-group
 | 
					  .fields-group
 | 
				
			||||||
    = f.input :locale, collection: I18n.available_locales, wrapper: :with_label, include_blank: false, label_method: lambda { |locale| human_locale(locale) }
 | 
					    = f.input :locale, collection: I18n.available_locales, wrapper: :with_label, include_blank: false, label_method: lambda { |locale| human_locale(locale) }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    = f.input :setting_default_privacy, collection: Status.visibilities.keys - ['direct'], wrapper: :with_label, include_blank: false, label_method: lambda { |visibility| I18n.t("statuses.visibilities.#{visibility}") }, required: false, as: :radio_buttons, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li'
 | 
					    = f.input :setting_default_privacy, collection: Status.visibilities.keys - ['direct'], wrapper: :with_label, include_blank: false, label_method: lambda { |visibility| safe_join([I18n.t("statuses.visibilities.#{visibility}"), content_tag(:span, I18n.t("statuses.visibilities.#{visibility}_long"), class: 'hint')]) }, required: false, as: :radio_buttons, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  .fields-group
 | 
					  .fields-group
 | 
				
			||||||
    = f.simple_fields_for :notification_emails, hash_to_object(current_user.settings.notification_emails) do |ff|
 | 
					    = f.simple_fields_for :notification_emails, hash_to_object(current_user.settings.notification_emails) do |ff|
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -4,6 +4,7 @@ require 'csv'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ImportWorker
 | 
					class ImportWorker
 | 
				
			||||||
  include Sidekiq::Worker
 | 
					  include Sidekiq::Worker
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  sidekiq_options queue: 'pull', retry: false
 | 
					  sidekiq_options queue: 'pull', retry: false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  attr_reader :import
 | 
					  attr_reader :import
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -8,12 +8,14 @@ class Pubsubhubbub::DistributionWorker
 | 
				
			||||||
  def perform(stream_entry_id)
 | 
					  def perform(stream_entry_id)
 | 
				
			||||||
    stream_entry = StreamEntry.find(stream_entry_id)
 | 
					    stream_entry = StreamEntry.find(stream_entry_id)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return if stream_entry.hidden?
 | 
					    return if stream_entry.status&.direct_visibility?
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    account = stream_entry.account
 | 
					    account = stream_entry.account
 | 
				
			||||||
    payload = AtomSerializer.render(AtomSerializer.new.feed(account, [stream_entry]))
 | 
					    payload = AtomSerializer.render(AtomSerializer.new.feed(account, [stream_entry]))
 | 
				
			||||||
 | 
					    domains = account.followers_domains
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Subscription.where(account: account).active.select('id, callback_url').find_each do |subscription|
 | 
					    Subscription.where(account: account).active.select('id, callback_url').find_each do |subscription|
 | 
				
			||||||
 | 
					      next unless domains.include?(Addressable::URI.parse(subscription.callback_url).host)
 | 
				
			||||||
      Pubsubhubbub::DeliveryWorker.perform_async(subscription.id, payload)
 | 
					      Pubsubhubbub::DeliveryWorker.perform_async(subscription.id, payload)
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
  rescue ActiveRecord::RecordNotFound
 | 
					  rescue ActiveRecord::RecordNotFound
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										13
									
								
								app/workers/soft_block_domain_followers_worker.rb
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								app/workers/soft_block_domain_followers_worker.rb
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,13 @@
 | 
				
			||||||
 | 
					# frozen_string_literal: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class SoftBlockDomainFollowersWorker
 | 
				
			||||||
 | 
					  include Sidekiq::Worker
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  sidekiq_options queue: 'pull'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def perform(account_id, domain)
 | 
				
			||||||
 | 
					    Account.find(account_id).followers.where(domain: domain).pluck(:id).each do |follower_id|
 | 
				
			||||||
 | 
					      SoftBlockWorker.perform_async(account_id, follower_id)
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
							
								
								
									
										17
									
								
								app/workers/soft_block_worker.rb
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								app/workers/soft_block_worker.rb
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,17 @@
 | 
				
			||||||
 | 
					# frozen_string_literal: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class SoftBlockWorker
 | 
				
			||||||
 | 
					  include Sidekiq::Worker
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  sidekiq_options queue: 'pull'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def perform(account_id, target_account_id)
 | 
				
			||||||
 | 
					    account        = Account.find(account_id)
 | 
				
			||||||
 | 
					    target_account = Account.find(target_account_id)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    BlockService.new.call(account, target_account)
 | 
				
			||||||
 | 
					    UnblockService.new.call(account, target_account)
 | 
				
			||||||
 | 
					  rescue ActiveRecord::RecordNotFound
 | 
				
			||||||
 | 
					    true
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
| 
						 | 
					@ -41,14 +41,14 @@ en:
 | 
				
			||||||
    remote_follow: Remote follow
 | 
					    remote_follow: Remote follow
 | 
				
			||||||
    unfollow: Unfollow
 | 
					    unfollow: Unfollow
 | 
				
			||||||
  activitypub:
 | 
					  activitypub:
 | 
				
			||||||
    outbox:
 | 
					 | 
				
			||||||
      name: "%{account_name}'s Outbox"
 | 
					 | 
				
			||||||
      summary: "A collection of activities from user %{account_name}."
 | 
					 | 
				
			||||||
    activity:
 | 
					    activity:
 | 
				
			||||||
      create:
 | 
					 | 
				
			||||||
        name: "%{account_name} created a note."
 | 
					 | 
				
			||||||
      announce:
 | 
					      announce:
 | 
				
			||||||
        name: "%{account_name} announced an activity."
 | 
					        name: "%{account_name} announced an activity."
 | 
				
			||||||
 | 
					      create:
 | 
				
			||||||
 | 
					        name: "%{account_name} created a note."
 | 
				
			||||||
 | 
					    outbox:
 | 
				
			||||||
 | 
					      name: "%{account_name}'s Outbox"
 | 
				
			||||||
 | 
					      summary: A collection of activities from user %{account_name}.
 | 
				
			||||||
  admin:
 | 
					  admin:
 | 
				
			||||||
    accounts:
 | 
					    accounts:
 | 
				
			||||||
      are_you_sure: Are you sure?
 | 
					      are_you_sure: Are you sure?
 | 
				
			||||||
| 
						 | 
					@ -227,6 +227,18 @@ en:
 | 
				
			||||||
    follows: You follow
 | 
					    follows: You follow
 | 
				
			||||||
    mutes: You mute
 | 
					    mutes: You mute
 | 
				
			||||||
    storage: Media storage
 | 
					    storage: Media storage
 | 
				
			||||||
 | 
					  followers:
 | 
				
			||||||
 | 
					    domain: Domain
 | 
				
			||||||
 | 
					    explanation_html: If you want to ensure the privacy of your statuses, you must be aware of who is following you. <strong>Your private statuses are delivered to all instances where you have followers</strong>. You may wish to review them, and remove followers if you do not trust your privacy to be respected by the staff or software of those instances.
 | 
				
			||||||
 | 
					    followers_count: Number of followers
 | 
				
			||||||
 | 
					    lock_link: Lock your account
 | 
				
			||||||
 | 
					    purge: Remove from followers
 | 
				
			||||||
 | 
					    success:
 | 
				
			||||||
 | 
					      one: In the process of soft-blocking followers from one domain...
 | 
				
			||||||
 | 
					      other: In the process of soft-blocking followers from %{count} domains...
 | 
				
			||||||
 | 
					    true_privacy_html: Please mind that <strong>true privacy can only be achieved with end-to-end encryption</strong>.
 | 
				
			||||||
 | 
					    unlocked_warning_html: Anyone can follow you to immediately view your private statuses. %{lock_link} to be able to review and reject followers.
 | 
				
			||||||
 | 
					    unlocked_warning_title: Your account is not locked
 | 
				
			||||||
  generic:
 | 
					  generic:
 | 
				
			||||||
    changes_saved_msg: Changes successfully saved!
 | 
					    changes_saved_msg: Changes successfully saved!
 | 
				
			||||||
    powered_by: powered by %{link}
 | 
					    powered_by: powered by %{link}
 | 
				
			||||||
| 
						 | 
					@ -286,6 +298,7 @@ en:
 | 
				
			||||||
    back: Back to Mastodon
 | 
					    back: Back to Mastodon
 | 
				
			||||||
    edit_profile: Edit profile
 | 
					    edit_profile: Edit profile
 | 
				
			||||||
    export: Data export
 | 
					    export: Data export
 | 
				
			||||||
 | 
					    followers: Authorized followers
 | 
				
			||||||
    import: Import
 | 
					    import: Import
 | 
				
			||||||
    preferences: Preferences
 | 
					    preferences: Preferences
 | 
				
			||||||
    settings: Settings
 | 
					    settings: Settings
 | 
				
			||||||
| 
						 | 
					@ -295,9 +308,12 @@ en:
 | 
				
			||||||
    over_character_limit: character limit of %{max} exceeded
 | 
					    over_character_limit: character limit of %{max} exceeded
 | 
				
			||||||
    show_more: Show more
 | 
					    show_more: Show more
 | 
				
			||||||
    visibilities:
 | 
					    visibilities:
 | 
				
			||||||
      private: Only show to followers
 | 
					      private: Followers-only
 | 
				
			||||||
 | 
					      private_long: Only show to followers
 | 
				
			||||||
      public: Public
 | 
					      public: Public
 | 
				
			||||||
      unlisted: Public, but do not display on the public timeline
 | 
					      public_long: Everyone can see
 | 
				
			||||||
 | 
					      unlisted: Unlisted
 | 
				
			||||||
 | 
					      unlisted_long: Everyone can see, but not listed on public timelines
 | 
				
			||||||
  stream_entries:
 | 
					  stream_entries:
 | 
				
			||||||
    click_to_show: Click to show
 | 
					    click_to_show: Click to show
 | 
				
			||||||
    reblogged: boosted
 | 
					    reblogged: boosted
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -39,6 +39,48 @@ nl:
 | 
				
			||||||
    posts: Berichten
 | 
					    posts: Berichten
 | 
				
			||||||
    remote_follow: Extern volgen
 | 
					    remote_follow: Extern volgen
 | 
				
			||||||
    unfollow: Ontvolgen
 | 
					    unfollow: Ontvolgen
 | 
				
			||||||
 | 
					  admin:
 | 
				
			||||||
 | 
					    settings:
 | 
				
			||||||
 | 
					      click_to_edit: Klik om te bewerken
 | 
				
			||||||
 | 
					      contact_information:
 | 
				
			||||||
 | 
					        email: Vul een openbaar gebruikt e-mailadres in
 | 
				
			||||||
 | 
					        label: Contactgegevens
 | 
				
			||||||
 | 
					        username: Vul een gebruikersnaam in
 | 
				
			||||||
 | 
					      registrations:
 | 
				
			||||||
 | 
					        closed_message:
 | 
				
			||||||
 | 
					          desc_html: Wordt op de voorpagina weergegeven wanneer registratie van nieuwe accounts is uitgeschakeld<br>En ook hier kan je HTML gebruiken
 | 
				
			||||||
 | 
					          title: Bericht wanneer registratie is uitgeschakeld
 | 
				
			||||||
 | 
					        open:
 | 
				
			||||||
 | 
					          disabled: Uitgeschakeld
 | 
				
			||||||
 | 
					          enabled: Ingeschakeld
 | 
				
			||||||
 | 
					          title: Open registratie
 | 
				
			||||||
 | 
					      setting: Instelling
 | 
				
			||||||
 | 
					      site_description:
 | 
				
			||||||
 | 
					        desc_html: Dit wordt als een alinea op de voorpagina getoond en gebruikt als meta-tag in de paginabron.<br>Je kan HTML gebruiken, zoals <code><a></code> en <code><em></code>.
 | 
				
			||||||
 | 
					        title: Omschrijving Mastodon-server
 | 
				
			||||||
 | 
					      site_description_extended:
 | 
				
			||||||
 | 
					        desc_html: Wordt op de uitgebreide informatiepagina weergegeven<br>Je kan ook hier HTML gebruiken
 | 
				
			||||||
 | 
					        title: Uitgebreide omschrijving Mastodon-server
 | 
				
			||||||
 | 
					      site_title: Naam Mastodon-server
 | 
				
			||||||
 | 
					      title: Server-instellingen
 | 
				
			||||||
 | 
					  admin.reports:
 | 
				
			||||||
 | 
					    comment:
 | 
				
			||||||
 | 
					      label: Opmerking
 | 
				
			||||||
 | 
					      none: Geen
 | 
				
			||||||
 | 
					    delete: Verwijderen
 | 
				
			||||||
 | 
					    id: ID
 | 
				
			||||||
 | 
					    mark_as_resolved: Markeer als opgelost
 | 
				
			||||||
 | 
					    report: 'Gerapporteerde toot #%{id}'
 | 
				
			||||||
 | 
					    reported_account: Gerapporteerde account
 | 
				
			||||||
 | 
					    reported_by: Gerapporteerd door
 | 
				
			||||||
 | 
					    resolved: Opgelost
 | 
				
			||||||
 | 
					    silence_account: Account stilzwijgen
 | 
				
			||||||
 | 
					    status: Toot
 | 
				
			||||||
 | 
					    suspend_account: Account blokkeren
 | 
				
			||||||
 | 
					    target: Target
 | 
				
			||||||
 | 
					    title: Gerapporteerde toots
 | 
				
			||||||
 | 
					    unresolved: Onopgelost
 | 
				
			||||||
 | 
					    view: Weergeven
 | 
				
			||||||
  application_mailer:
 | 
					  application_mailer:
 | 
				
			||||||
    settings: 'E-mailvoorkeuren wijzigen: %{link}'
 | 
					    settings: 'E-mailvoorkeuren wijzigen: %{link}'
 | 
				
			||||||
    signature: Mastodon-meldingen van %{instance}
 | 
					    signature: Mastodon-meldingen van %{instance}
 | 
				
			||||||
| 
						 | 
					@ -74,6 +116,12 @@ nl:
 | 
				
			||||||
      x_minutes: "%{count}m"
 | 
					      x_minutes: "%{count}m"
 | 
				
			||||||
      x_months: "%{count}ma"
 | 
					      x_months: "%{count}ma"
 | 
				
			||||||
      x_seconds: "%{count}s"
 | 
					      x_seconds: "%{count}s"
 | 
				
			||||||
 | 
					  errors:
 | 
				
			||||||
 | 
					    '404': De pagina waarnaar jij op zoek bent bestaat niet.
 | 
				
			||||||
 | 
					    '410': De pagina waarnaar jij op zoek bent bestaat niet meer.
 | 
				
			||||||
 | 
					    '422':
 | 
				
			||||||
 | 
					      content: Veiligheidsverificatie mislukt. Blokkeer je toevallig cookies?
 | 
				
			||||||
 | 
					      title: Veiligheidsverificatie mislukt
 | 
				
			||||||
  exports:
 | 
					  exports:
 | 
				
			||||||
    blocks: Jij blokkeert
 | 
					    blocks: Jij blokkeert
 | 
				
			||||||
    csv: CSV
 | 
					    csv: CSV
 | 
				
			||||||
| 
						 | 
					@ -161,52 +209,3 @@ nl:
 | 
				
			||||||
  users:
 | 
					  users:
 | 
				
			||||||
    invalid_email: E-mailadres is ongeldig
 | 
					    invalid_email: E-mailadres is ongeldig
 | 
				
			||||||
    invalid_otp_token: Ongeldige tweestaps-aanmeldcode
 | 
					    invalid_otp_token: Ongeldige tweestaps-aanmeldcode
 | 
				
			||||||
  errors:
 | 
					 | 
				
			||||||
      404: De pagina waarnaar jij op zoek bent bestaat niet.
 | 
					 | 
				
			||||||
      410: De pagina waarnaar jij op zoek bent bestaat niet meer.
 | 
					 | 
				
			||||||
      422:
 | 
					 | 
				
			||||||
        title: Veiligheidsverificatie mislukt
 | 
					 | 
				
			||||||
        content: Veiligheidsverificatie mislukt. Blokkeer je toevallig cookies?
 | 
					 | 
				
			||||||
  admin.reports:
 | 
					 | 
				
			||||||
    title: Gerapporteerde toots
 | 
					 | 
				
			||||||
    status: Toot
 | 
					 | 
				
			||||||
    unresolved: Onopgelost
 | 
					 | 
				
			||||||
    resolved: Opgelost
 | 
					 | 
				
			||||||
    id: ID
 | 
					 | 
				
			||||||
    target: Target
 | 
					 | 
				
			||||||
    reported_by: Gerapporteerd door
 | 
					 | 
				
			||||||
    comment:
 | 
					 | 
				
			||||||
      label: Opmerking
 | 
					 | 
				
			||||||
      none: Geen
 | 
					 | 
				
			||||||
    view: Weergeven
 | 
					 | 
				
			||||||
    report: 'Gerapporteerde toot #%{id}'
 | 
					 | 
				
			||||||
    delete: Verwijderen
 | 
					 | 
				
			||||||
    reported_account: Gerapporteerde account
 | 
					 | 
				
			||||||
    reported_by: Gerapporteerd door
 | 
					 | 
				
			||||||
    silence_account: Account stilzwijgen
 | 
					 | 
				
			||||||
    suspend_account: Account blokkeren
 | 
					 | 
				
			||||||
    mark_as_resolved: Markeer als opgelost
 | 
					 | 
				
			||||||
  admin:
 | 
					 | 
				
			||||||
    settings:
 | 
					 | 
				
			||||||
      title: Server-instellingen
 | 
					 | 
				
			||||||
      setting: Instelling
 | 
					 | 
				
			||||||
      click_to_edit: Klik om te bewerken
 | 
					 | 
				
			||||||
      contact_information:
 | 
					 | 
				
			||||||
        label: Contactgegevens
 | 
					 | 
				
			||||||
        username: Vul een gebruikersnaam in
 | 
					 | 
				
			||||||
        email: Vul een openbaar gebruikt e-mailadres in
 | 
					 | 
				
			||||||
      site_title: Naam Mastodon-server
 | 
					 | 
				
			||||||
      site_description:
 | 
					 | 
				
			||||||
        title: Omschrijving Mastodon-server
 | 
					 | 
				
			||||||
        desc_html: "Dit wordt als een alinea op de voorpagina getoond en gebruikt als meta-tag in de paginabron.<br>Je kan HTML gebruiken, zoals <code><a></code> en <code><em></code>."
 | 
					 | 
				
			||||||
      site_description_extended:
 | 
					 | 
				
			||||||
        title: Uitgebreide omschrijving Mastodon-server
 | 
					 | 
				
			||||||
        desc_html: "Wordt op de uitgebreide informatiepagina weergegeven<br>Je kan ook hier HTML gebruiken"
 | 
					 | 
				
			||||||
      registrations:
 | 
					 | 
				
			||||||
        open:
 | 
					 | 
				
			||||||
          title: Open registratie
 | 
					 | 
				
			||||||
          enabled: Ingeschakeld
 | 
					 | 
				
			||||||
          disabled: Uitgeschakeld
 | 
					 | 
				
			||||||
        closed_message:
 | 
					 | 
				
			||||||
          title: Bericht wanneer registratie is uitgeschakeld
 | 
					 | 
				
			||||||
          desc_html: "Wordt op de voorpagina weergegeven wanneer registratie van nieuwe accounts is uitgeschakeld<br>En ook hier kan je HTML gebruiken"
 | 
					 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -22,8 +22,8 @@ pt-BR:
 | 
				
			||||||
    features_headline: O que torna Mastodon diferente
 | 
					    features_headline: O que torna Mastodon diferente
 | 
				
			||||||
    get_started: Comece aqui
 | 
					    get_started: Comece aqui
 | 
				
			||||||
    links: Links
 | 
					    links: Links
 | 
				
			||||||
    source_code: Source code
 | 
					 | 
				
			||||||
    other_instances: Outras instâncias
 | 
					    other_instances: Outras instâncias
 | 
				
			||||||
 | 
					    source_code: Source code
 | 
				
			||||||
    terms: Termos
 | 
					    terms: Termos
 | 
				
			||||||
    user_count_after: usuários
 | 
					    user_count_after: usuários
 | 
				
			||||||
    user_count_before: Lugar de
 | 
					    user_count_before: Lugar de
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -23,7 +23,7 @@ en:
 | 
				
			||||||
        email: E-mail address
 | 
					        email: E-mail address
 | 
				
			||||||
        header: Header
 | 
					        header: Header
 | 
				
			||||||
        locale: Language
 | 
					        locale: Language
 | 
				
			||||||
        locked: Make account private
 | 
					        locked: Lock account
 | 
				
			||||||
        new_password: New password
 | 
					        new_password: New password
 | 
				
			||||||
        note: Bio
 | 
					        note: Bio
 | 
				
			||||||
        otp_attempt: Two-factor code
 | 
					        otp_attempt: Two-factor code
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -30,8 +30,8 @@ zh-CN:
 | 
				
			||||||
    user_count_before: 这里共注册有
 | 
					    user_count_before: 这里共注册有
 | 
				
			||||||
  accounts:
 | 
					  accounts:
 | 
				
			||||||
    follow: 关注
 | 
					    follow: 关注
 | 
				
			||||||
    followers: 粉丝 # "Fans"
 | 
					    followers: 粉丝
 | 
				
			||||||
    following: 关注 # "Follow"
 | 
					    following: 关注
 | 
				
			||||||
    nothing_here: 神马都没有!
 | 
					    nothing_here: 神马都没有!
 | 
				
			||||||
    people_followed_by: 正关注
 | 
					    people_followed_by: 正关注
 | 
				
			||||||
    people_who_follow: 粉丝
 | 
					    people_who_follow: 粉丝
 | 
				
			||||||
| 
						 | 
					@ -80,15 +80,14 @@ zh-CN:
 | 
				
			||||||
      web: 用户页面
 | 
					      web: 用户页面
 | 
				
			||||||
    domain_blocks:
 | 
					    domain_blocks:
 | 
				
			||||||
      add_new: 添加
 | 
					      add_new: 添加
 | 
				
			||||||
      domain: 域名阻隔
 | 
					 | 
				
			||||||
      created_msg: 正处理域名阻隔
 | 
					      created_msg: 正处理域名阻隔
 | 
				
			||||||
      destroyed_msg: 已撤销域名阻隔
 | 
					      destroyed_msg: 已撤销域名阻隔
 | 
				
			||||||
 | 
					      domain: 域名阻隔
 | 
				
			||||||
      new:
 | 
					      new:
 | 
				
			||||||
        create: 添加域名阻隔
 | 
					        create: 添加域名阻隔
 | 
				
			||||||
        hint: 「域名阻隔」不会隔绝该域名用户的嘟账户入本站数据库,但会嘟文抵达后,自动套用特定的审批操作。
 | 
					        hint: "「域名阻隔」不会隔绝该域名用户的嘟账户入本站数据库,但会嘟文抵达后,自动套用特定的审批操作。"
 | 
				
			||||||
        severity:
 | 
					        severity:
 | 
				
			||||||
          desc_html: 「<strong>自动静音</strong>」令该域名用户的嘟文,设为只对关注者显示,没有关注的人会看不到。
 | 
					          desc_html: "「<strong>自动静音</strong>」令该域名用户的嘟文,设为只对关注者显示,没有关注的人会看不到。 「<strong>自动除名</strong>」会自动将该域名用户的嘟文、媒体文件、个人资料自本服务站删除。"
 | 
				
			||||||
            「<strong>自动除名</strong>」会自动将该域名用户的嘟文、媒体文件、个人资料自本服务站删除。
 | 
					 | 
				
			||||||
          silence: 自动静音
 | 
					          silence: 自动静音
 | 
				
			||||||
          suspend: 自动除名
 | 
					          suspend: 自动除名
 | 
				
			||||||
        title: 添加域名阻隔
 | 
					        title: 添加域名阻隔
 | 
				
			||||||
| 
						 | 
					@ -99,10 +98,8 @@ zh-CN:
 | 
				
			||||||
        suspend: 自动除名
 | 
					        suspend: 自动除名
 | 
				
			||||||
      severity: 阻隔程度
 | 
					      severity: 阻隔程度
 | 
				
			||||||
      show:
 | 
					      show:
 | 
				
			||||||
        # It turns out that Chinese only uses an "other"
 | 
					 | 
				
			||||||
        # Well, we don't have these -s magic anyway...
 | 
					 | 
				
			||||||
        affected_accounts:
 | 
					        affected_accounts:
 | 
				
			||||||
          other: "数据库中有%{count}个账户受影响"
 | 
					          other: 数据库中有%{count}个账户受影响
 | 
				
			||||||
        retroactive:
 | 
					        retroactive:
 | 
				
			||||||
          silence: 对此域名的所有账户取消静音
 | 
					          silence: 对此域名的所有账户取消静音
 | 
				
			||||||
          suspend: 对此域名的所有账户取消除名
 | 
					          suspend: 对此域名的所有账户取消除名
 | 
				
			||||||
| 
						 | 
					@ -147,8 +144,7 @@ zh-CN:
 | 
				
			||||||
        username: 输入用户名称
 | 
					        username: 输入用户名称
 | 
				
			||||||
      registrations:
 | 
					      registrations:
 | 
				
			||||||
        closed_message:
 | 
					        closed_message:
 | 
				
			||||||
          desc_html: 当本站暂停接受注册时,会显示这个消息。<br/>
 | 
					          desc_html: 当本站暂停接受注册时,会显示这个消息。<br/> 可使用 HTML
 | 
				
			||||||
            可使用 HTML
 | 
					 | 
				
			||||||
          title: 暂停注册消息
 | 
					          title: 暂停注册消息
 | 
				
			||||||
        open:
 | 
					        open:
 | 
				
			||||||
          disabled: 停用
 | 
					          disabled: 停用
 | 
				
			||||||
| 
						 | 
					@ -187,11 +183,10 @@ zh-CN:
 | 
				
			||||||
    title: 关注 %{acct}
 | 
					    title: 关注 %{acct}
 | 
				
			||||||
  datetime:
 | 
					  datetime:
 | 
				
			||||||
    distance_in_words:
 | 
					    distance_in_words:
 | 
				
			||||||
      # Ditching "about" as in en
 | 
					 | 
				
			||||||
      about_x_hours: "%{count} 小时"
 | 
					      about_x_hours: "%{count} 小时"
 | 
				
			||||||
      about_x_months: "%{count} 个月"
 | 
					      about_x_months: "%{count} 个月"
 | 
				
			||||||
      about_x_years: "%{count} 年"
 | 
					      about_x_years: "%{count} 年"
 | 
				
			||||||
      almost_x_years: "接近 %{count} 年"
 | 
					      almost_x_years: 接近 %{count} 年
 | 
				
			||||||
      half_a_minute: 刚刚
 | 
					      half_a_minute: 刚刚
 | 
				
			||||||
      less_than_x_minutes: "%{count} 分不到"
 | 
					      less_than_x_minutes: "%{count} 分不到"
 | 
				
			||||||
      less_than_x_seconds: 刚刚
 | 
					      less_than_x_seconds: 刚刚
 | 
				
			||||||
| 
						 | 
					@ -232,7 +227,6 @@ zh-CN:
 | 
				
			||||||
      body: 自从你在%{since}使用%{instance}以后,错过了这些嘟嘟滴滴:
 | 
					      body: 自从你在%{since}使用%{instance}以后,错过了这些嘟嘟滴滴:
 | 
				
			||||||
      mention: "%{name} 在此提及了你︰"
 | 
					      mention: "%{name} 在此提及了你︰"
 | 
				
			||||||
      new_followers_summary:
 | 
					      new_followers_summary:
 | 
				
			||||||
        # censorship note: Better not mention "don't move your chicken", even if it's a phonetic joke
 | 
					 | 
				
			||||||
        one: 有人关注你了!耶!
 | 
					        one: 有人关注你了!耶!
 | 
				
			||||||
        other: 有 %{count} 个人关注了你!别激动!
 | 
					        other: 有 %{count} 个人关注了你!别激动!
 | 
				
			||||||
      subject:
 | 
					      subject:
 | 
				
			||||||
| 
						 | 
					@ -271,7 +265,6 @@ zh-CN:
 | 
				
			||||||
    settings: 设置
 | 
					    settings: 设置
 | 
				
			||||||
    two_factor_authentication: 两步认证
 | 
					    two_factor_authentication: 两步认证
 | 
				
			||||||
  statuses:
 | 
					  statuses:
 | 
				
			||||||
    # Hey, this is already in a web browser!
 | 
					 | 
				
			||||||
    open_in_web: 打开网页
 | 
					    open_in_web: 打开网页
 | 
				
			||||||
    over_character_limit: 超过了 %{max} 字的限制
 | 
					    over_character_limit: 超过了 %{max} 字的限制
 | 
				
			||||||
    show_more: 显示更多
 | 
					    show_more: 显示更多
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -12,6 +12,7 @@ SimpleNavigation::Configuration.run do |navigation|
 | 
				
			||||||
      settings.item :import, safe_join([fa_icon('cloud-upload fw'), t('settings.import')]), settings_import_url
 | 
					      settings.item :import, safe_join([fa_icon('cloud-upload fw'), t('settings.import')]), settings_import_url
 | 
				
			||||||
      settings.item :export, safe_join([fa_icon('cloud-download fw'), t('settings.export')]), settings_export_url
 | 
					      settings.item :export, safe_join([fa_icon('cloud-download fw'), t('settings.export')]), settings_export_url
 | 
				
			||||||
      settings.item :authorized_apps, safe_join([fa_icon('list fw'), t('settings.authorized_apps')]), oauth_authorized_applications_url
 | 
					      settings.item :authorized_apps, safe_join([fa_icon('list fw'), t('settings.authorized_apps')]), oauth_authorized_applications_url
 | 
				
			||||||
 | 
					      settings.item :follower_domains, safe_join([fa_icon('users fw'), t('settings.followers')]), settings_follower_domains_url
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    primary.item :admin, safe_join([fa_icon('cogs fw'), t('admin.title')]), admin_reports_url, if: proc { current_user.admin? } do |admin|
 | 
					    primary.item :admin, safe_join([fa_icon('cogs fw'), t('admin.title')]), admin_reports_url, if: proc { current_user.admin? } do |admin|
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -63,6 +63,8 @@ Rails.application.routes.draw do
 | 
				
			||||||
      resources :recovery_codes, only: [:create]
 | 
					      resources :recovery_codes, only: [:create]
 | 
				
			||||||
      resource :confirmation, only: [:new, :create]
 | 
					      resource :confirmation, only: [:new, :create]
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    resource :follower_domains, only: [:show, :update]
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  resources :media, only: [:show]
 | 
					  resources :media, only: [:show]
 | 
				
			||||||
| 
						 | 
					@ -109,9 +111,7 @@ Rails.application.routes.draw do
 | 
				
			||||||
    # ActivityPub
 | 
					    # ActivityPub
 | 
				
			||||||
    namespace :activitypub do
 | 
					    namespace :activitypub do
 | 
				
			||||||
      get '/users/:id/outbox', to: 'outbox#show', as: :outbox
 | 
					      get '/users/:id/outbox', to: 'outbox#show', as: :outbox
 | 
				
			||||||
 | 
					 | 
				
			||||||
      get '/statuses/:id', to: 'activities#show_status', as: :status
 | 
					      get '/statuses/:id', to: 'activities#show_status', as: :status
 | 
				
			||||||
 | 
					 | 
				
			||||||
      resources :notes, only: [:show]
 | 
					      resources :notes, only: [:show]
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,34 @@
 | 
				
			||||||
 | 
					require 'rails_helper'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe Settings::FollowerDomainsController do
 | 
				
			||||||
 | 
					  let(:user) { Fabricate(:user) }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  before do
 | 
				
			||||||
 | 
					    sign_in user, scope: :user
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  describe 'GET #show' do
 | 
				
			||||||
 | 
					    it 'returns http success' do
 | 
				
			||||||
 | 
					      get :show
 | 
				
			||||||
 | 
					      expect(response).to have_http_status(:success)
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  describe 'PATCH #update' do
 | 
				
			||||||
 | 
					    let(:poopfeast) { Fabricate(:account, username: 'poopfeast', domain: 'example.com', salmon_url: 'http://example.com/salmon') }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    before do
 | 
				
			||||||
 | 
					      stub_request(:post, 'http://example.com/salmon').to_return(status: 200)
 | 
				
			||||||
 | 
					      poopfeast.follow!(user.account)
 | 
				
			||||||
 | 
					      patch :update, params: { select: ['example.com'] }
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    it 'redirects back to followers page' do
 | 
				
			||||||
 | 
					      expect(response).to redirect_to(settings_follower_domains_path)
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    it 'soft-blocks followers from selected domains' do
 | 
				
			||||||
 | 
					      expect(poopfeast.following?(user.account)).to be false
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
| 
						 | 
					@ -2,6 +2,7 @@ require 'rails_helper'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
describe Settings::PreferencesController do
 | 
					describe Settings::PreferencesController do
 | 
				
			||||||
  let(:user) { Fabricate(:user) }
 | 
					  let(:user) { Fabricate(:user) }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  before do
 | 
					  before do
 | 
				
			||||||
    sign_in user, scope: :user
 | 
					    sign_in user, scope: :user
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
| 
						 | 
					@ -9,13 +10,12 @@ describe Settings::PreferencesController do
 | 
				
			||||||
  describe 'GET #show' do
 | 
					  describe 'GET #show' do
 | 
				
			||||||
    it 'returns http success' do
 | 
					    it 'returns http success' do
 | 
				
			||||||
      get :show
 | 
					      get :show
 | 
				
			||||||
 | 
					 | 
				
			||||||
      expect(response).to have_http_status(:success)
 | 
					      expect(response).to have_http_status(:success)
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  describe 'PUT #update' do
 | 
					  describe 'PUT #update' do
 | 
				
			||||||
    it 'udpates the user record' do
 | 
					    it 'updates the user record' do
 | 
				
			||||||
      put :update, params: { user: { locale: 'en' } }
 | 
					      put :update, params: { user: { locale: 'en' } }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      expect(response).to redirect_to(settings_preferences_path)
 | 
					      expect(response).to redirect_to(settings_preferences_path)
 | 
				
			||||||
| 
						 | 
					@ -31,7 +31,7 @@ describe Settings::PreferencesController do
 | 
				
			||||||
        user: {
 | 
					        user: {
 | 
				
			||||||
          setting_boost_modal: '1',
 | 
					          setting_boost_modal: '1',
 | 
				
			||||||
          notification_emails: { follow: '1' },
 | 
					          notification_emails: { follow: '1' },
 | 
				
			||||||
          interactions: { must_be_follower: '0' }
 | 
					          interactions: { must_be_follower: '0' },
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -12,7 +12,7 @@ require 'capybara/rspec'
 | 
				
			||||||
Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f }
 | 
					Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
ActiveRecord::Migration.maintain_test_schema!
 | 
					ActiveRecord::Migration.maintain_test_schema!
 | 
				
			||||||
WebMock.disable_net_connect!(allow: 'localhost:7575')
 | 
					WebMock.disable_net_connect!
 | 
				
			||||||
Sidekiq::Testing.inline!
 | 
					Sidekiq::Testing.inline!
 | 
				
			||||||
 | 
					
 | 
				
			||||||
RSpec.configure do |config|
 | 
					RSpec.configure do |config|
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		
		Reference in a new issue