Typescript (Optimize loading styles & register global shortcuts) (#629)

* Typescript - Optimize loading styles and register global shortcuts

* Typescript - Update version info logic to support BC

* Typescript - Implement memory refresh logic

* Typescript - Optimize memory refresh, devtools & appBridge code

* Typescript - Mock window-handler to fix unit tests

* Typescript - Change logic to remove if else and early exit
This commit is contained in:
Kiran Niranjan 2019-04-12 16:11:04 +05:30 committed by Vishwas Shashidhar
parent b9bc65008d
commit 6aca522a1e
21 changed files with 518 additions and 191 deletions

View File

@ -68,13 +68,6 @@
<button id='open-config-win'>Open configure window</button> <button id='open-config-win'>Open configure window</button>
<br> <br>
<hr> <hr>
<p>
Crash Process:
<p>
<button id='crash'>Crash Renderer</button>
</p>
<br>
<hr>
<textarea id="text-val" rows="4">Writes some thing to the file</textarea> <textarea id="text-val" rows="4">Writes some thing to the file</textarea>
<br/> <br/>
<input type="button" id="download-file1" value="Download"/> <input type="button" id="download-file1" value="Download"/>
@ -147,6 +140,8 @@
</body> </body>
<script> <script>
window.name = 'main';
const apiCmds = { const apiCmds = {
isOnline: 'is-online', isOnline: 'is-online',
getVersionInfo: 'get-version-info', getVersionInfo: 'get-version-info',
@ -294,15 +289,10 @@
} }
}); });
// crash the renderer process
const crash = document.getElementById('crash');
crash.addEventListener('click', function () {
ssf.crashRendererProcess();
});
var getVersionInfo = document.getElementById('get-version'); var getVersionInfo = document.getElementById('get-version');
getVersionInfo.addEventListener('click', function() { getVersionInfo.addEventListener('click', () => {
ssf.getVersionInfo().then(function(verInfo) { if (window.ssf) {
ssf.getVersionInfo().then((verInfo) => {
let apiVersionInfo = document.getElementById('api-version'); let apiVersionInfo = document.getElementById('api-version');
let containerIdentifier = document.getElementById('container-identifier'); let containerIdentifier = document.getElementById('container-identifier');
let version = document.getElementById('container-ver'); let version = document.getElementById('container-ver');
@ -315,11 +305,82 @@
buildNumber.innerText = verInfo.buildNumber; buildNumber.innerText = verInfo.buildNumber;
searchApiVer.innerText = verInfo.searchApiVer; searchApiVer.innerText = verInfo.searchApiVer;
}); });
} else {
postRequest(apiCmds.getVersionInfo, null, {
successCallback: (verInfo) => {
let apiVersionInfo = document.getElementById('api-version');
let containerIdentifier = document.getElementById('container-identifier');
let version = document.getElementById('container-ver');
let buildNumber = document.getElementById('build-number');
let searchApiVer = document.getElementById('search-api-ver');
apiVersionInfo.innerText = verInfo.apiVer;
containerIdentifier.innerText = verInfo.containerIdentifier;
version.innerText = verInfo.containerVer;
buildNumber.innerText = verInfo.buildNumber;
searchApiVer.innerText = verInfo.searchApiVer;
}
});
}
}); });
/** /**
* Core post message api handler * Core post message api handler
*/ */
const pending = [];
/**
* Delete the request once we get the response
* @returns {boolean}
* @param id
*/
const forgetRequest = (id) => {
return delete pending[ id ];
};
const broadcastMessage = (method, data) => {
window.postMessage({ method: method, data: data }, window.origin);
};
/**
*
* @param method
* @param data
* @param handlers
*/
const postRequest = (method, data = {}, handlers) => {
const request = Object.assign({ requestId: ++requestId }, data);
pending[ request.requestId ] = {
errorCallback: (error) => {
return handlers.errorCallback(error)
&& forgetRequest(request.requestId);
},
successCallback: (response) => {
return handlers.successCallback(response)
&& forgetRequest(request.requestId);
},
};
broadcastMessage(method, request);
return request.requestId;
};
const handleResponse = (data) => {
if (data && data.requestId) {
const id = data.requestId;
const content = data;
const resolver = pending[ id ];
if (!resolver) {
return;
}
if (content.error) {
resolver.errorCallback(content.error);
return;
}
if (content.response) {
resolver.successCallback(content.response);
return;
}
resolver.successCallback(content);
}
};
const postMessage = (method, data) => { const postMessage = (method, data) => {
window.postMessage({ method, data }, '*'); window.postMessage({ method, data }, '*');
@ -331,35 +392,6 @@
case 'activity-callback': case 'activity-callback':
activityCallback(data); activityCallback(data);
break; break;
case 'media-source-callback':
if (data.error) {
if (data.error.message === 'User Cancelled') {
console.error('user canceled');
} else {
throw new Error(data.error);
}
return;
}
const constraints = {
mandatory: {
chromeMediaSource: 'desktop',
maxWidth: screen.width > 1920 ? 1920 : screen.width,
maxHeight: screen.height > 1080 ? 1080 : screen.height,
chromeMediaSourceId: data.source.id
},
optional: []
};
navigator.mediaDevices.getUserMedia({ video: constraints }).then((stream) => {
handleStream(stream, data.source.display_id);
});
break;
case 'screen-sharing-indicator-callback':
if (!data || typeof data.type !== 'string' || !requestId) {
console.error(`screen-sharing-indicator-callback invalid args ${data}`);
} else {
console.log(`screen sharing will be stopped`);
}
break;
case 'screen-snippet-callback': case 'screen-snippet-callback':
if (data) { if (data) {
if (data.type && data.type === 'ERROR') { if (data.type && data.type === 'ERROR') {
@ -372,6 +404,12 @@
} }
} }
break; break;
case 'media-source-callback':
case 'screen-sharing-indicator-callback':
case 'get-version-info-callback':
handleResponse(data);
console.log(event.data);
break;
default: default:
console.log(event.data); console.log(event.data);
} }
@ -457,7 +495,33 @@
}, handleError); }, handleError);
}); });
} else { } else {
postMessage(apiCmds.getMediaSource, { types: ['window', 'screen'], requestId: ++requestId }); const successHandler = (data) => {
const constraints = {
mandatory: {
chromeMediaSource: 'desktop',
maxWidth: screen.width > 1920 ? 1920 : screen.width,
maxHeight: screen.height > 1080 ? 1080 : screen.height,
chromeMediaSourceId: data.source.id
},
optional: []
};
navigator.mediaDevices.getUserMedia({ video: constraints }).then((stream) => {
handleStream(stream, data.source.display_id);
});
};
const errorHandler = (data) => {
if (data.error) {
if (data.error.message === 'User Cancelled') {
console.error('user canceled');
} else {
throw new Error(data.error);
}
}
};
postRequest(apiCmds.getMediaSource, { types: ['window', 'screen'] }, {
successCallback: (data) => successHandler(data),
errorCallback: (error) => errorHandler(error),
});
} }
}); });
@ -475,7 +539,24 @@
} }
}); });
} else { } else {
postMessage(apiCmds.openScreenSharingIndicator, { streamId: stream.id, displayId, requestId: ++requestId }); const success = (data) => {
if (data.error) {
if (data.error.message === 'User Cancelled') {
console.error('user canceled');
} else {
throw new Error(data.error);
}
return;
}
if (!data || typeof data.type !== 'string' || !requestId) {
console.error(`screen-sharing-indicator-callback invalid args ${data}`);
} else {
console.log(`screen sharing will be stopped`);
}
};
postRequest(apiCmds.openScreenSharingIndicator, { streamId: stream.id, displayId }, {
successCallback: (data) => success(data),
});
} }
}; };

