diff --git a/i18n/en.json b/i18n/en.json index cf4a77d15ed..f5e04084602 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -128,8 +128,6 @@ "main.tray.tray.expired": "Session Expired: Please sign in to continue receiving notifications.", "main.tray.tray.mention": "You have been mentioned", "main.tray.tray.unread": "You have unread channels", - "main.views.viewManager.handleDeepLink.error.body": "There is no configured server in the app that matches the requested url: {url}", - "main.views.viewManager.handleDeepLink.error.title": "No matching server", "main.windows.mainWindow.closeApp.dialog.checkboxLabel": "Don't ask again", "main.windows.mainWindow.closeApp.dialog.detail": "You will no longer receive notifications for messages. If you want to leave {appName} running in the system tray, you can enable this in Settings.", "main.windows.mainWindow.closeApp.dialog.message": "Are you sure you want to quit?", diff --git a/src/app/serverViewState.test.js b/src/app/serverViewState.test.js index c5b11befe7a..4ae40b31dc9 100644 --- a/src/app/serverViewState.test.js +++ b/src/app/serverViewState.test.js @@ -209,7 +209,7 @@ describe('app/serverViewState', () => { serverViewState.showNewServerModal(); await promise; - expect(ServerManager.addServer).toHaveBeenCalledWith(data); + expect(ServerManager.addServer).toHaveBeenCalledWith(data, undefined); expect(serversCopy).toContainEqual(expect.objectContaining({ id: 'server-1', name: 'new-server', diff --git a/src/app/serverViewState.ts b/src/app/serverViewState.ts index 658f086fa70..e41ca85146e 100644 --- a/src/app/serverViewState.ts +++ b/src/app/serverViewState.ts @@ -44,7 +44,7 @@ export class ServerViewState { constructor() { ipcMain.on(SWITCH_SERVER, (event, serverId) => this.switchServer(serverId)); - ipcMain.on(SHOW_NEW_SERVER_MODAL, this.showNewServerModal); + ipcMain.on(SHOW_NEW_SERVER_MODAL, this.handleShowNewServerModal); ipcMain.on(SHOW_EDIT_SERVER_MODAL, this.showEditServerModal); ipcMain.on(SHOW_REMOVE_SERVER_MODAL, this.showRemoveServerModal); ipcMain.handle(VALIDATE_SERVER_URL, this.handleServerURLValidation); @@ -123,25 +123,32 @@ export class ServerViewState { * Server Modals */ - showNewServerModal = () => { - log.debug('showNewServerModal'); + showNewServerModal = (prefillURL?: string) => { + log.debug('showNewServerModal', {prefillURL}); const mainWindow = MainWindow.get(); if (!mainWindow) { return; } - const modalPromise = ModalManager.addModal( + const modalPromise = ModalManager.addModal<{prefillURL?: string}, Server>( 'newServer', 'mattermost-desktop://renderer/newServer.html', getLocalPreload('internalAPI.js'), - null, + {prefillURL}, mainWindow, !ServerManager.hasServers(), ); modalPromise.then((data) => { - const newServer = ServerManager.addServer(data); + let initialLoadURL; + if (prefillURL) { + const parsedServerURL = parseURL(data.url); + if (parsedServerURL) { + initialLoadURL = parseURL(`${parsedServerURL.origin}${prefillURL.substring(prefillURL.indexOf('/'))}`); + } + } + const newServer = ServerManager.addServer(data, initialLoadURL); this.switchServer(newServer.id, true); }).catch((e) => { // e is undefined for user cancellation @@ -151,6 +158,8 @@ export class ServerViewState { }); }; + private handleShowNewServerModal = () => this.showNewServerModal(); + private showEditServerModal = (e: IpcMainEvent, id: string) => { log.debug('showEditServerModal', id); @@ -281,6 +290,11 @@ export class ServerViewState { // If the original URL was invalid, don't replace that as they probably have a typo somewhere // Also strip the trailing slash if it's there so that the user can keep typing if (!remoteInfo) { + // If the URL provided has a path, try to validate the server with parts of the path removed, until we reach the root and then return a failure + if (parsedURL.pathname !== '/') { + return this.handleServerURLValidation(e, parsedURL.toString().substring(0, parsedURL.toString().lastIndexOf('/')), currentId); + } + return {status: URLValidationStatus.NotMattermost, validatedURL: parsedURL.toString().replace(/\/$/, '')}; } diff --git a/src/common/servers/MattermostServer.ts b/src/common/servers/MattermostServer.ts index d98c7a14d67..5210838798c 100644 --- a/src/common/servers/MattermostServer.ts +++ b/src/common/servers/MattermostServer.ts @@ -12,14 +12,16 @@ export class MattermostServer { name: string; url!: URL; isPredefined: boolean; + initialLoadURL?: URL; - constructor(server: Server, isPredefined: boolean) { + constructor(server: Server, isPredefined: boolean, initialLoadURL?: URL) { this.id = uuid(); this.name = server.name; this.updateURL(server.url); this.isPredefined = isPredefined; + this.initialLoadURL = initialLoadURL; } updateURL = (url: string) => { diff --git a/src/common/servers/serverManager.ts b/src/common/servers/serverManager.ts index d2edd1e6696..fad1a67c578 100644 --- a/src/common/servers/serverManager.ts +++ b/src/common/servers/serverManager.ts @@ -156,8 +156,8 @@ export class ServerManager extends EventEmitter { this.persistServers(); }; - addServer = (server: Server) => { - const newServer = new MattermostServer(server, false); + addServer = (server: Server, initialLoadURL?: URL) => { + const newServer = new MattermostServer(server, false, initialLoadURL); if (this.servers.has(newServer.id)) { throw new Error('ID Collision detected. Cannot add server.'); diff --git a/src/main/app/intercom.test.js b/src/main/app/intercom.test.js index c186a1fa37b..e7443f97457 100644 --- a/src/main/app/intercom.test.js +++ b/src/main/app/intercom.test.js @@ -63,7 +63,7 @@ describe('main/app/intercom', () => { ModalManager.addModal.mockReturnValue(promise); handleWelcomeScreenModal(); - expect(ModalManager.addModal).toHaveBeenCalledWith('welcomeScreen', 'mattermost-desktop://renderer/welcomeScreen.html', '/some/preload.js', null, {}, true); + expect(ModalManager.addModal).toHaveBeenCalledWith('welcomeScreen', 'mattermost-desktop://renderer/welcomeScreen.html', '/some/preload.js', {prefillURL: undefined}, {}, true); }); }); diff --git a/src/main/app/intercom.ts b/src/main/app/intercom.ts index 8acfac87d8e..41c4dfb1b10 100644 --- a/src/main/app/intercom.ts +++ b/src/main/app/intercom.ts @@ -8,6 +8,7 @@ import ServerViewState from 'app/serverViewState'; import {Logger} from 'common/log'; import ServerManager from 'common/servers/serverManager'; import {ping} from 'common/utils/requests'; +import {parseURL} from 'common/utils/url'; import NotificationManager from 'main/notifications'; import {getLocalPreload} from 'main/utils'; import ModalManager from 'main/views/modalManager'; @@ -85,7 +86,7 @@ export function handleMainWindowIsShown() { } } -export function handleWelcomeScreenModal() { +export function handleWelcomeScreenModal(prefillURL?: string) { log.debug('handleWelcomeScreenModal'); const html = 'mattermost-desktop://renderer/welcomeScreen.html'; @@ -96,10 +97,17 @@ export function handleWelcomeScreenModal() { if (!mainWindow) { return; } - const modalPromise = ModalManager.addModal('welcomeScreen', html, preload, null, mainWindow, !ServerManager.hasServers()); + const modalPromise = ModalManager.addModal<{prefillURL?: string}, UniqueServer>('welcomeScreen', html, preload, {prefillURL}, mainWindow, !ServerManager.hasServers()); if (modalPromise) { modalPromise.then((data) => { - const newServer = ServerManager.addServer(data); + let initialLoadURL; + if (prefillURL) { + const parsedServerURL = parseURL(data.url); + if (parsedServerURL) { + initialLoadURL = parseURL(`${parsedServerURL.origin}${prefillURL.substring(prefillURL.indexOf('/'))}`); + } + } + const newServer = ServerManager.addServer(data, initialLoadURL); ServerViewState.switchServer(newServer.id, true); }).catch((e) => { // e is undefined for user cancellation diff --git a/src/main/views/modalManager.ts b/src/main/views/modalManager.ts index 590fe25da46..b496deaac52 100644 --- a/src/main/views/modalManager.ts +++ b/src/main/views/modalManager.ts @@ -65,6 +65,18 @@ export class ModalManager { return this.modalPromises.get(key) as Promise; }; + removeModal = (key: string) => { + const modalView = this.modalQueue.find((modal) => modal.key === key); + if (!modalView) { + return; + } + + modalView.hide(); + modalView.resolve(null); + this.modalPromises.delete(key); + this.filterActive(); + }; + findModalByCaller = (event: IpcMainInvokeEvent) => { if (this.modalQueue.length) { const requestModal = this.modalQueue.find((modal) => { diff --git a/src/main/views/viewManager.test.js b/src/main/views/viewManager.test.js index 0075041900a..668c0e8bb38 100644 --- a/src/main/views/viewManager.test.js +++ b/src/main/views/viewManager.test.js @@ -1,8 +1,6 @@ // Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {dialog} from 'electron'; - import ServerViewState from 'app/serverViewState'; import {BROWSER_HISTORY_PUSH, LOAD_SUCCESS, SET_ACTIVE_VIEW} from 'common/communication'; import ServerManager from 'common/servers/serverManager'; @@ -20,9 +18,6 @@ jest.mock('electron', () => ({ getAppPath: () => '/path/to/app', getPath: jest.fn(() => '/valid/downloads/path'), }, - dialog: { - showErrorBox: jest.fn(), - }, ipcMain: { emit: jest.fn(), on: jest.fn(), @@ -33,6 +28,7 @@ jest.mock('app/serverViewState', () => ({ getCurrentServer: jest.fn(), updateCurrentView: jest.fn(), init: jest.fn(), + showNewServerModal: jest.fn(), })); jest.mock('common/views/View', () => ({ getViewName: jest.fn((a, b) => `${a}-${b}`), @@ -62,6 +58,10 @@ jest.mock('main/app/utils', () => ({ flushCookiesStore: jest.fn(), })); +jest.mock('main/app/intercom', () => ({ + handleWelcomeScreenModal: jest.fn(), +})); + jest.mock('main/i18nManager', () => ({ localizeMessage: jest.fn(), })); @@ -116,8 +116,9 @@ jest.mock('./MattermostWebContentsView', () => ({ MattermostWebContentsView: jest.fn(), })); -jest.mock('./modalManager', () => ({ +jest.mock('main/views/modalManager', () => ({ showModal: jest.fn(), + removeModal: jest.fn(), isModalDisplayed: jest.fn(), })); jest.mock('./webContentEvents', () => ({})); @@ -321,6 +322,7 @@ describe('main/views/viewManager', () => { isOpen: true, url: new URL('http://server1.com/view'), }, + undefined, ); makeSpy.mockRestore(); }); @@ -692,11 +694,12 @@ describe('main/views/viewManager', () => { expect(view.load).not.toHaveBeenCalled(); }); - it('should throw dialog when cannot find the view', () => { + it('should open new server modal when using a server that does not exist', () => { + ServerManager.hasServers.mockReturnValue(true); const view = {...baseView}; - viewManager.handleDeepLink('mattermost://server-1.com/deep/link?thing=yes'); + viewManager.handleDeepLink('mattermost://server-2.com/deep/link?thing=yes'); expect(view.load).not.toHaveBeenCalled(); - expect(dialog.showErrorBox).toHaveBeenCalled(); + expect(ServerViewState.showNewServerModal).toHaveBeenCalled(); }); it('should reopen closed view if called upon', () => { diff --git a/src/main/views/viewManager.ts b/src/main/views/viewManager.ts index 3de2c1596c2..0eae96890df 100644 --- a/src/main/views/viewManager.ts +++ b/src/main/views/viewManager.ts @@ -2,7 +2,7 @@ // See LICENSE.txt for license information. import type {IpcMainEvent, IpcMainInvokeEvent} from 'electron'; -import {WebContentsView, dialog, ipcMain} from 'electron'; +import {WebContentsView, ipcMain} from 'electron'; import isDev from 'electron-is-dev'; import ServerViewState from 'app/serverViewState'; @@ -41,18 +41,18 @@ import {getFormattedPathName, parseURL} from 'common/utils/url'; 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 DeveloperMode from 'main/developerMode'; -import {localizeMessage} from 'main/i18nManager'; import performanceMonitor from 'main/performanceMonitor'; import PermissionsManager from 'main/permissionsManager'; +import ModalManager from 'main/views/modalManager'; import MainWindow from 'main/windows/mainWindow'; import type {DeveloperSettings} from 'types/settings'; import LoadingScreen from './loadingScreen'; import {MattermostWebContentsView} from './MattermostWebContentsView'; -import modalManager from './modalManager'; import {getLocalPreload, getAdjustedWindowBoundaries} from '../utils'; @@ -158,14 +158,14 @@ export class ViewManager { } else { this.getViewLogger(viewId).warn(`Couldn't find a view with name: ${viewId}`); } - modalManager.showModal(); + ModalManager.showModal(); }; focusCurrentView = () => { log.debug('focusCurrentView'); - if (modalManager.isModalDisplayed()) { - modalManager.focusCurrentModal(); + if (ModalManager.isModalDisplayed()) { + ModalManager.focusCurrentModal(); return; } @@ -227,11 +227,11 @@ export class ViewManager { webContentsView.once(LOAD_FAILED, this.deeplinkFailed); } } + } else if (ServerManager.hasServers()) { + ServerViewState.showNewServerModal(`${parsedURL.host}${getFormattedPathName(parsedURL.pathname)}${parsedURL.search}`); } else { - dialog.showErrorBox( - localizeMessage('main.views.viewManager.handleDeepLink.error.title', 'No matching server'), - localizeMessage('main.views.viewManager.handleDeepLink.error.body', 'There is no configured server in the app that matches the requested url: {url}', {url: parsedURL.toString()}), - ); + ModalManager.removeModal('welcomeScreen'); + handleWelcomeScreenModal(`${parsedURL.host}${getFormattedPathName(parsedURL.pathname)}${parsedURL.search}`); } } }; @@ -439,7 +439,7 @@ export class ViewManager { } else if (recycle) { views.set(view.id, recycle); } else { - views.set(view.id, this.makeView(srv, view)); + views.set(view.id, this.makeView(srv, view, srv.initialLoadURL?.toString())); } } diff --git a/src/renderer/components/ConfigureServer.tsx b/src/renderer/components/ConfigureServer.tsx index a76cfa2eb96..86b239dd893 100644 --- a/src/renderer/components/ConfigureServer.tsx +++ b/src/renderer/components/ConfigureServer.tsx @@ -20,6 +20,7 @@ import 'renderer/css/components/LoadingScreen.css'; type ConfigureServerProps = { server?: UniqueServer; + prefillURL?: string; mobileView?: boolean; darkMode?: boolean; messageTitle?: string; @@ -33,6 +34,7 @@ type ConfigureServerProps = { function ConfigureServer({ server, + prefillURL, mobileView, darkMode, messageTitle, @@ -53,8 +55,8 @@ function ConfigureServer({ const mounted = useRef(false); const [transition, setTransition] = useState<'inFromRight' | 'outToLeft'>(); - const [name, setName] = useState(prevName || ''); - const [url, setUrl] = useState(prevURL || ''); + const [name, setName] = useState(prevName ?? ''); + const [url, setUrl] = useState(prevURL ?? prefillURL ?? ''); const [nameError, setNameError] = useState(''); const [urlError, setURLError] = useState<{type: STATUS; value: string}>(); const [showContent, setShowContent] = useState(false); @@ -71,6 +73,11 @@ function ConfigureServer({ setTransition('inFromRight'); setShowContent(true); mounted.current = true; + + if (url) { + fetchValidationResult(url); + } + return () => { mounted.current = false; }; diff --git a/src/renderer/components/NewServerModal.tsx b/src/renderer/components/NewServerModal.tsx index d39e6e4d347..3a6c28b32ef 100644 --- a/src/renderer/components/NewServerModal.tsx +++ b/src/renderer/components/NewServerModal.tsx @@ -27,6 +27,7 @@ type Props = { currentOrder?: number; setInputRef?: (inputRef: HTMLInputElement) => void; intl: IntlShape; + prefillURL?: string; }; type State = { @@ -77,6 +78,13 @@ class NewServerModal extends React.PureComponent { this.mounted = false; } + componentDidUpdate(prevProps: Readonly): void { + if (this.props.prefillURL && this.props.prefillURL !== prevProps.prefillURL) { + this.setState({serverUrl: this.props.prefillURL}); + this.validateServerURL(this.props.prefillURL); + } + } + initializeOnShow = async () => { const cameraDisabled = window.process.platform === 'win32' && await window.desktop.getMediaAccessStatus('camera') !== 'granted'; const microphoneDisabled = window.process.platform === 'win32' && await window.desktop.getMediaAccessStatus('microphone') !== 'granted'; diff --git a/src/renderer/modals/newServer/newServer.tsx b/src/renderer/modals/newServer/newServer.tsx index da1452b1798..4f08655448a 100644 --- a/src/renderer/modals/newServer/newServer.tsx +++ b/src/renderer/modals/newServer/newServer.tsx @@ -25,12 +25,18 @@ const onSave = (data: UniqueServer) => { }; const NewServerModalWrapper: React.FC = () => { + const [data, setData] = useState<{prefillURL?: string}>(); const [unremoveable, setUnremovable] = useState(); useEffect(() => { window.desktop.modals.isModalUncloseable().then((uncloseable) => { setUnremovable(uncloseable); }); + + window.desktop.modals.getModalInfo<{prefillURL?: string}>(). + then((data) => { + setData(data); + }); }, []); return ( @@ -39,6 +45,7 @@ const NewServerModalWrapper: React.FC = () => { onClose={unremoveable ? undefined : onClose} onSave={onSave} editMode={false} + prefillURL={data?.prefillURL} show={true} /> diff --git a/src/renderer/modals/welcomeScreen/welcomeScreen.tsx b/src/renderer/modals/welcomeScreen/welcomeScreen.tsx index b633914d653..962fbd35fb5 100644 --- a/src/renderer/modals/welcomeScreen/welcomeScreen.tsx +++ b/src/renderer/modals/welcomeScreen/welcomeScreen.tsx @@ -20,6 +20,7 @@ const onConnect = (data: UniqueServer) => { }; const WelcomeScreenModalWrapper = () => { + const [data, setData] = useState<{prefillURL?: string}>(); const [darkMode, setDarkMode] = useState(false); const [getStarted, setGetStarted] = useState(false); const [mobileView, setMobileView] = useState(false); @@ -37,6 +38,14 @@ const WelcomeScreenModalWrapper = () => { setDarkMode(result); }); + window.desktop.modals.getModalInfo<{prefillURL?: string}>(). + then((data) => { + setData(data); + if (data.prefillURL) { + setGetStarted(true); + } + }); + handleWindowResize(); window.addEventListener('resize', handleWindowResize); @@ -56,6 +65,7 @@ const WelcomeScreenModalWrapper = () => { mobileView={mobileView} darkMode={darkMode} onConnect={onConnect} + prefillURL={data?.prefillURL} /> ) : (