[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
This commit is contained in:
Devin Binnie
2025-02-21 10:17:49 -05:00
committed by GitHub
parent 2cf4aaaa02
commit 6fa5508588
17 changed files with 286 additions and 68 deletions

View File

@@ -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]) {

View File

@@ -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';

View File

@@ -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'],

View File

@@ -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;
}

View File

@@ -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';

View File

@@ -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)!));

View File

@@ -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()),

View File

@@ -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(() => {

View File

@@ -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;
}
};
};

View File

@@ -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});

View File

@@ -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 = (
<FormattedMessage
id='renderer.components.errorView.cannotConnectToThisServer'
defaultMessage="Couldn't connect to this server"
/>
);
const subHeader = (
<>
<FormattedMessage
id='renderer.components.errorView.havingTroubleConnecting'
defaultMessage={'We\'re having trouble connecting to this {appName} server. We\'ll keep trying to establish a connection.'}
values={{
appName,
}}
/>
<br/>
<FormattedMessage
id='renderer.components.errorView.refreshThenVerify'
defaultMessage="If refreshing this page (Ctrl+R or Command+R) doesn't help, please check the following:"
/>
</>
);
const bullets = (
<>
<li>
<FormattedMessage
id='renderer.components.errorView.troubleshooting.computerIsConnected'
defaultMessage='Ensure your computer is connected to the internet.'
/>
</li>
<li>
<FormattedMessage
id='renderer.components.errorView.troubleshooting.urlIsCorrect.appNameIsCorrect'
defaultMessage='Verify that the URL <link>{url}</link> is correct.'
values={{
appName,
url,
link: (msg: React.ReactNode) => (
<a
onClick={handleLink}
href='#'
>
{msg}
</a>
),
}}
/>
</li>
</>
);
const contactAdmin = (
<FormattedMessage
id='renderer.components.errorView.contactAdmin'
defaultMessage='If the issue persists, please contact your admin'
/>
);
return (
<ErrorView
darkMode={darkMode}
header={header}
subHeader={subHeader}
bullets={bullets}
contactAdmin={contactAdmin}
handleLink={handleLink}
errorInfo={errorInfo}
url={url}
/>
);
}

View File

@@ -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 (
<div className={classNames('ErrorView', {darkMode: props.darkMode})}>
<div className={classNames('ErrorView', {darkMode})}>
<AlertImage/>
<span className='ErrorView-header'>
<FormattedMessage
id='renderer.components.errorView.cannotConnectToThisServer'
defaultMessage="Couldn't connect to this server"
/>
{header}
</span>
<span>
<FormattedMessage
id='renderer.components.errorView.havingTroubleConnecting'
defaultMessage={'We\'re having trouble connecting to this {appName} server. We\'ll keep trying to establish a connection.'}
values={{
appName: props.appName,
}}
/>
<br/>
<FormattedMessage
id='renderer.components.errorView.refreshThenVerify'
defaultMessage="If refreshing this page (Ctrl+R or Command+R) doesn't help, please check the following:"
/>
{subHeader}
</span>
<ul className='ErrorView-bullets'>
<li>
<FormattedMessage
id='renderer.components.errorView.troubleshooting.computerIsConnected'
defaultMessage='Ensure your computer is connected to the internet.'
/>
</li>
<li>
<FormattedMessage
id='renderer.components.errorView.troubleshooting.urlIsCorrect.appNameIsCorrect'
defaultMessage='Verify that the URL <link>{url}</link> is correct.'
values={{
appName: props.appName,
url: props.url,
link: (msg: React.ReactNode) => (
<a
onClick={props.handleLink}
href='#'
>
{msg}
</a>
),
}}
/>
</li>
{bullets}
<li>
<FormattedMessage
id='renderer.components.errorView.troubleshooting.webContentsView.canReachFromBrowserWindow'
defaultMessage='Try opening <link>{url}</link> in a browser window.'
values={{
url: props.url,
url,
link: (msg: React.ReactNode) => (
<a
onClick={props.handleLink}
onClick={handleLink}
href='#'
>
{msg}
@@ -88,13 +63,10 @@ export default function ErrorView(props: Props) {
</li>
</ul>
<span>
<FormattedMessage
id='renderer.components.errorView.contactAdmin'
defaultMessage='If the issue persists, please contact your admin'
/>
{contactAdmin}
</span>
<span className='ErrorView-techInfo'>
{props.errorInfo}
{errorInfo}
</span>
</div>
);

View File

@@ -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 = (
<FormattedMessage
id='renderer.components.errorView.incompatibleServerVersion'
defaultMessage='Incompatible server version'
/>
);
const subHeader = (
<>
<FormattedMessage
id='renderer.components.errorView.serverVersionIsIncompatible'
defaultMessage={'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:'}
values={{
appName,
}}
/>
</>
);
const bullets = (
<>
<li>
<FormattedMessage
id='renderer.components.errorView.troubleshooting.downgradeApp'
defaultMessage='<link>Downgrade your {appName} Desktop App</link> to version v5.10 or earlier.'
values={{
appName,
link: (msg: React.ReactNode) => (
<a
href='#'
onClick={handleUpgradeLink}
>
{msg}
</a>
),
}}
/>
</li>
</>
);
const contactAdmin = (
<FormattedMessage
id='renderer.components.errorView.contactAdminUpgrade'
defaultMessage='If the issue persists, contact your {appName} Administrator or IT department to upgrade this {appName} Server.'
values={{
appName,
}}
/>
);
return (
<ErrorView
darkMode={darkMode}
header={header}
subHeader={subHeader}
bullets={bullets}
contactAdmin={contactAdmin}
handleLink={handleLink}
url={url}
/>
);
}

View File

@@ -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<Props, State> {
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<Props, State> {
switch (tabStatus.status) {
case Status.FAILED:
component = (
<ErrorView
<ConnectionErrorView
darkMode={this.props.darkMode}
errorInfo={tabStatus.extra?.error}
url={tabStatus.extra ? tabStatus.extra.url : ''}
@@ -515,6 +528,16 @@ class MainPage extends React.PureComponent<Props, State> {
handleLink={this.openServerExternally}
/>);
break;
case Status.INCOMPATIBLE:
component = (
<IncompatibleErrorView
darkMode={this.props.darkMode}
url={tabStatus.extra ? tabStatus.extra.url : ''}
appName={this.props.appName}
handleLink={this.openServerExternally}
handleUpgradeLink={() => window.desktop.openServerUpgradeLink()}
/>);
break;
case Status.LOADING:
case Status.RETRY:
case Status.DONE:

View File

@@ -107,6 +107,7 @@ export type BuildConfig = {
defaultServers?: Server[];
helpLink: string;
academyLink: string;
upgradeLink: string;
enableServerManagement: boolean;
enableAutoUpdater: boolean;
managedResources: string[];

View File

@@ -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;