Fix several re-renders on init (#26361)

* Fix several re-renders on init

* Fix tests

* Address feedback

---------

Co-authored-by: Mattermost Build <build@mattermost.com>
This commit is contained in:
Daniel Espino García 2024-07-11 12:58:56 +02:00 committed by GitHub
parent 19d59d1126
commit e8b4892877
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 162 additions and 159 deletions

View File

@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import {useState, useEffect, useMemo} from 'react'; import {useEffect, useMemo, useRef} from 'react';
import {useDispatch, useSelector} from 'react-redux'; import {useDispatch, useSelector} from 'react-redux';
import type {Product} from '@mattermost/types/cloud'; import type {Product} from '@mattermost/types/cloud';
@ -19,14 +19,15 @@ export default function useGetSelfHostedProducts(): [Record<string, Product>, bo
const products = useSelector(getSelfHostedProducts); const products = useSelector(getSelfHostedProducts);
const productsReceived = useSelector(getSelfHostedProductsLoaded); const productsReceived = useSelector(getSelfHostedProductsLoaded);
const dispatch = useDispatch(); const dispatch = useDispatch();
const [requested, setRequested] = useState(false); const requested = useRef(false);
useEffect(() => { useEffect(() => {
if (isLoggedIn && !isCloud && !requested && !productsReceived) { if (isLoggedIn && !isCloud && !requested.current && !productsReceived) {
dispatch(getSelfHostedProductsAction()); dispatch(getSelfHostedProductsAction());
setRequested(true); requested.current = true;
} }
}, [isLoggedIn, isCloud, requested, productsReceived]); }, [isLoggedIn, isCloud, productsReceived]);
const result: [Record<string, Product>, boolean] = useMemo(() => { const result: [Record<string, Product>, boolean] = useMemo(() => {
return [products, productsReceived]; return [products, productsReceived];
}, [products, productsReceived]); }, [products, productsReceived]);

View File

@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import React, {useState, useEffect} from 'react'; import React, {useMemo} from 'react';
import ThemeProvider, {lightTheme} from '@mattermost/compass-components/utilities/theme'; // eslint-disable-line no-restricted-imports import ThemeProvider, {lightTheme} from '@mattermost/compass-components/utilities/theme'; // eslint-disable-line no-restricted-imports
@ -12,45 +12,48 @@ type Props = {
children?: React.ReactNode; children?: React.ReactNode;
} }
const CompassThemeProvider = ({theme, children}: Props): JSX.Element | null => { const CompassThemeProvider = ({
const [compassTheme, setCompassTheme] = useState({ theme,
...lightTheme, children,
noStyleReset: true, }: Props) => {
noDefaultStyle: true, const compassTheme = useMemo(() => {
noFontFaces: true, const base = {
}); ...lightTheme,
noStyleReset: true,
noDefaultStyle: true,
noFontFaces: true,
};
useEffect(() => { return {
setCompassTheme({ ...base,
...compassTheme,
palette: { palette: {
...compassTheme.palette, ...base.palette,
primary: { primary: {
...compassTheme.palette.primary, ...base.palette.primary,
main: theme.sidebarHeaderBg, main: theme.sidebarHeaderBg,
contrast: theme.sidebarHeaderTextColor, contrast: theme.sidebarHeaderTextColor,
}, },
alert: { alert: {
...compassTheme.palette.alert, ...base.palette.alert,
main: theme.dndIndicator, main: theme.dndIndicator,
}, },
}, },
action: { action: {
...compassTheme.action, ...base.action,
hover: theme.sidebarHeaderTextColor, hover: theme.sidebarHeaderTextColor,
disabled: theme.sidebarHeaderTextColor, disabled: theme.sidebarHeaderTextColor,
}, },
badges: { badges: {
...compassTheme.badges, ...base.badges,
online: theme.onlineIndicator, online: theme.onlineIndicator,
away: theme.awayIndicator, away: theme.awayIndicator,
dnd: theme.dndIndicator, dnd: theme.dndIndicator,
}, },
text: { text: {
...compassTheme.text, ...base.text,
primary: theme.sidebarHeaderTextColor, primary: theme.sidebarHeaderTextColor,
}, },
}); };
}, [theme]); }, [theme]);
return ( return (

View File

@ -161,11 +161,11 @@ const Skeleton = styled.div`
`; `;
const OnBoardingTaskList = (): JSX.Element | null => { const OnBoardingTaskList = (): JSX.Element | null => {
const myPreferences = useSelector((state: GlobalState) => getMyPreferencesSelector(state)); const hasPreferences = useSelector((state: GlobalState) => Object.keys(getMyPreferencesSelector(state)).length !== 0);
useEffect(() => { useEffect(() => {
dispatch(getPrevTrialLicense()); dispatch(getPrevTrialLicense());
if (Object.keys(myPreferences).length === 0) { if (!hasPreferences) {
dispatch(getMyPreferences()); dispatch(getMyPreferences());
} }
}, []); }, []);
@ -182,7 +182,10 @@ const OnBoardingTaskList = (): JSX.Element | null => {
const isCurrentUserSystemAdmin = useIsCurrentUserSystemAdmin(); const isCurrentUserSystemAdmin = useIsCurrentUserSystemAdmin();
const isFirstAdmin = useFirstAdminUser(); const isFirstAdmin = useFirstAdminUser();
const isEnableOnboardingFlow = useSelector((state: GlobalState) => getConfig(state).EnableOnboardingFlow === 'true'); const isEnableOnboardingFlow = useSelector((state: GlobalState) => getConfig(state).EnableOnboardingFlow === 'true');
const [showTaskList, firstTimeOnboarding] = useSelector(getShowTaskListBool); const [showTaskList, firstTimeOnboarding] = useSelector(
getShowTaskListBool,
(a, b) => a[0] === b[0] && a[1] === b[1],
);
const theme = useSelector(getTheme); const theme = useSelector(getTheme);
const startTask = (taskName: string) => { const startTask = (taskName: string) => {
@ -284,7 +287,7 @@ const OnBoardingTaskList = (): JSX.Element | null => {
})); }));
}, []); }, []);
if (Object.keys(myPreferences).length === 0 || !showTaskList || !isEnableOnboardingFlow) { if (!hasPreferences || !showTaskList || !isEnableOnboardingFlow) {
return null; return null;
} }

View File

@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import React, {memo, useCallback} from 'react'; import React, {memo, useCallback, useMemo} from 'react';
import {FormattedMessage, useIntl} from 'react-intl'; import {FormattedMessage, useIntl} from 'react-intl';
import {useDispatch, useSelector} from 'react-redux'; import {useDispatch, useSelector} from 'react-redux';
@ -42,13 +42,12 @@ type Props = {
category: ChannelCategory; category: ChannelCategory;
}; };
const getUnreadsIdsForCategory = makeGetUnreadIdsForCategory();
const SidebarCategoryMenu = ({ const SidebarCategoryMenu = ({
category, category,
}: Props) => { }: Props) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const showUnreadsCategory = useSelector(shouldShowUnreadsCategory); const showUnreadsCategory = useSelector(shouldShowUnreadsCategory);
const getUnreadsIdsForCategory = useMemo(makeGetUnreadIdsForCategory, [category]);
const unreadsIds = useSelector((state: GlobalState) => getUnreadsIdsForCategory(state, category)); const unreadsIds = useSelector((state: GlobalState) => getUnreadsIdsForCategory(state, category));
const {formatMessage} = useIntl(); const {formatMessage} = useIntl();

View File

@ -8,7 +8,6 @@ import type {Channel} from '@mattermost/types/channels';
import {favoriteChannel, unfavoriteChannel, markMultipleChannelsAsRead} from 'mattermost-redux/actions/channels'; import {favoriteChannel, unfavoriteChannel, markMultipleChannelsAsRead} from 'mattermost-redux/actions/channels';
import Permissions from 'mattermost-redux/constants/permissions'; import Permissions from 'mattermost-redux/constants/permissions';
import {getCategoryInTeamWithChannel} from 'mattermost-redux/selectors/entities/channel_categories';
import {isFavoriteChannel} from 'mattermost-redux/selectors/entities/channels'; import {isFavoriteChannel} from 'mattermost-redux/selectors/entities/channels';
import {getMyChannelMemberships, getCurrentUserId} from 'mattermost-redux/selectors/entities/common'; import {getMyChannelMemberships, getCurrentUserId} from 'mattermost-redux/selectors/entities/common';
import {haveIChannelPermission} from 'mattermost-redux/selectors/entities/roles'; import {haveIChannelPermission} from 'mattermost-redux/selectors/entities/roles';
@ -17,9 +16,7 @@ import {isChannelMuted} from 'mattermost-redux/utils/channel_utils';
import {unmuteChannel, muteChannel} from 'actions/channel_actions'; import {unmuteChannel, muteChannel} from 'actions/channel_actions';
import {markMostRecentPostInChannelAsUnread} from 'actions/post_actions'; import {markMostRecentPostInChannelAsUnread} from 'actions/post_actions';
import {addChannelsInSidebar} from 'actions/views/channel_sidebar';
import {openModal} from 'actions/views/modals'; import {openModal} from 'actions/views/modals';
import {getCategoriesForCurrentTeam, getDisplayedChannels} from 'selectors/views/channel_sidebar';
import {getSiteURL} from 'utils/url'; import {getSiteURL} from 'utils/url';
@ -41,27 +38,19 @@ function mapStateToProps(state: GlobalState, ownProps: OwnProps) {
let managePublicChannelMembers = false; let managePublicChannelMembers = false;
let managePrivateChannelMembers = false; let managePrivateChannelMembers = false;
let categories;
let currentCategory;
if (currentTeam) { if (currentTeam) {
managePublicChannelMembers = haveIChannelPermission(state, currentTeam.id, ownProps.channel.id, Permissions.MANAGE_PUBLIC_CHANNEL_MEMBERS); managePublicChannelMembers = haveIChannelPermission(state, currentTeam.id, ownProps.channel.id, Permissions.MANAGE_PUBLIC_CHANNEL_MEMBERS);
managePrivateChannelMembers = haveIChannelPermission(state, currentTeam.id, ownProps.channel.id, Permissions.MANAGE_PRIVATE_CHANNEL_MEMBERS); managePrivateChannelMembers = haveIChannelPermission(state, currentTeam.id, ownProps.channel.id, Permissions.MANAGE_PRIVATE_CHANNEL_MEMBERS);
categories = getCategoriesForCurrentTeam(state);
currentCategory = getCategoryInTeamWithChannel(state, currentTeam.id, ownProps.channel.id);
} }
return { return {
currentUserId: getCurrentUserId(state), currentUserId: getCurrentUserId(state),
categories,
currentCategory,
isFavorite: isFavoriteChannel(state, ownProps.channel.id), isFavorite: isFavoriteChannel(state, ownProps.channel.id),
isMuted: isChannelMuted(member), isMuted: isChannelMuted(member),
channelLink: `${getSiteURL()}${ownProps.channelLink}`, channelLink: `${getSiteURL()}${ownProps.channelLink}`,
managePublicChannelMembers, managePublicChannelMembers,
managePrivateChannelMembers, managePrivateChannelMembers,
displayedChannels: getDisplayedChannels(state),
multiSelectedChannelIds: state.views.channelSidebar.multiSelectedChannelIds,
}; };
} }
@ -73,7 +62,6 @@ const mapDispatchToProps = {
muteChannel, muteChannel,
unmuteChannel, unmuteChannel,
openModal, openModal,
addChannelsInSidebar,
}; };
const connector = connect(mapStateToProps, mapDispatchToProps); const connector = connect(mapStateToProps, mapDispatchToProps);

View File

@ -29,22 +29,40 @@ import type {PropsFromRedux, OwnProps} from './index';
type Props = PropsFromRedux & OwnProps; type Props = PropsFromRedux & OwnProps;
const SidebarChannelMenu = (props: Props) => { const SidebarChannelMenu = ({
channel,
channelLink,
currentUserId,
favoriteChannel,
isFavorite,
isMuted,
isUnread,
managePrivateChannelMembers,
managePublicChannelMembers,
markMultipleChannelsAsRead,
markMostRecentPostInChannelAsUnread,
muteChannel,
onMenuToggle,
openModal,
unfavoriteChannel,
unmuteChannel,
channelLeaveHandler,
}: Props) => {
const isLeaving = useRef(false); const isLeaving = useRef(false);
const {formatMessage} = useIntl(); const {formatMessage} = useIntl();
let markAsReadUnreadMenuItem: JSX.Element | null = null; let markAsReadUnreadMenuItem: JSX.Element | null = null;
if (props.isUnread) { if (isUnread) {
function handleMarkAsRead() { function handleMarkAsRead() {
// We use mark multiple to not update the active channel in the server // We use mark multiple to not update the active channel in the server
props.markMultipleChannelsAsRead({[props.channel.id]: Date.now()}); markMultipleChannelsAsRead({[channel.id]: Date.now()});
trackEvent('ui', 'ui_sidebar_channel_menu_markAsRead'); trackEvent('ui', 'ui_sidebar_channel_menu_markAsRead');
} }
markAsReadUnreadMenuItem = ( markAsReadUnreadMenuItem = (
<Menu.Item <Menu.Item
id={`markAsRead-${props.channel.id}`} id={`markAsRead-${channel.id}`}
onClick={handleMarkAsRead} onClick={handleMarkAsRead}
leadingElement={<MarkAsUnreadIcon size={18}/>} leadingElement={<MarkAsUnreadIcon size={18}/>}
labels={( labels={(
@ -58,13 +76,13 @@ const SidebarChannelMenu = (props: Props) => {
); );
} else { } else {
function handleMarkAsUnread() { function handleMarkAsUnread() {
props.markMostRecentPostInChannelAsUnread(props.channel.id); markMostRecentPostInChannelAsUnread(channel.id);
trackEvent('ui', 'ui_sidebar_channel_menu_markAsUnread'); trackEvent('ui', 'ui_sidebar_channel_menu_markAsUnread');
} }
markAsReadUnreadMenuItem = ( markAsReadUnreadMenuItem = (
<Menu.Item <Menu.Item
id={`markAsUnread-${props.channel.id}`} id={`markAsUnread-${channel.id}`}
onClick={handleMarkAsUnread} onClick={handleMarkAsUnread}
leadingElement={<MarkAsUnreadIcon size={18}/>} leadingElement={<MarkAsUnreadIcon size={18}/>}
labels={( labels={(
@ -78,15 +96,15 @@ const SidebarChannelMenu = (props: Props) => {
} }
let favoriteUnfavoriteMenuItem: JSX.Element | null = null; let favoriteUnfavoriteMenuItem: JSX.Element | null = null;
if (props.isFavorite) { if (isFavorite) {
function handleUnfavoriteChannel() { function handleUnfavoriteChannel() {
props.unfavoriteChannel(props.channel.id); unfavoriteChannel(channel.id);
trackEvent('ui', 'ui_sidebar_channel_menu_unfavorite'); trackEvent('ui', 'ui_sidebar_channel_menu_unfavorite');
} }
favoriteUnfavoriteMenuItem = ( favoriteUnfavoriteMenuItem = (
<Menu.Item <Menu.Item
id={`unfavorite-${props.channel.id}`} id={`unfavorite-${channel.id}`}
onClick={handleUnfavoriteChannel} onClick={handleUnfavoriteChannel}
leadingElement={<StarIcon size={18}/>} leadingElement={<StarIcon size={18}/>}
labels={( labels={(
@ -99,14 +117,14 @@ const SidebarChannelMenu = (props: Props) => {
); );
} else { } else {
function handleFavoriteChannel() { function handleFavoriteChannel() {
props.favoriteChannel(props.channel.id); favoriteChannel(channel.id);
trackEvent('ui', 'ui_sidebar_channel_menu_favorite'); trackEvent('ui', 'ui_sidebar_channel_menu_favorite');
} }
favoriteUnfavoriteMenuItem = ( favoriteUnfavoriteMenuItem = (
<Menu.Item <Menu.Item
id={`favorite-${props.channel.id}`} id={`favorite-${channel.id}`}
onClick={handleFavoriteChannel} onClick={handleFavoriteChannel}
leadingElement={<StarOutlineIcon size={18}/>} leadingElement={<StarOutlineIcon size={18}/>}
labels={( labels={(
@ -120,14 +138,14 @@ const SidebarChannelMenu = (props: Props) => {
} }
let muteUnmuteChannelMenuItem: JSX.Element | null = null; let muteUnmuteChannelMenuItem: JSX.Element | null = null;
if (props.isMuted) { if (isMuted) {
let muteChannelText = ( let muteChannelText = (
<FormattedMessage <FormattedMessage
id='sidebar_left.sidebar_channel_menu.unmuteChannel' id='sidebar_left.sidebar_channel_menu.unmuteChannel'
defaultMessage='Unmute Channel' defaultMessage='Unmute Channel'
/> />
); );
if (props.channel.type === Constants.DM_CHANNEL || props.channel.type === Constants.GM_CHANNEL) { if (channel.type === Constants.DM_CHANNEL || channel.type === Constants.GM_CHANNEL) {
muteChannelText = ( muteChannelText = (
<FormattedMessage <FormattedMessage
id='sidebar_left.sidebar_channel_menu.unmuteConversation' id='sidebar_left.sidebar_channel_menu.unmuteConversation'
@ -137,12 +155,12 @@ const SidebarChannelMenu = (props: Props) => {
} }
function handleUnmuteChannel() { function handleUnmuteChannel() {
props.unmuteChannel(props.currentUserId, props.channel.id); unmuteChannel(currentUserId, channel.id);
} }
muteUnmuteChannelMenuItem = ( muteUnmuteChannelMenuItem = (
<Menu.Item <Menu.Item
id={`unmute-${props.channel.id}`} id={`unmute-${channel.id}`}
onClick={handleUnmuteChannel} onClick={handleUnmuteChannel}
leadingElement={<BellOffOutlineIcon size={18}/>} leadingElement={<BellOffOutlineIcon size={18}/>}
labels={muteChannelText} labels={muteChannelText}
@ -155,7 +173,7 @@ const SidebarChannelMenu = (props: Props) => {
defaultMessage='Mute Channel' defaultMessage='Mute Channel'
/> />
); );
if (props.channel.type === Constants.DM_CHANNEL || props.channel.type === Constants.GM_CHANNEL) { if (channel.type === Constants.DM_CHANNEL || channel.type === Constants.GM_CHANNEL) {
muteChannelText = ( muteChannelText = (
<FormattedMessage <FormattedMessage
id='sidebar_left.sidebar_channel_menu.muteConversation' id='sidebar_left.sidebar_channel_menu.muteConversation'
@ -165,12 +183,12 @@ const SidebarChannelMenu = (props: Props) => {
} }
function handleMuteChannel() { function handleMuteChannel() {
props.muteChannel(props.currentUserId, props.channel.id); muteChannel(currentUserId, channel.id);
} }
muteUnmuteChannelMenuItem = ( muteUnmuteChannelMenuItem = (
<Menu.Item <Menu.Item
id={`mute-${props.channel.id}`} id={`mute-${channel.id}`}
onClick={handleMuteChannel} onClick={handleMuteChannel}
leadingElement={<BellOutlineIcon size={18}/>} leadingElement={<BellOutlineIcon size={18}/>}
labels={muteChannelText} labels={muteChannelText}
@ -179,14 +197,14 @@ const SidebarChannelMenu = (props: Props) => {
} }
let copyLinkMenuItem: JSX.Element | null = null; let copyLinkMenuItem: JSX.Element | null = null;
if (props.channel.type === Constants.OPEN_CHANNEL || props.channel.type === Constants.PRIVATE_CHANNEL) { if (channel.type === Constants.OPEN_CHANNEL || channel.type === Constants.PRIVATE_CHANNEL) {
function handleCopyLink() { function handleCopyLink() {
copyToClipboard(props.channelLink); copyToClipboard(channelLink);
} }
copyLinkMenuItem = ( copyLinkMenuItem = (
<Menu.Item <Menu.Item
id={`copyLink-${props.channel.id}`} id={`copyLink-${channel.id}`}
onClick={handleCopyLink} onClick={handleCopyLink}
leadingElement={<LinkVariantIcon size={18}/>} leadingElement={<LinkVariantIcon size={18}/>}
labels={( labels={(
@ -200,19 +218,19 @@ const SidebarChannelMenu = (props: Props) => {
} }
let addMembersMenuItem: JSX.Element | null = null; let addMembersMenuItem: JSX.Element | null = null;
if ((props.channel.type === Constants.PRIVATE_CHANNEL && props.managePrivateChannelMembers) || (props.channel.type === Constants.OPEN_CHANNEL && props.managePublicChannelMembers)) { if ((channel.type === Constants.PRIVATE_CHANNEL && managePrivateChannelMembers) || (channel.type === Constants.OPEN_CHANNEL && managePublicChannelMembers)) {
function handleAddMembers() { function handleAddMembers() {
props.openModal({ openModal({
modalId: ModalIdentifiers.CHANNEL_INVITE, modalId: ModalIdentifiers.CHANNEL_INVITE,
dialogType: ChannelInviteModal, dialogType: ChannelInviteModal,
dialogProps: {channel: props.channel}, dialogProps: {channel},
}); });
trackEvent('ui', 'ui_sidebar_channel_menu_addMembers'); trackEvent('ui', 'ui_sidebar_channel_menu_addMembers');
} }
addMembersMenuItem = ( addMembersMenuItem = (
<Menu.Item <Menu.Item
id={`addMembers-${props.channel.id}`} id={`addMembers-${channel.id}`}
onClick={handleAddMembers} onClick={handleAddMembers}
aria-haspopup='true' aria-haspopup='true'
leadingElement={<AccountPlusOutlineIcon size={18}/>} leadingElement={<AccountPlusOutlineIcon size={18}/>}
@ -227,14 +245,14 @@ const SidebarChannelMenu = (props: Props) => {
} }
let leaveChannelMenuItem: JSX.Element | null = null; let leaveChannelMenuItem: JSX.Element | null = null;
if (props.channel.name !== Constants.DEFAULT_CHANNEL) { if (channel.name !== Constants.DEFAULT_CHANNEL) {
let leaveChannelText = ( let leaveChannelText = (
<FormattedMessage <FormattedMessage
id='sidebar_left.sidebar_channel_menu.leaveChannel' id='sidebar_left.sidebar_channel_menu.leaveChannel'
defaultMessage='Leave Channel' defaultMessage='Leave Channel'
/> />
); );
if (props.channel.type === Constants.DM_CHANNEL || props.channel.type === Constants.GM_CHANNEL) { if (channel.type === Constants.DM_CHANNEL || channel.type === Constants.GM_CHANNEL) {
leaveChannelText = ( leaveChannelText = (
<FormattedMessage <FormattedMessage
id='sidebar_left.sidebar_channel_menu.leaveConversation' id='sidebar_left.sidebar_channel_menu.leaveConversation'
@ -244,13 +262,13 @@ const SidebarChannelMenu = (props: Props) => {
} }
function handleLeaveChannel() { function handleLeaveChannel() {
if (isLeaving.current || !props.channelLeaveHandler) { if (isLeaving.current || !channelLeaveHandler) {
return; return;
} }
isLeaving.current = true; isLeaving.current = true;
props.channelLeaveHandler(() => { channelLeaveHandler(() => {
isLeaving.current = false; isLeaving.current = false;
}); });
trackEvent('ui', 'ui_sidebar_channel_menu_leave'); trackEvent('ui', 'ui_sidebar_channel_menu_leave');
@ -258,7 +276,7 @@ const SidebarChannelMenu = (props: Props) => {
leaveChannelMenuItem = ( leaveChannelMenuItem = (
<Menu.Item <Menu.Item
id={`leave-${props.channel.id}`} id={`leave-${channel.id}`}
onClick={handleLeaveChannel} onClick={handleLeaveChannel}
leadingElement={<ExitToAppIcon size={18}/>} leadingElement={<ExitToAppIcon size={18}/>}
labels={leaveChannelText} labels={leaveChannelText}
@ -270,30 +288,30 @@ const SidebarChannelMenu = (props: Props) => {
return ( return (
<Menu.Container <Menu.Container
menuButton={{ menuButton={{
id: `SidebarChannelMenu-Button-${props.channel.id}`, id: `SidebarChannelMenu-Button-${channel.id}`,
class: 'SidebarMenu_menuButton', class: 'SidebarMenu_menuButton',
'aria-label': formatMessage({ 'aria-label': formatMessage({
id: 'sidebar_left.sidebar_channel_menu.editChannel.ariaLabel', id: 'sidebar_left.sidebar_channel_menu.editChannel.ariaLabel',
defaultMessage: 'Channel options for {channelName}', defaultMessage: 'Channel options for {channelName}',
}, {channelName: props.channel.name}), }, {channelName: channel.name}),
children: <DotsVerticalIcon size={16}/>, children: <DotsVerticalIcon size={16}/>,
}} }}
menuButtonTooltip={{ menuButtonTooltip={{
id: `SidebarChannelMenu-ButtonTooltip-${props.channel.id}`, id: `SidebarChannelMenu-ButtonTooltip-${channel.id}`,
class: 'hidden-xs', class: 'hidden-xs',
text: formatMessage({id: 'sidebar_left.sidebar_channel_menu.editChannel', defaultMessage: 'Channel options'}), text: formatMessage({id: 'sidebar_left.sidebar_channel_menu.editChannel', defaultMessage: 'Channel options'}),
}} }}
menu={{ menu={{
id: `SidebarChannelMenu-MenuList-${props.channel.id}`, id: `SidebarChannelMenu-MenuList-${channel.id}`,
'aria-label': formatMessage({id: 'sidebar_left.sidebar_channel_menu.dropdownAriaLabel', defaultMessage: 'Edit channel menu'}), 'aria-label': formatMessage({id: 'sidebar_left.sidebar_channel_menu.dropdownAriaLabel', defaultMessage: 'Edit channel menu'}),
onToggle: props.onMenuToggle, onToggle: onMenuToggle,
}} }}
> >
{markAsReadUnreadMenuItem} {markAsReadUnreadMenuItem}
{favoriteUnfavoriteMenuItem} {favoriteUnfavoriteMenuItem}
{muteUnmuteChannelMenuItem} {muteUnmuteChannelMenuItem}
<Menu.Separator/> <Menu.Separator/>
<ChannelMoveToSubmenu channel={props.channel}/> <ChannelMoveToSubmenu channel={channel}/>
{(copyLinkMenuItem || addMembersMenuItem) && <Menu.Separator/>} {(copyLinkMenuItem || addMembersMenuItem) && <Menu.Separator/>}
{copyLinkMenuItem} {copyLinkMenuItem}
{addMembersMenuItem} {addMembersMenuItem}

View File

@ -35,7 +35,7 @@ import SidebarCategory from '../sidebar_category';
import UnreadChannelIndicator from '../unread_channel_indicator'; import UnreadChannelIndicator from '../unread_channel_indicator';
import UnreadChannels from '../unread_channels'; import UnreadChannels from '../unread_channels';
export function renderView(props: any) { export function renderView(props: React.HTMLProps<HTMLDivElement>) {
return ( return (
<div <div
{...props} {...props}
@ -44,7 +44,7 @@ export function renderView(props: any) {
); );
} }
export function renderThumbHorizontal(props: any) { export function renderThumbHorizontal(props: React.HTMLProps<HTMLDivElement>) {
return ( return (
<div <div
{...props} {...props}
@ -53,7 +53,7 @@ export function renderThumbHorizontal(props: any) {
); );
} }
export function renderTrackVertical(props: any) { export function renderTrackVertical(props: React.HTMLProps<HTMLDivElement>) {
return ( return (
<div <div
{...props} {...props}
@ -62,7 +62,7 @@ export function renderTrackVertical(props: any) {
); );
} }
export function renderThumbVertical(props: any) { export function renderThumbVertical(props: React.HTMLProps<HTMLDivElement>) {
return ( return (
<div <div
{...props} {...props}

View File

@ -61,10 +61,9 @@ const GlobalThreadsLink = () => {
const crtTutorialTrigger = useSelector((state: GlobalState) => getInt(state, Preferences.CRT_TUTORIAL_TRIGGERED, currentUserId, Constants.CrtTutorialTriggerSteps.START)); const crtTutorialTrigger = useSelector((state: GlobalState) => getInt(state, Preferences.CRT_TUTORIAL_TRIGGERED, currentUserId, Constants.CrtTutorialTriggerSteps.START));
const threads = useSelector(getThreadsInCurrentTeam); const threads = useSelector(getThreadsInCurrentTeam);
const showTutorialTip = crtTutorialTrigger === CrtTutorialTriggerSteps.STARTED && tipStep === CrtTutorialSteps.WELCOME_POPOVER && threads.length >= 1; const showTutorialTip = crtTutorialTrigger === CrtTutorialTriggerSteps.STARTED && tipStep === CrtTutorialSteps.WELCOME_POPOVER && threads.length >= 1;
const threadsCount = useSelector(getThreadCountsInCurrentTeam);
const rhsOpen = useSelector(getIsRhsOpen); const rhsOpen = useSelector(getIsRhsOpen);
const rhsState = useSelector(getRhsState); const rhsState = useSelector(getRhsState);
const showTutorialTrigger = isFeatureEnabled && crtTutorialTrigger === Constants.CrtTutorialTriggerSteps.START && !appHaveOpenModal && Boolean(threadsCount) && threadsCount.total >= 1; const showTutorialTrigger = isFeatureEnabled && crtTutorialTrigger === Constants.CrtTutorialTriggerSteps.START && !appHaveOpenModal && Boolean(counts) && counts.total >= 1;
const openThreads = useCallback((e) => { const openThreads = useCallback((e) => {
e.stopPropagation(); e.stopPropagation();
@ -79,7 +78,7 @@ const GlobalThreadsLink = () => {
if (rhsOpen && rhsState === RHSStates.EDIT_HISTORY) { if (rhsOpen && rhsState === RHSStates.EDIT_HISTORY) {
dispatch(closeRightHandSide()); dispatch(closeRightHandSide());
} }
}, [showTutorialTrigger, threadsCount, threads, rhsOpen, rhsState]); }, [showTutorialTrigger, counts, threads, rhsOpen, rhsState]);
useEffect(() => { useEffect(() => {
// load counts if necessary // load counts if necessary

View File

@ -1386,35 +1386,6 @@ describe('makeGetChannelsByCategory', () => {
expect(result).toBe(previousResult); expect(result).toBe(previousResult);
}); });
test('should return a new object when user profiles change', () => {
// This behaviour isn't ideal, but it's better than the previous version which returns a new object
// whenever anything user-related changes
const getChannelsByCategory = Selectors.makeGetChannelsByCategory();
const state = mergeObjects(baseState, {
entities: {
users: {
profiles: {
newUser: {id: 'newUser'},
},
},
},
});
const previousResult = getChannelsByCategory(baseState, 'team1');
const result = getChannelsByCategory(state, 'team1');
expect(result).not.toBe(previousResult);
expect(result).toEqual(previousResult);
// Categories not containing DMs/GMs and sorted alphabetically should still remain the same
expect(result.favoritesCategory).not.toBe(previousResult.favoritesCategory);
expect(result.favoritesCategory).toEqual(previousResult.favoritesCategory);
expect(result.channelsCategory).toBe(previousResult.channelsCategory);
expect(result.directMessagesCategory).toEqual(previousResult.directMessagesCategory);
expect(result.directMessagesCategory).toEqual(previousResult.directMessagesCategory);
});
test('should return the same object when other user state changes', () => { test('should return the same object when other user state changes', () => {
const getChannelsByCategory = Selectors.makeGetChannelsByCategory(); const getChannelsByCategory = Selectors.makeGetChannelsByCategory();

View File

@ -447,13 +447,18 @@ export function makeGetChannelsByCategory() {
const channelsByCategory: RelationOneToOne<ChannelCategory, Channel[]> = {}; const channelsByCategory: RelationOneToOne<ChannelCategory, Channel[]> = {};
// TODO: This avoids some rendering, but there is a bigger issue underneath
// Every time myPreferences or myChannels change (which can happen for many
// unrelated reasons) the whole list of channels gets reordered and re-filtered.
let allEquals = categoryIds === lastCategoryIds;
for (const category of categories) { for (const category of categories) {
const channels = getChannels[category.id](state, category.channel_ids); const channels = getChannels[category.id](state, category.channel_ids);
channelsByCategory[category.id] = filterAndSortChannels[category.id](state, channels, category); channelsByCategory[category.id] = filterAndSortChannels[category.id](state, channels, category);
allEquals = allEquals && shallowEquals(channelsByCategory[category.id], lastChannelsByCategory[category.id]);
} }
// Do a shallow equality check of channelsByCategory to avoid returning a new object containing the same data // Do a shallow equality check of channelsByCategory to avoid returning a new object containing the same data
if (shallowEquals(channelsByCategory, lastChannelsByCategory)) { if (allEquals) {
return lastChannelsByCategory; return lastChannelsByCategory;
} }

View File

@ -1259,7 +1259,10 @@ export function getChannelModerations(state: GlobalState, channelId: string): Ch
} }
const EMPTY_OBJECT = {}; const EMPTY_OBJECT = {};
export function getChannelMemberCountsByGroup(state: GlobalState, channelId: string): ChannelMemberCountsByGroup { export function getChannelMemberCountsByGroup(state: GlobalState, channelId?: string): ChannelMemberCountsByGroup {
if (!channelId) {
return EMPTY_OBJECT;
}
return state.entities.channels.channelMemberCountsByGroup[channelId] || EMPTY_OBJECT; return state.entities.channels.channelMemberCountsByGroup[channelId] || EMPTY_OBJECT;
} }

View File

@ -83,6 +83,56 @@ export function getGroupChannels(state: GlobalState, id: string) {
return getGroupSyncables(state, id).channels; return getGroupSyncables(state, id).channels;
} }
export const getAllCustomGroups: (state: GlobalState) => Group[] = createSelector(
'getAllCustomGroups',
getAllGroups,
(groups) => {
return Object.entries(groups).filter((entry) => (entry[1].allow_reference && entry[1].delete_at === 0 && entry[1].source === GroupSource.Custom)).map((entry) => entry[1]);
},
);
export const getGroupsAssociatedToTeamForReference: (state: GlobalState, teamID: string) => Group[] = createSelector(
'getGroupsAssociatedToTeamForReference',
getAllGroups,
(state: GlobalState, teamID: string) => getTeamGroupIDSet(state, teamID),
(allGroups, teamGroupIDSet) => {
return Object.entries(allGroups).filter(([groupID]) => teamGroupIDSet.has(groupID)).filter((entry) => (entry[1].allow_reference && entry[1].delete_at === 0)).map((entry) => entry[1]);
},
);
export const getAssociatedGroupsForReference: (state: GlobalState, teamId: string, channelId?: string) => Group[] = createSelector(
'getAssociatedGroupsForReference',
(state, teamId) => Boolean(getTeam(state, teamId)?.group_constrained),
(state, _, channelId) => Boolean(getChannel(state, channelId)?.group_constrained),
getGroupsAssociatedToTeamForReference,
(state, _, channelId) => (channelId ? getGroupsAssociatedToChannelForReference(state, channelId) : undefined),
getAllCustomGroups,
(state) => getAllAssociatedGroupsForReference(state, false),
(
teamConstrained,
channelConstrained,
groupsFromTeam,
groupsFromChannel,
customGroups,
allGroups,
) => {
if (teamConstrained && channelConstrained) {
const groupSet = new Set<Group>(groupsFromChannel);
return [...(groupsFromChannel || []), ...(groupsFromTeam.filter((item) => !groupSet.has(item))), ...customGroups];
}
if (teamConstrained) {
return [...customGroups, ...groupsFromTeam];
}
if (channelConstrained) {
return [...customGroups, ...(groupsFromChannel || [])];
}
return allGroups;
},
);
export const getAssociatedGroupsByName: (state: GlobalState, teamID: string, channelId: string) => Record<string, Group> = createSelector( export const getAssociatedGroupsByName: (state: GlobalState, teamID: string, channelId: string) => Record<string, Group> = createSelector(
'getAssociatedGroupsByName', 'getAssociatedGroupsByName',
getAssociatedGroupsForReference, getAssociatedGroupsForReference,
@ -100,7 +150,7 @@ export const getAssociatedGroupsByName: (state: GlobalState, teamID: string, cha
}, },
); );
export const getAssociatedGroupsForReferenceByMention: (state: GlobalState, teamID: string, channelId: string) => Map<string, Group> = createSelector( export const getAssociatedGroupsForReferenceByMention: (state: GlobalState, teamID: string, channelId?: string) => Map<string, Group> = createSelector(
'getAssociatedGroupsForReferenceByMention', 'getAssociatedGroupsForReferenceByMention',
getAssociatedGroupsForReference, getAssociatedGroupsForReference,
(groups) => { (groups) => {
@ -117,30 +167,6 @@ export function searchAssociatedGroupsForReferenceLocal(state: GlobalState, term
return filteredGroups; return filteredGroups;
} }
export function getAssociatedGroupsForReference(state: GlobalState, teamId: string, channelId: string): Group[] {
const team = getTeam(state, teamId);
const channel = getChannel(state, channelId);
let groupsForReference = [];
if (team && team.group_constrained && channel && channel.group_constrained) {
const groupsFromChannel = getGroupsAssociatedToChannelForReference(state, channelId);
const groupsFromTeam = getGroupsAssociatedToTeamForReference(state, teamId);
const customGroups = getAllCustomGroups(state);
groupsForReference = groupsFromChannel.concat(groupsFromTeam.filter((item) => groupsFromChannel.indexOf(item) < 0), customGroups);
} else if (team && team.group_constrained) {
const customGroups = getAllCustomGroups(state);
const groupsFromTeam = getGroupsAssociatedToTeamForReference(state, teamId);
groupsForReference = [...customGroups, ...groupsFromTeam];
} else if (channel && channel.group_constrained) {
const customGroups = getAllCustomGroups(state);
const groupsFromChannel = getGroupsAssociatedToChannelForReference(state, channelId);
groupsForReference = [...customGroups, ...groupsFromChannel];
} else {
groupsForReference = getAllAssociatedGroupsForReference(state, false);
}
return groupsForReference;
}
const teamGroupIDs = (state: GlobalState, teamID: string) => state.entities.teams.groupsAssociatedToTeam[teamID]?.ids || []; const teamGroupIDs = (state: GlobalState, teamID: string) => state.entities.teams.groupsAssociatedToTeam[teamID]?.ids || [];
const channelGroupIDs = (state: GlobalState, channelID: string) => state.entities.channels.groupsAssociatedToChannel[channelID]?.ids || []; const channelGroupIDs = (state: GlobalState, channelID: string) => state.entities.channels.groupsAssociatedToChannel[channelID]?.ids || [];
@ -209,15 +235,6 @@ export const getGroupsAssociatedToChannel: (state: GlobalState, channelID: strin
}, },
); );
export const getGroupsAssociatedToTeamForReference: (state: GlobalState, teamID: string) => Group[] = createSelector(
'getGroupsAssociatedToTeamForReference',
getAllGroups,
(state: GlobalState, teamID: string) => getTeamGroupIDSet(state, teamID),
(allGroups, teamGroupIDSet) => {
return Object.entries(allGroups).filter(([groupID]) => teamGroupIDSet.has(groupID)).filter((entry) => (entry[1].allow_reference && entry[1].delete_at === 0)).map((entry) => entry[1]);
},
);
export const getGroupsAssociatedToChannelForReference: (state: GlobalState, channelID: string) => Group[] = createSelector( export const getGroupsAssociatedToChannelForReference: (state: GlobalState, channelID: string) => Group[] = createSelector(
'getGroupsAssociatedToChannelForReference', 'getGroupsAssociatedToChannelForReference',
getAllGroups, getAllGroups,
@ -264,14 +281,6 @@ export const getAllGroupsForReferenceByName: (state: GlobalState) => Record<stri
}, },
); );
export const getAllCustomGroups: (state: GlobalState) => Group[] = createSelector(
'getAllCustomGroups',
getAllGroups,
(groups) => {
return Object.entries(groups).filter((entry) => (entry[1].allow_reference && entry[1].delete_at === 0 && entry[1].source === GroupSource.Custom)).map((entry) => entry[1]);
},
);
export const makeGetMyAllowReferencedGroups = () => { export const makeGetMyAllowReferencedGroups = () => {
return createSelector( return createSelector(
'makeGetMyAllowReferencedGroups', 'makeGetMyAllowReferencedGroups',

View File

@ -142,7 +142,11 @@ export function makeGetChannelDraft() {
const defaultDraft = Object.freeze({message: '', fileInfos: [], uploadsInProgress: [], createAt: 0, updateAt: 0, channelId: '', rootId: ''}); const defaultDraft = Object.freeze({message: '', fileInfos: [], uploadsInProgress: [], createAt: 0, updateAt: 0, channelId: '', rootId: ''});
const getDraft = makeGetGlobalItemWithDefault(defaultDraft); const getDraft = makeGetGlobalItemWithDefault(defaultDraft);
return (state: GlobalState, channelId: string): PostDraft => { return (state: GlobalState, channelId?: string): PostDraft => {
if (!channelId) {
return defaultDraft;
}
const draft = getDraft(state, StoragePrefixes.DRAFT + channelId); const draft = getDraft(state, StoragePrefixes.DRAFT + channelId);
if ( if (
typeof draft.message !== 'undefined' && typeof draft.message !== 'undefined' &&