kopia lustrzana https://gitlab.com/soapbox-pub/soapbox
Merge remote-tracking branch 'soapbox/develop' into ts
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>environments/review-develop-3zknud/deployments/194^2
commit
5bb26c9b47
|
@ -0,0 +1,43 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { defineMessages } from 'react-intl';
|
||||||
|
|
||||||
|
import { render, screen } from '../../jest/test-helpers';
|
||||||
|
import ScrollTopButton from '../scroll-top-button';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
queue: { id: 'status_list.queue_label', defaultMessage: 'Click to see {count} new {count, plural, one {post} other {posts}}' },
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('<ScrollTopButton />', () => {
|
||||||
|
it('renders correctly', async() => {
|
||||||
|
render(
|
||||||
|
<ScrollTopButton
|
||||||
|
key='scroll-top-button'
|
||||||
|
onClick={() => {}}
|
||||||
|
count={0}
|
||||||
|
message={messages.queue}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
expect(screen.queryAllByRole('link')).toHaveLength(0);
|
||||||
|
|
||||||
|
render(
|
||||||
|
<ScrollTopButton
|
||||||
|
key='scroll-top-button'
|
||||||
|
onClick={() => {}}
|
||||||
|
count={1}
|
||||||
|
message={messages.queue}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
expect(screen.getByText('Click to see 1 new post', { hidden: true })).toBeInTheDocument();
|
||||||
|
|
||||||
|
render(
|
||||||
|
<ScrollTopButton
|
||||||
|
key='scroll-top-button'
|
||||||
|
onClick={() => {}}
|
||||||
|
count={9999999}
|
||||||
|
message={messages.queue}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
expect(screen.getByText('Click to see 9999999 new posts', { hidden: true })).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,50 +0,0 @@
|
||||||
import { fromJS } from 'immutable';
|
|
||||||
import React from 'react';
|
|
||||||
import { defineMessages } from 'react-intl';
|
|
||||||
|
|
||||||
import { render, screen } from '../../jest/test-helpers';
|
|
||||||
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}}' },
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('<TimelineQueueButtonHeader />', () => {
|
|
||||||
it('renders correctly', async() => {
|
|
||||||
render(
|
|
||||||
<TimelineQueueButtonHeader
|
|
||||||
key='timeline-queue-button-header'
|
|
||||||
onClick={() => {}} // eslint-disable-line react/jsx-no-bind
|
|
||||||
timelineId='home'
|
|
||||||
message={messages.queue}
|
|
||||||
/>,
|
|
||||||
undefined,
|
|
||||||
{ timelines: fromJS({ home: { totalQueuedItemsCount: 0 } }) },
|
|
||||||
);
|
|
||||||
expect(screen.queryAllByRole('link')).toHaveLength(0);
|
|
||||||
|
|
||||||
render(
|
|
||||||
<TimelineQueueButtonHeader
|
|
||||||
key='timeline-queue-button-header'
|
|
||||||
onClick={() => {}} // eslint-disable-line react/jsx-no-bind
|
|
||||||
timelineId='home'
|
|
||||||
message={messages.queue}
|
|
||||||
/>,
|
|
||||||
undefined,
|
|
||||||
{ timelines: fromJS({ home: { totalQueuedItemsCount: 1 } }) },
|
|
||||||
);
|
|
||||||
expect(screen.getByText(/Click to see\s+1\s+new post/, { hidden: true })).toBeInTheDocument();
|
|
||||||
|
|
||||||
render(
|
|
||||||
<TimelineQueueButtonHeader
|
|
||||||
key='timeline-queue-button-header'
|
|
||||||
onClick={() => {}} // eslint-disable-line react/jsx-no-bind
|
|
||||||
timelineId='home'
|
|
||||||
message={messages.queue}
|
|
||||||
/>,
|
|
||||||
undefined,
|
|
||||||
{ timelines: fromJS({ home: { totalQueuedItemsCount: 9999999 } }) },
|
|
||||||
);
|
|
||||||
expect(screen.getByText(/10.*M/, { hidden: true })).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -5,27 +5,31 @@ import { useIntl, MessageDescriptor } from 'react-intl';
|
||||||
|
|
||||||
import Icon from 'soapbox/components/icon';
|
import Icon from 'soapbox/components/icon';
|
||||||
import { Text } from 'soapbox/components/ui';
|
import { Text } from 'soapbox/components/ui';
|
||||||
import { useAppSelector, useSettings } from 'soapbox/hooks';
|
import { useSettings } from 'soapbox/hooks';
|
||||||
import { shortNumberFormat } from 'soapbox/utils/numbers';
|
|
||||||
|
|
||||||
interface ITimelineQueueButtonHeader {
|
interface IScrollTopButton {
|
||||||
|
/** Callback when clicked, and also when scrolled to the top. */
|
||||||
onClick: () => void,
|
onClick: () => void,
|
||||||
timelineId: string,
|
/** Number of unread items. */
|
||||||
|
count: number,
|
||||||
|
/** Message to display in the button (should contain a `{count}` value). */
|
||||||
message: MessageDescriptor,
|
message: MessageDescriptor,
|
||||||
|
/** Distance from the top of the screen (scrolling down) before the button appears. */
|
||||||
threshold?: number,
|
threshold?: number,
|
||||||
|
/** Distance from the top of the screen (scrolling up) before the action is triggered. */
|
||||||
autoloadThreshold?: number,
|
autoloadThreshold?: number,
|
||||||
}
|
}
|
||||||
|
|
||||||
const TimelineQueueButtonHeader: React.FC<ITimelineQueueButtonHeader> = ({
|
/** Floating new post counter above timelines, clicked to scroll to top. */
|
||||||
|
const ScrollTopButton: React.FC<IScrollTopButton> = ({
|
||||||
onClick,
|
onClick,
|
||||||
timelineId,
|
count,
|
||||||
message,
|
message,
|
||||||
threshold = 400,
|
threshold = 400,
|
||||||
autoloadThreshold = 50,
|
autoloadThreshold = 50,
|
||||||
}) => {
|
}) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const settings = useSettings();
|
const settings = useSettings();
|
||||||
const count = useAppSelector(state => state.timelines.getIn([timelineId, 'totalQueuedItemsCount']));
|
|
||||||
|
|
||||||
const [scrolled, setScrolled] = useState<boolean>(false);
|
const [scrolled, setScrolled] = useState<boolean>(false);
|
||||||
const autoload = settings.get('autoloadTimelines') === true;
|
const autoload = settings.get('autoloadTimelines') === true;
|
||||||
|
@ -42,10 +46,10 @@ const TimelineQueueButtonHeader: React.FC<ITimelineQueueButtonHeader> = ({
|
||||||
} else {
|
} else {
|
||||||
setScrolled(false);
|
setScrolled(false);
|
||||||
}
|
}
|
||||||
}, 150, { trailing: true }), []);
|
}, 150, { trailing: true }), [autoload, threshold, autoloadThreshold]);
|
||||||
|
|
||||||
const scrollUp = () => {
|
const scrollUp = () => {
|
||||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
window.scrollTo({ top: 0 });
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClick: React.MouseEventHandler = () => {
|
const handleClick: React.MouseEventHandler = () => {
|
||||||
|
@ -74,7 +78,7 @@ const TimelineQueueButtonHeader: React.FC<ITimelineQueueButtonHeader> = ({
|
||||||
|
|
||||||
{(count > 0) && (
|
{(count > 0) && (
|
||||||
<Text theme='inherit' size='sm'>
|
<Text theme='inherit' size='sm'>
|
||||||
{intl.formatMessage(message, { count: shortNumberFormat(count) })}
|
{intl.formatMessage(message, { count })}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
</a>
|
</a>
|
||||||
|
@ -82,4 +86,4 @@ const TimelineQueueButtonHeader: React.FC<ITimelineQueueButtonHeader> = ({
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default TimelineQueueButtonHeader;
|
export default ScrollTopButton;
|
|
@ -9,6 +9,7 @@ import { useSettings } from 'soapbox/hooks';
|
||||||
import LoadMore from './load_more';
|
import LoadMore from './load_more';
|
||||||
import { Spinner, Text } from './ui';
|
import { Spinner, Text } from './ui';
|
||||||
|
|
||||||
|
/** Custom Viruoso component context. */
|
||||||
type Context = {
|
type Context = {
|
||||||
itemClassName?: string,
|
itemClassName?: string,
|
||||||
listClassName?: string,
|
listClassName?: string,
|
||||||
|
@ -20,6 +21,7 @@ type SavedScrollPosition = {
|
||||||
offset: number,
|
offset: number,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Custom Virtuoso Item component representing a single scrollable item. */
|
||||||
// NOTE: It's crucial to space lists with **padding** instead of margin!
|
// NOTE: It's crucial to space lists with **padding** instead of margin!
|
||||||
// Pass an `itemClassName` like `pb-3`, NOT a `space-y-3` className
|
// Pass an `itemClassName` like `pb-3`, NOT a `space-y-3` className
|
||||||
// https://virtuoso.dev/troubleshooting#list-does-not-scroll-to-the-bottom--items-jump-around
|
// https://virtuoso.dev/troubleshooting#list-does-not-scroll-to-the-bottom--items-jump-around
|
||||||
|
@ -27,6 +29,7 @@ const Item: Components<Context>['Item'] = ({ context, ...rest }) => (
|
||||||
<div className={context?.itemClassName} {...rest} />
|
<div className={context?.itemClassName} {...rest} />
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/** Custom Virtuoso List component for the outer container. */
|
||||||
// Ensure the className winds up here
|
// Ensure the className winds up here
|
||||||
const List: Components<Context>['List'] = React.forwardRef((props, ref) => {
|
const List: Components<Context>['List'] = React.forwardRef((props, ref) => {
|
||||||
const { context, ...rest } = props;
|
const { context, ...rest } = props;
|
||||||
|
@ -34,28 +37,47 @@ const List: Components<Context>['List'] = React.forwardRef((props, ref) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
interface IScrollableList extends VirtuosoProps<any, any> {
|
interface IScrollableList extends VirtuosoProps<any, any> {
|
||||||
|
/** Unique key to preserve the scroll position when navigating back. */
|
||||||
scrollKey?: string,
|
scrollKey?: string,
|
||||||
|
/** Pagination callback when the end of the list is reached. */
|
||||||
onLoadMore?: () => void,
|
onLoadMore?: () => void,
|
||||||
|
/** Whether the data is currently being fetched. */
|
||||||
isLoading?: boolean,
|
isLoading?: boolean,
|
||||||
|
/** Whether to actually display the loading state. */
|
||||||
showLoading?: boolean,
|
showLoading?: boolean,
|
||||||
|
/** Whether we expect an additional page of data. */
|
||||||
hasMore?: boolean,
|
hasMore?: boolean,
|
||||||
|
/** Additional element to display at the top of the list. */
|
||||||
prepend?: React.ReactNode,
|
prepend?: React.ReactNode,
|
||||||
|
/** Whether to display the prepended element. */
|
||||||
alwaysPrepend?: boolean,
|
alwaysPrepend?: boolean,
|
||||||
|
/** Message to display when the list is loaded but empty. */
|
||||||
emptyMessage?: React.ReactNode,
|
emptyMessage?: React.ReactNode,
|
||||||
|
/** Scrollable content. */
|
||||||
children: Iterable<React.ReactNode>,
|
children: Iterable<React.ReactNode>,
|
||||||
|
/** Callback when the list is scrolled to the top. */
|
||||||
onScrollToTop?: () => void,
|
onScrollToTop?: () => void,
|
||||||
|
/** Callback when the list is scrolled. */
|
||||||
onScroll?: () => void,
|
onScroll?: () => void,
|
||||||
|
/** Placeholder component to render while loading. */
|
||||||
placeholderComponent?: React.ComponentType | React.NamedExoticComponent,
|
placeholderComponent?: React.ComponentType | React.NamedExoticComponent,
|
||||||
|
/** Number of placeholders to render while loading. */
|
||||||
placeholderCount?: number,
|
placeholderCount?: number,
|
||||||
|
/** Pull to refresh callback. */
|
||||||
onRefresh?: () => Promise<any>,
|
onRefresh?: () => Promise<any>,
|
||||||
|
/** Extra class names on the Virtuoso element. */
|
||||||
className?: string,
|
className?: string,
|
||||||
|
/** Class names on each item container. */
|
||||||
itemClassName?: string,
|
itemClassName?: string,
|
||||||
|
/** `id` attribute on the Virtuoso element. */
|
||||||
id?: string,
|
id?: string,
|
||||||
|
/** CSS styles on the Virtuoso element. */
|
||||||
style?: React.CSSProperties,
|
style?: React.CSSProperties,
|
||||||
|
/** Whether to use the window to scroll the content instead of Virtuoso's container. */
|
||||||
useWindowScroll?: boolean
|
useWindowScroll?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Legacy ScrollableList with Virtuoso for backwards-compatibility */
|
/** Legacy ScrollableList with Virtuoso for backwards-compatibility. */
|
||||||
const ScrollableList = React.forwardRef<VirtuosoHandle, IScrollableList>(({
|
const ScrollableList = React.forwardRef<VirtuosoHandle, IScrollableList>(({
|
||||||
scrollKey,
|
scrollKey,
|
||||||
prepend = null,
|
prepend = null,
|
||||||
|
@ -88,7 +110,7 @@ const ScrollableList = React.forwardRef<VirtuosoHandle, IScrollableList>(({
|
||||||
const topIndex = useRef<number>(scrollData ? scrollData.index : 0);
|
const topIndex = useRef<number>(scrollData ? scrollData.index : 0);
|
||||||
const topOffset = useRef<number>(scrollData ? scrollData.offset : 0);
|
const topOffset = useRef<number>(scrollData ? scrollData.offset : 0);
|
||||||
|
|
||||||
/** Normalized children */
|
/** Normalized children. */
|
||||||
const elements = Array.from(children || []);
|
const elements = Array.from(children || []);
|
||||||
|
|
||||||
const showPlaceholder = showLoading && Placeholder && placeholderCount > 0;
|
const showPlaceholder = showLoading && Placeholder && placeholderCount > 0;
|
||||||
|
@ -129,7 +151,7 @@ const ScrollableList = React.forwardRef<VirtuosoHandle, IScrollableList>(({
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
/* Render an empty state instead of the scrollable list */
|
/* Render an empty state instead of the scrollable list. */
|
||||||
const renderEmpty = (): JSX.Element => {
|
const renderEmpty = (): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<div className='mt-2'>
|
<div className='mt-2'>
|
||||||
|
@ -146,7 +168,7 @@ const ScrollableList = React.forwardRef<VirtuosoHandle, IScrollableList>(({
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Render a single item */
|
/** Render a single item. */
|
||||||
const renderItem = (_i: number, element: JSX.Element): JSX.Element => {
|
const renderItem = (_i: number, element: JSX.Element): JSX.Element => {
|
||||||
if (showPlaceholder) {
|
if (showPlaceholder) {
|
||||||
return <Placeholder />;
|
return <Placeholder />;
|
||||||
|
@ -192,7 +214,7 @@ const ScrollableList = React.forwardRef<VirtuosoHandle, IScrollableList>(({
|
||||||
return 0;
|
return 0;
|
||||||
}, [showLoading, initialTopMostItemIndex]);
|
}, [showLoading, initialTopMostItemIndex]);
|
||||||
|
|
||||||
/** Render the actual Virtuoso list */
|
/** Render the actual Virtuoso list. */
|
||||||
const renderFeed = (): JSX.Element => (
|
const renderFeed = (): JSX.Element => (
|
||||||
<Virtuoso
|
<Virtuoso
|
||||||
ref={ref}
|
ref={ref}
|
||||||
|
@ -222,7 +244,7 @@ const ScrollableList = React.forwardRef<VirtuosoHandle, IScrollableList>(({
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
/** Conditionally render inner elements */
|
/** Conditionally render inner elements. */
|
||||||
const renderBody = (): JSX.Element => {
|
const renderBody = (): JSX.Element => {
|
||||||
if (isEmpty) {
|
if (isEmpty) {
|
||||||
return renderEmpty();
|
return renderEmpty();
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import { List as ImmutableList } from 'immutable';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { FormattedList, FormattedMessage } from 'react-intl';
|
import { FormattedList, FormattedMessage } from 'react-intl';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
@ -7,7 +6,7 @@ import { openModal } from 'soapbox/actions/modals';
|
||||||
import HoverRefWrapper from 'soapbox/components/hover_ref_wrapper';
|
import HoverRefWrapper from 'soapbox/components/hover_ref_wrapper';
|
||||||
import { useAppDispatch } from 'soapbox/hooks';
|
import { useAppDispatch } from 'soapbox/hooks';
|
||||||
|
|
||||||
import type { Status } from 'soapbox/types/entities';
|
import type { Account, Status } from 'soapbox/types/entities';
|
||||||
|
|
||||||
interface IStatusReplyMentions {
|
interface IStatusReplyMentions {
|
||||||
status: Status,
|
status: Status,
|
||||||
|
@ -19,8 +18,10 @@ const StatusReplyMentions: React.FC<IStatusReplyMentions> = ({ status }) => {
|
||||||
const handleOpenMentionsModal: React.MouseEventHandler<HTMLSpanElement> = (e) => {
|
const handleOpenMentionsModal: React.MouseEventHandler<HTMLSpanElement> = (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
|
const account = status.account as Account;
|
||||||
|
|
||||||
dispatch(openModal('MENTIONS', {
|
dispatch(openModal('MENTIONS', {
|
||||||
username: status.getIn(['account', 'acct']),
|
username: account.acct,
|
||||||
statusId: status.id,
|
statusId: status.id,
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
@ -29,7 +30,7 @@ const StatusReplyMentions: React.FC<IStatusReplyMentions> = ({ status }) => {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const to = status.mentions || ImmutableList();
|
const to = status.mentions;
|
||||||
|
|
||||||
// The post is a reply, but it has no mentions.
|
// The post is a reply, but it has no mentions.
|
||||||
// Rare, but it can happen.
|
// Rare, but it can happen.
|
||||||
|
@ -46,14 +47,14 @@ const StatusReplyMentions: React.FC<IStatusReplyMentions> = ({ status }) => {
|
||||||
|
|
||||||
// The typical case with a reply-to and a list of mentions.
|
// The typical case with a reply-to and a list of mentions.
|
||||||
const accounts = to.slice(0, 2).map(account => (
|
const accounts = to.slice(0, 2).map(account => (
|
||||||
<HoverRefWrapper accountId={account.get('id')} inline>
|
<HoverRefWrapper key={account.id} accountId={account.id} inline>
|
||||||
<Link to={`/@${account.get('acct')}`} className='reply-mentions__account'>@{account.get('username')}</Link>
|
<Link to={`/@${account.acct}`} className='reply-mentions__account'>@{account.username}</Link>
|
||||||
</HoverRefWrapper>
|
</HoverRefWrapper>
|
||||||
)).toArray();
|
)).toArray();
|
||||||
|
|
||||||
if (to.size > 2) {
|
if (to.size > 2) {
|
||||||
accounts.push(
|
accounts.push(
|
||||||
<span className='hover:underline cursor-pointer' role='presentation' onClick={handleOpenMentionsModal}>
|
<span key='more' className='hover:underline cursor-pointer' role='presentation' onClick={handleOpenMentionsModal}>
|
||||||
<FormattedMessage id='reply_mentions.more' defaultMessage='{count} more' values={{ count: to.size - 2 }} />
|
<FormattedMessage id='reply_mentions.more' defaultMessage='{count} more' values={{ count: to.size - 2 }} />
|
||||||
</span>,
|
</span>,
|
||||||
);
|
);
|
||||||
|
|
|
@ -14,24 +14,31 @@ import type { VirtuosoHandle } from 'react-virtuoso';
|
||||||
import type { IScrollableList } from 'soapbox/components/scrollable_list';
|
import type { IScrollableList } from 'soapbox/components/scrollable_list';
|
||||||
|
|
||||||
interface IStatusList extends Omit<IScrollableList, 'onLoadMore' | 'children'> {
|
interface IStatusList extends Omit<IScrollableList, 'onLoadMore' | 'children'> {
|
||||||
|
/** Unique key to preserve the scroll position when navigating back. */
|
||||||
scrollKey: string,
|
scrollKey: string,
|
||||||
|
/** List of status IDs to display. */
|
||||||
statusIds: ImmutableOrderedSet<string>,
|
statusIds: ImmutableOrderedSet<string>,
|
||||||
|
/** Last _unfiltered_ status ID (maxId) for pagination. */
|
||||||
lastStatusId?: string,
|
lastStatusId?: string,
|
||||||
|
/** Pinned statuses to show at the top of the feed. */
|
||||||
featuredStatusIds?: ImmutableOrderedSet<string>,
|
featuredStatusIds?: ImmutableOrderedSet<string>,
|
||||||
|
/** Pagination callback when the end of the list is reached. */
|
||||||
onLoadMore?: (lastStatusId: string) => void,
|
onLoadMore?: (lastStatusId: string) => void,
|
||||||
|
/** Whether the data is currently being fetched. */
|
||||||
isLoading: boolean,
|
isLoading: boolean,
|
||||||
|
/** Whether the server did not return a complete page. */
|
||||||
isPartial?: boolean,
|
isPartial?: boolean,
|
||||||
|
/** Whether we expect an additional page of data. */
|
||||||
hasMore: boolean,
|
hasMore: boolean,
|
||||||
prepend?: React.ReactNode,
|
/** Message to display when the list is loaded but empty. */
|
||||||
emptyMessage: React.ReactNode,
|
emptyMessage: React.ReactNode,
|
||||||
alwaysPrepend?: boolean,
|
/** ID of the timeline in Redux. */
|
||||||
timelineId?: string,
|
timelineId?: string,
|
||||||
queuedItemSize?: number,
|
/** Whether to display a gap or border between statuses in the list. */
|
||||||
onScrollToTop?: () => void,
|
|
||||||
onScroll?: () => void,
|
|
||||||
divideType: 'space' | 'border',
|
divideType: 'space' | 'border',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Feed of statuses, built atop ScrollableList. */
|
||||||
const StatusList: React.FC<IStatusList> = ({
|
const StatusList: React.FC<IStatusList> = ({
|
||||||
statusIds,
|
statusIds,
|
||||||
lastStatusId,
|
lastStatusId,
|
||||||
|
@ -68,11 +75,11 @@ const StatusList: React.FC<IStatusList> = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleLoadOlder = useCallback(debounce(() => {
|
const handleLoadOlder = useCallback(debounce(() => {
|
||||||
const loadMoreID = lastStatusId || statusIds.last();
|
const maxId = lastStatusId || statusIds.last();
|
||||||
if (onLoadMore && loadMoreID) {
|
if (onLoadMore && maxId) {
|
||||||
onLoadMore(loadMoreID);
|
onLoadMore(maxId);
|
||||||
}
|
}
|
||||||
}, 300, { leading: true }), []);
|
}, 300, { leading: true }), [onLoadMore, lastStatusId, statusIds.last()]);
|
||||||
|
|
||||||
const selectChild = (index: number) => {
|
const selectChild = (index: number) => {
|
||||||
node.current?.scrollIntoView({
|
node.current?.scrollIntoView({
|
||||||
|
|
|
@ -1,16 +1,30 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { HotKeys } from 'react-hotkeys';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
import { Text } from 'soapbox/components/ui';
|
import { Text } from 'soapbox/components/ui';
|
||||||
|
|
||||||
|
interface ITombstone {
|
||||||
|
id: string,
|
||||||
|
onMoveUp: (statusId: string) => void,
|
||||||
|
onMoveDown: (statusId: string) => void,
|
||||||
|
}
|
||||||
|
|
||||||
/** Represents a deleted item. */
|
/** Represents a deleted item. */
|
||||||
const Tombstone: React.FC = () => {
|
const Tombstone: React.FC<ITombstone> = ({ id, onMoveUp, onMoveDown }) => {
|
||||||
|
const handlers = {
|
||||||
|
moveUp: () => onMoveUp(id),
|
||||||
|
moveDown: () => onMoveDown(id),
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='p-9 flex items-center justify-center sm:rounded-xl bg-gray-100 border border-solid border-gray-200 dark:bg-slate-900 dark:border-slate-700'>
|
<HotKeys handlers={handlers}>
|
||||||
<Text>
|
<div className='p-9 flex items-center justify-center sm:rounded-xl bg-gray-100 border border-solid border-gray-200 dark:bg-slate-900 dark:border-slate-700 focusable' tabIndex={0}>
|
||||||
<FormattedMessage id='statuses.tombstone' defaultMessage='One or more posts is unavailable.' />
|
<Text>
|
||||||
</Text>
|
<FormattedMessage id='statuses.tombstone' defaultMessage='One or more posts is unavailable.' />
|
||||||
</div>
|
</Text>
|
||||||
|
</div>
|
||||||
|
</HotKeys>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,7 @@ import { expandCommunityTimeline } from 'soapbox/actions/timelines';
|
||||||
import SubNavigation from 'soapbox/components/sub_navigation';
|
import SubNavigation from 'soapbox/components/sub_navigation';
|
||||||
import { Column } from 'soapbox/components/ui';
|
import { Column } from 'soapbox/components/ui';
|
||||||
|
|
||||||
import StatusListContainer from '../ui/containers/status_list_container';
|
import Timeline from '../ui/components/timeline';
|
||||||
|
|
||||||
import ColumnSettings from './containers/column_settings_container';
|
import ColumnSettings from './containers/column_settings_container';
|
||||||
|
|
||||||
|
@ -81,7 +81,7 @@ class CommunityTimeline extends React.PureComponent {
|
||||||
return (
|
return (
|
||||||
<Column label={intl.formatMessage(messages.title)} transparent>
|
<Column label={intl.formatMessage(messages.title)} transparent>
|
||||||
<SubNavigation message={intl.formatMessage(messages.title)} settings={ColumnSettings} />
|
<SubNavigation message={intl.formatMessage(messages.title)} settings={ColumnSettings} />
|
||||||
<StatusListContainer
|
<Timeline
|
||||||
scrollKey={`${timelineId}_timeline`}
|
scrollKey={`${timelineId}_timeline`}
|
||||||
timelineId={`${timelineId}${onlyMedia ? ':media' : ''}`}
|
timelineId={`${timelineId}${onlyMedia ? ':media' : ''}`}
|
||||||
onLoadMore={this.handleLoadMore}
|
onLoadMore={this.handleLoadMore}
|
||||||
|
|
|
@ -9,7 +9,7 @@ import ColumnHeader from 'soapbox/components/column_header';
|
||||||
import { Column } from 'soapbox/components/ui';
|
import { Column } from 'soapbox/components/ui';
|
||||||
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
||||||
|
|
||||||
import StatusListContainer from '../ui/containers/status_list_container';
|
import Timeline from '../ui/components/timeline';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
title: { id: 'column.direct', defaultMessage: 'Direct messages' },
|
title: { id: 'column.direct', defaultMessage: 'Direct messages' },
|
||||||
|
@ -52,7 +52,7 @@ const DirectTimeline = () => {
|
||||||
onSelected={handleSuggestion}
|
onSelected={handleSuggestion}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<StatusListContainer
|
<Timeline
|
||||||
scrollKey='direct_timeline'
|
scrollKey='direct_timeline'
|
||||||
timelineId='direct'
|
timelineId='direct'
|
||||||
onLoadMore={handleLoadMore}
|
onLoadMore={handleLoadMore}
|
||||||
|
|
|
@ -12,7 +12,7 @@ import { connectGroupStream } from '../../../actions/streaming';
|
||||||
import { expandGroupTimeline } from '../../../actions/timelines';
|
import { expandGroupTimeline } from '../../../actions/timelines';
|
||||||
import Avatar from '../../../components/avatar';
|
import Avatar from '../../../components/avatar';
|
||||||
import MissingIndicator from '../../../components/missing_indicator';
|
import MissingIndicator from '../../../components/missing_indicator';
|
||||||
import StatusListContainer from '../../ui/containers/status_list_container';
|
import Timeline from '../../ui/components/timeline';
|
||||||
|
|
||||||
const mapStateToProps = (state, props) => {
|
const mapStateToProps = (state, props) => {
|
||||||
const me = state.get('me');
|
const me = state.get('me');
|
||||||
|
@ -90,7 +90,7 @@ class GroupTimeline extends React.PureComponent {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className='group__feed'>
|
<div className='group__feed'>
|
||||||
<StatusListContainer
|
<Timeline
|
||||||
alwaysPrepend
|
alwaysPrepend
|
||||||
scrollKey={`group_timeline-${columnId}`}
|
scrollKey={`group_timeline-${columnId}`}
|
||||||
timelineId={`group:${id}`}
|
timelineId={`group:${id}`}
|
||||||
|
|
|
@ -8,7 +8,7 @@ import { connectHashtagStream } from '../../actions/streaming';
|
||||||
import { expandHashtagTimeline, clearTimeline } from '../../actions/timelines';
|
import { expandHashtagTimeline, clearTimeline } from '../../actions/timelines';
|
||||||
import ColumnHeader from '../../components/column_header';
|
import ColumnHeader from '../../components/column_header';
|
||||||
import { Column } from '../../components/ui';
|
import { Column } from '../../components/ui';
|
||||||
import StatusListContainer from '../ui/containers/status_list_container';
|
import Timeline from '../ui/components/timeline';
|
||||||
|
|
||||||
const mapStateToProps = (state, props) => ({
|
const mapStateToProps = (state, props) => ({
|
||||||
hasUnread: state.getIn(['timelines', `hashtag:${props.params.id}`, 'unread']) > 0,
|
hasUnread: state.getIn(['timelines', `hashtag:${props.params.id}`, 'unread']) > 0,
|
||||||
|
@ -114,7 +114,7 @@ class HashtagTimeline extends React.PureComponent {
|
||||||
return (
|
return (
|
||||||
<Column label={`#${id}`} transparent>
|
<Column label={`#${id}`} transparent>
|
||||||
<ColumnHeader active={hasUnread} title={this.title()} />
|
<ColumnHeader active={hasUnread} title={this.title()} />
|
||||||
<StatusListContainer
|
<Timeline
|
||||||
scrollKey='hashtag_timeline'
|
scrollKey='hashtag_timeline'
|
||||||
timelineId={`hashtag:${id}`}
|
timelineId={`hashtag:${id}`}
|
||||||
onLoadMore={this.handleLoadMore}
|
onLoadMore={this.handleLoadMore}
|
||||||
|
|
|
@ -10,7 +10,7 @@ import { getFeatures } from 'soapbox/utils/features';
|
||||||
|
|
||||||
import { expandHomeTimeline } from '../../actions/timelines';
|
import { expandHomeTimeline } from '../../actions/timelines';
|
||||||
import { Column } from '../../components/ui';
|
import { Column } from '../../components/ui';
|
||||||
import StatusListContainer from '../ui/containers/status_list_container';
|
import Timeline from '../ui/components/timeline';
|
||||||
|
|
||||||
function FollowRecommendationsContainer() {
|
function FollowRecommendationsContainer() {
|
||||||
return import(/* webpackChunkName: "features/follow_recommendations" */'soapbox/features/follow_recommendations/components/follow_recommendations_container');
|
return import(/* webpackChunkName: "features/follow_recommendations" */'soapbox/features/follow_recommendations/components/follow_recommendations_container');
|
||||||
|
@ -114,7 +114,7 @@ class HomeTimeline extends React.PureComponent {
|
||||||
{Component => <Component onDone={this.handleDone} />}
|
{Component => <Component onDone={this.handleDone} />}
|
||||||
</BundleContainer>
|
</BundleContainer>
|
||||||
) : (
|
) : (
|
||||||
<StatusListContainer
|
<Timeline
|
||||||
scrollKey='home_timeline'
|
scrollKey='home_timeline'
|
||||||
onLoadMore={this.handleLoadMore}
|
onLoadMore={this.handleLoadMore}
|
||||||
onRefresh={this.handleRefresh}
|
onRefresh={this.handleRefresh}
|
||||||
|
|
|
@ -12,7 +12,7 @@ import { Button, Spinner } from 'soapbox/components/ui';
|
||||||
import Column from 'soapbox/features/ui/components/column';
|
import Column from 'soapbox/features/ui/components/column';
|
||||||
import { useAppSelector } from 'soapbox/hooks';
|
import { useAppSelector } from 'soapbox/hooks';
|
||||||
|
|
||||||
import StatusListContainer from '../ui/containers/status_list_container';
|
import Timeline from '../ui/components/timeline';
|
||||||
|
|
||||||
// const messages = defineMessages({
|
// const messages = defineMessages({
|
||||||
// deleteHeading: { id: 'confirmations.delete_list.heading', defaultMessage: 'Delete list' },
|
// deleteHeading: { id: 'confirmations.delete_list.heading', defaultMessage: 'Delete list' },
|
||||||
|
@ -110,7 +110,7 @@ const ListTimeline: React.FC = () => {
|
||||||
</div>
|
</div>
|
||||||
</HomeColumnHeader> */}
|
</HomeColumnHeader> */}
|
||||||
|
|
||||||
<StatusListContainer
|
<Timeline
|
||||||
scrollKey='list_timeline'
|
scrollKey='list_timeline'
|
||||||
timelineId={`list:${id}`}
|
timelineId={`list:${id}`}
|
||||||
onLoadMore={handleLoadMore}
|
onLoadMore={handleLoadMore}
|
||||||
|
|
|
@ -14,8 +14,8 @@ import {
|
||||||
dequeueNotifications,
|
dequeueNotifications,
|
||||||
} from 'soapbox/actions/notifications';
|
} from 'soapbox/actions/notifications';
|
||||||
import { getSettings } from 'soapbox/actions/settings';
|
import { getSettings } from 'soapbox/actions/settings';
|
||||||
|
import ScrollTopButton from 'soapbox/components/scroll-top-button';
|
||||||
import ScrollableList from 'soapbox/components/scrollable_list';
|
import ScrollableList from 'soapbox/components/scrollable_list';
|
||||||
import TimelineQueueButtonHeader from 'soapbox/components/timeline_queue_button_header';
|
|
||||||
import { Column } from 'soapbox/components/ui';
|
import { Column } from 'soapbox/components/ui';
|
||||||
import PlaceholderNotification from 'soapbox/features/placeholder/components/placeholder_notification';
|
import PlaceholderNotification from 'soapbox/features/placeholder/components/placeholder_notification';
|
||||||
|
|
||||||
|
@ -195,7 +195,7 @@ class Notifications extends React.PureComponent {
|
||||||
return (
|
return (
|
||||||
<Column ref={this.setColumnRef} label={intl.formatMessage(messages.title)} withHeader={false}>
|
<Column ref={this.setColumnRef} label={intl.formatMessage(messages.title)} withHeader={false}>
|
||||||
{filterBarContainer}
|
{filterBarContainer}
|
||||||
<TimelineQueueButtonHeader
|
<ScrollTopButton
|
||||||
onClick={this.handleDequeueNotifications}
|
onClick={this.handleDequeueNotifications}
|
||||||
count={totalQueuedNotificationsCount}
|
count={totalQueuedNotificationsCount}
|
||||||
message={messages.queue}
|
message={messages.queue}
|
||||||
|
|
|
@ -12,7 +12,7 @@ import { Column } from 'soapbox/components/ui';
|
||||||
import Accordion from 'soapbox/features/ui/components/accordion';
|
import Accordion from 'soapbox/features/ui/components/accordion';
|
||||||
|
|
||||||
import PinnedHostsPicker from '../remote_timeline/components/pinned_hosts_picker';
|
import PinnedHostsPicker from '../remote_timeline/components/pinned_hosts_picker';
|
||||||
import StatusListContainer from '../ui/containers/status_list_container';
|
import Timeline from '../ui/components/timeline';
|
||||||
|
|
||||||
import ColumnSettings from './containers/column_settings_container';
|
import ColumnSettings from './containers/column_settings_container';
|
||||||
|
|
||||||
|
@ -130,7 +130,7 @@ class CommunityTimeline extends React.PureComponent {
|
||||||
/>
|
/>
|
||||||
</Accordion>
|
</Accordion>
|
||||||
</div>}
|
</div>}
|
||||||
<StatusListContainer
|
<Timeline
|
||||||
scrollKey={`${timelineId}_timeline`}
|
scrollKey={`${timelineId}_timeline`}
|
||||||
timelineId={`${timelineId}${onlyMedia ? ':media' : ''}`}
|
timelineId={`${timelineId}${onlyMedia ? ':media' : ''}`}
|
||||||
onLoadMore={this.handleLoadMore}
|
onLoadMore={this.handleLoadMore}
|
||||||
|
|
|
@ -9,7 +9,7 @@ import { HStack, Text } from 'soapbox/components/ui';
|
||||||
import Column from 'soapbox/features/ui/components/column';
|
import Column from 'soapbox/features/ui/components/column';
|
||||||
import { useAppDispatch, useSettings } from 'soapbox/hooks';
|
import { useAppDispatch, useSettings } from 'soapbox/hooks';
|
||||||
|
|
||||||
import StatusListContainer from '../ui/containers/status_list_container';
|
import Timeline from '../ui/components/timeline';
|
||||||
|
|
||||||
import PinnedHostsPicker from './components/pinned_hosts_picker';
|
import PinnedHostsPicker from './components/pinned_hosts_picker';
|
||||||
|
|
||||||
|
@ -77,7 +77,7 @@ const RemoteTimeline: React.FC<IRemoteTimeline> = ({ params }) => {
|
||||||
/>
|
/>
|
||||||
</Text>
|
</Text>
|
||||||
</HStack>}
|
</HStack>}
|
||||||
<StatusListContainer
|
<Timeline
|
||||||
scrollKey={`${timelineId}_${instance}_timeline`}
|
scrollKey={`${timelineId}_${instance}_timeline`}
|
||||||
timelineId={`${timelineId}${onlyMedia ? ':media' : ''}:${instance}`}
|
timelineId={`${timelineId}${onlyMedia ? ':media' : ''}:${instance}`}
|
||||||
onLoadMore={handleLoadMore}
|
onLoadMore={handleLoadMore}
|
||||||
|
|
|
@ -561,7 +561,12 @@ class Status extends ImmutablePureComponent<IStatus, IStatusState> {
|
||||||
renderTombstone(id: string) {
|
renderTombstone(id: string) {
|
||||||
return (
|
return (
|
||||||
<div className='py-4 pb-8'>
|
<div className='py-4 pb-8'>
|
||||||
<Tombstone key={id} />
|
<Tombstone
|
||||||
|
key={id}
|
||||||
|
id={id}
|
||||||
|
onMoveUp={this.handleMoveUp}
|
||||||
|
onMoveDown={this.handleMoveDown}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -635,6 +640,8 @@ class Status extends ImmutablePureComponent<IStatus, IStatusState> {
|
||||||
index: this.props.ancestorsIds.size,
|
index: this.props.ancestorsIds.size,
|
||||||
offset: -80,
|
offset: -80,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
setImmediate(() => this.status?.querySelector('a')?.focus());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,7 @@ import { expandTimelineSuccess } from 'soapbox/actions/timelines';
|
||||||
import SubNavigation from 'soapbox/components/sub_navigation';
|
import SubNavigation from 'soapbox/components/sub_navigation';
|
||||||
|
|
||||||
import { Column } from '../../components/ui';
|
import { Column } from '../../components/ui';
|
||||||
import StatusListContainer from '../ui/containers/status_list_container';
|
import Timeline from '../ui/components/timeline';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
title: { id: 'column.test', defaultMessage: 'Test timeline' },
|
title: { id: 'column.test', defaultMessage: 'Test timeline' },
|
||||||
|
@ -40,7 +40,7 @@ const TestTimeline: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
<Column label={intl.formatMessage(messages.title)} transparent>
|
<Column label={intl.formatMessage(messages.title)} transparent>
|
||||||
<SubNavigation message={intl.formatMessage(messages.title)} />
|
<SubNavigation message={intl.formatMessage(messages.title)} />
|
||||||
<StatusListContainer
|
<Timeline
|
||||||
scrollKey={`${timelineId}_timeline`}
|
scrollKey={`${timelineId}_timeline`}
|
||||||
timelineId={`${timelineId}${onlyMedia ? ':media' : ''}`}
|
timelineId={`${timelineId}${onlyMedia ? ':media' : ''}`}
|
||||||
emptyMessage={<FormattedMessage id='empty_column.test' defaultMessage='The test timeline is empty.' />}
|
emptyMessage={<FormattedMessage id='empty_column.test' defaultMessage='The test timeline is empty.' />}
|
||||||
|
|
|
@ -5,8 +5,8 @@ import { defineMessages } from 'react-intl';
|
||||||
|
|
||||||
import { dequeueTimeline } from 'soapbox/actions/timelines';
|
import { dequeueTimeline } from 'soapbox/actions/timelines';
|
||||||
import { scrollTopTimeline } from 'soapbox/actions/timelines';
|
import { scrollTopTimeline } from 'soapbox/actions/timelines';
|
||||||
|
import ScrollTopButton from 'soapbox/components/scroll-top-button';
|
||||||
import StatusList, { IStatusList } from 'soapbox/components/status_list';
|
import StatusList, { IStatusList } from 'soapbox/components/status_list';
|
||||||
import TimelineQueueButtonHeader from 'soapbox/components/timeline_queue_button_header';
|
|
||||||
import { useAppSelector, useAppDispatch } from 'soapbox/hooks';
|
import { useAppSelector, useAppDispatch } from 'soapbox/hooks';
|
||||||
import { makeGetStatusIds } from 'soapbox/selectors';
|
import { makeGetStatusIds } from 'soapbox/selectors';
|
||||||
|
|
||||||
|
@ -14,11 +14,13 @@ const messages = defineMessages({
|
||||||
queue: { id: 'status_list.queue_label', defaultMessage: 'Click to see {count} new {count, plural, one {post} other {posts}}' },
|
queue: { id: 'status_list.queue_label', defaultMessage: 'Click to see {count} new {count, plural, one {post} other {posts}}' },
|
||||||
});
|
});
|
||||||
|
|
||||||
interface IStatusListContainer extends Omit<IStatusList, 'statusIds' | 'isLoading' | 'hasMore'> {
|
interface ITimeline extends Omit<IStatusList, 'statusIds' | 'isLoading' | 'hasMore'> {
|
||||||
|
/** ID of the timeline in Redux. */
|
||||||
timelineId: string,
|
timelineId: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
const StatusListContainer: React.FC<IStatusListContainer> = ({
|
/** Scrollable list of statuses from a timeline in the Redux store. */
|
||||||
|
const Timeline: React.FC<ITimeline> = ({
|
||||||
timelineId,
|
timelineId,
|
||||||
onLoadMore,
|
onLoadMore,
|
||||||
...rest
|
...rest
|
||||||
|
@ -31,6 +33,7 @@ const StatusListContainer: React.FC<IStatusListContainer> = ({
|
||||||
const isLoading = useAppSelector(state => state.timelines.getIn([timelineId, 'isLoading'], true) === true);
|
const isLoading = useAppSelector(state => state.timelines.getIn([timelineId, 'isLoading'], true) === true);
|
||||||
const isPartial = useAppSelector(state => state.timelines.getIn([timelineId, 'isPartial'], false) === true);
|
const isPartial = useAppSelector(state => state.timelines.getIn([timelineId, 'isPartial'], false) === true);
|
||||||
const hasMore = useAppSelector(state => state.timelines.getIn([timelineId, 'hasMore']) === true);
|
const hasMore = useAppSelector(state => state.timelines.getIn([timelineId, 'hasMore']) === true);
|
||||||
|
const totalQueuedItemsCount = useAppSelector(state => state.timelines.getIn([timelineId, 'totalQueuedItemsCount']));
|
||||||
|
|
||||||
const handleDequeueTimeline = () => {
|
const handleDequeueTimeline = () => {
|
||||||
dispatch(dequeueTimeline(timelineId, onLoadMore));
|
dispatch(dequeueTimeline(timelineId, onLoadMore));
|
||||||
|
@ -38,22 +41,23 @@ const StatusListContainer: React.FC<IStatusListContainer> = ({
|
||||||
|
|
||||||
const handleScrollToTop = useCallback(debounce(() => {
|
const handleScrollToTop = useCallback(debounce(() => {
|
||||||
dispatch(scrollTopTimeline(timelineId, true));
|
dispatch(scrollTopTimeline(timelineId, true));
|
||||||
}, 100), []);
|
}, 100), [timelineId]);
|
||||||
|
|
||||||
const handleScroll = useCallback(debounce(() => {
|
const handleScroll = useCallback(debounce(() => {
|
||||||
dispatch(scrollTopTimeline(timelineId, false));
|
dispatch(scrollTopTimeline(timelineId, false));
|
||||||
}, 100), []);
|
}, 100), [timelineId]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<TimelineQueueButtonHeader
|
<ScrollTopButton
|
||||||
key='timeline-queue-button-header'
|
key='timeline-queue-button-header'
|
||||||
onClick={handleDequeueTimeline}
|
onClick={handleDequeueTimeline}
|
||||||
timelineId={timelineId}
|
count={totalQueuedItemsCount}
|
||||||
message={messages.queue}
|
message={messages.queue}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<StatusList
|
<StatusList
|
||||||
|
timelineId={timelineId}
|
||||||
onScrollToTop={handleScrollToTop}
|
onScrollToTop={handleScrollToTop}
|
||||||
onScroll={handleScroll}
|
onScroll={handleScroll}
|
||||||
lastStatusId={lastStatusId}
|
lastStatusId={lastStatusId}
|
||||||
|
@ -68,4 +72,4 @@ const StatusListContainer: React.FC<IStatusListContainer> = ({
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default StatusListContainer;
|
export default Timeline;
|
Ładowanie…
Reference in New Issue