Add circle visibility

This commit is contained in:
KMY 2023-08-21 18:22:14 +09:00
parent 44eb57183e
commit 3af223275f
20 changed files with 154 additions and 12 deletions

View file

@ -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 (
<div className='compose-form__circle-select'>
<NonceProvider nonce={document.querySelector('meta[name=style-nonce]').content} cacheKey='circles'>
<Select
value={listValue}
options={listOptions}
noOptionsMessage={this.noOptionsMessage}
onChange={this.handleClick}
className='column-content-select__container'
classNamePrefix='column-content-select'
name='circles'
defaultOptions
/>
</NonceProvider>
</div>
);
}
}
export default injectIntl(CircleSelect);

View file

@ -14,6 +14,7 @@ import { Icon } from 'mastodon/components/icon';
import AutosuggestInput from '../../../components/autosuggest_input';
import AutosuggestTextarea from '../../../components/autosuggest_textarea';
import Button from '../../../components/button';
import CircleSelectContainer from '../containers/circle_select_container';
import EmojiPickerDropdown from '../containers/emoji_picker_dropdown_container';
import ExpirationDropdownContainer from '../containers/expiration_dropdown_container';
import LanguageDropdown from '../containers/language_dropdown_container';
@ -76,6 +77,7 @@ class ComposeForm extends ImmutablePureComponent {
isInReply: PropTypes.bool,
singleColumn: PropTypes.bool,
lang: PropTypes.string,
circleId: PropTypes.string,
};
static defaultProps = {
@ -101,11 +103,11 @@ class ComposeForm extends ImmutablePureComponent {
};
canSubmit = () => {
const { isSubmitting, isChangingUpload, isUploading, anyMedia } = this.props;
const { isSubmitting, isChangingUpload, isUploading, anyMedia, privacy, circleId } = this.props;
const fulltext = this.getFulltextForCharacterCounting();
const isOnlyWhitespace = fulltext.length !== 0 && fulltext.trim().length === 0;
return !(isSubmitting || isUploading || isChangingUpload || length(fulltext) > 500 || (isOnlyWhitespace && !anyMedia));
return !(isSubmitting || isUploading || isChangingUpload || length(fulltext) > 500 || (isOnlyWhitespace && !anyMedia) || (privacy === 'circle' && !circleId));
};
handleSubmit = (e) => {
@ -317,6 +319,8 @@ class ComposeForm extends ImmutablePureComponent {
</div>
</div>
<CircleSelectContainer />
<div className='compose-form__publish'>
<div className='compose-form__publish-button-wrapper'>
<Button

View file

@ -26,6 +26,8 @@ const messages = defineMessages({
private_long: { id: 'privacy.private.long', defaultMessage: 'Visible for followers only' },
mutual_short: { id: 'privacy.mutual.short', defaultMessage: 'Mutual' },
mutual_long: { id: 'privacy.mutual.long', defaultMessage: 'Mutual follows only' },
circle_short: { id: 'privacy.circle.short', defaultMessage: 'Circle' },
circle_long: { id: 'privacy.circle.long', defaultMessage: 'Circle members only' },
direct_short: { id: 'privacy.direct.short', defaultMessage: 'Mentioned people only' },
direct_long: { id: 'privacy.direct.long', defaultMessage: 'Visible for mentioned users only' },
change_privacy: { id: 'privacy.change', defaultMessage: 'Adjust status privacy' },
@ -235,6 +237,7 @@ class PrivacyDropdown extends PureComponent {
{ icon: 'unlock', value: 'unlisted', text: formatMessage(messages.unlisted_short), meta: formatMessage(messages.unlisted_long) },
{ icon: 'lock', value: 'private', text: formatMessage(messages.private_short), meta: formatMessage(messages.private_long) },
{ icon: 'exchange', value: 'mutual', text: formatMessage(messages.mutual_short), meta: formatMessage(messages.mutual_long) },
{ icon: 'user-circle', value: 'circle', text: formatMessage(messages.circle_short), meta: formatMessage(messages.circle_long) },
{ icon: 'at', value: 'direct', text: formatMessage(messages.direct_short), meta: formatMessage(messages.direct_long) },
];
this.selectableOptions = [...this.options];

View file

@ -0,0 +1,20 @@
import { connect } from 'react-redux';
import { changeCircle } from '../../../actions/compose';
import CircleSelect from '../components/circle_select';
const mapStateToProps = state => ({
unavailable: state.getIn(['compose', 'privacy']) !== 'circle',
circles: state.get('circles'),
circleId: state.getIn(['compose', 'circle_id']),
});
const mapDispatchToProps = dispatch => ({
onChange (circleId) {
dispatch(changeCircle(circleId));
},
});
export default connect(mapStateToProps, mapDispatchToProps)(CircleSelect);

View file

@ -30,6 +30,7 @@ const mapStateToProps = state => ({
anyMedia: state.getIn(['compose', 'media_attachments']).size > 0,
isInReply: state.getIn(['compose', 'in_reply_to']) !== null,
lang: state.getIn(['compose', 'language']),
circleId: state.getIn(['compose', 'circle_id']),
});
const mapDispatchToProps = (dispatch) => ({

View file

@ -11,10 +11,10 @@ import Warning from '../components/warning';
const mapStateToProps = state => ({
needsLockWarning: state.getIn(['compose', 'privacy']) === 'private' && !state.getIn(['accounts', me, 'locked']),
hashtagWarning: ['public', 'public_unlisted', 'login'].indexOf(state.getIn(['compose', 'privacy'])) < 0 && state.getIn(['compose', 'searchability']) !== 'public' && HASHTAG_PATTERN_REGEX.test(state.getIn(['compose', 'text'])),
hashtagWarning: ['public', 'public_unlisted', 'login'].includes(state.getIn(['compose', 'privacy'])) && state.getIn(['compose', 'searchability']) !== 'public' && HASHTAG_PATTERN_REGEX.test(state.getIn(['compose', 'text'])),
directMessageWarning: state.getIn(['compose', 'privacy']) === 'direct',
searchabilityWarning: state.getIn(['compose', 'searchability']) === 'limited',
limitedPostWarning: state.getIn(['compose', 'privacy']) === 'mutual',
limitedPostWarning: ['mutual', 'circle'].includes(state.getIn(['compose', 'privacy'])),
});
const WarningWrapper = ({ needsLockWarning, hashtagWarning, directMessageWarning, searchabilityWarning, limitedPostWarning }) => {

View file

@ -22,6 +22,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' },
});
@ -55,6 +56,7 @@ class StatusCheckBox extends PureComponent {
'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) },
};

View file

@ -33,6 +33,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' },
searchability_public_short: { id: 'searchability.public.short', defaultMessage: 'Public' },
searchability_private_short: { id: 'searchability.unlisted.short', defaultMessage: 'Followers' },
@ -256,6 +257,7 @@ class DetailedStatus 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) },
};

View file

@ -29,6 +29,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' },
});
@ -98,6 +99,7 @@ class BoostModal 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) },
};