diff --git a/app/soapbox/features/hashtag_timeline/index.js b/app/soapbox/features/hashtag_timeline/index.js deleted file mode 100644 index 888573579..000000000 --- a/app/soapbox/features/hashtag_timeline/index.js +++ /dev/null @@ -1,128 +0,0 @@ -import isEqual from 'lodash/isEqual'; -import PropTypes from 'prop-types'; -import React from 'react'; -import { FormattedMessage } from 'react-intl'; -import { connect } from 'react-redux'; - -import { connectHashtagStream } from '../../actions/streaming'; -import { expandHashtagTimeline, clearTimeline } from '../../actions/timelines'; -import ColumnHeader from '../../components/column_header'; -import { Column } from '../../components/ui'; -import Timeline from '../ui/components/timeline'; - -const mapStateToProps = (state, props) => ({ - hasUnread: state.getIn(['timelines', `hashtag:${props.params.id}`, 'unread']) > 0, -}); - -export default @connect(mapStateToProps) -class HashtagTimeline extends React.PureComponent { - - disconnects = []; - - static propTypes = { - params: PropTypes.object.isRequired, - dispatch: PropTypes.func.isRequired, - hasUnread: PropTypes.bool, - }; - - title = () => { - const title = [`#${this.props.params.id}`]; - - // TODO: wtf is all this? - // It exists in Mastodon's codebase, but undocumented - if (this.additionalFor('any')) { - title.push(' ', ); - } - - if (this.additionalFor('all')) { - title.push(' ', ); - } - - if (this.additionalFor('none')) { - title.push(' ', ); - } - - return title; - } - - // TODO: wtf is this? - // It exists in Mastodon's codebase, but undocumented - additionalFor = (mode) => { - const { tags } = this.props.params; - - if (tags && (tags[mode] || []).length > 0) { - return tags[mode].map(tag => tag.value).join('/'); - } else { - return ''; - } - } - - _subscribe(dispatch, id, tags = {}) { - const any = (tags.any || []).map(tag => tag.value); - const all = (tags.all || []).map(tag => tag.value); - const none = (tags.none || []).map(tag => tag.value); - - [id, ...any].map(tag => { - this.disconnects.push(dispatch(connectHashtagStream(id, tag, status => { - const tags = status.tags.map(tag => tag.name); - - return all.filter(tag => tags.includes(tag)).length === all.length && - none.filter(tag => tags.includes(tag)).length === 0; - }))); - }); - } - - _unsubscribe() { - this.disconnects.map(disconnect => disconnect()); - this.disconnects = []; - } - - componentDidMount() { - const { dispatch } = this.props; - const { id, tags } = this.props.params; - - this._subscribe(dispatch, id, tags); - dispatch(expandHashtagTimeline(id, { tags })); - } - - componentDidUpdate(prevProps) { - const { dispatch } = this.props; - const { id, tags } = this.props.params; - const { id: prevId, tags: prevTags } = prevProps.params; - - if (id !== prevId || !isEqual(tags, prevTags)) { - this._unsubscribe(); - this._subscribe(dispatch, id, tags); - dispatch(clearTimeline(`hashtag:${id}`)); - dispatch(expandHashtagTimeline(id, { tags })); - } - } - - componentWillUnmount() { - this._unsubscribe(); - } - - handleLoadMore = maxId => { - const { id, tags } = this.props.params; - this.props.dispatch(expandHashtagTimeline(id, { maxId, tags })); - } - - render() { - const { hasUnread } = this.props; - const { id } = this.props.params; - - return ( - - - } - divideType='space' - /> - - ); - } - -} diff --git a/app/soapbox/features/hashtag_timeline/index.tsx b/app/soapbox/features/hashtag_timeline/index.tsx new file mode 100644 index 000000000..a8b97cbab --- /dev/null +++ b/app/soapbox/features/hashtag_timeline/index.tsx @@ -0,0 +1,115 @@ +import React, { useEffect, useRef } from 'react'; +import { FormattedMessage } from 'react-intl'; + +import { connectHashtagStream } from 'soapbox/actions/streaming'; +import { expandHashtagTimeline, clearTimeline } from 'soapbox/actions/timelines'; +import ColumnHeader from 'soapbox/components/column_header'; +import { Column } from 'soapbox/components/ui'; +import Timeline from 'soapbox/features/ui/components/timeline'; +import { useAppDispatch, useAppSelector } from 'soapbox/hooks'; + +import type { Tag as TagEntity } from 'soapbox/types/entities'; + +type Mode = 'any' | 'all' | 'none'; + +type Tag = { value: string }; +type Tags = { [k in Mode]: Tag[] }; + +interface IHashtagTimeline { + params?: { + id?: string, + tags?: Tags, + }, +} + +export const HashtagTimeline: React.FC = ({ params }) => { + const id = params?.id || ''; + const tags = params?.tags || { any: [], all: [], none: [] }; + + const dispatch = useAppDispatch(); + const hasUnread = useAppSelector(state => (state.timelines.getIn([`hashtag:${id}`, 'unread']) as number) > 0); + const disconnects = useRef<(() => void)[]>([]); + + // Mastodon supports displaying results from multiple hashtags. + // https://github.com/mastodon/mastodon/issues/6359 + const title = () => { + const title: React.ReactNode[] = [`#${id}`]; + + if (additionalFor('any')) { + title.push(' ', ); + } + + if (additionalFor('all')) { + title.push(' ', ); + } + + if (additionalFor('none')) { + title.push(' ', ); + } + + return title; + }; + + const additionalFor = (mode: Mode) => { + if (tags && (tags[mode] || []).length > 0) { + return tags[mode].map(tag => tag.value).join('/'); + } else { + return ''; + } + }; + + const subscribe = () => { + const any = tags.any.map(tag => tag.value); + const all = tags.all.map(tag => tag.value); + const none = tags.none.map(tag => tag.value); + + [id, ...any].map(tag => { + disconnects.current.push(dispatch(connectHashtagStream(id, tag, status => { + const tags = status.tags.map((tag: TagEntity) => tag.name); + + return all.filter(tag => tags.includes(tag)).length === all.length && + none.filter(tag => tags.includes(tag)).length === 0; + }))); + }); + }; + + const unsubscribe = () => { + disconnects.current.map(disconnect => disconnect()); + disconnects.current = []; + }; + + const handleLoadMore = (maxId: string) => { + dispatch(expandHashtagTimeline(id, { maxId, tags })); + }; + + useEffect(() => { + subscribe(); + dispatch(expandHashtagTimeline(id, { tags })); + + return () => { + unsubscribe(); + }; + }, []); + + useEffect(() => { + unsubscribe(); + subscribe(); + dispatch(clearTimeline(`hashtag:${id}`)); + dispatch(expandHashtagTimeline(id, { tags })); + }, [id, tags]); + + return ( + + + } + divideType='space' + /> + + ); +}; + +export default HashtagTimeline; \ No newline at end of file