Typescript (Complete notification & notification settings modules) (#609)

* Typescript - Implement notification settings configuration window

* Typescript - Completed notification and notification settings for Context Isolation

* Typescript - Fix code comment

* Typescript - Enable contextIsolation for all windows

* Typescript - Fix notification setup condition
This commit is contained in:
Kiran Niranjan 2019-03-25 13:53:10 +05:30 committed by Kiran Niranjan
parent de6502035b
commit 5905b7507d
18 changed files with 495 additions and 23 deletions

View File

@ -110,10 +110,45 @@
</table>
</body>
<script>
const apiCmds = {
isOnline: 'is-online',
getVersionInfo: 'get-version-info',
registerLogger: 'register-logger',
setBadgeCount: 'set-badge-count',
badgeDataUrl: 'badge-data-url',
activate: 'activate',
registerBoundsChange: 'register-bounds-change',
registerProtocolHandler: 'register-protocol-handler',
registerActivityDetection: 'register-activity-detection',
showNotificationSettings: 'show-notification-settings',
sanitize: 'sanitize',
bringToFront: 'bring-to-front',
openScreenPickerWindow: 'open-screen-picker-window',
popupMenu: 'popup-menu',
optimizeMemoryConsumption: 'optimize-memory-consumption',
optimizeMemoryRegister: 'optimize-memory-register',
setIsInMeeting: 'set-is-in-meeting',
setLocale: 'set-locale',
openScreenSnippet: 'open-screen-snippet',
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',
};
var openConfigWin = document.getElementById('open-config-win');
openConfigWin.addEventListener('click', function() {
ssf.showNotificationSettings();
if (window.ssf) {
ssf.showNotificationSettings();
} else {
postMessage(apiCmds.showNotificationSettings)
}
});
var notfEl = document.getElementById('notf');
@ -281,7 +316,11 @@
document.location.reload();
});
window.addEventListener('message', (event) => console.log(event));
window.addEventListener('message', (event) => console.log(event.data));
const postMessage = (method, data) => {
window.postMessage({ method, data }, '*');
}
</script>
</html>

View File

@ -57,4 +57,4 @@
</div>
</footer>
</body>
</html>
</html>

View File

