diff --git a/webapp/channels/src/actions/global_actions.tsx b/webapp/channels/src/actions/global_actions.tsx index aa18c5a89e..cb22542fa4 100644 --- a/webapp/channels/src/actions/global_actions.tsx +++ b/webapp/channels/src/actions/global_actions.tsx @@ -65,7 +65,7 @@ export function emitChannelClickEvent(channel: Channel) { const currentChannelId = getCurrentChannelId(state); const previousRhsState = getPreviousRhsState(state); - dispatch(getChannelStats(chan.id)); + dispatch(getChannelStats(chan.id, true)); const penultimate = LocalStorageStore.getPreviousChannelName(userId, teamId); const penultimateType = LocalStorageStore.getPreviousViewedType(userId, teamId); diff --git a/webapp/channels/src/components/channel_info_rhs/channel_info_rhs.test.tsx b/webapp/channels/src/components/channel_info_rhs/channel_info_rhs.test.tsx index a9de0e72fd..4babb664d7 100644 --- a/webapp/channels/src/components/channel_info_rhs/channel_info_rhs.test.tsx +++ b/webapp/channels/src/components/channel_info_rhs/channel_info_rhs.test.tsx @@ -3,8 +3,11 @@ import React from 'react'; -import {Channel, ChannelStats} from '@mattermost/types/channels'; +import {act} from '@testing-library/react'; + import {renderWithIntl} from 'tests/react_testing_utils'; + +import {Channel, ChannelStats} from '@mattermost/types/channels'; import {UserProfile} from '@mattermost/types/users'; import {Team} from '@mattermost/types/teams'; @@ -40,6 +43,7 @@ describe('channel_info_rhs', () => { showChannelFiles: jest.fn(), showPinnedPosts: jest.fn(), showChannelMembers: jest.fn(), + getChannelStats: jest.fn().mockImplementation(() => Promise.resolve({data: {}})), }, }; let props = {...OriginalProps}; @@ -49,20 +53,24 @@ describe('channel_info_rhs', () => { }); describe('about area', () => { - test('should be editable', () => { + test('should be editable', async () => { renderWithIntl( , ); + await act(async () => { + props.actions.getChannelStats(); + }); + expect(mockAboutArea).toHaveBeenCalledWith( expect.objectContaining({ canEditChannelProperties: true, }), ); }); - test('should not be editable in archived channel', () => { + test('should not be editable in archived channel', async () => { props.isArchived = true; renderWithIntl( @@ -71,6 +79,10 @@ describe('channel_info_rhs', () => { />, ); + await act(async () => { + props.actions.getChannelStats(); + }); + expect(mockAboutArea).toHaveBeenCalledWith( expect.objectContaining({ canEditChannelProperties: false, diff --git a/webapp/channels/src/components/channel_info_rhs/channel_info_rhs.tsx b/webapp/channels/src/components/channel_info_rhs/channel_info_rhs.tsx index f16b9c77ea..7f75970911 100644 --- a/webapp/channels/src/components/channel_info_rhs/channel_info_rhs.tsx +++ b/webapp/channels/src/components/channel_info_rhs/channel_info_rhs.tsx @@ -64,6 +64,7 @@ export interface Props { showChannelFiles: (channelId: string) => void; showPinnedPosts: (channelId: string | undefined) => void; showChannelMembers: (channelId: string) => void; + getChannelStats: (channelId: string) => Promise<{data: ChannelStats}>; }; } @@ -192,6 +193,7 @@ const ChannelInfoRhs = ({ showChannelFiles: actions.showChannelFiles, showPinnedPosts: actions.showPinnedPosts, showChannelMembers: actions.showChannelMembers, + getChannelStats: actions.getChannelStats, }} /> diff --git a/webapp/channels/src/components/channel_info_rhs/index.ts b/webapp/channels/src/components/channel_info_rhs/index.ts index 09d6e8e49d..f30d8ebb8b 100644 --- a/webapp/channels/src/components/channel_info_rhs/index.ts +++ b/webapp/channels/src/components/channel_info_rhs/index.ts @@ -16,7 +16,7 @@ import {Constants, ModalIdentifiers} from 'utils/constants'; import {getCurrentUser} from 'mattermost-redux/selectors/entities/common'; import {getIsMobileView} from 'selectors/views/browser'; import {getCurrentTeam} from 'mattermost-redux/selectors/entities/teams'; -import {unfavoriteChannel, favoriteChannel} from 'mattermost-redux/actions/channels'; +import {unfavoriteChannel, favoriteChannel, getChannelStats} from 'mattermost-redux/actions/channels'; import {muteChannel, unmuteChannel} from 'actions/channel_actions'; import {openModal} from 'actions/views/modals'; import {getDisplayNameByUser, getUserIdFromChannelId} from 'utils/utils'; @@ -92,6 +92,7 @@ function mapDispatchToProps(dispatch: Dispatch) { showChannelFiles, showPinnedPosts, showChannelMembers, + getChannelStats, }, dispatch), }; } diff --git a/webapp/channels/src/components/channel_info_rhs/menu.test.tsx b/webapp/channels/src/components/channel_info_rhs/menu.test.tsx index 1e4a8d5537..d68f9a2392 100644 --- a/webapp/channels/src/components/channel_info_rhs/menu.test.tsx +++ b/webapp/channels/src/components/channel_info_rhs/menu.test.tsx @@ -2,12 +2,13 @@ // See LICENSE.txt for license information. import React from 'react'; -import {fireEvent, screen} from '@testing-library/react'; +import {act, fireEvent, screen} from '@testing-library/react'; -import {Channel, ChannelStats} from '@mattermost/types/channels'; import {renderWithIntl} from 'tests/react_testing_utils'; import Constants from 'utils/constants'; +import {Channel, ChannelStats} from '@mattermost/types/channels'; + import Menu from './menu'; describe('channel_info_rhs/menu', () => { @@ -20,6 +21,7 @@ describe('channel_info_rhs/menu', () => { showChannelFiles: jest.fn(), showPinnedPosts: jest.fn(), showChannelMembers: jest.fn(), + getChannelStats: jest.fn().mockImplementation(() => Promise.resolve({data: {files_count: 3, pinnedpost_count: 12, member_count: 32}})), }, }; @@ -29,10 +31,11 @@ describe('channel_info_rhs/menu', () => { showChannelFiles: jest.fn(), showPinnedPosts: jest.fn(), showChannelMembers: jest.fn(), + getChannelStats: jest.fn().mockImplementation(() => Promise.resolve({data: {files_count: 3, pinnedpost_count: 12, member_count: 32}})), }; }); - test('should display notifications preferences', () => { + test('should display notifications preferences', async () => { const props = {...defaultProps}; props.actions.openNotificationSettings = jest.fn(); @@ -42,13 +45,17 @@ describe('channel_info_rhs/menu', () => { />, ); + await act(async () => { + props.actions.getChannelStats(); + }); + expect(screen.getByText('Notification Preferences')).toBeInTheDocument(); fireEvent.click(screen.getByText('Notification Preferences')); expect(props.actions.openNotificationSettings).toHaveBeenCalled(); }); - test('should NOT display notifications preferences in a DM', () => { + test('should NOT display notifications preferences in a DM', async () => { const props = { ...defaultProps, channel: {type: Constants.DM_CHANNEL} as Channel, @@ -60,10 +67,14 @@ describe('channel_info_rhs/menu', () => { />, ); + await act(async () => { + props.actions.getChannelStats(); + }); + expect(screen.queryByText('Notification Preferences')).not.toBeInTheDocument(); }); - test('should NOT display notifications preferences in an archived channel', () => { + test('should NOT display notifications preferences in an archived channel', async () => { const props = { ...defaultProps, isArchived: true, @@ -75,10 +86,14 @@ describe('channel_info_rhs/menu', () => { />, ); + await act(async () => { + props.actions.getChannelStats(); + }); + expect(screen.queryByText('Notification Preferences')).not.toBeInTheDocument(); }); - test('should display the number of files', () => { + test('should display the number of files', async () => { const props = {...defaultProps}; props.actions.showChannelFiles = jest.fn(); @@ -88,6 +103,10 @@ describe('channel_info_rhs/menu', () => { />, ); + await act(async () => { + props.actions.getChannelStats(); + }); + const fileItem = screen.getByText('Files'); expect(fileItem).toBeInTheDocument(); expect(fileItem.parentElement).toHaveTextContent('3'); @@ -96,7 +115,7 @@ describe('channel_info_rhs/menu', () => { expect(props.actions.showChannelFiles).toHaveBeenCalled(); }); - test('should display the pinned messages', () => { + test('should display the pinned messages', async () => { const props = {...defaultProps}; props.actions.showPinnedPosts = jest.fn(); @@ -106,6 +125,10 @@ describe('channel_info_rhs/menu', () => { />, ); + await act(async () => { + props.actions.getChannelStats(); + }); + const fileItem = screen.getByText('Pinned Messages'); expect(fileItem).toBeInTheDocument(); expect(fileItem.parentElement).toHaveTextContent('12'); @@ -114,7 +137,7 @@ describe('channel_info_rhs/menu', () => { expect(props.actions.showPinnedPosts).toHaveBeenCalled(); }); - test('should display members', () => { + test('should display members', async () => { const props = {...defaultProps}; props.actions.showChannelMembers = jest.fn(); @@ -124,6 +147,10 @@ describe('channel_info_rhs/menu', () => { />, ); + await act(async () => { + props.actions.getChannelStats(); + }); + const membersItem = screen.getByText('Members'); expect(membersItem).toBeInTheDocument(); expect(membersItem.parentElement).toHaveTextContent('32'); @@ -132,7 +159,7 @@ describe('channel_info_rhs/menu', () => { expect(props.actions.showChannelMembers).toHaveBeenCalled(); }); - test('should NOT display members in DM', () => { + test('should NOT display members in DM', async () => { const props = { ...defaultProps, channel: {type: Constants.DM_CHANNEL} as Channel, @@ -144,6 +171,10 @@ describe('channel_info_rhs/menu', () => { />, ); + await act(async () => { + props.actions.getChannelStats(); + }); + const membersItem = screen.queryByText('Members'); expect(membersItem).not.toBeInTheDocument(); }); diff --git a/webapp/channels/src/components/channel_info_rhs/menu.tsx b/webapp/channels/src/components/channel_info_rhs/menu.tsx index 1092294e72..a45657c319 100644 --- a/webapp/channels/src/components/channel_info_rhs/menu.tsx +++ b/webapp/channels/src/components/channel_info_rhs/menu.tsx @@ -1,12 +1,14 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import React from 'react'; +import React, {useEffect, useState} from 'react'; import styled from 'styled-components'; import {useIntl} from 'react-intl'; import {Constants} from 'utils/constants'; +import LoadingSpinner from 'components/widgets/loading/loading_spinner'; + import {Channel, ChannelStats} from '@mattermost/types/channels'; const MenuItemContainer = styled.div` @@ -32,6 +34,9 @@ const RightSide = styled.div` const Badge = styled.div` font-size: 12px; line-height: 18px; + width: 20px; + display: flex; + place-content: center; `; interface MenuItemProps { @@ -39,7 +44,7 @@ interface MenuItemProps { icon: JSX.Element; text: string; opensSubpanel?: boolean; - badge?: string|number; + badge?: string|number|JSX.Element; onClick: () => void; } @@ -94,14 +99,26 @@ interface MenuProps { showChannelFiles: (channelId: string) => void; showPinnedPosts: (channelId: string | undefined) => void; showChannelMembers: (channelId: string) => void; + getChannelStats: (channelId: string) => Promise<{data: ChannelStats}>; }; } const Menu = ({channel, channelStats, isArchived, className, actions}: MenuProps) => { const {formatMessage} = useIntl(); + const [loadingStats, setLoadingStats] = useState(true); const showNotificationPreferences = channel.type !== Constants.DM_CHANNEL && !isArchived; const showMembers = channel.type !== Constants.DM_CHANNEL; + const fileCount = channelStats?.files_count >= 0 ? channelStats?.files_count : 0; + + useEffect(() => { + actions.getChannelStats(channel.id).then(() => { + setLoadingStats(false); + }); + return () => { + setLoadingStats(true); + }; + }, [channel.id]); return (
} text={formatMessage({id: 'channel_info_rhs.menu.files', defaultMessage: 'Files'})} opensSubpanel={true} - badge={channelStats?.files_count} + badge={loadingStats ? : fileCount} onClick={() => actions.showChannelFiles(channel.id)} />
diff --git a/webapp/channels/src/packages/mattermost-redux/src/actions/channels.ts b/webapp/channels/src/packages/mattermost-redux/src/actions/channels.ts index 29e07bd064..f5afb85aa2 100644 --- a/webapp/channels/src/packages/mattermost-redux/src/actions/channels.ts +++ b/webapp/channels/src/packages/mattermost-redux/src/actions/channels.ts @@ -8,7 +8,6 @@ import {ChannelTypes, PreferenceTypes, UserTypes} from 'mattermost-redux/action_ import {Client4} from 'mattermost-redux/client'; -import {General, Preferences} from '../constants'; import {CategoryTypes} from 'mattermost-redux/constants/channel_categories'; import {MarkUnread} from 'mattermost-redux/constants/channels'; @@ -25,12 +24,15 @@ import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams'; import {ActionFunc, ActionResult, DispatchFunc, GetStateFunc} from 'mattermost-redux/types/actions'; +import {getChannelsIdForTeam, getChannelByName} from 'mattermost-redux/utils/channel_utils'; + +import {isMinimumServerVersion} from 'mattermost-redux/utils/helpers'; + import {Channel, ChannelNotifyProps, ChannelMembership, ChannelModerationPatch, ChannelsWithTotalCount, ChannelSearchOpts} from '@mattermost/types/channels'; import {PreferenceType} from '@mattermost/types/preferences'; -import {getChannelsIdForTeam, getChannelByName} from 'mattermost-redux/utils/channel_utils'; -import {isMinimumServerVersion} from 'mattermost-redux/utils/helpers'; +import {General, Preferences} from '../constants'; import {addChannelToInitialCategory, addChannelToCategory} from './channel_categories'; import {logError} from './errors'; @@ -1074,11 +1076,11 @@ export function searchGroupChannels(term: string): ActionFunc { }); } -export function getChannelStats(channelId: string): ActionFunc { +export function getChannelStats(channelId: string, excludeFilesCount?: boolean): ActionFunc { return async (dispatch: DispatchFunc, getState: GetStateFunc) => { let stat; try { - stat = await Client4.getChannelStats(channelId); + stat = await Client4.getChannelStats(channelId, excludeFilesCount); } catch (error) { forceLogoutIfNecessary(error, dispatch, getState); dispatch(logError(error)); diff --git a/webapp/platform/client/src/client4.ts b/webapp/platform/client/src/client4.ts index 56e2f552f1..21a1a8798f 100644 --- a/webapp/platform/client/src/client4.ts +++ b/webapp/platform/client/src/client4.ts @@ -1782,9 +1782,10 @@ export default class Client4 { ); }; - getChannelStats = (channelId: string) => { + getChannelStats = (channelId: string, excludeFilesCount = false) => { + const param = excludeFilesCount ? `?exclude_files_count=${excludeFilesCount}` : ''; return this.doFetch( - `${this.getChannelRoute(channelId)}/stats`, + `${this.getChannelRoute(channelId)}/stats${param}`, {method: 'get'}, ); };