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,33 +11,20 @@ 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 => ({
|
||||||
|
text: state.getIn(['compose', 'text']),
|
||||||
const getMentionedDomains = createSelector(getMentionedUsernames, mentionedUsernamesWithDomains => {
|
suggestion_token: state.getIn(['compose', 'suggestion_token']),
|
||||||
return mentionedUsernamesWithDomains !== null ? [...new Set(mentionedUsernamesWithDomains.map(item => item.split('@')[2]))] : [];
|
suggestions: state.getIn(['compose', 'suggestions']),
|
||||||
|
spoiler: state.getIn(['compose', 'spoiler']),
|
||||||
|
spoiler_text: state.getIn(['compose', 'spoiler_text']),
|
||||||
|
privacy: state.getIn(['compose', 'privacy']),
|
||||||
|
focusDate: state.getIn(['compose', 'focusDate']),
|
||||||
|
preselectDate: state.getIn(['compose', 'preselectDate']),
|
||||||
|
is_submitting: state.getIn(['compose', 'is_submitting']),
|
||||||
|
is_uploading: state.getIn(['compose', 'is_uploading']),
|
||||||
|
me: state.getIn(['compose', 'me'])
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapStateToProps = (state, props) => {
|
|
||||||
const mentionedUsernames = getMentionedUsernames(state);
|
|
||||||
const mentionedUsernamesWithDomains = getMentionedDomains(state);
|
|
||||||
|
|
||||||
return {
|
|
||||||
text: state.getIn(['compose', 'text']),
|
|
||||||
suggestion_token: state.getIn(['compose', 'suggestion_token']),
|
|
||||||
suggestions: state.getIn(['compose', 'suggestions']),
|
|
||||||
spoiler: state.getIn(['compose', 'spoiler']),
|
|
||||||
spoiler_text: state.getIn(['compose', 'spoiler_text']),
|
|
||||||
privacy: state.getIn(['compose', 'privacy']),
|
|
||||||
focusDate: state.getIn(['compose', 'focusDate']),
|
|
||||||
preselectDate: state.getIn(['compose', 'preselectDate']),
|
|
||||||
is_submitting: state.getIn(['compose', 'is_submitting']),
|
|
||||||
is_uploading: state.getIn(['compose', 'is_uploading']),
|
|
||||||
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) => ({
|
||||||
|
|
||||||
onChange (text) {
|
onChange (text) {
|
||||||
|
|
|
@ -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 {
|
||||||
|
@ -1619,7 +1630,7 @@ a.status__content__spoiler-link {
|
||||||
}
|
}
|
||||||
|
|
||||||
.character-counter {
|
.character-counter {
|
||||||
cursor: default;
|
cursor: default;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1667,7 +1678,7 @@ a.status__content__spoiler-link {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@import 'boost';
|
@import 'boost';
|
||||||
|
|
||||||
button.icon-button i.fa-retweet {
|
button.icon-button i.fa-retweet {
|
||||||
|
@ -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…
Reference in a new issue