mirror of
https://github.com/finos/SymphonyElectron.git
synced 2024-11-21 16:38:41 -06:00
SDA-4220 (Implement API for call notification) (#1891)
* SDA-4420 - Implement API for call notification Signed-off-by: Kiran Niranjan <kiran.niranjan@symphony.com> * SDA-4420 - Add unit tests Signed-off-by: Kiran Niranjan <kiran.niranjan@symphony.com> * SDA-4420 - Fix caller name style Signed-off-by: Kiran Niranjan <kiran.niranjan@symphony.com> * SDA-4420 - Change to logger Signed-off-by: Kiran Niranjan <kiran.niranjan@symphony.com> * SDA-4420 - update call toast position based on notification setting change Signed-off-by: Kiran Niranjan <kiran.niranjan@symphony.com> --------- Signed-off-by: Kiran Niranjan <kiran.niranjan@symphony.com>
This commit is contained in:
parent
d5f2ef1178
commit
dcbe7a0b74
@ -270,6 +270,22 @@ export const session = {
|
||||
},
|
||||
};
|
||||
|
||||
export const screen = {
|
||||
getAllDisplays: jest.fn(),
|
||||
getPrimaryDisplay: jest.fn(() => {
|
||||
return {
|
||||
workArea: {
|
||||
x: '',
|
||||
y: '',
|
||||
},
|
||||
workAreaSize: {
|
||||
width: '',
|
||||
height: '',
|
||||
},
|
||||
};
|
||||
}),
|
||||
};
|
||||
|
||||
export const remote = {
|
||||
app,
|
||||
getCurrentWindow,
|
||||
|
157
spec/callNotification.spec.ts
Normal file
157
spec/callNotification.spec.ts
Normal file
@ -0,0 +1,157 @@
|
||||
import { shallow } from 'enzyme';
|
||||
import * as React from 'react';
|
||||
import CallNotificationComp from '../src/renderer/components/call-notification';
|
||||
import { Themes } from '../src/renderer/components/notification-settings';
|
||||
import { ipcRenderer } from './__mocks__/electron';
|
||||
|
||||
const IPC_RENDERER_NOTIFICATION_DATA_CHANNEL = 'call-notification-data';
|
||||
describe('Call toast notification component', () => {
|
||||
const defaultProps = {
|
||||
title: 'Incoming call',
|
||||
};
|
||||
const spy = jest.spyOn(CallNotificationComp.prototype, 'setState');
|
||||
let wrapper;
|
||||
beforeEach(() => {
|
||||
wrapper = shallow(React.createElement(CallNotificationComp));
|
||||
});
|
||||
|
||||
it('should render correctly', () => {
|
||||
ipcRenderer.send(IPC_RENDERER_NOTIFICATION_DATA_CHANNEL, defaultProps);
|
||||
expect(spy).toBeCalledWith(defaultProps);
|
||||
const container = wrapper.find('.title');
|
||||
expect(container.text()).toBe(defaultProps.title);
|
||||
});
|
||||
|
||||
it('should close the call notification when the reject button is clicked', async () => {
|
||||
const spy = jest.spyOn(ipcRenderer, 'send');
|
||||
const rejectButton = wrapper.find(
|
||||
'[data-testid="CALL_NOTIFICATION_REJECT_BUTTON"]',
|
||||
);
|
||||
expect(rejectButton).toBeTruthy();
|
||||
rejectButton.simulate('click', { stopPropagation: jest.fn() });
|
||||
expect(spy).toBeCalledWith('call-notification-on-reject', 0);
|
||||
});
|
||||
|
||||
it('should close the call notification when the accept button is clicked', async () => {
|
||||
const spy = jest.spyOn(ipcRenderer, 'send');
|
||||
const rejectButton = wrapper.find(
|
||||
'[data-testid="CALL_NOTIFICATION_ACCEPT_BUTTON"]',
|
||||
);
|
||||
expect(rejectButton).toBeTruthy();
|
||||
rejectButton.simulate('click', { stopPropagation: jest.fn() });
|
||||
expect(spy).toBeCalledWith('call-notification-on-reject', 0);
|
||||
expect(spy).toBeCalledWith('call-notification-on-accept', 0);
|
||||
});
|
||||
|
||||
it('should click on the notification when the user clicks on main container', async () => {
|
||||
const spy = jest.spyOn(ipcRenderer, 'send');
|
||||
const notificationContainer = wrapper.find('.container');
|
||||
expect(notificationContainer).toBeTruthy();
|
||||
notificationContainer.simulate('click', { stopPropagation: jest.fn() });
|
||||
expect(spy).toBeCalledWith('call-notification-clicked', 0);
|
||||
});
|
||||
|
||||
it('should render Symphony logo with IM chat type if no image provided', () => {
|
||||
const icon = '';
|
||||
const profilePlaceHolderText = 'LS';
|
||||
const callType = 'IM';
|
||||
ipcRenderer.send(IPC_RENDERER_NOTIFICATION_DATA_CHANNEL, {
|
||||
...defaultProps,
|
||||
icon,
|
||||
profilePlaceHolderText,
|
||||
callType,
|
||||
});
|
||||
const defaultLogoContainer = wrapper.find('.thumbnail');
|
||||
expect(defaultLogoContainer).toBeTruthy();
|
||||
const imageContainer = wrapper.find('.profilePlaceHolderText');
|
||||
expect(imageContainer.exists()).toBeTruthy();
|
||||
const imClass = wrapper.find('.profilePlaceHolderContainer');
|
||||
expect(imClass.exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render correct button text', () => {
|
||||
const acceptButtonText = 'Answer';
|
||||
const rejectButtonText = 'Decline';
|
||||
ipcRenderer.send(IPC_RENDERER_NOTIFICATION_DATA_CHANNEL, {
|
||||
...defaultProps,
|
||||
acceptButtonText,
|
||||
rejectButtonText,
|
||||
});
|
||||
const acceptButton = wrapper.find(
|
||||
'[data-testid="CALL_NOTIFICATION_ACCEPT_BUTTON"]',
|
||||
);
|
||||
expect(acceptButton.text()).toBe('Answer');
|
||||
const rejectButton = wrapper.find(
|
||||
'[data-testid="CALL_NOTIFICATION_REJECT_BUTTON"]',
|
||||
);
|
||||
expect(rejectButton.text()).toBe('Decline');
|
||||
});
|
||||
|
||||
it('should render default primary text as unknown when empty', () => {
|
||||
const icon = '';
|
||||
const profilePlaceHolderText = 'LS';
|
||||
const callType = 'IM';
|
||||
ipcRenderer.send(IPC_RENDERER_NOTIFICATION_DATA_CHANNEL, {
|
||||
...defaultProps,
|
||||
icon,
|
||||
profilePlaceHolderText,
|
||||
callType,
|
||||
});
|
||||
const callerName = wrapper.find('.caller-name');
|
||||
expect(callerName.text()).toBe('unknown');
|
||||
});
|
||||
|
||||
it('should render Symphony logo with ROOM chat type if no image provided', () => {
|
||||
const icon = '';
|
||||
const profilePlaceHolderText = 'LS';
|
||||
const callType = 'ROOM';
|
||||
ipcRenderer.send(IPC_RENDERER_NOTIFICATION_DATA_CHANNEL, {
|
||||
...defaultProps,
|
||||
icon,
|
||||
profilePlaceHolderText,
|
||||
callType,
|
||||
});
|
||||
const defaultLogoContainer = wrapper.find('.thumbnail');
|
||||
expect(defaultLogoContainer).toBeTruthy();
|
||||
const imageContainer = wrapper.find('.profilePlaceHolderText');
|
||||
expect(imageContainer.exists()).toBeTruthy();
|
||||
const imClass = wrapper.find('.roomPlaceHolderContainer');
|
||||
expect(imClass.exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render Symphony logo if Symphony default image provided', () => {
|
||||
const icon = './default.png';
|
||||
ipcRenderer.send(IPC_RENDERER_NOTIFICATION_DATA_CHANNEL, {
|
||||
...defaultProps,
|
||||
icon,
|
||||
});
|
||||
const defaultLogoContainer = wrapper.find('.default-logo');
|
||||
expect(defaultLogoContainer).toBeTruthy();
|
||||
const imageContainer = wrapper.find('.profile-picture');
|
||||
expect(imageContainer.exists()).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should flash in a custom way when theme is set', () => {
|
||||
const flash = true;
|
||||
const theme = Themes.DARK;
|
||||
ipcRenderer.send(IPC_RENDERER_NOTIFICATION_DATA_CHANNEL, {
|
||||
...defaultProps,
|
||||
flash,
|
||||
theme,
|
||||
});
|
||||
const flashingNotification = wrapper.find(`.${theme}-flashing`);
|
||||
expect(flashingNotification.exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should display ext badge when external', () => {
|
||||
let externalBadge = wrapper.find('.ext-badge-container');
|
||||
expect(externalBadge.exists()).toBeFalsy();
|
||||
const isExternal = true;
|
||||
ipcRenderer.send(IPC_RENDERER_NOTIFICATION_DATA_CHANNEL, {
|
||||
...defaultProps,
|
||||
isExternal,
|
||||
});
|
||||
externalBadge = wrapper.find('.ext-badge-container');
|
||||
expect(externalBadge.exists()).toBeTruthy();
|
||||
});
|
||||
});
|
@ -1,4 +1,5 @@
|
||||
import { activityDetection } from '../src/app/activity-detection';
|
||||
import * as c9PipeHandler from '../src/app/c9-pipe-handler';
|
||||
import { downloadHandler } from '../src/app/download-handler';
|
||||
import '../src/app/main-api-handler';
|
||||
import { protocolHandler } from '../src/app/protocol-handler';
|
||||
@ -7,7 +8,6 @@ import * as windowActions from '../src/app/window-actions';
|
||||
import { windowHandler } from '../src/app/window-handler';
|
||||
import * as utils from '../src/app/window-utils';
|
||||
import { apiCmds, apiName } from '../src/common/api-interface';
|
||||
import * as c9PipeHandler from '../src/app/c9-pipe-handler';
|
||||
import { logger } from '../src/common/logger';
|
||||
import { BrowserWindow, ipcMain } from './__mocks__/electron';
|
||||
|
||||
|
@ -13,6 +13,7 @@ import {
|
||||
apiName,
|
||||
IApiArgs,
|
||||
IAuthResponse,
|
||||
ICallNotificationData,
|
||||
INotificationData,
|
||||
} from '../common/api-interface';
|
||||
import { i18n, LocaleType } from '../common/i18n';
|
||||
@ -50,6 +51,7 @@ import {
|
||||
} from './window-utils';
|
||||
|
||||
import { getCommandLineArgs } from '../common/utils';
|
||||
import callNotificationHelper from '../renderer/call-notification-helper';
|
||||
import { autoUpdate, AutoUpdateTrigger } from './auto-update-handler';
|
||||
import { presenceStatus } from './presence-status-handler';
|
||||
import { presenceStatusStore } from './stores/index';
|
||||
@ -338,6 +340,17 @@ ipcMain.on(
|
||||
await notificationHelper.closeNotification(arg.notificationId);
|
||||
}
|
||||
break;
|
||||
case apiCmds.showCallNotification:
|
||||
if (typeof arg.notificationOpts === 'object') {
|
||||
const opts = arg.notificationOpts as ICallNotificationData;
|
||||
callNotificationHelper.showNotification(opts);
|
||||
}
|
||||
break;
|
||||
case apiCmds.closeCallNotification:
|
||||
if (typeof arg.notificationId === 'number') {
|
||||
await callNotificationHelper.closeNotification(arg.notificationId);
|
||||
}
|
||||
break;
|
||||
/**
|
||||
* This gets called from mana, when user logs out
|
||||
*/
|
||||
|
247
src/app/notifications/call-notification.ts
Normal file
247
src/app/notifications/call-notification.ts
Normal file
@ -0,0 +1,247 @@
|
||||
import { ipcMain } from 'electron';
|
||||
import {
|
||||
apiName,
|
||||
ElectronNotificationData,
|
||||
ICallNotificationData,
|
||||
NotificationActions,
|
||||
} from '../../common/api-interface';
|
||||
import { isDevEnv, isMac } from '../../common/env';
|
||||
import { logger } from '../../common/logger';
|
||||
import { notification } from '../../renderer/notification';
|
||||
import {
|
||||
AUX_CLICK,
|
||||
ICustomBrowserWindow,
|
||||
IS_NODE_INTEGRATION_ENABLED,
|
||||
IS_SAND_BOXED,
|
||||
} from '../window-handler';
|
||||
import { createComponentWindow, windowExists } from '../window-utils';
|
||||
|
||||
const CALL_NOTIFICATION_WIDTH = 264;
|
||||
const CALL_NOTIFICATION_HEIGHT = 290;
|
||||
|
||||
class CallNotification {
|
||||
private callNotificationWindow: ICustomBrowserWindow | undefined;
|
||||
private notificationCallbacks: Map<
|
||||
number,
|
||||
(event: NotificationActions, data: ElectronNotificationData) => void
|
||||
> = new Map();
|
||||
|
||||
constructor() {
|
||||
ipcMain.on('call-notification-clicked', (_event, windowId) => {
|
||||
this.notificationClicked(windowId);
|
||||
});
|
||||
ipcMain.on('call-notification-on-accept', (_event, windowId) => {
|
||||
this.onCallNotificationOnAccept(windowId);
|
||||
});
|
||||
ipcMain.on('call-notification-on-reject', (_event, windowId) => {
|
||||
this.onCallNotificationOnReject(windowId);
|
||||
});
|
||||
ipcMain.on('notification-settings-update', async (_event) => {
|
||||
setTimeout(() => {
|
||||
const { x, y } = notification.getCallNotificationPosition();
|
||||
if (
|
||||
this.callNotificationWindow &&
|
||||
windowExists(this.callNotificationWindow)
|
||||
) {
|
||||
try {
|
||||
this.callNotificationWindow.setPosition(
|
||||
parseInt(String(x), 10),
|
||||
parseInt(String(y), 10),
|
||||
);
|
||||
} catch (err) {
|
||||
logger.info(
|
||||
`Failed to set window position. x: ${x} y: ${y}. Contact the developers for more details`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}, 500);
|
||||
});
|
||||
}
|
||||
|
||||
public createCallNotificationWindow = (
|
||||
callNotificationData: ICallNotificationData,
|
||||
callback,
|
||||
) => {
|
||||
if (
|
||||
this.callNotificationWindow &&
|
||||
windowExists(this.callNotificationWindow) &&
|
||||
this.callNotificationWindow.notificationData?.id
|
||||
) {
|
||||
this.callNotificationWindow.notificationData = callNotificationData;
|
||||
this.callNotificationWindow.winName = apiName.notificationWindowName;
|
||||
this.notificationCallbacks.set(callNotificationData.id, callback);
|
||||
this.callNotificationWindow.webContents.send(
|
||||
'call-notification-data',
|
||||
callNotificationData,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Set stream id as winKey to link stream to the window
|
||||
this.callNotificationWindow = createComponentWindow(
|
||||
'call-notification',
|
||||
this.getCallNotificationOpts(),
|
||||
false,
|
||||
) as ICustomBrowserWindow;
|
||||
|
||||
this.callNotificationWindow.notificationData = callNotificationData;
|
||||
this.callNotificationWindow.winName = apiName.notificationWindowName;
|
||||
this.notificationCallbacks.set(callNotificationData.id, callback);
|
||||
|
||||
this.callNotificationWindow.setVisibleOnAllWorkspaces(true);
|
||||
this.callNotificationWindow.setSkipTaskbar(true);
|
||||
this.callNotificationWindow.setAlwaysOnTop(true, 'screen-saver');
|
||||
const { x, y } = notification.getCallNotificationPosition();
|
||||
try {
|
||||
this.callNotificationWindow.setPosition(
|
||||
parseInt(String(x), 10),
|
||||
parseInt(String(y), 10),
|
||||
);
|
||||
} catch (err) {
|
||||
logger.info(
|
||||
`Failed to set window position. x: ${x} y: ${y}. Contact the developers for more details`,
|
||||
);
|
||||
}
|
||||
this.callNotificationWindow.webContents.once('did-finish-load', () => {
|
||||
if (
|
||||
!this.callNotificationWindow ||
|
||||
!windowExists(this.callNotificationWindow)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
this.callNotificationWindow.webContents.setZoomFactor(1);
|
||||
this.callNotificationWindow.webContents.setVisualZoomLevelLimits(1, 1);
|
||||
this.callNotificationWindow.webContents.send(
|
||||
'call-notification-data',
|
||||
callNotificationData,
|
||||
);
|
||||
this.callNotificationWindow.showInactive();
|
||||
});
|
||||
|
||||
this.callNotificationWindow.once('closed', () => {
|
||||
this.callNotificationWindow = undefined;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles call notification click
|
||||
*
|
||||
* @param clientId {number}
|
||||
*/
|
||||
public notificationClicked(clientId): void {
|
||||
const browserWindow = this.callNotificationWindow;
|
||||
if (
|
||||
browserWindow &&
|
||||
windowExists(browserWindow) &&
|
||||
browserWindow.notificationData
|
||||
) {
|
||||
const data = browserWindow.notificationData;
|
||||
const callback = this.notificationCallbacks.get(clientId);
|
||||
if (typeof callback === 'function') {
|
||||
callback(NotificationActions.notificationClicked, data);
|
||||
}
|
||||
this.closeNotification(clientId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles call notification success action which updates client
|
||||
* @param clientId {number}
|
||||
*/
|
||||
public onCallNotificationOnAccept(clientId: number): void {
|
||||
const browserWindow = this.callNotificationWindow;
|
||||
if (
|
||||
browserWindow &&
|
||||
windowExists(browserWindow) &&
|
||||
browserWindow.notificationData
|
||||
) {
|
||||
const data = browserWindow.notificationData;
|
||||
const callback = this.notificationCallbacks.get(clientId);
|
||||
if (typeof callback === 'function') {
|
||||
callback(NotificationActions.notificationAccept, data);
|
||||
}
|
||||
this.closeNotification(clientId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles call notification success action which updates client
|
||||
* @param clientId {number}
|
||||
*/
|
||||
public onCallNotificationOnReject(clientId: number): void {
|
||||
const browserWindow = this.callNotificationWindow;
|
||||
if (
|
||||
browserWindow &&
|
||||
windowExists(browserWindow) &&
|
||||
browserWindow.notificationData
|
||||
) {
|
||||
const data = browserWindow.notificationData;
|
||||
const callback = this.notificationCallbacks.get(clientId);
|
||||
if (typeof callback === 'function') {
|
||||
callback(NotificationActions.notificationReject, data);
|
||||
}
|
||||
this.closeNotification(clientId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the notification window
|
||||
*/
|
||||
public closeNotification(clientId: number): void {
|
||||
const browserWindow = this.callNotificationWindow;
|
||||
if (browserWindow && windowExists(browserWindow)) {
|
||||
if (
|
||||
browserWindow.notificationData &&
|
||||
browserWindow.notificationData.id !== clientId
|
||||
) {
|
||||
logger.info(
|
||||
'call-notification',
|
||||
`notification with the id ${browserWindow.notificationData.id} does match with clientID ${clientId}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
browserWindow.close();
|
||||
logger.info(
|
||||
'call-notification',
|
||||
'successfully closed call notification window',
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
private getCallNotificationOpts =
|
||||
(): Electron.BrowserWindowConstructorOptions => {
|
||||
const callNotificationOpts: Electron.BrowserWindowConstructorOptions = {
|
||||
width: CALL_NOTIFICATION_WIDTH,
|
||||
height: CALL_NOTIFICATION_HEIGHT,
|
||||
alwaysOnTop: true,
|
||||
skipTaskbar: true,
|
||||
resizable: false,
|
||||
show: false,
|
||||
frame: false,
|
||||
transparent: true,
|
||||
fullscreenable: false,
|
||||
acceptFirstMouse: true,
|
||||
modal: false,
|
||||
focusable: true,
|
||||
autoHideMenuBar: true,
|
||||
minimizable: false,
|
||||
maximizable: false,
|
||||
title: 'Call Notification - Symphony',
|
||||
webPreferences: {
|
||||
sandbox: IS_SAND_BOXED,
|
||||
nodeIntegration: IS_NODE_INTEGRATION_ENABLED,
|
||||
devTools: isDevEnv,
|
||||
disableBlinkFeatures: AUX_CLICK,
|
||||
},
|
||||
};
|
||||
if (isMac) {
|
||||
callNotificationOpts.type = 'panel';
|
||||
}
|
||||
return callNotificationOpts;
|
||||
};
|
||||
}
|
||||
|
||||
const callNotification = new CallNotification();
|
||||
|
||||
export { callNotification };
|
@ -19,7 +19,13 @@ import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { format, parse } from 'url';
|
||||
|
||||
import { apiName, Themes, WindowTypes } from '../common/api-interface';
|
||||
import {
|
||||
apiName,
|
||||
ICallNotificationData,
|
||||
INotificationData,
|
||||
Themes,
|
||||
WindowTypes,
|
||||
} from '../common/api-interface';
|
||||
import { isDevEnv, isLinux, isMac, isWindowsOS } from '../common/env';
|
||||
import { i18n, LocaleType } from '../common/i18n';
|
||||
import { ScreenShotAnnotation } from '../common/ipcEvent';
|
||||
@ -99,7 +105,7 @@ export interface ICustomBrowserWindowConstructorOpts
|
||||
|
||||
export interface ICustomBrowserWindow extends Electron.BrowserWindow {
|
||||
winName: string;
|
||||
notificationData?: object;
|
||||
notificationData?: INotificationData | ICallNotificationData;
|
||||
origin?: string;
|
||||
}
|
||||
|
||||
|
@ -33,6 +33,7 @@ export enum apiCmds {
|
||||
getMediaSource = 'get-media-source',
|
||||
notification = 'notification',
|
||||
closeNotification = 'close-notification',
|
||||
closeCallNotification = 'close-call-notification',
|
||||
memoryInfo = 'memory-info',
|
||||
swiftSearch = 'swift-search',
|
||||
getConfigUrl = 'get-config-url',
|
||||
@ -47,6 +48,7 @@ export enum apiCmds {
|
||||
restartApp = 'restart-app',
|
||||
setIsMana = 'set-is-mana',
|
||||
showNotification = 'show-notification',
|
||||
showCallNotification = 'show-call-notification',
|
||||
closeAllWrapperWindows = 'close-all-windows',
|
||||
setZoomLevel = 'set-zoom-level',
|
||||
aboutAppClipBoardData = 'about-app-clip-board-data',
|
||||
@ -241,6 +243,7 @@ export enum KeyCodes {
|
||||
}
|
||||
|
||||
type Theme = '' | 'light' | 'dark';
|
||||
type CallType = 'IM' | 'ROOM' | 'OTHER';
|
||||
|
||||
/**
|
||||
* Notification
|
||||
@ -267,11 +270,37 @@ export interface INotificationData {
|
||||
hasMention?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Notification
|
||||
*/
|
||||
export interface ICallNotificationData {
|
||||
id: number;
|
||||
title: string;
|
||||
image: string;
|
||||
icon?: string;
|
||||
color: string;
|
||||
company: string;
|
||||
isExternal: boolean;
|
||||
theme: Theme;
|
||||
primaryText: string;
|
||||
callback?: () => void;
|
||||
secondaryText?: string;
|
||||
companyIconUrl?: string;
|
||||
profilePlaceHolderText: string;
|
||||
actionIconUrl?: string;
|
||||
callType: CallType;
|
||||
shouldDisplayBadge: boolean;
|
||||
acceptButtonText: string;
|
||||
rejectButtonText: string;
|
||||
}
|
||||
|
||||
export enum NotificationActions {
|
||||
notificationClicked = 'notification-clicked',
|
||||
notificationClosed = 'notification-closed',
|
||||
notificationReply = 'notification-reply',
|
||||
notificationIgnore = 'notification-ignore',
|
||||
notificationAccept = 'notification-accept',
|
||||
notificationReject = 'notification-reject',
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,10 +1,71 @@
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Segoe UI', 'Helvetica Neue', 'Verdana', 'Arial',
|
||||
sans-serif;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 6px 6px;
|
||||
margin-bottom: 6px;
|
||||
background-color: #3f51b5;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.input-container {
|
||||
position: relative;
|
||||
padding: 6px 0;
|
||||
}
|
||||
|
||||
input[type='text'] {
|
||||
height: 20px;
|
||||
width: 280px;
|
||||
border: 1px solid #c0c0c0;
|
||||
border-radius: 4px;
|
||||
box-sizing: border-box;
|
||||
padding: 16px;
|
||||
color: grey;
|
||||
}
|
||||
|
||||
.label {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
input[type='text'],
|
||||
.label .text {
|
||||
font-family: 'Segoe UI';
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.label .text {
|
||||
transition: all 0.15s ease-out;
|
||||
transform: translate(0, -100%);
|
||||
color: #3f51b5;
|
||||
background-color: #f8f8f8;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
input[type='text']:focus {
|
||||
outline: none;
|
||||
border: 2px solid blue;
|
||||
color: black;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
}
|
||||
|
||||
table,
|
||||
th,
|
||||
td {
|
||||
@ -27,13 +88,15 @@
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.origin-reminder {
|
||||
background: lightyellow;
|
||||
padding: 20px;
|
||||
margin: 40px;
|
||||
padding: 10px;
|
||||
margin: 20px;
|
||||
border: 2px solid black;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.origin-reminder-tt {
|
||||
font-weight: normal;
|
||||
background: yellow;
|
||||
@ -61,148 +124,338 @@
|
||||
<u>Make sure to comment it out again before you commit.</u>
|
||||
</p>
|
||||
</div>
|
||||
<hr />
|
||||
<p>Notifications:</p>
|
||||
<p>
|
||||
<button id="notf">show notification</button>
|
||||
</p>
|
||||
<!--Toast Notification-->
|
||||
<div
|
||||
style="
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 16px;
|
||||
padding: 8px;
|
||||
"
|
||||
>
|
||||
<div
|
||||
style="
|
||||
background-color: #f8f8f8;
|
||||
padding: 16px;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 4px;
|
||||
"
|
||||
>
|
||||
<h3>Notifications:</h3>
|
||||
<div>
|
||||
<button id="notf">show notification</button>
|
||||
</div>
|
||||
<div>
|
||||
<div style="padding-top: 10px">
|
||||
<button id="closeNotification">close notification</button>
|
||||
<input
|
||||
type="number"
|
||||
id="notificationTag"
|
||||
value="Notification Tag to close"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p id="reply">Reply content will display here</p>
|
||||
<div class="input-container">
|
||||
<label class="label" for="title">
|
||||
<div class="text">Title</div>
|
||||
</label>
|
||||
<input type="text" id="title" value="Callam Davis" />
|
||||
</div>
|
||||
<div class="input-container">
|
||||
<label class="label" for="body">
|
||||
<div class="text">body</div>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="body"
|
||||
value="Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."
|
||||
/>
|
||||
</div>
|
||||
<div class="input-container">
|
||||
<label class="label" for="image">
|
||||
<div class="text">image url</div>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="image"
|
||||
value="https://i.pinimg.com/originals/ea/11/fb/ea11fb152820f63d536e8396c89f81e9.jpg"
|
||||
/>
|
||||
</div>
|
||||
<div class="input-container">
|
||||
<label class="label" for="company">
|
||||
<div class="text">company</div>
|
||||
</label>
|
||||
<input type="text" id="company" value="Symphony" />
|
||||
</div>
|
||||
<div class="input-container">
|
||||
<label class="label" for="color">
|
||||
<div class="text">Color</div>
|
||||
</label>
|
||||
<input type="text" id="color" value="" />
|
||||
</div>
|
||||
<div class="input-container">
|
||||
<label class="label" for="tag">
|
||||
<div class="text">Tag</div>
|
||||
</label>
|
||||
<input type="text" id="tag" value="" />
|
||||
</div>
|
||||
<div class="input-container">
|
||||
<label for="flash">Flash</label>
|
||||
<input type="checkbox" id="flash" />
|
||||
</div>
|
||||
<div class="input-container">
|
||||
<label for="isExternal">External</label>
|
||||
<input type="checkbox" id="isExternal" />
|
||||
</div>
|
||||
<div class="input-container">
|
||||
<label for="isUpdated">Is updated</label>
|
||||
<input type="checkbox" id="isUpdated" />
|
||||
</div>
|
||||
<div class="input-container">
|
||||
<label for="hasMention">Has mention</label>
|
||||
<input type="checkbox" id="hasMention" />
|
||||
</div>
|
||||
<div class="input-container">
|
||||
<label for="isNativeNotification"
|
||||
>Native Electron Notification:</label
|
||||
>
|
||||
<input type="checkbox" id="isNativeNotification" />
|
||||
</div>
|
||||
<div class="input-container">
|
||||
<label for="sticky">Sticky</label>
|
||||
<input type="checkbox" id="sticky" />
|
||||
</div>
|
||||
<div style="padding-bottom: 10px">
|
||||
<label for="select-theme">Change theme</label>
|
||||
<select id="select-theme" name="theme">
|
||||
<option value="">select</option>
|
||||
<option selected="selected" value="dark">dark</option>
|
||||
<option value="light">light</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p>
|
||||
<button id="closeNotification">close notification</button>
|
||||
<input
|
||||
type="number"
|
||||
id="notificationTag"
|
||||
value="Notification Tag to close"
|
||||
/>
|
||||
</p>
|
||||
<button id="open-config-win">Open configure window</button>
|
||||
</div>
|
||||
<!--Call Notification-->
|
||||
<div
|
||||
style="
|
||||
background-color: #f8f8f8;
|
||||
padding: 16px;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 4px;
|
||||
"
|
||||
>
|
||||
<h3>Call Notification Input</h3>
|
||||
<div style="padding-bottom: 10px">
|
||||
<button id="call-notf">show call notification</button>
|
||||
<button id="close-call-notf">close call notification</button>
|
||||
</div>
|
||||
<div class="input-container">
|
||||
<label class="label" for="profilePlaceHolderText">
|
||||
<div class="text">Profile PlaceHolder Text</div>
|
||||
</label>
|
||||
<input type="text" id="profilePlaceHolderText" value="+3" />
|
||||
</div>
|
||||
<div class="input-container">
|
||||
<label class="label" for="primaryText">
|
||||
<div class="text">Primary Text</div>
|
||||
</label>
|
||||
<input type="text" id="primaryText" value="Anna Galbraith" />
|
||||
</div>
|
||||
<div class="input-container">
|
||||
<label class="label" for="secondaryText">
|
||||
<div class="text">Secondary Text</div>
|
||||
</label>
|
||||
<input type="text" id="secondaryText" value="Broker" />
|
||||
</div>
|
||||
<div class="input-container">
|
||||
<label class="label" for="companyIconUrl">
|
||||
<div class="text">Tertiary Text Icon</div>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="companyIconUrl"
|
||||
value="https://creazilla-store.fra1.digitaloceanspaces.com/icons/3431275/whatsapp-icon-md.png"
|
||||
/>
|
||||
</div>
|
||||
<div class="input-container">
|
||||
<label class="label" for="acceptButtonText">
|
||||
<div class="text">Accept Button Text</div>
|
||||
</label>
|
||||
<input type="text" id="acceptButtonText" value="" />
|
||||
</div>
|
||||
<div class="input-container">
|
||||
<label class="label" for="rejectButtonText">
|
||||
<div class="text">Reject Button Text</div>
|
||||
</label>
|
||||
<input type="text" id="rejectButtonText" value="" />
|
||||
</div>
|
||||
<div class="input-container">
|
||||
<label class="label" for="actionIconUrl">
|
||||
<div class="text">Action IconUrl</div>
|
||||
</label>
|
||||
<input type="text" id="actionIconUrl" value="" />
|
||||
</div>
|
||||
<div class="input-container">
|
||||
<label for="select-call-type">Notification Type</label>
|
||||
<select id="select-call-type" name="call-type">
|
||||
<option value="">select</option>
|
||||
<option selected="selected" value="IM">IM</option>
|
||||
<option value="ROOM">ROOM</option>
|
||||
<option value="OTHER">OTHER</option>
|
||||
</select>
|
||||
<br />
|
||||
</div>
|
||||
<div class="input-container">
|
||||
<label for="shouldDisplayBadge">shouldDisplayBadge:</label>
|
||||
<input type="checkbox" id="shouldDisplayBadge" checked />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p id="reply">Reply content will display here</p>
|
||||
<p>
|
||||
<label for="title">title:</label>
|
||||
<input type="text" id="title" value="Callam Davis" />
|
||||
</p>
|
||||
<p>
|
||||
<label for="body">body:</label>
|
||||
<input
|
||||
type="text"
|
||||
id="body"
|
||||
value="Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."
|
||||
/>
|
||||
</p>
|
||||
<p>
|
||||
<label for="image">image url:</label>
|
||||
<input
|
||||
type="text"
|
||||
id="image"
|
||||
value="https://i.pinimg.com/originals/ea/11/fb/ea11fb152820f63d536e8396c89f81e9.jpg"
|
||||
/>
|
||||
</p>
|
||||
<p>
|
||||
<label for="company">company:</label>
|
||||
<input type="text" id="company" value="Symphony" />
|
||||
</p>
|
||||
<p>
|
||||
<label for="flash">flash:</label>
|
||||
<input type="checkbox" id="flash" />
|
||||
</p>
|
||||
<p>
|
||||
<label for="isExternal">external:</label>
|
||||
<input type="checkbox" id="isExternal" />
|
||||
</p>
|
||||
<p>
|
||||
<label for="isUpdated">is updated:</label>
|
||||
<input type="checkbox" id="isUpdated" />
|
||||
</p>
|
||||
<p>
|
||||
<label for="hasMention">has mention:</label>
|
||||
<input type="checkbox" id="hasMention" />
|
||||
</p>
|
||||
<p>
|
||||
<label for="isNativeNotification">Native Electron Notification:</label>
|
||||
<input type="checkbox" id="isNativeNotification" />
|
||||
</p>
|
||||
<p>Change theme:</p>
|
||||
<p>
|
||||
<select id="select-theme" name="theme">
|
||||
<option value="">select</option>
|
||||
<option selected="selected" value="dark">dark</option>
|
||||
<option value="light">light</option>
|
||||
</select>
|
||||
<br />
|
||||
</p>
|
||||
<div
|
||||
style="
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
padding: 8px;
|
||||
"
|
||||
>
|
||||
<div
|
||||
style="
|
||||
background-color: #f8f8f8;
|
||||
padding: 16px;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 4px;
|
||||
"
|
||||
>
|
||||
<textarea id="text-val" rows="4">
|
||||
Writes some thing to the file</textarea
|
||||
>
|
||||
<div style="padding: 10px 0px">
|
||||
<button id="download-file1" value="Download">Download</button>
|
||||
<button type="button" id="download-file2" value="Download">
|
||||
Download 2
|
||||
</button>
|
||||
</div>
|
||||
<div id="footer" style="padding: 10px 0px" class="hidden">
|
||||
<div id="download-manager-footer" class="download-bar">
|
||||
Downloaded File
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" id="open-download-item">Open Latest Item</button>
|
||||
<button
|
||||
type="button"
|
||||
id="show-download-item"
|
||||
value="Show Latest Item in Finder"
|
||||
>
|
||||
Show Latest Item in Finder
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
id="close-download-manager"
|
||||
value="Close Download Manager"
|
||||
>
|
||||
Close Download Manager
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
<label for="sticky">sticky:</label>
|
||||
<input type="checkbox" id="sticky" />
|
||||
</p>
|
||||
<p>
|
||||
<label for="color">color:</label>
|
||||
<input type="text" id="color" value="" />
|
||||
</p>
|
||||
<p>
|
||||
<label for="tag">tag:</label>
|
||||
<input type="text" id="tag" value="" />
|
||||
</p>
|
||||
<button id="open-config-win">Open configure window</button>
|
||||
<br />
|
||||
<hr />
|
||||
<textarea id="text-val" rows="4">Writes some thing to the file</textarea>
|
||||
<br />
|
||||
<input type="button" id="download-file1" value="Download" />
|
||||
<input type="button" id="download-file2" value="Download" />
|
||||
<div id="footer" class="hidden">
|
||||
<div id="download-manager-footer" class="download-bar"></div>
|
||||
<div
|
||||
style="
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
padding: 8px;
|
||||
"
|
||||
>
|
||||
<div
|
||||
style="
|
||||
background-color: #f8f8f8;
|
||||
padding: 16px;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 4px;
|
||||
"
|
||||
>
|
||||
<div>Activity Detection:</div>
|
||||
|
||||
<div style="padding: 10px">
|
||||
<button id="activity-detection">Init activity</button>
|
||||
<span id="activity-status" class="green-dot"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
style="
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
padding: 8px;
|
||||
"
|
||||
>
|
||||
<div
|
||||
style="
|
||||
background-color: #f8f8f8;
|
||||
padding: 16px;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 4px;
|
||||
"
|
||||
>
|
||||
<p>Change language:</p>
|
||||
<p>
|
||||
<select id="locale-select" name="locale">
|
||||
<option value="en-US">en-US</option>
|
||||
<option value="ja-JP">ja-JP</option>
|
||||
<option value="fr-FR">fr-FR</option>
|
||||
</select>
|
||||
<button id="changeLocale">Submit</button>
|
||||
<br />
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
style="
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
padding: 8px;
|
||||
"
|
||||
>
|
||||
<div
|
||||
style="
|
||||
background-color: #f8f8f8;
|
||||
padding: 16px;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 4px;
|
||||
"
|
||||
>
|
||||
<p>Badge Count:</p>
|
||||
<p>
|
||||
<button id="inc-badge">increment badge count</button>
|
||||
<br />
|
||||
<button id="clear-badge">clear badge count</button>
|
||||
<br />
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
style="
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
padding: 8px;
|
||||
"
|
||||
>
|
||||
<div
|
||||
style="
|
||||
background-color: #f8f8f8;
|
||||
padding: 16px;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 4px;
|
||||
"
|
||||
>
|
||||
<p>Screen Snippet:</p>
|
||||
<button id="snippet">get snippet</button>
|
||||
<p>snippet output:</p>
|
||||
<image id="snippet-img" />
|
||||
<button id="cancel-snippet">cancel snippet</button>
|
||||
</div>
|
||||
</div>
|
||||
<input type="button" id="open-download-item" value="Open Latest Item" />
|
||||
<input
|
||||
type="button"
|
||||
id="show-download-item"
|
||||
value="Show Latest Item in Finder"
|
||||
/>
|
||||
<input
|
||||
type="button"
|
||||
id="close-download-manager"
|
||||
value="Close Download Manager"
|
||||
/>
|
||||
<br />
|
||||
<hr />
|
||||
<p>Activity Detection:</p>
|
||||
|
||||
<p>
|
||||
<button id="activity-detection">Init activity</button>
|
||||
<span id="activity-status" class="green-dot"></span>
|
||||
</p>
|
||||
<br />
|
||||
<hr />
|
||||
<p>Change language:</p>
|
||||
<p>
|
||||
<select id="locale-select" name="locale">
|
||||
<option value="en-US">en-US</option>
|
||||
<option value="ja-JP">ja-JP</option>
|
||||
<option value="fr-FR">fr-FR</option>
|
||||
</select>
|
||||
<button id="changeLocale">Submit</button>
|
||||
<br />
|
||||
</p>
|
||||
|
||||
<hr />
|
||||
<p>Badge Count:</p>
|
||||
<p>
|
||||
<button id="inc-badge">increment badge count</button>
|
||||
<br />
|
||||
<button id="clear-badge">clear badge count</button>
|
||||
<br />
|
||||
</p>
|
||||
|
||||
<hr />
|
||||
<p>Screen Snippet:</p>
|
||||
<button id="snippet">get snippet</button>
|
||||
<p>snippet output:</p>
|
||||
<image id="snippet-img" />
|
||||
<button id="cancel-snippet">cancel snippet</button>
|
||||
|
||||
<hr />
|
||||
<p>Logs:</p>
|
||||
Filename <input type="text" id="log-filename" value="test_log.log" />
|
||||
<p>Contents</p>
|
||||
@ -293,6 +546,14 @@
|
||||
<br />
|
||||
</body>
|
||||
<script>
|
||||
const inputs = document.querySelectorAll('input');
|
||||
|
||||
inputs.forEach((input) => {
|
||||
input.addEventListener('input', () => {
|
||||
input.setAttribute('value', input.value);
|
||||
});
|
||||
});
|
||||
|
||||
window.name = 'main';
|
||||
|
||||
const apiCmds = {
|
||||
@ -440,6 +701,97 @@
|
||||
}
|
||||
});
|
||||
|
||||
var callNotifEl = document.getElementById('call-notf');
|
||||
var closeCallNotifEl = document.getElementById('close-call-notf');
|
||||
// note: notification will close when clicked
|
||||
callNotifEl.addEventListener('click', function () {
|
||||
var title = document.getElementById('title').value;
|
||||
var body = document.getElementById('body').value;
|
||||
var imageUrl = document.getElementById('image').value;
|
||||
var shouldFlash = document.getElementById('flash').checked;
|
||||
var isExternal = document.getElementById('isExternal').checked;
|
||||
var color = document.getElementById('color').value;
|
||||
var company = document.getElementById('company').value;
|
||||
num++;
|
||||
|
||||
var theme = document.getElementById('select-theme').value;
|
||||
var notf = {
|
||||
id: num,
|
||||
title,
|
||||
image: imageUrl,
|
||||
icon: imageUrl,
|
||||
color: color,
|
||||
flash: shouldFlash,
|
||||
isExternal: isExternal,
|
||||
theme: theme,
|
||||
data: {
|
||||
hello: `Notification with id ${num} clicked')`,
|
||||
},
|
||||
method: 'notification',
|
||||
primaryText: document.getElementById('primaryText').value,
|
||||
secondaryText: document.getElementById('secondaryText').value,
|
||||
company: company,
|
||||
companyIconUrl: document.getElementById('companyIconUrl').value,
|
||||
profilePlaceHolderText: document.getElementById(
|
||||
'profilePlaceHolderText',
|
||||
).value,
|
||||
actionIconUrl: document.getElementById('actionIconUrl').value,
|
||||
callType: document.getElementById('select-call-type').value,
|
||||
shouldDisplayBadge:
|
||||
document.getElementById('shouldDisplayBadge').checked,
|
||||
acceptButtonText: document.getElementById('acceptButtonText').value,
|
||||
rejectButtonText: document.getElementById('rejectButtonText').value,
|
||||
};
|
||||
if (window.ssf) {
|
||||
console.log(notf);
|
||||
const ssfNotificationHandler = new window.ssf.Notification(
|
||||
notf.title,
|
||||
notf,
|
||||
);
|
||||
const onclick = (event) => {
|
||||
event.target.close();
|
||||
if (!process.env.NODE_ENV) {
|
||||
alert('notification clicked: ' + event.target.data.hello);
|
||||
}
|
||||
};
|
||||
ssfNotificationHandler.addEventListener('click', onclick);
|
||||
|
||||
const onclose = () => {
|
||||
if (!process.env.NODE_ENV) {
|
||||
alert('notification closed');
|
||||
}
|
||||
};
|
||||
ssfNotificationHandler.addEventListener('close', onclose);
|
||||
|
||||
const onerror = (event) => {
|
||||
alert('error=' + event.result);
|
||||
};
|
||||
ssfNotificationHandler.addEventListener('error', onerror);
|
||||
} else if (window.manaSSF) {
|
||||
const callback = (event, data) => {
|
||||
if (event === 'notification-reply') {
|
||||
document.getElementById('reply').innerText = data.notificationData;
|
||||
}
|
||||
};
|
||||
window.manaSSF.showCallNotification(notf, callback);
|
||||
} else {
|
||||
window.postMessage({ method: 'notification', data: notf }, '*');
|
||||
}
|
||||
});
|
||||
|
||||
closeNotificationEl.addEventListener('click', () => {
|
||||
if (window.manaSSF) {
|
||||
const id = document.getElementById('notificationTag').value;
|
||||
window.manaSSF.closeNotification(parseInt(id));
|
||||
}
|
||||
});
|
||||
|
||||
closeCallNotifEl.addEventListener('click', () => {
|
||||
if (window.manaSSF) {
|
||||
window.manaSSF.closeCallNotification(num);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Badge Count
|
||||
*/
|
||||
|
5
src/renderer/assets/call-icon.svg
Normal file
5
src/renderer/assets/call-icon.svg
Normal file
@ -0,0 +1,5 @@
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8.14883 7.63617L8.16856 7.62531L8.18402 7.61734L9.53575 8.78152C9.38859 8.96221 9.2077 9.16592 8.98784 9.38577L8.81947 9.55415C8.50878 9.86484 8.23285 9.85183 8.09941 9.78895C7.29391 9.40937 6.06773 8.67546 4.69585 7.30359C3.32398 5.93171 2.59007 4.70553 2.2105 3.90003C2.14761 3.76659 2.13461 3.49066 2.44529 3.17997L2.61367 3.0116C2.83352 2.79174 3.03723 2.61085 3.21792 2.46369L4.3821 3.81542L4.37395 3.83124L4.36327 3.85061C4.35222 3.87071 4.32457 3.92103 4.29602 3.98249C4.24027 4.10251 4.1372 4.35431 4.1396 4.68546C4.14257 5.09581 4.29293 5.45671 4.47009 5.75134C4.65177 6.05347 4.91489 6.37431 5.27001 6.72943C5.62513 7.08455 5.94597 7.34767 6.2481 7.52935C6.54273 7.70652 6.90363 7.85687 7.31398 7.85985C7.64513 7.86225 7.89693 7.75917 8.01695 7.70342C8.0784 7.67487 8.12873 7.64722 8.14883 7.63617ZM0.855676 4.54435C1.30373 5.49517 2.13409 6.8648 3.63436 8.36508C5.13464 9.86535 6.50427 10.6957 7.45509 11.1438C8.29271 11.5385 9.23041 11.2543 9.87502 10.6097L10.0434 10.4413C10.6461 9.83866 11.0186 9.31934 11.2305 8.97723C11.4073 8.69192 11.3253 8.34165 11.0812 8.13142L8.78342 6.15245C8.55282 5.95385 8.22429 5.91599 7.95295 6.05596L7.48137 6.29923C7.46833 6.30596 7.45508 6.31328 7.44351 6.31968C7.38962 6.34945 7.36067 6.3631 7.33322 6.3629C7.28523 6.36255 7.18873 6.34386 7.02481 6.24529C6.86134 6.147 6.63584 5.97228 6.3315 5.66794C6.02716 5.3636 5.85244 5.1381 5.75415 4.97463C5.65558 4.81071 5.63689 4.71421 5.63654 4.66622C5.63634 4.63878 5.65023 4.60939 5.68002 4.55548C5.68641 4.54391 5.69349 4.5311 5.70021 4.51807L5.94348 4.04649C6.08345 3.77515 6.04559 3.44662 5.84699 3.21602L3.86802 0.918224C3.65779 0.674129 3.30752 0.592164 3.02221 0.768898C2.6801 0.980817 2.16078 1.35338 1.55812 1.95605L1.38974 2.12442C0.745131 2.76903 0.460968 3.70673 0.855676 4.54435Z" fill="white"/>
|
||||
<path d="M9.07532 4.04907C9.07532 4.67039 8.57164 5.17407 7.95032 5.17407C7.329 5.17407 6.82532 4.67039 6.82532 4.04907C6.82532 3.42775 7.329 2.92407 7.95032 2.92407C8.57164 2.92407 9.07532 3.42775 9.07532 4.04907Z" fill="white"/>
|
||||
<path d="M11.3253 1.79907C11.3253 2.42039 10.8216 2.92407 10.2003 2.92407C9.579 2.92407 9.07532 2.42039 9.07532 1.79907C9.07532 1.17775 9.579 0.674072 10.2003 0.674072C10.8216 0.674072 11.3253 1.17775 11.3253 1.79907Z" fill="white"/>
|
||||
</svg>
|
After Width: | Height: | Size: 2.3 KiB |
52
src/renderer/call-notification-helper.ts
Normal file
52
src/renderer/call-notification-helper.ts
Normal file
@ -0,0 +1,52 @@
|
||||
import { callNotification } from '../app/notifications/call-notification';
|
||||
import { windowHandler } from '../app/window-handler';
|
||||
import {
|
||||
ElectronNotificationData,
|
||||
ICallNotificationData,
|
||||
NotificationActions,
|
||||
} from '../common/api-interface';
|
||||
|
||||
class CallNotificationHelper {
|
||||
/**
|
||||
* Displays HTML call notification
|
||||
*
|
||||
* @param options {ICallNotificationData}
|
||||
*/
|
||||
public showNotification(options: ICallNotificationData) {
|
||||
callNotification.createCallNotificationWindow(
|
||||
options,
|
||||
this.notificationCallback,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes a specific notification by id
|
||||
*
|
||||
* @param id {number} - unique id assigned to a specific notification
|
||||
*/
|
||||
public async closeNotification(id: number) {
|
||||
await callNotification.closeNotification(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the notification actions event to the web client
|
||||
*
|
||||
* @param event {NotificationActions}
|
||||
* @param data {ElectronNotificationData}
|
||||
*/
|
||||
public notificationCallback(
|
||||
event: NotificationActions,
|
||||
data: ElectronNotificationData,
|
||||
) {
|
||||
const mainWebContents = windowHandler.getMainWebContents();
|
||||
if (mainWebContents && !mainWebContents.isDestroyed()) {
|
||||
mainWebContents.send('call-notification-actions', {
|
||||
event,
|
||||
data,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const callNotificationHelper = new CallNotificationHelper();
|
||||
export default callNotificationHelper;
|
377
src/renderer/components/call-notification.tsx
Normal file
377
src/renderer/components/call-notification.tsx
Normal file
@ -0,0 +1,377 @@
|
||||
import classNames from 'classnames';
|
||||
import { ipcRenderer } from 'electron';
|
||||
import * as React from 'react';
|
||||
import {
|
||||
darkTheme,
|
||||
getContainerCssClasses,
|
||||
getThemeColors,
|
||||
isValidColor,
|
||||
Theme,
|
||||
whiteColorRegExp,
|
||||
} from '../notification-theme';
|
||||
import { Themes } from './notification-settings';
|
||||
|
||||
type CallType = 'IM' | 'ROOM' | 'OTHER';
|
||||
|
||||
interface ICallNotificationState {
|
||||
title: string;
|
||||
primaryText: string;
|
||||
secondaryText?: string;
|
||||
company: string;
|
||||
companyIconUrl?: string;
|
||||
profilePlaceHolderText: string;
|
||||
actionIconUrl?: string;
|
||||
callType: CallType;
|
||||
shouldDisplayBadge: boolean;
|
||||
acceptButtonText?: string;
|
||||
rejectButtonText?: string;
|
||||
image: string;
|
||||
icon: string | undefined;
|
||||
id: number;
|
||||
color: string;
|
||||
flash: boolean;
|
||||
isExternal: boolean;
|
||||
theme: Theme;
|
||||
}
|
||||
|
||||
type mouseEventButton =
|
||||
| React.MouseEvent<HTMLDivElement>
|
||||
| React.MouseEvent<HTMLButtonElement>;
|
||||
|
||||
export default class CallNotification extends React.Component<
|
||||
{},
|
||||
ICallNotificationState
|
||||
> {
|
||||
private readonly eventHandlers = {
|
||||
onClick: (data) => (_event: mouseEventButton) => this.click(data),
|
||||
onAccept: (data) => (event: mouseEventButton) => this.accept(event, data),
|
||||
onReject: (data) => (event: mouseEventButton) => this.reject(event, data),
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
title: 'Incoming call',
|
||||
primaryText: 'unknown',
|
||||
secondaryText: '',
|
||||
company: 'Symphony',
|
||||
companyIconUrl: '',
|
||||
profilePlaceHolderText: 'S',
|
||||
callType: 'IM',
|
||||
shouldDisplayBadge: true,
|
||||
image: '',
|
||||
icon: '',
|
||||
id: 0,
|
||||
color: '',
|
||||
flash: false,
|
||||
isExternal: false,
|
||||
theme: '',
|
||||
};
|
||||
this.updateState = this.updateState.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback to handle event when a component is mounted
|
||||
*/
|
||||
public componentDidMount(): void {
|
||||
ipcRenderer.on('call-notification-data', this.updateState);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback to handle event when a component is unmounted
|
||||
*/
|
||||
public componentWillUnmount(): void {
|
||||
ipcRenderer.removeListener('call-notification-data', this.updateState);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the component
|
||||
*/
|
||||
public render(): JSX.Element {
|
||||
const {
|
||||
id,
|
||||
title,
|
||||
primaryText,
|
||||
secondaryText,
|
||||
company,
|
||||
companyIconUrl,
|
||||
color,
|
||||
actionIconUrl,
|
||||
profilePlaceHolderText,
|
||||
callType,
|
||||
acceptButtonText,
|
||||
rejectButtonText,
|
||||
shouldDisplayBadge,
|
||||
isExternal,
|
||||
theme,
|
||||
flash,
|
||||
icon,
|
||||
} = this.state;
|
||||
|
||||
let themeClassName;
|
||||
if (theme) {
|
||||
themeClassName = theme;
|
||||
} else if (darkTheme.includes(color.toLowerCase())) {
|
||||
themeClassName = 'black-text';
|
||||
} else {
|
||||
themeClassName =
|
||||
color && color.match(whiteColorRegExp) ? Themes.LIGHT : Themes.DARK;
|
||||
}
|
||||
const themeColors = getThemeColors(theme, flash, isExternal, false, color);
|
||||
const customCssClasses = getContainerCssClasses(
|
||||
theme,
|
||||
flash,
|
||||
isExternal,
|
||||
false,
|
||||
);
|
||||
let containerCssClass = `container ${themeClassName} `;
|
||||
containerCssClass += customCssClasses.join(' ');
|
||||
|
||||
const acceptText = acceptButtonText
|
||||
? acceptButtonText
|
||||
: callType === 'IM' || callType === 'ROOM'
|
||||
? 'join'
|
||||
: 'answer';
|
||||
const rejectText = rejectButtonText
|
||||
? rejectButtonText
|
||||
: callType === 'IM' || callType === 'ROOM'
|
||||
? 'ignore'
|
||||
: 'decline';
|
||||
|
||||
return (
|
||||
<div
|
||||
data-testid='CALL_NOTIFICATION_CONTAINER'
|
||||
className={containerCssClass}
|
||||
style={{
|
||||
backgroundColor: themeColors.notificationBackgroundColor,
|
||||
borderColor: themeColors.notificationBorderColor,
|
||||
}}
|
||||
onClick={this.eventHandlers.onClick(id)}
|
||||
>
|
||||
<div className={`title ${themeClassName}`}>{title}</div>
|
||||
<div className='caller-info-container'>
|
||||
<div className='logo-container'>
|
||||
{this.renderImage(
|
||||
icon,
|
||||
profilePlaceHolderText,
|
||||
callType,
|
||||
shouldDisplayBadge,
|
||||
)}
|
||||
</div>
|
||||
<div className='info-text-container'>
|
||||
<div className='primary-text-container'>
|
||||
<div className='caller-name-container'>
|
||||
<div
|
||||
data-testid='CALL_NOTIFICATION_NAME'
|
||||
className={`caller-name ${themeClassName}`}
|
||||
>
|
||||
{primaryText}
|
||||
</div>
|
||||
{this.renderExtBadge(isExternal)}
|
||||
</div>
|
||||
</div>
|
||||
{secondaryText ? (
|
||||
<div className='secondary-text-container'>
|
||||
<div className='caller-details'>
|
||||
<div className={`caller-role ${themeClassName}`}>
|
||||
{secondaryText}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
{company || companyIconUrl ? (
|
||||
<div className='tertiary-text-container'>
|
||||
<div className='application-details'>
|
||||
{company && companyIconUrl && (
|
||||
<img
|
||||
className={'company-icon'}
|
||||
src={companyIconUrl}
|
||||
alt={'company logo'}
|
||||
/>
|
||||
)}
|
||||
<div className={`company-name ${themeClassName}`}>
|
||||
{company}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className='actions'>
|
||||
<button
|
||||
className={classNames('decline', {
|
||||
'call-type-other': callType === 'OTHER',
|
||||
})}
|
||||
>
|
||||
<div
|
||||
data-testid='CALL_NOTIFICATION_REJECT_BUTTON'
|
||||
className='label'
|
||||
onClick={this.eventHandlers.onReject(id)}
|
||||
>
|
||||
{rejectText}
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
className={classNames('accept', {
|
||||
'call-type-other': callType === 'OTHER',
|
||||
})}
|
||||
>
|
||||
{actionIconUrl ? (
|
||||
<img
|
||||
onError={(event) => {
|
||||
(event.target as any).src =
|
||||
'../renderer/assets/call-icon.svg';
|
||||
}}
|
||||
className={'action-icon'}
|
||||
src={actionIconUrl}
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
src='../renderer/assets/call-icon.svg'
|
||||
alt='join call icon'
|
||||
className='profile-picture-badge'
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
data-testid='CALL_NOTIFICATION_ACCEPT_BUTTON'
|
||||
className='label'
|
||||
onClick={this.eventHandlers.onAccept(id)}
|
||||
>
|
||||
{acceptText}
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private click = (id: number) => {
|
||||
ipcRenderer.send('call-notification-clicked', id);
|
||||
};
|
||||
|
||||
private accept = (event, id: number) => {
|
||||
event.stopPropagation();
|
||||
ipcRenderer.send('call-notification-on-accept', id);
|
||||
};
|
||||
|
||||
private reject = (event, id: number) => {
|
||||
event.stopPropagation();
|
||||
ipcRenderer.send('call-notification-on-reject', id);
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets the component state
|
||||
*
|
||||
* @param _event
|
||||
* @param data {Object}
|
||||
*/
|
||||
private updateState(_event, data): void {
|
||||
const { color } = data;
|
||||
// FYI: 1.5 sends hex color but without '#', reason why we check and add prefix if necessary.
|
||||
// Goal is to keep backward compatibility with 1.5 colors (SDA v. 9.2.0)
|
||||
const isOldColor = /^([A-Fa-f0-9]{6})/.test(color);
|
||||
data.color = isOldColor ? `#${color}` : isValidColor(color) ? color : '';
|
||||
data.isInputHidden = true;
|
||||
// FYI: 1.5 doesn't send current theme. We need to deduce it from the color that is sent.
|
||||
// Goal is to keep backward compatibility with 1.5 themes (SDA v. 9.2.0)
|
||||
data.theme =
|
||||
isOldColor && darkTheme.includes(data.color)
|
||||
? Themes.DARK
|
||||
: data.theme
|
||||
? data.theme
|
||||
: Themes.LIGHT;
|
||||
this.setState(data as ICallNotificationState);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders image if provided otherwise renders symphony logo
|
||||
* @param imageUrl
|
||||
* @param profilePlaceHolderText
|
||||
* @param callType
|
||||
* @param shouldDisplayBadge
|
||||
*/
|
||||
private renderImage(
|
||||
imageUrl: string | undefined,
|
||||
profilePlaceHolderText: string,
|
||||
callType: CallType,
|
||||
shouldDisplayBadge: boolean,
|
||||
): JSX.Element | undefined {
|
||||
let imgClass = 'default-logo';
|
||||
let url = '../renderer/assets/notification-symphony-logo.svg';
|
||||
let alt = 'Symphony logo';
|
||||
const isDefaultUrl = imageUrl && imageUrl.includes('default.png');
|
||||
|
||||
if (imageUrl && !isDefaultUrl) {
|
||||
imgClass = 'profile-picture';
|
||||
url = imageUrl;
|
||||
alt = 'Profile picture';
|
||||
}
|
||||
|
||||
if (!imageUrl) {
|
||||
const profilePlaceHolderClassName =
|
||||
callType === 'IM'
|
||||
? 'profilePlaceHolderContainer'
|
||||
: 'roomPlaceHolderContainer';
|
||||
return (
|
||||
<div className='logo'>
|
||||
<div className={`thumbnail ${profilePlaceHolderClassName}`}>
|
||||
<p className={'profilePlaceHolderText'}>{profilePlaceHolderText}</p>
|
||||
</div>
|
||||
{this.renderSymphonyBadge(shouldDisplayBadge, callType)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='logo'>
|
||||
<img className={imgClass} src={url} alt={alt} />
|
||||
{this.renderSymphonyBadge(shouldDisplayBadge)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders profile picture Symphony badge
|
||||
* @param hasImageUrl
|
||||
* @param callType
|
||||
*/
|
||||
private renderSymphonyBadge(
|
||||
hasImageUrl: boolean,
|
||||
callType: CallType = 'IM',
|
||||
): JSX.Element | undefined {
|
||||
const badgePositionClass =
|
||||
callType === 'IM' ? 'badge-position-im' : 'badge-position-room';
|
||||
if (hasImageUrl) {
|
||||
return (
|
||||
<img
|
||||
src='../renderer/assets/symphony-badge.svg'
|
||||
alt=''
|
||||
className={`profile-picture-badge ${badgePositionClass}`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders external badge if the content is from external
|
||||
* @param isExternal
|
||||
*/
|
||||
private renderExtBadge(isExternal: boolean): JSX.Element | undefined {
|
||||
if (!isExternal) {
|
||||
return;
|
||||
}
|
||||
return (
|
||||
<div className='ext-badge-container'>
|
||||
<img
|
||||
src='../renderer/assets/notification-ext-badge.svg'
|
||||
alt='ext-badge'
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
@ -3,39 +3,16 @@ import { ipcRenderer } from 'electron';
|
||||
import * as React from 'react';
|
||||
|
||||
import { i18n } from '../../common/i18n-preload';
|
||||
import {
|
||||
darkTheme,
|
||||
getContainerCssClasses,
|
||||
getThemeColors,
|
||||
isValidColor,
|
||||
Theme,
|
||||
whiteColorRegExp,
|
||||
} from '../notification-theme';
|
||||
import { Themes } from './notification-settings';
|
||||
|
||||
const whiteColorRegExp = new RegExp(
|
||||
/^(?:white|#fff(?:fff)?|rgba?\(\s*255\s*,\s*255\s*,\s*255\s*(?:,\s*1\s*)?\))$/i,
|
||||
);
|
||||
const darkTheme = [
|
||||
'#e23030',
|
||||
'#b5616a',
|
||||
'#ab8ead',
|
||||
'#ebc875',
|
||||
'#a3be77',
|
||||
'#58c6ff',
|
||||
'#ebab58',
|
||||
];
|
||||
|
||||
const Colors = {
|
||||
dark: {
|
||||
regularFlashingNotificationBgColor: '#27588e',
|
||||
notificationBackgroundColor: '#27292c',
|
||||
notificationBorderColor: '#717681',
|
||||
mentionBackgroundColor: '#99342c',
|
||||
mentionBorderColor: '#ff5d50',
|
||||
},
|
||||
light: {
|
||||
regularFlashingNotificationBgColor: '#aad4f8',
|
||||
notificationBackgroundColor: '#f1f1f3',
|
||||
notificationBorderColor: 'transparent',
|
||||
mentionBackgroundColor: '#fcc1b9',
|
||||
mentionBorderColor: 'transparent',
|
||||
},
|
||||
};
|
||||
|
||||
type Theme = '' | Themes.DARK | Themes.LIGHT;
|
||||
interface INotificationState {
|
||||
title: string;
|
||||
company: string;
|
||||
@ -64,8 +41,6 @@ type keyboardEvent = React.KeyboardEvent<HTMLInputElement>;
|
||||
// Notification container height
|
||||
const CONTAINER_HEIGHT = 100;
|
||||
const CONTAINER_HEIGHT_WITH_INPUT = 142;
|
||||
const LIGHT_THEME = '#EAEBEC';
|
||||
const DARK_THEME = '#25272B';
|
||||
|
||||
export default class NotificationComp extends React.Component<
|
||||
{},
|
||||
@ -144,7 +119,9 @@ export default class NotificationComp extends React.Component<
|
||||
isExternal,
|
||||
isUpdated,
|
||||
theme,
|
||||
hasMention,
|
||||
containerHeight,
|
||||
flash,
|
||||
icon,
|
||||
} = this.state;
|
||||
let themeClassName;
|
||||
@ -156,10 +133,21 @@ export default class NotificationComp extends React.Component<
|
||||
themeClassName =
|
||||
color && color.match(whiteColorRegExp) ? Themes.LIGHT : Themes.DARK;
|
||||
}
|
||||
const themeColors = this.getThemeColors();
|
||||
const themeColors = getThemeColors(
|
||||
theme,
|
||||
flash,
|
||||
isExternal,
|
||||
hasMention,
|
||||
color,
|
||||
);
|
||||
const closeImgFilePath = `../renderer/assets/close-icon-${themeClassName}.svg`;
|
||||
let containerCssClass = `container ${themeClassName} `;
|
||||
const customCssClasses = this.getContainerCssClasses();
|
||||
const customCssClasses = getContainerCssClasses(
|
||||
theme,
|
||||
flash,
|
||||
isExternal,
|
||||
hasMention,
|
||||
);
|
||||
containerCssClass += customCssClasses.join(' ');
|
||||
return (
|
||||
<div
|
||||
@ -470,11 +458,7 @@ export default class NotificationComp extends React.Component<
|
||||
// FYI: 1.5 sends hex color but without '#', reason why we check and add prefix if necessary.
|
||||
// Goal is to keep backward compatibility with 1.5 colors (SDA v. 9.2.0)
|
||||
const isOldColor = /^([A-Fa-f0-9]{6})/.test(color);
|
||||
data.color = isOldColor
|
||||
? `#${color}`
|
||||
: this.isValidColor(color)
|
||||
? color
|
||||
: '';
|
||||
data.color = isOldColor ? `#${color}` : isValidColor(color) ? color : '';
|
||||
data.isInputHidden = true;
|
||||
data.containerHeight = CONTAINER_HEIGHT;
|
||||
// FYI: 1.5 doesn't send current theme. We need to deduce it from the color that is sent.
|
||||
@ -489,15 +473,6 @@ export default class NotificationComp extends React.Component<
|
||||
this.setState(data as INotificationState);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the color
|
||||
* @param color
|
||||
* @private
|
||||
*/
|
||||
private isValidColor(color: string): boolean {
|
||||
return /^#([A-Fa-f0-9]{6})/.test(color);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset data for new notification
|
||||
* @private
|
||||
@ -508,65 +483,6 @@ export default class NotificationComp extends React.Component<
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns notification colors based on theme
|
||||
* @param theme Current theme, can be either light or dark
|
||||
*/
|
||||
private getThemeColors(): { [key: string]: string } {
|
||||
const { theme, flash, isExternal, hasMention, color } = this.state;
|
||||
const currentColors =
|
||||
theme === Themes.DARK ? { ...Colors.dark } : { ...Colors.light };
|
||||
const externalFlashingBackgroundColor =
|
||||
theme === Themes.DARK ? '#70511f' : '#f6e5a6';
|
||||
if (flash && theme) {
|
||||
if (isExternal) {
|
||||
if (!hasMention) {
|
||||
currentColors.notificationBorderColor = '#F7CA3B';
|
||||
currentColors.notificationBackgroundColor =
|
||||
externalFlashingBackgroundColor;
|
||||
if (this.isCustomColor(color)) {
|
||||
currentColors.notificationBorderColor =
|
||||
this.getThemedCustomBorderColor(theme, color);
|
||||
currentColors.notificationBackgroundColor = color;
|
||||
}
|
||||
} else {
|
||||
currentColors.notificationBorderColor = '#F7CA3B';
|
||||
}
|
||||
} else if (hasMention) {
|
||||
currentColors.notificationBorderColor =
|
||||
currentColors.notificationBorderColor;
|
||||
} else {
|
||||
// in case of regular message without mention
|
||||
// FYI: SDA versions prior to 9.2.3 do not support theme color properly, reason why SFE-lite is pushing notification default background color.
|
||||
// For this reason, to be backward compatible, we check if sent color correspond to 'default' background color. If yes, we should ignore it and not consider it as a custom color.
|
||||
currentColors.notificationBackgroundColor = this.isCustomColor(color)
|
||||
? color
|
||||
: currentColors.regularFlashingNotificationBgColor;
|
||||
currentColors.notificationBorderColor = this.isCustomColor(color)
|
||||
? this.getThemedCustomBorderColor(theme, color)
|
||||
: theme === Themes.DARK
|
||||
? '#2996fd'
|
||||
: 'transparent';
|
||||
}
|
||||
} else if (!flash) {
|
||||
if (hasMention) {
|
||||
currentColors.notificationBackgroundColor =
|
||||
currentColors.mentionBackgroundColor;
|
||||
currentColors.notificationBorderColor =
|
||||
currentColors.mentionBorderColor;
|
||||
} else if (this.isCustomColor(color)) {
|
||||
currentColors.notificationBackgroundColor = color;
|
||||
currentColors.notificationBorderColor = this.getThemedCustomBorderColor(
|
||||
theme,
|
||||
color,
|
||||
);
|
||||
} else if (isExternal) {
|
||||
currentColors.notificationBorderColor = '#F7CA3B';
|
||||
}
|
||||
}
|
||||
return currentColors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders reply button
|
||||
* @param id
|
||||
@ -613,83 +529,4 @@ export default class NotificationComp extends React.Component<
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function aims at providing toast notification css classes
|
||||
*/
|
||||
private getContainerCssClasses(): string[] {
|
||||
const customClasses: string[] = [];
|
||||
const { flash, theme, hasMention, isExternal } = this.state;
|
||||
if (flash && theme) {
|
||||
if (isExternal) {
|
||||
customClasses.push('external-border');
|
||||
if (hasMention) {
|
||||
customClasses.push(`${theme}-ext-mention-flashing`);
|
||||
} else {
|
||||
customClasses.push(`${theme}-ext-flashing`);
|
||||
}
|
||||
} else if (hasMention) {
|
||||
customClasses.push(`${theme}-mention-flashing`);
|
||||
} else {
|
||||
// In case it's a regular message notification
|
||||
customClasses.push(`${theme}-flashing`);
|
||||
}
|
||||
} else if (isExternal) {
|
||||
customClasses.push('external-border');
|
||||
}
|
||||
return customClasses;
|
||||
}
|
||||
|
||||
/**
|
||||
* SDA versions prior to 9.2.3 do not support theme color properly, reason why SFE-lite is pushing notification default background color and theme.
|
||||
* For that reason, we try to identify if provided color is the default one or not.
|
||||
* @param color color sent through SDABridge
|
||||
* @returns boolean
|
||||
*/
|
||||
private isCustomColor(color: string): boolean {
|
||||
if (color && color !== LIGHT_THEME && color !== DARK_THEME) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Function that allows to increase color brightness
|
||||
* @param hex hes color
|
||||
* @param percent percent
|
||||
* @returns new hex color
|
||||
*/
|
||||
private increaseBrightness(hex: string, percent: number) {
|
||||
// strip the leading # if it's there
|
||||
hex = hex.replace(/^\s*#|\s*$/g, '');
|
||||
const r = parseInt(hex.substr(0, 2), 16);
|
||||
const g = parseInt(hex.substr(2, 2), 16);
|
||||
const b = parseInt(hex.substr(4, 2), 16);
|
||||
|
||||
return (
|
||||
'#' +
|
||||
// tslint:disable-next-line: no-bitwise
|
||||
(0 | ((1 << 8) + r + ((256 - r) * percent) / 100))
|
||||
.toString(16)
|
||||
.substr(1) +
|
||||
// tslint:disable-next-line: no-bitwise
|
||||
(0 | ((1 << 8) + g + ((256 - g) * percent) / 100))
|
||||
.toString(16)
|
||||
.substr(1) +
|
||||
// tslint:disable-next-line: no-bitwise
|
||||
(0 | ((1 << 8) + b + ((256 - b) * percent) / 100)).toString(16).substr(1)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns custom border color
|
||||
* @param theme current theme
|
||||
* @param customColor color
|
||||
* @returns custom border color
|
||||
*/
|
||||
private getThemedCustomBorderColor(theme: string, customColor: string) {
|
||||
return theme === Themes.DARK
|
||||
? this.increaseBrightness(customColor, 50)
|
||||
: 'transparent';
|
||||
}
|
||||
}
|
||||
|
@ -20,7 +20,7 @@ interface ISettings {
|
||||
differentialHeight: number;
|
||||
}
|
||||
|
||||
interface ICorner {
|
||||
export interface ICorner {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
@ -29,9 +29,11 @@ type startCorner = 'upper-right' | 'upper-left' | 'lower-right' | 'lower-left';
|
||||
const NEXT_INSERT_POSITION = 100;
|
||||
const NEXT_INSERT_POSITION_WITH_INPUT = 142;
|
||||
const NOTIFICATIONS_PADDING_SEPARATION = 12;
|
||||
|
||||
const CALL_NOTIFICATION_WIDTH = 264;
|
||||
const CALL_NOTIFICATION_HEIGHT = 286;
|
||||
export default class NotificationHandler {
|
||||
public settings: ISettings;
|
||||
public callNotificationSettings: ICorner = { x: 0, y: 0 };
|
||||
public nextInsertPos: ICorner = { x: 0, y: 0 };
|
||||
|
||||
private readonly eventHandlers = {
|
||||
@ -98,6 +100,8 @@ export default class NotificationHandler {
|
||||
const display = this.externalDisplay || screen.getPrimaryDisplay();
|
||||
this.settings.corner.x = display.workArea.x;
|
||||
this.settings.corner.y = display.workArea.y;
|
||||
this.callNotificationSettings.x = display.workArea.x;
|
||||
this.callNotificationSettings.y = display.workArea.y;
|
||||
|
||||
// update corner x/y based on corner of screen where notification should appear
|
||||
const workAreaWidth = display.workAreaSize.width;
|
||||
@ -107,21 +111,42 @@ export default class NotificationHandler {
|
||||
case 'upper-right':
|
||||
this.settings.corner.x += workAreaWidth - offSet;
|
||||
this.settings.corner.y += offSet;
|
||||
// Call Notification settings
|
||||
this.callNotificationSettings.x +=
|
||||
workAreaWidth - offSet - CALL_NOTIFICATION_WIDTH;
|
||||
this.callNotificationSettings.y +=
|
||||
workAreaHeight - offSet - CALL_NOTIFICATION_HEIGHT;
|
||||
break;
|
||||
case 'lower-right':
|
||||
this.settings.corner.x += workAreaWidth - offSet;
|
||||
this.settings.corner.y += workAreaHeight - offSet;
|
||||
// Call Notification settings
|
||||
this.callNotificationSettings.x +=
|
||||
workAreaWidth - offSet - CALL_NOTIFICATION_WIDTH;
|
||||
this.callNotificationSettings.y += offSet;
|
||||
break;
|
||||
case 'lower-left':
|
||||
this.settings.corner.x += offSet;
|
||||
this.settings.corner.y += workAreaHeight - offSet;
|
||||
this.settings.corner.y +=
|
||||
workAreaHeight - offSet - CALL_NOTIFICATION_HEIGHT;
|
||||
// Call Notification settings
|
||||
this.callNotificationSettings.x += offSet;
|
||||
this.callNotificationSettings.y += offSet;
|
||||
break;
|
||||
case 'upper-left':
|
||||
this.settings.corner.x += offSet;
|
||||
this.settings.corner.y += offSet;
|
||||
// Call Notification settings
|
||||
this.callNotificationSettings.x += offSet;
|
||||
this.callNotificationSettings.y +=
|
||||
workAreaHeight - offSet - CALL_NOTIFICATION_HEIGHT;
|
||||
break;
|
||||
default:
|
||||
// no change needed
|
||||
this.callNotificationSettings.x +=
|
||||
workAreaWidth - offSet - CALL_NOTIFICATION_WIDTH;
|
||||
this.callNotificationSettings.y +=
|
||||
workAreaHeight - offSet - CALL_NOTIFICATION_HEIGHT;
|
||||
break;
|
||||
}
|
||||
this.calculateDimensions();
|
||||
|
187
src/renderer/notification-theme.ts
Normal file
187
src/renderer/notification-theme.ts
Normal file
@ -0,0 +1,187 @@
|
||||
import { Themes } from './components/notification-settings';
|
||||
|
||||
export const whiteColorRegExp = new RegExp(
|
||||
/^(?:white|#fff(?:fff)?|rgba?\(\s*255\s*,\s*255\s*,\s*255\s*(?:,\s*1\s*)?\))$/i,
|
||||
);
|
||||
export const darkTheme = [
|
||||
'#e23030',
|
||||
'#b5616a',
|
||||
'#ab8ead',
|
||||
'#ebc875',
|
||||
'#a3be77',
|
||||
'#58c6ff',
|
||||
'#ebab58',
|
||||
];
|
||||
|
||||
export const Colors = {
|
||||
dark: {
|
||||
regularFlashingNotificationBgColor: '#27588e',
|
||||
notificationBackgroundColor: '#27292c',
|
||||
notificationBorderColor: '#717681',
|
||||
mentionBackgroundColor: '#99342c',
|
||||
mentionBorderColor: '#ff5d50',
|
||||
},
|
||||
light: {
|
||||
regularFlashingNotificationBgColor: '#aad4f8',
|
||||
notificationBackgroundColor: '#f1f1f3',
|
||||
notificationBorderColor: 'transparent',
|
||||
mentionBackgroundColor: '#fcc1b9',
|
||||
mentionBorderColor: 'transparent',
|
||||
},
|
||||
};
|
||||
|
||||
export type Theme = '' | Themes.DARK | Themes.LIGHT;
|
||||
const LIGHT_THEME = '#EAEBEC';
|
||||
const DARK_THEME = '#25272B';
|
||||
|
||||
/**
|
||||
* Validates the color
|
||||
* @param color
|
||||
* @private
|
||||
*/
|
||||
export const isValidColor = (color: string): boolean => {
|
||||
return /^#([A-Fa-f0-9]{6})/.test(color);
|
||||
};
|
||||
|
||||
/**
|
||||
* SDA versions prior to 9.2.3 do not support theme color properly, reason why SFE-lite is pushing notification default background color and theme.
|
||||
* For that reason, we try to identify if provided color is the default one or not.
|
||||
* @param color color sent through SDABridge
|
||||
* @returns boolean
|
||||
*/
|
||||
export const isCustomColor = (color: string): boolean => {
|
||||
return !!(color && color !== LIGHT_THEME && color !== DARK_THEME);
|
||||
};
|
||||
|
||||
/**
|
||||
* This function aims at providing toast notification css classes
|
||||
*/
|
||||
export const getContainerCssClasses = (
|
||||
theme,
|
||||
flash,
|
||||
isExternal,
|
||||
hasMention,
|
||||
): string[] => {
|
||||
const customClasses: string[] = [];
|
||||
if (flash && theme) {
|
||||
if (isExternal) {
|
||||
customClasses.push('external-border');
|
||||
if (hasMention) {
|
||||
customClasses.push(`${theme}-ext-mention-flashing`);
|
||||
} else {
|
||||
customClasses.push(`${theme}-ext-flashing`);
|
||||
}
|
||||
} else if (hasMention) {
|
||||
customClasses.push(`${theme}-mention-flashing`);
|
||||
} else {
|
||||
// In case it's a regular message notification
|
||||
customClasses.push(`${theme}-flashing`);
|
||||
}
|
||||
} else if (isExternal) {
|
||||
customClasses.push('external-border');
|
||||
}
|
||||
return customClasses;
|
||||
};
|
||||
|
||||
/**
|
||||
* Function that allows to increase color brightness
|
||||
* @param hex hes color
|
||||
* @param percent percent
|
||||
* @returns new hex color
|
||||
*/
|
||||
export const increaseBrightness = (hex: string, percent: number) => {
|
||||
// strip the leading # if it's there
|
||||
hex = hex.replace(/^\s*#|\s*$/g, '');
|
||||
const r = parseInt(hex.substr(0, 2), 16);
|
||||
const g = parseInt(hex.substr(2, 2), 16);
|
||||
const b = parseInt(hex.substr(4, 2), 16);
|
||||
|
||||
return (
|
||||
'#' +
|
||||
// tslint:disable-next-line: no-bitwise
|
||||
(0 | ((1 << 8) + r + ((256 - r) * percent) / 100)).toString(16).substr(1) +
|
||||
// tslint:disable-next-line: no-bitwise
|
||||
(0 | ((1 << 8) + g + ((256 - g) * percent) / 100)).toString(16).substr(1) +
|
||||
// tslint:disable-next-line: no-bitwise
|
||||
(0 | ((1 << 8) + b + ((256 - b) * percent) / 100)).toString(16).substr(1)
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns custom border color
|
||||
* @param theme current theme
|
||||
* @param customColor color
|
||||
* @returns custom border color
|
||||
*/
|
||||
export const getThemedCustomBorderColor = (
|
||||
theme: string,
|
||||
customColor: string,
|
||||
) => {
|
||||
return theme === Themes.DARK
|
||||
? increaseBrightness(customColor, 50)
|
||||
: 'transparent';
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns notification colors based on theme
|
||||
*/
|
||||
export const getThemeColors = (
|
||||
theme,
|
||||
flash,
|
||||
isExternal,
|
||||
hasMention,
|
||||
color,
|
||||
): { [key: string]: string } => {
|
||||
const currentColors =
|
||||
theme === Themes.DARK ? { ...Colors.dark } : { ...Colors.light };
|
||||
const externalFlashingBackgroundColor =
|
||||
theme === Themes.DARK ? '#70511f' : '#f6e5a6';
|
||||
if (flash && theme) {
|
||||
if (isExternal) {
|
||||
if (!hasMention) {
|
||||
currentColors.notificationBorderColor = '#F7CA3B';
|
||||
currentColors.notificationBackgroundColor =
|
||||
externalFlashingBackgroundColor;
|
||||
if (isCustomColor(color)) {
|
||||
currentColors.notificationBorderColor = getThemedCustomBorderColor(
|
||||
theme,
|
||||
color,
|
||||
);
|
||||
currentColors.notificationBackgroundColor = color;
|
||||
}
|
||||
} else {
|
||||
currentColors.notificationBorderColor = '#F7CA3B';
|
||||
}
|
||||
} else if (hasMention) {
|
||||
currentColors.notificationBorderColor =
|
||||
currentColors.notificationBorderColor;
|
||||
} else {
|
||||
// in case of regular message without mention
|
||||
// FYI: SDA versions prior to 9.2.3 do not support theme color properly, reason why SFE-lite is pushing notification default background color.
|
||||
// For this reason, to be backward compatible, we check if sent color correspond to 'default' background color. If yes, we should ignore it and not consider it as a custom color.
|
||||
currentColors.notificationBackgroundColor = isCustomColor(color)
|
||||
? color
|
||||
: currentColors.regularFlashingNotificationBgColor;
|
||||
currentColors.notificationBorderColor = isCustomColor(color)
|
||||
? getThemedCustomBorderColor(theme, color)
|
||||
: theme === Themes.DARK
|
||||
? '#2996fd'
|
||||
: 'transparent';
|
||||
}
|
||||
} else if (!flash) {
|
||||
if (hasMention) {
|
||||
currentColors.notificationBackgroundColor =
|
||||
currentColors.mentionBackgroundColor;
|
||||
currentColors.notificationBorderColor = currentColors.mentionBorderColor;
|
||||
} else if (isCustomColor(color)) {
|
||||
currentColors.notificationBackgroundColor = color;
|
||||
currentColors.notificationBorderColor = getThemedCustomBorderColor(
|
||||
theme,
|
||||
color,
|
||||
);
|
||||
} else if (isExternal) {
|
||||
currentColors.notificationBorderColor = '#F7CA3B';
|
||||
}
|
||||
}
|
||||
return currentColors;
|
||||
};
|
@ -21,7 +21,7 @@ import {
|
||||
} from '../common/api-interface';
|
||||
import { isMac } from '../common/env';
|
||||
import { logger } from '../common/logger';
|
||||
import NotificationHandler from './notification-handler';
|
||||
import NotificationHandler, { ICorner } from './notification-handler';
|
||||
|
||||
const CLEAN_UP_INTERVAL = 60 * 1000; // Closes inactive notification
|
||||
const animationQueue = new AnimationQueue();
|
||||
@ -526,6 +526,14 @@ class Notification extends NotificationHandler {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the call notification insert position
|
||||
* @return ICorner
|
||||
*/
|
||||
public getCallNotificationPosition = (): ICorner => {
|
||||
return this.callNotificationSettings;
|
||||
};
|
||||
|
||||
/**
|
||||
* Waits for window to load and resolves
|
||||
*
|
||||
|
@ -5,6 +5,7 @@ import * as ReactDOM from 'react-dom';
|
||||
import { i18n } from '../common/i18n-preload';
|
||||
import AboutBox from './components/about-app';
|
||||
import BasicAuth from './components/basic-auth';
|
||||
import CallNotification from './components/call-notification';
|
||||
import NotificationComp from './components/notification-comp';
|
||||
import NotificationSettings from './components/notification-settings';
|
||||
import ScreenPicker from './components/screen-picker';
|
||||
@ -22,6 +23,7 @@ const enum components {
|
||||
basicAuth = 'basic-auth',
|
||||
notification = 'notification-comp',
|
||||
notificationSettings = 'notification-settings',
|
||||
callNotification = 'call-notification',
|
||||
welcome = 'welcome',
|
||||
snippingTool = 'snipping-tool',
|
||||
titleBar = 'windows-title-bar',
|
||||
@ -71,6 +73,10 @@ const load = () => {
|
||||
)();
|
||||
component = NotificationSettings;
|
||||
break;
|
||||
case components.callNotification:
|
||||
document.title = i18n.t('Call Notification - Symphony')();
|
||||
component = CallNotification;
|
||||
break;
|
||||
case components.welcome:
|
||||
document.title = i18n.t('WelcomeText', 'Welcome')();
|
||||
component = Welcome;
|
||||
|
@ -82,6 +82,8 @@ if (ssfWindow.ssf) {
|
||||
checkMediaPermission: ssfWindow.ssf.checkMediaPermission,
|
||||
showNotification: ssfWindow.ssf.showNotification,
|
||||
closeNotification: ssfWindow.ssf.closeNotification,
|
||||
showCallNotification: ssfWindow.ssf.showCallNotification,
|
||||
closeCallNotification: ssfWindow.ssf.closeCallNotification,
|
||||
restartApp: ssfWindow.ssf.restartApp,
|
||||
closeAllWrapperWindows: ssfWindow.ssf.closeAllWrapperWindows,
|
||||
setZoomLevel: ssfWindow.ssf.setZoomLevel,
|
||||
|
@ -15,6 +15,7 @@ import {
|
||||
ConfigUpdateType,
|
||||
EPresenceStatusCategory,
|
||||
IBoundsChange,
|
||||
ICallNotificationData,
|
||||
ICloud9Pipe,
|
||||
ICPUUsage,
|
||||
ILogMsg,
|
||||
@ -74,6 +75,11 @@ const notificationActionCallbacks = new Map<
|
||||
NotificationActionCallback
|
||||
>();
|
||||
|
||||
const callNotificationActionCallbacks = new Map<
|
||||
number,
|
||||
NotificationActionCallback
|
||||
>();
|
||||
|
||||
const DEFAULT_THROTTLE = 1000;
|
||||
|
||||
// Throttle func
|
||||
@ -697,6 +703,45 @@ export class SSFApi {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a call notification from the main process
|
||||
* @param notificationOpts {INotificationData}
|
||||
* @param notificationCallback {NotificationActionCallback}
|
||||
*/
|
||||
public showCallNotification(
|
||||
notificationOpts: ICallNotificationData,
|
||||
notificationCallback: NotificationActionCallback,
|
||||
): void {
|
||||
// Store callbacks based on notification id so,
|
||||
// we can use this to trigger on notification action
|
||||
if (typeof notificationOpts.id === 'number') {
|
||||
callNotificationActionCallbacks.set(
|
||||
notificationOpts.id,
|
||||
notificationCallback,
|
||||
);
|
||||
}
|
||||
// ipc does not support sending Functions, Promises, Symbols, WeakMaps,
|
||||
// or WeakSets will throw an exception
|
||||
if (notificationOpts.callback) {
|
||||
delete notificationOpts.callback;
|
||||
}
|
||||
ipcRenderer.send(apiName.symphonyApi, {
|
||||
cmd: apiCmds.showCallNotification,
|
||||
notificationOpts,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes a specific call notification based on id
|
||||
* @param notificationId {number} Id of a notification
|
||||
*/
|
||||
public closeCallNotification(notificationId: number): void {
|
||||
ipcRenderer.send(apiName.symphonyApi, {
|
||||
cmd: apiCmds.closeCallNotification,
|
||||
notificationId,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get zoom level
|
||||
*
|
||||
@ -1145,6 +1190,19 @@ local.ipcRenderer.on('notification-actions', (_event, args) => {
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* An event triggered by the main process on call notification actions
|
||||
* @param {ICallNotificationData}
|
||||
*/
|
||||
local.ipcRenderer.on('call-notification-actions', (_event, args) => {
|
||||
const callback = callNotificationActionCallbacks.get(args.data.id);
|
||||
const data = args.data;
|
||||
data.notificationData = args.notificationData;
|
||||
if (args && callback) {
|
||||
callback(args.event, data);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* An event triggered by the main process on updating the cloud config
|
||||
* @param {string[]}
|
||||
|
298
src/renderer/styles/call-notification.less
Normal file
298
src/renderer/styles/call-notification.less
Normal file
@ -0,0 +1,298 @@
|
||||
@import 'theme';
|
||||
@import 'variables';
|
||||
@import 'notifications-animations';
|
||||
|
||||
.black-text {
|
||||
--text-color: #000000;
|
||||
--button-bg-color: #52575f;
|
||||
--logo-bg: url('../renderer/assets/symphony-logo.png');
|
||||
}
|
||||
|
||||
.light {
|
||||
--text-color-title: @black;
|
||||
--text-color-primary: @graphite-80;
|
||||
--text-color-secondary: @graphite-60;
|
||||
--notification-bg-color: @light-notification-bg-color;
|
||||
--notification-border-color: @graphite-10;
|
||||
--profile-place-holder-text: @electricity-ui-50;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--text-color-title: @vanilla-white;
|
||||
--text-color-primary: @graphite-05;
|
||||
--text-color-secondary: @graphite-20;
|
||||
--notification-bg-color: @dark-notification-bg-color;
|
||||
--notification-border-color: @graphite-60;
|
||||
--profile-place-holder-text: @electricity-ui-40;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.container {
|
||||
height: 246px;
|
||||
padding: 16px 16px 24px 16px;
|
||||
background-color: var(--notification-bg-color);
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
display: flex;
|
||||
border-radius: 8px;
|
||||
border: 2px solid var(--yellow-20, #f7ca3b);
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 12px;
|
||||
font-family: @font-family;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
line-height: 24px;
|
||||
color: var(--text-color-title);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 200px;
|
||||
}
|
||||
|
||||
.thumbnail {
|
||||
width: 78px;
|
||||
height: 78px;
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
border: 4px solid var(--notification-border-color);
|
||||
background-color: var(--notification-bg-color);
|
||||
overflow: hidden;
|
||||
|
||||
.profilePlaceHolderText {
|
||||
color: var(--profile-place-holder-text);
|
||||
text-align: center;
|
||||
font-size: 32px;
|
||||
font-family: @font-family;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
line-height: 16px;
|
||||
text-transform: uppercase;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.profilePlaceHolderClassName {
|
||||
}
|
||||
}
|
||||
|
||||
.profilePlaceHolderContainer {
|
||||
border-radius: 70px;
|
||||
}
|
||||
|
||||
.roomPlaceHolderContainer {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.logo-container {
|
||||
display: flex;
|
||||
position: relative;
|
||||
.logo {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
.profile-picture {
|
||||
width: 86px;
|
||||
border-radius: 70px;
|
||||
}
|
||||
.default-logo {
|
||||
top: 0;
|
||||
width: 40px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.profile-picture-badge {
|
||||
width: 32px;
|
||||
position: absolute;
|
||||
}
|
||||
.badge-position-im {
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
.badge-position-room {
|
||||
right: -2px;
|
||||
bottom: -1px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.caller-name {
|
||||
color: var(--text-color-primary);
|
||||
font-family: @font-family;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
line-height: 24px;
|
||||
display: flex;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 200px;
|
||||
}
|
||||
|
||||
.ext-badge-container {
|
||||
width: 27px;
|
||||
height: 19px;
|
||||
padding: 0 4px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.caller-role {
|
||||
color: var(--text-color-secondary);
|
||||
font-family: @font-family;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 200px;
|
||||
}
|
||||
|
||||
.application-details {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.company-icon {
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
.company-name {
|
||||
color: var(--text-color-secondary);
|
||||
font-family: @font-family;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 200px;
|
||||
}
|
||||
|
||||
.info-text-container {
|
||||
display: flex;
|
||||
padding: 0 4px;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
.caller-name-container {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.caller-info-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex: 1 0 0;
|
||||
align-self: stretch;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
|
||||
.caller-info {
|
||||
flex: 1;
|
||||
height: 198px;
|
||||
}
|
||||
|
||||
.caller-info,
|
||||
text {
|
||||
align-self: stretch;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
align-self: stretch;
|
||||
|
||||
button {
|
||||
display: flex;
|
||||
padding: 4px 12px;
|
||||
justify-content: center;
|
||||
width: 72px;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
border-radius: 16px;
|
||||
border-style: none;
|
||||
cursor: pointer;
|
||||
|
||||
.label {
|
||||
color: @red-05;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
font-family: @font-family;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
line-height: 16px;
|
||||
text-transform: uppercase;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
.call-type-other {
|
||||
width: 94px;
|
||||
}
|
||||
}
|
||||
|
||||
.decline {
|
||||
background-color: @red-50;
|
||||
}
|
||||
|
||||
.action-icon {
|
||||
width: 12px;
|
||||
}
|
||||
|
||||
.accept {
|
||||
background-color: @green-50;
|
||||
}
|
||||
|
||||
.light-flashing {
|
||||
animation: light-flashing 1s infinite !important;
|
||||
}
|
||||
|
||||
.dark-flashing {
|
||||
animation: dark-flashing 1s infinite !important;
|
||||
}
|
||||
|
||||
.dark-ext-flashing {
|
||||
animation: dark-ext-flashing 1s infinite !important;
|
||||
}
|
||||
|
||||
.light-ext-flashing {
|
||||
animation: light-ext-flashing 1s infinite !important;
|
||||
}
|
||||
|
||||
.dark-mention-flashing {
|
||||
animation: dark-mention-flashing 1s infinite !important;
|
||||
}
|
||||
|
||||
.light-mention-flashing {
|
||||
animation: light-mention-flashing 1s infinite !important;
|
||||
}
|
||||
|
||||
.dark-ext-mention-flashing {
|
||||
animation: dark-ext-mention-flashing 1s infinite !important;
|
||||
}
|
||||
|
||||
.light-ext-mention-flashing {
|
||||
animation: light-ext-mention-flashing 1s infinite !important;
|
||||
}
|
@ -25,4 +25,9 @@
|
||||
@graphite-70: #3a3d43;
|
||||
|
||||
@vanilla-white: #fff;
|
||||
@black: #000000;
|
||||
@blue: #008eff;
|
||||
|
||||
@red-05: #fbeeed;
|
||||
@red-50: #dd342e;
|
||||
@green-50: #378535;
|
||||
|
Loading…
Reference in New Issue
Block a user