From d0d9c0b4605906cae79ad60f1750b3388ac66cd0 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 2 Jun 2022 13:32:08 -0500 Subject: [PATCH] StatusList: convert to TSX --- app/soapbox/components/load_gap.tsx | 4 +- app/soapbox/components/scrollable_list.tsx | 6 +- app/soapbox/components/status.tsx | 8 +- app/soapbox/components/status_list.js | 236 --------------------- app/soapbox/components/status_list.tsx | 233 ++++++++++++++++++++ 5 files changed, 243 insertions(+), 244 deletions(-) delete mode 100644 app/soapbox/components/status_list.js create mode 100644 app/soapbox/components/status_list.tsx diff --git a/app/soapbox/components/load_gap.tsx b/app/soapbox/components/load_gap.tsx index b784c871d..a21e64749 100644 --- a/app/soapbox/components/load_gap.tsx +++ b/app/soapbox/components/load_gap.tsx @@ -9,8 +9,8 @@ const messages = defineMessages({ interface ILoadGap { disabled?: boolean, - maxId: string, - onClick: (id: string) => void, + maxId: string | null, + onClick: (id: string | null) => void, } const LoadGap: React.FC = ({ disabled, maxId, onClick }) => { diff --git a/app/soapbox/components/scrollable_list.tsx b/app/soapbox/components/scrollable_list.tsx index 9f5af5601..a2df04005 100644 --- a/app/soapbox/components/scrollable_list.tsx +++ b/app/soapbox/components/scrollable_list.tsx @@ -36,13 +36,13 @@ interface IScrollableList extends VirtuosoProps { isLoading?: boolean, showLoading?: boolean, hasMore?: boolean, - prepend?: React.ReactElement, + prepend?: React.ReactNode, alwaysPrepend?: boolean, emptyMessage?: React.ReactNode, children: Iterable, onScrollToTop?: () => void, onScroll?: () => void, - placeholderComponent?: React.ComponentType, + placeholderComponent?: React.ComponentType | React.NamedExoticComponent, placeholderCount?: number, onRefresh?: () => Promise, className?: string, @@ -184,7 +184,7 @@ const ScrollableList = React.forwardRef(({ itemClassName, }} components={{ - Header: () => prepend, + Header: () => <>{prepend}, ScrollSeekPlaceholder: Placeholder as any, EmptyPlaceholder: () => renderEmpty(), List, diff --git a/app/soapbox/components/status.tsx b/app/soapbox/components/status.tsx index 370deb6de..a8e684a3a 100644 --- a/app/soapbox/components/status.tsx +++ b/app/soapbox/components/status.tsx @@ -61,6 +61,8 @@ export const defaultMediaVisibility = (status: StatusEntity, displayMedia: strin }; interface IStatus extends RouteComponentProps { + id?: string, + contextType?: string, intl: IntlShape, status: StatusEntity, account: AccountEntity, @@ -87,8 +89,8 @@ interface IStatus extends RouteComponentProps { muted: boolean, hidden: boolean, unread: boolean, - onMoveUp: (statusId: string, featured?: string) => void, - onMoveDown: (statusId: string, featured?: string) => void, + onMoveUp: (statusId: string, featured?: boolean) => void, + onMoveDown: (statusId: string, featured?: boolean) => void, getScrollPosition?: () => ScrollPosition | undefined, updateScrollBottom?: (bottom: number) => void, cacheMediaWidth: () => void, @@ -98,7 +100,7 @@ interface IStatus extends RouteComponentProps { allowedEmoji: ImmutableList, focusable: boolean, history: History, - featured?: string, + featured?: boolean, } interface IStatusState { diff --git a/app/soapbox/components/status_list.js b/app/soapbox/components/status_list.js deleted file mode 100644 index 13300a29a..000000000 --- a/app/soapbox/components/status_list.js +++ /dev/null @@ -1,236 +0,0 @@ -import classNames from 'classnames'; -import { debounce } from 'lodash'; -import PropTypes from 'prop-types'; -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import { FormattedMessage, defineMessages } from 'react-intl'; - -import StatusContainer from 'soapbox/containers/status_container'; -import PlaceholderStatus from 'soapbox/features/placeholder/components/placeholder_status'; -import PendingStatus from 'soapbox/features/ui/components/pending_status'; - -import LoadGap from './load_gap'; -import ScrollableList from './scrollable_list'; -import TimelineQueueButtonHeader from './timeline_queue_button_header'; - -const messages = defineMessages({ - queue: { id: 'status_list.queue_label', defaultMessage: 'Click to see {count} new {count, plural, one {post} other {posts}}' }, -}); - -export default class StatusList extends ImmutablePureComponent { - - static propTypes = { - scrollKey: PropTypes.string.isRequired, - statusIds: ImmutablePropTypes.orderedSet.isRequired, - lastStatusId: PropTypes.string, - featuredStatusIds: ImmutablePropTypes.orderedSet, - onLoadMore: PropTypes.func, - isLoading: PropTypes.bool, - isPartial: PropTypes.bool, - hasMore: PropTypes.bool, - prepend: PropTypes.node, - emptyMessage: PropTypes.node, - alwaysPrepend: PropTypes.bool, - timelineId: PropTypes.string, - queuedItemSize: PropTypes.number, - onDequeueTimeline: PropTypes.func, - group: ImmutablePropTypes.map, - withGroupAdmin: PropTypes.bool, - onScrollToTop: PropTypes.func, - onScroll: PropTypes.func, - divideType: PropTypes.oneOf(['space', 'border']), - }; - - static defaultProps = { - divideType: 'border', - } - - getFeaturedStatusCount = () => { - return this.props.featuredStatusIds ? this.props.featuredStatusIds.size : 0; - } - - getCurrentStatusIndex = (id, featured) => { - if (featured) { - return this.props.featuredStatusIds.keySeq().findIndex(key => key === id); - } else { - return this.props.statusIds.keySeq().findIndex(key => key === id) + this.getFeaturedStatusCount(); - } - } - - handleMoveUp = (id, featured) => { - const elementIndex = this.getCurrentStatusIndex(id, featured) - 1; - this._selectChild(elementIndex, true); - } - - handleMoveDown = (id, featured) => { - const elementIndex = this.getCurrentStatusIndex(id, featured) + 1; - this._selectChild(elementIndex, false); - } - - handleLoadOlder = debounce(() => { - const loadMoreID = this.props.lastStatusId ? this.props.lastStatusId : this.props.statusIds.last(); - this.props.onLoadMore(loadMoreID); - }, 300, { leading: true }) - - _selectChild(index) { - this.node.scrollIntoView({ - index, - behavior: 'smooth', - done: () => { - const element = document.querySelector(`#status-list [data-index="${index}"] .focusable`); - - if (element) { - element.focus(); - } - }, - }); - } - - handleDequeueTimeline = () => { - const { onDequeueTimeline, timelineId } = this.props; - if (!onDequeueTimeline || !timelineId) return; - onDequeueTimeline(timelineId); - } - - setRef = c => { - this.node = c; - } - - renderLoadGap(index) { - const { statusIds, onLoadMore, isLoading } = this.props; - - return ( - 0 ? statusIds.get(index - 1) : null} - onClick={onLoadMore} - /> - ); - } - - renderStatus(statusId) { - const { timelineId, withGroupAdmin, group } = this.props; - - return ( - - ); - } - - renderPendingStatus(statusId) { - const { timelineId, withGroupAdmin, group } = this.props; - const idempotencyKey = statusId.replace(/^末pending-/, ''); - - return ( - - ); - } - - renderFeaturedStatuses() { - const { featuredStatusIds, timelineId } = this.props; - if (!featuredStatusIds) return null; - - return featuredStatusIds.map(statusId => ( - - )); - } - - renderStatuses() { - const { statusIds, isLoading } = this.props; - - if (isLoading || statusIds.size > 0) { - return statusIds.map((statusId, index) => { - if (statusId === null) { - return this.renderLoadGap(index); - } else if (statusId.startsWith('末pending-')) { - return this.renderPendingStatus(statusId); - } else { - return this.renderStatus(statusId); - } - }); - } else { - return null; - } - } - - renderScrollableContent() { - const featuredStatuses = this.renderFeaturedStatuses(); - const statuses = this.renderStatuses(); - - if (featuredStatuses && statuses) { - return featuredStatuses.concat(statuses); - } else { - return statuses; - } - } - - render() { - const { statusIds, divideType, featuredStatusIds, onLoadMore, timelineId, totalQueuedItemsCount, isLoading, isPartial, withGroupAdmin, group, ...other } = this.props; - - if (isPartial) { - return ( -
-
-
- - -
-
-
- ); - } - - return [ - , - - {this.renderScrollableContent()} - , - ]; - } - -} diff --git a/app/soapbox/components/status_list.tsx b/app/soapbox/components/status_list.tsx new file mode 100644 index 000000000..7500f6a79 --- /dev/null +++ b/app/soapbox/components/status_list.tsx @@ -0,0 +1,233 @@ +import classNames from 'classnames'; +import { debounce } from 'lodash'; +import React, { useRef, useCallback } from 'react'; +import { FormattedMessage, defineMessages } from 'react-intl'; + +import StatusContainer from 'soapbox/containers/status_container'; +import PlaceholderStatus from 'soapbox/features/placeholder/components/placeholder_status'; +import PendingStatus from 'soapbox/features/ui/components/pending_status'; + +import LoadGap from './load_gap'; +import ScrollableList from './scrollable_list'; +import TimelineQueueButtonHeader from './timeline_queue_button_header'; + +import type { + Map as ImmutableMap, + OrderedSet as ImmutableOrderedSet, +} from 'immutable'; +import type { VirtuosoHandle } from 'react-virtuoso'; + +const messages = defineMessages({ + queue: { id: 'status_list.queue_label', defaultMessage: 'Click to see {count} new {count, plural, one {post} other {posts}}' }, +}); + +interface IStatusList { + scrollKey: string, + statusIds: ImmutableOrderedSet, + lastStatusId: string, + featuredStatusIds?: ImmutableOrderedSet, + onLoadMore: (lastStatusId: string | null) => void, + isLoading: boolean, + isPartial: boolean, + hasMore: boolean, + prepend: React.ReactNode, + emptyMessage: React.ReactNode, + alwaysPrepend: boolean, + timelineId: string, + queuedItemSize: number, + onDequeueTimeline: (timelineId: string) => void, + totalQueuedItemsCount: number, + group?: ImmutableMap, + withGroupAdmin: boolean, + onScrollToTop: () => void, + onScroll: () => void, + divideType: 'space' | 'border', +} + +const StatusList: React.FC = ({ + statusIds, + lastStatusId, + featuredStatusIds, + divideType = 'border', + onLoadMore, + timelineId, + totalQueuedItemsCount, + onDequeueTimeline, + isLoading, + isPartial, + withGroupAdmin, + group, + ...other +}) => { + const node = useRef(null); + + const getFeaturedStatusCount = () => { + return featuredStatusIds?.size || 0; + }; + + const getCurrentStatusIndex = (id: string, featured: boolean): number => { + if (featured) { + return featuredStatusIds?.keySeq().findIndex(key => key === id) || 0; + } else { + return statusIds.keySeq().findIndex(key => key === id) + getFeaturedStatusCount(); + } + }; + + const handleMoveUp = (id: string, featured: boolean = false) => { + const elementIndex = getCurrentStatusIndex(id, featured) - 1; + selectChild(elementIndex); + }; + + const handleMoveDown = (id: string, featured: boolean = false) => { + const elementIndex = getCurrentStatusIndex(id, featured) + 1; + selectChild(elementIndex); + }; + + const handleLoadOlder = useCallback(debounce(() => { + const loadMoreID = lastStatusId || statusIds.last(); + if (loadMoreID) { + onLoadMore(loadMoreID); + } + }, 300, { leading: true }), []); + + const selectChild = (index: number) => { + node.current?.scrollIntoView({ + index, + behavior: 'smooth', + done: () => { + const element: HTMLElement | null = document.querySelector(`#status-list [data-index="${index}"] .focusable`); + element?.focus(); + }, + }); + }; + + const handleDequeueTimeline = () => { + if (!onDequeueTimeline || !timelineId) return; + onDequeueTimeline(timelineId); + }; + + const renderLoadGap = (index: number) => { + const ids = statusIds.toList(); + + return ( + 0 ? ids.get(index - 1) || null : null} + onClick={onLoadMore} + /> + ); + }; + + const renderStatus = (statusId: string) => { + return ( + // @ts-ignore + + ); + }; + + const renderPendingStatus = (statusId: string) => { + const idempotencyKey = statusId.replace(/^末pending-/, ''); + + return ( + + ); + }; + + const renderFeaturedStatuses = (): JSX.Element[] => { + if (!featuredStatusIds) return []; + + return featuredStatusIds.toArray().map(statusId => ( + // @ts-ignore + + )); + }; + + const renderStatuses = (): JSX.Element[] => { + if (isLoading || statusIds.size > 0) { + return statusIds.toArray().map((statusId, index) => { + if (statusId === null) { + return renderLoadGap(index); + } else if (statusId.startsWith('末pending-')) { + return renderPendingStatus(statusId); + } else { + return renderStatus(statusId); + } + }); + } else { + return []; + } + }; + + const renderScrollableContent = () => { + const featuredStatuses = renderFeaturedStatuses(); + const statuses = renderStatuses(); + + if (featuredStatuses && statuses) { + return featuredStatuses.concat(statuses); + } else { + return statuses; + } + }; + + if (isPartial) { + return ( +
+
+
+ + +
+
+
+ ); + } + + return ( + <> + + + {renderScrollableContent()} + , + + ); +}; + +export default StatusList;