@ -66,12 +66,12 @@ ipcMain.on(apiName.symphonyApi, (event: Electron.Event, arg: IApiArgs) => {
activityDetection.setWindowAndThreshold(event.sender, arg.period);
}
break;
/*case ApiCmds.showNotificationSettings:
case apiCmds.showNotificationSettings:
if (typeof arg.windowName === 'string') {
configureNotification.openConfigurationWindow(arg.windowName);
windowHandler.createNotificationSettingsWindow(arg.windowName);
}
break;
*/case apiCmds.sanitize:
case apiCmds.sanitize:
if (typeof arg.windowName === 'string') {
sanitize(arg.windowName);
}

View File

@ -8,7 +8,9 @@ import DesktopCapturerSource = Electron.DesktopCapturerSource;
import { apiName, WindowTypes } from '../common/api-interface';
import { isMac, isWindowsOS } from '../common/env';
import { i18n } from '../common/i18n';
import { logger } from '../common/logger';
import { getCommandLineArgs, getGuid } from '../common/utils';
import { notification } from '../renderer/notification';
import { AppMenu } from './app-menu';
import { handleChildWindow } from './child-window-handler';
import { config, IConfig } from './config-handler';
@ -50,6 +52,7 @@ export class WindowHandler {
sandbox: true,
nodeIntegration: false,
devTools: false,
contextIsolation: true,
},
};
}
@ -70,6 +73,27 @@ export class WindowHandler {
webPreferences: {
nodeIntegration: false,
sandbox: true,
contextIsolation: true,
},
winKey: getGuid(),
};
}
/**
* Notification settings window opts
*/
private static getNotificationSettingsOpts(): ICustomBrowserWindowConstructorOpts {
return {
width: 460,
height: 360,
show: false,
modal: true,
autoHideMenuBar: true,
webPreferences: {
sandbox: true,
nodeIntegration: false,
devTools: false,
contextIsolation: true,
},
winKey: getGuid(),
};
@ -94,6 +118,7 @@ export class WindowHandler {
sandbox: true,
nodeIntegration: false,
devTools: false,
contextIsolation: true,
},
winKey: getGuid(),
};
@ -114,6 +139,7 @@ export class WindowHandler {
sandbox: true,
nodeIntegration: false,
devTools: false,
contextIsolation: true,
},
winKey: getGuid(),
};
@ -155,10 +181,11 @@ export class WindowHandler {
private screenPickerWindow: Electron.BrowserWindow | null = null;
private screenSharingIndicatorWindow: Electron.BrowserWindow | null = null;
private basicAuthWindow: Electron.BrowserWindow | null = null;
private notificationSettingsWindow: Electron.BrowserWindow | null = null;
constructor(opts?: Electron.BrowserViewConstructorOptions) {
// Settings
this.config = config.getConfigFields([ 'isCustomTitleBar', 'mainWinPos', 'minimizeOnClose' ]);
// Use these variables only on initial setup
this.config = config.getConfigFields([ 'isCustomTitleBar', 'mainWinPos', 'minimizeOnClose', 'notificationSettings' ]);
this.globalConfig = config.getGlobalConfigFields([ 'url', 'crashReporter' ]);
this.windows = {};
@ -258,7 +285,7 @@ export class WindowHandler {
return this.destroyAllWindow();
}
if (this.config.minimizeOnClose) {
if (config.getConfigFields([ 'minimizeOnClose' ]).minimizeOnClose) {
event.preventDefault();
isMac ? this.mainWindow.hide() : this.mainWindow.minimize();
} else {
@ -318,6 +345,11 @@ export class WindowHandler {
}
}
break;
case 'notification-settings':
if (this.notificationSettingsWindow && windowExists(this.notificationSettingsWindow)) {
this.notificationSettingsWindow.close();
}
break;
default:
break;
}
@ -488,6 +520,66 @@ export class WindowHandler {
ipcMain.once('basic-auth-login', login);
}
/**
* Creates and displays notification settings window
*
* @param windowName
*/
public createNotificationSettingsWindow(windowName: string): void {
const opts = WindowHandler.getNotificationSettingsOpts();
// This prevents creating multiple instances of the
// notification configuration window
if (this.notificationSettingsWindow && !this.notificationSettingsWindow.isDestroyed()) {
if (this.notificationSettingsWindow.isMinimized()) {
this.notificationSettingsWindow.restore();
}
this.notificationSettingsWindow.focus();
return;
}
const allWindows = BrowserWindow.getAllWindows();
const selectedParentWindow = allWindows.find((window) => {
return (window as ICustomBrowserWindow).winName === windowName;
});
if (selectedParentWindow) {
opts.parent = selectedParentWindow;
}
this.notificationSettingsWindow = createComponentWindow('notification-settings', opts);
this.notificationSettingsWindow.setVisibleOnAllWorkspaces(true);
this.notificationSettingsWindow.webContents.on('did-finish-load', () => {
if (this.notificationSettingsWindow && windowExists(this.notificationSettingsWindow)) {
let screens: Electron.Display[] = [];
if (app.isReady()) {
screens = electron.screen.getAllDisplays();
}
const { position, display } = config.getConfigFields([ 'notificationSettings' ]).notificationSettings;
this.notificationSettingsWindow.webContents.send('notification-settings-data', { screens, position, display });
}
});
this.addWindow(opts.winKey, this.notificationSettingsWindow);
ipcMain.once('notification-settings-update', async (_event, args) => {
const { display, position } = args;
try {
await config.updateUserConfig({ notificationSettings: { display, position } });
} catch (e) {
logger.error(`NotificationSettings: Could not update user config file error`, e);
}
if (this.notificationSettingsWindow && windowExists(this.notificationSettingsWindow)) {
this.notificationSettingsWindow.close();
}
// Update latest notification settings from config
notification.updateNotificationSettings();
});
this.notificationSettingsWindow.once('closed', () => {
this.removeWindow(opts.winKey);
this.notificationSettingsWindow = null;
});
}
/**
* Creates a screen sharing indicator whenever uses start
* sharing the screen

View File

@ -55,7 +55,7 @@ export interface IApiArgs {
type: string;
}
export type WindowTypes = 'screen-picker' | 'screen-sharing-indicator';
export type WindowTypes = 'screen-picker' | 'screen-sharing-indicator' | 'notification-settings';
export interface IBadgeCount {
count: number;

View File

@ -118,6 +118,9 @@ export default class AppBridge {
case apiCmds.closeNotification:
notification.hideNotification(data);
break;
case apiCmds.showNotificationSettings:
ssf.showNotificationSettings();
break;
}
}

View File

@ -0,0 +1,179 @@
import { ipcRenderer } from 'electron';
import * as React from 'react';
import { apiCmds, apiName } from '../../common/api-interface';
import { i18n } from '../../common/i18n-preload';
const NOTIFICATION_SETTINGS_NAMESPACE = 'NotificationSettings';
type startCorner = 'upper-right' | 'upper-left' | 'lower-right' | 'lower-left';
interface IState {
position: startCorner;
screens: Electron.Display[];
display: number;
}
/**
* Window that display app version and copyright info
*/
export default class NotificationSettings extends React.Component<{}, IState> {
private readonly eventHandlers = {
onTogglePosition: (e: React.ChangeEvent<HTMLInputElement>) => this.togglePosition(e),
onDisplaySelect: (e: React.ChangeEvent<HTMLSelectElement>) => this.selectDisplay(e),
onClose: () => this.close(),
onSubmit: () => this.submit(),
};
constructor(props) {
super(props);
this.state = {
position: 'upper-right',
screens: [],
display: 1,
};
this.updateState = this.updateState.bind(this);
}
/**
* main render function
*/
public render(): JSX.Element {
return (
<div className='content'>
<header className='header'>
<span className='header__title'>
{i18n.t('Notification Settings', NOTIFICATION_SETTINGS_NAMESPACE)()}
</span>
</header>
<div className='form'>
<form>
<label className='label'>{i18n.t('Monitor', NOTIFICATION_SETTINGS_NAMESPACE)()}</label>
<div id='screens' className='main'>
<label>
{i18n.t('Notification shown on Monitor: ', NOTIFICATION_SETTINGS_NAMESPACE)()}
</label>
<select className='selector' id='screen-selector' title='position' onChange={this.eventHandlers.onDisplaySelect}>
{this.renderScreens()}
</select>
</div>
<label className='label'>{i18n.t('Position', NOTIFICATION_SETTINGS_NAMESPACE)()}</label>
<div className='main'>
<div className='first-set'>
{this.renderRadioButtons('upper-left', 'Top Left')}
{this.renderRadioButtons('lower-left', 'Bottom Left')}
</div>
<div className='second-set'>
{this.renderRadioButtons('upper-right', 'Top Right')}
{this.renderRadioButtons('lower-right', 'Bottom Left')}
</div>
</div>
</form>
</div>
<footer className='footer'>
<div className='buttonLayout'>
<button id='cancel' className='buttonDismiss' onClick={this.eventHandlers.onClose}>
{i18n.t('CANCEL', NOTIFICATION_SETTINGS_NAMESPACE)()}
</button>
<button id='ok-button' className='button' onClick={this.eventHandlers.onSubmit}>
{i18n.t('OK', NOTIFICATION_SETTINGS_NAMESPACE)()}
</button>
</div>
</footer>
</div>
);
}
public componentDidMount(): void {
ipcRenderer.on('notification-settings-data', this.updateState);
}
public componentWillUnmount(): void {
ipcRenderer.removeListener('notification-settings-data', this.updateState);
}
/**
* Renders all 4 different notification position options
*
* @param id
* @param content
*/
private renderRadioButtons(id: startCorner, content: string): JSX.Element {
return (
<div className='radio'>
<label className='radio__label' htmlFor={id}>
{i18n.t(`${content}`, NOTIFICATION_SETTINGS_NAMESPACE)()}
</label>
<input
onChange={this.eventHandlers.onTogglePosition}
className={id}
id={id}
type='radio'
name='position'
checked={this.state.position === id}
value={id}/>
</div>
);
}
/**
* Renders the drop down list of available screen
*/
private renderScreens(): JSX.Element[] {
const { screens, display } = this.state;
return screens.map((screen, index) => {
return (
<option id={String(screen.id)} key={screen.id} value={display}>{index + 1}</option>
);
});
}
/**
* Updates the selected display state
*
* @param event
*/
private selectDisplay(event): void {
this.setState({ display: event.target.value });
}
/**
* Updated the selected notification position
*
* @param event
*/
private togglePosition(event): void {
this.setState({
position: event.currentTarget.value,
});
}
/**
* Sends the user selected notification settings options
*/
private submit(): void {
const { position, display } = this.state;
ipcRenderer.send('notification-settings-update', { position, display });
}
/**
* Closes the notification settings window
*/
private close(): void {
ipcRenderer.send(apiName.symphonyApi, {
cmd: apiCmds.closeWindow,
windowType: 'notification-settings',
});
}
/**
* Sets the About app state
*
* @param _event
* @param data {Object} { buildNumber, clientVersion, version }
*/
private updateState(_event, data): void {
this.setState(data as IState);
}
}

View File

@ -1,4 +1,5 @@
import * as asyncMap from 'async.map';
import { app } from 'electron';
import * as electron from 'electron';
import { windowExists } from '../app/window-utils';
@ -6,6 +7,7 @@ import { isMac } from '../common/env';
interface ISettings {
startCorner: startCorner;
displayId: string;
height: number;
width: number;
totalHeight: number;
@ -33,15 +35,16 @@ export default class NotificationHandler {
};
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);
app.once('ready', () => {
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);
});
}
/**
@ -62,14 +65,15 @@ export default class NotificationHandler {
*/
public setupNotificationPosition() {
// This feature only applies to windows
if (isMac) {
if (isMac || !app.isReady()) {
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;
return screenId === this.settings.displayId;
});
}

