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 ? (
-