import { List as ImmutableList, OrderedSet as ImmutableOrderedSet } from 'immutable'; import debounce from 'lodash/debounce'; import React, { useCallback, useEffect, useRef, useState } from 'react'; import { createSelector } from 'reselect'; import { eventDiscussionCompose } from 'soapbox/actions/compose'; import { fetchStatusWithContext, fetchNext } from 'soapbox/actions/statuses'; import MissingIndicator from 'soapbox/components/missing_indicator'; import ScrollableList from 'soapbox/components/scrollable_list'; import Tombstone from 'soapbox/components/tombstone'; import { Stack } from 'soapbox/components/ui'; import PlaceholderStatus from 'soapbox/features/placeholder/components/placeholder_status'; import PendingStatus from 'soapbox/features/ui/components/pending_status'; import { useAppDispatch, useAppSelector } from 'soapbox/hooks'; import { makeGetStatus } from 'soapbox/selectors'; import ComposeForm from '../compose/components/compose-form'; import ThreadStatus from '../status/components/thread-status'; import type { VirtuosoHandle } from 'react-virtuoso'; import type { RootState } from 'soapbox/store'; import type { Attachment as AttachmentEntity } from 'soapbox/types/entities'; const getStatus = makeGetStatus(); const getDescendantsIds = createSelector([ (_: RootState, statusId: string) => statusId, (state: RootState) => state.contexts.replies, ], (statusId, contextReplies) => { let descendantsIds = ImmutableOrderedSet(); const ids = [statusId]; while (ids.length > 0) { const id = ids.shift(); if (!id) break; const replies = contextReplies.get(id); if (descendantsIds.includes(id)) { break; } if (statusId !== id) { descendantsIds = descendantsIds.union([id]); } if (replies) { replies.reverse().forEach((reply: string) => { ids.unshift(reply); }); } } return descendantsIds; }); type RouteParams = { statusId: string }; interface IEventDiscussion { params: RouteParams, onOpenMedia: (media: ImmutableList, index: number) => void, onOpenVideo: (video: AttachmentEntity, time: number) => void, } const EventDiscussion: React.FC = (props) => { const dispatch = useAppDispatch(); const status = useAppSelector(state => getStatus(state, { id: props.params.statusId })); const descendantsIds = useAppSelector(state => { let descendantsIds = ImmutableOrderedSet(); if (status) { const statusId = status.id; descendantsIds = getDescendantsIds(state, statusId); descendantsIds = descendantsIds.delete(statusId); } return descendantsIds; }); const [isLoaded, setIsLoaded] = useState(!!status); const [next, setNext] = useState(); const node = useRef(null); const scroller = useRef(null); /** Fetch the status (and context) from the API. */ const fetchData = async() => { const { params } = props; const { statusId } = params; const { next } = await dispatch(fetchStatusWithContext(statusId)); setNext(next); }; // Load data. useEffect(() => { fetchData().then(() => { setIsLoaded(true); }).catch(() => { setIsLoaded(true); }); }, [props.params.statusId]); useEffect(() => { dispatch(eventDiscussionCompose(`reply:${props.params.statusId}`, status!)); }, [isLoaded]); const handleMoveUp = (id: string) => { const index = ImmutableList(descendantsIds).indexOf(id); _selectChild(index - 1); }; const handleMoveDown = (id: string) => { const index = ImmutableList(descendantsIds).indexOf(id); _selectChild(index + 1); }; const _selectChild = (index: number) => { scroller.current?.scrollIntoView({ index, behavior: 'smooth', done: () => { const element = document.querySelector(`#thread [data-index="${index}"] .focusable`); if (element) { element.focus(); } }, }); }; const renderTombstone = (id: string) => { return (
); }; const renderStatus = (id: string) => { return ( ); }; const renderPendingStatus = (id: string) => { const idempotencyKey = id.replace(/^末pending-/, ''); return ( ); }; const renderChildren = (list: ImmutableOrderedSet) => { return list.map(id => { if (id.endsWith('-tombstone')) { return renderTombstone(id); } else if (id.startsWith('末pending-')) { return renderPendingStatus(id); } else { return renderStatus(id); } }); }; const handleLoadMore = useCallback(debounce(() => { if (next && status) { dispatch(fetchNext(status.id, next)).then(({ next }) => { setNext(next); }).catch(() => {}); } }, 300, { leading: true }), [next, status]); const hasDescendants = descendantsIds.size > 0; if (!status && isLoaded) { return ( ); } else if (!status) { return ( ); } const children: JSX.Element[] = []; if (hasDescendants) { children.push(...renderChildren(descendantsIds).toArray()); } return (
} initialTopMostItemIndex={0} > {children}
); }; export default EventDiscussion;