View File

@ -1,12 +1,13 @@
import { ipcMain } from 'electron';
import { config } from '../app/config-handler';
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 CLEAN_UP_INTERVAL = 60 * 1000; // Closes inactive notification
const animationQueue = new AnimationQueue();
interface ICustomBrowserWindow extends Electron.BrowserWindow {
@ -32,6 +33,7 @@ type startCorner = 'upper-right' | 'upper-left' | 'lower-right' | 'lower-left';
const notificationSettings = {
startCorner: 'upper-right' as startCorner,
display: '',
width: 380,
height: 100,
totalHeight: 0,
@ -74,6 +76,8 @@ class Notification extends NotificationHandler {
ipcMain.on('notification-clicked', (_event, windowId) => {
this.notificationClicked(windowId);
});
// Update latest notification settings from config
this.updateNotificationSettings();
this.cleanUpTimer = setInterval(this.funcHandlers.onCleanUpInactiveNotification, CLEAN_UP_INTERVAL);
}
@ -100,6 +104,7 @@ class Notification extends NotificationHandler {
*/
public async createNotificationWindow(data): Promise<ICustomBrowserWindow | undefined> {
// TODO: Handle MAX_QUEUE_SIZE
if (data.tag) {
for (let i = 0; i < this.notificationQueue.length; i++) {
if (this.notificationQueue[ i ].tag === data.tag) {
@ -244,6 +249,19 @@ class Notification extends NotificationHandler {
return this.activeNotifications[ index ] as ICustomBrowserWindow;
}
/**
* Update latest notification settings from config
*/
public updateNotificationSettings(): void {
const { display, position } = config.getConfigFields([ 'notificationSettings' ]).notificationSettings;
this.settings.displayId = display;
this.settings.startCorner = position as startCorner;
// recalculate notification position
this.setupNotificationPosition();
this.moveNotificationDown(0, this.activeNotifications);
}
/**
* Waits for window to load and resolves
*

View File

@ -8,6 +8,7 @@ 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 NotificationSettings from './components/notification-settings';
import ScreenPicker from './components/screen-picker';
import ScreenSharingIndicator from './components/screen-sharing-indicator';
@ -19,6 +20,7 @@ const enum components {
screenSharingIndicator = 'screen-sharing-indicator',
basicAuth = 'basic-auth',
notification = 'notification-comp',
notificationSettings = 'notification-settings',
}
const loadStyle = (style) => {
@ -66,6 +68,10 @@ const load = () => {
loadStyle(components.notification);
component = NotificationComp;
break;
case components.notificationSettings:
loadStyle(components.notificationSettings);
component = NotificationSettings;
break;
}
const element = React.createElement(component);
ReactDOM.render(element, document.getElementById('Root'));

View File

@ -1,6 +1,7 @@
import { ipcRenderer, remote } from 'electron';
import { buildNumber } from '../../package.json';
import { ICustomBrowserWindow } from '../app/window-handler';
import {
apiCmds,
apiName,
@ -261,6 +262,17 @@ export class SSFApi {
}
}
/**
* Opens a modal window to configure notification preference.
*/
public showNotificationSettings(): void {
const windowName = (remote.getCurrentWindow() as ICustomBrowserWindow).winName;
local.ipcRenderer.send(apiName.symphonyApi, {
cmd: apiCmds.showNotificationSettings,
windowName,
});
}
/**
* Shows a banner that informs user that the screen is being shared.
*

View File

@ -1,7 +1,8 @@
@import "theme";
@white: rgb(255, 255, 255, 1);
@version-text-color: rgb(47, 47, 47, 1);
@copyright-text-color: rgb(127, 127, 127, 1);
@font-family: sans-serif;
@text-padding: 10px;
body {

View File

@ -1,5 +1,6 @@
@import "theme";
@color-red: red;
@font-family: sans-serif;
html {
margin: 0;

View File

@ -1,4 +1,5 @@
@font-family: sans-serif;
@import "theme";
@text-padding: 10px;
body {

View File

@ -1,4 +1,4 @@
@font-family: "Segoe UI", "Helvetica Neue", "Verdana", "Arial", sans-serif;
@import "theme";
.light {
--text-color: #4a4a4a;

View File

@ -0,0 +1,114 @@
@import "theme";
@color: rgba(0, 0, 0, .8);
html {
margin: 0;
height: 100%;
font-family: @font-family;
}
body {
margin: 0;
height: 100%;
font-family: @font-family;
}
.content {
border-radius: 2px;
width: 100%;
flex: 0 0 auto;
}
.header {
display: flex;
align-items: center;
line-height: 1.3;
justify-content: space-between;
border-bottom: 1px solid rgba(0, 0, 0, .1);
margin-bottom: 20px;
padding: 16px;
}
.header__title {
font-weight: 500;
font-style: normal;
margin: 0 0 0 4px;
font-size: 1.4rem;
min-height: 13px;
text-overflow: ellipsis;
text-align: center;
white-space: nowrap;
overflow: hidden;
width: 100%;
color: @color;
}
.form {
width: 95%;
margin: 0 auto;
}
.selector {
padding: 0 9px 0 16px;
cursor: pointer;
margin-left: 10px;
}
.main {
display: flex;
flex-direction: row;
margin-bottom: 20px;
height: 100%;
border: 1px solid #ccc !important;
padding: 10px;
.first-set {
flex-grow: 1;
}
.second-set {
flex-grow: 1;
text-align: right;
}
}
.radio {
line-height: 1.7;
.upper-right, .lower-right {
float: right;
margin-top: 6px;
}
.upper-left, .lower-left {
float: left;
margin-top: 6px;
}
}
.radio__label {
cursor: pointer;
padding: 5px;
input {
cursor: pointer;
}
}
.footer {
padding: 12px 10px;
border-top: 1px solid rgba(0, 0, 0, .1);
display: flex;
align-items: center;
}
.buttonLayout {
margin-left: auto;
display: flex;
align-items: center;
}
.buttonDismiss {
margin-right: 10px;
}

View File

@ -0,0 +1 @@
@font-family: "Segoe UI", "Helvetica Neue", "Verdana", "Arial", sans-serif;

View File

@ -1,6 +1,7 @@
@import "theme";
@color_1: rgba(255, 255, 255, 1);
@color_2: white;
@font_family_1: "Segoe UI", "Helvetica Neue", Arial, sans-serif;
@background_color_1: rgba(51, 51, 51, 1);
.title-bar {
@ -50,7 +51,7 @@
}
.title-bar-title {
font-family: @font_family_1;
font-family: @font-family;
color: @color_2;
margin: 0;
padding-left: 10px;