diff options
Diffstat (limited to 'app/javascript/themes/glitch/components')
33 files changed, 0 insertions, 3454 deletions
diff --git a/app/javascript/themes/glitch/components/account.js b/app/javascript/themes/glitch/components/account.js deleted file mode 100644 index d0ff77050..000000000 --- a/app/javascript/themes/glitch/components/account.js +++ /dev/null @@ -1,116 +0,0 @@ -import React from 'react'; -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'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import { me } from 'themes/glitch/util/initial_state'; - -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}' }, - mute_notifications: { id: 'account.mute_notifications', defaultMessage: 'You are not currently muting notifications from @{name}. Click to mute notifications' }, - unmute_notifications: { id: 'account.unmute_notifications', defaultMessage: 'You are currently muting notifications from @{name}. Click to unmute notifications' }, -}); - -@injectIntl -export default class Account extends ImmutablePureComponent { - - static propTypes = { - account: ImmutablePropTypes.map.isRequired, - onFollow: PropTypes.func.isRequired, - onBlock: PropTypes.func.isRequired, - onMute: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - hidden: PropTypes.bool, - }; - - handleFollow = () => { - this.props.onFollow(this.props.account); - } - - handleBlock = () => { - this.props.onBlock(this.props.account); - } - - handleMute = () => { - this.props.onMute(this.props.account); - } - - handleMuteNotifications = () => { - this.props.onMuteNotifications(this.props.account, true); - } - - handleUnmuteNotifications = () => { - this.props.onMuteNotifications(this.props.account, false); - } - - render () { - const { account, intl, hidden } = this.props; - - if (!account) { - return <div />; - } - - if (hidden) { - return ( - <div> - {account.get('display_name')} - {account.get('username')} - </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 icon='hourglass' title={intl.formatMessage(messages.requested)} />; - } else if (blocking) { - buttons = <IconButton active icon='unlock-alt' title={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.handleBlock} />; - } else if (muting) { - let hidingNotificationsButton; - if (muting.get('notifications')) { - hidingNotificationsButton = <IconButton active icon='bell' title={intl.formatMessage(messages.unmute_notifications, { name: account.get('username') })} onClick={this.handleUnmuteNotifications} />; - } else { - hidingNotificationsButton = <IconButton active icon='bell-slash' title={intl.formatMessage(messages.mute_notifications, { name: account.get('username') })} onClick={this.handleMuteNotifications} />; - } - buttons = ( - <div> - <IconButton active icon='volume-up' title={intl.formatMessage(messages.unmute, { name: account.get('username') })} onClick={this.handleMute} /> - {hidingNotificationsButton} - </div> - ); - } else { - buttons = <IconButton icon={following ? 'user-times' : 'user-plus'} title={intl.formatMessage(following ? messages.unfollow : messages.follow)} onClick={this.handleFollow} active={following ? true : false} />; - } - } - - 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 account={account} size={36} /></div> - <DisplayName account={account} /> - </Permalink> - - <div className='account__relationship'> - {buttons} - </div> - </div> - </div> - ); - } - -} diff --git a/app/javascript/themes/glitch/components/attachment_list.js b/app/javascript/themes/glitch/components/attachment_list.js deleted file mode 100644 index b3d00b335..000000000 --- a/app/javascript/themes/glitch/components/attachment_list.js +++ /dev/null @@ -1,33 +0,0 @@ -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import ImmutablePureComponent from 'react-immutable-pure-component'; - -const filename = url => url.split('/').pop().split('#')[0].split('?')[0]; - -export default class AttachmentList extends ImmutablePureComponent { - - static propTypes = { - media: ImmutablePropTypes.list.isRequired, - }; - - 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> - ); - } - -} diff --git a/app/javascript/themes/glitch/components/autosuggest_emoji.js b/app/javascript/themes/glitch/components/autosuggest_emoji.js deleted file mode 100644 index 3c6f915e4..000000000 --- a/app/javascript/themes/glitch/components/autosuggest_emoji.js +++ /dev/null @@ -1,42 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import unicodeMapping from 'themes/glitch/util/emoji/emoji_unicode_mapping_light'; - -const assetHost = process.env.CDN_HOST || ''; - -export default class AutosuggestEmoji extends React.PureComponent { - - static propTypes = { - emoji: PropTypes.object.isRequired, - }; - - render () { - const { emoji } = this.props; - let url; - - if (emoji.custom) { - url = emoji.imageUrl; - } else { - const mapping = unicodeMapping[emoji.native] || unicodeMapping[emoji.native.replace(/\uFE0F$/, '')]; - - if (!mapping) { - return null; - } - - url = `${assetHost}/emoji/${mapping.filename}.svg`; - } - - return ( - <div className='autosuggest-emoji'> - <img - className='emojione' - src={url} - alt={emoji.native || emoji.colons} - /> - - {emoji.colons} - </div> - ); - } - -} diff --git a/app/javascript/themes/glitch/components/autosuggest_textarea.js b/app/javascript/themes/glitch/components/autosuggest_textarea.js deleted file mode 100644 index fa93847a2..000000000 --- a/app/javascript/themes/glitch/components/autosuggest_textarea.js +++ /dev/null @@ -1,222 +0,0 @@ -import React from 'react'; -import AutosuggestAccountContainer from 'themes/glitch/features/compose/containers/autosuggest_account_container'; -import AutosuggestEmoji from './autosuggest_emoji'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import PropTypes from 'prop-types'; -import { isRtl } from 'themes/glitch/util/rtl'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import Textarea from 'react-textarea-autosize'; -import classNames from 'classnames'; - -const textAtCursorMatchesToken = (str, caretPosition) => { - let word; - - let left = str.slice(0, caretPosition).search(/[^\s\u200B]+$/); - let right = str.slice(caretPosition).search(/[\s\u200B]/); - - if (right < 0) { - word = str.slice(left); - } else { - word = str.slice(left, right + caretPosition); - } - - if (!word || word.trim().length < 3 || ['@', ':'].indexOf(word[0]) === -1) { - return [null, null]; - } - - word = word.trim().toLowerCase(); - - if (word.length > 0) { - return [left + 1, word]; - } else { - return [null, null]; - } -}; - -export default class AutosuggestTextarea extends ImmutablePureComponent { - - static 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, - autoFocus: PropTypes.bool, - }; - - static defaultProps = { - autoFocus: true, - }; - - state = { - suggestionsHidden: false, - selectedSuggestion: 0, - lastToken: null, - tokenStart: 0, - }; - - 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(); - } - - 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); - } - - onKeyUp = e => { - if (e.key === 'Escape' && this.state.suggestionsHidden) { - document.querySelector('.ui').parentElement.focus(); - } - - if (this.props.onKeyUp) { - this.props.onKeyUp(e); - } - } - - onBlur = () => { - this.setState({ suggestionsHidden: true }); - } - - onSuggestionClick = (e) => { - const suggestion = this.props.suggestions.get(e.currentTarget.getAttribute('data-index')); - 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(); - } - } - - renderSuggestion = (suggestion, i) => { - const { selectedSuggestion } = this.state; - let inner, key; - - if (typeof suggestion === 'object') { - inner = <AutosuggestEmoji emoji={suggestion} />; - key = suggestion.id; - } else { - inner = <AutosuggestAccountContainer id={suggestion} />; - key = suggestion; - } - - return ( - <div role='button' tabIndex='0' key={key} data-index={i} className={classNames('autosuggest-textarea__suggestions__item', { selected: i === selectedSuggestion })} onMouseDown={this.onSuggestionClick}> - {inner} - </div> - ); - } - - render () { - const { value, suggestions, disabled, placeholder, autoFocus } = this.props; - const { suggestionsHidden } = this.state; - const style = { direction: 'ltr' }; - - if (isRtl(value)) { - style.direction = 'rtl'; - } - - return ( - <div className='autosuggest-textarea'> - <label> - <span style={{ display: 'none' }}>{placeholder}</span> - - <Textarea - inputRef={this.setTextarea} - className='autosuggest-textarea__textarea' - disabled={disabled} - placeholder={placeholder} - autoFocus={autoFocus} - value={value} - onChange={this.onChange} - onKeyDown={this.onKeyDown} - onKeyUp={this.onKeyUp} - onBlur={this.onBlur} - onPaste={this.onPaste} - style={style} - /> - </label> - - <div className={`autosuggest-textarea__suggestions ${suggestionsHidden || suggestions.isEmpty() ? '' : 'autosuggest-textarea__suggestions--visible'}`}> - {suggestions.map(this.renderSuggestion)} - </div> - </div> - ); - } - -} diff --git a/app/javascript/themes/glitch/components/avatar.js b/app/javascript/themes/glitch/components/avatar.js deleted file mode 100644 index dd155f059..000000000 --- a/app/javascript/themes/glitch/components/avatar.js +++ /dev/null @@ -1,72 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; - -export default class Avatar extends React.PureComponent { - - static propTypes = { - account: ImmutablePropTypes.map.isRequired, - size: PropTypes.number.isRequired, - style: PropTypes.object, - animate: PropTypes.bool, - inline: PropTypes.bool, - }; - - static defaultProps = { - animate: false, - size: 20, - inline: false, - }; - - state = { - hovering: false, - }; - - handleMouseEnter = () => { - if (this.props.animate) return; - this.setState({ hovering: true }); - } - - handleMouseLeave = () => { - if (this.props.animate) return; - this.setState({ hovering: false }); - } - - render () { - const { account, size, animate, inline } = this.props; - const { hovering } = this.state; - - const src = account.get('avatar'); - const staticSrc = account.get('avatar_static'); - - let className = 'account__avatar'; - - if (inline) { - className = className + ' account__avatar-inline'; - } - - 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={className} - onMouseEnter={this.handleMouseEnter} - onMouseLeave={this.handleMouseLeave} - style={style} - data-avatar-of={`@${account.get('acct')}`} - /> - ); - } - -} diff --git a/app/javascript/themes/glitch/components/avatar_overlay.js b/app/javascript/themes/glitch/components/avatar_overlay.js deleted file mode 100644 index 2ecf9fa44..000000000 --- a/app/javascript/themes/glitch/components/avatar_overlay.js +++ /dev/null @@ -1,30 +0,0 @@ -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; - -export default class AvatarOverlay extends React.PureComponent { - - static propTypes = { - account: ImmutablePropTypes.map.isRequired, - friend: ImmutablePropTypes.map.isRequired, - }; - - render() { - const { account, friend } = this.props; - - const baseStyle = { - backgroundImage: `url(${account.get('avatar_static')})`, - }; - - const overlayStyle = { - backgroundImage: `url(${friend.get('avatar_static')})`, - }; - - return ( - <div className='account__avatar-overlay'> - <div className='account__avatar-overlay-base' style={baseStyle} data-avatar-of={`@${account.get('acct')}`} /> - <div className='account__avatar-overlay-overlay' style={overlayStyle} data-avatar-of={`@${friend.get('acct')}`} /> - </div> - ); - } - -} diff --git a/app/javascript/themes/glitch/components/button.js b/app/javascript/themes/glitch/components/button.js deleted file mode 100644 index 16868010c..000000000 --- a/app/javascript/themes/glitch/components/button.js +++ /dev/null @@ -1,64 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import classNames from 'classnames'; - -export default class Button extends React.PureComponent { - - static propTypes = { - text: PropTypes.node, - onClick: PropTypes.func, - disabled: PropTypes.bool, - block: PropTypes.bool, - secondary: PropTypes.bool, - size: PropTypes.number, - className: PropTypes.string, - style: PropTypes.object, - children: PropTypes.node, - title: PropTypes.string, - }; - - static defaultProps = { - size: 36, - }; - - handleClick = (e) => { - if (!this.props.disabled) { - this.props.onClick(e); - } - } - - setRef = (c) => { - this.node = c; - } - - focus() { - this.node.focus(); - } - - render () { - let attrs = { - className: classNames('button', this.props.className, { - 'button-secondary': this.props.secondary, - 'button--block': this.props.block, - }), - disabled: this.props.disabled, - onClick: this.handleClick, - ref: this.setRef, - style: { - padding: `0 ${this.props.size / 2.25}px`, - height: `${this.props.size}px`, - lineHeight: `${this.props.size}px`, - ...this.props.style, - }, - }; - - if (this.props.title) attrs.title = this.props.title; - - return ( - <button {...attrs}> - {this.props.text || this.props.children} - </button> - ); - } - -} diff --git a/app/javascript/themes/glitch/components/collapsable.js b/app/javascript/themes/glitch/components/collapsable.js deleted file mode 100644 index 8bc0a54f4..000000000 --- a/app/javascript/themes/glitch/components/collapsable.js +++ /dev/null @@ -1,22 +0,0 @@ -import React from 'react'; -import Motion from 'themes/glitch/util/optional_motion'; -import spring from 'react-motion/lib/spring'; -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/javascript/themes/glitch/components/column.js b/app/javascript/themes/glitch/components/column.js deleted file mode 100644 index adeba9cc1..000000000 --- a/app/javascript/themes/glitch/components/column.js +++ /dev/null @@ -1,54 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import detectPassiveEvents from 'detect-passive-events'; -import { scrollTop } from 'themes/glitch/util/scroll'; - -export default class Column extends React.PureComponent { - - static propTypes = { - children: PropTypes.node, - extraClasses: PropTypes.string, - name: PropTypes.string, - }; - - scrollTop () { - const scrollable = this.node.querySelector('.scrollable'); - - if (!scrollable) { - return; - } - - this._interruptScrollAnimation = scrollTop(scrollable); - } - - handleWheel = () => { - if (typeof this._interruptScrollAnimation !== 'function') { - return; - } - - this._interruptScrollAnimation(); - } - - setRef = c => { - this.node = c; - } - - componentDidMount () { - this.node.addEventListener('wheel', this.handleWheel, detectPassiveEvents.hasSupport ? { passive: true } : false); - } - - componentWillUnmount () { - this.node.removeEventListener('wheel', this.handleWheel); - } - - render () { - const { children, extraClasses, name } = this.props; - - return ( - <div role='region' data-column={name} className={`column ${extraClasses || ''}`} ref={this.setRef}> - {children} - </div> - ); - } - -} diff --git a/app/javascript/themes/glitch/components/column_back_button.js b/app/javascript/themes/glitch/components/column_back_button.js deleted file mode 100644 index 50c3bf11f..000000000 --- a/app/javascript/themes/glitch/components/column_back_button.js +++ /dev/null @@ -1,29 +0,0 @@ -import React from 'react'; -import { FormattedMessage } from 'react-intl'; -import PropTypes from 'prop-types'; - -export default class ColumnBackButton extends React.PureComponent { - - static contextTypes = { - router: PropTypes.object, - }; - - handleClick = () => { - // if history is exhausted, or we would leave mastodon, just go to root. - if (window.history && (window.history.length === 1 || window.history.length === window._mastoInitialHistoryLen)) { - this.context.router.history.push('/'); - } else { - this.context.router.history.goBack(); - } - } - - render () { - return ( - <button 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' /> - </button> - ); - } - -} diff --git a/app/javascript/themes/glitch/components/column_back_button_slim.js b/app/javascript/themes/glitch/components/column_back_button_slim.js deleted file mode 100644 index 2cdf1b25b..000000000 --- a/app/javascript/themes/glitch/components/column_back_button_slim.js +++ /dev/null @@ -1,31 +0,0 @@ -import React from 'react'; -import { FormattedMessage } from 'react-intl'; -import PropTypes from 'prop-types'; - -export default class ColumnBackButtonSlim extends React.PureComponent { - - static contextTypes = { - router: PropTypes.object, - }; - - handleClick = () => { - // if history is exhausted, or we would leave mastodon, just go to root. - if (window.history && (window.history.length === 1 || window.history.length === window._mastoInitialHistoryLen)) { - this.context.router.history.push('/'); - } else { - this.context.router.history.goBack(); - } - } - - 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> - ); - } - -} diff --git a/app/javascript/themes/glitch/components/column_header.js b/app/javascript/themes/glitch/components/column_header.js deleted file mode 100644 index e601082c8..000000000 --- a/app/javascript/themes/glitch/components/column_header.js +++ /dev/null @@ -1,214 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import classNames from 'classnames'; -import { defineMessages, FormattedMessage, injectIntl } from 'react-intl'; -import ImmutablePropTypes from 'react-immutable-proptypes'; - -// Glitch imports -import NotificationPurgeButtonsContainer from 'themes/glitch/containers/notification_purge_buttons_container'; - -const messages = defineMessages({ - show: { id: 'column_header.show_settings', defaultMessage: 'Show settings' }, - hide: { id: 'column_header.hide_settings', defaultMessage: 'Hide settings' }, - moveLeft: { id: 'column_header.moveLeft_settings', defaultMessage: 'Move column to the left' }, - moveRight: { id: 'column_header.moveRight_settings', defaultMessage: 'Move column to the right' }, - enterNotifCleaning : { id: 'notification_purge.start', defaultMessage: 'Enter notification cleaning mode' }, -}); - -@injectIntl -export default class ColumnHeader extends React.PureComponent { - - static contextTypes = { - router: PropTypes.object, - }; - - static propTypes = { - intl: PropTypes.object.isRequired, - title: PropTypes.node.isRequired, - icon: PropTypes.string.isRequired, - active: PropTypes.bool, - localSettings : ImmutablePropTypes.map, - multiColumn: PropTypes.bool, - focusable: PropTypes.bool, - showBackButton: PropTypes.bool, - notifCleaning: PropTypes.bool, // true only for the notification column - notifCleaningActive: PropTypes.bool, - onEnterCleaningMode: PropTypes.func, - children: PropTypes.node, - pinned: PropTypes.bool, - onPin: PropTypes.func, - onMove: PropTypes.func, - onClick: PropTypes.func, - intl: PropTypes.object.isRequired, - }; - - static defaultProps = { - focusable: true, - } - - state = { - collapsed: true, - animating: false, - animatingNCD: false, - }; - - handleToggleClick = (e) => { - e.stopPropagation(); - this.setState({ collapsed: !this.state.collapsed, animating: true }); - } - - handleTitleClick = () => { - this.props.onClick(); - } - - handleMoveLeft = () => { - this.props.onMove(-1); - } - - handleMoveRight = () => { - this.props.onMove(1); - } - - handleBackClick = () => { - // if history is exhausted, or we would leave mastodon, just go to root. - if (window.history && (window.history.length === 1 || window.history.length === window._mastoInitialHistoryLen)) { - this.context.router.history.push('/'); - } else { - this.context.router.history.goBack(); - } - } - - handleTransitionEnd = () => { - this.setState({ animating: false }); - } - - handleTransitionEndNCD = () => { - this.setState({ animatingNCD: false }); - } - - onEnterCleaningMode = () => { - this.setState({ animatingNCD: true }); - this.props.onEnterCleaningMode(!this.props.notifCleaningActive); - } - - render () { - const { intl, icon, active, children, pinned, onPin, multiColumn, focusable, showBackButton, intl: { formatMessage }, notifCleaning, notifCleaningActive } = this.props; - const { collapsed, animating, animatingNCD } = this.state; - - let title = this.props.title; - - const wrapperClassName = classNames('column-header__wrapper', { - 'active': active, - }); - - const buttonClassName = classNames('column-header', { - 'active': active, - }); - - const collapsibleClassName = classNames('column-header__collapsible', { - 'collapsed': collapsed, - 'animating': animating, - }); - - const collapsibleButtonClassName = classNames('column-header__button', { - 'active': !collapsed, - }); - - const notifCleaningButtonClassName = classNames('column-header__button', { - 'active': notifCleaningActive, - }); - - const notifCleaningDrawerClassName = classNames('ncd column-header__collapsible', { - 'collapsed': !notifCleaningActive, - 'animating': animatingNCD, - }); - - let extraContent, pinButton, moveButtons, backButton, collapseButton; - - //*glitch - const msgEnterNotifCleaning = intl.formatMessage(messages.enterNotifCleaning); - - if (children) { - extraContent = ( - <div key='extra-content' className='column-header__collapsible__extra'> - {children} - </div> - ); - } - - if (multiColumn && pinned) { - pinButton = <button key='pin-button' className='text-btn column-header__setting-btn' onClick={onPin}><i className='fa fa fa-times' /> <FormattedMessage id='column_header.unpin' defaultMessage='Unpin' /></button>; - - moveButtons = ( - <div key='move-buttons' className='column-header__setting-arrows'> - <button title={formatMessage(messages.moveLeft)} aria-label={formatMessage(messages.moveLeft)} className='text-btn column-header__setting-btn' onClick={this.handleMoveLeft}><i className='fa fa-chevron-left' /></button> - <button title={formatMessage(messages.moveRight)} aria-label={formatMessage(messages.moveRight)} className='text-btn column-header__setting-btn' onClick={this.handleMoveRight}><i className='fa fa-chevron-right' /></button> - </div> - ); - } else if (multiColumn) { - pinButton = <button key='pin-button' className='text-btn column-header__setting-btn' onClick={onPin}><i className='fa fa fa-plus' /> <FormattedMessage id='column_header.pin' defaultMessage='Pin' /></button>; - } - - if (!pinned && (multiColumn || showBackButton)) { - backButton = ( - <button onClick={this.handleBackClick} className='column-header__back-button'> - <i className='fa fa-fw fa-chevron-left column-back-button__icon' /> - <FormattedMessage id='column_back_button.label' defaultMessage='Back' /> - </button> - ); - } - - const collapsedContent = [ - extraContent, - ]; - - if (multiColumn) { - collapsedContent.push(moveButtons); - collapsedContent.push(pinButton); - } - - if (children || multiColumn) { - collapseButton = <button className={collapsibleButtonClassName} aria-label={formatMessage(collapsed ? messages.show : messages.hide)} aria-pressed={collapsed ? 'false' : 'true'} onClick={this.handleToggleClick}><i className='fa fa-sliders' /></button>; - } - - return ( - <div className={wrapperClassName}> - <h1 tabIndex={focusable ? 0 : null} role='button' className={buttonClassName} aria-label={title} onClick={this.handleTitleClick}> - <i className={`fa fa-fw fa-${icon} column-header__icon`} /> - <span className='column-header__title'> - {title} - </span> - <div className='column-header__buttons'> - {backButton} - { notifCleaning ? ( - <button - aria-label={msgEnterNotifCleaning} - title={msgEnterNotifCleaning} - onClick={this.onEnterCleaningMode} - className={notifCleaningButtonClassName} - > - <i className='fa fa-eraser' /> - </button> - ) : null} - {collapseButton} - </div> - </h1> - - { notifCleaning ? ( - <div className={notifCleaningDrawerClassName} onTransitionEnd={this.handleTransitionEndNCD}> - <div className='column-header__collapsible-inner nopad-drawer'> - {(notifCleaningActive || animatingNCD) ? (<NotificationPurgeButtonsContainer />) : null } - </div> - </div> - ) : null} - - <div className={collapsibleClassName} tabIndex={collapsed ? -1 : null} onTransitionEnd={this.handleTransitionEnd}> - <div className='column-header__collapsible-inner'> - {(!collapsed || animating) && collapsedContent} - </div> - </div> - </div> - ); - } - -} diff --git a/app/javascript/themes/glitch/components/display_name.js b/app/javascript/themes/glitch/components/display_name.js deleted file mode 100644 index 2cf84f8f4..000000000 --- a/app/javascript/themes/glitch/components/display_name.js +++ /dev/null @@ -1,20 +0,0 @@ -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; - -export default class DisplayName extends React.PureComponent { - - static propTypes = { - account: ImmutablePropTypes.map.isRequired, - }; - - render () { - const displayNameHtml = { __html: this.props.account.get('display_name_html') }; - - return ( - <span className='display-name'> - <strong className='display-name__html' dangerouslySetInnerHTML={displayNameHtml} /> <span className='display-name__account'>@{this.props.account.get('acct')}</span> - </span> - ); - } - -} diff --git a/app/javascript/themes/glitch/components/dropdown_menu.js b/app/javascript/themes/glitch/components/dropdown_menu.js deleted file mode 100644 index d30dc2aaf..000000000 --- a/app/javascript/themes/glitch/components/dropdown_menu.js +++ /dev/null @@ -1,211 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import IconButton from './icon_button'; -import Overlay from 'react-overlays/lib/Overlay'; -import Motion from 'themes/glitch/util/optional_motion'; -import spring from 'react-motion/lib/spring'; -import detectPassiveEvents from 'detect-passive-events'; - -const listenerOptions = detectPassiveEvents.hasSupport ? { passive: true } : false; - -class DropdownMenu extends React.PureComponent { - - static contextTypes = { - router: PropTypes.object, - }; - - static propTypes = { - items: PropTypes.array.isRequired, - onClose: PropTypes.func.isRequired, - style: PropTypes.object, - placement: PropTypes.string, - arrowOffsetLeft: PropTypes.string, - arrowOffsetTop: PropTypes.string, - }; - - static defaultProps = { - style: {}, - placement: 'bottom', - }; - - 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; - } - - handleClick = e => { - const i = Number(e.currentTarget.getAttribute('data-index')); - const { action, to } = this.props.items[i]; - - this.props.onClose(); - - if (typeof action === 'function') { - e.preventDefault(); - action(); - } else if (to) { - e.preventDefault(); - this.context.router.history.push(to); - } - } - - renderItem (option, i) { - if (option === null) { - return <li key={`sep-${i}`} className='dropdown-menu__separator' />; - } - - const { text, href = '#' } = option; - - return ( - <li className='dropdown-menu__item' key={`${text}-${i}`}> - <a href={href} target='_blank' rel='noopener' role='button' tabIndex='0' autoFocus={i === 0} onClick={this.handleClick} data-index={i}> - {text} - </a> - </li> - ); - } - - render () { - const { items, style, placement, arrowOffsetLeft, arrowOffsetTop } = this.props; - - return ( - <Motion defaultStyle={{ opacity: 0, scaleX: 0.85, scaleY: 0.75 }} style={{ opacity: spring(1, { damping: 35, stiffness: 400 }), scaleX: spring(1, { damping: 35, stiffness: 400 }), scaleY: spring(1, { damping: 35, stiffness: 400 }) }}> - {({ opacity, scaleX, scaleY }) => ( - <div className='dropdown-menu' style={{ ...style, opacity: opacity, transform: `scale(${scaleX}, ${scaleY})` }} ref={this.setRef}> - <div className={`dropdown-menu__arrow ${placement}`} style={{ left: arrowOffsetLeft, top: arrowOffsetTop }} /> - - <ul> - {items.map((option, i) => this.renderItem(option, i))} - </ul> - </div> - )} - </Motion> - ); - } - -} - -export default class Dropdown extends React.PureComponent { - - static contextTypes = { - router: PropTypes.object, - }; - - static propTypes = { - icon: PropTypes.string.isRequired, - items: PropTypes.array.isRequired, - size: PropTypes.number.isRequired, - ariaLabel: PropTypes.string, - disabled: PropTypes.bool, - status: ImmutablePropTypes.map, - isUserTouching: PropTypes.func, - isModalOpen: PropTypes.bool.isRequired, - onModalOpen: PropTypes.func, - onModalClose: PropTypes.func, - }; - - static defaultProps = { - ariaLabel: 'Menu', - }; - - state = { - expanded: false, - }; - - handleClick = () => { - if (!this.state.expanded && this.props.isUserTouching() && this.props.onModalOpen) { - const { status, items } = this.props; - - this.props.onModalOpen({ - status, - actions: items, - onClick: this.handleItemClick, - }); - - return; - } - - this.setState({ expanded: !this.state.expanded }); - } - - handleClose = () => { - if (this.props.onModalClose) { - this.props.onModalClose(); - } - - this.setState({ expanded: false }); - } - - handleKeyDown = e => { - switch(e.key) { - case 'Enter': - this.handleClick(); - break; - case 'Escape': - this.handleClose(); - break; - } - } - - handleItemClick = e => { - const i = Number(e.currentTarget.getAttribute('data-index')); - const { action, to } = this.props.items[i]; - - this.handleClose(); - - if (typeof action === 'function') { - e.preventDefault(); - action(); - } else if (to) { - e.preventDefault(); - this.context.router.history.push(to); - } - } - - setTargetRef = c => { - this.target = c; - } - - findTarget = () => { - return this.target; - } - - render () { - const { icon, items, size, ariaLabel, disabled } = this.props; - const { expanded } = this.state; - - return ( - <div onKeyDown={this.handleKeyDown}> - <IconButton - icon={icon} - title={ariaLabel} - active={expanded} - disabled={disabled} - size={size} - ref={this.setTargetRef} - onClick={this.handleClick} - /> - - <Overlay show={expanded} placement='bottom' target={this.findTarget}> - <DropdownMenu items={items} onClose={this.handleClose} /> - </Overlay> - </div> - ); - } - -} diff --git a/app/javascript/themes/glitch/components/extended_video_player.js b/app/javascript/themes/glitch/components/extended_video_player.js deleted file mode 100644 index f8bd067e8..000000000 --- a/app/javascript/themes/glitch/components/extended_video_player.js +++ /dev/null @@ -1,54 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; - -export default class ExtendedVideoPlayer extends React.PureComponent { - - static propTypes = { - src: PropTypes.string.isRequired, - alt: PropTypes.string, - width: PropTypes.number, - height: PropTypes.number, - time: PropTypes.number, - controls: PropTypes.bool.isRequired, - muted: PropTypes.bool.isRequired, - }; - - 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 () { - const { src, muted, controls, alt } = this.props; - - return ( - <div className='extended-video-player'> - <video - ref={this.setRef} - src={src} - autoPlay - role='button' - tabIndex='0' - aria-label={alt} - muted={muted} - controls={controls} - loop={!controls} - /> - </div> - ); - } - -} diff --git a/app/javascript/themes/glitch/components/icon_button.js b/app/javascript/themes/glitch/components/icon_button.js deleted file mode 100644 index 31cdf4703..000000000 --- a/app/javascript/themes/glitch/components/icon_button.js +++ /dev/null @@ -1,137 +0,0 @@ -import React from 'react'; -import Motion from 'themes/glitch/util/optional_motion'; -import spring from 'react-motion/lib/spring'; -import PropTypes from 'prop-types'; -import classNames from 'classnames'; - -export default class IconButton extends React.PureComponent { - - static propTypes = { - className: PropTypes.string, - title: PropTypes.string.isRequired, - icon: PropTypes.string.isRequired, - onClick: PropTypes.func, - size: PropTypes.number, - active: PropTypes.bool, - pressed: PropTypes.bool, - expanded: PropTypes.bool, - style: PropTypes.object, - activeStyle: PropTypes.object, - disabled: PropTypes.bool, - inverted: PropTypes.bool, - animate: PropTypes.bool, - flip: PropTypes.bool, - overlay: PropTypes.bool, - tabIndex: PropTypes.string, - label: PropTypes.string, - }; - - static defaultProps = { - size: 18, - active: false, - disabled: false, - animate: false, - overlay: false, - tabIndex: '0', - }; - - handleClick = (e) => { - e.preventDefault(); - - if (!this.props.disabled) { - this.props.onClick(e); - } - } - - render () { - let style = { - fontSize: `${this.props.size}px`, - height: `${this.props.size * 1.28571429}px`, - lineHeight: `${this.props.size}px`, - ...this.props.style, - ...(this.props.active ? this.props.activeStyle : {}), - }; - if (!this.props.label) { - style.width = `${this.props.size * 1.28571429}px`; - } else { - style.textAlign = 'left'; - } - - const { - active, - animate, - className, - disabled, - expanded, - icon, - inverted, - flip, - overlay, - pressed, - tabIndex, - title, - } = this.props; - - const classes = classNames(className, 'icon-button', { - active, - disabled, - inverted, - overlayed: overlay, - }); - - const flipDeg = flip ? -180 : -360; - const rotateDeg = active ? flipDeg : 0; - - const motionDefaultStyle = { - rotate: rotateDeg, - }; - - const springOpts = { - stiffness: this.props.flip ? 60 : 120, - damping: 7, - }; - const motionStyle = { - rotate: animate ? spring(rotateDeg, springOpts) : 0, - }; - - if (!animate) { - // Perf optimization: avoid unnecessary <Motion> components unless - // we actually need to animate. - return ( - <button - aria-label={title} - aria-pressed={pressed} - aria-expanded={expanded} - title={title} - className={classes} - onClick={this.handleClick} - style={style} - tabIndex={tabIndex} - > - <i className={`fa fa-fw fa-${icon}`} aria-hidden='true' /> - </button> - ); - } - - return ( - <Motion defaultStyle={motionDefaultStyle} style={motionStyle}> - {({ rotate }) => - <button - aria-label={title} - aria-pressed={pressed} - aria-expanded={expanded} - title={title} - className={classes} - onClick={this.handleClick} - style={style} - tabIndex={tabIndex} - > - <i style={{ transform: `rotate(${rotate}deg)` }} className={`fa fa-fw fa-${icon}`} aria-hidden='true' /> - {this.props.label} - </button> - } - </Motion> - ); - } - -} diff --git a/app/javascript/themes/glitch/components/intersection_observer_article.js b/app/javascript/themes/glitch/components/intersection_observer_article.js deleted file mode 100644 index f0139ac75..000000000 --- a/app/javascript/themes/glitch/components/intersection_observer_article.js +++ /dev/null @@ -1,130 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import scheduleIdleTask from 'themes/glitch/util/schedule_idle_task'; -import getRectFromEntry from 'themes/glitch/util/get_rect_from_entry'; -import { is } from 'immutable'; - -// Diff these props in the "rendered" state -const updateOnPropsForRendered = ['id', 'index', 'listLength']; -// Diff these props in the "unrendered" state -const updateOnPropsForUnrendered = ['id', 'index', 'listLength', 'cachedHeight']; - -export default class IntersectionObserverArticle extends React.Component { - - static propTypes = { - intersectionObserverWrapper: PropTypes.object.isRequired, - id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), - index: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), - listLength: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), - saveHeightKey: PropTypes.string, - cachedHeight: PropTypes.number, - onHeightChange: PropTypes.func, - children: PropTypes.node, - }; - - state = { - isHidden: false, // set to true in requestIdleCallback to trigger un-render - } - - shouldComponentUpdate (nextProps, nextState) { - const isUnrendered = !this.state.isIntersecting && (this.state.isHidden || this.props.cachedHeight); - const willBeUnrendered = !nextState.isIntersecting && (nextState.isHidden || nextProps.cachedHeight); - if (!!isUnrendered !== !!willBeUnrendered) { - // If we're going from rendered to unrendered (or vice versa) then update - return true; - } - // Otherwise, diff based on props - const propsToDiff = isUnrendered ? updateOnPropsForUnrendered : updateOnPropsForRendered; - return !propsToDiff.every(prop => is(nextProps[prop], this.props[prop])); - } - - componentDidMount () { - const { intersectionObserverWrapper, id } = this.props; - - intersectionObserverWrapper.observe( - id, - this.node, - this.handleIntersection - ); - - this.componentMounted = true; - } - - componentWillUnmount () { - const { intersectionObserverWrapper, id } = this.props; - intersectionObserverWrapper.unobserve(id, this.node); - - this.componentMounted = false; - } - - handleIntersection = (entry) => { - this.entry = entry; - - scheduleIdleTask(this.calculateHeight); - this.setState(this.updateStateAfterIntersection); - } - - updateStateAfterIntersection = (prevState) => { - if (prevState.isIntersecting && !this.entry.isIntersecting) { - scheduleIdleTask(this.hideIfNotIntersecting); - } - return { - isIntersecting: this.entry.isIntersecting, - isHidden: false, - }; - } - - calculateHeight = () => { - const { onHeightChange, saveHeightKey, id } = this.props; - // save the height of the fully-rendered element (this is expensive - // on Chrome, where we need to fall back to getBoundingClientRect) - this.height = getRectFromEntry(this.entry).height; - - if (onHeightChange && saveHeightKey) { - onHeightChange(saveHeightKey, id, this.height); - } - } - - hideIfNotIntersecting = () => { - if (!this.componentMounted) { - return; - } - - // When the browser gets a chance, test if we're still not intersecting, - // and if so, set our isHidden to true to trigger an unrender. The point of - // this is to save DOM nodes and avoid using up too much memory. - // See: https://github.com/tootsuite/mastodon/issues/2900 - this.setState((prevState) => ({ isHidden: !prevState.isIntersecting })); - } - - handleRef = (node) => { - this.node = node; - } - - render () { - const { children, id, index, listLength, cachedHeight } = this.props; - const { isIntersecting, isHidden } = this.state; - - if (!isIntersecting && (isHidden || cachedHeight)) { - return ( - <article - ref={this.handleRef} - aria-posinset={index} - aria-setsize={listLength} - style={{ height: `${this.height || cachedHeight}px`, opacity: 0, overflow: 'hidden' }} - data-id={id} - tabIndex='0' - > - {children && React.cloneElement(children, { hidden: true })} - </article> - ); - } - - return ( - <article ref={this.handleRef} aria-posinset={index} aria-setsize={listLength} data-id={id} tabIndex='0'> - {children && React.cloneElement(children, { hidden: false })} - </article> - ); - } - -} diff --git a/app/javascript/themes/glitch/components/load_more.js b/app/javascript/themes/glitch/components/load_more.js deleted file mode 100644 index c4c8c94a2..000000000 --- a/app/javascript/themes/glitch/components/load_more.js +++ /dev/null @@ -1,26 +0,0 @@ -import React from 'react'; -import { FormattedMessage } from 'react-intl'; -import PropTypes from 'prop-types'; - -export default class LoadMore extends React.PureComponent { - - static propTypes = { - onClick: PropTypes.func, - visible: PropTypes.bool, - } - - static defaultProps = { - visible: true, - } - - render() { - const { visible } = this.props; - - return ( - <button className='load-more' disabled={!visible} style={{ visibility: visible ? 'visible' : 'hidden' }} onClick={this.props.onClick}> - <FormattedMessage id='status.load_more' defaultMessage='Load more' /> - </button> - ); - } - -} diff --git a/app/javascript/themes/glitch/components/loading_indicator.js b/app/javascript/themes/glitch/components/loading_indicator.js deleted file mode 100644 index d6a5adb6f..000000000 --- a/app/javascript/themes/glitch/components/loading_indicator.js +++ /dev/null @@ -1,11 +0,0 @@ -import React from 'react'; -import { FormattedMessage } from 'react-intl'; - -const LoadingIndicator = () => ( - <div className='loading-indicator'> - <div className='loading-indicator__figure' /> - <FormattedMessage id='loading_indicator.label' defaultMessage='Loading...' /> - </div> -); - -export default LoadingIndicator; diff --git a/app/javascript/themes/glitch/components/media_gallery.js b/app/javascript/themes/glitch/components/media_gallery.js deleted file mode 100644 index b6b40c585..000000000 --- a/app/javascript/themes/glitch/components/media_gallery.js +++ /dev/null @@ -1,255 +0,0 @@ -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import PropTypes from 'prop-types'; -import { is } from 'immutable'; -import IconButton from './icon_button'; -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; -import { isIOS } from 'themes/glitch/util/is_mobile'; -import classNames from 'classnames'; -import { autoPlayGif } from 'themes/glitch/util/initial_state'; - -const messages = defineMessages({ - toggle_visible: { id: 'media_gallery.toggle_visible', defaultMessage: 'Toggle visibility' }, -}); - -class Item extends React.PureComponent { - - static contextTypes = { - router: PropTypes.object, - }; - - static propTypes = { - attachment: ImmutablePropTypes.map.isRequired, - standalone: PropTypes.bool, - index: PropTypes.number.isRequired, - size: PropTypes.number.isRequired, - letterbox: PropTypes.bool, - onClick: PropTypes.func.isRequired, - }; - - static defaultProps = { - standalone: false, - index: 0, - size: 1, - }; - - handleMouseEnter = (e) => { - if (this.hoverToPlay()) { - e.target.play(); - } - } - - handleMouseLeave = (e) => { - if (this.hoverToPlay()) { - e.target.pause(); - e.target.currentTime = 0; - } - } - - hoverToPlay () { - const { attachment } = this.props; - return !autoPlayGif && attachment.get('type') === 'gifv'; - } - - handleClick = (e) => { - const { index, onClick } = this.props; - - if (this.context.router && e.button === 0) { - e.preventDefault(); - onClick(index); - } - - e.stopPropagation(); - } - - render () { - const { attachment, index, size, standalone, letterbox } = 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') { - const previewUrl = attachment.get('preview_url'); - const previewWidth = attachment.getIn(['meta', 'small', 'width']); - - const originalUrl = attachment.get('url'); - const originalWidth = attachment.getIn(['meta', 'original', 'width']); - - const hasSize = typeof originalWidth === 'number' && typeof previewWidth === 'number'; - - const srcSet = hasSize ? `${originalUrl} ${originalWidth}w, ${previewUrl} ${previewWidth}w` : null; - const sizes = hasSize ? `(min-width: 1025px) ${320 * (width / 100)}px, ${width}vw` : null; - - thumbnail = ( - <a - className='media-gallery__item-thumbnail' - href={attachment.get('remote_url') || originalUrl} - onClick={this.handleClick} - target='_blank' - > - <img className={letterbox ? 'letterbox' : null} src={previewUrl} srcSet={srcSet} sizes={sizes} alt={attachment.get('description')} title={attachment.get('description')} /> - </a> - ); - } else if (attachment.get('type') === 'gifv') { - const autoPlay = !isIOS() && autoPlayGif; - - thumbnail = ( - <div className={classNames('media-gallery__gifv', { autoplay: autoPlay })}> - <video - className={`media-gallery__item-gifv-thumbnail${letterbox ? ' letterbox' : ''}`} - aria-label={attachment.get('description')} - role='application' - src={attachment.get('url')} - onClick={this.handleClick} - onMouseEnter={this.handleMouseEnter} - onMouseLeave={this.handleMouseLeave} - autoPlay={autoPlay} - loop - muted - /> - - <span className='media-gallery__gifv__label'>GIF</span> - </div> - ); - } - - return ( - <div className={classNames('media-gallery__item', { standalone })} key={attachment.get('id')} style={{ left: left, top: top, right: right, bottom: bottom, width: `${width}%`, height: `${height}%` }}> - {thumbnail} - </div> - ); - } - -} - -@injectIntl -export default class MediaGallery extends React.PureComponent { - - static propTypes = { - sensitive: PropTypes.bool, - standalone: PropTypes.bool, - letterbox: PropTypes.bool, - fullwidth: PropTypes.bool, - media: ImmutablePropTypes.list.isRequired, - size: PropTypes.object, - onOpenMedia: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - }; - - static defaultProps = { - standalone: false, - }; - - state = { - visible: !this.props.sensitive, - }; - - componentWillReceiveProps (nextProps) { - if (!is(nextProps.media, this.props.media)) { - this.setState({ visible: !nextProps.sensitive }); - } - } - - handleOpen = () => { - this.setState({ visible: !this.state.visible }); - } - - handleClick = (index) => { - this.props.onOpenMedia(this.props.media, index); - } - - isStandaloneEligible() { - const { media, standalone } = this.props; - return standalone && media.size === 1 && media.getIn([0, 'meta', 'small', 'aspect']); - } - - render () { - const { media, intl, sensitive, letterbox, fullwidth } = this.props; - const { visible } = this.state; - const size = media.take(4).size; - - let children; - - if (!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 = ( - <button 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> - </button> - ); - } else { - if (this.isStandaloneEligible()) { - children = <Item standalone onClick={this.handleClick} attachment={media.get(0)} />; - } else { - children = media.take(4).map((attachment, i) => <Item key={attachment.get('id')} onClick={this.handleClick} attachment={attachment} index={i} size={size} letterbox={letterbox} />); - } - } - - return ( - <div className={`media-gallery size-${size} ${fullwidth ? 'full-width' : ''}`}> - <div className={classNames('spoiler-button', { 'spoiler-button--visible': visible })}> - <IconButton title={intl.formatMessage(messages.toggle_visible)} icon={visible ? 'eye' : 'eye-slash'} overlay onClick={this.handleOpen} /> - </div> - - {children} - </div> - ); - } - -} diff --git a/app/javascript/themes/glitch/components/missing_indicator.js b/app/javascript/themes/glitch/components/missing_indicator.js deleted file mode 100644 index 87df7f61c..000000000 --- a/app/javascript/themes/glitch/components/missing_indicator.js +++ /dev/null @@ -1,12 +0,0 @@ -import React from 'react'; -import { FormattedMessage } from 'react-intl'; - -const MissingIndicator = () => ( - <div className='missing-indicator'> - <div> - <FormattedMessage id='missing_indicator.label' defaultMessage='Not found' /> - </div> - </div> -); - -export default MissingIndicator; diff --git a/app/javascript/themes/glitch/components/notification_purge_buttons.js b/app/javascript/themes/glitch/components/notification_purge_buttons.js deleted file mode 100644 index e0c1543b0..000000000 --- a/app/javascript/themes/glitch/components/notification_purge_buttons.js +++ /dev/null @@ -1,58 +0,0 @@ -/** - * Buttons widget for controlling the notification clearing mode. - * In idle state, the cleaning mode button is shown. When the mode is active, - * a Confirm and Abort buttons are shown in its place. - */ - - -// Package imports // -import React from 'react'; -import PropTypes from 'prop-types'; -import { defineMessages, injectIntl } from 'react-intl'; -import ImmutablePureComponent from 'react-immutable-pure-component'; - -const messages = defineMessages({ - btnAll : { id: 'notification_purge.btn_all', defaultMessage: 'Select\nall' }, - btnNone : { id: 'notification_purge.btn_none', defaultMessage: 'Select\nnone' }, - btnInvert : { id: 'notification_purge.btn_invert', defaultMessage: 'Invert\nselection' }, - btnApply : { id: 'notification_purge.btn_apply', defaultMessage: 'Clear\nselected' }, -}); - -@injectIntl -export default class NotificationPurgeButtons extends ImmutablePureComponent { - - static propTypes = { - onDeleteMarked : PropTypes.func.isRequired, - onMarkAll : PropTypes.func.isRequired, - onMarkNone : PropTypes.func.isRequired, - onInvert : PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - markNewForDelete: PropTypes.bool, - }; - - render () { - const { intl, markNewForDelete } = this.props; - - //className='active' - return ( - <div className='column-header__notif-cleaning-buttons'> - <button onClick={this.props.onMarkAll} className={markNewForDelete ? 'active' : ''}> - <b>∀</b><br />{intl.formatMessage(messages.btnAll)} - </button> - - <button onClick={this.props.onMarkNone} className={!markNewForDelete ? 'active' : ''}> - <b>∅</b><br />{intl.formatMessage(messages.btnNone)} - </button> - - <button onClick={this.props.onInvert}> - <b>¬</b><br />{intl.formatMessage(messages.btnInvert)} - </button> - - <button onClick={this.props.onDeleteMarked}> - <i className='fa fa-trash' /><br />{intl.formatMessage(messages.btnApply)} - </button> - </div> - ); - } - -} diff --git a/app/javascript/themes/glitch/components/permalink.js b/app/javascript/themes/glitch/components/permalink.js deleted file mode 100644 index d726d37a2..000000000 --- a/app/javascript/themes/glitch/components/permalink.js +++ /dev/null @@ -1,34 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; - -export default class Permalink extends React.PureComponent { - - static contextTypes = { - router: PropTypes.object, - }; - - static propTypes = { - className: PropTypes.string, - href: PropTypes.string.isRequired, - to: PropTypes.string.isRequired, - children: PropTypes.node, - }; - - handleClick = (e) => { - if (this.context.router && e.button === 0 && !(e.ctrlKey || e.metaKey)) { - e.preventDefault(); - this.context.router.history.push(this.props.to); - } - } - - render () { - const { href, children, className, ...other } = this.props; - - return ( - <a target='_blank' href={href} onClick={this.handleClick} {...other} className={`permalink${className ? ' ' + className : ''}`}> - {children} - </a> - ); - } - -} diff --git a/app/javascript/themes/glitch/components/relative_timestamp.js b/app/javascript/themes/glitch/components/relative_timestamp.js deleted file mode 100644 index 51588e78c..000000000 --- a/app/javascript/themes/glitch/components/relative_timestamp.js +++ /dev/null @@ -1,147 +0,0 @@ -import React from 'react'; -import { injectIntl, defineMessages } from 'react-intl'; -import PropTypes from 'prop-types'; - -const messages = defineMessages({ - just_now: { id: 'relative_time.just_now', defaultMessage: 'now' }, - seconds: { id: 'relative_time.seconds', defaultMessage: '{number}s' }, - minutes: { id: 'relative_time.minutes', defaultMessage: '{number}m' }, - hours: { id: 'relative_time.hours', defaultMessage: '{number}h' }, - days: { id: 'relative_time.days', defaultMessage: '{number}d' }, -}); - -const dateFormatOptions = { - hour12: false, - year: 'numeric', - month: 'short', - day: '2-digit', - hour: '2-digit', - minute: '2-digit', -}; - -const shortDateFormatOptions = { - month: 'numeric', - day: 'numeric', -}; - -const SECOND = 1000; -const MINUTE = 1000 * 60; -const HOUR = 1000 * 60 * 60; -const DAY = 1000 * 60 * 60 * 24; - -const MAX_DELAY = 2147483647; - -const selectUnits = delta => { - const absDelta = Math.abs(delta); - - if (absDelta < MINUTE) { - return 'second'; - } else if (absDelta < HOUR) { - return 'minute'; - } else if (absDelta < DAY) { - return 'hour'; - } - - return 'day'; -}; - -const getUnitDelay = units => { - switch (units) { - case 'second': - return SECOND; - case 'minute': - return MINUTE; - case 'hour': - return HOUR; - case 'day': - return DAY; - default: - return MAX_DELAY; - } -}; - -@injectIntl -export default class RelativeTimestamp extends React.Component { - - static propTypes = { - intl: PropTypes.object.isRequired, - timestamp: PropTypes.string.isRequired, - }; - - state = { - now: this.props.intl.now(), - }; - - shouldComponentUpdate (nextProps, nextState) { - // As of right now the locale doesn't change without a new page load, - // but we might as well check in case that ever changes. - return this.props.timestamp !== nextProps.timestamp || - this.props.intl.locale !== nextProps.intl.locale || - this.state.now !== nextState.now; - } - - componentWillReceiveProps (nextProps) { - if (this.props.timestamp !== nextProps.timestamp) { - this.setState({ now: this.props.intl.now() }); - } - } - - componentDidMount () { - this._scheduleNextUpdate(this.props, this.state); - } - - componentWillUpdate (nextProps, nextState) { - this._scheduleNextUpdate(nextProps, nextState); - } - - componentWillUnmount () { - clearTimeout(this._timer); - } - - _scheduleNextUpdate (props, state) { - clearTimeout(this._timer); - - const { timestamp } = props; - const delta = (new Date(timestamp)).getTime() - state.now; - const unitDelay = getUnitDelay(selectUnits(delta)); - const unitRemainder = Math.abs(delta % unitDelay); - const updateInterval = 1000 * 10; - const delay = delta < 0 ? Math.max(updateInterval, unitDelay - unitRemainder) : Math.max(updateInterval, unitRemainder); - - this._timer = setTimeout(() => { - this.setState({ now: this.props.intl.now() }); - }, delay); - } - - render () { - const { timestamp, intl } = this.props; - - const date = new Date(timestamp); - const delta = this.state.now - date.getTime(); - - let relativeTime; - - if (delta < 10 * SECOND) { - relativeTime = intl.formatMessage(messages.just_now); - } else if (delta < 3 * DAY) { - if (delta < MINUTE) { - relativeTime = intl.formatMessage(messages.seconds, { number: Math.floor(delta / SECOND) }); - } else if (delta < HOUR) { - relativeTime = intl.formatMessage(messages.minutes, { number: Math.floor(delta / MINUTE) }); - } else if (delta < DAY) { - relativeTime = intl.formatMessage(messages.hours, { number: Math.floor(delta / HOUR) }); - } else { - relativeTime = intl.formatMessage(messages.days, { number: Math.floor(delta / DAY) }); - } - } else { - relativeTime = intl.formatDate(date, shortDateFormatOptions); - } - - return ( - <time dateTime={timestamp} title={intl.formatDate(date, dateFormatOptions)}> - {relativeTime} - </time> - ); - } - -} diff --git a/app/javascript/themes/glitch/components/scrollable_list.js b/app/javascript/themes/glitch/components/scrollable_list.js deleted file mode 100644 index ccdcd7c85..000000000 --- a/app/javascript/themes/glitch/components/scrollable_list.js +++ /dev/null @@ -1,198 +0,0 @@ -import React, { PureComponent } from 'react'; -import { ScrollContainer } from 'react-router-scroll-4'; -import PropTypes from 'prop-types'; -import IntersectionObserverArticleContainer from 'themes/glitch/containers/intersection_observer_article_container'; -import LoadMore from './load_more'; -import IntersectionObserverWrapper from 'themes/glitch/util/intersection_observer_wrapper'; -import { throttle } from 'lodash'; -import { List as ImmutableList } from 'immutable'; -import classNames from 'classnames'; -import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from 'themes/glitch/util/fullscreen'; - -export default class ScrollableList extends PureComponent { - - static contextTypes = { - router: PropTypes.object, - }; - - static propTypes = { - scrollKey: PropTypes.string.isRequired, - onScrollToBottom: PropTypes.func, - onScrollToTop: PropTypes.func, - onScroll: PropTypes.func, - trackScroll: PropTypes.bool, - shouldUpdateScroll: PropTypes.func, - isLoading: PropTypes.bool, - hasMore: PropTypes.bool, - prepend: PropTypes.node, - emptyMessage: PropTypes.node, - children: PropTypes.node, - }; - - static defaultProps = { - trackScroll: true, - }; - - state = { - lastMouseMove: null, - }; - - intersectionObserverWrapper = new IntersectionObserverWrapper(); - - handleScroll = throttle(() => { - if (this.node) { - const { scrollTop, scrollHeight, clientHeight } = this.node; - const offset = scrollHeight - scrollTop - clientHeight; - this._oldScrollPosition = scrollHeight - scrollTop; - - if (400 > 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(); - } - } - }, 150, { - trailing: true, - }); - - handleMouseMove = throttle(() => { - this._lastMouseMove = new Date(); - }, 300); - - handleMouseLeave = () => { - this._lastMouseMove = null; - } - - componentDidMount () { - this.attachScrollListener(); - this.attachIntersectionObserver(); - attachFullscreenListener(this.onFullScreenChange); - - // Handle initial scroll posiiton - this.handleScroll(); - } - - componentDidUpdate (prevProps) { - const someItemInserted = React.Children.count(prevProps.children) > 0 && - React.Children.count(prevProps.children) < React.Children.count(this.props.children) && - this.getFirstChildKey(prevProps) !== this.getFirstChildKey(this.props); - - // Reset the scroll position when a new child comes in in order not to - // jerk the scrollbar around if you're already scrolled down the page. - if (someItemInserted && this._oldScrollPosition && this.node.scrollTop > 0) { - const newScrollTop = this.node.scrollHeight - this._oldScrollPosition; - - if (this.node.scrollTop !== newScrollTop) { - this.node.scrollTop = newScrollTop; - } - } else { - this._oldScrollPosition = this.node.scrollHeight - this.node.scrollTop; - } - } - - componentWillUnmount () { - this.detachScrollListener(); - this.detachIntersectionObserver(); - detachFullscreenListener(this.onFullScreenChange); - } - - onFullScreenChange = () => { - this.setState({ fullscreen: isFullscreen() }); - } - - attachIntersectionObserver () { - this.intersectionObserverWrapper.connect({ - root: this.node, - rootMargin: '300% 0px', - }); - } - - detachIntersectionObserver () { - this.intersectionObserverWrapper.disconnect(); - } - - attachScrollListener () { - this.node.addEventListener('scroll', this.handleScroll); - } - - detachScrollListener () { - this.node.removeEventListener('scroll', this.handleScroll); - } - - getFirstChildKey (props) { - const { children } = props; - let firstChild = children; - if (children instanceof ImmutableList) { - firstChild = children.get(0); - } else if (Array.isArray(children)) { - firstChild = children[0]; - } - return firstChild && firstChild.key; - } - - setRef = (c) => { - this.node = c; - } - - handleLoadMore = (e) => { - e.preventDefault(); - this.props.onScrollToBottom(); - } - - _recentlyMoved () { - return this._lastMouseMove !== null && ((new Date()) - this._lastMouseMove < 600); - } - - render () { - const { children, scrollKey, trackScroll, shouldUpdateScroll, isLoading, hasMore, prepend, emptyMessage } = this.props; - const { fullscreen } = this.state; - const childrenCount = React.Children.count(children); - - const loadMore = (hasMore && childrenCount > 0) ? <LoadMore visible={!isLoading} onClick={this.handleLoadMore} /> : null; - let scrollableArea = null; - - if (isLoading || childrenCount > 0 || !emptyMessage) { - scrollableArea = ( - <div className={classNames('scrollable', { fullscreen })} ref={this.setRef} onMouseMove={this.handleMouseMove} onMouseLeave={this.handleMouseLeave}> - <div role='feed' className='item-list'> - {prepend} - - {React.Children.map(this.props.children, (child, index) => ( - <IntersectionObserverArticleContainer - key={child.key} - id={child.key} - index={index} - listLength={childrenCount} - intersectionObserverWrapper={this.intersectionObserverWrapper} - saveHeightKey={trackScroll ? `${this.context.router.route.location.key}:${scrollKey}` : null} - > - {child} - </IntersectionObserverArticleContainer> - ))} - - {loadMore} - </div> - </div> - ); - } else { - scrollableArea = ( - <div className='empty-column-indicator' ref={this.setRef}> - {emptyMessage} - </div> - ); - } - - if (trackScroll) { - return ( - <ScrollContainer scrollKey={scrollKey} shouldUpdateScroll={shouldUpdateScroll}> - {scrollableArea} - </ScrollContainer> - ); - } else { - return scrollableArea; - } - } - -} diff --git a/app/javascript/themes/glitch/components/setting_text.js b/app/javascript/themes/glitch/components/setting_text.js deleted file mode 100644 index a6dde4c0f..000000000 --- a/app/javascript/themes/glitch/components/setting_text.js +++ /dev/null @@ -1,34 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; - -export default class SettingText extends React.PureComponent { - - static propTypes = { - settings: ImmutablePropTypes.map.isRequired, - settingKey: PropTypes.array.isRequired, - label: PropTypes.string.isRequired, - onChange: PropTypes.func.isRequired, - }; - - handleChange = (e) => { - this.props.onChange(this.props.settingKey, e.target.value); - } - - render () { - const { settings, settingKey, label } = this.props; - - return ( - <label> - <span style={{ display: 'none' }}>{label}</span> - <input - className='setting-text' - value={settings.getIn(settingKey)} - onChange={this.handleChange} - placeholder={label} - /> - </label> - ); - } - -} diff --git a/app/javascript/themes/glitch/components/status.js b/app/javascript/themes/glitch/components/status.js deleted file mode 100644 index e2ef47f5f..000000000 --- a/app/javascript/themes/glitch/components/status.js +++ /dev/null @@ -1,442 +0,0 @@ -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import PropTypes from 'prop-types'; -import StatusPrepend from './status_prepend'; -import StatusHeader from './status_header'; -import StatusContent from './status_content'; -import StatusActionBar from './status_action_bar'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import { MediaGallery, Video } from 'themes/glitch/util/async-components'; -import { HotKeys } from 'react-hotkeys'; -import NotificationOverlayContainer from 'themes/glitch/features/notifications/containers/overlay_container'; - -// We use the component (and not the container) since we do not want -// to use the progress bar to show download progress -import Bundle from '../features/ui/components/bundle'; - -export default class Status extends ImmutablePureComponent { - - static contextTypes = { - router: PropTypes.object, - }; - - static propTypes = { - id: PropTypes.string, - status: ImmutablePropTypes.map, - account: ImmutablePropTypes.map, - onReply: PropTypes.func, - onFavourite: PropTypes.func, - onReblog: PropTypes.func, - onDelete: PropTypes.func, - onPin: PropTypes.func, - onOpenMedia: PropTypes.func, - onOpenVideo: PropTypes.func, - onBlock: PropTypes.func, - onEmbed: PropTypes.func, - onHeightChange: PropTypes.func, - muted: PropTypes.bool, - collapse: PropTypes.bool, - hidden: PropTypes.bool, - prepend: PropTypes.string, - withDismiss: PropTypes.bool, - onMoveUp: PropTypes.func, - onMoveDown: PropTypes.func, - }; - - state = { - isExpanded: null, - markedForDelete: false, - } - - // Avoid checking props that are functions (and whose equality will always - // evaluate to false. See react-immutable-pure-component for usage. - updateOnProps = [ - 'status', - 'account', - 'settings', - 'prepend', - 'boostModal', - 'muted', - 'collapse', - 'notification', - ] - - updateOnStates = [ - 'isExpanded', - 'markedForDelete', - ] - - // If our settings have changed to disable collapsed statuses, then we - // need to make sure that we uncollapse every one. We do that by watching - // for changes to `settings.collapsed.enabled` in - // `componentWillReceiveProps()`. - - // We also need to watch for changes on the `collapse` prop---if this - // changes to anything other than `undefined`, then we need to collapse or - // uncollapse our status accordingly. - componentWillReceiveProps (nextProps) { - if (!nextProps.settings.getIn(['collapsed', 'enabled'])) { - if (this.state.isExpanded === false) { - this.setExpansion(null); - } - } else if ( - nextProps.collapse !== this.props.collapse && - nextProps.collapse !== undefined - ) this.setExpansion(nextProps.collapse ? false : null); - } - - // When mounting, we just check to see if our status should be collapsed, - // and collapse it if so. We don't need to worry about whether collapsing - // is enabled here, because `setExpansion()` already takes that into - // account. - - // The cases where a status should be collapsed are: - // - // - The `collapse` prop has been set to `true` - // - The user has decided in local settings to collapse all statuses. - // - The user has decided to collapse all notifications ('muted' - // statuses). - // - The user has decided to collapse long statuses and the status is - // over 400px (without media, or 650px with). - // - The status is a reply and the user has decided to collapse all - // replies. - // - The status contains media and the user has decided to collapse all - // statuses with media. - // - The status is a reblog the user has decided to collapse all - // statuses which are reblogs. - componentDidMount () { - const { node } = this; - const { - status, - settings, - collapse, - muted, - prepend, - } = this.props; - const autoCollapseSettings = settings.getIn(['collapsed', 'auto']); - - if (function () { - switch (true) { - case collapse: - case autoCollapseSettings.get('all'): - case autoCollapseSettings.get('notifications') && muted: - case autoCollapseSettings.get('lengthy') && node.clientHeight > ( - status.get('media_attachments').size && !muted ? 650 : 400 - ): - case autoCollapseSettings.get('reblogs') && prepend === 'reblogged_by': - case autoCollapseSettings.get('replies') && status.get('in_reply_to_id', null) !== null: - case autoCollapseSettings.get('media') && !(status.get('spoiler_text').length) && status.get('media_attachments').size: - return true; - default: - return false; - } - }()) this.setExpansion(false); - } - - // `setExpansion()` sets the value of `isExpanded` in our state. It takes - // one argument, `value`, which gives the desired value for `isExpanded`. - // The default for this argument is `null`. - - // `setExpansion()` automatically checks for us whether toot collapsing - // is enabled, so we don't have to. - setExpansion = (value) => { - switch (true) { - case value === undefined || value === null: - this.setState({ isExpanded: null }); - break; - case !value && this.props.settings.getIn(['collapsed', 'enabled']): - this.setState({ isExpanded: false }); - break; - case !!value: - this.setState({ isExpanded: true }); - break; - } - } - - // `parseClick()` takes a click event and responds appropriately. - // If our status is collapsed, then clicking on it should uncollapse it. - // If `Shift` is held, then clicking on it should collapse it. - // Otherwise, we open the url handed to us in `destination`, if - // applicable. - parseClick = (e, destination) => { - const { router } = this.context; - const { status } = this.props; - const { isExpanded } = this.state; - if (!router) return; - if (destination === undefined) { - destination = `/statuses/${ - status.getIn(['reblog', 'id'], status.get('id')) - }`; - } - if (e.button === 0) { - if (isExpanded === false) this.setExpansion(null); - else if (e.shiftKey) { - this.setExpansion(false); - document.getSelection().removeAllRanges(); - } else router.history.push(destination); - e.preventDefault(); - } - } - - handleAccountClick = (e) => { - if (this.context.router && e.button === 0) { - const id = e.currentTarget.getAttribute('data-id'); - e.preventDefault(); - this.context.router.history.push(`/accounts/${id}`); - } - } - - handleExpandedToggle = () => { - this.setExpansion(this.state.isExpanded || !this.props.status.get('spoiler') ? null : true); - }; - - handleOpenVideo = startTime => { - this.props.onOpenVideo(this.props.status.getIn(['media_attachments', 0]), startTime); - } - - handleHotkeyReply = e => { - e.preventDefault(); - this.props.onReply(this.props.status, this.context.router.history); - } - - handleHotkeyFavourite = () => { - this.props.onFavourite(this.props.status); - } - - handleHotkeyBoost = e => { - this.props.onReblog(this.props.status, e); - } - - handleHotkeyMention = e => { - e.preventDefault(); - this.props.onMention(this.props.status.get('account'), this.context.router.history); - } - - handleHotkeyOpen = () => { - this.context.router.history.push(`/statuses/${this.props.status.get('id')}`); - } - - handleHotkeyOpenProfile = () => { - this.context.router.history.push(`/accounts/${this.props.status.getIn(['account', 'id'])}`); - } - - handleHotkeyMoveUp = () => { - this.props.onMoveUp(this.props.status.get('id')); - } - - handleHotkeyMoveDown = () => { - this.props.onMoveDown(this.props.status.get('id')); - } - - handleRef = c => { - this.node = c; - } - - renderLoadingMediaGallery () { - return <div className='media_gallery' style={{ height: '110px' }} />; - } - - renderLoadingVideoPlayer () { - return <div className='media-spoiler-video' style={{ height: '110px' }} />; - } - - render () { - const { - handleRef, - parseClick, - setExpansion, - } = this; - const { router } = this.context; - const { - status, - account, - settings, - collapsed, - muted, - prepend, - intersectionObserverWrapper, - onOpenVideo, - onOpenMedia, - notification, - hidden, - ...other - } = this.props; - const { isExpanded } = this.state; - let background = null; - let attachments = null; - let media = null; - let mediaIcon = null; - - if (status === null) { - return null; - } - - if (hidden) { - return ( - <div - ref={this.handleRef} - data-id={status.get('id')} - style={{ - height: `${this.height}px`, - opacity: 0, - overflow: 'hidden', - }} - > - {status.getIn(['account', 'display_name']) || status.getIn(['account', 'username'])} - {' '} - {status.get('content')} - </div> - ); - } - - // If user backgrounds for collapsed statuses are enabled, then we - // initialize our background accordingly. This will only be rendered if - // the status is collapsed. - if (settings.getIn(['collapsed', 'backgrounds', 'user_backgrounds'])) { - background = status.getIn(['account', 'header']); - } - - // This handles our media attachments. Note that we don't show media on - // muted (notification) statuses. If the media type is unknown, then we - // simply ignore it. - - // After we have generated our appropriate media element and stored it in - // `media`, we snatch the thumbnail to use as our `background` if media - // backgrounds for collapsed statuses are enabled. - attachments = status.get('media_attachments'); - if (attachments.size > 0 && !muted) { - if (attachments.some(item => item.get('type') === 'unknown')) { // Media type is 'unknown' - /* Do nothing */ - } else if (attachments.getIn([0, 'type']) === 'video') { // Media type is 'video' - const video = status.getIn(['media_attachments', 0]); - - media = ( - <Bundle fetchComponent={Video} loading={this.renderLoadingVideoPlayer} > - {Component => <Component - preview={video.get('preview_url')} - src={video.get('url')} - sensitive={status.get('sensitive')} - letterbox={settings.getIn(['media', 'letterbox'])} - fullwidth={settings.getIn(['media', 'fullwidth'])} - onOpenVideo={this.handleOpenVideo} - />} - </Bundle> - ); - mediaIcon = 'video-camera'; - } else { // Media type is 'image' or 'gifv' - media = ( - <Bundle fetchComponent={MediaGallery} loading={this.renderLoadingMediaGallery} > - {Component => ( - <Component - media={attachments} - sensitive={status.get('sensitive')} - letterbox={settings.getIn(['media', 'letterbox'])} - fullwidth={settings.getIn(['media', 'fullwidth'])} - onOpenMedia={this.props.onOpenMedia} - /> - )} - </Bundle> - ); - mediaIcon = 'picture-o'; - } - - if (!status.get('sensitive') && !(status.get('spoiler_text').length > 0) && settings.getIn(['collapsed', 'backgrounds', 'preview_images'])) { - background = attachments.getIn([0, 'preview_url']); - } - } - - // Here we prepare extra data-* attributes for CSS selectors. - // Users can use those for theming, hiding avatars etc via UserStyle - const selectorAttribs = { - 'data-status-by': `@${status.getIn(['account', 'acct'])}`, - }; - - if (prepend && account) { - const notifKind = { - favourite: 'favourited', - reblog: 'boosted', - reblogged_by: 'boosted', - }[prepend]; - - selectorAttribs[`data-${notifKind}-by`] = `@${account.get('acct')}`; - } - - const handlers = { - reply: this.handleHotkeyReply, - favourite: this.handleHotkeyFavourite, - boost: this.handleHotkeyBoost, - mention: this.handleHotkeyMention, - open: this.handleHotkeyOpen, - openProfile: this.handleHotkeyOpenProfile, - moveUp: this.handleHotkeyMoveUp, - moveDown: this.handleHotkeyMoveDown, - }; - - return ( - <HotKeys handlers={handlers}> - <div - className={ - `status${ - muted ? ' muted' : '' - } status-${status.get('visibility')}${ - isExpanded === false ? ' collapsed' : '' - }${ - isExpanded === false && background ? ' has-background' : '' - }${ - this.state.markedForDelete ? ' marked-for-delete' : '' - }` - } - style={{ - backgroundImage: ( - isExpanded === false && background ? - `url(${background})` : - 'none' - ), - }} - {...selectorAttribs} - ref={handleRef} - > - {prepend && account ? ( - <StatusPrepend - type={prepend} - account={account} - parseClick={parseClick} - notificationId={this.props.notificationId} - /> - ) : null} - <StatusHeader - status={status} - friend={account} - mediaIcon={mediaIcon} - collapsible={settings.getIn(['collapsed', 'enabled'])} - collapsed={isExpanded === false} - parseClick={parseClick} - setExpansion={setExpansion} - /> - <StatusContent - status={status} - media={media} - mediaIcon={mediaIcon} - expanded={isExpanded} - setExpansion={setExpansion} - parseClick={parseClick} - disabled={!router} - /> - {isExpanded !== false ? ( - <StatusActionBar - {...other} - status={status} - account={status.get('account')} - /> - ) : null} - {notification ? ( - <NotificationOverlayContainer - notification={notification} - /> - ) : null} - </div> - </HotKeys> - ); - } - -} diff --git a/app/javascript/themes/glitch/components/status_action_bar.js b/app/javascript/themes/glitch/components/status_action_bar.js deleted file mode 100644 index 9d615ed7c..000000000 --- a/app/javascript/themes/glitch/components/status_action_bar.js +++ /dev/null @@ -1,188 +0,0 @@ -// THIS FILE EXISTS FOR UPSTREAM COMPATIBILITY & SHOULDN'T BE USED !! -// SEE INSTEAD : glitch/components/status/action_bar - -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import PropTypes from 'prop-types'; -import IconButton from './icon_button'; -import DropdownMenuContainer from 'themes/glitch/containers/dropdown_menu_container'; -import { defineMessages, injectIntl } from 'react-intl'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import { me } from 'themes/glitch/util/initial_state'; -import RelativeTimestamp from './relative_timestamp'; - -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' }, - share: { id: 'status.share', defaultMessage: 'Share' }, - more: { id: 'status.more', defaultMessage: 'More' }, - 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}' }, - muteConversation: { id: 'status.mute_conversation', defaultMessage: 'Mute conversation' }, - unmuteConversation: { id: 'status.unmute_conversation', defaultMessage: 'Unmute conversation' }, - pin: { id: 'status.pin', defaultMessage: 'Pin on profile' }, - unpin: { id: 'status.unpin', defaultMessage: 'Unpin from profile' }, - embed: { id: 'status.embed', defaultMessage: 'Embed' }, -}); - -@injectIntl -export default class StatusActionBar extends ImmutablePureComponent { - - static contextTypes = { - router: PropTypes.object, - }; - - static 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, - onEmbed: PropTypes.func, - onMuteConversation: PropTypes.func, - onPin: PropTypes.func, - withDismiss: PropTypes.bool, - intl: PropTypes.object.isRequired, - }; - - // Avoid checking props that are functions (and whose equality will always - // evaluate to false. See react-immutable-pure-component for usage. - updateOnProps = [ - 'status', - 'withDismiss', - ] - - handleReplyClick = () => { - this.props.onReply(this.props.status, this.context.router.history); - } - - handleShareClick = () => { - navigator.share({ - text: this.props.status.get('search_index'), - url: this.props.status.get('url'), - }); - } - - handleFavouriteClick = () => { - this.props.onFavourite(this.props.status); - } - - handleReblogClick = (e) => { - this.props.onReblog(this.props.status, e); - } - - handleDeleteClick = () => { - this.props.onDelete(this.props.status); - } - - handlePinClick = () => { - this.props.onPin(this.props.status); - } - - handleMentionClick = () => { - this.props.onMention(this.props.status.get('account'), this.context.router.history); - } - - handleMuteClick = () => { - this.props.onMute(this.props.status.get('account')); - } - - handleBlockClick = () => { - this.props.onBlock(this.props.status.get('account')); - } - - handleOpen = () => { - this.context.router.history.push(`/statuses/${this.props.status.get('id')}`); - } - - handleEmbed = () => { - this.props.onEmbed(this.props.status); - } - - handleReport = () => { - this.props.onReport(this.props.status); - } - - handleConversationMuteClick = () => { - this.props.onMuteConversation(this.props.status); - } - - render () { - const { status, intl, withDismiss } = this.props; - - const mutingConversation = status.get('muted'); - const anonymousAccess = !me; - const publicStatus = ['public', 'unlisted'].includes(status.get('visibility')); - - let menu = []; - let reblogIcon = 'retweet'; - let replyIcon; - let replyTitle; - - menu.push({ text: intl.formatMessage(messages.open), action: this.handleOpen }); - - if (publicStatus) { - menu.push({ text: intl.formatMessage(messages.embed), action: this.handleEmbed }); - } - - menu.push(null); - - if (status.getIn(['account', 'id']) === me || withDismiss) { - menu.push({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick }); - menu.push(null); - } - - if (status.getIn(['account', 'id']) === me) { - if (publicStatus) { - menu.push({ text: intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin), action: this.handlePinClick }); - } - - 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 }); - } - - if (status.get('in_reply_to_id', null) === null) { - replyIcon = 'reply'; - replyTitle = intl.formatMessage(messages.reply); - } else { - replyIcon = 'reply-all'; - replyTitle = intl.formatMessage(messages.replyAll); - } - - const shareButton = ('share' in navigator) && status.get('visibility') === 'public' && ( - <IconButton className='status__action-bar-button' title={intl.formatMessage(messages.share)} icon='share-alt' onClick={this.handleShareClick} /> - ); - - return ( - <div className='status__action-bar'> - <IconButton className='status__action-bar-button' disabled={anonymousAccess} title={replyTitle} icon={replyIcon} onClick={this.handleReplyClick} /> - <IconButton className='status__action-bar-button' disabled={anonymousAccess || !publicStatus} active={status.get('reblogged')} pressed={status.get('reblogged')} title={!publicStatus ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.reblog)} icon={reblogIcon} onClick={this.handleReblogClick} /> - <IconButton className='status__action-bar-button star-icon' disabled={anonymousAccess} animate active={status.get('favourited')} pressed={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} /> - {shareButton} - - <div className='status__action-bar-dropdown'> - <DropdownMenuContainer disabled={anonymousAccess} status={status} items={menu} icon='ellipsis-h' size={18} direction='right' ariaLabel={intl.formatMessage(messages.more)} /> - </div> - - <a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener'><RelativeTimestamp timestamp={status.get('created_at')} /></a> - </div> - ); - } - -} diff --git a/app/javascript/themes/glitch/components/status_content.js b/app/javascript/themes/glitch/components/status_content.js deleted file mode 100644 index 3eba6eaa0..000000000 --- a/app/javascript/themes/glitch/components/status_content.js +++ /dev/null @@ -1,245 +0,0 @@ -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import PropTypes from 'prop-types'; -import { isRtl } from 'themes/glitch/util/rtl'; -import { FormattedMessage } from 'react-intl'; -import Permalink from './permalink'; -import classnames from 'classnames'; - -export default class StatusContent extends React.PureComponent { - - static propTypes = { - status: ImmutablePropTypes.map.isRequired, - expanded: PropTypes.bool, - setExpansion: PropTypes.func, - media: PropTypes.element, - mediaIcon: PropTypes.string, - parseClick: PropTypes.func, - disabled: PropTypes.bool, - }; - - state = { - hidden: true, - }; - - _updateStatusLinks () { - const node = this.node; - const links = node.querySelectorAll('a'); - - for (var i = 0; i < links.length; ++i) { - let link = links[i]; - if (link.classList.contains('status-link')) { - continue; - } - link.classList.add('status-link'); - - let mention = this.props.status.get('mentions').find(item => link.href === item.get('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 { - link.addEventListener('click', this.onLinkClick.bind(this), false); - link.setAttribute('title', link.href); - } - - link.setAttribute('target', '_blank'); - link.setAttribute('rel', 'noopener'); - } - } - - componentDidMount () { - this._updateStatusLinks(); - } - - componentDidUpdate () { - this._updateStatusLinks(); - } - - onLinkClick = (e) => { - if (this.props.expanded === false) { - if (this.props.parseClick) this.props.parseClick(e); - } - } - - onMentionClick = (mention, e) => { - if (this.props.parseClick) { - this.props.parseClick(e, `/accounts/${mention.get('id')}`); - } - } - - onHashtagClick = (hashtag, e) => { - hashtag = hashtag.replace(/^#/, '').toLowerCase(); - - if (this.props.parseClick) { - this.props.parseClick(e, `/timelines/tag/${hashtag}`); - } - } - - handleMouseDown = (e) => { - this.startXY = [e.clientX, e.clientY]; - } - - handleMouseUp = (e) => { - const { parseClick } = this.props; - - if (!this.startXY) { - return; - } - - const [ startX, startY ] = this.startXY; - const [ deltaX, deltaY ] = [Math.abs(e.clientX - startX), Math.abs(e.clientY - startY)]; - - if (e.target.localName === 'button' || e.target.localName === 'a' || (e.target.parentNode && (e.target.parentNode.localName === 'button' || e.target.parentNode.localName === 'a'))) { - return; - } - - if (deltaX + deltaY < 5 && e.button === 0 && parseClick) { - parseClick(e); - } - - this.startXY = null; - } - - handleSpoilerClick = (e) => { - e.preventDefault(); - - if (this.props.setExpansion) { - this.props.setExpansion(this.props.expanded ? null : true); - } else { - this.setState({ hidden: !this.state.hidden }); - } - } - - setRef = (c) => { - this.node = c; - } - - render () { - const { - status, - media, - mediaIcon, - parseClick, - disabled, - } = this.props; - - const hidden = this.props.setExpansion ? !this.props.expanded : this.state.hidden; - - const content = { __html: status.get('contentHtml') }; - const spoilerContent = { __html: status.get('spoilerHtml') }; - const directionStyle = { direction: 'ltr' }; - const classNames = classnames('status__content', { - 'status__content--with-action': parseClick && !disabled, - 'status__content--with-spoiler': status.get('spoiler_text').length > 0, - }); - - if (isRtl(status.get('search_index'))) { - 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' - key='0' - />, - mediaIcon ? ( - <i - className={ - `fa fa-fw fa-${mediaIcon} status__content__spoiler-icon` - } - aria-hidden='true' - key='1' - /> - ) : null, - ] : [ - <FormattedMessage - id='status.show_less' - defaultMessage='Show less' - key='0' - />, - ]; - - if (hidden) { - mentionsPlaceholder = <div>{mentionLinks}</div>; - } - - return ( - <div className={classNames} tabIndex='0'> - <p - style={{ marginBottom: hidden && status.get('mentions').isEmpty() ? '0px' : null }} - onMouseDown={this.handleMouseDown} - onMouseUp={this.handleMouseUp} - > - <span dangerouslySetInnerHTML={spoilerContent} /> - {' '} - <button tabIndex='0' className='status__content__spoiler-link' onClick={this.handleSpoilerClick}> - {toggleText} - </button> - </p> - - {mentionsPlaceholder} - - <div className={`status__content__spoiler ${!hidden ? 'status__content__spoiler--visible' : ''}`}> - <div - ref={this.setRef} - style={directionStyle} - tabIndex={!hidden ? 0 : null} - onMouseDown={this.handleMouseDown} - onMouseUp={this.handleMouseUp} - dangerouslySetInnerHTML={content} - /> - {media} - </div> - - </div> - ); - } else if (parseClick) { - return ( - <div - className={classNames} - style={directionStyle} - tabIndex='0' - > - <div - ref={this.setRef} - onMouseDown={this.handleMouseDown} - onMouseUp={this.handleMouseUp} - dangerouslySetInnerHTML={content} - tabIndex='0' - /> - {media} - </div> - ); - } else { - return ( - <div - className='status__content' - style={directionStyle} - tabIndex='0' - > - <div ref={this.setRef} dangerouslySetInnerHTML={content} tabIndex='0' /> - {media} - </div> - ); - } - } - -} diff --git a/app/javascript/themes/glitch/components/status_header.js b/app/javascript/themes/glitch/components/status_header.js deleted file mode 100644 index bfa996cd5..000000000 --- a/app/javascript/themes/glitch/components/status_header.js +++ /dev/null @@ -1,120 +0,0 @@ -// Package imports. -import React from 'react'; -import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import { defineMessages, injectIntl } from 'react-intl'; - -// Mastodon imports. -import Avatar from './avatar'; -import AvatarOverlay from './avatar_overlay'; -import DisplayName from './display_name'; -import IconButton from './icon_button'; -import VisibilityIcon from './status_visibility_icon'; - -// Messages for use with internationalization stuff. -const messages = defineMessages({ - collapse: { id: 'status.collapse', defaultMessage: 'Collapse' }, - uncollapse: { id: 'status.uncollapse', defaultMessage: 'Uncollapse' }, - public: { id: 'privacy.public.short', defaultMessage: 'Public' }, - unlisted: { id: 'privacy.unlisted.short', defaultMessage: 'Unlisted' }, - private: { id: 'privacy.private.short', defaultMessage: 'Followers-only' }, - direct: { id: 'privacy.direct.short', defaultMessage: 'Direct' }, -}); - -@injectIntl -export default class StatusHeader extends React.PureComponent { - - static propTypes = { - status: ImmutablePropTypes.map.isRequired, - friend: ImmutablePropTypes.map, - mediaIcon: PropTypes.string, - collapsible: PropTypes.bool, - collapsed: PropTypes.bool, - parseClick: PropTypes.func.isRequired, - setExpansion: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - }; - - // Handles clicks on collapsed button - handleCollapsedClick = (e) => { - const { collapsed, setExpansion } = this.props; - if (e.button === 0) { - setExpansion(collapsed ? null : false); - e.preventDefault(); - } - } - - // Handles clicks on account name/image - handleAccountClick = (e) => { - const { status, parseClick } = this.props; - parseClick(e, `/accounts/${+status.getIn(['account', 'id'])}`); - } - - // Rendering. - render () { - const { - status, - friend, - mediaIcon, - collapsible, - collapsed, - intl, - } = this.props; - - const account = status.get('account'); - - return ( - <header className='status__info'> - <a - href={account.get('url')} - target='_blank' - className='status__avatar' - onClick={this.handleAccountClick} - > - { - friend ? ( - <AvatarOverlay account={account} friend={friend} /> - ) : ( - <Avatar account={account} size={48} /> - ) - } - </a> - <a - href={account.get('url')} - target='_blank' - className='status__display-name' - onClick={this.handleAccountClick} - > - <DisplayName account={account} /> - </a> - <div className='status__info__icons'> - {mediaIcon ? ( - <i - className={`fa fa-fw fa-${mediaIcon}`} - aria-hidden='true' - /> - ) : null} - {( - <VisibilityIcon visibility={status.get('visibility')} /> - )} - {collapsible ? ( - <IconButton - className='status__collapse-button' - animate flip - active={collapsed} - title={ - collapsed ? - intl.formatMessage(messages.uncollapse) : - intl.formatMessage(messages.collapse) - } - icon='angle-double-up' - onClick={this.handleCollapsedClick} - /> - ) : null} - </div> - - </header> - ); - } - -} diff --git a/app/javascript/themes/glitch/components/status_list.js b/app/javascript/themes/glitch/components/status_list.js deleted file mode 100644 index ddb1354c6..000000000 --- a/app/javascript/themes/glitch/components/status_list.js +++ /dev/null @@ -1,72 +0,0 @@ -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import PropTypes from 'prop-types'; -import StatusContainer from 'themes/glitch/containers/status_container'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import ScrollableList from './scrollable_list'; - -export default class StatusList extends ImmutablePureComponent { - - static propTypes = { - scrollKey: PropTypes.string.isRequired, - statusIds: ImmutablePropTypes.list.isRequired, - onScrollToBottom: PropTypes.func, - onScrollToTop: PropTypes.func, - onScroll: PropTypes.func, - trackScroll: PropTypes.bool, - shouldUpdateScroll: PropTypes.func, - isLoading: PropTypes.bool, - hasMore: PropTypes.bool, - prepend: PropTypes.node, - emptyMessage: PropTypes.node, - }; - - static defaultProps = { - trackScroll: true, - }; - - handleMoveUp = id => { - const elementIndex = this.props.statusIds.indexOf(id) - 1; - this._selectChild(elementIndex); - } - - handleMoveDown = id => { - const elementIndex = this.props.statusIds.indexOf(id) + 1; - this._selectChild(elementIndex); - } - - _selectChild (index) { - const element = this.node.node.querySelector(`article:nth-of-type(${index + 1}) .focusable`); - - if (element) { - element.focus(); - } - } - - setRef = c => { - this.node = c; - } - - render () { - const { statusIds, ...other } = this.props; - const { isLoading } = other; - - const scrollableContent = (isLoading || statusIds.size > 0) ? ( - statusIds.map((statusId) => ( - <StatusContainer - key={statusId} - id={statusId} - onMoveUp={this.handleMoveUp} - onMoveDown={this.handleMoveDown} - /> - )) - ) : null; - - return ( - <ScrollableList {...other} ref={this.setRef}> - {scrollableContent} - </ScrollableList> - ); - } - -} diff --git a/app/javascript/themes/glitch/components/status_prepend.js b/app/javascript/themes/glitch/components/status_prepend.js deleted file mode 100644 index bd2559e46..000000000 --- a/app/javascript/themes/glitch/components/status_prepend.js +++ /dev/null @@ -1,83 +0,0 @@ -// Package imports // -import React from 'react'; -import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import { FormattedMessage } from 'react-intl'; - -export default class StatusPrepend extends React.PureComponent { - - static propTypes = { - type: PropTypes.string.isRequired, - account: ImmutablePropTypes.map.isRequired, - parseClick: PropTypes.func.isRequired, - notificationId: PropTypes.number, - }; - - handleClick = (e) => { - const { account, parseClick } = this.props; - parseClick(e, `/accounts/${+account.get('id')}`); - } - - Message = () => { - const { type, account } = this.props; - let link = ( - <a - onClick={this.handleClick} - href={account.get('url')} - className='status__display-name' - > - <b - dangerouslySetInnerHTML={{ - __html : account.get('display_name_html') || account.get('username'), - }} - /> - </a> - ); - switch (type) { - case 'reblogged_by': - return ( - <FormattedMessage - id='status.reblogged_by' - defaultMessage='{name} boosted' - values={{ name : link }} - /> - ); - case 'favourite': - return ( - <FormattedMessage - id='notification.favourite' - defaultMessage='{name} favourited your status' - values={{ name : link }} - /> - ); - case 'reblog': - return ( - <FormattedMessage - id='notification.reblog' - defaultMessage='{name} boosted your status' - values={{ name : link }} - /> - ); - } - return null; - } - - render () { - const { Message } = this; - const { type } = this.props; - - return !type ? null : ( - <aside className={type === 'reblogged_by' ? 'status__prepend' : 'notification__message'}> - <div className={type === 'reblogged_by' ? 'status__prepend-icon-wrapper' : 'notification__favourite-icon-wrapper'}> - <i - className={`fa fa-fw fa-${ - type === 'favourite' ? 'star star-icon' : 'retweet' - } status__prepend-icon`} - /> - </div> - <Message /> - </aside> - ); - } - -} diff --git a/app/javascript/themes/glitch/components/status_visibility_icon.js b/app/javascript/themes/glitch/components/status_visibility_icon.js deleted file mode 100644 index 017b69cbb..000000000 --- a/app/javascript/themes/glitch/components/status_visibility_icon.js +++ /dev/null @@ -1,48 +0,0 @@ -// Package imports // -import React from 'react'; -import PropTypes from 'prop-types'; -import { defineMessages, injectIntl } from 'react-intl'; -import ImmutablePureComponent from 'react-immutable-pure-component'; - -const messages = defineMessages({ - public: { id: 'privacy.public.short', defaultMessage: 'Public' }, - unlisted: { id: 'privacy.unlisted.short', defaultMessage: 'Unlisted' }, - private: { id: 'privacy.private.short', defaultMessage: 'Followers-only' }, - direct: { id: 'privacy.direct.short', defaultMessage: 'Direct' }, -}); - -@injectIntl -export default class VisibilityIcon extends ImmutablePureComponent { - - static propTypes = { - visibility: PropTypes.string, - intl: PropTypes.object.isRequired, - withLabel: PropTypes.bool, - }; - - render() { - const { withLabel, visibility, intl } = this.props; - - const visibilityClass = { - public: 'globe', - unlisted: 'unlock-alt', - private: 'lock', - direct: 'envelope', - }[visibility]; - - const label = intl.formatMessage(messages[visibility]); - - const icon = (<i - className={`status__visibility-icon fa fa-fw fa-${visibilityClass}`} - title={label} - aria-hidden='true' - />); - - if (withLabel) { - return (<span style={{ whiteSpace: 'nowrap' }}>{icon} {label}</span>); - } else { - return icon; - } - } - -} |