diff options
author | kibigo! <marrus-sh@users.noreply.github.com> | 2017-12-03 23:26:40 -0800 |
---|---|---|
committer | kibigo! <marrus-sh@users.noreply.github.com> | 2017-12-03 23:26:40 -0800 |
commit | bc4fa6b198557a7f3989eb0865e2c77ac7451d29 (patch) | |
tree | a18543e1e0555e88b97cad60adc6d2abe0bffb00 /app/javascript/themes/glitch/features/ui | |
parent | d216547382cf1f3419de31e1ee06272e816ea339 (diff) |
Rename themes -> flavours ? ?
Diffstat (limited to 'app/javascript/themes/glitch/features/ui')
32 files changed, 0 insertions, 3202 deletions
diff --git a/app/javascript/themes/glitch/features/ui/components/actions_modal.js b/app/javascript/themes/glitch/features/ui/components/actions_modal.js deleted file mode 100644 index 7a2b78b63..000000000 --- a/app/javascript/themes/glitch/features/ui/components/actions_modal.js +++ /dev/null @@ -1,74 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import StatusContent from 'themes/glitch/components/status_content'; -import Avatar from 'themes/glitch/components/avatar'; -import RelativeTimestamp from 'themes/glitch/components/relative_timestamp'; -import DisplayName from 'themes/glitch/components/display_name'; -import IconButton from 'themes/glitch/components/icon_button'; -import classNames from 'classnames'; - -export default class ActionsModal extends ImmutablePureComponent { - - static propTypes = { - status: ImmutablePropTypes.map, - actions: PropTypes.array, - onClick: PropTypes.func, - }; - - renderAction = (action, i) => { - if (action === null) { - return <li key={`sep-${i}`} className='dropdown-menu__separator' />; - } - - const { icon = null, text, meta = null, active = false, href = '#' } = action; - - return ( - <li key={`${text}-${i}`}> - <a href={href} target='_blank' rel='noopener' onClick={this.props.onClick} data-index={i} className={classNames({ active })}> - {icon && <IconButton title={text} icon={icon} role='presentation' tabIndex='-1' />} - <div> - <div className={classNames({ 'actions-modal__item-label': !!meta })}>{text}</div> - <div>{meta}</div> - </div> - </a> - </li> - ); - } - - render () { - const status = this.props.status && ( - <div className='status light'> - <div className='boost-modal__status-header'> - <div className='boost-modal__status-time'> - <a href={this.props.status.get('url')} className='status__relative-time' target='_blank' rel='noopener'> - <RelativeTimestamp timestamp={this.props.status.get('created_at')} /> - </a> - </div> - - <a href={this.props.status.getIn(['account', 'url'])} className='status__display-name'> - <div className='status__avatar'> - <Avatar account={this.props.status.get('account')} size={48} /> - </div> - - <DisplayName account={this.props.status.get('account')} /> - </a> - </div> - - <StatusContent status={this.props.status} /> - </div> - ); - - return ( - <div className='modal-root__modal actions-modal'> - {status} - - <ul> - {this.props.actions.map(this.renderAction)} - </ul> - </div> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/ui/components/boost_modal.js b/app/javascript/themes/glitch/features/ui/components/boost_modal.js deleted file mode 100644 index 49781db10..000000000 --- a/app/javascript/themes/glitch/features/ui/components/boost_modal.js +++ /dev/null @@ -1,84 +0,0 @@ -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import PropTypes from 'prop-types'; -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; -import Button from 'themes/glitch/components/button'; -import StatusContent from 'themes/glitch/components/status_content'; -import Avatar from 'themes/glitch/components/avatar'; -import RelativeTimestamp from 'themes/glitch/components/relative_timestamp'; -import DisplayName from 'themes/glitch/components/display_name'; -import ImmutablePureComponent from 'react-immutable-pure-component'; - -const messages = defineMessages({ - reblog: { id: 'status.reblog', defaultMessage: 'Boost' }, -}); - -@injectIntl -export default class BoostModal extends ImmutablePureComponent { - - static contextTypes = { - router: PropTypes.object, - }; - - static propTypes = { - status: ImmutablePropTypes.map.isRequired, - onReblog: PropTypes.func.isRequired, - onClose: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - }; - - componentDidMount() { - this.button.focus(); - } - - handleReblog = () => { - this.props.onReblog(this.props.status); - this.props.onClose(); - } - - handleAccountClick = (e) => { - if (e.button === 0) { - e.preventDefault(); - this.props.onClose(); - this.context.router.history.push(`/accounts/${this.props.status.getIn(['account', 'id'])}`); - } - } - - setRef = (c) => { - this.button = c; - } - - render () { - const { status, intl } = this.props; - - return ( - <div className='modal-root__modal boost-modal'> - <div className='boost-modal__container'> - <div className='status light'> - <div className='boost-modal__status-header'> - <div className='boost-modal__status-time'> - <a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener'><RelativeTimestamp timestamp={status.get('created_at')} /></a> - </div> - - <a onClick={this.handleAccountClick} href={status.getIn(['account', 'url'])} className='status__display-name'> - <div className='status__avatar'> - <Avatar account={status.get('account')} size={48} /> - </div> - - <DisplayName account={status.get('account')} /> - </a> - </div> - - <StatusContent status={status} /> - </div> - </div> - - <div className='boost-modal__action-bar'> - <div><FormattedMessage id='boost_modal.combo' defaultMessage='You can press {combo} to skip this next time' values={{ combo: <span>Shift + <i className='fa fa-retweet' /></span> }} /></div> - <Button text={intl.formatMessage(messages.reblog)} onClick={this.handleReblog} ref={this.setRef} /> - </div> - </div> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/ui/components/bundle.js b/app/javascript/themes/glitch/features/ui/components/bundle.js deleted file mode 100644 index fc88e0c70..000000000 --- a/app/javascript/themes/glitch/features/ui/components/bundle.js +++ /dev/null @@ -1,102 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; - -const emptyComponent = () => null; -const noop = () => { }; - -class Bundle extends React.Component { - - static propTypes = { - fetchComponent: PropTypes.func.isRequired, - loading: PropTypes.func, - error: PropTypes.func, - children: PropTypes.func.isRequired, - renderDelay: PropTypes.number, - onFetch: PropTypes.func, - onFetchSuccess: PropTypes.func, - onFetchFail: PropTypes.func, - } - - static defaultProps = { - loading: emptyComponent, - error: emptyComponent, - renderDelay: 0, - onFetch: noop, - onFetchSuccess: noop, - onFetchFail: noop, - } - - static cache = {} - - state = { - mod: undefined, - forceRender: false, - } - - componentWillMount() { - this.load(this.props); - } - - componentWillReceiveProps(nextProps) { - if (nextProps.fetchComponent !== this.props.fetchComponent) { - this.load(nextProps); - } - } - - componentWillUnmount () { - if (this.timeout) { - clearTimeout(this.timeout); - } - } - - load = (props) => { - const { fetchComponent, onFetch, onFetchSuccess, onFetchFail, renderDelay } = props || this.props; - - onFetch(); - - if (Bundle.cache[fetchComponent.name]) { - const mod = Bundle.cache[fetchComponent.name]; - - this.setState({ mod: mod.default }); - onFetchSuccess(); - return Promise.resolve(); - } - - this.setState({ mod: undefined }); - - if (renderDelay !== 0) { - this.timestamp = new Date(); - this.timeout = setTimeout(() => this.setState({ forceRender: true }), renderDelay); - } - - return fetchComponent() - .then((mod) => { - Bundle.cache[fetchComponent.name] = mod; - this.setState({ mod: mod.default }); - onFetchSuccess(); - }) - .catch((error) => { - this.setState({ mod: null }); - onFetchFail(error); - }); - } - - render() { - const { loading: Loading, error: Error, children, renderDelay } = this.props; - const { mod, forceRender } = this.state; - const elapsed = this.timestamp ? (new Date() - this.timestamp) : renderDelay; - - if (mod === undefined) { - return (elapsed >= renderDelay || forceRender) ? <Loading /> : null; - } - - if (mod === null) { - return <Error onRetry={this.load} />; - } - - return children(mod); - } - -} - -export default Bundle; diff --git a/app/javascript/themes/glitch/features/ui/components/bundle_column_error.js b/app/javascript/themes/glitch/features/ui/components/bundle_column_error.js deleted file mode 100644 index daedc6299..000000000 --- a/app/javascript/themes/glitch/features/ui/components/bundle_column_error.js +++ /dev/null @@ -1,44 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { defineMessages, injectIntl } from 'react-intl'; - -import Column from './column'; -import ColumnHeader from './column_header'; -import ColumnBackButtonSlim from 'themes/glitch/components/column_back_button_slim'; -import IconButton from 'themes/glitch/components/icon_button'; - -const messages = defineMessages({ - title: { id: 'bundle_column_error.title', defaultMessage: 'Network error' }, - body: { id: 'bundle_column_error.body', defaultMessage: 'Something went wrong while loading this component.' }, - retry: { id: 'bundle_column_error.retry', defaultMessage: 'Try again' }, -}); - -class BundleColumnError extends React.Component { - - static propTypes = { - onRetry: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - } - - handleRetry = () => { - this.props.onRetry(); - } - - render () { - const { intl: { formatMessage } } = this.props; - - return ( - <Column> - <ColumnHeader icon='exclamation-circle' type={formatMessage(messages.title)} /> - <ColumnBackButtonSlim /> - <div className='error-column'> - <IconButton title={formatMessage(messages.retry)} icon='refresh' onClick={this.handleRetry} size={64} /> - {formatMessage(messages.body)} - </div> - </Column> - ); - } - -} - -export default injectIntl(BundleColumnError); diff --git a/app/javascript/themes/glitch/features/ui/components/bundle_modal_error.js b/app/javascript/themes/glitch/features/ui/components/bundle_modal_error.js deleted file mode 100644 index 8cca32ae9..000000000 --- a/app/javascript/themes/glitch/features/ui/components/bundle_modal_error.js +++ /dev/null @@ -1,53 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { defineMessages, injectIntl } from 'react-intl'; - -import IconButton from 'themes/glitch/components/icon_button'; - -const messages = defineMessages({ - error: { id: 'bundle_modal_error.message', defaultMessage: 'Something went wrong while loading this component.' }, - retry: { id: 'bundle_modal_error.retry', defaultMessage: 'Try again' }, - close: { id: 'bundle_modal_error.close', defaultMessage: 'Close' }, -}); - -class BundleModalError extends React.Component { - - static propTypes = { - onRetry: PropTypes.func.isRequired, - onClose: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - } - - handleRetry = () => { - this.props.onRetry(); - } - - render () { - const { onClose, intl: { formatMessage } } = this.props; - - // Keep the markup in sync with <ModalLoading /> - // (make sure they have the same dimensions) - return ( - <div className='modal-root__modal error-modal'> - <div className='error-modal__body'> - <IconButton title={formatMessage(messages.retry)} icon='refresh' onClick={this.handleRetry} size={64} /> - {formatMessage(messages.error)} - </div> - - <div className='error-modal__footer'> - <div> - <button - onClick={onClose} - className='error-modal__nav onboarding-modal__skip' - > - {formatMessage(messages.close)} - </button> - </div> - </div> - </div> - ); - } - -} - -export default injectIntl(BundleModalError); diff --git a/app/javascript/themes/glitch/features/ui/components/column.js b/app/javascript/themes/glitch/features/ui/components/column.js deleted file mode 100644 index 73a5bc15e..000000000 --- a/app/javascript/themes/glitch/features/ui/components/column.js +++ /dev/null @@ -1,74 +0,0 @@ -import React from 'react'; -import ColumnHeader from './column_header'; -import PropTypes from 'prop-types'; -import { debounce } from 'lodash'; -import { scrollTop } from 'themes/glitch/util/scroll'; -import { isMobile } from 'themes/glitch/util/is_mobile'; - -export default class Column extends React.PureComponent { - - static propTypes = { - heading: PropTypes.string, - icon: PropTypes.string, - children: PropTypes.node, - active: PropTypes.bool, - hideHeadingOnMobile: PropTypes.bool, - name: PropTypes.string, - }; - - handleHeaderClick = () => { - const scrollable = this.node.querySelector('.scrollable'); - - if (!scrollable) { - return; - } - - this._interruptScrollAnimation = scrollTop(scrollable); - } - - scrollTop () { - const scrollable = this.node.querySelector('.scrollable'); - - if (!scrollable) { - return; - } - - this._interruptScrollAnimation = scrollTop(scrollable); - } - - - handleScroll = debounce(() => { - if (typeof this._interruptScrollAnimation !== 'undefined') { - this._interruptScrollAnimation(); - } - }, 200) - - setRef = (c) => { - this.node = c; - } - - render () { - const { heading, icon, children, active, hideHeadingOnMobile, name } = this.props; - - const showHeading = heading && (!hideHeadingOnMobile || (hideHeadingOnMobile && !isMobile(window.innerWidth))); - - const columnHeaderId = showHeading && heading.replace(/ /g, '-'); - const header = showHeading && ( - <ColumnHeader icon={icon} active={active} type={heading} onClick={this.handleHeaderClick} columnHeaderId={columnHeaderId} /> - ); - return ( - <div - ref={this.setRef} - role='region' - data-column={name} - aria-labelledby={columnHeaderId} - className='column' - onScroll={this.handleScroll} - > - {header} - {children} - </div> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/ui/components/column_header.js b/app/javascript/themes/glitch/features/ui/components/column_header.js deleted file mode 100644 index af195ea9c..000000000 --- a/app/javascript/themes/glitch/features/ui/components/column_header.js +++ /dev/null @@ -1,35 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; - -export default class ColumnHeader extends React.PureComponent { - - static propTypes = { - icon: PropTypes.string, - type: PropTypes.string, - active: PropTypes.bool, - onClick: PropTypes.func, - columnHeaderId: PropTypes.string, - }; - - handleClick = () => { - this.props.onClick(); - } - - render () { - const { type, active, columnHeaderId } = this.props; - - let icon = ''; - - if (this.props.icon) { - icon = <i className={`fa fa-fw fa-${this.props.icon} column-header__icon`} />; - } - - return ( - <div role='heading' tabIndex='0' className={`column-header ${active ? 'active' : ''}`} onClick={this.handleClick} id={columnHeaderId || null}> - {icon} - {type} - </div> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/ui/components/column_link.js b/app/javascript/themes/glitch/features/ui/components/column_link.js deleted file mode 100644 index b845d1895..000000000 --- a/app/javascript/themes/glitch/features/ui/components/column_link.js +++ /dev/null @@ -1,39 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { Link } from 'react-router-dom'; - -const ColumnLink = ({ icon, text, to, onClick, href, method }) => { - if (href) { - return ( - <a href={href} className='column-link' data-method={method}> - <i className={`fa fa-fw fa-${icon} column-link__icon`} /> - {text} - </a> - ); - } else if (to) { - return ( - <Link to={to} className='column-link'> - <i className={`fa fa-fw fa-${icon} column-link__icon`} /> - {text} - </Link> - ); - } else { - return ( - <a onClick={onClick} className='column-link' role='button' tabIndex='0' data-method={method}> - <i className={`fa fa-fw fa-${icon} column-link__icon`} /> - {text} - </a> - ); - } -}; - -ColumnLink.propTypes = { - icon: PropTypes.string.isRequired, - text: PropTypes.string.isRequired, - to: PropTypes.string, - onClick: PropTypes.func, - href: PropTypes.string, - method: PropTypes.string, -}; - -export default ColumnLink; diff --git a/app/javascript/themes/glitch/features/ui/components/column_loading.js b/app/javascript/themes/glitch/features/ui/components/column_loading.js deleted file mode 100644 index 75f26218a..000000000 --- a/app/javascript/themes/glitch/features/ui/components/column_loading.js +++ /dev/null @@ -1,30 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; - -import Column from 'themes/glitch/components/column'; -import ColumnHeader from 'themes/glitch/components/column_header'; -import ImmutablePureComponent from 'react-immutable-pure-component'; - -export default class ColumnLoading extends ImmutablePureComponent { - - static propTypes = { - title: PropTypes.oneOfType([PropTypes.node, PropTypes.string]), - icon: PropTypes.string, - }; - - static defaultProps = { - title: '', - icon: '', - }; - - render() { - let { title, icon } = this.props; - return ( - <Column> - <ColumnHeader icon={icon} title={title} multiColumn={false} focusable={false} /> - <div className='scrollable' /> - </Column> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/ui/components/column_subheading.js b/app/javascript/themes/glitch/features/ui/components/column_subheading.js deleted file mode 100644 index 8160c4aa3..000000000 --- a/app/javascript/themes/glitch/features/ui/components/column_subheading.js +++ /dev/null @@ -1,16 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; - -const ColumnSubheading = ({ text }) => { - return ( - <div className='column-subheading'> - {text} - </div> - ); -}; - -ColumnSubheading.propTypes = { - text: PropTypes.string.isRequired, -}; - -export default ColumnSubheading; diff --git a/app/javascript/themes/glitch/features/ui/components/columns_area.js b/app/javascript/themes/glitch/features/ui/components/columns_area.js deleted file mode 100644 index 452950363..000000000 --- a/app/javascript/themes/glitch/features/ui/components/columns_area.js +++ /dev/null @@ -1,174 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { injectIntl } from 'react-intl'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import ImmutablePureComponent from 'react-immutable-pure-component'; - -import ReactSwipeableViews from 'react-swipeable-views'; -import { links, getIndex, getLink } from './tabs_bar'; - -import BundleContainer from '../containers/bundle_container'; -import ColumnLoading from './column_loading'; -import DrawerLoading from './drawer_loading'; -import BundleColumnError from './bundle_column_error'; -import { Compose, Notifications, HomeTimeline, CommunityTimeline, PublicTimeline, HashtagTimeline, DirectTimeline, FavouritedStatuses } from 'themes/glitch/util/async-components'; - -import detectPassiveEvents from 'detect-passive-events'; -import { scrollRight } from 'themes/glitch/util/scroll'; - -const componentMap = { - 'COMPOSE': Compose, - 'HOME': HomeTimeline, - 'NOTIFICATIONS': Notifications, - 'PUBLIC': PublicTimeline, - 'COMMUNITY': CommunityTimeline, - 'HASHTAG': HashtagTimeline, - 'DIRECT': DirectTimeline, - 'FAVOURITES': FavouritedStatuses, -}; - -@component => injectIntl(component, { withRef: true }) -export default class ColumnsArea extends ImmutablePureComponent { - - static contextTypes = { - router: PropTypes.object.isRequired, - }; - - static propTypes = { - intl: PropTypes.object.isRequired, - columns: ImmutablePropTypes.list.isRequired, - singleColumn: PropTypes.bool, - children: PropTypes.node, - }; - - state = { - shouldAnimate: false, - } - - componentWillReceiveProps() { - this.setState({ shouldAnimate: false }); - } - - componentDidMount() { - if (!this.props.singleColumn) { - this.node.addEventListener('wheel', this.handleWheel, detectPassiveEvents.hasSupport ? { passive: true } : false); - } - this.lastIndex = getIndex(this.context.router.history.location.pathname); - this.setState({ shouldAnimate: true }); - } - - componentWillUpdate(nextProps) { - if (this.props.singleColumn !== nextProps.singleColumn && nextProps.singleColumn) { - this.node.removeEventListener('wheel', this.handleWheel); - } - } - - componentDidUpdate(prevProps) { - if (this.props.singleColumn !== prevProps.singleColumn && !this.props.singleColumn) { - this.node.addEventListener('wheel', this.handleWheel, detectPassiveEvents.hasSupport ? { passive: true } : false); - } - this.lastIndex = getIndex(this.context.router.history.location.pathname); - this.setState({ shouldAnimate: true }); - } - - componentWillUnmount () { - if (!this.props.singleColumn) { - this.node.removeEventListener('wheel', this.handleWheel); - } - } - - handleChildrenContentChange() { - if (!this.props.singleColumn) { - this._interruptScrollAnimation = scrollRight(this.node, this.node.scrollWidth - window.innerWidth); - } - } - - handleSwipe = (index) => { - this.pendingIndex = index; - - const nextLinkTranslationId = links[index].props['data-preview-title-id']; - const currentLinkSelector = '.tabs-bar__link.active'; - const nextLinkSelector = `.tabs-bar__link[data-preview-title-id="${nextLinkTranslationId}"]`; - - // HACK: Remove the active class from the current link and set it to the next one - // React-router does this for us, but too late, feeling laggy. - document.querySelector(currentLinkSelector).classList.remove('active'); - document.querySelector(nextLinkSelector).classList.add('active'); - } - - handleAnimationEnd = () => { - if (typeof this.pendingIndex === 'number') { - this.context.router.history.push(getLink(this.pendingIndex)); - this.pendingIndex = null; - } - } - - handleWheel = () => { - if (typeof this._interruptScrollAnimation !== 'function') { - return; - } - - this._interruptScrollAnimation(); - } - - setRef = (node) => { - this.node = node; - } - - renderView = (link, index) => { - const columnIndex = getIndex(this.context.router.history.location.pathname); - const title = this.props.intl.formatMessage({ id: link.props['data-preview-title-id'] }); - const icon = link.props['data-preview-icon']; - - const view = (index === columnIndex) ? - React.cloneElement(this.props.children) : - <ColumnLoading title={title} icon={icon} />; - - return ( - <div className='columns-area' key={index}> - {view} - </div> - ); - } - - renderLoading = columnId => () => { - return columnId === 'COMPOSE' ? <DrawerLoading /> : <ColumnLoading />; - } - - renderError = (props) => { - return <BundleColumnError {...props} />; - } - - render () { - const { columns, children, singleColumn } = this.props; - const { shouldAnimate } = this.state; - - const columnIndex = getIndex(this.context.router.history.location.pathname); - this.pendingIndex = null; - - if (singleColumn) { - return columnIndex !== -1 ? ( - <ReactSwipeableViews index={columnIndex} onChangeIndex={this.handleSwipe} onTransitionEnd={this.handleAnimationEnd} animateTransitions={shouldAnimate} springConfig={{ duration: '400ms', delay: '0s', easeFunction: 'ease' }} style={{ height: '100%' }}> - {links.map(this.renderView)} - </ReactSwipeableViews> - ) : <div className='columns-area'>{children}</div>; - } - - return ( - <div className='columns-area' ref={this.setRef}> - {columns.map(column => { - const params = column.get('params', null) === null ? null : column.get('params').toJS(); - - return ( - <BundleContainer key={column.get('uuid')} fetchComponent={componentMap[column.get('id')]} loading={this.renderLoading(column.get('id'))} error={this.renderError}> - {SpecificComponent => <SpecificComponent columnId={column.get('uuid')} params={params} multiColumn />} - </BundleContainer> - ); - })} - - {React.Children.map(children, child => React.cloneElement(child, { multiColumn: true }))} - </div> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/ui/components/confirmation_modal.js b/app/javascript/themes/glitch/features/ui/components/confirmation_modal.js deleted file mode 100644 index 3d568aec3..000000000 --- a/app/javascript/themes/glitch/features/ui/components/confirmation_modal.js +++ /dev/null @@ -1,53 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { injectIntl, FormattedMessage } from 'react-intl'; -import Button from 'themes/glitch/components/button'; - -@injectIntl -export default class ConfirmationModal extends React.PureComponent { - - static propTypes = { - message: PropTypes.node.isRequired, - confirm: PropTypes.string.isRequired, - onClose: PropTypes.func.isRequired, - onConfirm: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - }; - - componentDidMount() { - this.button.focus(); - } - - handleClick = () => { - this.props.onClose(); - this.props.onConfirm(); - } - - handleCancel = () => { - this.props.onClose(); - } - - setRef = (c) => { - this.button = c; - } - - render () { - const { message, confirm } = this.props; - - return ( - <div className='modal-root__modal confirmation-modal'> - <div className='confirmation-modal__container'> - {message} - </div> - - <div className='confirmation-modal__action-bar'> - <Button onClick={this.handleCancel} className='confirmation-modal__cancel-button'> - <FormattedMessage id='confirmation_modal.cancel' defaultMessage='Cancel' /> - </Button> - <Button text={confirm} onClick={this.handleClick} ref={this.setRef} /> - </div> - </div> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/ui/components/doodle_modal.js b/app/javascript/themes/glitch/features/ui/components/doodle_modal.js deleted file mode 100644 index 819656dbf..000000000 --- a/app/javascript/themes/glitch/features/ui/components/doodle_modal.js +++ /dev/null @@ -1,614 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import Button from 'themes/glitch/components/button'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import Atrament from 'atrament'; // the doodling library -import { connect } from 'react-redux'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import { doodleSet, uploadCompose } from 'themes/glitch/actions/compose'; -import IconButton from 'themes/glitch/components/icon_button'; -import { debounce, mapValues } from 'lodash'; -import classNames from 'classnames'; - -// palette nicked from MyPaint, CC0 -const palette = [ - ['rgb( 0, 0, 0)', 'Black'], - ['rgb( 38, 38, 38)', 'Gray 15'], - ['rgb( 77, 77, 77)', 'Grey 30'], - ['rgb(128, 128, 128)', 'Grey 50'], - ['rgb(171, 171, 171)', 'Grey 67'], - ['rgb(217, 217, 217)', 'Grey 85'], - ['rgb(255, 255, 255)', 'White'], - ['rgb(128, 0, 0)', 'Maroon'], - ['rgb(209, 0, 0)', 'English-red'], - ['rgb(255, 54, 34)', 'Tomato'], - ['rgb(252, 60, 3)', 'Orange-red'], - ['rgb(255, 140, 105)', 'Salmon'], - ['rgb(252, 232, 32)', 'Cadium-yellow'], - ['rgb(243, 253, 37)', 'Lemon yellow'], - ['rgb(121, 5, 35)', 'Dark crimson'], - ['rgb(169, 32, 62)', 'Deep carmine'], - ['rgb(255, 140, 0)', 'Orange'], - ['rgb(255, 168, 18)', 'Dark tangerine'], - ['rgb(217, 144, 88)', 'Persian orange'], - ['rgb(194, 178, 128)', 'Sand'], - ['rgb(255, 229, 180)', 'Peach'], - ['rgb(100, 54, 46)', 'Bole'], - ['rgb(108, 41, 52)', 'Dark cordovan'], - ['rgb(163, 65, 44)', 'Chestnut'], - ['rgb(228, 136, 100)', 'Dark salmon'], - ['rgb(255, 195, 143)', 'Apricot'], - ['rgb(255, 219, 188)', 'Unbleached silk'], - ['rgb(242, 227, 198)', 'Straw'], - ['rgb( 53, 19, 13)', 'Bistre'], - ['rgb( 84, 42, 14)', 'Dark chocolate'], - ['rgb(102, 51, 43)', 'Burnt sienna'], - ['rgb(184, 66, 0)', 'Sienna'], - ['rgb(216, 153, 12)', 'Yellow ochre'], - ['rgb(210, 180, 140)', 'Tan'], - ['rgb(232, 204, 144)', 'Dark wheat'], - ['rgb( 0, 49, 83)', 'Prussian blue'], - ['rgb( 48, 69, 119)', 'Dark grey blue'], - ['rgb( 0, 71, 171)', 'Cobalt blue'], - ['rgb( 31, 117, 254)', 'Blue'], - ['rgb(120, 180, 255)', 'Bright french blue'], - ['rgb(171, 200, 255)', 'Bright steel blue'], - ['rgb(208, 231, 255)', 'Ice blue'], - ['rgb( 30, 51, 58)', 'Medium jungle green'], - ['rgb( 47, 79, 79)', 'Dark slate grey'], - ['rgb( 74, 104, 93)', 'Dark grullo green'], - ['rgb( 0, 128, 128)', 'Teal'], - ['rgb( 67, 170, 176)', 'Turquoise'], - ['rgb(109, 174, 199)', 'Cerulean frost'], - ['rgb(173, 217, 186)', 'Tiffany green'], - ['rgb( 22, 34, 29)', 'Gray-asparagus'], - ['rgb( 36, 48, 45)', 'Medium dark teal'], - ['rgb( 74, 104, 93)', 'Xanadu'], - ['rgb(119, 198, 121)', 'Mint'], - ['rgb(175, 205, 182)', 'Timberwolf'], - ['rgb(185, 245, 246)', 'Celeste'], - ['rgb(193, 255, 234)', 'Aquamarine'], - ['rgb( 29, 52, 35)', 'Cal Poly Pomona'], - ['rgb( 1, 68, 33)', 'Forest green'], - ['rgb( 42, 128, 0)', 'Napier green'], - ['rgb(128, 128, 0)', 'Olive'], - ['rgb( 65, 156, 105)', 'Sea green'], - ['rgb(189, 246, 29)', 'Green-yellow'], - ['rgb(231, 244, 134)', 'Bright chartreuse'], - ['rgb(138, 23, 137)', 'Purple'], - ['rgb( 78, 39, 138)', 'Violet'], - ['rgb(193, 75, 110)', 'Dark thulian pink'], - ['rgb(222, 49, 99)', 'Cerise'], - ['rgb(255, 20, 147)', 'Deep pink'], - ['rgb(255, 102, 204)', 'Rose pink'], - ['rgb(255, 203, 219)', 'Pink'], - ['rgb(255, 255, 255)', 'White'], - ['rgb(229, 17, 1)', 'RGB Red'], - ['rgb( 0, 255, 0)', 'RGB Green'], - ['rgb( 0, 0, 255)', 'RGB Blue'], - ['rgb( 0, 255, 255)', 'CMYK Cyan'], - ['rgb(255, 0, 255)', 'CMYK Magenta'], - ['rgb(255, 255, 0)', 'CMYK Yellow'], -]; - -// re-arrange to the right order for display -let palReordered = []; -for (let row = 0; row < 7; row++) { - for (let col = 0; col < 11; col++) { - palReordered.push(palette[col * 7 + row]); - } - palReordered.push(null); // null indicates a <br /> -} - -// Utility for converting base64 image to binary for upload -// https://stackoverflow.com/questions/35940290/how-to-convert-base64-string-to-javascript-file-object-like-as-from-file-input-f -function dataURLtoFile(dataurl, filename) { - let arr = dataurl.split(','), mime = arr[0].match(/:(.*?);/)[1], - bstr = atob(arr[1]), n = bstr.length, u8arr = new Uint8Array(n); - while(n--){ - u8arr[n] = bstr.charCodeAt(n); - } - return new File([u8arr], filename, { type: mime }); -} - -const DOODLE_SIZES = { - normal: [500, 500, 'Square 500'], - tootbanner: [702, 330, 'Tootbanner'], - s640x480: [640, 480, '640×480 - 480p'], - s800x600: [800, 600, '800×600 - SVGA'], - s720x480: [720, 405, '720x405 - 16:9'], -}; - - -const mapStateToProps = state => ({ - options: state.getIn(['compose', 'doodle']), -}); - -const mapDispatchToProps = dispatch => ({ - /** Set options in the redux store */ - setOpt: (opts) => dispatch(doodleSet(opts)), - /** Submit doodle for upload */ - submit: (file) => dispatch(uploadCompose([file])), -}); - -/** - * Doodling dialog with drawing canvas - * - * Keyboard shortcuts: - * - Delete: Clear screen, fill with background color - * - Backspace, Ctrl+Z: Undo one step - * - Ctrl held while drawing: Use background color - * - Shift held while clicking screen: Use fill tool - * - * Palette: - * - Left mouse button: pick foreground - * - Ctrl + left mouse button: pick background - * - Right mouse button: pick background - */ -@connect(mapStateToProps, mapDispatchToProps) -export default class DoodleModal extends ImmutablePureComponent { - - static propTypes = { - options: ImmutablePropTypes.map, - onClose: PropTypes.func.isRequired, - setOpt: PropTypes.func.isRequired, - submit: PropTypes.func.isRequired, - }; - - //region Option getters/setters - - /** Foreground color */ - get fg () { - return this.props.options.get('fg'); - } - set fg (value) { - this.props.setOpt({ fg: value }); - } - - /** Background color */ - get bg () { - return this.props.options.get('bg'); - } - set bg (value) { - this.props.setOpt({ bg: value }); - } - - /** Swap Fg and Bg for drawing */ - get swapped () { - return this.props.options.get('swapped'); - } - set swapped (value) { - this.props.setOpt({ swapped: value }); - } - - /** Mode - 'draw' or 'fill' */ - get mode () { - return this.props.options.get('mode'); - } - set mode (value) { - this.props.setOpt({ mode: value }); - } - - /** Base line weight */ - get weight () { - return this.props.options.get('weight'); - } - set weight (value) { - this.props.setOpt({ weight: value }); - } - - /** Drawing opacity */ - get opacity () { - return this.props.options.get('opacity'); - } - set opacity (value) { - this.props.setOpt({ opacity: value }); - } - - /** Adaptive stroke - change width with speed */ - get adaptiveStroke () { - return this.props.options.get('adaptiveStroke'); - } - set adaptiveStroke (value) { - this.props.setOpt({ adaptiveStroke: value }); - } - - /** Smoothing (for mouse drawing) */ - get smoothing () { - return this.props.options.get('smoothing'); - } - set smoothing (value) { - this.props.setOpt({ smoothing: value }); - } - - /** Size preset */ - get size () { - return this.props.options.get('size'); - } - set size (value) { - this.props.setOpt({ size: value }); - } - - //endregion - - /** Key up handler */ - handleKeyUp = (e) => { - if (e.target.nodeName === 'INPUT') return; - - if (e.key === 'Delete') { - e.preventDefault(); - this.handleClearBtn(); - return; - } - - if (e.key === 'Backspace' || (e.key === 'z' && (e.ctrlKey || e.metaKey))) { - e.preventDefault(); - this.undo(); - } - - if (e.key === 'Control' || e.key === 'Meta') { - this.controlHeld = false; - this.swapped = false; - } - - if (e.key === 'Shift') { - this.shiftHeld = false; - this.mode = 'draw'; - } - }; - - /** Key down handler */ - handleKeyDown = (e) => { - if (e.key === 'Control' || e.key === 'Meta') { - this.controlHeld = true; - this.swapped = true; - } - - if (e.key === 'Shift') { - this.shiftHeld = true; - this.mode = 'fill'; - } - }; - - /** - * Component installed in the DOM, do some initial set-up - */ - componentDidMount () { - this.controlHeld = false; - this.shiftHeld = false; - this.swapped = false; - window.addEventListener('keyup', this.handleKeyUp, false); - window.addEventListener('keydown', this.handleKeyDown, false); - }; - - /** - * Tear component down - */ - componentWillUnmount () { - window.removeEventListener('keyup', this.handleKeyUp, false); - window.removeEventListener('keydown', this.handleKeyDown, false); - if (this.sketcher) this.sketcher.destroy(); - } - - /** - * Set reference to the canvas element. - * This is called during component init - * - * @param elem - canvas element - */ - setCanvasRef = (elem) => { - this.canvas = elem; - if (elem) { - elem.addEventListener('dirty', () => { - this.saveUndo(); - this.sketcher._dirty = false; - }); - - elem.addEventListener('click', () => { - // sketcher bug - does not fire dirty on fill - if (this.mode === 'fill') { - this.saveUndo(); - } - }); - - // prevent context menu - elem.addEventListener('contextmenu', (e) => { - e.preventDefault(); - }); - - elem.addEventListener('mousedown', (e) => { - if (e.button === 2) { - this.swapped = true; - } - }); - - elem.addEventListener('mouseup', (e) => { - if (e.button === 2) { - this.swapped = this.controlHeld; - } - }); - - this.initSketcher(elem); - this.mode = 'draw'; // Reset mode - it's confusing if left at 'fill' - } - }; - - /** - * Set up the sketcher instance - * - * @param canvas - canvas element. Null if we're just resizing - */ - initSketcher (canvas = null) { - const sizepreset = DOODLE_SIZES[this.size]; - - if (this.sketcher) this.sketcher.destroy(); - this.sketcher = new Atrament(canvas || this.canvas, sizepreset[0], sizepreset[1]); - - if (canvas) { - this.ctx = this.sketcher.context; - this.updateSketcherSettings(); - } - - this.clearScreen(); - } - - /** - * Done button handler - */ - onDoneButton = () => { - const dataUrl = this.sketcher.toImage(); - const file = dataURLtoFile(dataUrl, 'doodle.png'); - this.props.submit(file); - this.props.onClose(); // close dialog - }; - - /** - * Cancel button handler - */ - onCancelButton = () => { - if (this.undos.length > 1 && !confirm('Discard doodle? All changes will be lost!')) { - return; - } - - this.props.onClose(); // close dialog - }; - - /** - * Update sketcher options based on state - */ - updateSketcherSettings () { - if (!this.sketcher) return; - - if (this.oldSize !== this.size) this.initSketcher(); - - this.sketcher.color = (this.swapped ? this.bg : this.fg); - this.sketcher.opacity = this.opacity; - this.sketcher.weight = this.weight; - this.sketcher.mode = this.mode; - this.sketcher.smoothing = this.smoothing; - this.sketcher.adaptiveStroke = this.adaptiveStroke; - - this.oldSize = this.size; - } - - /** - * Fill screen with background color - */ - clearScreen = () => { - this.ctx.fillStyle = this.bg; - this.ctx.fillRect(-1, -1, this.canvas.width+2, this.canvas.height+2); - this.undos = []; - - this.doSaveUndo(); - }; - - /** - * Undo one step - */ - undo = () => { - if (this.undos.length > 1) { - this.undos.pop(); - const buf = this.undos.pop(); - - this.sketcher.clear(); - this.ctx.putImageData(buf, 0, 0); - this.doSaveUndo(); - } - }; - - /** - * Save canvas content into the undo buffer immediately - */ - doSaveUndo = () => { - this.undos.push(this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height)); - }; - - /** - * Called on each canvas change. - * Saves canvas content to the undo buffer after some period of inactivity. - */ - saveUndo = debounce(() => { - this.doSaveUndo(); - }, 100); - - /** - * Palette left click. - * Selects Fg color (or Bg, if Control/Meta is held) - * - * @param e - event - */ - onPaletteClick = (e) => { - const c = e.target.dataset.color; - - if (this.controlHeld) { - this.bg = c; - } else { - this.fg = c; - } - - e.target.blur(); - e.preventDefault(); - }; - - /** - * Palette right click. - * Selects Bg color - * - * @param e - event - */ - onPaletteRClick = (e) => { - this.bg = e.target.dataset.color; - e.target.blur(); - e.preventDefault(); - }; - - /** - * Handle click on the Draw mode button - * - * @param e - event - */ - setModeDraw = (e) => { - this.mode = 'draw'; - e.target.blur(); - }; - - /** - * Handle click on the Fill mode button - * - * @param e - event - */ - setModeFill = (e) => { - this.mode = 'fill'; - e.target.blur(); - }; - - /** - * Handle click on Smooth checkbox - * - * @param e - event - */ - tglSmooth = (e) => { - this.smoothing = !this.smoothing; - e.target.blur(); - }; - - /** - * Handle click on Adaptive checkbox - * - * @param e - event - */ - tglAdaptive = (e) => { - this.adaptiveStroke = !this.adaptiveStroke; - e.target.blur(); - }; - - /** - * Handle change of the Weight input field - * - * @param e - event - */ - setWeight = (e) => { - this.weight = +e.target.value || 1; - }; - - /** - * Set size - clalback from the select box - * - * @param e - event - */ - changeSize = (e) => { - let newSize = e.target.value; - if (newSize === this.oldSize) return; - - if (this.undos.length > 1 && !confirm('Change size? This will erase your drawing!')) { - return; - } - - this.size = newSize; - }; - - handleClearBtn = () => { - if (this.undos.length > 1 && !confirm('Clear screen? This will erase your drawing!')) { - return; - } - - this.clearScreen(); - }; - - /** - * Render the component - */ - render () { - this.updateSketcherSettings(); - - return ( - <div className='modal-root__modal doodle-modal'> - <div className='doodle-modal__container'> - <canvas ref={this.setCanvasRef} /> - </div> - - <div className='doodle-modal__action-bar'> - <div className='doodle-toolbar'> - <Button text='Done' onClick={this.onDoneButton} /> - <Button text='Cancel' onClick={this.onCancelButton} /> - </div> - <div className='filler' /> - <div className='doodle-toolbar with-inputs'> - <div> - <label htmlFor='dd_smoothing'>Smoothing</label> - <span className='val'> - <input type='checkbox' id='dd_smoothing' onChange={this.tglSmooth} checked={this.smoothing} /> - </span> - </div> - <div> - <label htmlFor='dd_adaptive'>Adaptive</label> - <span className='val'> - <input type='checkbox' id='dd_adaptive' onChange={this.tglAdaptive} checked={this.adaptiveStroke} /> - </span> - </div> - <div> - <label htmlFor='dd_weight'>Weight</label> - <span className='val'> - <input type='number' min={1} id='dd_weight' value={this.weight} onChange={this.setWeight} /> - </span> - </div> - <div> - <select aria-label='Canvas size' onInput={this.changeSize} defaultValue={this.size}> - { Object.values(mapValues(DOODLE_SIZES, (val, k) => - <option key={k} value={k}>{val[2]}</option> - )) } - </select> - </div> - </div> - <div className='doodle-toolbar'> - <IconButton icon='pencil' title='Draw' label='Draw' onClick={this.setModeDraw} size={18} active={this.mode === 'draw'} inverted /> - <IconButton icon='bath' title='Fill' label='Fill' onClick={this.setModeFill} size={18} active={this.mode === 'fill'} inverted /> - <IconButton icon='undo' title='Undo' label='Undo' onClick={this.undo} size={18} inverted /> - <IconButton icon='trash' title='Clear' label='Clear' onClick={this.handleClearBtn} size={18} inverted /> - </div> - <div className='doodle-palette'> - { - palReordered.map((c, i) => - c === null ? - <br key={i} /> : - <button - key={i} - style={{ backgroundColor: c[0] }} - onClick={this.onPaletteClick} - onContextMenu={this.onPaletteRClick} - data-color={c[0]} - title={c[1]} - className={classNames({ - 'foreground': this.fg === c[0], - 'background': this.bg === c[0], - })} - /> - ) - } - </div> - </div> - </div> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/ui/components/drawer_loading.js b/app/javascript/themes/glitch/features/ui/components/drawer_loading.js deleted file mode 100644 index 08b0d2347..000000000 --- a/app/javascript/themes/glitch/features/ui/components/drawer_loading.js +++ /dev/null @@ -1,11 +0,0 @@ -import React from 'react'; - -const DrawerLoading = () => ( - <div className='drawer'> - <div className='drawer__pager'> - <div className='drawer__inner' /> - </div> - </div> -); - -export default DrawerLoading; diff --git a/app/javascript/themes/glitch/features/ui/components/embed_modal.js b/app/javascript/themes/glitch/features/ui/components/embed_modal.js deleted file mode 100644 index 1afffb51b..000000000 --- a/app/javascript/themes/glitch/features/ui/components/embed_modal.js +++ /dev/null @@ -1,84 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import { FormattedMessage, injectIntl } from 'react-intl'; -import axios from 'axios'; - -@injectIntl -export default class EmbedModal extends ImmutablePureComponent { - - static propTypes = { - url: PropTypes.string.isRequired, - onClose: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - } - - state = { - loading: false, - oembed: null, - }; - - componentDidMount () { - const { url } = this.props; - - this.setState({ loading: true }); - - axios.post('/api/web/embed', { url }).then(res => { - this.setState({ loading: false, oembed: res.data }); - - const iframeDocument = this.iframe.contentWindow.document; - - iframeDocument.open(); - iframeDocument.write(res.data.html); - iframeDocument.close(); - - iframeDocument.body.style.margin = 0; - this.iframe.width = iframeDocument.body.scrollWidth; - this.iframe.height = iframeDocument.body.scrollHeight; - }); - } - - setIframeRef = c => { - this.iframe = c; - } - - handleTextareaClick = (e) => { - e.target.select(); - } - - render () { - const { oembed } = this.state; - - return ( - <div className='modal-root__modal embed-modal'> - <h4><FormattedMessage id='status.embed' defaultMessage='Embed' /></h4> - - <div className='embed-modal__container'> - <p className='hint'> - <FormattedMessage id='embed.instructions' defaultMessage='Embed this status on your website by copying the code below.' /> - </p> - - <input - type='text' - className='embed-modal__html' - readOnly - value={oembed && oembed.html || ''} - onClick={this.handleTextareaClick} - /> - - <p className='hint'> - <FormattedMessage id='embed.preview' defaultMessage='Here is what it will look like:' /> - </p> - - <iframe - className='embed-modal__iframe' - frameBorder='0' - ref={this.setIframeRef} - title='preview' - /> - </div> - </div> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/ui/components/image_loader.js b/app/javascript/themes/glitch/features/ui/components/image_loader.js deleted file mode 100644 index aad594380..000000000 --- a/app/javascript/themes/glitch/features/ui/components/image_loader.js +++ /dev/null @@ -1,152 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import classNames from 'classnames'; - -export default class ImageLoader extends React.PureComponent { - - static propTypes = { - alt: PropTypes.string, - src: PropTypes.string.isRequired, - previewSrc: PropTypes.string.isRequired, - width: PropTypes.number, - height: PropTypes.number, - } - - static defaultProps = { - alt: '', - width: null, - height: null, - }; - - state = { - loading: true, - error: false, - } - - removers = []; - - get canvasContext() { - if (!this.canvas) { - return null; - } - this._canvasContext = this._canvasContext || this.canvas.getContext('2d'); - return this._canvasContext; - } - - componentDidMount () { - this.loadImage(this.props); - } - - componentWillReceiveProps (nextProps) { - if (this.props.src !== nextProps.src) { - this.loadImage(nextProps); - } - } - - loadImage (props) { - this.removeEventListeners(); - this.setState({ loading: true, error: false }); - Promise.all([ - this.loadPreviewCanvas(props), - this.hasSize() && this.loadOriginalImage(props), - ].filter(Boolean)) - .then(() => { - this.setState({ loading: false, error: false }); - this.clearPreviewCanvas(); - }) - .catch(() => this.setState({ loading: false, error: true })); - } - - loadPreviewCanvas = ({ previewSrc, width, height }) => new Promise((resolve, reject) => { - const image = new Image(); - const removeEventListeners = () => { - image.removeEventListener('error', handleError); - image.removeEventListener('load', handleLoad); - }; - const handleError = () => { - removeEventListeners(); - reject(); - }; - const handleLoad = () => { - removeEventListeners(); - this.canvasContext.drawImage(image, 0, 0, width, height); - resolve(); - }; - image.addEventListener('error', handleError); - image.addEventListener('load', handleLoad); - image.src = previewSrc; - this.removers.push(removeEventListeners); - }) - - clearPreviewCanvas () { - const { width, height } = this.canvas; - this.canvasContext.clearRect(0, 0, width, height); - } - - loadOriginalImage = ({ src }) => new Promise((resolve, reject) => { - const image = new Image(); - const removeEventListeners = () => { - image.removeEventListener('error', handleError); - image.removeEventListener('load', handleLoad); - }; - const handleError = () => { - removeEventListeners(); - reject(); - }; - const handleLoad = () => { - removeEventListeners(); - resolve(); - }; - image.addEventListener('error', handleError); - image.addEventListener('load', handleLoad); - image.src = src; - this.removers.push(removeEventListeners); - }); - - removeEventListeners () { - this.removers.forEach(listeners => listeners()); - this.removers = []; - } - - hasSize () { - const { width, height } = this.props; - return typeof width === 'number' && typeof height === 'number'; - } - - setCanvasRef = c => { - this.canvas = c; - } - - render () { - const { alt, src, width, height } = this.props; - const { loading } = this.state; - - const className = classNames('image-loader', { - 'image-loader--loading': loading, - 'image-loader--amorphous': !this.hasSize(), - }); - - return ( - <div className={className}> - <canvas - className='image-loader__preview-canvas' - width={width} - height={height} - ref={this.setCanvasRef} - style={{ opacity: loading ? 1 : 0 }} - /> - - {!loading && ( - <img - alt={alt} - className='image-loader__img' - src={src} - width={width} - height={height} - /> - )} - </div> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/ui/components/media_modal.js b/app/javascript/themes/glitch/features/ui/components/media_modal.js deleted file mode 100644 index 1dad972b2..000000000 --- a/app/javascript/themes/glitch/features/ui/components/media_modal.js +++ /dev/null @@ -1,126 +0,0 @@ -import React from 'react'; -import ReactSwipeableViews from 'react-swipeable-views'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import PropTypes from 'prop-types'; -import ExtendedVideoPlayer from 'themes/glitch/components/extended_video_player'; -import { defineMessages, injectIntl } from 'react-intl'; -import IconButton from 'themes/glitch/components/icon_button'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import ImageLoader from './image_loader'; - -const messages = defineMessages({ - close: { id: 'lightbox.close', defaultMessage: 'Close' }, - previous: { id: 'lightbox.previous', defaultMessage: 'Previous' }, - next: { id: 'lightbox.next', defaultMessage: 'Next' }, -}); - -@injectIntl -export default class MediaModal extends ImmutablePureComponent { - - static propTypes = { - media: ImmutablePropTypes.list.isRequired, - index: PropTypes.number.isRequired, - onClose: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - }; - - state = { - index: null, - }; - - handleSwipe = (index) => { - this.setState({ index: index % this.props.media.size }); - } - - handleNextClick = () => { - this.setState({ index: (this.getIndex() + 1) % this.props.media.size }); - } - - handlePrevClick = () => { - this.setState({ index: (this.props.media.size + this.getIndex() - 1) % this.props.media.size }); - } - - handleChangeIndex = (e) => { - const index = Number(e.currentTarget.getAttribute('data-index')); - this.setState({ index: index % this.props.media.size }); - } - - handleKeyUp = (e) => { - switch(e.key) { - case 'ArrowLeft': - this.handlePrevClick(); - break; - case 'ArrowRight': - this.handleNextClick(); - break; - } - } - - componentDidMount () { - window.addEventListener('keyup', this.handleKeyUp, false); - } - - componentWillUnmount () { - window.removeEventListener('keyup', this.handleKeyUp); - } - - getIndex () { - return this.state.index !== null ? this.state.index : this.props.index; - } - - render () { - const { media, intl, onClose } = this.props; - - const index = this.getIndex(); - let pagination = []; - - const leftNav = media.size > 1 && <button tabIndex='0' className='modal-container__nav modal-container__nav--left' onClick={this.handlePrevClick} aria-label={intl.formatMessage(messages.previous)}><i className='fa fa-fw fa-chevron-left' /></button>; - const rightNav = media.size > 1 && <button tabIndex='0' className='modal-container__nav modal-container__nav--right' onClick={this.handleNextClick} aria-label={intl.formatMessage(messages.next)}><i className='fa fa-fw fa-chevron-right' /></button>; - - if (media.size > 1) { - pagination = media.map((item, i) => { - const classes = ['media-modal__button']; - if (i === index) { - classes.push('media-modal__button--active'); - } - return (<li className='media-modal__page-dot' key={i}><button tabIndex='0' className={classes.join(' ')} onClick={this.handleChangeIndex} data-index={i}>{i + 1}</button></li>); - }); - } - - const content = media.map((image) => { - const width = image.getIn(['meta', 'original', 'width']) || null; - const height = image.getIn(['meta', 'original', 'height']) || null; - - if (image.get('type') === 'image') { - return <ImageLoader previewSrc={image.get('preview_url')} src={image.get('url')} width={width} height={height} alt={image.get('description')} key={image.get('preview_url')} />; - } else if (image.get('type') === 'gifv') { - return <ExtendedVideoPlayer src={image.get('url')} muted controls={false} width={width} height={height} key={image.get('preview_url')} alt={image.get('description')} />; - } - - return null; - }).toArray(); - - const containerStyle = { - alignItems: 'center', // center vertically - }; - - return ( - <div className='modal-root__modal media-modal'> - {leftNav} - - <div className='media-modal__content'> - <IconButton className='media-modal__close' title={intl.formatMessage(messages.close)} icon='times' onClick={onClose} size={16} /> - <ReactSwipeableViews containerStyle={containerStyle} onChangeIndex={this.handleSwipe} index={index}> - {content} - </ReactSwipeableViews> - </div> - <ul className='media-modal__pagination'> - {pagination} - </ul> - - {rightNav} - </div> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/ui/components/modal_loading.js b/app/javascript/themes/glitch/features/ui/components/modal_loading.js deleted file mode 100644 index e14d20fbb..000000000 --- a/app/javascript/themes/glitch/features/ui/components/modal_loading.js +++ /dev/null @@ -1,20 +0,0 @@ -import React from 'react'; - -import LoadingIndicator from 'themes/glitch/components/loading_indicator'; - -// Keep the markup in sync with <BundleModalError /> -// (make sure they have the same dimensions) -const ModalLoading = () => ( - <div className='modal-root__modal error-modal'> - <div className='error-modal__body'> - <LoadingIndicator /> - </div> - <div className='error-modal__footer'> - <div> - <button className='error-modal__nav onboarding-modal__skip' /> - </div> - </div> - </div> -); - -export default ModalLoading; diff --git a/app/javascript/themes/glitch/features/ui/components/modal_root.js b/app/javascript/themes/glitch/features/ui/components/modal_root.js deleted file mode 100644 index fbe794170..000000000 --- a/app/javascript/themes/glitch/features/ui/components/modal_root.js +++ /dev/null @@ -1,131 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import BundleContainer from '../containers/bundle_container'; -import BundleModalError from './bundle_modal_error'; -import ModalLoading from './modal_loading'; -import ActionsModal from './actions_modal'; -import MediaModal from './media_modal'; -import VideoModal from './video_modal'; -import BoostModal from './boost_modal'; -import DoodleModal from './doodle_modal'; -import ConfirmationModal from './confirmation_modal'; -import { - OnboardingModal, - MuteModal, - ReportModal, - SettingsModal, - EmbedModal, -} from 'themes/glitch/util/async-components'; - -const MODAL_COMPONENTS = { - 'MEDIA': () => Promise.resolve({ default: MediaModal }), - 'ONBOARDING': OnboardingModal, - 'VIDEO': () => Promise.resolve({ default: VideoModal }), - 'BOOST': () => Promise.resolve({ default: BoostModal }), - 'DOODLE': () => Promise.resolve({ default: DoodleModal }), - 'CONFIRM': () => Promise.resolve({ default: ConfirmationModal }), - 'MUTE': MuteModal, - 'REPORT': ReportModal, - 'SETTINGS': SettingsModal, - 'ACTIONS': () => Promise.resolve({ default: ActionsModal }), - 'EMBED': EmbedModal, -}; - -export default class ModalRoot extends React.PureComponent { - - static propTypes = { - type: PropTypes.string, - props: PropTypes.object, - onClose: PropTypes.func.isRequired, - }; - - state = { - revealed: false, - }; - - handleKeyUp = (e) => { - if ((e.key === 'Escape' || e.key === 'Esc' || e.keyCode === 27) - && !!this.props.type && !this.props.props.noEsc) { - this.props.onClose(); - } - } - - componentDidMount () { - window.addEventListener('keyup', this.handleKeyUp, false); - } - - componentWillReceiveProps (nextProps) { - if (!!nextProps.type && !this.props.type) { - this.activeElement = document.activeElement; - - this.getSiblings().forEach(sibling => sibling.setAttribute('inert', true)); - } else if (!nextProps.type) { - this.setState({ revealed: false }); - } - } - - componentDidUpdate (prevProps) { - if (!this.props.type && !!prevProps.type) { - this.getSiblings().forEach(sibling => sibling.removeAttribute('inert')); - this.activeElement.focus(); - this.activeElement = null; - } - if (this.props.type) { - requestAnimationFrame(() => { - this.setState({ revealed: true }); - }); - } - } - - componentWillUnmount () { - window.removeEventListener('keyup', this.handleKeyUp); - } - - getSiblings = () => { - return Array(...this.node.parentElement.childNodes).filter(node => node !== this.node); - } - - setRef = ref => { - this.node = ref; - } - - renderLoading = modalId => () => { - return ['MEDIA', 'VIDEO', 'BOOST', 'DOODLE', 'CONFIRM', 'ACTIONS'].indexOf(modalId) === -1 ? <ModalLoading /> : null; - } - - renderError = (props) => { - const { onClose } = this.props; - - return <BundleModalError {...props} onClose={onClose} />; - } - - render () { - const { type, props, onClose } = this.props; - const { revealed } = this.state; - const visible = !!type; - - if (!visible) { - return ( - <div className='modal-root' ref={this.setRef} style={{ opacity: 0 }} /> - ); - } - - return ( - <div className='modal-root' ref={this.setRef} style={{ opacity: revealed ? 1 : 0 }}> - <div style={{ pointerEvents: visible ? 'auto' : 'none' }}> - <div role='presentation' className='modal-root__overlay' onClick={onClose} /> - <div role='dialog' className='modal-root__container'> - { - visible ? - (<BundleContainer fetchComponent={MODAL_COMPONENTS[type]} loading={this.renderLoading(type)} error={this.renderError} renderDelay={200}> - {(SpecificComponent) => <SpecificComponent {...props} onClose={onClose} />} - </BundleContainer>) : - null - } - </div> - </div> - </div> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/ui/components/mute_modal.js b/app/javascript/themes/glitch/features/ui/components/mute_modal.js deleted file mode 100644 index ffccdc84d..000000000 --- a/app/javascript/themes/glitch/features/ui/components/mute_modal.js +++ /dev/null @@ -1,105 +0,0 @@ -import React from 'react'; -import { connect } from 'react-redux'; -import PropTypes from 'prop-types'; -import { injectIntl, FormattedMessage } from 'react-intl'; -import Toggle from 'react-toggle'; -import Button from 'themes/glitch/components/button'; -import { closeModal } from 'themes/glitch/actions/modal'; -import { muteAccount } from 'themes/glitch/actions/accounts'; -import { toggleHideNotifications } from 'themes/glitch/actions/mutes'; - - -const mapStateToProps = state => { - return { - isSubmitting: state.getIn(['reports', 'new', 'isSubmitting']), - account: state.getIn(['mutes', 'new', 'account']), - notifications: state.getIn(['mutes', 'new', 'notifications']), - }; -}; - -const mapDispatchToProps = dispatch => { - return { - onConfirm(account, notifications) { - dispatch(muteAccount(account.get('id'), notifications)); - }, - - onClose() { - dispatch(closeModal()); - }, - - onToggleNotifications() { - dispatch(toggleHideNotifications()); - }, - }; -}; - -@connect(mapStateToProps, mapDispatchToProps) -@injectIntl -export default class MuteModal extends React.PureComponent { - - static propTypes = { - isSubmitting: PropTypes.bool.isRequired, - account: PropTypes.object.isRequired, - notifications: PropTypes.bool.isRequired, - onClose: PropTypes.func.isRequired, - onConfirm: PropTypes.func.isRequired, - onToggleNotifications: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - }; - - componentDidMount() { - this.button.focus(); - } - - handleClick = () => { - this.props.onClose(); - this.props.onConfirm(this.props.account, this.props.notifications); - } - - handleCancel = () => { - this.props.onClose(); - } - - setRef = (c) => { - this.button = c; - } - - toggleNotifications = () => { - this.props.onToggleNotifications(); - } - - render () { - const { account, notifications } = this.props; - - return ( - <div className='modal-root__modal mute-modal'> - <div className='mute-modal__container'> - <p> - <FormattedMessage - id='confirmations.mute.message' - defaultMessage='Are you sure you want to mute {name}?' - values={{ name: <strong>@{account.get('acct')}</strong> }} - /> - </p> - <div> - <label htmlFor='mute-modal__hide-notifications-checkbox'> - <FormattedMessage id='mute_modal.hide_notifications' defaultMessage='Hide notifications from this user?' /> - {' '} - <Toggle id='mute-modal__hide-notifications-checkbox' checked={notifications} onChange={this.toggleNotifications} /> - </label> - </div> - </div> - - <div className='mute-modal__action-bar'> - <Button onClick={this.handleCancel} className='mute-modal__cancel-button'> - <FormattedMessage id='confirmation_modal.cancel' defaultMessage='Cancel' /> - </Button> - <Button onClick={this.handleClick} ref={this.setRef}> - <FormattedMessage id='confirmations.mute.confirm' defaultMessage='Mute' /> - </Button> - </div> - </div> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/ui/components/onboarding_modal.js b/app/javascript/themes/glitch/features/ui/components/onboarding_modal.js deleted file mode 100644 index 58875262e..000000000 --- a/app/javascript/themes/glitch/features/ui/components/onboarding_modal.js +++ /dev/null @@ -1,323 +0,0 @@ -import React from 'react'; -import { connect } from 'react-redux'; -import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; -import ReactSwipeableViews from 'react-swipeable-views'; -import classNames from 'classnames'; -import Permalink from 'themes/glitch/components/permalink'; -import ComposeForm from 'themes/glitch/features/compose/components/compose_form'; -import Search from 'themes/glitch/features/compose/components/search'; -import NavigationBar from 'themes/glitch/features/compose/components/navigation_bar'; -import ColumnHeader from './column_header'; -import { - List as ImmutableList, - Map as ImmutableMap, -} from 'immutable'; -import { me } from 'themes/glitch/util/initial_state'; - -const noop = () => { }; - -const messages = defineMessages({ - home_title: { id: 'column.home', defaultMessage: 'Home' }, - notifications_title: { id: 'column.notifications', defaultMessage: 'Notifications' }, - local_title: { id: 'column.community', defaultMessage: 'Local timeline' }, - federated_title: { id: 'column.public', defaultMessage: 'Federated timeline' }, -}); - -const PageOne = ({ acct, domain }) => ( - <div className='onboarding-modal__page onboarding-modal__page-one'> - <div style={{ flex: '0 0 auto' }}> - <div className='onboarding-modal__page-one__elephant-friend' /> - </div> - - <div> - <h1><FormattedMessage id='onboarding.page_one.welcome' defaultMessage='Welcome to {domain}!' values={{ domain }} /></h1> - <p><FormattedMessage id='onboarding.page_one.federation' defaultMessage='{domain} is an "instance" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.' values={{ domain }} /></p> - <p><FormattedMessage id='onboarding.page_one.handle' defaultMessage='You are on {domain}, so your full handle is {handle}' values={{ domain, handle: <strong>@{acct}@{domain}</strong> }} /></p> - </div> - </div> -); - -PageOne.propTypes = { - acct: PropTypes.string.isRequired, - domain: PropTypes.string.isRequired, -}; - -const PageTwo = ({ myAccount }) => ( - <div className='onboarding-modal__page onboarding-modal__page-two'> - <div className='figure non-interactive'> - <div className='pseudo-drawer'> - <NavigationBar onClose={noop} account={myAccount} /> - </div> - <ComposeForm - text='Awoo! #introductions' - suggestions={ImmutableList()} - mentionedDomains={[]} - spoiler={false} - onChange={noop} - onSubmit={noop} - onPaste={noop} - onPickEmoji={noop} - onChangeSpoilerText={noop} - onClearSuggestions={noop} - onFetchSuggestions={noop} - onSuggestionSelected={noop} - onPrivacyChange={noop} - showSearch - settings={ImmutableMap.of('side_arm', 'none')} - /> - </div> - - <p><FormattedMessage id='onboarding.page_two.compose' defaultMessage='Write posts from the compose column. You can upload images, change privacy settings, and add content warnings with the icons below.' /></p> - </div> -); - -PageTwo.propTypes = { - myAccount: ImmutablePropTypes.map.isRequired, -}; - -const PageThree = ({ myAccount }) => ( - <div className='onboarding-modal__page onboarding-modal__page-three'> - <div className='figure non-interactive'> - <Search - value='' - onChange={noop} - onSubmit={noop} - onClear={noop} - onShow={noop} - /> - - <div className='pseudo-drawer'> - <NavigationBar onClose={noop} account={myAccount} /> - </div> - </div> - - <p><FormattedMessage id='onboarding.page_three.search' defaultMessage='Use the search bar to find people and look at hashtags, such as {illustration} and {introductions}. To look for a person who is not on this instance, use their full handle.' values={{ illustration: <Permalink to='/timelines/tag/illustration' href='/tags/illustration'>#illustration</Permalink>, introductions: <Permalink to='/timelines/tag/introductions' href='/tags/introductions'>#introductions</Permalink> }} /></p> - <p><FormattedMessage id='onboarding.page_three.profile' defaultMessage='Edit your profile to change your avatar, bio, and display name. There, you will also find other preferences.' /></p> - </div> -); - -PageThree.propTypes = { - myAccount: ImmutablePropTypes.map.isRequired, -}; - -const PageFour = ({ domain, intl }) => ( - <div className='onboarding-modal__page onboarding-modal__page-four'> - <div className='onboarding-modal__page-four__columns'> - <div className='row'> - <div> - <div className='figure non-interactive'><ColumnHeader icon='home' type={intl.formatMessage(messages.home_title)} /></div> - <p><FormattedMessage id='onboarding.page_four.home' defaultMessage='The home timeline shows posts from people you follow.' /></p> - </div> - - <div> - <div className='figure non-interactive'><ColumnHeader icon='bell' type={intl.formatMessage(messages.notifications_title)} /></div> - <p><FormattedMessage id='onboarding.page_four.notifications' defaultMessage='The notifications column shows when someone interacts with you.' /></p> - </div> - </div> - - <div className='row'> - <div> - <div className='figure non-interactive' style={{ marginBottom: 0 }}><ColumnHeader icon='users' type={intl.formatMessage(messages.local_title)} /></div> - </div> - - <div> - <div className='figure non-interactive' style={{ marginBottom: 0 }}><ColumnHeader icon='globe' type={intl.formatMessage(messages.federated_title)} /></div> - </div> - </div> - - <p><FormattedMessage id='onboarding.page_five.public_timelines' defaultMessage='The local timeline shows public posts from everyone on {domain}. The federated timeline shows public posts from everyone who people on {domain} follow. These are the Public Timelines, a great way to discover new people.' values={{ domain }} /></p> - </div> - </div> -); - -PageFour.propTypes = { - domain: PropTypes.string.isRequired, - intl: PropTypes.object.isRequired, -}; - -const PageSix = ({ admin, domain }) => { - let adminSection = ''; - - if (admin) { - adminSection = ( - <p> - <FormattedMessage id='onboarding.page_six.admin' defaultMessage="Your instance's admin is {admin}." values={{ admin: <Permalink href={admin.get('url')} to={`/accounts/${admin.get('id')}`}>@{admin.get('acct')}</Permalink> }} /> - <br /> - <FormattedMessage id='onboarding.page_six.read_guidelines' defaultMessage="Please read {domain}'s {guidelines}!" values={{ domain, guidelines: <a href='/about/more' target='_blank'><FormattedMessage id='onboarding.page_six.guidelines' defaultMessage='community guidelines' /></a> }} /> - </p> - ); - } - - return ( - <div className='onboarding-modal__page onboarding-modal__page-six'> - <h1><FormattedMessage id='onboarding.page_six.almost_done' defaultMessage='Almost done...' /></h1> - {adminSection} - <p><FormattedMessage id='onboarding.page_six.github' defaultMessage='{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.' values={{ domain, fork: <a href='https://en.wikipedia.org/wiki/Fork_(software_development)' target='_blank' rel='noopener'>fork</a>, Mastodon: <a href='https://github.com/tootsuite/mastodon' target='_blank' rel='noopener'>Mastodon</a>, github: <a href='https://github.com/glitch-soc/mastodon' target='_blank' rel='noopener'>GitHub</a> }} /></p> - <p><FormattedMessage id='onboarding.page_six.apps_available' defaultMessage='There are {apps} available for iOS, Android and other platforms.' values={{ domain, apps: <a href='https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/Apps.md' target='_blank' rel='noopener'><FormattedMessage id='onboarding.page_six.various_app' defaultMessage='mobile apps' /></a> }} /></p> - <p><em><FormattedMessage id='onboarding.page_six.appetoot' defaultMessage='Bon Appetoot!' /></em></p> - </div> - ); -}; - -PageSix.propTypes = { - admin: ImmutablePropTypes.map, - domain: PropTypes.string.isRequired, -}; - -const mapStateToProps = state => ({ - myAccount: state.getIn(['accounts', me]), - admin: state.getIn(['accounts', state.getIn(['meta', 'admin'])]), - domain: state.getIn(['meta', 'domain']), -}); - -@connect(mapStateToProps) -@injectIntl -export default class OnboardingModal extends React.PureComponent { - - static propTypes = { - onClose: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - myAccount: ImmutablePropTypes.map.isRequired, - domain: PropTypes.string.isRequired, - admin: ImmutablePropTypes.map, - }; - - state = { - currentIndex: 0, - }; - - componentWillMount() { - const { myAccount, admin, domain, intl } = this.props; - this.pages = [ - <PageOne acct={myAccount.get('acct')} domain={domain} />, - <PageTwo myAccount={myAccount} />, - <PageThree myAccount={myAccount} />, - <PageFour domain={domain} intl={intl} />, - <PageSix admin={admin} domain={domain} />, - ]; - }; - - componentDidMount() { - window.addEventListener('keyup', this.handleKeyUp); - } - - componentWillUnmount() { - window.addEventListener('keyup', this.handleKeyUp); - } - - handleSkip = (e) => { - e.preventDefault(); - this.props.onClose(); - } - - handleDot = (e) => { - const i = Number(e.currentTarget.getAttribute('data-index')); - e.preventDefault(); - this.setState({ currentIndex: i }); - } - - handlePrev = () => { - this.setState(({ currentIndex }) => ({ - currentIndex: Math.max(0, currentIndex - 1), - })); - } - - handleNext = () => { - const { pages } = this; - this.setState(({ currentIndex }) => ({ - currentIndex: Math.min(currentIndex + 1, pages.length - 1), - })); - } - - handleSwipe = (index) => { - this.setState({ currentIndex: index }); - } - - handleKeyUp = ({ key }) => { - switch (key) { - case 'ArrowLeft': - this.handlePrev(); - break; - case 'ArrowRight': - this.handleNext(); - break; - } - } - - handleClose = () => { - this.props.onClose(); - } - - render () { - const { pages } = this; - const { currentIndex } = this.state; - const hasMore = currentIndex < pages.length - 1; - - const nextOrDoneBtn = hasMore ? ( - <button - onClick={this.handleNext} - className='onboarding-modal__nav onboarding-modal__next' - > - <FormattedMessage id='onboarding.next' defaultMessage='Next' /> - </button> - ) : ( - <button - onClick={this.handleClose} - className='onboarding-modal__nav onboarding-modal__done' - > - <FormattedMessage id='onboarding.done' defaultMessage='Done' /> - </button> - ); - - return ( - <div className='modal-root__modal onboarding-modal'> - <ReactSwipeableViews index={currentIndex} onChangeIndex={this.handleSwipe} className='onboarding-modal__pager'> - {pages.map((page, i) => { - const className = classNames('onboarding-modal__page__wrapper', { - 'onboarding-modal__page__wrapper--active': i === currentIndex, - }); - return ( - <div key={i} className={className}>{page}</div> - ); - })} - </ReactSwipeableViews> - - <div className='onboarding-modal__paginator'> - <div> - <button - onClick={this.handleSkip} - className='onboarding-modal__nav onboarding-modal__skip' - > - <FormattedMessage id='onboarding.skip' defaultMessage='Skip' /> - </button> - </div> - - <div className='onboarding-modal__dots'> - {pages.map((_, i) => { - const className = classNames('onboarding-modal__dot', { - active: i === currentIndex, - }); - return ( - <div - key={`dot-${i}`} - role='button' - tabIndex='0' - data-index={i} - onClick={this.handleDot} - className={className} - /> - ); - })} - </div> - - <div> - {nextOrDoneBtn} - </div> - </div> - </div> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/ui/components/report_modal.js b/app/javascript/themes/glitch/features/ui/components/report_modal.js deleted file mode 100644 index e6153948e..000000000 --- a/app/javascript/themes/glitch/features/ui/components/report_modal.js +++ /dev/null @@ -1,105 +0,0 @@ -import React from 'react'; -import { connect } from 'react-redux'; -import { changeReportComment, submitReport } from 'themes/glitch/actions/reports'; -import { refreshAccountTimeline } from 'themes/glitch/actions/timelines'; -import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import { makeGetAccount } from 'themes/glitch/selectors'; -import { defineMessages, FormattedMessage, injectIntl } from 'react-intl'; -import StatusCheckBox from 'themes/glitch/features/report/containers/status_check_box_container'; -import { OrderedSet } from 'immutable'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import Button from 'themes/glitch/components/button'; - -const messages = defineMessages({ - placeholder: { id: 'report.placeholder', defaultMessage: 'Additional comments' }, - submit: { id: 'report.submit', defaultMessage: 'Submit' }, -}); - -const makeMapStateToProps = () => { - const getAccount = makeGetAccount(); - - const mapStateToProps = state => { - const accountId = state.getIn(['reports', 'new', 'account_id']); - - return { - isSubmitting: state.getIn(['reports', 'new', 'isSubmitting']), - account: getAccount(state, accountId), - comment: state.getIn(['reports', 'new', 'comment']), - statusIds: OrderedSet(state.getIn(['timelines', `account:${accountId}`, 'items'])).union(state.getIn(['reports', 'new', 'status_ids'])), - }; - }; - - return mapStateToProps; -}; - -@connect(makeMapStateToProps) -@injectIntl -export default class ReportModal extends ImmutablePureComponent { - - static propTypes = { - isSubmitting: PropTypes.bool, - account: ImmutablePropTypes.map, - statusIds: ImmutablePropTypes.orderedSet.isRequired, - comment: PropTypes.string.isRequired, - dispatch: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - }; - - handleCommentChange = (e) => { - this.props.dispatch(changeReportComment(e.target.value)); - } - - handleSubmit = () => { - this.props.dispatch(submitReport()); - } - - componentDidMount () { - this.props.dispatch(refreshAccountTimeline(this.props.account.get('id'))); - } - - componentWillReceiveProps (nextProps) { - if (this.props.account !== nextProps.account && nextProps.account) { - this.props.dispatch(refreshAccountTimeline(nextProps.account.get('id'))); - } - } - - render () { - const { account, comment, intl, statusIds, isSubmitting } = this.props; - - if (!account) { - return null; - } - - return ( - <div className='modal-root__modal report-modal'> - <div className='report-modal__target'> - <FormattedMessage id='report.target' defaultMessage='Report {target}' values={{ target: <strong>{account.get('acct')}</strong> }} /> - </div> - - <div className='report-modal__container'> - <div className='report-modal__statuses'> - <div> - {statusIds.map(statusId => <StatusCheckBox id={statusId} key={statusId} disabled={isSubmitting} />)} - </div> - </div> - - <div className='report-modal__comment'> - <textarea - className='setting-text light' - placeholder={intl.formatMessage(messages.placeholder)} - value={comment} - onChange={this.handleCommentChange} - disabled={isSubmitting} - /> - </div> - </div> - - <div className='report-modal__action-bar'> - <Button disabled={isSubmitting} text={intl.formatMessage(messages.submit)} onClick={this.handleSubmit} /> - </div> - </div> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/ui/components/tabs_bar.js b/app/javascript/themes/glitch/features/ui/components/tabs_bar.js deleted file mode 100644 index ef5deae99..000000000 --- a/app/javascript/themes/glitch/features/ui/components/tabs_bar.js +++ /dev/null @@ -1,84 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { NavLink } from 'react-router-dom'; -import { FormattedMessage, injectIntl } from 'react-intl'; -import { debounce } from 'lodash'; -import { isUserTouching } from 'themes/glitch/util/is_mobile'; - -export const links = [ - <NavLink className='tabs-bar__link primary' to='/statuses/new' data-preview-title-id='tabs_bar.compose' data-preview-icon='pencil' ><i className='fa fa-fw fa-pencil' /><FormattedMessage id='tabs_bar.compose' defaultMessage='Compose' /></NavLink>, - <NavLink className='tabs-bar__link primary' to='/timelines/home' data-preview-title-id='column.home' data-preview-icon='home' ><i className='fa fa-fw fa-home' /><FormattedMessage id='tabs_bar.home' defaultMessage='Home' /></NavLink>, - <NavLink className='tabs-bar__link primary' to='/notifications' data-preview-title-id='column.notifications' data-preview-icon='bell' ><i className='fa fa-fw fa-bell' /><FormattedMessage id='tabs_bar.notifications' defaultMessage='Notifications' /></NavLink>, - - <NavLink className='tabs-bar__link secondary' to='/timelines/public/local' data-preview-title-id='column.community' data-preview-icon='users' ><i className='fa fa-fw fa-users' /><FormattedMessage id='tabs_bar.local_timeline' defaultMessage='Local' /></NavLink>, - <NavLink className='tabs-bar__link secondary' exact to='/timelines/public' data-preview-title-id='column.public' data-preview-icon='globe' ><i className='fa fa-fw fa-globe' /><FormattedMessage id='tabs_bar.federated_timeline' defaultMessage='Federated' /></NavLink>, - - <NavLink className='tabs-bar__link primary' style={{ flexGrow: '0', flexBasis: '30px' }} to='/getting-started' data-preview-title-id='getting_started.heading' data-preview-icon='asterisk' ><i className='fa fa-fw fa-asterisk' /></NavLink>, -]; - -export function getIndex (path) { - return links.findIndex(link => link.props.to === path); -} - -export function getLink (index) { - return links[index].props.to; -} - -@injectIntl -export default class TabsBar extends React.Component { - - static contextTypes = { - router: PropTypes.object.isRequired, - } - - static propTypes = { - intl: PropTypes.object.isRequired, - } - - setRef = ref => { - this.node = ref; - } - - handleClick = (e) => { - // Only apply optimization for touch devices, which we assume are slower - // We thus avoid the 250ms delay for non-touch devices and the lag for touch devices - if (isUserTouching()) { - e.preventDefault(); - e.persist(); - - requestAnimationFrame(() => { - const tabs = Array(...this.node.querySelectorAll('.tabs-bar__link')); - const currentTab = tabs.find(tab => tab.classList.contains('active')); - const nextTab = tabs.find(tab => tab.contains(e.target)); - const { props: { to } } = links[Array(...this.node.childNodes).indexOf(nextTab)]; - - - if (currentTab !== nextTab) { - if (currentTab) { - currentTab.classList.remove('active'); - } - - const listener = debounce(() => { - nextTab.removeEventListener('transitionend', listener); - this.context.router.history.push(to); - }, 50); - - nextTab.addEventListener('transitionend', listener); - nextTab.classList.add('active'); - } - }); - } - - } - - render () { - const { intl: { formatMessage } } = this.props; - - return ( - <nav className='tabs-bar' ref={this.setRef}> - {links.map(link => React.cloneElement(link, { key: link.props.to, onClick: this.handleClick, 'aria-label': formatMessage({ id: link.props['data-preview-title-id'] }) }))} - </nav> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/ui/components/upload_area.js b/app/javascript/themes/glitch/features/ui/components/upload_area.js deleted file mode 100644 index 72a450215..000000000 --- a/app/javascript/themes/glitch/features/ui/components/upload_area.js +++ /dev/null @@ -1,52 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import Motion from 'themes/glitch/util/optional_motion'; -import spring from 'react-motion/lib/spring'; -import { FormattedMessage } from 'react-intl'; - -export default class UploadArea extends React.PureComponent { - - static propTypes = { - active: PropTypes.bool, - onClose: PropTypes.func, - }; - - handleKeyUp = (e) => { - const keyCode = e.keyCode; - if (this.props.active) { - switch(keyCode) { - case 27: - e.preventDefault(); - e.stopPropagation(); - this.props.onClose(); - break; - } - } - } - - componentDidMount () { - window.addEventListener('keyup', this.handleKeyUp, false); - } - - componentWillUnmount () { - window.removeEventListener('keyup', this.handleKeyUp); - } - - render () { - const { active } = this.props; - - return ( - <Motion defaultStyle={{ backgroundOpacity: 0, backgroundScale: 0.95 }} style={{ backgroundOpacity: spring(active ? 1 : 0, { stiffness: 150, damping: 15 }), backgroundScale: spring(active ? 1 : 0.95, { stiffness: 200, damping: 3 }) }}> - {({ backgroundOpacity, backgroundScale }) => - <div className='upload-area' style={{ visibility: active ? 'visible' : 'hidden', opacity: backgroundOpacity }}> - <div className='upload-area__drop'> - <div className='upload-area__background' style={{ transform: `scale(${backgroundScale})` }} /> - <div className='upload-area__content'><FormattedMessage id='upload_area.title' defaultMessage='Drag & drop to upload' /></div> - </div> - </div> - } - </Motion> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/ui/components/video_modal.js b/app/javascript/themes/glitch/features/ui/components/video_modal.js deleted file mode 100644 index 91168c790..000000000 --- a/app/javascript/themes/glitch/features/ui/components/video_modal.js +++ /dev/null @@ -1,33 +0,0 @@ -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import PropTypes from 'prop-types'; -import Video from 'themes/glitch/features/video'; -import ImmutablePureComponent from 'react-immutable-pure-component'; - -export default class VideoModal extends ImmutablePureComponent { - - static propTypes = { - media: ImmutablePropTypes.map.isRequired, - time: PropTypes.number, - onClose: PropTypes.func.isRequired, - }; - - render () { - const { media, time, onClose } = this.props; - - return ( - <div className='modal-root__modal media-modal'> - <div> - <Video - preview={media.get('preview_url')} - src={media.get('url')} - startTime={time} - onCloseVideo={onClose} - description={media.get('description')} - /> - </div> - </div> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/ui/containers/bundle_container.js b/app/javascript/themes/glitch/features/ui/containers/bundle_container.js deleted file mode 100644 index e6f9afcf7..000000000 --- a/app/javascript/themes/glitch/features/ui/containers/bundle_container.js +++ /dev/null @@ -1,19 +0,0 @@ -import { connect } from 'react-redux'; - -import Bundle from '../components/bundle'; - -import { fetchBundleRequest, fetchBundleSuccess, fetchBundleFail } from 'themes/glitch/actions/bundles'; - -const mapDispatchToProps = dispatch => ({ - onFetch () { - dispatch(fetchBundleRequest()); - }, - onFetchSuccess () { - dispatch(fetchBundleSuccess()); - }, - onFetchFail (error) { - dispatch(fetchBundleFail(error)); - }, -}); - -export default connect(null, mapDispatchToProps)(Bundle); diff --git a/app/javascript/themes/glitch/features/ui/containers/columns_area_container.js b/app/javascript/themes/glitch/features/ui/containers/columns_area_container.js deleted file mode 100644 index 95f95618b..000000000 --- a/app/javascript/themes/glitch/features/ui/containers/columns_area_container.js +++ /dev/null @@ -1,8 +0,0 @@ -import { connect } from 'react-redux'; -import ColumnsArea from '../components/columns_area'; - -const mapStateToProps = state => ({ - columns: state.getIn(['settings', 'columns']), -}); - -export default connect(mapStateToProps, null, null, { withRef: true })(ColumnsArea); diff --git a/app/javascript/themes/glitch/features/ui/containers/loading_bar_container.js b/app/javascript/themes/glitch/features/ui/containers/loading_bar_container.js deleted file mode 100644 index 4bb90fb68..000000000 --- a/app/javascript/themes/glitch/features/ui/containers/loading_bar_container.js +++ /dev/null @@ -1,8 +0,0 @@ -import { connect } from 'react-redux'; -import LoadingBar from 'react-redux-loading-bar'; - -const mapStateToProps = (state) => ({ - loading: state.get('loadingBar'), -}); - -export default connect(mapStateToProps)(LoadingBar.WrappedComponent); diff --git a/app/javascript/themes/glitch/features/ui/containers/modal_container.js b/app/javascript/themes/glitch/features/ui/containers/modal_container.js deleted file mode 100644 index c26f19886..000000000 --- a/app/javascript/themes/glitch/features/ui/containers/modal_container.js +++ /dev/null @@ -1,16 +0,0 @@ -import { connect } from 'react-redux'; -import { closeModal } from 'themes/glitch/actions/modal'; -import ModalRoot from '../components/modal_root'; - -const mapStateToProps = state => ({ - type: state.get('modal').modalType, - props: state.get('modal').modalProps, -}); - -const mapDispatchToProps = dispatch => ({ - onClose () { - dispatch(closeModal()); - }, -}); - -export default connect(mapStateToProps, mapDispatchToProps)(ModalRoot); diff --git a/app/javascript/themes/glitch/features/ui/containers/notifications_container.js b/app/javascript/themes/glitch/features/ui/containers/notifications_container.js deleted file mode 100644 index 5bd4017f5..000000000 --- a/app/javascript/themes/glitch/features/ui/containers/notifications_container.js +++ /dev/null @@ -1,18 +0,0 @@ -import { connect } from 'react-redux'; -import { NotificationStack } from 'react-notification'; -import { dismissAlert } from 'themes/glitch/actions/alerts'; -import { getAlerts } from 'themes/glitch/selectors'; - -const mapStateToProps = state => ({ - notifications: getAlerts(state), -}); - -const mapDispatchToProps = (dispatch) => { - return { - onDismiss: alert => { - dispatch(dismissAlert(alert)); - }, - }; -}; - -export default connect(mapStateToProps, mapDispatchToProps)(NotificationStack); diff --git a/app/javascript/themes/glitch/features/ui/containers/status_list_container.js b/app/javascript/themes/glitch/features/ui/containers/status_list_container.js deleted file mode 100644 index 807c82e16..000000000 --- a/app/javascript/themes/glitch/features/ui/containers/status_list_container.js +++ /dev/null @@ -1,73 +0,0 @@ -import { connect } from 'react-redux'; -import StatusList from 'themes/glitch/components/status_list'; -import { scrollTopTimeline } from 'themes/glitch/actions/timelines'; -import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; -import { createSelector } from 'reselect'; -import { debounce } from 'lodash'; -import { me } from 'themes/glitch/util/initial_state'; - -const makeGetStatusIds = () => createSelector([ - (state, { type }) => state.getIn(['settings', type], ImmutableMap()), - (state, { type }) => state.getIn(['timelines', type, 'items'], ImmutableList()), - (state) => state.get('statuses'), -], (columnSettings, statusIds, statuses) => { - const rawRegex = columnSettings.getIn(['regex', 'body'], '').trim(); - let regex = null; - - try { - regex = rawRegex && new RegExp(rawRegex, 'i'); - } catch (e) { - // Bad regex, don't affect filters - } - - return statusIds.filter(id => { - const statusForId = statuses.get(id); - let showStatus = true; - - if (columnSettings.getIn(['shows', 'reblog']) === false) { - showStatus = showStatus && statusForId.get('reblog') === null; - } - - if (columnSettings.getIn(['shows', 'reply']) === false) { - showStatus = showStatus && (statusForId.get('in_reply_to_id') === null || statusForId.get('in_reply_to_account_id') === me); - } - - if (showStatus && regex && statusForId.get('account') !== me) { - const searchIndex = statusForId.get('reblog') ? statuses.getIn([statusForId.get('reblog'), 'search_index']) : statusForId.get('search_index'); - showStatus = !regex.test(searchIndex); - } - - return showStatus; - }); -}); - -const makeMapStateToProps = () => { - const getStatusIds = makeGetStatusIds(); - - const mapStateToProps = (state, { timelineId }) => ({ - statusIds: getStatusIds(state, { type: timelineId }), - isLoading: state.getIn(['timelines', timelineId, 'isLoading'], true), - hasMore: !!state.getIn(['timelines', timelineId, 'next']), - }); - - return mapStateToProps; -}; - -const mapDispatchToProps = (dispatch, { timelineId, loadMore }) => ({ - - onScrollToBottom: debounce(() => { - dispatch(scrollTopTimeline(timelineId, false)); - loadMore(); - }, 300, { leading: true }), - - onScrollToTop: debounce(() => { - dispatch(scrollTopTimeline(timelineId, true)); - }, 100), - - onScroll: debounce(() => { - dispatch(scrollTopTimeline(timelineId, false)); - }, 100), - -}); - -export default connect(makeMapStateToProps, mapDispatchToProps)(StatusList); diff --git a/app/javascript/themes/glitch/features/ui/index.js b/app/javascript/themes/glitch/features/ui/index.js deleted file mode 100644 index b59a2e637..000000000 --- a/app/javascript/themes/glitch/features/ui/index.js +++ /dev/null @@ -1,442 +0,0 @@ -import React from 'react'; -import NotificationsContainer from './containers/notifications_container'; -import PropTypes from 'prop-types'; -import LoadingBarContainer from './containers/loading_bar_container'; -import TabsBar from './components/tabs_bar'; -import ModalContainer from './containers/modal_container'; -import { connect } from 'react-redux'; -import { Redirect, withRouter } from 'react-router-dom'; -import { isMobile } from 'themes/glitch/util/is_mobile'; -import { debounce } from 'lodash'; -import { uploadCompose, resetCompose } from 'themes/glitch/actions/compose'; -import { refreshHomeTimeline } from 'themes/glitch/actions/timelines'; -import { refreshNotifications } from 'themes/glitch/actions/notifications'; -import { clearHeight } from 'themes/glitch/actions/height_cache'; -import { WrappedSwitch, WrappedRoute } from 'themes/glitch/util/react_router_helpers'; -import UploadArea from './components/upload_area'; -import ColumnsAreaContainer from './containers/columns_area_container'; -import classNames from 'classnames'; -import { - Compose, - Status, - GettingStarted, - PublicTimeline, - CommunityTimeline, - AccountTimeline, - AccountGallery, - HomeTimeline, - Followers, - Following, - Reblogs, - Favourites, - DirectTimeline, - HashtagTimeline, - Notifications, - FollowRequests, - GenericNotFound, - FavouritedStatuses, - Blocks, - Mutes, - PinnedStatuses, -} from 'themes/glitch/util/async-components'; -import { HotKeys } from 'react-hotkeys'; -import { me } from 'themes/glitch/util/initial_state'; -import { defineMessages, injectIntl } from 'react-intl'; - -// Dummy import, to make sure that <Status /> ends up in the application bundle. -// Without this it ends up in ~8 very commonly used bundles. -import '../../../glitch/components/status'; - -const messages = defineMessages({ - beforeUnload: { id: 'ui.beforeunload', defaultMessage: 'Your draft will be lost if you leave Mastodon.' }, -}); - -const mapStateToProps = state => ({ - isComposing: state.getIn(['compose', 'is_composing']), - hasComposingText: state.getIn(['compose', 'text']) !== '', - layout: state.getIn(['local_settings', 'layout']), - isWide: state.getIn(['local_settings', 'stretch']), - navbarUnder: state.getIn(['local_settings', 'navbar_under']), -}); - -const keyMap = { - new: 'n', - search: 's', - forceNew: 'option+n', - focusColumn: ['1', '2', '3', '4', '5', '6', '7', '8', '9'], - reply: 'r', - favourite: 'f', - boost: 'b', - mention: 'm', - open: ['enter', 'o'], - openProfile: 'p', - moveDown: ['down', 'j'], - moveUp: ['up', 'k'], - back: 'backspace', - goToHome: 'g h', - goToNotifications: 'g n', - goToLocal: 'g l', - goToFederated: 'g t', - goToDirect: 'g d', - goToStart: 'g s', - goToFavourites: 'g f', - goToPinned: 'g p', - goToProfile: 'g u', - goToBlocked: 'g b', - goToMuted: 'g m', -}; - -@connect(mapStateToProps) -@injectIntl -@withRouter -export default class UI extends React.Component { - - static contextTypes = { - router: PropTypes.object.isRequired, - }; - - static propTypes = { - dispatch: PropTypes.func.isRequired, - children: PropTypes.node, - layout: PropTypes.string, - isWide: PropTypes.bool, - systemFontUi: PropTypes.bool, - navbarUnder: PropTypes.bool, - isComposing: PropTypes.bool, - hasComposingText: PropTypes.bool, - location: PropTypes.object, - intl: PropTypes.object.isRequired, - }; - - state = { - width: window.innerWidth, - draggingOver: false, - }; - - handleBeforeUnload = (e) => { - const { intl, isComposing, hasComposingText } = this.props; - - if (isComposing && hasComposingText) { - // Setting returnValue to any string causes confirmation dialog. - // Many browsers no longer display this text to users, - // but we set user-friendly message for other browsers, e.g. Edge. - e.returnValue = intl.formatMessage(messages.beforeUnload); - } - } - - handleResize = debounce(() => { - // The cached heights are no longer accurate, invalidate - this.props.dispatch(clearHeight()); - - this.setState({ width: window.innerWidth }); - }, 500, { - trailing: true, - }); - - handleDragEnter = (e) => { - e.preventDefault(); - - if (!this.dragTargets) { - this.dragTargets = []; - } - - if (this.dragTargets.indexOf(e.target) === -1) { - this.dragTargets.push(e.target); - } - - if (e.dataTransfer && e.dataTransfer.types.includes('Files')) { - this.setState({ draggingOver: true }); - } - } - - handleDragOver = (e) => { - e.preventDefault(); - e.stopPropagation(); - - try { - e.dataTransfer.dropEffect = 'copy'; - } catch (err) { - - } - - return false; - } - - handleDrop = (e) => { - e.preventDefault(); - - this.setState({ draggingOver: false }); - - if (e.dataTransfer && e.dataTransfer.files.length === 1) { - this.props.dispatch(uploadCompose(e.dataTransfer.files)); - } - } - - handleDragLeave = (e) => { - e.preventDefault(); - e.stopPropagation(); - - this.dragTargets = this.dragTargets.filter(el => el !== e.target && this.node.contains(el)); - - if (this.dragTargets.length > 0) { - return; - } - - this.setState({ draggingOver: false }); - } - - closeUploadModal = () => { - this.setState({ draggingOver: false }); - } - - handleServiceWorkerPostMessage = ({ data }) => { - if (data.type === 'navigate') { - this.context.router.history.push(data.path); - } else { - console.warn('Unknown message type:', data.type); - } - } - - componentWillMount () { - window.addEventListener('beforeunload', this.handleBeforeUnload, false); - window.addEventListener('resize', this.handleResize, { passive: true }); - document.addEventListener('dragenter', this.handleDragEnter, false); - document.addEventListener('dragover', this.handleDragOver, false); - document.addEventListener('drop', this.handleDrop, false); - document.addEventListener('dragleave', this.handleDragLeave, false); - document.addEventListener('dragend', this.handleDragEnd, false); - - if ('serviceWorker' in navigator) { - navigator.serviceWorker.addEventListener('message', this.handleServiceWorkerPostMessage); - } - - this.props.dispatch(refreshHomeTimeline()); - this.props.dispatch(refreshNotifications()); - } - - componentDidMount () { - this.hotkeys.__mousetrap__.stopCallback = (e, element) => { - return ['TEXTAREA', 'SELECT', 'INPUT'].includes(element.tagName); - }; - } - - shouldComponentUpdate (nextProps) { - if (nextProps.isComposing !== this.props.isComposing) { - // Avoid expensive update just to toggle a class - this.node.classList.toggle('is-composing', nextProps.isComposing); - this.node.classList.toggle('navbar-under', nextProps.navbarUnder); - - return false; - } - - // Why isn't this working?!? - // return super.shouldComponentUpdate(nextProps, nextState); - return true; - } - - componentDidUpdate (prevProps) { - if (![this.props.location.pathname, '/'].includes(prevProps.location.pathname)) { - this.columnsAreaNode.handleChildrenContentChange(); - } - } - - componentWillUnmount () { - window.removeEventListener('beforeunload', this.handleBeforeUnload); - window.removeEventListener('resize', this.handleResize); - document.removeEventListener('dragenter', this.handleDragEnter); - document.removeEventListener('dragover', this.handleDragOver); - document.removeEventListener('drop', this.handleDrop); - document.removeEventListener('dragleave', this.handleDragLeave); - document.removeEventListener('dragend', this.handleDragEnd); - } - - setRef = c => { - this.node = c; - } - - setColumnsAreaRef = c => { - this.columnsAreaNode = c.getWrappedInstance().getWrappedInstance(); - } - - handleHotkeyNew = e => { - e.preventDefault(); - - const element = this.node.querySelector('.compose-form__autosuggest-wrapper textarea'); - - if (element) { - element.focus(); - } - } - - handleHotkeySearch = e => { - e.preventDefault(); - - const element = this.node.querySelector('.search__input'); - - if (element) { - element.focus(); - } - } - - handleHotkeyForceNew = e => { - this.handleHotkeyNew(e); - this.props.dispatch(resetCompose()); - } - - handleHotkeyFocusColumn = e => { - const index = (e.key * 1) + 1; // First child is drawer, skip that - const column = this.node.querySelector(`.column:nth-child(${index})`); - - if (column) { - const status = column.querySelector('.focusable'); - - if (status) { - status.focus(); - } - } - } - - handleHotkeyBack = () => { - if (window.history && window.history.length === 1) { - this.context.router.history.push('/'); - } else { - this.context.router.history.goBack(); - } - } - - setHotkeysRef = c => { - this.hotkeys = c; - } - - handleHotkeyGoToHome = () => { - this.context.router.history.push('/timelines/home'); - } - - handleHotkeyGoToNotifications = () => { - this.context.router.history.push('/notifications'); - } - - handleHotkeyGoToLocal = () => { - this.context.router.history.push('/timelines/public/local'); - } - - handleHotkeyGoToFederated = () => { - this.context.router.history.push('/timelines/public'); - } - - handleHotkeyGoToDirect = () => { - this.context.router.history.push('/timelines/direct'); - } - - handleHotkeyGoToStart = () => { - this.context.router.history.push('/getting-started'); - } - - handleHotkeyGoToFavourites = () => { - this.context.router.history.push('/favourites'); - } - - handleHotkeyGoToPinned = () => { - this.context.router.history.push('/pinned'); - } - - handleHotkeyGoToProfile = () => { - this.context.router.history.push(`/accounts/${me}`); - } - - handleHotkeyGoToBlocked = () => { - this.context.router.history.push('/blocks'); - } - - handleHotkeyGoToMuted = () => { - this.context.router.history.push('/mutes'); - } - - render () { - const { width, draggingOver } = this.state; - const { children, layout, isWide, navbarUnder } = this.props; - - const columnsClass = layout => { - switch (layout) { - case 'single': - return 'single-column'; - case 'multiple': - return 'multi-columns'; - default: - return 'auto-columns'; - } - }; - - const className = classNames('ui', columnsClass(layout), { - 'wide': isWide, - 'system-font': this.props.systemFontUi, - 'navbar-under': navbarUnder, - }); - - const handlers = { - new: this.handleHotkeyNew, - search: this.handleHotkeySearch, - forceNew: this.handleHotkeyForceNew, - focusColumn: this.handleHotkeyFocusColumn, - back: this.handleHotkeyBack, - goToHome: this.handleHotkeyGoToHome, - goToNotifications: this.handleHotkeyGoToNotifications, - goToLocal: this.handleHotkeyGoToLocal, - goToFederated: this.handleHotkeyGoToFederated, - goToDirect: this.handleHotkeyGoToDirect, - goToStart: this.handleHotkeyGoToStart, - goToFavourites: this.handleHotkeyGoToFavourites, - goToPinned: this.handleHotkeyGoToPinned, - goToProfile: this.handleHotkeyGoToProfile, - goToBlocked: this.handleHotkeyGoToBlocked, - goToMuted: this.handleHotkeyGoToMuted, - }; - - return ( - <HotKeys keyMap={keyMap} handlers={handlers} ref={this.setHotkeysRef}> - <div className={className} ref={this.setRef}> - {navbarUnder ? null : (<TabsBar />)} - - <ColumnsAreaContainer ref={this.setColumnsAreaRef} singleColumn={isMobile(width, layout)}> - <WrappedSwitch> - <Redirect from='/' to='/getting-started' exact /> - <WrappedRoute path='/getting-started' component={GettingStarted} content={children} /> - <WrappedRoute path='/timelines/home' component={HomeTimeline} content={children} /> - <WrappedRoute path='/timelines/public' exact component={PublicTimeline} content={children} /> - <WrappedRoute path='/timelines/public/local' component={CommunityTimeline} content={children} /> - <WrappedRoute path='/timelines/direct' component={DirectTimeline} content={children} /> - <WrappedRoute path='/timelines/tag/:id' component={HashtagTimeline} content={children} /> - - <WrappedRoute path='/notifications' component={Notifications} content={children} /> - <WrappedRoute path='/favourites' component={FavouritedStatuses} content={children} /> - <WrappedRoute path='/pinned' component={PinnedStatuses} content={children} /> - - <WrappedRoute path='/statuses/new' component={Compose} content={children} /> - <WrappedRoute path='/statuses/:statusId' exact component={Status} content={children} /> - <WrappedRoute path='/statuses/:statusId/reblogs' component={Reblogs} content={children} /> - <WrappedRoute path='/statuses/:statusId/favourites' component={Favourites} content={children} /> - - <WrappedRoute path='/accounts/:accountId' exact component={AccountTimeline} content={children} /> - <WrappedRoute path='/accounts/:accountId/followers' component={Followers} content={children} /> - <WrappedRoute path='/accounts/:accountId/following' component={Following} content={children} /> - <WrappedRoute path='/accounts/:accountId/media' component={AccountGallery} content={children} /> - - <WrappedRoute path='/follow_requests' component={FollowRequests} content={children} /> - <WrappedRoute path='/blocks' component={Blocks} content={children} /> - <WrappedRoute path='/mutes' component={Mutes} content={children} /> - - <WrappedRoute component={GenericNotFound} content={children} /> - </WrappedSwitch> - </ColumnsAreaContainer> - - <NotificationsContainer /> - {navbarUnder ? (<TabsBar />) : null} - <LoadingBarContainer className='loading-bar' /> - <ModalContainer /> - <UploadArea active={draggingOver} onClose={this.closeUploadModal} /> - </div> - </HotKeys> - ); - } - -} |