Merge TS context isolation branch onto Typescript master branch (#598)

* Typescript 🎉

* Typescript 🎉 (logger, get-guid, string-format and throttle)

* Refactor typescript code

* consolidate all the utility functions to one file

* refactor protocol handler feature

* Typescript:

Add code documentation
Add pre-commit hooks

* Typescript: Fix logger formatting

* Typescript: Add support for react

* Typescript: Completed about app

* Typescript: Completed about app

* Typescript: Completed about app

* Typescript - Fix issues with about-app and add login to convert less to css

* Typescript - Fix loading screen

* Typescript - Add custom title bar

* Typescript - Add method to get locale

* Typescript - Add logic to clean up old logs

* Typescript - Add set badge count api

* Typescript - Complete application menu

* Typescript - Add logic to translate menu items

* Typescript - freeze window.ssf api

* Typescript - Handle popup menu on alt key press

* Typescript - Completed activity detection

* Typescript - Completed screen snippet

* Typescript - Add login to close screen snippet

* Typescript - Completed window actions & snackbar, Updated i18n module

* Typescript - Completed native crypto implementation & fixed bugs

* Typescript - Completed Desktop capturer & screen picker implementation

* Typescript - Optimize window actions

* Typescript - Add support for child window

* Typescript - fix pop url validation issue & browserify preload

* Typescript - Completed context menu implementation and fixed screen snippet

* Typescript - Completed screen sharing indicator and fixed i18n usage issue

* Typescript - Fix i18n locale setting issue

* Typescript - Completed download manager

* Typescript - Completed Basic auth

* Typescript - Network connectivity dialog

* Typescript - Handle certificate error

* Typescript - Add translation for certificate error dialog buttons

* Typescript - Add gulp tasks to compile less, typescript and copy files

* Typescript - Fix some issues with custom title bar, loading screen & screen snippet

* Typescript - Remove ES2015 lib

* :typescript: - Do not inject custom title bar for mac

* :typescript: - Fix screen sharing indicator text and format string

* Typescript - Fix esc to full screen

* Typescript - handle multiple/single instance of the client and add safety checks

* Typescript - Refactor code base

* Typescript - Optimize window validation and fix screen picker issue

* Typescript - Optimize protocol handler

* typescript: logger unit test

* typescript: activityDetection unit test (#560)

* ELECTRON-1022 - Create app bridge that communicates between renderer and preload via postMessage

* ELECTRON-1024 - Add support for screen share and screen sharing indicator

* config unit test (#566)

* ELECTRON-1024 - Fix screen sharing indicator close issue

* ELECTRON-1022 - Bump Symphony version to 5.0.0 (1.55)

* fixing jest coverage output report (#575)

* protocol handle unit test (#576)

* Typescript - Remove unwanted checks in protocol handler and add test cases

* added more tests to increase coverage to 100 for protocol handler

* Typescript download manager unit test (#579)

* adding enzyme

* download manager unit test

* Typescript - Completed notification workflow

* about app unit test

* Typescript - Fix notification styles

* fixing Compiler error: Generic type ReactElement<P, T> (#583)

* fix app path on windows (#580)

* basic auth unit test (#582)

* screen picker unit test (#587)

* screen picker unit test

* screen sharing indicator unit test

* loading screen unit test (#588)

* improving snapshot using snapshotSerializers to remove unnecessary things (#596)

* Typescript - Enforce braces for if/for/do/while statements.

* Typescript - Fix Lint issues and Unit test

* Typescript - Enable eofline (Ensure the file ends with a newline.)

* Typescript - Update logger logic and format

* Typescript - Provide option for user to set custom log path

* Typescript - Fix eofline in css files

* Typescript - ignore spec from compiling and remove unwanted rebuild command
This commit is contained in:
Kiran Niranjan 2019-03-19 16:22:39 +05:30 committed by Kiran Niranjan
parent e3f4830c4a
commit 13e82bac00
49 changed files with 1577 additions and 252 deletions

View File

@ -132,7 +132,9 @@
num++;
var notf = new ssf.Notification(title, {
var notf = {
id: num,
title,
body: (body + ' num=' + num + ' tag=' + tag),
image: imageUrl,
flash: shouldFlash,
@ -142,24 +144,11 @@
hello: 'hello word'
},
tag: tag,
company: company
});
notf.addEventListener('click', onclick);
function onclick(event) {
event.target.close();
alert('notification clicked: ' + event.target.data.hello);
}
notf.addEventListener('close', onclose);
function onclose() {
alert('notification closed');
company: company,
method: 'notification',
};
notf.addEventListener('error', onerror);
function onerror(event) {
alert('error=' + event.result);
};
window.postMessage({ method: 'notification', data: notf }, '*');
});
var badgeCount = 0;
@ -292,5 +281,7 @@
document.location.reload();
});
window.addEventListener('message', (event) => console.log(event));
</script>
</html>

View File

@ -12,7 +12,7 @@ gulp.task('clean', function() {
});
gulp.task('compile', function() {
return gulp.src(['src/**/*.ts'])
return gulp.src(['src/**/*.ts', 'src/**/*.tsx'])
.pipe(tsc({ project: './tsconfig.json' }))
.pipe(gulp.dest('lib/'))
});
@ -35,4 +35,4 @@ gulp.task('copy', function () {
}).pipe(gulp.dest('lib/src'))
});
gulp.task('build', gulp.series('clean', 'compile', 'less', 'copy'));
gulp.task('build', gulp.series('clean', 'compile', 'less', 'copy'));

View File

@ -1,8 +1,8 @@
{
"name": "Symphony",
"productName": "Symphony",
"version": "4.5.0",
"clientVersion": "1.55.0",
"version": "5.0.0",
"clientVersion": "1.55",
"buildNumber": "0",
"description": "Symphony desktop app (Foundation ODP)",
"author": "Symphony",
@ -16,7 +16,7 @@
"browserify-preload": "browserify -o lib/src/renderer/_preload-main.js -x electron --insert-global-vars=__filename,__dirname lib/src/renderer/preload-main.js",
"rebuild": "electron-rebuild -f",
"dev": "npm run prebuild && cross-env ELECTRON_DEV=true electron .",
"test": "npm run lint && npm rebuild --build-from-source && cross-env ELECTRON_QA=true jest --config jest.unit.config.json --runInBand && npm run rebuild",
"test": "npm run lint && cross-env ELECTRON_QA=true jest --config jest.unit.config.json --runInBand",
"demo-win": "npm run prebuild && cross-env ELECTRON_DEV=true electron . --url=file:///demo/index.html",
"demo-mac": "npm run prebuild && cross-env ELECTRON_DEV=true electron . --url=file://$(pwd)/demo/index.html",
"unpacked-mac": "npm run prebuild && npm run test && build --mac --dir",

View File

@ -30,9 +30,11 @@ describe('screen sharing indicator', () => {
const closeIpcRendererMock = {
cmd: 'close-window',
windowType: 'screen-sharing-indicator',
winKey: 'id-123',
};
const spy = jest.spyOn(ipcRenderer, sendEventLabel);
const wrapper = shallow(React.createElement(ScreenSharingIndicator));
wrapper.setState({ streamId: 'id-123' });
wrapper.find(customSelector).simulate('click');
expect(spy).lastCalledWith(symphonyAPIEventLabel, closeIpcRendererMock);
});

View File

@ -46,7 +46,9 @@ class ActivityDetection {
const idleTimeInMillis = idleTime * 1000;
if (idleTimeInMillis < this.idleThreshold) {
this.sendActivity(idleTimeInMillis);
if (this.timer) clearInterval(this.timer);
if (this.timer) {
clearInterval(this.timer);
}
this.timer = undefined;
logger.info(`activity-detection: activity occurred`);
return;
@ -78,4 +80,4 @@ class ActivityDetection {
const activityDetection = new ActivityDetection();
export { activityDetection };
export { activityDetection };

View File

@ -22,4 +22,4 @@ export const cleanUpAppCache = async (): Promise<void> => {
*/
export const createAppCacheFile = (): void => {
fs.writeFileSync(cacheCheckFilePath, '');
};
};

View File

@ -111,4 +111,4 @@ const autoLaunchInstance = new AutoLaunchController(props);
export {
autoLaunchInstance,
};
};

View File

@ -33,8 +33,12 @@ const getParsedUrl = (configURL: string): Url => {
export const handleChildWindow = (webContents: WebContents): void => {
const childWindow = (event, newWinUrl, frameName, disposition, newWinOptions): void => {
const mainWindow = windowHandler.getMainWindow();
if (!mainWindow || mainWindow.isDestroyed()) return;
if (!windowHandler.url) return;
if (!mainWindow || mainWindow.isDestroyed()) {
return;
}
if (!windowHandler.url) {
return;
}
if (!newWinOptions.webPreferences) {
newWinOptions.webPreferences = {};
@ -104,7 +108,9 @@ export const handleChildWindow = (webContents: WebContents): void => {
childWebContents.once('did-finish-load', async () => {
const browserWin: ICustomBrowserWindow = BrowserWindow.fromWebContents(childWebContents) as ICustomBrowserWindow;
if (!browserWin) return;
if (!browserWin) {
return;
}
windowHandler.addWindow(newWinKey, browserWin);
browserWin.webContents.send('page-load', { isWindowsOS });
// Inserts css on to the window
@ -129,4 +135,4 @@ export const handleChildWindow = (webContents: WebContents): void => {
}
};
webContents.on('new-window', childWindow);
};
};

View File

@ -63,4 +63,4 @@ export const setChromeFlags = () => {
}
}
}
};
};

View File

@ -224,4 +224,4 @@ const config = new Config();
export {
config,
};
};

View File

@ -10,7 +10,7 @@ import { logger } from '../common/logger';
const TAG_LENGTH = 16;
const arch = process.arch === 'ia32';
const winLibraryPath = isDevEnv ? path.join(__dirname, '..', '..', 'library') : path.join(execPath, 'library');
const macLibraryPath = isDevEnv ? path.join(__dirname, '..', '..', 'library') : path.join(execPath, '..', 'library');
const macLibraryPath = isDevEnv ? path.join(__dirname, '..', '..', '..', 'library') : path.join(execPath, '..', 'library');
const cryptoLibPath = isMac ?
path.join(macLibraryPath, 'cryptoLib.dylib') :
@ -133,4 +133,4 @@ class CryptoLibrary implements ICryptoLib {
const cryptoLibrary = new CryptoLibrary();
export { cryptoLibrary };
export { cryptoLibrary };

View File

@ -96,8 +96,12 @@ electron.app.on('certificate-error', (event, webContents, url, error, _certifica
*/
export const showLoadFailure = (browserWindow: Electron.BrowserWindow, url: string, errorDesc: string, errorCode: number, retryCallback: () => void, showDialog: boolean): void => {
let message = url ? `${i18n.t('Error loading URL')()}:\n${url}` : i18n.t('Error loading window')();
if (errorDesc) message += `\n\n${errorDesc}`;
if (errorCode) message += `\n\nError Code: ${errorCode}`;
if (errorDesc) {
message += `\n\n${errorDesc}`;
}
if (errorCode) {
message += `\n\nError Code: ${errorCode}`;
}
// async handle of user input
const response = (buttonId: number): void => {
@ -132,4 +136,4 @@ export const showLoadFailure = (browserWindow: Electron.BrowserWindow, url: stri
export const showNetworkConnectivityError = (browserWindow: Electron.BrowserWindow, url: string = '', retryCallback: () => void): void => {
const errorDesc = i18n.t('Network connectivity has been lost. Check your internet connection.')();
showLoadFailure(browserWindow, url, errorDesc, 0, retryCallback, true);
};
};

View File

@ -80,7 +80,9 @@ ipcMain.on(apiName.symphonyApi, (event: Electron.Event, arg: IApiArgs) => {
// validates the user bring to front config and activates the wrapper
if (typeof arg.reason === 'string' && arg.reason === 'notification') {
const shouldBringToFront = config.getConfigFields([ 'bringToFront' ]);
if (shouldBringToFront) activate(arg.windowName, false);
if (shouldBringToFront) {
activate(arg.windowName, false);
}
}
break;
case apiCmds.openScreenPickerWindow:
@ -95,21 +97,6 @@ ipcMain.on(apiName.symphonyApi, (event: Electron.Event, arg: IApiArgs) => {
}
break;
}
/*case ApiCmds.optimizeMemoryConsumption:
if (typeof arg.memory === 'object'
&& typeof arg.cpuUsage === 'object'
&& typeof arg.memory.workingSetSize === 'number') {
setPreloadMemoryInfo(arg.memory, arg.cpuUsage);
}
break;
case ApiCmds.optimizeMemoryRegister:
setPreloadWindow(event.sender);
break;
case ApiCmds.setIsInMeeting:
if (typeof arg.isInMeeting === 'boolean') {
setIsInMeeting(arg.isInMeeting);
}
break;*/
case apiCmds.setLocale:
if (typeof arg.locale === 'string') {
updateLocale(arg.locale as LocaleType);
@ -124,12 +111,12 @@ ipcMain.on(apiName.symphonyApi, (event: Electron.Event, arg: IApiArgs) => {
screenSnippet.capture(event.sender);
break;
case apiCmds.closeWindow:
windowHandler.closeWindow(arg.windowType);
windowHandler.closeWindow(arg.windowType, arg.winKey);
break;
case apiCmds.openScreenSharingIndicator:
const { displayId, id } = arg;
if (typeof displayId === 'string' && typeof id === 'number') {
windowHandler.createScreenSharingIndicatorWindow(event.sender, displayId, id);
const { displayId, id, streamId } = arg;
if (typeof displayId === 'string' && typeof id === 'number' && typeof streamId === 'string') {
windowHandler.createScreenSharingIndicatorWindow(event.sender, displayId, id, streamId);
}
break;
case apiCmds.downloadManagerAction:

View File

@ -66,7 +66,9 @@ if (!allowMultiInstance) {
// Someone tried to run a second instance, we should focus our window.
const mainWindow = windowHandler.getMainWindow();
if (mainWindow && !mainWindow.isDestroyed()) {
if (isMac) return mainWindow.show();
if (isMac) {
return mainWindow.show();
}
if (mainWindow.isMinimized()) {
mainWindow.restore();
}
@ -117,4 +119,4 @@ app.on('activate', () => {
*
* This event is emitted only on macOS at this moment
*/
app.on('open-url', (_event, url) => protocolHandler.sendProtocol(url));
app.on('open-url', (_event, url) => protocolHandler.sendProtocol(url));

View File

@ -69,4 +69,4 @@ class ProtocolHandler {
const protocolHandler = new ProtocolHandler();
export { protocolHandler };
export { protocolHandler };

View File

@ -131,4 +131,4 @@ export const exportCrashDumps = (): void => {
});
}
});
};
};

View File

@ -53,7 +53,9 @@ class ScreenSnippet {
updateAlwaysOnTop(false, false);
}
// only allow one screen capture at a time.
if (this.child) this.child.kill();
if (this.child) {
this.child.kill();
}
try {
await this.execCmd(this.captureUtil, this.captureUtilArgs);
const { message, data, type }: IScreenSnippet = await this.convertFileToData();
@ -86,7 +88,9 @@ class ScreenSnippet {
private execCmd(captureUtil: string, captureUtilArgs: ReadonlyArray<string>): Promise<ChildProcess> {
return new Promise<ChildProcess>((resolve, reject) => {
return this.child = execFile(captureUtil, captureUtilArgs, (error: ExecException | null) => {
if (this.isAlwaysOnTop) updateAlwaysOnTop(true, false);
if (this.isAlwaysOnTop) {
updateAlwaysOnTop(true, false);
}
if (error && error.killed) {
// processs was killed, just resolve with no data.
return reject(error);
@ -140,4 +144,4 @@ class ScreenSnippet {
const screenSnippet = new ScreenSnippet();
export { screenSnippet };
export { screenSnippet };

View File

@ -99,4 +99,4 @@ export class SpellChecker {
}
return menu;
}
}
}

View File

@ -1,7 +1,7 @@
import { BrowserWindow } from 'electron';
import { apiName, IBoundsChange, KeyCodes } from '../common/api-interface';
import { isWindowsOS } from '../common/env';
import { isMac, isWindowsOS } from '../common/env';
import { throttle } from '../common/utils';
import { config } from './config-handler';
import { ICustomBrowserWindow, windowHandler } from './window-handler';
@ -14,7 +14,7 @@ export const saveWindowSettings = (): void => {
const [ x, y ] = browserWindow.getPosition();
const [ width, height ] = browserWindow.getSize();
if (x && y && width && height) {
browserWindow.webContents.send('boundChanges', { x, y, width, height, windowName: browserWindow.winName } as IBoundsChange);
browserWindow.webContents.send('boundsChange', { x, y, width, height, windowName: browserWindow.winName } as IBoundsChange);
if (browserWindow.winName === apiName.mainWindowName) {
const isMaximized = browserWindow.isMaximized();
@ -54,19 +54,26 @@ export const throttledWindowChanges = throttle(saveWindowSettings, 1000);
export const activate = (windowName: string, shouldFocus: boolean = true): void => {
// Electron-136: don't activate when the app is reloaded programmatically
if (windowHandler.isAutoReload) return;
if (windowHandler.isAutoReload) {
return;
}
const windows = windowHandler.getAllWindows();
for (const key in windows) {
if (windows.hasOwnProperty(key)) {
if (Object.prototype.hasOwnProperty.call(windows, key)) {
const window = windows[ key ];
if (window && !window.isDestroyed() && window.winName === windowName) {
// Bring the window to the top without focusing
// Flash task bar icon in Windows for windows
if (!shouldFocus) {
window.moveTop();
return isWindowsOS ? window.flashFrame(true) : null;
return isMac ? window.showInactive() : window.flashFrame(true);
}
// Note: On window just focusing will preserve window snapped state
// Hiding the window and just calling the focus() won't display the window
if (isWindowsOS) {
return window.isMinimized() ? window.restore() : window.focus();
}
return window.isMinimized() ? window.restore() : window.show();
@ -130,11 +137,15 @@ export const handleKeyPress = (key: number): void => {
* @param window {BrowserWindow}
*/
export const monitorWindowActions = (window: BrowserWindow): void => {
if (!window || window.isDestroyed()) return;
if (!window || window.isDestroyed()) {
return;
}
const eventNames = [ 'move', 'resize', 'maximize', 'unmaximize' ];
eventNames.forEach((event: string) => {
// @ts-ignore
if (window) window.on(event, throttledWindowChanges);
if (window) {
// @ts-ignore
window.on(event, throttledWindowChanges);
}
});
window.on('enter-full-screen', enterFullScreen);
window.on('leave-full-screen', leaveFullScreen);
@ -146,12 +157,16 @@ export const monitorWindowActions = (window: BrowserWindow): void => {
* @param window
*/
export const removeWindowEventListener = (window: BrowserWindow): void => {
if (!window || window.isDestroyed()) return;
if (!window || window.isDestroyed()) {
return;
}
const eventNames = [ 'move', 'resize', 'maximize', 'unmaximize' ];
eventNames.forEach((event: string) => {
// @ts-ignore
if (window) window.removeListener(event, throttledWindowChanges);
if (window) {
// @ts-ignore
window.removeListener(event, throttledWindowChanges);
}
});
window.removeListener('enter-full-screen', enterFullScreen);
window.removeListener('leave-full-screen', leaveFullScreen);
};
};

View File

@ -14,7 +14,7 @@ import { handleChildWindow } from './child-window-handler';
import { config, IConfig } from './config-handler';
import { showNetworkConnectivityError } from './dialog-handler';
import { monitorWindowActions } from './window-actions';
import { createComponentWindow, getBounds, handleDownloadManager, injectStyles } from './window-utils';
import { createComponentWindow, getBounds, handleDownloadManager, injectStyles, windowExists } from './window-utils';
interface ICustomBrowserWindowConstructorOpts extends Electron.BrowserWindowConstructorOptions {
winKey: string;
@ -189,7 +189,7 @@ export class WindowHandler {
// Event needed to hide native menu bar on Windows 10 as we use custom menu bar
this.mainWindow.webContents.once('did-start-loading', () => {
if ((this.config.isCustomTitleBar || isWindowsOS) && this.mainWindow && this.windowExists(this.mainWindow)) {
if ((this.config.isCustomTitleBar || isWindowsOS) && this.mainWindow && windowExists(this.mainWindow)) {
this.mainWindow.setMenuBarVisibility(false);
}
});
@ -204,25 +204,36 @@ export class WindowHandler {
// Displays a dialog if network connectivity has been lost
const retry = () => {
if (!this.mainWindow) return;
if (!this.isOnline) showNetworkConnectivityError(this.mainWindow, this.url, retry);
if (!this.mainWindow) {
return;
}
if (!this.isOnline) {
showNetworkConnectivityError(this.mainWindow, this.url, retry);
}
this.mainWindow.webContents.reload();
};
if (!this.isOnline && this.mainWindow) showNetworkConnectivityError(this.mainWindow, this.url, retry);
if (!this.isOnline && this.mainWindow) {
showNetworkConnectivityError(this.mainWindow, this.url, retry);
}
// early exit if the window has already been destroyed
if (!this.mainWindow || !this.windowExists(this.mainWindow)) return;
if (!this.mainWindow || !windowExists(this.mainWindow)) {
return;
}
this.url = this.mainWindow.webContents.getURL();
// Injects custom title bar css into the webContents
// only for Window and if it is enabled
await injectStyles(this.mainWindow, this.isCustomTitleBarAndWindowOS);
if (this.isCustomTitleBarAndWindowOS) this.mainWindow.webContents.send('initiate-custom-title-bar');
if (this.isCustomTitleBarAndWindowOS) {
this.mainWindow.webContents.send('initiate-custom-title-bar');
}
this.mainWindow.webContents.send('page-load', {
isWindowsOS,
locale: i18n.getLocale(),
resources: i18n.loadedResources,
origin: this.globalConfig.url,
});
this.appMenu = new AppMenu();
@ -239,9 +250,13 @@ export class WindowHandler {
// Handle main window close
this.mainWindow.on('close', (event) => {
if (!this.mainWindow || !this.windowExists(this.mainWindow)) return;
if (!this.mainWindow || !windowExists(this.mainWindow)) {
return;
}
if (this.willQuitApp) return this.destroyAllWindow();
if (this.willQuitApp) {
return this.destroyAllWindow();
}
if (this.config.minimizeOnClose) {
event.preventDefault();
@ -285,16 +300,23 @@ export class WindowHandler {
/**
* Closes the window from an event emitted by the render processes
*
* @param windowType
* @param windowType {WindowTypes}
* @param winKey {string} - Unique ID assigned to the window
*/
public closeWindow(windowType: WindowTypes): void {
public closeWindow(windowType: WindowTypes, winKey?: string): void {
switch (windowType) {
case 'screen-picker':
if (this.screenPickerWindow && this.windowExists(this.screenPickerWindow)) this.screenPickerWindow.close();
if (this.screenPickerWindow && windowExists(this.screenPickerWindow)) {
this.screenPickerWindow.close();
}
break;
case 'screen-sharing-indicator':
if (this.screenSharingIndicatorWindow
&& this.windowExists(this.screenSharingIndicatorWindow)) this.screenSharingIndicatorWindow.close();
if (winKey) {
const browserWindow = this.windows[ winKey ];
if (browserWindow && windowExists(browserWindow)) {
browserWindow.close();
}
}
break;
default:
break;
@ -329,7 +351,9 @@ export class WindowHandler {
public showLoadingScreen(): void {
this.loadingWindow = createComponentWindow('loading-screen', WindowHandler.getLoadingWindowOpts());
this.loadingWindow.webContents.once('did-finish-load', () => {
if (!this.loadingWindow || !this.windowExists(this.loadingWindow)) return;
if (!this.loadingWindow || !windowExists(this.loadingWindow)) {
return;
}
this.loadingWindow.webContents.send('data');
});
@ -342,7 +366,9 @@ export class WindowHandler {
public createAboutAppWindow(): void {
this.aboutAppWindow = createComponentWindow('about-app');
this.aboutAppWindow.webContents.once('did-finish-load', () => {
if (!this.aboutAppWindow || !this.windowExists(this.aboutAppWindow)) return;
if (!this.aboutAppWindow || !windowExists(this.aboutAppWindow)) {
return;
}
this.aboutAppWindow.webContents.send('about-app-data', { buildNumber, clientVersion, version });
});
}
@ -353,7 +379,9 @@ export class WindowHandler {
public createMoreInfoWindow(): void {
this.moreInfoWindow = createComponentWindow('more-info');
this.moreInfoWindow.webContents.once('did-finish-load', () => {
if (!this.moreInfoWindow || !this.windowExists(this.moreInfoWindow)) return;
if (!this.moreInfoWindow || !windowExists(this.moreInfoWindow)) {
return;
}
this.moreInfoWindow.webContents.send('more-info-data');
});
}
@ -367,18 +395,22 @@ export class WindowHandler {
*/
public createScreenPickerWindow(window: Electron.WebContents, sources: DesktopCapturerSource[], id: number): void {
if (this.screenPickerWindow && this.windowExists(this.screenPickerWindow)) this.screenPickerWindow.close();
if (this.screenPickerWindow && windowExists(this.screenPickerWindow)) {
this.screenPickerWindow.close();
}
const opts = WindowHandler.getScreenPickerWindowOpts();
this.screenPickerWindow = createComponentWindow('screen-picker', opts);
this.screenPickerWindow.webContents.once('did-finish-load', () => {
if (!this.screenPickerWindow || !this.windowExists(this.screenPickerWindow)) return;
if (!this.screenPickerWindow || !windowExists(this.screenPickerWindow)) {
return;
}
this.screenPickerWindow.webContents.send('screen-picker-data', { sources, id });
this.addWindow(opts.winKey, this.screenPickerWindow);
});
ipcMain.once('screen-source-selected', (_event, source) => {
window.send('start-share' + id, source);
if (this.screenPickerWindow && this.windowExists(this.screenPickerWindow)) {
if (this.screenPickerWindow && windowExists(this.screenPickerWindow)) {
this.screenPickerWindow.close();
}
});
@ -388,50 +420,6 @@ export class WindowHandler {
});
}
/**
* Creates a screen sharing indicator whenever uses start
* sharing the screen
*
* @param screenSharingWebContents {Electron.webContents}
* @param displayId {string}
* @param id {number}
*/
public createScreenSharingIndicatorWindow(screenSharingWebContents: Electron.webContents, displayId: string, id: number): void {
if (this.screenSharingIndicatorWindow
&& this.windowExists(this.screenSharingIndicatorWindow)) this.screenSharingIndicatorWindow.close();
const indicatorScreen =
(displayId && electron.screen.getAllDisplays().filter((d) =>
displayId.includes(d.id.toString()))[ 0 ]) || electron.screen.getPrimaryDisplay();
const screenRect = indicatorScreen.workArea;
let opts = WindowHandler.getScreenSharingIndicatorOpts();
if (opts.width && opts.height) {
opts = Object.assign({}, opts, {
x: screenRect.x + Math.round((screenRect.width - opts.width) / 2),
y: screenRect.y + screenRect.height - opts.height,
});
}
this.screenSharingIndicatorWindow = createComponentWindow('screen-sharing-indicator', opts);
this.screenSharingIndicatorWindow.setVisibleOnAllWorkspaces(true);
this.screenSharingIndicatorWindow.webContents.once('did-finish-load', () => {
if (!this.screenSharingIndicatorWindow || !this.windowExists(this.screenSharingIndicatorWindow)) return;
this.screenSharingIndicatorWindow.webContents.send('screen-sharing-indicator-data', { id });
});
const stopScreenSharing = (_event, indicatorId) => {
if (id === indicatorId) {
screenSharingWebContents.send('screen-sharing-stopped', id);
}
};
this.screenSharingIndicatorWindow.once('close', () => {
ipcMain.removeListener('stop-screen-sharing', stopScreenSharing);
});
ipcMain.once('stop-screen-sharing', stopScreenSharing);
}
/**
* Creates a Basic auth window whenever the network
* requires authentications
@ -450,12 +438,16 @@ export class WindowHandler {
this.basicAuthWindow = createComponentWindow('basic-auth', opts);
this.basicAuthWindow.setVisibleOnAllWorkspaces(true);
this.basicAuthWindow.webContents.once('did-finish-load', () => {
if (!this.basicAuthWindow || !this.windowExists(this.basicAuthWindow)) return;
if (!this.basicAuthWindow || !windowExists(this.basicAuthWindow)) {
return;
}
this.basicAuthWindow.webContents.send('basic-auth-data', { hostname, isValidCredentials: isMultipleTries });
});
const closeBasicAuth = (shouldClearSettings = true) => {
if (shouldClearSettings) clearSettings();
if (this.basicAuthWindow && !this.windowExists(this.basicAuthWindow)) {
if (shouldClearSettings) {
clearSettings();
}
if (this.basicAuthWindow && !windowExists(this.basicAuthWindow)) {
this.basicAuthWindow.close();
this.basicAuthWindow = null;
}
@ -476,6 +468,58 @@ export class WindowHandler {
ipcMain.once('basic-auth-login', login);
}
/**
* Creates a screen sharing indicator whenever uses start
* sharing the screen
*
* @param screenSharingWebContents {Electron.webContents}
* @param displayId {string} - current display id
* @param id {number} - postMessage request id
* @param streamId {string} - MediaStream id
*/
public createScreenSharingIndicatorWindow(
screenSharingWebContents: Electron.webContents,
displayId: string,
id: number,
streamId,
): void {
const indicatorScreen =
(displayId && electron.screen.getAllDisplays().filter((d) =>
displayId.includes(d.id.toString()))[ 0 ]) || electron.screen.getPrimaryDisplay();
const screenRect = indicatorScreen.workArea;
// Set stream id as winKey to link stream to the window
let opts = { ...WindowHandler.getScreenSharingIndicatorOpts(), ...{ winKey: streamId } };
if (opts.width && opts.height) {
opts = Object.assign({}, opts, {
x: screenRect.x + Math.round((screenRect.width - opts.width) / 2),
y: screenRect.y + screenRect.height - opts.height,
});
}
this.screenSharingIndicatorWindow = createComponentWindow('screen-sharing-indicator', opts);
this.screenSharingIndicatorWindow.setVisibleOnAllWorkspaces(true);
this.screenSharingIndicatorWindow.webContents.once('did-finish-load', () => {
if (!this.screenSharingIndicatorWindow || !windowExists(this.screenSharingIndicatorWindow)) {
return;
}
this.screenSharingIndicatorWindow.webContents.send('screen-sharing-indicator-data', { id, streamId });
});
const stopScreenSharing = (_event, indicatorId) => {
if (id === indicatorId) {
screenSharingWebContents.send('screen-sharing-stopped', id);
}
};
this.addWindow(opts.winKey, this.screenSharingIndicatorWindow);
this.screenSharingIndicatorWindow.once('close', () => {
this.removeWindow(streamId);
ipcMain.removeListener('stop-screen-sharing', stopScreenSharing);
});
ipcMain.once('stop-screen-sharing', stopScreenSharing);
}
/**
* Opens an external url in the system's default browser
*
@ -519,14 +563,6 @@ export class WindowHandler {
this.mainWindow = null;
}
/**
* Checks if window is valid and exists
*
* @param window
* @return boolean
*/
private windowExists = (window: BrowserWindow): boolean => !!window && typeof window.isDestroyed === 'function' && !window.isDestroyed();
/**
* Main window opts
*/
@ -542,6 +578,7 @@ export class WindowHandler {
nodeIntegration: false,
preload: path.join(__dirname, '../renderer/_preload-main.js'),
sandbox: true,
contextIsolation: true,
},
winKey: getGuid(),
};
@ -550,4 +587,4 @@ export class WindowHandler {
const windowHandler = new WindowHandler();
export { windowHandler };
export { windowHandler };

View File

@ -29,7 +29,9 @@ export const preventWindowNavigation = (browserWindow: Electron.BrowserWindow, i
if (browserWindow.isDestroyed()
|| browserWindow.webContents.isDestroyed()
|| winUrl === browserWindow.webContents.getURL()) return;
|| winUrl === browserWindow.webContents.getURL()) {
return;
}
e.preventDefault();
};
@ -46,10 +48,12 @@ export const preventWindowNavigation = (browserWindow: Electron.BrowserWindow, i
*
* @param componentName
* @param opts
* @param shouldFocus {boolean}
*/
export const createComponentWindow = (
componentName: string,
opts?: Electron.BrowserWindowConstructorOptions,
shouldFocus: boolean = true,
): BrowserWindow => {
const options: Electron.BrowserWindowConstructorOptions = {
@ -67,9 +71,13 @@ export const createComponentWindow = (
};
const browserWindow: ICustomBrowserWindow = new BrowserWindow(options) as ICustomBrowserWindow;
browserWindow.on('ready-to-show', () => browserWindow.show());
if (shouldFocus) {
browserWindow.once('ready-to-show', () => browserWindow.show());
}
browserWindow.webContents.once('did-finish-load', () => {
if (!browserWindow || browserWindow.isDestroyed()) return;
if (!browserWindow || browserWindow.isDestroyed()) {
return;
}
browserWindow.webContents.send('set-locale-resource', { locale: i18n.getLocale(), resource: i18n.loadedResources });
});
browserWindow.setMenu(null as any);
@ -169,7 +177,9 @@ export const updateLocale = (locale: LocaleType): void => {
// sets the new locale
i18n.setLocale(locale);
const appMenu = windowHandler.appMenu;
if (appMenu) appMenu.update(locale);
if (appMenu) {
appMenu.update(locale);
}
};
/**
@ -181,7 +191,9 @@ export const showPopupMenu = (opts: Electron.PopupOptions): void => {
const { x, y } = mainWindow.isFullScreen() ? { x: 0, y: 0 } : { x: 10, y: -20 };
const popupOpts = { window: mainWindow, x, y };
const appMenu = windowHandler.appMenu;
if (appMenu) appMenu.popupMenu({ ...popupOpts, ...opts });
if (appMenu) {
appMenu.popupMenu({ ...popupOpts, ...opts });
}
}
};
@ -219,7 +231,9 @@ export const sanitize = (windowName: string): void => {
* @return {x?: Number, y?: Number, width: Number, height: Number}
*/
export const getBounds = (winPos: Electron.Rectangle, defaultWidth: number, defaultHeight: number): Partial<Electron.Rectangle> => {
if (!winPos) return { width: defaultWidth, height: defaultHeight };
if (!winPos) {
return { width: defaultWidth, height: defaultHeight };
}
const displays = electron.screen.getAllDisplays();
for (let i = 0, len = displays.length; i < len; i++) {
@ -327,3 +341,11 @@ export const injectStyles = async (mainWindow: BrowserWindow, isCustomTitleBarAn
return await readAndInsertCSS(mainWindow, paths);
};
/**
* Checks if window is valid and exists
*
* @param window {BrowserWindow}
* @return boolean
*/
export const windowExists = (window: BrowserWindow): boolean => !!window && typeof window.isDestroyed === 'function' && !window.isDestroyed();

View File

@ -0,0 +1,50 @@
import { logger } from './logger';
export class AnimationQueue {
private queue: any[] = [];
private running: boolean = false;
constructor() {
this.animate = this.animate.bind(this);
}
/**
* Pushes each animation to a queue
*
* @param object
*/
public push(object) {
if (this.running) {
this.queue.push(object);
} else {
this.running = true;
setTimeout(() => this.animate(object), 0);
}
}
/**
* Animates an animation that is part of the queue
* @param object
*/
public async animate(object): Promise<void> {
try {
await object.func.apply(null, object.args);
} catch (err) {
logger.error(`animationQueue: encountered an error: ${err} with stack trace: ${err.stack}`);
} finally {
if (this.queue.length > 0) {
// Run next animation
this.animate.call(this, this.queue.shift());
} else {
this.running = false;
}
}
}
/**
* Clears the queue
*/
public clear() {
this.queue = [];
}
}

View File

@ -1,5 +1,6 @@
export enum apiCmds {
isOnline = 'is-online',
getVersionInfo = 'get-version-info',
registerLogger = 'register-logger',
setBadgeCount = 'set-badge-count',
badgeDataUrl = 'badge-data-url',
@ -20,7 +21,11 @@ export enum apiCmds {
keyPress = 'key-press',
closeWindow = 'close-window',
openScreenSharingIndicator = 'open-screen-sharing-indicator',
closeScreenSharingIndicator = 'close-screen-sharing-indicator',
downloadManagerAction = 'download-manager-action',
getMediaSource = 'get-media-source',
notification = 'notification',
closeNotification = 'close-notification',
}
export enum apiName {
@ -43,6 +48,8 @@ export interface IApiArgs {
locale: string;
keyCode: number;
windowType: WindowTypes;
winKey: string;
streamId: string;
displayId: string;
path: string;
type: string;
@ -50,13 +57,6 @@ export interface IApiArgs {
export type WindowTypes = 'screen-picker' | 'screen-sharing-indicator';
/**
* Activity detection
*/
export interface IActivityDetection {
idleTime: number;
}
export interface IBadgeCount {
count: number;
}
@ -80,10 +80,28 @@ export interface IBoundsChange extends Electron.Rectangle {
*/
export interface IScreenSharingIndicator {
type: string;
requestId: number;
reason?: string;
}
export enum KeyCodes {
Esc = 27,
Alt = 18,
}
}
export interface IVersionInfo {
containerIdentifier: string;
containerVer: string;
buildNumber: string;
apiVer: string;
searchApiVer: string;
}
export interface ILogMsg {
level: LogLevel;
details: any;
showInConsole: boolean;
startTime: number;
}
export type LogLevel = 'error' | 'warn' | 'info' | 'verbose' | 'debug' | 'silly';

View File

@ -5,4 +5,4 @@ export const isElectronQA = !!process.env.ELECTRON_QA;
export const isMac = (process.platform === 'darwin');
export const isWindowsOS = (process.platform === 'win32');
export const isNodeEnv = !!process.env.NODE_ENV;
export const isNodeEnv = !!process.env.NODE_ENV;

View File

@ -85,4 +85,4 @@ class Translation {
const i18n = new Translation();
export { i18n };
export { i18n };

View File

@ -82,4 +82,4 @@ class Translation {
const i18n = new Translation();
export { i18n };
export { i18n };

View File

@ -33,13 +33,19 @@ class Logger {
this.loggerWindow = null;
this.logQueue = [];
// If the user has specified a custom log path use it.
const customLogPathArg = getCommandLineArgs(process.argv, '--logPath=', false);
const customLogsFolder = customLogPathArg && customLogPathArg.substring(customLogPathArg.indexOf('=') + 1);
if (customLogsFolder && fs.existsSync(customLogsFolder)) {
app.setPath('logs', customLogsFolder);
}
this.logPath = app.getPath('logs');
if (!isElectronQA) {
transports.file.file = path.join(this.logPath, 'app.log');
transports.file.file = path.join(this.logPath, `app_${Date.now()}.log`);
transports.file.level = 'debug';
transports.file.format = '{h}:{i}:{s}:{ms} {text}';
transports.file.maxSize = 10 * 1024 * 1024;
transports.file.format = '{y}-{m}-{d} {h}:{i}:{s}:{ms} {z} | {level} | {text}';
transports.file.appName = 'Symphony';
}
@ -136,9 +142,15 @@ class Logger {
if (this.loggerWindow) {
const logMsgs: IClientLogMsg = {};
if (this.logQueue.length) logMsgs.msgs = this.logQueue;
if (this.desiredLogLevel) logMsgs.logLevel = this.desiredLogLevel;
if (Object.keys(logMsgs).length) this.loggerWindow.send('log', logMsgs);
if (this.logQueue.length) {
logMsgs.msgs = this.logQueue;
}
if (this.desiredLogLevel) {
logMsgs.logLevel = this.desiredLogLevel;
}
if (Object.keys(logMsgs).length) {
this.loggerWindow.send('log', logMsgs);
}
}
}
@ -198,7 +210,7 @@ class Logger {
}
if (this.loggerWindow) {
this.loggerWindow.send('log', { msgs: [ logMsg ] });
this.loggerWindow.send('log', { msgs: [ logMsg ], logLevel: this.desiredLogLevel, showInConsole: this.showInConsole });
} else {
this.logQueue.push(logMsg);
// don't store more than 100 msgs. keep most recent log msgs.
@ -230,4 +242,4 @@ class Logger {
const logger = new Logger();
export { logger };
export { logger };

View File

@ -60,8 +60,12 @@ export const compareVersions = (v1: string, v2: string): number => {
const n1 = parseInt(s1[i] || '0', 10);
const n2 = parseInt(s2[i] || '0', 10);
if (n1 > n2) return 1;
if (n2 > n1) return -1;
if (n1 > n2) {
return 1;
}
if (n2 > n1) {
return -1;
}
}
if ([s1[2], s2[2]].every(patch.test.bind(patch))) {
@ -71,11 +75,19 @@ export const compareVersions = (v1: string, v2: string): number => {
const p2 = patch.exec(s2[2])[1].split('.').map(tryParse);
for (let k = 0; k < Math.max(p1.length, p2.length); k++) {
if (p1[k] === undefined || typeof p2[k] === 'string' && typeof p1[k] === 'number') return -1;
if (p2[k] === undefined || typeof p1[k] === 'string' && typeof p2[k] === 'number') return 1;
if (p1[k] === undefined || typeof p2[k] === 'string' && typeof p1[k] === 'number') {
return -1;
}
if (p2[k] === undefined || typeof p1[k] === 'string' && typeof p2[k] === 'number') {
return 1;
}
if (p1[k] > p2[k]) return 1;
if (p2[k] > p1[k]) return -1;
if (p1[k] > p2[k]) {
return 1;
}
if (p2[k] > p1[k]) {
return -1;
}
}
} else if ([s1[2], s2[2]].some(patch.test.bind(patch))) {
return patch.test(s1[2]) ? -1 : 1;
@ -179,7 +191,9 @@ export const throttle = (func: (...args) => void, wait: number): (...args) => vo
*/
export const formatString = (str: string, data?: object): string => {
if (!str || !data) return str;
if (!str || !data) {
return str;
}
for (const key in data) {
if (Object.prototype.hasOwnProperty.call(data, key)) {
@ -193,4 +207,4 @@ export const formatString = (str: string, data?: object): string => {
}
}
return str;
};
};

196
src/renderer/app-bridge.ts Normal file
View File

@ -0,0 +1,196 @@
import { DesktopCapturerSource, remote } from 'electron';
import {
apiCmds,
IBoundsChange,
ILogMsg,
IScreenSharingIndicator,
IScreenSnippet,
LogLevel,
} from '../common/api-interface';
import { IScreenSourceError } from './desktop-capturer';
import { SSFApi } from './ssf-api';
const ssf = new SSFApi();
const notification = remote.require('../renderer/notification').notification;
export default class AppBridge {
/**
* Validates the incoming postMessage
* events based on the host name
*
* @param event
*/
private static isValidEvent(event): boolean {
if (!event) {
return false;
}
return event.source && event.source === window;
}
public origin: string;
private readonly callbackHandlers = {
onMessage: (event) => this.handleMessage(event),
onActivityCallback: (idleTime: number) => this.activityCallback(idleTime),
onScreenSnippetCallback: (arg: IScreenSnippet) => this.screenSnippetCallback(arg),
onRegisterBoundsChangeCallback: (arg: IBoundsChange) => this.registerBoundsChangeCallback(arg),
onRegisterLoggerCallback: (msg: ILogMsg, logLevel: LogLevel, showInConsole: boolean) =>
this.registerLoggerCallback(msg, logLevel, showInConsole),
onRegisterProtocolHandlerCallback: (uri: string) => this.protocolHandlerCallback(uri),
onScreenSharingIndicatorCallback: (arg: IScreenSharingIndicator) => this.screenSharingIndicatorCallback(arg),
onMediaSourceCallback: (
requestId: number | undefined,
error: IScreenSourceError | null,
source: DesktopCapturerSource | undefined,
): void => this.gotMediaSource(requestId, error, source),
onNotificationCallback: (event, data) => this.notificationCallback(event, data),
};
constructor() {
// starts with corporate pod and
// will be updated with the global config url
this.origin = 'https://corporate.symphony.com';
window.addEventListener('message', this.callbackHandlers.onMessage);
}
/**
* Switch case that validates and handle
* incoming messages from postMessage
*
* @param event
*/
private handleMessage(event): void {
if (!AppBridge.isValidEvent(event)) {
return;
}
const { method, data } = event.data;
switch (method) {
case apiCmds.getVersionInfo:
this.broadcastMessage('get-version-info-callback', ssf.getVersionInfo());
break;
case apiCmds.activate:
ssf.activate(data);
break;
case apiCmds.bringToFront:
const { windowName, reason } = data;
ssf.bringToFront(windowName, reason);
break;
case apiCmds.setBadgeCount:
if (typeof data === 'number') {
ssf.setBadgeCount(data);
}
break;
case apiCmds.setLocale:
if (typeof data === 'string') {
ssf.setLocale(data);
}
break;
case apiCmds.registerActivityDetection:
ssf.registerActivityDetection(data, this.callbackHandlers.onActivityCallback);
break;
case apiCmds.openScreenSnippet:
ssf.openScreenSnippet(this.callbackHandlers.onScreenSnippetCallback);
break;
case apiCmds.registerBoundsChange:
ssf.registerBoundsChange(this.callbackHandlers.onRegisterBoundsChangeCallback);
break;
case apiCmds.registerLogger:
ssf.registerLogger(this.callbackHandlers.onRegisterLoggerCallback);
break;
case apiCmds.registerProtocolHandler:
ssf.registerProtocolHandler(this.callbackHandlers.onRegisterProtocolHandlerCallback);
break;
case apiCmds.openScreenSharingIndicator:
ssf.showScreenSharingIndicator(data, this.callbackHandlers.onScreenSharingIndicatorCallback);
break;
case apiCmds.closeScreenSharingIndicator:
ssf.closeScreenSharingIndicator(data.streamId);
break;
case apiCmds.getMediaSource:
ssf.getMediaSource(data, this.callbackHandlers.onMediaSourceCallback);
break;
case apiCmds.notification:
notification.showNotification(data, this.callbackHandlers.onNotificationCallback);
break;
case apiCmds.closeNotification:
notification.hideNotification(data);
break;
}
}
/**
* Broadcast user activity
* @param idleTime {number} - system idle tick
*/
private activityCallback = (idleTime: number): void => this.broadcastMessage('activity-callback', idleTime);
/**
* Broadcast snippet data
* @param arg {IScreenSnippet}
*/
private screenSnippetCallback = (arg: IScreenSnippet): void => this.broadcastMessage('screen-snippet-callback', arg);
/**
* Broadcast bound changes
* @param arg {IBoundsChange}
*/
private registerBoundsChangeCallback = (arg: IBoundsChange): void => this.broadcastMessage('bound-changes-callback', arg);
/**
* Broadcast logs
* @param msg {ILogMsg}
* @param logLevel {LogLevel}
* @param showInConsole {boolean}
*/
private registerLoggerCallback(msg: ILogMsg, logLevel: LogLevel, showInConsole: boolean): void {
this.broadcastMessage('logger-callback', { msg, logLevel, showInConsole });
}
/**
* Broadcast protocol uri
* @param uri {string}
*/
private protocolHandlerCallback = (uri: string): void => this.broadcastMessage('protocol-callback', uri);
/**
* Broadcast event that stops screen sharing
* @param arg {IScreenSharingIndicator}
*/
private screenSharingIndicatorCallback(arg: IScreenSharingIndicator): void {
this.broadcastMessage('screen-sharing-indicator-callback', arg);
}
/**
* Broadcast the user selected source
* @param requestId {number}
* @param error {Error}
* @param source {DesktopCapturerSource}
*/
private gotMediaSource(requestId: number | undefined, error: IScreenSourceError | null, source: DesktopCapturerSource | undefined): void {
this.broadcastMessage('media-source-callback', { requestId, source, error });
}
/**
* Broadcast notification events
*
* @param event {string}
* @param data {Object}
*/
private notificationCallback(event, data) {
this.broadcastMessage(event, data);
}
/**
* Method that broadcast messages to a specific origin via postMessage
*
* @param method {string}
* @param data {any}
*/
private broadcastMessage(method: string, data: any): void {
window.postMessage({ method, data }, this.origin);
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -60,4 +60,4 @@ export default class AboutApp extends React.Component<{}, IState> {
private updateState(_event, data): void {
this.setState(data as IState);
}
}
}

View File

@ -77,7 +77,9 @@ export default class DownloadManager extends React.Component<{}, IManagerState>
* @param item
*/
private mapItems(item: IDownloadManager): JSX.Element | undefined {
if (!item) return;
if (!item) {
return;
}
const { _id, total, fileName }: IDownloadManager = item;
const fileDisplayName = this.getFileDisplayName(fileName);

View File

@ -23,4 +23,4 @@ export default class LoadingScreen extends React.PureComponent {
</div>
);
}
}
}

View File

@ -27,4 +27,4 @@ export default class MoreInfo extends React.Component<{}, {}> {
</div>
);
}
}
}

View File

@ -0,0 +1,108 @@
import classNames from 'classnames';
import { ipcRenderer } from 'electron';
import * as React from 'react';
import { i18n } from '../../common/i18n-preload';
const whiteColorRegExp = new RegExp(/^(?:white|#fff(?:fff)?|rgba?\(\s*255\s*,\s*255\s*,\s*255\s*(?:,\s*1\s*)?\))$/i);
interface IState {
title: string;
company: string;
body: string;
image: string;
id: number;
color: string;
}
type mouseEventButton = React.MouseEvent<HTMLDivElement>;
export default class NotificationComp extends React.Component<{}, IState> {
private readonly eventHandlers = {
onClose: (winKey) => (_event: mouseEventButton) => this.close(winKey),
onClick: (data) => (_event: mouseEventButton) => this.click(data),
};
constructor(props) {
super(props);
this.state = {
title: '',
company: 'Symphony',
body: '',
image: '',
id: 0,
color: '',
};
this.updateState = this.updateState.bind(this);
}
public componentDidMount(): void {
ipcRenderer.on('notification-data', this.updateState);
}
public componentWillUnmount(): void {
ipcRenderer.removeListener('notification-data', this.updateState);
}
/**
* Renders the custom title bar
*/
public render(): JSX.Element {
const { title, company, body, image, id, color } = this.state;
const isLightTheme = color ? color.match(whiteColorRegExp) : true;
const theme = classNames({ light: isLightTheme, dark: !isLightTheme });
const bgColor = { backgroundColor: color || '#ffffff' };
return (
<div className='container' style={bgColor} onClick={this.eventHandlers.onClick(id)}>
<div className='logo-container'>
<img className={`logo ${theme}`} alt='symphony logo'/>
</div>
<div className='header'>
<span className={`title ${theme}`}>{title}</span>
<span className='company' style={{color: color || '#4a4a4a'}}>{company}</span>
<span className={`message ${theme}`}>{body}</span>
</div>
<div className='user-profile-pic-container'>
<img src={image} className='user-profile-pic' alt='user profile picture'/>
</div>
<div className='close' title={i18n.t('Close')()} onClick={this.eventHandlers.onClose(id)}>
<svg fill='#000000' height='16' viewBox='0 0 24 24' width='16' xmlns='http://www.w3.org/2000/svg'>
<path d='M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z'/>
<path d='M0 0h24v24H0z' fill='none'/>
</svg>
</div>
</div>
);
}
/**
* Invoked when the notification window is clicked
*
* @param id {number}
*/
private click(id: number) {
ipcRenderer.send('notification-clicked', id);
}
/**
* Closes the notification
*
* @param id {number}
*/
private close(id: number) {
ipcRenderer.send('close-notification', id);
}
/**
* Sets the About app state
*
* @param _event
* @param data {Object}
*/
private updateState(_event, data): void {
this.setState(data as IState);
}
}

View File

@ -62,7 +62,9 @@ export default class ScreenPicker extends React.Component<{}, IState> {
public componentDidMount(): void {
ipcRenderer.on('screen-picker-data', this.updateState);
document.addEventListener('keyup', this.handleKeyUpPress, true);
if (isWindowsOS) document.body.classList.add('ScreenPicker-window-border');
if (isWindowsOS) {
document.body.classList.add('ScreenPicker-window-border');
}
}
public componentWillUnmount(): void {
@ -370,4 +372,4 @@ export default class ScreenPicker extends React.Component<{}, IState> {
private updateState(_event, data): void {
this.setState(data as IState);
}
}
}

View File

@ -8,6 +8,7 @@ import { i18n } from '../../common/i18n-preload';
interface IState {
id: number;
streamId: string;
}
type mouseEventButton = React.MouseEvent<HTMLButtonElement>;
@ -25,6 +26,7 @@ export default class ScreenSharingIndicator extends React.Component<{}, IState>
super(props);
this.state = {
id: 0,
streamId: '',
};
this.updateState = this.updateState.bind(this);
}
@ -70,9 +72,11 @@ export default class ScreenSharingIndicator extends React.Component<{}, IState>
* Closes the screen sharing indicator window
*/
private close(): void {
const { streamId } = this.state;
ipcRenderer.send(apiName.symphonyApi, {
cmd: apiCmds.closeWindow,
windowType: 'screen-sharing-indicator',
winKey: streamId,
});
}
@ -85,4 +89,4 @@ export default class ScreenSharingIndicator extends React.Component<{}, IState>
private updateState(_event, data): void {
this.setState(data as IState);
}
}
}

View File

@ -66,4 +66,4 @@ export default class SnackBar extends React.Component<{}, IState> {
</div>
) : <div />;
}
}
}

View File

@ -17,7 +17,16 @@ let nextId = 0;
let isScreenShareEnabled = true;
let screenShareArgv: string;
type CallbackType = (error: Error | null, source?: DesktopCapturerSource) => DesktopCapturerSource | Error;
export interface ICustomSourcesOptions extends SourcesOptions {
requestId?: number;
}
export interface IScreenSourceError {
name: string;
message: string;
}
export type CallbackType = (requestId: number | undefined, error: IScreenSourceError | null, source?: DesktopCapturerSource) => void;
const getNextId = () => ++nextId;
/**
@ -25,7 +34,7 @@ const getNextId = () => ++nextId;
* @param options |options.type| can not be empty and has to include 'window' or 'screen'.
* @returns {boolean}
*/
const isValid = (options: SourcesOptions) => {
const isValid = (options: ICustomSourcesOptions) => {
return ((options !== null ? options.types : undefined) !== null) && Array.isArray(options.types);
};
@ -36,19 +45,19 @@ const isValid = (options: SourcesOptions) => {
* @param callback {CallbackType}
* @returns {*}
*/
export const getSource = (options: SourcesOptions, callback: CallbackType) => {
export const getSource = (options: ICustomSourcesOptions, callback: CallbackType) => {
let captureWindow;
let captureScreen;
let id;
const sourcesOpts: string[] = [];
const { requestId, ...updatedOptions } = options;
if (!isValid(options)) {
callback(new Error('Invalid options'));
callback(requestId, { name: 'Invalid options', message: 'Invalid options' });
return;
}
captureWindow = includes.call(options.types, 'window');
captureScreen = includes.call(options.types, 'screen');
const updatedOptions = options;
if (!updatedOptions.thumbnailSize) {
updatedOptions.thumbnailSize = {
height: 150,
@ -83,7 +92,7 @@ export const getSource = (options: SourcesOptions, callback: CallbackType) => {
title: `${i18n.t('Permission Denied')()}!`,
type: 'error',
});
callback(new Error('Permission Denied'));
callback(requestId, { name: 'Permission Denied', message: 'Permission Denied' });
return;
}
}
@ -97,11 +106,11 @@ export const getSource = (options: SourcesOptions, callback: CallbackType) => {
const filteredSource: DesktopCapturerSource[] = sources.filter((source) => source.name === title);
if (Array.isArray(filteredSource) && filteredSource.length > 0) {
return callback(null, filteredSource[ 0 ]);
return callback(requestId, null, filteredSource[ 0 ]);
}
if (sources.length > 0) {
return callback(null, sources[ 0 ]);
return callback(requestId, null, sources[ 0 ]);
}
}
@ -122,9 +131,9 @@ export const getSource = (options: SourcesOptions, callback: CallbackType) => {
// Cleaning up the event listener to prevent memory leaks
if (!source) {
ipcRenderer.removeListener('start-share' + id, successCallback);
return callback(new Error('User Cancelled'));
return callback(requestId, { name: 'User Cancelled', message: 'User Cancelled' });
}
return callback(null, source);
return callback(requestId, null, source);
};
ipcRenderer.once('start-share' + id, successCallback);
return null;
@ -143,4 +152,4 @@ ipcRenderer.on('is-screen-share-enabled', (_event, screenShare) => {
if (typeof screenShare === 'boolean' && screenShare) {
isScreenShareEnabled = true;
}
});
});

View File

@ -0,0 +1,223 @@
import * as asyncMap from 'async.map';
import * as electron from 'electron';
import { windowExists } from '../app/window-utils';
import { isMac } from '../common/env';
interface ISettings {
startCorner: startCorner;
height: number;
width: number;
totalHeight: number;
totalWidth: number;
corner: ICorner;
firstPos: ICorner;
maxVisibleNotifications: number;
animationSteps: number;
animationStepMs: number;
}
interface ICorner {
x: number;
y: number;
}
type startCorner = 'upper-right' | 'upper-left' | 'lower-right' | 'lower-left';
export default class NotificationHandler {
public settings: ISettings;
public nextInsertPos: ICorner = { x: 0, y: 0 };
private readonly eventHandlers = {
onSetup: () => this.setupNotificationPosition(),
};
private externalDisplay: Electron.Display | undefined;
private displayId: string = '';
constructor(opts) {
this.settings = opts as ISettings;
this.setupNotificationPosition();
electron.screen.on('display-added', this.eventHandlers.onSetup);
electron.screen.on('display-removed', this.eventHandlers.onSetup);
electron.screen.on('display-metrics-changed', this.eventHandlers.onSetup);
}
/**
* Sets the position of the notification window
*
* @param window {BrowserWindow}
* @param x {number}
* @param y {number}
*/
public setWindowPosition(window: Electron.BrowserWindow, x: number = 0, y: number = 0) {
if (window && !window.isDestroyed()) {
window.setPosition(parseInt(String(x), 10), parseInt(String(y), 10));
}
}
/**
* Initializes / resets the notification positional values
*/
public setupNotificationPosition() {
// This feature only applies to windows
if (isMac) {
return;
}
const screens = electron.screen.getAllDisplays();
if (screens && screens.length >= 0) {
this.externalDisplay = screens.find((screen) => {
const screenId = screen.id.toString();
return screenId === this.displayId;
});
}
const display = this.externalDisplay || electron.screen.getPrimaryDisplay();
this.settings.corner.x = display.workArea.x;
this.settings.corner.y = display.workArea.y;
// update corner x/y based on corner of screen where notification should appear
const workAreaWidth = display.workAreaSize.width;
const workAreaHeight = display.workAreaSize.height;
switch (this.settings.startCorner) {
case 'upper-right':
this.settings.corner.x += workAreaWidth;
break;
case 'lower-right':
this.settings.corner.x += workAreaWidth;
this.settings.corner.y += workAreaHeight;
break;
case 'lower-left':
this.settings.corner.y += workAreaHeight;
break;
case 'upper-left':
default:
// no change needed
break;
}
this.calculateDimensions();
// Maximum amount of Notifications we can show:
this.settings.maxVisibleNotifications = Math.floor(display.workAreaSize.height / this.settings.totalHeight);
}
/**
* Find next possible insert position (on top)
*/
public calcNextInsertPos(activeNotificationLength) {
if (activeNotificationLength < this.settings.maxVisibleNotifications) {
switch (this.settings.startCorner) {
case 'upper-right':
case 'upper-left':
this.nextInsertPos.y = this.settings.corner.y + (this.settings.totalHeight * activeNotificationLength);
break;
default:
case 'lower-right':
case 'lower-left':
this.nextInsertPos.y = this.settings.corner.y - (this.settings.totalHeight * (activeNotificationLength + 1));
break;
}
}
}
/**
* Moves the notification by one step
*
* @param startPos {number}
* @param activeNotifications {ICustomBrowserWindow[]}
*/
public moveNotificationDown(startPos, activeNotifications) {
if (startPos >= activeNotifications || startPos === -1) {
return;
}
// Build array with index of affected notifications
const notificationPosArray: number[] = [];
for (let i = startPos; i < activeNotifications.length; i++) {
notificationPosArray.push(i);
}
asyncMap(notificationPosArray, (i, done) => {
// Get notification to move
const notificationWindow = activeNotifications[i];
// Calc new y position
let newY;
switch (this.settings.startCorner) {
case 'upper-right':
case 'upper-left':
newY = this.settings.corner.y + (this.settings.totalHeight * i);
break;
default:
case 'lower-right':
case 'lower-left':
newY = this.settings.corner.y - (this.settings.totalHeight * (i + 1));
break;
}
if (!windowExists(notificationWindow)) {
return;
}
// Get startPos, calc step size and start animationInterval
const startY = notificationWindow.getPosition()[1];
const step = (newY - startY) / this.settings.animationSteps;
let curStep = 1;
const animationInterval = setInterval(() => {
// Abort condition
if (curStep === this.settings.animationSteps) {
this.setWindowPosition(notificationWindow, this.settings.firstPos.x, newY);
clearInterval(animationInterval);
done(null, 'done');
return;
}
// Move one step down
this.setWindowPosition(notificationWindow, this.settings.firstPos.x, startY + curStep * step);
curStep++;
}, this.settings.animationStepMs);
});
}
/**
* Calculates the first and next notification insert position
*/
private calculateDimensions() {
const vertSpace = 8;
// Calc totalHeight & totalWidth
this.settings.totalHeight = this.settings.height + vertSpace;
this.settings.totalWidth = this.settings.width;
let firstPosX;
let firstPosY;
switch (this.settings.startCorner) {
case 'upper-right':
firstPosX = this.settings.corner.x - this.settings.totalWidth;
firstPosY = this.settings.corner.y;
break;
case 'lower-right':
firstPosX = this.settings.corner.x - this.settings.totalWidth;
firstPosY = this.settings.corner.y - this.settings.totalHeight;
break;
case 'lower-left':
firstPosX = this.settings.corner.x;
firstPosY = this.settings.corner.y - this.settings.totalHeight;
break;
case 'upper-left':
default:
firstPosX = this.settings.corner.x;
firstPosY = this.settings.corner.y;
break;
}
// Calc pos of first notification:
this.settings.firstPos = {
x: firstPosX,
y: firstPosY,
};
// Set nextInsertPos
this.nextInsertPos.x = this.settings.firstPos.x;
this.nextInsertPos.y = this.settings.firstPos.y;
}
}

View File

@ -0,0 +1,321 @@
import { ipcMain } from 'electron';
import { createComponentWindow, windowExists } from '../app/window-utils';
import { AnimationQueue } from '../common/animation-queue';
import { logger } from '../common/logger';
import NotificationHandler from './notification-handler';
// const MAX_QUEUE_SIZE = 30;
const CLEAN_UP_INTERVAL = 60 * 100;
const animationQueue = new AnimationQueue();
interface ICustomBrowserWindow extends Electron.BrowserWindow {
notificationData: INotificationData;
displayTimer: NodeJS.Timer;
clientId: number;
}
interface INotificationData {
id: number;
title: string;
text: string;
image: string;
flash: boolean;
color: string;
tag: string;
sticky: boolean;
company: string;
displayTime: number;
}
type startCorner = 'upper-right' | 'upper-left' | 'lower-right' | 'lower-left';
const notificationSettings = {
startCorner: 'upper-right' as startCorner,
width: 380,
height: 100,
totalHeight: 0,
totalWidth: 0,
corner: {
x: 0,
y: 0,
},
firstPos: {
x: 0,
y: 0,
},
templatePath: '',
maxVisibleNotifications: 6,
borderRadius: 5,
displayTime: 5000,
animationSteps: 5,
animationStepMs: 5,
logging: true,
};
class Notification extends NotificationHandler {
private readonly funcHandlers = {
onCleanUpInactiveNotification: () => this.cleanUpInactiveNotification(),
onCreateNotificationWindow: (data: INotificationData) => this.createNotificationWindow(data),
};
private readonly activeNotifications: Electron.BrowserWindow[] = [];
private readonly inactiveWindows: Electron.BrowserWindow[] = [];
private readonly notificationQueue: INotificationData[] = [];
private readonly notificationCallbacks: any[] = [];
private cleanUpTimer: NodeJS.Timer;
constructor(opts) {
super(opts);
ipcMain.on('close-notification', (_event, windowId) => {
this.hideNotification(windowId);
});
ipcMain.on('notification-clicked', (_event, windowId) => {
this.notificationClicked(windowId);
});
this.cleanUpTimer = setInterval(this.funcHandlers.onCleanUpInactiveNotification, CLEAN_UP_INTERVAL);
}
/**
* Displays a new notification
*
* @param data
* @param callback
*/
public showNotification(data: INotificationData, callback): void {
clearInterval(this.cleanUpTimer);
animationQueue.push({
func: this.funcHandlers.onCreateNotificationWindow,
args: [ data ],
});
this.notificationCallbacks[ data.id ] = callback;
this.cleanUpTimer = setInterval(this.funcHandlers.onCleanUpInactiveNotification, CLEAN_UP_INTERVAL);
}
/**
* Creates a new notification window
*
* @param data
*/
public async createNotificationWindow(data): Promise<ICustomBrowserWindow | undefined> {
if (data.tag) {
for (let i = 0; i < this.notificationQueue.length; i++) {
if (this.notificationQueue[ i ].tag === data.tag) {
this.notificationQueue[ i ] = data;
return;
}
}
for (const window of this.activeNotifications) {
const notificationWin = window as ICustomBrowserWindow;
if (window && notificationWin.notificationData.tag === data.tag) {
this.setNotificationContent(notificationWin, data);
return;
}
}
}
// Checks if number of active notification displayed is greater than or equal to the
// max displayable notification and queues them
if (this.activeNotifications.length >= this.settings.maxVisibleNotifications) {
this.notificationQueue.push(data);
return;
}
// Checks for the cashed window and use them
if (this.inactiveWindows.length > 0) {
const inactiveWin = this.inactiveWindows[0] as ICustomBrowserWindow;
if (windowExists(inactiveWin)) {
this.inactiveWindows.splice(0, 1);
this.renderNotification(inactiveWin, data);
return;
}
}
const notificationWindow = createComponentWindow(
'notification-comp',
this.getNotificationOpts(),
false,
) as ICustomBrowserWindow;
notificationWindow.notificationData = data;
notificationWindow.once('closed', () => {
const activeWindowIndex = this.activeNotifications.indexOf(notificationWindow);
const inactiveWindowIndex = this.inactiveWindows.indexOf(notificationWindow);
if (activeWindowIndex !== -1) {
this.activeNotifications.splice(activeWindowIndex, 1);
}
if (inactiveWindowIndex !== -1) {
this.inactiveWindows.splice(inactiveWindowIndex, 1);
}
});
return await this.didFinishLoad(notificationWindow, data);
}
/**
* Sets the notification contents
*
* @param notificationWindow
* @param data {INotificationData}
*/
public setNotificationContent(notificationWindow: ICustomBrowserWindow, data: INotificationData): void {
notificationWindow.clientId = data.id;
const displayTime = data.displayTime ? data.displayTime : notificationSettings.displayTime;
let timeoutId;
if (!data.sticky) {
timeoutId = setTimeout(async () => {
await this.hideNotification(notificationWindow.clientId);
}, displayTime);
notificationWindow.displayTimer = timeoutId;
}
notificationWindow.webContents.send('notification-data', data);
notificationWindow.showInactive();
}
/**
* Hides the notification window
*
* @param clientId
*/
public async hideNotification(clientId: number): Promise<void> {
const browserWindow = this.getNotificationWindow(clientId);
if (browserWindow && windowExists(browserWindow)) {
// send empty to reset the state
const pos = this.activeNotifications.indexOf(browserWindow);
this.activeNotifications.splice(pos, 1);
if (this.inactiveWindows.length < this.settings.maxVisibleNotifications || 5) {
this.inactiveWindows.push(browserWindow);
browserWindow.hide();
} else {
browserWindow.close();
}
this.moveNotificationDown(pos, this.activeNotifications);
if (this.notificationQueue.length > 0 && this.activeNotifications.length < this.settings.maxVisibleNotifications) {
const notificationData = this.notificationQueue[0];
this.notificationQueue.splice(0, 1);
animationQueue.push({
func: this.funcHandlers.onCreateNotificationWindow,
args: [ notificationData ],
});
}
}
return;
}
/**
* Handles notification click
*
* @param clientId {number}
*/
public notificationClicked(clientId): void {
const browserWindow = this.getNotificationWindow(clientId);
if (browserWindow && windowExists(browserWindow) && browserWindow.notificationData) {
const data = browserWindow.notificationData;
const callback = this.notificationCallbacks[ clientId ];
if (typeof callback === 'function') {
this.notificationCallbacks[ clientId ]('notification-clicked', data);
}
this.hideNotification(clientId);
}
}
/**
* Returns the notification based on the client id
*
* @param clientId {number}
*/
public getNotificationWindow(clientId: number): ICustomBrowserWindow | undefined {
const index: number = this.activeNotifications.findIndex((win) => {
const notificationWindow = win as ICustomBrowserWindow;
return notificationWindow.clientId === clientId;
});
if (index === -1) {
return;
}
return this.activeNotifications[ index ] as ICustomBrowserWindow;
}
/**
* Waits for window to load and resolves
*
* @param window
* @param data
*/
private didFinishLoad(window, data) {
return new Promise<ICustomBrowserWindow>((resolve) => {
window.webContents.once('did-finish-load', () => {
if (windowExists(window)) {
this.renderNotification(window, data);
}
return resolve(window);
});
});
}
/**
* Calculates all the required attributes and displays the notification
*
* @param notificationWindow {BrowserWindow}
* @param data {INotificationData}
*/
private renderNotification(notificationWindow, data): void {
this.calcNextInsertPos(this.activeNotifications.length);
this.setWindowPosition(notificationWindow, this.nextInsertPos.x, this.nextInsertPos.y);
this.setNotificationContent(notificationWindow, { ...data, windowId: notificationWindow.id });
this.activeNotifications.push(notificationWindow);
}
/**
* Closes the active notification after certain period
*/
private cleanUpInactiveNotification() {
logger.info('active notification', this.activeNotifications.length);
logger.info('inactive notification', this.inactiveWindows.length);
if (this.inactiveWindows.length > 0) {
logger.info('cleaning up inactive notification windows', { inactiveNotification: this.inactiveWindows.length });
this.inactiveWindows.forEach((window) => {
if (windowExists(window)) {
window.close();
}
});
logger.info(`Cleaned up inactive notification windows`, { inactiveNotification: this.inactiveWindows.length });
}
}
/**
* notification window opts
*/
private getNotificationOpts(): Electron.BrowserWindowConstructorOptions {
return {
width: 380,
height: 100,
alwaysOnTop: true,
skipTaskbar: true,
resizable: false,
show: false,
frame: false,
transparent: true,
acceptFirstMouse: true,
webPreferences: {
sandbox: true,
nodeIntegration: false,
devTools: true,
},
};
}
}
const notification = new Notification(notificationSettings);
export {
notification,
};

View File

@ -7,6 +7,7 @@ import AboutBox from './components/about-app';
import BasicAuth from './components/basic-auth';
import LoadingScreen from './components/loading-screen';
import MoreInfo from './components/more-info';
import NotificationComp from './components/notification-comp';
import ScreenPicker from './components/screen-picker';
import ScreenSharingIndicator from './components/screen-sharing-indicator';
@ -17,6 +18,7 @@ const enum components {
screenPicker = 'screen-picker',
screenSharingIndicator = 'screen-sharing-indicator',
basicAuth = 'basic-auth',
notification = 'notification-comp',
}
const loadStyle = (style) => {
@ -60,6 +62,10 @@ const load = () => {
loadStyle(components.basicAuth);
component = BasicAuth;
break;
case components.notification:
loadStyle(components.notification);
component = NotificationComp;
break;
}
const element = React.createElement(component);
ReactDOM.render(element, document.getElementById('Root'));
@ -70,4 +76,4 @@ document.addEventListener('DOMContentLoaded', load);
ipcRenderer.on('set-locale-resource', (_event, data) => {
const { locale, resource } = data;
i18n.setResource(locale, resource);
});
});

View File

@ -3,6 +3,7 @@ import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { i18n } from '../common/i18n-preload';
import AppBridge from './app-bridge';
import DownloadManager from './components/download-manager';
import SnackBar from './components/snack-bar';
import WindowsTitleBar from './components/windows-title-bar';
@ -13,6 +14,7 @@ interface ISSFWindow extends Window {
}
const ssfWindow: ISSFWindow = window;
const appBridge = new AppBridge();
/**
* creates API exposed from electron.
@ -39,7 +41,11 @@ const createAPI = () => {
createAPI();
// When the window is completely loaded
ipcRenderer.on('page-load', (_event, { locale, resources }) => {
ipcRenderer.on('page-load', (_event, { locale, resources, origin }) => {
// origin for postMessage targetOrigin communication
if (origin) {
appBridge.origin = origin;
}
i18n.setResource(locale, resources);
@ -63,4 +69,4 @@ ipcRenderer.on('initiate-custom-title-bar', () => {
const div = document.createElement( 'div' );
document.body.appendChild(div);
ReactDOM.render(element, div);
});
});

View File

@ -1,12 +1,17 @@
import { ipcRenderer, remote } from 'electron';
import { buildNumber } from '../../package.json';
import {
apiCmds,
apiName,
IActivityDetection,
IBadgeCount,
IBoundsChange, IScreenSharingIndicator,
IScreenSnippet, KeyCodes,
IBoundsChange,
ILogMsg,
IScreenSharingIndicator,
IScreenSnippet,
IVersionInfo,
KeyCodes,
LogLevel,
} from '../common/api-interface';
import { i18n, LocaleType } from '../common/i18n-preload';
import { throttle } from '../common/utils';
@ -14,19 +19,20 @@ import { getSource } from './desktop-capturer';
let isAltKey: boolean = false;
let isMenuOpen: boolean = false;
let nextId = 0;
interface ICryptoLib {
AESGCMEncrypt: (name: string, base64IV: string, base64AAD: string, base64Key: string, base64In: string) => string | null;
AESGCMDecrypt: (base64IV: string, base64AAD: string, base64Key: string, base64In: string) => string | null;
}
interface ILocalObject {
export interface ILocalObject {
ipcRenderer;
activityDetectionCallback?: (arg: IActivityDetection) => void;
logger?: (msg: ILogMsg, logLevel: LogLevel, showInConsole: boolean) => void;
activityDetectionCallback?: (arg: number) => void;
screenSnippetCallback?: (arg: IScreenSnippet) => void;
boundsChangeCallback?: (arg: IBoundsChange) => void;
screenSharingIndicatorCallback?: (arg: IScreenSharingIndicator) => void;
protocolActionCallback?: (arg: string) => void;
}
const local: ILocalObject = {
@ -48,6 +54,29 @@ const throttledSetLocale = throttle((locale) => {
});
}, 1000);
const throttledActivate = throttle((windowName) => {
local.ipcRenderer.send(apiName.symphonyApi, {
cmd: apiCmds.activate,
windowName,
});
}, 1000);
const throttledBringToFront = throttle((windowName, reason) => {
local.ipcRenderer.send(apiName.symphonyApi, {
cmd: apiCmds.bringToFront,
windowName,
reason,
});
}, 1000);
const throttledCloseScreenShareIndicator = throttle((streamId) => {
ipcRenderer.send(apiName.symphonyApi, {
cmd: apiCmds.closeWindow,
windowType: 'screen-sharing-indicator',
winKey: streamId,
});
}, 1000);
let cryptoLib: ICryptoLib | null;
try {
cryptoLib = remote.require('../app/crypto-handler.js').cryptoLibrary;
@ -75,6 +104,45 @@ export class SSFApi {
*/
public getMediaSource = getSource;
/**
* Brings window forward and gives focus.
*
* @param {String} windowName - Name of window. Note: main window name is 'main'
*/
public activate(windowName) {
if (typeof windowName === 'string') {
throttledActivate(windowName);
}
}
/**
* Brings window forward and gives focus.
*
* @param {String} windowName Name of window. Note: main window name is 'main'
* @param {String} reason, The reason for which the window is to be activated
*/
public bringToFront(windowName, reason) {
if (typeof windowName === 'string') {
throttledBringToFront(windowName, reason);
}
}
/**
* Method that returns various version info
*/
public getVersionInfo(): IVersionInfo {
const appName = remote.app.getName();
const appVer = remote.app.getVersion();
return {
containerIdentifier: appName,
containerVer: appVer,
buildNumber,
apiVer: '2.0.0',
searchApiVer: '3.0.0',
};
}
/**
* Allows JS to register a activity detector that can be used by electron main process.
*
@ -82,7 +150,7 @@ export class SSFApi {
* @param {Object} activityDetectionCallback - function that can be called accepting
* @example registerActivityDetection(40000, func)
*/
public registerActivityDetection(period: number, activityDetectionCallback: Partial<ILocalObject>): void {
public registerActivityDetection(period: number, activityDetectionCallback: (arg: number) => void): void {
if (typeof activityDetectionCallback === 'function') {
local.activityDetectionCallback = activityDetectionCallback;
@ -101,18 +169,64 @@ export class SSFApi {
* only one window can register for bounds change.
* @param {Function} callback Function invoked when bounds changes.
*/
public registerBoundsChange(callback: () => void): void {
public registerBoundsChange(callback: (arg: IBoundsChange) => void): void {
if (typeof callback === 'function') {
local.boundsChangeCallback = callback;
}
}
/**
* Allows JS to register a logger that can be used by electron main process.
* @param {Object} logger function that can be called accepting
* object: {
* logLevel: 'ERROR'|'CONFLICT'|'WARN'|'ACTION'|'INFO'|'DEBUG',
* logDetails: String
* }
*/
public registerLogger(logger) {
if (typeof logger === 'function') {
local.logger = logger;
// only main window can register
local.ipcRenderer.send(apiName.symphonyApi, {
cmd: apiCmds.registerLogger,
});
}
}
/**
* Allows JS to register a protocol handler that can be used by the
* electron main process.
*
* @param protocolHandler {Function} callback will be called when app is
* invoked with registered protocol (e.g., symphony). The callback
* receives a single string argument: full uri that the app was
* invoked with e.g., symphony://?streamId=xyz123&streamType=chatroom
*
* Note: this function should only be called after client app is fully
* able for protocolHandler callback to be invoked. It is possible
* the app was started using protocol handler, in this case as soon as
* this registration func is invoked then the protocolHandler callback
* will be immediately called.
*/
public registerProtocolHandler(protocolHandler) {
if (typeof protocolHandler === 'function') {
local.protocolActionCallback = protocolHandler;
local.ipcRenderer.send(apiName.symphonyApi, {
cmd: apiCmds.registerProtocolHandler,
});
}
}
/**
* Allow user to capture portion of screen
*
* @param screenSnippetCallback {function}
*/
public openScreenSnippet(screenSnippetCallback: Partial<IScreenSnippet>): void {
public openScreenSnippet(screenSnippetCallback: (arg: IScreenSnippet) => void): void {
if (typeof screenSnippetCallback === 'function') {
local.screenSnippetCallback = screenSnippetCallback;
@ -160,35 +274,40 @@ export class SSFApi {
* - 'stopRequested' - user clicked "Stop Sharing" button.
*/
public showScreenSharingIndicator(options, callback): void {
const { stream, displayId } = options;
const { displayId, requestId, streamId } = options;
if (typeof callback === 'function') {
if (!stream || !stream.active || stream.getVideoTracks().length !== 1) {
callback({ type: 'error', reason: 'bad stream' });
return;
}
if (displayId && typeof (displayId) !== 'string') {
callback({ type: 'error', reason: 'bad displayId' });
return;
}
local.screenSharingIndicatorCallback = callback;
const id = ++nextId;
ipcRenderer.send(apiName.symphonyApi, {
cmd: apiCmds.openScreenSharingIndicator,
displayId: options.displayId,
id,
displayId,
id: requestId,
streamId,
});
}
}
/**
* Closes the screen sharing indicator
*/
public closeScreenSharingIndicator(winKey: string): void {
throttledCloseScreenShareIndicator(winKey);
}
}
/**
* Ipc events
*/
// Creates a data url
/**
* An event triggered by the main process
* to construct a canvas for the Windows badge count image
*
* @param {IBadgeCount} arg {
* count: number
* }
*/
local.ipcRenderer.on('create-badge-data-url', (_event: Event, arg: IBadgeCount) => {
const count = arg && arg.count || 0;
@ -228,19 +347,47 @@ local.ipcRenderer.on('create-badge-data-url', (_event: Event, arg: IBadgeCount)
}
});
/**
* An event triggered by the main process
* when the snippet is complete
*
* @param {IScreenSnippet} arg {
* message: string,
* data: base64,
* type: 'ERROR' | 'image/jpg;base64',
* }
*/
local.ipcRenderer.on('screen-snippet-data', (_event: Event, arg: IScreenSnippet) => {
if (typeof arg === 'object' && typeof local.screenSnippetCallback === 'function') {
local.screenSnippetCallback(arg);
}
});
local.ipcRenderer.on('activity', (_event: Event, arg: IActivityDetection) => {
if (typeof arg === 'object' && typeof local.activityDetectionCallback === 'function') {
local.activityDetectionCallback(arg);
/**
* An event triggered by the main process
* for ever few minutes if the user is active
*
* @param {number} idleTime - current system idle tick
*/
local.ipcRenderer.on('activity', (_event: Event, idleTime: number) => {
if (typeof idleTime === 'number' && typeof local.activityDetectionCallback === 'function') {
local.activityDetectionCallback(idleTime);
}
});
// listen for notifications that some window size/position has changed
/**
* An event triggered by the main process
* Whenever some Window position or dimension changes
*
* @param {IBoundsChange} arg {
* x: number,
* y: number,
* height: number,
* width: number,
* windowName: string
* }
*
*/
local.ipcRenderer.on('boundsChange', (_event, arg: IBoundsChange): void => {
const { x, y, height, width, windowName } = arg;
if (x && y && height && width && windowName && typeof local.boundsChangeCallback === 'function') {
@ -254,14 +401,40 @@ local.ipcRenderer.on('boundsChange', (_event, arg: IBoundsChange): void => {
}
});
local.ipcRenderer.on('screen-sharing-stopped', () => {
/**
* An event triggered by the main process
* when the screen sharing has been stopper
*/
local.ipcRenderer.on('screen-sharing-stopped', (_event, id) => {
if (typeof local.screenSharingIndicatorCallback === 'function') {
local.screenSharingIndicatorCallback({ type: 'stopRequested' });
// closes the screen sharing indicator
ipcRenderer.send(apiName.symphonyApi, {
cmd: apiCmds.closeWindow,
windowType: 'screen-sharing-indicator',
});
local.screenSharingIndicatorCallback({ type: 'stopRequested', requestId: id });
}
});
/**
* An event triggered by the main process
* for send logs on to web app
*
* @param {object} arg {
* msgs: ILogMsg[],
* logLevel: LogLevel,
* showInConsole: boolean
* }
*
*/
local.ipcRenderer.on('log', (_event, arg) => {
if (arg && local.logger) {
local.logger(arg.msgs || [], arg.logLevel, arg.showInConsole);
}
});
/**
* An event triggered by the main process for processing protocol urls
* @param {String} arg - the protocol url
*/
local.ipcRenderer.on('protocol-action', (_event, arg: string) => {
if (typeof local.protocolActionCallback === 'function' && typeof arg === 'string') {
local.protocolActionCallback(arg);
}
});
@ -284,7 +457,7 @@ const updateOnlineStatus = (): void => {
};
// Handle key down events
const throttledKeyDown = throttle( (event) => {
const throttledKeyDown = throttle((event) => {
isAltKey = event.keyCode === KeyCodes.Alt;
if (event.keyCode === KeyCodes.Esc) {
local.ipcRenderer.send(apiName.symphonyApi, {
@ -295,7 +468,7 @@ const throttledKeyDown = throttle( (event) => {
}, 500);
// Handle key up events
const throttledKeyUp = throttle( (event) => {
const throttledKeyUp = throttle((event) => {
if (isAltKey && (event.keyCode === KeyCodes.Alt || KeyCodes.Esc)) {
isMenuOpen = !isMenuOpen;
}
@ -323,4 +496,4 @@ window.addEventListener('offline', updateOnlineStatus, false);
window.addEventListener('online', updateOnlineStatus, false);
window.addEventListener('keyup', throttledKeyUp, true);
window.addEventListener('keydown', throttledKeyDown, true);
window.addEventListener('mousedown', throttleMouseDown, { capture: true });
window.addEventListener('mousedown', throttleMouseDown, { capture: true });

View File

@ -0,0 +1,109 @@
@font-family: "Segoe UI", "Helvetica Neue", "Verdana", "Arial", sans-serif;
.light {
--text-color: #4a4a4a;
--logo-bg: url('../assets/symphony-logo-black.png');
}
.dark {
--text-color: #ffffff;
--logo-bg: url('../assets/symphony-logo-white.png');
}
body {
margin: 0;
overflow: hidden;
-webkit-user-select: none;
font-family: @font-family;
}
.container {
width: 380px;
height: 100px;
display: flex;
justify-content: center;
background-color: #ffffff;
overflow: hidden;
position: relative;
line-height: 15px;
box-sizing: border-box;
border-radius: 5px;
}
.header {
width: 245px;
min-width: 230px;
margin: auto;
display: flex;
flex-direction: column;
align-items: flex-start;
}
.user-profile-pic-container {
align-items: center;
display: flex;
}
.user-profile-pic {
height: 43px;
border-radius: 4px;
width: 43px;
}
.close {
width: 16px;
height: 80px;
display: flex;
margin: auto;
opacity: 0.54;
font-size: 12px;
color: #CCC;
cursor: pointer;
}
.title {
font-family: @font-family;
font-size: 14px;
font-weight: 700;
color: var(--text-color);
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
}
.company {
font-family: @font-family;
font-size: 11px;
overflow: hidden;
filter: brightness(70%);
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
}
.message {
font-family: @font-family;
width: 100%;
overflow-wrap: break-word;
font-size: 12px;
margin-top: 5px;
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
cursor: default;
text-overflow: ellipsis;
color: var(--text-color);
}
.logo-container {
display: flex;
align-items: center;
}
.logo {
margin-left: 5px;
opacity: 0.6;
width: 43px;
content: var(--logo-bg);
}

View File

@ -83,4 +83,3 @@ body {
}
}

View File

@ -30,6 +30,7 @@
"exclude": [
"node_modules",
"lib",
"tests"
"tests",
"spec"
]
}
}

View File

@ -8,8 +8,8 @@
]
},
"rules": {
"curly": false,
"eofline": false,
"curly": true,
"eofline": true,
"align": [
true,
"parameters"
@ -73,4 +73,4 @@
],
"completed-docs": [true, "functions", "methods"]
}
}
}