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 @@ +
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.
-