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 = ( + + ); + + const subHeader = ( + <> + +
+ + + ); + + const bullets = ( + <> +
  • + +
  • +
  • + ( + + {msg} + + ), + }} + /> +
  • + + ); + + const contactAdmin = ( + + ); + + return ( + + ); +} diff --git a/src/renderer/components/ErrorView.tsx b/src/renderer/components/ErrorView.tsx index 06ab5fec..3337d5f0 100644 --- a/src/renderer/components/ErrorView.tsx +++ b/src/renderer/components/ErrorView.tsx @@ -12,72 +12,47 @@ import AlertImage from './Images/alert'; import 'renderer/css/components/ErrorView.scss'; -type Props = { +type ErrorViewProps = { darkMode: boolean; + header: React.ReactNode; + subHeader: React.ReactNode; + bullets: React.ReactNode; + contactAdmin: React.ReactNode; errorInfo?: string; url?: string; - appName?: string; handleLink: () => void; }; -export default function ErrorView(props: Props) { +export default function ErrorView({ + darkMode, + header, + subHeader, + bullets, + contactAdmin, + errorInfo, + url, + handleLink, +}: ErrorViewProps) { return ( -
    + ); diff --git a/src/renderer/components/IncompatibleErrorView.tsx b/src/renderer/components/IncompatibleErrorView.tsx new file mode 100644 index 00000000..c54ef4a3 --- /dev/null +++ b/src/renderer/components/IncompatibleErrorView.tsx @@ -0,0 +1,80 @@ +// 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; + handleLink: () => void; + handleUpgradeLink: () => void; +}; + +export default function IncompatibleErrorView({darkMode, appName, url, handleLink, handleUpgradeLink}: Props) { + const header = ( + + ); + + const subHeader = ( + <> + + + ); + + const bullets = ( + <> +
  • + ( + + {msg} + + ), + }} + /> +
  • + + ); + + const contactAdmin = ( + + ); + + return ( + + ); +} diff --git a/src/renderer/components/MainPage.tsx b/src/renderer/components/MainPage.tsx index 7cbb0bfb..756cb4f0 100644 --- a/src/renderer/components/MainPage.tsx +++ b/src/renderer/components/MainPage.tsx @@ -11,9 +11,10 @@ import {injectIntl} from 'react-intl'; import type {UniqueView, UniqueServer} from 'types/config'; import type {DownloadedItems} from 'types/downloads'; +import ConnectionErrorView from './ConnectionErrorView'; import DeveloperModeIndicator from './DeveloperModeIndicator'; import DownloadsDropdownButton from './DownloadsDropdown/DownloadsDropdownButton'; -import ErrorView from './ErrorView'; +import IncompatibleErrorView from './IncompatibleErrorView'; import ServerDropdownButton from './ServerDropdownButton'; import TabBar from './TabBar'; @@ -28,6 +29,7 @@ enum Status { RETRY = -1, FAILED = 0, NOSERVERS = -2, + INCOMPATIBLE = -3, } type Props = { @@ -61,7 +63,7 @@ type TabViewStatus = { status: Status; extra?: { url: string; - error: string; + error?: string; }; } @@ -186,6 +188,17 @@ class MainPage extends React.PureComponent { this.updateTabStatus(viewId, statusValue); }); + window.desktop.onLoadIncompatibleServer((viewId, loadUrl) => { + console.error(`${viewId}: tried to load incompatible server`); + const statusValue = { + status: Status.INCOMPATIBLE, + extra: { + url: loadUrl, + }, + }; + this.updateTabStatus(viewId, statusValue); + }); + // can't switch tabs sequentially for some reason... window.desktop.onSetActiveView(this.setActiveView); @@ -507,7 +520,7 @@ class MainPage extends React.PureComponent { switch (tabStatus.status) { case Status.FAILED: component = ( - { handleLink={this.openServerExternally} />); break; + case Status.INCOMPATIBLE: + component = ( + window.desktop.openServerUpgradeLink()} + />); + break; case Status.LOADING: case Status.RETRY: case Status.DONE: diff --git a/src/types/config.ts b/src/types/config.ts index 75997fbe..03c97fee 100644 --- a/src/types/config.ts +++ b/src/types/config.ts @@ -107,6 +107,7 @@ export type BuildConfig = { defaultServers?: Server[]; helpLink: string; academyLink: string; + upgradeLink: string; enableServerManagement: boolean; enableAutoUpdater: boolean; managedResources: string[]; diff --git a/src/types/window.ts b/src/types/window.ts index d64a82e2..352a33ba 100644 --- a/src/types/window.ts +++ b/src/types/window.ts @@ -38,6 +38,7 @@ declare global { doubleClickOnWindow: (windowName?: string) => void; focusCurrentView: () => void; openServerExternally: () => void; + openServerUpgradeLink: () => void; closeDownloadsDropdown: () => void; closeDownloadsDropdownMenu: () => void; openDownloadsDropdown: () => void; @@ -72,6 +73,7 @@ declare global { onLoadRetry: (listener: (viewId: string, retry: Date, err: string, loadUrl: string) => void) => void; onLoadSuccess: (listener: (viewId: string) => void) => void; onLoadFailed: (listener: (viewId: string, err: string, loadUrl: string) => void) => void; + onLoadIncompatibleServer: (listener: (viewId: string, loadUrl: string) => void) => void; onSetActiveView: (listener: (serverId: string, viewId: string) => void) => void; onMaximizeChange: (listener: (maximize: boolean) => void) => void; onEnterFullScreen: (listener: () => void) => void;