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 (
+