[MM-51961] Migrate calls widget to singleton (#2667)
* Migrate callsWidgetWindow to singleton * REVERT ME: removed references to ServerManager
This commit is contained in:
@@ -160,6 +160,9 @@ jest.mock('main/UserActivityMonitor', () => ({
|
|||||||
on: jest.fn(),
|
on: jest.fn(),
|
||||||
startMonitoring: jest.fn(),
|
startMonitoring: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
jest.mock('main/windows/callsWidgetWindow', () => ({
|
||||||
|
isCallsWidget: jest.fn(),
|
||||||
|
}));
|
||||||
jest.mock('main/windows/windowManager', () => ({
|
jest.mock('main/windows/windowManager', () => ({
|
||||||
showMainWindow: jest.fn(),
|
showMainWindow: jest.fn(),
|
||||||
sendToRenderer: jest.fn(),
|
sendToRenderer: jest.fn(),
|
||||||
|
@@ -62,6 +62,7 @@ import TrustedOriginsStore from 'main/trustedOrigins';
|
|||||||
import {refreshTrayImages, setupTray} from 'main/tray/tray';
|
import {refreshTrayImages, setupTray} from 'main/tray/tray';
|
||||||
import UserActivityMonitor from 'main/UserActivityMonitor';
|
import UserActivityMonitor from 'main/UserActivityMonitor';
|
||||||
import ViewManager from 'main/views/viewManager';
|
import ViewManager from 'main/views/viewManager';
|
||||||
|
import CallsWidgetWindow from 'main/windows/callsWidgetWindow';
|
||||||
import WindowManager from 'main/windows/windowManager';
|
import WindowManager from 'main/windows/windowManager';
|
||||||
import MainWindow from 'main/windows/mainWindow';
|
import MainWindow from 'main/windows/mainWindow';
|
||||||
|
|
||||||
@@ -435,16 +436,9 @@ function initializeAfterAppReady() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const callsWidgetWindow = WindowManager.callsWidgetWindow;
|
if (CallsWidgetWindow.isCallsWidget(webContents.id)) {
|
||||||
if (callsWidgetWindow) {
|
callback(true);
|
||||||
if (webContents.id === callsWidgetWindow.win.webContents.id) {
|
return;
|
||||||
callback(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (callsWidgetWindow.popOut && webContents.id === callsWidgetWindow.popOut.webContents.id) {
|
|
||||||
callback(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const requestingURL = webContents.getURL();
|
const requestingURL = webContents.getURL();
|
||||||
|
@@ -25,6 +25,7 @@ jest.mock('electron', () => ({
|
|||||||
jest.mock('main/contextMenu', () => jest.fn());
|
jest.mock('main/contextMenu', () => jest.fn());
|
||||||
|
|
||||||
jest.mock('../allowProtocolDialog', () => ({}));
|
jest.mock('../allowProtocolDialog', () => ({}));
|
||||||
|
jest.mock('main/windows/callsWidgetWindow', () => ({}));
|
||||||
jest.mock('main/views/viewManager', () => ({
|
jest.mock('main/views/viewManager', () => ({
|
||||||
getViewByWebContentsId: jest.fn(),
|
getViewByWebContentsId: jest.fn(),
|
||||||
getViewByURL: jest.fn(),
|
getViewByURL: jest.fn(),
|
||||||
|
@@ -10,7 +10,8 @@ import urlUtils from 'common/utils/url';
|
|||||||
import {flushCookiesStore} from 'main/app/utils';
|
import {flushCookiesStore} from 'main/app/utils';
|
||||||
import ContextMenu from 'main/contextMenu';
|
import ContextMenu from 'main/contextMenu';
|
||||||
|
|
||||||
import WindowManager from '../windows/windowManager';
|
import CallsWidgetWindow from 'main/windows/callsWidgetWindow';
|
||||||
|
import WindowManager from 'main/windows/windowManager';
|
||||||
|
|
||||||
import {protocols} from '../../../electron-builder.json';
|
import {protocols} from '../../../electron-builder.json';
|
||||||
|
|
||||||
@@ -84,7 +85,7 @@ export class WebContentsEventManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const callID = WindowManager.callsWidgetWindow?.getCallID();
|
const callID = CallsWidgetWindow.callID;
|
||||||
if (serverURL && callID && urlUtils.isCallsPopOutURL(serverURL, parsedURL, callID)) {
|
if (serverURL && callID && urlUtils.isCallsPopOutURL(serverURL, parsedURL, callID)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
File diff suppressed because it is too large
Load Diff
@@ -1,11 +1,14 @@
|
|||||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
import {EventEmitter} from 'events';
|
import {BrowserWindow, desktopCapturer, ipcMain, IpcMainEvent, Rectangle, systemPreferences} from 'electron';
|
||||||
import {BrowserWindow, ipcMain, IpcMainEvent, Rectangle} from 'electron';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
CallsErrorMessage,
|
||||||
|
CallsEventHandler,
|
||||||
|
CallsJoinCallMessage,
|
||||||
CallsJoinedCallMessage,
|
CallsJoinedCallMessage,
|
||||||
|
CallsLinkClickMessage,
|
||||||
CallsWidgetResizeMessage,
|
CallsWidgetResizeMessage,
|
||||||
CallsWidgetShareScreenMessage,
|
CallsWidgetShareScreenMessage,
|
||||||
CallsWidgetWindowConfig,
|
CallsWidgetWindowConfig,
|
||||||
@@ -13,32 +16,41 @@ import {
|
|||||||
|
|
||||||
import {MattermostView} from 'main/views/MattermostView';
|
import {MattermostView} from 'main/views/MattermostView';
|
||||||
|
|
||||||
import {getLocalPreload} from 'main/utils';
|
import {getLocalPreload, openScreensharePermissionsSettingsMacOS, resetScreensharePermissionsMacOS} from 'main/utils';
|
||||||
|
|
||||||
import {Logger} from 'common/log';
|
import {Logger} from 'common/log';
|
||||||
import {CALLS_PLUGIN_ID, MINIMUM_CALLS_WIDGET_HEIGHT, MINIMUM_CALLS_WIDGET_WIDTH} from 'common/utils/constants';
|
import {CALLS_PLUGIN_ID, MINIMUM_CALLS_WIDGET_HEIGHT, MINIMUM_CALLS_WIDGET_WIDTH} from 'common/utils/constants';
|
||||||
import Utils from 'common/utils/util';
|
import Utils from 'common/utils/util';
|
||||||
import urlUtils, {getFormattedPathName} from 'common/utils/url';
|
import urlUtils, {getFormattedPathName} from 'common/utils/url';
|
||||||
import {
|
import {
|
||||||
|
BROWSER_HISTORY_PUSH,
|
||||||
|
CALLS_ERROR,
|
||||||
|
CALLS_JOIN_CALL,
|
||||||
CALLS_JOINED_CALL,
|
CALLS_JOINED_CALL,
|
||||||
|
CALLS_LEAVE_CALL,
|
||||||
|
CALLS_LINK_CLICK,
|
||||||
CALLS_POPOUT_FOCUS,
|
CALLS_POPOUT_FOCUS,
|
||||||
|
CALLS_WIDGET_CHANNEL_LINK_CLICK,
|
||||||
CALLS_WIDGET_RESIZE,
|
CALLS_WIDGET_RESIZE,
|
||||||
CALLS_WIDGET_SHARE_SCREEN,
|
CALLS_WIDGET_SHARE_SCREEN,
|
||||||
|
DESKTOP_SOURCES_MODAL_REQUEST,
|
||||||
|
DESKTOP_SOURCES_RESULT,
|
||||||
|
DISPATCH_GET_DESKTOP_SOURCES,
|
||||||
} from 'common/communication';
|
} from 'common/communication';
|
||||||
import webContentsEventManager from 'main/views/webContentEvents';
|
import webContentsEventManager from 'main/views/webContentEvents';
|
||||||
|
import MainWindow from 'main/windows/mainWindow';
|
||||||
type LoadURLOpts = {
|
import WindowManager from 'main/windows/windowManager';
|
||||||
extraHeaders: string;
|
import ViewManager from 'main/views/viewManager';
|
||||||
}
|
|
||||||
|
|
||||||
const log = new Logger('CallsWidgetWindow');
|
const log = new Logger('CallsWidgetWindow');
|
||||||
|
|
||||||
export default class CallsWidgetWindow extends EventEmitter {
|
export class CallsWidgetWindow {
|
||||||
public win: BrowserWindow;
|
private win?: BrowserWindow;
|
||||||
private main: BrowserWindow;
|
private mainView?: MattermostView;
|
||||||
public popOut: BrowserWindow | null = null;
|
private options?: CallsWidgetWindowConfig;
|
||||||
private mainView: MattermostView;
|
private missingScreensharePermissions?: boolean;
|
||||||
private config: CallsWidgetWindowConfig;
|
|
||||||
|
private popOut?: BrowserWindow;
|
||||||
private boundsErr: Rectangle = {
|
private boundsErr: Rectangle = {
|
||||||
x: 0,
|
x: 0,
|
||||||
y: 0,
|
y: 0,
|
||||||
@@ -46,12 +58,67 @@ export default class CallsWidgetWindow extends EventEmitter {
|
|||||||
height: 0,
|
height: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(mainWindow: BrowserWindow, mainView: MattermostView, config: CallsWidgetWindowConfig) {
|
constructor() {
|
||||||
super();
|
ipcMain.on(CALLS_WIDGET_RESIZE, this.handleResize);
|
||||||
|
ipcMain.on(CALLS_WIDGET_SHARE_SCREEN, this.handleShareScreen);
|
||||||
|
ipcMain.on(CALLS_JOINED_CALL, this.handleJoinedCall);
|
||||||
|
ipcMain.on(CALLS_POPOUT_FOCUS, this.handlePopOutFocus);
|
||||||
|
ipcMain.on(DISPATCH_GET_DESKTOP_SOURCES, this.genCallsEventHandler(this.handleGetDesktopSources));
|
||||||
|
ipcMain.on(DESKTOP_SOURCES_MODAL_REQUEST, this.genCallsEventHandler(this.handleDesktopSourcesModalRequest));
|
||||||
|
ipcMain.on(CALLS_JOIN_CALL, this.genCallsEventHandler(this.handleCreateCallsWidgetWindow));
|
||||||
|
ipcMain.on(CALLS_LEAVE_CALL, this.genCallsEventHandler(this.handleCallsLeave));
|
||||||
|
ipcMain.on(CALLS_WIDGET_CHANNEL_LINK_CLICK, this.genCallsEventHandler(this.handleCallsWidgetChannelLinkClick));
|
||||||
|
ipcMain.on(CALLS_ERROR, this.genCallsEventHandler(this.handleCallsError));
|
||||||
|
ipcMain.on(CALLS_LINK_CLICK, this.genCallsEventHandler(this.handleCallsLinkClick));
|
||||||
|
}
|
||||||
|
|
||||||
this.config = config;
|
/**
|
||||||
this.main = mainWindow;
|
* Getters
|
||||||
this.mainView = mainView;
|
*/
|
||||||
|
|
||||||
|
get callID() {
|
||||||
|
return this.options?.callID;
|
||||||
|
}
|
||||||
|
|
||||||
|
private get serverName() {
|
||||||
|
return this.mainView?.tab.server.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper functions
|
||||||
|
*/
|
||||||
|
|
||||||
|
getURL = () => {
|
||||||
|
return this.win && urlUtils.parseURL(this.win?.webContents.getURL());
|
||||||
|
}
|
||||||
|
|
||||||
|
isCallsWidget = (webContentsId: number) => {
|
||||||
|
return webContentsId === this.win?.webContents.id || webContentsId === this.popOut?.webContents.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getWidgetURL = () => {
|
||||||
|
if (!this.mainView) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const u = urlUtils.parseURL(this.mainView.tab.server.url.toString()) as URL;
|
||||||
|
|
||||||
|
u.pathname = getFormattedPathName(u.pathname);
|
||||||
|
u.pathname += `plugins/${CALLS_PLUGIN_ID}/standalone/widget.html`;
|
||||||
|
|
||||||
|
if (this.options?.callID) {
|
||||||
|
u.searchParams.append('call_id', this.options.callID);
|
||||||
|
}
|
||||||
|
if (this.options?.title) {
|
||||||
|
u.searchParams.append('title', this.options.title);
|
||||||
|
}
|
||||||
|
if (this.options?.rootID) {
|
||||||
|
u.searchParams.append('root_id', this.options.rootID);
|
||||||
|
}
|
||||||
|
|
||||||
|
return u.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private init = (view: MattermostView, options: CallsWidgetWindowConfig) => {
|
||||||
this.win = new BrowserWindow({
|
this.win = new BrowserWindow({
|
||||||
width: MINIMUM_CALLS_WIDGET_WIDTH,
|
width: MINIMUM_CALLS_WIDGET_WIDTH,
|
||||||
height: MINIMUM_CALLS_WIDGET_HEIGHT,
|
height: MINIMUM_CALLS_WIDGET_HEIGHT,
|
||||||
@@ -67,15 +134,12 @@ export default class CallsWidgetWindow extends EventEmitter {
|
|||||||
preload: getLocalPreload('callsWidget.js'),
|
preload: getLocalPreload('callsWidget.js'),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
this.mainView = view;
|
||||||
|
this.options = options;
|
||||||
|
|
||||||
this.win.once('ready-to-show', () => this.win.show());
|
this.win.once('ready-to-show', () => this.win?.show());
|
||||||
this.win.once('show', this.onShow);
|
this.win.once('show', this.onShow);
|
||||||
|
|
||||||
this.win.on('closed', this.onClosed);
|
this.win.on('closed', this.onClosed);
|
||||||
ipcMain.on(CALLS_WIDGET_RESIZE, this.onResize);
|
|
||||||
ipcMain.on(CALLS_WIDGET_SHARE_SCREEN, this.onShareScreen);
|
|
||||||
ipcMain.on(CALLS_JOINED_CALL, this.onJoinedCall);
|
|
||||||
ipcMain.on(CALLS_POPOUT_FOCUS, this.onPopOutFocus);
|
|
||||||
|
|
||||||
this.win.webContents.setWindowOpenHandler(this.onPopOutOpen);
|
this.win.webContents.setWindowOpenHandler(this.onPopOutOpen);
|
||||||
this.win.webContents.on('did-create-window', this.onPopOutCreate);
|
this.win.webContents.on('did-create-window', this.onPopOutCreate);
|
||||||
@@ -84,31 +148,80 @@ export default class CallsWidgetWindow extends EventEmitter {
|
|||||||
this.win.webContents.on('will-navigate', this.onNavigate);
|
this.win.webContents.on('will-navigate', this.onNavigate);
|
||||||
this.win.webContents.on('did-start-navigation', this.onNavigate);
|
this.win.webContents.on('did-start-navigation', this.onNavigate);
|
||||||
|
|
||||||
this.load();
|
const widgetURL = this.getWidgetURL();
|
||||||
}
|
if (!widgetURL) {
|
||||||
|
return;
|
||||||
public async close() {
|
}
|
||||||
log.debug('close');
|
this.win?.loadURL(widgetURL).catch((reason) => {
|
||||||
return new Promise<void>((resolve) => {
|
log.error(`failed to load: ${reason}`);
|
||||||
if (this.win.isDestroyed()) {
|
|
||||||
resolve();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.once('closed', resolve);
|
|
||||||
this.win.close();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public getServerName() {
|
private close = async () => {
|
||||||
return this.mainView.serverInfo.server.name;
|
log.debug('close');
|
||||||
|
if (!this.win) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
if (this.win.isDestroyed()) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise<void>((resolve) => {
|
||||||
|
if (!this.win) {
|
||||||
|
resolve();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.win?.on('closed', resolve);
|
||||||
|
this.win?.close();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public getChannelURL() {
|
private setBounds(bounds: Rectangle) {
|
||||||
return this.config.channelURL;
|
if (!this.win) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: this hack is needed to fix positioning on certain systems where
|
||||||
|
// BrowserWindow.setBounds() is not consistent.
|
||||||
|
bounds.x += this.boundsErr.x;
|
||||||
|
bounds.y += this.boundsErr.y;
|
||||||
|
bounds.height += this.boundsErr.height;
|
||||||
|
bounds.width += this.boundsErr.width;
|
||||||
|
|
||||||
|
this.win.setBounds(bounds);
|
||||||
|
this.boundsErr = Utils.boundsDiff(bounds, this.win.getBounds());
|
||||||
}
|
}
|
||||||
|
|
||||||
public getCallID() {
|
private isAllowedEvent = (event: IpcMainEvent) => {
|
||||||
return this.config.callID;
|
// Allow events when a call isn't in progress
|
||||||
|
if (!(this.win && this.mainView)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only allow events coming from either the widget window or the
|
||||||
|
// original Mattermost view that initiated it.
|
||||||
|
return event.sender.id === this.win?.webContents.id ||
|
||||||
|
event.sender.id === this.mainView?.webContentsId;
|
||||||
|
}
|
||||||
|
|
||||||
|
private genCallsEventHandler = (handler: CallsEventHandler) => {
|
||||||
|
return (event: IpcMainEvent, viewId: string, msg?: any) => {
|
||||||
|
if (!this.isAllowedEvent(event)) {
|
||||||
|
log.warn('genCallsEventHandler', 'Disallowed calls event');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
handler(viewId, msg);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* BrowserWindow/WebContents handlers
|
||||||
|
*/
|
||||||
|
|
||||||
|
private onClosed = () => {
|
||||||
|
delete this.win;
|
||||||
|
delete this.mainView;
|
||||||
|
delete this.options;
|
||||||
}
|
}
|
||||||
|
|
||||||
private onNavigate = (ev: Event, url: string) => {
|
private onNavigate = (ev: Event, url: string) => {
|
||||||
@@ -119,41 +232,81 @@ export default class CallsWidgetWindow extends EventEmitter {
|
|||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
private load() {
|
private onShow = () => {
|
||||||
const opts = {} as LoadURLOpts;
|
log.debug('onShow');
|
||||||
this.win.loadURL(this.getWidgetURL(), opts).catch((reason) => {
|
const mainWindow = MainWindow.get();
|
||||||
log.error(`Calls widget window failed to load: ${reason}`);
|
if (!(this.win && mainWindow)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.win.focus();
|
||||||
|
this.win.setVisibleOnAllWorkspaces(true, {visibleOnFullScreen: true, skipTransformProcessType: true});
|
||||||
|
this.win.setAlwaysOnTop(true, 'screen-saver');
|
||||||
|
|
||||||
|
const bounds = this.win.getBounds();
|
||||||
|
const mainBounds = mainWindow.getBounds();
|
||||||
|
const initialBounds = {
|
||||||
|
x: mainBounds.x + 12,
|
||||||
|
y: (mainBounds.y + mainBounds.height) - bounds.height - 12,
|
||||||
|
width: MINIMUM_CALLS_WIDGET_WIDTH,
|
||||||
|
height: MINIMUM_CALLS_WIDGET_HEIGHT,
|
||||||
|
};
|
||||||
|
this.win.setMenuBarVisibility(false);
|
||||||
|
|
||||||
|
if (process.env.MM_DEBUG_CALLS_WIDGET) {
|
||||||
|
this.win.webContents.openDevTools({mode: 'detach'});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setBounds(initialBounds);
|
||||||
|
}
|
||||||
|
|
||||||
|
private onPopOutOpen = ({url}: {url: string}) => {
|
||||||
|
if (!(this.mainView && this.options)) {
|
||||||
|
return {action: 'deny' as const};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (urlUtils.isCallsPopOutURL(this.mainView?.tab.server.url, url, this.options?.callID)) {
|
||||||
|
return {
|
||||||
|
action: 'allow' as const,
|
||||||
|
overrideBrowserWindowOptions: {
|
||||||
|
autoHideMenuBar: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
log.warn(`onPopOutOpen: prevented window open to ${url}`);
|
||||||
|
return {action: 'deny' as const};
|
||||||
|
}
|
||||||
|
|
||||||
|
private onPopOutCreate = (win: BrowserWindow) => {
|
||||||
|
this.popOut = win;
|
||||||
|
|
||||||
|
// Let the webContentsEventManager handle links that try to open a new window.
|
||||||
|
webContentsEventManager.addWebContentsEventListeners(this.popOut.webContents);
|
||||||
|
|
||||||
|
// Need to capture and handle redirects for security.
|
||||||
|
this.popOut.webContents.on('will-redirect', (event: Event) => {
|
||||||
|
// There's no reason we would allow a redirect from the call's popout. Eventually we may, so revise then.
|
||||||
|
// Note for the future: the code from https://github.com/mattermost/desktop/pull/2580 will not work for us.
|
||||||
|
event.preventDefault();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.popOut.on('closed', () => {
|
||||||
|
delete this.popOut;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private onClosed = () => {
|
/************************
|
||||||
log.debug('onClosed');
|
* IPC HANDLERS
|
||||||
this.emit('closed');
|
************************/
|
||||||
this.removeAllListeners('closed');
|
|
||||||
ipcMain.off(CALLS_WIDGET_RESIZE, this.onResize);
|
|
||||||
ipcMain.off(CALLS_WIDGET_SHARE_SCREEN, this.onShareScreen);
|
|
||||||
ipcMain.off(CALLS_JOINED_CALL, this.onJoinedCall);
|
|
||||||
ipcMain.off(CALLS_POPOUT_FOCUS, this.onPopOutFocus);
|
|
||||||
}
|
|
||||||
|
|
||||||
private getWidgetURL() {
|
private handleResize = (ev: IpcMainEvent, _: string, msg: CallsWidgetResizeMessage) => {
|
||||||
const u = urlUtils.parseURL(this.mainView.serverInfo.server.url.toString()) as URL;
|
|
||||||
u.pathname = getFormattedPathName(u.pathname);
|
|
||||||
u.pathname += `plugins/${CALLS_PLUGIN_ID}/standalone/widget.html`;
|
|
||||||
u.searchParams.append('call_id', this.config.callID);
|
|
||||||
if (this.config.title) {
|
|
||||||
u.searchParams.append('title', this.config.title);
|
|
||||||
}
|
|
||||||
if (this.config.rootID) {
|
|
||||||
u.searchParams.append('root_id', this.config.rootID);
|
|
||||||
}
|
|
||||||
|
|
||||||
return u.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
private onResize = (ev: IpcMainEvent, _: string, msg: CallsWidgetResizeMessage) => {
|
|
||||||
log.debug('onResize', msg);
|
log.debug('onResize', msg);
|
||||||
|
|
||||||
|
if (!this.win) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!this.isAllowedEvent(ev)) {
|
if (!this.isAllowedEvent(ev)) {
|
||||||
log.warn('onResize', 'Disallowed calls event');
|
log.warn('onResize', 'Disallowed calls event');
|
||||||
return;
|
return;
|
||||||
@@ -171,103 +324,29 @@ export default class CallsWidgetWindow extends EventEmitter {
|
|||||||
this.setBounds(newBounds);
|
this.setBounds(newBounds);
|
||||||
}
|
}
|
||||||
|
|
||||||
private onShareScreen = (ev: IpcMainEvent, _: string, message: CallsWidgetShareScreenMessage) => {
|
private handleShareScreen = (ev: IpcMainEvent, _: string, message: CallsWidgetShareScreenMessage) => {
|
||||||
log.debug('onShareScreen');
|
log.debug('handleShareScreen');
|
||||||
|
|
||||||
if (!this.isAllowedEvent(ev)) {
|
if (!this.isAllowedEvent(ev)) {
|
||||||
log.warn('Disallowed calls event');
|
log.warn('Disallowed calls event');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.win.webContents.send(CALLS_WIDGET_SHARE_SCREEN, message);
|
this.win?.webContents.send(CALLS_WIDGET_SHARE_SCREEN, message);
|
||||||
}
|
}
|
||||||
|
|
||||||
private onJoinedCall = (ev: IpcMainEvent, _: string, message: CallsJoinedCallMessage) => {
|
private handleJoinedCall = (ev: IpcMainEvent, _: string, message: CallsJoinedCallMessage) => {
|
||||||
log.debug('onJoinedCall');
|
log.debug('handleJoinedCall');
|
||||||
|
|
||||||
if (!this.isAllowedEvent(ev)) {
|
if (!this.isAllowedEvent(ev)) {
|
||||||
log.warn('onJoinedCall', 'Disallowed calls event');
|
log.warn('handleJoinedCall', 'Disallowed calls event');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.mainView.sendToRenderer(CALLS_JOINED_CALL, message);
|
this.mainView?.sendToRenderer(CALLS_JOINED_CALL, message);
|
||||||
}
|
}
|
||||||
|
|
||||||
private setBounds(bounds: Rectangle) {
|
private handlePopOutFocus = () => {
|
||||||
// NOTE: this hack is needed to fix positioning on certain systems where
|
|
||||||
// BrowserWindow.setBounds() is not consistent.
|
|
||||||
bounds.x += this.boundsErr.x;
|
|
||||||
bounds.y += this.boundsErr.y;
|
|
||||||
bounds.height += this.boundsErr.height;
|
|
||||||
bounds.width += this.boundsErr.width;
|
|
||||||
|
|
||||||
this.win.setBounds(bounds);
|
|
||||||
this.boundsErr = Utils.boundsDiff(bounds, this.win.getBounds());
|
|
||||||
}
|
|
||||||
|
|
||||||
private onShow = () => {
|
|
||||||
log.debug('onShow');
|
|
||||||
|
|
||||||
this.win.focus();
|
|
||||||
this.win.setVisibleOnAllWorkspaces(true, {visibleOnFullScreen: true, skipTransformProcessType: true});
|
|
||||||
this.win.setAlwaysOnTop(true, 'screen-saver');
|
|
||||||
|
|
||||||
const bounds = this.win.getBounds();
|
|
||||||
const mainBounds = this.main.getBounds();
|
|
||||||
const initialBounds = {
|
|
||||||
x: mainBounds.x + 12,
|
|
||||||
y: (mainBounds.y + mainBounds.height) - bounds.height - 12,
|
|
||||||
width: MINIMUM_CALLS_WIDGET_WIDTH,
|
|
||||||
height: MINIMUM_CALLS_WIDGET_HEIGHT,
|
|
||||||
};
|
|
||||||
this.win.setMenuBarVisibility(false);
|
|
||||||
|
|
||||||
if (process.env.MM_DEBUG_CALLS_WIDGET) {
|
|
||||||
this.win.webContents.openDevTools({mode: 'detach'});
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setBounds(initialBounds);
|
|
||||||
}
|
|
||||||
|
|
||||||
private onPopOutOpen = ({url}: { url: string }) => {
|
|
||||||
if (urlUtils.isCallsPopOutURL(this.mainView.serverInfo.server.url, url, this.config.callID)) {
|
|
||||||
return {
|
|
||||||
action: 'allow' as const,
|
|
||||||
overrideBrowserWindowOptions: {
|
|
||||||
autoHideMenuBar: true,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
log.warn(`onPopOutOpen: prevented window open to ${url}`);
|
|
||||||
return {action: 'deny' as const};
|
|
||||||
}
|
|
||||||
|
|
||||||
private onPopOutCreate = (win: BrowserWindow) => {
|
|
||||||
this.popOut = win;
|
|
||||||
this.popOut.on('closed', this.onPopOutClosed);
|
|
||||||
|
|
||||||
// Let the webContentsEventManager handle links that try to open a new window.
|
|
||||||
webContentsEventManager.addWebContentsEventListeners(this.popOut.webContents);
|
|
||||||
|
|
||||||
// Need to capture and handle redirects for security.
|
|
||||||
this.popOut.webContents.on('will-redirect', this.onWillRedirect);
|
|
||||||
}
|
|
||||||
|
|
||||||
private onPopOutClosed = () => {
|
|
||||||
log.debug('onPopOutClosed');
|
|
||||||
this.popOut?.removeAllListeners('closed');
|
|
||||||
this.popOut = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
private onWillRedirect = (event: Event, url: string) => {
|
|
||||||
// There's no reason we would allow a redirect from the call's popout. Eventually we may, so revise then.
|
|
||||||
// Note for the future: the code from https://github.com/mattermost/desktop/pull/2580 will not work for us.
|
|
||||||
event.preventDefault();
|
|
||||||
}
|
|
||||||
|
|
||||||
private onPopOutFocus = () => {
|
|
||||||
if (!this.popOut) {
|
if (!this.popOut) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -277,27 +356,151 @@ export default class CallsWidgetWindow extends EventEmitter {
|
|||||||
this.popOut.focus();
|
this.popOut.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
public getWebContentsId() {
|
private handleGetDesktopSources = async (viewId: string, opts: Electron.SourcesOptions) => {
|
||||||
return this.win.webContents.id;
|
log.debug('handleGetDesktopSources', opts);
|
||||||
|
|
||||||
|
const view = ViewManager.getView(viewId);
|
||||||
|
if (!view) {
|
||||||
|
log.error('handleGetDesktopSources: view not found');
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.platform === 'darwin' && systemPreferences.getMediaAccessStatus('screen') === 'denied') {
|
||||||
|
try {
|
||||||
|
// If permissions are missing we reset them so that the system
|
||||||
|
// prompt can be showed.
|
||||||
|
await resetScreensharePermissionsMacOS();
|
||||||
|
|
||||||
|
// We only open the system settings if permissions were already missing since
|
||||||
|
// on the first attempt to get the sources the OS will correctly show a prompt.
|
||||||
|
if (this.missingScreensharePermissions) {
|
||||||
|
await openScreensharePermissionsSettingsMacOS();
|
||||||
|
}
|
||||||
|
this.missingScreensharePermissions = true;
|
||||||
|
} catch (err) {
|
||||||
|
log.error('failed to reset screen sharing permissions', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const screenPermissionsErrMsg = {err: 'screen-permissions'};
|
||||||
|
|
||||||
|
return desktopCapturer.getSources(opts).then((sources) => {
|
||||||
|
let hasScreenPermissions = true;
|
||||||
|
if (systemPreferences.getMediaAccessStatus) {
|
||||||
|
const screenPermissions = systemPreferences.getMediaAccessStatus('screen');
|
||||||
|
log.debug('screenPermissions', screenPermissions);
|
||||||
|
if (screenPermissions === 'denied') {
|
||||||
|
log.info('no screen sharing permissions');
|
||||||
|
hasScreenPermissions = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasScreenPermissions || !sources.length) {
|
||||||
|
log.info('missing screen permissions');
|
||||||
|
view.sendToRenderer(CALLS_ERROR, screenPermissionsErrMsg);
|
||||||
|
this.win?.webContents.send(CALLS_ERROR, screenPermissionsErrMsg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const message = sources.map((source) => {
|
||||||
|
return {
|
||||||
|
id: source.id,
|
||||||
|
name: source.name,
|
||||||
|
thumbnailURL: source.thumbnail.toDataURL(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
if (message.length > 0) {
|
||||||
|
view.sendToRenderer(DESKTOP_SOURCES_RESULT, message);
|
||||||
|
}
|
||||||
|
}).catch((err) => {
|
||||||
|
log.error('desktopCapturer.getSources failed', err);
|
||||||
|
|
||||||
|
view.sendToRenderer(CALLS_ERROR, screenPermissionsErrMsg);
|
||||||
|
this.win?.webContents.send(CALLS_ERROR, screenPermissionsErrMsg);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public getPopOutWebContentsId() {
|
private handleCreateCallsWidgetWindow = async (viewId: string, msg: CallsJoinCallMessage) => {
|
||||||
return this.popOut?.webContents?.id;
|
log.debug('createCallsWidgetWindow');
|
||||||
|
|
||||||
|
// trying to join again the call we are already in should not be allowed.
|
||||||
|
if (this.options?.callID === msg.callID) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// to switch from one call to another we need to wait for the existing
|
||||||
|
// window to be fully closed.
|
||||||
|
await this.close();
|
||||||
|
|
||||||
|
const currentView = ViewManager.getView(viewId);
|
||||||
|
if (!currentView) {
|
||||||
|
log.error('unable to create calls widget window: currentView is missing');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.init(currentView, {
|
||||||
|
callID: msg.callID,
|
||||||
|
title: msg.title,
|
||||||
|
rootID: msg.rootID,
|
||||||
|
channelURL: msg.channelURL,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public getURL() {
|
private handleDesktopSourcesModalRequest = () => {
|
||||||
return urlUtils.parseURL(this.win.webContents.getURL());
|
log.debug('handleDesktopSourcesModalRequest');
|
||||||
|
|
||||||
|
if (!this.serverName) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
WindowManager.switchServer(this.serverName);
|
||||||
|
MainWindow.get()?.focus();
|
||||||
|
this.mainView?.sendToRenderer(DESKTOP_SOURCES_MODAL_REQUEST);
|
||||||
}
|
}
|
||||||
|
|
||||||
public getMainView() {
|
private handleCallsLeave = () => {
|
||||||
return this.mainView;
|
log.debug('handleCallsLeave');
|
||||||
|
|
||||||
|
this.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
public isAllowedEvent(event: IpcMainEvent) {
|
private handleCallsWidgetChannelLinkClick = () => {
|
||||||
// Only allow events coming from either the widget window or the
|
log.debug('handleCallsWidgetChannelLinkClick');
|
||||||
// original Mattermost view that initiated it.
|
|
||||||
return event.sender.id === this.getWebContentsId() ||
|
if (!this.serverName) {
|
||||||
event.sender.id === this.getMainView().webContentsId;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
WindowManager.switchServer(this.serverName);
|
||||||
|
MainWindow.get()?.focus();
|
||||||
|
this.mainView?.sendToRenderer(BROWSER_HISTORY_PUSH, this.options?.channelURL);
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleCallsError = (_: string, msg: CallsErrorMessage) => {
|
||||||
|
log.debug('handleCallsError', msg);
|
||||||
|
|
||||||
|
if (!this.serverName) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
WindowManager.switchServer(this.serverName);
|
||||||
|
MainWindow.get()?.focus();
|
||||||
|
this.mainView?.sendToRenderer(CALLS_ERROR, msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleCallsLinkClick = (_: string, msg: CallsLinkClickMessage) => {
|
||||||
|
log.debug('handleCallsLinkClick with linkURL', msg.link);
|
||||||
|
|
||||||
|
if (!this.serverName) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
WindowManager.switchServer(this.serverName);
|
||||||
|
MainWindow.get()?.focus();
|
||||||
|
this.mainView?.sendToRenderer(BROWSER_HISTORY_PUSH, msg.link);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const callsWidgetWindow = new CallsWidgetWindow();
|
||||||
|
export default callsWidgetWindow;
|
||||||
|
@@ -4,16 +4,12 @@
|
|||||||
/* eslint-disable max-lines */
|
/* eslint-disable max-lines */
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
import {systemPreferences, desktopCapturer} from 'electron';
|
import {systemPreferences} from 'electron';
|
||||||
|
|
||||||
import Config from 'common/config';
|
import Config from 'common/config';
|
||||||
import {getTabViewName} from 'common/tabs/TabView';
|
import {getTabViewName} from 'common/tabs/TabView';
|
||||||
|
|
||||||
import {
|
import {getAdjustedWindowBoundaries} from 'main/utils';
|
||||||
getAdjustedWindowBoundaries,
|
|
||||||
resetScreensharePermissionsMacOS,
|
|
||||||
openScreensharePermissionsSettingsMacOS,
|
|
||||||
} from 'main/utils';
|
|
||||||
import LoadingScreen from '../views/loadingScreen';
|
import LoadingScreen from '../views/loadingScreen';
|
||||||
|
|
||||||
import ViewManager from 'main/views/viewManager';
|
import ViewManager from 'main/views/viewManager';
|
||||||
@@ -44,10 +40,6 @@ jest.mock('electron', () => ({
|
|||||||
},
|
},
|
||||||
systemPreferences: {
|
systemPreferences: {
|
||||||
getUserDefault: jest.fn(),
|
getUserDefault: jest.fn(),
|
||||||
getMediaAccessStatus: jest.fn(() => 'granted'),
|
|
||||||
},
|
|
||||||
desktopCapturer: {
|
|
||||||
getSources: jest.fn(),
|
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -95,7 +87,10 @@ jest.mock('../downloadsManager', () => ({
|
|||||||
getDownloads: () => {},
|
getDownloads: () => {},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest.mock('./callsWidgetWindow');
|
jest.mock('./callsWidgetWindow', () => ({
|
||||||
|
isCallsWidget: jest.fn(),
|
||||||
|
getURL: jest.fn(),
|
||||||
|
}));
|
||||||
jest.mock('main/views/webContentEvents', () => ({}));
|
jest.mock('main/views/webContentEvents', () => ({}));
|
||||||
|
|
||||||
describe('main/windows/windowManager', () => {
|
describe('main/windows/windowManager', () => {
|
||||||
@@ -640,486 +635,13 @@ describe('main/windows/windowManager', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('createCallsWidgetWindow', () => {
|
|
||||||
const windowManager = new WindowManager();
|
|
||||||
const view = {
|
|
||||||
name: 'server-1_tab-messaging',
|
|
||||||
serverInfo: {
|
|
||||||
server: {
|
|
||||||
url: new URL('http://server-1.com'),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
CallsWidgetWindow.mockImplementation(() => {
|
|
||||||
return {
|
|
||||||
win: {
|
|
||||||
isDestroyed: jest.fn(() => true),
|
|
||||||
},
|
|
||||||
on: jest.fn(),
|
|
||||||
close: jest.fn(),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
ViewManager.getView.mockReturnValue(view);
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
jest.resetAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should create calls widget window', async () => {
|
|
||||||
expect(windowManager.callsWidgetWindow).toBeUndefined();
|
|
||||||
await windowManager.createCallsWidgetWindow('server-1_tab-messaging', {callID: 'test'});
|
|
||||||
expect(windowManager.callsWidgetWindow).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not create a new window if call is the same', async () => {
|
|
||||||
const widgetWindow = windowManager.callsWidgetWindow;
|
|
||||||
expect(widgetWindow).toBeDefined();
|
|
||||||
widgetWindow.getCallID = jest.fn(() => 'test');
|
|
||||||
await windowManager.createCallsWidgetWindow('server-1_tab-messaging', {callID: 'test'});
|
|
||||||
expect(windowManager.callsWidgetWindow).toEqual(widgetWindow);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should create a new window if switching calls', async () => {
|
|
||||||
const widgetWindow = windowManager.callsWidgetWindow;
|
|
||||||
expect(widgetWindow).toBeDefined();
|
|
||||||
widgetWindow.getCallID = jest.fn(() => 'test');
|
|
||||||
await windowManager.createCallsWidgetWindow('server-1_tab-messaging', {callID: 'test2'});
|
|
||||||
expect(windowManager.callsWidgetWindow).not.toEqual(widgetWindow);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('handleGetDesktopSources', () => {
|
|
||||||
const windowManager = new WindowManager();
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
CallsWidgetWindow.mockImplementation(() => {
|
|
||||||
return {
|
|
||||||
isAllowedEvent: jest.fn().mockReturnValue(true),
|
|
||||||
win: {
|
|
||||||
webContents: {
|
|
||||||
send: jest.fn(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
windowManager.callsWidgetWindow = new CallsWidgetWindow();
|
|
||||||
|
|
||||||
Config.teams = [
|
|
||||||
{
|
|
||||||
name: 'server-1',
|
|
||||||
order: 1,
|
|
||||||
tabs: [
|
|
||||||
{
|
|
||||||
name: 'tab-1',
|
|
||||||
order: 0,
|
|
||||||
isOpen: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'tab-2',
|
|
||||||
order: 2,
|
|
||||||
isOpen: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}, {
|
|
||||||
name: 'server-2',
|
|
||||||
order: 0,
|
|
||||||
tabs: [
|
|
||||||
{
|
|
||||||
name: 'tab-1',
|
|
||||||
order: 0,
|
|
||||||
isOpen: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'tab-2',
|
|
||||||
order: 2,
|
|
||||||
isOpen: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
lastActiveTab: 2,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const map = Config.teams.reduce((arr, item) => {
|
|
||||||
item.tabs.forEach((tab) => {
|
|
||||||
arr.push([`${item.name}_${tab.name}`, {
|
|
||||||
sendToRenderer: jest.fn(),
|
|
||||||
}]);
|
|
||||||
});
|
|
||||||
return arr;
|
|
||||||
}, []);
|
|
||||||
const views = new Map(map);
|
|
||||||
ViewManager.getView.mockImplementation((name) => views.get(name));
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
jest.resetAllMocks();
|
|
||||||
Config.teams = [];
|
|
||||||
windowManager.missingScreensharePermissions = undefined;
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should send sources back', async () => {
|
|
||||||
jest.spyOn(desktopCapturer, 'getSources').mockResolvedValue([
|
|
||||||
{
|
|
||||||
id: 'screen0',
|
|
||||||
thumbnail: {
|
|
||||||
toDataURL: jest.fn(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'window0',
|
|
||||||
thumbnail: {
|
|
||||||
toDataURL: jest.fn(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
await windowManager.handleGetDesktopSources('server-1_tab-1', null);
|
|
||||||
|
|
||||||
expect(ViewManager.getView('server-1_tab-1').sendToRenderer).toHaveBeenCalledWith('desktop-sources-result', [
|
|
||||||
{
|
|
||||||
id: 'screen0',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'window0',
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should send error with no sources', async () => {
|
|
||||||
jest.spyOn(desktopCapturer, 'getSources').mockResolvedValue([]);
|
|
||||||
await windowManager.handleGetDesktopSources('server-2_tab-1', null);
|
|
||||||
expect(windowManager.callsWidgetWindow.win.webContents.send).toHaveBeenCalledWith('calls-error', {
|
|
||||||
err: 'screen-permissions',
|
|
||||||
});
|
|
||||||
expect(ViewManager.getView('server-2_tab-1').sendToRenderer).toHaveBeenCalledWith('calls-error', {
|
|
||||||
err: 'screen-permissions',
|
|
||||||
});
|
|
||||||
expect(windowManager.callsWidgetWindow.win.webContents.send).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should send error with no permissions', async () => {
|
|
||||||
jest.spyOn(desktopCapturer, 'getSources').mockResolvedValue([
|
|
||||||
{
|
|
||||||
id: 'screen0',
|
|
||||||
thumbnail: {
|
|
||||||
toDataURL: jest.fn(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
jest.spyOn(systemPreferences, 'getMediaAccessStatus').mockReturnValue('denied');
|
|
||||||
|
|
||||||
await windowManager.handleGetDesktopSources('server-1_tab-1', null);
|
|
||||||
|
|
||||||
expect(systemPreferences.getMediaAccessStatus).toHaveBeenCalledWith('screen');
|
|
||||||
expect(windowManager.callsWidgetWindow.win.webContents.send).toHaveBeenCalledWith('calls-error', {
|
|
||||||
err: 'screen-permissions',
|
|
||||||
});
|
|
||||||
expect(ViewManager.getView('server-1_tab-1').sendToRenderer).toHaveBeenCalledWith('calls-error', {
|
|
||||||
err: 'screen-permissions',
|
|
||||||
});
|
|
||||||
expect(ViewManager.getView('server-1_tab-1').sendToRenderer).toHaveBeenCalledTimes(1);
|
|
||||||
expect(windowManager.callsWidgetWindow.win.webContents.send).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('macos - no permissions', async () => {
|
|
||||||
const originalPlatform = process.platform;
|
|
||||||
Object.defineProperty(process, 'platform', {
|
|
||||||
value: 'darwin',
|
|
||||||
});
|
|
||||||
|
|
||||||
jest.spyOn(desktopCapturer, 'getSources').mockResolvedValue([
|
|
||||||
{
|
|
||||||
id: 'screen0',
|
|
||||||
thumbnail: {
|
|
||||||
toDataURL: jest.fn(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
jest.spyOn(systemPreferences, 'getMediaAccessStatus').mockReturnValue('denied');
|
|
||||||
|
|
||||||
await windowManager.handleGetDesktopSources('server-1_tab-1', null);
|
|
||||||
|
|
||||||
expect(windowManager.missingScreensharePermissions).toBe(true);
|
|
||||||
expect(resetScreensharePermissionsMacOS).toHaveBeenCalledTimes(1);
|
|
||||||
expect(openScreensharePermissionsSettingsMacOS).toHaveBeenCalledTimes(0);
|
|
||||||
expect(windowManager.callsWidgetWindow.win.webContents.send).toHaveBeenCalledWith('calls-error', {
|
|
||||||
err: 'screen-permissions',
|
|
||||||
});
|
|
||||||
expect(ViewManager.getView('server-1_tab-1').sendToRenderer).toHaveBeenCalledWith('calls-error', {
|
|
||||||
err: 'screen-permissions',
|
|
||||||
});
|
|
||||||
|
|
||||||
await windowManager.handleGetDesktopSources('server-1_tab-1', null);
|
|
||||||
|
|
||||||
expect(resetScreensharePermissionsMacOS).toHaveBeenCalledTimes(2);
|
|
||||||
expect(openScreensharePermissionsSettingsMacOS).toHaveBeenCalledTimes(1);
|
|
||||||
|
|
||||||
Object.defineProperty(process, 'platform', {
|
|
||||||
value: originalPlatform,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('handleDesktopSourcesModalRequest', () => {
|
|
||||||
const windowManager = new WindowManager();
|
|
||||||
windowManager.switchServer = jest.fn();
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
CallsWidgetWindow.mockImplementation(() => {
|
|
||||||
return {
|
|
||||||
getServerName: () => 'server-1',
|
|
||||||
getMainView: jest.fn().mockReturnValue({
|
|
||||||
sendToRenderer: jest.fn(),
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
Config.teams = [
|
|
||||||
{
|
|
||||||
name: 'server-1',
|
|
||||||
order: 1,
|
|
||||||
tabs: [
|
|
||||||
{
|
|
||||||
name: 'tab-1',
|
|
||||||
order: 0,
|
|
||||||
isOpen: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'tab-2',
|
|
||||||
order: 2,
|
|
||||||
isOpen: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}, {
|
|
||||||
name: 'server-2',
|
|
||||||
order: 0,
|
|
||||||
tabs: [
|
|
||||||
{
|
|
||||||
name: 'tab-1',
|
|
||||||
order: 0,
|
|
||||||
isOpen: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'tab-2',
|
|
||||||
order: 2,
|
|
||||||
isOpen: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
lastActiveTab: 2,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const map = Config.teams.reduce((arr, item) => {
|
|
||||||
item.tabs.forEach((tab) => {
|
|
||||||
arr.push([`${item.name}_${tab.name}`, {}]);
|
|
||||||
});
|
|
||||||
return arr;
|
|
||||||
}, []);
|
|
||||||
const views = new Map(map);
|
|
||||||
ViewManager.getView.mockImplementation((name) => views.get(name));
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
jest.resetAllMocks();
|
|
||||||
Config.teams = [];
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should switch server', () => {
|
|
||||||
windowManager.callsWidgetWindow = new CallsWidgetWindow();
|
|
||||||
windowManager.handleDesktopSourcesModalRequest();
|
|
||||||
expect(windowManager.switchServer).toHaveBeenCalledWith('server-1');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('handleCallsWidgetChannelLinkClick', () => {
|
|
||||||
const windowManager = new WindowManager();
|
|
||||||
windowManager.switchServer = jest.fn();
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
CallsWidgetWindow.mockImplementation(() => {
|
|
||||||
return {
|
|
||||||
getServerName: () => 'server-2',
|
|
||||||
getMainView: jest.fn().mockReturnValue({
|
|
||||||
sendToRenderer: jest.fn(),
|
|
||||||
}),
|
|
||||||
getChannelURL: jest.fn(),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
Config.teams = [
|
|
||||||
{
|
|
||||||
name: 'server-1',
|
|
||||||
order: 1,
|
|
||||||
tabs: [
|
|
||||||
{
|
|
||||||
name: 'tab-1',
|
|
||||||
order: 0,
|
|
||||||
isOpen: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'tab-2',
|
|
||||||
order: 2,
|
|
||||||
isOpen: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}, {
|
|
||||||
name: 'server-2',
|
|
||||||
order: 0,
|
|
||||||
tabs: [
|
|
||||||
{
|
|
||||||
name: 'tab-1',
|
|
||||||
order: 0,
|
|
||||||
isOpen: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'tab-2',
|
|
||||||
order: 2,
|
|
||||||
isOpen: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
lastActiveTab: 2,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const map = Config.teams.reduce((arr, item) => {
|
|
||||||
item.tabs.forEach((tab) => {
|
|
||||||
arr.push([`${item.name}_${tab.name}`, {}]);
|
|
||||||
});
|
|
||||||
return arr;
|
|
||||||
}, []);
|
|
||||||
const views = new Map(map);
|
|
||||||
ViewManager.getView.mockImplementation((name) => views.get(name));
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
jest.resetAllMocks();
|
|
||||||
Config.teams = [];
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should switch server', () => {
|
|
||||||
windowManager.callsWidgetWindow = new CallsWidgetWindow();
|
|
||||||
windowManager.handleCallsWidgetChannelLinkClick();
|
|
||||||
expect(windowManager.switchServer).toHaveBeenCalledWith('server-2');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('handleCallsError', () => {
|
|
||||||
const windowManager = new WindowManager();
|
|
||||||
const mainWindow = {
|
|
||||||
focus: jest.fn(),
|
|
||||||
};
|
|
||||||
windowManager.switchServer = jest.fn();
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
CallsWidgetWindow.mockImplementation(() => {
|
|
||||||
return {
|
|
||||||
getServerName: () => 'server-2',
|
|
||||||
getMainView: jest.fn().mockReturnValue({
|
|
||||||
sendToRenderer: jest.fn(),
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
MainWindow.get.mockReturnValue(mainWindow);
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
jest.resetAllMocks();
|
|
||||||
Config.teams = [];
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should focus view and propagate error to main view', () => {
|
|
||||||
windowManager.callsWidgetWindow = new CallsWidgetWindow();
|
|
||||||
windowManager.handleCallsError('', {err: 'client-error'});
|
|
||||||
expect(windowManager.switchServer).toHaveBeenCalledWith('server-2');
|
|
||||||
expect(mainWindow.focus).toHaveBeenCalled();
|
|
||||||
expect(windowManager.callsWidgetWindow.getMainView().sendToRenderer).toHaveBeenCalledWith('calls-error', {err: 'client-error'});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('handleCallsLinkClick', () => {
|
|
||||||
const windowManager = new WindowManager();
|
|
||||||
windowManager.switchServer = jest.fn();
|
|
||||||
const view1 = {
|
|
||||||
sendToRenderer: jest.fn(),
|
|
||||||
};
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
CallsWidgetWindow.mockImplementation(() => {
|
|
||||||
return {
|
|
||||||
getServerName: () => 'server-1',
|
|
||||||
getMainView: jest.fn().mockReturnValue(view1),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
ViewManager.getView.mockReturnValue(view1);
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
jest.resetAllMocks();
|
|
||||||
Config.teams = [];
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should pass through the click link to browser history push', () => {
|
|
||||||
windowManager.callsWidgetWindow = new CallsWidgetWindow();
|
|
||||||
windowManager.handleCallsLinkClick('', {link: '/other/subpath'});
|
|
||||||
expect(windowManager.switchServer).toHaveBeenCalledWith('server-1');
|
|
||||||
expect(view1.sendToRenderer).toBeCalledWith('browser-history-push', '/other/subpath');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getServerURLFromWebContentsId', () => {
|
describe('getServerURLFromWebContentsId', () => {
|
||||||
const windowManager = new WindowManager();
|
const windowManager = new WindowManager();
|
||||||
|
|
||||||
it('should return calls widget URL', () => {
|
it('should return calls widget URL', () => {
|
||||||
ViewManager.getView.mockReturnValue({name: 'server-1_tab-messaging'});
|
CallsWidgetWindow.getURL.mockReturnValue('http://server-1.com');
|
||||||
CallsWidgetWindow.mockImplementation(() => {
|
CallsWidgetWindow.isCallsWidget.mockReturnValue(true);
|
||||||
return {
|
expect(windowManager.getServerURLFromWebContentsId('callsID')).toBe('http://server-1.com');
|
||||||
on: jest.fn(),
|
|
||||||
getURL: jest.fn(() => 'http://localhost:8065'),
|
|
||||||
getWebContentsId: jest.fn(() => 'callsID'),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
windowManager.createCallsWidgetWindow('server-1_tab-messaging', 'http://localhost:8065', {callID: 'test'});
|
|
||||||
expect(windowManager.getServerURLFromWebContentsId('callsID')).toBe(windowManager.callsWidgetWindow.getURL());
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('genCallsEventHandler', () => {
|
|
||||||
const windowManager = new WindowManager();
|
|
||||||
|
|
||||||
const handler = jest.fn();
|
|
||||||
|
|
||||||
it('should call handler if callsWidgetWindow is not defined', () => {
|
|
||||||
windowManager.genCallsEventHandler(handler)();
|
|
||||||
expect(handler).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not call handler if source is not allowed', () => {
|
|
||||||
CallsWidgetWindow.mockImplementation(() => {
|
|
||||||
return {
|
|
||||||
isAllowedEvent: jest.fn().mockReturnValue(false),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
windowManager.callsWidgetWindow = new CallsWidgetWindow();
|
|
||||||
windowManager.genCallsEventHandler(handler)();
|
|
||||||
expect(handler).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should call handler if source is allowed', () => {
|
|
||||||
CallsWidgetWindow.mockImplementation(() => {
|
|
||||||
return {
|
|
||||||
isAllowedEvent: jest.fn().mockReturnValue(true),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
windowManager.callsWidgetWindow = new CallsWidgetWindow();
|
|
||||||
windowManager.genCallsEventHandler(handler)();
|
|
||||||
expect(handler).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -4,32 +4,16 @@
|
|||||||
/* eslint-disable max-lines */
|
/* eslint-disable max-lines */
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|
||||||
import {app, BrowserWindow, systemPreferences, ipcMain, IpcMainEvent, IpcMainInvokeEvent, desktopCapturer} from 'electron';
|
import {app, BrowserWindow, systemPreferences, ipcMain, IpcMainEvent, IpcMainInvokeEvent} from 'electron';
|
||||||
|
|
||||||
import {
|
|
||||||
CallsJoinCallMessage,
|
|
||||||
CallsErrorMessage,
|
|
||||||
CallsLinkClickMessage,
|
|
||||||
CallsEventHandler,
|
|
||||||
} from 'types/calls';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
MAXIMIZE_CHANGE,
|
MAXIMIZE_CHANGE,
|
||||||
FOCUS_THREE_DOT_MENU,
|
FOCUS_THREE_DOT_MENU,
|
||||||
GET_DARK_MODE,
|
GET_DARK_MODE,
|
||||||
UPDATE_SHORTCUT_MENU,
|
UPDATE_SHORTCUT_MENU,
|
||||||
BROWSER_HISTORY_PUSH,
|
|
||||||
GET_VIEW_WEBCONTENTS_ID,
|
GET_VIEW_WEBCONTENTS_ID,
|
||||||
RESIZE_MODAL,
|
RESIZE_MODAL,
|
||||||
DISPATCH_GET_DESKTOP_SOURCES,
|
|
||||||
DESKTOP_SOURCES_RESULT,
|
|
||||||
VIEW_FINISHED_RESIZING,
|
VIEW_FINISHED_RESIZING,
|
||||||
CALLS_JOIN_CALL,
|
|
||||||
CALLS_LEAVE_CALL,
|
|
||||||
DESKTOP_SOURCES_MODAL_REQUEST,
|
|
||||||
CALLS_WIDGET_CHANNEL_LINK_CLICK,
|
|
||||||
CALLS_ERROR,
|
|
||||||
CALLS_LINK_CLICK,
|
|
||||||
} from 'common/communication';
|
} from 'common/communication';
|
||||||
import {Logger} from 'common/log';
|
import {Logger} from 'common/log';
|
||||||
import {SECOND} from 'common/utils/constants';
|
import {SECOND} from 'common/utils/constants';
|
||||||
@@ -41,8 +25,6 @@ import {MattermostView} from 'main/views/MattermostView';
|
|||||||
import {
|
import {
|
||||||
getAdjustedWindowBoundaries,
|
getAdjustedWindowBoundaries,
|
||||||
shouldHaveBackBar,
|
shouldHaveBackBar,
|
||||||
resetScreensharePermissionsMacOS,
|
|
||||||
openScreensharePermissionsSettingsMacOS,
|
|
||||||
} from '../utils';
|
} from '../utils';
|
||||||
|
|
||||||
import ViewManager from '../views/viewManager';
|
import ViewManager from '../views/viewManager';
|
||||||
@@ -62,7 +44,6 @@ const log = new Logger('WindowManager');
|
|||||||
export class WindowManager {
|
export class WindowManager {
|
||||||
assetsDir: string;
|
assetsDir: string;
|
||||||
|
|
||||||
callsWidgetWindow?: CallsWidgetWindow;
|
|
||||||
teamDropdown?: TeamDropdownView;
|
teamDropdown?: TeamDropdownView;
|
||||||
downloadsDropdown?: DownloadsDropdownView;
|
downloadsDropdown?: DownloadsDropdownView;
|
||||||
downloadsDropdownMenu?: DownloadsDropdownMenuView;
|
downloadsDropdownMenu?: DownloadsDropdownMenuView;
|
||||||
@@ -75,99 +56,6 @@ export class WindowManager {
|
|||||||
ipcMain.handle(GET_DARK_MODE, this.handleGetDarkMode);
|
ipcMain.handle(GET_DARK_MODE, this.handleGetDarkMode);
|
||||||
ipcMain.handle(GET_VIEW_WEBCONTENTS_ID, this.handleGetWebContentsId);
|
ipcMain.handle(GET_VIEW_WEBCONTENTS_ID, this.handleGetWebContentsId);
|
||||||
ipcMain.on(VIEW_FINISHED_RESIZING, this.handleViewFinishedResizing);
|
ipcMain.on(VIEW_FINISHED_RESIZING, this.handleViewFinishedResizing);
|
||||||
|
|
||||||
// Calls handlers
|
|
||||||
ipcMain.on(DISPATCH_GET_DESKTOP_SOURCES, this.genCallsEventHandler(this.handleGetDesktopSources));
|
|
||||||
ipcMain.on(DESKTOP_SOURCES_MODAL_REQUEST, this.genCallsEventHandler(this.handleDesktopSourcesModalRequest));
|
|
||||||
ipcMain.on(CALLS_JOIN_CALL, this.genCallsEventHandler(this.createCallsWidgetWindow));
|
|
||||||
ipcMain.on(CALLS_LEAVE_CALL, this.genCallsEventHandler(this.handleCallsLeave));
|
|
||||||
ipcMain.on(CALLS_WIDGET_CHANNEL_LINK_CLICK, this.genCallsEventHandler(this.handleCallsWidgetChannelLinkClick));
|
|
||||||
ipcMain.on(CALLS_ERROR, this.genCallsEventHandler(this.handleCallsError));
|
|
||||||
ipcMain.on(CALLS_LINK_CLICK, this.genCallsEventHandler(this.handleCallsLinkClick));
|
|
||||||
}
|
|
||||||
|
|
||||||
genCallsEventHandler = (handler: CallsEventHandler) => {
|
|
||||||
return (event: IpcMainEvent, viewName: string, msg?: any) => {
|
|
||||||
if (this.callsWidgetWindow && !this.callsWidgetWindow.isAllowedEvent(event)) {
|
|
||||||
log.warn('genCallsEventHandler', 'Disallowed calls event');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
handler(viewName, msg);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
createCallsWidgetWindow = async (viewName: string, msg: CallsJoinCallMessage) => {
|
|
||||||
log.debug('createCallsWidgetWindow');
|
|
||||||
if (this.callsWidgetWindow) {
|
|
||||||
// trying to join again the call we are already in should not be allowed.
|
|
||||||
if (this.callsWidgetWindow.getCallID() === msg.callID) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// to switch from one call to another we need to wait for the existing
|
|
||||||
// window to be fully closed.
|
|
||||||
await this.callsWidgetWindow.close();
|
|
||||||
}
|
|
||||||
const currentView = ViewManager.getView(viewName);
|
|
||||||
if (!currentView) {
|
|
||||||
log.error('unable to create calls widget window: currentView is missing');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.callsWidgetWindow = new CallsWidgetWindow(MainWindow.get()!, currentView, {
|
|
||||||
callID: msg.callID,
|
|
||||||
title: msg.title,
|
|
||||||
rootID: msg.rootID,
|
|
||||||
channelURL: msg.channelURL,
|
|
||||||
});
|
|
||||||
|
|
||||||
this.callsWidgetWindow.on('closed', () => delete this.callsWidgetWindow);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleDesktopSourcesModalRequest = () => {
|
|
||||||
log.debug('handleDesktopSourcesModalRequest');
|
|
||||||
|
|
||||||
if (this.callsWidgetWindow) {
|
|
||||||
this.switchServer(this.callsWidgetWindow.getServerName());
|
|
||||||
MainWindow.get()?.focus();
|
|
||||||
this.callsWidgetWindow.getMainView().sendToRenderer(DESKTOP_SOURCES_MODAL_REQUEST);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleCallsWidgetChannelLinkClick = () => {
|
|
||||||
log.debug('handleCallsWidgetChannelLinkClick');
|
|
||||||
|
|
||||||
if (this.callsWidgetWindow) {
|
|
||||||
this.switchServer(this.callsWidgetWindow.getServerName());
|
|
||||||
MainWindow.get()?.focus();
|
|
||||||
this.callsWidgetWindow.getMainView().sendToRenderer(BROWSER_HISTORY_PUSH, this.callsWidgetWindow.getChannelURL());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleCallsError = (_: string, msg: CallsErrorMessage) => {
|
|
||||||
log.debug('handleCallsError', msg);
|
|
||||||
|
|
||||||
if (this.callsWidgetWindow) {
|
|
||||||
this.switchServer(this.callsWidgetWindow.getServerName());
|
|
||||||
MainWindow.get()?.focus();
|
|
||||||
this.callsWidgetWindow.getMainView().sendToRenderer(CALLS_ERROR, msg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleCallsLinkClick = (_: string, msg: CallsLinkClickMessage) => {
|
|
||||||
log.debug('handleCallsLinkClick with linkURL', msg.link);
|
|
||||||
|
|
||||||
if (this.callsWidgetWindow) {
|
|
||||||
this.switchServer(this.callsWidgetWindow.getServerName());
|
|
||||||
MainWindow.get()?.focus();
|
|
||||||
this.callsWidgetWindow.getMainView().sendToRenderer(BROWSER_HISTORY_PUSH, msg.link);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleCallsLeave = () => {
|
|
||||||
log.debug('handleCallsLeave');
|
|
||||||
|
|
||||||
this.callsWidgetWindow?.close();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
showMainWindow = (deeplinkingURL?: string | URL) => {
|
showMainWindow = (deeplinkingURL?: string | URL) => {
|
||||||
@@ -562,74 +450,9 @@ export class WindowManager {
|
|||||||
return event.sender.id;
|
return event.sender.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
handleGetDesktopSources = async (viewName: string, opts: Electron.SourcesOptions) => {
|
|
||||||
log.debug('handleGetDesktopSources', {viewName, opts});
|
|
||||||
|
|
||||||
const view = ViewManager.getView(viewName);
|
|
||||||
if (!view) {
|
|
||||||
log.error('handleGetDesktopSources: view not found');
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (process.platform === 'darwin' && systemPreferences.getMediaAccessStatus('screen') === 'denied') {
|
|
||||||
try {
|
|
||||||
// If permissions are missing we reset them so that the system
|
|
||||||
// prompt can be showed.
|
|
||||||
await resetScreensharePermissionsMacOS();
|
|
||||||
|
|
||||||
// We only open the system settings if permissions were already missing since
|
|
||||||
// on the first attempt to get the sources the OS will correctly show a prompt.
|
|
||||||
if (this.missingScreensharePermissions) {
|
|
||||||
await openScreensharePermissionsSettingsMacOS();
|
|
||||||
}
|
|
||||||
this.missingScreensharePermissions = true;
|
|
||||||
} catch (err) {
|
|
||||||
log.error('failed to reset screen sharing permissions', err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const screenPermissionsErrMsg = {err: 'screen-permissions'};
|
|
||||||
|
|
||||||
return desktopCapturer.getSources(opts).then((sources) => {
|
|
||||||
let hasScreenPermissions = true;
|
|
||||||
if (systemPreferences.getMediaAccessStatus) {
|
|
||||||
const screenPermissions = systemPreferences.getMediaAccessStatus('screen');
|
|
||||||
log.debug('screenPermissions', screenPermissions);
|
|
||||||
if (screenPermissions === 'denied') {
|
|
||||||
log.info('no screen sharing permissions');
|
|
||||||
hasScreenPermissions = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!hasScreenPermissions || !sources.length) {
|
|
||||||
log.info('missing screen permissions');
|
|
||||||
view.sendToRenderer(CALLS_ERROR, screenPermissionsErrMsg);
|
|
||||||
this.callsWidgetWindow?.win.webContents.send(CALLS_ERROR, screenPermissionsErrMsg);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const message = sources.map((source) => {
|
|
||||||
return {
|
|
||||||
id: source.id,
|
|
||||||
name: source.name,
|
|
||||||
thumbnailURL: source.thumbnail.toDataURL(),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
if (message.length > 0) {
|
|
||||||
view.sendToRenderer(DESKTOP_SOURCES_RESULT, message);
|
|
||||||
}
|
|
||||||
}).catch((err) => {
|
|
||||||
log.error('desktopCapturer.getSources failed', err);
|
|
||||||
|
|
||||||
view.sendToRenderer(CALLS_ERROR, screenPermissionsErrMsg);
|
|
||||||
this.callsWidgetWindow?.win.webContents.send(CALLS_ERROR, screenPermissionsErrMsg);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
getServerURLFromWebContentsId = (id: number) => {
|
getServerURLFromWebContentsId = (id: number) => {
|
||||||
if (this.callsWidgetWindow && (id === this.callsWidgetWindow.getWebContentsId() || id === this.callsWidgetWindow.getPopOutWebContentsId())) {
|
if (CallsWidgetWindow.isCallsWidget(id)) {
|
||||||
return this.callsWidgetWindow.getURL();
|
return CallsWidgetWindow.getURL();
|
||||||
}
|
}
|
||||||
|
|
||||||
return ViewManager.getViewByWebContentsId(id)?.tab.server.url;
|
return ViewManager.getViewByWebContentsId(id)?.tab.server.url;
|
||||||
|
Reference in New Issue
Block a user