diff --git a/angular.json b/angular.json index 2ca10f49..2210dd0b 100644 --- a/angular.json +++ b/angular.json @@ -1445,6 +1445,76 @@ } } } + }, + "ucap-webmessenger-electron-notification": { + "projectType": "library", + "root": "projects/ucap-webmessenger-electron-notification", + "sourceRoot": "projects/ucap-webmessenger-electron-notification/src", + "prefix": "ucap-electron-notification", + "architect": { + "build": { + "builder": "@angular-devkit/build-ng-packagr:build", + "options": { + "tsConfig": "projects/ucap-webmessenger-electron-notification/tsconfig.lib.json", + "project": "projects/ucap-webmessenger-electron-notification/ng-package.json" + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "projects/ucap-webmessenger-electron-notification/src/test.ts", + "tsConfig": "projects/ucap-webmessenger-electron-notification/tsconfig.spec.json", + "karmaConfig": "projects/ucap-webmessenger-electron-notification/karma.conf.js" + } + }, + "lint": { + "builder": "@angular-devkit/build-angular:tslint", + "options": { + "tsConfig": [ + "projects/ucap-webmessenger-electron-notification/tsconfig.lib.json", + "projects/ucap-webmessenger-electron-notification/tsconfig.spec.json" + ], + "exclude": [ + "**/node_modules/**" + ] + } + } + } + }, + "ucap-webmessenger-electron-core": { + "projectType": "library", + "root": "projects/ucap-webmessenger-electron-core", + "sourceRoot": "projects/ucap-webmessenger-electron-core/src", + "prefix": "ucap-electron-core", + "architect": { + "build": { + "builder": "@angular-devkit/build-ng-packagr:build", + "options": { + "tsConfig": "projects/ucap-webmessenger-electron-core/tsconfig.lib.json", + "project": "projects/ucap-webmessenger-electron-core/ng-package.json" + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "projects/ucap-webmessenger-electron-core/src/test.ts", + "tsConfig": "projects/ucap-webmessenger-electron-core/tsconfig.spec.json", + "karmaConfig": "projects/ucap-webmessenger-electron-core/karma.conf.js" + } + }, + "lint": { + "builder": "@angular-devkit/build-angular:tslint", + "options": { + "tsConfig": [ + "projects/ucap-webmessenger-electron-core/tsconfig.lib.json", + "projects/ucap-webmessenger-electron-core/tsconfig.spec.json" + ], + "exclude": [ + "**/node_modules/**" + ] + } + } + } } }, "defaultProject": "ucap-webmessenger-app" diff --git a/config/main.webpack.config.ts b/config/main.webpack.config.ts index 523d6814..d42998ac 100644 --- a/config/main.webpack.config.ts +++ b/config/main.webpack.config.ts @@ -79,6 +79,16 @@ const mainConfig: webpack.Configuration = { resolve: { extensions: ['.js', '.ts'], alias: { + '@ucap-webmessenger/electron-core': path.resolve( + __dirname, + '..', + 'projects/ucap-webmessenger-electron-core/src/public-api' + ), + '@ucap-webmessenger/electron-notification': path.resolve( + __dirname, + '..', + 'projects/ucap-webmessenger-electron-notification/src/public-api' + ), '@ucap-webmessenger/native': path.resolve( __dirname, '..', diff --git a/main/resources/notification/image/btn_call_message.png b/main/resources/notification/image/btn_call_message.png new file mode 100644 index 00000000..0e2c7949 Binary files /dev/null and b/main/resources/notification/image/btn_call_message.png differ diff --git a/main/resources/notification/image/btn_call_receive.png b/main/resources/notification/image/btn_call_receive.png new file mode 100644 index 00000000..7f60010e Binary files /dev/null and b/main/resources/notification/image/btn_call_receive.png differ diff --git a/main/resources/notification/image/btn_call_refuse.png b/main/resources/notification/image/btn_call_refuse.png new file mode 100644 index 00000000..674a5843 Binary files /dev/null and b/main/resources/notification/image/btn_call_refuse.png differ diff --git a/main/resources/notification/image/btn_call_transfer.png b/main/resources/notification/image/btn_call_transfer.png new file mode 100644 index 00000000..fdd11523 Binary files /dev/null and b/main/resources/notification/image/btn_call_transfer.png differ diff --git a/main/resources/notification/image/btn_close.png b/main/resources/notification/image/btn_close.png new file mode 100644 index 00000000..2cdaee37 Binary files /dev/null and b/main/resources/notification/image/btn_close.png differ diff --git a/main/resources/notification/image/btn_close_gray.png b/main/resources/notification/image/btn_close_gray.png new file mode 100644 index 00000000..0ce0aba0 Binary files /dev/null and b/main/resources/notification/image/btn_close_gray.png differ diff --git a/main/resources/notification/image/btn_noti_call.png b/main/resources/notification/image/btn_noti_call.png new file mode 100644 index 00000000..9e695905 Binary files /dev/null and b/main/resources/notification/image/btn_noti_call.png differ diff --git a/main/resources/notification/image/img_nophoto_50.png b/main/resources/notification/image/img_nophoto_50.png new file mode 100644 index 00000000..029f81bd Binary files /dev/null and b/main/resources/notification/image/img_nophoto_50.png differ diff --git a/main/resources/notification/preload.js b/main/resources/notification/preload.js new file mode 100644 index 00000000..86975a25 --- /dev/null +++ b/main/resources/notification/preload.js @@ -0,0 +1,143 @@ +'use strict'; + +const electron = require('electron'); +const ipc = electron.ipcRenderer; +const winId = electron.remote.getCurrentWindow().id; + +function setStyle(config) { + // Style it + let notiDoc = global.window.document; + let container = notiDoc.getElementById('container'); + let appIcon = notiDoc.getElementById('appIcon'); + let image = notiDoc.getElementById('image'); + let close = notiDoc.getElementById('close'); + let message = notiDoc.getElementById('message'); + // Default style + setStyleOnDomElement(config.defaultStyleContainer, container); + // Size and radius + let style = { + height: + config.height - + 2 * config.borderRadius - + 2 * config.defaultStyleContainer.padding, + width: + config.width - + 2 * config.borderRadius - + 2 * config.defaultStyleContainer.padding, + borderRadius: config.borderRadius + 'px' + }; + setStyleOnDomElement(style, container); + // Style appIcon or hide + if (config.appIcon) { + setStyleOnDomElement(config.defaultStyleAppIcon, appIcon); + appIcon.src = config.appIcon; + } else { + setStyleOnDomElement( + { + display: 'none' + }, + appIcon + ); + } + // Style image + setStyleOnDomElement(config.defaultStyleImage, image); + // Style close button + setStyleOnDomElement(config.defaultStyleClose, close); + // Remove margin from text p + setStyleOnDomElement(config.defaultStyleText, message); +} + +function setContents(event, notificationObj) { + // sound + if (notificationObj.sound) { + // Check if file is accessible + try { + // If it's a local file, check it's existence + // Won't check remote files e.g. http:// + if ( + notificationObj.sound.match(/^file\:/) !== null || + notificationObj.sound.match(/^\//) !== null + ) { + let audio = new global.window.Audio(notificationObj.sound); + audio.play(); + } + } catch (e) { + log( + 'electron-notify: ERROR could not find sound file: ' + + notificationObj.sound.replace('file://', ''), + e, + e.stack + ); + } + } + + let notiDoc = global.window.document; + // Title + let titleDoc = notiDoc.getElementById('title'); + titleDoc.innerHTML = notificationObj.title || ''; + // message + let messageDoc = notiDoc.getElementById('message'); + messageDoc.innerHTML = notificationObj.text || ''; + // Image + let imageDoc = notiDoc.getElementById('image'); + if (notificationObj.image) { + imageDoc.src = notificationObj.image; + } else { + setStyleOnDomElement({ display: 'none' }, imageDoc); + } + + // Close button + let closeButton = notiDoc.getElementById('close'); + closeButton.addEventListener('click', function(event) { + event.stopPropagation(); + ipc.send('UCAP::ElectronNotification::close', winId, notificationObj); + }); + + // URL + let container = notiDoc.getElementById('container'); + container.addEventListener('click', function() { + ipc.send('UCAP::ElectronNotification::click', winId, notificationObj); + }); +} + +function setStyleOnDomElement(styleObj, domElement) { + try { + for (let styleAttr in styleObj) { + domElement.style[styleAttr] = styleObj[styleAttr]; + } + } catch (e) { + throw new Error( + 'electron-notify: Could not set style on domElement', + styleObj, + domElement + ); + } +} + +function loadConfig(event, conf) { + setStyle(conf || {}); +} + +function reset() { + let notiDoc = global.window.document; + let container = notiDoc.getElementById('container'); + let closeButton = notiDoc.getElementById('close'); + + // Remove event listener + let newContainer = container.cloneNode(true); + container.parentNode.replaceChild(newContainer, container); + let newCloseButton = closeButton.cloneNode(true); + closeButton.parentNode.replaceChild(newCloseButton, closeButton); +} + +ipc.on('UCAP::ElectronNotification::BrowserWindowSetContents', setContents); +ipc.on('UCAP::ElectronNotification::loadConfig', loadConfig); +ipc.on('UCAP::ElectronNotification::reset', reset); + +function log() { + console.log.apply(console, arguments); +} + +delete global.require; +delete global.exports; +delete global.module; diff --git a/main/resources/notification/sound/messageAlarm.mp3 b/main/resources/notification/sound/messageAlarm.mp3 new file mode 100644 index 00000000..abaa3831 Binary files /dev/null and b/main/resources/notification/sound/messageAlarm.mp3 differ diff --git a/main/resources/notification/styles/noti_messege.css b/main/resources/notification/styles/noti_messege.css new file mode 100644 index 00000000..02cbf609 --- /dev/null +++ b/main/resources/notification/styles/noti_messege.css @@ -0,0 +1,130 @@ +html { + height: 100%; + overflow-y: scroll; +} +body { + position: relative; + width: 100%; + height: 100%; + padding: 0; + margin: 0; + color: #333; + font-family: '나눔고딕', Malgun Gothic, '맑은고딕', Arial, Dotum, '돋움', + Gulim, '굴림'; + font-size: 12px; + line-height: 18px !important; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} +body * { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} +ul, +ol { + list-style: none; + margin: 0; + padding: 0; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} +img { + border: none; +} +a:link, +a:visited, +a:hover, +a:active { + text-decoration: none; +} + +.noti_messege { + width: 340px; + height: 100px; + border: 1px solid #666; + background-color: #fff; + box-shadow: 0px 0px 3px 0px #e7e7e7; +} +.info { + position: relative; + width: 100%; + height: 100%; + box-sizing: border-box; + padding: 16px 14px; + color: #fff; +} +.btn_close { + position: absolute; + z-index: 1; + right: 6px; + top: 6px; + width: 20px; + height: 20px; + background: url(../image/btn_close_gray.png) no-repeat 50% 50%; +} +.btn_close:hover { + opacity: 0.7; +} +.photo { + position: relative; + top: 0px; + right: 0; + bottom: 0; + left: 0; + margin: 4px 0; + width: 54px; + height: 54px; + border-radius: 50%; + background: #5bc1ff url(../image/img_nophoto_50.png) no-repeat 50% 50%; + border: 2px solid #ddd; +} +.info .profile { + position: absolute; + width: 60px; + text-align: center; +} +.photo img { + overflow: hidden; + width: 50px; + height: 50px; + border-radius: 50px; +} +.noti_messege .info .profile + div { + padding-left: 70px; + position: relative; + line-height: 180%; + height: 100%; +} +.sender { + font-size: 14px; + font-weight: bold; + margin-bottom: 4px; + color: #333; + width: 94%; +} +.sender .name { + color: #2e7fb5; +} +.message { + color: #666; +} +.ellipsis { + display: block; + text-overflow: ellipsis; + white-space: nowrap; + word-wrap: normal; + overflow: hidden; +} +.ellipsis_row2 { + overflow: hidden; + text-overflow: ellipsis; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + word-wrap: break-word; + line-height: 1.6em; + height: 3.2em; +} diff --git a/main/resources/notification/template.html b/main/resources/notification/template.html new file mode 100644 index 00000000..5bec4a80 --- /dev/null +++ b/main/resources/notification/template.html @@ -0,0 +1,50 @@ + + + + [개발]M Messenger - 메시지 알림 + + + + + + + +
+
+ +
+
+ + +
+
+
+
    +
  • + 김 수안무 거북이와 두루미님이 메시지를 + 보냈습니다. +
  • +
  • + 홍길동 대리(솔루션사업팀)홍길동 대리(솔루션사업팀)홍길동 + 대리(솔루션사업팀)홍길동 대리(솔루션사업팀) +
  • +
+
+
+
+ + diff --git a/main/src/app/AppWindow.ts b/main/src/app/AppWindow.ts index 4e4e1f11..32d06ded 100644 --- a/main/src/app/AppWindow.ts +++ b/main/src/app/AppWindow.ts @@ -7,6 +7,11 @@ import { EventEmitter } from 'events'; import { now } from '../util/now'; import { registerWindowStateChangedEvents } from '../lib/window-state'; +import { + ElectronAppChannel, + ElectronBrowserWindowChannel, + ElectronWebContentsChannel +} from '@ucap-webmessenger/electron-core'; export class AppWindow { private window: BrowserWindow | null = null; @@ -62,11 +67,11 @@ export class AppWindow { savedWindowState.manage(this.window); let quitting = false; - app.on('before-quit', () => { + app.on(ElectronAppChannel.BeforeQuit, () => { quitting = true; }); - ipcMain.on('will-quit', (event: IpcMainEvent) => { + ipcMain.on(ElectronAppChannel.WillQuit, (event: IpcMainEvent) => { quitting = true; event.returnValue = true; }); @@ -75,7 +80,7 @@ export class AppWindow { // lets us activate quickly and keep all our interesting logic in the // renderer. if (__DARWIN__) { - this.window.on('close', e => { + this.window.on(ElectronBrowserWindowChannel.Close, e => { if (!quitting) { e.preventDefault(); } @@ -92,8 +97,8 @@ export class AppWindow { // // can be tidied up once https://github.com/electron/electron/issues/12971 // has been confirmed as resolved - this.window.once('ready-to-show', () => { - this.window.on('unmaximize', () => { + this.window.once(ElectronBrowserWindowChannel.ReadyToShow, () => { + this.window.on(ElectronBrowserWindowChannel.Unmaximize, () => { setTimeout(() => { const bounds = this.window.getBounds(); bounds.width += 1; @@ -109,26 +114,30 @@ export class AppWindow { public load(): void { let startLoad = 0; - this.window.webContents.once('did-start-loading', () => { - this._rendererReadyTime = null; - this._loadTime = null; + this.window.webContents.once( + ElectronWebContentsChannel.DidStartLoading, + () => { + this._rendererReadyTime = null; + this._loadTime = null; - startLoad = now(); - }); - - this.window.webContents.once('did-finish-load', () => { - if (process.env.NODE_ENV === 'development') { - this.window.webContents.openDevTools(); + startLoad = now(); } + ); - this._loadTime = now() - startLoad; - }); + this.window.webContents.once( + ElectronWebContentsChannel.DidFinishLoad, + () => { + this.window.webContents.setVisualZoomLevelLimits(1, 1); - this.window.webContents.on('did-finish-load', () => { - this.window.webContents.setVisualZoomLevelLimits(1, 1); - }); + if (process.env.NODE_ENV === 'development') { + this.window.webContents.openDevTools(); + } - this.window.webContents.on('did-fail-load', () => { + this._loadTime = now() - startLoad; + } + ); + + this.window.webContents.on(ElectronWebContentsChannel.DidFailLoad, () => { this.window.webContents.openDevTools(); this.window.show(); }); @@ -158,7 +167,7 @@ export class AppWindow { } public onClose(fn: () => void) { - this.window.on('closed', fn); + this.window.on(ElectronBrowserWindowChannel.Closed, fn); } /** diff --git a/main/src/index.ts b/main/src/index.ts index 2a0a55fd..743ad281 100644 --- a/main/src/index.ts +++ b/main/src/index.ts @@ -7,12 +7,22 @@ import * as fs from 'fs'; import { AppWindow } from './app/AppWindow'; import { now } from './util/now'; import { showUncaughtException } from './crash/show-uncaught-exception'; -import { Channel } from '@ucap-webmessenger/native-electron'; + +import { + UpdaterChannel, + FileChannel, + IdleStateChannel, + NotificationChannel +} from '@ucap-webmessenger/native-electron'; +import { ElectronNotificationService } from '@ucap-webmessenger/electron-notification'; + import { root } from './util/root'; import { DefaultFolder } from './lib/default-folder'; import { FileUtil } from './lib/file-util'; import { IdleChecker } from './lib/idle-checker'; +import { NotificationRequest } from '@ucap-webmessenger/native'; +import { ElectronAppChannel } from '@ucap-webmessenger/electron-core'; let appWindow: AppWindow | null = null; @@ -23,6 +33,9 @@ type OnDidLoadFn = (window: AppWindow) => void; let onDidLoadFns: Array | null = []; let preventQuit = false; + +let notificationService: ElectronNotificationService | null; + function handleUncaughtException(error: Error) { preventQuit = true; @@ -57,7 +70,7 @@ const gotSingleInstanceLock = app.requestSingleInstanceLock(); isDuplicateInstance = !gotSingleInstanceLock; let idle: IdleChecker | null; -app.on('second-instance', (event, args, workingDirectory) => { +app.on(ElectronAppChannel.SecondInstance, (event, args, workingDirectory) => { // Someone tried to run a second instance, we should focus our window. if (appWindow) { if (appWindow.isMinimized()) { @@ -132,7 +145,7 @@ function createWindow() { // This method will be called when Electron has finished // initialization and is ready to create browser windows. // Some APIs can only be used after this event occurs. -app.on('ready', () => { +app.on(ElectronAppChannel.Ready, () => { if (isDuplicateInstance) { return; } @@ -141,6 +154,30 @@ app.on('ready', () => { createWindow(); + notificationService = new ElectronNotificationService({ + width: 340, + height: 100, + padding: 0, + borderRadius: 0, + // appIcon: iconPath, + displayTime: 5000, + defaultStyleContainer: {}, + defaultStyleAppIcon: { display: 'none' }, + defaultStyleImage: {}, + defaultStyleClose: {}, + defaultStyleText: {} + }); + + notificationService.options.defaultWindow.webPreferences.preload = path.join( + __dirname, + 'resources/notification/preload.js' + ); + + notificationService.templatePath = path.join( + __dirname, + 'resources/notification/template.html' + ); + ipcMain.on('uncaught-exception', (event: IpcMainEvent, error: Error) => { handleUncaughtException(error); }); @@ -155,7 +192,7 @@ app.on('ready', () => { }); // Quit when all windows are closed. -app.on('window-all-closed', () => { +app.on(ElectronAppChannel.WindowAllClosed, () => { // On OS X it is common for applications and their menu bar // to stay active until the user quits explicitly with Cmd + Q if (process.platform !== 'darwin') { @@ -163,7 +200,7 @@ app.on('window-all-closed', () => { } }); -app.on('activate', () => { +app.on(ElectronAppChannel.Activate, () => { onDidLoad(window => { window.show(); }); @@ -179,11 +216,11 @@ function onDidLoad(fn: OnDidLoadFn) { } } -ipcMain.on(Channel.checkForUpdates, (event: IpcMainEvent, ...args: any[]) => { +ipcMain.on(UpdaterChannel.Check, (event: IpcMainEvent, ...args: any[]) => { event.returnValue = false; }); -ipcMain.on(Channel.readFile, (event: IpcMainEvent, ...args: any[]) => { +ipcMain.on(FileChannel.ReadFile, (event: IpcMainEvent, ...args: any[]) => { try { fse.readFile(root(args[0]), (err, data) => { if (!!err) { @@ -197,37 +234,73 @@ ipcMain.on(Channel.readFile, (event: IpcMainEvent, ...args: any[]) => { } }); -ipcMain.on(Channel.saveFile, async (event: IpcMainEvent, ...args: any[]) => { - try { - const buffer: Buffer = args[0]; - const fileName: string = args[1]; - let savePath: string = path.join( - !!args[2] ? args[2] : DefaultFolder.downloads(), - fileName - ); - savePath = await FileUtil.uniqueFileName(savePath); +ipcMain.on( + FileChannel.SaveFile, + async (event: IpcMainEvent, ...args: any[]) => { + try { + const buffer: Buffer = args[0]; + const fileName: string = args[1]; + let savePath: string = path.join( + !!args[2] ? args[2] : DefaultFolder.downloads(), + fileName + ); + savePath = await FileUtil.uniqueFileName(savePath); - fse.writeFile(savePath, buffer, err => { - if (!err) { - event.returnValue = savePath; - } else { - event.returnValue = undefined; + fse.writeFile(savePath, buffer, err => { + if (!err) { + event.returnValue = savePath; + } else { + event.returnValue = undefined; + } + }); + } catch (error) { + event.returnValue = undefined; + } + } +); + +ipcMain.on( + IdleStateChannel.StartCheck, + (event: IpcMainEvent, ...args: any[]) => { + if (!!idle) { + idle.destoryChecker(); + idle = null; + } + idle = new IdleChecker(appWindow.browserWindow); // default 10min + idle.startChecker(); + } +); + +ipcMain.on( + NotificationChannel.Notify, + (event: IpcMainEvent, ...args: any[]) => { + const noti: NotificationRequest = args[0]; + + notificationService.notify({ + title: noti.title, + text: noti.contents, + image: + noti.image || + path.join(__dirname, 'resources/notification/image/img_nophoto_50.png'), + sound: noti.useSound + ? path.join( + 'file://', + __dirname, + 'resources/notification/sound/messageAlarm.mp3' + ) + : '', + onClick: () => { + console.log('onClick'); } }); - } catch (error) { - event.returnValue = undefined; - } -}); -ipcMain.on(Channel.idleStateStart, (event: IpcMainEvent, ...args: any[]) => { - if (!!idle) { - idle.destoryChecker(); - idle = null; + console.log('Channel.notify', noti); } - idle = new IdleChecker(appWindow.browserWindow); // default 10min - idle.startChecker(); -}); +); -ipcMain.on(Channel.showNotify, (event: IpcMainEvent, ...args: any[]) => { - console.log('Channel.showNotify', args); -}); +ipcMain.on( + NotificationChannel.CloseAllNotify, + (event: IpcMainEvent, ...args: any[]) => { + console.log('Channel.closeAllNotify', args); + } +); diff --git a/main/src/lib/idle-checker.ts b/main/src/lib/idle-checker.ts index f66c3da6..4d222bf5 100644 --- a/main/src/lib/idle-checker.ts +++ b/main/src/lib/idle-checker.ts @@ -1,5 +1,5 @@ import { powerMonitor, BrowserWindow } from 'electron'; -import { Channel } from '@ucap-webmessenger/native-electron'; +import { IdleStateChannel } from '@ucap-webmessenger/native-electron'; import { setInterval } from 'timers'; export enum IdleType { @@ -28,13 +28,13 @@ export class IdleChecker { if (this.status === IdleType.ACTIVE) { this.status = IdleType.IDLE; // TODO :: USER_STATUS change away - this.window.webContents.send(Channel.idleStateChanged, this.status); + this.window.webContents.send(IdleStateChannel.Changed, this.status); } } else { if (this.status === IdleType.IDLE) { this.status = IdleType.ACTIVE; // TODO :: USER_STATUS chage online - this.window.webContents.send(Channel.idleStateChanged, this.status); + this.window.webContents.send(IdleStateChannel.Changed, this.status); } } } diff --git a/main/src/lib/window-state.ts b/main/src/lib/window-state.ts index 1c5af590..93eb1c1a 100644 --- a/main/src/lib/window-state.ts +++ b/main/src/lib/window-state.ts @@ -1,6 +1,7 @@ import { BrowserWindow } from 'electron'; import { WindowState } from '@ucap-webmessenger/native'; -import { Channel } from '@ucap-webmessenger/native-electron'; +import { WindowStateChannel } from '@ucap-webmessenger/native-electron'; +import { ElectronBrowserWindowChannel } from '@ucap-webmessenger/electron-core'; export function getWindowState(window: Electron.BrowserWindow): WindowState { if (window.isFullScreen()) { @@ -17,26 +18,30 @@ export function getWindowState(window: Electron.BrowserWindow): WindowState { } export function registerWindowStateChangedEvents(window: BrowserWindow) { - window.on('enter-full-screen', () => + window.on(ElectronBrowserWindowChannel.EnterFullScreen, () => sendWindowStateEvent(window, WindowState.FullScreen) ); - window.on('leave-full-screen', () => + window.on(ElectronBrowserWindowChannel.LeaveFullScreen, () => sendWindowStateEvent(window, WindowState.Normal) ); - window.on('maximize', () => + window.on(ElectronBrowserWindowChannel.Maximize, () => sendWindowStateEvent(window, WindowState.Maximized) ); - window.on('minimize', () => + window.on(ElectronBrowserWindowChannel.Minimize, () => sendWindowStateEvent(window, WindowState.Minimized) ); - window.on('unmaximize', () => + window.on(ElectronBrowserWindowChannel.Unmaximize, () => sendWindowStateEvent(window, WindowState.Normal) ); - window.on('restore', () => sendWindowStateEvent(window, WindowState.Normal)); - window.on('hide', () => sendWindowStateEvent(window, WindowState.Hidden)); - window.on('show', () => { + window.on(ElectronBrowserWindowChannel.Restore, () => + sendWindowStateEvent(window, WindowState.Normal) + ); + window.on(ElectronBrowserWindowChannel.Hide, () => + sendWindowStateEvent(window, WindowState.Hidden) + ); + window.on(ElectronBrowserWindowChannel.Show, () => { // because the app can be maximized before being closed - which will restore it // maximized on the next launch - this function should inspect the current state // rather than always assume it is a 'normal' launch @@ -45,5 +50,5 @@ export function registerWindowStateChangedEvents(window: BrowserWindow) { } function sendWindowStateEvent(window: BrowserWindow, windowState: WindowState) { - window.webContents.send(Channel.windowStateChanged, windowState); + window.webContents.send(WindowStateChannel.Changed, windowState); } diff --git a/main/tsconfig.main.json b/main/tsconfig.main.json index f2a2e501..39a77301 100644 --- a/main/tsconfig.main.json +++ b/main/tsconfig.main.json @@ -13,6 +13,12 @@ "types": ["node"], "lib": ["es2017", "es2016", "es2015", "dom"], "paths": { + "@ucap-webmessenger/electron-core": [ + "../projects/ucap-webmessenger-electron-core/src/public-api" + ], + "@ucap-webmessenger/electron-notification": [ + "../projects/ucap-webmessenger-electron-notification/src/public-api" + ], "@ucap-webmessenger/native": [ "../projects/ucap-webmessenger-native/src/public-api" ], diff --git a/package-lock.json b/package-lock.json index bade9cfb..53a09073 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3101,15 +3101,6 @@ "integrity": "sha1-9wtzXGvKGlycItmCw+Oef+ujva0=", "dev": true }, - "async": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz", - "integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==", - "dev": true, - "requires": { - "lodash": "^4.17.14" - } - }, "async-each": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.3.tgz", @@ -8061,6 +8052,15 @@ "once": "^1.4.0" }, "dependencies": { + "async": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz", + "integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==", + "dev": true, + "requires": { + "lodash": "^4.17.14" + } + }, "istanbul-lib-coverage": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz", @@ -11239,6 +11239,15 @@ "mkdirp": "^0.5.1" }, "dependencies": { + "async": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz", + "integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==", + "dev": true, + "requires": { + "lodash": "^4.17.14" + } + }, "debug": { "version": "3.2.6", "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", @@ -13255,6 +13264,17 @@ "requires": { "async": "^2.5.0", "loader-utils": "^1.1.0" + }, + "dependencies": { + "async": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz", + "integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==", + "dev": true, + "requires": { + "lodash": "^4.17.14" + } + } } }, "source-map-resolve": { @@ -13519,6 +13539,15 @@ "lodash": "^4.17.14" }, "dependencies": { + "async": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz", + "integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==", + "dev": true, + "requires": { + "lodash": "^4.17.14" + } + }, "debug": { "version": "3.2.6", "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", @@ -14116,8 +14145,7 @@ "tslib": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz", - "integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==", - "dev": true + "integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==" }, "tslint": { "version": "5.15.0", diff --git a/package.json b/package.json index 61d69f6a..bb242173 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "start": "npm-run-all -p start:renderer start:main", "start:main": "wait-on http-get://localhost:4200/ && npm run build:main:dev && electron --nolazy --inspect-brk=9229 .", "start:renderer": "ng serve", + "start:web": "cross-env UCAP_ENV=WEB ng serve", "start:production": "npm run build:renderer && npm run build:main:prod && electron --nolazy --inspect-brk=9229 .", "build:renderer": "cross-env NODE_ENV=production ng build --base-href ./", "build:main:dev": "cross-env NODE_ENV=development TS_NODE_PROJECT='./config/tsconfig.webpack.json' parallel-webpack --config=config/main.webpack.config.ts", @@ -15,7 +16,9 @@ "e2e": "ng e2e" }, "private": true, - "dependencies": {}, + "dependencies": { + "tslib": "^1.10.0" + }, "devDependencies": { "@angular-builders/custom-webpack": "^8.2.0", "@angular-devkit/build-angular": "~0.803.14", diff --git a/projects/ucap-webmessenger-electron-core/README.md b/projects/ucap-webmessenger-electron-core/README.md new file mode 100644 index 00000000..80da145b --- /dev/null +++ b/projects/ucap-webmessenger-electron-core/README.md @@ -0,0 +1,24 @@ +# UcapWebmessengerElectronCore + +This library was generated with [Angular CLI](https://github.com/angular/angular-cli) version 8.2.11. + +## Code scaffolding + +Run `ng generate component component-name --project ucap-webmessenger-electron-core` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module --project ucap-webmessenger-electron-core`. +> Note: Don't forget to add `--project ucap-webmessenger-electron-core` or else it will be added to the default project in your `angular.json` file. + +## Build + +Run `ng build ucap-webmessenger-electron-core` to build the project. The build artifacts will be stored in the `dist/` directory. + +## Publishing + +After building your library with `ng build ucap-webmessenger-electron-core`, go to the dist folder `cd dist/ucap-webmessenger-electron-core` and run `npm publish`. + +## Running unit tests + +Run `ng test ucap-webmessenger-electron-core` to execute the unit tests via [Karma](https://karma-runner.github.io). + +## Further help + +To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md). diff --git a/projects/ucap-webmessenger-electron-core/karma.conf.js b/projects/ucap-webmessenger-electron-core/karma.conf.js new file mode 100644 index 00000000..5eed1538 --- /dev/null +++ b/projects/ucap-webmessenger-electron-core/karma.conf.js @@ -0,0 +1,32 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage-istanbul-reporter'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + coverageIstanbulReporter: { + dir: require('path').join(__dirname, '../../coverage/ucap-webmessenger-electron-core'), + reports: ['html', 'lcovonly', 'text-summary'], + fixWebpackSourcePaths: true + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/projects/ucap-webmessenger-electron-core/ng-package.json b/projects/ucap-webmessenger-electron-core/ng-package.json new file mode 100644 index 00000000..e9e6f76e --- /dev/null +++ b/projects/ucap-webmessenger-electron-core/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../dist/ucap-webmessenger-electron-core", + "lib": { + "entryFile": "src/public-api.ts" + } +} \ No newline at end of file diff --git a/projects/ucap-webmessenger-electron-core/package.json b/projects/ucap-webmessenger-electron-core/package.json new file mode 100644 index 00000000..a7ad1a34 --- /dev/null +++ b/projects/ucap-webmessenger-electron-core/package.json @@ -0,0 +1,8 @@ +{ + "name": "@ucap-webmessenger/electron-core", + "version": "0.0.1", + "peerDependencies": { + "@angular/common": "^8.2.11", + "@angular/core": "^8.2.11" + } +} diff --git a/projects/ucap-webmessenger-electron-core/src/lib/types/channel.type.ts b/projects/ucap-webmessenger-electron-core/src/lib/types/channel.type.ts new file mode 100644 index 00000000..aab87d73 --- /dev/null +++ b/projects/ucap-webmessenger-electron-core/src/lib/types/channel.type.ts @@ -0,0 +1,37 @@ +export enum ElectronAppChannel { + WillFinishLaunching = 'will-finish-launching', + Ready = 'ready', + WindowAllClosed = 'window-all-closed', + BeforeQuit = 'before-quit', + WillQuit = 'will-quit', + Quit = 'quit', + OpenFile = 'open-file', + OpenUrl = 'open-url', + Activate = 'activate', + ContinueActivity = 'continue-activity', + WillContinueActivity = 'will-continue-activity', + ContinueActivityError = 'continue-activity-error', + ActivityWasContinued = 'activity-was-continued', + SecondInstance = 'second-instance' +} + +export enum ElectronBrowserWindowChannel { + EnterFullScreen = 'enter-full-screen', + LeaveFullScreen = 'leave-full-screen', + Maximize = 'maximize', + Minimize = 'minimize', + Unmaximize = 'unmaximize', + Restore = 'restore', + Hide = 'hide', + Show = 'show', + Close = 'close', + Closed = 'closed', + ReadyToShow = 'ready-to-show' +} + +export enum ElectronWebContentsChannel { + DevtoolsOpened = 'devtools-opened', + DidStartLoading = 'did-start-loading', + DidFinishLoad = 'did-finish-load', + DidFailLoad = 'did-fail-load' +} diff --git a/projects/ucap-webmessenger-electron-core/src/public-api.ts b/projects/ucap-webmessenger-electron-core/src/public-api.ts new file mode 100644 index 00000000..31f5a3de --- /dev/null +++ b/projects/ucap-webmessenger-electron-core/src/public-api.ts @@ -0,0 +1,5 @@ +/* + * Public API Surface of ucap-webmessenger-electron-core + */ + +export * from './lib/types/channel.type'; diff --git a/projects/ucap-webmessenger-electron-core/src/test.ts b/projects/ucap-webmessenger-electron-core/src/test.ts new file mode 100644 index 00000000..978c64fb --- /dev/null +++ b/projects/ucap-webmessenger-electron-core/src/test.ts @@ -0,0 +1,21 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/dist/zone'; +import 'zone.js/dist/zone-testing'; +import { getTestBed } from '@angular/core/testing'; +import { + BrowserDynamicTestingModule, + platformBrowserDynamicTesting +} from '@angular/platform-browser-dynamic/testing'; + +declare const require: any; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment( + BrowserDynamicTestingModule, + platformBrowserDynamicTesting() +); +// Then we find all the tests. +const context = require.context('./', true, /\.spec\.ts$/); +// And load the modules. +context.keys().map(context); diff --git a/projects/ucap-webmessenger-electron-core/tsconfig.lib.json b/projects/ucap-webmessenger-electron-core/tsconfig.lib.json new file mode 100644 index 00000000..bd23948e --- /dev/null +++ b/projects/ucap-webmessenger-electron-core/tsconfig.lib.json @@ -0,0 +1,26 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "../../out-tsc/lib", + "target": "es2015", + "declaration": true, + "inlineSources": true, + "types": [], + "lib": [ + "dom", + "es2018" + ] + }, + "angularCompilerOptions": { + "annotateForClosureCompiler": true, + "skipTemplateCodegen": true, + "strictMetadataEmit": true, + "fullTemplateTypeCheck": true, + "strictInjectionParameters": true, + "enableResourceInlining": true + }, + "exclude": [ + "src/test.ts", + "**/*.spec.ts" + ] +} diff --git a/projects/ucap-webmessenger-electron-core/tsconfig.spec.json b/projects/ucap-webmessenger-electron-core/tsconfig.spec.json new file mode 100644 index 00000000..16da33db --- /dev/null +++ b/projects/ucap-webmessenger-electron-core/tsconfig.spec.json @@ -0,0 +1,17 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "../../out-tsc/spec", + "types": [ + "jasmine", + "node" + ] + }, + "files": [ + "src/test.ts" + ], + "include": [ + "**/*.spec.ts", + "**/*.d.ts" + ] +} diff --git a/projects/ucap-webmessenger-electron-core/tslint.json b/projects/ucap-webmessenger-electron-core/tslint.json new file mode 100644 index 00000000..97d86d7a --- /dev/null +++ b/projects/ucap-webmessenger-electron-core/tslint.json @@ -0,0 +1,17 @@ +{ + "extends": "../../tslint.json", + "rules": { + "directive-selector": [ + true, + "attribute", + "ucapElectronCore", + "camelCase" + ], + "component-selector": [ + true, + "element", + "ucap-electron-core", + "kebab-case" + ] + } +} diff --git a/projects/ucap-webmessenger-electron-notification/README.md b/projects/ucap-webmessenger-electron-notification/README.md new file mode 100644 index 00000000..3da195b7 --- /dev/null +++ b/projects/ucap-webmessenger-electron-notification/README.md @@ -0,0 +1,24 @@ +# UcapWebmessengerElectronNotification + +This library was generated with [Angular CLI](https://github.com/angular/angular-cli) version 8.2.11. + +## Code scaffolding + +Run `ng generate component component-name --project ucap-webmessenger-electron-notification` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module --project ucap-webmessenger-electron-notification`. +> Note: Don't forget to add `--project ucap-webmessenger-electron-notification` or else it will be added to the default project in your `angular.json` file. + +## Build + +Run `ng build ucap-webmessenger-electron-notification` to build the project. The build artifacts will be stored in the `dist/` directory. + +## Publishing + +After building your library with `ng build ucap-webmessenger-electron-notification`, go to the dist folder `cd dist/ucap-webmessenger-electron-notification` and run `npm publish`. + +## Running unit tests + +Run `ng test ucap-webmessenger-electron-notification` to execute the unit tests via [Karma](https://karma-runner.github.io). + +## Further help + +To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md). diff --git a/projects/ucap-webmessenger-electron-notification/karma.conf.js b/projects/ucap-webmessenger-electron-notification/karma.conf.js new file mode 100644 index 00000000..2b6d790e --- /dev/null +++ b/projects/ucap-webmessenger-electron-notification/karma.conf.js @@ -0,0 +1,32 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage-istanbul-reporter'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + coverageIstanbulReporter: { + dir: require('path').join(__dirname, '../../coverage/ucap-webmessenger-electron-notification'), + reports: ['html', 'lcovonly', 'text-summary'], + fixWebpackSourcePaths: true + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/projects/ucap-webmessenger-electron-notification/ng-package.json b/projects/ucap-webmessenger-electron-notification/ng-package.json new file mode 100644 index 00000000..fc7aaf11 --- /dev/null +++ b/projects/ucap-webmessenger-electron-notification/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../dist/ucap-webmessenger-electron-notification", + "lib": { + "entryFile": "src/public-api.ts" + } +} \ No newline at end of file diff --git a/projects/ucap-webmessenger-electron-notification/package.json b/projects/ucap-webmessenger-electron-notification/package.json new file mode 100644 index 00000000..b9dbe1af --- /dev/null +++ b/projects/ucap-webmessenger-electron-notification/package.json @@ -0,0 +1,5 @@ +{ + "name": "@ucap-webmessenger/electron-notification", + "version": "0.0.1", + "peerDependencies": {} +} diff --git a/projects/ucap-webmessenger-electron-notification/src/lib/models/electron-notification-options.ts b/projects/ucap-webmessenger-electron-notification/src/lib/models/electron-notification-options.ts new file mode 100644 index 00000000..d25ec28d --- /dev/null +++ b/projects/ucap-webmessenger-electron-notification/src/lib/models/electron-notification-options.ts @@ -0,0 +1,113 @@ +import * as path from 'path'; +import { BrowserWindowConstructorOptions } from 'electron'; + +export interface ElectronNotificationOptions { + width?: number; + height?: number; + padding?: number; + borderRadius?: number; + displayTime?: number; + animationSteps?: number; + animationStepMs?: number; + animateInParallel?: boolean; + appIcon?: string; + pathToModule?: string; + logging?: boolean; + defaultStyleContainer?: { + [attribute: string]: any; + }; + defaultStyleAppIcon?: { + [attribute: string]: any; + }; + defaultStyleImage?: { + [attribute: string]: any; + }; + defaultStyleClose?: { + [attribute: string]: any; + }; + defaultStyleText?: { + [attribute: string]: any; + }; + defaultWindow?: BrowserWindowConstructorOptions; + templatePath?: string; + htmlTemplate?: string; +} + +export const DefaultElectronNotificationOptions: ElectronNotificationOptions = { + width: 300, + height: 65, + padding: 10, + borderRadius: 5, + displayTime: 5000, + animationSteps: 5, + animationStepMs: 20, + appIcon: null, + pathToModule: '', + logging: true, + + defaultStyleContainer: { + backgroundColor: '#f0f0f0', + overflow: 'hidden', + padding: 8, + border: '1px solid #CCC', + fontFamily: 'Arial', + fontSize: 12, + position: 'relative', + lineHeight: '15px' + }, + defaultStyleAppIcon: { + overflow: 'hidden', + float: 'left', + height: 40, + width: 40, + marginRight: 10 + }, + defaultStyleImage: { + overflow: 'hidden', + float: 'right', + height: 40, + width: 40, + marginLeft: 10 + }, + defaultStyleClose: { + position: 'absolute', + top: 1, + right: 3, + fontSize: 11, + color: '#CCC' + }, + defaultStyleText: { + margin: 0, + overflow: 'hidden', + cursor: 'default' + }, + defaultWindow: { + alwaysOnTop: true, + skipTaskbar: true, + resizable: false, + show: false, + frame: false, + transparent: true, + acceptFirstMouse: true, + webPreferences: { + preload: path.join(__dirname, 'preload.js'), + webSecurity: false, + allowRunningInsecureContent: true + } + }, + htmlTemplate: + '\n' + + '\n' + + '\n' + + '
\n' + + ' \n' + + ' \n' + + '
\n' + + ' \n' + + '

\n' + + '
\n' + + '
X
\n' + + '
\n' + + '\n' + + '' +}; diff --git a/projects/ucap-webmessenger-electron-notification/src/lib/models/electron-notification.ts b/projects/ucap-webmessenger-electron-notification/src/lib/models/electron-notification.ts new file mode 100644 index 00000000..c7416e57 --- /dev/null +++ b/projects/ucap-webmessenger-electron-notification/src/lib/models/electron-notification.ts @@ -0,0 +1,20 @@ +import { ElectronNotificationEventType } from '../types/event.type'; + +export interface ElectronNotificationEvent { + type: ElectronNotificationEventType; + id: number; + close?: (reason: any) => void; +} + +export interface ElectronNotification { + id?: number; + displayTime?: number; + title?: string; + text?: string; + image?: string; + url?: string; + sound?: string; + onClick?: (e: ElectronNotificationEvent) => void; + onShow?: (e: ElectronNotificationEvent) => void; + onClose?: (e: ElectronNotificationEvent) => void; +} diff --git a/projects/ucap-webmessenger-electron-notification/src/lib/services/electron-notification.service.ts b/projects/ucap-webmessenger-electron-notification/src/lib/services/electron-notification.service.ts new file mode 100644 index 00000000..1800f793 --- /dev/null +++ b/projects/ucap-webmessenger-electron-notification/src/lib/services/electron-notification.service.ts @@ -0,0 +1,461 @@ +import * as path from 'path'; +import * as url from 'url'; + +import { AnimationQueue } from '../utils/animation-queue'; +import { + ElectronNotificationOptions, + DefaultElectronNotificationOptions +} from '../models/electron-notification-options'; +import { screen, BrowserWindow, ipcMain, IpcMainEvent, shell } from 'electron'; +import { ElectronNotification } from '../models/electron-notification'; +import { ElectronNotificationEventType } from '../types/event.type'; +import { Channel } from '../types/channel.type'; +import { ElectronWebContentsChannel } from '@ucap-webmessenger/electron-core'; + +const onClickElectronNotification = 'onClickElectronNotification'; +const onCloseElectronNotification = 'onCloseElectronNotification'; + +interface ENPoint { + x: number; + y: number; +} + +interface ENDimension { + width: number; + height: number; +} + +export class ElectronNotificationService { + private animationQueue: AnimationQueue; + private customOptions: ElectronNotificationOptions; + private nextInsertPosition: ENPoint; + private totalDimension: ENDimension; + private firstPosition: ENPoint; + private lowerRightCornerPosition: ENPoint; + private maxVisibleNotifications: number; + private activeNotifications: BrowserWindow[]; + private inactiveWindows: BrowserWindow[]; + private notificationQueue: ElectronNotification[]; + private closedNotifications: Map; + private latestId: number; + private templateUrl: string; + + constructor(options?: ElectronNotificationOptions) { + this.customOptions = { + ...DefaultElectronNotificationOptions + }; + if (!!options) { + this.customOptions = { + ...this.customOptions, + ...options + }; + } + + this.setup(); + this.setupEvents(); + } + + set options(options: ElectronNotificationOptions) { + if (!!options) { + this.customOptions = { + ...this.customOptions, + ...options + }; + } + this.calcDimensions(); + } + + get options(): ElectronNotificationOptions { + return this.customOptions; + } + + set templatePath(templatePath: string) { + if (!!templatePath) { + this.customOptions.templatePath = templatePath; + this.updateTemplatePath(); + } + } + + get templatePath(): string { + if (!this.templateUrl) { + this.updateTemplatePath(); + } + return this.templateUrl; + } + + notify(notification: ElectronNotification): number { + notification.id = this.latestId++; + this.animationQueue.push({ + context: this, + func: this.showNotification, + args: [notification] + }); + return notification.id; + } + + dispose(): void { + this.animationQueue.clear(); + this.activeNotifications.forEach(window => window.close()); + this.inactiveWindows.forEach(window => window.close()); + } + + closeAll(): void { + this.animationQueue.clear(); + this.activeNotifications.forEach(window => window.close()); + this.inactiveWindows.forEach(window => window.close()); + + this.setup(); + } + + private setup(): void { + this.nextInsertPosition = { x: 0, y: 0 }; + this.totalDimension = { width: 0, height: 0 }; + this.firstPosition = { x: 0, y: 0 }; + this.activeNotifications = []; + this.inactiveWindows = []; + this.notificationQueue = []; + this.closedNotifications = new Map(); + this.latestId = 0; + + this.animationQueue = new AnimationQueue(); + + const display = screen.getPrimaryDisplay(); + + this.lowerRightCornerPosition = { + x: display.bounds.x + display.workArea.x + display.workAreaSize.width, + y: display.bounds.y + display.workArea.y + display.workAreaSize.height + }; + + this.calcDimensions(); + + this.maxVisibleNotifications = Math.floor( + display.workAreaSize.height / this.totalDimension.height + ); + + this.maxVisibleNotifications = + 7 < this.maxVisibleNotifications ? 7 : this.maxVisibleNotifications; + } + + private setupEvents(): void { + const self = this; + ipcMain.on( + Channel.close, + ( + event: IpcMainEvent, + windowId: number, + notification: ElectronNotification + ) => { + const onClose = self.buildCloseNotification( + BrowserWindow.fromId(windowId), + notification + ); + self.buildCloseNotificationSafely(onClose)('close'); + } + ); + + ipcMain.on( + Channel.click, + ( + event: IpcMainEvent, + windowId: number, + notification: ElectronNotification + ) => { + if (!!notification.url) { + shell.openExternal(notification.url); + } + const notificationWindow = BrowserWindow.fromId(windowId); + + if ( + notificationWindow && + notificationWindow[onClickElectronNotification] + ) { + const onClose = self.buildCloseNotification( + BrowserWindow.fromId(windowId), + notification + ); + notificationWindow[onClickElectronNotification]({ + type: ElectronNotificationEventType.Click, + id: notification.id, + close: self.buildCloseNotificationSafely(onClose) + }); + delete notificationWindow[onClickElectronNotification]; + } + } + ); + } + + private calcDimensions() { + this.totalDimension = { + width: this.customOptions.width + this.customOptions.padding, + height: this.customOptions.height + this.customOptions.padding + }; + + this.firstPosition = { + x: this.lowerRightCornerPosition.x - this.totalDimension.width, + y: this.lowerRightCornerPosition.y - this.totalDimension.height + }; + + this.nextInsertPosition = { + x: this.firstPosition.x, + y: this.firstPosition.y + }; + } + + private calcInsertPosition() { + if (this.activeNotifications.length < this.maxVisibleNotifications) { + this.nextInsertPosition.y = + this.lowerRightCornerPosition.y - + this.totalDimension.height * (this.activeNotifications.length + 1); + } + } + + private updateTemplatePath() { + try { + import('fs') + .then(fs => { + fs.statSync(this.customOptions.templatePath).isFile(); + + this.templateUrl = url.format({ + pathname: this.customOptions.templatePath, + protocol: 'file:', + slashes: true + }); + }) + .catch(reason => { + throw reason; + }); + } catch (e) { + console.log( + 'electron-notify: Could not find template ("' + + this.customOptions.templatePath + + '").' + ); + console.log( + 'electron-notify: To use a different template you need to correct the config.templatePath or simply adapt config.htmlTemplate' + ); + } + } + + private showNotification(notification: ElectronNotification): Promise { + const self = this; + return new Promise((resolve, reject) => { + if (this.activeNotifications.length < this.maxVisibleNotifications) { + self.getWindow().then(notificationWindow => { + self.calcInsertPosition(); + notificationWindow.setPosition( + self.nextInsertPosition.x, + self.nextInsertPosition.y + ); + self.activeNotifications.push(notificationWindow); + + const displayTime = !!notification.displayTime + ? notification.displayTime + : self.customOptions.displayTime; + let timeoutId: any; + const onClose = self.buildCloseNotification( + notificationWindow, + notification, + () => timeoutId + ); + const onCloseNotificationSafely = self.buildCloseNotificationSafely( + onClose + ); + timeoutId = setTimeout(() => { + if (notificationWindow.isDestroyed()) { + return; + } + onCloseNotificationSafely('timeout'); + }, displayTime); + + if (!!notification.onShow) { + notification.onShow({ + type: ElectronNotificationEventType.Show, + id: notification.id, + close: onCloseNotificationSafely + }); + } + + if (!!notification.onClose) { + notificationWindow[onClickElectronNotification] = + notification.onClick; + } else { + delete notificationWindow[onClickElectronNotification]; + } + + if (!!notification.onClose) { + notificationWindow[onCloseElectronNotification] = + notification.onClose; + } else { + delete notificationWindow[onCloseElectronNotification]; + } + + notificationWindow.webContents.send( + Channel.browserWindowSetContents, + notification + ); + notificationWindow.showInactive(); + resolve(notificationWindow); + }); + } else { + self.notificationQueue.push(notification); + resolve(); + } + }); + } + + private buildCloseNotification( + notificationWindow: BrowserWindow, + notification: ElectronNotification, + timeoutIdFunc?: () => number + ) { + const self = this; + return (e: ElectronNotificationEventType): Promise => { + if (notificationWindow.isDestroyed()) { + return; + } + + if (self.closedNotifications.has(notification.id)) { + self.closedNotifications.delete(notification.id); + return new Promise(resolve => { + resolve(); + }); + } else { + self.closedNotifications.set(notification.id, true); + } + + if (!!notificationWindow[onCloseElectronNotification]) { + notificationWindow[onCloseElectronNotification]({ + type: e, + id: notification.id + }); + delete notificationWindow[onCloseElectronNotification]; + } + + notificationWindow.webContents.send(Channel.reset); + + if (!!timeoutIdFunc) { + clearTimeout(timeoutIdFunc()); + } + const i = self.activeNotifications.indexOf(notificationWindow); + self.activeNotifications.splice(i, 1); + self.inactiveWindows.push(notificationWindow); + + notificationWindow.hide(); + self.checkForQueuedNotifications(); + + return self.moveOneDown(i); + }; + } + + private buildCloseNotificationSafely( + onClose: (e: ElectronNotificationEventType) => any + ) { + const self = this; + return (reason: any) => { + if (!reason) { + reason = 'closedByAPI'; + } + self.animationQueue.push({ + context: self, + func: onClose, + args: [reason] + }); + }; + } + + private checkForQueuedNotifications(): void { + if ( + 0 < this.notificationQueue.length && + this.activeNotifications.length < this.maxVisibleNotifications + ) { + this.animationQueue.push({ + context: this, + func: this.showNotification, + args: [this.notificationQueue.shift()] + }); + } + } + + private getWindow(): Promise { + const slef = this; + return new Promise((resolve, reject) => { + if (0 < slef.inactiveWindows.length) { + resolve(slef.inactiveWindows.pop()); + } else { + const windowProperties = slef.customOptions.defaultWindow; + windowProperties.width = slef.customOptions.width; + windowProperties.height = slef.customOptions.height; + + const notificationWindow = new BrowserWindow(windowProperties); + notificationWindow.setVisibleOnAllWorkspaces(true); + notificationWindow.loadURL(slef.templatePath); + notificationWindow.webContents.on( + ElectronWebContentsChannel.DidFinishLoad, + () => { + // Done + notificationWindow.webContents.send( + Channel.loadConfig, + slef.customOptions + ); + resolve(notificationWindow); + } + ); + notificationWindow.webContents.on( + ElectronWebContentsChannel.DevtoolsOpened, + () => { + notificationWindow.webContents.closeDevTools(); + } + ); + } + }); + } + + private moveOneDown(startPos: number): Promise { + const self = this; + return new Promise(async (resolve, reject) => { + if (startPos >= self.activeNotifications.length || -1 === startPos) { + resolve(); + return; + } + + const aryNotificationPos: number[] = []; + for (let i = startPos; i < self.activeNotifications.length; i++) { + aryNotificationPos.push(i); + } + + await Promise.all( + aryNotificationPos.map(async index => { + await self.moveNotificationAnimation(index); + }) + ); + resolve(); + }); + } + + private moveNotificationAnimation(index: number): Promise { + const self = this; + return new Promise((resolve, reject) => { + const notificationWindow = self.activeNotifications[index]; + const newY = + self.lowerRightCornerPosition.y - + self.totalDimension.height * (index + 1); + const startY = notificationWindow.getPosition()[1]; + const step = (newY - startY) / self.customOptions.animationSteps; + let curStep = 1; + const animationInterval = setInterval(() => { + // Abort condition + if (curStep === self.customOptions.animationSteps) { + notificationWindow.setPosition(self.firstPosition.x, newY); + clearInterval(animationInterval); + return resolve(); + } + // Move one step down + notificationWindow.setPosition( + self.firstPosition.x, + Math.trunc(startY + curStep * step) + ); + curStep++; + }, self.customOptions.animationStepMs); + }); + } +} diff --git a/projects/ucap-webmessenger-electron-notification/src/lib/types/channel.type.ts b/projects/ucap-webmessenger-electron-notification/src/lib/types/channel.type.ts new file mode 100644 index 00000000..088e0e91 --- /dev/null +++ b/projects/ucap-webmessenger-electron-notification/src/lib/types/channel.type.ts @@ -0,0 +1,7 @@ +export enum Channel { + close = 'UCAP::ElectronNotification::close', + click = 'UCAP::ElectronNotification::click', + loadConfig = 'UCAP::ElectronNotification::loadConfig', + reset = 'UCAP::ElectronNotification::reset', + browserWindowSetContents = 'UCAP::ElectronNotification::BrowserWindowSetContents' +} diff --git a/projects/ucap-webmessenger-electron-notification/src/lib/types/event.type.ts b/projects/ucap-webmessenger-electron-notification/src/lib/types/event.type.ts new file mode 100644 index 00000000..2953e3ce --- /dev/null +++ b/projects/ucap-webmessenger-electron-notification/src/lib/types/event.type.ts @@ -0,0 +1,5 @@ +export enum ElectronNotificationEventType { + Show = 'Show', + Click = 'Click', + Close = 'Close' +} diff --git a/projects/ucap-webmessenger-electron-notification/src/lib/utils/animation-queue.ts b/projects/ucap-webmessenger-electron-notification/src/lib/utils/animation-queue.ts new file mode 100644 index 00000000..c421f726 --- /dev/null +++ b/projects/ucap-webmessenger-electron-notification/src/lib/utils/animation-queue.ts @@ -0,0 +1,42 @@ +export interface AnimationQueueObject { + context: any; + func: (...args: any[]) => Promise; + args: any[]; +} + +export class AnimationQueue { + private running = false; + private queue: AnimationQueueObject[] = []; + + push(o: AnimationQueueObject): void { + if (this.running) { + this.queue.push(o); + } else { + this.running = true; + this.animate(o); + } + } + + animate(o: AnimationQueueObject): void { + const self = this; + try { + (o.func.apply(o.context, o.args) as Promise) + .then(() => { + if (self.queue.length > 0) { + self.animate.call(self, self.queue.shift()); + } else { + self.running = false; + } + }) + .catch(reason => { + console.log(reason); + }); + } catch (e) { + console.log(e); + } + } + + clear(): void { + this.queue = []; + } +} diff --git a/projects/ucap-webmessenger-electron-notification/src/public-api.ts b/projects/ucap-webmessenger-electron-notification/src/public-api.ts new file mode 100644 index 00000000..d09fea3e --- /dev/null +++ b/projects/ucap-webmessenger-electron-notification/src/public-api.ts @@ -0,0 +1,13 @@ +/* + * Public API Surface of ucap-webmessenger-electron-notification + */ + +export * from './lib/models/electron-notification-options'; +export * from './lib/models/electron-notification'; + +export * from './lib/services/electron-notification.service'; + +export * from './lib/types/channel.type'; +export * from './lib/types/event.type'; + +export * from './lib/utils/animation-queue'; diff --git a/projects/ucap-webmessenger-electron-notification/src/test.ts b/projects/ucap-webmessenger-electron-notification/src/test.ts new file mode 100644 index 00000000..978c64fb --- /dev/null +++ b/projects/ucap-webmessenger-electron-notification/src/test.ts @@ -0,0 +1,21 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/dist/zone'; +import 'zone.js/dist/zone-testing'; +import { getTestBed } from '@angular/core/testing'; +import { + BrowserDynamicTestingModule, + platformBrowserDynamicTesting +} from '@angular/platform-browser-dynamic/testing'; + +declare const require: any; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment( + BrowserDynamicTestingModule, + platformBrowserDynamicTesting() +); +// Then we find all the tests. +const context = require.context('./', true, /\.spec\.ts$/); +// And load the modules. +context.keys().map(context); diff --git a/projects/ucap-webmessenger-electron-notification/tsconfig.lib.json b/projects/ucap-webmessenger-electron-notification/tsconfig.lib.json new file mode 100644 index 00000000..bd23948e --- /dev/null +++ b/projects/ucap-webmessenger-electron-notification/tsconfig.lib.json @@ -0,0 +1,26 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "../../out-tsc/lib", + "target": "es2015", + "declaration": true, + "inlineSources": true, + "types": [], + "lib": [ + "dom", + "es2018" + ] + }, + "angularCompilerOptions": { + "annotateForClosureCompiler": true, + "skipTemplateCodegen": true, + "strictMetadataEmit": true, + "fullTemplateTypeCheck": true, + "strictInjectionParameters": true, + "enableResourceInlining": true + }, + "exclude": [ + "src/test.ts", + "**/*.spec.ts" + ] +} diff --git a/projects/ucap-webmessenger-electron-notification/tsconfig.spec.json b/projects/ucap-webmessenger-electron-notification/tsconfig.spec.json new file mode 100644 index 00000000..16da33db --- /dev/null +++ b/projects/ucap-webmessenger-electron-notification/tsconfig.spec.json @@ -0,0 +1,17 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "../../out-tsc/spec", + "types": [ + "jasmine", + "node" + ] + }, + "files": [ + "src/test.ts" + ], + "include": [ + "**/*.spec.ts", + "**/*.d.ts" + ] +} diff --git a/projects/ucap-webmessenger-electron-notification/tslint.json b/projects/ucap-webmessenger-electron-notification/tslint.json new file mode 100644 index 00000000..e37a3555 --- /dev/null +++ b/projects/ucap-webmessenger-electron-notification/tslint.json @@ -0,0 +1,17 @@ +{ + "extends": "../../tslint.json", + "rules": { + "directive-selector": [ + true, + "attribute", + "ucapElectronNotification", + "camelCase" + ], + "component-selector": [ + true, + "element", + "ucap-electron-notification", + "kebab-case" + ] + } +} diff --git a/projects/ucap-webmessenger-native-browser/src/lib/services/browser-native.service.ts b/projects/ucap-webmessenger-native-browser/src/lib/services/browser-native.service.ts index a00afa78..17a18a6d 100644 --- a/projects/ucap-webmessenger-native-browser/src/lib/services/browser-native.service.ts +++ b/projects/ucap-webmessenger-native-browser/src/lib/services/browser-native.service.ts @@ -3,14 +3,15 @@ import { Observable } from 'rxjs'; import { NativeService, WindowState, - NotiRequest, + NotificationRequest, WindowIdle } from '@ucap-webmessenger/native'; import { HttpClient } from '@angular/common/http'; import { map } from 'rxjs/operators'; export class BrowserNativeService implements NativeService { - showNotify(noti: NotiRequest): void {} + notify(noti: NotificationRequest): void {} + closeAllNotify(): void {} checkForUpdates(): Observable { return new Observable(subscriber => { diff --git a/projects/ucap-webmessenger-native-electron/src/lib/services/electron-native.service.ts b/projects/ucap-webmessenger-native-electron/src/lib/services/electron-native.service.ts index a051e22d..1d3b967c 100644 --- a/projects/ucap-webmessenger-native-electron/src/lib/services/electron-native.service.ts +++ b/projects/ucap-webmessenger-native-electron/src/lib/services/electron-native.service.ts @@ -5,11 +5,17 @@ import { Observable, Subject } from 'rxjs'; import { NativeService, WindowState, - NotiRequest, + NotificationRequest, WindowIdle } from '@ucap-webmessenger/native'; -import { Channel } from '../types/channel.type'; import { share } from 'rxjs/operators'; +import { + NotificationChannel, + UpdaterChannel, + FileChannel, + WindowStateChannel, + IdleStateChannel +} from '../types/channel.type'; export class ElectronNativeService implements NativeService { private windowStateChangedSubject: Subject | null = null; @@ -18,22 +24,18 @@ export class ElectronNativeService implements NativeService { private idleStateChangedSubject: Subject | null = null; private idleStateChanged$: Observable | null = null; - showNotify(noti: NotiRequest): void { - ipcRenderer.send( - Channel.showNotify, - noti.roomSeq, - noti.title, - noti.contents, - noti.image, - noti.useSound, - noti.interval - ); + notify(noti: NotificationRequest): void { + ipcRenderer.send(NotificationChannel.Notify, noti); + } + + closeAllNotify(): void { + ipcRenderer.send(NotificationChannel.CloseAllNotify); } checkForUpdates(): Observable { return new Observable(subscriber => { try { - subscriber.next(ipcRenderer.sendSync(Channel.checkForUpdates)); + subscriber.next(ipcRenderer.sendSync(UpdaterChannel.Check)); } catch (error) { subscriber.error(error); } finally { @@ -43,13 +45,13 @@ export class ElectronNativeService implements NativeService { } showImageViewer(): void { - ipcRenderer.send(Channel.showImageViewer); + ipcRenderer.send(FileChannel.ShowImageViewer); } readFile(path: string): Observable { return new Observable(subscriber => { try { - subscriber.next(ipcRenderer.sendSync(Channel.readFile, path)); + subscriber.next(ipcRenderer.sendSync(FileChannel.ReadFile, path)); } catch (error) { subscriber.error(error); } finally { @@ -66,7 +68,7 @@ export class ElectronNativeService implements NativeService { return new Observable(subscriber => { try { subscriber.next( - ipcRenderer.sendSync(Channel.saveFile, buffer, fileName, path) + ipcRenderer.sendSync(FileChannel.SaveFile, buffer, fileName, path) ); } catch (error) { subscriber.error(error); @@ -85,9 +87,8 @@ export class ElectronNativeService implements NativeService { } ipcRenderer.on( - Channel.windowStateChanged, + WindowStateChannel.Changed, (event: IpcRendererEvent, windowState: WindowState) => { - console.log('windowStateChanged', windowState); this.windowStateChangedSubject.next(windowState); } ); @@ -133,10 +134,10 @@ export class ElectronNativeService implements NativeService { .pipe(share()); } - ipcRenderer.send(Channel.idleStateStart, 'start'); + ipcRenderer.send(IdleStateChannel.StartCheck); ipcRenderer.on( - Channel.idleStateChanged, + IdleStateChannel.Changed, (event: IpcRendererEvent, idleState: WindowIdle) => { this.idleStateChangedSubject.next(idleState); } diff --git a/projects/ucap-webmessenger-native-electron/src/lib/types/channel.type.ts b/projects/ucap-webmessenger-native-electron/src/lib/types/channel.type.ts index 89e518ad..c698832c 100644 --- a/projects/ucap-webmessenger-native-electron/src/lib/types/channel.type.ts +++ b/projects/ucap-webmessenger-native-electron/src/lib/types/channel.type.ts @@ -1,11 +1,23 @@ -export enum Channel { - windowStateChanged = 'window-state-changed', - idleStateChanged = 'window-idle-state-changed', - idleStateStart = 'window-idle-state-check-start', - - showNotify = 'UCAP::showNotify', - checkForUpdates = 'UCAP::checkForUpdates', - showImageViewer = 'UCAP::showImageViewer', - saveFile = 'UCAP::saveFile', - readFile = 'UCAP::readFile' +export enum NotificationChannel { + Notify = 'UCAP::notification::notify', + CloseAllNotify = 'UCAP::notification::closeAllNotify' +} + +export enum UpdaterChannel { + Check = 'UCAP::updater::check' +} + +export enum FileChannel { + ShowImageViewer = 'UCAP::file::showImageViewer', + SaveFile = 'UCAP::file::saveFile', + ReadFile = 'UCAP::file::readFile' +} + +export enum WindowStateChannel { + Changed = 'UCAP::windowState::windowStateChanged' +} + +export enum IdleStateChannel { + Changed = 'UCAP::idleState::changed', + StartCheck = 'UCAP::idleState::startCheck' } diff --git a/projects/ucap-webmessenger-native/src/lib/models/notification.ts b/projects/ucap-webmessenger-native/src/lib/models/notification.ts new file mode 100644 index 00000000..3eb08394 --- /dev/null +++ b/projects/ucap-webmessenger-native/src/lib/models/notification.ts @@ -0,0 +1,8 @@ +export interface NotificationRequest { + roomSeq: string; + title: string; + contents: string; + image: string; + useSound: boolean; + interval?: number; +} diff --git a/projects/ucap-webmessenger-native/src/lib/services/native.service.ts b/projects/ucap-webmessenger-native/src/lib/services/native.service.ts index bd9c01a0..94ed535f 100644 --- a/projects/ucap-webmessenger-native/src/lib/services/native.service.ts +++ b/projects/ucap-webmessenger-native/src/lib/services/native.service.ts @@ -2,9 +2,11 @@ import { Observable } from 'rxjs'; import { WindowState } from '../types/window-state.type'; import { WindowIdle } from '../types/window-idle.type'; +import { NotificationRequest } from '../models/notification'; export interface NativeService { - showNotify(noti: NotiRequest): void; + notify(noti: NotificationRequest): void; + closeAllNotify(): void; checkForUpdates(): Observable; @@ -20,12 +22,3 @@ export interface NativeService { idleStateChanged(): Observable; } - -export interface NotiRequest { - roomSeq: string; - title: string; - contents: string; - image: string; - useSound: boolean; - interval?: number; -} diff --git a/projects/ucap-webmessenger-native/src/public-api.ts b/projects/ucap-webmessenger-native/src/public-api.ts index bcd36c4f..54f16191 100644 --- a/projects/ucap-webmessenger-native/src/public-api.ts +++ b/projects/ucap-webmessenger-native/src/public-api.ts @@ -2,6 +2,8 @@ * Public API Surface of ucap-webmessenger-native */ +export * from './lib/models/notification'; + export * from './lib/services/native.service'; export * from './lib/types/token'; diff --git a/tsconfig.json b/tsconfig.json index f79668c2..18fcd298 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -125,6 +125,12 @@ ], "@ucap-webmessenger/util": [ "projects/ucap-webmessenger-util/src/public-api" + ], + "@ucap-webmessenger/electron-core": [ + "projects/ucap-webmessenger-electron-core/src/public-api" + ], + "@ucap-webmessenger/electron-notification": [ + "projects/ucap-webmessenger-electron-notification/src/public-api" ] } },