View File

@ -30,7 +30,11 @@ Test Window has been opened
var incBadgeEl = document.getElementById('inc-badge'); var incBadgeEl = document.getElementById('inc-badge');
incBadgeEl.addEventListener('click', function() { incBadgeEl.addEventListener('click', function() {
badgeCount++; badgeCount++;
if (window.ssf) {
ssf.setBadgeCount(badgeCount); ssf.setBadgeCount(badgeCount);
} else {
postMessage({ method: 'set-badge-count', data: badgeCount }, window.origin);
}
}); });
var openWinButton = document.getElementById('open-win'); var openWinButton = document.getElementById('open-win');

View File

@ -1,5 +1,13 @@
jest.mock('electron-log'); jest.mock('electron-log');
jest.mock('../src/app/window-handler', () => {
return {
windowHandler: {
setIsAutoReload: jest.fn(() => true),
},
};
});
describe('activity detection', () => { describe('activity detection', () => {
const originalTimeout: number = jasmine.DEFAULT_TIMEOUT_INTERVAL; const originalTimeout: number = jasmine.DEFAULT_TIMEOUT_INTERVAL;
jasmine.DEFAULT_TIMEOUT_INTERVAL = 60000; jasmine.DEFAULT_TIMEOUT_INTERVAL = 60000;

View File

@ -3,6 +3,7 @@ import { app } from 'electron';
import Timer = NodeJS.Timer; import Timer = NodeJS.Timer;
import { logger } from '../common/logger'; import { logger } from '../common/logger';
import { windowHandler } from './window-handler';
class ActivityDetection { class ActivityDetection {
private idleThreshold: number; private idleThreshold: number;
@ -49,6 +50,9 @@ class ActivityDetection {
if (this.timer) { if (this.timer) {
clearInterval(this.timer); clearInterval(this.timer);
} }
// set auto reload to false so the
// activate func works normally
windowHandler.setIsAutoReload(false);
this.timer = undefined; this.timer = undefined;
logger.info(`activity-detection: activity occurred`); logger.info(`activity-detection: activity occurred`);
return; return;

View File

@ -96,11 +96,11 @@ export class AppMenu {
* @param opts {Electron.PopupOptions} * @param opts {Electron.PopupOptions}
*/ */
public popupMenu(opts: Electron.PopupOptions): void { public popupMenu(opts: Electron.PopupOptions): void {
if (this.menu) { if (!this.menu) {
this.menu.popup(opts);
} else {
logger.error(`app-menu: tried popup menu, but failed menu not defined`); logger.error(`app-menu: tried popup menu, but failed menu not defined`);
return;
} }
this.menu.popup(opts);
} }
/** /**
@ -311,18 +311,19 @@ export class AppMenu {
accelerator: isMac ? 'Alt+Command+I' : 'Ctrl+Shift+I', accelerator: isMac ? 'Alt+Command+I' : 'Ctrl+Shift+I',
click(_item, focusedWindow) { click(_item, focusedWindow) {
const devToolsEnabled = config.getGlobalConfigFields([ 'devToolsEnabled' ]); const devToolsEnabled = config.getGlobalConfigFields([ 'devToolsEnabled' ]);
if (focusedWindow && windowExists(focusedWindow)) { if (!focusedWindow || !windowExists(focusedWindow)) {
return;
}
if (devToolsEnabled) { if (devToolsEnabled) {
focusedWindow.webContents.toggleDevTools(); focusedWindow.webContents.toggleDevTools();
} else { return;
}
dialog.showMessageBox(focusedWindow, { dialog.showMessageBox(focusedWindow, {
type: 'warning', type: 'warning',
buttons: [ 'Ok' ], buttons: [ 'Ok' ],
title: i18n.t('Dev Tools disabled')(), title: i18n.t('Dev Tools disabled')(),
message: i18n.t('Dev Tools has been disabled. Please contact your system administrator')(), message: i18n.t('Dev Tools has been disabled. Please contact your system administrator')(),
}); });
}
}
}, },
},{ },{
click: () => windowHandler.createMoreInfoWindow(), click: () => windowHandler.createMoreInfoWindow(),

View File

@ -100,13 +100,13 @@ class AutoLaunchController extends AutoLaunch {
if (!isAutoLaunchEnabled) { if (!isAutoLaunchEnabled) {
await this.enableAutoLaunch(); await this.enableAutoLaunch();
} }
} else { return;
}
if (isAutoLaunchEnabled) { if (isAutoLaunchEnabled) {
await this.disableAutoLaunch(); await this.disableAutoLaunch();
} }
} }
} }
}
const autoLaunchInstance = new AutoLaunchController(props); const autoLaunchInstance = new AutoLaunchController(props);

View File

@ -5,6 +5,7 @@ import { LocaleType } from '../common/i18n';
import { logger } from '../common/logger'; import { logger } from '../common/logger';
import { activityDetection } from './activity-detection'; import { activityDetection } from './activity-detection';
import { config } from './config-handler'; import { config } from './config-handler';
import { memoryMonitor } from './memory-monitor';
import { protocolHandler } from './protocol-handler'; import { protocolHandler } from './protocol-handler';
import { screenSnippet } from './screen-snippet-handler'; import { screenSnippet } from './screen-snippet-handler';
import { activate, handleKeyPress } from './window-actions'; import { activate, handleKeyPress } from './window-actions';
@ -132,6 +133,16 @@ ipcMain.on(apiName.symphonyApi, (event: Electron.Event, arg: IApiArgs) => {
event.returnValue = windowHandler.spellchecker ? windowHandler.spellchecker.isMisspelled(arg.word) : false; event.returnValue = windowHandler.spellchecker ? windowHandler.spellchecker.isMisspelled(arg.word) : false;
} }
break; break;
case apiCmds.setIsInMeeting:
if (typeof arg.isInMeeting === 'boolean') {
memoryMonitor.setMeetingStatus(arg.isInMeeting);
}
break;
case apiCmds.memoryInfo:
if (typeof arg.memoryInfo === 'object') {
memoryMonitor.setMemoryInfo(arg.memoryInfo);
}
break;
default: default:
} }

View File

@ -108,9 +108,9 @@ app.on('activate', () => {
const mainWindow: ICustomBrowserWindow | null = windowHandler.getMainWindow(); const mainWindow: ICustomBrowserWindow | null = windowHandler.getMainWindow();
if (!mainWindow || mainWindow.isDestroyed()) { if (!mainWindow || mainWindow.isDestroyed()) {
startApplication(); startApplication();
} else { return;
mainWindow.show();
} }
mainWindow.show();
}); });
/** /**

91
src/app/memory-monitor.ts Normal file
View File

@ -0,0 +1,91 @@
import * as electron from 'electron';
import { logger } from '../common/logger';
import { config } from './config-handler';
import { windowHandler } from './window-handler';
import { windowExists } from './window-utils';
class MemoryMonitor {
private memoryInfo: Electron.ProcessMemoryInfo | undefined = undefined;
private isInMeeting: boolean;
private canReload: boolean;
private readonly maxIdleTime: number;
private readonly memoryThreshold: number;
private readonly memoryRefreshThreshold: number;
constructor() {
this.isInMeeting = false;
this.canReload = true;
this.maxIdleTime = 4 * 60 * 60 * 1000; // 4 hours
this.memoryThreshold = 800 * 1024; // 800MB
this.memoryRefreshThreshold = 60 * 60 * 1000; // 1 hour
}
/**
* Sets process memory from ipc events every hour
* and refreshes the client if the conditions passes
*
* @param memoryInfo {Electron.ProcessMemoryInfo}
*/
public setMemoryInfo(memoryInfo: Electron.ProcessMemoryInfo): void {
this.memoryInfo = memoryInfo;
this.validateMemory();
}
/**
* Sets the web app's RTC meeting status
*
* @param isInMeeting {boolean} whether user is in an active RTC meeting
*/
public setMeetingStatus(isInMeeting: boolean): void {
this.isInMeeting = isInMeeting;
}
/**
* Validates the predefined conditions and refreshes the client
*/
private validateMemory(): void {
const { memoryRefresh } = config.getConfigFields([ 'memoryRefresh' ]);
if (!memoryRefresh) {
logger.info(`memory refresh is disabled`);
return;
}
(electron.powerMonitor as any).querySystemIdleTime((time) => {
const idleTime = time * 1000;
if (!(!this.isInMeeting
&& windowHandler.isOnline
&& this.canReload
&& idleTime > this.maxIdleTime
&& (this.memoryInfo && this.memoryInfo.private > this.memoryThreshold))
) {
logger.info(`Not Reloading the app as
application was refreshed less than a hour ago? ${this.canReload ? 'no' : 'yes'}
memory consumption is ${(this.memoryInfo && this.memoryInfo.private) || 'unknown'}kb is less than? ${this.memoryThreshold}kb
system idle tick was ${idleTime}ms is less than? ${this.maxIdleTime}ms
user was in a meeting? ${this.isInMeeting}
is network online? ${windowHandler.isOnline}`);
return;
}
const mainWindow = windowHandler.getMainWindow();
if (mainWindow && windowExists(mainWindow)) {
logger.info(`Reloading the app to optimize memory usage as
memory consumption is ${this.memoryInfo.private}kb is greater than? ${this.memoryThreshold}kb threshold
system idle tick was ${idleTime}ms is greater than ${this.maxIdleTime}ms
user was in a meeting? ${this.isInMeeting}
is network online? ${windowHandler.isOnline}`);
windowHandler.setIsAutoReload(true);
mainWindow.reload();
this.canReload = false;
setTimeout(() => {
this.canReload = true;
}, this.memoryRefreshThreshold);
}
});
}
}
const memoryMonitor = new MemoryMonitor();
export { memoryMonitor };

View File

@ -25,15 +25,11 @@ class ScreenSnippet {
constructor() { constructor() {
this.tempDir = os.tmpdir(); this.tempDir = os.tmpdir();
this.isAlwaysOnTop = false; this.isAlwaysOnTop = false;
if (isMac) { this.captureUtil = isMac ? '/usr/sbin/screencapture' : isDevEnv
this.captureUtil = '/usr/sbin/screencapture';
} else {
this.captureUtil = isDevEnv
? path.join(__dirname, ? path.join(__dirname,
'../../node_modules/screen-snippet/bin/Release/ScreenSnippet.exe') '../../node_modules/screen-snippet/bin/Release/ScreenSnippet.exe')
: path.join(path.dirname(app.getPath('exe')), 'ScreenSnippet.exe'); : path.join(path.dirname(app.getPath('exe')), 'ScreenSnippet.exe');
} }
}
/** /**
* Captures a user selected portion of the monitor and returns jpeg image * Captures a user selected portion of the monitor and returns jpeg image
@ -119,14 +115,12 @@ class ScreenSnippet {
const output = Buffer.from(data).toString('base64'); const output = Buffer.from(data).toString('base64');
return { message: 'success', data: output, type: 'image/png;base64' }; return { message: 'success', data: output, type: 'image/png;base64' };
} catch (error) { } catch (error) {
if (error && error.code === 'ENOENT') { // no such file exists or user likely aborted
// no such file exists, user likely aborted
// creating snippet. also include any error when // creating snippet. also include any error when
// creating child process. // creating child process.
return { message: `file does not exist`, type: 'ERROR' }; return error && error.code === 'ENOENT'
} else { ? { message: `file does not exist`, type: 'ERROR' }
return { message: `${error}`, type: 'ERROR' }; : { message: `${error}`, type: 'ERROR' };
}
} finally { } finally {
// remove tmp file (async) // remove tmp file (async)
if (this.outputFileName) { if (this.outputFileName) {

View File

@ -127,6 +127,9 @@ export const handleKeyPress = (key: number): void => {
break; break;
} }
case KeyCodes.Alt: case KeyCodes.Alt:
if (isMac) {
return;
}
const browserWin = BrowserWindow.getFocusedWindow(); const browserWin = BrowserWindow.getFocusedWindow();
if (browserWin && !browserWin.isDestroyed()) { if (browserWin && !browserWin.isDestroyed()) {
showPopupMenu({ window: browserWin }); showPopupMenu({ window: browserWin });

View File

@ -1,5 +1,5 @@
import * as electron from 'electron'; import * as electron from 'electron';
import { app, BrowserWindow, crashReporter, ipcMain } from 'electron'; import { app, BrowserWindow, crashReporter, globalShortcut, ipcMain } from 'electron';
import * as path from 'path'; import * as path from 'path';
import { format, parse } from 'url'; import { format, parse } from 'url';
@ -98,6 +98,9 @@ export class WindowHandler {
height: 360, height: 360,
show: false, show: false,
modal: true, modal: true,
minimizable: false,
maximizable: false,
fullscreenable: false,
autoHideMenuBar: true, autoHideMenuBar: true,
webPreferences: { webPreferences: {
sandbox: true, sandbox: true,
@ -257,8 +260,7 @@ export class WindowHandler {
} }
this.url = this.mainWindow.webContents.getURL(); this.url = this.mainWindow.webContents.getURL();
// Injects custom title bar css into the webContents // Injects custom title bar and snack bar css into the webContents
// only for Window and if it is enabled
await injectStyles(this.mainWindow, this.isCustomTitleBar); await injectStyles(this.mainWindow, this.isCustomTitleBar);
this.mainWindow.webContents.send('page-load', { this.mainWindow.webContents.send('page-load', {
@ -297,6 +299,23 @@ export class WindowHandler {
} }
}); });
this.mainWindow.webContents.on('crashed', (_event: Event, killed: boolean) => {
if (killed) {
return;
}
electron.dialog.showMessageBox({
type: 'error',
title: i18n.t('Renderer Process Crashed')(),
message: i18n.t('Oops! Looks like we have had a crash. Please reload or close this window.')(),
buttons: [ 'Reload', 'Close' ],
}, (index: number) => {
if (!this.mainWindow || !windowExists(this.mainWindow)) {
return;
}
index === 0 ? this.mainWindow.reload() : this.mainWindow.close();
});
});
// Handle main window close // Handle main window close
this.mainWindow.on('close', (event) => { this.mainWindow.on('close', (event) => {
if (!this.mainWindow || !windowExists(this.mainWindow)) { if (!this.mainWindow || !windowExists(this.mainWindow)) {
@ -304,15 +323,20 @@ export class WindowHandler {
} }
if (this.willQuitApp) { if (this.willQuitApp) {
return this.destroyAllWindow(); return this.destroyAllWindows();
} }
if (config.getConfigFields([ 'minimizeOnClose' ]).minimizeOnClose) { const { minimizeOnClose } = config.getConfigFields([ 'minimizeOnClose' ]);
if (minimizeOnClose) {
event.preventDefault(); event.preventDefault();
isMac ? this.mainWindow.hide() : this.mainWindow.minimize(); isMac ? this.mainWindow.hide() : this.mainWindow.minimize();
} else { return;
app.quit();
} }
app.quit();
});
this.mainWindow.once('closed', () => {
this.destroyAllWindows();
}); });
// Certificate verification proxy // Certificate verification proxy
@ -320,6 +344,9 @@ export class WindowHandler {
this.mainWindow.webContents.session.setCertificateVerifyProc(handleCertificateProxyVerification); this.mainWindow.webContents.session.setCertificateVerifyProc(handleCertificateProxyVerification);
} }
// Register global shortcuts
this.registerGlobalShortcuts();
// Validate window navigation // Validate window navigation
preventWindowNavigation(this.mainWindow, false); preventWindowNavigation(this.mainWindow, false);
@ -355,9 +382,11 @@ export class WindowHandler {
} }
// Ready to show the window // Ready to show the window
if (!this.isAutoReload) {
this.mainWindow.show(); this.mainWindow.show();
} }
} }
}
/** /**
* Gets the main window * Gets the main window
@ -760,10 +789,48 @@ export class WindowHandler {
delete this.windows[ key ]; delete this.windows[ key ];
} }
/**
* Registers keyboard shortcuts or devtools
*/
private registerGlobalShortcuts(): void {
globalShortcut.register(isMac ? 'Cmd+Alt+I' : 'Ctrl+Shift+I', this.onRegisterDevtools);
app.on('browser-window-focus', () => {
globalShortcut.register(isMac ? 'Cmd+Alt+I' : 'Ctrl+Shift+I', this.onRegisterDevtools);
});
app.on('browser-window-blur', () => {
globalShortcut.unregister(isMac ? 'Cmd+Alt+I' : 'Ctrl+Shift+I');
});
}
/**
* Verifies and toggle devtool based on global config settings
* else displays a dialog
*/
private onRegisterDevtools(): void {
const focusedWindow = BrowserWindow.getFocusedWindow();
const { devToolsEnabled } = config.getGlobalConfigFields([ 'devToolsEnabled' ]);
if (!focusedWindow || !windowExists(focusedWindow)) {
return;
}
if (devToolsEnabled) {
focusedWindow.webContents.toggleDevTools();
return;
}
focusedWindow.webContents.closeDevTools();
electron.dialog.showMessageBox(focusedWindow, {
type: 'warning',
buttons: [ 'Ok' ],
title: i18n.t('Dev Tools disabled')(),
message: i18n.t('Dev Tools has been disabled! Please contact your system administrator to enable it!')(),
});
}
/** /**
* Cleans up reference * Cleans up reference
*/ */
private destroyAllWindow(): void { private destroyAllWindows(): void {
for (const key in this.windows) { for (const key in this.windows) {
if (Object.prototype.hasOwnProperty.call(this.windows, key)) { if (Object.prototype.hasOwnProperty.call(this.windows, key)) {
const winKey = this.windows[ key ]; const winKey = this.windows[ key ];

View File

@ -15,6 +15,16 @@ import { config } from './config-handler';
import { screenSnippet } from './screen-snippet-handler'; import { screenSnippet } from './screen-snippet-handler';
import { ICustomBrowserWindow, windowHandler } from './window-handler'; import { ICustomBrowserWindow, windowHandler } from './window-handler';
interface IStyles {
name: styleNames;
content: string;
}
enum styleNames {
titleBar = 'title-bar',
snackBar = 'snack-bar',
}
const checkValidWindow = true; const checkValidWindow = true;
const { url: configUrl, ctWhitelist } = config.getGlobalConfigFields([ 'url', 'ctWhitelist' ]); const { url: configUrl, ctWhitelist } = config.getGlobalConfigFields([ 'url', 'ctWhitelist' ]);
@ -22,6 +32,8 @@ const { url: configUrl, ctWhitelist } = config.getGlobalConfigFields([ 'url', 'c
const networkStatusCheckInterval = 10 * 1000; const networkStatusCheckInterval = 10 * 1000;
let networkStatusCheckIntervalId; let networkStatusCheckIntervalId;
const styles: IStyles[] = [];
/** /**
* Checks if window is valid and exists * Checks if window is valid and exists
* *
@ -153,16 +165,19 @@ export const showBadgeCount = (count: number): void => {
// handle ms windows... // handle ms windows...
const mainWindow = windowHandler.getMainWindow(); const mainWindow = windowHandler.getMainWindow();
if (mainWindow) { if (!mainWindow || !windowExists(mainWindow)) {
if (count > 0) { return;
}
// get badge img from renderer process, will return // get badge img from renderer process, will return
// img dataUrl in setDataUrl func. // img dataUrl in setDataUrl func.
if (count > 0) {
mainWindow.webContents.send('create-badge-data-url', { count }); mainWindow.webContents.send('create-badge-data-url', { count });
} else { return;
}
// clear badge count icon // clear badge count icon
mainWindow.setOverlayIcon(null, ''); mainWindow.setOverlayIcon(null, '');
}
}
}; };
/** /**
@ -288,10 +303,15 @@ export const getBounds = (winPos: Electron.Rectangle, defaultWidth: number, defa
* @param type * @param type
* @param filePath * @param filePath
*/ */
export const downloadManagerAction = (type, filePath) => { export const downloadManagerAction = (type, filePath): void => {
const focusedWindow = electron.BrowserWindow.getFocusedWindow();
if (!focusedWindow || !windowExists(focusedWindow)) {
return;
}
if (type === 'open') { if (type === 'open') {
const openResponse = electron.shell.openExternal(`file:///${filePath}`); const openResponse = electron.shell.openExternal(`file:///${filePath}`);
const focusedWindow = electron.BrowserWindow.getFocusedWindow();
if (!openResponse && focusedWindow && !focusedWindow.isDestroyed()) { if (!openResponse && focusedWindow && !focusedWindow.isDestroyed()) {
electron.dialog.showMessageBox(focusedWindow, { electron.dialog.showMessageBox(focusedWindow, {
message: i18n.t('The file you are trying to open cannot be found in the specified path.')(), message: i18n.t('The file you are trying to open cannot be found in the specified path.')(),
@ -299,17 +319,17 @@ export const downloadManagerAction = (type, filePath) => {
type: 'error', type: 'error',
}); });
} }
} else { return;
}
const showResponse = electron.shell.showItemInFolder(filePath); const showResponse = electron.shell.showItemInFolder(filePath);
const focusedWindow = electron.BrowserWindow.getFocusedWindow(); if (!showResponse) {
if (!showResponse && focusedWindow && !focusedWindow.isDestroyed()) {
electron.dialog.showMessageBox(focusedWindow, { electron.dialog.showMessageBox(focusedWindow, {
message: i18n.t('The file you are trying to open cannot be found in the specified path.')(), message: i18n.t('The file you are trying to open cannot be found in the specified path.')(),
title: i18n.t('File not Found')(), title: i18n.t('File not Found')(),
type: 'error', type: 'error',
}); });
} }
}
}; };
/** /**
@ -339,10 +359,11 @@ export const handleDownloadManager = (_event, item: Electron.DownloadItem, webCo
* Inserts css in to the window * Inserts css in to the window
* *
* @param window {BrowserWindow} * @param window {BrowserWindow}
* @param paths {string[]}
*/ */
const readAndInsertCSS = async (window, paths): Promise<void> => { const readAndInsertCSS = async (window): Promise<IStyles[] | void> => {
return paths.map((filePath) => window.webContents.insertCSS(fs.readFileSync(filePath, 'utf8').toString())); if (window && windowExists(window)) {
return styles.map(({ content }) => window.webContents.insertCSS(content));
}
}; };
/** /**
@ -351,9 +372,10 @@ const readAndInsertCSS = async (window, paths): Promise<void> => {
* @param mainWindow {BrowserWindow} * @param mainWindow {BrowserWindow}
* @param isCustomTitleBar {boolean} - whether custom title bar enabled * @param isCustomTitleBar {boolean} - whether custom title bar enabled
*/ */
export const injectStyles = async (mainWindow: BrowserWindow, isCustomTitleBar: boolean): Promise<void> => { export const injectStyles = async (mainWindow: BrowserWindow, isCustomTitleBar: boolean): Promise<IStyles[] | void> => {
const paths: string[] = [];
if (isCustomTitleBar) { if (isCustomTitleBar) {
const index = styles.findIndex(({ name }) => name === styleNames.titleBar);
if (index === -1) {
let titleBarStylesPath; let titleBarStylesPath;
const stylesFileName = path.join('config', 'titleBarStyles.css'); const stylesFileName = path.join('config', 'titleBarStyles.css');
if (isDevEnv) { if (isDevEnv) {
@ -364,18 +386,22 @@ export const injectStyles = async (mainWindow: BrowserWindow, isCustomTitleBar:
} }
// Window custom title bar styles // Window custom title bar styles
if (fs.existsSync(titleBarStylesPath)) { if (fs.existsSync(titleBarStylesPath)) {
paths.push(titleBarStylesPath); styles.push({ name: styleNames.titleBar, content: fs.readFileSync(titleBarStylesPath, 'utf8').toString() });
} else { } else {
paths.push(path.join(__dirname, '..', '/renderer/styles/title-bar.css')); const stylePath = path.join(__dirname, '..', '/renderer/styles/title-bar.css');
styles.push({ name: styleNames.titleBar, content: fs.readFileSync(stylePath, 'utf8').toString() });
}
} }
} else {
paths.push(path.join(__dirname, '..', '/renderer/styles/title-bar.css'));
} }
// Snack bar styles // Snack bar styles
paths.push(path.join(__dirname, '..', '/renderer/styles/snack-bar.css')); if (styles.findIndex(({ name }) => name === styleNames.snackBar) === -1) {
styles.push({
name: styleNames.snackBar,
content: fs.readFileSync(path.join(__dirname, '..', '/renderer/styles/snack-bar.css'), 'utf8').toString(),
});
}
return await readAndInsertCSS(mainWindow, paths); return await readAndInsertCSS(mainWindow);
}; };
/** /**
@ -430,9 +456,9 @@ export const isSymphonyReachable = (window: ICustomBrowserWindow | null) => {
clearInterval(networkStatusCheckIntervalId); clearInterval(networkStatusCheckIntervalId);
networkStatusCheckIntervalId = null; networkStatusCheckIntervalId = null;
} }
} else { return;
logger.warn(`Symphony down! statusCode: ${rsp.status} is online: ${windowHandler.isOnline}`);
} }
logger.warn(`Symphony down! statusCode: ${rsp.status} is online: ${windowHandler.isOnline}`);
}).catch((error) => { }).catch((error) => {
logger.error(`Network status check: No active network connection ${error}`); logger.error(`Network status check: No active network connection ${error}`);
}); });

View File

@ -13,14 +13,14 @@ export class AnimationQueue {
* *
* @param object * @param object
*/ */
public push(object) { public push(object): void {
if (this.running) { if (this.running) {
this.queue.push(object); this.queue.push(object);
} else { return;
}
this.running = true; this.running = true;
setTimeout(() => this.animate(object), 0); setTimeout(() => this.animate(object), 0);
} }
}
/** /**
* Animates an animation that is part of the queue * Animates an animation that is part of the queue
@ -44,7 +44,7 @@ export class AnimationQueue {
/** /**
* Clears the queue * Clears the queue
*/ */
public clear() { public clear(): void {
this.queue = []; this.queue = [];
} }
} }

View File

@ -28,6 +28,7 @@ export enum apiCmds {
closeNotification = 'close-notification', closeNotification = 'close-notification',
initMainWindow = 'init-main-window', initMainWindow = 'init-main-window',
isMisspelled = 'is-misspelled', isMisspelled = 'is-misspelled',
memoryInfo = 'memory-info',
} }
export enum apiName { export enum apiName {
@ -37,6 +38,7 @@ export enum apiName {
} }
export interface IApiArgs { export interface IApiArgs {
memoryInfo: Electron.ProcessMemoryInfo;
word: string; word: string;
cmd: apiCmds; cmd: apiCmds;
isOnline: boolean; isOnline: boolean;

View File

@ -211,14 +211,15 @@ class Logger {
if (this.loggerWindow) { if (this.loggerWindow) {
this.loggerWindow.send('log', { msgs: [ logMsg ], logLevel: this.desiredLogLevel, showInConsole: this.showInConsole }); this.loggerWindow.send('log', { msgs: [ logMsg ], logLevel: this.desiredLogLevel, showInConsole: this.showInConsole });
} else { return;
}
this.logQueue.push(logMsg); this.logQueue.push(logMsg);
// don't store more than 100 msgs. keep most recent log msgs. // don't store more than 100 msgs. keep most recent log msgs.
if (this.logQueue.length > MAX_LOG_QUEUE_LENGTH) { if (this.logQueue.length > MAX_LOG_QUEUE_LENGTH) {
this.logQueue.shift(); this.logQueue.shift();
} }
} }
}
/** /**
* Cleans up logs older than a day * Cleans up logs older than a day

View File

@ -60,7 +60,7 @@ export default class AppBridge {
* *
* @param event * @param event
*/ */
private handleMessage(event): void { private async handleMessage(event): Promise<void> {
if (!AppBridge.isValidEvent(event)) { if (!AppBridge.isValidEvent(event)) {
return; return;
} }
@ -68,7 +68,11 @@ export default class AppBridge {
const { method, data } = event.data; const { method, data } = event.data;
switch (method) { switch (method) {
case apiCmds.getVersionInfo: case apiCmds.getVersionInfo:
this.broadcastMessage('get-version-info-callback', ssf.getVersionInfo()); const versionInfo = await ssf.getVersionInfo();
this.broadcastMessage('get-version-info-callback', {
requestId: data.requestId,
response: versionInfo,
});
break; break;
case apiCmds.activate: case apiCmds.activate:
ssf.activate(data as string); ssf.activate(data as string);
@ -120,6 +124,11 @@ export default class AppBridge {
case apiCmds.showNotificationSettings: case apiCmds.showNotificationSettings:
ssf.showNotificationSettings(); ssf.showNotificationSettings();
break; break;
case apiCmds.setIsInMeeting:
if (typeof data === 'boolean') {
ssf.setIsInMeeting(data as boolean);
}
break;
} }
} }

View File

@ -39,17 +39,19 @@ export default class WindowsTitleBar extends React.Component<{}, IState> {
this.window.on('leave-full-screen', () => this.updateState({ isFullScreen: false })); this.window.on('leave-full-screen', () => this.updateState({ isFullScreen: false }));
} }
public componentDidMount() { public componentDidMount(): void {
const contentWrapper = document.getElementById('content-wrapper'); const contentWrapper = document.getElementById('content-wrapper');
if (contentWrapper) { if (!contentWrapper) {
document.body.style.marginTop = this.state.titleBarHeight;
return;
}
if (this.state.isFullScreen) { if (this.state.isFullScreen) {
contentWrapper.style.marginTop = '0px'; contentWrapper.style.marginTop = '0px';
document.body.style.removeProperty('margin-top'); document.body.style.removeProperty('margin-top');
} else { return;
}
contentWrapper.style.marginTop = this.state.titleBarHeight; contentWrapper.style.marginTop = this.state.titleBarHeight;
} }
}
}
public componentWillMount() { public componentWillMount() {
this.window.removeListener('maximize', this.updateState); this.window.removeListener('maximize', this.updateState);
@ -146,7 +148,7 @@ export default class WindowsTitleBar extends React.Component<{}, IState> {
</svg> </svg>
</button> </button>
); );
} else { }
return ( return (
<button <button
className='title-bar-button' className='title-bar-button'
@ -163,7 +165,6 @@ export default class WindowsTitleBar extends React.Component<{}, IState> {
</button> </button>
); );
} }
}
/** /**
* Method that closes the browser window * Method that closes the browser window

View File

@ -16,6 +16,7 @@ interface ISSFWindow extends Window {
const ssfWindow: ISSFWindow = window; const ssfWindow: ISSFWindow = window;
const appBridge = new AppBridge(); const appBridge = new AppBridge();
const memoryInfoFetchInterval = 60 * 60 * 1000;
/** /**
* creates API exposed from electron. * creates API exposed from electron.
@ -87,5 +88,13 @@ ipcRenderer.on('page-load', (_event, { locale, resources, origin, enableCustomTi
ipcRenderer.send(apiName.symphonyApi, { ipcRenderer.send(apiName.symphonyApi, {
cmd: apiCmds.initMainWindow, cmd: apiCmds.initMainWindow,
}); });
setInterval(async () => {
const memoryInfo = await process.getProcessMemoryInfo();
ipcRenderer.send(apiName.symphonyApi, {
cmd: apiCmds.memoryInfo,
memoryInfo,
});
}, memoryInfoFetchInterval);
} }
}); });

View File

@ -79,6 +79,13 @@ const throttledCloseScreenShareIndicator = throttle((streamId) => {
}); });
}, 1000); }, 1000);
const throttledSetIsInMeetingStatus = throttle((isInMeeting) => {
local.ipcRenderer.send(apiName.symphonyApi, {
cmd: apiCmds.setIsInMeeting,
isInMeeting,
});
}, 1000);
let cryptoLib: ICryptoLib | null; let cryptoLib: ICryptoLib | null;
try { try {
cryptoLib = remote.require('../app/crypto-handler.js').cryptoLibrary; cryptoLib = remote.require('../app/crypto-handler.js').cryptoLibrary;
@ -134,17 +141,17 @@ export class SSFApi {
/** /**
* Method that returns various version info * Method that returns various version info
*/ */
public getVersionInfo(): IVersionInfo { public getVersionInfo(): Promise<IVersionInfo> {
const appName = remote.app.getName(); const appName = remote.app.getName();
const appVer = remote.app.getVersion(); const appVer = remote.app.getVersion();
return { return Promise.resolve({
containerIdentifier: appName, containerIdentifier: appName,
containerVer: appVer, containerVer: appVer,
buildNumber, buildNumber,
apiVer: '2.0.0', apiVer: '2.0.0',
searchApiVer: '3.0.0', searchApiVer: '3.0.0',
}; });
} }
/** /**
@ -265,6 +272,14 @@ export class SSFApi {
} }
} }
/**
* Sets if the user is in an active meeting
* will be used to handle memory refresh functionality
*/
public setIsInMeeting(isInMeeting): void {
throttledSetIsInMeetingStatus(isInMeeting);
}
/** /**
* Opens a modal window to configure notification preference. * Opens a modal window to configure notification preference.
*/ */

View File

@ -22,7 +22,7 @@
opacity: 0; opacity: 0;
} }
} }
.snackbar { .SnackBar {
font-family: @font-family; font-family: @font-family;
visibility: hidden; visibility: hidden;
min-width: 250px; min-width: 250px;