diff --git a/api-types/index.ts b/api-types/index.ts index d48fd1dc..6918bd70 100644 --- a/api-types/index.ts +++ b/api-types/index.ts @@ -38,30 +38,36 @@ export type DesktopAPI = { onBrowserHistoryPush: (listener: (pathName: string) => void) => () => void; sendBrowserHistoryPush: (path: string) => void; - // Calls widget - openLinkFromCallsWidget: (url: string) => void; - openScreenShareModal: () => void; - onScreenShared: (listener: (sourceID: string, withAudio: boolean) => void) => () => void; - callsWidgetConnected: (callID: string, sessionID: string) => void; - onJoinCallRequest: (listener: (callID: string) => void) => () => void; - resizeCallsWidget: (width: number, height: number) => void; - focusPopout: () => void; - leaveCall: () => void; - sendCallsError: (err: string, callID?: string, errMsg?: string) => void; - - // Calls plugin - getDesktopSources: (opts: DesktopSourcesOptions) => Promise; - onOpenScreenShareModal: (listener: () => void) => () => void; - shareScreen: (sourceID: string, withAudi: boolean) => void; + // Calls joinCall: (opts: { callID: string; title: string; rootID: string; channelURL: string; }) => Promise<{callID: string; sessionID: string}>; - sendJoinCallRequest: (callId: string) => void; + leaveCall: () => void; + + callsWidgetConnected: (callID: string, sessionID: string) => void; + resizeCallsWidget: (width: number, height: number) => void; + + sendCallsError: (err: string, callID?: string, errMsg?: string) => void; onCallsError: (listener: (err: string, callID?: string, errMsg?: string) => void) => () => void; + getDesktopSources: (opts: DesktopSourcesOptions) => Promise; + + openScreenShareModal: () => void; + onOpenScreenShareModal: (listener: () => void) => () => void; + + shareScreen: (sourceID: string, withAudio: boolean) => void; + onScreenShared: (listener: (sourceID: string, withAudio: boolean) => void) => () => void; + + sendJoinCallRequest: (callId: string) => void; + onJoinCallRequest: (listener: (callID: string) => void) => () => void; + + openLinkFromCalls: (url: string) => void; + + focusPopout: () => void; + // Utility unregister: (channel: string) => void; } diff --git a/api-types/lib/index.d.ts b/api-types/lib/index.d.ts index 9ebc44a3..a92e2b2c 100644 --- a/api-types/lib/index.d.ts +++ b/api-types/lib/index.d.ts @@ -1,3 +1,5 @@ +// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. export declare type DesktopSourcesOptions = { types: Array<'screen' | 'window'>; thumbnailSize?: { @@ -30,18 +32,6 @@ export declare type DesktopAPI = { onBrowserHistoryStatusUpdated: (listener: (canGoBack: boolean, canGoForward: boolean) => void) => () => void; onBrowserHistoryPush: (listener: (pathName: string) => void) => () => void; sendBrowserHistoryPush: (path: string) => void; - openLinkFromCallsWidget: (url: string) => void; - openScreenShareModal: () => void; - onScreenShared: (listener: (sourceID: string, withAudio: boolean) => void) => () => void; - callsWidgetConnected: (callID: string, sessionID: string) => void; - onJoinCallRequest: (listener: (callID: string) => void) => () => void; - resizeCallsWidget: (width: number, height: number) => void; - focusPopout: () => void; - leaveCall: () => void; - sendCallsError: (err: string, callID?: string, errMsg?: string) => void; - getDesktopSources: (opts: DesktopSourcesOptions) => Promise; - onOpenScreenShareModal: (listener: () => void) => () => void; - shareScreen: (sourceID: string, withAudi: boolean) => void; joinCall: (opts: { callID: string; title: string; @@ -51,7 +41,19 @@ export declare type DesktopAPI = { callID: string; sessionID: string; }>; - sendJoinCallRequest: (callId: string) => void; + leaveCall: () => void; + callsWidgetConnected: (callID: string, sessionID: string) => void; + resizeCallsWidget: (width: number, height: number) => void; + sendCallsError: (err: string, callID?: string, errMsg?: string) => void; onCallsError: (listener: (err: string, callID?: string, errMsg?: string) => void) => () => void; + getDesktopSources: (opts: DesktopSourcesOptions) => Promise; + openScreenShareModal: () => void; + onOpenScreenShareModal: (listener: () => void) => () => void; + shareScreen: (sourceID: string, withAudio: boolean) => void; + onScreenShared: (listener: (sourceID: string, withAudio: boolean) => void) => () => void; + sendJoinCallRequest: (callId: string) => void; + onJoinCallRequest: (listener: (callID: string) => void) => () => void; + openLinkFromCalls: (url: string) => void; + focusPopout: () => void; unregister: (channel: string) => void; }; diff --git a/api-types/lib/index.js b/api-types/lib/index.js index 96ab4ebb..351eb9fc 100644 --- a/api-types/lib/index.js +++ b/api-types/lib/index.js @@ -1,4 +1,7 @@ -"use strict"; // Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -Object.defineProperty(exports, "__esModule", { value: true }); +'use strict'; + +// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +Object.defineProperty(exports, '__esModule', {value: true}); diff --git a/src/main/preload/externalAPI.ts b/src/main/preload/externalAPI.ts index 9273ddca..28609279 100644 --- a/src/main/preload/externalAPI.ts +++ b/src/main/preload/externalAPI.ts @@ -81,25 +81,30 @@ const desktopAPI: DesktopAPI = { onBrowserHistoryPush: (listener) => createListener(BROWSER_HISTORY_PUSH, listener), sendBrowserHistoryPush: (path) => ipcRenderer.send(BROWSER_HISTORY_PUSH, path), - // Calls widget - openLinkFromCallsWidget: (url) => ipcRenderer.send(CALLS_LINK_CLICK, url), - openScreenShareModal: () => ipcRenderer.send(DESKTOP_SOURCES_MODAL_REQUEST), - onScreenShared: (listener) => createListener(CALLS_WIDGET_SHARE_SCREEN, listener), - callsWidgetConnected: (callID, sessionID) => ipcRenderer.send(CALLS_JOINED_CALL, callID, sessionID), - onJoinCallRequest: (listener) => createListener(CALLS_JOIN_REQUEST, listener), - resizeCallsWidget: (width, height) => ipcRenderer.send(CALLS_WIDGET_RESIZE, width, height), - focusPopout: () => ipcRenderer.send(CALLS_POPOUT_FOCUS), - leaveCall: () => ipcRenderer.send(CALLS_LEAVE_CALL), - sendCallsError: (error) => ipcRenderer.send(CALLS_ERROR, error), - - // Calls plugin - getDesktopSources: (opts) => ipcRenderer.invoke(GET_DESKTOP_SOURCES, opts), - onOpenScreenShareModal: (listener) => createListener(DESKTOP_SOURCES_MODAL_REQUEST, listener), - shareScreen: (sourceID, withAudio) => ipcRenderer.send(CALLS_WIDGET_SHARE_SCREEN, sourceID, withAudio), + // Calls joinCall: (opts) => ipcRenderer.invoke(CALLS_JOIN_CALL, opts), - sendJoinCallRequest: (callId) => ipcRenderer.send(CALLS_JOIN_REQUEST, callId), + leaveCall: () => ipcRenderer.send(CALLS_LEAVE_CALL), + + callsWidgetConnected: (callID, sessionID) => ipcRenderer.send(CALLS_JOINED_CALL, callID, sessionID), + resizeCallsWidget: (width, height) => ipcRenderer.send(CALLS_WIDGET_RESIZE, width, height), + + sendCallsError: (err, callID, errMsg) => ipcRenderer.send(CALLS_ERROR, err, callID, errMsg), onCallsError: (listener) => createListener(CALLS_ERROR, listener), + getDesktopSources: (opts) => ipcRenderer.invoke(GET_DESKTOP_SOURCES, opts), + openScreenShareModal: () => ipcRenderer.send(DESKTOP_SOURCES_MODAL_REQUEST), + onOpenScreenShareModal: (listener) => createListener(DESKTOP_SOURCES_MODAL_REQUEST, listener), + + shareScreen: (sourceID, withAudio) => ipcRenderer.send(CALLS_WIDGET_SHARE_SCREEN, sourceID, withAudio), + onScreenShared: (listener) => createListener(CALLS_WIDGET_SHARE_SCREEN, listener), + + sendJoinCallRequest: (callId) => ipcRenderer.send(CALLS_JOIN_REQUEST, callId), + onJoinCallRequest: (listener) => createListener(CALLS_JOIN_REQUEST, listener), + + openLinkFromCalls: (url) => ipcRenderer.send(CALLS_LINK_CLICK, url), + + focusPopout: () => ipcRenderer.send(CALLS_POPOUT_FOCUS), + // Utility unregister: (channel) => ipcRenderer.removeAllListeners(channel), }; diff --git a/src/main/windows/callsWidgetWindow.test.js b/src/main/windows/callsWidgetWindow.test.js index 5739f7e2..9ea63a32 100644 --- a/src/main/windows/callsWidgetWindow.test.js +++ b/src/main/windows/callsWidgetWindow.test.js @@ -7,7 +7,7 @@ import {BrowserWindow, desktopCapturer, systemPreferences, ipcMain} from 'electr import ServerViewState from 'app/serverViewState'; -import {CALLS_WIDGET_SHARE_SCREEN, UPDATE_SHORTCUT_MENU} from 'common/communication'; +import {CALLS_WIDGET_SHARE_SCREEN, BROWSER_HISTORY_PUSH, UPDATE_SHORTCUT_MENU} from 'common/communication'; import { MINIMUM_CALLS_WIDGET_WIDTH, MINIMUM_CALLS_WIDGET_HEIGHT, @@ -534,6 +534,7 @@ describe('main/windows/callsWidgetWindow', () => { describe('handleGetDesktopSources', () => { const callsWidgetWindow = new CallsWidgetWindow(); + callsWidgetWindow.options = {callID: 'callID'}; callsWidgetWindow.win = { webContents: { send: jest.fn(), @@ -626,12 +627,8 @@ describe('main/windows/callsWidgetWindow', () => { it('should send error with no sources', async () => { jest.spyOn(desktopCapturer, 'getSources').mockResolvedValue([]); await callsWidgetWindow.handleGetDesktopSources({sender: {id: 1}}, null); - expect(callsWidgetWindow.win.webContents.send).toHaveBeenCalledWith('calls-error', { - err: 'screen-permissions', - }); - expect(views.get('server-1_view-1').sendToRenderer).toHaveBeenCalledWith('calls-error', { - err: 'screen-permissions', - }); + expect(callsWidgetWindow.win.webContents.send).toHaveBeenCalledWith('calls-error', 'screen-permissions', 'callID'); + expect(views.get('server-1_view-1').sendToRenderer).toHaveBeenCalledWith('calls-error', 'screen-permissions', 'callID'); expect(callsWidgetWindow.win.webContents.send).toHaveBeenCalledTimes(1); }); @@ -649,12 +646,8 @@ describe('main/windows/callsWidgetWindow', () => { await callsWidgetWindow.handleGetDesktopSources({sender: {id: 1}}, null); expect(systemPreferences.getMediaAccessStatus).toHaveBeenCalledWith('screen'); - expect(callsWidgetWindow.win.webContents.send).toHaveBeenCalledWith('calls-error', { - err: 'screen-permissions', - }); - expect(views.get('server-1_view-1').sendToRenderer).toHaveBeenCalledWith('calls-error', { - err: 'screen-permissions', - }); + expect(callsWidgetWindow.win.webContents.send).toHaveBeenCalledWith('calls-error', 'screen-permissions', 'callID'); + expect(views.get('server-1_view-1').sendToRenderer).toHaveBeenCalledWith('calls-error', 'screen-permissions', 'callID'); expect(views.get('server-1_view-1').sendToRenderer).toHaveBeenCalledTimes(1); expect(callsWidgetWindow.win.webContents.send).toHaveBeenCalledTimes(1); }); @@ -680,12 +673,8 @@ describe('main/windows/callsWidgetWindow', () => { expect(callsWidgetWindow.missingScreensharePermissions).toBe(true); expect(resetScreensharePermissionsMacOS).toHaveBeenCalledTimes(1); expect(openScreensharePermissionsSettingsMacOS).toHaveBeenCalledTimes(0); - expect(callsWidgetWindow.win.webContents.send).toHaveBeenCalledWith('calls-error', { - err: 'screen-permissions', - }); - expect(views.get('server-1_view-1').sendToRenderer).toHaveBeenCalledWith('calls-error', { - err: 'screen-permissions', - }); + expect(callsWidgetWindow.win.webContents.send).toHaveBeenCalledWith('calls-error', 'screen-permissions', 'callID'); + expect(views.get('server-1_view-1').sendToRenderer).toHaveBeenCalledWith('calls-error', 'screen-permissions', 'callID'); await callsWidgetWindow.handleGetDesktopSources({sender: {id: 1}}, null); @@ -799,6 +788,49 @@ describe('main/windows/callsWidgetWindow', () => { }); }); + describe('handleCallsLinkClick', () => { + const view = { + view: { + server: { + id: 'server-1', + }, + }, + sendToRenderer: jest.fn(), + }; + const callsWidgetWindow = new CallsWidgetWindow(); + callsWidgetWindow.mainView = view; + callsWidgetWindow.win = {webContents: {id: 1}}; + + const focus = jest.fn(); + + beforeEach(() => { + urlUtils.parseURL.mockImplementation((url) => { + try { + return new URL(url); + } catch (e) { + return undefined; + } + }); + MainWindow.get.mockReturnValue({focus}); + ViewManager.getView.mockReturnValue(view); + ViewManager.handleDeepLink = jest.fn(); + }); + + it('should switch server, focus and send history push event', () => { + const url = '/team/channel'; + callsWidgetWindow.handleCallsLinkClick({sender: {id: 1}}, url); + expect(ServerViewState.switchServer).toHaveBeenCalledWith('server-1'); + expect(focus).toHaveBeenCalled(); + expect(view.sendToRenderer).toBeCalledWith(BROWSER_HISTORY_PUSH, url); + }); + + it('should call ViewManager.handleDeepLink for parseable urls', () => { + const url = 'http://localhost:8065/team/channel'; + callsWidgetWindow.handleCallsLinkClick({sender: {id: 1}}, url); + expect(ViewManager.handleDeepLink).toHaveBeenCalledWith(new URL(url)); + }); + }); + describe('isOpen', () => { const callsWidgetWindow = new CallsWidgetWindow(); diff --git a/src/main/windows/callsWidgetWindow.ts b/src/main/windows/callsWidgetWindow.ts index 84e939cd..9e80963e 100644 --- a/src/main/windows/callsWidgetWindow.ts +++ b/src/main/windows/callsWidgetWindow.ts @@ -69,7 +69,7 @@ export class CallsWidgetWindow { // forwards to the main app ipcMain.on(DESKTOP_SOURCES_MODAL_REQUEST, this.forwardToMainApp(DESKTOP_SOURCES_MODAL_REQUEST)); ipcMain.on(CALLS_ERROR, this.forwardToMainApp(CALLS_ERROR)); - ipcMain.on(CALLS_LINK_CLICK, this.forwardToMainApp(CALLS_LINK_CLICK)); + ipcMain.on(CALLS_LINK_CLICK, this.handleCallsLinkClick); ipcMain.on(CALLS_JOIN_REQUEST, this.forwardToMainApp(CALLS_JOIN_REQUEST)); // deprecated in favour of CALLS_LINK_CLICK @@ -394,7 +394,7 @@ export class CallsWidgetWindow { } } - const screenPermissionsErrMsg = {err: 'screen-permissions'}; + const screenPermissionsErrArgs = ['screen-permissions', this.callID]; return desktopCapturer.getSources(opts).then((sources) => { let hasScreenPermissions = true; @@ -409,8 +409,8 @@ export class CallsWidgetWindow { if (!hasScreenPermissions || !sources.length) { log.info('missing screen permissions'); - view.sendToRenderer(CALLS_ERROR, screenPermissionsErrMsg); - this.win?.webContents.send(CALLS_ERROR, screenPermissionsErrMsg); + view.sendToRenderer(CALLS_ERROR, ...screenPermissionsErrArgs); + this.win?.webContents.send(CALLS_ERROR, ...screenPermissionsErrArgs); return []; } @@ -426,8 +426,8 @@ export class CallsWidgetWindow { }).catch((err) => { log.error('desktopCapturer.getSources failed', err); - view.sendToRenderer(CALLS_ERROR, screenPermissionsErrMsg); - this.win?.webContents.send(CALLS_ERROR, screenPermissionsErrMsg); + view.sendToRenderer(CALLS_ERROR, ...screenPermissionsErrArgs); + this.win?.webContents.send(CALLS_ERROR, ...screenPermissionsErrArgs); return []; }); @@ -505,6 +505,31 @@ export class CallsWidgetWindow { }; } + private handleCallsLinkClick = (event: IpcMainEvent, url: string) => { + log.debug('handleCallsLinkClick', url); + + if (!this.isCallsWidget(event.sender.id)) { + return; + } + + if (!this.serverID) { + return; + } + + const parsedURL = parseURL(url); + if (parsedURL) { + ViewManager.handleDeepLink(parsedURL); + return; + } + + // If parsing above fails it means it's a relative path (e.g. + // pointing to a channel). + + ServerViewState.switchServer(this.serverID); + MainWindow.get()?.focus(); + this.mainView?.sendToRenderer(BROWSER_HISTORY_PUSH, url); + } + /** * @deprecated */ diff --git a/src/types/externalAPI.ts b/src/types/externalAPI.ts index 949c59df..4c33863a 100644 --- a/src/types/externalAPI.ts +++ b/src/types/externalAPI.ts @@ -20,5 +20,6 @@ export interface ExternalAPI { createListener(event: 'calls-widget-share-screen', listener: (sourceID: string, withAudio: boolean) => void): () => void; createListener(event: 'calls-join-request', listener: (callID: string) => void): () => void; createListener(event: 'calls-error', listener: (err: string, callID?: string, errMsg?: string) => void): () => void; + createListener(event: 'calls-link-click', listener: (url: string) => void): () => void; createListener(event: 'desktop-sources-modal-request', listener: () => void): () => void; }