Progress
This commit is contained in:
@@ -10,6 +10,7 @@ export default class ComposeExtraButton extends PureComponent {
|
||||
onClick: PropTypes.func,
|
||||
icon: PropTypes.string,
|
||||
small: PropTypes.bool,
|
||||
active: PropTypes.bool,
|
||||
}
|
||||
|
||||
state = {
|
||||
@@ -25,7 +26,15 @@ export default class ComposeExtraButton extends PureComponent {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { title, disabled, onClick, icon, children, small } = this.props
|
||||
const {
|
||||
title,
|
||||
disabled,
|
||||
onClick,
|
||||
icon,
|
||||
children,
|
||||
small,
|
||||
active
|
||||
} = this.props
|
||||
const { hovering } = this.state
|
||||
|
||||
const containerClasses = cx({
|
||||
@@ -39,8 +48,10 @@ export default class ComposeExtraButton extends PureComponent {
|
||||
circle: 1,
|
||||
flexRow: 1,
|
||||
cursorPointer: 1,
|
||||
backgroundSubtle: !hovering,
|
||||
backgroundSubtle2: hovering,
|
||||
outlineNone: 1,
|
||||
backgroundSubtle: !hovering && !active,
|
||||
backgroundSubtle2: hovering && !active,
|
||||
backgroundColorBrandLight: active,
|
||||
paddingVertical10PX: !small,
|
||||
paddingHorizontal10PX: !small,
|
||||
paddingVertical5PX: small,
|
||||
@@ -54,10 +65,16 @@ export default class ComposeExtraButton extends PureComponent {
|
||||
lineHeight15: 1,
|
||||
fontSize12PX: 1,
|
||||
fontWeightMedium: 1,
|
||||
colorSecondary: 1,
|
||||
colorSecondary: !active,
|
||||
colorWhite: active,
|
||||
displayNone: !hovering,
|
||||
})
|
||||
|
||||
const iconClasses = cx({
|
||||
fillColorSecondary: !active,
|
||||
fillColorWhite: active,
|
||||
})
|
||||
|
||||
const iconSize = !!small ? '12px' : '18px'
|
||||
|
||||
return (
|
||||
@@ -70,9 +87,9 @@ export default class ComposeExtraButton extends PureComponent {
|
||||
onMouseEnter={() => this.handleOnMouseEnter()}
|
||||
onMouseLeave={() => this.handleOnMouseLeave()}
|
||||
>
|
||||
<Icon id={icon} width={iconSize} height={iconSize} className={_s.fillColorSecondary} />
|
||||
<Icon id={icon} width={iconSize} height={iconSize} className={iconClasses} />
|
||||
{
|
||||
!small &&
|
||||
(!small && !!title) &&
|
||||
<span className={titleClasses}>
|
||||
{title}
|
||||
</span>
|
||||
|
||||
@@ -11,9 +11,9 @@ 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 EmojiPickerButton from '../../components/emoji_picker_button'
|
||||
import EmojiPickerDropdown from '../../containers/emoji_picker_dropdown_container';
|
||||
import PollFormContainer from '../../containers/poll_form_container';
|
||||
import WarningContainer from '../../containers/warning_container';
|
||||
import SchedulePostDropdown from '../../components/schedule_post_dropdown';
|
||||
import QuotedStatusPreviewContainer from '../../containers/quoted_status_preview_container';
|
||||
import Icon from '../../../../components/icon';
|
||||
@@ -228,7 +228,8 @@ class ComposeForm extends ImmutablePureComponent {
|
||||
isModalOpen,
|
||||
quoteOfId,
|
||||
edit,
|
||||
scheduledAt
|
||||
scheduledAt,
|
||||
spoiler
|
||||
} = this.props
|
||||
const condensed = shouldCondense && !this.props.text && !this.state.composeFocused;
|
||||
const disabled = this.props.isSubmitting;
|
||||
@@ -258,6 +259,15 @@ class ComposeForm extends ImmutablePureComponent {
|
||||
marginTop5PX: shouldCondense,
|
||||
})
|
||||
|
||||
const contentWarningClasses = cx({
|
||||
default: 1,
|
||||
paddingTop5PX: 1,
|
||||
paddingBottom10PX: 1,
|
||||
borderBottom1PX: 1,
|
||||
borderColorSecondary: 1,
|
||||
displayNone: !spoiler
|
||||
})
|
||||
|
||||
const avatarSize = shouldCondense ? 28 : 46
|
||||
|
||||
return (
|
||||
@@ -265,34 +275,32 @@ class ComposeForm extends ImmutablePureComponent {
|
||||
<div className={avatarContainerClasses}>
|
||||
<Avatar account={account} size={avatarSize} />
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={containerClasses}
|
||||
ref={this.setForm}
|
||||
onClick={this.handleClick}
|
||||
>
|
||||
{ /* <WarningContainer /> */}
|
||||
|
||||
{ /* !shouldCondense && <ReplyIndicatorContainer /> */}
|
||||
|
||||
{ /*
|
||||
<div className={`spoiler-input ${this.props.spoiler ? 'spoiler-input--visible' : ''}`}>
|
||||
<AutosuggestTextbox
|
||||
placeholder={intl.formatMessage(messages.spoiler_placeholder)}
|
||||
value={this.props.spoilerText}
|
||||
onChange={this.handleChangeSpoilerText}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
disabled={!this.props.spoiler}
|
||||
ref={this.setSpoilerText}
|
||||
suggestions={this.props.suggestions}
|
||||
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
|
||||
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
|
||||
onSuggestionSelected={this.onSpoilerSuggestionSelected}
|
||||
searchTokens={[':']}
|
||||
id='cw-spoiler-input'
|
||||
className='spoiler-input__input'
|
||||
/>
|
||||
</div>
|
||||
*/ }
|
||||
<div className={contentWarningClasses}>
|
||||
<AutosuggestTextbox
|
||||
placeholder={intl.formatMessage(messages.spoiler_placeholder)}
|
||||
value={this.props.spoilerText}
|
||||
onChange={this.handleChangeSpoilerText}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
disabled={!this.props.spoiler}
|
||||
ref={this.setSpoilerText}
|
||||
suggestions={this.props.suggestions}
|
||||
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
|
||||
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
|
||||
onSuggestionSelected={this.onSpoilerSuggestionSelected}
|
||||
searchTokens={[':']}
|
||||
id='cw-spoiler-input'
|
||||
className='spoiler-input__input'
|
||||
/>
|
||||
</div>
|
||||
|
||||
{ /*
|
||||
<div className='emoji-picker-wrapper'>
|
||||
@@ -341,6 +349,7 @@ class ComposeForm extends ImmutablePureComponent {
|
||||
}
|
||||
<SpoilerButton small={shouldCondense} />
|
||||
<SchedulePostDropdown small={shouldCondense} position={isModalOpen ? 'top' : undefined} />
|
||||
<EmojiPickerButton />
|
||||
</div>
|
||||
<CharacterCounter max={maxPostCharacterCount} text={text} small={shouldCondense} />
|
||||
{
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import { addPoll, removePoll } from '../../../actions/compose'
|
||||
import ComposeExtraButton from './compose_extra_button'
|
||||
|
||||
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)
|
||||
class EmojiPickerButton 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 { active, unavailable, disabled, small } = this.props
|
||||
|
||||
if (unavailable) return null
|
||||
|
||||
return (
|
||||
<ComposeExtraButton
|
||||
disabled={disabled}
|
||||
onClick={this.handleClick}
|
||||
icon='happy'
|
||||
small={small}
|
||||
active={active}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,31 +1,31 @@
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import { addPoll, removePoll } from '../../../actions/compose';
|
||||
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());
|
||||
dispatch(removePoll())
|
||||
} else {
|
||||
dispatch(addPoll());
|
||||
dispatch(addPoll())
|
||||
}
|
||||
});
|
||||
})
|
||||
},
|
||||
|
||||
});
|
||||
})
|
||||
|
||||
export default
|
||||
@connect(mapStateToProps, mapDispatchToProps)
|
||||
@@ -39,16 +39,16 @@ class PollButton extends PureComponent {
|
||||
onClick: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
small: PropTypes.bool,
|
||||
};
|
||||
}
|
||||
|
||||
handleClick = () => {
|
||||
this.props.onClick();
|
||||
this.props.onClick()
|
||||
}
|
||||
|
||||
render() {
|
||||
const { intl, active, unavailable, disabled, small } = this.props;
|
||||
const { intl, active, unavailable, disabled, small } = this.props
|
||||
|
||||
if (unavailable) return null;
|
||||
if (unavailable) return null
|
||||
|
||||
return (
|
||||
<ComposeExtraButton
|
||||
@@ -57,6 +57,7 @@ class PollButton extends PureComponent {
|
||||
onClick={this.handleClick}
|
||||
icon='poll'
|
||||
small={small}
|
||||
active={active}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,247 @@
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes'
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component'
|
||||
import classNames from 'classnames/bind'
|
||||
import { defineMessages, injectIntl } from 'react-intl'
|
||||
import Button from '../../../components/button'
|
||||
import Text from '../../../components/text'
|
||||
import Select from '../../../components/select'
|
||||
import AutosuggestTextbox from '../../../components/autosuggest_textbox'
|
||||
|
||||
const cx = classNames.bind(_s)
|
||||
|
||||
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' },
|
||||
remove_option: { id: 'compose_form.poll.remove_option', defaultMessage: 'Remove this choice' },
|
||||
poll_duration: { id: 'compose_form.poll.duration', defaultMessage: 'Poll duration' },
|
||||
minutes: { id: 'intervals.full.minutes', defaultMessage: '{number, plural, one {# minute} other {# minutes}}' },
|
||||
hours: { id: 'intervals.full.hours', defaultMessage: '{number, plural, one {# hour} other {# hours}}' },
|
||||
days: { id: 'intervals.full.days', defaultMessage: '{number, plural, one {# day} other {# days}}' },
|
||||
})
|
||||
|
||||
@injectIntl
|
||||
class PollFormOption extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
index: PropTypes.number.isRequired,
|
||||
isPollMultiple: PropTypes.bool,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onRemove: PropTypes.func.isRequired,
|
||||
onToggleMultiple: PropTypes.func.isRequired,
|
||||
suggestions: ImmutablePropTypes.list,
|
||||
onClearSuggestions: PropTypes.func.isRequired,
|
||||
onFetchSuggestions: PropTypes.func.isRequired,
|
||||
onSuggestionSelected: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
handleOptionTitleChange = e => {
|
||||
this.props.onChange(this.props.index, e.target.value);
|
||||
};
|
||||
|
||||
handleOptionRemove = () => {
|
||||
this.props.onRemove(this.props.index);
|
||||
};
|
||||
|
||||
|
||||
handleToggleMultiple = e => {
|
||||
this.props.onToggleMultiple();
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
onSuggestionsClearRequested = () => {
|
||||
this.props.onClearSuggestions();
|
||||
}
|
||||
|
||||
onSuggestionsFetchRequested = (token) => {
|
||||
this.props.onFetchSuggestions(token);
|
||||
}
|
||||
|
||||
onSuggestionSelected = (tokenStart, token, value) => {
|
||||
this.props.onSuggestionSelected(tokenStart, token, value, ['poll', 'options', this.props.index]);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { isPollMultiple, title, index, intl } = this.props;
|
||||
|
||||
const toggleClasses = cx({
|
||||
default: 1,
|
||||
paddingHorizontal10PX: 1,
|
||||
paddingVertical10PX: 1,
|
||||
borderColorSecondary: 1,
|
||||
border1PX: 1,
|
||||
outlineNone: 1,
|
||||
marginRight10PX: 1,
|
||||
circle: !isPollMultiple,
|
||||
})
|
||||
|
||||
return (
|
||||
<li className={[_s.default, _s.flexRow, _s.marginBottom10PX].join(' ')}>
|
||||
<label className={[_s.default, _s.flexRow, _s.flexGrow1, _s.alignItemsCenter].join(' ')}>
|
||||
<span
|
||||
className={toggleClasses}
|
||||
onClick={this.handleToggleMultiple}
|
||||
role='button'
|
||||
tabIndex='0'
|
||||
/>
|
||||
|
||||
<AutosuggestTextbox
|
||||
placeholder={intl.formatMessage(messages.option_placeholder, { number: index + 1 })}
|
||||
maxLength={25}
|
||||
value={title}
|
||||
onChange={this.handleOptionTitleChange}
|
||||
suggestions={this.props.suggestions}
|
||||
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
|
||||
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
|
||||
onSuggestionSelected={this.onSuggestionSelected}
|
||||
searchTokens={[':']}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<Button
|
||||
narrow
|
||||
circle
|
||||
backgroundColor='none'
|
||||
className={[_s.marginLeft5PX, _s.justifyContentCenter].join(' ')}
|
||||
icon='close'
|
||||
iconWidth='8px'
|
||||
iconHeight='8px'
|
||||
iconClassName={_s.fillColorSecondary}
|
||||
disabled={index <= 1}
|
||||
title={intl.formatMessage(messages.remove_option)}
|
||||
onClick={this.handleOptionRemove}
|
||||
/>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default
|
||||
@injectIntl
|
||||
class PollForm extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
options: ImmutablePropTypes.list,
|
||||
expiresIn: PropTypes.number,
|
||||
isMultiple: PropTypes.bool,
|
||||
onChangeOption: PropTypes.func.isRequired,
|
||||
onAddOption: PropTypes.func.isRequired,
|
||||
onRemoveOption: PropTypes.func.isRequired,
|
||||
onChangeSettings: PropTypes.func.isRequired,
|
||||
suggestions: ImmutablePropTypes.list,
|
||||
onClearSuggestions: PropTypes.func.isRequired,
|
||||
onFetchSuggestions: PropTypes.func.isRequired,
|
||||
onSuggestionSelected: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
handleAddOption = () => {
|
||||
this.props.onAddOption('');
|
||||
};
|
||||
|
||||
handleSelectDuration = e => {
|
||||
this.props.onChangeSettings(e.target.value, this.props.isMultiple);
|
||||
};
|
||||
|
||||
handleToggleMultiple = () => {
|
||||
this.props.onChangeSettings(this.props.expiresIn, !this.props.isMultiple);
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
options,
|
||||
expiresIn,
|
||||
isMultiple,
|
||||
onChangeOption,
|
||||
onRemoveOption,
|
||||
intl,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
if (!options) return null
|
||||
|
||||
return (
|
||||
<div className={[_s.default, _s.paddingHorizontal10PX, _s.paddingVertical10PX, _s.borderColorSecondary, _s.border1PX, _s.radiusSmall].join(' ')}>
|
||||
<ul className={[_s.default, _s.listStyleNone].join(' ')}>
|
||||
{
|
||||
options.map((title, i) => (
|
||||
<PollFormOption
|
||||
title={title}
|
||||
key={`poll-form-option-${i}`}
|
||||
index={i}
|
||||
onChange={onChangeOption}
|
||||
onRemove={onRemoveOption}
|
||||
isPollMultiple={isMultiple}
|
||||
onToggleMultiple={this.handleToggleMultiple}
|
||||
{...otherProps}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
|
||||
<div className={[_s.default, _s.flexRow].join(' ')}>
|
||||
{
|
||||
options.size < 4 && (
|
||||
<Button
|
||||
outline
|
||||
backgroundColor='none'
|
||||
color='brand'
|
||||
className={[_s.alignItemsCenter, _s.marginRight10PX].join(' ')}
|
||||
onClick={this.handleAddOption}
|
||||
icon='add'
|
||||
iconWidth='14px'
|
||||
iconHeight='14px'
|
||||
iconClassName={[_s.fillColorBrand, _s.marginRight5PX].join(' ')}
|
||||
>
|
||||
<Text color='brand'>
|
||||
{intl.formatMessage(messages.add_option)}
|
||||
</Text>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
<div className={[_s.default, _s.flexGrow1].join(' ')}>
|
||||
<Select
|
||||
value={expiresIn}
|
||||
onChange={this.handleSelectDuration}
|
||||
options={[
|
||||
{
|
||||
value: 300,
|
||||
title: intl.formatMessage(messages.minutes, { number: 5 }),
|
||||
},
|
||||
{
|
||||
value: 1800,
|
||||
title: intl.formatMessage(messages.minutes, { number: 30 }),
|
||||
},
|
||||
{
|
||||
value: 3600,
|
||||
title: intl.formatMessage(messages.hours, { number: 1 }),
|
||||
},
|
||||
{
|
||||
value: 21600,
|
||||
title: intl.formatMessage(messages.hours, { number: 6 }),
|
||||
},
|
||||
{
|
||||
value: 86400,
|
||||
title: intl.formatMessage(messages.days, { number: 1 }),
|
||||
},
|
||||
{
|
||||
value: 259200,
|
||||
title: intl.formatMessage(messages.days, { number: 3 }),
|
||||
},
|
||||
{
|
||||
value: 604800,
|
||||
title: intl.formatMessage(messages.days, { number: 7 }),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from './poll_form'
|
||||
@@ -1,161 +0,0 @@
|
||||
import classNames from 'classnames';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
import IconButton from '../../../../components/icon_button';
|
||||
import Icon from '../../../../components/icon';
|
||||
import AutosuggestTextbox from '../../../../components/autosuggest_textbox';
|
||||
|
||||
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' },
|
||||
remove_option: { id: 'compose_form.poll.remove_option', defaultMessage: 'Remove this choice' },
|
||||
poll_duration: { id: 'compose_form.poll.duration', defaultMessage: 'Poll duration' },
|
||||
minutes: { id: 'intervals.full.minutes', defaultMessage: '{number, plural, one {# minute} other {# minutes}}' },
|
||||
hours: { id: 'intervals.full.hours', defaultMessage: '{number, plural, one {# hour} other {# hours}}' },
|
||||
days: { id: 'intervals.full.days', defaultMessage: '{number, plural, one {# day} other {# days}}' },
|
||||
});
|
||||
|
||||
@injectIntl
|
||||
class Option extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
index: PropTypes.number.isRequired,
|
||||
isPollMultiple: PropTypes.bool,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onRemove: PropTypes.func.isRequired,
|
||||
onToggleMultiple: PropTypes.func.isRequired,
|
||||
suggestions: ImmutablePropTypes.list,
|
||||
onClearSuggestions: PropTypes.func.isRequired,
|
||||
onFetchSuggestions: PropTypes.func.isRequired,
|
||||
onSuggestionSelected: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
handleOptionTitleChange = e => {
|
||||
this.props.onChange(this.props.index, e.target.value);
|
||||
};
|
||||
|
||||
handleOptionRemove = () => {
|
||||
this.props.onRemove(this.props.index);
|
||||
};
|
||||
|
||||
|
||||
handleToggleMultiple = e => {
|
||||
this.props.onToggleMultiple();
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
onSuggestionsClearRequested = () => {
|
||||
this.props.onClearSuggestions();
|
||||
}
|
||||
|
||||
onSuggestionsFetchRequested = (token) => {
|
||||
this.props.onFetchSuggestions(token);
|
||||
}
|
||||
|
||||
onSuggestionSelected = (tokenStart, token, value) => {
|
||||
this.props.onSuggestionSelected(tokenStart, token, value, ['poll', 'options', this.props.index]);
|
||||
}
|
||||
|
||||
render () {
|
||||
const { isPollMultiple, title, index, intl } = this.props;
|
||||
|
||||
return (
|
||||
<li>
|
||||
<label className='poll__text editable'>
|
||||
<span
|
||||
className={classNames('poll__input', { checkbox: isPollMultiple })}
|
||||
onClick={this.handleToggleMultiple}
|
||||
role='button'
|
||||
tabIndex='0'
|
||||
/>
|
||||
|
||||
<AutosuggestTextbox
|
||||
placeholder={intl.formatMessage(messages.option_placeholder, { number: index + 1 })}
|
||||
maxLength={25}
|
||||
value={title}
|
||||
onChange={this.handleOptionTitleChange}
|
||||
suggestions={this.props.suggestions}
|
||||
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
|
||||
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
|
||||
onSuggestionSelected={this.onSuggestionSelected}
|
||||
searchTokens={[':']}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<div className='poll__cancel'>
|
||||
<IconButton disabled={index <= 1} title={intl.formatMessage(messages.remove_option)} icon='times' onClick={this.handleOptionRemove} />
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default
|
||||
@injectIntl
|
||||
class PollForm extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
options: ImmutablePropTypes.list,
|
||||
expiresIn: PropTypes.number,
|
||||
isMultiple: PropTypes.bool,
|
||||
onChangeOption: PropTypes.func.isRequired,
|
||||
onAddOption: PropTypes.func.isRequired,
|
||||
onRemoveOption: PropTypes.func.isRequired,
|
||||
onChangeSettings: PropTypes.func.isRequired,
|
||||
suggestions: ImmutablePropTypes.list,
|
||||
onClearSuggestions: PropTypes.func.isRequired,
|
||||
onFetchSuggestions: PropTypes.func.isRequired,
|
||||
onSuggestionSelected: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
handleAddOption = () => {
|
||||
this.props.onAddOption('');
|
||||
};
|
||||
|
||||
handleSelectDuration = e => {
|
||||
this.props.onChangeSettings(e.target.value, this.props.isMultiple);
|
||||
};
|
||||
|
||||
handleToggleMultiple = () => {
|
||||
this.props.onChangeSettings(this.props.expiresIn, !this.props.isMultiple);
|
||||
};
|
||||
|
||||
render () {
|
||||
const { options, expiresIn, isMultiple, onChangeOption, onRemoveOption, intl, ...other } = this.props;
|
||||
|
||||
if (!options) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='compose-form__poll-wrapper'>
|
||||
<ul>
|
||||
{options.map((title, i) => <Option title={title} key={i} index={i} onChange={onChangeOption} onRemove={onRemoveOption} isPollMultiple={isMultiple} onToggleMultiple={this.handleToggleMultiple} {...other} />)}
|
||||
</ul>
|
||||
|
||||
<div className='poll__footer'>
|
||||
{options.size < 4 && (
|
||||
<button className='button button--secondary' onClick={this.handleAddOption}><Icon id='plus' /> <FormattedMessage {...messages.add_option} /></button>
|
||||
)}
|
||||
|
||||
<select value={expiresIn} onChange={this.handleSelectDuration}>
|
||||
<option value={300}>{intl.formatMessage(messages.minutes, { number: 5 })}</option>
|
||||
<option value={1800}>{intl.formatMessage(messages.minutes, { number: 30 })}</option>
|
||||
<option value={3600}>{intl.formatMessage(messages.hours, { number: 1 })}</option>
|
||||
<option value={21600}>{intl.formatMessage(messages.hours, { number: 6 })}</option>
|
||||
<option value={86400}>{intl.formatMessage(messages.days, { number: 1 })}</option>
|
||||
<option value={259200}>{intl.formatMessage(messages.days, { number: 3 })}</option>
|
||||
<option value={604800}>{intl.formatMessage(messages.days, { number: 7 })}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
.poll-list-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&__text {
|
||||
flex: 0 0 auto;
|
||||
width: calc(100% - (23px + 6px));
|
||||
margin-right: 6px;
|
||||
}
|
||||
}
|
||||
@@ -45,6 +45,7 @@ class SpoilerButton extends PureComponent {
|
||||
icon='warning'
|
||||
onClick={this.handleClick}
|
||||
small={small}
|
||||
active={active}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,9 +2,8 @@ import ImmutablePropTypes from 'react-immutable-proptypes'
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component'
|
||||
import { defineMessages, injectIntl } from 'react-intl'
|
||||
import classNames from 'classnames'
|
||||
import spring from 'react-motion/lib/spring'
|
||||
import Motion from '../../../ui/util/optional_motion'
|
||||
import Button from '../../../../components/button'
|
||||
import Image from '../../../../components/image'
|
||||
|
||||
const messages = defineMessages({
|
||||
description: { id: 'upload_form.description', defaultMessage: 'Describe for the visually impaired' },
|
||||
@@ -72,7 +71,10 @@ class Upload extends ImmutablePureComponent {
|
||||
handleInputBlur = () => {
|
||||
const { dirtyDescription } = this.state
|
||||
|
||||
this.setState({ focused: false, dirtyDescription: null })
|
||||
this.setState({
|
||||
focused: false,
|
||||
dirtyDescription: null,
|
||||
})
|
||||
|
||||
if (dirtyDescription !== null) {
|
||||
this.props.onDescriptionChange(this.props.media.get('id'), dirtyDescription)
|
||||
@@ -90,37 +92,33 @@ class Upload extends ImmutablePureComponent {
|
||||
|
||||
return (
|
||||
<div className='compose-form-upload' tabIndex='0' onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave} onClick={this.handleClick} role='button'>
|
||||
<Motion defaultStyle={{ scale: 0.8 }} style={{ scale: spring(1, { stiffness: 180, damping: 12 }) }}>
|
||||
{({ scale }) => (
|
||||
<div className='compose-form__upload-thumbnail' style={{ transform: `scale(${scale})`, 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='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>
|
||||
<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>
|
||||
)}
|
||||
</Motion>
|
||||
<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 +0,0 @@
|
||||
export { default } from './warning'
|
||||
@@ -1,24 +0,0 @@
|
||||
import Motion from '../../../ui/util/optional_motion';
|
||||
import spring from 'react-motion/lib/spring';
|
||||
|
||||
export default class Warning extends PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
message: PropTypes.node.isRequired,
|
||||
};
|
||||
|
||||
render () {
|
||||
const { message } = 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 }) => (
|
||||
<div className='compose-form-warning' style={{ opacity: opacity, transform: `scale(${scaleX}, ${scaleY})` }}>
|
||||
{message}
|
||||
</div>
|
||||
)}
|
||||
</Motion>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
.compose-form-warning {
|
||||
color: $inverted-text-color;
|
||||
margin-bottom: 10px;
|
||||
background: $ui-primary-color;
|
||||
box-shadow: 0 2px 6px rgba($base-shadow-color, 0.3);
|
||||
padding: 8px 10px;
|
||||
border-radius: 4px;
|
||||
|
||||
@include text-sizing(13px, 400);
|
||||
|
||||
strong {
|
||||
color: $inverted-text-color;
|
||||
font-weight: 500;
|
||||
|
||||
@each $lang in $cjk-langs {
|
||||
&:lang(#{$lang}) {
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
color: $lighter-text-color;
|
||||
font-weight: 500;
|
||||
text-decoration: underline;
|
||||
|
||||
&:hover,
|
||||
&:active,
|
||||
&:focus {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
import Warning from '../components/warning';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { me } from '../../../initial_state';
|
||||
|
||||
const APPROX_HASHTAG_RE = /(?:^|[^\/\)\w])#(\w*[a-zA-Z·]\w*)/i;
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
needsLockWarning: state.getIn(['compose', 'privacy']) === 'private' && !state.getIn(['accounts', me, 'locked']),
|
||||
hashtagWarning: state.getIn(['compose', 'privacy']) !== 'public' && APPROX_HASHTAG_RE.test(state.getIn(['compose', 'text'])),
|
||||
directMessageWarning: state.getIn(['compose', 'privacy']) === 'direct',
|
||||
});
|
||||
|
||||
const WarningWrapper = ({ needsLockWarning, hashtagWarning, directMessageWarning }) => {
|
||||
if (needsLockWarning) {
|
||||
return <Warning message={<FormattedMessage id='compose_form.lock_disclaimer' defaultMessage='Your account is not {locked}. Anyone can follow you to view your follower-only posts.' values={{ locked: <a href='/settings/profile'><FormattedMessage id='compose_form.lock_disclaimer.lock' defaultMessage='locked' /></a> }} />} />;
|
||||
}
|
||||
|
||||
if (hashtagWarning) {
|
||||
return <Warning message={<FormattedMessage id='compose_form.hashtag_warning' defaultMessage="This gab won't be listed under any hashtag as it is unlisted. Only public gabs can be searched by hashtag." />} />;
|
||||
}
|
||||
|
||||
if (directMessageWarning) {
|
||||
const message = (
|
||||
<span>
|
||||
<FormattedMessage id='compose_form.direct_message_warning' defaultMessage='This gab will only be sent to all the mentioned users.' /> <a href='/about/tos' target='_blank'><FormattedMessage id='compose_form.direct_message_warning_learn_more' defaultMessage='Learn more' /></a>
|
||||
</span>
|
||||
);
|
||||
|
||||
return <Warning message={message} />;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
WarningWrapper.propTypes = {
|
||||
needsLockWarning: PropTypes.bool,
|
||||
hashtagWarning: PropTypes.bool,
|
||||
directMessageWarning: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(WarningWrapper);
|
||||
@@ -372,6 +372,7 @@ class Status extends ImmutablePureComponent {
|
||||
}
|
||||
|
||||
renderChildren(list) {
|
||||
// : todo : comments
|
||||
return list.map(id => (
|
||||
<StatusContainer
|
||||
key={id}
|
||||
@@ -464,7 +465,7 @@ class Status extends ImmutablePureComponent {
|
||||
{ancestors}
|
||||
|
||||
<HotKeys handlers={handlers}>
|
||||
<div className={classNames('focusable', 'detailed-status__wrapper')} tabIndex='0' aria-label={textForScreenReader(intl, status, false)}>
|
||||
<div className={_s.outlineNone} tabIndex='0' aria-label={textForScreenReader(intl, status, false)}>
|
||||
{ /* <DetailedStatus
|
||||
status={status}
|
||||
onOpenVideo={this.handleOpenVideo}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'
|
||||
import { fromJS, is } from 'immutable'
|
||||
import { defineMessages, injectIntl } from 'react-intl'
|
||||
import { is } from 'immutable'
|
||||
import { throttle } from 'lodash'
|
||||
import classNames from 'classnames/bind'
|
||||
import { decode } from 'blurhash'
|
||||
@@ -7,7 +7,6 @@ import { isFullscreen, requestFullscreen, exitFullscreen } from '../../utils/ful
|
||||
import { isPanoramic, isPortrait, minimumAspectRatio, maximumAspectRatio } from '../../utils/media_aspect_ratio'
|
||||
import { displayMedia } from '../../initial_state'
|
||||
import Button from '../../components/button'
|
||||
import Icon from '../../components/icon'
|
||||
import Text from '../../components/text'
|
||||
|
||||
const cx = classNames.bind(_s)
|
||||
@@ -20,72 +19,74 @@ const messages = defineMessages({
|
||||
hide: { id: 'video.hide', defaultMessage: 'Hide video' },
|
||||
fullscreen: { id: 'video.fullscreen', defaultMessage: 'Full screen' },
|
||||
exit_fullscreen: { id: 'video.exit_fullscreen', defaultMessage: 'Exit full screen' },
|
||||
});
|
||||
sensitive: { id: 'status.sensitive_warning', defaultMessage: 'Sensitive content' },
|
||||
hidden: { id: 'status.media_hidden', defaultMessage: 'Media hidden' },
|
||||
})
|
||||
|
||||
const formatTime = secondsNum => {
|
||||
let hours = Math.floor(secondsNum / 3600);
|
||||
let minutes = Math.floor((secondsNum - (hours * 3600)) / 60);
|
||||
let seconds = secondsNum - (hours * 3600) - (minutes * 60);
|
||||
let hours = Math.floor(secondsNum / 3600)
|
||||
let minutes = Math.floor((secondsNum - (hours * 3600)) / 60)
|
||||
let seconds = secondsNum - (hours * 3600) - (minutes * 60)
|
||||
|
||||
if (hours < 10) hours = '0' + hours;
|
||||
if (minutes < 10) minutes = '0' + minutes;
|
||||
if (seconds < 10) seconds = '0' + seconds;
|
||||
if (hours < 10) hours = '0' + hours
|
||||
if (minutes < 10) minutes = '0' + minutes
|
||||
if (seconds < 10) seconds = '0' + seconds
|
||||
|
||||
return (hours === '00' ? '' : `${hours}:`) + `${minutes}:${seconds}`;
|
||||
};
|
||||
return (hours === '00' ? '' : `${hours}:`) + `${minutes}:${seconds}`
|
||||
}
|
||||
|
||||
export const findElementPosition = el => {
|
||||
let box;
|
||||
let box
|
||||
|
||||
if (el.getBoundingClientRect && el.parentNode) {
|
||||
box = el.getBoundingClientRect();
|
||||
box = el.getBoundingClientRect()
|
||||
}
|
||||
|
||||
if (!box) {
|
||||
return {
|
||||
left: 0,
|
||||
top: 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const docEl = document.documentElement;
|
||||
const body = document.body;
|
||||
const docEl = document.documentElement
|
||||
const body = document.body
|
||||
|
||||
const clientLeft = docEl.clientLeft || body.clientLeft || 0;
|
||||
const scrollLeft = window.pageXOffset || body.scrollLeft;
|
||||
const left = (box.left + scrollLeft) - clientLeft;
|
||||
const clientLeft = docEl.clientLeft || body.clientLeft || 0
|
||||
const scrollLeft = window.pageXOffset || body.scrollLeft
|
||||
const left = (box.left + scrollLeft) - clientLeft
|
||||
|
||||
const clientTop = docEl.clientTop || body.clientTop || 0;
|
||||
const scrollTop = window.pageYOffset || body.scrollTop;
|
||||
const top = (box.top + scrollTop) - clientTop;
|
||||
const clientTop = docEl.clientTop || body.clientTop || 0
|
||||
const scrollTop = window.pageYOffset || body.scrollTop
|
||||
const top = (box.top + scrollTop) - clientTop
|
||||
|
||||
return {
|
||||
left: Math.round(left),
|
||||
top: Math.round(top),
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const getPointerPosition = (el, event) => {
|
||||
const position = {};
|
||||
const box = findElementPosition(el);
|
||||
const boxW = el.offsetWidth;
|
||||
const boxH = el.offsetHeight;
|
||||
const boxY = box.top;
|
||||
const boxX = box.left;
|
||||
const position = {}
|
||||
const box = findElementPosition(el)
|
||||
const boxW = el.offsetWidth
|
||||
const boxH = el.offsetHeight
|
||||
const boxY = box.top
|
||||
const boxX = box.left
|
||||
|
||||
let pageY = event.pageY;
|
||||
let pageX = event.pageX;
|
||||
let pageY = event.pageY
|
||||
let pageX = event.pageX
|
||||
|
||||
if (event.changedTouches) {
|
||||
pageX = event.changedTouches[0].pageX;
|
||||
pageY = event.changedTouches[0].pageY;
|
||||
pageX = event.changedTouches[0].pageX
|
||||
pageY = event.changedTouches[0].pageY
|
||||
}
|
||||
|
||||
position.y = Math.max(0, Math.min(1, (pageY - boxY) / boxH));
|
||||
position.x = Math.max(0, Math.min(1, (pageX - boxX) / boxW));
|
||||
position.y = Math.max(0, Math.min(1, (pageY - boxY) / boxH))
|
||||
position.x = Math.max(0, Math.min(1, (pageX - boxX) / boxW))
|
||||
|
||||
return position;
|
||||
};
|
||||
return position
|
||||
}
|
||||
|
||||
export default
|
||||
@injectIntl
|
||||
@@ -99,8 +100,6 @@ class Video extends PureComponent {
|
||||
height: PropTypes.number,
|
||||
sensitive: PropTypes.bool,
|
||||
startTime: PropTypes.number,
|
||||
onOpenVideo: PropTypes.func,
|
||||
onCloseVideo: PropTypes.func,
|
||||
detailed: PropTypes.bool,
|
||||
inline: PropTypes.bool,
|
||||
cacheWidth: PropTypes.func,
|
||||
@@ -109,7 +108,7 @@ class Video extends PureComponent {
|
||||
intl: PropTypes.object.isRequired,
|
||||
blurhash: PropTypes.string,
|
||||
aspectRatio: PropTypes.number,
|
||||
};
|
||||
}
|
||||
|
||||
state = {
|
||||
currentTime: 0,
|
||||
@@ -121,237 +120,267 @@ class Video extends PureComponent {
|
||||
fullscreen: false,
|
||||
hovered: false,
|
||||
muted: false,
|
||||
hoveringVolumeButton: false,
|
||||
hoveringVolumeControl: false,
|
||||
revealed: this.props.visible !== undefined ? this.props.visible : (displayMedia !== 'hide_all' && !this.props.sensitive || displayMedia === 'show_all'),
|
||||
};
|
||||
}
|
||||
|
||||
volHeight = 100
|
||||
volOffset = 13
|
||||
|
||||
// hard coded in components.scss
|
||||
// any way to get ::before values programatically?
|
||||
volWidth = 50;
|
||||
volOffset = 70;
|
||||
volHandleOffset = v => {
|
||||
const offset = v * this.volWidth + this.volOffset;
|
||||
return (offset > 110) ? 110 : offset;
|
||||
const offset = v * this.volHeight + this.volOffset
|
||||
return (offset > 110) ? 110 : offset
|
||||
}
|
||||
|
||||
setPlayerRef = c => {
|
||||
this.player = c;
|
||||
this.player = c
|
||||
|
||||
if (c) {
|
||||
if (this.props.cacheWidth) this.props.cacheWidth(this.player.offsetWidth);
|
||||
if (this.props.cacheWidth) this.props.cacheWidth(this.player.offsetWidth)
|
||||
this.setState({
|
||||
containerWidth: c.offsetWidth,
|
||||
});
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
setVideoRef = c => {
|
||||
this.video = c;
|
||||
this.video = c
|
||||
|
||||
if (this.video) {
|
||||
this.setState({ volume: this.video.volume, muted: this.video.muted });
|
||||
const { volume, muted } = this.video
|
||||
this.setState({
|
||||
volume,
|
||||
muted,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
setSeekRef = c => {
|
||||
this.seek = c;
|
||||
this.seek = c
|
||||
}
|
||||
|
||||
setVolumeRef = c => {
|
||||
this.volume = c;
|
||||
this.volume = c
|
||||
}
|
||||
|
||||
setCanvasRef = c => {
|
||||
this.canvas = c;
|
||||
this.canvas = c
|
||||
}
|
||||
|
||||
handleClickRoot = e => e.stopPropagation();
|
||||
handleClickRoot = e => e.stopPropagation()
|
||||
|
||||
handlePlay = () => {
|
||||
this.setState({ paused: false });
|
||||
this.setState({ paused: false })
|
||||
}
|
||||
|
||||
handlePause = () => {
|
||||
this.setState({ paused: true });
|
||||
this.setState({ paused: true })
|
||||
}
|
||||
|
||||
handleTimeUpdate = () => {
|
||||
const { currentTime, duration } = this.video
|
||||
this.setState({
|
||||
currentTime: Math.floor(this.video.currentTime),
|
||||
duration: Math.floor(this.video.duration),
|
||||
});
|
||||
currentTime: Math.floor(currentTime),
|
||||
duration: Math.floor(duration),
|
||||
})
|
||||
}
|
||||
|
||||
handleVolumeMouseDown = e => {
|
||||
document.addEventListener('mousemove', this.handleMouseVolSlide, true);
|
||||
document.addEventListener('mouseup', this.handleVolumeMouseUp, true);
|
||||
document.addEventListener('touchmove', this.handleMouseVolSlide, true);
|
||||
document.addEventListener('touchend', this.handleVolumeMouseUp, true);
|
||||
document.addEventListener('mousemove', this.handleMouseVolSlide, true)
|
||||
document.addEventListener('mouseup', this.handleVolumeMouseUp, true)
|
||||
document.addEventListener('touchmove', this.handleMouseVolSlide, true)
|
||||
document.addEventListener('touchend', this.handleVolumeMouseUp, true)
|
||||
|
||||
this.handleMouseVolSlide(e);
|
||||
this.handleMouseVolSlide(e)
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
}
|
||||
|
||||
handleVolumeMouseUp = () => {
|
||||
document.removeEventListener('mousemove', this.handleMouseVolSlide, true);
|
||||
document.removeEventListener('mouseup', this.handleVolumeMouseUp, true);
|
||||
document.removeEventListener('touchmove', this.handleMouseVolSlide, true);
|
||||
document.removeEventListener('touchend', this.handleVolumeMouseUp, true);
|
||||
document.removeEventListener('mousemove', this.handleMouseVolSlide, true)
|
||||
document.removeEventListener('mouseup', this.handleVolumeMouseUp, true)
|
||||
document.removeEventListener('touchmove', this.handleMouseVolSlide, true)
|
||||
document.removeEventListener('touchend', this.handleVolumeMouseUp, true)
|
||||
}
|
||||
|
||||
handleMouseVolSlide = throttle(e => {
|
||||
const rect = this.volume.getBoundingClientRect();
|
||||
const x = (e.clientX - rect.left) / this.volWidth; //x position within the element.
|
||||
const rect = this.volume.getBoundingClientRect()
|
||||
const y = 1 - ((e.clientY - rect.top) / this.volHeight)
|
||||
|
||||
if (!isNaN(x)) {
|
||||
var slideamt = x;
|
||||
if (x > 1) {
|
||||
slideamt = 1;
|
||||
} else if (x < 0) {
|
||||
slideamt = 0;
|
||||
if (!isNaN(y)) {
|
||||
var slideamt = y
|
||||
if (y > 1) {
|
||||
slideamt = 1
|
||||
} else if (y < 0) {
|
||||
slideamt = 0
|
||||
}
|
||||
this.video.volume = slideamt;
|
||||
this.setState({ volume: slideamt });
|
||||
this.video.volume = slideamt
|
||||
this.setState({ volume: slideamt })
|
||||
}
|
||||
}, 60);
|
||||
}, 60)
|
||||
|
||||
handleMouseDown = e => {
|
||||
document.addEventListener('mousemove', this.handleMouseMove, true);
|
||||
document.addEventListener('mouseup', this.handleMouseUp, true);
|
||||
document.addEventListener('touchmove', this.handleMouseMove, true);
|
||||
document.addEventListener('touchend', this.handleMouseUp, true);
|
||||
document.addEventListener('mousemove', this.handleMouseMove, true)
|
||||
document.addEventListener('mouseup', this.handleMouseUp, true)
|
||||
document.addEventListener('touchmove', this.handleMouseMove, true)
|
||||
document.addEventListener('touchend', this.handleMouseUp, true)
|
||||
|
||||
this.setState({ dragging: true });
|
||||
this.video.pause();
|
||||
this.handleMouseMove(e);
|
||||
this.setState({ dragging: true })
|
||||
this.video.pause()
|
||||
this.handleMouseMove(e)
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
}
|
||||
|
||||
handleMouseUp = () => {
|
||||
document.removeEventListener('mousemove', this.handleMouseMove, true);
|
||||
document.removeEventListener('mouseup', this.handleMouseUp, true);
|
||||
document.removeEventListener('touchmove', this.handleMouseMove, true);
|
||||
document.removeEventListener('touchend', this.handleMouseUp, true);
|
||||
document.removeEventListener('mousemove', this.handleMouseMove, true)
|
||||
document.removeEventListener('mouseup', this.handleMouseUp, true)
|
||||
document.removeEventListener('touchmove', this.handleMouseMove, true)
|
||||
document.removeEventListener('touchend', this.handleMouseUp, true)
|
||||
|
||||
this.setState({ dragging: false });
|
||||
this.video.play();
|
||||
this.setState({ dragging: false })
|
||||
this.video.play()
|
||||
}
|
||||
|
||||
handleMouseMove = throttle(e => {
|
||||
const { x } = getPointerPosition(this.seek, e);
|
||||
const currentTime = Math.floor(this.video.duration * x);
|
||||
const { x } = getPointerPosition(this.seek, e)
|
||||
const currentTime = Math.floor(this.video.duration * x)
|
||||
|
||||
if (!isNaN(currentTime)) {
|
||||
this.video.currentTime = currentTime;
|
||||
this.setState({ currentTime });
|
||||
this.video.currentTime = currentTime
|
||||
this.setState({ currentTime })
|
||||
}
|
||||
}, 60);
|
||||
}, 60)
|
||||
|
||||
togglePlay = () => {
|
||||
if (this.state.paused) {
|
||||
this.video.play();
|
||||
this.video.play()
|
||||
} else {
|
||||
this.video.pause();
|
||||
this.video.pause()
|
||||
}
|
||||
}
|
||||
|
||||
toggleFullscreen = () => {
|
||||
if (isFullscreen()) {
|
||||
exitFullscreen();
|
||||
exitFullscreen()
|
||||
} else {
|
||||
requestFullscreen(this.player);
|
||||
requestFullscreen(this.player)
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
document.addEventListener('fullscreenchange', this.handleFullscreenChange, true);
|
||||
document.addEventListener('webkitfullscreenchange', this.handleFullscreenChange, true);
|
||||
document.addEventListener('mozfullscreenchange', this.handleFullscreenChange, true);
|
||||
document.addEventListener('MSFullscreenChange', this.handleFullscreenChange, true);
|
||||
document.addEventListener('fullscreenchange', this.handleFullscreenChange, true)
|
||||
document.addEventListener('webkitfullscreenchange', this.handleFullscreenChange, true)
|
||||
document.addEventListener('mozfullscreenchange', this.handleFullscreenChange, true)
|
||||
document.addEventListener('MSFullscreenChange', this.handleFullscreenChange, true)
|
||||
|
||||
if (this.props.blurhash) {
|
||||
this._decode();
|
||||
this._decode()
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
document.removeEventListener('fullscreenchange', this.handleFullscreenChange, true);
|
||||
document.removeEventListener('webkitfullscreenchange', this.handleFullscreenChange, true);
|
||||
document.removeEventListener('mozfullscreenchange', this.handleFullscreenChange, true);
|
||||
document.removeEventListener('MSFullscreenChange', this.handleFullscreenChange, true);
|
||||
document.removeEventListener('fullscreenchange', this.handleFullscreenChange, true)
|
||||
document.removeEventListener('webkitfullscreenchange', this.handleFullscreenChange, true)
|
||||
document.removeEventListener('mozfullscreenchange', this.handleFullscreenChange, true)
|
||||
document.removeEventListener('MSFullscreenChange', this.handleFullscreenChange, true)
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (!is(nextProps.visible, this.props.visible) && nextProps.visible !== undefined) {
|
||||
this.setState({ revealed: nextProps.visible });
|
||||
this.setState({ revealed: nextProps.visible })
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
if (prevState.revealed && !this.state.revealed && this.video) {
|
||||
this.video.pause();
|
||||
this.video.pause()
|
||||
}
|
||||
if (prevProps.blurhash !== this.props.blurhash && this.props.blurhash) {
|
||||
this._decode();
|
||||
this._decode()
|
||||
}
|
||||
}
|
||||
|
||||
_decode() {
|
||||
const hash = this.props.blurhash;
|
||||
const pixels = decode(hash, 32, 32);
|
||||
const hash = this.props.blurhash
|
||||
const pixels = decode(hash, 32, 32)
|
||||
|
||||
if (pixels && this.canvas) {
|
||||
const ctx = this.canvas.getContext('2d');
|
||||
const imageData = new ImageData(pixels, 32, 32);
|
||||
const ctx = this.canvas.getContext('2d')
|
||||
const imageData = new ImageData(pixels, 32, 32)
|
||||
|
||||
ctx.putImageData(imageData, 0, 0);
|
||||
ctx.putImageData(imageData, 0, 0)
|
||||
}
|
||||
}
|
||||
|
||||
handleFullscreenChange = () => {
|
||||
this.setState({ fullscreen: isFullscreen() });
|
||||
this.setState({ fullscreen: isFullscreen() })
|
||||
}
|
||||
|
||||
handleMouseEnter = () => {
|
||||
this.setState({ hovered: true });
|
||||
this.setState({ hovered: true })
|
||||
}
|
||||
|
||||
handleMouseLeave = () => {
|
||||
this.setState({ hovered: false });
|
||||
this.setState({ hovered: false })
|
||||
}
|
||||
|
||||
handleMouseEnterAudio = () => {
|
||||
this.setState({ hoveringVolumeButton: true })
|
||||
}
|
||||
|
||||
handleMouseLeaveAudio = throttle(e => {
|
||||
this.setState({ hoveringVolumeButton: false })
|
||||
}, 2000)
|
||||
|
||||
handleMouseEnterVolumeControl = () => {
|
||||
this.setState({ hoveringVolumeControl: true })
|
||||
}
|
||||
|
||||
handleMouseLeaveVolumeControl = throttle(e => {
|
||||
this.setState({ hoveringVolumeControl: false })
|
||||
}, 2000)
|
||||
|
||||
toggleMute = () => {
|
||||
this.video.muted = !this.video.muted;
|
||||
this.setState({ muted: this.video.muted });
|
||||
this.video.muted = !this.video.muted
|
||||
this.setState({ muted: this.video.muted })
|
||||
}
|
||||
|
||||
toggleReveal = () => {
|
||||
if (this.props.onToggleVisibility) {
|
||||
this.props.onToggleVisibility();
|
||||
this.props.onToggleVisibility()
|
||||
} else {
|
||||
this.setState({ revealed: !this.state.revealed });
|
||||
this.setState({ revealed: !this.state.revealed })
|
||||
}
|
||||
}
|
||||
|
||||
handleLoadedData = () => {
|
||||
if (this.props.startTime) {
|
||||
this.video.currentTime = this.props.startTime;
|
||||
this.video.play();
|
||||
this.video.currentTime = this.props.startTime
|
||||
this.video.play()
|
||||
}
|
||||
}
|
||||
|
||||
handleProgress = () => {
|
||||
if (!this.video.buffered) return;
|
||||
if (this.video.buffered.length > 0) {
|
||||
this.setState({ buffer: this.video.buffered.end(0) / this.video.duration * 100 });
|
||||
const { buffered, duration } = this.video
|
||||
|
||||
if (!buffered) return
|
||||
if (buffered.length > 0) {
|
||||
this.setState({
|
||||
buffer: buffered.end(0) / duration * 100,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
handleVolumeChange = () => {
|
||||
this.setState({ volume: this.video.volume, muted: this.video.muted });
|
||||
const { volume, muted } = this.video
|
||||
this.setState({
|
||||
volume,
|
||||
muted,
|
||||
})
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -360,8 +389,6 @@ class Video extends PureComponent {
|
||||
src,
|
||||
inline,
|
||||
startTime,
|
||||
onOpenVideo,
|
||||
onCloseVideo,
|
||||
intl,
|
||||
alt,
|
||||
detailed,
|
||||
@@ -380,54 +407,44 @@ class Video extends PureComponent {
|
||||
fullscreen,
|
||||
hovered,
|
||||
muted,
|
||||
revealed
|
||||
revealed,
|
||||
hoveringVolumeButton,
|
||||
hoveringVolumeControl
|
||||
} = this.state
|
||||
|
||||
const progress = (currentTime / duration) * 100;
|
||||
const progress = (currentTime / duration) * 100
|
||||
|
||||
const volumeWidth = (muted) ? 0 : volume * this.volWidth;
|
||||
const volumeHandleLoc = (muted) ? this.volHandleOffset(0) : this.volHandleOffset(volume);
|
||||
const playerStyle = {};
|
||||
const volumeHeight = (muted) ? 0 : volume * this.volHeight
|
||||
const volumeHandleLoc = (muted) ? this.volHandleOffset(0) : this.volHandleOffset(volume)
|
||||
const playerStyle = {}
|
||||
|
||||
let { width, height } = this.props;
|
||||
|
||||
console.log("buffer, progress:", buffer, progress)
|
||||
let { width, height } = this.props
|
||||
|
||||
if (inline && containerWidth) {
|
||||
width = containerWidth;
|
||||
const minSize = containerWidth / (16 / 9);
|
||||
width = containerWidth
|
||||
const minSize = containerWidth / (16 / 9)
|
||||
|
||||
if (isPanoramic(aspectRatio)) {
|
||||
height = Math.max(Math.floor(containerWidth / maximumAspectRatio), minSize);
|
||||
height = Math.max(Math.floor(containerWidth / maximumAspectRatio), minSize)
|
||||
} else if (isPortrait(aspectRatio)) {
|
||||
height = Math.max(Math.floor(containerWidth / minimumAspectRatio), minSize);
|
||||
height = Math.max(Math.floor(containerWidth / minimumAspectRatio), minSize)
|
||||
} else {
|
||||
height = Math.floor(containerWidth / aspectRatio);
|
||||
height = Math.floor(containerWidth / aspectRatio)
|
||||
}
|
||||
|
||||
playerStyle.height = height;
|
||||
playerStyle.height = height
|
||||
}
|
||||
|
||||
let preload;
|
||||
let preload
|
||||
|
||||
if (startTime || fullscreen || dragging) {
|
||||
preload = 'auto';
|
||||
preload = 'auto'
|
||||
} else if (detailed) {
|
||||
preload = 'metadata';
|
||||
preload = 'metadata'
|
||||
} else {
|
||||
preload = 'none';
|
||||
preload = 'none'
|
||||
}
|
||||
|
||||
let warning;
|
||||
|
||||
if (sensitive) {
|
||||
warning = <FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' />;
|
||||
} else {
|
||||
warning = <FormattedMessage id='status.media_hidden' defaultMessage='Media hidden' />;
|
||||
}
|
||||
|
||||
// console.log("width, height:", width, height)
|
||||
|
||||
// className={classNames('video-player', {
|
||||
// inactive: !revealed,
|
||||
// detailed,
|
||||
@@ -444,6 +461,7 @@ class Video extends PureComponent {
|
||||
backgroundColorBrand: 1,
|
||||
marginLeftNeg5PX: 1,
|
||||
z3: 1,
|
||||
boxShadow1: 1,
|
||||
opacity0: !dragging,
|
||||
opacity1: dragging || hovered,
|
||||
})
|
||||
@@ -456,6 +474,16 @@ class Video extends PureComponent {
|
||||
height4PX: 1,
|
||||
})
|
||||
|
||||
const volumeControlClasses = cx({
|
||||
default: 1,
|
||||
positionAbsolute: 1,
|
||||
backgroundColorOpaque: 1,
|
||||
videoPlayerVolume: 1,
|
||||
height122PX: 1,
|
||||
circle: 1,
|
||||
displayNone: !hoveringVolumeButton && !hoveringVolumeControl || !hovered,
|
||||
})
|
||||
|
||||
return (
|
||||
<div
|
||||
role='menuitem'
|
||||
@@ -478,7 +506,6 @@ class Video extends PureComponent {
|
||||
/>
|
||||
}
|
||||
|
||||
|
||||
{
|
||||
revealed &&
|
||||
<video
|
||||
@@ -508,16 +535,38 @@ class Video extends PureComponent {
|
||||
|
||||
{ /* <div className={classNames('spoiler-button', { 'spoiler-button--hidden': revealed })}>
|
||||
<button type='button' className='spoiler-button__overlay' onClick={this.toggleReveal}>
|
||||
<span className='spoiler-button__overlay__label'>{warning}</span>
|
||||
<span className='spoiler-button__overlay__label'>
|
||||
{intl.formatMessage(sensitive ? messages.sensitive : messages.hidden)}
|
||||
</span>
|
||||
</button>
|
||||
</div> */ }
|
||||
|
||||
<div className='video-player__volume' onMouseDown={this.handleVolumeMouseDown} ref={this.setVolumeRef}>
|
||||
<div className='video-player__volume__current' style={{ height: `${volumeWidth}px` }} />
|
||||
<div
|
||||
className={volumeControlClasses}
|
||||
onMouseDown={this.handleVolumeMouseDown}
|
||||
onMouseEnter={this.handleMouseEnterVolumeControl}
|
||||
onMouseLeave={this.handleMouseLeaveVolumeControl}
|
||||
ref={this.setVolumeRef}
|
||||
>
|
||||
<div
|
||||
className={[_s.default, _s.radiusSmall, _s.marginVertical10PX, _s.positionAbsolute, _s.width4PX, _s.marginLeft10PX, _s.backgroundColorPrimaryOpaque].join(' ')}
|
||||
style={{
|
||||
height: '102px',
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className={[_s.default, _s.radiusSmall, _s.marginVertical10PX, _s.bottom0, _s.positionAbsolute, _s.width4PX, _s.marginLeft10PX, _s.backgroundColorPrimary].join(' ')}
|
||||
style={{
|
||||
height: `${volumeHeight}px`
|
||||
}}
|
||||
/>
|
||||
<span
|
||||
className={classNames('video-player__volume__handle')}
|
||||
className={[_s.default, _s.cursorPointer, _s.positionAbsolute, _s.circle, _s.paddingHorizontal5PX, _s.boxShadow1, _s.marginBottomNeg5PX, _s.paddingVertical5PX, _s.backgroundColorPrimary, _s.z3].join(' ')}
|
||||
tabIndex='0'
|
||||
style={{ left: `${volumeHandleLoc}px` }}
|
||||
style={{
|
||||
marginLeft: '7px',
|
||||
bottom: `${volumeHandleLoc}px`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -562,7 +611,6 @@ class Video extends PureComponent {
|
||||
{formatTime(duration)}
|
||||
</Text>
|
||||
|
||||
|
||||
<Button
|
||||
narrow
|
||||
backgroundColor='none'
|
||||
@@ -574,6 +622,8 @@ class Video extends PureComponent {
|
||||
iconHeight='24px'
|
||||
iconClassName={_s.fillColorWhite}
|
||||
className={[_s.paddingHorizontal10PX, _s.marginLeft10PX].join(' ')}
|
||||
onMouseEnter={this.handleMouseEnterAudio}
|
||||
onMouseLeave={this.handleMouseLeaveAudio}
|
||||
/>
|
||||
|
||||
<Button
|
||||
@@ -592,7 +642,7 @@ class Video extends PureComponent {
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -26,30 +26,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
&.inline {
|
||||
video {
|
||||
object-fit: contain;
|
||||
position: relative;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
}
|
||||
|
||||
&__controls {
|
||||
z-index: 2;
|
||||
box-sizing: border-box;
|
||||
background: linear-gradient(0deg, rgba($base-shadow-color, 0.85) 0, rgba($base-shadow-color, 0.45) 60%, transparent);
|
||||
padding: 0 15px;
|
||||
opacity: 0;
|
||||
transition: opacity .1s ease;
|
||||
|
||||
@include abs-position(auto, 0, 0, 0);
|
||||
|
||||
&.active {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&.inactive {
|
||||
|
||||
video,
|
||||
@@ -92,145 +68,4 @@
|
||||
@include text-sizing(11px, 500);
|
||||
}
|
||||
}
|
||||
|
||||
&__buttons-bar {
|
||||
padding-bottom: 10px;
|
||||
|
||||
@include flex(space-between);
|
||||
}
|
||||
|
||||
&__buttons {
|
||||
font-size: 16px;
|
||||
|
||||
@include text-overflow(nowrap);
|
||||
|
||||
&.left {
|
||||
button {
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&.right {
|
||||
button {
|
||||
padding-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
background: transparent;
|
||||
padding: 2px 10px;
|
||||
font-size: 16px;
|
||||
border: 0;
|
||||
color: rgba($white, 0.75);
|
||||
|
||||
&:active,
|
||||
&:hover,
|
||||
&:focus {
|
||||
color: $white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__time-sep,
|
||||
&__time-total,
|
||||
&__time-current {
|
||||
@include text-sizing(14px, 500);
|
||||
}
|
||||
|
||||
&__time-current {
|
||||
color: $white;
|
||||
margin-left: 60px;
|
||||
}
|
||||
|
||||
&__time-sep {
|
||||
display: inline-block;
|
||||
margin: 0 6px;
|
||||
}
|
||||
|
||||
&__time-sep,
|
||||
&__time-total {
|
||||
color: $white;
|
||||
}
|
||||
|
||||
&__volume {
|
||||
display: inline;
|
||||
height: 24px;
|
||||
cursor: pointer;
|
||||
|
||||
&::before {
|
||||
background: rgba($white, 0.35);
|
||||
border-radius: 4px;
|
||||
|
||||
@include pseudo;
|
||||
@include size(50px, 4px);
|
||||
@include abs-position(auto, auto, 20px, 70px, false);
|
||||
}
|
||||
|
||||
&__current {
|
||||
display: block;
|
||||
height: 4px;
|
||||
border-radius: 4px;
|
||||
background: lighten($ui-highlight-color, 8%);
|
||||
|
||||
@include abs-position(auto, auto, 20px, 70px);
|
||||
}
|
||||
|
||||
&__handle {
|
||||
z-index: 3;
|
||||
transition: opacity .1s ease;
|
||||
background: lighten($ui-highlight-color, 8%);
|
||||
box-shadow: 1px 2px 6px rgba($base-shadow-color, 0.2);
|
||||
pointer-events: none;
|
||||
|
||||
@include circle(12px);
|
||||
@include abs-position(auto, auto, 16px, 70px);
|
||||
}
|
||||
}
|
||||
|
||||
&__link {
|
||||
padding: 2px 10px;
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: $white;
|
||||
|
||||
@include text-sizing(14px, 500);
|
||||
|
||||
&:hover,
|
||||
&:active,
|
||||
&:focus {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__seek {
|
||||
cursor: pointer;
|
||||
height: 24px;
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
background: rgba($white, 0.35);
|
||||
border-radius: 4px;
|
||||
top: 10px;
|
||||
|
||||
@include pseudo;
|
||||
@include size(100%, 40x);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.video-player__seek__handle {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.detailed,
|
||||
&.fullscreen {
|
||||
.video-player__buttons {
|
||||
button {
|
||||
@include vertical-padding(10px);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user