This commit is contained in:
mgabdev
2020-02-08 01:12:01 -05:00
parent fa66d082f8
commit 1c98dd283e
146 changed files with 1462 additions and 1951 deletions

View File

@@ -1,8 +1,6 @@
import { length } from 'stringz';
import classNames from 'classnames';
import './character_counter.scss';
export default class CharacterCounter extends PureComponent {
static propTypes = {

View File

@@ -0,0 +1,71 @@
import classNames from 'classnames/bind'
import Icon from '../../../components/icon'
export default class ComposeExtraButton extends PureComponent {
static propTypes = {
title: PropTypes.string,
disabled: PropTypes.bool,
onClick: PropTypes.func,
icon: PropTypes.string,
}
state = {
hovering: false,
}
handleOnMouseEnter = () => {
this.setState({ hovering: true })
}
handleOnMouseLeave = () => {
this.setState({ hovering: false })
}
render() {
const { title, disabled, onClick, icon, children } = this.props
const { hovering } = this.state
const cx = classNames.bind(styles)
const btnClasses = cx({
default: 1,
circle: 1,
flexRow: 1,
paddingVertical10PX: 1,
paddingHorizontal10PX: 1,
cursorPointer: 1,
backgroundSubtle: !hovering,
backgroundSubtle2: hovering,
})
const titleClasses = cx({
default: 1,
marginLeft5PX: 1,
text: 1,
lineHeight15: 1,
fontSize12PX: 1,
fontWeight500: 1,
colorSubtle: 1,
displayNone: !hovering,
})
return (
<div className={[styles.default, styles.marginRight10PX].join(' ')}>
<button
className={btnClasses}
title={title}
disabled={disabled}
onClick={onClick}
onMouseEnter={() => this.handleOnMouseEnter()}
onMouseLeave={() => this.handleOnMouseLeave()}
>
<Icon icon={icon} width='18px' height='18px' />
<span className={titleClasses}>
{title}
</span>
</button>
{children}
</div>
)
}
}

View File

@@ -7,22 +7,20 @@ import CharacterCounter from '../character_counter';
import UploadForm from '../upload_form';
import ReplyIndicatorContainer from '../../containers/reply_indicator_container';
import AutosuggestTextbox from '../../../../components/autosuggest_textbox';
import PollButtonContainer from '../../containers/poll_button_container';
import UploadButtonContainer from '../../containers/upload_button_container';
import SpoilerButtonContainer from '../../containers/spoiler_button_container';
import PrivacyDropdownContainer from '../../containers/privacy_dropdown_container';
import PollButton from '../../components/poll_button';
import UploadButton from '../../components/upload_button';
import SpoilerButton from '../../components/spoiler_button';
import PrivacyDropdown from '../../components/privacy_dropdown';
import EmojiPickerDropdown from '../../containers/emoji_picker_dropdown_container';
import PollFormContainer from '../../containers/poll_form_container';
import WarningContainer from '../../containers/warning_container';
import SchedulePostDropdownContainer from '../../containers/schedule_post_dropdown_container';
import SchedulePostDropdown from '../../components/schedule_post_dropdown';
import QuotedStatusPreviewContainer from '../../containers/quoted_status_preview_container';
import Icon from '../../../../components/icon';
import Button from '../../../../components/button';
import { isMobile } from '../../../../utils/is_mobile';
import { countableText } from '../../util/counter';
import './compose_form.scss';
const allowedAroundShortCode = '><\u0085\u0020\u00a0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029\u0009\u000a\u000b\u000c\u000d';
const maxPostCharacterCount = 3000;
@@ -30,7 +28,7 @@ const messages = defineMessages({
placeholder: { id: 'compose_form.placeholder', defaultMessage: 'What is on your mind?' },
spoiler_placeholder: { id: 'compose_form.spoiler_placeholder', defaultMessage: 'Write your warning here' },
publish: { id: 'compose_form.publish', defaultMessage: 'Gab' },
publishLoud: { id: 'compose_form.publish_loud', defaultMessage: '{publish}!' },
publishLoud: { id: 'compose_form.publish_loud', defaultMessage: '{publish}' },
schedulePost: { id: 'compose_form.schedule_post', defaultMessage: 'Schedule Post' }
});
@@ -220,29 +218,22 @@ class ComposeForm extends ImmutablePureComponent {
const disabledButton = disabled || this.props.isUploading || this.props.isChangingUpload || length(text) > maxPostCharacterCount || (text.length !== 0 && text.trim().length === 0 && !anyMedia);
const shouldAutoFocus = autoFocus && !showSearch && !isMobile(window.innerWidth)
let publishText = '';
if (this.props.privacy === 'private' || this.props.privacy === 'direct') {
publishText = <span className='compose-form__publish-private'><Icon id='lock' /> {intl.formatMessage(messages.publish)}</span>;
} else {
publishText = this.props.privacy !== 'unlisted' ? intl.formatMessage(messages.publishLoud, { publish: intl.formatMessage(messages.publish) }) : intl.formatMessage(messages.publish);
}
if (scheduledAt) {
publishText = intl.formatMessage(messages.schedulePost);
}
const composeClassNames = classNames({
'compose-form': true,
'condensed': condensed,
});
return (
<div className={composeClassNames} ref={this.setForm} onClick={this.handleClick}>
<WarningContainer />
<div
className={[styles.default, styles.flexGrow1].join(' ')}
ref={this.setForm}
onClick={this.handleClick}
>
{ /* <WarningContainer /> */ }
{ !shouldCondense && <ReplyIndicatorContainer /> }
{ /* !shouldCondense && <ReplyIndicatorContainer /> */ }
{ /*
<div className={`spoiler-input ${this.props.spoiler ? 'spoiler-input--visible' : ''}`}>
<AutosuggestTextbox
placeholder={intl.formatMessage(messages.spoiler_placeholder)}
@@ -260,13 +251,14 @@ class ComposeForm extends ImmutablePureComponent {
className='spoiler-input__input'
/>
</div>
*/ }
{ /*
<div className='emoji-picker-wrapper'>
<EmojiPickerDropdown onPickEmoji={this.handleEmojiPick} />
</div>
</div> */ }
<AutosuggestTextbox
textarea={true}
ref={(isModalOpen && shouldCondense) ? null : this.setAutosuggestTextarea}
placeholder={intl.formatMessage(messages.placeholder)}
disabled={disabled}
@@ -280,38 +272,37 @@ class ComposeForm extends ImmutablePureComponent {
onSuggestionSelected={this.onSuggestionSelected}
onPaste={onPaste}
autoFocus={shouldAutoFocus}
textarea
>
{
{ /*
!condensed &&
<div className='compose-form__modifiers'>
<UploadForm />
{!edit && <PollFormContainer />}
</div>
}
*/ }
</AutosuggestTextbox>
{quoteOfId && <QuotedStatusPreviewContainer id={quoteOfId} />}
{ /* quoteOfId && <QuotedStatusPreviewContainer id={quoteOfId} /> */ }
{
!condensed &&
<div className='compose-form__buttons-wrapper'>
<div className='compose-form__buttons'>
<UploadButtonContainer />
{!edit && <PollButtonContainer />}
<PrivacyDropdownContainer />
<SpoilerButtonContainer />
<SchedulePostDropdownContainer
position={isModalOpen ? 'top' : undefined}
/>
/* !condensed && */
<div className={[styles.default, styles.flexRow, styles.marginTop10PX].join(' ')}>
<div className={[styles.default, styles.flexRow, styles.marginRightAuto].join(' ')}>
<UploadButton />
{
!edit && <PollButton />
}
<PrivacyDropdown />
<SpoilerButton />
<SchedulePostDropdown position={isModalOpen ? 'top' : undefined} />
</div>
<CharacterCounter max={maxPostCharacterCount} text={text} />
</div>
}
{
!condensed &&
<div className='compose-form__publish'>
<div className='compose-form__publish-button-wrapper'><Button text={publishText} onClick={this.handleSubmit} disabled={disabledButton} block /></div>
<Button
text={intl.formatMessage(scheduledAt ? messages.schedulePost : messages.publish)}
onClick={this.handleSubmit}
disabled={disabledButton}
/>
</div>
}

View File

@@ -7,8 +7,6 @@ import Overlay from 'react-overlays/lib/Overlay';
import { EmojiPicker as EmojiPickerAsync } from '../../../ui/util/async-components';
import { buildCustomEmojis } from '../../../../components/emoji/emoji';
import './emoji_picker_dropdown.scss';
const messages = defineMessages({
emoji: { id: 'emoji_button.label', defaultMessage: 'Insert emoji' },
emoji_search: { id: 'emoji_button.search', defaultMessage: 'Search...' },

View File

@@ -7,8 +7,6 @@ import Permalink from '../../../../components/permalink';
import IconButton from '../../../../components/icon_button';
import { me } from '../../../../initial_state';
import './navigation_bar.scss';
const mapStateToProps = state => {
return {
account: state.getIn(['accounts', me]),

View File

@@ -0,0 +1,62 @@
import { defineMessages, injectIntl } from 'react-intl';
import { addPoll, removePoll } from '../../../actions/compose';
import ComposeExtraButton from './compose_extra_button'
const messages = defineMessages({
add_poll: { id: 'poll_button.add_poll', defaultMessage: 'Add poll' },
title: { id: 'poll_button.title', defaultMessage: 'Poll' },
remove_poll: { id: 'poll_button.remove_poll', defaultMessage: 'Remove poll' },
});
const mapStateToProps = state => ({
unavailable: state.getIn(['compose', 'is_uploading']) || (state.getIn(['compose', 'media_attachments']).size > 0),
active: state.getIn(['compose', 'poll']) !== null,
});
const mapDispatchToProps = dispatch => ({
onClick() {
dispatch((_, getState) => {
if (getState().getIn(['compose', 'poll'])) {
dispatch(removePoll());
} else {
dispatch(addPoll());
}
});
},
});
export default
@connect(mapStateToProps, mapDispatchToProps)
@injectIntl
class PollButton extends PureComponent {
static propTypes = {
disabled: PropTypes.bool,
unavailable: PropTypes.bool,
active: PropTypes.bool,
onClick: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
};
handleClick = () => {
this.props.onClick();
}
render() {
const { intl, active, unavailable, disabled } = this.props;
if (unavailable) return null;
return (
<ComposeExtraButton
title={intl.formatMessage(active ? messages.remove_poll : messages.title)}
disabled={disabled}
onClick={this.handleClick}
icon='tasks'
/>
)
}
}

View File

@@ -1 +0,0 @@
export { default } from './poll_button';

View File

@@ -1,53 +0,0 @@
import IconButton from '../../../../components/icon_button';
import { defineMessages, injectIntl } from 'react-intl';
const messages = defineMessages({
add_poll: { id: 'poll_button.add_poll', defaultMessage: 'Add a poll' },
remove_poll: { id: 'poll_button.remove_poll', defaultMessage: 'Remove poll' },
});
const iconStyle = {
height: null,
lineHeight: '27px',
};
export default
@injectIntl
class PollButton extends PureComponent {
static propTypes = {
disabled: PropTypes.bool,
unavailable: PropTypes.bool,
active: PropTypes.bool,
onClick: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
};
handleClick = () => {
this.props.onClick();
}
render () {
const { intl, active, unavailable, disabled } = this.props;
if (unavailable) {
return null;
}
return (
<div>
<IconButton
icon='tasks'
title={intl.formatMessage(active ? messages.remove_poll : messages.add_poll)}
disabled={disabled}
onClick={this.handleClick}
className={`${active ? 'active' : ''}`}
size={18}
inverted
style={iconStyle}
/>
</div>
);
}
}

View File

@@ -6,8 +6,6 @@ import IconButton from '../../../../components/icon_button';
import Icon from '../../../../components/icon';
import AutosuggestTextbox from '../../../../components/autosuggest_textbox';
import './poll_form.scss';
const messages = defineMessages({
option_placeholder: { id: 'compose_form.poll.option_placeholder', defaultMessage: 'Choice {number}' },
add_option: { id: 'compose_form.poll.add_option', defaultMessage: 'Add a choice' },

View File

@@ -1,13 +1,14 @@
import { injectIntl, defineMessages } from 'react-intl';
import spring from 'react-motion/lib/spring';
import detectPassiveEvents from 'detect-passive-events';
import classNames from 'classnames';
import Overlay from 'react-overlays/lib/Overlay';
import IconButton from '../../../../components/icon_button';
import Motion from '../../../ui/util/optional_motion';
import Icon from '../../../../components/icon';
import './privacy_dropdown.scss';
import { injectIntl, defineMessages } from 'react-intl'
import spring from 'react-motion/lib/spring'
import detectPassiveEvents from 'detect-passive-events'
import classNames from 'classnames'
import Overlay from 'react-overlays/lib/Overlay'
import { changeComposeVisibility } from '../../../actions/compose'
import { openModal, closeModal } from '../../../actions/modal'
import { isUserTouching } from '../../../utils/is_mobile'
import Motion from '../../ui/util/optional_motion'
import Icon from '../../../components/icon'
import ComposeExtraButton from './compose_extra_button'
const messages = defineMessages({
public_short: { id: 'privacy.public.short', defaultMessage: 'Public' },
@@ -16,11 +17,11 @@ const messages = defineMessages({
unlisted_long: { id: 'privacy.unlisted.long', defaultMessage: 'Do not show in public timelines' },
private_short: { id: 'privacy.private.short', defaultMessage: 'Followers-only' },
private_long: { id: 'privacy.private.long', defaultMessage: 'Post to followers only' },
direct_short: { id: 'privacy.direct.short', defaultMessage: 'Direct' },
change_privacy: { id: 'privacy.change', defaultMessage: 'Adjust status privacy' },
});
visibility: { id: 'privacy.visibility', defaultMessage: 'Visibility' },
})
const listenerOptions = detectPassiveEvents.hasSupport ? { passive: true } : false;
const listenerOptions = detectPassiveEvents.hasSupport ? { passive: true } : false
class PrivacyDropdownMenu extends PureComponent {
@@ -31,96 +32,96 @@ class PrivacyDropdownMenu extends PureComponent {
placement: PropTypes.string.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();
this.props.onClose()
}
}
handleKeyDown = e => {
const { items } = this.props;
const value = e.currentTarget.getAttribute('data-index');
const { items } = this.props
const value = e.currentTarget.getAttribute('data-index')
const index = items.findIndex(item => {
return (item.value === value);
});
let element;
return (item.value === value)
})
let element
switch(e.key) {
case 'Escape':
this.props.onClose();
break;
this.props.onClose()
break
case 'Enter':
this.handleClick(e);
break;
this.handleClick(e)
break
case 'ArrowDown':
element = this.node.childNodes[index + 1];
element = this.node.childNodes[index + 1]
if (element) {
element.focus();
this.props.onChange(element.getAttribute('data-index'));
element.focus()
this.props.onChange(element.getAttribute('data-index'))
}
break;
break
case 'ArrowUp':
element = this.node.childNodes[index - 1];
element = this.node.childNodes[index - 1]
if (element) {
element.focus();
this.props.onChange(element.getAttribute('data-index'));
element.focus()
this.props.onChange(element.getAttribute('data-index'))
}
break;
break
case 'Home':
element = this.node.firstChild;
element = this.node.firstChild
if (element) {
element.focus();
this.props.onChange(element.getAttribute('data-index'));
element.focus()
this.props.onChange(element.getAttribute('data-index'))
}
break;
break
case 'End':
element = this.node.lastChild;
element = this.node.lastChild
if (element) {
element.focus();
this.props.onChange(element.getAttribute('data-index'));
element.focus()
this.props.onChange(element.getAttribute('data-index'))
}
break;
break
}
}
handleClick = e => {
const value = e.currentTarget.getAttribute('data-index');
const value = e.currentTarget.getAttribute('data-index')
e.preventDefault();
e.preventDefault()
this.props.onClose();
this.props.onChange(value);
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 });
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);
document.removeEventListener('click', this.handleDocumentClick, false)
document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions)
}
setRef = c => {
this.node = c;
this.node = c
}
setFocusRef = c => {
this.focusedItem = c;
this.focusedItem = c
}
render () {
const { mounted } = this.state;
const { style, items, placement, value } = this.props;
const { mounted } = this.state
const { style, items, placement, 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 }) }}>
@@ -144,12 +145,31 @@ class PrivacyDropdownMenu extends PureComponent {
</div>
)}
</Motion>
);
)
}
}
export default @injectIntl
const mapStateToProps = state => ({
isModalOpen: state.get('modal').modalType === 'ACTIONS',
value: state.getIn(['compose', 'privacy']),
})
const mapDispatchToProps = dispatch => ({
onChange (value) {
dispatch(changeComposeVisibility(value))
},
isUserTouching,
onModalOpen: props => dispatch(openModal('ACTIONS', props)),
onModalClose: () => dispatch(closeModal()),
})
export default
@connect(mapStateToProps, mapDispatchToProps)
@injectIntl
class PrivacyDropdown extends PureComponent {
static propTypes = {
@@ -160,84 +180,78 @@ class PrivacyDropdown extends PureComponent {
value: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
};
}
state = {
open: false,
placement: 'bottom',
};
}
handleToggle = ({ target }) => {
if (this.props.isUserTouching()) {
if (this.state.open) {
this.props.onModalClose();
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 });
const { top } = target.getBoundingClientRect()
this.setState({ placement: top * 2 < innerHeight ? 'bottom' : 'top' })
this.setState({ open: !this.state.open })
}
}
handleModalActionClick = (e) => {
e.preventDefault();
e.preventDefault()
const { value } = this.options[e.currentTarget.getAttribute('data-index')];
const { value } = this.options[e.currentTarget.getAttribute('data-index')]
this.props.onModalClose();
this.props.onChange(value);
this.props.onModalClose()
this.props.onChange(value)
}
handleKeyDown = e => {
switch(e.key) {
case 'Escape':
this.handleClose();
break;
this.handleClose()
break
}
}
handleClose = () => {
this.setState({ open: false });
this.setState({ open: false })
}
handleChange = value => {
this.props.onChange(value);
this.props.onChange(value)
}
componentWillMount () {
const { intl: { formatMessage } } = this.props;
const { intl: { formatMessage } } = this.props
this.options = [
{ icon: 'globe', value: 'public', text: formatMessage(messages.public_short), meta: formatMessage(messages.public_long) },
{ 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) },
];
]
}
render () {
const { value, intl } = this.props;
const { open, placement } = this.state;
const { value, intl } = this.props
const { open, placement } = this.state
const valueOption = this.options.find(item => item.value === value);
const valueOption = this.options.find(item => item.value === value)
return (
<div className={classNames('privacy-dropdown', placement, { active: open })} onKeyDown={this.handleKeyDown}>
<div className={classNames('privacy-dropdown__value', { active: this.options.indexOf(valueOption) === 0 })}>
<IconButton
className='privacy-dropdown__value-icon'
<ComposeExtraButton
icon={valueOption.icon}
title={intl.formatMessage(messages.change_privacy)}
size={18}
expanded={open}
active={open}
inverted
title={intl.formatMessage(messages.visibility)}
onClick={this.handleToggle}
style={{ height: null, lineHeight: '27px' }}
/>
</div>
@@ -251,7 +265,7 @@ class PrivacyDropdown extends PureComponent {
/>
</Overlay>
</div>
);
)
}
}

