diff --git a/app/controllers/api/v1/statuses_controller.rb b/app/controllers/api/v1/statuses_controller.rb index db9fcce905..1df399405a 100644 --- a/app/controllers/api/v1/statuses_controller.rb +++ b/app/controllers/api/v1/statuses_controller.rb @@ -67,6 +67,7 @@ class Api::V1::StatusesController < Api::BaseController visibility: status_params[:visibility], force_visibility: status_params[:force_visibility], searchability: status_params[:searchability], + circle_id: status_params[:circle_id], language: status_params[:language], scheduled_at: status_params[:scheduled_at], application: doorkeeper_token.application, @@ -144,6 +145,7 @@ class Api::V1::StatusesController < Api::BaseController :visibility, :force_visibility, :searchability, + :circle_id, :language, :markdown, :scheduled_at, diff --git a/app/javascript/mastodon/actions/compose.js b/app/javascript/mastodon/actions/compose.js index cf695f79b4..381cfdd42f 100644 --- a/app/javascript/mastodon/actions/compose.js +++ b/app/javascript/mastodon/actions/compose.js @@ -75,6 +75,8 @@ export const COMPOSE_POLL_OPTION_CHANGE = 'COMPOSE_POLL_OPTION_CHANGE'; export const COMPOSE_POLL_OPTION_REMOVE = 'COMPOSE_POLL_OPTION_REMOVE'; export const COMPOSE_POLL_SETTINGS_CHANGE = 'COMPOSE_POLL_SETTINGS_CHANGE'; +export const COMPOSE_CIRCLE_CHANGE = 'COMPOSE_CIRCLE_CHANGE'; + export const INIT_MEDIA_EDIT_MODAL = 'INIT_MEDIA_EDIT_MODAL'; export const COMPOSE_CHANGE_MEDIA_DESCRIPTION = 'COMPOSE_CHANGE_MEDIA_DESCRIPTION'; @@ -211,6 +213,7 @@ export function submitCompose(routerHistory) { markdown: getState().getIn(['compose', 'markdown']), visibility: getState().getIn(['compose', 'privacy']), searchability: getState().getIn(['compose', 'searchability']), + circle_id: getState().getIn(['compose', 'circle_id']), poll: getState().getIn(['compose', 'poll'], null), language: getState().getIn(['compose', 'language']), }, @@ -837,3 +840,10 @@ export function changePollSettings(expiresIn, isMultiple) { isMultiple, }; } + +export function changeCircle(circleId) { + return { + type: COMPOSE_CIRCLE_CHANGE, + circleId, + }; +} diff --git a/app/javascript/mastodon/components/status.jsx b/app/javascript/mastodon/components/status.jsx index 93195e61ea..43c31dca0e 100644 --- a/app/javascript/mastodon/components/status.jsx +++ b/app/javascript/mastodon/components/status.jsx @@ -72,6 +72,7 @@ const messages = defineMessages({ private_short: { id: 'privacy.private.short', defaultMessage: 'Followers only' }, limited_short: { id: 'privacy.limited.short', defaultMessage: 'Limited menbers only' }, mutual_short: { id: 'privacy.mutual.short', defaultMessage: 'Mutual followers only' }, + circle_short: { id: 'privacy.circle.short', defaultMessage: 'Circle members only' }, direct_short: { id: 'privacy.direct.short', defaultMessage: 'Mentioned people only' }, edited: { id: 'status.edited', defaultMessage: 'Edited {date}' }, }); @@ -403,6 +404,7 @@ class Status extends ImmutablePureComponent { 'private': { icon: 'lock', text: intl.formatMessage(messages.private_short) }, 'limited': { icon: 'get-pocket', text: intl.formatMessage(messages.limited_short) }, 'mutual': { icon: 'exchange', text: intl.formatMessage(messages.mutual_short) }, + 'circle': { icon: 'user-circle', text: intl.formatMessage(messages.circle_short) }, 'direct': { icon: 'at', text: intl.formatMessage(messages.direct_short) }, }; diff --git a/app/javascript/mastodon/containers/mastodon.jsx b/app/javascript/mastodon/containers/mastodon.jsx index da76bd1c0b..579432794d 100644 --- a/app/javascript/mastodon/containers/mastodon.jsx +++ b/app/javascript/mastodon/containers/mastodon.jsx @@ -8,6 +8,7 @@ import { Provider as ReduxProvider } from 'react-redux'; import { ScrollContext } from 'react-router-scroll-4'; +import { fetchCircles } from 'mastodon/actions/circles'; import { fetchCustomEmojis } from 'mastodon/actions/custom_emojis'; import { fetchReactionDeck } from 'mastodon/actions/reaction_deck'; import { hydrateStore } from 'mastodon/actions/store'; @@ -27,6 +28,7 @@ store.dispatch(hydrateAction); if (initialState.meta.me) { store.dispatch(fetchCustomEmojis()); store.dispatch(fetchReactionDeck()); + store.dispatch(fetchCircles()); } const createIdentityContext = state => ({ diff --git a/app/javascript/mastodon/features/compose/components/circle_select.jsx b/app/javascript/mastodon/features/compose/components/circle_select.jsx new file mode 100644 index 0000000000..251e095f55 --- /dev/null +++ b/app/javascript/mastodon/features/compose/components/circle_select.jsx @@ -0,0 +1,59 @@ +import PropTypes from 'prop-types'; +import { PureComponent } from 'react'; + +import { injectIntl } from 'react-intl'; + +import ImmutablePropTypes from 'react-immutable-proptypes'; + + +import Select, { NonceProvider } from 'react-select'; + +class CircleSelect extends PureComponent { + + static propTypes = { + unavailable: PropTypes.bool, + intl: PropTypes.object.isRequired, + circles: ImmutablePropTypes.list, + circleId: PropTypes.string, + onChange: PropTypes.func.isRequired, + }; + + handleClick = value => { + this.props.onChange(value.value); + }; + + noOptionsMessage = () => ''; + + render () { + const { unavailable, circles, circleId } = this.props; + + if (unavailable) { + return null; + } + + const listOptions = circles.toArray().filter((circle) => circle[1]).map((circle) => { + return { value: circle[1].get('id'), label: circle[1].get('title') }; + }); + const listValue = listOptions.find((opt) => opt.value === circleId); + + return ( +
+ +