From 83bae0c2b8e3b479988cd3c9507bb47eab0c3422 Mon Sep 17 00:00:00 2001 From: Dean Whillier Date: Tue, 17 Nov 2020 09:13:03 -0500 Subject: [PATCH] [MM-22810] Update loading screen with new design & animation (#1409) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update loading screen with new design & animation * add prop back in * adjust z-index for tests * tweaks to pass tests * address offline feedback - shrink initial logo size - introduce a slight delay before fading loading spinner out - fix horizontal scrollbar showing on load screen * add missing css variable * no need to remove loading icon * Apply suggestions from code review Co-authored-by: Guillermo VayĆ” * Move LoadingScreen.jsx to file-only component * Rename prop for better clarity * Default prop to none and check when needed * Update import paths * Add ESDocs and remove unecessary conditional * Forgot to remove the eslint override Co-authored-by: Guillermo VayĆ” --- .eslintrc.json | 4 +- src/assets/window-background-dots.svg | 148 +++++++++ src/assets/window-background-dots_dark.svg | 148 +++++++++ src/assets/window-background.svg | 38 +++ src/assets/window-background_dark.svg | 38 +++ .../LoadingAnimation/LoadingAnimation.jsx | 93 ++++++ .../LoadingAnimation/LoadingIcon.jsx | 197 ++++++++++++ .../components/LoadingAnimation/index.js | 4 + src/browser/components/LoadingScreen.jsx | 75 +++++ src/browser/components/MainPage.jsx | 1 + src/browser/components/MattermostView.jsx | 57 ++-- .../css/components/LoadingAnimation.css | 294 ++++++++++++++++++ src/browser/css/components/LoadingScreen.css | 55 ++++ src/browser/css/components/MattermostView.css | 27 +- src/browser/css/components/index.css | 2 + src/browser/hooks/useAnimationEnd.js | 44 +++ src/browser/hooks/useTransitionEnd.js | 57 ++++ src/package.json | 1 + 18 files changed, 1232 insertions(+), 51 deletions(-) create mode 100644 src/assets/window-background-dots.svg create mode 100644 src/assets/window-background-dots_dark.svg create mode 100644 src/assets/window-background.svg create mode 100644 src/assets/window-background_dark.svg create mode 100644 src/browser/components/LoadingAnimation/LoadingAnimation.jsx create mode 100644 src/browser/components/LoadingAnimation/LoadingIcon.jsx create mode 100644 src/browser/components/LoadingAnimation/index.js create mode 100644 src/browser/components/LoadingScreen.jsx create mode 100644 src/browser/css/components/LoadingAnimation.css create mode 100644 src/browser/css/components/LoadingScreen.css create mode 100644 src/browser/hooks/useAnimationEnd.js create mode 100644 src/browser/hooks/useTransitionEnd.js diff --git a/.eslintrc.json b/.eslintrc.json index 9bf7de2d..d75e2422 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -24,7 +24,9 @@ "react/jsx-indent-props": [2, 2], "react/no-find-dom-node": 2, "react/no-set-state": 1, - "react/require-optimization": 0 + "react/require-optimization": 0, + "multiline-ternary": ["warn", "always-multiline"], + "consistent-return": "off" }, "overrides": [ { diff --git a/src/assets/window-background-dots.svg b/src/assets/window-background-dots.svg new file mode 100644 index 00000000..4a17343c --- /dev/null +++ b/src/assets/window-background-dots.svg @@ -0,0 +1,148 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/window-background-dots_dark.svg b/src/assets/window-background-dots_dark.svg new file mode 100644 index 00000000..83377239 --- /dev/null +++ b/src/assets/window-background-dots_dark.svg @@ -0,0 +1,148 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/window-background.svg b/src/assets/window-background.svg new file mode 100644 index 00000000..4bf54f0a --- /dev/null +++ b/src/assets/window-background.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/window-background_dark.svg b/src/assets/window-background_dark.svg new file mode 100644 index 00000000..896f13fd --- /dev/null +++ b/src/assets/window-background_dark.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/browser/components/LoadingAnimation/LoadingAnimation.jsx b/src/browser/components/LoadingAnimation/LoadingAnimation.jsx new file mode 100644 index 00000000..01e89c61 --- /dev/null +++ b/src/browser/components/LoadingAnimation/LoadingAnimation.jsx @@ -0,0 +1,93 @@ +// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; +import classNames from 'classnames'; +import PropTypes from 'prop-types'; + +import useAnimationEnd from '../../hooks/useAnimationEnd.js'; + +import LoadingIcon from './LoadingIcon.jsx'; + +const LOADING_STATE = { + INITIALIZING: 'initializing', // animation graphics are hidden + LOADING: 'loading', // animation graphics fade in and animate + LOADED: 'loaded', // animation graphics fade out + COMPLETE: 'complete', // animation graphics are removed from the DOM +}; + +const ANIMATION_COMPLETION_DELAY = 500; + +/** + * A function component for rendering the animated MM logo loading sequence + * @param {boolean} loading - Prop that indicates whether currently loading or not + * @param {boolean} darkMode - Prop that indicates if dark mode is enabled + * @param {function} onLoadingAnimationComplete - Callback function to update when internal loading animation is complete + */ +function LoadingAnimation({ + loading = false, + darkMode = false, + onLoadAnimationComplete = null} +) { + const loadingIconContainerRef = React.useRef(null); + const [animationState, setAnimationState] = React.useState(LOADING_STATE.INITIALIZING); + const [loadingAnimationComplete, setLoadingAnimationComplete] = React.useState(false); + + React.useEffect(() => { + if (loading) { + setAnimationState(LOADING_STATE.LOADING); + setLoadingAnimationComplete(false); + } + + // in order for the logo animation to fully complete before fading out, the LOADED state is not set until + // both the external loaded prop changes back to false and the internal loading animation is complete + if (!loading && loadingAnimationComplete) { + setAnimationState(LOADING_STATE.LOADED); + } + }, [loading]); + + React.useEffect(() => { + // in order for the logo animation to fully complete before fading out, the LOADED state is not set until + // both the external loaded prop goes back to false and the internal loading animation is complete + if (!loading && loadingAnimationComplete) { + setAnimationState(LOADING_STATE.LOADED); + } + }, [loadingAnimationComplete]); + + // listen for end of the css logo animation sequence + useAnimationEnd(loadingIconContainerRef, () => { + setTimeout(() => { + setLoadingAnimationComplete(true); + }, ANIMATION_COMPLETION_DELAY); + }, 'LoadingAnimation__compass-shrink'); + + // listen for end of final css logo fade/shrink animation sequence + useAnimationEnd(loadingIconContainerRef, () => { + if (onLoadAnimationComplete) { + onLoadAnimationComplete(); + } + setAnimationState(LOADING_STATE.COMPLETE); + }, 'LoadingAnimation__shrink'); + + return ( +
+ +
+ ); +} + +LoadingAnimation.propTypes = { + loading: PropTypes.bool, + darkMode: PropTypes.bool, + onLoadAnimationComplete: PropTypes.func, +}; + +export default LoadingAnimation; diff --git a/src/browser/components/LoadingAnimation/LoadingIcon.jsx b/src/browser/components/LoadingAnimation/LoadingIcon.jsx new file mode 100644 index 00000000..2b279e5b --- /dev/null +++ b/src/browser/components/LoadingAnimation/LoadingIcon.jsx @@ -0,0 +1,197 @@ +// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; + +/** + * A function component for inlining SVG code for animation logo loader + */ +function LoadingAnimation() { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +} + +export default LoadingAnimation; diff --git a/src/browser/components/LoadingAnimation/index.js b/src/browser/components/LoadingAnimation/index.js new file mode 100644 index 00000000..c6f27884 --- /dev/null +++ b/src/browser/components/LoadingAnimation/index.js @@ -0,0 +1,4 @@ +// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +export {default} from './LoadingAnimation.jsx'; diff --git a/src/browser/components/LoadingScreen.jsx b/src/browser/components/LoadingScreen.jsx new file mode 100644 index 00000000..cddd5047 --- /dev/null +++ b/src/browser/components/LoadingScreen.jsx @@ -0,0 +1,75 @@ +// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; +import classNames from 'classnames'; +import PropTypes from 'prop-types'; + +import useTransitionEnd from '../hooks/useTransitionEnd.js'; + +import LoadingAnimation from './LoadingAnimation'; + +/** + * A function component for rendering the desktop app loading screen + * @param {boolean} loading - Prop that indicates whether currently loading or not + * @param {boolean} darkMode - Prop that indicates if dark mode is enabled + */ +function LoadingScreen({loading = false, darkMode = false}) { + const loadingScreenRef = React.useRef(null); + + const [loadingIsComplete, setLoadingIsComplete] = React.useState(true); + const [loadAnimationIsComplete, setLoadAnimationIsComplete] = React.useState(true); + const [fadeOutIsComplete, setFadeOutIsComplete] = React.useState(true); + + React.useEffect(() => { + // reset internal state if loading restarts + if (loading) { + resetState(); + } else { + setLoadingIsComplete(true); + } + }, [loading]); + + function handleLoadAnimationComplete() { + setLoadAnimationIsComplete(true); + } + + useTransitionEnd(loadingScreenRef, React.useCallback(() => { + setFadeOutIsComplete(true); + }), ['opacity']); + + function loadingInProgress() { + return !(loadingIsComplete && loadAnimationIsComplete && fadeOutIsComplete); + } + + function resetState() { + setLoadingIsComplete(false); + setLoadAnimationIsComplete(false); + setFadeOutIsComplete(false); + } + + const loadingScreen = ( +
+ +
+ ); + + return loadingInProgress() ? loadingScreen : null; +} + +LoadingScreen.propTypes = { + loading: PropTypes.bool, + darkMode: PropTypes.bool, +}; + +export default LoadingScreen; diff --git a/src/browser/components/MainPage.jsx b/src/browser/components/MainPage.jsx index 37e32954..8d632383 100644 --- a/src/browser/components/MainPage.jsx +++ b/src/browser/components/MainPage.jsx @@ -794,6 +794,7 @@ export default class MainPage extends React.Component { ref={id} active={isActive} allowExtraBar={this.showExtraBar()} + isDarkMode={this.state.isDarkMode} />); }); diff --git a/src/browser/components/MattermostView.jsx b/src/browser/components/MattermostView.jsx index ea70b4c3..f2205a09 100644 --- a/src/browser/components/MattermostView.jsx +++ b/src/browser/components/MattermostView.jsx @@ -8,6 +8,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import {ipcRenderer, remote, shell} from 'electron'; +import classNames from 'classnames'; import contextMenu from '../js/contextMenu'; import Utils from '../../utils/util'; @@ -16,11 +17,14 @@ import {protocols} from '../../../electron-builder.json'; const scheme = protocols[0].schemes[0]; import ErrorView from './ErrorView.jsx'; +import LoadingScreen from './LoadingScreen.jsx'; const preloadJS = `file://${remote.app.getAppPath()}/browser/webview/mattermost_bundle.js`; const ERR_NOT_IMPLEMENTED = -11; const U2F_EXTENSION_URL = 'chrome-extension://kmendfapggjehodndflmmgagdbamhnfd/u2f-comms.html'; +const ERR_USER_ABORTED = -3; +const AUTO_RELOAD_TIMER = 30000; export default class MattermostView extends React.Component { constructor(props) { @@ -30,7 +34,7 @@ export default class MattermostView extends React.Component { errorInfo: null, isContextMenuAdded: false, reloadTimeoutID: null, - isLoaded: false, + isWebviewLoaded: false, basename: '/', }; @@ -49,7 +53,7 @@ export default class MattermostView extends React.Component { webview.addEventListener('did-fail-load', (e) => { console.log(self.props.name, 'webview did-fail-load', e); - if (e.errorCode === -3) { // An operation was aborted (due to user action). + if (e.errorCode === ERR_USER_ABORTED) { // An operation was aborted (due to user action). return; } if (e.errorCode === ERR_NOT_IMPLEMENTED && e.validatedURL === U2F_EXTENSION_URL) { @@ -60,7 +64,7 @@ export default class MattermostView extends React.Component { self.setState({ errorInfo: e, - isLoaded: true, + isWebviewLoaded: true, }); function reload() { window.removeEventListener('online', reload); @@ -68,7 +72,7 @@ export default class MattermostView extends React.Component { } if (navigator.onLine) { self.setState({ - reloadTimeoutID: setTimeout(reload, 30000), + reloadTimeoutID: setTimeout(reload, AUTO_RELOAD_TIMER), }); } else { window.addEventListener('online', reload); @@ -154,7 +158,7 @@ export default class MattermostView extends React.Component { switch (event.channel) { case 'onGuestInitialized': self.setState({ - isLoaded: true, + isWebviewLoaded: true, basename: event.args[0] || '/', }); break; @@ -221,7 +225,7 @@ export default class MattermostView extends React.Component { this.setState({ errorInfo: null, reloadTimeoutID: null, - isLoaded: false, + isWebviewLoaded: false, }); const webview = this.webviewRef.current; if (webview) { @@ -289,7 +293,7 @@ export default class MattermostView extends React.Component { ); } - handleUserActivityUpdate = (event, status) => { + handleUserActivityUpdate = (_, status) => { // pass user activity update to the webview this.webviewRef.current.send('user-activity-update', status); } @@ -306,45 +310,29 @@ export default class MattermostView extends React.Component { className='errorView' errorInfo={this.state.errorInfo} active={this.props.active} - />) : null; - - // Need to keep webview mounted when failed to load. - const classNames = ['mattermostView']; - if (this.props.withTab) { - classNames.push('mattermostView-with-tab'); - } - if (!this.props.active) { - classNames.push('mattermostView-hidden'); - } - if (this.state.errorInfo) { - classNames.push('mattermostView-error'); - } - if (this.props.allowExtraBar) { - classNames.push('allow-extra-bar'); - } - - const loadingImage = !this.state.errorInfo && this.props.active && !this.state.isLoaded ? ( -
- -
+ /> ) : null; return (
{ errorView } + - { loadingImage }
); } } @@ -362,6 +350,7 @@ MattermostView.propTypes = { onSelectSpellCheckerLocale: PropTypes.func, handleInterTeamLink: PropTypes.func, allowExtraBar: PropTypes.bool, + isDarkMode: PropTypes.bool, }; /* eslint-enable react/no-set-state */ diff --git a/src/browser/css/components/LoadingAnimation.css b/src/browser/css/components/LoadingAnimation.css new file mode 100644 index 00000000..a9d10881 --- /dev/null +++ b/src/browser/css/components/LoadingAnimation.css @@ -0,0 +1,294 @@ +.LoadingAnimation { + --fade-duration: 150ms; + --colour: #166de0; + --animation-initial-delay: 500ms; + --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: 0; + 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-mask { + transform: scale3d(1.03, 1.03, 1); +} +.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); +} +.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-origin: 54px 46px; + transform: rotate3d(0, 0, 1, 0deg); +} +.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; + + animation: + LoadingAnimation__fade-in var(--fade-in-duration) var(--fade-in-delay) var(--ease-in) forwards; + + 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/src/browser/css/components/LoadingScreen.css b/src/browser/css/components/LoadingScreen.css new file mode 100644 index 00000000..ccffd659 --- /dev/null +++ b/src/browser/css/components/LoadingScreen.css @@ -0,0 +1,55 @@ +.LoadingScreen { + display: flex; + justify-content: center; + align-items: center; + position: absolute; + top: 40px; + right: 0px; + bottom: 0px; + left: 0px; + vertical-align: middle; + background: white url(../../../assets/window-background.svg) no-repeat center; + background-size: cover; + opacity: 1; + visibility: visible; + z-index: 10; + overflow:hidden; + + transition: opacity 150ms 0ms ease-out, visibility 150ms 0ms step-start; +} + +.LoadingScreen::before, .LoadingScreen::after { + content: ""; + display: block; + position: absolute; + width: 460px; + height: 460px; + opacity: 0.1; + background-image: url(../../../assets/window-background-dots.svg); + background-repeat: no-repeat; + background-position: center; +} +.LoadingScreen::before { + left: -210px; + bottom: 10px; +} +.LoadingScreen::after { + right: -80px; + top: 50%; + margin-top: -230px; +} + +.LoadingScreen--darkMode { + background-color: #323639; + background-image: url(../../../assets/window-background_dark.svg); +} +.LoadingScreen--darkMode::before, .LoadingScreen--darkMode::after { + background-image: url(../../../assets/window-background-dots_dark.svg); +} + +.LoadingScreen--loaded { + opacity: 0; + visibility: hidden; + + transition: opacity 150ms 0ms ease-in, visibility 150ms 0ms step-end; +} diff --git a/src/browser/css/components/MattermostView.css b/src/browser/css/components/MattermostView.css index 86317ab0..476e45e8 100644 --- a/src/browser/css/components/MattermostView.css +++ b/src/browser/css/components/MattermostView.css @@ -1,12 +1,21 @@ .mattermostView { text-align: center; + z-index: 1; + opacity: 1; + visibility: visible; +} + +.mattermostView-hidden { + z-index: -1; + opacity: 0; + visibility: hidden; } .mattermostView .ErrorView { text-align: left; } -.mattermostView webview, .mattermostView .mattermostView-loadingScreen { +.mattermostView webview { position: absolute; top: 40px; right: 0px; @@ -23,20 +32,6 @@ z-index: -1; } -.mattermostView-loadingScreen { - vertical-align: middle; - background: white; -} - -.mattermostView-loadingImage { - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; - margin: auto; -} - .allow-extra-bar webview { top: 76px; -} \ No newline at end of file +} diff --git a/src/browser/css/components/index.css b/src/browser/css/components/index.css index 58b039ca..03377a92 100644 --- a/src/browser/css/components/index.css +++ b/src/browser/css/components/index.css @@ -10,3 +10,5 @@ @import url("UpdaterPage.css"); @import url("CertificateModal.css"); @import url("ExtraBar.css"); +@import url("LoadingScreen.css"); +@import url("LoadingAnimation.css"); diff --git a/src/browser/hooks/useAnimationEnd.js b/src/browser/hooks/useAnimationEnd.js new file mode 100644 index 00000000..be7a0310 --- /dev/null +++ b/src/browser/hooks/useAnimationEnd.js @@ -0,0 +1,44 @@ +// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; + +/** + * A custom hook to implement an animationend listener on the provided ref + * @param {object} ref - A reference to a DOM element to add the listener to + * @param {function} callback - A callback function that will be run for matching animation events + * @param {string} animationName - The name of the animation to listen for + * @param {boolean} listenForEventBubbling - A parameter that when true, listens for events on the target element and + * bubbled from all descendent elements but when false, only listens for events coming from the target element and + * ignores events bubbling up from descendent elements + */ +function useAnimationEnd( + ref, + callback, + animationName, + listenForEventBubbling = true, +) { + React.useEffect(() => { + if (!ref.current) { + return; + } + + function handleAnimationend(event) { + if (!listenForEventBubbling && event.target !== ref.current) { + return; + } + if (animationName && animationName !== event.animationName) { + return; + } + callback(event); + } + + ref.current.addEventListener('animationend', handleAnimationend); + + return () => { + ref.current.removeEventListener('animationend', handleAnimationend); + }; + }, [ref, callback, animationName, listenForEventBubbling]); +} + +export default useAnimationEnd; diff --git a/src/browser/hooks/useTransitionEnd.js b/src/browser/hooks/useTransitionEnd.js new file mode 100644 index 00000000..e98a566d --- /dev/null +++ b/src/browser/hooks/useTransitionEnd.js @@ -0,0 +1,57 @@ +// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; + +/** + * A custom hook to implement a transitionend listener on the provided ref + * @param {object} ref - A reference to a DOM element to add the listener to + * @param {function} callback - A callback function that will be run for matching animation events + * @param {array} properties - An array of css property strings to listen for + * @param {boolean} listenForEventBubbling - A parameter that when true, listens for events on the target element and + * bubbled from all descendent elements but when false, only listens for events coming from the target element and + * ignores events bubbling up from descendent elements + */ +function useTransitionend( + ref, + callback, + properties, + listenForEventBubbling = true +) { + React.useEffect(() => { + if (!ref.current) { + return; + } + + function handleTransitionEnd(event) { + if (!listenForEventBubbling && event.target !== ref.current) { + return; + } + + if (properties && typeof properties === 'object') { + const property = properties.find( + (propertyName) => propertyName === event.propertyName + ); + if (property) { + callback(event); + } + return; + } + callback(event); + } + + ref.current.addEventListener('transitionend', handleTransitionEnd); + + return () => { + if (!ref.current) { + return; + } + ref.current.removeEventListener( + 'transitionend', + handleTransitionEnd + ); + }; + }, [ref, callback, properties, listenForEventBubbling]); +} + +export default useTransitionend; diff --git a/src/package.json b/src/package.json index 0dd170c1..3854ad6d 100644 --- a/src/package.json +++ b/src/package.json @@ -12,6 +12,7 @@ "@hapi/joi": "^16.1.8", "auto-launch": "^5.0.5", "bootstrap": "^3.3.7", + "classnames": "^2.2.6", "electron-context-menu": "^0.16.0", "electron-devtools-installer": "^2.2.4", "electron-is-dev": "^1.0.1",