View File

@@ -1 +0,0 @@
export { default } from './privacy_dropdown';

View File

@@ -1,101 +0,0 @@
.privacy-dropdown {
&__dropdown {
position: absolute;
background: $simple-background-color;
box-shadow: 2px 4px 15px rgba($base-shadow-color, 0.4);
border-radius: 4px;
margin-left: 40px;
overflow: hidden;
z-index: 10000;
&.top {
transform-origin: 50% 100%;
}
&.bottom {
transform-origin: 50% 0;
}
}
&__option {
color: $inverted-text-color;
padding: 10px;
cursor: pointer;
display: flex;
&:hover,
&.active {
background: $ui-highlight-color;
color: $primary-text-color;
outline: 0;
.privacy-dropdown__option__content {
color: $primary-text-color;
strong {
color: $primary-text-color;
}
}
}
&.active:hover {
background: lighten($ui-highlight-color, 4%);
}
&__icon {
margin-right: 10px;
@include flex(center, center);
}
&__content {
flex: 1 1 auto;
color: $lighter-text-color;
strong {
font-weight: 500;
display: block;
color: $inverted-text-color;
@each $lang in $cjk-langs {
&:lang(#{$lang}) {
font-weight: 700;
}
}
}
}
}
&.active & {
&__dropdown {
display: block;
box-shadow: 2px 4px 6px rgba($base-shadow-color, 0.1);
}
&__value {
background: $simple-background-color;
border-radius: 4px 4px 0 0;
box-shadow: 0 -4px 4px rgba($base-shadow-color, 0.1);
.icon-button {
transition: none;
}
&.active {
background: $ui-highlight-color;
.icon-button {
color: $primary-text-color;
}
}
}
}
&.active.top & {
&__value {
border-radius: 0 0 4px 4px;
}
}
}

