From 501514960a9de238e23cd607d2e8f4c1ff9f16c1 Mon Sep 17 00:00:00 2001 From: Eugen Date: Mon, 24 Apr 2017 00:38:37 +0200 Subject: 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 --- .../features/compose/components/compose_form.jsx | 20 ++------ .../compose/components/privacy_dropdown.jsx | 2 +- .../features/compose/components/warning.jsx | 25 ++++++++++ .../compose/containers/compose_form_container.jsx | 38 +++++---------- .../compose/containers/warning_container.jsx | 48 ++++++++++++++++++ app/assets/javascripts/components/locales/en.jsx | 2 +- app/assets/stylesheets/accounts.scss | 3 +- app/assets/stylesheets/components.scss | 30 +++++++++--- app/assets/stylesheets/forms.scss | 57 ++++++++++++++++++++++ 9 files changed, 172 insertions(+), 53 deletions(-) create mode 100644 app/assets/javascripts/components/features/compose/components/warning.jsx create mode 100644 app/assets/javascripts/components/features/compose/containers/warning_container.jsx (limited to 'app/assets') diff --git a/app/assets/javascripts/components/features/compose/components/compose_form.jsx b/app/assets/javascripts/components/features/compose/components/compose_form.jsx index c148dded5..464327cb5 100644 --- a/app/assets/javascripts/components/features/compose/components/compose_form.jsx +++ b/app/assets/javascripts/components/features/compose/components/compose_form.jsx @@ -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 = ( -
- {mentionedDomains.join(', ')}, domainsCount: mentionedDomains.length }} - /> -
- ); - } - if (this.props.privacy === 'private' || this.props.privacy === 'direct') { publishText = {intl.formatMessage(messages.publish)}; } else { @@ -150,7 +138,7 @@ class ComposeForm extends React.PureComponent { - {privacyWarning} + @@ -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, diff --git a/app/assets/javascripts/components/features/compose/components/privacy_dropdown.jsx b/app/assets/javascripts/components/features/compose/components/privacy_dropdown.jsx index 507fe7b58..82b3454c6 100644 --- a/app/assets/javascripts/components/features/compose/components/privacy_dropdown.jsx +++ b/app/assets/javascripts/components/features/compose/components/privacy_dropdown.jsx @@ -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' }, diff --git a/app/assets/javascripts/components/features/compose/components/warning.jsx b/app/assets/javascripts/components/features/compose/components/warning.jsx new file mode 100644 index 000000000..ff1989755 --- /dev/null +++ b/app/assets/javascripts/components/features/compose/components/warning.jsx @@ -0,0 +1,25 @@ +import PropTypes from 'prop-types'; + +class Warning extends React.PureComponent { + + constructor (props) { + super(props); + } + + render () { + const { message } = this.props; + + return ( +
+ {message} +
+ ); + } + +} + +Warning.propTypes = { + message: PropTypes.node.isRequired +}; + +export default Warning; diff --git a/app/assets/javascripts/components/features/compose/containers/compose_form_container.jsx b/app/assets/javascripts/components/features/compose/containers/compose_form_container.jsx index 604e1182f..892183b83 100644 --- a/app/assets/javascripts/components/features/compose/containers/compose_form_container.jsx +++ b/app/assets/javascripts/components/features/compose/containers/compose_form_container.jsx @@ -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) { diff --git a/app/assets/javascripts/components/features/compose/containers/warning_container.jsx b/app/assets/javascripts/components/features/compose/containers/warning_container.jsx new file mode 100644 index 000000000..62a9bb571 --- /dev/null +++ b/app/assets/javascripts/components/features/compose/containers/warning_container.jsx @@ -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 }} />} />; + } else if (needsLeakWarning) { + return ( + {mentionedDomains.join(', ')}, domainsCount: mentionedDomains.length }} + />} + /> + ); + } + + return null; +}; + +WarningWrapper.propTypes = { + needsLeakWarning: PropTypes.bool, + needsLockWarning: PropTypes.bool, + mentionedDomains: PropTypes.array.isRequired, +}; + +export default connect(mapStateToProps)(WarningWrapper); diff --git a/app/assets/javascripts/components/locales/en.jsx b/app/assets/javascripts/components/locales/en.jsx index 180caeaf1..ae14843c1 100644 --- a/app/assets/javascripts/components/locales/en.jsx +++ b/app/assets/javascripts/components/locales/en.jsx @@ -99,7 +99,7 @@ const en = { "privacy.direct.long": "Post to mentioned users only", "privacy.direct.short": "Direct", "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.short": "Public", "privacy.unlisted.long": "Do not show in public timelines", diff --git a/app/assets/stylesheets/accounts.scss b/app/assets/stylesheets/accounts.scss index 11d155d54..99af9c982 100644 --- a/app/assets/stylesheets/accounts.scss +++ b/app/assets/stylesheets/accounts.scss @@ -173,7 +173,7 @@ text-align: center; overflow: hidden; - a, .current, .page, .gap { + a, .current, .next, .prev, .page, .gap { font-size: 14px; color: $color5; font-weight: 500; @@ -187,6 +187,7 @@ border-radius: 100px; color: $color1; cursor: default; + margin: 0 10px; } .gap { diff --git a/app/assets/stylesheets/components.scss b/app/assets/stylesheets/components.scss index 1c798f2f2..800c97a6b 100644 --- a/app/assets/stylesheets/components.scss +++ b/app/assets/stylesheets/components.scss @@ -1,6 +1,6 @@ @import 'variables'; -.app-body{ +.app-body { -webkit-overflow-scrolling: touch; -ms-overflow-style: -ms-autohiding-scrollbar; } @@ -203,18 +203,29 @@ } .compose-form__warning { - color: $color2; + color: darken($color3, 33%); margin-bottom: 15px; - border: 1px solid $color3; + background: $color3; + box-shadow: 0 2px 6px rgba($color8, 0.3); padding: 8px 10px; border-radius: 4px; - font-size: 12px; + font-size: 13px; font-weight: 400; strong { - color: $color5; + color: darken($color3, 33%); font-weight: 500; } + + a { + color: darken($color3, 33%); + font-weight: 500; + text-decoration: underline; + + &:hover, &:active, &:focus { + text-decoration: none; + } + } } .compose-form__modifiers { @@ -1619,7 +1630,7 @@ a.status__content__spoiler-link { } .character-counter { - cursor: default; + cursor: default; font-size: 16px; } @@ -1667,7 +1678,7 @@ a.status__content__spoiler-link { font-size: 16px; } } - + @import 'boost'; button.icon-button i.fa-retweet { @@ -1766,6 +1777,7 @@ button.icon-button.active i.fa-retweet { cursor: pointer; position: relative; z-index: 2; + outline: 0; &.active { box-shadow: 0 1px 0 rgba($color4, 0.3); @@ -1781,6 +1793,10 @@ button.icon-button.active i.fa-retweet { display: none; } } + + &:focus, &:active { + outline: 0; + } } .column-header__icon { diff --git a/app/assets/stylesheets/forms.scss b/app/assets/stylesheets/forms.scss index c6a8b5b02..890a00510 100644 --- a/app/assets/stylesheets/forms.scss +++ b/app/assets/stylesheets/forms.scss @@ -269,3 +269,60 @@ code { 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; + } +} -- cgit