This commit is contained in:
mgabdev
2020-03-27 11:29:52 -04:00
parent 3d0a85cde4
commit 2bd344a594
46 changed files with 545 additions and 1448 deletions

View File

@@ -139,6 +139,7 @@ export default class Button extends PureComponent {
backgroundColorBrand_onHover: color === COLORS.brand && outline,
colorWhite_onHover: !!children && color === COLORS.brand && outline,
fillColorSecondary: !!icon && color === COLORS.secondary,
fillColorWhite: !!icon && color === COLORS.white,
fillColorBrand: !!icon && color === COLORS.brand,
fillColorWhite_onHover: !!icon && color === COLORS.brand && outline,

View File

@@ -58,7 +58,7 @@ class ModalLayout extends PureComponent {
const childrenContainerClasses = cx({
default: 1,
heightMax80VH: 1,
overflowScroll: 1,
overflowYScroll: 1,
px15: !noPadding,
py10: !noPadding,
})
@@ -77,6 +77,7 @@ class ModalLayout extends PureComponent {
title={intl.formatMessage(messages.close)}
className={_s.marginLeftAuto}
onClick={this.onHandleCloseModal}
color='secondary'
icon='close'
iconWidth='10px'
iconWidth='10px'

View File

@@ -0,0 +1,47 @@
import ImmutablePropTypes from 'react-immutable-proptypes';
import { FormattedMessage } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { NavLink } from 'react-router-dom';
import DisplayName from './display_name';
import Icon from './icon';
export default class MovedNote extends ImmutablePureComponent {
static contextTypes = {
router: PropTypes.object,
};
static propTypes = {
to: ImmutablePropTypes.map.isRequired,
};
render () {
const { to } = this.props;
const displayNameHtml = { __html: from.get('display_name_html') };
return (
<div className='moved-note'>
<div className='moved-note__message'>
<div className='moved-note__icon-wrapper'>
<Icon id='suitcase' className='moved-note__icon' fixedWidth />
</div>
<FormattedMessage
id='account.moved_to'
defaultMessage='{name} has moved to:'
values={{
name: <bdi><strong dangerouslySetInnerHTML={displayNameHtml} /></bdi>
}}
/>
</div>
<NavLink to={`/${this.props.to.get('acct')}`} className='moved-note__display-name'>
<div className='moved-note__display-avatar'>
<Avatar account={to} />
</div>
<DisplayName account={to} />
</NavLink>
</div>
);
}
}

View File

@@ -8,6 +8,8 @@ import Button from '../button'
import Divider from '../divider'
import Heading from '../heading'
import Icon from '../icon'
import Input from '../input'
import Switch from '../switch'
import Text from '../text'
const messages = defineMessages({
@@ -33,11 +35,26 @@ class NotificationFilterPanel extends ImmutablePureComponent {
return (
<PanelLayout title={intl.formatMessage(messages.title)}>
<Text>Date</Text>
<Text>Verified</Text>
<Text>Users</Text>
<Text>Status Id</Text>
<Text>Only People I Follow</Text>
<Text>Start Date</Text>
<Text>End Date</Text>
<Divider small />
<Input title='From specific user(s)' small />
<Input title='From a specific status' small />
<Divider small />
<Switch
id='notifications-verified'
label='Only Verified Users'
/>
<Switch
id='notifications-verified'
label='Only People I Follow'
/>
</PanelLayout>
)
}

View File

@@ -54,10 +54,10 @@ class TrendsPanel extends ImmutablePureComponent {
{ /* trends && trends.map(hashtag => (
<TrendingItem key={hashtag.get('name')} hashtag={hashtag} />
)) */ }
<TrendingItem />
<TrendingItem />
<TrendingItem />
<TrendingItem />
<TrendingItem index='1' />
<TrendingItem index='2' />
<TrendingItem index='3' />
<TrendingItem index='4' />
</div>
</PanelLayout>
)

View File

@@ -0,0 +1,450 @@
import { defineMessages, injectIntl } from 'react-intl'
import ImmutablePropTypes from 'react-immutable-proptypes'
import ImmutablePureComponent from 'react-immutable-pure-component'
import { Map as ImmutableMap } from 'immutable'
import classNames from 'classnames'
import { createSelector } from 'reselect'
import detectPassiveEvents from 'detect-passive-events'
import { changeSetting } from '../../actions/settings'
import { useEmoji } from '../../actions/emojis'
import { EmojiPicker as EmojiPickerAsync } from '../../features/ui/util/async_components'
import { buildCustomEmojis } from '../emoji/emoji'
const messages = defineMessages({
emoji: { id: 'emoji_button.label', defaultMessage: 'Insert emoji' },
emoji_search: { id: 'emoji_button.search', defaultMessage: 'Search...' },
emoji_not_found: { id: 'emoji_button.not_found', defaultMessage: 'No emojos!! (╯°□°)╯︵ ┻━┻' },
custom: { id: 'emoji_button.custom', defaultMessage: 'Custom' },
recent: { id: 'emoji_button.recent', defaultMessage: 'Frequently used' },
search_results: { id: 'emoji_button.search_results', defaultMessage: 'Search results' },
people: { id: 'emoji_button.people', defaultMessage: 'People' },
nature: { id: 'emoji_button.nature', defaultMessage: 'Nature' },
food: { id: 'emoji_button.food', defaultMessage: 'Food & Drink' },
activity: { id: 'emoji_button.activity', defaultMessage: 'Activity' },
travel: { id: 'emoji_button.travel', defaultMessage: 'Travel & Places' },
objects: { id: 'emoji_button.objects', defaultMessage: 'Objects' },
symbols: { id: 'emoji_button.symbols', defaultMessage: 'Symbols' },
flags: { id: 'emoji_button.flags', defaultMessage: 'Flags' },
})
const assetHost = process.env.CDN_HOST || ''
let EmojiPicker, Emoji // load asynchronously
const backgroundImageFn = () => `${assetHost}/emoji/sheet_10.png`
const listenerOptions = detectPassiveEvents.hasSupport ? { passive: true } : false
const categoriesSort = [
'recent',
'custom',
'people',
'nature',
'foods',
'activity',
'places',
'objects',
'symbols',
'flags',
];
class ModifierPickerMenu extends PureComponent {
static propTypes = {
active: PropTypes.bool,
onSelect: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired,
};
handleClick = e => {
this.props.onSelect(e.currentTarget.getAttribute('data-index') * 1);
}
componentWillReceiveProps (nextProps) {
if (nextProps.active) {
this.attachListeners();
} else {
this.removeListeners();
}
}
componentWillUnmount () {
this.removeListeners();
}
handleDocumentClick = e => {
if (this.node && !this.node.contains(e.target)) {
this.props.onClose();
}
}
attachListeners () {
document.addEventListener('click', this.handleDocumentClick, false);
document.addEventListener('touchend', this.handleDocumentClick, listenerOptions);
}
removeListeners () {
document.removeEventListener('click', this.handleDocumentClick, false);
document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions);
}
setRef = c => {
this.node = c;
}
render () {
const { active } = this.props;
return (
<div className='emoji-picker-dropdown__modifiers__menu' style={{ display: active ? 'block' : 'none' }} ref={this.setRef}>
<button onClick={this.handleClick} data-index={1}>
{/*<Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={1} backgroundImageFn={backgroundImageFn} />*/}
</button>
{/*<button onClick={this.handleClick} data-index={2}>
<Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={2} backgroundImageFn={backgroundImageFn} />
</button>
<button onClick={this.handleClick} data-index={3}>
<Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={3} backgroundImageFn={backgroundImageFn} />
</button>
<button onClick={this.handleClick} data-index={4}>
<Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={4} backgroundImageFn={backgroundImageFn} />
</button>
<button onClick={this.handleClick} data-index={5}>
<Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={5} backgroundImageFn={backgroundImageFn} />
</button>
<button onClick={this.handleClick} data-index={6}>
<Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={6} backgroundImageFn={backgroundImageFn} />
</button>*/}
</div>
);
}
}
class ModifierPicker extends PureComponent {
static propTypes = {
active: PropTypes.bool,
modifier: PropTypes.number,
onChange: PropTypes.func,
onClose: PropTypes.func,
onOpen: PropTypes.func,
};
handleClick = () => {
if (this.props.active) {
this.props.onClose();
} else {
this.props.onOpen();
}
}
handleSelect = modifier => {
this.props.onChange(modifier);
this.props.onClose();
}
render () {
const { active, modifier } = this.props;
return (
<div className='emoji-picker-dropdown__modifiers'>
{ /* <Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={modifier} onClick={this.handleClick} backgroundImageFn={backgroundImageFn} /> */ }
<ModifierPickerMenu active={active} onSelect={this.handleSelect} onClose={this.props.onClose} />
</div>
);
}
}
@injectIntl
class EmojiPickerMenu extends ImmutablePureComponent {
static propTypes = {
custom_emojis: ImmutablePropTypes.list,
frequentlyUsedEmojis: PropTypes.arrayOf(PropTypes.string),
loading: PropTypes.bool,
onClose: PropTypes.func.isRequired,
onPick: PropTypes.func.isRequired,
style: PropTypes.object,
arrowOffsetLeft: PropTypes.string,
arrowOffsetTop: PropTypes.string,
intl: PropTypes.object.isRequired,
skinTone: PropTypes.number.isRequired,
onSkinTone: PropTypes.func.isRequired,
};
static defaultProps = {
style: {},
loading: true,
frequentlyUsedEmojis: [],
};
state = {
modifierOpen: false,
};
handleDocumentClick = e => {
if (this.node && !this.node.contains(e.target)) {
this.props.onClose();
}
}
componentDidMount () {
document.addEventListener('click', this.handleDocumentClick, false);
document.addEventListener('touchend', this.handleDocumentClick, listenerOptions);
}
componentWillUnmount () {
document.removeEventListener('click', this.handleDocumentClick, false);
document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions);
}
setRef = c => {
this.node = c;
}
getI18n = () => {
const { intl } = this.props;
return {
search: intl.formatMessage(messages.emoji_search),
notfound: intl.formatMessage(messages.emoji_not_found),
categories: {
search: intl.formatMessage(messages.search_results),
recent: intl.formatMessage(messages.recent),
people: intl.formatMessage(messages.people),
nature: intl.formatMessage(messages.nature),
foods: intl.formatMessage(messages.food),
activity: intl.formatMessage(messages.activity),
places: intl.formatMessage(messages.travel),
objects: intl.formatMessage(messages.objects),
symbols: intl.formatMessage(messages.symbols),
flags: intl.formatMessage(messages.flags),
custom: intl.formatMessage(messages.custom),
},
};
}
handleClick = emoji => {
if (!emoji.native) {
emoji.native = emoji.colons;
}
this.props.onClose();
this.props.onPick(emoji);
}
handleModifierOpen = () => {
this.setState({ modifierOpen: true });
}
handleModifierClose = () => {
this.setState({ modifierOpen: false });
}
handleModifierChange = modifier => {
this.props.onSkinTone(modifier);
}
render () {
const { loading, style, intl, custom_emojis, skinTone, frequentlyUsedEmojis } = this.props;
if (loading) {
return <div style={{ width: 299 }} />;
}
const title = intl.formatMessage(messages.emoji);
const { modifierOpen } = this.state
return (
<div className={classNames('emoji-picker-dropdown__menu', { selecting: modifierOpen })} style={style} ref={this.setRef}>
<EmojiPicker
perLine={8}
emojiSize={22}
sheetSize={32}
custom={buildCustomEmojis(custom_emojis)}
color=''
emoji=''
set='twitter'
title={title}
i18n={this.getI18n()}
onClick={this.handleClick}
include={categoriesSort}
recent={frequentlyUsedEmojis}
skin={skinTone}
showPreview={false}
backgroundImageFn={backgroundImageFn}
autoFocus
emojiTooltip
/>
<ModifierPicker
active={modifierOpen}
modifier={skinTone}
onOpen={this.handleModifierOpen}
onClose={this.handleModifierClose}
onChange={this.handleModifierChange}
/>
</div>
);
}
}
const perLine = 8
const lines = 2
const DEFAULTS = [
'+1',
'grinning',
'kissing_heart',
'heart_eyes',
'laughing',
'stuck_out_tongue_winking_eye',
'sweat_smile',
'joy',
'yum',
'disappointed',
'thinking_face',
'weary',
'sob',
'sunglasses',
'heart',
'ok_hand',
];
const getFrequentlyUsedEmojis = createSelector([
state => state.getIn(['settings', 'frequentlyUsedEmojis'], ImmutableMap()),
], emojiCounters => {
let emojis = emojiCounters
.keySeq()
.sort((a, b) => emojiCounters.get(a) - emojiCounters.get(b))
.reverse()
.slice(0, perLine * lines)
.toArray();
if (emojis.length < DEFAULTS.length) {
let uniqueDefaults = DEFAULTS.filter(emoji => !emojis.includes(emoji));
emojis = emojis.concat(uniqueDefaults.slice(0, DEFAULTS.length - emojis.length));
}
return emojis;
});
const getCustomEmojis = createSelector([
state => state.get('custom_emojis'),
], emojis => emojis.filter(e => e.get('visible_in_picker')).sort((a, b) => {
const aShort = a.get('shortcode').toLowerCase();
const bShort = b.get('shortcode').toLowerCase();
if (aShort < bShort) {
return -1;
} else if (aShort > bShort ) {
return 1;
}
return 0;
}));
const mapStateToProps = state => ({
custom_emojis: getCustomEmojis(state),
skinTone: state.getIn(['settings', 'skinTone']),
frequentlyUsedEmojis: getFrequentlyUsedEmojis(state),
});
const mapDispatchToProps = (dispatch, { onPickEmoji }) => ({
onSkinTone: skinTone => {
dispatch(changeSetting(['skinTone'], skinTone));
},
onPickEmoji: emoji => {
dispatch(useEmoji(emoji));
if (onPickEmoji) {
onPickEmoji(emoji);
}
},
});
export default
@injectIntl
@connect(mapStateToProps, mapDispatchToProps)
class EmojiPickerPopover extends ImmutablePureComponent {
static propTypes = {
custom_emojis: ImmutablePropTypes.list,
frequentlyUsedEmojis: PropTypes.arrayOf(PropTypes.string),
intl: PropTypes.object.isRequired,
onPickEmoji: PropTypes.func.isRequired,
onSkinTone: PropTypes.func.isRequired,
skinTone: PropTypes.number.isRequired,
};
state = {
active: false,
loading: false,
};
componentWillMount = () => {
this.setState({ active: true });
if (!EmojiPicker) {
this.setState({ loading: true });
EmojiPickerAsync().then(EmojiMart => {
EmojiPicker = EmojiMart.Picker;
Emoji = EmojiMart.Emoji;
this.setState({ loading: false });
}).catch(() => {
this.setState({ loading: false });
});
}
}
onHideDropdown = () => {
this.setState({ active: false });
}
onToggle = (e) => {
if (!this.state.loading && (!e.key || e.key === 'Enter')) {
if (this.state.active) {
this.onHideDropdown();
} else {
this.onShowDropdown(e);
}
}
}
handleKeyDown = e => {
if (e.key === 'Escape') {
this.onHideDropdown();
}
}
render () {
const {
intl,
onPickEmoji,
onSkinTone,
skinTone,
frequentlyUsedEmojis
} = this.props
const { active, loading } = this.state;
return (
<div className='emoji-picker-dropdown' onKeyDown={this.handleKeyDown}>
<EmojiPickerMenu
custom_emojis={this.props.custom_emojis}
loading={loading}
onClose={this.onHideDropdown}
onPick={onPickEmoji}
onSkinTone={onSkinTone}
skinTone={skinTone}
frequentlyUsedEmojis={frequentlyUsedEmojis}
/>
</div>
);
}
}

View File

@@ -5,6 +5,7 @@ import BundleModalError from '../bundle_modal_error'
import PopoverBase from './popover_base'
import ContentWarningPopover from './content_warning_popover'
import DatePickerPopover from './date_picker_popover'
import EmojiPickerPopover from './emoji_picker_popover'
import GroupInfoPopover from './group_info_popover'
import ProfileOptionsPopover from './profile_options_popover'
import SearchPopover from './search_popover'
@@ -19,6 +20,7 @@ const listenerOptions = detectPassiveEvents.hasSupport ? { passive: true } : fal
const POPOVER_COMPONENTS = {
CONTENT_WARNING: () => Promise.resolve({ default: ContentWarningPopover }),
DATE_PICKER: () => Promise.resolve({ default: DatePickerPopover }),
EMOJI_PICKER: () => Promise.resolve({ default: EmojiPickerPopover }),
GROUP_INFO: () => GroupInfoPopover,
PROFILE_OPTIONS: () => Promise.resolve({ default: ProfileOptionsPopover }),
SEARCH: () => Promise.resolve({ default: SearchPopover }),

View File

@@ -1,259 +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 '../../features/ui/util/optional_motion'
import Icon from '../icon'
import ComposeExtraButton from '../../features/compose/components/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 (
<PrivacyDropdownMenu
items={this.options}
value={value}
onClose={this.handleClose}
onChange={this.handleChange}
placement={placement}
/>
)
}
}

View File

@@ -1,9 +1,129 @@
export default class StatusVisibilityPopover extends PureComponent {
render() {
import { injectIntl, defineMessages } from 'react-intl'
import classNames from 'classnames/bind'
import { changeComposeVisibility } from '../../actions/compose'
import { closePopover } from '../../actions/popover'
import PopoverLayout from './popover_layout'
import Icon from '../icon'
import Text from '../text'
const cx = classNames.bind(_s)
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 mapStateToProps = state => ({
value: state.getIn(['compose', 'privacy']),
})
const mapDispatchToProps = dispatch => ({
onChange (value) {
dispatch(changeComposeVisibility(value))
dispatch(closePopover())
},
})
export default
@connect(mapStateToProps, mapDispatchToProps)
@injectIntl
class StatusVisibilityDropdown extends PureComponent {
static propTypes = {
value: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
}
handleChange = value => {
this.props.onChange(value)
}
componentWillMount () {
const { intl } = this.props
this.options = [
{
icon: 'globe',
value: 'public',
title: intl.formatMessage(messages.public_short),
subtitle: intl.formatMessage(messages.public_long)
},
{
icon: 'unlock',
value: 'unlisted',
title: intl.formatMessage(messages.unlisted_short),
subtitle: intl.formatMessage(messages.unlisted_long)
},
{
icon: 'lock',
value: 'private',
title: intl.formatMessage(messages.private_short),
subtitle: intl.formatMessage(messages.private_long)
},
]
}
render () {
const { value } = this.props
return (
<div>
{ /* */ }
</div>
<PopoverLayout className={_s.width240PX}>
<div className={[_s.default].join(' ')}>
{
this.options.map((option, i) => {
const isActive = option.value === value
const isLast = i === this.options.length - 1
const containerClasses = cx({
default: 1,
flexRow: 1,
py10: 1,
cursorPointer: 1,
borderBottom1PX: !isLast,
borderColorSecondary: !isLast,
backgroundSubtle_onHover: !isActive,
backgroundColorBrand: isActive,
})
const iconClasses = cx({
ml10: 1,
mt2: 1,
fillColorWhite: isActive,
})
return (
<div
role='button'
onClick={() => this.handleChange(option.value)}
className={containerClasses}
>
<Icon id={option.icon} height='16px' width='16px' className={iconClasses} />
<div className={[_s.default, _s.px10, _s.pt2].join(' ')}>
<Text size='medium' color={isActive ? 'white' : 'primary'}>
{option.title}
</Text>
<Text size='small' weight='medium' color={isActive ? 'white' : 'secondary'}>
{option.subtitle}
</Text>
</div>
</div>
)
})
}
</div>
</PopoverLayout>
)
}
}
}

View File

@@ -242,6 +242,8 @@ class ProfileHeader extends ImmutablePureComponent {
console.log("buttonOptions:", buttonText, buttonOptions)
// : todo : "follows you", "mutual follow"
return (
<div className={[_s.default, _s.z1, _s.width100PC].join(' ')}>

View File

@@ -103,7 +103,7 @@ export default class SidebarSectionItem extends PureComponent {
buttonRef={buttonRef}
onMouseEnter={() => this.handleOnMouseEnter()}
onMouseLeave={() => this.handleOnMouseLeave()}
className={[_s.default, _s.noUnderline, _s.cursorPointer, _s.width100PC, _s.alignItemsStart, _s.backgroundTransparent].join(' ')}
className={[_s.default, _s.noUnderline, _s.cursorPointer, _s.width100PC, _s.backgroundTransparent].join(' ')}
>
<div className={containerClasses}>
<div className={[_s.default]}>

View File

@@ -0,0 +1,245 @@
import Immutable from 'immutable';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import punycode from 'punycode';
import classnames from 'classnames';
import Icon from './icon';
const IDNA_PREFIX = 'xn--';
const decodeIDNA = domain => {
return domain
.split('.')
.map(part => part.indexOf(IDNA_PREFIX) === 0 ? punycode.decode(part.slice(IDNA_PREFIX.length)) : part)
.join('.');
};
const getHostname = url => {
const parser = document.createElement('a');
parser.href = url;
return parser.hostname;
};
const trim = (text, len) => {
const cut = text.indexOf(' ', len);
if (cut === -1) {
return text;
}
return text.substring(0, cut) + (text.length > len ? '…' : '');
};
const domParser = new DOMParser();
const addAutoPlay = html => {
const document = domParser.parseFromString(html, 'text/html').documentElement;
const iframe = document.querySelector('iframe');
if (iframe) {
if (iframe.src.indexOf('?') !== -1) {
iframe.src += '&';
} else {
iframe.src += '?';
}
iframe.src += 'autoplay=1&auto_play=1';
// DOM parser creates html/body elements around original HTML fragment,
// so we need to get innerHTML out of the body and not the entire document
return document.querySelector('body').innerHTML;
}
return html;
};
export default class Card extends ImmutablePureComponent {
static propTypes = {
card: ImmutablePropTypes.map,
onOpenMedia: PropTypes.func.isRequired,
defaultWidth: PropTypes.number,
cacheWidth: PropTypes.func,
};
static defaultProps = {
};
state = {
width: this.props.defaultWidth || 280,
embedded: false,
};
componentWillReceiveProps (nextProps) {
if (!Immutable.is(this.props.card, nextProps.card)) {
this.setState({ embedded: false });
}
}
handlePhotoClick = () => {
const { card, onOpenMedia } = this.props;
onOpenMedia(
Immutable.fromJS([
{
type: 'image',
url: card.get('embed_url'),
description: card.get('title'),
meta: {
original: {
width: card.get('width'),
height: card.get('height'),
},
},
},
]),
0
);
};
handleEmbedClick = () => {
const { card } = this.props;
if (card.get('type') === 'photo') {
this.handlePhotoClick();
} else {
this.setState({ embedded: true });
}
}
setRef = c => {
if (c) {
if (this.props.cacheWidth) this.props.cacheWidth(c.offsetWidth);
this.setState({ width: c.offsetWidth });
}
}
renderVideo () {
const { card } = this.props
const content = { __html: addAutoPlay(card.get('html')) }
const { width } = this.state
const ratio = card.get('width') / card.get('height')
const height = width / ratio
return (
<div
ref={this.setRef}
className={[_s.default, _s.backgroundColorSecondary3, _s.positionAbsolute, _s.top0, _s.right0, _s.bottom0, _s.left0, _s.statusCardVideo].join(' ')}
dangerouslySetInnerHTML={content}
/>
)
}
render () {
const { card } = this.props
const { width, embedded } = this.state
if (card === null) return null
const maxDescription = 160
const cardImg = card.get('image')
const provider = card.get('provider_name').length === 0 ? decodeIDNA(getHostname(card.get('url'))) : card.get('provider_name')
const horizontal = (card.get('width') > card.get('height') && (card.get('width') + 100 >= width)) || card.get('type') !== 'link' || embedded
const interactive = card.get('type') !== 'link'
const title = interactive ?
(
<a
className={[_s.default, _s.displayFlex, _s.text, _s.noUnderline, _s.overflowWrapBreakWord, _s.colorPrimary, _s.fontSize15PX, _s.fontWeightMedium].join(' ')}
href={card.get('url')}
title={card.get('title')}
rel='noopener'
target='_blank'
>
{card.get('title')}
</a>
)
: (
<span className={[_s.default, _s.displayFlex, _s.text, _s.overflowWrapBreakWord, _s.colorPrimary, _s.fontSize15PX, _s.fontWeightMedium].join(' ')}>
{card.get('title')}
</span>
)
const description = (
<div className={[_s.default, _s.flexNormal, _s.px10, _s.py10, _s.borderColorSecondary, _s.borderLeft1PX].join(' ')}>
{title}
<p className={[_s.default, _s.displayFlex, _s.text, _s.my5, _s.overflowWrapBreakWord, _s.colorSecondary, _s.fontSize13PX, _s.fontWeightNormal].join(' ')}>
{trim(card.get('description') || '', maxDescription)}
</p>
<span className={[_s.default, _s.marginTopAuto, _s.flexRow, _s.alignItemsCenter, _s.colorSecondary, _s.text, _s.displayFlex, _s.textOverflowEllipsis, _s.fontSize13PX].join(' ')}>
<Icon id='link' width='10px' height='10px' className={[_s.fillColorSecondary, _s.mr5].join(' ')} fixedWidth />
{provider}
</span>
</div>
)
let embed = ''
let thumbnail = interactive ?
<img src={cardImg} className={[_s.default, _s.objectFitCover, _s.positionAbsolute, _s.width100PC, _s.height100PC, _s.top0, _s.right0, _s.bottom0, _s.left0].join(' ')} />
:
<img src={cardImg} className={[_s.default, _s.objectFitCover, _s.width330PX, _s.height220PX].join(' ')} />
if (interactive) {
if (embedded) {
embed = this.renderVideo()
}
let iconVariant = 'play'
if (card.get('type') === 'photo') {
iconVariant = 'search-plus'
}
return (
<div className={[_s.default, _s.width100PC, _s.px10].join(' ')}>
<div className={[_s.default, _s.overflowHidden, _s.width100PC, _s.borderColorSecondary2, _s.border1PX, _s.radiusSmall].join(' ')}>
<div className={[_s.default, _s.width100PC].join(' ')}>
<div className={[_s.default, _s.width100PC, _s.paddingTop5625PC].join(' ')}>
{ !!embed && embed}
{ !embed && thumbnail}
{ !embed &&
<div className={[_s.default, _s.positionAbsolute, _s.top0, _s.right0, _s.left0, _s.bottom0, _s.alignItemsCenter, _s.justifyContentCenter].join(' ')}>
<button
className={[_s.default, _s.cursorPointer, _s.backgroundColorOpaque, _s.radiusSmall, _s.py15, _s.px15].join(' ')}
onClick={this.handleEmbedClick}
>
<Icon id={iconVariant} className={[_s.fillColorWhite].join(' ')}/>
</button>
</div>
}
</div>
</div>
{description}
</div>
</div>
)
} else if (cardImg) {
embed = (
<div className={[_s.default].join(' ')}>
{thumbnail}
</div>
)
} else {
embed = (
<div className={[_s.default, _s.py15, _s.px15, _s.width72PX, _s.alignItemsCenter, _s.justifyContentCenter].join(' ')}>
<Icon id='file-text' width='22px' height='22px' className={_s.fillColorSecondary} />
</div>
)
}
return (
<div className={[_s.default, _s.width100PC, _s.px10].join(' ')}>
<a
href={card.get('url')}
className={[_s.default, _s.cursorPointer, _s.flexRow, _s.overflowHidden, _s.noUnderline, _s.width100PC, _s.backgroundSubtle_onHover, _s.borderColorSecondary2, _s.border1PX, _s.radiusSmall].join(' ')}
rel='noopener'
ref={this.setRef}
>
{embed}
{description}
</a>
</div>
)
}
}

View File

@@ -285,12 +285,18 @@ class StatusContent extends ImmutablePureComponent {
/>
{
this.state.collapsed &&
<button
className={[_s.default, _s.displayFlex, _s.cursorPointer, _s.py2, _s.text, _s.colorPrimary, _s.fontWeightBold, _s.fontSize15PX].join(' ')}
<Button
text
underlineOnHover
color='primary'
backgroundColor='none'
className={[_s.py2].join(' ')}
onClick={this.handleReadMore}
>
{intl.formatMessage(messages.readMore)}
</button>
<Text size='medium' color='inherit' weight='bold'>
{intl.formatMessage(messages.readMore)}
</Text>
</Button>
}
</div>
)

View File

@@ -15,6 +15,7 @@ const cx = classNames.bind(_s)
export default class TrendingItem extends ImmutablePureComponent {
static propTypes = {
index: PropTypes.number,
trend: ImmutablePropTypes.map.isRequired,
}
@@ -31,7 +32,7 @@ export default class TrendingItem extends ImmutablePureComponent {
}
render() {
const { trend } = this.props
const { trend, index } = this.props
const { hovering } = this.state
const subtitleClasses = cx({
@@ -44,7 +45,7 @@ export default class TrendingItem extends ImmutablePureComponent {
underline: hovering,
})
return null;
// return null;
// : todo :
@@ -56,7 +57,7 @@ export default class TrendingItem extends ImmutablePureComponent {
onMouseLeave={() => this.handleOnMouseLeave()}
>
<div className={[_s.default, _s.flexRow, _s.mt5].join(' ')}>
<Text size='small' color='secondary'>1</Text>
<Text size='small' color='secondary'>{index}</Text>
<DotTextSeperator />
<Text size='small' color='secondary' className={_s.ml5}>Politics</Text>
</div>

View File

@@ -58,7 +58,7 @@ export default class TrendingItemCard extends ImmutablePureComponent {
The best flower subscription services: BloomsyBox, Bouqs...
</Text>
</div>
<Image width='92px' height='92px' />
<Image width='92px' height='96px' />
</div>
)
}

View File

@@ -0,0 +1,650 @@
import { defineMessages, injectIntl } from 'react-intl'
import { is } from 'immutable'
import { throttle } from 'lodash'
import classNames from 'classnames/bind'
import { decode } from 'blurhash'
import { isFullscreen, requestFullscreen, exitFullscreen } from '../../utils/fullscreen'
import { isPanoramic, isPortrait, minimumAspectRatio, maximumAspectRatio } from '../../utils/media_aspect_ratio'
import { displayMedia } from '../../initial_state'
import Button from '../../components/button'
import Text from '../../components/text'
const cx = classNames.bind(_s)
const messages = defineMessages({
play: { id: 'video.play', defaultMessage: 'Play' },
pause: { id: 'video.pause', defaultMessage: 'Pause' },
mute: { id: 'video.mute', defaultMessage: 'Mute sound' },
unmute: { id: 'video.unmute', defaultMessage: 'Unmute sound' },
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)
if (hours < 10) hours = '0' + hours
if (minutes < 10) minutes = '0' + minutes
if (seconds < 10) seconds = '0' + seconds
return (hours === '00' ? '' : `${hours}:`) + `${minutes}:${seconds}`
}
export const findElementPosition = el => {
let box
if (el.getBoundingClientRect && el.parentNode) {
box = el.getBoundingClientRect()
}
if (!box) {
return {
left: 0,
top: 0,
}
}
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 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
let pageY = event.pageY
let pageX = event.pageX
if (event.changedTouches) {
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))
return position
}
export default
@injectIntl
class Video extends PureComponent {
static propTypes = {
preview: PropTypes.string,
src: PropTypes.string.isRequired,
alt: PropTypes.string,
width: PropTypes.number,
height: PropTypes.number,
sensitive: PropTypes.bool,
startTime: PropTypes.number,
detailed: PropTypes.bool,
inline: PropTypes.bool,
cacheWidth: PropTypes.func,
visible: PropTypes.bool,
onToggleVisibility: PropTypes.func,
intl: PropTypes.object.isRequired,
blurhash: PropTypes.string,
aspectRatio: PropTypes.number,
}
state = {
currentTime: 0,
duration: 0,
volume: 0.5,
paused: true,
dragging: false,
containerWidth: this.props.width,
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
volHandleOffset = v => {
const offset = v * this.volHeight + this.volOffset
return (offset > 110) ? 110 : offset
}
setPlayerRef = c => {
this.player = c
if (c) {
if (this.props.cacheWidth) this.props.cacheWidth(this.player.offsetWidth)
this.setState({
containerWidth: c.offsetWidth,
})
}
}
setVideoRef = c => {
this.video = c
if (this.video) {
const { volume, muted } = this.video
this.setState({
volume,
muted,
})
}
}
setSeekRef = c => {
this.seek = c
}
setVolumeRef = c => {
this.volume = c
}
setCanvasRef = c => {
this.canvas = c
}
handleClickRoot = e => e.stopPropagation()
handlePlay = () => {
this.setState({ paused: false })
}
handlePause = () => {
this.setState({ paused: true })
}
handleTimeUpdate = () => {
const { currentTime, duration } = this.video
this.setState({
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)
this.handleMouseVolSlide(e)
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)
}
handleMouseVolSlide = throttle(e => {
const rect = this.volume.getBoundingClientRect()
const y = 1 - ((e.clientY - rect.top) / this.volHeight)
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 })
}
}, 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)
this.setState({ dragging: true })
this.video.pause()
this.handleMouseMove(e)
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)
this.setState({ dragging: false })
this.video.play()
}
handleMouseMove = throttle(e => {
const { x } = getPointerPosition(this.seek, e)
const currentTime = Math.floor(this.video.duration * x)
if (!isNaN(currentTime)) {
this.video.currentTime = currentTime
this.setState({ currentTime })
}
}, 60)
togglePlay = () => {
if (this.state.paused) {
this.video.play()
} else {
this.video.pause()
}
}
toggleFullscreen = () => {
if (isFullscreen()) {
exitFullscreen()
} else {
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)
if (this.props.blurhash) {
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)
}
componentWillReceiveProps(nextProps) {
if (!is(nextProps.visible, this.props.visible) && nextProps.visible !== undefined) {
this.setState({ revealed: nextProps.visible })
}
}
componentDidUpdate(prevProps, prevState) {
if (prevState.revealed && !this.state.revealed && this.video) {
this.video.pause()
}
if (prevProps.blurhash !== this.props.blurhash && this.props.blurhash) {
this._decode()
}
}
_decode() {
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)
ctx.putImageData(imageData, 0, 0)
}
}
handleFullscreenChange = () => {
this.setState({ fullscreen: isFullscreen() })
}
handleMouseEnter = () => {
this.setState({ hovered: true })
}
handleMouseLeave = () => {
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 })
}
toggleReveal = () => {
if (this.props.onToggleVisibility) {
this.props.onToggleVisibility()
} else {
this.setState({ revealed: !this.state.revealed })
}
}
handleLoadedData = () => {
if (this.props.startTime) {
this.video.currentTime = this.props.startTime
this.video.play()
}
}
handleProgress = () => {
const { buffered, duration } = this.video
if (!buffered) return
if (buffered.length > 0) {
this.setState({
buffer: buffered.end(0) / duration * 100,
})
}
}
handleVolumeChange = () => {
const { volume, muted } = this.video
this.setState({
volume,
muted,
})
}
render() {
const {
preview,
src,
inline,
startTime,
intl,
alt,
detailed,
sensitive,
aspectRatio
} = this.props
const {
containerWidth,
currentTime,
duration,
volume,
buffer,
dragging,
paused,
fullscreen,
hovered,
muted,
revealed,
hoveringVolumeButton,
hoveringVolumeControl
} = this.state
const progress = (currentTime / duration) * 100
const volumeHeight = (muted) ? 0 : volume * this.volHeight
const volumeHandleLoc = (muted) ? this.volHandleOffset(0) : this.volHandleOffset(volume)
const playerStyle = {}
let { width, height } = this.props
if (inline && containerWidth) {
width = containerWidth
const minSize = containerWidth / (16 / 9)
if (isPanoramic(aspectRatio)) {
height = Math.max(Math.floor(containerWidth / maximumAspectRatio), minSize)
} else if (isPortrait(aspectRatio)) {
height = Math.max(Math.floor(containerWidth / minimumAspectRatio), minSize)
} else {
height = Math.floor(containerWidth / aspectRatio)
}
playerStyle.height = height
}
let preload
if (startTime || fullscreen || dragging) {
preload = 'auto'
} else if (detailed) {
preload = 'metadata'
} else {
preload = 'none'
}
// className={classNames('video-player', {
// inactive: !revealed,
// detailed,
// inline: inline && !fullscreen,
// fullscreen
// })}
// : todo spoiler :
const seekHandleClasses = cx({
default: 1,
positionAbsolute: 1,
circle: 1,
px10: 1,
py10: 1,
backgroundColorBrand: 1,
marginLeftNeg5PX: 1,
z3: 1,
boxShadow1: 1,
opacity0: !dragging,
opacity1: dragging || hovered,
})
const progressClasses = cx({
default: 1,
radiusSmall: 1,
mt10: 1,
positionAbsolute: 1,
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'
className={[_s.default].join(' ')}
style={playerStyle}
ref={this.setPlayerRef}
onMouseEnter={this.handleMouseEnter}
onMouseLeave={this.handleMouseLeave}
onClick={this.handleClickRoot}
tabIndex={0}
>
{
!revealed &&
<canvas
width={32}
height={32}
ref={this.setCanvasRef}
className={[_s.default, _s.positionAbsolute, _s.height100PC, _s.width100PC, _s.top0, _s.left0].join(' ')}
/>
}
{
revealed &&
<video
className={[_s.default, _s.height100PC, _s.width100PC, _s.outlineNone].join(' ')}
playsInline
ref={this.setVideoRef}
src={src}
poster={preview}
preload={preload}
loop
role='button'
tabIndex='0'
aria-label={alt}
title={alt}
width={width}
height={height}
volume={volume}
onClick={this.togglePlay}
onPlay={this.handlePlay}
onPause={this.handlePause}
onTimeUpdate={this.handleTimeUpdate}
onLoadedData={this.handleLoadedData}
onProgress={this.handleProgress}
onVolumeChange={this.handleVolumeChange}
/>
}
{ /* <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'>
{intl.formatMessage(sensitive ? messages.sensitive : messages.hidden)}
</span>
</button>
</div> */ }
<div
className={volumeControlClasses}
onMouseDown={this.handleVolumeMouseDown}
onMouseEnter={this.handleMouseEnterVolumeControl}
onMouseLeave={this.handleMouseLeaveVolumeControl}
ref={this.setVolumeRef}
>
<div
className={[_s.default, _s.radiusSmall, _s.my10, _s.positionAbsolute, _s.width4PX, _s.ml10, _s.backgroundColorPrimaryOpaque].join(' ')}
style={{
height: '102px',
}}
/>
<div
className={[_s.default, _s.radiusSmall, _s.my10, _s.bottom0, _s.positionAbsolute, _s.width4PX, _s.ml10, _s.backgroundColorPrimary].join(' ')}
style={{
height: `${volumeHeight}px`
}}
/>
<span
className={[_s.default, _s.cursorPointer, _s.positionAbsolute, _s.circle, _s.px5, _s.boxShadow1, _s.marginBottomNeg5PX, _s.py5, _s.backgroundColorPrimary, _s.z3].join(' ')}
tabIndex='0'
style={{
marginLeft: '7px',
bottom: `${volumeHandleLoc}px`,
}}
/>
</div>
<div className={[_s.default, _s.z2, _s.px15, _s.videoPlayerControlsBackground, _s.positionAbsolute, _s.bottom0, _s.right0, _s.left0].join(' ')}>
<div
className={[_s.default, _s.cursorPointer, _s.height22PX, _s.videoPlayerSeek].join(' ')}
onMouseDown={this.handleMouseDown}
ref={this.setSeekRef}
>
<div className={[progressClasses, _s.backgroundPanel, _s.width100PC].join(' ')} />
<div className={[progressClasses, _s.backgroundSubtle].join(' ')} style={{ width: `${buffer}%` }} />
<div className={[progressClasses, _s.backgroundColorBrand].join(' ')} style={{ width: `${progress}%` }} />
<span
className={seekHandleClasses}
tabIndex='0'
style={{
left: `${progress}%`
}}
/>
</div>
<div className={[_s.default, _s.flexRow, _s.alignItemsCenter, _s.pb5, _s.noSelect].join(' ')}>
<Button
narrow
backgroundColor='none'
aria-label={intl.formatMessage(paused ? messages.play : messages.pause)}
onClick={this.togglePlay}
icon={paused ? 'play' : 'pause'}
iconWidth='16px'
iconHeight='16px'
iconClassName={_s.fillColorWhite}
className={_s.pl0}
/>
<div className={[_s.default, _s.marginLeftAuto, _s.flexRow, _s.alignItemsCenter].join(' ')}>
<Text color='white' size='small'>
{formatTime(currentTime)}
&nbsp;/&nbsp;
{formatTime(duration)}
</Text>
<Button
narrow
backgroundColor='none'
type='button'
aria-label={intl.formatMessage(muted ? messages.unmute : messages.mute)}
onClick={this.toggleMute}
icon={muted ? 'audio-mute' : 'audio'}
iconWidth='24px'
iconHeight='24px'
iconClassName={_s.fillColorWhite}
className={[_s.px10, _s.ml10].join(' ')}
onMouseEnter={this.handleMouseEnterAudio}
onMouseLeave={this.handleMouseLeaveAudio}
/>
<Button
narrow
backgroundColor='none'
aria-label={intl.formatMessage(fullscreen ? messages.exit_fullscreen : messages.fullscreen)}
onClick={this.toggleFullscreen}
icon={fullscreen ? 'minimize-fullscreen' : 'fullscreen'}
iconWidth='20px'
iconHeight='20px'
iconClassName={_s.fillColorWhite}
className={[_s.px10, _s.pr0].join(' ')}
/>
</div>
</div>
</div>
</div>
)
}
}