From 6fa55085885364a162c768b83068f57f58e68a88 Mon Sep 17 00:00:00 2001
From: Devin Binnie <52460000+devinbinnie@users.noreply.github.com>
Date: Fri, 21 Feb 2025 10:17:49 -0500
Subject: [PATCH] [MM-63224] Add incompatible server screen (#3348)
* [MM-63224] Add incompatible server screen
* Fixed issue where init isn't called on no server case
* Amend check
---
i18n/en.json | 4 +
src/app/serverViewState.ts | 5 ++
src/common/communication.ts | 2 +
src/common/config/buildConfig.ts | 3 +-
src/common/config/index.ts | 3 +
src/common/constants.ts | 1 +
src/main/app/initialize.ts | 1 -
src/main/preload/internalAPI.js | 4 +
.../views/MattermostWebContentsView.test.js | 12 +++
src/main/views/MattermostWebContentsView.ts | 27 ++++--
src/main/views/viewManager.ts | 16 +++-
.../components/ConnectionErrorView.tsx | 90 +++++++++++++++++++
src/renderer/components/ErrorView.tsx | 74 +++++----------
.../components/IncompatibleErrorView.tsx | 80 +++++++++++++++++
src/renderer/components/MainPage.tsx | 29 +++++-
src/types/config.ts | 1 +
src/types/window.ts | 2 +
17 files changed, 286 insertions(+), 68 deletions(-)
create mode 100644 src/renderer/components/ConnectionErrorView.tsx
create mode 100644 src/renderer/components/IncompatibleErrorView.tsx
diff --git a/i18n/en.json b/i18n/en.json
index cbf6377b..8db31408 100644
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -169,9 +169,13 @@
"renderer.components.developerModeIndicator.tooltip": "Developer mode is enabled. You should only have this enabled if a Mattermost developer has instructed you to.",
"renderer.components.errorView.cannotConnectToThisServer": "Couldn't connect to this server",
"renderer.components.errorView.contactAdmin": "If the issue persists, please contact your admin",
+ "renderer.components.errorView.contactAdminUpgrade": "If the issue persists, contact your {appName} Administrator or IT department to upgrade this {appName} Server.",
"renderer.components.errorView.havingTroubleConnecting": "We're having trouble connecting to this {appName} server. We'll keep trying to establish a connection.",
+ "renderer.components.errorView.incompatibleServerVersion": "Incompatible server version",
"renderer.components.errorView.refreshThenVerify": "If refreshing this page (Ctrl+R or Command+R) doesn't help, please check the following:",
+ "renderer.components.errorView.serverVersionIsIncompatible": "The {appName} Server you are accessing is incompatible with this version of the {appName} Desktop App. To connect to this server, please try the following:",
"renderer.components.errorView.troubleshooting.computerIsConnected": "Ensure your computer is connected to the internet.",
+ "renderer.components.errorView.troubleshooting.downgradeApp": "Downgrade your {appName} Desktop App to version v5.10 or earlier.",
"renderer.components.errorView.troubleshooting.urlIsCorrect.appNameIsCorrect": "Verify that the URL {url} is correct.",
"renderer.components.errorView.troubleshooting.webContentsView.canReachFromBrowserWindow": "Try opening {url} in a browser window.",
"renderer.components.input.required": "This field is required",
diff --git a/src/app/serverViewState.ts b/src/app/serverViewState.ts
index 125b43a2..8d562cce 100644
--- a/src/app/serverViewState.ts
+++ b/src/app/serverViewState.ts
@@ -59,6 +59,11 @@ export class ServerViewState {
}
init = () => {
+ // Don't need to init twice
+ if (this.currentServerId) {
+ return;
+ }
+
const orderedServers = ServerManager.getOrderedServers();
if (orderedServers.length) {
if (Config.lastActiveServer && orderedServers[Config.lastActiveServer]) {
diff --git a/src/common/communication.ts b/src/common/communication.ts
index 71109655..9febeabd 100644
--- a/src/common/communication.ts
+++ b/src/common/communication.ts
@@ -30,6 +30,7 @@ export const APP_MENU_WILL_CLOSE = 'app-menu-will-close';
export const LOAD_RETRY = 'load_retry';
export const LOAD_SUCCESS = 'load_success';
export const LOAD_FAILED = 'load_fail';
+export const LOAD_INCOMPATIBLE_SERVER = 'load_incompatible_server';
export const MAXIMIZE_CHANGE = 'maximized_change';
@@ -102,6 +103,7 @@ export const UPDATE_PATHS = 'update-paths';
export const UPDATE_URL_VIEW_WIDTH = 'update-url-view-width';
export const OPEN_SERVER_EXTERNALLY = 'open-server-externally';
+export const OPEN_SERVER_UPGRADE_LINK = 'open-server-upgrade-link';
export const PING_DOMAIN = 'ping-domain';
diff --git a/src/common/config/buildConfig.ts b/src/common/config/buildConfig.ts
index 2272c4f2..17a700bb 100644
--- a/src/common/config/buildConfig.ts
+++ b/src/common/config/buildConfig.ts
@@ -4,7 +4,7 @@
import type {BuildConfig} from 'types/config';
-import {DEFAULT_ACADEMY_LINK, DEFAULT_HELP_LINK} from '../../common/constants';
+import {DEFAULT_ACADEMY_LINK, DEFAULT_HELP_LINK, DEFAULT_UPGRADE_LINK} from '../../common/constants';
// For detailed guides, please refer to https://docs.mattermost.com/deployment/desktop-app-deployment.html
@@ -31,6 +31,7 @@ const buildConfig: BuildConfig = {
*/],
helpLink: DEFAULT_HELP_LINK,
academyLink: DEFAULT_ACADEMY_LINK,
+ upgradeLink: DEFAULT_UPGRADE_LINK,
enableServerManagement: true,
enableAutoUpdater: true,
managedResources: ['trusted'],
diff --git a/src/common/config/index.ts b/src/common/config/index.ts
index 746703a7..f2ad4bf3 100644
--- a/src/common/config/index.ts
+++ b/src/common/config/index.ts
@@ -219,6 +219,9 @@ export class Config extends EventEmitter {
get academyLink() {
return this.combinedData?.academyLink;
}
+ get upgradeLink() {
+ return this.combinedData?.upgradeLink;
+ }
get minimizeToTray() {
return this.combinedData?.minimizeToTray;
}
diff --git a/src/common/constants.ts b/src/common/constants.ts
index 0f0739ac..d96e142e 100644
--- a/src/common/constants.ts
+++ b/src/common/constants.ts
@@ -48,3 +48,4 @@ export const DEFAULT_HELP_LINK = 'https://docs.mattermost.com/guides/collaborate
export const DEFAULT_ACADEMY_LINK = 'https://academy.mattermost.com/';
export const DEFAULT_TE_REPORT_PROBLEM_LINK = 'https://mattermost.com/pl/report-a-bug';
export const DEFAULT_EE_REPORT_PROBLEM_LINK = 'https://support.mattermost.com/hc/en-us/requests/new';
+export const DEFAULT_UPGRADE_LINK = 'https://forum.mattermost.com/t/mattermost-desktop-app-5-11-important-compatibility-notice/22599';
diff --git a/src/main/app/initialize.ts b/src/main/app/initialize.ts
index fe84e04e..b6642b57 100644
--- a/src/main/app/initialize.ts
+++ b/src/main/app/initialize.ts
@@ -308,7 +308,6 @@ async function initializeAfterAppReady() {
});
ServerManager.reloadFromConfig();
- updateServerInfos(ServerManager.getAllServers());
ServerManager.on(SERVERS_URL_MODIFIED, (serverIds?: string[]) => {
if (serverIds && serverIds.length) {
updateServerInfos(serverIds.map((srvId) => ServerManager.getServer(srvId)!));
diff --git a/src/main/preload/internalAPI.js b/src/main/preload/internalAPI.js
index 1d8aefc1..8f951973 100644
--- a/src/main/preload/internalAPI.js
+++ b/src/main/preload/internalAPI.js
@@ -93,6 +93,8 @@ import {
IS_DEVELOPER_MODE_ENABLED,
METRICS_REQUEST,
METRICS_RECEIVE,
+ LOAD_INCOMPATIBLE_SERVER,
+ OPEN_SERVER_UPGRADE_LINK,
} from 'common/communication';
console.log('Preload initialized');
@@ -120,6 +122,7 @@ contextBridge.exposeInMainWorld('desktop', {
doubleClickOnWindow: (windowName) => ipcRenderer.send(DOUBLE_CLICK_ON_WINDOW, windowName),
focusCurrentView: () => ipcRenderer.send(FOCUS_BROWSERVIEW),
openServerExternally: () => ipcRenderer.send(OPEN_SERVER_EXTERNALLY),
+ openServerUpgradeLink: () => ipcRenderer.send(OPEN_SERVER_UPGRADE_LINK),
closeDownloadsDropdown: () => ipcRenderer.send(CLOSE_DOWNLOADS_DROPDOWN),
closeDownloadsDropdownMenu: () => ipcRenderer.send(CLOSE_DOWNLOADS_DROPDOWN_MENU),
openDownloadsDropdown: () => ipcRenderer.send(OPEN_DOWNLOADS_DROPDOWN),
@@ -154,6 +157,7 @@ contextBridge.exposeInMainWorld('desktop', {
onLoadRetry: (listener) => ipcRenderer.on(LOAD_RETRY, (_, viewId, retry, err, loadUrl) => listener(viewId, retry, err, loadUrl)),
onLoadSuccess: (listener) => ipcRenderer.on(LOAD_SUCCESS, (_, viewId) => listener(viewId)),
onLoadFailed: (listener) => ipcRenderer.on(LOAD_FAILED, (_, viewId, err, loadUrl) => listener(viewId, err, loadUrl)),
+ onLoadIncompatibleServer: (listener) => ipcRenderer.on(LOAD_INCOMPATIBLE_SERVER, (_, viewId, loadUrl) => listener(viewId, loadUrl)),
onSetActiveView: (listener) => ipcRenderer.on(SET_ACTIVE_VIEW, (_, serverId, viewId) => listener(serverId, viewId)),
onMaximizeChange: (listener) => ipcRenderer.on(MAXIMIZE_CHANGE, (_, maximize) => listener(maximize)),
onEnterFullScreen: (listener) => ipcRenderer.on('enter-full-screen', () => listener()),
diff --git a/src/main/views/MattermostWebContentsView.test.js b/src/main/views/MattermostWebContentsView.test.js
index b0e78971..faf9e11f 100644
--- a/src/main/views/MattermostWebContentsView.test.js
+++ b/src/main/views/MattermostWebContentsView.test.js
@@ -6,6 +6,7 @@
import AppState from 'common/appState';
import {LOAD_FAILED, UPDATE_TARGET_URL} from 'common/communication';
import {MattermostServer} from 'common/servers/MattermostServer';
+import ServerManager from 'common/servers/serverManager';
import MessagingView from 'common/views/MessagingView';
import {MattermostWebContentsView} from './MattermostWebContentsView';
@@ -68,6 +69,16 @@ jest.mock('main/performanceMonitor', () => ({
registerServerView: jest.fn(),
unregisterView: jest.fn(),
}));
+jest.mock('common/servers/serverManager', () => ({
+ getRemoteInfo: jest.fn(),
+ getViewLog: jest.fn().mockReturnValue({
+ verbose: jest.fn(),
+ info: jest.fn(),
+ error: jest.fn(),
+ silly: jest.fn(),
+ }),
+ on: jest.fn(),
+}));
const server = new MattermostServer({name: 'server_name', url: 'http://server-1.com'});
const view = new MessagingView(server, true);
@@ -268,6 +279,7 @@ describe('main/views/MattermostWebContentsView', () => {
mattermostView.setInitialized = jest.fn();
mattermostView.updateMentionsFromTitle = jest.fn();
mattermostView.findUnreadState = jest.fn();
+ ServerManager.getRemoteInfo.mockReturnValue({serverVersion: '10.0.0'});
});
afterAll(() => {
diff --git a/src/main/views/MattermostWebContentsView.ts b/src/main/views/MattermostWebContentsView.ts
index 45a2d2c1..e91ad0e4 100644
--- a/src/main/views/MattermostWebContentsView.ts
+++ b/src/main/views/MattermostWebContentsView.ts
@@ -4,6 +4,7 @@
import {WebContentsView, app, ipcMain} from 'electron';
import type {WebContentsViewConstructorOptions, Event, Input} from 'electron/main';
import {EventEmitter} from 'events';
+import semver from 'semver';
import AppState from 'common/appState';
import {
@@ -16,6 +17,7 @@ import {
BROWSER_HISTORY_STATUS_UPDATED,
CLOSE_SERVERS_DROPDOWN,
CLOSE_DOWNLOADS_DROPDOWN,
+ LOAD_INCOMPATIBLE_SERVER,
} from 'common/communication';
import type {Logger} from 'common/log';
import ServerManager from 'common/servers/serverManager';
@@ -426,15 +428,22 @@ export class MattermostWebContentsView extends EventEmitter {
private loadSuccess = (loadURL: string) => {
return () => {
- this.log.verbose(`finished loading ${loadURL}`);
- MainWindow.sendToRenderer(LOAD_SUCCESS, this.id);
- this.maxRetries = MAX_SERVER_RETRIES;
- this.status = Status.WAITING_MM;
- this.removeLoading = setTimeout(this.setInitialized, MAX_LOADING_SCREEN_SECONDS, true);
- this.emit(LOAD_SUCCESS, this.id, loadURL);
- const mainWindow = MainWindow.get();
- if (mainWindow && this.currentURL) {
- this.setBounds(getWindowBoundaries(mainWindow));
+ const serverInfo = ServerManager.getRemoteInfo(this.view.server.id);
+ if (serverInfo?.serverVersion && semver.gte(serverInfo.serverVersion, '9.4.0')) {
+ this.log.verbose(`finished loading ${loadURL}`);
+ MainWindow.sendToRenderer(LOAD_SUCCESS, this.id);
+ this.maxRetries = MAX_SERVER_RETRIES;
+ this.status = Status.WAITING_MM;
+ this.removeLoading = setTimeout(this.setInitialized, MAX_LOADING_SCREEN_SECONDS, true);
+ this.emit(LOAD_SUCCESS, this.id, loadURL);
+ const mainWindow = MainWindow.get();
+ if (mainWindow && this.currentURL) {
+ this.setBounds(getWindowBoundaries(mainWindow));
+ }
+ } else {
+ MainWindow.sendToRenderer(LOAD_INCOMPATIBLE_SERVER, this.id, loadURL.toString());
+ this.emit(LOAD_FAILED, this.id, 'Incompatible server version', loadURL.toString());
+ this.status = Status.ERROR;
}
};
};
diff --git a/src/main/views/viewManager.ts b/src/main/views/viewManager.ts
index d6b539e8..2dba923e 100644
--- a/src/main/views/viewManager.ts
+++ b/src/main/views/viewManager.ts
@@ -31,6 +31,7 @@ import {
UNREADS_AND_MENTIONS,
TAB_LOGIN_CHANGED,
DEVELOPER_MODE_UPDATED,
+ OPEN_SERVER_UPGRADE_LINK,
} from 'common/communication';
import Config from 'common/config';
import {Logger} from 'common/log';
@@ -42,7 +43,7 @@ import Utils from 'common/utils/util';
import type {MattermostView} from 'common/views/View';
import {TAB_MESSAGING} from 'common/views/View';
import {handleWelcomeScreenModal} from 'main/app/intercom';
-import {flushCookiesStore} from 'main/app/utils';
+import {flushCookiesStore, updateServerInfos} from 'main/app/utils';
import DeveloperMode from 'main/developerMode';
import performanceMonitor from 'main/performanceMonitor';
import PermissionsManager from 'main/permissionsManager';
@@ -82,6 +83,7 @@ export class ViewManager {
ipcMain.on(BROWSER_HISTORY_PUSH, this.handleBrowserHistoryPush);
ipcMain.on(TAB_LOGIN_CHANGED, this.handleTabLoginChanged);
ipcMain.on(OPEN_SERVER_EXTERNALLY, this.handleOpenServerExternally);
+ ipcMain.on(OPEN_SERVER_UPGRADE_LINK, this.handleOpenServerUpgradeLink);
ipcMain.on(UNREADS_AND_MENTIONS, this.handleUnreadsAndMentionsChanged);
ipcMain.on(SESSION_EXPIRED, this.handleSessionExpired);
@@ -91,8 +93,11 @@ export class ViewManager {
DeveloperMode.on(DEVELOPER_MODE_UPDATED, this.handleDeveloperModeUpdated);
}
- private init = () => {
+ private init = async () => {
if (ServerManager.hasServers()) {
+ // TODO: This init should be happening elsewhere, future refactor will fix this
+ ServerViewState.init();
+ await updateServerInfos(ServerManager.getAllServers());
LoadingScreen.show();
ServerManager.getAllServers().forEach((server) => this.loadServer(server));
this.showInitial();
@@ -303,7 +308,6 @@ export class ViewManager {
private showInitial = () => {
log.verbose('showInitial');
- // TODO: This init should be happening elsewhere, future refactor will fix this
ServerViewState.init();
if (ServerManager.hasServers()) {
const lastActiveServer = ServerViewState.getCurrentServer();
@@ -577,6 +581,12 @@ export class ViewManager {
shell.openExternal(view.view.server.url.toString());
};
+ private handleOpenServerUpgradeLink = () => {
+ if (Config.upgradeLink) {
+ shell.openExternal(Config.upgradeLink);
+ }
+ };
+
private handleUnreadsAndMentionsChanged = (e: IpcMainEvent, isUnread: boolean, mentionCount: number) => {
log.silly('handleUnreadsAndMentionsChanged', {webContentsId: e.sender.id, isUnread, mentionCount});
diff --git a/src/renderer/components/ConnectionErrorView.tsx b/src/renderer/components/ConnectionErrorView.tsx
new file mode 100644
index 00000000..27886551
--- /dev/null
+++ b/src/renderer/components/ConnectionErrorView.tsx
@@ -0,0 +1,90 @@
+// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import React from 'react';
+import {FormattedMessage} from 'react-intl';
+
+import ErrorView from './ErrorView';
+
+type Props = {
+ darkMode: boolean;
+ appName?: string;
+ url?: string;
+ errorInfo?: string;
+ handleLink: () => void;
+};
+
+export default function ConnectionErrorView({darkMode, appName, url, handleLink, errorInfo}: Props) {
+ const header = (
+
+