diff --git a/app/soapbox/components/ui/layout/layout.tsx b/app/soapbox/components/ui/layout/layout.tsx index d01abbc1f..59e860406 100644 --- a/app/soapbox/components/ui/layout/layout.tsx +++ b/app/soapbox/components/ui/layout/layout.tsx @@ -30,7 +30,7 @@ const Sidebar: React.FC = ({ children }) => ( const Main: React.FC> = ({ children, className }) => (
{children} diff --git a/app/soapbox/features/home_timeline/index.js b/app/soapbox/features/home_timeline/index.js deleted file mode 100644 index 01276be6d..000000000 --- a/app/soapbox/features/home_timeline/index.js +++ /dev/null @@ -1,130 +0,0 @@ -import { OrderedSet as ImmutableOrderedSet } from 'immutable'; -import PropTypes from 'prop-types'; -import React from 'react'; -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; -import { connect } from 'react-redux'; -import { Link } from 'react-router-dom'; - -import BundleContainer from 'soapbox/features/ui/containers/bundle_container'; -import { getFeatures } from 'soapbox/utils/features'; - -import { expandHomeTimeline } from '../../actions/timelines'; -import { Column } from '../../components/ui'; -import Timeline from '../ui/components/timeline'; - -function FollowRecommendationsContainer() { - return import(/* webpackChunkName: "features/follow_recommendations" */'soapbox/features/follow_recommendations/components/follow_recommendations_container'); -} - -const messages = defineMessages({ - title: { id: 'column.home', defaultMessage: 'Home' }, -}); - -const mapStateToProps = state => { - const instance = state.get('instance'); - const features = getFeatures(instance); - - return { - hasUnread: state.getIn(['timelines', 'home', 'unread']) > 0, - isPartial: state.getIn(['timelines', 'home', 'isPartial']), - siteTitle: state.getIn(['instance', 'title']), - isLoading: state.getIn(['timelines', 'home', 'isLoading'], true), - loadingFailed: state.getIn(['timelines', 'home', 'loadingFailed'], false), - isEmpty: state.getIn(['timelines', 'home', 'items'], ImmutableOrderedSet()).isEmpty(), - features, - }; -}; - -export default @connect(mapStateToProps) -@injectIntl -class HomeTimeline extends React.PureComponent { - - static propTypes = { - dispatch: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - hasUnread: PropTypes.bool, - isPartial: PropTypes.bool, - siteTitle: PropTypes.string, - isLoading: PropTypes.bool, - loadingFailed: PropTypes.bool, - isEmpty: PropTypes.bool, - features: PropTypes.object.isRequired, - }; - - state = { - done: false, - } - - handleLoadMore = maxId => { - this.props.dispatch(expandHomeTimeline({ maxId })); - } - - componentDidMount() { - this._checkIfReloadNeeded(false, this.props.isPartial); - } - - componentDidUpdate(prevProps) { - this._checkIfReloadNeeded(prevProps.isPartial, this.props.isPartial); - } - - componentWillUnmount() { - this._stopPolling(); - } - - _checkIfReloadNeeded(wasPartial, isPartial) { - const { dispatch } = this.props; - - if (wasPartial === isPartial) { - return; - } else if (!wasPartial && isPartial) { - this.polling = setInterval(() => { - dispatch(expandHomeTimeline()); - }, 3000); - } else if (wasPartial && !isPartial) { - this._stopPolling(); - } - } - - _stopPolling() { - if (this.polling) { - clearInterval(this.polling); - this.polling = null; - } - } - - handleDone = e => { - this.props.dispatch(expandHomeTimeline()); - this.setState({ done: true }); - } - - handleRefresh = () => { - const { dispatch } = this.props; - return dispatch(expandHomeTimeline()); - } - - render() { - const { intl, siteTitle, isLoading, loadingFailed, isEmpty, features } = this.props; - const { done } = this.state; - const showSuggestions = features.suggestions && isEmpty && !isLoading && !loadingFailed && !done; - - return ( - - {showSuggestions ? ( - - {Component => } - - ) : ( - }} />} - /> - )} - - ); - } - -} diff --git a/app/soapbox/features/home_timeline/index.tsx b/app/soapbox/features/home_timeline/index.tsx new file mode 100644 index 000000000..99ed0d569 --- /dev/null +++ b/app/soapbox/features/home_timeline/index.tsx @@ -0,0 +1,71 @@ +import React, { useEffect, useRef } from 'react'; +import { defineMessages, useIntl, FormattedMessage } from 'react-intl'; +import { Link } from 'react-router-dom'; + +import { expandHomeTimeline } from 'soapbox/actions/timelines'; +import { Column } from 'soapbox/components/ui'; +import Timeline from 'soapbox/features/ui/components/timeline'; +import { useAppSelector, useAppDispatch } from 'soapbox/hooks'; + +const messages = defineMessages({ + title: { id: 'column.home', defaultMessage: 'Home' }, +}); + +const HomeTimeline: React.FC = () => { + const intl = useIntl(); + const dispatch = useAppDispatch(); + const polling = useRef(null); + + const isPartial = useAppSelector(state => state.timelines.getIn(['home', 'isPartial']) === true); + const siteTitle = useAppSelector(state => state.instance.title); + + const handleLoadMore = (maxId: string) => { + dispatch(expandHomeTimeline({ maxId })); + }; + + // Mastodon generates the feed in Redis, and can return a partial timeline + // (HTTP 206) for new users. Poll until we get a full page of results. + const checkIfReloadNeeded = () => { + if (isPartial) { + polling.current = setInterval(() => { + dispatch(expandHomeTimeline()); + }, 3000); + } else { + stopPolling(); + } + }; + + const stopPolling = () => { + if (polling.current) { + clearInterval(polling.current); + polling.current = null; + } + }; + + const handleRefresh = () => { + return dispatch(expandHomeTimeline()); + }; + + useEffect(() => { + checkIfReloadNeeded(); + + return () => { + stopPolling(); + }; + }, [isPartial]); + + return ( + + }} />} + /> + + ); +}; + +export default HomeTimeline; diff --git a/app/soapbox/features/status/components/quoted_status.tsx b/app/soapbox/features/status/components/quoted_status.tsx index 1b8f25c02..bf8069aa8 100644 --- a/app/soapbox/features/status/components/quoted_status.tsx +++ b/app/soapbox/features/status/components/quoted_status.tsx @@ -1,9 +1,7 @@ import classNames from 'classnames'; -import { History } from 'history'; import React from 'react'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import { defineMessages, injectIntl, FormattedMessage, IntlShape, FormattedList } from 'react-intl'; -import { withRouter } from 'react-router-dom'; +import { defineMessages, useIntl, FormattedMessage, FormattedList } from 'react-intl'; +import { useHistory } from 'react-router-dom'; import StatusMedia from 'soapbox/components/status-media'; import { Stack, Text } from 'soapbox/components/ui'; @@ -16,49 +14,42 @@ const messages = defineMessages({ }); interface IQuotedStatus { + /** The quoted status entity. */ status?: StatusEntity, + /** Callback when cancelled (during compose). */ onCancel?: Function, - intl: IntlShape, + /** Whether the status is shown in the post composer. */ compose?: boolean, - history: History, } -class QuotedStatus extends ImmutablePureComponent { - - handleExpandClick = (e: React.MouseEvent) => { - const { compose, status } = this.props; +/** Status embedded in a quote post. */ +const QuotedStatus: React.FC = ({ status, onCancel, compose }) => { + const intl = useIntl(); + const history = useHistory(); + const handleExpandClick = (e: React.MouseEvent) => { if (!status) return; - const account = status.account as AccountEntity; if (!compose && e.button === 0) { - if (!this.props.history) { - return; - } - - this.props.history.push(`/@${account.acct}/posts/${status.id}`); - + history.push(`/@${account.acct}/posts/${status.id}`); e.stopPropagation(); e.preventDefault(); } - } + }; - handleClose = () => { - if (this.props.onCancel) { - this.props.onCancel(); + const handleClose = () => { + if (onCancel) { + onCancel(); } - } - - renderReplyMentions = () => { - const { status } = this.props; + }; + const renderReplyMentions = () => { if (!status?.in_reply_to_id) { return null; } const account = status.account as AccountEntity; - const to = status.mentions || []; if (to.size === 0) { @@ -102,58 +93,51 @@ class QuotedStatus extends ImmutablePureComponent { /> ); + }; + + if (!status) { + return null; } - render() { - const { status, onCancel, intl, compose } = this.props; + const account = status.account as AccountEntity; - if (!status) { - return null; - } - - const account = status.account as AccountEntity; - - let actions = {}; - if (onCancel) { - actions = { - onActionClick: this.handleClose, - actionIcon: require('@tabler/icons/icons/x.svg'), - actionAlignment: 'top', - actionTitle: intl.formatMessage(messages.cancel), - }; - } - - const quotedStatus = ( - - - - {this.renderReplyMentions()} - - - - - - ); - - return quotedStatus; + let actions = {}; + if (onCancel) { + actions = { + onActionClick: handleClose, + actionIcon: require('@tabler/icons/icons/x.svg'), + actionAlignment: 'top', + actionTitle: intl.formatMessage(messages.cancel), + }; } -} + return ( + + -export default withRouter(injectIntl(QuotedStatus) as any); + {renderReplyMentions()} + + + + + + ); +}; + +export default QuotedStatus; diff --git a/app/soapbox/pages/home_page.tsx b/app/soapbox/pages/home_page.tsx index c1e0b433e..20f13817d 100644 --- a/app/soapbox/pages/home_page.tsx +++ b/app/soapbox/pages/home_page.tsx @@ -36,7 +36,7 @@ const HomePage: React.FC = ({ children }) => { return ( <> - + {me && (