diff --git a/app/javascript/mastodon/actions/compose.js b/app/javascript/mastodon/actions/compose.js index 030922520264f0..3fba4c6890de70 100644 --- a/app/javascript/mastodon/actions/compose.js +++ b/app/javascript/mastodon/actions/compose.js @@ -139,13 +139,22 @@ export function submitCompose(routerHistory) { dispatch(submitComposeRequest()); + let visibility = getState().getIn(['compose', 'privacy']); + let circleId = null; + + if (!(['public', 'unlisted', 'private', 'direct'].includes(visibility))) { + circleId = visibility; + visibility = 'limited'; + } + api(getState).post('/api/v1/statuses', { status, in_reply_to_id: getState().getIn(['compose', 'in_reply_to'], null), media_ids: media.map(item => item.get('id')), sensitive: getState().getIn(['compose', 'sensitive']), spoiler_text: getState().getIn(['compose', 'spoiler']) ? getState().getIn(['compose', 'spoiler_text'], '') : '', - visibility: getState().getIn(['compose', 'privacy']), + visibility: visibility, + circle_id: circleId, poll: getState().getIn(['compose', 'poll'], null), }, { headers: { diff --git a/app/javascript/mastodon/containers/mastodon.js b/app/javascript/mastodon/containers/mastodon.js index 3ac58cf7c5e678..64745fb111e1f7 100644 --- a/app/javascript/mastodon/containers/mastodon.js +++ b/app/javascript/mastodon/containers/mastodon.js @@ -8,6 +8,7 @@ import { ScrollContext } from 'react-router-scroll-4'; import UI from '../features/ui'; import Introduction from '../features/introduction'; import { fetchCustomEmojis } from '../actions/custom_emojis'; +import { fetchCircles } from '../actions/circles'; import { hydrateStore } from '../actions/store'; import { connectUserStream } from '../actions/streaming'; import { IntlProvider, addLocaleData } from 'react-intl'; @@ -25,6 +26,7 @@ const hydrateAction = hydrateStore(initialState); store.dispatch(hydrateAction); store.dispatch(fetchCustomEmojis()); +store.dispatch(fetchCircles()); const mapStateToProps = state => ({ showIntroduction: state.getIn(['settings', 'introductionVersion'], 0) < INTRODUCTION_VERSION, diff --git a/app/javascript/mastodon/features/compose/components/privacy_dropdown.js b/app/javascript/mastodon/features/compose/components/privacy_dropdown.js index 5223025fb5f412..4d2e8dfbfe0ebf 100644 --- a/app/javascript/mastodon/features/compose/components/privacy_dropdown.js +++ b/app/javascript/mastodon/features/compose/components/privacy_dropdown.js @@ -1,5 +1,7 @@ import React from 'react'; +import { connect } from 'react-redux'; import PropTypes from 'prop-types'; +import ImmutablePropTypes from 'react-immutable-proptypes'; import { injectIntl, defineMessages } from 'react-intl'; import IconButton from '../../../components/icon_button'; import Overlay from 'react-overlays/lib/Overlay'; @@ -8,6 +10,7 @@ import spring from 'react-motion/lib/spring'; import detectPassiveEvents from 'detect-passive-events'; import classNames from 'classnames'; import Icon from 'mastodon/components/icon'; +import { createSelector } from 'reselect'; const messages = defineMessages({ public_short: { id: 'privacy.public.short', defaultMessage: 'Public' }, @@ -18,6 +21,7 @@ const messages = defineMessages({ private_long: { id: 'privacy.private.long', defaultMessage: 'Visible for followers only' }, direct_short: { id: 'privacy.direct.short', defaultMessage: 'Direct' }, direct_long: { id: 'privacy.direct.long', defaultMessage: 'Visible for mentioned users only' }, + limited_long: { id: 'privacy.limited.long', defaultMessage: 'Visible for circle users only' }, change_privacy: { id: 'privacy.change', defaultMessage: 'Adjust status privacy' }, }); @@ -148,7 +152,22 @@ class PrivacyDropdownMenu extends React.PureComponent { } -export default @injectIntl +const getOrderedCircles = createSelector([state => state.get('circles')], circles => { + if (!circles) { + return circles; + } + + return circles.toList().filter(item => !!item).sort((a, b) => a.get('title').localeCompare(b.get('title'))); +}); + +const mapStateToProps = (state) => { + return { + circles: getOrderedCircles(state), + }; +}; + +export default @connect(mapStateToProps) +@injectIntl class PrivacyDropdown extends React.PureComponent { static propTypes = { @@ -156,6 +175,7 @@ class PrivacyDropdown extends React.PureComponent { isModalOpen: PropTypes.bool.isRequired, onModalOpen: PropTypes.func, onModalClose: PropTypes.func, + circles: ImmutablePropTypes.list, value: PropTypes.string.isRequired, onChange: PropTypes.func.isRequired, intl: PropTypes.object.isRequired, @@ -230,7 +250,15 @@ class PrivacyDropdown extends React.PureComponent { } componentWillMount () { - const { intl: { formatMessage } } = this.props; + this.setOptions(); + } + + componentWillUpdate () { + this.setOptions(); + } + + setOptions () { + const { intl: { formatMessage }, circles } = this.props; this.options = [ { icon: 'globe', value: 'public', text: formatMessage(messages.public_short), meta: formatMessage(messages.public_long) }, @@ -238,6 +266,8 @@ class PrivacyDropdown extends React.PureComponent { { icon: 'lock', value: 'private', text: formatMessage(messages.private_short), meta: formatMessage(messages.private_long) }, { icon: 'envelope', value: 'direct', text: formatMessage(messages.direct_short), meta: formatMessage(messages.direct_long) }, ]; + + circles.forEach(circle => this.options.push({ icon: 'circle-o', value: circle.get('id'), text: circle.get('title'), meta: formatMessage(messages.limited_long) })); } render () { diff --git a/app/javascript/mastodon/features/compose/containers/warning_container.js b/app/javascript/mastodon/features/compose/containers/warning_container.js index bf0660ea9dfeb6..b219683c858591 100644 --- a/app/javascript/mastodon/features/compose/containers/warning_container.js +++ b/app/javascript/mastodon/features/compose/containers/warning_container.js @@ -34,9 +34,11 @@ const mapStateToProps = state => ({ needsLockWarning: state.getIn(['compose', 'privacy']) === 'private' && !state.getIn(['accounts', me, 'locked']), hashtagWarning: state.getIn(['compose', 'privacy']) !== 'public' && APPROX_HASHTAG_RE.test(state.getIn(['compose', 'text'])), directMessageWarning: state.getIn(['compose', 'privacy']) === 'direct', + limitedMessageWarning: !(['public', 'unlisted', 'private', 'direct'].includes(state.getIn(['compose', 'privacy']))), + limitedTitle: state.getIn(['circles', state.getIn(['compose', 'privacy']), 'title']), }); -const WarningWrapper = ({ needsLockWarning, hashtagWarning, directMessageWarning }) => { +const WarningWrapper = ({ needsLockWarning, hashtagWarning, directMessageWarning, limitedMessageWarning, limitedTitle }) => { if (needsLockWarning) { return }} />} />; } @@ -55,6 +57,10 @@ const WarningWrapper = ({ needsLockWarning, hashtagWarning, directMessageWarning return ; } + if (limitedMessageWarning) { + return } />; + } + return null; }; @@ -62,6 +68,8 @@ WarningWrapper.propTypes = { needsLockWarning: PropTypes.bool, hashtagWarning: PropTypes.bool, directMessageWarning: PropTypes.bool, + limitedMessageWarning: PropTypes.bool, + limitedTitle: PropTypes.string, }; export default connect(mapStateToProps)(WarningWrapper);