Base frontend on privacy dropdown
This commit is contained in:
parent
ffaa814bbe
commit
4969ea4f2c
@ -40,6 +40,7 @@ export const COMPOSE_SENSITIVITY_CHANGE = 'COMPOSE_SENSITIVITY_CHANGE';
|
||||
export const COMPOSE_SPOILERNESS_CHANGE = 'COMPOSE_SPOILERNESS_CHANGE';
|
||||
export const COMPOSE_SPOILER_TEXT_CHANGE = 'COMPOSE_SPOILER_TEXT_CHANGE';
|
||||
export const COMPOSE_VISIBILITY_CHANGE = 'COMPOSE_VISIBILITY_CHANGE';
|
||||
export const COMPOSE_FEDERATION_CHANGE = 'COMPOSE_FEDERATION_CHANGE';
|
||||
export const COMPOSE_LISTABILITY_CHANGE = 'COMPOSE_LISTABILITY_CHANGE';
|
||||
export const COMPOSE_COMPOSING_CHANGE = 'COMPOSE_COMPOSING_CHANGE';
|
||||
|
||||
@ -125,6 +126,7 @@ export function submitCompose() {
|
||||
sensitive: getState().getIn(['compose', 'sensitive']),
|
||||
spoiler_text: getState().getIn(['compose', 'spoiler_text'], ''),
|
||||
visibility: getState().getIn(['compose', 'privacy']),
|
||||
local_only: !getState().getIn(['compose', 'federation']),
|
||||
}, {
|
||||
headers: {
|
||||
'Idempotency-Key': getState().getIn(['compose', 'idempotencyKey']),
|
||||
@ -445,6 +447,13 @@ export function changeComposeVisibility(value) {
|
||||
};
|
||||
};
|
||||
|
||||
export function changeComposeFederation(value) {
|
||||
return {
|
||||
type: COMPOSE_FEDERATION_CHANGE,
|
||||
value,
|
||||
};
|
||||
};
|
||||
|
||||
export function insertEmojiCompose(position, emoji, needsSpace) {
|
||||
return {
|
||||
type: COMPOSE_EMOJI_INSERT,
|
||||
|
@ -9,6 +9,7 @@ import UploadButtonContainer from '../containers/upload_button_container';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import SpoilerButtonContainer from '../containers/spoiler_button_container';
|
||||
import PrivacyDropdownContainer from '../containers/privacy_dropdown_container';
|
||||
import FederationDropdownContainer from '../containers/federation_dropdown_container';
|
||||
import SensitiveButtonContainer from '../containers/sensitive_button_container';
|
||||
import EmojiPickerDropdown from '../containers/emoji_picker_dropdown_container';
|
||||
import UploadFormContainer from '../containers/upload_form_container';
|
||||
@ -37,6 +38,7 @@ export default class ComposeForm extends ImmutablePureComponent {
|
||||
suggestions: ImmutablePropTypes.list,
|
||||
spoiler: PropTypes.bool,
|
||||
privacy: PropTypes.string,
|
||||
federation: PropTypes.bool,
|
||||
spoiler_text: PropTypes.string,
|
||||
focusDate: PropTypes.instanceOf(Date),
|
||||
caretPosition: PropTypes.number,
|
||||
@ -207,6 +209,7 @@ export default class ComposeForm extends ImmutablePureComponent {
|
||||
<PrivacyDropdownContainer />
|
||||
<SensitiveButtonContainer />
|
||||
<SpoilerButtonContainer />
|
||||
<FederationDropdownContainer />
|
||||
</div>
|
||||
<div className='character-counter__wrapper'><CharacterCounter max={500} text={text} /></div>
|
||||
</div>
|
||||
|
@ -0,0 +1,250 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { injectIntl, defineMessages } from 'react-intl';
|
||||
import IconButton from '../../../components/icon_button';
|
||||
import Overlay from 'react-overlays/lib/Overlay';
|
||||
import Motion from '../../ui/util/optional_motion';
|
||||
import spring from 'react-motion/lib/spring';
|
||||
import detectPassiveEvents from 'detect-passive-events';
|
||||
import classNames from 'classnames';
|
||||
|
||||
const messages = defineMessages({
|
||||
federate_short: { id: 'federation.federated.short', defaultMessage: 'Federated' },
|
||||
federate_long: { id: 'federation.federated.long', defaultMessage: 'Allow toot to reach other instances' },
|
||||
local_only_short: { id: 'federation.local_only.short', defaultMessage: 'Local-only' },
|
||||
local_only_long: { id: 'federation.local_only.long', defaultMessage: 'Restrict this toot only to my instance' },
|
||||
change_federation: { id: 'federation.change', defaultMessage: 'Adjust status federation' },
|
||||
});
|
||||
|
||||
const listenerOptions = detectPassiveEvents.hasSupport ? { passive: true } : false;
|
||||
|
||||
class FederationDropdownMenu extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
style: PropTypes.object,
|
||||
items: PropTypes.array.isRequired,
|
||||
value: PropTypes.bool.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
state = {
|
||||
mounted: false,
|
||||
};
|
||||
|
||||
handleDocumentClick = e => {
|
||||
if (this.node && !this.node.contains(e.target)) {
|
||||
this.props.onClose();
|
||||
}
|
||||
}
|
||||
|
||||
handleKeyDown = e => {
|
||||
const { items } = this.props;
|
||||
const value = Boolean(e.currentTarget.getAttribute('data-index'));
|
||||
const index = items.findIndex(item => {
|
||||
return (item.value === value);
|
||||
});
|
||||
let element;
|
||||
|
||||
switch(e.key) {
|
||||
case 'Escape':
|
||||
this.props.onClose();
|
||||
break;
|
||||
case 'Enter':
|
||||
this.handleClick(e);
|
||||
break;
|
||||
case 'ArrowDown':
|
||||
element = this.node.childNodes[index + 1];
|
||||
if (element) {
|
||||
element.focus();
|
||||
this.props.onChange(Boolean(element.getAttribute('data-index')));
|
||||
}
|
||||
break;
|
||||
case 'ArrowUp':
|
||||
element = this.node.childNodes[index - 1];
|
||||
if (element) {
|
||||
element.focus();
|
||||
this.props.onChange(Boolean(element.getAttribute('data-index')));
|
||||
}
|
||||
break;
|
||||
case 'Home':
|
||||
element = this.node.firstChild;
|
||||
if (element) {
|
||||
element.focus();
|
||||
this.props.onChange(Boolean(element.getAttribute('data-index')));
|
||||
}
|
||||
break;
|
||||
case 'End':
|
||||
element = this.node.lastChild;
|
||||
if (element) {
|
||||
element.focus();
|
||||
this.props.onChange(Boolean(element.getAttribute('data-index')));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
handleClick = e => {
|
||||
const value = Boolean(e.currentTarget.getAttribute('data-index'));
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
this.props.onClose();
|
||||
this.props.onChange(value);
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
document.addEventListener('click', this.handleDocumentClick, false);
|
||||
document.addEventListener('touchend', this.handleDocumentClick, listenerOptions);
|
||||
if (this.focusedItem) this.focusedItem.focus();
|
||||
this.setState({ mounted: true });
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
document.removeEventListener('click', this.handleDocumentClick, false);
|
||||
document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions);
|
||||
}
|
||||
|
||||
setRef = c => {
|
||||
this.node = c;
|
||||
}
|
||||
|
||||
setFocusRef = c => {
|
||||
this.focusedItem = c;
|
||||
}
|
||||
|
||||
render () {
|
||||
const { mounted } = this.state;
|
||||
const { style, items, value } = this.props;
|
||||
|
||||
return (
|
||||
<Motion defaultStyle={{ opacity: 0, scaleX: 0.85, scaleY: 0.75 }} style={{ opacity: spring(1, { damping: 35, stiffness: 400 }), scaleX: spring(1, { damping: 35, stiffness: 400 }), scaleY: spring(1, { damping: 35, stiffness: 400 }) }}>
|
||||
{({ opacity, scaleX, scaleY }) => (
|
||||
// It should not be transformed when mounting because the resulting
|
||||
// size will be used to determine the coordinate of the menu by
|
||||
// react-overlays
|
||||
<div className='privacy-dropdown__dropdown' style={{ ...style, opacity: opacity, transform: mounted ? `scale(${scaleX}, ${scaleY})` : null }} role='listbox' ref={this.setRef}>
|
||||
{items.map(item => (
|
||||
<div role='option' tabIndex='0' key={item.value} data-index={item.value ? item.value : undefined} onKeyDown={this.handleKeyDown} onClick={this.handleClick} className={classNames('privacy-dropdown__option', { active: item.value === value })} aria-selected={item.value === value} ref={item.value === value ? this.setFocusRef : null}>
|
||||
<div className='privacy-dropdown__option__icon'>
|
||||
<i className={`fa fa-fw fa-${item.icon}`} />
|
||||
</div>
|
||||
|
||||
<div className='privacy-dropdown__option__content'>
|
||||
<strong>{item.text}</strong>
|
||||
{item.meta}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Motion>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@injectIntl
|
||||
export default class FederationDropdown extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
isUserTouching: PropTypes.func,
|
||||
isModalOpen: PropTypes.bool.isRequired,
|
||||
onModalOpen: PropTypes.func,
|
||||
onModalClose: PropTypes.func,
|
||||
value: PropTypes.bool.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
state = {
|
||||
open: false,
|
||||
placement: null,
|
||||
};
|
||||
|
||||
handleToggle = ({ target }) => {
|
||||
if (this.props.isUserTouching()) {
|
||||
if (this.state.open) {
|
||||
this.props.onModalClose();
|
||||
} else {
|
||||
this.props.onModalOpen({
|
||||
actions: this.options.map(option => ({ ...option, active: option.value === this.props.value })),
|
||||
onClick: this.handleModalActionClick,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const { top } = target.getBoundingClientRect();
|
||||
this.setState({ placement: top * 2 < innerHeight ? 'bottom' : 'top' });
|
||||
this.setState({ open: !this.state.open });
|
||||
}
|
||||
}
|
||||
|
||||
handleModalActionClick = (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const { value } = this.options[Boolean(e.currentTarget.getAttribute('data-index'))];
|
||||
|
||||
this.props.onModalClose();
|
||||
this.props.onChange(value);
|
||||
}
|
||||
|
||||
handleKeyDown = e => {
|
||||
switch(e.key) {
|
||||
case 'Escape':
|
||||
this.handleClose();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
handleClose = () => {
|
||||
this.setState({ open: false });
|
||||
}
|
||||
|
||||
handleChange = value => {
|
||||
this.props.onChange(value);
|
||||
}
|
||||
|
||||
componentWillMount () {
|
||||
const { intl: { formatMessage } } = this.props;
|
||||
|
||||
this.options = [
|
||||
{ icon: 'link', value: true, text: formatMessage(messages.federate_short), meta: formatMessage(messages.federate_long) },
|
||||
{ icon: 'chain-broken', value: false, text: formatMessage(messages.local_only_short), meta: formatMessage(messages.local_only_long) },
|
||||
];
|
||||
}
|
||||
|
||||
render () {
|
||||
const { value, intl } = this.props;
|
||||
const { open, placement } = this.state;
|
||||
|
||||
const valueOption = this.options.find(item => item.value === value);
|
||||
|
||||
return (
|
||||
<div className={classNames('privacy-dropdown', { active: open })} onKeyDown={this.handleKeyDown}>
|
||||
<div className={classNames('privacy-dropdown__value', { active: this.options.indexOf(valueOption) === 0 })}>
|
||||
<IconButton
|
||||
className='privacy-dropdown__value-icon'
|
||||
icon={valueOption.icon}
|
||||
title={intl.formatMessage(messages.change_federation)}
|
||||
size={18}
|
||||
expanded={open}
|
||||
active={open}
|
||||
inverted
|
||||
onClick={this.handleToggle}
|
||||
style={{ height: null, lineHeight: '27px' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Overlay show={open} placement={placement} target={this}>
|
||||
<FederationDropdownMenu
|
||||
items={this.options}
|
||||
value={value}
|
||||
onClose={this.handleClose}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
</Overlay>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
@ -18,6 +18,7 @@ const mapStateToProps = state => ({
|
||||
spoiler: state.getIn(['compose', 'spoiler']),
|
||||
spoiler_text: state.getIn(['compose', 'spoiler_text']),
|
||||
privacy: state.getIn(['compose', 'privacy']),
|
||||
federation: state.getIn(['compose', 'federation']),
|
||||
focusDate: state.getIn(['compose', 'focusDate']),
|
||||
caretPosition: state.getIn(['compose', 'caretPosition']),
|
||||
preselectDate: state.getIn(['compose', 'preselectDate']),
|
||||
|
@ -0,0 +1,24 @@
|
||||
import { connect } from 'react-redux';
|
||||
import FederationDropdown from '../components/federation_dropdown';
|
||||
import { changeComposeFederation } from '../../../actions/compose';
|
||||
import { openModal, closeModal } from '../../../actions/modal';
|
||||
import { isUserTouching } from '../../../is_mobile';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
isModalOpen: state.get('modal').modalType === 'ACTIONS',
|
||||
value: state.getIn(['compose', 'federation']),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
|
||||
onChange (value) {
|
||||
dispatch(changeComposeFederation(value));
|
||||
},
|
||||
|
||||
isUserTouching,
|
||||
onModalOpen: props => dispatch(openModal('ACTIONS', props)),
|
||||
onModalClose: () => dispatch(closeModal()),
|
||||
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(FederationDropdown);
|
@ -23,6 +23,7 @@ import {
|
||||
COMPOSE_SPOILERNESS_CHANGE,
|
||||
COMPOSE_SPOILER_TEXT_CHANGE,
|
||||
COMPOSE_VISIBILITY_CHANGE,
|
||||
COMPOSE_FEDERATION_CHANGE,
|
||||
COMPOSE_COMPOSING_CHANGE,
|
||||
COMPOSE_EMOJI_INSERT,
|
||||
COMPOSE_UPLOAD_CHANGE_REQUEST,
|
||||
@ -44,6 +45,7 @@ const initialState = ImmutableMap({
|
||||
spoiler: false,
|
||||
spoiler_text: '',
|
||||
privacy: null,
|
||||
federation: null,
|
||||
text: '',
|
||||
focusDate: null,
|
||||
caretPosition: null,
|
||||
@ -57,6 +59,7 @@ const initialState = ImmutableMap({
|
||||
suggestion_token: null,
|
||||
suggestions: ImmutableList(),
|
||||
default_privacy: 'public',
|
||||
default_federation: true,
|
||||
default_sensitive: false,
|
||||
resetFileKey: Math.floor((Math.random() * 0x10000)),
|
||||
idempotencyKey: null,
|
||||
@ -81,6 +84,7 @@ function clearAll(state) {
|
||||
map.set('is_submitting', false);
|
||||
map.set('in_reply_to', null);
|
||||
map.set('privacy', state.get('default_privacy'));
|
||||
map.set('federation', state.get('default_federation'));
|
||||
map.set('sensitive', false);
|
||||
map.update('media_attachments', list => list.clear());
|
||||
map.set('idempotencyKey', uuid());
|
||||
@ -209,6 +213,10 @@ export default function compose(state = initialState, action) {
|
||||
return state
|
||||
.set('spoiler_text', action.text)
|
||||
.set('idempotencyKey', uuid());
|
||||
case COMPOSE_FEDERATION_CHANGE:
|
||||
return state
|
||||
.set('federation', action.value)
|
||||
.set('idempotencyKey', uuid());
|
||||
case COMPOSE_VISIBILITY_CHANGE:
|
||||
return state
|
||||
.set('privacy', action.value)
|
||||
@ -224,6 +232,7 @@ export default function compose(state = initialState, action) {
|
||||
map.set('in_reply_to', action.status.get('id'));
|
||||
map.set('text', statusToTextMentions(state, action.status));
|
||||
map.set('privacy', privacyPreference(action.status.get('visibility'), state.get('default_privacy')));
|
||||
map.set('federation', !action.status.get('local_only'));
|
||||
map.set('focusDate', new Date());
|
||||
map.set('caretPosition', null);
|
||||
map.set('preselectDate', new Date());
|
||||
@ -245,6 +254,7 @@ export default function compose(state = initialState, action) {
|
||||
map.set('spoiler', false);
|
||||
map.set('spoiler_text', '');
|
||||
map.set('privacy', state.get('default_privacy'));
|
||||
map.set('federation', state.get('default_federation'));
|
||||
map.set('idempotencyKey', uuid());
|
||||
});
|
||||
case COMPOSE_SUBMIT_REQUEST:
|
||||
@ -313,6 +323,7 @@ export default function compose(state = initialState, action) {
|
||||
map.set('text', unescapeHTML(expandMentions(action.status)));
|
||||
map.set('in_reply_to', action.status.get('in_reply_to_id'));
|
||||
map.set('privacy', action.status.get('visibility'));
|
||||
map.set('federation', !action.status.get('local_only'));
|
||||
map.set('media_attachments', action.status.get('media_attachments'));
|
||||
map.set('focusDate', new Date());
|
||||
map.set('caretPosition', null);
|
||||
|
Loading…
Reference in New Issue
Block a user