This commit is contained in:
mgabdev
2020-03-25 23:11:32 -04:00
parent 0f01c1bc97
commit 3d0a85cde4
68 changed files with 1206 additions and 1275 deletions

View File

@@ -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));

View File

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

View File

@@ -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;
}
}

View File

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

View File

@@ -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>
)

View File

@@ -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} />

View File

@@ -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>
);

View File

@@ -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}
/>
)
}
}

View File

@@ -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>
)
}
}

View File

@@ -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,

View File

@@ -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'>

View File

@@ -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>
)
}
}

View 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>
)
}
}

View File

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

View File

@@ -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>
)
}
}

View File

@@ -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("");
}
}

View File

@@ -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>
);
)
}
}

View File

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

View File

@@ -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>
);
}
}

View File

@@ -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);
}
}