mirror of
https://github.com/finos/SymphonyElectron.git
synced 2025-02-25 18:55:29 -06:00
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:
parent
e3f4830c4a
commit
13e82bac00
@ -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>
|
||||
|
@ -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'));
|
||||
|
@ -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",
|
||||
|
@ -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);
|
||||
});
|
||||
|
@ -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 };
|
||||
|
@ -22,4 +22,4 @@ export const cleanUpAppCache = async (): Promise<void> => {
|
||||
*/
|
||||
export const createAppCacheFile = (): void => {
|
||||
fs.writeFileSync(cacheCheckFilePath, '');
|
||||
};
|
||||
};
|
||||
|
@ -111,4 +111,4 @@ const autoLaunchInstance = new AutoLaunchController(props);
|
||||
|
||||
export {
|
||||
autoLaunchInstance,
|
||||
};
|
||||
};
|
||||
|
@ -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);
|
||||
};
|
||||
};
|
||||
|
@ -63,4 +63,4 @@ export const setChromeFlags = () => {
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
@ -224,4 +224,4 @@ const config = new Config();
|
||||
|
||||
export {
|
||||
config,
|
||||
};
|
||||
};
|
||||
|
@ -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 };
|
||||
|
@ -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);
|
||||
};
|
||||
};
|
||||
|
@ -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:
|
||||
|
@ -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));
|
||||
|
@ -69,4 +69,4 @@ class ProtocolHandler {
|
||||
|
||||
const protocolHandler = new ProtocolHandler();
|
||||
|
||||
export { protocolHandler };
|
||||
export { protocolHandler };
|
||||
|
@ -131,4 +131,4 @@ export const exportCrashDumps = (): void => {
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
};
|
||||
|
@ -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 };
|
||||
|
@ -99,4 +99,4 @@ export class SpellChecker {
|
||||
}
|
||||
return menu;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
};
|
||||
};
|
||||
|
@ -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 };
|
||||
|
@ -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();
|
||||
|
50
src/common/animation-queue.ts
Normal file
50
src/common/animation-queue.ts
Normal 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 = [];
|
||||
}
|
||||
}
|
@ -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';
|
||||
|
@ -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;
|
||||
|
@ -85,4 +85,4 @@ class Translation {
|
||||
|
||||
const i18n = new Translation();
|
||||
|
||||
export { i18n };
|
||||
export { i18n };
|
||||
|
@ -82,4 +82,4 @@ class Translation {
|
||||
|
||||
const i18n = new Translation();
|
||||
|
||||
export { i18n };
|
||||
export { i18n };
|
||||
|
@ -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 };
|
||||
|
@ -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
196
src/renderer/app-bridge.ts
Normal 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);
|
||||
}
|
||||
|
||||
}
|
BIN
src/renderer/assets/symphony-logo-black.png
Normal file
BIN
src/renderer/assets/symphony-logo-black.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.7 KiB |
BIN
src/renderer/assets/symphony-logo-white.png
Normal file
BIN
src/renderer/assets/symphony-logo-white.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.8 KiB |
@ -60,4 +60,4 @@ export default class AboutApp extends React.Component<{}, IState> {
|
||||
private updateState(_event, data): void {
|
||||
this.setState(data as IState);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -23,4 +23,4 @@ export default class LoadingScreen extends React.PureComponent {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -27,4 +27,4 @@ export default class MoreInfo extends React.Component<{}, {}> {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
108
src/renderer/components/notification-comp.tsx
Normal file
108
src/renderer/components/notification-comp.tsx
Normal 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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -66,4 +66,4 @@ export default class SnackBar extends React.Component<{}, IState> {
|
||||
</div>
|
||||
) : <div />;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
223
src/renderer/notification-handler.ts
Normal file
223
src/renderer/notification-handler.ts
Normal 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;
|
||||
}
|
||||
|
||||
}
|
321
src/renderer/notification.ts
Normal file
321
src/renderer/notification.ts
Normal 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,
|
||||
};
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
@ -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 });
|
||||
|
109
src/renderer/styles/notification-comp.less
Normal file
109
src/renderer/styles/notification-comp.less
Normal 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);
|
||||
}
|
@ -83,4 +83,3 @@ body {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
@ -30,6 +30,7 @@
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"lib",
|
||||
"tests"
|
||||
"tests",
|
||||
"spec"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@ -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"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user