diff options
author | beatrix <beatrix.bitrot@gmail.com> | 2017-12-06 17:44:07 -0500 |
---|---|---|
committer | GitHub <noreply@github.com> | 2017-12-06 17:44:07 -0500 |
commit | 81b01457598459c42a7b14d9aa14f91ba60dcae1 (patch) | |
tree | 7d3e6dadb75f3be95e5a5ed8b7ecfe90e7711831 /app/javascript/themes/glitch/features | |
parent | f1cbea77a4a52929244198dcbde26d63d837489a (diff) | |
parent | 017fc81caf8f265e5c5543186877437485625795 (diff) |
Merge pull request #229 from glitch-soc/glitch-theme
Advanced Next-Level Flavours And Skins For Mastodon™
Diffstat (limited to 'app/javascript/themes/glitch/features')
130 files changed, 0 insertions, 10384 deletions
diff --git a/app/javascript/themes/glitch/features/account/components/action_bar.js b/app/javascript/themes/glitch/features/account/components/action_bar.js deleted file mode 100644 index 0edd5c848..000000000 --- a/app/javascript/themes/glitch/features/account/components/action_bar.js +++ /dev/null @@ -1,145 +0,0 @@ -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import PropTypes from 'prop-types'; -import DropdownMenuContainer from 'themes/glitch/containers/dropdown_menu_container'; -import { Link } from 'react-router-dom'; -import { defineMessages, injectIntl, FormattedMessage, FormattedNumber } from 'react-intl'; -import { me } from 'themes/glitch/util/initial_state'; - -const messages = defineMessages({ - mention: { id: 'account.mention', defaultMessage: 'Mention @{name}' }, - edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' }, - unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' }, - unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' }, - unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' }, - block: { id: 'account.block', defaultMessage: 'Block @{name}' }, - mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' }, - follow: { id: 'account.follow', defaultMessage: 'Follow' }, - report: { id: 'account.report', defaultMessage: 'Report @{name}' }, - share: { id: 'account.share', defaultMessage: 'Share @{name}\'s profile' }, - media: { id: 'account.media', defaultMessage: 'Media' }, - blockDomain: { id: 'account.block_domain', defaultMessage: 'Hide everything from {domain}' }, - unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unhide {domain}' }, - hideReblogs: { id: 'account.hide_reblogs', defaultMessage: 'Hide boosts from @{name}' }, - showReblogs: { id: 'account.show_reblogs', defaultMessage: 'Show boosts from @{name}' }, -}); - -@injectIntl -export default class ActionBar extends React.PureComponent { - - static propTypes = { - account: ImmutablePropTypes.map.isRequired, - onFollow: PropTypes.func, - onBlock: PropTypes.func.isRequired, - onMention: PropTypes.func.isRequired, - onReblogToggle: PropTypes.func.isRequired, - onReport: PropTypes.func.isRequired, - onMute: PropTypes.func.isRequired, - onBlockDomain: PropTypes.func.isRequired, - onUnblockDomain: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - }; - - handleShare = () => { - navigator.share({ - url: this.props.account.get('url'), - }); - } - - render () { - const { account, intl } = this.props; - - let menu = []; - let extraInfo = ''; - - menu.push({ text: intl.formatMessage(messages.mention, { name: account.get('username') }), action: this.props.onMention }); - if ('share' in navigator) { - menu.push({ text: intl.formatMessage(messages.share, { name: account.get('username') }), action: this.handleShare }); - } - menu.push(null); - menu.push({ text: intl.formatMessage(messages.media), to: `/accounts/${account.get('id')}/media` }); - menu.push(null); - - if (account.get('id') === me) { - menu.push({ text: intl.formatMessage(messages.edit_profile), href: '/settings/profile' }); - } else { - const following = account.getIn(['relationship', 'following']); - if (following) { - if (following.get('reblogs')) { - menu.push({ text: intl.formatMessage(messages.hideReblogs, { name: account.get('username') }), action: this.props.onReblogToggle }); - } else { - menu.push({ text: intl.formatMessage(messages.showReblogs, { name: account.get('username') }), action: this.props.onReblogToggle }); - } - } - - if (account.getIn(['relationship', 'muting'])) { - menu.push({ text: intl.formatMessage(messages.unmute, { name: account.get('username') }), action: this.props.onMute }); - } else { - menu.push({ text: intl.formatMessage(messages.mute, { name: account.get('username') }), action: this.props.onMute }); - } - - if (account.getIn(['relationship', 'blocking'])) { - menu.push({ text: intl.formatMessage(messages.unblock, { name: account.get('username') }), action: this.props.onBlock }); - } else { - menu.push({ text: intl.formatMessage(messages.block, { name: account.get('username') }), action: this.props.onBlock }); - } - - menu.push({ text: intl.formatMessage(messages.report, { name: account.get('username') }), action: this.props.onReport }); - } - - if (account.get('acct') !== account.get('username')) { - const domain = account.get('acct').split('@')[1]; - - extraInfo = ( - <div className='account__disclaimer'> - <FormattedMessage - id='account.disclaimer_full' - defaultMessage="Information below may reflect the user's profile incompletely." - /> - {' '} - <a target='_blank' rel='noopener' href={account.get('url')}> - <FormattedMessage id='account.view_full_profile' defaultMessage='View full profile' /> - </a> - </div> - ); - - menu.push(null); - - if (account.getIn(['relationship', 'domain_blocking'])) { - menu.push({ text: intl.formatMessage(messages.unblockDomain, { domain }), action: this.props.onUnblockDomain }); - } else { - menu.push({ text: intl.formatMessage(messages.blockDomain, { domain }), action: this.props.onBlockDomain }); - } - } - - return ( - <div> - {extraInfo} - - <div className='account__action-bar'> - <div className='account__action-bar-dropdown'> - <DropdownMenuContainer items={menu} icon='bars' size={24} direction='right' /> - </div> - - <div className='account__action-bar-links'> - <Link className='account__action-bar__tab' to={`/accounts/${account.get('id')}`}> - <span><FormattedMessage id='account.posts' defaultMessage='Posts' /></span> - <strong><FormattedNumber value={account.get('statuses_count')} /></strong> - </Link> - - <Link className='account__action-bar__tab' to={`/accounts/${account.get('id')}/following`}> - <span><FormattedMessage id='account.follows' defaultMessage='Follows' /></span> - <strong><FormattedNumber value={account.get('following_count')} /></strong> - </Link> - - <Link className='account__action-bar__tab' to={`/accounts/${account.get('id')}/followers`}> - <span><FormattedMessage id='account.followers' defaultMessage='Followers' /></span> - <strong><FormattedNumber value={account.get('followers_count')} /></strong> - </Link> - </div> - </div> - </div> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/account/components/header.js b/app/javascript/themes/glitch/features/account/components/header.js deleted file mode 100644 index 696bb1991..000000000 --- a/app/javascript/themes/glitch/features/account/components/header.js +++ /dev/null @@ -1,99 +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 ImmutablePureComponent from 'react-immutable-pure-component'; - -import Avatar from 'themes/glitch/components/avatar'; -import IconButton from 'themes/glitch/components/icon_button'; - -import emojify from 'themes/glitch/util/emoji'; -import { me } from 'themes/glitch/util/initial_state'; -import { processBio } from 'themes/glitch/util/bio_metadata'; - -const messages = defineMessages({ - unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' }, - follow: { id: 'account.follow', defaultMessage: 'Follow' }, - requested: { id: 'account.requested', defaultMessage: 'Awaiting approval. Click to cancel follow request' }, -}); - -@injectIntl -export default class Header extends ImmutablePureComponent { - - static propTypes = { - account: ImmutablePropTypes.map, - onFollow: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - }; - - render () { - const { account, intl } = this.props; - - if (!account) { - return null; - } - - let displayName = account.get('display_name_html'); - let info = ''; - let actionBtn = ''; - - if (me !== account.get('id') && account.getIn(['relationship', 'followed_by'])) { - info = <span className='account--follows-info'><FormattedMessage id='account.follows_you' defaultMessage='Follows you' /></span>; - } - - if (me !== account.get('id')) { - if (account.getIn(['relationship', 'requested'])) { - actionBtn = ( - <div className='account--action-button'> - <IconButton size={26} active icon='hourglass' title={intl.formatMessage(messages.requested)} onClick={this.props.onFollow} /> - </div> - ); - } else if (!account.getIn(['relationship', 'blocking'])) { - actionBtn = ( - <div className='account--action-button'> - <IconButton size={26} icon={account.getIn(['relationship', 'following']) ? 'user-times' : 'user-plus'} active={account.getIn(['relationship', 'following'])} title={intl.formatMessage(account.getIn(['relationship', 'following']) ? messages.unfollow : messages.follow)} onClick={this.props.onFollow} /> - </div> - ); - } - } - - const { text, metadata } = processBio(account.get('note')); - - return ( - <div className='account__header__wrapper'> - <div className='account__header' style={{ backgroundImage: `url(${account.get('header')})` }}> - <div> - <Avatar account={account} size={90} /> - - <span className='account__header__display-name' dangerouslySetInnerHTML={{ __html: displayName }} /> - <span className='account__header__username'>@{account.get('acct')} {account.get('locked') ? <i className='fa fa-lock' /> : null}</span> - <div className='account__header__content' dangerouslySetInnerHTML={{ __html: emojify(text) }} /> - - {info} - {actionBtn} - </div> - </div> - - {metadata.length && ( - <table className='account__metadata'> - <tbody> - {(() => { - let data = []; - for (let i = 0; i < metadata.length; i++) { - data.push( - <tr key={i}> - <th scope='row'><div dangerouslySetInnerHTML={{ __html: emojify(metadata[i][0]) }} /></th> - <td><div dangerouslySetInnerHTML={{ __html: emojify(metadata[i][1]) }} /></td> - </tr> - ); - } - return data; - })()} - </tbody> - </table> - ) || null} - </div> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/account_gallery/components/media_item.js b/app/javascript/themes/glitch/features/account_gallery/components/media_item.js deleted file mode 100644 index 88c9156b5..000000000 --- a/app/javascript/themes/glitch/features/account_gallery/components/media_item.js +++ /dev/null @@ -1,39 +0,0 @@ -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import Permalink from 'themes/glitch/components/permalink'; - -export default class MediaItem extends ImmutablePureComponent { - - static propTypes = { - media: ImmutablePropTypes.map.isRequired, - }; - - render () { - const { media } = this.props; - const status = media.get('status'); - - let content, style; - - if (media.get('type') === 'gifv') { - content = <span className='media-gallery__gifv__label'>GIF</span>; - } - - if (!status.get('sensitive')) { - style = { backgroundImage: `url(${media.get('preview_url')})` }; - } - - return ( - <div className='account-gallery__item'> - <Permalink - to={`/statuses/${status.get('id')}`} - href={status.get('url')} - style={style} - > - {content} - </Permalink> - </div> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/account_gallery/index.js b/app/javascript/themes/glitch/features/account_gallery/index.js deleted file mode 100644 index a21c089da..000000000 --- a/app/javascript/themes/glitch/features/account_gallery/index.js +++ /dev/null @@ -1,111 +0,0 @@ -import React from 'react'; -import { connect } from 'react-redux'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import PropTypes from 'prop-types'; -import { fetchAccount } from 'themes/glitch/actions/accounts'; -import { refreshAccountMediaTimeline, expandAccountMediaTimeline } from 'themes/glitch/actions/timelines'; -import LoadingIndicator from 'themes/glitch/components/loading_indicator'; -import Column from 'themes/glitch/features/ui/components/column'; -import ColumnBackButton from 'themes/glitch/components/column_back_button'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import { getAccountGallery } from 'themes/glitch/selectors'; -import MediaItem from './components/media_item'; -import HeaderContainer from 'themes/glitch/features/account_timeline/containers/header_container'; -import { FormattedMessage } from 'react-intl'; -import { ScrollContainer } from 'react-router-scroll-4'; -import LoadMore from 'themes/glitch/components/load_more'; - -const mapStateToProps = (state, props) => ({ - medias: getAccountGallery(state, props.params.accountId), - isLoading: state.getIn(['timelines', `account:${props.params.accountId}:media`, 'isLoading']), - hasMore: !!state.getIn(['timelines', `account:${props.params.accountId}:media`, 'next']), -}); - -@connect(mapStateToProps) -export default class AccountGallery extends ImmutablePureComponent { - - static propTypes = { - params: PropTypes.object.isRequired, - dispatch: PropTypes.func.isRequired, - medias: ImmutablePropTypes.list.isRequired, - isLoading: PropTypes.bool, - hasMore: PropTypes.bool, - }; - - componentDidMount () { - this.props.dispatch(fetchAccount(this.props.params.accountId)); - this.props.dispatch(refreshAccountMediaTimeline(this.props.params.accountId)); - } - - componentWillReceiveProps (nextProps) { - if (nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) { - this.props.dispatch(fetchAccount(nextProps.params.accountId)); - this.props.dispatch(refreshAccountMediaTimeline(this.props.params.accountId)); - } - } - - handleScrollToBottom = () => { - if (this.props.hasMore) { - this.props.dispatch(expandAccountMediaTimeline(this.props.params.accountId)); - } - } - - handleScroll = (e) => { - const { scrollTop, scrollHeight, clientHeight } = e.target; - const offset = scrollHeight - scrollTop - clientHeight; - - if (150 > offset && !this.props.isLoading) { - this.handleScrollToBottom(); - } - } - - handleLoadMore = (e) => { - e.preventDefault(); - this.handleScrollToBottom(); - } - - render () { - const { medias, isLoading, hasMore } = this.props; - - let loadMore = null; - - if (!medias && isLoading) { - return ( - <Column> - <LoadingIndicator /> - </Column> - ); - } - - if (!isLoading && medias.size > 0 && hasMore) { - loadMore = <LoadMore onClick={this.handleLoadMore} />; - } - - return ( - <Column> - <ColumnBackButton /> - - <ScrollContainer scrollKey='account_gallery'> - <div className='scrollable' onScroll={this.handleScroll}> - <HeaderContainer accountId={this.props.params.accountId} /> - - <div className='account-section-headline'> - <FormattedMessage id='account.media' defaultMessage='Media' /> - </div> - - <div className='account-gallery__container'> - {medias.map(media => - <MediaItem - key={media.get('id')} - media={media} - /> - )} - {loadMore} - </div> - </div> - </ScrollContainer> - </Column> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/account_timeline/components/header.js b/app/javascript/themes/glitch/features/account_timeline/components/header.js deleted file mode 100644 index c719a7bcb..000000000 --- a/app/javascript/themes/glitch/features/account_timeline/components/header.js +++ /dev/null @@ -1,95 +0,0 @@ -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import PropTypes from 'prop-types'; -import InnerHeader from 'themes/glitch/features/account/components/header'; -import ActionBar from 'themes/glitch/features/account/components/action_bar'; -import MissingIndicator from 'themes/glitch/components/missing_indicator'; -import ImmutablePureComponent from 'react-immutable-pure-component'; - -export default class Header extends ImmutablePureComponent { - - static propTypes = { - account: ImmutablePropTypes.map, - onFollow: PropTypes.func.isRequired, - onBlock: PropTypes.func.isRequired, - onMention: PropTypes.func.isRequired, - onReblogToggle: PropTypes.func.isRequired, - onReport: PropTypes.func.isRequired, - onMute: PropTypes.func.isRequired, - onBlockDomain: PropTypes.func.isRequired, - onUnblockDomain: PropTypes.func.isRequired, - }; - - static contextTypes = { - router: PropTypes.object, - }; - - handleFollow = () => { - this.props.onFollow(this.props.account); - } - - handleBlock = () => { - this.props.onBlock(this.props.account); - } - - handleMention = () => { - this.props.onMention(this.props.account, this.context.router.history); - } - - handleReport = () => { - this.props.onReport(this.props.account); - } - - handleReblogToggle = () => { - this.props.onReblogToggle(this.props.account); - } - - handleMute = () => { - this.props.onMute(this.props.account); - } - - handleBlockDomain = () => { - const domain = this.props.account.get('acct').split('@')[1]; - - if (!domain) return; - - this.props.onBlockDomain(domain, this.props.account.get('id')); - } - - handleUnblockDomain = () => { - const domain = this.props.account.get('acct').split('@')[1]; - - if (!domain) return; - - this.props.onUnblockDomain(domain, this.props.account.get('id')); - } - - render () { - const { account } = this.props; - - if (account === null) { - return <MissingIndicator />; - } - - return ( - <div className='account-timeline__header'> - <InnerHeader - account={account} - onFollow={this.handleFollow} - /> - - <ActionBar - account={account} - onBlock={this.handleBlock} - onMention={this.handleMention} - onReblogToggle={this.handleReblogToggle} - onReport={this.handleReport} - onMute={this.handleMute} - onBlockDomain={this.handleBlockDomain} - onUnblockDomain={this.handleUnblockDomain} - /> - </div> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/account_timeline/containers/header_container.js b/app/javascript/themes/glitch/features/account_timeline/containers/header_container.js deleted file mode 100644 index 766b57b56..000000000 --- a/app/javascript/themes/glitch/features/account_timeline/containers/header_container.js +++ /dev/null @@ -1,104 +0,0 @@ -import React from 'react'; -import { connect } from 'react-redux'; -import { makeGetAccount } from 'themes/glitch/selectors'; -import Header from '../components/header'; -import { - followAccount, - unfollowAccount, - blockAccount, - unblockAccount, - unmuteAccount, -} from 'themes/glitch/actions/accounts'; -import { mentionCompose } from 'themes/glitch/actions/compose'; -import { initMuteModal } from 'themes/glitch/actions/mutes'; -import { initReport } from 'themes/glitch/actions/reports'; -import { openModal } from 'themes/glitch/actions/modal'; -import { blockDomain, unblockDomain } from 'themes/glitch/actions/domain_blocks'; -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; -import { unfollowModal } from 'themes/glitch/util/initial_state'; - -const messages = defineMessages({ - unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' }, - blockConfirm: { id: 'confirmations.block.confirm', defaultMessage: 'Block' }, - blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Hide entire domain' }, -}); - -const makeMapStateToProps = () => { - const getAccount = makeGetAccount(); - - const mapStateToProps = (state, { accountId }) => ({ - account: getAccount(state, accountId), - }); - - return mapStateToProps; -}; - -const mapDispatchToProps = (dispatch, { intl }) => ({ - - onFollow (account) { - if (account.getIn(['relationship', 'following']) || account.getIn(['relationship', 'requested'])) { - if (unfollowModal) { - dispatch(openModal('CONFIRM', { - message: <FormattedMessage id='confirmations.unfollow.message' defaultMessage='Are you sure you want to unfollow {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />, - confirm: intl.formatMessage(messages.unfollowConfirm), - onConfirm: () => dispatch(unfollowAccount(account.get('id'))), - })); - } else { - dispatch(unfollowAccount(account.get('id'))); - } - } else { - dispatch(followAccount(account.get('id'))); - } - }, - - onBlock (account) { - if (account.getIn(['relationship', 'blocking'])) { - dispatch(unblockAccount(account.get('id'))); - } else { - dispatch(openModal('CONFIRM', { - message: <FormattedMessage id='confirmations.block.message' defaultMessage='Are you sure you want to block {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />, - confirm: intl.formatMessage(messages.blockConfirm), - onConfirm: () => dispatch(blockAccount(account.get('id'))), - })); - } - }, - - onMention (account, router) { - dispatch(mentionCompose(account, router)); - }, - - onReblogToggle (account) { - if (account.getIn(['relationship', 'following', 'reblogs'])) { - dispatch(followAccount(account.get('id'), false)); - } else { - dispatch(followAccount(account.get('id'), true)); - } - }, - - onReport (account) { - dispatch(initReport(account)); - }, - - onMute (account) { - if (account.getIn(['relationship', 'muting'])) { - dispatch(unmuteAccount(account.get('id'))); - } else { - dispatch(initMuteModal(account)); - } - }, - - onBlockDomain (domain, accountId) { - dispatch(openModal('CONFIRM', { - message: <FormattedMessage id='confirmations.domain_block.message' defaultMessage='Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.' values={{ domain: <strong>{domain}</strong> }} />, - confirm: intl.formatMessage(messages.blockDomainConfirm), - onConfirm: () => dispatch(blockDomain(domain, accountId)), - })); - }, - - onUnblockDomain (domain, accountId) { - dispatch(unblockDomain(domain, accountId)); - }, - -}); - -export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Header)); diff --git a/app/javascript/themes/glitch/features/account_timeline/index.js b/app/javascript/themes/glitch/features/account_timeline/index.js deleted file mode 100644 index 81336ef3a..000000000 --- a/app/javascript/themes/glitch/features/account_timeline/index.js +++ /dev/null @@ -1,77 +0,0 @@ -import React from 'react'; -import { connect } from 'react-redux'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import PropTypes from 'prop-types'; -import { fetchAccount } from 'themes/glitch/actions/accounts'; -import { refreshAccountTimeline, expandAccountTimeline } from 'themes/glitch/actions/timelines'; -import StatusList from '../../components/status_list'; -import LoadingIndicator from '../../components/loading_indicator'; -import Column from '../ui/components/column'; -import HeaderContainer from './containers/header_container'; -import ColumnBackButton from '../../components/column_back_button'; -import { List as ImmutableList } from 'immutable'; -import ImmutablePureComponent from 'react-immutable-pure-component'; - -const mapStateToProps = (state, props) => ({ - statusIds: state.getIn(['timelines', `account:${props.params.accountId}`, 'items'], ImmutableList()), - isLoading: state.getIn(['timelines', `account:${props.params.accountId}`, 'isLoading']), - hasMore: !!state.getIn(['timelines', `account:${props.params.accountId}`, 'next']), -}); - -@connect(mapStateToProps) -export default class AccountTimeline extends ImmutablePureComponent { - - static propTypes = { - params: PropTypes.object.isRequired, - dispatch: PropTypes.func.isRequired, - statusIds: ImmutablePropTypes.list, - isLoading: PropTypes.bool, - hasMore: PropTypes.bool, - }; - - componentWillMount () { - this.props.dispatch(fetchAccount(this.props.params.accountId)); - this.props.dispatch(refreshAccountTimeline(this.props.params.accountId)); - } - - componentWillReceiveProps (nextProps) { - if (nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) { - this.props.dispatch(fetchAccount(nextProps.params.accountId)); - this.props.dispatch(refreshAccountTimeline(nextProps.params.accountId)); - } - } - - handleScrollToBottom = () => { - if (!this.props.isLoading && this.props.hasMore) { - this.props.dispatch(expandAccountTimeline(this.props.params.accountId)); - } - } - - render () { - const { statusIds, isLoading, hasMore } = this.props; - - if (!statusIds && isLoading) { - return ( - <Column> - <LoadingIndicator /> - </Column> - ); - } - - return ( - <Column name='account'> - <ColumnBackButton /> - - <StatusList - prepend={<HeaderContainer accountId={this.props.params.accountId} />} - scrollKey='account_timeline' - statusIds={statusIds} - isLoading={isLoading} - hasMore={hasMore} - onScrollToBottom={this.handleScrollToBottom} - /> - </Column> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/blocks/index.js b/app/javascript/themes/glitch/features/blocks/index.js deleted file mode 100644 index 70630818c..000000000 --- a/app/javascript/themes/glitch/features/blocks/index.js +++ /dev/null @@ -1,70 +0,0 @@ -import React from 'react'; -import { connect } from 'react-redux'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import PropTypes from 'prop-types'; -import LoadingIndicator from 'themes/glitch/components/loading_indicator'; -import { ScrollContainer } from 'react-router-scroll-4'; -import Column from 'themes/glitch/features/ui/components/column'; -import ColumnBackButtonSlim from 'themes/glitch/components/column_back_button_slim'; -import AccountContainer from 'themes/glitch/containers/account_container'; -import { fetchBlocks, expandBlocks } from 'themes/glitch/actions/blocks'; -import { defineMessages, injectIntl } from 'react-intl'; -import ImmutablePureComponent from 'react-immutable-pure-component'; - -const messages = defineMessages({ - heading: { id: 'column.blocks', defaultMessage: 'Blocked users' }, -}); - -const mapStateToProps = state => ({ - accountIds: state.getIn(['user_lists', 'blocks', 'items']), -}); - -@connect(mapStateToProps) -@injectIntl -export default class Blocks extends ImmutablePureComponent { - - static propTypes = { - params: PropTypes.object.isRequired, - dispatch: PropTypes.func.isRequired, - accountIds: ImmutablePropTypes.list, - intl: PropTypes.object.isRequired, - }; - - componentWillMount () { - this.props.dispatch(fetchBlocks()); - } - - handleScroll = (e) => { - const { scrollTop, scrollHeight, clientHeight } = e.target; - - if (scrollTop === scrollHeight - clientHeight) { - this.props.dispatch(expandBlocks()); - } - } - - render () { - const { intl, accountIds } = this.props; - - if (!accountIds) { - return ( - <Column> - <LoadingIndicator /> - </Column> - ); - } - - return ( - <Column name='blocks' icon='ban' heading={intl.formatMessage(messages.heading)}> - <ColumnBackButtonSlim /> - <ScrollContainer scrollKey='blocks'> - <div className='scrollable' onScroll={this.handleScroll}> - {accountIds.map(id => - <AccountContainer key={id} id={id} /> - )} - </div> - </ScrollContainer> - </Column> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/community_timeline/components/column_settings.js b/app/javascript/themes/glitch/features/community_timeline/components/column_settings.js deleted file mode 100644 index 988e36308..000000000 --- a/app/javascript/themes/glitch/features/community_timeline/components/column_settings.js +++ /dev/null @@ -1,35 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; -import SettingText from 'themes/glitch/components/setting_text'; - -const messages = defineMessages({ - filter_regex: { id: 'home.column_settings.filter_regex', defaultMessage: 'Filter out by regular expressions' }, - settings: { id: 'home.settings', defaultMessage: 'Column settings' }, -}); - -@injectIntl -export default class ColumnSettings extends React.PureComponent { - - static propTypes = { - settings: ImmutablePropTypes.map.isRequired, - onChange: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - }; - - render () { - const { settings, onChange, intl } = this.props; - - return ( - <div> - <span className='column-settings__section'><FormattedMessage id='home.column_settings.advanced' defaultMessage='Advanced' /></span> - - <div className='column-settings__row'> - <SettingText settings={settings} settingKey={['regex', 'body']} onChange={onChange} label={intl.formatMessage(messages.filter_regex)} /> - </div> - </div> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/community_timeline/containers/column_settings_container.js b/app/javascript/themes/glitch/features/community_timeline/containers/column_settings_container.js deleted file mode 100644 index cd9c34365..000000000 --- a/app/javascript/themes/glitch/features/community_timeline/containers/column_settings_container.js +++ /dev/null @@ -1,17 +0,0 @@ -import { connect } from 'react-redux'; -import ColumnSettings from '../components/column_settings'; -import { changeSetting } from 'themes/glitch/actions/settings'; - -const mapStateToProps = state => ({ - settings: state.getIn(['settings', 'community']), -}); - -const mapDispatchToProps = dispatch => ({ - - onChange (key, checked) { - dispatch(changeSetting(['community', ...key], checked)); - }, - -}); - -export default connect(mapStateToProps, mapDispatchToProps)(ColumnSettings); diff --git a/app/javascript/themes/glitch/features/community_timeline/index.js b/app/javascript/themes/glitch/features/community_timeline/index.js deleted file mode 100644 index 9d255bd01..000000000 --- a/app/javascript/themes/glitch/features/community_timeline/index.js +++ /dev/null @@ -1,107 +0,0 @@ -import React from 'react'; -import { connect } from 'react-redux'; -import PropTypes from 'prop-types'; -import StatusListContainer from 'themes/glitch/features/ui/containers/status_list_container'; -import Column from 'themes/glitch/components/column'; -import ColumnHeader from 'themes/glitch/components/column_header'; -import { - refreshCommunityTimeline, - expandCommunityTimeline, -} from 'themes/glitch/actions/timelines'; -import { addColumn, removeColumn, moveColumn } from 'themes/glitch/actions/columns'; -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; -import ColumnSettingsContainer from './containers/column_settings_container'; -import { connectCommunityStream } from 'themes/glitch/actions/streaming'; - -const messages = defineMessages({ - title: { id: 'column.community', defaultMessage: 'Local timeline' }, -}); - -const mapStateToProps = state => ({ - hasUnread: state.getIn(['timelines', 'community', 'unread']) > 0, -}); - -@connect(mapStateToProps) -@injectIntl -export default class CommunityTimeline extends React.PureComponent { - - static propTypes = { - dispatch: PropTypes.func.isRequired, - columnId: PropTypes.string, - intl: PropTypes.object.isRequired, - hasUnread: PropTypes.bool, - multiColumn: PropTypes.bool, - }; - - handlePin = () => { - const { columnId, dispatch } = this.props; - - if (columnId) { - dispatch(removeColumn(columnId)); - } else { - dispatch(addColumn('COMMUNITY', {})); - } - } - - handleMove = (dir) => { - const { columnId, dispatch } = this.props; - dispatch(moveColumn(columnId, dir)); - } - - handleHeaderClick = () => { - this.column.scrollTop(); - } - - componentDidMount () { - const { dispatch } = this.props; - - dispatch(refreshCommunityTimeline()); - this.disconnect = dispatch(connectCommunityStream()); - } - - componentWillUnmount () { - if (this.disconnect) { - this.disconnect(); - this.disconnect = null; - } - } - - setRef = c => { - this.column = c; - } - - handleLoadMore = () => { - this.props.dispatch(expandCommunityTimeline()); - } - - render () { - const { intl, hasUnread, columnId, multiColumn } = this.props; - const pinned = !!columnId; - - return ( - <Column ref={this.setRef} name='local'> - <ColumnHeader - icon='users' - active={hasUnread} - title={intl.formatMessage(messages.title)} - onPin={this.handlePin} - onMove={this.handleMove} - onClick={this.handleHeaderClick} - pinned={pinned} - multiColumn={multiColumn} - > - <ColumnSettingsContainer /> - </ColumnHeader> - - <StatusListContainer - trackScroll={!pinned} - scrollKey={`community_timeline-${columnId}`} - timelineId='community' - loadMore={this.handleLoadMore} - emptyMessage={<FormattedMessage id='empty_column.community' defaultMessage='The local timeline is empty. Write something publicly to get the ball rolling!' />} - /> - </Column> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/compose/components/advanced_options.js b/app/javascript/themes/glitch/features/compose/components/advanced_options.js deleted file mode 100644 index 045bad2e5..000000000 --- a/app/javascript/themes/glitch/features/compose/components/advanced_options.js +++ /dev/null @@ -1,62 +0,0 @@ -// Package imports. -import React from 'react'; -import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import { injectIntl, defineMessages } from 'react-intl'; - -// Our imports. -import ComposeAdvancedOptionsToggle from './advanced_options_toggle'; -import ComposeDropdown from './dropdown'; - -const messages = defineMessages({ - local_only_short : - { id: 'advanced-options.local-only.short', defaultMessage: 'Local-only' }, - local_only_long : - { id: 'advanced-options.local-only.long', defaultMessage: 'Do not post to other instances' }, - advanced_options_icon_title : - { id: 'advanced_options.icon_title', defaultMessage: 'Advanced options' }, -}); - -@injectIntl -export default class ComposeAdvancedOptions extends React.PureComponent { - - static propTypes = { - values : ImmutablePropTypes.contains({ - do_not_federate : PropTypes.bool.isRequired, - }).isRequired, - onChange : PropTypes.func.isRequired, - intl : PropTypes.object.isRequired, - }; - - render () { - const { intl, values } = this.props; - const options = [ - { icon: 'wifi', shortText: messages.local_only_short, longText: messages.local_only_long, name: 'do_not_federate' }, - ]; - const anyEnabled = values.some((enabled) => enabled); - - const optionElems = options.map((option) => { - return ( - <ComposeAdvancedOptionsToggle - onChange={this.props.onChange} - active={values.get(option.name)} - key={option.name} - name={option.name} - shortText={intl.formatMessage(option.shortText)} - longText={intl.formatMessage(option.longText)} - /> - ); - }); - - return ( - <ComposeDropdown - title={intl.formatMessage(messages.advanced_options_icon_title)} - icon='home' - highlight={anyEnabled} - > - {optionElems} - </ComposeDropdown> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/compose/components/advanced_options_toggle.js b/app/javascript/themes/glitch/features/compose/components/advanced_options_toggle.js deleted file mode 100644 index 98b3b6a44..000000000 --- a/app/javascript/themes/glitch/features/compose/components/advanced_options_toggle.js +++ /dev/null @@ -1,35 +0,0 @@ -// Package imports. -import React from 'react'; -import PropTypes from 'prop-types'; -import Toggle from 'react-toggle'; - -export default class ComposeAdvancedOptionsToggle extends React.PureComponent { - - static propTypes = { - onChange: PropTypes.func.isRequired, - active: PropTypes.bool.isRequired, - name: PropTypes.string.isRequired, - shortText: PropTypes.string.isRequired, - longText: PropTypes.string.isRequired, - } - - onToggle = () => { - this.props.onChange(this.props.name); - } - - render() { - const { active, shortText, longText } = this.props; - return ( - <div role='button' tabIndex='0' className='advanced-options-dropdown__option' onClick={this.onToggle}> - <div className='advanced-options-dropdown__option__toggle'> - <Toggle checked={active} onChange={this.onToggle} /> - </div> - <div className='advanced-options-dropdown__option__content'> - <strong>{shortText}</strong> - {longText} - </div> - </div> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/compose/components/attach_options.js b/app/javascript/themes/glitch/features/compose/components/attach_options.js deleted file mode 100644 index c396714f3..000000000 --- a/app/javascript/themes/glitch/features/compose/components/attach_options.js +++ /dev/null @@ -1,131 +0,0 @@ -// Package imports // -import React from 'react'; -import PropTypes from 'prop-types'; -import { connect } from 'react-redux'; -import { injectIntl, defineMessages } from 'react-intl'; - -// Our imports // -import ComposeDropdown from './dropdown'; -import { uploadCompose } from 'themes/glitch/actions/compose'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import { openModal } from 'themes/glitch/actions/modal'; - -const messages = defineMessages({ - upload : - { id: 'compose.attach.upload', defaultMessage: 'Upload a file' }, - doodle : - { id: 'compose.attach.doodle', defaultMessage: 'Draw something' }, - attach : - { id: 'compose.attach', defaultMessage: 'Attach...' }, -}); - -const mapStateToProps = state => ({ - // This horrible expression is copied from vanilla upload_button_container - disabled: state.getIn(['compose', 'is_uploading']) || (state.getIn(['compose', 'media_attachments']).size > 3 || state.getIn(['compose', 'media_attachments']).some(m => m.get('type') === 'video')), - resetFileKey: state.getIn(['compose', 'resetFileKey']), - acceptContentTypes: state.getIn(['media_attachments', 'accept_content_types']), -}); - -const mapDispatchToProps = dispatch => ({ - onSelectFile (files) { - dispatch(uploadCompose(files)); - }, - onOpenDoodle () { - dispatch(openModal('DOODLE', { noEsc: true })); - }, -}); - -@injectIntl -@connect(mapStateToProps, mapDispatchToProps) -export default class ComposeAttachOptions extends ImmutablePureComponent { - - static propTypes = { - intl : PropTypes.object.isRequired, - resetFileKey: PropTypes.number, - acceptContentTypes: ImmutablePropTypes.listOf(PropTypes.string).isRequired, - disabled: PropTypes.bool, - onSelectFile: PropTypes.func.isRequired, - onOpenDoodle: PropTypes.func.isRequired, - }; - - handleItemClick = bt => { - if (bt === 'upload') { - this.fileElement.click(); - } - - if (bt === 'doodle') { - this.props.onOpenDoodle(); - } - - this.dropdown.setState({ open: false }); - }; - - handleFileChange = (e) => { - if (e.target.files.length > 0) { - this.props.onSelectFile(e.target.files); - } - } - - setFileRef = (c) => { - this.fileElement = c; - } - - setDropdownRef = (c) => { - this.dropdown = c; - } - - render () { - const { intl, resetFileKey, disabled, acceptContentTypes } = this.props; - - const options = [ - { icon: 'cloud-upload', text: messages.upload, name: 'upload' }, - { icon: 'paint-brush', text: messages.doodle, name: 'doodle' }, - ]; - - const optionElems = options.map((item) => { - const hdl = () => this.handleItemClick(item.name); - return ( - <div - role='button' - tabIndex='0' - key={item.name} - onClick={hdl} - className='privacy-dropdown__option' - > - <div className='privacy-dropdown__option__icon'> - <i className={`fa fa-fw fa-${item.icon}`} /> - </div> - - <div className='privacy-dropdown__option__content'> - <strong>{intl.formatMessage(item.text)}</strong> - </div> - </div> - ); - }); - - return ( - <div> - <ComposeDropdown - title={intl.formatMessage(messages.attach)} - icon='paperclip' - disabled={disabled} - ref={this.setDropdownRef} - > - {optionElems} - </ComposeDropdown> - <input - key={resetFileKey} - ref={this.setFileRef} - type='file' - multiple={false} - accept={acceptContentTypes.toArray().join(',')} - onChange={this.handleFileChange} - disabled={disabled} - style={{ display: 'none' }} - /> - </div> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/compose/components/autosuggest_account.js b/app/javascript/themes/glitch/features/compose/components/autosuggest_account.js deleted file mode 100644 index 4a98d89fe..000000000 --- a/app/javascript/themes/glitch/features/compose/components/autosuggest_account.js +++ /dev/null @@ -1,24 +0,0 @@ -import React from 'react'; -import Avatar from 'themes/glitch/components/avatar'; -import DisplayName from 'themes/glitch/components/display_name'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import ImmutablePureComponent from 'react-immutable-pure-component'; - -export default class AutosuggestAccount extends ImmutablePureComponent { - - static propTypes = { - account: ImmutablePropTypes.map.isRequired, - }; - - render () { - const { account } = this.props; - - return ( - <div className='autosuggest-account'> - <div className='autosuggest-account-icon'><Avatar account={account} size={18} /></div> - <DisplayName account={account} /> - </div> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/compose/components/character_counter.js b/app/javascript/themes/glitch/features/compose/components/character_counter.js deleted file mode 100644 index 0ecfc9141..000000000 --- a/app/javascript/themes/glitch/features/compose/components/character_counter.js +++ /dev/null @@ -1,25 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { length } from 'stringz'; - -export default class CharacterCounter extends React.PureComponent { - - static propTypes = { - text: PropTypes.string.isRequired, - max: PropTypes.number.isRequired, - }; - - checkRemainingText (diff) { - if (diff < 0) { - return <span className='character-counter character-counter--over'>{diff}</span>; - } - - return <span className='character-counter'>{diff}</span>; - } - - render () { - const diff = this.props.max - length(this.props.text); - return this.checkRemainingText(diff); - } - -} diff --git a/app/javascript/themes/glitch/features/compose/components/compose_form.js b/app/javascript/themes/glitch/features/compose/components/compose_form.js deleted file mode 100644 index 54b1944a4..000000000 --- a/app/javascript/themes/glitch/features/compose/components/compose_form.js +++ /dev/null @@ -1,286 +0,0 @@ -import React from 'react'; -import CharacterCounter from './character_counter'; -import Button from 'themes/glitch/components/button'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import PropTypes from 'prop-types'; -import ReplyIndicatorContainer from '../containers/reply_indicator_container'; -import AutosuggestTextarea from 'themes/glitch/components/autosuggest_textarea'; -import { defineMessages, injectIntl } from 'react-intl'; -import Collapsable from 'themes/glitch/components/collapsable'; -import SpoilerButtonContainer from '../containers/spoiler_button_container'; -import PrivacyDropdownContainer from '../containers/privacy_dropdown_container'; -import ComposeAdvancedOptionsContainer from '../containers/advanced_options_container'; -import SensitiveButtonContainer from '../containers/sensitive_button_container'; -import EmojiPickerDropdown from '../containers/emoji_picker_dropdown_container'; -import UploadFormContainer from '../containers/upload_form_container'; -import WarningContainer from '../containers/warning_container'; -import { isMobile } from 'themes/glitch/util/is_mobile'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import { length } from 'stringz'; -import { countableText } from 'themes/glitch/util/counter'; -import ComposeAttachOptions from './attach_options'; -import initialState from 'themes/glitch/util/initial_state'; - -const maxChars = initialState.max_toot_chars; - -const messages = defineMessages({ - placeholder: { id: 'compose_form.placeholder', defaultMessage: 'What is on your mind?' }, - spoiler_placeholder: { id: 'compose_form.spoiler_placeholder', defaultMessage: 'Write your warning here' }, - publish: { id: 'compose_form.publish', defaultMessage: 'Toot' }, - publishLoud: { id: 'compose_form.publish_loud', defaultMessage: '{publish}!' }, -}); - -@injectIntl -export default class ComposeForm extends ImmutablePureComponent { - - static propTypes = { - intl: PropTypes.object.isRequired, - text: PropTypes.string.isRequired, - suggestion_token: PropTypes.string, - suggestions: ImmutablePropTypes.list, - spoiler: PropTypes.bool, - privacy: PropTypes.string, - advanced_options: ImmutablePropTypes.contains({ - do_not_federate: PropTypes.bool, - }), - spoiler_text: PropTypes.string, - focusDate: PropTypes.instanceOf(Date), - preselectDate: PropTypes.instanceOf(Date), - is_submitting: PropTypes.bool, - is_uploading: PropTypes.bool, - onChange: PropTypes.func.isRequired, - onSubmit: PropTypes.func.isRequired, - onClearSuggestions: PropTypes.func.isRequired, - onFetchSuggestions: PropTypes.func.isRequired, - onPrivacyChange: PropTypes.func.isRequired, - onSuggestionSelected: PropTypes.func.isRequired, - onChangeSpoilerText: PropTypes.func.isRequired, - onPaste: PropTypes.func.isRequired, - onPickEmoji: PropTypes.func.isRequired, - showSearch: PropTypes.bool, - settings : ImmutablePropTypes.map.isRequired, - }; - - static defaultProps = { - showSearch: false, - }; - - handleChange = (e) => { - this.props.onChange(e.target.value); - } - - handleKeyDown = (e) => { - if (e.keyCode === 13 && (e.ctrlKey || e.metaKey)) { - this.handleSubmit(); - } - } - - handleSubmit2 = () => { - this.props.onPrivacyChange(this.props.settings.get('side_arm')); - this.handleSubmit(); - } - - handleSubmit = () => { - if (this.props.text !== this.autosuggestTextarea.textarea.value) { - // Something changed the text inside the textarea (e.g. browser extensions like Grammarly) - // Update the state to match the current text - this.props.onChange(this.autosuggestTextarea.textarea.value); - } - - this.props.onSubmit(); - } - - onSuggestionsClearRequested = () => { - this.props.onClearSuggestions(); - } - - onSuggestionsFetchRequested = (token) => { - this.props.onFetchSuggestions(token); - } - - onSuggestionSelected = (tokenStart, token, value) => { - this._restoreCaret = null; - this.props.onSuggestionSelected(tokenStart, token, value); - } - - handleChangeSpoilerText = (e) => { - this.props.onChangeSpoilerText(e.target.value); - } - - componentWillReceiveProps (nextProps) { - // If this is the update where we've finished uploading, - // save the last caret position so we can restore it below! - if (!nextProps.is_uploading && this.props.is_uploading) { - this._restoreCaret = this.autosuggestTextarea.textarea.selectionStart; - } - } - - componentDidUpdate (prevProps) { - // This statement does several things: - // - If we're beginning a reply, and, - // - Replying to zero or one users, places the cursor at the end of the textbox. - // - Replying to more than one user, selects any usernames past the first; - // this provides a convenient shortcut to drop everyone else from the conversation. - // - If we've just finished uploading an image, and have a saved caret position, - // restores the cursor to that position after the text changes! - if (this.props.focusDate !== prevProps.focusDate || (prevProps.is_uploading && !this.props.is_uploading && typeof this._restoreCaret === 'number')) { - let selectionEnd, selectionStart; - - if (this.props.preselectDate !== prevProps.preselectDate) { - selectionEnd = this.props.text.length; - selectionStart = this.props.text.search(/\s/) + 1; - } else if (typeof this._restoreCaret === 'number') { - selectionStart = this._restoreCaret; - selectionEnd = this._restoreCaret; - } else { - selectionEnd = this.props.text.length; - selectionStart = selectionEnd; - } - - this.autosuggestTextarea.textarea.setSelectionRange(selectionStart, selectionEnd); - this.autosuggestTextarea.textarea.focus(); - } else if(prevProps.is_submitting && !this.props.is_submitting) { - this.autosuggestTextarea.textarea.focus(); - } - } - - setAutosuggestTextarea = (c) => { - this.autosuggestTextarea = c; - } - - handleEmojiPick = (data) => { - const position = this.autosuggestTextarea.textarea.selectionStart; - const emojiChar = data.native; - this._restoreCaret = position + emojiChar.length + 1; - this.props.onPickEmoji(position, data); - } - - render () { - const { intl, onPaste, showSearch } = this.props; - const disabled = this.props.is_submitting; - const maybeEye = (this.props.advanced_options && this.props.advanced_options.do_not_federate) ? ' 👁️' : ''; - const text = [this.props.spoiler_text, countableText(this.props.text), maybeEye].join(''); - - const secondaryVisibility = this.props.settings.get('side_arm'); - let showSideArm = secondaryVisibility !== 'none'; - - let publishText = ''; - let publishText2 = ''; - let title = ''; - let title2 = ''; - - const privacyIcons = { - none: '', - public: 'globe', - unlisted: 'unlock-alt', - private: 'lock', - direct: 'envelope', - }; - - title = `${intl.formatMessage(messages.publish)}: ${intl.formatMessage({ id: `privacy.${this.props.privacy}.short` })}`; - - if (showSideArm) { - // Enhanced behavior with dual toot buttons - publishText = ( - <span> - { - <i - className={`fa fa-${privacyIcons[this.props.privacy]}`} - style={{ paddingRight: '5px' }} - /> - }{intl.formatMessage(messages.publish)} - </span> - ); - - title2 = `${intl.formatMessage(messages.publish)}: ${intl.formatMessage({ id: `privacy.${secondaryVisibility}.short` })}`; - publishText2 = ( - <i - className={`fa fa-${privacyIcons[secondaryVisibility]}`} - aria-label={title2} - /> - ); - } else { - // Original vanilla behavior - no icon if public or unlisted - if (this.props.privacy === 'private' || this.props.privacy === 'direct') { - publishText = <span className='compose-form__publish-private'><i className='fa fa-lock' /> {intl.formatMessage(messages.publish)}</span>; - } else { - publishText = this.props.privacy !== 'unlisted' ? intl.formatMessage(messages.publishLoud, { publish: intl.formatMessage(messages.publish) }) : intl.formatMessage(messages.publish); - } - } - - const submitDisabled = disabled || this.props.is_uploading || length(text) > maxChars || (text.length !== 0 && text.trim().length === 0); - - return ( - <div className='compose-form'> - <Collapsable isVisible={this.props.spoiler} fullHeight={50}> - <div className='spoiler-input'> - <label> - <span style={{ display: 'none' }}>{intl.formatMessage(messages.spoiler_placeholder)}</span> - <input placeholder={intl.formatMessage(messages.spoiler_placeholder)} value={this.props.spoiler_text} onChange={this.handleChangeSpoilerText} onKeyDown={this.handleKeyDown} type='text' className='spoiler-input__input' id='cw-spoiler-input' /> - </label> - </div> - </Collapsable> - - <WarningContainer /> - - <ReplyIndicatorContainer /> - - <div className='compose-form__autosuggest-wrapper'> - <AutosuggestTextarea - ref={this.setAutosuggestTextarea} - placeholder={intl.formatMessage(messages.placeholder)} - disabled={disabled} - value={this.props.text} - onChange={this.handleChange} - suggestions={this.props.suggestions} - onKeyDown={this.handleKeyDown} - onSuggestionsFetchRequested={this.onSuggestionsFetchRequested} - onSuggestionsClearRequested={this.onSuggestionsClearRequested} - onSuggestionSelected={this.onSuggestionSelected} - onPaste={onPaste} - autoFocus={!showSearch && !isMobile(window.innerWidth)} - /> - - <EmojiPickerDropdown onPickEmoji={this.handleEmojiPick} /> - </div> - - <div className='compose-form__modifiers'> - <UploadFormContainer /> - </div> - - <div className='compose-form__buttons'> - <ComposeAttachOptions /> - <SensitiveButtonContainer /> - <div className='compose-form__buttons-separator' /> - <PrivacyDropdownContainer /> - <SpoilerButtonContainer /> - <ComposeAdvancedOptionsContainer /> - </div> - - <div className='compose-form__publish'> - <div className='character-counter__wrapper'><CharacterCounter max={maxChars} text={text} /></div> - <div className='compose-form__publish-button-wrapper'> - { - showSideArm ? - <Button - className='compose-form__publish__side-arm' - text={publishText2} - title={title2} - onClick={this.handleSubmit2} - disabled={submitDisabled} - /> : '' - } - <Button - className='compose-form__publish__primary' - text={publishText} - title={title} - onClick={this.handleSubmit} - disabled={submitDisabled} - /> - </div> - </div> - </div> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/compose/components/dropdown.js b/app/javascript/themes/glitch/features/compose/components/dropdown.js deleted file mode 100644 index f3d9f094e..000000000 --- a/app/javascript/themes/glitch/features/compose/components/dropdown.js +++ /dev/null @@ -1,77 +0,0 @@ -// Package imports. -import React from 'react'; -import PropTypes from 'prop-types'; - -// Our imports. -import IconButton from 'themes/glitch/components/icon_button'; - -const iconStyle = { - height : null, - lineHeight : '27px', -}; - -export default class ComposeDropdown extends React.PureComponent { - - static propTypes = { - title: PropTypes.string.isRequired, - icon: PropTypes.string, - highlight: PropTypes.bool, - disabled: PropTypes.bool, - children: PropTypes.arrayOf(PropTypes.node).isRequired, - }; - - state = { - open: false, - }; - - onGlobalClick = (e) => { - if (e.target !== this.node && !this.node.contains(e.target) && this.state.open) { - this.setState({ open: false }); - } - }; - - componentDidMount () { - window.addEventListener('click', this.onGlobalClick); - window.addEventListener('touchstart', this.onGlobalClick); - } - componentWillUnmount () { - window.removeEventListener('click', this.onGlobalClick); - window.removeEventListener('touchstart', this.onGlobalClick); - } - - onToggleDropdown = () => { - if (this.props.disabled) return; - this.setState({ open: !this.state.open }); - }; - - setRef = (c) => { - this.node = c; - }; - - render () { - const { open } = this.state; - let { highlight, title, icon, disabled } = this.props; - - if (!icon) icon = 'ellipsis-h'; - - return ( - <div ref={this.setRef} className={`advanced-options-dropdown ${open ? 'open' : ''} ${highlight ? 'active' : ''} `}> - <div className='advanced-options-dropdown__value'> - <IconButton - className={'inverted'} - title={title} - icon={icon} active={open || highlight} - size={18} - style={iconStyle} - disabled={disabled} - onClick={this.onToggleDropdown} - /> - </div> - <div className='advanced-options-dropdown__dropdown'> - {this.props.children} - </div> - </div> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/compose/components/emoji_picker_dropdown.js b/app/javascript/themes/glitch/features/compose/components/emoji_picker_dropdown.js deleted file mode 100644 index fd59efb85..000000000 --- a/app/javascript/themes/glitch/features/compose/components/emoji_picker_dropdown.js +++ /dev/null @@ -1,376 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { defineMessages, injectIntl } from 'react-intl'; -import { EmojiPicker as EmojiPickerAsync } from 'themes/glitch/util/async-components'; -import Overlay from 'react-overlays/lib/Overlay'; -import classNames from 'classnames'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import detectPassiveEvents from 'detect-passive-events'; -import { buildCustomEmojis } from 'themes/glitch/util/emoji'; - -const messages = defineMessages({ - emoji: { id: 'emoji_button.label', defaultMessage: 'Insert emoji' }, - emoji_search: { id: 'emoji_button.search', defaultMessage: 'Search...' }, - emoji_not_found: { id: 'emoji_button.not_found', defaultMessage: 'No emojos!! (╯°□°)╯︵ ┻━┻' }, - custom: { id: 'emoji_button.custom', defaultMessage: 'Custom' }, - recent: { id: 'emoji_button.recent', defaultMessage: 'Frequently used' }, - search_results: { id: 'emoji_button.search_results', defaultMessage: 'Search results' }, - people: { id: 'emoji_button.people', defaultMessage: 'People' }, - nature: { id: 'emoji_button.nature', defaultMessage: 'Nature' }, - food: { id: 'emoji_button.food', defaultMessage: 'Food & Drink' }, - activity: { id: 'emoji_button.activity', defaultMessage: 'Activity' }, - travel: { id: 'emoji_button.travel', defaultMessage: 'Travel & Places' }, - objects: { id: 'emoji_button.objects', defaultMessage: 'Objects' }, - symbols: { id: 'emoji_button.symbols', defaultMessage: 'Symbols' }, - flags: { id: 'emoji_button.flags', defaultMessage: 'Flags' }, -}); - -const assetHost = process.env.CDN_HOST || ''; -let EmojiPicker, Emoji; // load asynchronously - -const backgroundImageFn = () => `${assetHost}/emoji/sheet.png`; -const listenerOptions = detectPassiveEvents.hasSupport ? { passive: true } : false; - -const categoriesSort = [ - 'recent', - 'custom', - 'people', - 'nature', - 'foods', - 'activity', - 'places', - 'objects', - 'symbols', - 'flags', -]; - -class ModifierPickerMenu extends React.PureComponent { - - static propTypes = { - active: PropTypes.bool, - onSelect: PropTypes.func.isRequired, - onClose: PropTypes.func.isRequired, - }; - - handleClick = e => { - this.props.onSelect(e.currentTarget.getAttribute('data-index') * 1); - } - - componentWillReceiveProps (nextProps) { - if (nextProps.active) { - this.attachListeners(); - } else { - this.removeListeners(); - } - } - - componentWillUnmount () { - this.removeListeners(); - } - - handleDocumentClick = e => { - if (this.node && !this.node.contains(e.target)) { - this.props.onClose(); - } - } - - attachListeners () { - document.addEventListener('click', this.handleDocumentClick, false); - document.addEventListener('touchend', this.handleDocumentClick, listenerOptions); - } - - removeListeners () { - document.removeEventListener('click', this.handleDocumentClick, false); - document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions); - } - - setRef = c => { - this.node = c; - } - - render () { - const { active } = this.props; - - return ( - <div className='emoji-picker-dropdown__modifiers__menu' style={{ display: active ? 'block' : 'none' }} ref={this.setRef}> - <button onClick={this.handleClick} data-index={1}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={1} backgroundImageFn={backgroundImageFn} /></button> - <button onClick={this.handleClick} data-index={2}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={2} backgroundImageFn={backgroundImageFn} /></button> - <button onClick={this.handleClick} data-index={3}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={3} backgroundImageFn={backgroundImageFn} /></button> - <button onClick={this.handleClick} data-index={4}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={4} backgroundImageFn={backgroundImageFn} /></button> - <button onClick={this.handleClick} data-index={5}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={5} backgroundImageFn={backgroundImageFn} /></button> - <button onClick={this.handleClick} data-index={6}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={6} backgroundImageFn={backgroundImageFn} /></button> - </div> - ); - } - -} - -class ModifierPicker extends React.PureComponent { - - static propTypes = { - active: PropTypes.bool, - modifier: PropTypes.number, - onChange: PropTypes.func, - onClose: PropTypes.func, - onOpen: PropTypes.func, - }; - - handleClick = () => { - if (this.props.active) { - this.props.onClose(); - } else { - this.props.onOpen(); - } - } - - handleSelect = modifier => { - this.props.onChange(modifier); - this.props.onClose(); - } - - render () { - const { active, modifier } = this.props; - - return ( - <div className='emoji-picker-dropdown__modifiers'> - <Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={modifier} onClick={this.handleClick} backgroundImageFn={backgroundImageFn} /> - <ModifierPickerMenu active={active} onSelect={this.handleSelect} onClose={this.props.onClose} /> - </div> - ); - } - -} - -@injectIntl -class EmojiPickerMenu extends React.PureComponent { - - static propTypes = { - custom_emojis: ImmutablePropTypes.list, - frequentlyUsedEmojis: PropTypes.arrayOf(PropTypes.string), - loading: PropTypes.bool, - onClose: PropTypes.func.isRequired, - onPick: PropTypes.func.isRequired, - style: PropTypes.object, - placement: PropTypes.string, - arrowOffsetLeft: PropTypes.string, - arrowOffsetTop: PropTypes.string, - intl: PropTypes.object.isRequired, - skinTone: PropTypes.number.isRequired, - onSkinTone: PropTypes.func.isRequired, - }; - - static defaultProps = { - style: {}, - loading: true, - placement: 'bottom', - frequentlyUsedEmojis: [], - }; - - state = { - modifierOpen: false, - }; - - handleDocumentClick = e => { - if (this.node && !this.node.contains(e.target)) { - this.props.onClose(); - } - } - - componentDidMount () { - document.addEventListener('click', this.handleDocumentClick, false); - document.addEventListener('touchend', this.handleDocumentClick, listenerOptions); - } - - componentWillUnmount () { - document.removeEventListener('click', this.handleDocumentClick, false); - document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions); - } - - setRef = c => { - this.node = c; - } - - getI18n = () => { - const { intl } = this.props; - - return { - search: intl.formatMessage(messages.emoji_search), - notfound: intl.formatMessage(messages.emoji_not_found), - categories: { - search: intl.formatMessage(messages.search_results), - recent: intl.formatMessage(messages.recent), - people: intl.formatMessage(messages.people), - nature: intl.formatMessage(messages.nature), - foods: intl.formatMessage(messages.food), - activity: intl.formatMessage(messages.activity), - places: intl.formatMessage(messages.travel), - objects: intl.formatMessage(messages.objects), - symbols: intl.formatMessage(messages.symbols), - flags: intl.formatMessage(messages.flags), - custom: intl.formatMessage(messages.custom), - }, - }; - } - - handleClick = emoji => { - if (!emoji.native) { - emoji.native = emoji.colons; - } - - this.props.onClose(); - this.props.onPick(emoji); - } - - handleModifierOpen = () => { - this.setState({ modifierOpen: true }); - } - - handleModifierClose = () => { - this.setState({ modifierOpen: false }); - } - - handleModifierChange = modifier => { - this.props.onSkinTone(modifier); - } - - render () { - const { loading, style, intl, custom_emojis, skinTone, frequentlyUsedEmojis } = this.props; - - if (loading) { - return <div style={{ width: 299 }} />; - } - - const title = intl.formatMessage(messages.emoji); - const { modifierOpen } = this.state; - - return ( - <div className={classNames('emoji-picker-dropdown__menu', { selecting: modifierOpen })} style={style} ref={this.setRef}> - <EmojiPicker - perLine={8} - emojiSize={22} - sheetSize={32} - custom={buildCustomEmojis(custom_emojis)} - color='' - emoji='' - set='twitter' - title={title} - i18n={this.getI18n()} - onClick={this.handleClick} - include={categoriesSort} - recent={frequentlyUsedEmojis} - skin={skinTone} - showPreview={false} - backgroundImageFn={backgroundImageFn} - emojiTooltip - /> - - <ModifierPicker - active={modifierOpen} - modifier={skinTone} - onOpen={this.handleModifierOpen} - onClose={this.handleModifierClose} - onChange={this.handleModifierChange} - /> - </div> - ); - } - -} - -@injectIntl -export default class EmojiPickerDropdown extends React.PureComponent { - - static propTypes = { - custom_emojis: ImmutablePropTypes.list, - frequentlyUsedEmojis: PropTypes.arrayOf(PropTypes.string), - intl: PropTypes.object.isRequired, - onPickEmoji: PropTypes.func.isRequired, - onSkinTone: PropTypes.func.isRequired, - skinTone: PropTypes.number.isRequired, - }; - - state = { - active: false, - loading: false, - }; - - setRef = (c) => { - this.dropdown = c; - } - - onShowDropdown = () => { - this.setState({ active: true }); - - if (!EmojiPicker) { - this.setState({ loading: true }); - - EmojiPickerAsync().then(EmojiMart => { - EmojiPicker = EmojiMart.Picker; - Emoji = EmojiMart.Emoji; - - this.setState({ loading: false }); - }).catch(() => { - this.setState({ loading: false }); - }); - } - } - - onHideDropdown = () => { - this.setState({ active: false }); - } - - onToggle = (e) => { - if (!this.state.loading && (!e.key || e.key === 'Enter')) { - if (this.state.active) { - this.onHideDropdown(); - } else { - this.onShowDropdown(); - } - } - } - - handleKeyDown = e => { - if (e.key === 'Escape') { - this.onHideDropdown(); - } - } - - setTargetRef = c => { - this.target = c; - } - - findTarget = () => { - return this.target; - } - - render () { - const { intl, onPickEmoji, onSkinTone, skinTone, frequentlyUsedEmojis } = this.props; - const title = intl.formatMessage(messages.emoji); - const { active, loading } = this.state; - - return ( - <div className='emoji-picker-dropdown' onKeyDown={this.handleKeyDown}> - <div ref={this.setTargetRef} className='emoji-button' title={title} aria-label={title} aria-expanded={active} role='button' onClick={this.onToggle} onKeyDown={this.onToggle} tabIndex={0}> - <img - className={classNames('emojione', { 'pulse-loading': active && loading })} - alt='🙂' - src={`${assetHost}/emoji/1f602.svg`} - /> - </div> - - <Overlay show={active} placement='bottom' target={this.findTarget}> - <EmojiPickerMenu - custom_emojis={this.props.custom_emojis} - loading={loading} - onClose={this.onHideDropdown} - onPick={onPickEmoji} - onSkinTone={onSkinTone} - skinTone={skinTone} - frequentlyUsedEmojis={frequentlyUsedEmojis} - /> - </Overlay> - </div> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/compose/components/navigation_bar.js b/app/javascript/themes/glitch/features/compose/components/navigation_bar.js deleted file mode 100644 index 24a70949b..000000000 --- a/app/javascript/themes/glitch/features/compose/components/navigation_bar.js +++ /dev/null @@ -1,38 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import Avatar from 'themes/glitch/components/avatar'; -import IconButton from 'themes/glitch/components/icon_button'; -import Permalink from 'themes/glitch/components/permalink'; -import { FormattedMessage } from 'react-intl'; -import ImmutablePureComponent from 'react-immutable-pure-component'; - -export default class NavigationBar extends ImmutablePureComponent { - - static propTypes = { - account: ImmutablePropTypes.map.isRequired, - onClose: PropTypes.func.isRequired, - }; - - render () { - return ( - <div className='navigation-bar'> - <Permalink href={this.props.account.get('url')} to={`/accounts/${this.props.account.get('id')}`}> - <span style={{ display: 'none' }}>{this.props.account.get('acct')}</span> - <Avatar account={this.props.account} size={40} /> - </Permalink> - - <div className='navigation-bar__profile'> - <Permalink href={this.props.account.get('url')} to={`/accounts/${this.props.account.get('id')}`}> - <strong className='navigation-bar__profile-account'>@{this.props.account.get('acct')}</strong> - </Permalink> - - <a href='/settings/profile' className='navigation-bar__profile-edit'><FormattedMessage id='navigation_bar.edit_profile' defaultMessage='Edit profile' /></a> - </div> - - <IconButton title='' icon='close' onClick={this.props.onClose} /> - </div> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/compose/components/privacy_dropdown.js b/app/javascript/themes/glitch/features/compose/components/privacy_dropdown.js deleted file mode 100644 index 0cd92d174..000000000 --- a/app/javascript/themes/glitch/features/compose/components/privacy_dropdown.js +++ /dev/null @@ -1,200 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { injectIntl, defineMessages } from 'react-intl'; -import IconButton from 'themes/glitch/components/icon_button'; -import Overlay from 'react-overlays/lib/Overlay'; -import Motion from 'themes/glitch/util/optional_motion'; -import spring from 'react-motion/lib/spring'; -import detectPassiveEvents from 'detect-passive-events'; -import classNames from 'classnames'; - -const messages = defineMessages({ - public_short: { id: 'privacy.public.short', defaultMessage: 'Public' }, - public_long: { id: 'privacy.public.long', defaultMessage: 'Post to public timelines' }, - unlisted_short: { id: 'privacy.unlisted.short', defaultMessage: 'Unlisted' }, - unlisted_long: { id: 'privacy.unlisted.long', defaultMessage: 'Do not show in public timelines' }, - private_short: { id: 'privacy.private.short', defaultMessage: 'Followers-only' }, - private_long: { id: 'privacy.private.long', defaultMessage: 'Post to followers only' }, - direct_short: { id: 'privacy.direct.short', defaultMessage: 'Direct' }, - direct_long: { id: 'privacy.direct.long', defaultMessage: 'Post to mentioned users only' }, - change_privacy: { id: 'privacy.change', defaultMessage: 'Adjust status privacy' }, -}); - -const listenerOptions = detectPassiveEvents.hasSupport ? { passive: true } : false; - -class PrivacyDropdownMenu extends React.PureComponent { - - static propTypes = { - style: PropTypes.object, - items: PropTypes.array.isRequired, - value: PropTypes.string.isRequired, - onClose: PropTypes.func.isRequired, - onChange: PropTypes.func.isRequired, - }; - - handleDocumentClick = e => { - if (this.node && !this.node.contains(e.target)) { - this.props.onClose(); - } - } - - handleClick = e => { - if (e.key === 'Escape') { - this.props.onClose(); - } else if (!e.key || e.key === 'Enter') { - const value = e.currentTarget.getAttribute('data-index'); - - e.preventDefault(); - - this.props.onClose(); - this.props.onChange(value); - } - } - - componentDidMount () { - document.addEventListener('click', this.handleDocumentClick, false); - document.addEventListener('touchend', this.handleDocumentClick, listenerOptions); - } - - componentWillUnmount () { - document.removeEventListener('click', this.handleDocumentClick, false); - document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions); - } - - setRef = c => { - this.node = c; - } - - render () { - const { style, items, value } = this.props; - - return ( - <Motion defaultStyle={{ opacity: 0, scaleX: 0.85, scaleY: 0.75 }} style={{ opacity: spring(1, { damping: 35, stiffness: 400 }), scaleX: spring(1, { damping: 35, stiffness: 400 }), scaleY: spring(1, { damping: 35, stiffness: 400 }) }}> - {({ opacity, scaleX, scaleY }) => ( - <div className='privacy-dropdown__dropdown' style={{ ...style, opacity: opacity, transform: `scale(${scaleX}, ${scaleY})` }} ref={this.setRef}> - {items.map(item => - <div role='button' tabIndex='0' key={item.value} data-index={item.value} onKeyDown={this.handleClick} onClick={this.handleClick} className={classNames('privacy-dropdown__option', { active: item.value === value })}> - <div className='privacy-dropdown__option__icon'> - <i className={`fa fa-fw fa-${item.icon}`} /> - </div> - - <div className='privacy-dropdown__option__content'> - <strong>{item.text}</strong> - {item.meta} - </div> - </div> - )} - </div> - )} - </Motion> - ); - } - -} - -@injectIntl -export default class PrivacyDropdown extends React.PureComponent { - - static propTypes = { - isUserTouching: PropTypes.func, - isModalOpen: PropTypes.bool.isRequired, - onModalOpen: PropTypes.func, - onModalClose: PropTypes.func, - value: PropTypes.string.isRequired, - onChange: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - }; - - state = { - open: false, - }; - - handleToggle = () => { - if (this.props.isUserTouching()) { - if (this.state.open) { - this.props.onModalClose(); - } else { - this.props.onModalOpen({ - actions: this.options.map(option => ({ ...option, active: option.value === this.props.value })), - onClick: this.handleModalActionClick, - }); - } - } else { - this.setState({ open: !this.state.open }); - } - } - - handleModalActionClick = (e) => { - e.preventDefault(); - - const { value } = this.options[e.currentTarget.getAttribute('data-index')]; - - this.props.onModalClose(); - this.props.onChange(value); - } - - handleKeyDown = e => { - switch(e.key) { - case 'Enter': - this.handleToggle(); - break; - case 'Escape': - this.handleClose(); - break; - } - } - - handleClose = () => { - this.setState({ open: false }); - } - - handleChange = value => { - this.props.onChange(value); - } - - componentWillMount () { - const { intl: { formatMessage } } = this.props; - - this.options = [ - { icon: 'globe', value: 'public', text: formatMessage(messages.public_short), meta: formatMessage(messages.public_long) }, - { icon: 'unlock-alt', value: 'unlisted', text: formatMessage(messages.unlisted_short), meta: formatMessage(messages.unlisted_long) }, - { icon: 'lock', value: 'private', text: formatMessage(messages.private_short), meta: formatMessage(messages.private_long) }, - { icon: 'envelope', value: 'direct', text: formatMessage(messages.direct_short), meta: formatMessage(messages.direct_long) }, - ]; - } - - render () { - const { value, intl } = this.props; - const { open } = this.state; - - const valueOption = this.options.find(item => item.value === value); - - return ( - <div className={classNames('privacy-dropdown', { active: open })} onKeyDown={this.handleKeyDown}> - <div className={classNames('privacy-dropdown__value', { active: this.options.indexOf(valueOption) === 0 })}> - <IconButton - className='privacy-dropdown__value-icon' - icon={valueOption.icon} - title={intl.formatMessage(messages.change_privacy)} - size={18} - expanded={open} - active={open} - inverted - onClick={this.handleToggle} - style={{ height: null, lineHeight: '27px' }} - /> - </div> - - <Overlay show={open} placement='bottom' target={this}> - <PrivacyDropdownMenu - items={this.options} - value={value} - onClose={this.handleClose} - onChange={this.handleChange} - /> - </Overlay> - </div> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/compose/components/reply_indicator.js b/app/javascript/themes/glitch/features/compose/components/reply_indicator.js deleted file mode 100644 index 9a8d10ceb..000000000 --- a/app/javascript/themes/glitch/features/compose/components/reply_indicator.js +++ /dev/null @@ -1,63 +0,0 @@ -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import PropTypes from 'prop-types'; -import Avatar from 'themes/glitch/components/avatar'; -import IconButton from 'themes/glitch/components/icon_button'; -import DisplayName from 'themes/glitch/components/display_name'; -import { defineMessages, injectIntl } from 'react-intl'; -import ImmutablePureComponent from 'react-immutable-pure-component'; - -const messages = defineMessages({ - cancel: { id: 'reply_indicator.cancel', defaultMessage: 'Cancel' }, -}); - -@injectIntl -export default class ReplyIndicator extends ImmutablePureComponent { - - static contextTypes = { - router: PropTypes.object, - }; - - static propTypes = { - status: ImmutablePropTypes.map, - onCancel: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - }; - - handleClick = () => { - this.props.onCancel(); - } - - handleAccountClick = (e) => { - if (e.button === 0) { - e.preventDefault(); - this.context.router.history.push(`/accounts/${this.props.status.getIn(['account', 'id'])}`); - } - } - - render () { - const { status, intl } = this.props; - - if (!status) { - return null; - } - - const content = { __html: status.get('contentHtml') }; - - return ( - <div className='reply-indicator'> - <div className='reply-indicator__header'> - <div className='reply-indicator__cancel'><IconButton title={intl.formatMessage(messages.cancel)} icon='times' onClick={this.handleClick} /></div> - - <a href={status.getIn(['account', 'url'])} onClick={this.handleAccountClick} className='reply-indicator__display-name'> - <div className='reply-indicator__display-avatar'><Avatar account={status.get('account')} size={24} /></div> - <DisplayName account={status.get('account')} /> - </a> - </div> - - <div className='reply-indicator__content' dangerouslySetInnerHTML={content} /> - </div> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/compose/components/search.js b/app/javascript/themes/glitch/features/compose/components/search.js deleted file mode 100644 index c3218137f..000000000 --- a/app/javascript/themes/glitch/features/compose/components/search.js +++ /dev/null @@ -1,129 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; -import Overlay from 'react-overlays/lib/Overlay'; -import Motion from 'themes/glitch/util/optional_motion'; -import spring from 'react-motion/lib/spring'; - -const messages = defineMessages({ - placeholder: { id: 'search.placeholder', defaultMessage: 'Search' }, -}); - -class SearchPopout extends React.PureComponent { - - static propTypes = { - style: PropTypes.object, - }; - - render () { - const { style } = this.props; - - return ( - <div style={{ ...style, position: 'absolute', width: 285 }}> - <Motion defaultStyle={{ opacity: 0, scaleX: 0.85, scaleY: 0.75 }} style={{ opacity: spring(1, { damping: 35, stiffness: 400 }), scaleX: spring(1, { damping: 35, stiffness: 400 }), scaleY: spring(1, { damping: 35, stiffness: 400 }) }}> - {({ opacity, scaleX, scaleY }) => ( - <div className='search-popout' style={{ opacity: opacity, transform: `scale(${scaleX}, ${scaleY})` }}> - <h4><FormattedMessage id='search_popout.search_format' defaultMessage='Advanced search format' /></h4> - - <ul> - <li><em>#example</em> <FormattedMessage id='search_popout.tips.hashtag' defaultMessage='hashtag' /></li> - <li><em>@username@domain</em> <FormattedMessage id='search_popout.tips.user' defaultMessage='user' /></li> - <li><em>URL</em> <FormattedMessage id='search_popout.tips.user' defaultMessage='user' /></li> - <li><em>URL</em> <FormattedMessage id='search_popout.tips.status' defaultMessage='status' /></li> - </ul> - - <FormattedMessage id='search_popout.tips.text' defaultMessage='Simple text returns matching display names, usernames and hashtags' /> - </div> - )} - </Motion> - </div> - ); - } - -} - -@injectIntl -export default class Search extends React.PureComponent { - - static propTypes = { - value: PropTypes.string.isRequired, - submitted: PropTypes.bool, - onChange: PropTypes.func.isRequired, - onSubmit: PropTypes.func.isRequired, - onClear: PropTypes.func.isRequired, - onShow: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - }; - - state = { - expanded: false, - }; - - handleChange = (e) => { - this.props.onChange(e.target.value); - } - - handleClear = (e) => { - e.preventDefault(); - - if (this.props.value.length > 0 || this.props.submitted) { - this.props.onClear(); - } - } - - handleKeyDown = (e) => { - if (e.key === 'Enter') { - e.preventDefault(); - this.props.onSubmit(); - } else if (e.key === 'Escape') { - document.querySelector('.ui').parentElement.focus(); - } - } - - noop () { - - } - - handleFocus = () => { - this.setState({ expanded: true }); - this.props.onShow(); - } - - handleBlur = () => { - this.setState({ expanded: false }); - } - - render () { - const { intl, value, submitted } = this.props; - const { expanded } = this.state; - const hasValue = value.length > 0 || submitted; - - return ( - <div className='search'> - <label> - <span style={{ display: 'none' }}>{intl.formatMessage(messages.placeholder)}</span> - <input - className='search__input' - type='text' - placeholder={intl.formatMessage(messages.placeholder)} - value={value} - onChange={this.handleChange} - onKeyUp={this.handleKeyDown} - onFocus={this.handleFocus} - onBlur={this.handleBlur} - /> - </label> - - <div role='button' tabIndex='0' className='search__icon' onClick={this.handleClear}> - <i className={`fa fa-search ${hasValue ? '' : 'active'}`} /> - <i aria-label={intl.formatMessage(messages.placeholder)} className={`fa fa-times-circle ${hasValue ? 'active' : ''}`} /> - </div> - - <Overlay show={expanded && !hasValue} placement='bottom' target={this}> - <SearchPopout /> - </Overlay> - </div> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/compose/components/search_results.js b/app/javascript/themes/glitch/features/compose/components/search_results.js deleted file mode 100644 index 3fdafa5f3..000000000 --- a/app/javascript/themes/glitch/features/compose/components/search_results.js +++ /dev/null @@ -1,65 +0,0 @@ -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import { FormattedMessage } from 'react-intl'; -import AccountContainer from 'themes/glitch/containers/account_container'; -import StatusContainer from 'themes/glitch/containers/status_container'; -import { Link } from 'react-router-dom'; -import ImmutablePureComponent from 'react-immutable-pure-component'; - -export default class SearchResults extends ImmutablePureComponent { - - static propTypes = { - results: ImmutablePropTypes.map.isRequired, - }; - - render () { - const { results } = this.props; - - let accounts, statuses, hashtags; - let count = 0; - - if (results.get('accounts') && results.get('accounts').size > 0) { - count += results.get('accounts').size; - accounts = ( - <div className='search-results__section'> - {results.get('accounts').map(accountId => <AccountContainer key={accountId} id={accountId} />)} - </div> - ); - } - - if (results.get('statuses') && results.get('statuses').size > 0) { - count += results.get('statuses').size; - statuses = ( - <div className='search-results__section'> - {results.get('statuses').map(statusId => <StatusContainer key={statusId} id={statusId} />)} - </div> - ); - } - - if (results.get('hashtags') && results.get('hashtags').size > 0) { - count += results.get('hashtags').size; - hashtags = ( - <div className='search-results__section'> - {results.get('hashtags').map(hashtag => - <Link key={hashtag} className='search-results__hashtag' to={`/timelines/tag/${hashtag}`}> - #{hashtag} - </Link> - )} - </div> - ); - } - - return ( - <div className='search-results'> - <div className='search-results__header'> - <FormattedMessage id='search_results.total' defaultMessage='{count, number} {count, plural, one {result} other {results}}' values={{ count }} /> - </div> - - {accounts} - {statuses} - {hashtags} - </div> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/compose/components/text_icon_button.js b/app/javascript/themes/glitch/features/compose/components/text_icon_button.js deleted file mode 100644 index 9c8ffab1f..000000000 --- a/app/javascript/themes/glitch/features/compose/components/text_icon_button.js +++ /dev/null @@ -1,29 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; - -export default class TextIconButton extends React.PureComponent { - - static propTypes = { - label: PropTypes.string.isRequired, - title: PropTypes.string, - active: PropTypes.bool, - onClick: PropTypes.func.isRequired, - ariaControls: PropTypes.string, - }; - - handleClick = (e) => { - e.preventDefault(); - this.props.onClick(); - } - - render () { - const { label, title, active, ariaControls } = this.props; - - return ( - <button title={title} aria-label={title} className={`text-icon-button ${active ? 'active' : ''}`} aria-expanded={active} onClick={this.handleClick} aria-controls={ariaControls}> - {label} - </button> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/compose/components/upload.js b/app/javascript/themes/glitch/features/compose/components/upload.js deleted file mode 100644 index ded376ada..000000000 --- a/app/javascript/themes/glitch/features/compose/components/upload.js +++ /dev/null @@ -1,96 +0,0 @@ -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import PropTypes from 'prop-types'; -import IconButton from 'themes/glitch/components/icon_button'; -import Motion from 'themes/glitch/util/optional_motion'; -import spring from 'react-motion/lib/spring'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import { defineMessages, injectIntl } from 'react-intl'; -import classNames from 'classnames'; - -const messages = defineMessages({ - undo: { id: 'upload_form.undo', defaultMessage: 'Undo' }, - description: { id: 'upload_form.description', defaultMessage: 'Describe for the visually impaired' }, -}); - -@injectIntl -export default class Upload extends ImmutablePureComponent { - - static propTypes = { - media: ImmutablePropTypes.map.isRequired, - intl: PropTypes.object.isRequired, - onUndo: PropTypes.func.isRequired, - onDescriptionChange: PropTypes.func.isRequired, - }; - - state = { - hovered: false, - focused: false, - dirtyDescription: null, - }; - - handleUndoClick = () => { - this.props.onUndo(this.props.media.get('id')); - } - - handleInputChange = e => { - this.setState({ dirtyDescription: e.target.value }); - } - - handleMouseEnter = () => { - this.setState({ hovered: true }); - } - - handleMouseLeave = () => { - this.setState({ hovered: false }); - } - - handleInputFocus = () => { - this.setState({ focused: true }); - } - - handleInputBlur = () => { - const { dirtyDescription } = this.state; - - this.setState({ focused: false, dirtyDescription: null }); - - if (dirtyDescription !== null) { - this.props.onDescriptionChange(this.props.media.get('id'), dirtyDescription); - } - } - - render () { - const { intl, media } = this.props; - const active = this.state.hovered || this.state.focused; - const description = this.state.dirtyDescription || media.get('description') || ''; - - return ( - <div className='compose-form__upload' onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}> - <Motion defaultStyle={{ scale: 0.8 }} style={{ scale: spring(1, { stiffness: 180, damping: 12 }) }}> - {({ scale }) => ( - <div className='compose-form__upload-thumbnail' style={{ transform: `scale(${scale})`, backgroundImage: `url(${media.get('preview_url')})` }}> - <IconButton icon='times' title={intl.formatMessage(messages.undo)} size={36} onClick={this.handleUndoClick} /> - - <div className={classNames('compose-form__upload-description', { active })}> - <label> - <span style={{ display: 'none' }}>{intl.formatMessage(messages.description)}</span> - - <input - placeholder={intl.formatMessage(messages.description)} - type='text' - value={description} - maxLength={420} - onFocus={this.handleInputFocus} - onChange={this.handleInputChange} - onBlur={this.handleInputBlur} - /> - </label> - </div> - </div> - )} - </Motion> - </div> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/compose/components/upload_button.js b/app/javascript/themes/glitch/features/compose/components/upload_button.js deleted file mode 100644 index d7742adfe..000000000 --- a/app/javascript/themes/glitch/features/compose/components/upload_button.js +++ /dev/null @@ -1,77 +0,0 @@ -import React from 'react'; -import IconButton from 'themes/glitch/components/icon_button'; -import PropTypes from 'prop-types'; -import { defineMessages, injectIntl } from 'react-intl'; -import { connect } from 'react-redux'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import ImmutablePropTypes from 'react-immutable-proptypes'; - -const messages = defineMessages({ - upload: { id: 'upload_button.label', defaultMessage: 'Add media' }, -}); - -const makeMapStateToProps = () => { - const mapStateToProps = state => ({ - acceptContentTypes: state.getIn(['media_attachments', 'accept_content_types']), - }); - - return mapStateToProps; -}; - -const iconStyle = { - height: null, - lineHeight: '27px', -}; - -@connect(makeMapStateToProps) -@injectIntl -export default class UploadButton extends ImmutablePureComponent { - - static propTypes = { - disabled: PropTypes.bool, - onSelectFile: PropTypes.func.isRequired, - style: PropTypes.object, - resetFileKey: PropTypes.number, - acceptContentTypes: ImmutablePropTypes.listOf(PropTypes.string).isRequired, - intl: PropTypes.object.isRequired, - }; - - handleChange = (e) => { - if (e.target.files.length > 0) { - this.props.onSelectFile(e.target.files); - } - } - - handleClick = () => { - this.fileElement.click(); - } - - setRef = (c) => { - this.fileElement = c; - } - - render () { - - const { intl, resetFileKey, disabled, acceptContentTypes } = this.props; - - return ( - <div className='compose-form__upload-button'> - <IconButton icon='camera' title={intl.formatMessage(messages.upload)} disabled={disabled} onClick={this.handleClick} className='compose-form__upload-button-icon' size={18} inverted style={iconStyle} /> - <label> - <span style={{ display: 'none' }}>{intl.formatMessage(messages.upload)}</span> - <input - key={resetFileKey} - ref={this.setRef} - type='file' - multiple={false} - accept={acceptContentTypes.toArray().join(',')} - onChange={this.handleChange} - disabled={disabled} - style={{ display: 'none' }} - /> - </label> - </div> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/compose/components/upload_form.js b/app/javascript/themes/glitch/features/compose/components/upload_form.js deleted file mode 100644 index b7f112205..000000000 --- a/app/javascript/themes/glitch/features/compose/components/upload_form.js +++ /dev/null @@ -1,29 +0,0 @@ -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import UploadProgressContainer from '../containers/upload_progress_container'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import UploadContainer from '../containers/upload_container'; - -export default class UploadForm extends ImmutablePureComponent { - - static propTypes = { - mediaIds: ImmutablePropTypes.list.isRequired, - }; - - render () { - const { mediaIds } = this.props; - - return ( - <div className='compose-form__upload-wrapper'> - <UploadProgressContainer /> - - <div className='compose-form__uploads-wrapper'> - {mediaIds.map(id => ( - <UploadContainer id={id} key={id} /> - ))} - </div> - </div> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/compose/components/upload_progress.js b/app/javascript/themes/glitch/features/compose/components/upload_progress.js deleted file mode 100644 index b923d0a22..000000000 --- a/app/javascript/themes/glitch/features/compose/components/upload_progress.js +++ /dev/null @@ -1,42 +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 UploadProgress extends React.PureComponent { - - static propTypes = { - active: PropTypes.bool, - progress: PropTypes.number, - }; - - render () { - const { active, progress } = this.props; - - if (!active) { - return null; - } - - return ( - <div className='upload-progress'> - <div className='upload-progress__icon'> - <i className='fa fa-upload' /> - </div> - - <div className='upload-progress__message'> - <FormattedMessage id='upload_progress.label' defaultMessage='Uploading...' /> - - <div className='upload-progress__backdrop'> - <Motion defaultStyle={{ width: 0 }} style={{ width: spring(progress) }}> - {({ width }) => - <div className='upload-progress__tracker' style={{ width: `${width}%` }} /> - } - </Motion> - </div> - </div> - </div> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/compose/components/warning.js b/app/javascript/themes/glitch/features/compose/components/warning.js deleted file mode 100644 index 82df55a31..000000000 --- a/app/javascript/themes/glitch/features/compose/components/warning.js +++ /dev/null @@ -1,26 +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'; - -export default class Warning extends React.PureComponent { - - static propTypes = { - message: PropTypes.node.isRequired, - }; - - render () { - const { message } = this.props; - - return ( - <Motion defaultStyle={{ opacity: 0, scaleX: 0.85, scaleY: 0.75 }} style={{ opacity: spring(1, { damping: 35, stiffness: 400 }), scaleX: spring(1, { damping: 35, stiffness: 400 }), scaleY: spring(1, { damping: 35, stiffness: 400 }) }}> - {({ opacity, scaleX, scaleY }) => ( - <div className='compose-form__warning' style={{ opacity: opacity, transform: `scale(${scaleX}, ${scaleY})` }}> - {message} - </div> - )} - </Motion> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/compose/containers/advanced_options_container.js b/app/javascript/themes/glitch/features/compose/containers/advanced_options_container.js deleted file mode 100644 index 9f168942a..000000000 --- a/app/javascript/themes/glitch/features/compose/containers/advanced_options_container.js +++ /dev/null @@ -1,20 +0,0 @@ -// Package imports. -import { connect } from 'react-redux'; - -// Our imports. -import { toggleComposeAdvancedOption } from 'themes/glitch/actions/compose'; -import ComposeAdvancedOptions from '../components/advanced_options'; - -const mapStateToProps = state => ({ - values: state.getIn(['compose', 'advanced_options']), -}); - -const mapDispatchToProps = dispatch => ({ - - onChange (option) { - dispatch(toggleComposeAdvancedOption(option)); - }, - -}); - -export default connect(mapStateToProps, mapDispatchToProps)(ComposeAdvancedOptions); diff --git a/app/javascript/themes/glitch/features/compose/containers/autosuggest_account_container.js b/app/javascript/themes/glitch/features/compose/containers/autosuggest_account_container.js deleted file mode 100644 index 96eb70c18..000000000 --- a/app/javascript/themes/glitch/features/compose/containers/autosuggest_account_container.js +++ /dev/null @@ -1,15 +0,0 @@ -import { connect } from 'react-redux'; -import AutosuggestAccount from '../components/autosuggest_account'; -import { makeGetAccount } from 'themes/glitch/selectors'; - -const makeMapStateToProps = () => { - const getAccount = makeGetAccount(); - - const mapStateToProps = (state, { id }) => ({ - account: getAccount(state, id), - }); - - return mapStateToProps; -}; - -export default connect(makeMapStateToProps)(AutosuggestAccount); diff --git a/app/javascript/themes/glitch/features/compose/containers/compose_form_container.js b/app/javascript/themes/glitch/features/compose/containers/compose_form_container.js deleted file mode 100644 index 7afa988f1..000000000 --- a/app/javascript/themes/glitch/features/compose/containers/compose_form_container.js +++ /dev/null @@ -1,71 +0,0 @@ -import { connect } from 'react-redux'; -import ComposeForm from '../components/compose_form'; -import { changeComposeVisibility, uploadCompose } from 'themes/glitch/actions/compose'; -import { - changeCompose, - submitCompose, - clearComposeSuggestions, - fetchComposeSuggestions, - selectComposeSuggestion, - changeComposeSpoilerText, - insertEmojiCompose, -} from 'themes/glitch/actions/compose'; - -const mapStateToProps = state => ({ - text: state.getIn(['compose', 'text']), - suggestion_token: state.getIn(['compose', 'suggestion_token']), - suggestions: state.getIn(['compose', 'suggestions']), - advanced_options: state.getIn(['compose', 'advanced_options']), - spoiler: state.getIn(['compose', 'spoiler']), - spoiler_text: state.getIn(['compose', 'spoiler_text']), - privacy: state.getIn(['compose', 'privacy']), - focusDate: state.getIn(['compose', 'focusDate']), - preselectDate: state.getIn(['compose', 'preselectDate']), - is_submitting: state.getIn(['compose', 'is_submitting']), - is_uploading: state.getIn(['compose', 'is_uploading']), - showSearch: state.getIn(['search', 'submitted']) && !state.getIn(['search', 'hidden']), - settings: state.get('local_settings'), - filesAttached: state.getIn(['compose', 'media_attachments']).size > 0, -}); - -const mapDispatchToProps = (dispatch) => ({ - - onChange (text) { - dispatch(changeCompose(text)); - }, - - onPrivacyChange (value) { - dispatch(changeComposeVisibility(value)); - }, - - onSubmit () { - dispatch(submitCompose()); - }, - - onClearSuggestions () { - dispatch(clearComposeSuggestions()); - }, - - onFetchSuggestions (token) { - dispatch(fetchComposeSuggestions(token)); - }, - - onSuggestionSelected (position, token, accountId) { - dispatch(selectComposeSuggestion(position, token, accountId)); - }, - - onChangeSpoilerText (checked) { - dispatch(changeComposeSpoilerText(checked)); - }, - - onPaste (files) { - dispatch(uploadCompose(files)); - }, - - onPickEmoji (position, data) { - dispatch(insertEmojiCompose(position, data)); - }, - -}); - -export default connect(mapStateToProps, mapDispatchToProps)(ComposeForm); diff --git a/app/javascript/themes/glitch/features/compose/containers/emoji_picker_dropdown_container.js b/app/javascript/themes/glitch/features/compose/containers/emoji_picker_dropdown_container.js deleted file mode 100644 index 55a13bd65..000000000 --- a/app/javascript/themes/glitch/features/compose/containers/emoji_picker_dropdown_container.js +++ /dev/null @@ -1,82 +0,0 @@ -import { connect } from 'react-redux'; -import EmojiPickerDropdown from '../components/emoji_picker_dropdown'; -import { changeSetting } from 'themes/glitch/actions/settings'; -import { createSelector } from 'reselect'; -import { Map as ImmutableMap } from 'immutable'; -import { useEmoji } from 'themes/glitch/actions/emojis'; - -const perLine = 8; -const lines = 2; - -const DEFAULTS = [ - '+1', - 'grinning', - 'kissing_heart', - 'heart_eyes', - 'laughing', - 'stuck_out_tongue_winking_eye', - 'sweat_smile', - 'joy', - 'yum', - 'disappointed', - 'thinking_face', - 'weary', - 'sob', - 'sunglasses', - 'heart', - 'ok_hand', -]; - -const getFrequentlyUsedEmojis = createSelector([ - state => state.getIn(['settings', 'frequentlyUsedEmojis'], ImmutableMap()), -], emojiCounters => { - let emojis = emojiCounters - .keySeq() - .sort((a, b) => emojiCounters.get(a) - emojiCounters.get(b)) - .reverse() - .slice(0, perLine * lines) - .toArray(); - - if (emojis.length < DEFAULTS.length) { - emojis = emojis.concat(DEFAULTS.slice(0, DEFAULTS.length - emojis.length)); - } - - return emojis; -}); - -const getCustomEmojis = createSelector([ - state => state.get('custom_emojis'), -], emojis => emojis.filter(e => e.get('visible_in_picker')).sort((a, b) => { - const aShort = a.get('shortcode').toLowerCase(); - const bShort = b.get('shortcode').toLowerCase(); - - if (aShort < bShort) { - return -1; - } else if (aShort > bShort ) { - return 1; - } else { - return 0; - } -})); - -const mapStateToProps = state => ({ - custom_emojis: getCustomEmojis(state), - skinTone: state.getIn(['settings', 'skinTone']), - frequentlyUsedEmojis: getFrequentlyUsedEmojis(state), -}); - -const mapDispatchToProps = (dispatch, { onPickEmoji }) => ({ - onSkinTone: skinTone => { - dispatch(changeSetting(['skinTone'], skinTone)); - }, - - onPickEmoji: emoji => { - dispatch(useEmoji(emoji)); - - if (onPickEmoji) { - onPickEmoji(emoji); - } - }, -}); - -export default connect(mapStateToProps, mapDispatchToProps)(EmojiPickerDropdown); diff --git a/app/javascript/themes/glitch/features/compose/containers/navigation_container.js b/app/javascript/themes/glitch/features/compose/containers/navigation_container.js deleted file mode 100644 index b6d737b46..000000000 --- a/app/javascript/themes/glitch/features/compose/containers/navigation_container.js +++ /dev/null @@ -1,11 +0,0 @@ -import { connect } from 'react-redux'; -import NavigationBar from '../components/navigation_bar'; -import { me } from 'themes/glitch/util/initial_state'; - -const mapStateToProps = state => { - return { - account: state.getIn(['accounts', me]), - }; -}; - -export default connect(mapStateToProps)(NavigationBar); diff --git a/app/javascript/themes/glitch/features/compose/containers/privacy_dropdown_container.js b/app/javascript/themes/glitch/features/compose/containers/privacy_dropdown_container.js deleted file mode 100644 index 9636ceab2..000000000 --- a/app/javascript/themes/glitch/features/compose/containers/privacy_dropdown_container.js +++ /dev/null @@ -1,24 +0,0 @@ -import { connect } from 'react-redux'; -import PrivacyDropdown from '../components/privacy_dropdown'; -import { changeComposeVisibility } from 'themes/glitch/actions/compose'; -import { openModal, closeModal } from 'themes/glitch/actions/modal'; -import { isUserTouching } from 'themes/glitch/util/is_mobile'; - -const mapStateToProps = state => ({ - isModalOpen: state.get('modal').modalType === 'ACTIONS', - value: state.getIn(['compose', 'privacy']), -}); - -const mapDispatchToProps = dispatch => ({ - - onChange (value) { - dispatch(changeComposeVisibility(value)); - }, - - isUserTouching, - onModalOpen: props => dispatch(openModal('ACTIONS', props)), - onModalClose: () => dispatch(closeModal()), - -}); - -export default connect(mapStateToProps, mapDispatchToProps)(PrivacyDropdown); diff --git a/app/javascript/themes/glitch/features/compose/containers/reply_indicator_container.js b/app/javascript/themes/glitch/features/compose/containers/reply_indicator_container.js deleted file mode 100644 index 6dcabb3cd..000000000 --- a/app/javascript/themes/glitch/features/compose/containers/reply_indicator_container.js +++ /dev/null @@ -1,24 +0,0 @@ -import { connect } from 'react-redux'; -import { cancelReplyCompose } from 'themes/glitch/actions/compose'; -import { makeGetStatus } from 'themes/glitch/selectors'; -import ReplyIndicator from '../components/reply_indicator'; - -const makeMapStateToProps = () => { - const getStatus = makeGetStatus(); - - const mapStateToProps = state => ({ - status: getStatus(state, state.getIn(['compose', 'in_reply_to'])), - }); - - return mapStateToProps; -}; - -const mapDispatchToProps = dispatch => ({ - - onCancel () { - dispatch(cancelReplyCompose()); - }, - -}); - -export default connect(makeMapStateToProps, mapDispatchToProps)(ReplyIndicator); diff --git a/app/javascript/themes/glitch/features/compose/containers/search_container.js b/app/javascript/themes/glitch/features/compose/containers/search_container.js deleted file mode 100644 index a450d27e7..000000000 --- a/app/javascript/themes/glitch/features/compose/containers/search_container.js +++ /dev/null @@ -1,35 +0,0 @@ -import { connect } from 'react-redux'; -import { - changeSearch, - clearSearch, - submitSearch, - showSearch, -} from 'themes/glitch/actions/search'; -import Search from '../components/search'; - -const mapStateToProps = state => ({ - value: state.getIn(['search', 'value']), - submitted: state.getIn(['search', 'submitted']), -}); - -const mapDispatchToProps = dispatch => ({ - - onChange (value) { - dispatch(changeSearch(value)); - }, - - onClear () { - dispatch(clearSearch()); - }, - - onSubmit () { - dispatch(submitSearch()); - }, - - onShow () { - dispatch(showSearch()); - }, - -}); - -export default connect(mapStateToProps, mapDispatchToProps)(Search); diff --git a/app/javascript/themes/glitch/features/compose/containers/search_results_container.js b/app/javascript/themes/glitch/features/compose/containers/search_results_container.js deleted file mode 100644 index 16d95d417..000000000 --- a/app/javascript/themes/glitch/features/compose/containers/search_results_container.js +++ /dev/null @@ -1,8 +0,0 @@ -import { connect } from 'react-redux'; -import SearchResults from '../components/search_results'; - -const mapStateToProps = state => ({ - results: state.getIn(['search', 'results']), -}); - -export default connect(mapStateToProps)(SearchResults); diff --git a/app/javascript/themes/glitch/features/compose/containers/sensitive_button_container.js b/app/javascript/themes/glitch/features/compose/containers/sensitive_button_container.js deleted file mode 100644 index a710dd104..000000000 --- a/app/javascript/themes/glitch/features/compose/containers/sensitive_button_container.js +++ /dev/null @@ -1,71 +0,0 @@ -import React from 'react'; -import { connect } from 'react-redux'; -import PropTypes from 'prop-types'; -import classNames from 'classnames'; -import IconButton from 'themes/glitch/components/icon_button'; -import { changeComposeSensitivity } from 'themes/glitch/actions/compose'; -import Motion from 'themes/glitch/util/optional_motion'; -import spring from 'react-motion/lib/spring'; -import { injectIntl, defineMessages } from 'react-intl'; - -const messages = defineMessages({ - title: { id: 'compose_form.sensitive', defaultMessage: 'Mark media as sensitive' }, -}); - -const mapStateToProps = state => ({ - visible: state.getIn(['compose', 'media_attachments']).size > 0, - active: state.getIn(['compose', 'sensitive']), - disabled: state.getIn(['compose', 'spoiler']), -}); - -const mapDispatchToProps = dispatch => ({ - - onClick () { - dispatch(changeComposeSensitivity()); - }, - -}); - -class SensitiveButton extends React.PureComponent { - - static propTypes = { - visible: PropTypes.bool, - active: PropTypes.bool, - disabled: PropTypes.bool, - onClick: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - }; - - render () { - const { visible, active, disabled, onClick, intl } = this.props; - - return ( - <Motion defaultStyle={{ scale: 0.87 }} style={{ scale: spring(visible ? 1 : 0.87, { stiffness: 200, damping: 3 }) }}> - {({ scale }) => { - const icon = active ? 'eye-slash' : 'eye'; - const className = classNames('compose-form__sensitive-button', { - 'compose-form__sensitive-button--visible': visible, - }); - return ( - <div className={className} style={{ transform: `scale(${scale})` }}> - <IconButton - className='compose-form__sensitive-button__icon' - title={intl.formatMessage(messages.title)} - icon={icon} - onClick={onClick} - size={18} - active={active} - disabled={disabled} - style={{ lineHeight: null, height: null }} - inverted - /> - </div> - ); - }} - </Motion> - ); - } - -} - -export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(SensitiveButton)); diff --git a/app/javascript/themes/glitch/features/compose/containers/spoiler_button_container.js b/app/javascript/themes/glitch/features/compose/containers/spoiler_button_container.js deleted file mode 100644 index 160e71ba9..000000000 --- a/app/javascript/themes/glitch/features/compose/containers/spoiler_button_container.js +++ /dev/null @@ -1,25 +0,0 @@ -import { connect } from 'react-redux'; -import TextIconButton from '../components/text_icon_button'; -import { changeComposeSpoilerness } from 'themes/glitch/actions/compose'; -import { injectIntl, defineMessages } from 'react-intl'; - -const messages = defineMessages({ - title: { id: 'compose_form.spoiler', defaultMessage: 'Hide text behind warning' }, -}); - -const mapStateToProps = (state, { intl }) => ({ - label: 'CW', - title: intl.formatMessage(messages.title), - active: state.getIn(['compose', 'spoiler']), - ariaControls: 'cw-spoiler-input', -}); - -const mapDispatchToProps = dispatch => ({ - - onClick () { - dispatch(changeComposeSpoilerness()); - }, - -}); - -export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(TextIconButton)); diff --git a/app/javascript/themes/glitch/features/compose/containers/upload_button_container.js b/app/javascript/themes/glitch/features/compose/containers/upload_button_container.js deleted file mode 100644 index f332eae1a..000000000 --- a/app/javascript/themes/glitch/features/compose/containers/upload_button_container.js +++ /dev/null @@ -1,18 +0,0 @@ -import { connect } from 'react-redux'; -import UploadButton from '../components/upload_button'; -import { uploadCompose } from 'themes/glitch/actions/compose'; - -const mapStateToProps = state => ({ - disabled: state.getIn(['compose', 'is_uploading']) || (state.getIn(['compose', 'media_attachments']).size > 3 || state.getIn(['compose', 'media_attachments']).some(m => m.get('type') === 'video')), - resetFileKey: state.getIn(['compose', 'resetFileKey']), -}); - -const mapDispatchToProps = dispatch => ({ - - onSelectFile (files) { - dispatch(uploadCompose(files)); - }, - -}); - -export default connect(mapStateToProps, mapDispatchToProps)(UploadButton); diff --git a/app/javascript/themes/glitch/features/compose/containers/upload_container.js b/app/javascript/themes/glitch/features/compose/containers/upload_container.js deleted file mode 100644 index eea514bf5..000000000 --- a/app/javascript/themes/glitch/features/compose/containers/upload_container.js +++ /dev/null @@ -1,21 +0,0 @@ -import { connect } from 'react-redux'; -import Upload from '../components/upload'; -import { undoUploadCompose, changeUploadCompose } from 'themes/glitch/actions/compose'; - -const mapStateToProps = (state, { id }) => ({ - media: state.getIn(['compose', 'media_attachments']).find(item => item.get('id') === id), -}); - -const mapDispatchToProps = dispatch => ({ - - onUndo: id => { - dispatch(undoUploadCompose(id)); - }, - - onDescriptionChange: (id, description) => { - dispatch(changeUploadCompose(id, description)); - }, - -}); - -export default connect(mapStateToProps, mapDispatchToProps)(Upload); diff --git a/app/javascript/themes/glitch/features/compose/containers/upload_form_container.js b/app/javascript/themes/glitch/features/compose/containers/upload_form_container.js deleted file mode 100644 index a6798bf51..000000000 --- a/app/javascript/themes/glitch/features/compose/containers/upload_form_container.js +++ /dev/null @@ -1,8 +0,0 @@ -import { connect } from 'react-redux'; -import UploadForm from '../components/upload_form'; - -const mapStateToProps = state => ({ - mediaIds: state.getIn(['compose', 'media_attachments']).map(item => item.get('id')), -}); - -export default connect(mapStateToProps)(UploadForm); diff --git a/app/javascript/themes/glitch/features/compose/containers/upload_progress_container.js b/app/javascript/themes/glitch/features/compose/containers/upload_progress_container.js deleted file mode 100644 index 0cfee96da..000000000 --- a/app/javascript/themes/glitch/features/compose/containers/upload_progress_container.js +++ /dev/null @@ -1,9 +0,0 @@ -import { connect } from 'react-redux'; -import UploadProgress from '../components/upload_progress'; - -const mapStateToProps = state => ({ - active: state.getIn(['compose', 'is_uploading']), - progress: state.getIn(['compose', 'progress']), -}); - -export default connect(mapStateToProps)(UploadProgress); diff --git a/app/javascript/themes/glitch/features/compose/containers/warning_container.js b/app/javascript/themes/glitch/features/compose/containers/warning_container.js deleted file mode 100644 index 225d6a1dd..000000000 --- a/app/javascript/themes/glitch/features/compose/containers/warning_container.js +++ /dev/null @@ -1,24 +0,0 @@ -import React from 'react'; -import { connect } from 'react-redux'; -import Warning from '../components/warning'; -import PropTypes from 'prop-types'; -import { FormattedMessage } from 'react-intl'; -import { me } from 'themes/glitch/util/initial_state'; - -const mapStateToProps = state => ({ - needsLockWarning: state.getIn(['compose', 'privacy']) === 'private' && !state.getIn(['accounts', me, 'locked']), -}); - -const WarningWrapper = ({ needsLockWarning }) => { - if (needsLockWarning) { - return <Warning message={<FormattedMessage id='compose_form.lock_disclaimer' defaultMessage='Your account is not {locked}. Anyone can follow you to view your follower-only posts.' values={{ locked: <a href='/settings/profile'><FormattedMessage id='compose_form.lock_disclaimer.lock' defaultMessage='locked' /></a> }} />} />; - } - - return null; -}; - -WarningWrapper.propTypes = { - needsLockWarning: PropTypes.bool, -}; - -export default connect(mapStateToProps)(WarningWrapper); diff --git a/app/javascript/themes/glitch/features/compose/index.js b/app/javascript/themes/glitch/features/compose/index.js deleted file mode 100644 index 3fcaf416f..000000000 --- a/app/javascript/themes/glitch/features/compose/index.js +++ /dev/null @@ -1,126 +0,0 @@ -import React from 'react'; -import ComposeFormContainer from './containers/compose_form_container'; -import NavigationContainer from './containers/navigation_container'; -import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import { connect } from 'react-redux'; -import { mountCompose, unmountCompose } from 'themes/glitch/actions/compose'; -import { openModal } from 'themes/glitch/actions/modal'; -import { changeLocalSetting } from 'themes/glitch/actions/local_settings'; -import { Link } from 'react-router-dom'; -import { injectIntl, defineMessages } from 'react-intl'; -import SearchContainer from './containers/search_container'; -import Motion from 'themes/glitch/util/optional_motion'; -import spring from 'react-motion/lib/spring'; -import SearchResultsContainer from './containers/search_results_container'; -import { changeComposing } from 'themes/glitch/actions/compose'; - -const messages = defineMessages({ - start: { id: 'getting_started.heading', defaultMessage: 'Getting started' }, - home_timeline: { id: 'tabs_bar.home', defaultMessage: 'Home' }, - notifications: { id: 'tabs_bar.notifications', defaultMessage: 'Notifications' }, - public: { id: 'navigation_bar.public_timeline', defaultMessage: 'Federated timeline' }, - community: { id: 'navigation_bar.community_timeline', defaultMessage: 'Local timeline' }, - settings: { id: 'navigation_bar.app_settings', defaultMessage: 'App settings' }, - logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' }, -}); - -const mapStateToProps = state => ({ - columns: state.getIn(['settings', 'columns']), - showSearch: state.getIn(['search', 'submitted']) && !state.getIn(['search', 'hidden']), -}); - -@connect(mapStateToProps) -@injectIntl -export default class Compose extends React.PureComponent { - - static propTypes = { - dispatch: PropTypes.func.isRequired, - columns: ImmutablePropTypes.list.isRequired, - multiColumn: PropTypes.bool, - showSearch: PropTypes.bool, - intl: PropTypes.object.isRequired, - }; - - componentDidMount () { - this.props.dispatch(mountCompose()); - } - - componentWillUnmount () { - this.props.dispatch(unmountCompose()); - } - - onLayoutClick = (e) => { - const layout = e.currentTarget.getAttribute('data-mastodon-layout'); - this.props.dispatch(changeLocalSetting(['layout'], layout)); - e.preventDefault(); - } - - openSettings = () => { - this.props.dispatch(openModal('SETTINGS', {})); - } - - onFocus = () => { - this.props.dispatch(changeComposing(true)); - } - - onBlur = () => { - this.props.dispatch(changeComposing(false)); - } - - render () { - const { multiColumn, showSearch, intl } = this.props; - - let header = ''; - - if (multiColumn) { - const { columns } = this.props; - header = ( - <nav className='drawer__header'> - <Link to='/getting-started' className='drawer__tab' title={intl.formatMessage(messages.start)} aria-label={intl.formatMessage(messages.start)}><i role='img' className='fa fa-fw fa-asterisk' /></Link> - {!columns.some(column => column.get('id') === 'HOME') && ( - <Link to='/timelines/home' className='drawer__tab' title={intl.formatMessage(messages.home_timeline)} aria-label={intl.formatMessage(messages.home_timeline)}><i role='img' className='fa fa-fw fa-home' /></Link> - )} - {!columns.some(column => column.get('id') === 'NOTIFICATIONS') && ( - <Link to='/notifications' className='drawer__tab' title={intl.formatMessage(messages.notifications)} aria-label={intl.formatMessage(messages.notifications)}><i role='img' className='fa fa-fw fa-bell' /></Link> - )} - {!columns.some(column => column.get('id') === 'COMMUNITY') && ( - <Link to='/timelines/public/local' className='drawer__tab' title={intl.formatMessage(messages.community)} aria-label={intl.formatMessage(messages.community)}><i role='img' className='fa fa-fw fa-users' /></Link> - )} - {!columns.some(column => column.get('id') === 'PUBLIC') && ( - <Link to='/timelines/public' className='drawer__tab' title={intl.formatMessage(messages.public)} aria-label={intl.formatMessage(messages.public)}><i role='img' className='fa fa-fw fa-globe' /></Link> - )} - <a onClick={this.openSettings} role='button' tabIndex='0' className='drawer__tab' title={intl.formatMessage(messages.settings)} aria-label={intl.formatMessage(messages.settings)}><i role='img' className='fa fa-fw fa-cogs' /></a> - <a href='/auth/sign_out' className='drawer__tab' data-method='delete' title={intl.formatMessage(messages.logout)} aria-label={intl.formatMessage(messages.logout)}><i role='img' className='fa fa-fw fa-sign-out' /></a> - </nav> - ); - } - - - - return ( - <div className='drawer'> - {header} - - <SearchContainer /> - - <div className='drawer__pager'> - <div className='drawer__inner scrollable optionally-scrollable' onFocus={this.onFocus}> - <NavigationContainer onClose={this.onBlur} /> - <ComposeFormContainer /> - </div> - - <Motion defaultStyle={{ x: -100 }} style={{ x: spring(showSearch ? 0 : -100, { stiffness: 210, damping: 20 }) }}> - {({ x }) => - <div className='drawer__inner darker' style={{ transform: `translateX(${x}%)`, visibility: x === -100 ? 'hidden' : 'visible' }}> - <SearchResultsContainer /> - </div> - } - </Motion> - </div> - - </div> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/direct_timeline/containers/column_settings_container.js b/app/javascript/themes/glitch/features/direct_timeline/containers/column_settings_container.js deleted file mode 100644 index 2a40c65a5..000000000 --- a/app/javascript/themes/glitch/features/direct_timeline/containers/column_settings_container.js +++ /dev/null @@ -1,17 +0,0 @@ -import { connect } from 'react-redux'; -import ColumnSettings from 'themes/glitch/features/community_timeline/components/column_settings'; -import { changeSetting } from 'themes/glitch/actions/settings'; - -const mapStateToProps = state => ({ - settings: state.getIn(['settings', 'direct']), -}); - -const mapDispatchToProps = dispatch => ({ - - onChange (key, checked) { - dispatch(changeSetting(['direct', ...key], checked)); - }, - -}); - -export default connect(mapStateToProps, mapDispatchToProps)(ColumnSettings); diff --git a/app/javascript/themes/glitch/features/direct_timeline/index.js b/app/javascript/themes/glitch/features/direct_timeline/index.js deleted file mode 100644 index 6b29cf94d..000000000 --- a/app/javascript/themes/glitch/features/direct_timeline/index.js +++ /dev/null @@ -1,107 +0,0 @@ -import React from 'react'; -import { connect } from 'react-redux'; -import PropTypes from 'prop-types'; -import StatusListContainer from 'themes/glitch/features/ui/containers/status_list_container'; -import Column from 'themes/glitch/components/column'; -import ColumnHeader from 'themes/glitch/components/column_header'; -import { - refreshDirectTimeline, - expandDirectTimeline, -} from 'themes/glitch/actions/timelines'; -import { addColumn, removeColumn, moveColumn } from 'themes/glitch/actions/columns'; -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; -import ColumnSettingsContainer from './containers/column_settings_container'; -import { connectDirectStream } from 'themes/glitch/actions/streaming'; - -const messages = defineMessages({ - title: { id: 'column.direct', defaultMessage: 'Direct messages' }, -}); - -const mapStateToProps = state => ({ - hasUnread: state.getIn(['timelines', 'direct', 'unread']) > 0, -}); - -@connect(mapStateToProps) -@injectIntl -export default class DirectTimeline extends React.PureComponent { - - static propTypes = { - dispatch: PropTypes.func.isRequired, - columnId: PropTypes.string, - intl: PropTypes.object.isRequired, - hasUnread: PropTypes.bool, - multiColumn: PropTypes.bool, - }; - - handlePin = () => { - const { columnId, dispatch } = this.props; - - if (columnId) { - dispatch(removeColumn(columnId)); - } else { - dispatch(addColumn('DIRECT', {})); - } - } - - handleMove = (dir) => { - const { columnId, dispatch } = this.props; - dispatch(moveColumn(columnId, dir)); - } - - handleHeaderClick = () => { - this.column.scrollTop(); - } - - componentDidMount () { - const { dispatch } = this.props; - - dispatch(refreshDirectTimeline()); - this.disconnect = dispatch(connectDirectStream()); - } - - componentWillUnmount () { - if (this.disconnect) { - this.disconnect(); - this.disconnect = null; - } - } - - setRef = c => { - this.column = c; - } - - handleLoadMore = () => { - this.props.dispatch(expandDirectTimeline()); - } - - render () { - const { intl, hasUnread, columnId, multiColumn } = this.props; - const pinned = !!columnId; - - return ( - <Column ref={this.setRef}> - <ColumnHeader - icon='envelope' - active={hasUnread} - title={intl.formatMessage(messages.title)} - onPin={this.handlePin} - onMove={this.handleMove} - onClick={this.handleHeaderClick} - pinned={pinned} - multiColumn={multiColumn} - > - <ColumnSettingsContainer /> - </ColumnHeader> - - <StatusListContainer - trackScroll={!pinned} - scrollKey={`direct_timeline-${columnId}`} - timelineId='direct' - loadMore={this.handleLoadMore} - emptyMessage={<FormattedMessage id='empty_column.direct' defaultMessage="You don't have any direct messages yet. When you send or receive one, it will show up here." />} - /> - </Column> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/favourited_statuses/index.js b/app/javascript/themes/glitch/features/favourited_statuses/index.js deleted file mode 100644 index 80345e0e2..000000000 --- a/app/javascript/themes/glitch/features/favourited_statuses/index.js +++ /dev/null @@ -1,94 +0,0 @@ -import React from 'react'; -import { connect } from 'react-redux'; -import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import { fetchFavouritedStatuses, expandFavouritedStatuses } from 'themes/glitch/actions/favourites'; -import Column from 'themes/glitch/features/ui/components/column'; -import ColumnHeader from 'themes/glitch/components/column_header'; -import { addColumn, removeColumn, moveColumn } from 'themes/glitch/actions/columns'; -import StatusList from 'themes/glitch/components/status_list'; -import { defineMessages, injectIntl } from 'react-intl'; -import ImmutablePureComponent from 'react-immutable-pure-component'; - -const messages = defineMessages({ - heading: { id: 'column.favourites', defaultMessage: 'Favourites' }, -}); - -const mapStateToProps = state => ({ - statusIds: state.getIn(['status_lists', 'favourites', 'items']), - hasMore: !!state.getIn(['status_lists', 'favourites', 'next']), -}); - -@connect(mapStateToProps) -@injectIntl -export default class Favourites extends ImmutablePureComponent { - - static propTypes = { - dispatch: PropTypes.func.isRequired, - statusIds: ImmutablePropTypes.list.isRequired, - intl: PropTypes.object.isRequired, - columnId: PropTypes.string, - multiColumn: PropTypes.bool, - hasMore: PropTypes.bool, - }; - - componentWillMount () { - this.props.dispatch(fetchFavouritedStatuses()); - } - - handlePin = () => { - const { columnId, dispatch } = this.props; - - if (columnId) { - dispatch(removeColumn(columnId)); - } else { - dispatch(addColumn('FAVOURITES', {})); - } - } - - handleMove = (dir) => { - const { columnId, dispatch } = this.props; - dispatch(moveColumn(columnId, dir)); - } - - handleHeaderClick = () => { - this.column.scrollTop(); - } - - setRef = c => { - this.column = c; - } - - handleScrollToBottom = () => { - this.props.dispatch(expandFavouritedStatuses()); - } - - render () { - const { intl, statusIds, columnId, multiColumn, hasMore } = this.props; - const pinned = !!columnId; - - return ( - <Column ref={this.setRef} name='favourites'> - <ColumnHeader - icon='star' - title={intl.formatMessage(messages.heading)} - onPin={this.handlePin} - onMove={this.handleMove} - onClick={this.handleHeaderClick} - pinned={pinned} - multiColumn={multiColumn} - showBackButton - /> - - <StatusList - trackScroll={!pinned} - statusIds={statusIds} - scrollKey={`favourited_statuses-${columnId}`} - hasMore={hasMore} - onScrollToBottom={this.handleScrollToBottom} - /> - </Column> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/favourites/index.js b/app/javascript/themes/glitch/features/favourites/index.js deleted file mode 100644 index d7b8ac3b1..000000000 --- a/app/javascript/themes/glitch/features/favourites/index.js +++ /dev/null @@ -1,60 +0,0 @@ -import React from 'react'; -import { connect } from 'react-redux'; -import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import LoadingIndicator from 'themes/glitch/components/loading_indicator'; -import { fetchFavourites } from 'themes/glitch/actions/interactions'; -import { ScrollContainer } from 'react-router-scroll-4'; -import AccountContainer from 'themes/glitch/containers/account_container'; -import Column from 'themes/glitch/features/ui/components/column'; -import ColumnBackButton from 'themes/glitch/components/column_back_button'; -import ImmutablePureComponent from 'react-immutable-pure-component'; - -const mapStateToProps = (state, props) => ({ - accountIds: state.getIn(['user_lists', 'favourited_by', props.params.statusId]), -}); - -@connect(mapStateToProps) -export default class Favourites extends ImmutablePureComponent { - - static propTypes = { - params: PropTypes.object.isRequired, - dispatch: PropTypes.func.isRequired, - accountIds: ImmutablePropTypes.list, - }; - - componentWillMount () { - this.props.dispatch(fetchFavourites(this.props.params.statusId)); - } - - componentWillReceiveProps (nextProps) { - if (nextProps.params.statusId !== this.props.params.statusId && nextProps.params.statusId) { - this.props.dispatch(fetchFavourites(nextProps.params.statusId)); - } - } - - render () { - const { accountIds } = this.props; - - if (!accountIds) { - return ( - <Column> - <LoadingIndicator /> - </Column> - ); - } - - return ( - <Column> - <ColumnBackButton /> - - <ScrollContainer scrollKey='favourites'> - <div className='scrollable'> - {accountIds.map(id => <AccountContainer key={id} id={id} withNote={false} />)} - </div> - </ScrollContainer> - </Column> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/follow_requests/components/account_authorize.js b/app/javascript/themes/glitch/features/follow_requests/components/account_authorize.js deleted file mode 100644 index ce386d888..000000000 --- a/app/javascript/themes/glitch/features/follow_requests/components/account_authorize.js +++ /dev/null @@ -1,49 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import Permalink from 'themes/glitch/components/permalink'; -import Avatar from 'themes/glitch/components/avatar'; -import DisplayName from 'themes/glitch/components/display_name'; -import IconButton from 'themes/glitch/components/icon_button'; -import { defineMessages, injectIntl } from 'react-intl'; -import ImmutablePureComponent from 'react-immutable-pure-component'; - -const messages = defineMessages({ - authorize: { id: 'follow_request.authorize', defaultMessage: 'Authorize' }, - reject: { id: 'follow_request.reject', defaultMessage: 'Reject' }, -}); - -@injectIntl -export default class AccountAuthorize extends ImmutablePureComponent { - - static propTypes = { - account: ImmutablePropTypes.map.isRequired, - onAuthorize: PropTypes.func.isRequired, - onReject: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - }; - - render () { - const { intl, account, onAuthorize, onReject } = this.props; - const content = { __html: account.get('note_emojified') }; - - return ( - <div className='account-authorize__wrapper'> - <div className='account-authorize'> - <Permalink href={account.get('url')} to={`/accounts/${account.get('id')}`} className='detailed-status__display-name'> - <div className='account-authorize__avatar'><Avatar account={account} size={48} /></div> - <DisplayName account={account} /> - </Permalink> - - <div className='account__header__content' dangerouslySetInnerHTML={content} /> - </div> - - <div className='account--panel'> - <div className='account--panel__button'><IconButton title={intl.formatMessage(messages.authorize)} icon='check' onClick={onAuthorize} /></div> - <div className='account--panel__button'><IconButton title={intl.formatMessage(messages.reject)} icon='times' onClick={onReject} /></div> - </div> - </div> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/follow_requests/containers/account_authorize_container.js b/app/javascript/themes/glitch/features/follow_requests/containers/account_authorize_container.js deleted file mode 100644 index 78ae77eee..000000000 --- a/app/javascript/themes/glitch/features/follow_requests/containers/account_authorize_container.js +++ /dev/null @@ -1,26 +0,0 @@ -import { connect } from 'react-redux'; -import { makeGetAccount } from 'themes/glitch/selectors'; -import AccountAuthorize from '../components/account_authorize'; -import { authorizeFollowRequest, rejectFollowRequest } from 'themes/glitch/actions/accounts'; - -const makeMapStateToProps = () => { - const getAccount = makeGetAccount(); - - const mapStateToProps = (state, props) => ({ - account: getAccount(state, props.id), - }); - - return mapStateToProps; -}; - -const mapDispatchToProps = (dispatch, { id }) => ({ - onAuthorize () { - dispatch(authorizeFollowRequest(id)); - }, - - onReject () { - dispatch(rejectFollowRequest(id)); - }, -}); - -export default connect(makeMapStateToProps, mapDispatchToProps)(AccountAuthorize); diff --git a/app/javascript/themes/glitch/features/follow_requests/index.js b/app/javascript/themes/glitch/features/follow_requests/index.js deleted file mode 100644 index 3f44f518a..000000000 --- a/app/javascript/themes/glitch/features/follow_requests/index.js +++ /dev/null @@ -1,71 +0,0 @@ -import React from 'react'; -import { connect } from 'react-redux'; -import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import LoadingIndicator from 'themes/glitch/components/loading_indicator'; -import { ScrollContainer } from 'react-router-scroll-4'; -import Column from 'themes/glitch/features/ui/components/column'; -import ColumnBackButtonSlim from 'themes/glitch/components/column_back_button_slim'; -import AccountAuthorizeContainer from './containers/account_authorize_container'; -import { fetchFollowRequests, expandFollowRequests } from 'themes/glitch/actions/accounts'; -import { defineMessages, injectIntl } from 'react-intl'; -import ImmutablePureComponent from 'react-immutable-pure-component'; - -const messages = defineMessages({ - heading: { id: 'column.follow_requests', defaultMessage: 'Follow requests' }, -}); - -const mapStateToProps = state => ({ - accountIds: state.getIn(['user_lists', 'follow_requests', 'items']), -}); - -@connect(mapStateToProps) -@injectIntl -export default class FollowRequests extends ImmutablePureComponent { - - static propTypes = { - params: PropTypes.object.isRequired, - dispatch: PropTypes.func.isRequired, - accountIds: ImmutablePropTypes.list, - intl: PropTypes.object.isRequired, - }; - - componentWillMount () { - this.props.dispatch(fetchFollowRequests()); - } - - handleScroll = (e) => { - const { scrollTop, scrollHeight, clientHeight } = e.target; - - if (scrollTop === scrollHeight - clientHeight) { - this.props.dispatch(expandFollowRequests()); - } - } - - render () { - const { intl, accountIds } = this.props; - - if (!accountIds) { - return ( - <Column name='follow-requests'> - <LoadingIndicator /> - </Column> - ); - } - - return ( - <Column name='follow-requests' icon='users' heading={intl.formatMessage(messages.heading)}> - <ColumnBackButtonSlim /> - - <ScrollContainer scrollKey='follow_requests'> - <div className='scrollable' onScroll={this.handleScroll}> - {accountIds.map(id => - <AccountAuthorizeContainer key={id} id={id} /> - )} - </div> - </ScrollContainer> - </Column> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/followers/index.js b/app/javascript/themes/glitch/features/followers/index.js deleted file mode 100644 index d586bf41d..000000000 --- a/app/javascript/themes/glitch/features/followers/index.js +++ /dev/null @@ -1,93 +0,0 @@ -import React from 'react'; -import { connect } from 'react-redux'; -import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import LoadingIndicator from 'themes/glitch/components/loading_indicator'; -import { - fetchAccount, - fetchFollowers, - expandFollowers, -} from 'themes/glitch/actions/accounts'; -import { ScrollContainer } from 'react-router-scroll-4'; -import AccountContainer from 'themes/glitch/containers/account_container'; -import Column from 'themes/glitch/features/ui/components/column'; -import HeaderContainer from 'themes/glitch/features/account_timeline/containers/header_container'; -import LoadMore from 'themes/glitch/components/load_more'; -import ColumnBackButton from 'themes/glitch/components/column_back_button'; -import ImmutablePureComponent from 'react-immutable-pure-component'; - -const mapStateToProps = (state, props) => ({ - accountIds: state.getIn(['user_lists', 'followers', props.params.accountId, 'items']), - hasMore: !!state.getIn(['user_lists', 'followers', props.params.accountId, 'next']), -}); - -@connect(mapStateToProps) -export default class Followers extends ImmutablePureComponent { - - static propTypes = { - params: PropTypes.object.isRequired, - dispatch: PropTypes.func.isRequired, - accountIds: ImmutablePropTypes.list, - hasMore: PropTypes.bool, - }; - - componentWillMount () { - this.props.dispatch(fetchAccount(this.props.params.accountId)); - this.props.dispatch(fetchFollowers(this.props.params.accountId)); - } - - componentWillReceiveProps (nextProps) { - if (nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) { - this.props.dispatch(fetchAccount(nextProps.params.accountId)); - this.props.dispatch(fetchFollowers(nextProps.params.accountId)); - } - } - - handleScroll = (e) => { - const { scrollTop, scrollHeight, clientHeight } = e.target; - - if (scrollTop === scrollHeight - clientHeight && this.props.hasMore) { - this.props.dispatch(expandFollowers(this.props.params.accountId)); - } - } - - handleLoadMore = (e) => { - e.preventDefault(); - this.props.dispatch(expandFollowers(this.props.params.accountId)); - } - - render () { - const { accountIds, hasMore } = this.props; - - let loadMore = null; - - if (!accountIds) { - return ( - <Column> - <LoadingIndicator /> - </Column> - ); - } - - if (hasMore) { - loadMore = <LoadMore onClick={this.handleLoadMore} />; - } - - return ( - <Column> - <ColumnBackButton /> - - <ScrollContainer scrollKey='followers'> - <div className='scrollable' onScroll={this.handleScroll}> - <div className='followers'> - <HeaderContainer accountId={this.props.params.accountId} /> - {accountIds.map(id => <AccountContainer key={id} id={id} withNote={false} />)} - {loadMore} - </div> - </div> - </ScrollContainer> - </Column> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/following/index.js b/app/javascript/themes/glitch/features/following/index.js deleted file mode 100644 index c306faf21..000000000 --- a/app/javascript/themes/glitch/features/following/index.js +++ /dev/null @@ -1,93 +0,0 @@ -import React from 'react'; -import { connect } from 'react-redux'; -import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import LoadingIndicator from 'themes/glitch/components/loading_indicator'; -import { - fetchAccount, - fetchFollowing, - expandFollowing, -} from 'themes/glitch/actions/accounts'; -import { ScrollContainer } from 'react-router-scroll-4'; -import AccountContainer from 'themes/glitch/containers/account_container'; -import Column from 'themes/glitch/features/ui/components/column'; -import HeaderContainer from 'themes/glitch/features/account_timeline/containers/header_container'; -import LoadMore from 'themes/glitch/components/load_more'; -import ColumnBackButton from 'themes/glitch/components/column_back_button'; -import ImmutablePureComponent from 'react-immutable-pure-component'; - -const mapStateToProps = (state, props) => ({ - accountIds: state.getIn(['user_lists', 'following', props.params.accountId, 'items']), - hasMore: !!state.getIn(['user_lists', 'following', props.params.accountId, 'next']), -}); - -@connect(mapStateToProps) -export default class Following extends ImmutablePureComponent { - - static propTypes = { - params: PropTypes.object.isRequired, - dispatch: PropTypes.func.isRequired, - accountIds: ImmutablePropTypes.list, - hasMore: PropTypes.bool, - }; - - componentWillMount () { - this.props.dispatch(fetchAccount(this.props.params.accountId)); - this.props.dispatch(fetchFollowing(this.props.params.accountId)); - } - - componentWillReceiveProps (nextProps) { - if (nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) { - this.props.dispatch(fetchAccount(nextProps.params.accountId)); - this.props.dispatch(fetchFollowing(nextProps.params.accountId)); - } - } - - handleScroll = (e) => { - const { scrollTop, scrollHeight, clientHeight } = e.target; - - if (scrollTop === scrollHeight - clientHeight && this.props.hasMore) { - this.props.dispatch(expandFollowing(this.props.params.accountId)); - } - } - - handleLoadMore = (e) => { - e.preventDefault(); - this.props.dispatch(expandFollowing(this.props.params.accountId)); - } - - render () { - const { accountIds, hasMore } = this.props; - - let loadMore = null; - - if (!accountIds) { - return ( - <Column> - <LoadingIndicator /> - </Column> - ); - } - - if (hasMore) { - loadMore = <LoadMore onClick={this.handleLoadMore} />; - } - - return ( - <Column> - <ColumnBackButton /> - - <ScrollContainer scrollKey='following'> - <div className='scrollable' onScroll={this.handleScroll}> - <div className='following'> - <HeaderContainer accountId={this.props.params.accountId} /> - {accountIds.map(id => <AccountContainer key={id} id={id} withNote={false} />)} - {loadMore} - </div> - </div> - </ScrollContainer> - </Column> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/generic_not_found/index.js b/app/javascript/themes/glitch/features/generic_not_found/index.js deleted file mode 100644 index ccd2b87b2..000000000 --- a/app/javascript/themes/glitch/features/generic_not_found/index.js +++ /dev/null @@ -1,11 +0,0 @@ -import React from 'react'; -import Column from 'themes/glitch/features/ui/components/column'; -import MissingIndicator from 'themes/glitch/components/missing_indicator'; - -const GenericNotFound = () => ( - <Column> - <MissingIndicator /> - </Column> -); - -export default GenericNotFound; diff --git a/app/javascript/themes/glitch/features/getting_started/index.js b/app/javascript/themes/glitch/features/getting_started/index.js deleted file mode 100644 index 74b019cf1..000000000 --- a/app/javascript/themes/glitch/features/getting_started/index.js +++ /dev/null @@ -1,145 +0,0 @@ -import React from 'react'; -import Column from 'themes/glitch/features/ui/components/column'; -import ColumnLink from 'themes/glitch/features/ui/components/column_link'; -import ColumnSubheading from 'themes/glitch/features/ui/components/column_subheading'; -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; -import { connect } from 'react-redux'; -import { openModal } from 'themes/glitch/actions/modal'; -import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import { me } from 'themes/glitch/util/initial_state'; - -const messages = defineMessages({ - heading: { id: 'getting_started.heading', defaultMessage: 'Getting started' }, - home_timeline: { id: 'tabs_bar.home', defaultMessage: 'Home' }, - notifications: { id: 'tabs_bar.notifications', defaultMessage: 'Notifications' }, - public_timeline: { id: 'navigation_bar.public_timeline', defaultMessage: 'Federated timeline' }, - navigation_subheading: { id: 'column_subheading.navigation', defaultMessage: 'Navigation' }, - settings_subheading: { id: 'column_subheading.settings', defaultMessage: 'Settings' }, - community_timeline: { id: 'navigation_bar.community_timeline', defaultMessage: 'Local timeline' }, - direct: { id: 'navigation_bar.direct', defaultMessage: 'Direct messages' }, - preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' }, - settings: { id: 'navigation_bar.app_settings', defaultMessage: 'App settings' }, - follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' }, - sign_out: { id: 'navigation_bar.logout', defaultMessage: 'Logout' }, - favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favourites' }, - blocks: { id: 'navigation_bar.blocks', defaultMessage: 'Blocked users' }, - mutes: { id: 'navigation_bar.mutes', defaultMessage: 'Muted users' }, - info: { id: 'navigation_bar.info', defaultMessage: 'Extended information' }, - show_me_around: { id: 'getting_started.onboarding', defaultMessage: 'Show me around' }, - pins: { id: 'navigation_bar.pins', defaultMessage: 'Pinned toots' }, -}); - -const mapStateToProps = state => ({ - myAccount: state.getIn(['accounts', me]), - columns: state.getIn(['settings', 'columns']), -}); - -@connect(mapStateToProps) -@injectIntl -export default class GettingStarted extends ImmutablePureComponent { - - static propTypes = { - intl: PropTypes.object.isRequired, - myAccount: ImmutablePropTypes.map.isRequired, - columns: ImmutablePropTypes.list, - multiColumn: PropTypes.bool, - dispatch: PropTypes.func.isRequired, - }; - - openSettings = () => { - this.props.dispatch(openModal('SETTINGS', {})); - } - - openOnboardingModal = (e) => { - e.preventDefault(); - this.props.dispatch(openModal('ONBOARDING')); - } - - render () { - const { intl, myAccount, columns, multiColumn } = this.props; - - let navItems = []; - - if (multiColumn) { - if (!columns.find(item => item.get('id') === 'HOME')) { - navItems.push(<ColumnLink key='0' icon='home' text={intl.formatMessage(messages.home_timeline)} to='/timelines/home' />); - } - - if (!columns.find(item => item.get('id') === 'NOTIFICATIONS')) { - navItems.push(<ColumnLink key='1' icon='bell' text={intl.formatMessage(messages.notifications)} to='/notifications' />); - } - - if (!columns.find(item => item.get('id') === 'COMMUNITY')) { - navItems.push(<ColumnLink key='2' icon='users' text={intl.formatMessage(messages.community_timeline)} to='/timelines/public/local' />); - } - - if (!columns.find(item => item.get('id') === 'PUBLIC')) { - navItems.push(<ColumnLink key='3' icon='globe' text={intl.formatMessage(messages.public_timeline)} to='/timelines/public' />); - } - } - - if (!multiColumn || !columns.find(item => item.get('id') === 'DIRECT')) { - navItems.push(<ColumnLink key='4' icon='envelope' text={intl.formatMessage(messages.direct)} to='/timelines/direct' />); - } - - navItems = navItems.concat([ - <ColumnLink key='5' icon='star' text={intl.formatMessage(messages.favourites)} to='/favourites' />, - <ColumnLink key='6' icon='thumb-tack' text={intl.formatMessage(messages.pins)} to='/pinned' />, - ]); - - if (myAccount.get('locked')) { - navItems.push(<ColumnLink key='7' icon='users' text={intl.formatMessage(messages.follow_requests)} to='/follow_requests' />); - } - - navItems = navItems.concat([ - <ColumnLink key='8' icon='volume-off' text={intl.formatMessage(messages.mutes)} to='/mutes' />, - <ColumnLink key='9' icon='ban' text={intl.formatMessage(messages.blocks)} to='/blocks' />, - ]); - - return ( - <Column name='getting-started' icon='asterisk' heading={intl.formatMessage(messages.heading)} hideHeadingOnMobile> - <div className='scrollable optionally-scrollable'> - <div className='getting-started__wrapper'> - <ColumnSubheading text={intl.formatMessage(messages.navigation_subheading)} /> - {navItems} - <ColumnSubheading text={intl.formatMessage(messages.settings_subheading)} /> - <ColumnLink icon='book' text={intl.formatMessage(messages.info)} href='/about/more' /> - <ColumnLink icon='hand-o-right' text={intl.formatMessage(messages.show_me_around)} onClick={this.openOnboardingModal} /> - <ColumnLink icon='cog' text={intl.formatMessage(messages.preferences)} href='/settings/preferences' /> - <ColumnLink icon='cogs' text={intl.formatMessage(messages.settings)} onClick={this.openSettings} /> - <ColumnLink icon='sign-out' text={intl.formatMessage(messages.sign_out)} href='/auth/sign_out' method='delete' /> - </div> - - <div className='getting-started__footer'> - <div className='static-content getting-started'> - <p> - <a href='https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/FAQ.md' rel='noopener' target='_blank'> - <FormattedMessage id='getting_started.faq' defaultMessage='FAQ' /> - </a> • - <a href='https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/User-guide.md' rel='noopener' target='_blank'> - <FormattedMessage id='getting_started.userguide' defaultMessage='User Guide' /> - </a> • - <a href='https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/Apps.md' rel='noopener' target='_blank'> - <FormattedMessage id='getting_started.appsshort' defaultMessage='Apps' /> - </a> - </p> - <p> - <FormattedMessage - id='getting_started.open_source_notice' - defaultMessage='Glitchsoc is open source software, a friendly fork of {Mastodon}. You can contribute or report issues on GitHub at {github}.' - values={{ - github: <a href='https://github.com/glitch-soc/mastodon' rel='noopener' target='_blank'>glitch-soc/mastodon</a>, - Mastodon: <a href='https://github.com/tootsuite/mastodon' rel='noopener' target='_blank'>Mastodon</a>, - }} - /> - </p> - </div> - </div> - </div> - </Column> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/hashtag_timeline/index.js b/app/javascript/themes/glitch/features/hashtag_timeline/index.js deleted file mode 100644 index a878931b3..000000000 --- a/app/javascript/themes/glitch/features/hashtag_timeline/index.js +++ /dev/null @@ -1,118 +0,0 @@ -import React from 'react'; -import { connect } from 'react-redux'; -import PropTypes from 'prop-types'; -import StatusListContainer from 'themes/glitch/features/ui/containers/status_list_container'; -import Column from 'themes/glitch/components/column'; -import ColumnHeader from 'themes/glitch/components/column_header'; -import { - refreshHashtagTimeline, - expandHashtagTimeline, -} from 'themes/glitch/actions/timelines'; -import { addColumn, removeColumn, moveColumn } from 'themes/glitch/actions/columns'; -import { FormattedMessage } from 'react-intl'; -import { connectHashtagStream } from 'themes/glitch/actions/streaming'; - -const mapStateToProps = (state, props) => ({ - hasUnread: state.getIn(['timelines', `hashtag:${props.params.id}`, 'unread']) > 0, -}); - -@connect(mapStateToProps) -export default class HashtagTimeline extends React.PureComponent { - - static propTypes = { - params: PropTypes.object.isRequired, - columnId: PropTypes.string, - dispatch: PropTypes.func.isRequired, - hasUnread: PropTypes.bool, - multiColumn: PropTypes.bool, - }; - - handlePin = () => { - const { columnId, dispatch } = this.props; - - if (columnId) { - dispatch(removeColumn(columnId)); - } else { - dispatch(addColumn('HASHTAG', { id: this.props.params.id })); - } - } - - handleMove = (dir) => { - const { columnId, dispatch } = this.props; - dispatch(moveColumn(columnId, dir)); - } - - handleHeaderClick = () => { - this.column.scrollTop(); - } - - _subscribe (dispatch, id) { - this.disconnect = dispatch(connectHashtagStream(id)); - } - - _unsubscribe () { - if (this.disconnect) { - this.disconnect(); - this.disconnect = null; - } - } - - componentDidMount () { - const { dispatch } = this.props; - const { id } = this.props.params; - - dispatch(refreshHashtagTimeline(id)); - this._subscribe(dispatch, id); - } - - componentWillReceiveProps (nextProps) { - if (nextProps.params.id !== this.props.params.id) { - this.props.dispatch(refreshHashtagTimeline(nextProps.params.id)); - this._unsubscribe(); - this._subscribe(this.props.dispatch, nextProps.params.id); - } - } - - componentWillUnmount () { - this._unsubscribe(); - } - - setRef = c => { - this.column = c; - } - - handleLoadMore = () => { - this.props.dispatch(expandHashtagTimeline(this.props.params.id)); - } - - render () { - const { hasUnread, columnId, multiColumn } = this.props; - const { id } = this.props.params; - const pinned = !!columnId; - - return ( - <Column ref={this.setRef} name='hashtag'> - <ColumnHeader - icon='hashtag' - active={hasUnread} - title={id} - onPin={this.handlePin} - onMove={this.handleMove} - onClick={this.handleHeaderClick} - pinned={pinned} - multiColumn={multiColumn} - showBackButton - /> - - <StatusListContainer - trackScroll={!pinned} - scrollKey={`hashtag_timeline-${columnId}`} - timelineId={`hashtag:${id}`} - loadMore={this.handleLoadMore} - emptyMessage={<FormattedMessage id='empty_column.hashtag' defaultMessage='There is nothing in this hashtag yet.' />} - /> - </Column> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/home_timeline/components/column_settings.js b/app/javascript/themes/glitch/features/home_timeline/components/column_settings.js deleted file mode 100644 index 533da6c36..000000000 --- a/app/javascript/themes/glitch/features/home_timeline/components/column_settings.js +++ /dev/null @@ -1,46 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; -import SettingToggle from 'themes/glitch/features/notifications/components/setting_toggle'; -import SettingText from 'themes/glitch/components/setting_text'; - -const messages = defineMessages({ - filter_regex: { id: 'home.column_settings.filter_regex', defaultMessage: 'Filter out by regular expressions' }, - settings: { id: 'home.settings', defaultMessage: 'Column settings' }, -}); - -@injectIntl -export default class ColumnSettings extends React.PureComponent { - - static propTypes = { - settings: ImmutablePropTypes.map.isRequired, - onChange: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - }; - - render () { - const { settings, onChange, intl } = this.props; - - return ( - <div> - <span className='column-settings__section'><FormattedMessage id='home.column_settings.basic' defaultMessage='Basic' /></span> - - <div className='column-settings__row'> - <SettingToggle prefix='home_timeline' settings={settings} settingKey={['shows', 'reblog']} onChange={onChange} label={<FormattedMessage id='home.column_settings.show_reblogs' defaultMessage='Show boosts' />} /> - </div> - - <div className='column-settings__row'> - <SettingToggle prefix='home_timeline' settings={settings} settingKey={['shows', 'reply']} onChange={onChange} label={<FormattedMessage id='home.column_settings.show_replies' defaultMessage='Show replies' />} /> - </div> - - <span className='column-settings__section'><FormattedMessage id='home.column_settings.advanced' defaultMessage='Advanced' /></span> - - <div className='column-settings__row'> - <SettingText prefix='home_timeline' settings={settings} settingKey={['regex', 'body']} onChange={onChange} label={intl.formatMessage(messages.filter_regex)} /> - </div> - </div> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/home_timeline/containers/column_settings_container.js b/app/javascript/themes/glitch/features/home_timeline/containers/column_settings_container.js deleted file mode 100644 index a0062f564..000000000 --- a/app/javascript/themes/glitch/features/home_timeline/containers/column_settings_container.js +++ /dev/null @@ -1,21 +0,0 @@ -import { connect } from 'react-redux'; -import ColumnSettings from '../components/column_settings'; -import { changeSetting, saveSettings } from 'themes/glitch/actions/settings'; - -const mapStateToProps = state => ({ - settings: state.getIn(['settings', 'home']), -}); - -const mapDispatchToProps = dispatch => ({ - - onChange (key, checked) { - dispatch(changeSetting(['home', ...key], checked)); - }, - - onSave () { - dispatch(saveSettings()); - }, - -}); - -export default connect(mapStateToProps, mapDispatchToProps)(ColumnSettings); diff --git a/app/javascript/themes/glitch/features/home_timeline/index.js b/app/javascript/themes/glitch/features/home_timeline/index.js deleted file mode 100644 index 8a65891cd..000000000 --- a/app/javascript/themes/glitch/features/home_timeline/index.js +++ /dev/null @@ -1,90 +0,0 @@ -import React from 'react'; -import { connect } from 'react-redux'; -import { expandHomeTimeline } from 'themes/glitch/actions/timelines'; -import PropTypes from 'prop-types'; -import StatusListContainer from 'themes/glitch/features/ui/containers/status_list_container'; -import Column from 'themes/glitch/components/column'; -import ColumnHeader from 'themes/glitch/components/column_header'; -import { addColumn, removeColumn, moveColumn } from 'themes/glitch/actions/columns'; -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; -import ColumnSettingsContainer from './containers/column_settings_container'; -import { Link } from 'react-router-dom'; - -const messages = defineMessages({ - title: { id: 'column.home', defaultMessage: 'Home' }, -}); - -const mapStateToProps = state => ({ - hasUnread: state.getIn(['timelines', 'home', 'unread']) > 0, -}); - -@connect(mapStateToProps) -@injectIntl -export default class HomeTimeline extends React.PureComponent { - - static propTypes = { - dispatch: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - hasUnread: PropTypes.bool, - columnId: PropTypes.string, - multiColumn: PropTypes.bool, - }; - - handlePin = () => { - const { columnId, dispatch } = this.props; - - if (columnId) { - dispatch(removeColumn(columnId)); - } else { - dispatch(addColumn('HOME', {})); - } - } - - handleMove = (dir) => { - const { columnId, dispatch } = this.props; - dispatch(moveColumn(columnId, dir)); - } - - handleHeaderClick = () => { - this.column.scrollTop(); - } - - setRef = c => { - this.column = c; - } - - handleLoadMore = () => { - this.props.dispatch(expandHomeTimeline()); - } - - render () { - const { intl, hasUnread, columnId, multiColumn } = this.props; - const pinned = !!columnId; - - return ( - <Column ref={this.setRef} name='home'> - <ColumnHeader - icon='home' - active={hasUnread} - title={intl.formatMessage(messages.title)} - onPin={this.handlePin} - onMove={this.handleMove} - onClick={this.handleHeaderClick} - pinned={pinned} - multiColumn={multiColumn} - > - <ColumnSettingsContainer /> - </ColumnHeader> - - <StatusListContainer - trackScroll={!pinned} - scrollKey={`home_timeline-${columnId}`} - loadMore={this.handleLoadMore} - timelineId='home' - emptyMessage={<FormattedMessage id='empty_column.home' defaultMessage='Your home timeline is empty! Visit {public} or use search to get started and meet other users.' values={{ public: <Link to='/timelines/public'><FormattedMessage id='empty_column.home.public_timeline' defaultMessage='the public timeline' /></Link> }} />} - /> - </Column> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/local_settings/index.js b/app/javascript/themes/glitch/features/local_settings/index.js deleted file mode 100644 index 6c5d51413..000000000 --- a/app/javascript/themes/glitch/features/local_settings/index.js +++ /dev/null @@ -1,68 +0,0 @@ -// Package imports. -import React from 'react'; -import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import { connect } from 'react-redux'; - -// Our imports -import LocalSettingsPage from './page'; -import LocalSettingsNavigation from './navigation'; -import { closeModal } from 'themes/glitch/actions/modal'; -import { changeLocalSetting } from 'themes/glitch/actions/local_settings'; - -// Stylesheet imports -import './style.scss'; - -const mapStateToProps = state => ({ - settings: state.get('local_settings'), -}); - -const mapDispatchToProps = dispatch => ({ - onChange (setting, value) { - dispatch(changeLocalSetting(setting, value)); - }, - onClose () { - dispatch(closeModal()); - }, -}); - -class LocalSettings extends React.PureComponent { - - static propTypes = { - onChange: PropTypes.func.isRequired, - onClose: PropTypes.func.isRequired, - settings: ImmutablePropTypes.map.isRequired, - }; - - state = { - currentIndex: 0, - }; - - navigateTo = (index) => - this.setState({ currentIndex: +index }); - - render () { - - const { navigateTo } = this; - const { onChange, onClose, settings } = this.props; - const { currentIndex } = this.state; - - return ( - <div className='glitch modal-root__modal local-settings'> - <LocalSettingsNavigation - index={currentIndex} - onClose={onClose} - onNavigate={navigateTo} - /> - <LocalSettingsPage - index={currentIndex} - onChange={onChange} - settings={settings} - /> - </div> - ); - } - -} - -export default connect(mapStateToProps, mapDispatchToProps)(LocalSettings); diff --git a/app/javascript/themes/glitch/features/local_settings/navigation/index.js b/app/javascript/themes/glitch/features/local_settings/navigation/index.js deleted file mode 100644 index fa35e83c7..000000000 --- a/app/javascript/themes/glitch/features/local_settings/navigation/index.js +++ /dev/null @@ -1,74 +0,0 @@ -// Package imports -import React from 'react'; -import PropTypes from 'prop-types'; -import { injectIntl, defineMessages } from 'react-intl'; - -// Our imports -import LocalSettingsNavigationItem from './item'; - -// Stylesheet imports -import './style.scss'; - -// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - -const messages = defineMessages({ - general: { id: 'settings.general', defaultMessage: 'General' }, - collapsed: { id: 'settings.collapsed_statuses', defaultMessage: 'Collapsed toots' }, - media: { id: 'settings.media', defaultMessage: 'Media' }, - preferences: { id: 'settings.preferences', defaultMessage: 'Preferences' }, - close: { id: 'settings.close', defaultMessage: 'Close' }, -}); - -@injectIntl -export default class LocalSettingsNavigation extends React.PureComponent { - - static propTypes = { - index : PropTypes.number, - intl : PropTypes.object.isRequired, - onClose : PropTypes.func.isRequired, - onNavigate : PropTypes.func.isRequired, - }; - - render () { - - const { index, intl, onClose, onNavigate } = this.props; - - return ( - <nav className='glitch local-settings__navigation'> - <LocalSettingsNavigationItem - active={index === 0} - index={0} - onNavigate={onNavigate} - title={intl.formatMessage(messages.general)} - /> - <LocalSettingsNavigationItem - active={index === 1} - index={1} - onNavigate={onNavigate} - title={intl.formatMessage(messages.collapsed)} - /> - <LocalSettingsNavigationItem - active={index === 2} - index={2} - onNavigate={onNavigate} - title={intl.formatMessage(messages.media)} - /> - <LocalSettingsNavigationItem - active={index === 3} - href='/settings/preferences' - index={3} - icon='cog' - title={intl.formatMessage(messages.preferences)} - /> - <LocalSettingsNavigationItem - active={index === 4} - className='close' - index={4} - onNavigate={onClose} - title={intl.formatMessage(messages.close)} - /> - </nav> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/local_settings/navigation/item/index.js b/app/javascript/themes/glitch/features/local_settings/navigation/item/index.js deleted file mode 100644 index a352d5fb2..000000000 --- a/app/javascript/themes/glitch/features/local_settings/navigation/item/index.js +++ /dev/null @@ -1,69 +0,0 @@ -// Package imports -import React from 'react'; -import PropTypes from 'prop-types'; -import classNames from 'classnames'; - -// Stylesheet imports -import './style.scss'; - -// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - -export default class LocalSettingsPage extends React.PureComponent { - - static propTypes = { - active: PropTypes.bool, - className: PropTypes.string, - href: PropTypes.string, - icon: PropTypes.string, - index: PropTypes.number.isRequired, - onNavigate: PropTypes.func, - title: PropTypes.string, - }; - - handleClick = (e) => { - const { index, onNavigate } = this.props; - if (onNavigate) { - onNavigate(index); - e.preventDefault(); - } - } - - render () { - const { handleClick } = this; - const { - active, - className, - href, - icon, - onNavigate, - title, - } = this.props; - - const finalClassName = classNames('glitch', 'local-settings__navigation__item', { - active, - }, className); - - const iconElem = icon ? <i className={`fa fa-fw fa-${icon}`} /> : null; - - if (href) return ( - <a - href={href} - className={finalClassName} - > - {iconElem} {title} - </a> - ); - else if (onNavigate) return ( - <a - onClick={handleClick} - role='button' - tabIndex='0' - className={finalClassName} - > - {iconElem} {title} - </a> - ); - else return null; - } - -} diff --git a/app/javascript/themes/glitch/features/local_settings/navigation/item/style.scss b/app/javascript/themes/glitch/features/local_settings/navigation/item/style.scss deleted file mode 100644 index 7f7371993..000000000 --- a/app/javascript/themes/glitch/features/local_settings/navigation/item/style.scss +++ /dev/null @@ -1,27 +0,0 @@ -@import 'styles/mastodon/variables'; - -.glitch.local-settings__navigation__item { - display: block; - padding: 15px 20px; - color: inherit; - background: $primary-text-color; - border-bottom: 1px $ui-primary-color solid; - cursor: pointer; - text-decoration: none; - outline: none; - transition: background .3s; - - &:hover { - background: $ui-secondary-color; - } - - &.active { - background: $ui-highlight-color; - color: $primary-text-color; - } - - &.close, &.close:hover { - background: $error-value-color; - color: $primary-text-color; - } -} diff --git a/app/javascript/themes/glitch/features/local_settings/navigation/style.scss b/app/javascript/themes/glitch/features/local_settings/navigation/style.scss deleted file mode 100644 index 0336f943b..000000000 --- a/app/javascript/themes/glitch/features/local_settings/navigation/style.scss +++ /dev/null @@ -1,10 +0,0 @@ -@import 'styles/mastodon/variables'; - -.glitch.local-settings__navigation { - background: $primary-text-color; - color: $ui-base-color; - width: 200px; - font-size: 15px; - line-height: 20px; - overflow-y: auto; -} diff --git a/app/javascript/themes/glitch/features/local_settings/page/index.js b/app/javascript/themes/glitch/features/local_settings/page/index.js deleted file mode 100644 index 498230f7b..000000000 --- a/app/javascript/themes/glitch/features/local_settings/page/index.js +++ /dev/null @@ -1,212 +0,0 @@ -// Package imports -import React from 'react'; -import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import { defineMessages, FormattedMessage, injectIntl } from 'react-intl'; - -// Our imports -import LocalSettingsPageItem from './item'; - -// Stylesheet imports -import './style.scss'; - -// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - -const messages = defineMessages({ - layout_auto: { id: 'layout.auto', defaultMessage: 'Auto' }, - layout_desktop: { id: 'layout.desktop', defaultMessage: 'Desktop' }, - layout_mobile: { id: 'layout.single', defaultMessage: 'Mobile' }, - side_arm_none: { id: 'settings.side_arm.none', defaultMessage: 'None' }, -}); - -@injectIntl -export default class LocalSettingsPage extends React.PureComponent { - - static propTypes = { - index : PropTypes.number, - intl : PropTypes.object.isRequired, - onChange : PropTypes.func.isRequired, - settings : ImmutablePropTypes.map.isRequired, - }; - - pages = [ - ({ intl, onChange, settings }) => ( - <div className='glitch local-settings__page general'> - <h1><FormattedMessage id='settings.general' defaultMessage='General' /></h1> - <LocalSettingsPageItem - settings={settings} - item={['layout']} - id='mastodon-settings--layout' - options={[ - { value: 'auto', message: intl.formatMessage(messages.layout_auto) }, - { value: 'multiple', message: intl.formatMessage(messages.layout_desktop) }, - { value: 'single', message: intl.formatMessage(messages.layout_mobile) }, - ]} - onChange={onChange} - > - <FormattedMessage id='settings.layout' defaultMessage='Layout:' /> - </LocalSettingsPageItem> - <LocalSettingsPageItem - settings={settings} - item={['stretch']} - id='mastodon-settings--stretch' - onChange={onChange} - > - <FormattedMessage id='settings.wide_view' defaultMessage='Wide view (Desktop mode only)' /> - </LocalSettingsPageItem> - <LocalSettingsPageItem - settings={settings} - item={['navbar_under']} - id='mastodon-settings--navbar_under' - onChange={onChange} - > - <FormattedMessage id='settings.navbar_under' defaultMessage='Navbar at the bottom (Mobile only)' /> - </LocalSettingsPageItem> - <section> - <h2><FormattedMessage id='settings.compose_box_opts' defaultMessage='Compose box options' /></h2> - <LocalSettingsPageItem - settings={settings} - item={['side_arm']} - id='mastodon-settings--side_arm' - options={[ - { value: 'none', message: intl.formatMessage(messages.side_arm_none) }, - { value: 'direct', message: intl.formatMessage({ id: 'privacy.direct.short' }) }, - { value: 'private', message: intl.formatMessage({ id: 'privacy.private.short' }) }, - { value: 'unlisted', message: intl.formatMessage({ id: 'privacy.unlisted.short' }) }, - { value: 'public', message: intl.formatMessage({ id: 'privacy.public.short' }) }, - ]} - onChange={onChange} - > - <FormattedMessage id='settings.side_arm' defaultMessage='Secondary toot button:' /> - </LocalSettingsPageItem> - </section> - </div> - ), - ({ onChange, settings }) => ( - <div className='glitch local-settings__page collapsed'> - <h1><FormattedMessage id='settings.collapsed_statuses' defaultMessage='Collapsed toots' /></h1> - <LocalSettingsPageItem - settings={settings} - item={['collapsed', 'enabled']} - id='mastodon-settings--collapsed-enabled' - onChange={onChange} - > - <FormattedMessage id='settings.enable_collapsed' defaultMessage='Enable collapsed toots' /> - </LocalSettingsPageItem> - <section> - <h2><FormattedMessage id='settings.auto_collapse' defaultMessage='Automatic collapsing' /></h2> - <LocalSettingsPageItem - settings={settings} - item={['collapsed', 'auto', 'all']} - id='mastodon-settings--collapsed-auto-all' - onChange={onChange} - dependsOn={[['collapsed', 'enabled']]} - > - <FormattedMessage id='settings.auto_collapse_all' defaultMessage='Everything' /> - </LocalSettingsPageItem> - <LocalSettingsPageItem - settings={settings} - item={['collapsed', 'auto', 'notifications']} - id='mastodon-settings--collapsed-auto-notifications' - onChange={onChange} - dependsOn={[['collapsed', 'enabled']]} - dependsOnNot={[['collapsed', 'auto', 'all']]} - > - <FormattedMessage id='settings.auto_collapse_notifications' defaultMessage='Notifications' /> - </LocalSettingsPageItem> - <LocalSettingsPageItem - settings={settings} - item={['collapsed', 'auto', 'lengthy']} - id='mastodon-settings--collapsed-auto-lengthy' - onChange={onChange} - dependsOn={[['collapsed', 'enabled']]} - dependsOnNot={[['collapsed', 'auto', 'all']]} - > - <FormattedMessage id='settings.auto_collapse_lengthy' defaultMessage='Lengthy toots' /> - </LocalSettingsPageItem> - <LocalSettingsPageItem - settings={settings} - item={['collapsed', 'auto', 'reblogs']} - id='mastodon-settings--collapsed-auto-reblogs' - onChange={onChange} - dependsOn={[['collapsed', 'enabled']]} - dependsOnNot={[['collapsed', 'auto', 'all']]} - > - <FormattedMessage id='settings.auto_collapse_reblogs' defaultMessage='Boosts' /> - </LocalSettingsPageItem> - <LocalSettingsPageItem - settings={settings} - item={['collapsed', 'auto', 'replies']} - id='mastodon-settings--collapsed-auto-replies' - onChange={onChange} - dependsOn={[['collapsed', 'enabled']]} - dependsOnNot={[['collapsed', 'auto', 'all']]} - > - <FormattedMessage id='settings.auto_collapse_replies' defaultMessage='Replies' /> - </LocalSettingsPageItem> - <LocalSettingsPageItem - settings={settings} - item={['collapsed', 'auto', 'media']} - id='mastodon-settings--collapsed-auto-media' - onChange={onChange} - dependsOn={[['collapsed', 'enabled']]} - dependsOnNot={[['collapsed', 'auto', 'all']]} - > - <FormattedMessage id='settings.auto_collapse_media' defaultMessage='Toots with media' /> - </LocalSettingsPageItem> - </section> - <section> - <h2><FormattedMessage id='settings.image_backgrounds' defaultMessage='Image backgrounds' /></h2> - <LocalSettingsPageItem - settings={settings} - item={['collapsed', 'backgrounds', 'user_backgrounds']} - id='mastodon-settings--collapsed-user-backgrouns' - onChange={onChange} - dependsOn={[['collapsed', 'enabled']]} - > - <FormattedMessage id='settings.image_backgrounds_users' defaultMessage='Give collapsed toots an image background' /> - </LocalSettingsPageItem> - <LocalSettingsPageItem - settings={settings} - item={['collapsed', 'backgrounds', 'preview_images']} - id='mastodon-settings--collapsed-preview-images' - onChange={onChange} - dependsOn={[['collapsed', 'enabled']]} - > - <FormattedMessage id='settings.image_backgrounds_media' defaultMessage='Preview collapsed toot media' /> - </LocalSettingsPageItem> - </section> - </div> - ), - ({ onChange, settings }) => ( - <div className='glitch local-settings__page media'> - <h1><FormattedMessage id='settings.media' defaultMessage='Media' /></h1> - <LocalSettingsPageItem - settings={settings} - item={['media', 'letterbox']} - id='mastodon-settings--media-letterbox' - onChange={onChange} - > - <FormattedMessage id='settings.media_letterbox' defaultMessage='Letterbox media' /> - </LocalSettingsPageItem> - <LocalSettingsPageItem - settings={settings} - item={['media', 'fullwidth']} - id='mastodon-settings--media-fullwidth' - onChange={onChange} - > - <FormattedMessage id='settings.media_fullwidth' defaultMessage='Full-width media previews' /> - </LocalSettingsPageItem> - </div> - ), - ]; - - render () { - const { pages } = this; - const { index, intl, onChange, settings } = this.props; - const CurrentPage = pages[index] || pages[0]; - - return <CurrentPage intl={intl} onChange={onChange} settings={settings} />; - } - -} diff --git a/app/javascript/themes/glitch/features/local_settings/page/item/index.js b/app/javascript/themes/glitch/features/local_settings/page/item/index.js deleted file mode 100644 index 37e28c084..000000000 --- a/app/javascript/themes/glitch/features/local_settings/page/item/index.js +++ /dev/null @@ -1,90 +0,0 @@ -// Package imports -import React from 'react'; -import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; - -// Stylesheet imports -import './style.scss'; - -// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - -export default class LocalSettingsPageItem extends React.PureComponent { - - static propTypes = { - children: PropTypes.element.isRequired, - dependsOn: PropTypes.array, - dependsOnNot: PropTypes.array, - id: PropTypes.string.isRequired, - item: PropTypes.array.isRequired, - onChange: PropTypes.func.isRequired, - options: PropTypes.arrayOf(PropTypes.shape({ - value: PropTypes.string.isRequired, - message: PropTypes.string.isRequired, - })), - settings: ImmutablePropTypes.map.isRequired, - }; - - handleChange = e => { - const { target } = e; - const { item, onChange, options } = this.props; - if (options && options.length > 0) onChange(item, target.value); - else onChange(item, target.checked); - } - - render () { - const { handleChange } = this; - const { settings, item, id, options, children, dependsOn, dependsOnNot } = this.props; - let enabled = true; - - if (dependsOn) { - for (let i = 0; i < dependsOn.length; i++) { - enabled = enabled && settings.getIn(dependsOn[i]); - } - } - if (dependsOnNot) { - for (let i = 0; i < dependsOnNot.length; i++) { - enabled = enabled && !settings.getIn(dependsOnNot[i]); - } - } - - if (options && options.length > 0) { - const currentValue = settings.getIn(item); - const optionElems = options && options.length > 0 && options.map((opt) => ( - <option - key={opt.value} - value={opt.value} - > - {opt.message} - </option> - )); - return ( - <label className='glitch local-settings__page__item' htmlFor={id}> - <p>{children}</p> - <p> - <select - id={id} - disabled={!enabled} - onBlur={handleChange} - onChange={handleChange} - value={currentValue} - > - {optionElems} - </select> - </p> - </label> - ); - } else return ( - <label className='glitch local-settings__page__item' htmlFor={id}> - <input - id={id} - type='checkbox' - checked={settings.getIn(item)} - onChange={handleChange} - disabled={!enabled} - /> - {children} - </label> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/local_settings/page/item/style.scss b/app/javascript/themes/glitch/features/local_settings/page/item/style.scss deleted file mode 100644 index b2d8f7185..000000000 --- a/app/javascript/themes/glitch/features/local_settings/page/item/style.scss +++ /dev/null @@ -1,7 +0,0 @@ -@import 'styles/mastodon/variables'; - -.glitch.local-settings__page__item { - select { - margin-bottom: 5px; - } -} diff --git a/app/javascript/themes/glitch/features/local_settings/page/style.scss b/app/javascript/themes/glitch/features/local_settings/page/style.scss deleted file mode 100644 index e9eedcad0..000000000 --- a/app/javascript/themes/glitch/features/local_settings/page/style.scss +++ /dev/null @@ -1,9 +0,0 @@ -@import 'styles/mastodon/variables'; - -.glitch.local-settings__page { - display: block; - flex: auto; - padding: 15px 20px 15px 20px; - width: 360px; - overflow-y: auto; -} diff --git a/app/javascript/themes/glitch/features/local_settings/style.scss b/app/javascript/themes/glitch/features/local_settings/style.scss deleted file mode 100644 index 765294607..000000000 --- a/app/javascript/themes/glitch/features/local_settings/style.scss +++ /dev/null @@ -1,34 +0,0 @@ -@import 'styles/mastodon/variables'; - -.glitch.local-settings { - position: relative; - display: flex; - flex-direction: row; - background: $ui-secondary-color; - color: $ui-base-color; - border-radius: 8px; - height: 80vh; - width: 80vw; - max-width: 740px; - max-height: 450px; - overflow: hidden; - - label { - display: block; - } - - h1 { - font-size: 18px; - font-weight: 500; - line-height: 24px; - margin-bottom: 20px; - } - - h2 { - font-size: 15px; - font-weight: 500; - line-height: 20px; - margin-top: 20px; - margin-bottom: 10px; - } -} diff --git a/app/javascript/themes/glitch/features/mutes/index.js b/app/javascript/themes/glitch/features/mutes/index.js deleted file mode 100644 index 1158b8262..000000000 --- a/app/javascript/themes/glitch/features/mutes/index.js +++ /dev/null @@ -1,70 +0,0 @@ -import React from 'react'; -import { connect } from 'react-redux'; -import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import LoadingIndicator from 'themes/glitch/components/loading_indicator'; -import { ScrollContainer } from 'react-router-scroll-4'; -import Column from 'themes/glitch/features/ui/components/column'; -import ColumnBackButtonSlim from 'themes/glitch/components/column_back_button_slim'; -import AccountContainer from 'themes/glitch/containers/account_container'; -import { fetchMutes, expandMutes } from 'themes/glitch/actions/mutes'; -import { defineMessages, injectIntl } from 'react-intl'; -import ImmutablePureComponent from 'react-immutable-pure-component'; - -const messages = defineMessages({ - heading: { id: 'column.mutes', defaultMessage: 'Muted users' }, -}); - -const mapStateToProps = state => ({ - accountIds: state.getIn(['user_lists', 'mutes', 'items']), -}); - -@connect(mapStateToProps) -@injectIntl -export default class Mutes extends ImmutablePureComponent { - - static propTypes = { - params: PropTypes.object.isRequired, - dispatch: PropTypes.func.isRequired, - accountIds: ImmutablePropTypes.list, - intl: PropTypes.object.isRequired, - }; - - componentWillMount () { - this.props.dispatch(fetchMutes()); - } - - handleScroll = (e) => { - const { scrollTop, scrollHeight, clientHeight } = e.target; - - if (scrollTop === scrollHeight - clientHeight) { - this.props.dispatch(expandMutes()); - } - } - - render () { - const { intl, accountIds } = this.props; - - if (!accountIds) { - return ( - <Column> - <LoadingIndicator /> - </Column> - ); - } - - return ( - <Column name='mutes' icon='volume-off' heading={intl.formatMessage(messages.heading)}> - <ColumnBackButtonSlim /> - <ScrollContainer scrollKey='mutes'> - <div className='scrollable mutes' onScroll={this.handleScroll}> - {accountIds.map(id => - <AccountContainer key={id} id={id} /> - )} - </div> - </ScrollContainer> - </Column> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/notifications/components/clear_column_button.js b/app/javascript/themes/glitch/features/notifications/components/clear_column_button.js deleted file mode 100644 index 22a10753f..000000000 --- a/app/javascript/themes/glitch/features/notifications/components/clear_column_button.js +++ /dev/null @@ -1,17 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { FormattedMessage } from 'react-intl'; - -export default class ClearColumnButton extends React.Component { - - static propTypes = { - onClick: PropTypes.func.isRequired, - }; - - render () { - return ( - <button className='text-btn column-header__setting-btn' tabIndex='0' onClick={this.props.onClick}><i className='fa fa-eraser' /> <FormattedMessage id='notifications.clear' defaultMessage='Clear notifications' /></button> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/notifications/components/column_settings.js b/app/javascript/themes/glitch/features/notifications/components/column_settings.js deleted file mode 100644 index 88a29d4d3..000000000 --- a/app/javascript/themes/glitch/features/notifications/components/column_settings.js +++ /dev/null @@ -1,86 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import { FormattedMessage } from 'react-intl'; -import ClearColumnButton from './clear_column_button'; -import SettingToggle from './setting_toggle'; - -export default class ColumnSettings extends React.PureComponent { - - static propTypes = { - settings: ImmutablePropTypes.map.isRequired, - pushSettings: ImmutablePropTypes.map.isRequired, - onChange: PropTypes.func.isRequired, - onSave: PropTypes.func.isRequired, - onClear: PropTypes.func.isRequired, - }; - - onPushChange = (key, checked) => { - this.props.onChange(['push', ...key], checked); - } - - render () { - const { settings, pushSettings, onChange, onClear } = this.props; - - const alertStr = <FormattedMessage id='notifications.column_settings.alert' defaultMessage='Desktop notifications' />; - const showStr = <FormattedMessage id='notifications.column_settings.show' defaultMessage='Show in column' />; - const soundStr = <FormattedMessage id='notifications.column_settings.sound' defaultMessage='Play sound' />; - - const showPushSettings = pushSettings.get('browserSupport') && pushSettings.get('isSubscribed'); - const pushStr = showPushSettings && <FormattedMessage id='notifications.column_settings.push' defaultMessage='Push notifications' />; - const pushMeta = showPushSettings && <FormattedMessage id='notifications.column_settings.push_meta' defaultMessage='This device' />; - - return ( - <div> - <div className='column-settings__row'> - <ClearColumnButton onClick={onClear} /> - </div> - - <div role='group' aria-labelledby='notifications-follow'> - <span id='notifications-follow' className='column-settings__section'><FormattedMessage id='notifications.column_settings.follow' defaultMessage='New followers:' /></span> - - <div className='column-settings__row'> - <SettingToggle prefix='notifications_desktop' settings={settings} settingKey={['alerts', 'follow']} onChange={onChange} label={alertStr} /> - {showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingKey={['alerts', 'follow']} meta={pushMeta} onChange={this.onPushChange} label={pushStr} />} - <SettingToggle prefix='notifications' settings={settings} settingKey={['shows', 'follow']} onChange={onChange} label={showStr} /> - <SettingToggle prefix='notifications' settings={settings} settingKey={['sounds', 'follow']} onChange={onChange} label={soundStr} /> - </div> - </div> - - <div role='group' aria-labelledby='notifications-favourite'> - <span id='notifications-favourite' className='column-settings__section'><FormattedMessage id='notifications.column_settings.favourite' defaultMessage='Favourites:' /></span> - - <div className='column-settings__row'> - <SettingToggle prefix='notifications_desktop' settings={settings} settingKey={['alerts', 'favourite']} onChange={onChange} label={alertStr} /> - {showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingKey={['alerts', 'favourite']} meta={pushMeta} onChange={this.onPushChange} label={pushStr} />} - <SettingToggle prefix='notifications' settings={settings} settingKey={['shows', 'favourite']} onChange={onChange} label={showStr} /> - <SettingToggle prefix='notifications' settings={settings} settingKey={['sounds', 'favourite']} onChange={onChange} label={soundStr} /> - </div> - </div> - - <div role='group' aria-labelledby='notifications-mention'> - <span id='notifications-mention' className='column-settings__section'><FormattedMessage id='notifications.column_settings.mention' defaultMessage='Mentions:' /></span> - - <div className='column-settings__row'> - <SettingToggle prefix='notifications_desktop' settings={settings} settingKey={['alerts', 'mention']} onChange={onChange} label={alertStr} /> - {showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingKey={['alerts', 'mention']} meta={pushMeta} onChange={this.onPushChange} label={pushStr} />} - <SettingToggle prefix='notifications' settings={settings} settingKey={['shows', 'mention']} onChange={onChange} label={showStr} /> - <SettingToggle prefix='notifications' settings={settings} settingKey={['sounds', 'mention']} onChange={onChange} label={soundStr} /> - </div> - </div> - - <div role='group' aria-labelledby='notifications-reblog'> - <span id='notifications-reblog' className='column-settings__section'><FormattedMessage id='notifications.column_settings.reblog' defaultMessage='Boosts:' /></span> - - <div className='column-settings__row'> - <SettingToggle prefix='notifications_desktop' settings={settings} settingKey={['alerts', 'reblog']} onChange={onChange} label={alertStr} /> - {showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingKey={['alerts', 'reblog']} meta={pushMeta} onChange={this.onPushChange} label={pushStr} />} - <SettingToggle prefix='notifications' settings={settings} settingKey={['shows', 'reblog']} onChange={onChange} label={showStr} /> - <SettingToggle prefix='notifications' settings={settings} settingKey={['sounds', 'reblog']} onChange={onChange} label={soundStr} /> - </div> - </div> - </div> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/notifications/components/follow.js b/app/javascript/themes/glitch/features/notifications/components/follow.js deleted file mode 100644 index 96cfe83e6..000000000 --- a/app/javascript/themes/glitch/features/notifications/components/follow.js +++ /dev/null @@ -1,98 +0,0 @@ -// Package imports. -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import PropTypes from 'prop-types'; -import { FormattedMessage } from 'react-intl'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import { HotKeys } from 'react-hotkeys'; - -// Our imports. -import Permalink from 'themes/glitch/components/permalink'; -import AccountContainer from 'themes/glitch/containers/account_container'; -import NotificationOverlayContainer from '../containers/overlay_container'; - -export default class NotificationFollow extends ImmutablePureComponent { - - static propTypes = { - hidden: PropTypes.bool, - id: PropTypes.string.isRequired, - account: ImmutablePropTypes.map.isRequired, - notification: ImmutablePropTypes.map.isRequired, - }; - - handleMoveUp = () => { - const { notification, onMoveUp } = this.props; - onMoveUp(notification.get('id')); - } - - handleMoveDown = () => { - const { notification, onMoveDown } = this.props; - onMoveDown(notification.get('id')); - } - - handleOpen = () => { - this.handleOpenProfile(); - } - - handleOpenProfile = () => { - const { notification } = this.props; - this.context.router.history.push(`/accounts/${notification.getIn(['account', 'id'])}`); - } - - handleMention = e => { - e.preventDefault(); - - const { notification, onMention } = this.props; - onMention(notification.get('account'), this.context.router.history); - } - - getHandlers () { - return { - moveUp: this.handleMoveUp, - moveDown: this.handleMoveDown, - open: this.handleOpen, - openProfile: this.handleOpenProfile, - mention: this.handleMention, - reply: this.handleMention, - }; - } - - render () { - const { account, notification, hidden } = this.props; - - // Links to the display name. - const displayName = account.get('display_name_html') || account.get('username'); - const link = ( - <Permalink - className='notification__display-name' - href={account.get('url')} - title={account.get('acct')} - to={`/accounts/${account.get('id')}`} - dangerouslySetInnerHTML={{ __html: displayName }} - /> - ); - - // Renders. - return ( - <HotKeys handlers={this.getHandlers()}> - <div className='notification notification-follow focusable' tabIndex='0'> - <div className='notification__message'> - <div className='notification__favourite-icon-wrapper'> - <i className='fa fa-fw fa-user-plus' /> - </div> - - <FormattedMessage - id='notification.follow' - defaultMessage='{name} followed you' - values={{ name: link }} - /> - </div> - - <AccountContainer hidden={hidden} id={account.get('id')} withNote={false} /> - <NotificationOverlayContainer notification={notification} /> - </div> - </HotKeys> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/notifications/components/notification.js b/app/javascript/themes/glitch/features/notifications/components/notification.js deleted file mode 100644 index 47f5770bf..000000000 --- a/app/javascript/themes/glitch/features/notifications/components/notification.js +++ /dev/null @@ -1,93 +0,0 @@ -// Package imports. -import React from 'react'; -import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import ImmutablePureComponent from 'react-immutable-pure-component'; - -// Our imports, -import StatusContainer from 'themes/glitch/containers/status_container'; -import NotificationFollow from './follow'; - -export default class Notification extends ImmutablePureComponent { - - static propTypes = { - notification: ImmutablePropTypes.map.isRequired, - hidden: PropTypes.bool, - onMoveUp: PropTypes.func.isRequired, - onMoveDown: PropTypes.func.isRequired, - onMention: PropTypes.func.isRequired, - }; - - render () { - const { - hidden, - notification, - onMoveDown, - onMoveUp, - onMention, - } = this.props; - - switch(notification.get('type')) { - case 'follow': - return ( - <NotificationFollow - hidden={hidden} - id={notification.get('id')} - account={notification.get('account')} - notification={notification} - onMoveDown={onMoveDown} - onMoveUp={onMoveUp} - onMention={onMention} - /> - ); - case 'mention': - return ( - <StatusContainer - containerId={notification.get('id')} - hidden={hidden} - id={notification.get('status')} - notification={notification} - onMoveDown={onMoveDown} - onMoveUp={onMoveUp} - onMention={onMention} - withDismiss - /> - ); - case 'favourite': - return ( - <StatusContainer - containerId={notification.get('id')} - hidden={hidden} - id={notification.get('status')} - account={notification.get('account')} - prepend='favourite' - muted - notification={notification} - onMoveDown={onMoveDown} - onMoveUp={onMoveUp} - onMention={onMention} - withDismiss - /> - ); - case 'reblog': - return ( - <StatusContainer - containerId={notification.get('id')} - hidden={hidden} - id={notification.get('status')} - account={notification.get('account')} - prepend='reblog' - muted - notification={notification} - onMoveDown={onMoveDown} - onMoveUp={onMoveUp} - onMention={onMention} - withDismiss - /> - ); - default: - return null; - } - } - -} diff --git a/app/javascript/themes/glitch/features/notifications/components/overlay.js b/app/javascript/themes/glitch/features/notifications/components/overlay.js deleted file mode 100644 index e56f9c628..000000000 --- a/app/javascript/themes/glitch/features/notifications/components/overlay.js +++ /dev/null @@ -1,57 +0,0 @@ -/** - * Notification overlay - */ - - -// Package imports. -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import PropTypes from 'prop-types'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import { defineMessages, injectIntl } from 'react-intl'; - -const messages = defineMessages({ - markForDeletion: { id: 'notification.markForDeletion', defaultMessage: 'Mark for deletion' }, -}); - -@injectIntl -export default class NotificationOverlay extends ImmutablePureComponent { - - static propTypes = { - notification : ImmutablePropTypes.map.isRequired, - onMarkForDelete : PropTypes.func.isRequired, - show : PropTypes.bool.isRequired, - intl : PropTypes.object.isRequired, - }; - - onToggleMark = () => { - const mark = !this.props.notification.get('markedForDelete'); - const id = this.props.notification.get('id'); - this.props.onMarkForDelete(id, mark); - } - - render () { - const { notification, show, intl } = this.props; - - const active = notification.get('markedForDelete'); - const label = intl.formatMessage(messages.markForDeletion); - - return show ? ( - <div - aria-label={label} - role='checkbox' - aria-checked={active} - tabIndex={0} - className={`notification__dismiss-overlay ${active ? 'active' : ''}`} - onClick={this.onToggleMark} - > - <div className='wrappy'> - <div className='ckbox' aria-hidden='true' title={label}> - {active ? (<i className='fa fa-check' />) : ''} - </div> - </div> - </div> - ) : null; - } - -} diff --git a/app/javascript/themes/glitch/features/notifications/components/setting_toggle.js b/app/javascript/themes/glitch/features/notifications/components/setting_toggle.js deleted file mode 100644 index 281359d2a..000000000 --- a/app/javascript/themes/glitch/features/notifications/components/setting_toggle.js +++ /dev/null @@ -1,34 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import Toggle from 'react-toggle'; - -export default class SettingToggle extends React.PureComponent { - - static propTypes = { - prefix: PropTypes.string, - settings: ImmutablePropTypes.map.isRequired, - settingKey: PropTypes.array.isRequired, - label: PropTypes.node.isRequired, - meta: PropTypes.node, - onChange: PropTypes.func.isRequired, - } - - onChange = ({ target }) => { - this.props.onChange(this.props.settingKey, target.checked); - } - - render () { - const { prefix, settings, settingKey, label, meta } = this.props; - const id = ['setting-toggle', prefix, ...settingKey].filter(Boolean).join('-'); - - return ( - <div className='setting-toggle'> - <Toggle id={id} checked={settings.getIn(settingKey)} onChange={this.onChange} onKeyDown={this.onKeyDown} /> - <label htmlFor={id} className='setting-toggle__label'>{label}</label> - {meta && <span className='setting-meta__label'>{meta}</span>} - </div> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/notifications/containers/column_settings_container.js b/app/javascript/themes/glitch/features/notifications/containers/column_settings_container.js deleted file mode 100644 index ddc8495f4..000000000 --- a/app/javascript/themes/glitch/features/notifications/containers/column_settings_container.js +++ /dev/null @@ -1,44 +0,0 @@ -import { connect } from 'react-redux'; -import { defineMessages, injectIntl } from 'react-intl'; -import ColumnSettings from '../components/column_settings'; -import { changeSetting, saveSettings } from 'themes/glitch/actions/settings'; -import { clearNotifications } from 'themes/glitch/actions/notifications'; -import { changeAlerts as changePushNotifications, saveSettings as savePushNotificationSettings } from 'themes/glitch/actions/push_notifications'; -import { openModal } from 'themes/glitch/actions/modal'; - -const messages = defineMessages({ - clearMessage: { id: 'notifications.clear_confirmation', defaultMessage: 'Are you sure you want to permanently clear all your notifications?' }, - clearConfirm: { id: 'notifications.clear', defaultMessage: 'Clear notifications' }, -}); - -const mapStateToProps = state => ({ - settings: state.getIn(['settings', 'notifications']), - pushSettings: state.get('push_notifications'), -}); - -const mapDispatchToProps = (dispatch, { intl }) => ({ - - onChange (key, checked) { - if (key[0] === 'push') { - dispatch(changePushNotifications(key.slice(1), checked)); - } else { - dispatch(changeSetting(['notifications', ...key], checked)); - } - }, - - onSave () { - dispatch(saveSettings()); - dispatch(savePushNotificationSettings()); - }, - - onClear () { - dispatch(openModal('CONFIRM', { - message: intl.formatMessage(messages.clearMessage), - confirm: intl.formatMessage(messages.clearConfirm), - onConfirm: () => dispatch(clearNotifications()), - })); - }, - -}); - -export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(ColumnSettings)); diff --git a/app/javascript/themes/glitch/features/notifications/containers/notification_container.js b/app/javascript/themes/glitch/features/notifications/containers/notification_container.js deleted file mode 100644 index 033c61e3d..000000000 --- a/app/javascript/themes/glitch/features/notifications/containers/notification_container.js +++ /dev/null @@ -1,26 +0,0 @@ -// Package imports. -import { connect } from 'react-redux'; - -// Our imports. -import { makeGetNotification } from 'themes/glitch/selectors'; -import Notification from '../components/notification'; -import { mentionCompose } from 'themes/glitch/actions/compose'; - -const makeMapStateToProps = () => { - const getNotification = makeGetNotification(); - - const mapStateToProps = (state, props) => ({ - notification: getNotification(state, props.notification, props.accountId), - notifCleaning: state.getIn(['notifications', 'cleaningMode']), - }); - - return mapStateToProps; -}; - -const mapDispatchToProps = dispatch => ({ - onMention: (account, router) => { - dispatch(mentionCompose(account, router)); - }, -}); - -export default connect(makeMapStateToProps, mapDispatchToProps)(Notification); diff --git a/app/javascript/themes/glitch/features/notifications/containers/overlay_container.js b/app/javascript/themes/glitch/features/notifications/containers/overlay_container.js deleted file mode 100644 index 52649cdd7..000000000 --- a/app/javascript/themes/glitch/features/notifications/containers/overlay_container.js +++ /dev/null @@ -1,18 +0,0 @@ -// Package imports. -import { connect } from 'react-redux'; - -// Our imports. -import NotificationOverlay from '../components/overlay'; -import { markNotificationForDelete } from 'themes/glitch/actions/notifications'; - -const mapDispatchToProps = dispatch => ({ - onMarkForDelete(id, yes) { - dispatch(markNotificationForDelete(id, yes)); - }, -}); - -const mapStateToProps = state => ({ - show: state.getIn(['notifications', 'cleaningMode']), -}); - -export default connect(mapStateToProps, mapDispatchToProps)(NotificationOverlay); diff --git a/app/javascript/themes/glitch/features/notifications/index.js b/app/javascript/themes/glitch/features/notifications/index.js deleted file mode 100644 index 1ecde660a..000000000 --- a/app/javascript/themes/glitch/features/notifications/index.js +++ /dev/null @@ -1,193 +0,0 @@ -import React from 'react'; -import { connect } from 'react-redux'; -import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import Column from 'themes/glitch/components/column'; -import ColumnHeader from 'themes/glitch/components/column_header'; -import { - enterNotificationClearingMode, - expandNotifications, - scrollTopNotifications, -} from 'themes/glitch/actions/notifications'; -import { addColumn, removeColumn, moveColumn } from 'themes/glitch/actions/columns'; -import NotificationContainer from './containers/notification_container'; -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; -import ColumnSettingsContainer from './containers/column_settings_container'; -import { createSelector } from 'reselect'; -import { List as ImmutableList } from 'immutable'; -import { debounce } from 'lodash'; -import ScrollableList from 'themes/glitch/components/scrollable_list'; - -const messages = defineMessages({ - title: { id: 'column.notifications', defaultMessage: 'Notifications' }, -}); - -const getNotifications = createSelector([ - state => ImmutableList(state.getIn(['settings', 'notifications', 'shows']).filter(item => !item).keys()), - state => state.getIn(['notifications', 'items']), -], (excludedTypes, notifications) => notifications.filterNot(item => excludedTypes.includes(item.get('type')))); - -const mapStateToProps = state => ({ - notifications: getNotifications(state), - localSettings: state.get('local_settings'), - isLoading: state.getIn(['notifications', 'isLoading'], true), - isUnread: state.getIn(['notifications', 'unread']) > 0, - hasMore: !!state.getIn(['notifications', 'next']), - notifCleaningActive: state.getIn(['notifications', 'cleaningMode']), -}); - -/* glitch */ -const mapDispatchToProps = dispatch => ({ - onEnterCleaningMode(yes) { - dispatch(enterNotificationClearingMode(yes)); - }, - dispatch, -}); - -@connect(mapStateToProps, mapDispatchToProps) -@injectIntl -export default class Notifications extends React.PureComponent { - - static propTypes = { - columnId: PropTypes.string, - notifications: ImmutablePropTypes.list.isRequired, - dispatch: PropTypes.func.isRequired, - shouldUpdateScroll: PropTypes.func, - intl: PropTypes.object.isRequired, - isLoading: PropTypes.bool, - isUnread: PropTypes.bool, - multiColumn: PropTypes.bool, - hasMore: PropTypes.bool, - localSettings: ImmutablePropTypes.map, - notifCleaningActive: PropTypes.bool, - onEnterCleaningMode: PropTypes.func, - }; - - static defaultProps = { - trackScroll: true, - }; - - handleScrollToBottom = debounce(() => { - this.props.dispatch(scrollTopNotifications(false)); - this.props.dispatch(expandNotifications()); - }, 300, { leading: true }); - - handleScrollToTop = debounce(() => { - this.props.dispatch(scrollTopNotifications(true)); - }, 100); - - handleScroll = debounce(() => { - this.props.dispatch(scrollTopNotifications(false)); - }, 100); - - handlePin = () => { - const { columnId, dispatch } = this.props; - - if (columnId) { - dispatch(removeColumn(columnId)); - } else { - dispatch(addColumn('NOTIFICATIONS', {})); - } - } - - handleMove = (dir) => { - const { columnId, dispatch } = this.props; - dispatch(moveColumn(columnId, dir)); - } - - handleHeaderClick = () => { - this.column.scrollTop(); - } - - setColumnRef = c => { - this.column = c; - } - - handleMoveUp = id => { - const elementIndex = this.props.notifications.findIndex(item => item.get('id') === id) - 1; - this._selectChild(elementIndex); - } - - handleMoveDown = id => { - const elementIndex = this.props.notifications.findIndex(item => item.get('id') === id) + 1; - this._selectChild(elementIndex); - } - - _selectChild (index) { - const element = this.column.node.querySelector(`article:nth-of-type(${index + 1}) .focusable`); - - if (element) { - element.focus(); - } - } - - render () { - const { intl, notifications, shouldUpdateScroll, isLoading, isUnread, columnId, multiColumn, hasMore } = this.props; - const pinned = !!columnId; - const emptyMessage = <FormattedMessage id='empty_column.notifications' defaultMessage="You don't have any notifications yet. Interact with others to start the conversation." />; - - let scrollableContent = null; - - if (isLoading && this.scrollableContent) { - scrollableContent = this.scrollableContent; - } else if (notifications.size > 0 || hasMore) { - scrollableContent = notifications.map((item) => ( - <NotificationContainer - key={item.get('id')} - notification={item} - accountId={item.get('account')} - onMoveUp={this.handleMoveUp} - onMoveDown={this.handleMoveDown} - /> - )); - } else { - scrollableContent = null; - } - - this.scrollableContent = scrollableContent; - - const scrollContainer = ( - <ScrollableList - scrollKey={`notifications-${columnId}`} - trackScroll={!pinned} - isLoading={isLoading} - hasMore={hasMore} - emptyMessage={emptyMessage} - onScrollToBottom={this.handleScrollToBottom} - onScrollToTop={this.handleScrollToTop} - onScroll={this.handleScroll} - shouldUpdateScroll={shouldUpdateScroll} - > - {scrollableContent} - </ScrollableList> - ); - - return ( - <Column - ref={this.setColumnRef} - name='notifications' - extraClasses={this.props.notifCleaningActive ? 'notif-cleaning' : null} - > - <ColumnHeader - icon='bell' - active={isUnread} - title={intl.formatMessage(messages.title)} - onPin={this.handlePin} - onMove={this.handleMove} - onClick={this.handleHeaderClick} - pinned={pinned} - multiColumn={multiColumn} - localSettings={this.props.localSettings} - notifCleaning - notifCleaningActive={this.props.notifCleaningActive} // this is used to toggle the header text - onEnterCleaningMode={this.props.onEnterCleaningMode} - > - <ColumnSettingsContainer /> - </ColumnHeader> - - {scrollContainer} - </Column> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/pinned_statuses/index.js b/app/javascript/themes/glitch/features/pinned_statuses/index.js deleted file mode 100644 index 0a3997850..000000000 --- a/app/javascript/themes/glitch/features/pinned_statuses/index.js +++ /dev/null @@ -1,59 +0,0 @@ -import React from 'react'; -import { connect } from 'react-redux'; -import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import { fetchPinnedStatuses } from 'themes/glitch/actions/pin_statuses'; -import Column from 'themes/glitch/features/ui/components/column'; -import ColumnBackButtonSlim from 'themes/glitch/components/column_back_button_slim'; -import StatusList from 'themes/glitch/components/status_list'; -import { defineMessages, injectIntl } from 'react-intl'; -import ImmutablePureComponent from 'react-immutable-pure-component'; - -const messages = defineMessages({ - heading: { id: 'column.pins', defaultMessage: 'Pinned toot' }, -}); - -const mapStateToProps = state => ({ - statusIds: state.getIn(['status_lists', 'pins', 'items']), - hasMore: !!state.getIn(['status_lists', 'pins', 'next']), -}); - -@connect(mapStateToProps) -@injectIntl -export default class PinnedStatuses extends ImmutablePureComponent { - - static propTypes = { - dispatch: PropTypes.func.isRequired, - statusIds: ImmutablePropTypes.list.isRequired, - intl: PropTypes.object.isRequired, - hasMore: PropTypes.bool.isRequired, - }; - - componentWillMount () { - this.props.dispatch(fetchPinnedStatuses()); - } - - handleHeaderClick = () => { - this.column.scrollTop(); - } - - setRef = c => { - this.column = c; - } - - render () { - const { intl, statusIds, hasMore } = this.props; - - return ( - <Column icon='thumb-tack' heading={intl.formatMessage(messages.heading)} ref={this.setRef}> - <ColumnBackButtonSlim /> - <StatusList - statusIds={statusIds} - scrollKey='pinned_statuses' - hasMore={hasMore} - /> - </Column> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/public_timeline/containers/column_settings_container.js b/app/javascript/themes/glitch/features/public_timeline/containers/column_settings_container.js deleted file mode 100644 index 0185a7724..000000000 --- a/app/javascript/themes/glitch/features/public_timeline/containers/column_settings_container.js +++ /dev/null @@ -1,17 +0,0 @@ -import { connect } from 'react-redux'; -import ColumnSettings from 'themes/glitch/features/community_timeline/components/column_settings'; -import { changeSetting } from 'themes/glitch/actions/settings'; - -const mapStateToProps = state => ({ - settings: state.getIn(['settings', 'public']), -}); - -const mapDispatchToProps = dispatch => ({ - - onChange (key, checked) { - dispatch(changeSetting(['public', ...key], checked)); - }, - -}); - -export default connect(mapStateToProps, mapDispatchToProps)(ColumnSettings); diff --git a/app/javascript/themes/glitch/features/public_timeline/index.js b/app/javascript/themes/glitch/features/public_timeline/index.js deleted file mode 100644 index f5b3865af..000000000 --- a/app/javascript/themes/glitch/features/public_timeline/index.js +++ /dev/null @@ -1,107 +0,0 @@ -import React from 'react'; -import { connect } from 'react-redux'; -import PropTypes from 'prop-types'; -import StatusListContainer from 'themes/glitch/features/ui/containers/status_list_container'; -import Column from 'themes/glitch/components/column'; -import ColumnHeader from 'themes/glitch/components/column_header'; -import { - refreshPublicTimeline, - expandPublicTimeline, -} from 'themes/glitch/actions/timelines'; -import { addColumn, removeColumn, moveColumn } from 'themes/glitch/actions/columns'; -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; -import ColumnSettingsContainer from './containers/column_settings_container'; -import { connectPublicStream } from 'themes/glitch/actions/streaming'; - -const messages = defineMessages({ - title: { id: 'column.public', defaultMessage: 'Federated timeline' }, -}); - -const mapStateToProps = state => ({ - hasUnread: state.getIn(['timelines', 'public', 'unread']) > 0, -}); - -@connect(mapStateToProps) -@injectIntl -export default class PublicTimeline extends React.PureComponent { - - static propTypes = { - dispatch: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - columnId: PropTypes.string, - multiColumn: PropTypes.bool, - hasUnread: PropTypes.bool, - }; - - handlePin = () => { - const { columnId, dispatch } = this.props; - - if (columnId) { - dispatch(removeColumn(columnId)); - } else { - dispatch(addColumn('PUBLIC', {})); - } - } - - handleMove = (dir) => { - const { columnId, dispatch } = this.props; - dispatch(moveColumn(columnId, dir)); - } - - handleHeaderClick = () => { - this.column.scrollTop(); - } - - componentDidMount () { - const { dispatch } = this.props; - - dispatch(refreshPublicTimeline()); - this.disconnect = dispatch(connectPublicStream()); - } - - componentWillUnmount () { - if (this.disconnect) { - this.disconnect(); - this.disconnect = null; - } - } - - setRef = c => { - this.column = c; - } - - handleLoadMore = () => { - this.props.dispatch(expandPublicTimeline()); - } - - render () { - const { intl, columnId, hasUnread, multiColumn } = this.props; - const pinned = !!columnId; - - return ( - <Column ref={this.setRef} name='federated'> - <ColumnHeader - icon='globe' - active={hasUnread} - title={intl.formatMessage(messages.title)} - onPin={this.handlePin} - onMove={this.handleMove} - onClick={this.handleHeaderClick} - pinned={pinned} - multiColumn={multiColumn} - > - <ColumnSettingsContainer /> - </ColumnHeader> - - <StatusListContainer - timelineId='public' - loadMore={this.handleLoadMore} - trackScroll={!pinned} - scrollKey={`public_timeline-${columnId}`} - emptyMessage={<FormattedMessage id='empty_column.public' defaultMessage='There is nothing here! Write something publicly, or manually follow users from other instances to fill it up' />} - /> - </Column> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/reblogs/index.js b/app/javascript/themes/glitch/features/reblogs/index.js deleted file mode 100644 index 8723f7c7c..000000000 --- a/app/javascript/themes/glitch/features/reblogs/index.js +++ /dev/null @@ -1,60 +0,0 @@ -import React from 'react'; -import { connect } from 'react-redux'; -import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import LoadingIndicator from 'themes/glitch/components/loading_indicator'; -import { fetchReblogs } from 'themes/glitch/actions/interactions'; -import { ScrollContainer } from 'react-router-scroll-4'; -import AccountContainer from 'themes/glitch/containers/account_container'; -import Column from 'themes/glitch/features/ui/components/column'; -import ColumnBackButton from 'themes/glitch/components/column_back_button'; -import ImmutablePureComponent from 'react-immutable-pure-component'; - -const mapStateToProps = (state, props) => ({ - accountIds: state.getIn(['user_lists', 'reblogged_by', props.params.statusId]), -}); - -@connect(mapStateToProps) -export default class Reblogs extends ImmutablePureComponent { - - static propTypes = { - params: PropTypes.object.isRequired, - dispatch: PropTypes.func.isRequired, - accountIds: ImmutablePropTypes.list, - }; - - componentWillMount () { - this.props.dispatch(fetchReblogs(this.props.params.statusId)); - } - - componentWillReceiveProps(nextProps) { - if (nextProps.params.statusId !== this.props.params.statusId && nextProps.params.statusId) { - this.props.dispatch(fetchReblogs(nextProps.params.statusId)); - } - } - - render () { - const { accountIds } = this.props; - - if (!accountIds) { - return ( - <Column> - <LoadingIndicator /> - </Column> - ); - } - - return ( - <Column> - <ColumnBackButton /> - - <ScrollContainer scrollKey='reblogs'> - <div className='scrollable reblogs'> - {accountIds.map(id => <AccountContainer key={id} id={id} withNote={false} />)} - </div> - </ScrollContainer> - </Column> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/report/components/status_check_box.js b/app/javascript/themes/glitch/features/report/components/status_check_box.js deleted file mode 100644 index cc9232201..000000000 --- a/app/javascript/themes/glitch/features/report/components/status_check_box.js +++ /dev/null @@ -1,37 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import Toggle from 'react-toggle'; - -export default class StatusCheckBox extends React.PureComponent { - - static propTypes = { - status: ImmutablePropTypes.map.isRequired, - checked: PropTypes.bool, - onToggle: PropTypes.func.isRequired, - disabled: PropTypes.bool, - }; - - render () { - const { status, checked, onToggle, disabled } = this.props; - const content = { __html: status.get('contentHtml') }; - - if (status.get('reblog')) { - return null; - } - - return ( - <div className='status-check-box'> - <div - className='status__content' - dangerouslySetInnerHTML={content} - /> - - <div className='status-check-box-toggle'> - <Toggle checked={checked} onChange={onToggle} disabled={disabled} /> - </div> - </div> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/report/containers/status_check_box_container.js b/app/javascript/themes/glitch/features/report/containers/status_check_box_container.js deleted file mode 100644 index 40d55fb3c..000000000 --- a/app/javascript/themes/glitch/features/report/containers/status_check_box_container.js +++ /dev/null @@ -1,19 +0,0 @@ -import { connect } from 'react-redux'; -import StatusCheckBox from '../components/status_check_box'; -import { toggleStatusReport } from 'themes/glitch/actions/reports'; -import { Set as ImmutableSet } from 'immutable'; - -const mapStateToProps = (state, { id }) => ({ - status: state.getIn(['statuses', id]), - checked: state.getIn(['reports', 'new', 'status_ids'], ImmutableSet()).includes(id), -}); - -const mapDispatchToProps = (dispatch, { id }) => ({ - - onToggle (e) { - dispatch(toggleStatusReport(id, e.target.checked)); - }, - -}); - -export default connect(mapStateToProps, mapDispatchToProps)(StatusCheckBox); diff --git a/app/javascript/themes/glitch/features/standalone/compose/index.js b/app/javascript/themes/glitch/features/standalone/compose/index.js deleted file mode 100644 index 8a8118178..000000000 --- a/app/javascript/themes/glitch/features/standalone/compose/index.js +++ /dev/null @@ -1,20 +0,0 @@ -import React from 'react'; -import ComposeFormContainer from 'themes/glitch/features/compose/containers/compose_form_container'; -import NotificationsContainer from 'themes/glitch/features/ui/containers/notifications_container'; -import LoadingBarContainer from 'themes/glitch/features/ui/containers/loading_bar_container'; -import ModalContainer from 'themes/glitch/features/ui/containers/modal_container'; - -export default class Compose extends React.PureComponent { - - render () { - return ( - <div> - <ComposeFormContainer /> - <NotificationsContainer /> - <ModalContainer /> - <LoadingBarContainer className='loading-bar' /> - </div> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/standalone/hashtag_timeline/index.js b/app/javascript/themes/glitch/features/standalone/hashtag_timeline/index.js deleted file mode 100644 index 7c56f264f..000000000 --- a/app/javascript/themes/glitch/features/standalone/hashtag_timeline/index.js +++ /dev/null @@ -1,70 +0,0 @@ -import React from 'react'; -import { connect } from 'react-redux'; -import PropTypes from 'prop-types'; -import StatusListContainer from 'themes/glitch/features/ui/containers/status_list_container'; -import { - refreshHashtagTimeline, - expandHashtagTimeline, -} from 'themes/glitch/actions/timelines'; -import Column from 'themes/glitch/components/column'; -import ColumnHeader from 'themes/glitch/components/column_header'; - -@connect() -export default class HashtagTimeline extends React.PureComponent { - - static propTypes = { - dispatch: PropTypes.func.isRequired, - hashtag: PropTypes.string.isRequired, - }; - - handleHeaderClick = () => { - this.column.scrollTop(); - } - - setRef = c => { - this.column = c; - } - - componentDidMount () { - const { dispatch, hashtag } = this.props; - - dispatch(refreshHashtagTimeline(hashtag)); - - this.polling = setInterval(() => { - dispatch(refreshHashtagTimeline(hashtag)); - }, 10000); - } - - componentWillUnmount () { - if (typeof this.polling !== 'undefined') { - clearInterval(this.polling); - this.polling = null; - } - } - - handleLoadMore = () => { - this.props.dispatch(expandHashtagTimeline(this.props.hashtag)); - } - - render () { - const { hashtag } = this.props; - - return ( - <Column ref={this.setRef}> - <ColumnHeader - icon='hashtag' - title={hashtag} - onClick={this.handleHeaderClick} - /> - - <StatusListContainer - trackScroll={false} - scrollKey='standalone_hashtag_timeline' - timelineId={`hashtag:${hashtag}`} - loadMore={this.handleLoadMore} - /> - </Column> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/standalone/public_timeline/index.js b/app/javascript/themes/glitch/features/standalone/public_timeline/index.js deleted file mode 100644 index b3fb55288..000000000 --- a/app/javascript/themes/glitch/features/standalone/public_timeline/index.js +++ /dev/null @@ -1,76 +0,0 @@ -import React from 'react'; -import { connect } from 'react-redux'; -import PropTypes from 'prop-types'; -import StatusListContainer from 'themes/glitch/features/ui/containers/status_list_container'; -import { - refreshPublicTimeline, - expandPublicTimeline, -} from 'themes/glitch/actions/timelines'; -import Column from 'themes/glitch/components/column'; -import ColumnHeader from 'themes/glitch/components/column_header'; -import { defineMessages, injectIntl } from 'react-intl'; - -const messages = defineMessages({ - title: { id: 'standalone.public_title', defaultMessage: 'A look inside...' }, -}); - -@connect() -@injectIntl -export default class PublicTimeline extends React.PureComponent { - - static propTypes = { - dispatch: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - }; - - handleHeaderClick = () => { - this.column.scrollTop(); - } - - setRef = c => { - this.column = c; - } - - componentDidMount () { - const { dispatch } = this.props; - - dispatch(refreshPublicTimeline()); - - this.polling = setInterval(() => { - dispatch(refreshPublicTimeline()); - }, 3000); - } - - componentWillUnmount () { - if (typeof this.polling !== 'undefined') { - clearInterval(this.polling); - this.polling = null; - } - } - - handleLoadMore = () => { - this.props.dispatch(expandPublicTimeline()); - } - - render () { - const { intl } = this.props; - - return ( - <Column ref={this.setRef}> - <ColumnHeader - icon='globe' - title={intl.formatMessage(messages.title)} - onClick={this.handleHeaderClick} - /> - - <StatusListContainer - timelineId='public' - loadMore={this.handleLoadMore} - scrollKey='standalone_public_timeline' - trackScroll={false} - /> - </Column> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/status/components/action_bar.js b/app/javascript/themes/glitch/features/status/components/action_bar.js deleted file mode 100644 index 6cda988d1..000000000 --- a/app/javascript/themes/glitch/features/status/components/action_bar.js +++ /dev/null @@ -1,129 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import IconButton from 'themes/glitch/components/icon_button'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import DropdownMenuContainer from 'themes/glitch/containers/dropdown_menu_container'; -import { defineMessages, injectIntl } from 'react-intl'; -import { me } from 'themes/glitch/util/initial_state'; - -const messages = defineMessages({ - delete: { id: 'status.delete', defaultMessage: 'Delete' }, - mention: { id: 'status.mention', defaultMessage: 'Mention @{name}' }, - reply: { id: 'status.reply', defaultMessage: 'Reply' }, - reblog: { id: 'status.reblog', defaultMessage: 'Boost' }, - cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' }, - favourite: { id: 'status.favourite', defaultMessage: 'Favourite' }, - report: { id: 'status.report', defaultMessage: 'Report @{name}' }, - share: { id: 'status.share', defaultMessage: 'Share' }, - pin: { id: 'status.pin', defaultMessage: 'Pin on profile' }, - unpin: { id: 'status.unpin', defaultMessage: 'Unpin from profile' }, - embed: { id: 'status.embed', defaultMessage: 'Embed' }, -}); - -@injectIntl -export default class ActionBar extends React.PureComponent { - - static contextTypes = { - router: PropTypes.object, - }; - - static propTypes = { - status: ImmutablePropTypes.map.isRequired, - onReply: PropTypes.func.isRequired, - onReblog: PropTypes.func.isRequired, - onFavourite: PropTypes.func.isRequired, - onDelete: PropTypes.func.isRequired, - onMention: PropTypes.func.isRequired, - onReport: PropTypes.func, - onPin: PropTypes.func, - onEmbed: PropTypes.func, - intl: PropTypes.object.isRequired, - }; - - handleReplyClick = () => { - this.props.onReply(this.props.status); - } - - handleReblogClick = (e) => { - this.props.onReblog(this.props.status, e); - } - - handleFavouriteClick = () => { - this.props.onFavourite(this.props.status); - } - - handleDeleteClick = () => { - this.props.onDelete(this.props.status); - } - - handleMentionClick = () => { - this.props.onMention(this.props.status.get('account'), this.context.router.history); - } - - handleReport = () => { - this.props.onReport(this.props.status); - } - - handlePinClick = () => { - this.props.onPin(this.props.status); - } - - handleShare = () => { - navigator.share({ - text: this.props.status.get('search_index'), - url: this.props.status.get('url'), - }); - } - - handleEmbed = () => { - this.props.onEmbed(this.props.status); - } - - render () { - const { status, intl } = this.props; - - const publicStatus = ['public', 'unlisted'].includes(status.get('visibility')); - - let menu = []; - - if (publicStatus) { - menu.push({ text: intl.formatMessage(messages.embed), action: this.handleEmbed }); - } - - if (me === status.getIn(['account', 'id'])) { - if (publicStatus) { - menu.push({ text: intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin), action: this.handlePinClick }); - } - - menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick }); - } else { - menu.push({ text: intl.formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }), action: this.handleMentionClick }); - menu.push(null); - menu.push({ text: intl.formatMessage(messages.report, { name: status.getIn(['account', 'username']) }), action: this.handleReport }); - } - - const shareButton = ('share' in navigator) && status.get('visibility') === 'public' && ( - <div className='detailed-status__button'><IconButton title={intl.formatMessage(messages.share)} icon='share-alt' onClick={this.handleShare} /></div> - ); - - let reblogIcon = 'retweet'; - //if (status.get('visibility') === 'direct') reblogIcon = 'envelope'; - // else if (status.get('visibility') === 'private') reblogIcon = 'lock'; - - let reblog_disabled = (status.get('visibility') === 'direct' || status.get('visibility') === 'private'); - - return ( - <div className='detailed-status__action-bar'> - <div className='detailed-status__button'><IconButton title={intl.formatMessage(messages.reply)} icon={status.get('in_reply_to_id', null) === null ? 'reply' : 'reply-all'} onClick={this.handleReplyClick} /></div> - <div className='detailed-status__button'><IconButton disabled={reblog_disabled} active={status.get('reblogged')} title={reblog_disabled ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.reblog)} icon={reblogIcon} onClick={this.handleReblogClick} /></div> - <div className='detailed-status__button'><IconButton animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} activeStyle={{ color: '#ca8f04' }} /></div> - {shareButton} - - <div className='detailed-status__action-bar-dropdown'> - <DropdownMenuContainer size={18} icon='ellipsis-h' items={menu} direction='left' ariaLabel='More' /> - </div> - </div> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/status/components/card.js b/app/javascript/themes/glitch/features/status/components/card.js deleted file mode 100644 index bb83374b9..000000000 --- a/app/javascript/themes/glitch/features/status/components/card.js +++ /dev/null @@ -1,125 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import punycode from 'punycode'; -import classnames from 'classnames'; - -const IDNA_PREFIX = 'xn--'; - -const decodeIDNA = domain => { - return domain - .split('.') - .map(part => part.indexOf(IDNA_PREFIX) === 0 ? punycode.decode(part.slice(IDNA_PREFIX.length)) : part) - .join('.'); -}; - -const getHostname = url => { - const parser = document.createElement('a'); - parser.href = url; - return parser.hostname; -}; - -export default class Card extends React.PureComponent { - - static propTypes = { - card: ImmutablePropTypes.map, - maxDescription: PropTypes.number, - }; - - static defaultProps = { - maxDescription: 50, - }; - - state = { - width: 0, - }; - - renderLink () { - const { card, maxDescription } = this.props; - - let image = ''; - let provider = card.get('provider_name'); - - if (card.get('image')) { - image = ( - <div className='status-card__image'> - <img src={card.get('image')} alt={card.get('title')} className='status-card__image-image' width={card.get('width')} height={card.get('height')} /> - </div> - ); - } - - if (provider.length < 1) { - provider = decodeIDNA(getHostname(card.get('url'))); - } - - const className = classnames('status-card', { - 'horizontal': card.get('width') > card.get('height'), - }); - - return ( - <a href={card.get('url')} className={className} target='_blank' rel='noopener'> - {image} - - <div className='status-card__content'> - <strong className='status-card__title' title={card.get('title')}>{card.get('title')}</strong> - <p className='status-card__description'>{(card.get('description') || '').substring(0, maxDescription)}</p> - <span className='status-card__host'>{provider}</span> - </div> - </a> - ); - } - - renderPhoto () { - const { card } = this.props; - - return ( - <a href={card.get('url')} className='status-card-photo' target='_blank' rel='noopener'> - <img src={card.get('url')} alt={card.get('title')} width={card.get('width')} height={card.get('height')} /> - </a> - ); - } - - setRef = c => { - if (c) { - this.setState({ width: c.offsetWidth }); - } - } - - renderVideo () { - const { card } = this.props; - const content = { __html: card.get('html') }; - const { width } = this.state; - const ratio = card.get('width') / card.get('height'); - const height = card.get('width') > card.get('height') ? (width / ratio) : (width * ratio); - - return ( - <div - ref={this.setRef} - className='status-card-video' - dangerouslySetInnerHTML={content} - style={{ height }} - /> - ); - } - - render () { - const { card } = this.props; - - if (card === null) { - return null; - } - - switch(card.get('type')) { - case 'link': - return this.renderLink(); - case 'photo': - return this.renderPhoto(); - case 'video': - return this.renderVideo(); - case 'rich': - default: - return null; - } - } - -} diff --git a/app/javascript/themes/glitch/features/status/components/detailed_status.js b/app/javascript/themes/glitch/features/status/components/detailed_status.js deleted file mode 100644 index df78ce4b6..000000000 --- a/app/javascript/themes/glitch/features/status/components/detailed_status.js +++ /dev/null @@ -1,130 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import Avatar from 'themes/glitch/components/avatar'; -import DisplayName from 'themes/glitch/components/display_name'; -import StatusContent from 'themes/glitch/components/status_content'; -import StatusGallery from 'themes/glitch/components/media_gallery'; -import AttachmentList from 'themes/glitch/components/attachment_list'; -import { Link } from 'react-router-dom'; -import { FormattedDate, FormattedNumber } from 'react-intl'; -import CardContainer from '../containers/card_container'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import Video from 'themes/glitch/features/video'; -import VisibilityIcon from 'themes/glitch/components/status_visibility_icon'; - -export default class DetailedStatus extends ImmutablePureComponent { - - static contextTypes = { - router: PropTypes.object, - }; - - static propTypes = { - status: ImmutablePropTypes.map.isRequired, - settings: ImmutablePropTypes.map.isRequired, - onOpenMedia: PropTypes.func.isRequired, - onOpenVideo: PropTypes.func.isRequired, - }; - - handleAccountClick = (e) => { - if (e.button === 0) { - e.preventDefault(); - this.context.router.history.push(`/accounts/${this.props.status.getIn(['account', 'id'])}`); - } - - e.stopPropagation(); - } - - // handleOpenVideo = startTime => { - // this.props.onOpenVideo(this.props.status.getIn(['media_attachments', 0]), startTime); - // } - - render () { - const status = this.props.status.get('reblog') ? this.props.status.get('reblog') : this.props.status; - const { expanded, setExpansion, settings } = this.props; - - let media = ''; - let mediaIcon = null; - let applicationLink = ''; - let reblogLink = ''; - let reblogIcon = 'retweet'; - - if (status.get('media_attachments').size > 0) { - if (status.get('media_attachments').some(item => item.get('type') === 'unknown')) { - media = <AttachmentList media={status.get('media_attachments')} />; - } else if (status.getIn(['media_attachments', 0, 'type']) === 'video') { - media = ( - <Video - sensitive={status.get('sensitive')} - media={status.getIn(['media_attachments', 0])} - letterbox={settings.getIn(['media', 'letterbox'])} - fullwidth={settings.getIn(['media', 'fullwidth'])} - onOpenVideo={this.props.onOpenVideo} - autoplay - /> - ); - mediaIcon = 'video-camera'; - } else { - media = ( - <StatusGallery - sensitive={status.get('sensitive')} - media={status.get('media_attachments')} - letterbox={settings.getIn(['media', 'letterbox'])} - onOpenMedia={this.props.onOpenMedia} - /> - ); - mediaIcon = 'picture-o'; - } - } else media = <CardContainer statusId={status.get('id')} />; - - if (status.get('application')) { - applicationLink = <span> · <a className='detailed-status__application' href={status.getIn(['application', 'website'])} target='_blank' rel='noopener'>{status.getIn(['application', 'name'])}</a></span>; - } - - if (status.get('visibility') === 'direct') { - reblogIcon = 'envelope'; - } else if (status.get('visibility') === 'private') { - reblogIcon = 'lock'; - } - - if (status.get('visibility') === 'private') { - reblogLink = <i className={`fa fa-${reblogIcon}`} />; - } else { - reblogLink = (<Link to={`/statuses/${status.get('id')}/reblogs`} className='detailed-status__link'> - <i className={`fa fa-${reblogIcon}`} /> - <span className='detailed-status__reblogs'> - <FormattedNumber value={status.get('reblogs_count')} /> - </span> - </Link>); - } - - return ( - <div className='detailed-status'> - <a href={status.getIn(['account', 'url'])} onClick={this.handleAccountClick} className='detailed-status__display-name'> - <div className='detailed-status__display-avatar'><Avatar account={status.get('account')} size={48} /></div> - <DisplayName account={status.get('account')} /> - </a> - - <StatusContent - status={status} - media={media} - mediaIcon={mediaIcon} - expanded={expanded} - setExpansion={setExpansion} - /> - - <div className='detailed-status__meta'> - <a className='detailed-status__datetime' href={status.get('url')} target='_blank' rel='noopener'> - <FormattedDate value={new Date(status.get('created_at'))} hour12={false} year='numeric' month='short' day='2-digit' hour='2-digit' minute='2-digit' /> - </a>{applicationLink} · {reblogLink} · <Link to={`/statuses/${status.get('id')}/favourites`} className='detailed-status__link'> - <i className='fa fa-star' /> - <span className='detailed-status__favorites'> - <FormattedNumber value={status.get('favourites_count')} /> - </span> - </Link> · <VisibilityIcon visibility={status.get('visibility')} /> - </div> - </div> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/status/containers/card_container.js b/app/javascript/themes/glitch/features/status/containers/card_container.js deleted file mode 100644 index a97404de1..000000000 --- a/app/javascript/themes/glitch/features/status/containers/card_container.js +++ /dev/null @@ -1,8 +0,0 @@ -import { connect } from 'react-redux'; -import Card from '../components/card'; - -const mapStateToProps = (state, { statusId }) => ({ - card: state.getIn(['cards', statusId], null), -}); - -export default connect(mapStateToProps)(Card); diff --git a/app/javascript/themes/glitch/features/status/index.js b/app/javascript/themes/glitch/features/status/index.js deleted file mode 100644 index 8561bd4de..000000000 --- a/app/javascript/themes/glitch/features/status/index.js +++ /dev/null @@ -1,358 +0,0 @@ -import React from 'react'; -import { connect } from 'react-redux'; -import PropTypes from 'prop-types'; -import classNames from 'classnames'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import { fetchStatus } from 'themes/glitch/actions/statuses'; -import MissingIndicator from 'themes/glitch/components/missing_indicator'; -import DetailedStatus from './components/detailed_status'; -import ActionBar from './components/action_bar'; -import Column from 'themes/glitch/features/ui/components/column'; -import { - favourite, - unfavourite, - reblog, - unreblog, - pin, - unpin, -} from 'themes/glitch/actions/interactions'; -import { - replyCompose, - mentionCompose, -} from 'themes/glitch/actions/compose'; -import { deleteStatus } from 'themes/glitch/actions/statuses'; -import { initReport } from 'themes/glitch/actions/reports'; -import { makeGetStatus } from 'themes/glitch/selectors'; -import { ScrollContainer } from 'react-router-scroll-4'; -import ColumnBackButton from 'themes/glitch/components/column_back_button'; -import StatusContainer from 'themes/glitch/containers/status_container'; -import { openModal } from 'themes/glitch/actions/modal'; -import { defineMessages, injectIntl } from 'react-intl'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import { HotKeys } from 'react-hotkeys'; -import { boostModal, deleteModal } from 'themes/glitch/util/initial_state'; -import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from 'themes/glitch/util/fullscreen'; - -const messages = defineMessages({ - deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' }, - deleteMessage: { id: 'confirmations.delete.message', defaultMessage: 'Are you sure you want to delete this status?' }, -}); - -const makeMapStateToProps = () => { - const getStatus = makeGetStatus(); - - const mapStateToProps = (state, props) => ({ - status: getStatus(state, props.params.statusId), - settings: state.get('local_settings'), - ancestorsIds: state.getIn(['contexts', 'ancestors', props.params.statusId]), - descendantsIds: state.getIn(['contexts', 'descendants', props.params.statusId]), - }); - - return mapStateToProps; -}; - -@injectIntl -@connect(makeMapStateToProps) -export default class Status extends ImmutablePureComponent { - - static contextTypes = { - router: PropTypes.object, - }; - - static propTypes = { - params: PropTypes.object.isRequired, - dispatch: PropTypes.func.isRequired, - status: ImmutablePropTypes.map, - settings: ImmutablePropTypes.map.isRequired, - ancestorsIds: ImmutablePropTypes.list, - descendantsIds: ImmutablePropTypes.list, - intl: PropTypes.object.isRequired, - }; - - state = { - fullscreen: false, - isExpanded: null, - }; - - componentWillMount () { - this.props.dispatch(fetchStatus(this.props.params.statusId)); - } - - componentDidMount () { - attachFullscreenListener(this.onFullScreenChange); - } - - componentWillReceiveProps (nextProps) { - if (nextProps.params.statusId !== this.props.params.statusId && nextProps.params.statusId) { - this._scrolledIntoView = false; - this.props.dispatch(fetchStatus(nextProps.params.statusId)); - } - } - - handleExpandedToggle = () => { - if (this.props.status.get('spoiler_text')) { - this.setExpansion(this.state.isExpanded ? null : true); - } - }; - - handleFavouriteClick = (status) => { - if (status.get('favourited')) { - this.props.dispatch(unfavourite(status)); - } else { - this.props.dispatch(favourite(status)); - } - } - - handlePin = (status) => { - if (status.get('pinned')) { - this.props.dispatch(unpin(status)); - } else { - this.props.dispatch(pin(status)); - } - } - - handleReplyClick = (status) => { - this.props.dispatch(replyCompose(status, this.context.router.history)); - } - - handleModalReblog = (status) => { - this.props.dispatch(reblog(status)); - } - - handleReblogClick = (status, e) => { - if (status.get('reblogged')) { - this.props.dispatch(unreblog(status)); - } else { - if (e.shiftKey || !boostModal) { - this.handleModalReblog(status); - } else { - this.props.dispatch(openModal('BOOST', { status, onReblog: this.handleModalReblog })); - } - } - } - - handleDeleteClick = (status) => { - const { dispatch, intl } = this.props; - - if (!deleteModal) { - dispatch(deleteStatus(status.get('id'))); - } else { - dispatch(openModal('CONFIRM', { - message: intl.formatMessage(messages.deleteMessage), - confirm: intl.formatMessage(messages.deleteConfirm), - onConfirm: () => dispatch(deleteStatus(status.get('id'))), - })); - } - } - - handleMentionClick = (account, router) => { - this.props.dispatch(mentionCompose(account, router)); - } - - handleOpenMedia = (media, index) => { - this.props.dispatch(openModal('MEDIA', { media, index })); - } - - handleOpenVideo = (media, time) => { - this.props.dispatch(openModal('VIDEO', { media, time })); - } - - handleReport = (status) => { - this.props.dispatch(initReport(status.get('account'), status)); - } - - handleEmbed = (status) => { - this.props.dispatch(openModal('EMBED', { url: status.get('url') })); - } - - handleHotkeyMoveUp = () => { - this.handleMoveUp(this.props.status.get('id')); - } - - handleHotkeyMoveDown = () => { - this.handleMoveDown(this.props.status.get('id')); - } - - handleHotkeyReply = e => { - e.preventDefault(); - this.handleReplyClick(this.props.status); - } - - handleHotkeyFavourite = () => { - this.handleFavouriteClick(this.props.status); - } - - handleHotkeyBoost = () => { - this.handleReblogClick(this.props.status); - } - - handleHotkeyMention = e => { - e.preventDefault(); - this.handleMentionClick(this.props.status); - } - - handleHotkeyOpenProfile = () => { - this.context.router.history.push(`/accounts/${this.props.status.getIn(['account', 'id'])}`); - } - - handleMoveUp = id => { - const { status, ancestorsIds, descendantsIds } = this.props; - - if (id === status.get('id')) { - this._selectChild(ancestorsIds.size - 1); - } else { - let index = ancestorsIds.indexOf(id); - - if (index === -1) { - index = descendantsIds.indexOf(id); - this._selectChild(ancestorsIds.size + index); - } else { - this._selectChild(index - 1); - } - } - } - - handleMoveDown = id => { - const { status, ancestorsIds, descendantsIds } = this.props; - - if (id === status.get('id')) { - this._selectChild(ancestorsIds.size + 1); - } else { - let index = ancestorsIds.indexOf(id); - - if (index === -1) { - index = descendantsIds.indexOf(id); - this._selectChild(ancestorsIds.size + index + 2); - } else { - this._selectChild(index + 1); - } - } - } - - _selectChild (index) { - const element = this.node.querySelectorAll('.focusable')[index]; - - if (element) { - element.focus(); - } - } - - renderChildren (list) { - return list.map(id => ( - <StatusContainer - key={id} - id={id} - onMoveUp={this.handleMoveUp} - onMoveDown={this.handleMoveDown} - /> - )); - } - - setExpansion = value => { - this.setState({ isExpanded: value ? true : null }); - } - - setRef = c => { - this.node = c; - } - - componentDidUpdate () { - if (this._scrolledIntoView) { - return; - } - - const { status, ancestorsIds } = this.props; - - if (status && ancestorsIds && ancestorsIds.size > 0) { - const element = this.node.querySelectorAll('.focusable')[ancestorsIds.size - 1]; - - if (element) { - element.scrollIntoView(true); - this._scrolledIntoView = true; - } - } - } - - componentWillUnmount () { - detachFullscreenListener(this.onFullScreenChange); - } - - onFullScreenChange = () => { - this.setState({ fullscreen: isFullscreen() }); - } - - render () { - let ancestors, descendants; - const { setExpansion } = this; - const { status, settings, ancestorsIds, descendantsIds } = this.props; - const { fullscreen, isExpanded } = this.state; - - if (status === null) { - return ( - <Column> - <ColumnBackButton /> - <MissingIndicator /> - </Column> - ); - } - - if (ancestorsIds && ancestorsIds.size > 0) { - ancestors = <div>{this.renderChildren(ancestorsIds)}</div>; - } - - if (descendantsIds && descendantsIds.size > 0) { - descendants = <div>{this.renderChildren(descendantsIds)}</div>; - } - - const handlers = { - moveUp: this.handleHotkeyMoveUp, - moveDown: this.handleHotkeyMoveDown, - reply: this.handleHotkeyReply, - favourite: this.handleHotkeyFavourite, - boost: this.handleHotkeyBoost, - mention: this.handleHotkeyMention, - openProfile: this.handleHotkeyOpenProfile, - toggleSpoiler: this.handleExpandedToggle, - }; - - return ( - <Column> - <ColumnBackButton /> - - <ScrollContainer scrollKey='thread'> - <div className={classNames('scrollable', 'detailed-status__wrapper', { fullscreen })} ref={this.setRef}> - {ancestors} - - <HotKeys handlers={handlers}> - <div className='focusable' tabIndex='0'> - <DetailedStatus - status={status} - settings={settings} - onOpenVideo={this.handleOpenVideo} - onOpenMedia={this.handleOpenMedia} - expanded={isExpanded} - setExpansion={setExpansion} - /> - - <ActionBar - status={status} - onReply={this.handleReplyClick} - onFavourite={this.handleFavouriteClick} - onReblog={this.handleReblogClick} - onDelete={this.handleDeleteClick} - onMention={this.handleMentionClick} - onReport={this.handleReport} - onPin={this.handlePin} - onEmbed={this.handleEmbed} - /> - </div> - </HotKeys> - - {descendants} - </div> - </ScrollContainer> - </Column> - ); - } - -} 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 3eea63189..000000000 --- a/app/javascript/themes/glitch/features/ui/index.js +++ /dev/null @@ -1,443 +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', - toggleSpoiler: 'x', -}; - -@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> - ); - } - -} diff --git a/app/javascript/themes/glitch/features/video/index.js b/app/javascript/themes/glitch/features/video/index.js deleted file mode 100644 index 0ecbe37c9..000000000 --- a/app/javascript/themes/glitch/features/video/index.js +++ /dev/null @@ -1,288 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; -import { throttle } from 'lodash'; -import classNames from 'classnames'; -import { isFullscreen, requestFullscreen, exitFullscreen } from 'themes/glitch/util/fullscreen'; - -const messages = defineMessages({ - play: { id: 'video.play', defaultMessage: 'Play' }, - pause: { id: 'video.pause', defaultMessage: 'Pause' }, - mute: { id: 'video.mute', defaultMessage: 'Mute sound' }, - unmute: { id: 'video.unmute', defaultMessage: 'Unmute sound' }, - hide: { id: 'video.hide', defaultMessage: 'Hide video' }, - expand: { id: 'video.expand', defaultMessage: 'Expand video' }, - close: { id: 'video.close', defaultMessage: 'Close video' }, - fullscreen: { id: 'video.fullscreen', defaultMessage: 'Full screen' }, - exit_fullscreen: { id: 'video.exit_fullscreen', defaultMessage: 'Exit full screen' }, -}); - -const findElementPosition = el => { - let box; - - if (el.getBoundingClientRect && el.parentNode) { - box = el.getBoundingClientRect(); - } - - if (!box) { - return { - left: 0, - top: 0, - }; - } - - const docEl = document.documentElement; - const body = document.body; - - const clientLeft = docEl.clientLeft || body.clientLeft || 0; - const scrollLeft = window.pageXOffset || body.scrollLeft; - const left = (box.left + scrollLeft) - clientLeft; - - const clientTop = docEl.clientTop || body.clientTop || 0; - const scrollTop = window.pageYOffset || body.scrollTop; - const top = (box.top + scrollTop) - clientTop; - - return { - left: Math.round(left), - top: Math.round(top), - }; -}; - -const getPointerPosition = (el, event) => { - const position = {}; - const box = findElementPosition(el); - const boxW = el.offsetWidth; - const boxH = el.offsetHeight; - const boxY = box.top; - const boxX = box.left; - - let pageY = event.pageY; - let pageX = event.pageX; - - if (event.changedTouches) { - pageX = event.changedTouches[0].pageX; - pageY = event.changedTouches[0].pageY; - } - - position.y = Math.max(0, Math.min(1, ((boxY - pageY) + boxH) / boxH)); - position.x = Math.max(0, Math.min(1, (pageX - boxX) / boxW)); - - return position; -}; - -@injectIntl -export default class Video extends React.PureComponent { - - static propTypes = { - preview: PropTypes.string, - src: PropTypes.string.isRequired, - alt: PropTypes.string, - width: PropTypes.number, - height: PropTypes.number, - sensitive: PropTypes.bool, - startTime: PropTypes.number, - onOpenVideo: PropTypes.func, - onCloseVideo: PropTypes.func, - letterbox: PropTypes.bool, - fullwidth: PropTypes.bool, - intl: PropTypes.object.isRequired, - }; - - state = { - progress: 0, - paused: true, - dragging: false, - fullscreen: false, - hovered: false, - muted: false, - revealed: !this.props.sensitive, - }; - - setPlayerRef = c => { - this.player = c; - } - - setVideoRef = c => { - this.video = c; - } - - setSeekRef = c => { - this.seek = c; - } - - handlePlay = () => { - this.setState({ paused: false }); - } - - handlePause = () => { - this.setState({ paused: true }); - } - - handleTimeUpdate = () => { - this.setState({ progress: 100 * (this.video.currentTime / this.video.duration) }); - } - - handleMouseDown = e => { - document.addEventListener('mousemove', this.handleMouseMove, true); - document.addEventListener('mouseup', this.handleMouseUp, true); - document.addEventListener('touchmove', this.handleMouseMove, true); - document.addEventListener('touchend', this.handleMouseUp, true); - - this.setState({ dragging: true }); - this.video.pause(); - this.handleMouseMove(e); - } - - handleMouseUp = () => { - document.removeEventListener('mousemove', this.handleMouseMove, true); - document.removeEventListener('mouseup', this.handleMouseUp, true); - document.removeEventListener('touchmove', this.handleMouseMove, true); - document.removeEventListener('touchend', this.handleMouseUp, true); - - this.setState({ dragging: false }); - this.video.play(); - } - - handleMouseMove = throttle(e => { - const { x } = getPointerPosition(this.seek, e); - this.video.currentTime = this.video.duration * x; - this.setState({ progress: x * 100 }); - }, 60); - - togglePlay = () => { - if (this.state.paused) { - this.video.play(); - } else { - this.video.pause(); - } - } - - toggleFullscreen = () => { - if (isFullscreen()) { - exitFullscreen(); - } else { - requestFullscreen(this.player); - } - } - - componentDidMount () { - document.addEventListener('fullscreenchange', this.handleFullscreenChange, true); - document.addEventListener('webkitfullscreenchange', this.handleFullscreenChange, true); - document.addEventListener('mozfullscreenchange', this.handleFullscreenChange, true); - document.addEventListener('MSFullscreenChange', this.handleFullscreenChange, true); - } - - componentWillUnmount () { - document.removeEventListener('fullscreenchange', this.handleFullscreenChange, true); - document.removeEventListener('webkitfullscreenchange', this.handleFullscreenChange, true); - document.removeEventListener('mozfullscreenchange', this.handleFullscreenChange, true); - document.removeEventListener('MSFullscreenChange', this.handleFullscreenChange, true); - } - - handleFullscreenChange = () => { - this.setState({ fullscreen: isFullscreen() }); - } - - handleMouseEnter = () => { - this.setState({ hovered: true }); - } - - handleMouseLeave = () => { - this.setState({ hovered: false }); - } - - toggleMute = () => { - this.video.muted = !this.video.muted; - this.setState({ muted: this.video.muted }); - } - - toggleReveal = () => { - if (this.state.revealed) { - this.video.pause(); - } - - this.setState({ revealed: !this.state.revealed }); - } - - handleLoadedData = () => { - if (this.props.startTime) { - this.video.currentTime = this.props.startTime; - this.video.play(); - } - } - - handleProgress = () => { - if (this.video.buffered.length > 0) { - this.setState({ buffer: this.video.buffered.end(0) / this.video.duration * 100 }); - } - } - - handleOpenVideo = () => { - this.video.pause(); - this.props.onOpenVideo(this.video.currentTime); - } - - handleCloseVideo = () => { - this.video.pause(); - this.props.onCloseVideo(); - } - - render () { - const { preview, src, width, height, startTime, onOpenVideo, onCloseVideo, intl, alt, letterbox, fullwidth } = this.props; - const { progress, buffer, dragging, paused, fullscreen, hovered, muted, revealed } = this.state; - - return ( - <div className={classNames('video-player', { inactive: !revealed, inline: !fullscreen, fullscreen, letterbox, 'full-width': fullwidth })} ref={this.setPlayerRef} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}> - <video - ref={this.setVideoRef} - src={src} - poster={preview} - preload={startTime ? 'auto' : 'none'} - loop - role='button' - tabIndex='0' - aria-label={alt} - width={width} - height={height} - onClick={this.togglePlay} - onPlay={this.handlePlay} - onPause={this.handlePause} - onTimeUpdate={this.handleTimeUpdate} - onLoadedData={this.handleLoadedData} - onProgress={this.handleProgress} - /> - - <button className={classNames('video-player__spoiler', { active: !revealed })} onClick={this.toggleReveal}> - <span className='video-player__spoiler__title'><FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' /></span> - <span className='video-player__spoiler__subtitle'><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span> - </button> - - <div className={classNames('video-player__controls', { active: paused || hovered })}> - <div className='video-player__seek' onMouseDown={this.handleMouseDown} ref={this.setSeekRef}> - <div className='video-player__seek__buffer' style={{ width: `${buffer}%` }} /> - <div className='video-player__seek__progress' style={{ width: `${progress}%` }} /> - - <span - className={classNames('video-player__seek__handle', { active: dragging })} - tabIndex='0' - style={{ left: `${progress}%` }} - /> - </div> - - <div className='video-player__buttons left'> - <button aria-label={intl.formatMessage(paused ? messages.play : messages.pause)} onClick={this.togglePlay}><i className={classNames('fa fa-fw', { 'fa-play': paused, 'fa-pause': !paused })} /></button> - <button aria-label={intl.formatMessage(muted ? messages.unmute : messages.mute)} onClick={this.toggleMute}><i className={classNames('fa fa-fw', { 'fa-volume-off': muted, 'fa-volume-up': !muted })} /></button> - {!onCloseVideo && <button aria-label={intl.formatMessage(messages.hide)} onClick={this.toggleReveal}><i className='fa fa-fw fa-eye' /></button>} - </div> - - <div className='video-player__buttons right'> - {(!fullscreen && onOpenVideo) && <button aria-label={intl.formatMessage(messages.expand)} onClick={this.handleOpenVideo}><i className='fa fa-fw fa-expand' /></button>} - {onCloseVideo && <button aria-label={intl.formatMessage(messages.close)} onClick={this.handleCloseVideo}><i className='fa fa-fw fa-times' /></button>} - <button aria-label={intl.formatMessage(fullscreen ? messages.exit_fullscreen : messages.fullscreen)} onClick={this.toggleFullscreen}><i className={classNames('fa fa-fw', { 'fa-arrows-alt': !fullscreen, 'fa-compress': fullscreen })} /></button> - </div> - </div> - </div> - ); - } - -} |