diff options
Diffstat (limited to 'app/assets/javascripts/components/components')
24 files changed, 0 insertions, 1857 deletions
diff --git a/app/assets/javascripts/components/components/account.jsx b/app/assets/javascripts/components/components/account.jsx deleted file mode 100644 index 81439bd25..000000000 --- a/app/assets/javascripts/components/components/account.jsx +++ /dev/null @@ -1,91 +0,0 @@ -import ImmutablePropTypes from 'react-immutable-proptypes'; -import PropTypes from 'prop-types'; -import Avatar from './avatar'; -import DisplayName from './display_name'; -import Permalink from './permalink'; -import IconButton from './icon_button'; -import { defineMessages, injectIntl } from 'react-intl'; - -const messages = defineMessages({ - follow: { id: 'account.follow', defaultMessage: 'Follow' }, - unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' }, - requested: { id: 'account.requested', defaultMessage: 'Awaiting approval' }, - unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' }, - unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' } -}); - -class Account extends React.PureComponent { - - constructor (props, context) { - super(props, context); - this.handleFollow = this.handleFollow.bind(this); - this.handleBlock = this.handleBlock.bind(this); - this.handleMute = this.handleMute.bind(this); - } - - handleFollow () { - this.props.onFollow(this.props.account); - } - - handleBlock () { - this.props.onBlock(this.props.account); - } - - handleMute () { - this.props.onMute(this.props.account); - } - - render () { - const { account, me, intl } = this.props; - - if (!account) { - return <div />; - } - - let buttons; - - if (account.get('id') !== me && account.get('relationship', null) !== null) { - const following = account.getIn(['relationship', 'following']); - const requested = account.getIn(['relationship', 'requested']); - const blocking = account.getIn(['relationship', 'blocking']); - const muting = account.getIn(['relationship', 'muting']); - - if (requested) { - buttons = <IconButton disabled={true} icon='hourglass' title={intl.formatMessage(messages.requested)} /> - } else if (blocking) { - buttons = <IconButton active={true} icon='unlock-alt' title={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.handleBlock} />; - } else if (muting) { - buttons = <IconButton active={true} icon='volume-up' title={intl.formatMessage(messages.unmute, { name: account.get('username') })} onClick={this.handleMute} />; - } else { - buttons = <IconButton icon={following ? 'user-times' : 'user-plus'} title={intl.formatMessage(following ? messages.unfollow : messages.follow)} onClick={this.handleFollow} active={following} />; - } - } - - return ( - <div className='account'> - <div className='account__wrapper'> - <Permalink key={account.get('id')} className='account__display-name' href={account.get('url')} to={`/accounts/${account.get('id')}`}> - <div className='account__avatar-wrapper'><Avatar src={account.get('avatar')} staticSrc={account.get('avatar_static')} size={36} /></div> - <DisplayName account={account} /> - </Permalink> - - <div className='account__relationship'> - {buttons} - </div> - </div> - </div> - ); - } - -} - -Account.propTypes = { - account: ImmutablePropTypes.map.isRequired, - me: PropTypes.number.isRequired, - onFollow: PropTypes.func.isRequired, - onBlock: PropTypes.func.isRequired, - onMute: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired -} - -export default injectIntl(Account); diff --git a/app/assets/javascripts/components/components/attachment_list.jsx b/app/assets/javascripts/components/components/attachment_list.jsx deleted file mode 100644 index 54841fa51..000000000 --- a/app/assets/javascripts/components/components/attachment_list.jsx +++ /dev/null @@ -1,32 +0,0 @@ -import ImmutablePropTypes from 'react-immutable-proptypes'; - -const filename = url => url.split('/').pop().split('#')[0].split('?')[0]; - -class AttachmentList extends React.PureComponent { - - render () { - const { media } = this.props; - - return ( - <div className='attachment-list'> - <div className='attachment-list__icon'> - <i className='fa fa-link' /> - </div> - - <ul className='attachment-list__list'> - {media.map(attachment => - <li key={attachment.get('id')}> - <a href={attachment.get('remote_url')} target='_blank' rel='noopener'>{filename(attachment.get('remote_url'))}</a> - </li> - )} - </ul> - </div> - ); - } -} - -AttachmentList.propTypes = { - media: ImmutablePropTypes.list.isRequired -}; - -export default AttachmentList; diff --git a/app/assets/javascripts/components/components/autosuggest_textarea.jsx b/app/assets/javascripts/components/components/autosuggest_textarea.jsx deleted file mode 100644 index 9a4d5b7e3..000000000 --- a/app/assets/javascripts/components/components/autosuggest_textarea.jsx +++ /dev/null @@ -1,211 +0,0 @@ -import AutosuggestAccountContainer from '../features/compose/containers/autosuggest_account_container'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import PropTypes from 'prop-types'; -import { isRtl } from '../rtl'; - -const textAtCursorMatchesToken = (str, caretPosition) => { - let word; - - let left = str.slice(0, caretPosition).search(/\S+$/); - let right = str.slice(caretPosition).search(/\s/); - - if (right < 0) { - word = str.slice(left); - } else { - word = str.slice(left, right + caretPosition); - } - - if (!word || word.trim().length < 2 || word[0] !== '@') { - return [null, null]; - } - - word = word.trim().toLowerCase().slice(1); - - if (word.length > 0) { - return [left + 1, word]; - } else { - return [null, null]; - } -}; - -class AutosuggestTextarea extends React.Component { - - constructor (props, context) { - super(props, context); - this.state = { - suggestionsHidden: false, - selectedSuggestion: 0, - lastToken: null, - tokenStart: 0 - }; - this.onChange = this.onChange.bind(this); - this.onKeyDown = this.onKeyDown.bind(this); - this.onBlur = this.onBlur.bind(this); - this.onSuggestionClick = this.onSuggestionClick.bind(this); - this.setTextarea = this.setTextarea.bind(this); - this.onPaste = this.onPaste.bind(this); - } - - onChange (e) { - const [ tokenStart, token ] = textAtCursorMatchesToken(e.target.value, e.target.selectionStart); - - if (token !== null && this.state.lastToken !== token) { - this.setState({ lastToken: token, selectedSuggestion: 0, tokenStart }); - this.props.onSuggestionsFetchRequested(token); - } else if (token === null) { - this.setState({ lastToken: null }); - this.props.onSuggestionsClearRequested(); - } - - // auto-resize textarea - e.target.style.height = `${e.target.scrollHeight}px`; - - this.props.onChange(e); - } - - onKeyDown (e) { - const { suggestions, disabled } = this.props; - const { selectedSuggestion, suggestionsHidden } = this.state; - - if (disabled) { - e.preventDefault(); - return; - } - - switch(e.key) { - case 'Escape': - if (!suggestionsHidden) { - e.preventDefault(); - this.setState({ suggestionsHidden: true }); - } - - break; - case 'ArrowDown': - if (suggestions.size > 0 && !suggestionsHidden) { - e.preventDefault(); - this.setState({ selectedSuggestion: Math.min(selectedSuggestion + 1, suggestions.size - 1) }); - } - - break; - case 'ArrowUp': - if (suggestions.size > 0 && !suggestionsHidden) { - e.preventDefault(); - this.setState({ selectedSuggestion: Math.max(selectedSuggestion - 1, 0) }); - } - - break; - case 'Enter': - case 'Tab': - // Select suggestion - if (this.state.lastToken !== null && suggestions.size > 0 && !suggestionsHidden) { - e.preventDefault(); - e.stopPropagation(); - this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestions.get(selectedSuggestion)); - } - - break; - } - - if (e.defaultPrevented || !this.props.onKeyDown) { - return; - } - - this.props.onKeyDown(e); - } - - onBlur () { - // If we hide the suggestions immediately, then this will prevent the - // onClick for the suggestions themselves from firing. - // Setting a short window for that to take place before hiding the - // suggestions ensures that can't happen. - setTimeout(() => { - this.setState({ suggestionsHidden: true }); - }, 100); - } - - onSuggestionClick (suggestion, e) { - e.preventDefault(); - this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestion); - this.textarea.focus(); - } - - componentWillReceiveProps (nextProps) { - if (nextProps.suggestions !== this.props.suggestions && nextProps.suggestions.size > 0 && this.state.suggestionsHidden) { - this.setState({ suggestionsHidden: false }); - } - } - - setTextarea (c) { - this.textarea = c; - } - - onPaste (e) { - if (e.clipboardData && e.clipboardData.files.length === 1) { - this.props.onPaste(e.clipboardData.files) - e.preventDefault(); - } - } - - reset () { - this.textarea.style.height = 'auto'; - } - - render () { - const { value, suggestions, disabled, placeholder, onKeyUp } = this.props; - const { suggestionsHidden, selectedSuggestion } = this.state; - const style = { direction: 'ltr' }; - - if (isRtl(value)) { - style.direction = 'rtl'; - } - - return ( - <div className='autosuggest-textarea'> - <textarea - ref={this.setTextarea} - className='autosuggest-textarea__textarea' - disabled={disabled} - placeholder={placeholder} - autoFocus={true} - value={value} - onChange={this.onChange} - onKeyDown={this.onKeyDown} - onKeyUp={onKeyUp} - onBlur={this.onBlur} - onPaste={this.onPaste} - style={style} - /> - - <div style={{ display: (suggestions.size > 0 && !suggestionsHidden) ? 'block' : 'none' }} className='autosuggest-textarea__suggestions'> - {suggestions.map((suggestion, i) => ( - <div - role='button' - tabIndex='0' - key={suggestion} - className={`autosuggest-textarea__suggestions__item ${i === selectedSuggestion ? 'selected' : ''}`} - onClick={this.onSuggestionClick.bind(this, suggestion)}> - <AutosuggestAccountContainer id={suggestion} /> - </div> - ))} - </div> - </div> - ); - } - -}; - -AutosuggestTextarea.propTypes = { - value: PropTypes.string, - suggestions: ImmutablePropTypes.list, - disabled: PropTypes.bool, - placeholder: PropTypes.string, - onSuggestionSelected: PropTypes.func.isRequired, - onSuggestionsClearRequested: PropTypes.func.isRequired, - onSuggestionsFetchRequested: PropTypes.func.isRequired, - onChange: PropTypes.func.isRequired, - onKeyUp: PropTypes.func, - onKeyDown: PropTypes.func, - onPaste: PropTypes.func.isRequired, -}; - -export default AutosuggestTextarea; diff --git a/app/assets/javascripts/components/components/avatar.jsx b/app/assets/javascripts/components/components/avatar.jsx deleted file mode 100644 index d1a00ac39..000000000 --- a/app/assets/javascripts/components/components/avatar.jsx +++ /dev/null @@ -1,63 +0,0 @@ -import PropTypes from 'prop-types'; - -class Avatar extends React.PureComponent { - - constructor (props, context) { - super(props, context); - this.state = { - hovering: false - }; - this.handleMouseEnter = this.handleMouseEnter.bind(this); - this.handleMouseLeave = this.handleMouseLeave.bind(this); - } - - handleMouseEnter () { - this.setState({ hovering: true }); - } - - handleMouseLeave () { - this.setState({ hovering: false }); - } - - render () { - const { src, size, staticSrc, animate } = this.props; - const { hovering } = this.state; - - const style = { - ...this.props.style, - width: `${size}px`, - height: `${size}px`, - backgroundSize: `${size}px ${size}px` - }; - - if (hovering || animate) { - style.backgroundImage = `url(${src})`; - } else { - style.backgroundImage = `url(${staticSrc})`; - } - - return ( - <div - className='account__avatar' - onMouseEnter={this.handleMouseEnter} - onMouseLeave={this.handleMouseLeave} - style={style} - /> - ); - } - -} - -Avatar.propTypes = { - src: PropTypes.string.isRequired, - staticSrc: PropTypes.string, - size: PropTypes.number.isRequired, - style: PropTypes.object, - animate: PropTypes.bool -}; - -Avatar.defaultProps = { - animate: false -}; - -export default Avatar; diff --git a/app/assets/javascripts/components/components/button.jsx b/app/assets/javascripts/components/components/button.jsx deleted file mode 100644 index 00d80b1fd..000000000 --- a/app/assets/javascripts/components/components/button.jsx +++ /dev/null @@ -1,49 +0,0 @@ -import PropTypes from 'prop-types'; - -class Button extends React.PureComponent { - - constructor (props, context) { - super(props, context); - this.handleClick = this.handleClick.bind(this); - } - - handleClick (e) { - if (!this.props.disabled) { - this.props.onClick(); - } - } - - render () { - const style = { - display: this.props.block ? 'block' : 'inline-block', - width: this.props.block ? '100%' : 'auto', - padding: `0 ${this.props.size / 2.25}px`, - height: `${this.props.size}px`, - lineHeight: `${this.props.size}px` - }; - - return ( - <button className={`button ${this.props.secondary ? 'button-secondary' : ''}`} disabled={this.props.disabled} onClick={this.handleClick} style={{ ...style, ...this.props.style }}> - {this.props.text || this.props.children} - </button> - ); - } - -} - -Button.propTypes = { - text: PropTypes.node, - onClick: PropTypes.func, - disabled: PropTypes.bool, - block: PropTypes.bool, - secondary: PropTypes.bool, - size: PropTypes.number, - style: PropTypes.object, - children: PropTypes.node -}; - -Button.defaultProps = { - size: 36 -}; - -export default Button; diff --git a/app/assets/javascripts/components/components/collapsable.jsx b/app/assets/javascripts/components/components/collapsable.jsx deleted file mode 100644 index 0810768f0..000000000 --- a/app/assets/javascripts/components/components/collapsable.jsx +++ /dev/null @@ -1,20 +0,0 @@ -import { Motion, spring } from 'react-motion'; -import PropTypes from 'prop-types'; - -const Collapsable = ({ fullHeight, isVisible, children }) => ( - <Motion defaultStyle={{ opacity: !isVisible ? 0 : 100, height: isVisible ? fullHeight : 0 }} style={{ opacity: spring(!isVisible ? 0 : 100), height: spring(!isVisible ? 0 : fullHeight) }}> - {({ opacity, height }) => - <div style={{ height: `${height}px`, overflow: 'hidden', opacity: opacity / 100, display: Math.floor(opacity) === 0 ? 'none' : 'block' }}> - {children} - </div> - } - </Motion> -); - -Collapsable.propTypes = { - fullHeight: PropTypes.number.isRequired, - isVisible: PropTypes.bool.isRequired, - children: PropTypes.node.isRequired -}; - -export default Collapsable; diff --git a/app/assets/javascripts/components/components/column_back_button.jsx b/app/assets/javascripts/components/components/column_back_button.jsx deleted file mode 100644 index 70110f0aa..000000000 --- a/app/assets/javascripts/components/components/column_back_button.jsx +++ /dev/null @@ -1,31 +0,0 @@ -import { FormattedMessage } from 'react-intl'; -import PropTypes from 'prop-types'; - -class ColumnBackButton extends React.PureComponent { - - constructor (props, context) { - super(props, context); - this.handleClick = this.handleClick.bind(this); - } - - handleClick () { - if (window.history && window.history.length === 1) this.context.router.push("/"); - else this.context.router.goBack(); - } - - render () { - return ( - <div role='button' tabIndex='0' onClick={this.handleClick} className='column-back-button'> - <i className='fa fa-fw fa-chevron-left column-back-button__icon'/> - <FormattedMessage id='column_back_button.label' defaultMessage='Back' /> - </div> - ); - } - -}; - -ColumnBackButton.contextTypes = { - router: PropTypes.object -}; - -export default ColumnBackButton; diff --git a/app/assets/javascripts/components/components/column_back_button_slim.jsx b/app/assets/javascripts/components/components/column_back_button_slim.jsx deleted file mode 100644 index 719690097..000000000 --- a/app/assets/javascripts/components/components/column_back_button_slim.jsx +++ /dev/null @@ -1,31 +0,0 @@ -import { FormattedMessage } from 'react-intl'; -import PropTypes from 'prop-types'; - -class ColumnBackButtonSlim extends React.PureComponent { - - constructor (props, context) { - super(props, context); - this.handleClick = this.handleClick.bind(this); - } - - handleClick () { - this.context.router.push('/'); - } - - render () { - return ( - <div className='column-back-button--slim'> - <div role='button' tabIndex='0' onClick={this.handleClick} className='column-back-button column-back-button--slim-button'> - <i className='fa fa-fw fa-chevron-left column-back-button__icon' /> - <FormattedMessage id='column_back_button.label' defaultMessage='Back' /> - </div> - </div> - ); - } -} - -ColumnBackButtonSlim.contextTypes = { - router: PropTypes.object -}; - -export default ColumnBackButtonSlim; diff --git a/app/assets/javascripts/components/components/column_collapsable.jsx b/app/assets/javascripts/components/components/column_collapsable.jsx deleted file mode 100644 index 2cb440862..000000000 --- a/app/assets/javascripts/components/components/column_collapsable.jsx +++ /dev/null @@ -1,56 +0,0 @@ -import { Motion, spring } from 'react-motion'; -import PropTypes from 'prop-types'; - -class ColumnCollapsable extends React.PureComponent { - - constructor (props, context) { - super(props, context); - this.state = { - collapsed: true - }; - - this.handleToggleCollapsed = this.handleToggleCollapsed.bind(this); - } - - handleToggleCollapsed () { - const currentState = this.state.collapsed; - - this.setState({ collapsed: !currentState }); - - if (!currentState && this.props.onCollapse) { - this.props.onCollapse(); - } - } - - render () { - const { icon, title, fullHeight, children } = this.props; - const { collapsed } = this.state; - const collapsedClassName = collapsed ? 'collapsable-collapsed' : 'collapsable'; - - return ( - <div className='column-collapsable'> - <div role='button' tabIndex='0' title={`${title}`} className={`column-icon ${collapsedClassName}`} onClick={this.handleToggleCollapsed}> - <i className={`fa fa-${icon}`} /> - </div> - - <Motion defaultStyle={{ opacity: 0, height: 0 }} style={{ opacity: spring(collapsed ? 0 : 100), height: spring(collapsed ? 0 : fullHeight, collapsed ? undefined : { stiffness: 150, damping: 9 }) }}> - {({ opacity, height }) => - <div style={{ overflow: height === fullHeight ? 'auto' : 'hidden', height: `${height}px`, opacity: opacity / 100, maxHeight: '70vh' }}> - {children} - </div> - } - </Motion> - </div> - ); - } -} - -ColumnCollapsable.propTypes = { - icon: PropTypes.string.isRequired, - title: PropTypes.string, - fullHeight: PropTypes.number.isRequired, - children: PropTypes.node, - onCollapse: PropTypes.func -}; - -export default ColumnCollapsable; diff --git a/app/assets/javascripts/components/components/display_name.jsx b/app/assets/javascripts/components/components/display_name.jsx deleted file mode 100644 index d7257e092..000000000 --- a/app/assets/javascripts/components/components/display_name.jsx +++ /dev/null @@ -1,24 +0,0 @@ -import ImmutablePropTypes from 'react-immutable-proptypes'; -import escapeTextContentForBrowser from 'escape-html'; -import emojify from '../emoji'; - -class DisplayName extends React.PureComponent { - - render () { - const displayName = this.props.account.get('display_name').length === 0 ? this.props.account.get('username') : this.props.account.get('display_name'); - const displayNameHTML = { __html: emojify(escapeTextContentForBrowser(displayName)) }; - - return ( - <span className='display-name'> - <strong className='display-name__html' dangerouslySetInnerHTML={displayNameHTML} /> <span className='display-name__account'>@{this.props.account.get('acct')}</span> - </span> - ); - } - -}; - -DisplayName.propTypes = { - account: ImmutablePropTypes.map.isRequired -} - -export default DisplayName; diff --git a/app/assets/javascripts/components/components/dropdown_menu.jsx b/app/assets/javascripts/components/components/dropdown_menu.jsx deleted file mode 100644 index f5ee27a11..000000000 --- a/app/assets/javascripts/components/components/dropdown_menu.jsx +++ /dev/null @@ -1,78 +0,0 @@ -import Dropdown, { DropdownTrigger, DropdownContent } from 'react-simple-dropdown'; -import PropTypes from 'prop-types'; - -class DropdownMenu extends React.PureComponent { - - constructor (props, context) { - super(props, context); - this.state = { - direction: 'left' - }; - this.setRef = this.setRef.bind(this); - this.renderItem = this.renderItem.bind(this); - } - - setRef (c) { - this.dropdown = c; - } - - handleClick (i, e) { - const { action } = this.props.items[i]; - - if (typeof action === 'function') { - e.preventDefault(); - action(); - this.dropdown.hide(); - } - } - - renderItem (item, i) { - if (item === null) { - return <li key={ 'sep' + i } className='dropdown__sep' />; - } - - const { text, action, href = '#' } = item; - - return ( - <li className='dropdown__content-list-item' key={ text + i }> - <a href={href} target='_blank' rel='noopener' onClick={this.handleClick.bind(this, i)} className='dropdown__content-list-link'> - {text} - </a> - </li> - ); - } - - render () { - const { icon, items, size, direction, ariaLabel } = this.props; - const directionClass = (direction === "left") ? "dropdown__left" : "dropdown__right"; - - return ( - <Dropdown ref={this.setRef}> - <DropdownTrigger className='icon-button' style={{ fontSize: `${size}px`, width: `${size}px`, lineHeight: `${size}px` }} aria-label={ariaLabel}> - <i className={ `fa fa-fw fa-${icon} dropdown__icon` } aria-hidden={true} /> - </DropdownTrigger> - - <DropdownContent className={directionClass}> - <ul className='dropdown__content-list'> - {items.map(this.renderItem)} - </ul> - </DropdownContent> - </Dropdown> - ); - } - -} - -DropdownMenu.propTypes = { - icon: PropTypes.string.isRequired, - items: PropTypes.array.isRequired, - size: PropTypes.number.isRequired, - direction: PropTypes.string, - ariaLabel: PropTypes.string -}; - -DropdownMenu.defaultProps = { - ariaLabel: "Menu" -}; - -export default DropdownMenu; diff --git a/app/assets/javascripts/components/components/extended_video_player.jsx b/app/assets/javascripts/components/components/extended_video_player.jsx deleted file mode 100644 index bbbe09da8..000000000 --- a/app/assets/javascripts/components/components/extended_video_player.jsx +++ /dev/null @@ -1,53 +0,0 @@ -import PropTypes from 'prop-types'; - -class ExtendedVideoPlayer extends React.PureComponent { - - constructor (props, context) { - super(props, context); - this.handleLoadedData = this.handleLoadedData.bind(this); - this.setRef = this.setRef.bind(this); - } - - handleLoadedData () { - if (this.props.time) { - this.video.currentTime = this.props.time; - } - } - - componentDidMount () { - this.video.addEventListener('loadeddata', this.handleLoadedData); - } - - componentWillUnmount () { - this.video.removeEventListener('loadeddata', this.handleLoadedData); - } - - setRef (c) { - this.video = c; - } - - render () { - return ( - <div className='extended-video-player'> - <video - ref={this.setRef} - src={this.props.src} - autoPlay - muted={this.props.muted} - controls={this.props.controls} - loop={!this.props.controls} - /> - </div> - ); - } - -} - -ExtendedVideoPlayer.propTypes = { - src: PropTypes.string.isRequired, - time: PropTypes.number, - controls: PropTypes.bool.isRequired, - muted: PropTypes.bool.isRequired -}; - -export default ExtendedVideoPlayer; diff --git a/app/assets/javascripts/components/components/icon_button.jsx b/app/assets/javascripts/components/components/icon_button.jsx deleted file mode 100644 index 67c6513fd..000000000 --- a/app/assets/javascripts/components/components/icon_button.jsx +++ /dev/null @@ -1,95 +0,0 @@ -import { Motion, spring } from 'react-motion'; -import PropTypes from 'prop-types'; - -class IconButton extends React.PureComponent { - - constructor (props, context) { - super(props, context); - this.handleClick = this.handleClick.bind(this); - } - - handleClick (e) { - e.preventDefault(); - - if (!this.props.disabled) { - this.props.onClick(e); - } - } - - render () { - let style = { - fontSize: `${this.props.size}px`, - width: `${this.props.size * 1.28571429}px`, - height: `${this.props.size * 1.28571429}px`, - lineHeight: `${this.props.size}px`, - ...this.props.style - }; - - if (this.props.active) { - style = { ...style, ...this.props.activeStyle }; - } - - const classes = ['icon-button']; - - if (this.props.active) { - classes.push('active'); - } - - if (this.props.disabled) { - classes.push('disabled'); - } - - if (this.props.inverted) { - classes.push('inverted'); - } - - if (this.props.overlay) { - classes.push('overlayed'); - } - - if (this.props.className) { - classes.push(this.props.className) - } - - return ( - <Motion defaultStyle={{ rotate: this.props.active ? -360 : 0 }} style={{ rotate: this.props.animate ? spring(this.props.active ? -360 : 0, { stiffness: 120, damping: 7 }) : 0 }}> - {({ rotate }) => - <button - aria-label={this.props.title} - title={this.props.title} - className={classes.join(' ')} - onClick={this.handleClick} - style={style}> - <i style={{ transform: `rotate(${rotate}deg)` }} className={`fa fa-fw fa-${this.props.icon}`} aria-hidden='true' /> - </button> - } - </Motion> - ); - } - -} - -IconButton.propTypes = { - className: PropTypes.string, - title: PropTypes.string.isRequired, - icon: PropTypes.string.isRequired, - onClick: PropTypes.func, - size: PropTypes.number, - active: PropTypes.bool, - style: PropTypes.object, - activeStyle: PropTypes.object, - disabled: PropTypes.bool, - inverted: PropTypes.bool, - animate: PropTypes.bool, - overlay: PropTypes.bool -}; - -IconButton.defaultProps = { - size: 18, - active: false, - disabled: false, - animate: false, - overlay: false -}; - -export default IconButton; diff --git a/app/assets/javascripts/components/components/load_more.jsx b/app/assets/javascripts/components/components/load_more.jsx deleted file mode 100644 index f59ff1103..000000000 --- a/app/assets/javascripts/components/components/load_more.jsx +++ /dev/null @@ -1,14 +0,0 @@ -import { FormattedMessage } from 'react-intl'; -import PropTypes from 'prop-types'; - -const LoadMore = ({ onClick }) => ( - <a href="#" className='load-more' role='button' onClick={onClick}> - <FormattedMessage id='status.load_more' defaultMessage='Load more' /> - </a> -); - -LoadMore.propTypes = { - onClick: PropTypes.func -}; - -export default LoadMore; diff --git a/app/assets/javascripts/components/components/loading_indicator.jsx b/app/assets/javascripts/components/components/loading_indicator.jsx deleted file mode 100644 index 61e0a0f15..000000000 --- a/app/assets/javascripts/components/components/loading_indicator.jsx +++ /dev/null @@ -1,9 +0,0 @@ -import { FormattedMessage } from 'react-intl'; - -const LoadingIndicator = () => ( - <div className='loading-indicator'> - <FormattedMessage id='loading_indicator.label' defaultMessage='Loading...' /> - </div> -); - -export default LoadingIndicator; diff --git a/app/assets/javascripts/components/components/media_gallery.jsx b/app/assets/javascripts/components/components/media_gallery.jsx deleted file mode 100644 index 58e134f50..000000000 --- a/app/assets/javascripts/components/components/media_gallery.jsx +++ /dev/null @@ -1,195 +0,0 @@ -import ImmutablePropTypes from 'react-immutable-proptypes'; -import PropTypes from 'prop-types'; -import IconButton from './icon_button'; -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; -import { isIOS } from '../is_mobile'; - -const messages = defineMessages({ - toggle_visible: { id: 'media_gallery.toggle_visible', defaultMessage: 'Toggle visibility' } -}); - -class Item extends React.PureComponent { - constructor (props, context) { - super(props, context); - this.handleClick = this.handleClick.bind(this); - } - - handleClick (e) { - const { index, onClick } = this.props; - - if (e.button === 0) { - e.preventDefault(); - onClick(index); - } - - e.stopPropagation(); - } - - render () { - const { attachment, index, size } = this.props; - - let width = 50; - let height = 100; - let top = 'auto'; - let left = 'auto'; - let bottom = 'auto'; - let right = 'auto'; - - if (size === 1) { - width = 100; - } - - if (size === 4 || (size === 3 && index > 0)) { - height = 50; - } - - if (size === 2) { - if (index === 0) { - right = '2px'; - } else { - left = '2px'; - } - } else if (size === 3) { - if (index === 0) { - right = '2px'; - } else if (index > 0) { - left = '2px'; - } - - if (index === 1) { - bottom = '2px'; - } else if (index > 1) { - top = '2px'; - } - } else if (size === 4) { - if (index === 0 || index === 2) { - right = '2px'; - } - - if (index === 1 || index === 3) { - left = '2px'; - } - - if (index < 2) { - bottom = '2px'; - } else { - top = '2px'; - } - } - - let thumbnail = ''; - - if (attachment.get('type') === 'image') { - thumbnail = ( - <a - className='media-gallery__item-thumbnail' - href={attachment.get('remote_url') || attachment.get('url')} - onClick={this.handleClick} - target='_blank' - style={{ backgroundImage: `url(${attachment.get('preview_url')})` }} - /> - ); - } else if (attachment.get('type') === 'gifv') { - const autoPlay = !isIOS() && this.props.autoPlayGif; - - thumbnail = ( - <div className={`media-gallery__gifv ${autoPlay ? 'autoplay' : ''}`}> - <video - className='media-gallery__item-gifv-thumbnail' - role='application' - src={attachment.get('url')} - onClick={this.handleClick} - autoPlay={autoPlay} - loop={true} - muted={true} - /> - - <span className='media-gallery__gifv__label'>GIF</span> - </div> - ); - } - - return ( - <div className='media-gallery__item' key={attachment.get('id')} style={{ left: left, top: top, right: right, bottom: bottom, width: `${width}%`, height: `${height}%` }}> - {thumbnail} - </div> - ); - } - -} - -Item.propTypes = { - attachment: ImmutablePropTypes.map.isRequired, - index: PropTypes.number.isRequired, - size: PropTypes.number.isRequired, - onClick: PropTypes.func.isRequired, - autoPlayGif: PropTypes.bool.isRequired -}; - -class MediaGallery extends React.PureComponent { - - constructor (props, context) { - super(props, context); - this.state = { - visible: !props.sensitive - }; - this.handleOpen = this.handleOpen.bind(this); - this.handleClick = this.handleClick.bind(this); - } - - handleOpen (e) { - this.setState({ visible: !this.state.visible }); - } - - handleClick (index) { - this.props.onOpenMedia(this.props.media, index); - } - - render () { - const { media, intl, sensitive } = this.props; - - let children; - - if (!this.state.visible) { - let warning; - - if (sensitive) { - warning = <FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' />; - } else { - warning = <FormattedMessage id='status.media_hidden' defaultMessage='Media hidden' />; - } - - children = ( - <div role='button' tabIndex='0' className='media-spoiler' onClick={this.handleOpen}> - <span className='media-spoiler__warning'>{warning}</span> - <span className='media-spoiler__trigger'><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span> - </div> - ); - } else { - const size = media.take(4).size; - children = media.take(4).map((attachment, i) => <Item key={attachment.get('id')} onClick={this.handleClick} attachment={attachment} autoPlayGif={this.props.autoPlayGif} index={i} size={size} />); - } - - return ( - <div className='media-gallery' style={{ height: `${this.props.height}px` }}> - <div className='spoiler-button' style={{ display: !this.state.visible ? 'none' : 'block' }}> - <IconButton title={intl.formatMessage(messages.toggle_visible)} icon={this.state.visible ? 'eye' : 'eye-slash'} overlay onClick={this.handleOpen} /> - </div> - - {children} - </div> - ); - } - -} - -MediaGallery.propTypes = { - sensitive: PropTypes.bool, - media: ImmutablePropTypes.list.isRequired, - height: PropTypes.number.isRequired, - onOpenMedia: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - autoPlayGif: PropTypes.bool.isRequired -}; - -export default injectIntl(MediaGallery); diff --git a/app/assets/javascripts/components/components/missing_indicator.jsx b/app/assets/javascripts/components/components/missing_indicator.jsx deleted file mode 100644 index 75129ae14..000000000 --- a/app/assets/javascripts/components/components/missing_indicator.jsx +++ /dev/null @@ -1,9 +0,0 @@ -import { FormattedMessage } from 'react-intl'; - -const MissingIndicator = () => ( - <div className='missing-indicator'> - <FormattedMessage id='missing_indicator.label' defaultMessage='Not found' /> - </div> -); - -export default MissingIndicator; diff --git a/app/assets/javascripts/components/components/permalink.jsx b/app/assets/javascripts/components/components/permalink.jsx deleted file mode 100644 index ccbe4944f..000000000 --- a/app/assets/javascripts/components/components/permalink.jsx +++ /dev/null @@ -1,36 +0,0 @@ -import PropTypes from 'prop-types'; - -class Permalink extends React.Component { - - constructor (props, context) { - super(props, context); - this.handleClick = this.handleClick.bind(this); - } - - handleClick (e) { - if (e.button === 0) { - e.preventDefault(); - this.context.router.push(this.props.to); - } - } - - render () { - const { href, children, className, ...other } = this.props; - - return <a href={href} onClick={this.handleClick} {...other} className={'permalink ' + className}>{children}</a>; - } - -} - -Permalink.contextTypes = { - router: PropTypes.object -}; - -Permalink.propTypes = { - className: PropTypes.string, - href: PropTypes.string.isRequired, - to: PropTypes.string.isRequired, - children: PropTypes.node -}; - -export default Permalink; diff --git a/app/assets/javascripts/components/components/relative_timestamp.jsx b/app/assets/javascripts/components/components/relative_timestamp.jsx deleted file mode 100644 index 9ab472e2c..000000000 --- a/app/assets/javascripts/components/components/relative_timestamp.jsx +++ /dev/null @@ -1,19 +0,0 @@ -import { injectIntl, FormattedRelative } from 'react-intl'; -import PropTypes from 'prop-types'; - -const RelativeTimestamp = ({ intl, timestamp }) => { - const date = new Date(timestamp); - - return ( - <time dateTime={timestamp} title={intl.formatDate(date, { hour12: false, year: 'numeric', month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit' })}> - <FormattedRelative value={date} /> - </time> - ); -}; - -RelativeTimestamp.propTypes = { - intl: PropTypes.object.isRequired, - timestamp: PropTypes.string.isRequired -}; - -export default injectIntl(RelativeTimestamp); diff --git a/app/assets/javascripts/components/components/status.jsx b/app/assets/javascripts/components/components/status.jsx deleted file mode 100644 index 193231837..000000000 --- a/app/assets/javascripts/components/components/status.jsx +++ /dev/null @@ -1,121 +0,0 @@ -import ImmutablePropTypes from 'react-immutable-proptypes'; -import PropTypes from 'prop-types'; -import Avatar from './avatar'; -import RelativeTimestamp from './relative_timestamp'; -import DisplayName from './display_name'; -import MediaGallery from './media_gallery'; -import VideoPlayer from './video_player'; -import AttachmentList from './attachment_list'; -import StatusContent from './status_content'; -import StatusActionBar from './status_action_bar'; -import { FormattedMessage } from 'react-intl'; -import emojify from '../emoji'; -import escapeTextContentForBrowser from 'escape-html'; - -class Status extends React.PureComponent { - - constructor (props, context) { - super(props, context); - this.handleClick = this.handleClick.bind(this); - this.handleAccountClick = this.handleAccountClick.bind(this); - } - - handleClick () { - const { status } = this.props; - this.context.router.push(`/statuses/${status.getIn(['reblog', 'id'], status.get('id'))}`); - } - - handleAccountClick (id, e) { - if (e.button === 0) { - e.preventDefault(); - this.context.router.push(`/accounts/${id}`); - } - } - - render () { - let media = ''; - const { status, ...other } = this.props; - - if (status === null) { - return <div />; - } - - if (status.get('reblog', null) !== null && typeof status.get('reblog') === 'object') { - let displayName = status.getIn(['account', 'display_name']); - - if (displayName.length === 0) { - displayName = status.getIn(['account', 'username']); - } - - const displayNameHTML = { __html: emojify(escapeTextContentForBrowser(displayName)) }; - - return ( - <div className='status__wrapper'> - <div className='status__prepend'> - <div className='status__prepend-icon-wrapper'><i className='fa fa-fw fa-retweet status__prepend-icon' /></div> - <FormattedMessage id='status.reblogged_by' defaultMessage='{name} boosted' values={{ name: <a onClick={this.handleAccountClick.bind(this, status.getIn(['account', 'id']))} href={status.getIn(['account', 'url'])} className='status__display-name muted'><strong dangerouslySetInnerHTML={displayNameHTML} /></a> }} /> - </div> - - <Status {...other} wrapped={true} status={status.get('reblog')} /> - </div> - ); - } - - if (status.get('media_attachments').size > 0 && !this.props.muted) { - if (status.get('media_attachments').some(item => item.get('type') === 'unknown')) { - - } else if (status.getIn(['media_attachments', 0, 'type']) === 'video') { - media = <VideoPlayer media={status.getIn(['media_attachments', 0])} sensitive={status.get('sensitive')} onOpenVideo={this.props.onOpenVideo} />; - } else { - media = <MediaGallery media={status.get('media_attachments')} sensitive={status.get('sensitive')} height={110} onOpenMedia={this.props.onOpenMedia} autoPlayGif={this.props.autoPlayGif} />; - } - } - - return ( - <div className={this.props.muted ? 'status muted' : 'status'}> - <div className='status__info'> - <div className='status__info-time'> - <a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener'><RelativeTimestamp timestamp={status.get('created_at')} /></a> - </div> - - <a onClick={this.handleAccountClick.bind(this, status.getIn(['account', 'id']))} href={status.getIn(['account', 'url'])} className='status__display-name'> - <div className='status__avatar'> - <Avatar src={status.getIn(['account', 'avatar'])} staticSrc={status.getIn(['account', 'avatar_static'])} size={48} /> - </div> - - <DisplayName account={status.get('account')} /> - </a> - </div> - - <StatusContent status={status} onClick={this.handleClick} /> - - {media} - - <StatusActionBar {...this.props} /> - </div> - ); - } - -} - -Status.contextTypes = { - router: PropTypes.object -}; - -Status.propTypes = { - status: ImmutablePropTypes.map, - wrapped: PropTypes.bool, - onReply: PropTypes.func, - onFavourite: PropTypes.func, - onReblog: PropTypes.func, - onDelete: PropTypes.func, - onOpenMedia: PropTypes.func, - onOpenVideo: PropTypes.func, - onBlock: PropTypes.func, - me: PropTypes.number, - boostModal: PropTypes.bool, - autoPlayGif: PropTypes.bool, - muted: PropTypes.bool -}; - -export default Status; diff --git a/app/assets/javascripts/components/components/status_action_bar.jsx b/app/assets/javascripts/components/components/status_action_bar.jsx deleted file mode 100644 index 29938f23e..000000000 --- a/app/assets/javascripts/components/components/status_action_bar.jsx +++ /dev/null @@ -1,137 +0,0 @@ -import ImmutablePropTypes from 'react-immutable-proptypes'; -import PropTypes from 'prop-types'; -import IconButton from './icon_button'; -import DropdownMenu from './dropdown_menu'; -import { defineMessages, injectIntl } from 'react-intl'; - -const messages = defineMessages({ - delete: { id: 'status.delete', defaultMessage: 'Delete' }, - mention: { id: 'status.mention', defaultMessage: 'Mention @{name}' }, - mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' }, - block: { id: 'account.block', defaultMessage: 'Block @{name}' }, - reply: { id: 'status.reply', defaultMessage: 'Reply' }, - replyAll: { id: 'status.replyAll', defaultMessage: 'Reply to thread' }, - reblog: { id: 'status.reblog', defaultMessage: 'Boost' }, - cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' }, - favourite: { id: 'status.favourite', defaultMessage: 'Favourite' }, - open: { id: 'status.open', defaultMessage: 'Expand this status' }, - report: { id: 'status.report', defaultMessage: 'Report @{name}' } -}); - -class StatusActionBar extends React.PureComponent { - - constructor (props, context) { - super(props, context); - this.handleReplyClick = this.handleReplyClick.bind(this); - this.handleFavouriteClick = this.handleFavouriteClick.bind(this); - this.handleReblogClick = this.handleReblogClick.bind(this); - this.handleDeleteClick = this.handleDeleteClick.bind(this); - this.handleMentionClick = this.handleMentionClick.bind(this); - this.handleMuteClick = this.handleMuteClick.bind(this); - this.handleBlockClick = this.handleBlockClick.bind(this); - this.handleOpen = this.handleOpen.bind(this); - this.handleReport = this.handleReport.bind(this); - } - - handleReplyClick () { - this.props.onReply(this.props.status, this.context.router); - } - - handleFavouriteClick () { - this.props.onFavourite(this.props.status); - } - - handleReblogClick (e) { - this.props.onReblog(this.props.status, e); - } - - handleDeleteClick () { - this.props.onDelete(this.props.status); - } - - handleMentionClick () { - this.props.onMention(this.props.status.get('account'), this.context.router); - } - - handleMuteClick () { - this.props.onMute(this.props.status.get('account')); - } - - handleBlockClick () { - this.props.onBlock(this.props.status.get('account')); - } - - handleOpen () { - this.context.router.push(`/statuses/${this.props.status.get('id')}`); - } - - handleReport () { - this.props.onReport(this.props.status); - this.context.router.push('/report'); - } - - render () { - const { status, me, intl } = this.props; - const reblog_disabled = status.get('visibility') === 'private' || status.get('visibility') === 'direct'; - let menu = []; - - menu.push({ text: intl.formatMessage(messages.open), action: this.handleOpen }); - menu.push(null); - - if (status.getIn(['account', 'id']) === me) { - menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick }); - } else { - menu.push({ text: intl.formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }), action: this.handleMentionClick }); - menu.push(null); - menu.push({ text: intl.formatMessage(messages.mute, { name: status.getIn(['account', 'username']) }), action: this.handleMuteClick }); - menu.push({ text: intl.formatMessage(messages.block, { name: status.getIn(['account', 'username']) }), action: this.handleBlockClick }); - menu.push({ text: intl.formatMessage(messages.report, { name: status.getIn(['account', 'username']) }), action: this.handleReport }); - } - - let reblogIcon = 'retweet'; - if (status.get('visibility') === 'direct') reblogIcon = 'envelope'; - else if (status.get('visibility') === 'private') reblogIcon = 'lock'; - let reply_icon; - let reply_title; - if (status.get('in_reply_to_id', null) === null) { - reply_icon = "reply"; - reply_title = intl.formatMessage(messages.reply); - } else { - reply_icon = "reply-all"; - reply_title = intl.formatMessage(messages.replyAll); - } - - return ( - <div className='status__action-bar'> - <div className='status__action-bar-button-wrapper'><IconButton title={reply_title} icon={reply_icon} onClick={this.handleReplyClick} /></div> - <div className='status__action-bar-button-wrapper'><IconButton disabled={reblog_disabled} active={status.get('reblogged')} title={reblog_disabled ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.reblog)} icon={reblogIcon} onClick={this.handleReblogClick} /></div> - <div className='status__action-bar-button-wrapper'><IconButton animate={true} active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} className='star-icon' /></div> - - <div className='status__action-bar-dropdown'> - <DropdownMenu items={menu} icon='ellipsis-h' size={18} direction="right" ariaLabel="More"/> - </div> - </div> - ); - } - -} - -StatusActionBar.contextTypes = { - router: PropTypes.object -}; - -StatusActionBar.propTypes = { - status: ImmutablePropTypes.map.isRequired, - onReply: PropTypes.func, - onFavourite: PropTypes.func, - onReblog: PropTypes.func, - onDelete: PropTypes.func, - onMention: PropTypes.func, - onMute: PropTypes.func, - onBlock: PropTypes.func, - onReport: PropTypes.func, - me: PropTypes.number.isRequired, - intl: PropTypes.object.isRequired -}; - -export default injectIntl(StatusActionBar); diff --git a/app/assets/javascripts/components/components/status_content.jsx b/app/assets/javascripts/components/components/status_content.jsx deleted file mode 100644 index 714c00951..000000000 --- a/app/assets/javascripts/components/components/status_content.jsx +++ /dev/null @@ -1,157 +0,0 @@ -import ImmutablePropTypes from 'react-immutable-proptypes'; -import escapeTextContentForBrowser from 'escape-html'; -import PropTypes from 'prop-types'; -import emojify from '../emoji'; -import { isRtl } from '../rtl'; -import { FormattedMessage } from 'react-intl'; -import Permalink from './permalink'; - -class StatusContent extends React.PureComponent { - - constructor (props, context) { - super(props, context); - this.state = { - hidden: true - }; - this.onMentionClick = this.onMentionClick.bind(this); - this.onHashtagClick = this.onHashtagClick.bind(this); - this.handleMouseDown = this.handleMouseDown.bind(this) - this.handleMouseUp = this.handleMouseUp.bind(this); - this.handleSpoilerClick = this.handleSpoilerClick.bind(this); - }; - - componentDidMount () { - const node = ReactDOM.findDOMNode(this); - const links = node.querySelectorAll('a'); - - for (var i = 0; i < links.length; ++i) { - let link = links[i]; - let mention = this.props.status.get('mentions').find(item => link.href === item.get('url')); - let media = this.props.status.get('media_attachments').find(item => link.href === item.get('text_url') || (item.get('remote_url').length > 0 && link.href === item.get('remote_url'))); - - if (mention) { - link.addEventListener('click', this.onMentionClick.bind(this, mention), false); - link.setAttribute('title', mention.get('acct')); - } else if (link.textContent[0] === '#' || (link.previousSibling && link.previousSibling.textContent && link.previousSibling.textContent[link.previousSibling.textContent.length - 1] === '#')) { - link.addEventListener('click', this.onHashtagClick.bind(this, link.text), false); - } else if (media) { - link.innerHTML = '<i class="fa fa-fw fa-photo"></i>'; - } else { - link.setAttribute('target', '_blank'); - link.setAttribute('rel', 'noopener'); - link.setAttribute('title', link.href); - } - } - } - - onMentionClick (mention, e) { - if (e.button === 0) { - e.preventDefault(); - this.context.router.push(`/accounts/${mention.get('id')}`); - } - } - - onHashtagClick (hashtag, e) { - hashtag = hashtag.replace(/^#/, '').toLowerCase(); - - if (e.button === 0) { - e.preventDefault(); - this.context.router.push(`/timelines/tag/${hashtag}`); - } - } - - handleMouseDown (e) { - this.startXY = [e.clientX, e.clientY]; - } - - handleMouseUp (e) { - const [ startX, startY ] = this.startXY; - const [ deltaX, deltaY ] = [Math.abs(e.clientX - startX), Math.abs(e.clientY - startY)]; - - if (e.target.localName === 'a' || (e.target.parentNode && e.target.parentNode.localName === 'a')) { - return; - } - - if (deltaX + deltaY < 5 && e.button === 0) { - this.props.onClick(); - } - - this.startXY = null; - } - - handleSpoilerClick (e) { - e.preventDefault(); - this.setState({ hidden: !this.state.hidden }); - } - - render () { - const { status } = this.props; - const { hidden } = this.state; - - const content = { __html: emojify(status.get('content')) }; - const spoilerContent = { __html: emojify(escapeTextContentForBrowser(status.get('spoiler_text', ''))) }; - const directionStyle = { direction: 'ltr' }; - - if (isRtl(status.get('content'))) { - directionStyle.direction = 'rtl'; - } - - if (status.get('spoiler_text').length > 0) { - let mentionsPlaceholder = ''; - - const mentionLinks = status.get('mentions').map(item => ( - <Permalink to={`/accounts/${item.get('id')}`} href={item.get('url')} key={item.get('id')} className='mention'> - @<span>{item.get('username')}</span> - </Permalink> - )).reduce((aggregate, item) => [...aggregate, item, ' '], []) - - const toggleText = hidden ? <FormattedMessage id='status.show_more' defaultMessage='Show more' /> : <FormattedMessage id='status.show_less' defaultMessage='Show less' />; - - if (hidden) { - mentionsPlaceholder = <div>{mentionLinks}</div>; - } - - return ( - <div className='status__content' onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp}> - <p style={{ marginBottom: hidden && status.get('mentions').size === 0 ? '0px' : '' }} > - <span dangerouslySetInnerHTML={spoilerContent} /> <a tabIndex='0' className='status__content__spoiler-link' role='button' onClick={this.handleSpoilerClick}>{toggleText}</a> - </p> - - {mentionsPlaceholder} - - <div style={{ display: hidden ? 'none' : 'block', ...directionStyle }} dangerouslySetInnerHTML={content} /> - </div> - ); - } else if (this.props.onClick) { - return ( - <div - className='status__content' - style={{ ...directionStyle }} - onMouseDown={this.handleMouseDown} - onMouseUp={this.handleMouseUp} - dangerouslySetInnerHTML={content} - /> - ); - } else { - return ( - <div - className='status__content status__content--no-action' - style={{ ...directionStyle }} - dangerouslySetInnerHTML={content} - /> - ); - } - } - -} - -StatusContent.contextTypes = { - router: PropTypes.object -}; - -StatusContent.propTypes = { - status: ImmutablePropTypes.map.isRequired, - onClick: PropTypes.func -}; - -export default StatusContent; diff --git a/app/assets/javascripts/components/components/status_list.jsx b/app/assets/javascripts/components/components/status_list.jsx deleted file mode 100644 index 517c8fe5d..000000000 --- a/app/assets/javascripts/components/components/status_list.jsx +++ /dev/null @@ -1,128 +0,0 @@ -import Status from './status'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import { ScrollContainer } from 'react-router-scroll'; -import PropTypes from 'prop-types'; -import StatusContainer from '../containers/status_container'; -import LoadMore from './load_more'; - -class StatusList extends React.PureComponent { - - constructor (props, context) { - super(props, context); - this.handleScroll = this.handleScroll.bind(this); - this.setRef = this.setRef.bind(this); - this.handleLoadMore = this.handleLoadMore.bind(this); - } - - handleScroll (e) { - const { scrollTop, scrollHeight, clientHeight } = e.target; - const offset = scrollHeight - scrollTop - clientHeight; - this._oldScrollPosition = scrollHeight - scrollTop; - - if (250 > offset && this.props.onScrollToBottom && !this.props.isLoading) { - this.props.onScrollToBottom(); - } else if (scrollTop < 100 && this.props.onScrollToTop) { - this.props.onScrollToTop(); - } else if (this.props.onScroll) { - this.props.onScroll(); - } - } - - componentDidMount () { - this.attachScrollListener(); - } - - componentDidUpdate (prevProps) { - if (this.node.scrollTop > 0 && (prevProps.statusIds.size < this.props.statusIds.size && prevProps.statusIds.first() !== this.props.statusIds.first() && !!this._oldScrollPosition)) { - this.node.scrollTop = this.node.scrollHeight - this._oldScrollPosition; - } - } - - componentWillUnmount () { - this.detachScrollListener(); - } - - attachScrollListener () { - this.node.addEventListener('scroll', this.handleScroll); - } - - detachScrollListener () { - this.node.removeEventListener('scroll', this.handleScroll); - } - - setRef (c) { - this.node = c; - } - - handleLoadMore (e) { - e.preventDefault(); - this.props.onScrollToBottom(); - } - - render () { - const { statusIds, onScrollToBottom, scrollKey, shouldUpdateScroll, isLoading, isUnread, hasMore, prepend, emptyMessage } = this.props; - - let loadMore = ''; - let scrollableArea = ''; - let unread = ''; - - if (!isLoading && statusIds.size > 0 && hasMore) { - loadMore = <LoadMore onClick={this.handleLoadMore} />; - } - - if (isUnread) { - unread = <div className='status-list__unread-indicator' />; - } - - if (isLoading || statusIds.size > 0 || !emptyMessage) { - scrollableArea = ( - <div className='scrollable' ref={this.setRef}> - {unread} - - <div className='status-list'> - {prepend} - - {statusIds.map((statusId) => { - return <StatusContainer key={statusId} id={statusId} />; - })} - - {loadMore} - </div> - </div> - ); - } else { - scrollableArea = ( - <div className='empty-column-indicator' ref={this.setRef}> - {emptyMessage} - </div> - ); - } - - return ( - <ScrollContainer scrollKey={scrollKey} shouldUpdateScroll={shouldUpdateScroll}> - {scrollableArea} - </ScrollContainer> - ); - } - -} - -StatusList.propTypes = { - scrollKey: PropTypes.string.isRequired, - statusIds: ImmutablePropTypes.list.isRequired, - onScrollToBottom: PropTypes.func, - onScrollToTop: PropTypes.func, - onScroll: PropTypes.func, - shouldUpdateScroll: PropTypes.func, - isLoading: PropTypes.bool, - isUnread: PropTypes.bool, - hasMore: PropTypes.bool, - prepend: PropTypes.node, - emptyMessage: PropTypes.node -}; - -StatusList.defaultProps = { - trackScroll: true -}; - -export default StatusList; diff --git a/app/assets/javascripts/components/components/video_player.jsx b/app/assets/javascripts/components/components/video_player.jsx deleted file mode 100644 index 09c8ed875..000000000 --- a/app/assets/javascripts/components/components/video_player.jsx +++ /dev/null @@ -1,198 +0,0 @@ -import ImmutablePropTypes from 'react-immutable-proptypes'; -import PropTypes from 'prop-types'; -import IconButton from './icon_button'; -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; -import { isIOS } from '../is_mobile'; - -const messages = defineMessages({ - toggle_sound: { id: 'video_player.toggle_sound', defaultMessage: 'Toggle sound' }, - toggle_visible: { id: 'video_player.toggle_visible', defaultMessage: 'Toggle visibility' }, - expand_video: { id: 'video_player.expand', defaultMessage: 'Expand video' }, - expand_video: { id: 'video_player.video_error', defaultMessage: 'Video could not be played' } -}); - -class VideoPlayer extends React.PureComponent { - - constructor (props, context) { - super(props, context); - this.state = { - visible: !this.props.sensitive, - preview: true, - muted: true, - hasAudio: true, - videoError: false - }; - - this.handleClick = this.handleClick.bind(this); - this.handleVideoClick = this.handleVideoClick.bind(this); - this.handleOpen = this.handleOpen.bind(this); - this.handleVisibility = this.handleVisibility.bind(this); - this.handleExpand = this.handleExpand.bind(this); - this.setRef = this.setRef.bind(this); - this.handleLoadedData = this.handleLoadedData.bind(this); - this.handleVideoError = this.handleVideoError.bind(this); - } - - handleClick () { - this.setState({ muted: !this.state.muted }); - } - - handleVideoClick (e) { - e.stopPropagation(); - - const node = ReactDOM.findDOMNode(this).querySelector('video'); - - if (node.paused) { - node.play(); - } else { - node.pause(); - } - } - - handleOpen () { - this.setState({ preview: !this.state.preview }); - } - - handleVisibility () { - this.setState({ - visible: !this.state.visible, - preview: true - }); - } - - handleExpand () { - this.video.pause(); - this.props.onOpenVideo(this.props.media, this.video.currentTime); - } - - setRef (c) { - this.video = c; - } - - handleLoadedData () { - if (('WebkitAppearance' in document.documentElement.style && this.video.audioTracks.length === 0) || this.video.mozHasAudio === false) { - this.setState({ hasAudio: false }); - } - } - - handleVideoError () { - this.setState({ videoError: true }); - } - - componentDidMount () { - if (!this.video) { - return; - } - - this.video.addEventListener('loadeddata', this.handleLoadedData); - this.video.addEventListener('error', this.handleVideoError); - } - - componentDidUpdate () { - if (!this.video) { - return; - } - - this.video.addEventListener('loadeddata', this.handleLoadedData); - this.video.addEventListener('error', this.handleVideoError); - } - - componentWillUnmount () { - if (!this.video) { - return; - } - - this.video.removeEventListener('loadeddata', this.handleLoadedData); - this.video.removeEventListener('error', this.handleVideoError); - } - - render () { - const { media, intl, width, height, sensitive, autoplay } = this.props; - - let spoilerButton = ( - <div className='status__video-player-spoiler' style={{ display: !this.state.visible ? 'none' : 'block' }} > - <IconButton overlay title={intl.formatMessage(messages.toggle_visible)} icon={this.state.visible ? 'eye' : 'eye-slash'} onClick={this.handleVisibility} /> - </div> - ); - - let expandButton = ( - <div className='status__video-player-expand'> - <IconButton overlay title={intl.formatMessage(messages.expand_video)} icon='expand' onClick={this.handleExpand} /> - </div> - ); - - let muteButton = ''; - - if (this.state.hasAudio) { - muteButton = ( - <div className='status__video-player-mute'> - <IconButton overlay title={intl.formatMessage(messages.toggle_sound)} icon={this.state.muted ? 'volume-off' : 'volume-up'} onClick={this.handleClick} /> - </div> - ); - } - - if (!this.state.visible) { - if (sensitive) { - return ( - <div role='button' tabIndex='0' style={{ width: `${width}px`, height: `${height}px` }} className='media-spoiler' onClick={this.handleVisibility}> - {spoilerButton} - <span className='media-spoiler__warning'><FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' /></span> - <span className='media-spoiler__trigger'><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span> - </div> - ); - } else { - return ( - <div role='button' tabIndex='0' style={{ width: `${width}px`, height: `${height}px` }} className='media-spoiler' onClick={this.handleVisibility}> - {spoilerButton} - <span className='media-spoiler__warning'><FormattedMessage id='status.media_hidden' defaultMessage='Media hidden' /></span> - <span className='media-spoiler__trigger'><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span> - </div> - ); - } - } - - if (this.state.preview && !autoplay) { - return ( - <div role='button' tabIndex='0' className='media-spoiler-video' style={{ width: `${width}px`, height: `${height}px`, background: `url(${media.get('preview_url')}) no-repeat center` }} onClick={this.handleOpen}> - {spoilerButton} - <div className='media-spoiler-video-play-icon'><i className='fa fa-play' /></div> - </div> - ); - } - - if (this.state.videoError) { - return ( - <div style={{ width: `${width}px`, height: `${height}px` }} className='video-error-cover' > - <span className='media-spoiler__warning'><FormattedMessage id='video_player.video_error' defaultMessage='Video could not be played' /></span> - </div> - ); - } - - return ( - <div className='status__video-player' style={{ width: `${width}px`, height: `${height}px` }}> - {spoilerButton} - {muteButton} - {expandButton} - <video className='status__video-player-video' role='button' tabIndex='0' ref={this.setRef} src={media.get('url')} autoPlay={!isIOS()} loop={true} muted={this.state.muted} onClick={this.handleVideoClick} /> - </div> - ); - } - -} - -VideoPlayer.propTypes = { - media: ImmutablePropTypes.map.isRequired, - width: PropTypes.number, - height: PropTypes.number, - sensitive: PropTypes.bool, - intl: PropTypes.object.isRequired, - autoplay: PropTypes.bool, - onOpenVideo: PropTypes.func.isRequired -}; - -VideoPlayer.defaultProps = { - width: 239, - height: 110 -}; - -export default injectIntl(VideoPlayer); |