diff --git a/app/javascript/mastodon/features/bookmarked_statuses/index.jsx b/app/javascript/mastodon/features/bookmarked_statuses/index.jsx deleted file mode 100644 index 122baafd6c..0000000000 --- a/app/javascript/mastodon/features/bookmarked_statuses/index.jsx +++ /dev/null @@ -1,115 +0,0 @@ -import PropTypes from 'prop-types'; - -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; - -import { Helmet } from 'react-helmet'; - -import ImmutablePropTypes from 'react-immutable-proptypes'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import { connect } from 'react-redux'; - -import { debounce } from 'lodash'; - -import BookmarksIcon from '@/material-icons/400-24px/bookmarks-fill.svg?react'; -import { fetchBookmarkedStatuses, expandBookmarkedStatuses } from 'mastodon/actions/bookmarks'; -import { addColumn, removeColumn, moveColumn } from 'mastodon/actions/columns'; -import ColumnHeader from 'mastodon/components/column_header'; -import StatusList from 'mastodon/components/status_list'; -import Column from 'mastodon/features/ui/components/column'; -import { getStatusList } from 'mastodon/selectors'; - -const messages = defineMessages({ - heading: { id: 'column.bookmarks', defaultMessage: 'Bookmarks' }, -}); - -const mapStateToProps = state => ({ - statusIds: getStatusList(state, 'bookmarks'), - isLoading: state.getIn(['status_lists', 'bookmarks', 'isLoading'], true), - hasMore: !!state.getIn(['status_lists', 'bookmarks', 'next']), -}); - -class Bookmarks 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, - isLoading: PropTypes.bool, - }; - - UNSAFE_componentWillMount () { - this.props.dispatch(fetchBookmarkedStatuses()); - } - - handlePin = () => { - const { columnId, dispatch } = this.props; - - if (columnId) { - dispatch(removeColumn(columnId)); - } else { - dispatch(addColumn('BOOKMARKS', {})); - } - }; - - handleMove = (dir) => { - const { columnId, dispatch } = this.props; - dispatch(moveColumn(columnId, dir)); - }; - - handleHeaderClick = () => { - this.column.scrollTop(); - }; - - setRef = c => { - this.column = c; - }; - - handleLoadMore = debounce(() => { - this.props.dispatch(expandBookmarkedStatuses()); - }, 300, { leading: true }); - - render () { - const { intl, statusIds, columnId, multiColumn, hasMore, isLoading } = this.props; - const pinned = !!columnId; - - const emptyMessage = ; - - return ( - - - - - - - {intl.formatMessage(messages.heading)} - - - - ); - } - -} - -export default connect(mapStateToProps)(injectIntl(Bookmarks)); diff --git a/app/javascript/mastodon/features/bookmarked_statuses/index.tsx b/app/javascript/mastodon/features/bookmarked_statuses/index.tsx new file mode 100644 index 0000000000..5d4574b05b --- /dev/null +++ b/app/javascript/mastodon/features/bookmarked_statuses/index.tsx @@ -0,0 +1,116 @@ +import { useEffect, useRef, useCallback } from 'react'; + +import { defineMessages, useIntl, FormattedMessage } from 'react-intl'; + +import { Helmet } from 'react-helmet'; + +import BookmarksIcon from '@/material-icons/400-24px/bookmarks-fill.svg?react'; +import { + fetchBookmarkedStatuses, + expandBookmarkedStatuses, +} from 'mastodon/actions/bookmarks'; +import { addColumn, removeColumn, moveColumn } from 'mastodon/actions/columns'; +import { Column } from 'mastodon/components/column'; +import type { ColumnRef } from 'mastodon/components/column'; +import { ColumnHeader } from 'mastodon/components/column_header'; +import StatusList from 'mastodon/components/status_list'; +import { getStatusList } from 'mastodon/selectors'; +import { useAppDispatch, useAppSelector } from 'mastodon/store'; + +const messages = defineMessages({ + heading: { id: 'column.bookmarks', defaultMessage: 'Bookmarks' }, +}); + +const Bookmarks: React.FC<{ + columnId: string; + multiColumn: boolean; +}> = ({ columnId, multiColumn }) => { + const intl = useIntl(); + const dispatch = useAppDispatch(); + const columnRef = useRef(null); + const statusIds = useAppSelector((state) => + getStatusList(state, 'bookmarks'), + ); + const isLoading = useAppSelector( + (state) => + state.status_lists.getIn(['bookmarks', 'isLoading'], true) as boolean, + ); + const hasMore = useAppSelector( + (state) => !!state.status_lists.getIn(['bookmarks', 'next']), + ); + + useEffect(() => { + dispatch(fetchBookmarkedStatuses()); + }, [dispatch]); + + const handlePin = useCallback(() => { + if (columnId) { + dispatch(removeColumn(columnId)); + } else { + dispatch(addColumn('BOOKMARKS', {})); + } + }, [dispatch, columnId]); + + const handleMove = useCallback( + (dir: number) => { + dispatch(moveColumn(columnId, dir)); + }, + [dispatch, columnId], + ); + + const handleHeaderClick = useCallback(() => { + columnRef.current?.scrollTop(); + }, []); + + const handleLoadMore = useCallback(() => { + dispatch(expandBookmarkedStatuses()); + }, [dispatch]); + + const pinned = !!columnId; + + const emptyMessage = ( + + ); + + return ( + + + + + + + {intl.formatMessage(messages.heading)} + + + + ); +}; + +// eslint-disable-next-line import/no-default-export +export default Bookmarks; diff --git a/app/javascript/mastodon/features/favourited_statuses/index.jsx b/app/javascript/mastodon/features/favourited_statuses/index.jsx deleted file mode 100644 index 9e0b982239..0000000000 --- a/app/javascript/mastodon/features/favourited_statuses/index.jsx +++ /dev/null @@ -1,115 +0,0 @@ -import PropTypes from 'prop-types'; - -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; - -import { Helmet } from 'react-helmet'; - -import ImmutablePropTypes from 'react-immutable-proptypes'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import { connect } from 'react-redux'; - -import { debounce } from 'lodash'; - -import StarIcon from '@/material-icons/400-24px/star-fill.svg?react'; -import { addColumn, removeColumn, moveColumn } from 'mastodon/actions/columns'; -import { fetchFavouritedStatuses, expandFavouritedStatuses } from 'mastodon/actions/favourites'; -import ColumnHeader from 'mastodon/components/column_header'; -import StatusList from 'mastodon/components/status_list'; -import Column from 'mastodon/features/ui/components/column'; -import { getStatusList } from 'mastodon/selectors'; - -const messages = defineMessages({ - heading: { id: 'column.favourites', defaultMessage: 'Favorites' }, -}); - -const mapStateToProps = state => ({ - statusIds: getStatusList(state, 'favourites'), - isLoading: state.getIn(['status_lists', 'favourites', 'isLoading'], true), - hasMore: !!state.getIn(['status_lists', 'favourites', 'next']), -}); - -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, - isLoading: PropTypes.bool, - }; - - UNSAFE_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; - }; - - handleLoadMore = debounce(() => { - this.props.dispatch(expandFavouritedStatuses()); - }, 300, { leading: true }); - - render () { - const { intl, statusIds, columnId, multiColumn, hasMore, isLoading } = this.props; - const pinned = !!columnId; - - const emptyMessage = ; - - return ( - - - - - - - {intl.formatMessage(messages.heading)} - - - - ); - } - -} - -export default connect(mapStateToProps)(injectIntl(Favourites)); diff --git a/app/javascript/mastodon/features/favourited_statuses/index.tsx b/app/javascript/mastodon/features/favourited_statuses/index.tsx new file mode 100644 index 0000000000..908a8ae4a1 --- /dev/null +++ b/app/javascript/mastodon/features/favourited_statuses/index.tsx @@ -0,0 +1,116 @@ +import { useEffect, useRef, useCallback } from 'react'; + +import { defineMessages, useIntl, FormattedMessage } from 'react-intl'; + +import { Helmet } from 'react-helmet'; + +import StarIcon from '@/material-icons/400-24px/star-fill.svg?react'; +import { addColumn, removeColumn, moveColumn } from 'mastodon/actions/columns'; +import { + fetchFavouritedStatuses, + expandFavouritedStatuses, +} from 'mastodon/actions/favourites'; +import { Column } from 'mastodon/components/column'; +import type { ColumnRef } from 'mastodon/components/column'; +import { ColumnHeader } from 'mastodon/components/column_header'; +import StatusList from 'mastodon/components/status_list'; +import { getStatusList } from 'mastodon/selectors'; +import { useAppDispatch, useAppSelector } from 'mastodon/store'; + +const messages = defineMessages({ + heading: { id: 'column.favourites', defaultMessage: 'Favorites' }, +}); + +const Favourites: React.FC<{ columnId: string; multiColumn: boolean }> = ({ + columnId, + multiColumn, +}) => { + const dispatch = useAppDispatch(); + const intl = useIntl(); + const columnRef = useRef(null); + const statusIds = useAppSelector((state) => + getStatusList(state, 'favourites'), + ); + const isLoading = useAppSelector( + (state) => + state.status_lists.getIn(['favourites', 'isLoading'], true) as boolean, + ); + const hasMore = useAppSelector( + (state) => !!state.status_lists.getIn(['favourites', 'next']), + ); + + useEffect(() => { + dispatch(fetchFavouritedStatuses()); + }, [dispatch]); + + const handlePin = useCallback(() => { + if (columnId) { + dispatch(removeColumn(columnId)); + } else { + dispatch(addColumn('FAVOURITES', {})); + } + }, [dispatch, columnId]); + + const handleMove = useCallback( + (dir: number) => { + dispatch(moveColumn(columnId, dir)); + }, + [dispatch, columnId], + ); + + const handleHeaderClick = useCallback(() => { + columnRef.current?.scrollTop(); + }, []); + + const handleLoadMore = useCallback(() => { + dispatch(expandFavouritedStatuses()); + }, [dispatch]); + + const pinned = !!columnId; + + const emptyMessage = ( + + ); + + return ( + + + + + + + {intl.formatMessage(messages.heading)} + + + + ); +}; + +// eslint-disable-next-line import/no-default-export +export default Favourites; diff --git a/app/javascript/mastodon/reducers/status_lists.js b/app/javascript/mastodon/reducers/status_lists.js index 6cb6a937bb..c9d39130ee 100644 --- a/app/javascript/mastodon/reducers/status_lists.js +++ b/app/javascript/mastodon/reducers/status_lists.js @@ -96,6 +96,7 @@ const removeOneFromList = (state, listType, status) => { return state.updateIn([listType, 'items'], (list) => list.delete(status.get('id'))); }; +/** @type {import('@reduxjs/toolkit').Reducer} */ export default function statusLists(state = initialState, action) { switch(action.type) { case FAVOURITED_STATUSES_FETCH_REQUEST: diff --git a/app/javascript/mastodon/selectors/index.js b/app/javascript/mastodon/selectors/index.js index 5ccaba23fd..3119b285b2 100644 --- a/app/javascript/mastodon/selectors/index.js +++ b/app/javascript/mastodon/selectors/index.js @@ -6,6 +6,7 @@ import { me } from '../initial_state'; import { getFilters } from './filters'; export { makeGetAccount } from "./accounts"; +export { getStatusList } from "./statuses"; export const makeGetStatus = () => { return createSelector( @@ -77,7 +78,3 @@ export const makeGetReport = () => createSelector([ (_, base) => base, (state, _, targetAccountId) => state.getIn(['accounts', targetAccountId]), ], (base, targetAccount) => base.set('target_account', targetAccount)); - -export const getStatusList = createSelector([ - (state, type) => state.getIn(['status_lists', type, 'items']), -], (items) => items.toList()); diff --git a/app/javascript/mastodon/selectors/statuses.ts b/app/javascript/mastodon/selectors/statuses.ts new file mode 100644 index 0000000000..4d045e924a --- /dev/null +++ b/app/javascript/mastodon/selectors/statuses.ts @@ -0,0 +1,15 @@ +import { createSelector } from '@reduxjs/toolkit'; +import type { OrderedSet as ImmutableOrderedSet } from 'immutable'; + +import type { RootState } from 'mastodon/store'; + +export const getStatusList = createSelector( + [ + ( + state: RootState, + type: 'favourites' | 'bookmarks' | 'pins' | 'trending', + ) => + state.status_lists.getIn([type, 'items']) as ImmutableOrderedSet, + ], + (items) => items.toList(), +);