From 5d7aa17a27b8d1e5fbb7be4618a184a630e72db6 Mon Sep 17 00:00:00 2001 From: M-ZubairAhmed Date: Mon, 26 Aug 2024 12:41:59 +0000 Subject: [PATCH] [MM-58726] Add a loading screen to initial load of the web app (#27740) --- webapp/channels/package.json | 1 + .../initial_loading_screen/index.ts | 8 + .../initial_loading_screen.css | 359 ++++++++++++++++++ .../initial_loading_screen_class.ts | 122 ++++++ .../initial_loading_screen_template.html | 221 +++++++++++ .../no_script/no_script_template.html | 17 + .../src/components/root/root.test.tsx | 48 ++- webapp/channels/src/components/root/root.tsx | 14 +- .../team_controller/team_controller.tsx | 2 + webapp/channels/src/root.html | 65 +--- webapp/channels/webpack.config.js | 3 +- webapp/package-lock.json | 53 ++- 12 files changed, 849 insertions(+), 64 deletions(-) create mode 100644 webapp/channels/src/components/initial_loading_screen/index.ts create mode 100644 webapp/channels/src/components/initial_loading_screen/initial_loading_screen.css create mode 100644 webapp/channels/src/components/initial_loading_screen/initial_loading_screen_class.ts create mode 100644 webapp/channels/src/components/initial_loading_screen/initial_loading_screen_template.html create mode 100644 webapp/channels/src/components/no_script/no_script_template.html diff --git a/webapp/channels/package.json b/webapp/channels/package.json index f3c10c4c37..17d187a7ce 100644 --- a/webapp/channels/package.json +++ b/webapp/channels/package.json @@ -149,6 +149,7 @@ "enzyme-to-json": "3.6.2", "eslint-plugin-no-only-tests": "3.1.0", "external-remotes-plugin": "1.0.0", + "html-loader": "5.1.0", "html-webpack-plugin": "5.5.0", "identity-obj-proxy": "3.0.0", "image-webpack-loader": "8.1.0", diff --git a/webapp/channels/src/components/initial_loading_screen/index.ts b/webapp/channels/src/components/initial_loading_screen/index.ts new file mode 100644 index 0000000000..45c91b4c2f --- /dev/null +++ b/webapp/channels/src/components/initial_loading_screen/index.ts @@ -0,0 +1,8 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {InitialLoadingScreenClass} from './initial_loading_screen_class'; + +const InitialLoadingScreen = new InitialLoadingScreenClass(); + +export default InitialLoadingScreen; diff --git a/webapp/channels/src/components/initial_loading_screen/initial_loading_screen.css b/webapp/channels/src/components/initial_loading_screen/initial_loading_screen.css new file mode 100644 index 0000000000..2d3ae5af1a --- /dev/null +++ b/webapp/channels/src/components/initial_loading_screen/initial_loading_screen.css @@ -0,0 +1,359 @@ +body { + margin: 0; +} + +.LoadingScreen.disabled { + opacity: 0; +} + +.LoadingScreen { + --background-color: #f4f4f6; + --background-color-highlight: #ffffff; + --stipple-color: #1e325c; + --stipple-opacity: 0.08; + position: absolute; + z-index: 100; + top: 0px; + bottom: 0px; + display: flex; + overflow: hidden; + width: 100vw; + height: 100vh; + align-items: center; + justify-content: center; + opacity: 1; + transition: opacity 150ms 0ms ease-out, visibility 150ms 0ms step-start; + vertical-align: middle; + visibility: visible; +} + +.LoadingScreen--darkMode { + --background-color-highlight: #28427b; + --background-color: #1e325c; + --stipple-color: #14213e; + --stipple-opacity: 0.8; +} + +.LoadingScreen__background, +.LoadingScreen__background > svg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; +} +.LoadingScreen__background { + background: var(--background-color); +} + +.LoadingScreen .Pill__stipple { + fill: var(--stipple-color); + fill-opacity: var(--stipple-opacity); +} + +.LoadingScreen .Pill__gradient { + stop-color: var(--background-color); +} + +.LoadingScreen .Pill__gradientHighlight { + stop-color: var(--background-color-highlight); +} + +.LoadingScreen--loaded { + opacity: 0; + transition: opacity 150ms 0ms ease-in, visibility 150ms 0ms step-end; + visibility: hidden; +} + +.LoadingAnimation { + --fade-duration: 150ms; + --colour: #1E325C; + --animation-initial-delay: 750ms; + --animation-start-duration: 750ms; + --animation-end-duration: 600ms; + --animation-spinner-speed: 500ms; + --animation-spinner-mask-stroke-length: 169.6; + --ease-in-cubic: cubic-bezier(0.32, 0, 0.67, 0); + --ease-in: var(--ease-in-cubic); + --ease-in-out-cubic: cubic-bezier(0.65, 0, 0.35, 1); + --ease-in-out: var(--ease-in-out-cubic); + --ease-in-out-compass-shrink: cubic-bezier(0.1, 0.25, 0.3, 1); + + opacity: 1; + transform: scale3d(1, 1, 1); +} + +.LoadingAnimation--darkMode { + --colour: white; +} + +.LoadingAnimation g, +.LoadingAnimation rect, +.LoadingAnimation path, +.LoadingAnimation circle { + transform-origin: center center; +} +.LoadingAnimation svg { + color: var(--colour); +} +.LoadingAnimation .LoadingAnimation__spinner-gradient-color { + stop-color: var(--colour); +} +.LoadingAnimation .LoadingAnimation__spinner-container { + opacity: 0; + transform: scale3D(2.08, 2.08, 1) rotate3d(0, 0, 1, -10deg); +} +.LoadingAnimation .LoadingAnimation__spinner-mask-container { + transform: rotate3d(0, 0, 1, -86deg); +} +.LoadingAnimation .LoadingAnimation__spinner-mask { + stroke-dasharray: var(--animation-spinner-mask-stroke-length); + stroke-dashoffset: var(--animation-spinner-mask-stroke-length); + transform: scale3d(1.03, 1.03, 1); +} +.LoadingAnimation .LoadingAnimation__compass { + opacity: 1; + transform: scale3d(1, 1, 1); +} +.LoadingAnimation .LoadingAnimation__compass-needle-container { + transform: scale3d(1, 1, 1); +} +.LoadingAnimation .LoadingAnimation__compass-needle, +.LoadingAnimation .LoadingAnimation__compass-needle-front-mask, +.LoadingAnimation .LoadingAnimation__compass-needle-behind-mask { + transform: rotate3d(0, 0, 1, 0deg); + transform-origin: 54px 46px; +} +.LoadingAnimation .LoadingAnimation__compass-base-mask-container { + transform: rotate3d(0, 0, 1, -86deg); +} +.LoadingAnimation .LoadingAnimation__compass-base-mask { + stroke-dasharray: var(--animation-spinner-mask-stroke-length); + stroke-dashoffset: var(--animation-spinner-mask-stroke-length); +} + +.LoadingAnimation--loading { + --fade-in-duration: 150ms; + --fade-in-delay: 0ms; + + transform: scale3d(1, 1, 1); +} + +.LoadingAnimation.LoadingAnimation--loading .LoadingAnimation__spinner-container { + --shrink-duration: calc(var(--animation-end-duration) * 0.5); + --shrink-delay: calc( ( var(--animation-start-duration) + var(--animation-end-duration) ) * 0.91 + var(--animation-initial-delay)); + --fade-in-duration: calc(var(--animation-end-duration) * 0.25); + --fade-in-delay: calc( var(--animation-start-duration) + var(--animation-end-duration) * 0.24 + var(--animation-initial-delay)); + + animation: LoadingAnimation__spinner-shrink var(--shrink-duration) var(--shrink-delay) var(--ease-in-out-compass-shrink) forwards, LoadingAnimation__fade-in var(--fade-in-duration) var(--fade-in-delay) var(--ease-in) forwards; +} +.LoadingAnimation.LoadingAnimation--loading .LoadingAnimation__spinner-mask { + --reveal-duration: var(--animation-end-duration); + --reveal-delay: calc(var(--animation-start-duration) + var(--animation-initial-delay)); + + animation: LoadingAnimation__spinner-reveal var(--reveal-duration) var(--reveal-delay) var(--ease-in) forwards; +} +.LoadingAnimation.LoadingAnimation--loading .LoadingAnimation__compass { + --shrink-duration: calc(var(--animation-end-duration) * 0.5); + --shrink-delay: calc( ( var(--animation-start-duration) + var(--animation-end-duration) ) * 0.91 + var(--animation-initial-delay)); + + animation: LoadingAnimation__compass-shrink var(--shrink-duration) var(--shrink-delay) var(--ease-in-out-compass-shrink) forwards; +} +.LoadingAnimation.LoadingAnimation--loading .LoadingAnimation__compass-needle-container { + --shrink-duration: calc(var(--animation-end-duration) * 0.25); + --shrink-delay: calc( var(--animation-start-duration) + var(--animation-end-duration) - var(--animation-end-duration) * 0.25 + var(--animation-initial-delay)); + + animation: LoadingAnimation__needle-shrink var(--shrink-duration) var(--shrink-delay) var(--ease-in) forwards; +} +.LoadingAnimation.LoadingAnimation--loading .LoadingAnimation__compass-needle { + --spin-left-duration: var(--animation-start-duration); + --spin-left-delay: var(--animation-initial-delay); + --spin-right-duration: var(--animation-end-duration); + --spin-right-delay: calc(var(--animation-start-duration) + var(--animation-initial-delay)); + --fade-out-duration: calc(var(--animation-end-duration) * 0.25); + --fade-out-delay: calc( var(--animation-start-duration) + var(--animation-end-duration) - var(--animation-end-duration) * 0.25 + var(--animation-initial-delay)); + + animation: + LoadingAnimation__needle-spin-left var(--spin-left-duration) var(--spin-left-delay) var(--ease-in-out) forwards, + LoadingAnimation__needle-spin-right var(--spin-right-duration) var(--spin-right-delay) var(--ease-in) forwards, + LoadingAnimation__fade-out var(--fade-out-duration) var(--fade-out-delay) var(--ease-in) forwards; +} +.LoadingAnimation.LoadingAnimation--loading .LoadingAnimation__compass-needle-behind-mask { + --spin-left-duration: var(--animation-start-duration); + --spin-left-delay: var(--animation-initial-delay); + --spin-right-duration: calc(var(--animation-end-duration) * 0.3666); + --spin-right-delay: calc(var(--animation-start-duration) + var(--animation-initial-delay)); + + animation: LoadingAnimation__needle-mask-spin-left var(--spin-left-duration) var(--spin-left-delay) var(--ease-in-out) forwards, LoadingAnimation__needle-mask-spin-right var(--spin-right-duration) var(--spin-right-delay) var(--ease-in) forwards; +} +.LoadingAnimation.LoadingAnimation--loading .LoadingAnimation__compass-needle-front-mask { + --spin-left-duration: var(--animation-start-duration); + --spin-left-delay: var(--animation-initial-delay); + --spin-right-duration: var(--animation-end-duration); + --spin-right-delay: calc(var(--animation-start-duration) + var(--animation-initial-delay)); + + animation: LoadingAnimation__needle-spin-left var(--spin-left-duration) var(--spin-left-delay) var(--ease-in-out) forwards, LoadingAnimation__needle-spin-right var(--spin-right-duration) var(--spin-right-delay) var(--ease-in) forwards; +} +.LoadingAnimation.LoadingAnimation--loading .LoadingAnimation__compass-base-mask { + --conceal-duration: var(--animation-end-duration); + --conceal-delay: calc(var(--animation-start-duration) + var(--animation-initial-delay)); + + animation: LoadingAnimation__compass-base-conceal var(--conceal-duration) var(--conceal-delay) var(--ease-in) forwards; +} +.LoadingAnimation.LoadingAnimation--spinning .LoadingAnimation__spinner { + --spin-duration: var(--animation-spinner-speed); + --spin-delay: calc( ( var(--animation-start-duration) + var(--animation-end-duration) ) * 0.95 + var(--animation-initial-delay)); + + animation: LoadingAnimation__spinner-spin var(--spin-duration) var(--spin-delay) linear infinite; +} + +.LoadingAnimation--loaded { + --duration: 150ms; + --delay: 0ms; + + animation: LoadingAnimation__fade-out var(--duration) var(--delay) var(--ease-in) forwards, LoadingAnimation__shrink var(--duration) var(--delay) var(--ease-in) forwards; +} + +.LoadingAnimation--loaded .LoadingAnimation__spinner-container { + opacity: 1; + transform: scale3D(1, 1, 1) rotate3d(0, 0, 1, -10deg); +} +.LoadingAnimation--loaded .LoadingAnimation__spinner-mask { + stroke-dashoffset: 0; +} +.LoadingAnimation--loaded .LoadingAnimation__compass { + transform: scale3D(0.4166666667, 0.4166666667, 1); +} +.LoadingAnimation--loaded .LoadingAnimation__compass-needle-container { + transform: scale3d(0.35, 0.35, 1); +} +.LoadingAnimation--loaded .LoadingAnimation__compass-needle { + opacity: 0; + transform: rotate3d(0, 0, 1, 405deg); +} +.LoadingAnimation--loaded .LoadingAnimation__compass-needle-behind-mask { + transform: rotate3d(0, 0, 1, 0deg); +} +.LoadingAnimation--loaded .LoadingAnimation__compass-needle-front-mask { + transform: rotate3d(0, 0, 1, 405deg); +} +.LoadingAnimation--loaded .LoadingAnimation__compass-base-mask { + stroke-dashoffset: 0; +} + +@keyframes LoadingAnimation__fade-in { + 0% { + opacity: 0; + } + 100% { + opacity: 1; + } +} + +@keyframes LoadingAnimation__fade-out { + 0% { + opacity: 1; + } + 100% { + opacity: 0; + } +} + +@keyframes LoadingAnimation__shrink { + 0% { + transform: scale3d(1, 1, 1); + } + 100% { + transform: scale3d(0.35, 0.35, 1); + } +} + +@keyframes LoadingAnimation__spinner-shrink { + 0% { + transform: scale3D(2.08, 2.08, 1) rotate3d(0, 0, 1, -10deg); + } + 100% { + transform: scale3D(1, 1, 1) rotate3d(0, 0, 1, -10deg); + } +} + +@keyframes LoadingAnimation__spinner-spin { + from { + transform: rotate3d(0, 0, 1, 0deg); + } + to { + transform: rotate3d(0, 0, 1, 359deg); + } +} + +@keyframes LoadingAnimation__spinner-reveal { + 0%, 5% { + stroke-dashoffset: var(--animation-spinner-mask-stroke-length); + } + 95%, 100% { + stroke-dashoffset: 0; + } +} + +@keyframes LoadingAnimation__needle-spin-left { + 0% { + transform: rotate3d(0, 0, 1, 0deg); + } + 100% { + transform: rotate3d(0, 0, 1, -20deg); + } +} + +@keyframes LoadingAnimation__needle-spin-right { + 0% { + transform: rotate3d(0, 0, 1, -20deg); + } + 100% { + transform: rotate3d(0, 0, 1, 405deg); + } +} + +@keyframes LoadingAnimation__needle-mask-spin-left { + 0% { + transform: rotate3d(0, 0, 1, 0deg); + } + 100% { + transform: rotate3d(0, 0, 1, -20deg); + } +} + +@keyframes LoadingAnimation__needle-mask-spin-right { + 0% { + transform: rotate3d(0, 0, 1, -20deg); + } + 100% { + transform: rotate3d(0, 0, 1, 0deg); + } +} + +@keyframes LoadingAnimation__needle-shrink { + 0% { + transform: scale3d(1, 1, 1); + } + 100% { + transform: scale3d(0.35, 0.35, 1); + } +} + +@keyframes LoadingAnimation__compass-shrink { + 0% { + transform: scale3D(1, 1, 1); + } + 100% { + transform: scale3D(0.4166666667, 0.4166666667, 1); + } +} + +@keyframes LoadingAnimation__compass-base-conceal { + 0%, 5% { + stroke-dashoffset: var(--animation-spinner-mask-stroke-length); + } + 95%, 100% { + stroke-dashoffset: 0; + } +} diff --git a/webapp/channels/src/components/initial_loading_screen/initial_loading_screen_class.ts b/webapp/channels/src/components/initial_loading_screen/initial_loading_screen_class.ts new file mode 100644 index 0000000000..64a4ac45c2 --- /dev/null +++ b/webapp/channels/src/components/initial_loading_screen/initial_loading_screen_class.ts @@ -0,0 +1,122 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {isDesktopApp} from 'utils/user_agent'; + +const ANIMATION_CLASS_FOR_MATTERMOST_LOGO_HIDE = 'LoadingAnimation__compass-shrink'; +const ANIMATION_CLASS_FOR_COMPLETE_LOADER_HIDE = 'LoadingAnimation__shrink'; + +const LOADING_CLASS_FOR_SCREEN = 'LoadingScreen LoadingScreen--darkMode'; +const LOADING_COMPLETE_CLASS_FOR_SCREEN = 'LoadingScreen LoadingScreen--darkMode LoadingScreen--loaded'; + +const STATIC_CLASS_FOR_ANIMATION = 'LoadingAnimation LoadingAnimation--darkMode'; +const LOADING_CLASS_FOR_ANIMATION = STATIC_CLASS_FOR_ANIMATION + ' LoadingAnimation--spinning LoadingAnimation--loading'; +const LOADING_COMPLETE_CLASS_FOR_ANIMATION = STATIC_CLASS_FOR_ANIMATION + ' LoadingAnimation--spinning LoadingAnimation--loaded'; + +const DESTROY_DELAY_AFTER_ANIMATION_END = 1000; + +export class InitialLoadingScreenClass { + private isLoading: boolean | null = true; + + private loadingScreenElement: HTMLElement | null; + private loadingAnimationElement: HTMLElement | null; + + private initialLoadingScreenCSS: HTMLLinkElement | null; + + constructor() { + this.loadingScreenElement = document.getElementById('initialPageLoadingScreen'); + this.loadingAnimationElement = document.getElementById('initialPageLoadingAnimation'); + + this.initialLoadingScreenCSS = document.getElementById('initialLoadingScreenCSS') as HTMLLinkElement | null; + + this.handleAnimationEndEvent = this.handleAnimationEndEvent.bind(this); + } + + private handleAnimationEndEvent(event: AnimationEvent) { + if (!this.loadingAnimationElement) { + return; + } + + if (event.animationName === ANIMATION_CLASS_FOR_MATTERMOST_LOGO_HIDE || event.animationName === ANIMATION_CLASS_FOR_COMPLETE_LOADER_HIDE) { + if (!this.isLoading) { + this.loadingAnimationElement.className = STATIC_CLASS_FOR_ANIMATION; + + // Automatically destroy the loading screen after the animation has finished. + // Should be changed if we want to loading animation to start again. + setTimeout(() => { + this.destroy(); + }, DESTROY_DELAY_AFTER_ANIMATION_END); + } + } + } + + private addAnimationEndListener() { + if (this.loadingAnimationElement) { + this.loadingAnimationElement.addEventListener('animationend', this.handleAnimationEndEvent); + } + } + + private removeAnimationEndListener() { + if (this.loadingAnimationElement) { + this.loadingAnimationElement.removeEventListener('animationend', this.handleAnimationEndEvent); + } + } + + private destroy() { + this.removeAnimationEndListener(); + + if (this.loadingScreenElement) { + this.loadingScreenElement.remove(); + this.loadingScreenElement = null; + } + + if (this.initialLoadingScreenCSS) { + this.initialLoadingScreenCSS.remove(); + this.initialLoadingScreenCSS = null; + } + + if (this.loadingAnimationElement) { + this.loadingAnimationElement = null; + } + + this.isLoading = null; + } + + /** + * The loading animations are always started as soon as the loading indicator is shown in the screen for the first time. + * But we still want to have this start method incase we need to start the loading animations manually any time. + * If we do want to do that then we should remove the set timeout destroy call doing above. + */ + public start() { + if (isDesktopApp()) { + // Let Mattermost desktop handle the loading screen + this.destroy(); + return; + } + + this.addAnimationEndListener(); + + if (!this.loadingScreenElement || !this.loadingAnimationElement) { + // eslint-disable-next-line no-console + console.error('InitialLoadingScreen: No loading screen or animation element found'); + return; + } + + this.isLoading = true; + + this.loadingScreenElement.className = LOADING_CLASS_FOR_SCREEN; + this.loadingAnimationElement.className = LOADING_CLASS_FOR_ANIMATION; + } + + public stop() { + if (!this.loadingScreenElement || !this.loadingAnimationElement) { + return; + } + + this.isLoading = false; + + this.loadingScreenElement.className = LOADING_COMPLETE_CLASS_FOR_SCREEN; + this.loadingAnimationElement.className = LOADING_COMPLETE_CLASS_FOR_ANIMATION; + } +} + diff --git a/webapp/channels/src/components/initial_loading_screen/initial_loading_screen_template.html b/webapp/channels/src/components/initial_loading_screen/initial_loading_screen_template.html new file mode 100644 index 0000000000..be72103f75 --- /dev/null +++ b/webapp/channels/src/components/initial_loading_screen/initial_loading_screen_template.html @@ -0,0 +1,221 @@ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
diff --git a/webapp/channels/src/components/no_script/no_script_template.html b/webapp/channels/src/components/no_script/no_script_template.html new file mode 100644 index 0000000000..588d07d866 --- /dev/null +++ b/webapp/channels/src/components/no_script/no_script_template.html @@ -0,0 +1,17 @@ + diff --git a/webapp/channels/src/components/root/root.test.tsx b/webapp/channels/src/components/root/root.test.tsx index a470edeace..9024726413 100644 --- a/webapp/channels/src/components/root/root.test.tsx +++ b/webapp/channels/src/components/root/root.test.tsx @@ -18,7 +18,7 @@ import {StoragePrefixes} from 'utils/constants'; import {handleLoginLogoutSignal, redirectToOnboardingOrDefaultTeam} from './actions'; import type {Props} from './root'; -import Root from './root'; +import Root, {doesRouteBelongToTeamControllerRoutes} from './root'; jest.mock('mattermost-redux/client/rudder', () => ({ rudderAnalytics: { @@ -358,3 +358,49 @@ describe('components/Root', () => { }); }); }); + +describe('doesRouteBelongToTeamControllerRoutes', () => { + test('should return true for some of team_controller routes', () => { + expect(doesRouteBelongToTeamControllerRoutes('/team_name_example_1/messages/abc')).toBe(true); + expect(doesRouteBelongToTeamControllerRoutes('/team_name_example_1/messages')).toBe(true); + expect(doesRouteBelongToTeamControllerRoutes('/team_name_example_1/channels/cde')).toBe(true); + expect(doesRouteBelongToTeamControllerRoutes('/team_name_example_1/channels')).toBe(true); + expect(doesRouteBelongToTeamControllerRoutes('/team_name_example_1/threads/efg')).toBe(true); + expect(doesRouteBelongToTeamControllerRoutes('/team_name_example_1/threads')).toBe(true); + expect(doesRouteBelongToTeamControllerRoutes('/team_name_example_1/drafts')).toBe(true); + expect(doesRouteBelongToTeamControllerRoutes('/team_name_example_1/integrations/klm')).toBe(true); + expect(doesRouteBelongToTeamControllerRoutes('/team_name_example_1/emoji/nop')).toBe(true); + expect(doesRouteBelongToTeamControllerRoutes('/team_name_example_1/integrations')).toBe(true); + expect(doesRouteBelongToTeamControllerRoutes('/team_name_example_1/emoji')).toBe(true); + }); + + test('should return false for other of team_controller routes', () => { + expect(doesRouteBelongToTeamControllerRoutes('/team_name_example_2')).toBe(false); + expect(doesRouteBelongToTeamControllerRoutes('/team_name_example_2/pl/permalink123')).toBe(false); + expect(doesRouteBelongToTeamControllerRoutes('/team_name_example_2/needs_team_component_plugin')).toBe(false); + }); + + test('should return false for other routes of root', () => { + expect(doesRouteBelongToTeamControllerRoutes('/plug/custom_route_component')).toBe(false); + expect(doesRouteBelongToTeamControllerRoutes('/main_component_product_1')).toBe(false); + expect(doesRouteBelongToTeamControllerRoutes('/product_1/public')).toBe(false); + expect(doesRouteBelongToTeamControllerRoutes('/_redirect/pl/message_1')).toBe(false); + expect(doesRouteBelongToTeamControllerRoutes('/preparing-workspace')).toBe(false); + expect(doesRouteBelongToTeamControllerRoutes('/mfa')).toBe(false); + expect(doesRouteBelongToTeamControllerRoutes('/create_team')).toBe(false); + expect(doesRouteBelongToTeamControllerRoutes('/oauth/authorize')).toBe(false); + expect(doesRouteBelongToTeamControllerRoutes('/select_team')).toBe(false); + expect(doesRouteBelongToTeamControllerRoutes('/admin_console')).toBe(false); + expect(doesRouteBelongToTeamControllerRoutes('/landing')).toBe(false); + expect(doesRouteBelongToTeamControllerRoutes('/terms_of_service')).toBe(false); + expect(doesRouteBelongToTeamControllerRoutes('/claim')).toBe(false); + expect(doesRouteBelongToTeamControllerRoutes('/do_verify_email')).toBe(false); + expect(doesRouteBelongToTeamControllerRoutes('/should_verify_email')).toBe(false); + expect(doesRouteBelongToTeamControllerRoutes('/signup_user_complete')).toBe(false); + expect(doesRouteBelongToTeamControllerRoutes('/reset_password_complete')).toBe(false); + expect(doesRouteBelongToTeamControllerRoutes('/reset_password')).toBe(false); + expect(doesRouteBelongToTeamControllerRoutes('/access_problem')).toBe(false); + expect(doesRouteBelongToTeamControllerRoutes('/login')).toBe(false); + expect(doesRouteBelongToTeamControllerRoutes('/error')).toBe(false); + }); +}); diff --git a/webapp/channels/src/components/root/root.tsx b/webapp/channels/src/components/root/root.tsx index 6d265278b9..22d4f709b5 100644 --- a/webapp/channels/src/components/root/root.tsx +++ b/webapp/channels/src/components/root/root.tsx @@ -28,6 +28,7 @@ import OpenPluginInstallPost from 'components/custom_open_plugin_install_post_re import GlobalHeader from 'components/global_header/global_header'; import {HFRoute} from 'components/header_footer_route/header_footer_route'; import {HFTRoute, LoggedInHFTRoute} from 'components/header_footer_template_route'; +import InitialLoadingScreen from 'components/initial_loading_screen'; import MobileViewWatcher from 'components/mobile_view_watcher'; import ModalController from 'components/modal_controller'; import LaunchingWorkspace, {LAUNCHING_WORKSPACE_FULLSCREEN_Z_INDEX} from 'components/preparing_workspace/launching_workspace'; @@ -309,7 +310,7 @@ export default class Root extends React.PureComponent { BrowserStore.setLandingPageSeen(true); }; - componentDidUpdate(prevProps: Props) { + componentDidUpdate(prevProps: Props, prevState: State) { if (!deepEqual(prevProps.theme, this.props.theme)) { Utils.applyTheme(this.props.theme); } @@ -333,6 +334,12 @@ export default class Root extends React.PureComponent { if (!prevProps.isConfigLoaded && this.props.isConfigLoaded) { this.setRudderConfig(); } + + if (prevState.shouldMountAppRoutes === false && this.state.shouldMountAppRoutes === true) { + if (!doesRouteBelongToTeamControllerRoutes(this.props.location.pathname)) { + InitialLoadingScreen.stop(); + } + } } captureUTMParams() { @@ -600,3 +607,8 @@ export default class Root extends React.PureComponent { ); } } + +export function doesRouteBelongToTeamControllerRoutes(pathname: RouteComponentProps['location']['pathname']): boolean { + const TEAM_CONTROLLER_PATH_PATTERN = /^\/([a-z0-9\-_]+)\/(channels|messages|threads|drafts|integrations|emoji)(\/.*)?$/; + return TEAM_CONTROLLER_PATH_PATTERN.test(pathname); +} diff --git a/webapp/channels/src/components/team_controller/team_controller.tsx b/webapp/channels/src/components/team_controller/team_controller.tsx index c81e616471..89fa10b0ea 100644 --- a/webapp/channels/src/components/team_controller/team_controller.tsx +++ b/webapp/channels/src/components/team_controller/team_controller.tsx @@ -16,6 +16,7 @@ import LocalStorageStore from 'stores/local_storage_store'; import {makeAsyncComponent} from 'components/async_load'; import ChannelController from 'components/channel_layout/channel_controller'; import useTelemetryIdentitySync from 'components/common/hooks/useTelemetryIdentifySync'; +import InitialLoadingScreen from 'components/initial_loading_screen'; import Constants from 'utils/constants'; import {cmdOrCtrlPressed, isKeyPressed} from 'utils/keyboard'; @@ -53,6 +54,7 @@ function TeamController(props: Props) { useTelemetryIdentitySync(); useEffect(() => { + InitialLoadingScreen.stop(); async function fetchInitialChannels() { await props.fetchAllMyTeamsChannelsAndChannelMembersREST(); diff --git a/webapp/channels/src/root.html b/webapp/channels/src/root.html index 58c5ba8047..14d73dd718 100644 --- a/webapp/channels/src/root.html +++ b/webapp/channels/src/root.html @@ -19,68 +19,13 @@ - - - + + -
-
-

Cannot connect to Mattermost

-
-

We're having trouble connecting to Mattermost. If refreshing this page (Ctrl+R or Command+R) does not work, please verify that your computer is connected to the internet.

-
-
-
-
-
-
-
-
-
-
+ <%= require('html-loader!./components/initial_loading_screen/initial_loading_screen_template.html').default %> +
- + <%= require('html-loader!./components/no_script/no_script_template.html').default %> diff --git a/webapp/channels/webpack.config.js b/webapp/channels/webpack.config.js index 3b281a938a..ec216e61ae 100644 --- a/webapp/channels/webpack.config.js +++ b/webapp/channels/webpack.config.js @@ -133,7 +133,7 @@ var config = { 'mattermost-redux': 'packages/mattermost-redux/src', '@mui/styled-engine': '@mui/styled-engine-sc', - // This alias restricts single version of styled components acros all packages + // This alias restricts single version of styled components across all packages 'styled-components': path.resolve(__dirname, '..', 'node_modules', 'styled-components'), }, extensions: ['.ts', '.tsx', '.js', '.jsx'], @@ -195,6 +195,7 @@ var config = { {from: 'src/images/payment_processing.png', to: 'images'}, {from: 'src/images/purchase_alert.png', to: 'images'}, {from: '../node_modules/pdfjs-dist/cmaps', to: 'cmaps'}, + {from: 'src/components/initial_loading_screen/initial_loading_screen.css', to: 'css'}, ], }), diff --git a/webapp/package-lock.json b/webapp/package-lock.json index b49d4ca546..31fb095466 100644 --- a/webapp/package-lock.json +++ b/webapp/package-lock.json @@ -198,6 +198,7 @@ "enzyme-to-json": "3.6.2", "eslint-plugin-no-only-tests": "3.1.0", "external-remotes-plugin": "1.0.0", + "html-loader": "5.1.0", "html-webpack-plugin": "5.5.0", "identity-obj-proxy": "3.0.0", "image-webpack-loader": "8.1.0", @@ -12443,6 +12444,56 @@ "dev": true, "license": "MIT" }, + "node_modules/html-loader": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/html-loader/-/html-loader-5.1.0.tgz", + "integrity": "sha512-Jb3xwDbsm0W3qlXrCZwcYqYGnYz55hb6aoKQTlzyZPXsPpi6tHXzAfqalecglMQgNvtEfxrCQPaKT90Irt5XDA==", + "dev": true, + "dependencies": { + "html-minifier-terser": "^7.2.0", + "parse5": "^7.1.2" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, + "node_modules/html-loader/node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "dev": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/html-loader/node_modules/html-minifier-terser": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-7.2.0.tgz", + "integrity": "sha512-tXgn3QfqPIpGl9o+K5tpcj3/MN4SfLtsx2GWwBC3SSd0tXQGyF3gsSqad8loJgKZGM3ZxbYDd5yhiBIdWpmvLA==", + "dev": true, + "dependencies": { + "camel-case": "^4.1.2", + "clean-css": "~5.3.2", + "commander": "^10.0.0", + "entities": "^4.4.0", + "param-case": "^3.0.4", + "relateurl": "^0.2.7", + "terser": "^5.15.1" + }, + "bin": { + "html-minifier-terser": "cli.js" + }, + "engines": { + "node": "^14.13.1 || >=16.0.0" + } + }, "node_modules/html-minifier-terser": { "version": "6.1.0", "dev": true, @@ -23719,7 +23770,7 @@ "typescript": "^5.0.0" }, "peerDependencies": { - "@mattermost/types": "9.3.0", + "@mattermost/types": "^9.3.0 || ^10.0.0", "typescript": "^4.3.0 || ^5.0.0" }, "peerDependenciesMeta": {