View File

@@ -7,8 +7,6 @@ import IconButton from '../../../../components/icon_button';
import DisplayName from '../../../../components/display_name';
import { isRtl } from '../../../../utils/rtl';
import './reply_indicator.scss';
const messages = defineMessages({
cancel: { id: 'reply_indicator.cancel', defaultMessage: 'Cancel' },
});

View File

@@ -1,32 +1,53 @@
import { injectIntl, defineMessages } from 'react-intl';
import DatePicker from 'react-datepicker';
import IconButton from '../../../components/icon_button';
import { isMobile } from '../../../utils/is_mobile';
import { injectIntl, defineMessages } from 'react-intl'
import DatePicker from 'react-datepicker'
import IconButton from '../../../components/icon_button'
import { changeScheduledAt } from '../../../actions/compose'
import { openModal } from '../../../actions/modal'
import { me } from '../../../initial_state'
import { isMobile } from '../../../utils/is_mobile'
import ComposeExtraButton from './compose_extra_button'
import "react-datepicker/dist/react-datepicker.css";
import 'react-datepicker/dist/react-datepicker.css'
const messages = defineMessages({
schedule_status: { id: 'schedule_status.title', defaultMessage: 'Schedule Status' },
});
schedule_status: { id: 'schedule_status.title', defaultMessage: 'Schedule' },
})
const mapStateToProps = state => ({
date: state.getIn(['compose', 'scheduled_at']),
isPro: state.getIn(['accounts', me, 'is_pro']),
})
const mapDispatchToProps = dispatch => ({
setScheduledAt (date) {
dispatch(changeScheduledAt(date))
},
onOpenProUpgradeModal() {
dispatch(openModal('PRO_UPGRADE'))
},
})
class DatePickerWrapper extends PureComponent {
static propTypes = {
value: PropTypes.string,
onClick: PropTypes.func,
};
}
render() {
const { value, onClick } = this.props;
const { value, onClick } = this.props
return (
<button className="schedule-post-dropdown-wrapper" onClick={onClick}>
<button className='schedule-post-dropdown-wrapper' onClick={onClick}>
{value}
</button>
)
}
}
export default @injectIntl
export default
@injectIntl
@connect(mapStateToProps, mapDispatchToProps)
class SchedulePostDropdown extends PureComponent {
static propTypes = {
@@ -36,43 +57,37 @@ class SchedulePostDropdown extends PureComponent {
isPro: PropTypes.bool,
onOpenProUpgradeModal: PropTypes.func.isRequired,
position: PropTypes.string,
};
}
handleToggle = () => {
if (!this.props.isPro) {
return this.props.onOpenProUpgradeModal();
return this.props.onOpenProUpgradeModal()
}
const { date } = this.props;
const value = date ? null : new Date();
this.handleSetDate(value);
const { date } = this.props
const value = date ? null : new Date()
this.handleSetDate(value)
}
handleSetDate = (date) => {
this.props.setScheduledAt(date);
this.props.setScheduledAt(date)
}
render () {
const { intl, date, isPro, position } = this.props;
const { intl, date, isPro, position } = this.props
const open = !!date;
const datePickerDisabled = !isPro;
const withPortal = isMobile(window.innerWidth);
const popperPlacement = position || undefined;
const open = !!date
const datePickerDisabled = !isPro
const withPortal = isMobile(window.innerWidth)
const popperPlacement = position || undefined
return (
<div className='schedule-post-dropdown'>
<div className='schedule-post-dropdown__container'>
<IconButton
inverted
className='schedule-post-dropdown__icon'
<div className={[].join(' ')}>
<div className={[].join(' ')}>
<ComposeExtraButton
icon='calendar'
title={intl.formatMessage(messages.schedule_status)}
size={18}
expanded={open}
active={open}
onClick={this.handleToggle}
style={{ height: null, lineHeight: '27px' }}
/>
</div>
{
@@ -83,10 +98,10 @@ class SchedulePostDropdown extends PureComponent {
minDate={new Date()}
selected={date}
onChange={date => this.handleSetDate(date)}
timeFormat="p"
timeFormat='p'
timeIntervals={15}
timeCaption="Time"
dateFormat="MMM d, yyyy h:mm aa"
timeCaption='Time'
dateFormat='MMM d, yyyy h:mm aa'
disabled={datePickerDisabled}
showTimeSelect
customInput={<DatePickerWrapper />}
@@ -95,18 +110,18 @@ class SchedulePostDropdown extends PureComponent {
popperModifiers={{
offset: {
enabled: true,
offset: "0px, 5px"
offset: '0px, 5px'
},
preventOverflow: {
enabled: true,
escapeWithReference: false,
boundariesElement: "viewport"
boundariesElement: 'viewport'
}
}}
/>
}
</div>
);
)
}
}

View File

@@ -0,0 +1,50 @@
import { injectIntl, defineMessages } from 'react-intl'
import { changeComposeSpoilerness } from '../../../actions/compose'
import ComposeExtraButton from './compose_extra_button'
const messages = defineMessages({
marked: { id: 'compose_form.spoiler.marked', defaultMessage: 'Text is hidden behind warning' },
unmarked: { id: 'compose_form.spoiler.unmarked', defaultMessage: 'Text is not hidden' },
title: { id: 'compose_form.spoiler.title', defaultMessage: 'Warning' },
})
const mapStateToProps = (state) => ({
active: state.getIn(['compose', 'spoiler']),
})
const mapDispatchToProps = dispatch => ({
onClick () {
dispatch(changeComposeSpoilerness())
},
})
export default
@injectIntl
@connect(mapStateToProps, mapDispatchToProps)
class SpoilerButton extends PureComponent {
static propTypes = {
active: PropTypes.bool,
intl: PropTypes.map,
}
handleClick = (e) => {
e.preventDefault()
this.props.onClick()
}
render () {
const { active, intl } = this.props
return (
<ComposeExtraButton
title={intl.formatMessage(messages.title)}
icon='warning'
onClick={this.handleClick}
/>
)
}
}

View File

@@ -1 +0,0 @@
export { default } from './text_icon_button';

View File

@@ -1,33 +0,0 @@
export default class TextIconButton extends PureComponent {
static propTypes = {
label: PropTypes.string.isRequired,
title: PropTypes.string,
active: PropTypes.bool,
onClick: PropTypes.func.isRequired,
ariaControls: PropTypes.string,
};
handleClick = (e) => {
e.preventDefault();
this.props.onClick();
}
render () {
const { label, title, active, ariaControls } = this.props;
return (
<button
title={title}
aria-label={title}
className={`text-icon-button ${active ? 'text-icon-button--active' : ''}`}
aria-expanded={active}
onClick={this.handleClick}
aria-controls={ariaControls}
>
{label}
</button>
);
}
}

View File

@@ -1,32 +0,0 @@
.text-icon-button {
color: $gab-secondary-text;
border: none;
background: transparent;
cursor: pointer;
padding: 0 3px;
outline: 0;
transition: color 100ms ease-in;
@include text-sizing(11px, 600, 27px);
&:hover,
&:active,
&:focus {
color: darken($lighter-text-color, 7%);
transition: color 200ms ease-out;
}
&--active {
color: $highlight-text-color;
}
&::-moz-focus-inner {
border: 0;
}
&::-moz-focus-inner,
&:focus,
&:active {
outline: 0 !important;
}
}

View File

@@ -6,8 +6,6 @@ import spring from 'react-motion/lib/spring';
import Motion from '../../../ui/util/optional_motion';
import IconButton from '../../../../components/icon_button';
import './upload.scss';
const messages = defineMessages({
description: { id: 'upload_form.description', defaultMessage: 'Describe for the visually impaired' },
delete: { id: 'upload_form.undo', defaultMessage: 'Delete' },

View File

@@ -0,0 +1,87 @@
import ImmutablePureComponent from 'react-immutable-pure-component'
import ImmutablePropTypes from 'react-immutable-proptypes'
import { defineMessages, injectIntl } from 'react-intl'
import { uploadCompose } from '../../../actions/compose'
import ComposeExtraButton from './compose_extra_button'
const messages = defineMessages({
upload: { id: 'upload_button.label', defaultMessage: 'Add media (JPEG, PNG, GIF, WebM, MP4, MOV)' },
title: { id: 'upload_button.title', defaultMessage: 'Photo/Video' },
})
const makeMapStateToProps = () => {
const mapStateToProps = state => ({
acceptContentTypes: state.getIn(['media_attachments', 'accept_content_types']),
disabled: state.getIn(['compose', 'is_uploading']) || (state.getIn(['compose', 'media_attachments']).size > 3 || state.getIn(['compose', 'media_attachments']).some(m => m.get('type') === 'video')),
unavailable: state.getIn(['compose', 'poll']) !== null,
resetFileKey: state.getIn(['compose', 'resetFileKey']),
})
return mapStateToProps
}
const mapDispatchToProps = dispatch => ({
onSelectFile(files) {
dispatch(uploadCompose(files))
},
})
export default
@connect(makeMapStateToProps, mapDispatchToProps)
@injectIntl
class UploadButton extends ImmutablePureComponent {
static propTypes = {
disabled: PropTypes.bool,
unavailable: PropTypes.bool,
onSelectFile: PropTypes.func.isRequired,
style: PropTypes.object,
resetFileKey: PropTypes.number,
acceptContentTypes: ImmutablePropTypes.listOf(PropTypes.string).isRequired,
intl: PropTypes.object.isRequired,
}
handleChange = (e) => {
if (e.target.files.length > 0) {
this.props.onSelectFile(e.target.files)
}
}
handleClick = () => {
this.fileElement.click()
}
setRef = (c) => {
this.fileElement = c
}
render() {
const { intl, resetFileKey, unavailable, disabled, acceptContentTypes } = this.props
if (unavailable) return null
return (
<ComposeExtraButton
title={intl.formatMessage(messages.title)}
disabled={disabled}
onClick={this.handleClick}
icon='upload'
>
<label>
<span className={styles.displayNone}>{intl.formatMessage(messages.upload)}</span>
<input
key={resetFileKey}
ref={this.setRef}
type='file'
accept={acceptContentTypes.toArray().join(',')}
onChange={this.handleChange}
disabled={disabled}
className={styles.displayNone}
multiple
/>
</label>
</ComposeExtraButton>
)
}
}

View File

@@ -1 +0,0 @@
export { default } from './upload_button';

View File

@@ -1,89 +0,0 @@
import ImmutablePureComponent from 'react-immutable-pure-component';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { defineMessages, injectIntl } from 'react-intl';
import IconButton from '../../../../components/icon_button';
import './upload_button.scss';
const messages = defineMessages({
upload: { id: 'upload_button.label', defaultMessage: 'Add media (JPEG, PNG, GIF, WebM, MP4, MOV)' },
});
const makeMapStateToProps = () => {
const mapStateToProps = state => ({
acceptContentTypes: state.getIn(['media_attachments', 'accept_content_types']),
});
return mapStateToProps;
};
const iconStyle = {
height: null,
lineHeight: '27px',
};
export default @connect(makeMapStateToProps)
@injectIntl
class UploadButton extends ImmutablePureComponent {
static propTypes = {
disabled: PropTypes.bool,
unavailable: PropTypes.bool,
onSelectFile: PropTypes.func.isRequired,
style: PropTypes.object,
resetFileKey: PropTypes.number,
acceptContentTypes: ImmutablePropTypes.listOf(PropTypes.string).isRequired,
intl: PropTypes.object.isRequired,
};
handleChange = (e) => {
if (e.target.files.length > 0) {
this.props.onSelectFile(e.target.files);
}
}
handleClick = () => {
this.fileElement.click();
}
setRef = (c) => {
this.fileElement = c;
}
render () {
const { intl, resetFileKey, unavailable, disabled, acceptContentTypes } = this.props;
if (unavailable) {
return null;
}
return (
<div className='compose-form-upload-button'>
<IconButton
inverted
icon='camera'
title={intl.formatMessage(messages.upload)}
disabled={disabled}
onClick={this.handleClick}
className='compose-form-upload-button__icon'
size={18}
style={iconStyle}
/>
<label>
<span style={{ display: 'none' }}>{intl.formatMessage(messages.upload)}</span>
<input
key={resetFileKey}
ref={this.setRef}
type='file'
multiple
accept={acceptContentTypes.toArray().join(',')}
onChange={this.handleChange}
disabled={disabled}
style={{ display: 'none' }}
/>
</label>
</div>
);
}
}

View File

@@ -1,5 +0,0 @@
.compose-form-upload-button {
&__icon {
line-height: 27px;
}
}

View File

@@ -4,8 +4,6 @@ import UploadProgress from '../upload_progress';
import UploadContainer from '../../containers/upload_container';
import SensitiveButtonContainer from '../../containers/sensitive_button_container';
import './upload_form.scss';
const mapStateToProps = state => ({
mediaIds: state.getIn(['compose', 'media_attachments']).map(item => item.get('id')),
});

View File

@@ -3,8 +3,6 @@ import spring from 'react-motion/lib/spring';
import Motion from '../../../ui/util/optional_motion';
import Icon from '../../../../components/icon';
import './upload_progress.scss'
const mapStateToProps = state => ({
active: state.getIn(['compose', 'is_uploading']),
progress: state.getIn(['compose', 'progress']),

View File

@@ -1,8 +1,6 @@
import Motion from '../../../ui/util/optional_motion';
import spring from 'react-motion/lib/spring';
import './warning.scss';
export default class Warning extends PureComponent {
static propTypes = {