MM-66940 Fix layout shift in ChannelView during loading (#37652)

* MM-69940 Fix height of AdvancedTextEditor placeholder while loading

The previous height included the margin which it shouldn't have. The
100% height on the post list also seemed to cause its height to change
as well. That 98px assumes that this is the empty AdvancedTextEditor
which is likely the most common case.

* MM-69940 Change ChannelView to not split code as much

I don't think this code splitting was necessary given we would always
immediately load those components right away whenever the ChannelView
is shown. It's not like the contents of a modal or the routing where
we only need the async component conditionally.

I did this so that the height of the PostView would change less while
the app is loading to prevent layout shift and make accurate scrolling
easier.

* Move getPopoutChannelTitle to fix circular imports and update snapshots
This commit is contained in:
Harrison Healey
2026-07-27 13:57:30 -04:00
committed by GitHub
parent a0475a696c
commit ef002933bf
11 changed files with 95 additions and 76 deletions
@@ -2,7 +2,7 @@
.AdvancedTextEditor__skeleton {
display: flex;
height: 122px;
height: 98px;
align-items: center;
justify-content: center;
padding-left: 10px;
@@ -84,7 +84,7 @@ exports[`components/ChannelHeader should match snapshot with last active display
id="channel-info-btn"
>
<i
class="Icon-esDcos cHAVtG icon-information-outline"
class="Icon-clPswv hCFmLO icon-information-outline"
/>
</button>
</div>
@@ -165,7 +165,7 @@ exports[`components/ChannelHeader should match snapshot with no last active disp
id="channel-info-btn"
>
<i
class="Icon-esDcos cHAVtG icon-information-outline"
class="Icon-clPswv hCFmLO icon-information-outline"
/>
</button>
</div>
@@ -263,7 +263,7 @@ exports[`components/ChannelHeader should render active channel files 1`] = `
id="channel-info-btn"
>
<i
class="Icon-esDcos cHAVtG icon-information-outline"
class="Icon-clPswv hCFmLO icon-information-outline"
/>
</button>
</div>
@@ -361,7 +361,7 @@ exports[`components/ChannelHeader should render active flagged posts 1`] = `
id="channel-info-btn"
>
<i
class="Icon-esDcos cHAVtG icon-information-outline"
class="Icon-clPswv hCFmLO icon-information-outline"
/>
</button>
</div>
@@ -459,7 +459,7 @@ exports[`components/ChannelHeader should render active mentions posts 1`] = `
id="channel-info-btn"
>
<i
class="Icon-esDcos cHAVtG icon-information-outline"
class="Icon-clPswv hCFmLO icon-information-outline"
/>
</button>
</div>
@@ -557,7 +557,7 @@ exports[`components/ChannelHeader should render active pinned posts 1`] = `
id="channel-info-btn"
>
<i
class="Icon-esDcos cHAVtG icon-information-outline"
class="Icon-clPswv hCFmLO icon-information-outline"
/>
</button>
</div>
@@ -655,7 +655,7 @@ exports[`components/ChannelHeader should render archived view 1`] = `
id="channel-info-btn"
>
<i
class="Icon-esDcos cHAVtG icon-information-outline"
class="Icon-clPswv hCFmLO icon-information-outline"
/>
</button>
</div>
@@ -763,7 +763,7 @@ exports[`components/ChannelHeader should render correct menu when muted 1`] = `
id="channel-info-btn"
>
<i
class="Icon-esDcos cHAVtG icon-information-outline"
class="Icon-clPswv hCFmLO icon-information-outline"
/>
</button>
</div>
@@ -861,7 +861,7 @@ exports[`components/ChannelHeader should render not active channel files 1`] = `
id="channel-info-btn"
>
<i
class="Icon-esDcos cHAVtG icon-information-outline"
class="Icon-clPswv hCFmLO icon-information-outline"
/>
</button>
</div>
@@ -953,7 +953,7 @@ exports[`components/ChannelHeader should render properly when custom status is e
id="channel-info-btn"
>
<i
class="Icon-esDcos cHAVtG icon-information-outline"
class="Icon-clPswv hCFmLO icon-information-outline"
/>
</button>
</div>
@@ -1048,7 +1048,7 @@ exports[`components/ChannelHeader should render properly when custom status is s
id="channel-info-btn"
>
<i
class="Icon-esDcos cHAVtG icon-information-outline"
class="Icon-clPswv hCFmLO icon-information-outline"
/>
</button>
</div>
@@ -1146,7 +1146,7 @@ exports[`components/ChannelHeader should render properly when empty 1`] = `
id="channel-info-btn"
>
<i
class="Icon-esDcos cHAVtG icon-information-outline"
class="Icon-clPswv hCFmLO icon-information-outline"
/>
</button>
</div>
@@ -1244,7 +1244,7 @@ exports[`components/ChannelHeader should render properly when populated 1`] = `
id="channel-info-btn"
>
<i
class="Icon-esDcos cHAVtG icon-information-outline"
class="Icon-clPswv hCFmLO icon-information-outline"
/>
</button>
</div>
@@ -1342,7 +1342,7 @@ exports[`components/ChannelHeader should render properly when populated with cha
id="channel-info-btn"
>
<i
class="Icon-esDcos cHAVtG icon-information-outline"
class="Icon-clPswv hCFmLO icon-information-outline"
/>
</button>
</div>
@@ -1543,7 +1543,7 @@ exports[`components/ChannelHeader should render shared view 1`] = `
id="channel-info-btn"
>
<i
class="Icon-esDcos cHAVtG icon-information-outline"
class="Icon-clPswv hCFmLO icon-information-outline"
/>
</button>
</div>
@@ -1657,7 +1657,7 @@ exports[`components/ChannelHeader should render the pinned icon with the pinned
id="channel-info-btn"
>
<i
class="Icon-esDcos cHAVtG icon-information-outline"
class="Icon-clPswv hCFmLO icon-information-outline"
/>
</button>
</div>
@@ -9,7 +9,6 @@ import type {WrappedComponentProps} from 'react-intl';
import {WithTooltip} from '@mattermost/shared/components/tooltip';
import {getPopoutChannelTitle} from 'components/channel_popout/channel_popout';
import CustomStatusEmoji from 'components/custom_status/custom_status_emoji';
import CustomStatusText from 'components/custom_status/custom_status_text';
import PopoutButton from 'components/popout_button';
@@ -25,7 +24,7 @@ import {
NotificationLevels,
RHSStates,
} from 'utils/constants';
import {canPopout, isChannelPopoutWindow, popoutChannel} from 'utils/popouts/popout_windows';
import {canPopout, getPopoutChannelTitle, isChannelPopoutWindow, popoutChannel} from 'utils/popouts/popout_windows';
import {isEmptyObject} from 'utils/utils';
import ChannelHeaderText from './channel_header_text';
@@ -13,11 +13,8 @@ import {TestHelper} from 'utils/test_helper';
import MenuItemOpenInNewWindow from './open_in_new_window';
jest.mock('components/channel_popout/channel_popout', () => ({
getPopoutChannelTitle: jest.fn(() => ({id: 'test.title', defaultMessage: 'Test Title'})),
}));
jest.mock('utils/popouts/popout_windows', () => ({
getPopoutChannelTitle: jest.fn(() => ({id: 'test.title', defaultMessage: 'Test Title'})),
isChannelPopoutWindow: jest.fn(() => false),
popoutChannel: jest.fn(),
canPopout: jest.fn(() => true),
@@ -11,13 +11,12 @@ import {getCurrentTeam} from 'mattermost-redux/selectors/entities/teams';
import {getCurrentUserId, getUser} from 'mattermost-redux/selectors/entities/users';
import {getUserIdFromChannelName} from 'mattermost-redux/utils/channel_utils';
import {getPopoutChannelTitle} from 'components/channel_popout/channel_popout';
import * as Menu from 'components/menu';
import PopoutMenuItem, {type PopoutMenuItemProps} from 'components/popout_menu_item';
import {getChannelRoutePathAndIdentifier} from 'utils/channel_utils';
import {Constants} from 'utils/constants';
import {popoutChannel} from 'utils/popouts/popout_windows';
import {getPopoutChannelTitle, popoutChannel} from 'utils/popouts/popout_windows';
import type {GlobalState} from 'types/store';
@@ -12,10 +12,9 @@ import {selectTeam} from 'mattermost-redux/actions/teams';
import {useTeamByName} from 'components/common/hooks/use_team';
import {renderWithContext, screen, waitFor} from 'tests/react_testing_utils';
import {getPopoutChannelTitle} from 'utils/popouts/popout_windows';
import {TestHelper} from 'utils/test_helper';
import {getPopoutChannelTitle} from './channel_popout';
import ChannelPopout from './index';
const MOCK_ACTION = {type: 'MOCK'};
@@ -7,7 +7,6 @@ import {useDispatch, useSelector} from 'react-redux';
import {useParams} from 'react-router-dom';
import {isDesktopApp} from '@mattermost/shared/utils/user_agent';
import type {ChannelType} from '@mattermost/types/channels';
import {fetchMyCategories} from 'mattermost-redux/actions/channel_categories';
import {fetchChannelsAndMembers, getChannelStats} from 'mattermost-redux/actions/channels';
@@ -23,19 +22,12 @@ import LoadingScreen from 'components/loading_screen';
import SidebarRight from 'components/sidebar_right';
import UnreadsStatusHandler from 'components/unreads_status_handler';
import Constants from 'utils/constants';
import {getPopoutChannelTitle} from 'utils/popouts/popout_windows';
import usePopoutFocus from 'utils/popouts/use_popout_focus';
import usePopoutTitle from 'utils/popouts/use_popout_title';
import './channel_popout.scss';
export function getPopoutChannelTitle(channelType?: ChannelType) {
if (channelType === Constants.DM_CHANNEL || channelType === Constants.GM_CHANNEL) {
return {id: 'channel_popout.title.dm', defaultMessage: '{channelName} - {serverName}'};
}
return {id: 'channel_popout.title', defaultMessage: '{channelName} - {teamName} - {serverName}'};
}
export default function ChannelPopout() {
const dispatch = useDispatch();
const {team: teamName, postid} = useParams<{team: string; path: string; identifier: string; postid?: string}>();
@@ -7,14 +7,25 @@ exports[`components/channel_view Should match snapshot if channel is archived 1`
id="app-content"
>
<div
data-testid="FileUploadOverlay"
/>
<div
data-testid="ChannelHeader"
/>
<div
data-testid="ChannelBanner"
/>
class="file-overlay hidden center-file-overlay"
id="centerChannelFileDropOverlay"
>
<div
class="overlay__indent"
>
<div
class="overlay__circle vertical"
>
<img
alt=""
class="overlay__files"
loading="lazy"
src=""
/>
Drop a file to upload it.
</div>
</div>
</div>
<div
data-channel-id="channelId"
data-testid="deferred-post-view"
@@ -50,14 +61,25 @@ exports[`components/channel_view Should match snapshot if channel is deactivated
id="app-content"
>
<div
data-testid="FileUploadOverlay"
/>
<div
data-testid="ChannelHeader"
/>
<div
data-testid="ChannelBanner"
/>
class="file-overlay hidden center-file-overlay"
id="centerChannelFileDropOverlay"
>
<div
class="overlay__indent"
>
<div
class="overlay__circle vertical"
>
<img
alt=""
class="overlay__files"
loading="lazy"
src=""
/>
Drop a file to upload it.
</div>
</div>
</div>
<div
data-channel-id="channelId"
data-testid="deferred-post-view"
@@ -92,14 +114,25 @@ exports[`components/channel_view Should match snapshot with base props 1`] = `
id="app-content"
>
<div
data-testid="FileUploadOverlay"
/>
<div
data-testid="ChannelHeader"
/>
<div
data-testid="ChannelBanner"
/>
class="file-overlay hidden center-file-overlay"
id="centerChannelFileDropOverlay"
>
<div
class="overlay__indent"
>
<div
class="overlay__circle vertical"
>
<img
alt=""
class="overlay__files"
loading="lazy"
src=""
/>
Drop a file to upload it.
</div>
</div>
</div>
<div
data-channel-id="channelId"
data-testid="deferred-post-view"
@@ -108,11 +141,7 @@ exports[`components/channel_view Should match snapshot with base props 1`] = `
class="post-create__container AdvancedTextEditor__ctr"
data-testid="post-create"
id="post-create"
>
<div
data-testid="AdvancedCreatePost"
/>
</div>
/>
</div>
</div>
`;
@@ -1,15 +1,18 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {lazy} from 'react';
import React from 'react';
import {FormattedMessage} from 'react-intl';
import type {RouteComponentProps} from 'react-router-dom';
import {Button} from '@mattermost/shared/components/button';
import {makeAsyncComponent} from 'components/async_load';
import AdvancedCreatePost from 'components/advanced_create_post';
import ChannelBanner from 'components/channel_banner/channel_banner';
import ChannelBookmarks from 'components/channel_bookmarks';
import ChannelHeader from 'components/channel_header';
import deferComponentRender from 'components/deferComponentRender';
import {DropOverlayIdCenterChannel} from 'components/file_upload_overlay/file_upload_overlay';
import {DropOverlayIdCenterChannel, FileUploadOverlay} from 'components/file_upload_overlay/file_upload_overlay';
import PostView from 'components/post_view';
import WebSocketClient from 'client/web_websocket_client';
@@ -19,12 +22,6 @@ import InputLoading from './input_loading';
import type {PropsFromRedux} from './index';
const ChannelHeader = makeAsyncComponent('ChannelHeader', lazy(() => import('components/channel_header')));
const FileUploadOverlay = makeAsyncComponent('FileUploadOverlay', lazy(() => import('components/file_upload_overlay')));
const ChannelBookmarks = makeAsyncComponent('ChannelBookmarks', lazy(() => import('components/channel_bookmarks')));
const AdvancedCreatePost = makeAsyncComponent('AdvancedCreatePost', lazy(() => import('components/advanced_create_post')));
const ChannelBanner = makeAsyncComponent('ChannelBanner', lazy(() => import('components/channel_banner/channel_banner')));
export type Props = PropsFromRedux & RouteComponentProps<{
postid?: string;
}>;
@@ -134,7 +134,6 @@
#post-list {
position: relative;
height: 100%;
flex: 1 1 auto;
overflow-y: hidden;
@@ -3,10 +3,11 @@
import type {PopoutViewProps} from '@mattermost/desktop-api';
import {isDesktopApp} from '@mattermost/shared/utils/user_agent';
import type {ChannelType} from '@mattermost/types/channels';
import {Client4} from 'mattermost-redux/client';
import {RHSStates} from 'utils/constants';
import Constants, {RHSStates} from 'utils/constants';
import DesktopApp from 'utils/desktop_api';
import {getBasePath} from 'utils/url';
@@ -24,6 +25,13 @@ export function registerRHSPluginPopoutListener(pluginId: string, onPopoutOpened
pluginPopoutListeners.set(pluginId, onPopoutOpened);
}
export function getPopoutChannelTitle(channelType?: ChannelType) {
if (channelType === Constants.DM_CHANNEL || channelType === Constants.GM_CHANNEL) {
return {id: 'channel_popout.title.dm', defaultMessage: '{channelName} - {serverName}'};
}
return {id: 'channel_popout.title', defaultMessage: '{channelName} - {teamName} - {serverName}'};
}
export const FOCUS_REPLY_POST = 'focus-reply-post';
export async function popoutThread(
titleTemplate: string,