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 UploadFormContainer from '../containers/upload_form_container';
|
||||
import TextIconButton from './text_icon_button';
|
||||
import WarningContainer from '../containers/warning_container';
|
||||
|
||||
const messages = defineMessages({
|
||||
placeholder: { id: 'compose_form.placeholder', defaultMessage: 'What is on your mind?' },
|
||||
|
@ -116,26 +117,13 @@ class ComposeForm extends React.PureComponent {
|
|||
}
|
||||
|
||||
render () {
|
||||
const { intl, needsPrivacyWarning, mentionedDomains, onPaste } = this.props;
|
||||
const { intl, onPaste } = this.props;
|
||||
const disabled = this.props.is_submitting;
|
||||
const text = [this.props.spoiler_text, this.props.text].join('');
|
||||
|
||||
let publishText = '';
|
||||
let privacyWarning = '';
|
||||
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') {
|
||||
publishText = <span className='compose-form__publish-private'><i className='fa fa-lock' /> {intl.formatMessage(messages.publish)}</span>;
|
||||
} else {
|
||||
|
@ -150,7 +138,7 @@ class ComposeForm extends React.PureComponent {
|
|||
</div>
|
||||
</Collapsable>
|
||||
|
||||
{privacyWarning}
|
||||
<WarningContainer />
|
||||
|
||||
<ReplyIndicatorContainer />
|
||||
|
||||
|
@ -208,8 +196,6 @@ ComposeForm.propTypes = {
|
|||
is_submitting: PropTypes.bool,
|
||||
is_uploading: PropTypes.bool,
|
||||
me: PropTypes.number,
|
||||
needsPrivacyWarning: PropTypes.bool,
|
||||
mentionedDomains: PropTypes.array.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onSubmit: 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' },
|
||||
unlisted_short: { id: 'privacy.unlisted.short', defaultMessage: 'Unlisted' },
|
||||
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' },
|
||||
direct_short: { id: 'privacy.direct.short', defaultMessage: 'Direct' },
|
||||
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 ComposeForm from '../components/compose_form';
|
||||
import { uploadCompose } from '../../../actions/compose';
|
||||
import { createSelector } from 'reselect';
|
||||
import {
|
||||
changeCompose,
|
||||
submitCompose,
|
||||
|
@ -12,33 +11,20 @@ import {
|
|||
insertEmojiCompose
|
||||
} from '../../../actions/compose';
|
||||
|
||||
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 => ({
|
||||
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'])
|
||||
});
|
||||
|
||||
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) => ({
|
||||
|
||||
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);
|
Loading…
Add table
Add a link
Reference in a new issue