diff --git a/webapp/channels/src/components/threading/channel_threads/thread_footer/thread_footer.tsx b/webapp/channels/src/components/threading/channel_threads/thread_footer/thread_footer.tsx index 2ea6deca3e..2e8b23b2d8 100644 --- a/webapp/channels/src/components/threading/channel_threads/thread_footer/thread_footer.tsx +++ b/webapp/channels/src/components/threading/channel_threads/thread_footer/thread_footer.tsx @@ -60,6 +60,7 @@ function ThreadFooter({ channel_id: channelId, }, } = thread; + const participantIds = useMemo(() => (participants || []).map(({id}) => id).reverse(), [participants]); const handleReply = useCallback((e) => { diff --git a/webapp/channels/src/components/threading/thread_viewer/thread_viewer.test.tsx b/webapp/channels/src/components/threading/thread_viewer/thread_viewer.test.tsx index f2a1a284c0..e29ca45ad3 100644 --- a/webapp/channels/src/components/threading/thread_viewer/thread_viewer.test.tsx +++ b/webapp/channels/src/components/threading/thread_viewer/thread_viewer.test.tsx @@ -32,6 +32,7 @@ describe('components/threading/ThreadViewer', () => { user_id: post.user_id, channel_id: post.channel_id, message: post.message, + reply_count: 3, }; const channel: Channel = TestHelper.getChannelMock({ diff --git a/webapp/channels/src/components/threading/virtualized_thread_viewer/thread_viewer_row.tsx b/webapp/channels/src/components/threading/virtualized_thread_viewer/thread_viewer_row.tsx index 85ea0d3764..ff758a4f00 100644 --- a/webapp/channels/src/components/threading/virtualized_thread_viewer/thread_viewer_row.tsx +++ b/webapp/channels/src/components/threading/virtualized_thread_viewer/thread_viewer_row.tsx @@ -22,6 +22,7 @@ import Reply from './reply'; type Props = { a11yIndex: number; currentUserId: string; + replyCount: number; isRootPost: boolean; isLastPost: boolean; listId: string; @@ -40,6 +41,7 @@ function ThreadViewerRow({ isRootPost, isLastPost, listId, + replyCount, onCardClick, previousPostId, timestampProps, @@ -70,13 +72,20 @@ function ThreadViewerRow({ case isRootPost: return ( - + <> + + {replyCount > 0 && ( +
+
{`${replyCount} Replies`}
+
+ )} + ); case PostListUtils.isCombinedUserActivityPost(listId): { return ( diff --git a/webapp/channels/src/components/threading/virtualized_thread_viewer/virtualized_thread_viewer.test.tsx b/webapp/channels/src/components/threading/virtualized_thread_viewer/virtualized_thread_viewer.test.tsx index b8e11ed76b..1fd485777f 100644 --- a/webapp/channels/src/components/threading/virtualized_thread_viewer/virtualized_thread_viewer.test.tsx +++ b/webapp/channels/src/components/threading/virtualized_thread_viewer/virtualized_thread_viewer.test.tsx @@ -26,6 +26,7 @@ function getBasePropsAndState(): [Props, DeepPartial] { const currentUser = TestHelper.getUserMock({roles: 'role'}); const post = TestHelper.getPostMock({ channel_id: channel.id, + reply_count: 0, }); const directTeammate: UserProfile = TestHelper.getUserMock(); diff --git a/webapp/channels/src/components/threading/virtualized_thread_viewer/virtualized_thread_viewer.tsx b/webapp/channels/src/components/threading/virtualized_thread_viewer/virtualized_thread_viewer.tsx index 0b958e8544..015a168755 100644 --- a/webapp/channels/src/components/threading/virtualized_thread_viewer/virtualized_thread_viewer.tsx +++ b/webapp/channels/src/components/threading/virtualized_thread_viewer/virtualized_thread_viewer.tsx @@ -3,14 +3,17 @@ import {DynamicSizeList} from 'dynamic-virtualized-list'; import type {OnScrollArgs, OnItemsRenderedArgs} from 'dynamic-virtualized-list'; -import React, {PureComponent} from 'react'; +import React, {PureComponent, useMemo} from 'react'; import type {RefObject} from 'react'; +import {useSelector} from 'react-redux'; import AutoSizer from 'react-virtualized-auto-sizer'; import type {Channel} from '@mattermost/types/channels'; import type {Post} from '@mattermost/types/posts'; import type {UserProfile} from '@mattermost/types/users'; +import {getPost} from 'mattermost-redux/selectors/entities/posts'; +import {makeGetThreadOrSynthetic} from 'mattermost-redux/selectors/entities/threads'; import {isDateLine, isStartOfNewMessages, isCreateComment} from 'mattermost-redux/utils/post_list'; import NewRepliesBanner from 'components/new_replies_banner'; @@ -22,6 +25,7 @@ import DelayedAction from 'utils/delayed_action'; import {getNewMessageIndex, getPreviousPostId, getLatestPostId} from 'utils/post_utils'; import * as Utils from 'utils/utils'; +import type {GlobalState} from 'types/store'; import type {PluginComponent} from 'types/store/plugins'; import type {FakePost} from 'types/store/rhs'; @@ -350,6 +354,14 @@ class ThreadViewerVirtualized extends PureComponent { const isLastPost = itemId === this.props.lastPost.id; const isRootPost = itemId === this.props.selected.id; + const post = useSelector((state: GlobalState) => getPost(state, this.props.selected.id)); + const getThreadOrSynthetic = useMemo(makeGetThreadOrSynthetic, []); + + const totalReplies = useSelector((state: GlobalState) => { + const thread = getThreadOrSynthetic(state, post); + return thread.reply_count || 0; + }); + if (!isDateLine(itemId) && !isStartOfNewMessages(itemId) && !isCreateComment(itemId) && !isRootPost) { a11yIndex++; } @@ -379,6 +391,7 @@ class ThreadViewerVirtualized extends PureComponent { isRootPost={isRootPost} isLastPost={isLastPost} listId={itemId} + replyCount={totalReplies} onCardClick={this.props.onCardClick} previousPostId={getPreviousPostId(data, index)} timestampProps={this.props.useRelativeTimestamp ? THREADING_TIME : undefined} diff --git a/webapp/channels/src/sass/components/_post-right.scss b/webapp/channels/src/sass/components/_post-right.scss index f75dcd8bbd..8c4326297b 100644 --- a/webapp/channels/src/sass/components/_post-right.scss +++ b/webapp/channels/src/sass/components/_post-right.scss @@ -38,9 +38,37 @@ display: none; } + .root-post__divider { + position: relative; + display: flex; + height: 28px; + align-items: center; + margin: 0 0 4px 30px; + + div { + z-index: 1; + padding: 0 12px; + margin-left: 34px; + background: rgba(v(center-channel-bg-rgb), 1); + color: rgba(v(center-channel-color-rgb), 0.72); + font-size: 12px; + font-weight: 600; + } + + &::before { + position: absolute; + top: calc(50% - 1px); + left: 0; + display: block; + width: 100%; + border-top: 1px solid rgba(var(--center-channel-color-rgb), 0.12); + content: ""; + } + } + .post { &.post--root { - padding-top: 2rem; + padding-top: 16px; .post__body { background: transparent !important; @@ -52,9 +80,9 @@ } .post-pre-header__icons-container { - width: 60px; // If the width of post__img changes, this needs to be adjusted accordingly - padding-right: 12px; // If the padding of post__img changes, this needs to be adjusted accordingly - margin-left: 0; // if left margin of post__content changes, this needs to be adjusted accordingly + width: 54px; // If the width of post__img changes, this needs to be adjusted accordingly; + padding-right: 12px; // If the padding of post__img changes, this needs to be adjusted accordingly; + margin-left: 0; // if left margin of post__content changes, this needs to be adjusted accordingly; } .post__header { @@ -119,8 +147,8 @@ } .post__img { - width: 60px; // if this changes, the width of post-pre-header__icons-container needs to be adjusted accordingly - padding: 2px 12px 0 0; // if the right padding changes, the padding of post-pre-header__icons-container needs to be adjusted accordingly + width: 54px; // if this changes, the width of post-pre-header__icons-container needs to be adjusted accordingly; + padding: 2px 12px 0 0; // if the right padding changes, the padding of post-pre-header__icons-container needs to be adjusted accordingly; } .post-body { diff --git a/webapp/channels/src/sass/responsive/_mobile.scss b/webapp/channels/src/sass/responsive/_mobile.scss index 9fdcf0f6ac..08bba0ec53 100644 --- a/webapp/channels/src/sass/responsive/_mobile.scss +++ b/webapp/channels/src/sass/responsive/_mobile.scss @@ -440,7 +440,7 @@ .post { &.post--thread { .post-pre-header__icons-container { - width: 60px; // If the width of post__img changes, this needs to be adjusted accordingly + width: 44px; // If the width of post__img changes, this needs to be adjusted accordingly padding-right: 12px; // If the padding of post__img changes, this needs to be adjusted accordingly margin-left: 0; // if left margin of post__content changes, this needs to be adjusted accordingly } diff --git a/webapp/channels/src/selectors/rhs.ts b/webapp/channels/src/selectors/rhs.ts index 42b924e6ce..b7cffd4a19 100644 --- a/webapp/channels/src/selectors/rhs.ts +++ b/webapp/channels/src/selectors/rhs.ts @@ -94,6 +94,7 @@ export const getSelectedPost = createSelector( message: localizeMessage('rhs_thread.rootPostDeletedMessage.body', 'Part of this thread has been deleted due to a data retention policy. You can no longer reply to this thread.'), channel_id: selectedPostChannelId, user_id: currentUserId, + reply_count: 0, }; }, ); diff --git a/webapp/channels/src/types/store/rhs.ts b/webapp/channels/src/types/store/rhs.ts index c45dc2fc63..446e70a70c 100644 --- a/webapp/channels/src/types/store/rhs.ts +++ b/webapp/channels/src/types/store/rhs.ts @@ -16,6 +16,7 @@ export type FakePost = { exists: boolean; type: PostType; message: string; + reply_count: number; channel_id: Channel['id']; user_id: UserProfile['id']; };