[MM-55503] Update Calls Desktop API (#2937)

* Update Calls Desktop API

* Update tests

* Handle Calls links internally

---------

Co-authored-by: Devin Binnie <52460000+devinbinnie@users.noreply.github.com>
This commit is contained in:
Claudio Costa 2024-01-24 09:54:19 -06:00 committed by GitHub
parent 37829f11d2
commit 1613eb17bc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 146 additions and 72 deletions

View file

@ -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<DesktopCaptureSource[]>;
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<DesktopCaptureSource[]>;
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;
}

View file

@ -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<DesktopCaptureSource[]>;
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<DesktopCaptureSource[]>;
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;
};

View file

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

View file

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

View file

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

View file

@ -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
*/

View file

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