;
-}
-
-function Drafts({
- displayName,
- drafts,
- draftRemotes,
- status,
- user,
-}: Props) {
- const dispatch = useDispatch();
-
- const history = useHistory();
- const match: match<{team: string}> = useRouteMatch();
- const isDraftsTab = useRouteMatch('/:team/drafts');
-
- const isScheduledPostsTab = useRouteMatch('/:team/' + SCHEDULED_POST_URL_SUFFIX);
-
- const currentTeamId = useSelector(getCurrentTeamId);
- const getScheduledPostsByTeam = useMemo(() => makeGetScheduledPostsByTeam(), []);
- const scheduledPosts = useSelector((state: GlobalState) => getScheduledPostsByTeam(state, currentTeamId, true));
- const isScheduledPostEnabled = useSelector(isScheduledPostsEnabled);
-
- useEffect(() => {
- dispatch(selectLhsItem(LhsItemType.Page, LhsPage.Drafts));
- dispatch(suppressRHS);
-
- return () => {
- dispatch(unsuppressRHS);
- };
- }, [dispatch]);
-
- const handleSwitchTabs = useCallback((key) => {
- if (key === 0 && isScheduledPostsTab) {
- history.push(`/${match.params.team}/drafts`);
- } else if (key === 1 && isDraftsTab) {
- history.push(`/${match.params.team}/scheduled_posts`);
- }
- }, [history, isDraftsTab, isScheduledPostsTab, match]);
-
- const scheduledPostsTabHeading = useMemo(() => {
- return (
-
-
-
- {
- scheduledPosts?.length > 0 &&
-
- }
-
- );
- }, [scheduledPosts?.length]);
-
- const draftTabHeading = useMemo(() => {
- return (
-
-
-
- {
- drafts.length > 0 &&
-
- }
-
- );
- }, [drafts?.length]);
-
- const heading = useMemo(() => {
- return (
-
- );
- }, []);
-
- const subtitle = useMemo(() => {
- return (
-
- );
- }, []);
-
- const activeTab = isDraftsTab ? 0 : 1;
-
- return (
-
-
-
- {
- isScheduledPostEnabled &&
-
-
-
-
-
-
-
-
-
- }
-
- {
- !isScheduledPostEnabled &&
-
- }
-
- );
-}
-
-export default memo(Drafts);
diff --git a/webapp/channels/src/components/drafts/drafts.scss b/webapp/channels/src/components/drafts/drafts_and_schedule_posts.scss
similarity index 87%
rename from webapp/channels/src/components/drafts/drafts.scss
rename to webapp/channels/src/components/drafts/drafts_and_schedule_posts.scss
index c867f6b8fd..accfd43bc1 100644
--- a/webapp/channels/src/components/drafts/drafts.scss
+++ b/webapp/channels/src/components/drafts/drafts_and_schedule_posts.scss
@@ -18,9 +18,8 @@
display: flex;
overflow: auto;
width: 100%;
- height: 100%;
+ height: calc(100% - 56px);
flex-flow: column nowrap;
- padding: 24px;
}
display: grid;
@@ -72,6 +71,7 @@
.DraftList.Drafts__main {
top: 0;
+ height: calc(100%);
}
}
}
@@ -81,6 +81,14 @@
top: -2px;
}
+ .virtualizedVariableListRowWrapper {
+ padding: 0 24px 16px 24px;
+
+ &.firstRow {
+ padding-top: 24px;
+ }
+ }
+
@media screen and (max-width: 768px) {
grid-template-rows: 0 1fr;
@@ -92,4 +100,11 @@
border-top: var(--border-default);
}
}
+
+ .Panel {
+ .post--editing__wrapper {
+ margin: 16px;
+ }
+ }
+
}
diff --git a/webapp/channels/src/components/drafts/drafts_and_schedule_posts_page_header.tsx b/webapp/channels/src/components/drafts/drafts_and_schedule_posts_page_header.tsx
new file mode 100644
index 0000000000..b2b7c7266d
--- /dev/null
+++ b/webapp/channels/src/components/drafts/drafts_and_schedule_posts_page_header.tsx
@@ -0,0 +1,39 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import React from 'react';
+import type {ReactNode} from 'react';
+import {FormattedMessage} from 'react-intl';
+
+import Header from 'components/widgets/header';
+
+type Props = {
+ children: ReactNode;
+}
+
+export default function DraftsAndSchedulePostsPageHeader(props: Props) {
+ return (
+
+
+ }
+ subtitle={
+
+ }
+ />
+ {props.children}
+
+ );
+}
diff --git a/webapp/channels/src/components/drafts/drafts_and_schedule_posts_tabs.tsx b/webapp/channels/src/components/drafts/drafts_and_schedule_posts_tabs.tsx
new file mode 100644
index 0000000000..9abff02f41
--- /dev/null
+++ b/webapp/channels/src/components/drafts/drafts_and_schedule_posts_tabs.tsx
@@ -0,0 +1,143 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import {Badge} from '@mui/base';
+import React, {useCallback, useMemo} from 'react';
+import {FormattedMessage} from 'react-intl';
+import {useSelector} from 'react-redux';
+import {useHistory, useLocation} from 'react-router-dom';
+
+import type {ScheduledPost} from '@mattermost/types/schedule_post';
+import type {UserProfile, UserStatus} from '@mattermost/types/users';
+
+import {makeGetScheduledPostsByTeam} from 'mattermost-redux/selectors/entities/scheduled_posts';
+import {getCurrentTeam} from 'mattermost-redux/selectors/entities/teams';
+
+import {type Draft} from 'selectors/drafts';
+
+import DraftList from 'components/drafts/draft_list';
+import ScheduledPostList from 'components/drafts/scheduled_post_list';
+import Tab from 'components/tabs/tab';
+import Tabs from 'components/tabs/tabs';
+
+import {DRAFT_URL_SUFFIX, SCHEDULED_POST_URL_SUFFIX} from 'utils/constants';
+
+import type {GlobalState} from 'types/store';
+
+const EMPTY_SCHEDULED_POSTS: ScheduledPost[] = [];
+
+const TAB_KEYS = {
+ DRAFTS: 'drafts',
+ SCHEDULED_POSTS: 'scheduled_posts',
+};
+
+type Props = {
+ drafts: Draft[];
+ currentUser: UserProfile;
+ userDisplayName: string;
+ userStatus: UserStatus['status'];
+}
+
+export default function DraftsAndSchedulePostsTabs(props: Props) {
+ const history = useHistory();
+ const location = useLocation();
+ const isDraftsTab = location.pathname.includes(DRAFT_URL_SUFFIX);
+ const isScheduledPostsTab = location.pathname.includes(SCHEDULED_POST_URL_SUFFIX);
+
+ const currentTeam = useSelector(getCurrentTeam);
+ const currentTeamName = currentTeam?.name ?? '';
+ const currentTeamId = currentTeam?.id ?? '';
+
+ const getScheduledPostsByTeam = useMemo(() => makeGetScheduledPostsByTeam(), []);
+ const scheduledPosts = useSelector((state: GlobalState) => getScheduledPostsByTeam(state, currentTeamId, true));
+
+ const handleSwitchTabs = useCallback((key) => {
+ if (key === TAB_KEYS.DRAFTS) {
+ history.push(`/${currentTeamName}/drafts`);
+ } else if (key === TAB_KEYS.SCHEDULED_POSTS) {
+ history.push(`/${currentTeamName}/scheduled_posts`);
+ }
+ }, [history, currentTeamName]);
+
+ const scheduledPostsTabHeading = useMemo(() => {
+ return (
+
+
+ {scheduledPosts?.length > 0 && (
+
+ )}
+
+ );
+ }, [scheduledPosts?.length]);
+
+ const draftTabHeading = useMemo(() => {
+ return (
+
+
+ {props.drafts.length > 0 && (
+
+ )}
+
+ );
+ }, [props.drafts.length]);
+
+ const activeTab = useMemo(() => {
+ if (isDraftsTab) {
+ return TAB_KEYS.DRAFTS;
+ } else if (isScheduledPostsTab) {
+ return TAB_KEYS.SCHEDULED_POSTS;
+ }
+ return '';
+ }, [isDraftsTab, isScheduledPostsTab]);
+
+ return (
+
+
+
+
+
+
+
+
+ );
+}
+
diff --git a/webapp/channels/src/components/drafts/index.ts b/webapp/channels/src/components/drafts/index.ts
deleted file mode 100644
index 264ff391be..0000000000
--- a/webapp/channels/src/components/drafts/index.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
-// See LICENSE.txt for license information.
-
-import {connect} from 'react-redux';
-
-import {getTeammateNameDisplaySetting} from 'mattermost-redux/selectors/entities/preferences';
-import {getCurrentUser, getStatusForUserId} from 'mattermost-redux/selectors/entities/users';
-import {displayUsername} from 'mattermost-redux/utils/user_utils';
-
-import {makeGetDrafts} from 'selectors/drafts';
-
-import type {GlobalState} from 'types/store';
-
-import Drafts from './drafts';
-
-function makeMapStateToProps() {
- const getDrafts = makeGetDrafts();
- return (state: GlobalState) => {
- const user = getCurrentUser(state);
- const status = getStatusForUserId(state, user.id);
-
- return {
- displayName: displayUsername(user, getTeammateNameDisplaySetting(state)),
- drafts: getDrafts(state),
- draftRemotes: state.views.drafts.remotes,
- status,
- user,
- };
- };
-}
-
-export default connect(makeMapStateToProps)(Drafts);
diff --git a/webapp/channels/src/components/drafts/index.tsx b/webapp/channels/src/components/drafts/index.tsx
new file mode 100644
index 0000000000..255dd38c56
--- /dev/null
+++ b/webapp/channels/src/components/drafts/index.tsx
@@ -0,0 +1,81 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import React, {memo, useEffect, useMemo} from 'react';
+import {useDispatch, useSelector} from 'react-redux';
+
+import {getCurrentUser} from 'mattermost-redux/selectors/entities/common';
+import {getTeammateNameDisplaySetting} from 'mattermost-redux/selectors/entities/preferences';
+import {isScheduledPostsEnabled} from 'mattermost-redux/selectors/entities/scheduled_posts';
+import {getStatusForUserId} from 'mattermost-redux/selectors/entities/users';
+import {displayUsername} from 'mattermost-redux/utils/user_utils';
+
+import {selectLhsItem} from 'actions/views/lhs';
+import {suppressRHS, unsuppressRHS} from 'actions/views/rhs';
+import type {Draft} from 'selectors/drafts';
+import {makeGetDrafts} from 'selectors/drafts';
+
+import DraftList from 'components/drafts/draft_list';
+
+import type {GlobalState} from 'types/store';
+import {LhsItemType, LhsPage} from 'types/store/lhs';
+
+import DraftsAndSchedulePostsPageHeader from './drafts_and_schedule_posts_page_header';
+import DraftsAndSchedulePostsTabs from './drafts_and_schedule_posts_tabs';
+
+import './drafts_and_schedule_posts.scss';
+
+const EMPTY_DRAFTS: Draft[] = [];
+
+function Drafts() {
+ const dispatch = useDispatch();
+
+ const scheduledPostsEnabled = useSelector(isScheduledPostsEnabled);
+
+ // We would need to get drafts here early since its needed by the draft list component
+ const getDrafts = useMemo(() => makeGetDrafts(), []);
+ const drafts = useSelector(getDrafts);
+
+ const currentUser = useSelector(getCurrentUser);
+ const userStatus = useSelector((state: GlobalState) => getStatusForUserId(state, currentUser.id));
+
+ const teammateNameDisplaySetting = useSelector(getTeammateNameDisplaySetting);
+ const userDisplayName = useMemo(() => displayUsername(currentUser, teammateNameDisplaySetting), [currentUser, teammateNameDisplaySetting]);
+
+ // When Drafts component mounts, select Drafts in the LHS
+ // and suppress the RHS and restore RHS when component unmounts
+ useEffect(() => {
+ dispatch(selectLhsItem(LhsItemType.Page, LhsPage.Drafts));
+ dispatch(suppressRHS);
+
+ return () => {
+ dispatch(unsuppressRHS);
+ };
+ }, [dispatch]);
+
+ if (scheduledPostsEnabled) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+
+ );
+}
+
+export default memo(Drafts);
diff --git a/webapp/channels/src/components/drafts/panel/panel.tsx b/webapp/channels/src/components/drafts/panel/panel.tsx
index 04c84e3851..4a18f8268d 100644
--- a/webapp/channels/src/components/drafts/panel/panel.tsx
+++ b/webapp/channels/src/components/drafts/panel/panel.tsx
@@ -14,6 +14,8 @@ type Props = {
hasError: boolean;
innerRef?: React.Ref;
isHighlighted?: boolean;
+ style?: React.CSSProperties;
+ className?: string;
};
const isEligibleForClick = makeIsEligibleForClick('.hljs, code');
@@ -24,6 +26,8 @@ function Panel({
hasError,
innerRef,
isHighlighted,
+ style,
+ className,
}: Props) {
const [hover, setHover] = useState(false);
@@ -49,7 +53,9 @@ function Panel({
draftError: hasError,
highlighted: isHighlighted,
},
+ className,
)}
+ style={style}
onMouseOver={handleMouseOver}
onClick={handleOnClick}
onMouseLeave={handleMouseLeave}
diff --git a/webapp/channels/src/components/drafts/scheduled_post_list/empty_scheduled_post_list.tsx b/webapp/channels/src/components/drafts/scheduled_post_list/empty_scheduled_post_list.tsx
new file mode 100644
index 0000000000..46a0905a25
--- /dev/null
+++ b/webapp/channels/src/components/drafts/scheduled_post_list/empty_scheduled_post_list.tsx
@@ -0,0 +1,29 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import React from 'react';
+import {useIntl} from 'react-intl';
+
+import NoResultsIndicator from 'components/no_results_indicator';
+
+import NoScheduledPostsIllustration from './empty_scheduled_post_list_illustration';
+
+export default function EmptyScheduledPostList() {
+ const {formatMessage} = useIntl();
+
+ return (
+
+ );
+}
diff --git a/webapp/channels/src/components/drafts/scheduled_post_list/index.tsx b/webapp/channels/src/components/drafts/scheduled_post_list/index.tsx
new file mode 100644
index 0000000000..4300e4d43e
--- /dev/null
+++ b/webapp/channels/src/components/drafts/scheduled_post_list/index.tsx
@@ -0,0 +1,60 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import React, {useEffect} from 'react';
+import {useDispatch, useSelector} from 'react-redux';
+
+import type {ScheduledPost} from '@mattermost/types/schedule_post';
+import type {UserProfile, UserStatus} from '@mattermost/types/users';
+
+import {fetchMissingChannels} from 'mattermost-redux/actions/channels';
+import {hasScheduledPostError} from 'mattermost-redux/selectors/entities/scheduled_posts';
+import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
+
+import type {GlobalState} from 'types/store';
+
+import EmptyScheduledPostList from './empty_scheduled_post_list';
+import NonVirtualizedScheduledPostList from './non_virtualized_scheduled_post_list';
+import ScheduledPostError from './scheduled_post_error';
+
+import './scheduled_post_list.scss';
+
+type Props = {
+ scheduledPosts: ScheduledPost[];
+ currentUser: UserProfile;
+ userDisplayName: string;
+ userStatus: UserStatus['status'];
+};
+
+export default function ScheduledPostList(props: Props) {
+ const dispatch = useDispatch();
+
+ const currentTeamId = useSelector(getCurrentTeamId);
+
+ const scheduledPostsHasError = useSelector((state: GlobalState) => hasScheduledPostError(state, currentTeamId));
+
+ useEffect(() => {
+ if (props.scheduledPosts.length > 0) {
+ dispatch(fetchMissingChannels(props.scheduledPosts.map((post) => post.channel_id)));
+ }
+ }, [dispatch, props.scheduledPosts]);
+
+ if (props.scheduledPosts.length === 0) {
+ return (
+
+ );
+ }
+
+ return (
+
+ {scheduledPostsHasError && ()}
+
+
+
+ );
+}
diff --git a/webapp/channels/src/components/drafts/scheduled_post_list/non_virtualized_scheduled_post_list.tsx b/webapp/channels/src/components/drafts/scheduled_post_list/non_virtualized_scheduled_post_list.tsx
new file mode 100644
index 0000000000..7ab70a210d
--- /dev/null
+++ b/webapp/channels/src/components/drafts/scheduled_post_list/non_virtualized_scheduled_post_list.tsx
@@ -0,0 +1,54 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import classNames from 'classnames';
+import React, {useRef} from 'react';
+
+import type {ScheduledPost} from '@mattermost/types/schedule_post';
+import type {UserProfile, UserStatus} from '@mattermost/types/users';
+
+import DraftRow from 'components/drafts/draft_row';
+
+import {useQuery} from 'utils/http_utils';
+
+type Props = {
+ scheduledPosts: ScheduledPost[];
+ currentUser: UserProfile;
+ userDisplayName: string;
+ userStatus: UserStatus['status'];
+}
+
+export default function NonVirtualizedScheduledPostList(props: Props) {
+ const query = useQuery();
+ const targetId = query.get('target_id');
+ const targetScheduledPostId = useRef();
+
+ return (
+ <>
+ {
+ props.scheduledPosts.map((scheduledPost, index) => {
+ // find the first scheduled posst with the target and no error
+ const isInTargetChannelOrThread = scheduledPost.channel_id === targetId || scheduledPost.root_id === targetId;
+ const hasError = Boolean(scheduledPost.error_code);
+ const scrollIntoView = !targetScheduledPostId.current && (isInTargetChannelOrThread && !hasError);
+ if (scrollIntoView) {
+ // if found, save the scheduled post's ID
+ targetScheduledPostId.current = scheduledPost.id;
+ }
+
+ return (
+
+ );
+ })
+ }
+ >
+ );
+}
diff --git a/webapp/channels/src/components/drafts/scheduled_post_list/scheduled_post_error.tsx b/webapp/channels/src/components/drafts/scheduled_post_list/scheduled_post_error.tsx
new file mode 100644
index 0000000000..58456bd6bf
--- /dev/null
+++ b/webapp/channels/src/components/drafts/scheduled_post_list/scheduled_post_error.tsx
@@ -0,0 +1,22 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import React from 'react';
+import {FormattedMessage} from 'react-intl';
+
+import AlertBanner from 'components/alert_banner';
+
+export default function ScheduledPostError() {
+ return (
+
+ }
+ />
+ );
+}
diff --git a/webapp/channels/src/components/drafts/scheduled_post_list/style.scss b/webapp/channels/src/components/drafts/scheduled_post_list/scheduled_post_list.scss
similarity index 72%
rename from webapp/channels/src/components/drafts/scheduled_post_list/style.scss
rename to webapp/channels/src/components/drafts/scheduled_post_list/scheduled_post_list.scss
index c57bdbf154..91a36367ee 100644
--- a/webapp/channels/src/components/drafts/scheduled_post_list/style.scss
+++ b/webapp/channels/src/components/drafts/scheduled_post_list/scheduled_post_list.scss
@@ -2,17 +2,26 @@
// See LICENSE.txt for license information.
.ScheduledPostList {
- position: absolute;
- width: 100%;
height: 100%;
- padding: 24px;
- overflow-y: auto;
+
+ &.nonVirtualizedScheduledPostList {
+ position: absolute;
+ overflow-y: auto;
+ }
+
+ .nonVirtualizedScheduledPostRow {
+ margin: 0 24px 16px 24px;
+
+ &.firstRow {
+ margin-top: 24px;
+ }
+ }
.scheduledPostListErrorIndicator {
display: flex;
flex-direction: row;
align-items: center;
- margin-bottom: 16px;
+ margin: 24px 24px 16px 24px;
font-weight: bold;
padding-block: 10px;
}
@@ -35,6 +44,4 @@
.Panel.highlighted {
animation: borderColorAnimation 10s;
}
-
-
}
diff --git a/webapp/channels/src/components/drafts/scheduled_post_list/scheduled_post_list.tsx b/webapp/channels/src/components/drafts/scheduled_post_list/scheduled_post_list.tsx
deleted file mode 100644
index 24015b1676..0000000000
--- a/webapp/channels/src/components/drafts/scheduled_post_list/scheduled_post_list.tsx
+++ /dev/null
@@ -1,112 +0,0 @@
-// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
-// See LICENSE.txt for license information.
-
-import React, {useEffect, useRef} from 'react';
-import {FormattedMessage, useIntl} from 'react-intl';
-import {useDispatch, useSelector} from 'react-redux';
-
-import type {ScheduledPost} from '@mattermost/types/schedule_post';
-import type {UserProfile, UserStatus} from '@mattermost/types/users';
-
-import {fetchMissingChannels} from 'mattermost-redux/actions/channels';
-import {hasScheduledPostError} from 'mattermost-redux/selectors/entities/scheduled_posts';
-import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
-
-import AlertBanner from 'components/alert_banner';
-import NoScheduledPostsIllustration from 'components/drafts/scheduled_post_list/empty_scheduled_post_list_illustration';
-import NoResultsIndicator from 'components/no_results_indicator';
-
-import {useQuery} from 'utils/http_utils';
-
-import type {GlobalState} from 'types/store';
-
-import DraftRow from '../draft_row';
-
-import './style.scss';
-
-type Props = {
- scheduledPosts: ScheduledPost[];
- user: UserProfile;
- displayName: string;
- status: UserStatus['status'];
-}
-
-export default function ScheduledPostList({
- scheduledPosts,
- user,
- displayName,
- status,
-}: Props) {
- const {formatMessage} = useIntl();
-
- const currentTeamId = useSelector(getCurrentTeamId);
- const scheduledPostsHasError = useSelector((state: GlobalState) => hasScheduledPostError(state, currentTeamId));
-
- const query = useQuery();
- const targetId = query.get('target_id');
- const targetScheduledPostId = useRef();
-
- const dispatch = useDispatch();
- useEffect(() => {
- dispatch(fetchMissingChannels(scheduledPosts.map((post) => post.channel_id)));
- }, [scheduledPosts]);
-
- return (
-
- {
- scheduledPostsHasError &&
-
- }
- />
- }
-
- {
- scheduledPosts.map((scheduledPost) => {
- // find the first scheduled posst with the target and no error
- const isInTargetChannelOrThread = scheduledPost.channel_id === targetId || scheduledPost.root_id === targetId;
- const hasError = Boolean(scheduledPost.error_code);
- const scrollIntoView = !targetScheduledPostId.current && (isInTargetChannelOrThread && !hasError);
- if (scrollIntoView) {
- // if found, save the scheduled post's ID
- targetScheduledPostId.current = scheduledPost.id;
- }
-
- return (
-
- );
- })
- }
-
- {
- scheduledPosts.length === 0 && (
-
- )
- }
-
- );
-}
diff --git a/webapp/channels/src/components/drafts/scheduled_post_list/virtualized_scheduled_post_list.tsx b/webapp/channels/src/components/drafts/scheduled_post_list/virtualized_scheduled_post_list.tsx
new file mode 100644
index 0000000000..0490c734ed
--- /dev/null
+++ b/webapp/channels/src/components/drafts/scheduled_post_list/virtualized_scheduled_post_list.tsx
@@ -0,0 +1,275 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import classNames from 'classnames';
+import debounce from 'lodash/debounce';
+import type {CSSProperties} from 'react';
+import React, {useMemo, useRef, useCallback, useEffect} from 'react';
+import AutoSizer from 'react-virtualized-auto-sizer';
+import {VariableSizeList} from 'react-window';
+
+import type {ScheduledPost} from '@mattermost/types/schedule_post';
+import type {UserProfile, UserStatus} from '@mattermost/types/users';
+
+import DraftRow from 'components/drafts/draft_row';
+
+import {useQuery} from 'utils/http_utils';
+
+const TARGET_ID_QUERY_PARAM = 'target_id';
+const OVERSCAN_ROW_COUNT = 10; // no. of rows
+const ROW_HEIGHT_CHANGE_TOLERANCE = 2; // in px
+
+const FRAME_RATE = 60; // in Hz
+const RESIZE_DEBOUNCE_TIME = Math.round(1000 / FRAME_RATE); // in ms
+
+type Props = {
+ scheduledPosts: ScheduledPost[];
+ currentUser: UserProfile;
+ userDisplayName: string;
+ userStatus: UserStatus['status'];
+}
+
+export default function ScheduledPostList(props: Props) {
+ const query = useQuery();
+ const scheduledPostTargetId = query.get(TARGET_ID_QUERY_PARAM);
+ const targetScheduledPostId = useRef();
+ const listRef = useRef(null);
+ const itemHeightCacheMap = useRef