Skip to content

Commit 7970f69

Browse files
committed
Separate circle choice from privacy
1 parent 36f6a68 commit 7970f69

File tree

11 files changed

+167
-51
lines changed

11 files changed

+167
-51
lines changed

app/javascript/mastodon/actions/compose.js

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ export const COMPOSE_SENSITIVITY_CHANGE = 'COMPOSE_SENSITIVITY_CHANGE';
4747
export const COMPOSE_SPOILERNESS_CHANGE = 'COMPOSE_SPOILERNESS_CHANGE';
4848
export const COMPOSE_SPOILER_TEXT_CHANGE = 'COMPOSE_SPOILER_TEXT_CHANGE';
4949
export const COMPOSE_VISIBILITY_CHANGE = 'COMPOSE_VISIBILITY_CHANGE';
50+
export const COMPOSE_CIRCLE_CHANGE = 'COMPOSE_CIRCLE_CHANGE';
5051
export const COMPOSE_LISTABILITY_CHANGE = 'COMPOSE_LISTABILITY_CHANGE';
5152
export const COMPOSE_COMPOSING_CHANGE = 'COMPOSE_COMPOSING_CHANGE';
5253

@@ -139,22 +140,14 @@ export function submitCompose(routerHistory) {
139140

140141
dispatch(submitComposeRequest());
141142

142-
let visibility = getState().getIn(['compose', 'privacy']);
143-
let circleId = null;
144-
145-
if (!(['public', 'unlisted', 'private', 'direct'].includes(visibility))) {
146-
circleId = visibility;
147-
visibility = 'limited';
148-
}
149-
150143
api(getState).post('/api/v1/statuses', {
151144
status,
152145
in_reply_to_id: getState().getIn(['compose', 'in_reply_to'], null),
153146
media_ids: media.map(item => item.get('id')),
154147
sensitive: getState().getIn(['compose', 'sensitive']),
155148
spoiler_text: getState().getIn(['compose', 'spoiler']) ? getState().getIn(['compose', 'spoiler_text'], '') : '',
156-
visibility: visibility,
157-
circle_id: circleId,
149+
visibility: getState().getIn(['compose', 'privacy']),
150+
circle_id: getState().getIn(['compose', 'circle_id']),
158151
poll: getState().getIn(['compose', 'poll'], null),
159152
}, {
160153
headers: {
@@ -603,6 +596,13 @@ export function changeComposeVisibility(value) {
603596
};
604597
};
605598

599+
export function changeComposeCircle(value) {
600+
return {
601+
type: COMPOSE_CIRCLE_CHANGE,
602+
value,
603+
};
604+
};
605+
606606
export function insertEmojiCompose(position, emoji, needsSpace) {
607607
return {
608608
type: COMPOSE_EMOJI_INSERT,
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import React from 'react';
2+
import { connect } from 'react-redux';
3+
import PropTypes from 'prop-types';
4+
import ImmutablePropTypes from 'react-immutable-proptypes';
5+
import { injectIntl, defineMessages } from 'react-intl';
6+
import classNames from 'classnames';
7+
import Icon from 'mastodon/components/icon';
8+
import { createSelector } from 'reselect';
9+
10+
const messages = defineMessages({
11+
circle_unselect: { id: 'circle.unselect', defaultMessage: 'Select a circle' },
12+
});
13+
14+
const getOrderedCircles = createSelector([state => state.get('circles')], circles => {
15+
if (!circles) {
16+
return circles;
17+
}
18+
19+
return circles.toList().filter(item => !!item).sort((a, b) => a.get('title').localeCompare(b.get('title')));
20+
});
21+
22+
const mapStateToProps = (state) => {
23+
return {
24+
circles: getOrderedCircles(state),
25+
};
26+
};
27+
28+
export default @connect(mapStateToProps)
29+
@injectIntl
30+
class CircleDropdown extends React.PureComponent {
31+
32+
static propTypes = {
33+
circles: ImmutablePropTypes.list,
34+
value: PropTypes.string.isRequired,
35+
visible: PropTypes.bool.isRequired,
36+
onChange: PropTypes.func.isRequired,
37+
intl: PropTypes.object.isRequired,
38+
};
39+
40+
handleChange = e => {
41+
this.props.onChange(e.target.value);
42+
};
43+
44+
render () {
45+
const { circles, value, visible, intl } = this.props;
46+
47+
return (
48+
<div className={classNames('circle-dropdown', { 'circle-dropdown--visible': visible })}>
49+
<Icon id='circle-o' className='circle-dropdown__icon' />
50+
51+
{/* eslint-disable-next-line jsx-a11y/no-onchange */}
52+
<select className='circle-dropdown__menu' value={value} onChange={this.handleChange}>
53+
<option value='' key='unselect'>{intl.formatMessage(messages.circle_unselect)}</option>
54+
{circles.map(circle =>
55+
<option value={circle.get('id')} key={circle.get('id')}>{circle.get('title')}</option>,
56+
)}
57+
</select>
58+
</div>
59+
);
60+
}
61+
62+
}

app/javascript/mastodon/features/compose/components/compose_form.js

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import UploadButtonContainer from '../containers/upload_button_container';
1111
import { defineMessages, injectIntl } from 'react-intl';
1212
import SpoilerButtonContainer from '../containers/spoiler_button_container';
1313
import PrivacyDropdownContainer from '../containers/privacy_dropdown_container';
14+
import CircleDropdownContainer from '../containers/circle_dropdown_container';
1415
import EmojiPickerDropdown from '../containers/emoji_picker_dropdown_container';
1516
import PollFormContainer from '../containers/poll_form_container';
1617
import UploadFormContainer from '../containers/upload_form_container';
@@ -50,6 +51,7 @@ class ComposeForm extends ImmutablePureComponent {
5051
isSubmitting: PropTypes.bool,
5152
isChangingUpload: PropTypes.bool,
5253
isUploading: PropTypes.bool,
54+
isCircleUnselected: PropTypes.bool,
5355
onChange: PropTypes.func.isRequired,
5456
onSubmit: PropTypes.func.isRequired,
5557
onClearSuggestions: PropTypes.func.isRequired,
@@ -85,10 +87,10 @@ class ComposeForm extends ImmutablePureComponent {
8587
}
8688

8789
// Submit disabled:
88-
const { isSubmitting, isChangingUpload, isUploading, anyMedia } = this.props;
90+
const { isSubmitting, isChangingUpload, isUploading, isCircleUnselected, anyMedia } = this.props;
8991
const fulltext = [this.props.spoilerText, countableText(this.props.text)].join('');
9092

91-
if (isSubmitting || isUploading || isChangingUpload || length(fulltext) > 500 || (fulltext.length !== 0 && fulltext.trim().length === 0 && !anyMedia)) {
93+
if (isSubmitting || isUploading || isChangingUpload || isCircleUnselected || length(fulltext) > 500 || (fulltext.length !== 0 && fulltext.trim().length === 0 && !anyMedia)) {
9294
return;
9395
}
9496

@@ -181,7 +183,7 @@ class ComposeForm extends ImmutablePureComponent {
181183
const { intl, onPaste, showSearch, anyMedia } = this.props;
182184
const disabled = this.props.isSubmitting;
183185
const text = [this.props.spoilerText, countableText(this.props.text)].join('');
184-
const disabledButton = disabled || this.props.isUploading || this.props.isChangingUpload || length(text) > 500 || (text.length !== 0 && text.trim().length === 0 && !anyMedia);
186+
const disabledButton = disabled || this.props.isUploading || this.props.isChangingUpload || this.props.isCircleUnselected || length(text) > 500 || (text.length !== 0 && text.trim().length === 0 && !anyMedia);
185187
let publishText = '';
186188

187189
if (this.props.privacy === 'private' || this.props.privacy === 'direct') {
@@ -246,6 +248,8 @@ class ComposeForm extends ImmutablePureComponent {
246248
<div className='character-counter__wrapper'><CharacterCounter max={500} text={text} /></div>
247249
</div>
248250

251+
<CircleDropdownContainer />
252+
249253
<div className='compose-form__publish'>
250254
<div className='compose-form__publish-button-wrapper'><Button text={publishText} onClick={this.handleSubmit} disabled={disabledButton} block /></div>
251255
</div>

app/javascript/mastodon/features/compose/components/privacy_dropdown.js

Lines changed: 4 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
import React from 'react';
2-
import { connect } from 'react-redux';
32
import PropTypes from 'prop-types';
4-
import ImmutablePropTypes from 'react-immutable-proptypes';
53
import { injectIntl, defineMessages } from 'react-intl';
64
import IconButton from '../../../components/icon_button';
75
import Overlay from 'react-overlays/lib/Overlay';
@@ -10,7 +8,6 @@ import spring from 'react-motion/lib/spring';
108
import detectPassiveEvents from 'detect-passive-events';
119
import classNames from 'classnames';
1210
import Icon from 'mastodon/components/icon';
13-
import { createSelector } from 'reselect';
1411

1512
const messages = defineMessages({
1613
public_short: { id: 'privacy.public.short', defaultMessage: 'Public' },
@@ -21,6 +18,7 @@ const messages = defineMessages({
2118
private_long: { id: 'privacy.private.long', defaultMessage: 'Visible for followers only' },
2219
direct_short: { id: 'privacy.direct.short', defaultMessage: 'Direct' },
2320
direct_long: { id: 'privacy.direct.long', defaultMessage: 'Visible for mentioned users only' },
21+
limited_short: { id: 'privacy.limited.short', defaultMessage: 'Circle' },
2422
limited_long: { id: 'privacy.limited.long', defaultMessage: 'Visible for circle users only' },
2523
change_privacy: { id: 'privacy.change', defaultMessage: 'Adjust status privacy' },
2624
});
@@ -152,30 +150,14 @@ class PrivacyDropdownMenu extends React.PureComponent {
152150

153151
}
154152

155-
const getOrderedCircles = createSelector([state => state.get('circles')], circles => {
156-
if (!circles) {
157-
return circles;
158-
}
159-
160-
return circles.toList().filter(item => !!item).sort((a, b) => a.get('title').localeCompare(b.get('title')));
161-
});
162-
163-
const mapStateToProps = (state) => {
164-
return {
165-
circles: getOrderedCircles(state),
166-
};
167-
};
168-
169-
export default @connect(mapStateToProps)
170-
@injectIntl
153+
export default @injectIntl
171154
class PrivacyDropdown extends React.PureComponent {
172155

173156
static propTypes = {
174157
isUserTouching: PropTypes.func,
175158
isModalOpen: PropTypes.bool.isRequired,
176159
onModalOpen: PropTypes.func,
177160
onModalClose: PropTypes.func,
178-
circles: ImmutablePropTypes.list,
179161
value: PropTypes.string.isRequired,
180162
onChange: PropTypes.func.isRequired,
181163
intl: PropTypes.object.isRequired,
@@ -250,24 +232,15 @@ class PrivacyDropdown extends React.PureComponent {
250232
}
251233

252234
componentWillMount () {
253-
this.setOptions();
254-
}
255-
256-
componentWillUpdate () {
257-
this.setOptions();
258-
}
259-
260-
setOptions () {
261-
const { intl: { formatMessage }, circles } = this.props;
235+
const { intl: { formatMessage } } = this.props;
262236

263237
this.options = [
264238
{ icon: 'globe', value: 'public', text: formatMessage(messages.public_short), meta: formatMessage(messages.public_long) },
265239
{ icon: 'unlock', value: 'unlisted', text: formatMessage(messages.unlisted_short), meta: formatMessage(messages.unlisted_long) },
266240
{ icon: 'lock', value: 'private', text: formatMessage(messages.private_short), meta: formatMessage(messages.private_long) },
241+
{ icon: 'circle-o', value: 'limited', text: formatMessage(messages.limited_short), meta: formatMessage(messages.limited_long) },
267242
{ icon: 'envelope', value: 'direct', text: formatMessage(messages.direct_short), meta: formatMessage(messages.direct_long) },
268243
];
269-
270-
circles.forEach(circle => this.options.push({ icon: 'circle-o', value: circle.get('id'), text: circle.get('title'), meta: formatMessage(messages.limited_long) }));
271244
}
272245

273246
render () {
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { connect } from 'react-redux';
2+
import CircleDropdown from '../components/circle_dropdown';
3+
import { changeComposeCircle } from '../../../actions/compose';
4+
5+
const mapStateToProps = state => {
6+
let value = state.getIn(['compose', 'circle_id']);
7+
value = value === null ? '' : value;
8+
9+
return {
10+
value: value,
11+
visible: state.getIn(['compose', 'privacy']) === 'limited',
12+
};
13+
};
14+
15+
const mapDispatchToProps = dispatch => ({
16+
17+
onChange (value) {
18+
dispatch(changeComposeCircle(value));
19+
},
20+
21+
});
22+
23+
export default connect(mapStateToProps, mapDispatchToProps)(CircleDropdown);

app/javascript/mastodon/features/compose/containers/compose_form_container.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ const mapStateToProps = state => ({
2323
isSubmitting: state.getIn(['compose', 'is_submitting']),
2424
isChangingUpload: state.getIn(['compose', 'is_changing_upload']),
2525
isUploading: state.getIn(['compose', 'is_uploading']),
26+
isCircleUnselected: state.getIn(['compose', 'privacy']) === 'limited' && !state.getIn(['compose', 'circle_id']),
2627
showSearch: state.getIn(['search', 'submitted']) && !state.getIn(['search', 'hidden']),
2728
anyMedia: state.getIn(['compose', 'media_attachments']).size > 0,
2829
});

app/javascript/mastodon/features/compose/containers/warning_container.js

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,7 @@ const mapStateToProps = state => ({
3434
needsLockWarning: state.getIn(['compose', 'privacy']) === 'private' && !state.getIn(['accounts', me, 'locked']),
3535
hashtagWarning: state.getIn(['compose', 'privacy']) !== 'public' && APPROX_HASHTAG_RE.test(state.getIn(['compose', 'text'])),
3636
directMessageWarning: state.getIn(['compose', 'privacy']) === 'direct',
37-
limitedMessageWarning: !(['public', 'unlisted', 'private', 'direct'].includes(state.getIn(['compose', 'privacy']))),
38-
limitedTitle: state.getIn(['circles', state.getIn(['compose', 'privacy']), 'title']),
37+
limitedMessageWarning: state.getIn(['compose', 'privacy']) === 'limited',
3938
});
4039

4140
const WarningWrapper = ({ needsLockWarning, hashtagWarning, directMessageWarning, limitedMessageWarning, limitedTitle }) => {
@@ -58,7 +57,7 @@ const WarningWrapper = ({ needsLockWarning, hashtagWarning, directMessageWarning
5857
}
5958

6059
if (limitedMessageWarning) {
61-
return <Warning message={<FormattedMessage id='compose_form.limited_message_warning' defaultMessage='This toot will only be sent to users in the circle "{title}".' values={{ title: limitedTitle }} />} />;
60+
return <Warning message={<FormattedMessage id='compose_form.limited_message_warning' defaultMessage='This toot will only be sent to users in the circle.' />} />;
6261
}
6362

6463
return null;
@@ -69,7 +68,6 @@ WarningWrapper.propTypes = {
6968
hashtagWarning: PropTypes.bool,
7069
directMessageWarning: PropTypes.bool,
7170
limitedMessageWarning: PropTypes.bool,
72-
limitedTitle: PropTypes.string,
7371
};
7472

7573
export default connect(mapStateToProps)(WarningWrapper);

app/javascript/mastodon/reducers/compose.js

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {
2727
COMPOSE_SPOILERNESS_CHANGE,
2828
COMPOSE_SPOILER_TEXT_CHANGE,
2929
COMPOSE_VISIBILITY_CHANGE,
30+
COMPOSE_CIRCLE_CHANGE,
3031
COMPOSE_COMPOSING_CHANGE,
3132
COMPOSE_EMOJI_INSERT,
3233
COMPOSE_UPLOAD_CHANGE_REQUEST,
@@ -54,6 +55,7 @@ const initialState = ImmutableMap({
5455
spoiler: false,
5556
spoiler_text: '',
5657
privacy: null,
58+
circle_id: null,
5759
text: '',
5860
focusDate: null,
5961
caretPosition: null,
@@ -103,6 +105,7 @@ function clearAll(state) {
103105
map.set('is_changing_upload', false);
104106
map.set('in_reply_to', null);
105107
map.set('privacy', state.get('default_privacy'));
108+
map.set('circle_id', null);
106109
map.set('sensitive', false);
107110
map.update('media_attachments', list => list.clear());
108111
map.set('poll', null);
@@ -185,7 +188,7 @@ const insertEmoji = (state, position, emojiData, needsSpace) => {
185188
};
186189

187190
const privacyPreference = (a, b) => {
188-
const order = ['public', 'unlisted', 'private', 'direct'];
191+
const order = ['public', 'unlisted', 'private', 'limited', 'direct'];
189192
return order[Math.max(order.indexOf(a), order.indexOf(b), 0)];
190193
};
191194

@@ -280,8 +283,16 @@ export default function compose(state = initialState, action) {
280283
.set('spoiler_text', action.text)
281284
.set('idempotencyKey', uuid());
282285
case COMPOSE_VISIBILITY_CHANGE:
286+
return state.withMutations(map => {
287+
map.set('privacy', action.value);
288+
map.set('idempotencyKey', uuid());
289+
if (action.value !== 'limited') {
290+
map.set('circle_id', null);
291+
}
292+
});
293+
case COMPOSE_CIRCLE_CHANGE:
283294
return state
284-
.set('privacy', action.value)
295+
.set('circle_id', action.value)
285296
.set('idempotencyKey', uuid());
286297
case COMPOSE_CHANGE:
287298
return state
@@ -294,6 +305,7 @@ export default function compose(state = initialState, action) {
294305
map.set('in_reply_to', action.status.get('id'));
295306
map.set('text', statusToTextMentions(state, action.status));
296307
map.set('privacy', privacyPreference(action.status.get('visibility'), state.get('default_privacy')));
308+
map.set('circle_id', null);
297309
map.set('focusDate', new Date());
298310
map.set('caretPosition', null);
299311
map.set('preselectDate', new Date());
@@ -315,6 +327,7 @@ export default function compose(state = initialState, action) {
315327
map.set('spoiler', false);
316328
map.set('spoiler_text', '');
317329
map.set('privacy', state.get('default_privacy'));
330+
map.set('circle_id', null);
318331
map.set('poll', null);
319332
map.set('idempotencyKey', uuid());
320333
});
@@ -365,6 +378,7 @@ export default function compose(state = initialState, action) {
365378
return state.withMutations(map => {
366379
map.update('text', text => [text.trim(), `@${action.account.get('acct')} `].filter((str) => str.length !== 0).join(' '));
367380
map.set('privacy', 'direct');
381+
map.set('circle_id', null);
368382
map.set('focusDate', new Date());
369383
map.set('caretPosition', null);
370384
map.set('idempotencyKey', uuid());
@@ -402,6 +416,7 @@ export default function compose(state = initialState, action) {
402416
map.set('text', action.raw_text || unescapeHTML(expandMentions(action.status)));
403417
map.set('in_reply_to', action.status.get('in_reply_to_id'));
404418
map.set('privacy', action.status.get('visibility'));
419+
map.set('circle_id', null);
405420
map.set('media_attachments', action.status.get('media_attachments'));
406421
map.set('focusDate', new Date());
407422
map.set('caretPosition', null);

app/javascript/styles/mastodon-light/diff.scss

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ html {
145145
.poll__option input[type="text"],
146146
.compose-form .spoiler-input__input,
147147
.compose-form__poll-wrapper select,
148+
.circle-dropdown .circle-dropdown__menu,
148149
.search__input,
149150
.setting-text,
150151
.box-widget input[type="text"],
@@ -168,7 +169,8 @@ html {
168169
border-bottom: 0;
169170
}
170171

171-
.compose-form__poll-wrapper select {
172+
.compose-form__poll-wrapper select,
173+
.circle-dropdown .circle-dropdown__menu {
172174
background: $simple-background-color url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14.933 18.467' height='19.698' width='15.929'><path d='M3.467 14.967l-3.393-3.5H14.86l-3.392 3.5c-1.866 1.925-3.666 3.5-4 3.5-.335 0-2.135-1.575-4-3.5zm.266-11.234L7.467 0 11.2 3.733l3.733 3.734H0l3.733-3.734z' fill='#{hex-color(lighten($ui-base-color, 8%))}'/></svg>") no-repeat right 8px center / auto 16px;
173175
}
174176

0 commit comments

Comments
 (0)