Progress
This commit is contained in:
@@ -1,62 +0,0 @@
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import { openModal } from '../../../../actions/modal';
|
||||
import { meUsername } from '../../../../initial_state';
|
||||
|
||||
const messages = defineMessages({
|
||||
profile: { id: 'account.profile', defaultMessage: 'Profile' },
|
||||
preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' },
|
||||
follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' },
|
||||
blocks: { id: 'navigation_bar.blocks', defaultMessage: 'Blocked users' },
|
||||
domain_blocks: { id: 'navigation_bar.domain_blocks', defaultMessage: 'Hidden domains' },
|
||||
mutes: { id: 'navigation_bar.mutes', defaultMessage: 'Muted users' },
|
||||
filters: { id: 'navigation_bar.filters', defaultMessage: 'Muted words' },
|
||||
logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' },
|
||||
keyboard_shortcuts: { id: 'navigation_bar.keyboard_shortcuts', defaultMessage: 'Hotkeys' },
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
onOpenHotkeys() {
|
||||
dispatch(openModal('HOTKEYS'));
|
||||
},
|
||||
});
|
||||
|
||||
class ActionBar extends PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
intl: PropTypes.object.isRequired,
|
||||
size: PropTypes.number,
|
||||
};
|
||||
|
||||
handleHotkeyClick = () => {
|
||||
this.props.onOpenHotkeys();
|
||||
}
|
||||
|
||||
render () {
|
||||
const { intl } = this.props;
|
||||
const size = this.props.size || 16;
|
||||
|
||||
let menu = [];
|
||||
|
||||
menu.push({ text: intl.formatMessage(messages.profile), to: `/${meUsername}` });
|
||||
menu.push(null);
|
||||
menu.push({ text: intl.formatMessage(messages.follow_requests), to: '/follow_requests' });
|
||||
menu.push({ text: intl.formatMessage(messages.mutes), to: '/mutes' });
|
||||
menu.push({ text: intl.formatMessage(messages.blocks), to: '/blocks' });
|
||||
menu.push({ text: intl.formatMessage(messages.domain_blocks), to: '/domain_blocks' });
|
||||
menu.push({ text: intl.formatMessage(messages.filters), href: '/filters' });
|
||||
menu.push(null);
|
||||
menu.push({ text: intl.formatMessage(messages.keyboard_shortcuts), action: this.handleHotkeyClick });
|
||||
menu.push({ text: intl.formatMessage(messages.preferences), href: '/settings/preferences' });
|
||||
menu.push({ text: intl.formatMessage(messages.logout), href: '/auth/sign_out', isLogout: true });
|
||||
|
||||
return (
|
||||
<div style={{'marginTop':'-6px'}}>
|
||||
<div>
|
||||
{ /* <DropdownMenuContainer items={menu} icon='chevron-down' size={size} direction='right' /> */ }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default injectIntl(connect(null, mapDispatchToProps)(ActionBar));
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from './action_bar'
|
||||
@@ -1,16 +0,0 @@
|
||||
.character-counter {
|
||||
cursor: default;
|
||||
font-family: $font-sans-serif, sans-serif;
|
||||
color: $gab-secondary-text;
|
||||
|
||||
@include text-sizing(14px, 600);
|
||||
|
||||
&--over {
|
||||
color: $warning-red;
|
||||
}
|
||||
|
||||
&__wrapper {
|
||||
align-self: center;
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from './character_counter'
|
||||
@@ -1,5 +1,5 @@
|
||||
import classNames from 'classnames/bind'
|
||||
import Icon from '../../../components/icon'
|
||||
import Button from '../../../components/button'
|
||||
|
||||
const cx = classNames.bind(_s)
|
||||
|
||||
@@ -13,18 +13,6 @@ export default class ComposeExtraButton extends PureComponent {
|
||||
active: PropTypes.bool,
|
||||
}
|
||||
|
||||
state = {
|
||||
hovering: false,
|
||||
}
|
||||
|
||||
handleOnMouseEnter = () => {
|
||||
this.setState({ hovering: true })
|
||||
}
|
||||
|
||||
handleOnMouseLeave = () => {
|
||||
this.setState({ hovering: false })
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
title,
|
||||
@@ -35,7 +23,6 @@ export default class ComposeExtraButton extends PureComponent {
|
||||
small,
|
||||
active
|
||||
} = this.props
|
||||
const { hovering } = this.state
|
||||
|
||||
const containerClasses = cx({
|
||||
default: 1,
|
||||
@@ -44,13 +31,6 @@ export default class ComposeExtraButton extends PureComponent {
|
||||
})
|
||||
|
||||
const btnClasses = cx({
|
||||
default: 1,
|
||||
circle: 1,
|
||||
flexRow: 1,
|
||||
cursorPointer: 1,
|
||||
outlineNone: 1,
|
||||
backgroundSubtle: !hovering && !active,
|
||||
backgroundSubtle2: hovering && !active,
|
||||
backgroundColorBrandLight: active,
|
||||
py10: !small,
|
||||
px10: !small,
|
||||
@@ -58,18 +38,6 @@ export default class ComposeExtraButton extends PureComponent {
|
||||
px5: small,
|
||||
})
|
||||
|
||||
const titleClasses = cx({
|
||||
default: 1,
|
||||
ml5: 1,
|
||||
text: 1,
|
||||
lineHeight15: 1,
|
||||
fontSize12PX: 1,
|
||||
fontWeightMedium: 1,
|
||||
colorSecondary: !active,
|
||||
colorWhite: active,
|
||||
displayNone: !hovering,
|
||||
})
|
||||
|
||||
const iconClasses = cx({
|
||||
fillColorSecondary: !active,
|
||||
fillColorWhite: active,
|
||||
@@ -78,23 +46,18 @@ export default class ComposeExtraButton extends PureComponent {
|
||||
const iconSize = !!small ? '12px' : '18px'
|
||||
|
||||
return (
|
||||
<div className={containerClasses}>
|
||||
<button
|
||||
<div className={containerClasses} data-tip={title}>
|
||||
<Button
|
||||
className={btnClasses}
|
||||
title={title}
|
||||
disabled={disabled}
|
||||
onClick={onClick}
|
||||
onMouseEnter={() => this.handleOnMouseEnter()}
|
||||
onMouseLeave={() => this.handleOnMouseLeave()}
|
||||
>
|
||||
<Icon id={icon} width={iconSize} height={iconSize} className={iconClasses} />
|
||||
{
|
||||
(!small && !!title) &&
|
||||
<span className={titleClasses}>
|
||||
{title}
|
||||
</span>
|
||||
}
|
||||
</button>
|
||||
backgroundColor='secondary'
|
||||
iconClassName={iconClasses}
|
||||
icon={icon}
|
||||
iconWidth={iconSize}
|
||||
iconHeight={iconSize}
|
||||
/>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -10,7 +10,7 @@ import AutosuggestTextbox from '../../../../components/autosuggest_textbox';
|
||||
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 PostPrivacyButton from '../../../../components/post_privacy_button';
|
||||
import EmojiPickerButton from '../../components/emoji_picker_button'
|
||||
import EmojiPickerDropdown from '../../containers/emoji_picker_dropdown_container';
|
||||
import PollFormContainer from '../../containers/poll_form_container';
|
||||
@@ -230,7 +230,8 @@ class ComposeForm extends ImmutablePureComponent {
|
||||
quoteOfId,
|
||||
edit,
|
||||
scheduledAt,
|
||||
spoiler
|
||||
spoiler,
|
||||
replyToId
|
||||
} = this.props
|
||||
const disabled = this.props.isSubmitting;
|
||||
const text = [this.props.spoilerText, countableText(this.props.text)].join('');
|
||||
@@ -239,7 +240,7 @@ class ComposeForm extends ImmutablePureComponent {
|
||||
|
||||
const containerClasses = cx({
|
||||
default: 1,
|
||||
flexGrow1: 1,
|
||||
flexNormal: 1,
|
||||
flexRow: shouldCondense,
|
||||
radiusSmall: shouldCondense,
|
||||
backgroundSubtle: shouldCondense,
|
||||
@@ -326,10 +327,10 @@ class ComposeForm extends ImmutablePureComponent {
|
||||
>
|
||||
|
||||
<div className='compose-form__modifiers'>
|
||||
<UploadForm />
|
||||
<UploadForm replyToId={replyToId} />
|
||||
{
|
||||
!edit &&
|
||||
<PollFormContainer />
|
||||
<PollFormContainer replyToId={replyToId} />
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -345,7 +346,7 @@ class ComposeForm extends ImmutablePureComponent {
|
||||
}
|
||||
{
|
||||
!shouldCondense &&
|
||||
<PrivacyDropdown />
|
||||
<PostPrivacyButton />
|
||||
}
|
||||
<SpoilerButton small={shouldCondense} />
|
||||
<SchedulePostDropdown small={shouldCondense} position={isModalOpen ? 'top' : undefined} />
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import ActionBar from '../action_bar';
|
||||
import Avatar from '../../../../components/avatar';
|
||||
import Button from '../../../../components/button'
|
||||
import IconButton from '../../../../components/icon_button';
|
||||
import { me } from '../../../../initial_state';
|
||||
|
||||
const mapStateToProps = state => {
|
||||
@@ -43,8 +41,7 @@ class NavigationBar extends ImmutablePureComponent {
|
||||
</div>
|
||||
|
||||
<div className='navigation-bar__actions'>
|
||||
<IconButton className='close' title='' icon='close' onClick={this.props.onClose} />
|
||||
<ActionBar account={account} />
|
||||
<Button className='close' title='' icon='close' onClick={this.props.onClose} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
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,
|
||||
small: PropTypes.bool,
|
||||
}
|
||||
|
||||
handleClick = () => {
|
||||
this.props.onClick()
|
||||
}
|
||||
|
||||
render() {
|
||||
const { intl, active, unavailable, disabled, small } = this.props
|
||||
|
||||
if (unavailable) return null
|
||||
|
||||
return (
|
||||
<ComposeExtraButton
|
||||
title={intl.formatMessage(active ? messages.remove_poll : messages.title)}
|
||||
disabled={disabled}
|
||||
onClick={this.handleClick}
|
||||
icon='poll'
|
||||
small={small}
|
||||
active={active}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,271 +0,0 @@
|
||||
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' },
|
||||
public_long: { id: 'privacy.public.long', defaultMessage: 'Post to public timelines' },
|
||||
unlisted_short: { id: 'privacy.unlisted.short', defaultMessage: 'Unlisted' },
|
||||
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' },
|
||||
change_privacy: { id: 'privacy.change', defaultMessage: 'Adjust status privacy' },
|
||||
visibility: { id: 'privacy.visibility', defaultMessage: 'Visibility' },
|
||||
})
|
||||
|
||||
const listenerOptions = detectPassiveEvents.hasSupport ? { passive: true } : false
|
||||
|
||||
class PrivacyDropdownMenu extends PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
style: PropTypes.object,
|
||||
items: PropTypes.array.isRequired,
|
||||
value: PropTypes.string.isRequired,
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
handleKeyDown = e => {
|
||||
const { items } = this.props
|
||||
const value = 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(element.getAttribute('data-index'))
|
||||
}
|
||||
break
|
||||
case 'ArrowUp':
|
||||
element = this.node.childNodes[index - 1]
|
||||
if (element) {
|
||||
element.focus()
|
||||
this.props.onChange(element.getAttribute('data-index'))
|
||||
}
|
||||
break
|
||||
case 'Home':
|
||||
element = this.node.firstChild
|
||||
if (element) {
|
||||
element.focus()
|
||||
this.props.onChange(element.getAttribute('data-index'))
|
||||
}
|
||||
break
|
||||
case 'End':
|
||||
element = this.node.lastChild
|
||||
if (element) {
|
||||
element.focus()
|
||||
this.props.onChange(element.getAttribute('data-index'))
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
handleClick = e => {
|
||||
const value = 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, 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 }) }}>
|
||||
{({ 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 ${placement}`} 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} 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'>
|
||||
<Icon id={item.icon} fixedWidth />
|
||||
</div>
|
||||
|
||||
<div className='privacy-dropdown__option__content'>
|
||||
<strong>{item.text}</strong>
|
||||
{item.meta}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Motion>
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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 = {
|
||||
isUserTouching: PropTypes.func,
|
||||
isModalOpen: PropTypes.bool.isRequired,
|
||||
onModalOpen: PropTypes.func,
|
||||
onModalClose: PropTypes.func,
|
||||
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()
|
||||
} 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[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: '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 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 })}>
|
||||
<ComposeExtraButton
|
||||
icon={valueOption.icon}
|
||||
title={intl.formatMessage(messages.visibility)}
|
||||
onClick={this.handleToggle}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Overlay show={open} placement={placement} target={this}>
|
||||
<PrivacyDropdownMenu
|
||||
items={this.options}
|
||||
value={value}
|
||||
onClose={this.handleClose}
|
||||
onChange={this.handleChange}
|
||||
placement={placement}
|
||||
/>
|
||||
</Overlay>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -2,6 +2,8 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import DisplayName from '../../../components/display_name';
|
||||
import StatusContent from '../../../components/status_content';
|
||||
|
||||
// : todo : do we need this? make work inside of status/status content
|
||||
|
||||
export default class QuotedStatusPreview extends PureComponent {
|
||||
static propTypes = {
|
||||
status: ImmutablePropTypes.map,
|
||||
|
||||
@@ -3,7 +3,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
import Avatar from '../../../../components/avatar';
|
||||
import IconButton from '../../../../components/icon_button';
|
||||
import Button from '../../../../components/button';
|
||||
import DisplayName from '../../../../components/display_name';
|
||||
import { isRtl } from '../../../../utils/rtl';
|
||||
|
||||
@@ -45,7 +45,7 @@ class ReplyIndicator extends ImmutablePureComponent {
|
||||
<div className='reply-indicator'>
|
||||
<div className='reply-indicator__header'>
|
||||
<div className='reply-indicator__cancel'>
|
||||
<IconButton title={intl.formatMessage(messages.cancel)} icon='times' onClick={this.handleClick} inverted />
|
||||
<Button title={intl.formatMessage(messages.cancel)} icon='times' onClick={this.handleClick} inverted />
|
||||
</div>
|
||||
|
||||
<NavLink to={`/${status.getIn(['account', 'acct'])}`} className='reply-indicator__display-name'>
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import { injectIntl, defineMessages } from 'react-intl'
|
||||
import { changeComposeSensitivity } from '../../../actions/compose'
|
||||
import Switch from '../../../components/switch'
|
||||
|
||||
const messages = defineMessages({
|
||||
markAsSensitive: { id: 'compose_form.sensitive.hide', defaultMessage: 'Mark media as sensitive' },
|
||||
})
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
active: state.getIn(['compose', 'sensitive']),
|
||||
disabled: state.getIn(['compose', 'spoiler']),
|
||||
})
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
|
||||
onClick () {
|
||||
dispatch(changeComposeSensitivity())
|
||||
},
|
||||
|
||||
})
|
||||
|
||||
export default
|
||||
@injectIntl
|
||||
@connect(mapStateToProps, mapDispatchToProps)
|
||||
class SensitiveMediaButton extends PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
active: PropTypes.bool,
|
||||
disabled: PropTypes.bool,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
}
|
||||
|
||||
render () {
|
||||
const { active, disabled, onClick, intl } = this.props
|
||||
|
||||
return (
|
||||
<div className={[_s.default, _s.alignItemsStart, _s.px5].join(' ')}>
|
||||
<Switch
|
||||
id='mark-sensitive'
|
||||
type='checkbox'
|
||||
checked={active}
|
||||
onChange={onClick}
|
||||
disabled={disabled}
|
||||
label={intl.formatMessage(messages.markAsSensitive)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
178
app/javascript/gabsocial/features/compose/components/upload.js
Normal file
178
app/javascript/gabsocial/features/compose/components/upload.js
Normal file
@@ -0,0 +1,178 @@
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes'
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component'
|
||||
import { defineMessages, injectIntl } from 'react-intl'
|
||||
import classNames from 'classnames/bind'
|
||||
import { undoUploadCompose, changeUploadCompose } from '../../../actions/compose'
|
||||
import { submitCompose } from '../../../actions/compose';
|
||||
import Button from '../../../components/button'
|
||||
import Image from '../../../components/image'
|
||||
import Input from '../../../components/input'
|
||||
|
||||
const cx = classNames.bind(_s)
|
||||
|
||||
const messages = defineMessages({
|
||||
description: { id: 'upload_form.description', defaultMessage: 'Describe for the visually impaired' },
|
||||
delete: { id: 'upload_form.undo', defaultMessage: 'Delete' },
|
||||
})
|
||||
|
||||
const mapStateToProps = (state, { id, otherProps }) => {
|
||||
console.log("otherProps:", otherProps)
|
||||
return {
|
||||
media: state.getIn(['compose', 'media_attachments']).find(item => item.get('id') === id),
|
||||
}
|
||||
}
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
|
||||
onUndo: id => {
|
||||
dispatch(undoUploadCompose(id));
|
||||
},
|
||||
|
||||
onDescriptionChange: (id, description) => {
|
||||
dispatch(changeUploadCompose(id, { description }));
|
||||
},
|
||||
|
||||
onSubmit (router) {
|
||||
dispatch(submitCompose(router));
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
export default
|
||||
@injectIntl
|
||||
@connect(mapStateToProps, mapDispatchToProps)
|
||||
class Upload extends ImmutablePureComponent {
|
||||
|
||||
static contextTypes = {
|
||||
router: PropTypes.object,
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
media: ImmutablePropTypes.map.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
onUndo: PropTypes.func.isRequired,
|
||||
onDescriptionChange: PropTypes.func.isRequired,
|
||||
onSubmit: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
state = {
|
||||
hovered: false,
|
||||
focused: false,
|
||||
dirtyDescription: null,
|
||||
}
|
||||
|
||||
handleKeyDown = (e) => {
|
||||
if (e.keyCode === 13 && (e.ctrlKey || e.metaKey)) {
|
||||
this.handleSubmit()
|
||||
}
|
||||
}
|
||||
|
||||
handleSubmit = () => {
|
||||
this.handleInputBlur()
|
||||
this.props.onSubmit(this.context.router.history)
|
||||
}
|
||||
|
||||
handleUndoClick = e => {
|
||||
e.stopPropagation()
|
||||
this.props.onUndo(this.props.media.get('id'))
|
||||
}
|
||||
|
||||
handleInputChange = e => {
|
||||
this.setState({ dirtyDescription: e.target.value })
|
||||
}
|
||||
|
||||
handleMouseEnter = () => {
|
||||
this.setState({ hovered: true })
|
||||
}
|
||||
|
||||
handleMouseLeave = () => {
|
||||
this.setState({ hovered: false })
|
||||
}
|
||||
|
||||
handleInputFocus = () => {
|
||||
this.setState({ focused: true })
|
||||
}
|
||||
|
||||
handleClick = () => {
|
||||
this.setState({ focused: true })
|
||||
}
|
||||
|
||||
handleInputBlur = () => {
|
||||
const { dirtyDescription } = this.state
|
||||
|
||||
this.setState({
|
||||
focused: false,
|
||||
dirtyDescription: null,
|
||||
})
|
||||
|
||||
if (dirtyDescription !== null) {
|
||||
this.props.onDescriptionChange(this.props.media.get('id'), dirtyDescription)
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { intl, media } = this.props
|
||||
const active = this.state.hovered || this.state.focused
|
||||
const description = this.state.dirtyDescription || (this.state.dirtyDescription !== '' && media.get('description')) || ''
|
||||
|
||||
const descriptionContainerClasses = cx({
|
||||
default: 1,
|
||||
positionAbsolute: 1,
|
||||
right0: 1,
|
||||
bottom0: 1,
|
||||
left0: 1,
|
||||
my5: 1,
|
||||
ml5: 1,
|
||||
mr5: 1,
|
||||
displayNone: !active,
|
||||
})
|
||||
|
||||
console.log("media:", media)
|
||||
|
||||
return (
|
||||
<div
|
||||
tabIndex='0'
|
||||
className={[_s.default, _s.width50PC, _s.px5, _s.py5].join(' ')}
|
||||
onMouseEnter={this.handleMouseEnter}
|
||||
onMouseLeave={this.handleMouseLeave}
|
||||
onClick={this.handleClick}
|
||||
role='button'
|
||||
>
|
||||
<div className={[_s.default, _s.radiusSmall, _s.overflowHidden, _s.height158PX].join(' ')}>
|
||||
<Image
|
||||
className={[_s.default, _s.height158PX].join(' ')}
|
||||
src={media.get('preview_url')}
|
||||
/>
|
||||
<Button
|
||||
backgroundColor='black'
|
||||
color='white'
|
||||
title={intl.formatMessage(messages.delete)}
|
||||
onClick={this.handleUndoClick}
|
||||
icon='close'
|
||||
iconWidth='10px'
|
||||
iconHeight='10px'
|
||||
iconClassName={_s.inherit}
|
||||
className={[_s.top0, _s.right0, _s.positionAbsolute, _s.mr5, _s.mt5, _s.px10].join(' ')}
|
||||
/>
|
||||
|
||||
<div className={descriptionContainerClasses}>
|
||||
<Input
|
||||
small
|
||||
hideLabel
|
||||
id={`input-${media.get('id')}`}
|
||||
title={intl.formatMessage(messages.description)}
|
||||
placeholder={intl.formatMessage(messages.description)}
|
||||
value={description}
|
||||
maxLength={420}
|
||||
onFocus={this.handleInputFocus}
|
||||
onChange={this.handleInputChange}
|
||||
onBlur={this.handleInputBlur}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from './upload'
|
||||
@@ -1,126 +0,0 @@
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes'
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component'
|
||||
import { defineMessages, injectIntl } from 'react-intl'
|
||||
import classNames from 'classnames'
|
||||
import Button from '../../../../components/button'
|
||||
import Image from '../../../../components/image'
|
||||
|
||||
const messages = defineMessages({
|
||||
description: { id: 'upload_form.description', defaultMessage: 'Describe for the visually impaired' },
|
||||
delete: { id: 'upload_form.undo', defaultMessage: 'Delete' },
|
||||
})
|
||||
|
||||
export default
|
||||
@injectIntl
|
||||
class Upload extends ImmutablePureComponent {
|
||||
|
||||
static contextTypes = {
|
||||
router: PropTypes.object,
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
media: ImmutablePropTypes.map.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
onUndo: PropTypes.func.isRequired,
|
||||
onDescriptionChange: PropTypes.func.isRequired,
|
||||
onSubmit: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
state = {
|
||||
hovered: false,
|
||||
focused: false,
|
||||
dirtyDescription: null,
|
||||
}
|
||||
|
||||
handleKeyDown = (e) => {
|
||||
if (e.keyCode === 13 && (e.ctrlKey || e.metaKey)) {
|
||||
this.handleSubmit()
|
||||
}
|
||||
}
|
||||
|
||||
handleSubmit = () => {
|
||||
this.handleInputBlur()
|
||||
this.props.onSubmit(this.context.router.history)
|
||||
}
|
||||
|
||||
handleUndoClick = e => {
|
||||
e.stopPropagation()
|
||||
this.props.onUndo(this.props.media.get('id'))
|
||||
}
|
||||
|
||||
handleInputChange = e => {
|
||||
this.setState({ dirtyDescription: e.target.value })
|
||||
}
|
||||
|
||||
handleMouseEnter = () => {
|
||||
this.setState({ hovered: true })
|
||||
}
|
||||
|
||||
handleMouseLeave = () => {
|
||||
this.setState({ hovered: false })
|
||||
}
|
||||
|
||||
handleInputFocus = () => {
|
||||
this.setState({ focused: true })
|
||||
}
|
||||
|
||||
handleClick = () => {
|
||||
this.setState({ focused: true })
|
||||
}
|
||||
|
||||
handleInputBlur = () => {
|
||||
const { dirtyDescription } = this.state
|
||||
|
||||
this.setState({
|
||||
focused: false,
|
||||
dirtyDescription: null,
|
||||
})
|
||||
|
||||
if (dirtyDescription !== null) {
|
||||
this.props.onDescriptionChange(this.props.media.get('id'), dirtyDescription)
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { intl, media } = this.props
|
||||
const active = this.state.hovered || this.state.focused
|
||||
const description = this.state.dirtyDescription || (this.state.dirtyDescription !== '' && media.get('description')) || ''
|
||||
const focusX = media.getIn(['meta', 'focus', 'x'])
|
||||
const focusY = media.getIn(['meta', 'focus', 'y'])
|
||||
const x = ((focusX / 2) + .5) * 100
|
||||
const y = ((focusY / -2) + .5) * 100
|
||||
|
||||
return (
|
||||
<div className='compose-form-upload' tabIndex='0' onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave} onClick={this.handleClick} role='button'>
|
||||
<div className='compose-form__upload-thumbnail' style={{ backgroundImage: `url(${media.get('preview_url')})`, backgroundPosition: `${x}% ${y}%` }}>
|
||||
<div className={classNames('compose-form__upload__actions', { active })}>
|
||||
<Button
|
||||
title={intl.formatMessage(messages.delete)}
|
||||
onClick={this.handleUndoClick}
|
||||
icon='cancel'
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={classNames('compose-form-upload__description', { active })}>
|
||||
<label>
|
||||
<span style={{ display: 'none' }}>
|
||||
{intl.formatMessage(messages.description)}
|
||||
</span>
|
||||
|
||||
<textarea
|
||||
placeholder={intl.formatMessage(messages.description)}
|
||||
value={description}
|
||||
maxLength={420}
|
||||
onFocus={this.handleInputFocus}
|
||||
onChange={this.handleInputChange}
|
||||
onBlur={this.handleInputBlur}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
.compose-form-upload {
|
||||
flex: 1 1 0;
|
||||
min-width: 40%;
|
||||
margin: 5px;
|
||||
|
||||
&__actions {
|
||||
background: linear-gradient(180deg, rgba($base-shadow-color, 0.8) 0, rgba($base-shadow-color, 0.35) 80%, transparent);
|
||||
opacity: 0;
|
||||
transition: opacity .1s ease;
|
||||
|
||||
@include flex(space-between, flex-start);
|
||||
|
||||
.icon-button {
|
||||
flex: 0 1 auto;
|
||||
color: $gab-secondary-text;
|
||||
padding: 10px;
|
||||
font-family: inherit;
|
||||
|
||||
@include text-sizing(14px, 500);
|
||||
|
||||
&:hover,
|
||||
&:focus,
|
||||
&:active {
|
||||
color: $gab-text-highlight;
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&__description {
|
||||
z-index: 2;
|
||||
box-sizing: border-box;
|
||||
background: linear-gradient(0deg, rgba($base-shadow-color, 0.8) 0, rgba($base-shadow-color, 0.35) 80%, transparent);
|
||||
padding: 10px;
|
||||
opacity: 0;
|
||||
transition: opacity .1s ease;
|
||||
|
||||
@include abs-position(auto, 0, 0, 0);
|
||||
|
||||
textarea {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
box-sizing: border-box;
|
||||
background: transparent;
|
||||
color: $gab-secondary-text;
|
||||
border: 1px solid $gab-secondary-text;
|
||||
outline: none;
|
||||
padding: 10px;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
font-family: inherit;
|
||||
|
||||
@include text-sizing(14px, 500);
|
||||
|
||||
&:focus {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: $gab-secondary-text;
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&__thumbnail {
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
|
||||
@include size(100%, 140px);
|
||||
@include background-image("");
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,13 @@
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import UploadProgress from '../upload_progress';
|
||||
import UploadContainer from '../../containers/upload_container';
|
||||
import SensitiveButtonContainer from '../../containers/sensitive_button_container';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes'
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component'
|
||||
import ProgressBar from '../../../../components/progress_bar'
|
||||
import Upload from '../upload'
|
||||
import SensitiveMediaButton from '../sensitive_media_button'
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
mediaIds: state.getIn(['compose', 'media_attachments']).map(item => item.get('id')),
|
||||
isUploading: state.getIn(['compose', 'is_uploading']),
|
||||
uploadProgress: state.getIn(['compose', 'progress']),
|
||||
});
|
||||
|
||||
export default
|
||||
@@ -14,24 +16,38 @@ class UploadForm extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
mediaIds: ImmutablePropTypes.list.isRequired,
|
||||
isUploading: PropTypes.bool,
|
||||
uploadProgress: PropTypes.number,
|
||||
};
|
||||
|
||||
render () {
|
||||
const { mediaIds } = this.props;
|
||||
const {
|
||||
mediaIds,
|
||||
isUploading,
|
||||
uploadProgress
|
||||
} = this.props
|
||||
|
||||
return (
|
||||
<div className='compose-form-upload-wrapper'>
|
||||
<UploadProgress />
|
||||
|
||||
<div className='compose-form-uploads-wrapper'>
|
||||
{mediaIds.map(id => (
|
||||
<UploadContainer id={id} key={id} />
|
||||
))}
|
||||
<div className={_s.default}>
|
||||
<div className={[_s.default, _s.flexRow, _s.flexWrap].join(' ')}>
|
||||
{
|
||||
mediaIds.map(id => (
|
||||
<Upload id={id} key={id} />
|
||||
))
|
||||
}
|
||||
</div>
|
||||
|
||||
{!mediaIds.isEmpty() && <SensitiveButtonContainer />}
|
||||
{
|
||||
!mediaIds.isEmpty() &&
|
||||
<SensitiveMediaButton />
|
||||
}
|
||||
|
||||
{
|
||||
isUploading &&
|
||||
<ProgressBar small progress={uploadProgress} />
|
||||
}
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from './upload_progress'
|
||||
@@ -1,48 +0,0 @@
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import spring from 'react-motion/lib/spring';
|
||||
import Motion from '../../../ui/util/optional_motion';
|
||||
import Icon from '../../../../components/icon';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
active: state.getIn(['compose', 'is_uploading']),
|
||||
progress: state.getIn(['compose', 'progress']),
|
||||
});
|
||||
|
||||
export default
|
||||
@connect(mapStateToProps)
|
||||
class UploadProgress extends PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
active: PropTypes.bool,
|
||||
progress: PropTypes.number,
|
||||
};
|
||||
|
||||
render () {
|
||||
const { active, progress } = this.props;
|
||||
|
||||
if (!active) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='upload-progress'>
|
||||
<div className='upload-progress__icon'>
|
||||
<Icon id='upload' />
|
||||
</div>
|
||||
|
||||
<div className='upload-progress__message'>
|
||||
<FormattedMessage id='upload_progress.label' defaultMessage='Uploading...' />
|
||||
|
||||
<div className='upload-progress__backdrop'>
|
||||
<Motion defaultStyle={{ width: 0 }} style={{ width: spring(progress) }}>
|
||||
{({ width }) =>
|
||||
<div className='upload-progress__tracker' style={{ width: `${width}%` }} />
|
||||
}
|
||||
</Motion>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
.upload-progress {
|
||||
padding: 10px;
|
||||
color: $lighter-text-color;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
|
||||
.fa {
|
||||
font-size: 34px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
span {
|
||||
text-transform: uppercase;
|
||||
display: block;
|
||||
|
||||
@include text-sizing(12px, 500);
|
||||
}
|
||||
|
||||
&__message {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
&__backdrop {
|
||||
border-radius: 6px;
|
||||
background: $ui-base-lighter-color;
|
||||
position: relative;
|
||||
margin-top: 5px;
|
||||
|
||||
@include size(100%, 6px);
|
||||
}
|
||||
|
||||
&__tracker {
|
||||
height: 6px;
|
||||
background: $ui-highlight-color;
|
||||
border-radius: 6px;
|
||||
|
||||
@include abs-position(0, auto, auto, 0);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user