mirror of
https://github.com/finos/SymphonyElectron.git
synced 2025-02-25 18:55:29 -06:00
Merge branch 'main' into SDA-3151
This commit is contained in:
commit
141b190669
95
gulpfile.js
95
gulpfile.js
@ -9,7 +9,7 @@ const path = require('path');
|
||||
const tsProject = tsc.createProject('./tsconfig.json');
|
||||
|
||||
gulp.task('clean', function () {
|
||||
return del('lib');
|
||||
return del('lib');
|
||||
});
|
||||
|
||||
/**
|
||||
@ -17,71 +17,78 @@ gulp.task('clean', function () {
|
||||
* and copy to the destination
|
||||
*/
|
||||
gulp.task('compile', function () {
|
||||
return tsProject.src()
|
||||
.pipe(sourcemaps.init())
|
||||
.pipe(tsProject())
|
||||
.pipe(sourcemaps.write('.', { sourceRoot: './', includeContent: false }))
|
||||
.on('error', (err) => console.log(err))
|
||||
.pipe(gulp.dest('lib'))
|
||||
return tsProject
|
||||
.src()
|
||||
.pipe(sourcemaps.init())
|
||||
.pipe(tsProject())
|
||||
.pipe(sourcemaps.write('.', { sourceRoot: './', includeContent: false }))
|
||||
.on('error', (err) => console.log(err))
|
||||
.pipe(gulp.dest('lib'));
|
||||
});
|
||||
|
||||
gulp.task('less', function () {
|
||||
return gulp.src('./src/**/*.less')
|
||||
.pipe(sourcemaps.init())
|
||||
.pipe(less())
|
||||
.pipe(sourcemaps.write())
|
||||
.pipe(gulp.dest(path.join(__dirname, 'lib/src')));
|
||||
return gulp
|
||||
.src('./src/**/*.less')
|
||||
.pipe(sourcemaps.init())
|
||||
.pipe(less())
|
||||
.pipe(sourcemaps.write())
|
||||
.pipe(gulp.dest(path.join(__dirname, 'lib/src')));
|
||||
});
|
||||
|
||||
/**
|
||||
* Copy all assets to JS codebase
|
||||
*/
|
||||
gulp.task('copy', function () {
|
||||
return gulp.src([
|
||||
return gulp
|
||||
.src(
|
||||
[
|
||||
'./src/renderer/assets/*',
|
||||
'./src/renderer/*.html',
|
||||
'./src/locale/*',
|
||||
'./package.json'
|
||||
], {
|
||||
"base": "./src"
|
||||
}).pipe(gulp.dest('lib/src'))
|
||||
'./package.json',
|
||||
],
|
||||
{
|
||||
base: './src',
|
||||
},
|
||||
)
|
||||
.pipe(gulp.dest('lib/src'));
|
||||
});
|
||||
|
||||
/**
|
||||
* Set expiry time for test builds
|
||||
*/
|
||||
gulp.task('setExpiry', function (done) {
|
||||
// Set expiry of 15 days for test builds we create from CI
|
||||
const expiryDays = process.argv[4] || 15;
|
||||
if (expiryDays < 1) {
|
||||
console.log(`Not setting expiry as the value provided is ${expiryDays}`);
|
||||
done();
|
||||
return;
|
||||
// Set expiry of 15 days for test builds we create from CI
|
||||
const expiryDays = process.argv[4] || 15;
|
||||
if (expiryDays < 1) {
|
||||
console.log(`Not setting expiry as the value provided is ${expiryDays}`);
|
||||
done();
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Setting expiry to ${expiryDays} days`);
|
||||
const milliseconds = 24 * 60 * 60 * 1000;
|
||||
const expiryTime = new Date().getTime() + expiryDays * milliseconds;
|
||||
console.log(`Setting expiry time to ${expiryTime}`);
|
||||
|
||||
const ttlHandlerFile = path.join(__dirname, 'src/app/ttl-handler.ts');
|
||||
fs.readFile(ttlHandlerFile, 'utf8', function (err, data) {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
return done(err);
|
||||
}
|
||||
|
||||
console.log(`Setting expiry to ${expiryDays} days`);
|
||||
const milliseconds = 24 * 60 * 60 * 1000;
|
||||
const expiryTime = new Date().getTime() + (expiryDays * milliseconds);
|
||||
console.log(`Setting expiry time to ${expiryTime}`);
|
||||
// Do a simple search and replace in the `ttl-handler.ts` file
|
||||
const replacementString = `const ttlExpiryTime = ${expiryTime}`;
|
||||
const result = data.replace(/const ttlExpiryTime = -1/g, replacementString);
|
||||
|
||||
const ttlHandlerFile = path.join(__dirname, 'src/app/ttl-handler.ts');
|
||||
fs.readFile(ttlHandlerFile, 'utf8', function (err, data) {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
return done(err);
|
||||
}
|
||||
|
||||
// Do a simple search and replace in the `ttl-handler.ts` file
|
||||
const replacementString = `const ttlExpiryTime = ${expiryTime}`;
|
||||
const result = data.replace(/const ttlExpiryTime = -1/g, replacementString);
|
||||
|
||||
fs.writeFile(ttlHandlerFile, result, 'utf8', function (err) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
done();
|
||||
});
|
||||
fs.writeFile(ttlHandlerFile, result, 'utf8', function (err) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
gulp.task('build', gulp.series('clean', 'compile', 'less', 'copy'));
|
||||
|
@ -498,7 +498,7 @@ The default (if not specified, or if specified as empty string "") is
|
||||
Expected values:
|
||||
|
||||
* "true"
|
||||
"Electron/X.X" is removed from user-agents.
|
||||
User-agent value "Electron/X.X" is replaced with "ElectronSymphony/X.X"
|
||||
* "false"
|
||||
User-agents are not modified (default)
|
||||
|
||||
|
141
spec/notificationComp.spec.ts
Normal file
141
spec/notificationComp.spec.ts
Normal file
@ -0,0 +1,141 @@
|
||||
import { shallow } from 'enzyme';
|
||||
import * as React from 'react';
|
||||
import NotificationComp from '../src/renderer/components/notification-comp';
|
||||
import { Themes } from '../src/renderer/components/notification-settings';
|
||||
import { ipcRenderer } from './__mocks__/electron';
|
||||
|
||||
const IPC_RENDERER_NOTIFICATION_DATA_CHANNEL = 'notification-data';
|
||||
describe('Toast notification component', () => {
|
||||
const defaultProps = {
|
||||
title: 'Oompa Loompa',
|
||||
};
|
||||
const spy = jest.spyOn(NotificationComp.prototype, 'setState');
|
||||
let wrapper;
|
||||
beforeEach(() => {
|
||||
wrapper = shallow(React.createElement(NotificationComp));
|
||||
});
|
||||
|
||||
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 render Symphony logo if no image provided', () => {
|
||||
const logo = '';
|
||||
ipcRenderer.send(IPC_RENDERER_NOTIFICATION_DATA_CHANNEL, {
|
||||
...defaultProps,
|
||||
logo,
|
||||
});
|
||||
const defaultLogoContainer = wrapper.find('.default-logo');
|
||||
expect(defaultLogoContainer).toBeTruthy();
|
||||
const imageContainer = wrapper.find('.profile-picture');
|
||||
expect(imageContainer.exists()).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should render Symphony logo if no icon sent - Client 1.5 settings use-case with "See sample" ', () => {
|
||||
ipcRenderer.send(IPC_RENDERER_NOTIFICATION_DATA_CHANNEL, {
|
||||
...defaultProps,
|
||||
});
|
||||
const defaultLogoContainer = wrapper.find('.default-logo');
|
||||
expect(defaultLogoContainer).toBeTruthy();
|
||||
const imageContainer = wrapper.find('.profile-picture');
|
||||
expect(imageContainer.exists()).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should render Symphony logo if Symphony default image provided', () => {
|
||||
const logo = './default.png';
|
||||
ipcRenderer.send(IPC_RENDERER_NOTIFICATION_DATA_CHANNEL, {
|
||||
...defaultProps,
|
||||
logo,
|
||||
});
|
||||
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();
|
||||
});
|
||||
|
||||
it('should flash as a mention when mention sent', () => {
|
||||
const theme = Themes.DARK;
|
||||
const flash = true;
|
||||
const hasMention = true;
|
||||
const themedMentionFlashing = `.${theme}-mention-flashing`;
|
||||
let themedToastNotification = wrapper.find(themedMentionFlashing);
|
||||
expect(themedToastNotification.exists()).toBeFalsy();
|
||||
ipcRenderer.send(IPC_RENDERER_NOTIFICATION_DATA_CHANNEL, {
|
||||
...defaultProps,
|
||||
hasMention,
|
||||
theme,
|
||||
flash,
|
||||
});
|
||||
themedToastNotification = wrapper.find(themedMentionFlashing);
|
||||
expect(themedToastNotification.exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should flash as mention even if it is a message from an external user', () => {
|
||||
const theme = Themes.DARK;
|
||||
const isExternal = true;
|
||||
const hasMention = true;
|
||||
const flash = true;
|
||||
const themedMentionFlashing = `.${theme}-ext-mention-flashing`;
|
||||
let themedToastNotification = wrapper.find(themedMentionFlashing);
|
||||
expect(themedToastNotification.exists()).toBeFalsy();
|
||||
ipcRenderer.send(IPC_RENDERER_NOTIFICATION_DATA_CHANNEL, {
|
||||
...defaultProps,
|
||||
hasMention,
|
||||
theme,
|
||||
isExternal,
|
||||
flash,
|
||||
});
|
||||
themedToastNotification = wrapper.find(themedMentionFlashing);
|
||||
expect(themedToastNotification.exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should display reply button when requested', () => {
|
||||
const hasReply = true;
|
||||
const replyButtonSelector = `.action-button`;
|
||||
let toastNotificationReplyButton = wrapper.find(replyButtonSelector);
|
||||
expect(toastNotificationReplyButton.exists()).toBeFalsy();
|
||||
ipcRenderer.send(IPC_RENDERER_NOTIFICATION_DATA_CHANNEL, {
|
||||
...defaultProps,
|
||||
hasReply,
|
||||
});
|
||||
toastNotificationReplyButton = wrapper.find(replyButtonSelector);
|
||||
expect(toastNotificationReplyButton.exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should trigger mouse hovering function while hovering a notification', async () => {
|
||||
const spy = jest.spyOn(ipcRenderer, 'send');
|
||||
const notificationContainer = wrapper.find(
|
||||
'[data-testid="NOTIFICATION_CONTAINER"]',
|
||||
);
|
||||
expect(notificationContainer).toBeTruthy();
|
||||
notificationContainer.simulate('mouseenter');
|
||||
expect(spy).toBeCalledWith('notification-mouseenter', 0);
|
||||
});
|
||||
});
|
@ -182,9 +182,9 @@ class VersionHandler {
|
||||
|
||||
/* Get SFE version */
|
||||
let urlSfeVersion: string;
|
||||
if (mainUrl?.includes('/client-bff/')) {
|
||||
urlSfeVersion = mainUrl?.includes('/client-bff/daily/')
|
||||
? `${protocol}//${hostname}/client-bff/daily/version.json`
|
||||
if (mainUrl?.includes('bff')) {
|
||||
urlSfeVersion = mainUrl?.includes('/daily/')
|
||||
? `${protocol}//${hostname}/bff-daily/daily/version.json`
|
||||
: `${protocol}//${hostname}/client-bff/version.json`;
|
||||
this.versionInfo.sfeClientType = '2.0';
|
||||
} else {
|
||||
|
@ -948,10 +948,8 @@ export class WindowHandler {
|
||||
this.aboutAppWindow.webContents.once('did-finish-load', async () => {
|
||||
let client = '';
|
||||
if (this.url && this.url.startsWith('https://corporate.symphony.com')) {
|
||||
const manaPath = 'client-bff';
|
||||
const daily = 'daily';
|
||||
client = this.url.includes(manaPath)
|
||||
? this.url.includes(daily)
|
||||
client = this.url.includes('bff')
|
||||
? this.url.includes('daily')
|
||||
? 'Symphony 2.0 - Daily'
|
||||
: 'Symphony 2.0'
|
||||
: 'Symphony Classic';
|
||||
@ -1960,8 +1958,6 @@ export class WindowHandler {
|
||||
this.url = this.globalConfig.url;
|
||||
}
|
||||
const parsedUrl = parse(this.url);
|
||||
const manaPath = 'client-bff';
|
||||
const manaChannel = 'daily';
|
||||
const csrfToken = await this.mainWindow.webContents.executeJavaScript(
|
||||
`localStorage.getItem('x-km-csrf-token')`,
|
||||
);
|
||||
@ -1970,10 +1966,10 @@ export class WindowHandler {
|
||||
this.url = this.startUrl + `?x-km-csrf-token=${csrfToken}`;
|
||||
break;
|
||||
case ClientSwitchType.CLIENT_2_0:
|
||||
this.url = `https://${parsedUrl.hostname}/${manaPath}/index.html?x-km-csrf-token=${csrfToken}`;
|
||||
this.url = `https://${parsedUrl.hostname}/client-bff/index.html?x-km-csrf-token=${csrfToken}`;
|
||||
break;
|
||||
case ClientSwitchType.CLIENT_2_0_DAILY:
|
||||
this.url = `https://${parsedUrl.hostname}/${manaPath}/${manaChannel}/index.html?x-km-csrf-token=${csrfToken}`;
|
||||
this.url = `https://${parsedUrl.hostname}/bff-daily/daily/index.html?x-km-csrf-token=${csrfToken}`;
|
||||
break;
|
||||
default:
|
||||
this.url = this.globalConfig.url + `?x-km-csrf-token=${csrfToken}`;
|
||||
|
@ -32,6 +32,7 @@ import { screenSnippet } from './screen-snippet-handler';
|
||||
import { updateAlwaysOnTop } from './window-actions';
|
||||
import { ICustomBrowserWindow, windowHandler } from './window-handler';
|
||||
|
||||
import { notification } from '../renderer/notification';
|
||||
interface IStyles {
|
||||
name: styleNames;
|
||||
content: string;
|
||||
@ -293,6 +294,9 @@ export const updateLocale = async (locale: LocaleType): Promise<void> => {
|
||||
appMenu.update(locale);
|
||||
}
|
||||
|
||||
// Update notification after new locale
|
||||
notification.cleanUpInactiveNotification();
|
||||
|
||||
if (i18n.isValidLocale(locale)) {
|
||||
// Update user config file with latest locale changes
|
||||
await config.updateUserConfig({ locale });
|
||||
|
@ -150,6 +150,7 @@ export interface INotificationData {
|
||||
isElectronNotification?: boolean;
|
||||
callback?: () => void;
|
||||
hasReply?: boolean;
|
||||
hasMention?: boolean;
|
||||
}
|
||||
|
||||
export enum NotificationActions {
|
||||
|
1947
src/demo/index.html
1947
src/demo/index.html
File diff suppressed because it is too large
Load Diff
3
src/renderer/assets/close-icon-dark.svg
Normal file
3
src/renderer/assets/close-icon-dark.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0 8C0 12.4183 3.58172 16 8 16C12.4183 16 16 12.4183 16 8C16 3.58173 12.4183 0 8 0C3.58172 0 0 3.58173 0 8ZM10.7071 6.70711L9.41421 8L10.7071 9.29289C11.0976 9.68342 11.0976 10.3166 10.7071 10.7071C10.3166 11.0976 9.68342 11.0976 9.29289 10.7071L8 9.41421L6.70711 10.7071C6.31658 11.0976 5.68342 11.0976 5.29289 10.7071C4.90237 10.3166 4.90237 9.68342 5.29289 9.29289L6.58579 8L5.29289 6.70711C4.90237 6.31658 4.90237 5.68342 5.29289 5.29289C5.68342 4.90237 6.31658 4.90237 6.70711 5.29289L8 6.58579L9.29289 5.29289C9.68342 4.90237 10.3166 4.90237 10.7071 5.29289C11.0976 5.68342 11.0976 6.31658 10.7071 6.70711Z" fill="#B0B3BA"/>
|
||||
</svg>
|
After Width: | Height: | Size: 743 B |
3
src/renderer/assets/close-icon-light.svg
Normal file
3
src/renderer/assets/close-icon-light.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0 8C0 12.4183 3.58172 16 8 16C12.4183 16 16 12.4183 16 8C16 3.58173 12.4183 0 8 0C3.58172 0 0 3.58173 0 8ZM10.7071 6.70711L9.41421 8L10.7071 9.29289C11.0976 9.68342 11.0976 10.3166 10.7071 10.7071C10.3166 11.0976 9.68342 11.0976 9.29289 10.7071L8 9.41421L6.70711 10.7071C6.31658 11.0976 5.68342 11.0976 5.29289 10.7071C4.90237 10.3166 4.90237 9.68342 5.29289 9.29289L6.58579 8L5.29289 6.70711C4.90237 6.31658 4.90237 5.68342 5.29289 5.29289C5.68342 4.90237 6.31658 4.90237 6.70711 5.29289L8 6.58579L9.29289 5.29289C9.68342 4.90237 10.3166 4.90237 10.7071 5.29289C11.0976 5.68342 11.0976 6.31658 10.7071 6.70711Z" fill="#717681"/>
|
||||
</svg>
|
After Width: | Height: | Size: 743 B |
3
src/renderer/assets/close-icon.svg
Normal file
3
src/renderer/assets/close-icon.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0 8C0 12.4183 3.58172 16 8 16C12.4183 16 16 12.4183 16 8C16 3.58173 12.4183 0 8 0C3.58172 0 0 3.58173 0 8ZM10.7071 6.70711L9.41421 8L10.7071 9.29289C11.0976 9.68342 11.0976 10.3166 10.7071 10.7071C10.3166 11.0976 9.68342 11.0976 9.29289 10.7071L8 9.41421L6.70711 10.7071C6.31658 11.0976 5.68342 11.0976 5.29289 10.7071C4.90237 10.3166 4.90237 9.68342 5.29289 9.29289L6.58579 8L5.29289 6.70711C4.90237 6.31658 4.90237 5.68342 5.29289 5.29289C5.68342 4.90237 6.31658 4.90237 6.70711 5.29289L8 6.58579L9.29289 5.29289C9.68342 4.90237 10.3166 4.90237 10.7071 5.29289C11.0976 5.68342 11.0976 6.31658 10.7071 6.70711Z" fill="#717681"/>
|
||||
</svg>
|
After Width: | Height: | Size: 743 B |
18
src/renderer/assets/symphony-badge.svg
Normal file
18
src/renderer/assets/symphony-badge.svg
Normal file
@ -0,0 +1,18 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0 12C0 7.30475 0 4.95713 1.0832 3.24656C1.63551 2.37437 2.37437 1.63551 3.24656 1.0832C4.95713 0 7.30475 0 12 0C16.6952 0 19.0429 0 20.7534 1.0832C21.6256 1.63551 22.3645 2.37437 22.9168 3.24656C24 4.95713 24 7.30475 24 12C24 16.6952 24 19.0429 22.9168 20.7534C22.3645 21.6256 21.6256 22.3645 20.7534 22.9168C19.0429 24 16.6952 24 12 24C7.30475 24 4.95713 24 3.24656 22.9168C2.37437 22.3645 1.63551 21.6256 1.0832 20.7534C0 19.0429 0 16.6952 0 12Z" fill="#000028"/>
|
||||
<path d="M0 12C0 7.30475 0 4.95713 1.0832 3.24656C1.63551 2.37437 2.37437 1.63551 3.24656 1.0832C4.95713 0 7.30475 0 12 0C16.6952 0 19.0429 0 20.7534 1.0832C21.6256 1.63551 22.3645 2.37437 22.9168 3.24656C24 4.95713 24 7.30475 24 12C24 16.6952 24 19.0429 22.9168 20.7534C22.3645 21.6256 21.6256 22.3645 20.7534 22.9168C19.0429 24 16.6952 24 12 24C7.30475 24 4.95713 24 3.24656 22.9168C2.37437 22.3645 1.63551 21.6256 1.0832 20.7534C0 19.0429 0 16.6952 0 12Z" fill="url(#paint0_linear)"/>
|
||||
<g opacity="0.8">
|
||||
<path d="M17.4168 9.45882V6.69586C17.4168 6.12987 17.1178 5.59854 16.6331 5.30746C15.9106 4.87315 14.3456 4.14545 11.9811 4.14545C9.61656 4.14545 8.05154 4.87315 7.32905 5.30977C6.84437 5.59854 6.54541 6.12987 6.54541 6.69586V10.8449L15.1519 13.3861V15.2342C15.1519 15.4837 14.9979 15.6593 14.7352 15.791L11.9811 17.1979L9.21115 15.784C8.96428 15.6593 8.81027 15.4837 8.81027 15.2342V13.8481L6.54541 13.1551V15.2342C6.54541 16.3754 7.18637 17.3457 8.20556 17.8516L11.9811 19.8545L15.7408 17.8609C16.7758 17.3457 17.4168 16.3754 17.4168 15.2342V11.769L8.81027 9.2278V7.11862C9.42632 6.81599 10.4727 6.45561 11.9811 6.45561C13.4895 6.45561 14.5358 6.81599 15.1519 7.11862V8.76577L17.4168 9.45882Z" fill="#0098FF"/>
|
||||
<path d="M17.4168 9.45882V6.69586C17.4168 6.12987 17.1178 5.59854 16.6331 5.30746C15.9106 4.87315 14.3456 4.14545 11.9811 4.14545C9.61656 4.14545 8.05154 4.87315 7.32905 5.30977C6.84437 5.59854 6.54541 6.12987 6.54541 6.69586V10.8449L15.1519 13.3861V15.2342C15.1519 15.4837 14.9979 15.6593 14.7352 15.791L11.9811 17.1979L9.21115 15.784C8.96428 15.6593 8.81027 15.4837 8.81027 15.2342V13.8481L6.54541 13.1551V15.2342C6.54541 16.3754 7.18637 17.3457 8.20556 17.8516L11.9811 19.8545L15.7408 17.8609C16.7758 17.3457 17.4168 16.3754 17.4168 15.2342V11.769L8.81027 9.2278V7.11862C9.42632 6.81599 10.4727 6.45561 11.9811 6.45561C13.4895 6.45561 14.5358 6.81599 15.1519 7.11862V8.76577L17.4168 9.45882Z" fill="url(#paint1_radial)"/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear" x1="12" y1="0" x2="12" y2="24" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white" stop-opacity="0.2"/>
|
||||
<stop offset="1" stop-color="white" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<radialGradient id="paint1_radial" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(12 4.14545) rotate(90) scale(10.6364 13.6898)">
|
||||
<stop stop-color="white" stop-opacity="0.4"/>
|
||||
<stop offset="1" stop-color="white" stop-opacity="0"/>
|
||||
</radialGradient>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 3.0 KiB |
@ -3,6 +3,7 @@ import { ipcRenderer } from 'electron';
|
||||
import * as React from 'react';
|
||||
|
||||
import { i18n } from '../../common/i18n-preload';
|
||||
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,
|
||||
@ -16,20 +17,38 @@ const darkTheme = [
|
||||
'#58c6ff',
|
||||
'#ebab58',
|
||||
];
|
||||
type Theme = '' | 'light' | 'dark';
|
||||
|
||||
interface IState {
|
||||
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;
|
||||
body: string;
|
||||
image: string;
|
||||
icon: string;
|
||||
icon: string | undefined;
|
||||
id: number;
|
||||
color: string;
|
||||
flash: boolean;
|
||||
isExternal: boolean;
|
||||
theme: Theme;
|
||||
hasReply: boolean;
|
||||
hasMention: boolean;
|
||||
isInputHidden: boolean;
|
||||
containerHeight: number;
|
||||
canSendMessage: boolean;
|
||||
@ -41,12 +60,18 @@ type mouseEventButton =
|
||||
type keyboardEvent = React.KeyboardEvent<HTMLInputElement>;
|
||||
|
||||
// Notification container height
|
||||
const CONTAINER_HEIGHT = 88;
|
||||
const CONTAINER_HEIGHT_WITH_INPUT = 120;
|
||||
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<{}, IState> {
|
||||
export default class NotificationComp extends React.Component<
|
||||
{},
|
||||
INotificationState
|
||||
> {
|
||||
private readonly eventHandlers = {
|
||||
onClose: (winKey) => (_event: mouseEventButton) => this.close(winKey),
|
||||
onClose: (winKey) => (_event: mouseEventButton) =>
|
||||
this.close(_event, winKey),
|
||||
onClick: (data) => (_event: mouseEventButton) => this.click(data),
|
||||
onContextMenu: (event) => this.contextMenu(event),
|
||||
onMouseEnter: (winKey) => (_event: mouseEventButton) =>
|
||||
@ -59,7 +84,6 @@ export default class NotificationComp extends React.Component<{}, IState> {
|
||||
onReply: (winKey) => (_event: mouseEventButton) => this.onReply(winKey),
|
||||
onKeyUp: (winKey) => (event: keyboardEvent) => this.onKeyUp(event, winKey),
|
||||
};
|
||||
private flashTimer: NodeJS.Timer | undefined;
|
||||
private input: React.RefObject<HTMLInputElement>;
|
||||
|
||||
constructor(props) {
|
||||
@ -77,6 +101,7 @@ export default class NotificationComp extends React.Component<{}, IState> {
|
||||
theme: '',
|
||||
isInputHidden: true,
|
||||
hasReply: false,
|
||||
hasMention: false,
|
||||
containerHeight: CONTAINER_HEIGHT,
|
||||
canSendMessage: false,
|
||||
};
|
||||
@ -100,7 +125,6 @@ export default class NotificationComp extends React.Component<{}, IState> {
|
||||
*/
|
||||
public componentWillUnmount(): void {
|
||||
ipcRenderer.removeListener('notification-data', this.updateState);
|
||||
this.clearFlashInterval();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -114,91 +138,84 @@ export default class NotificationComp extends React.Component<{}, IState> {
|
||||
color,
|
||||
isExternal,
|
||||
theme,
|
||||
isInputHidden,
|
||||
containerHeight,
|
||||
hasReply,
|
||||
canSendMessage,
|
||||
icon,
|
||||
} = this.state;
|
||||
let themeClassName;
|
||||
if (theme) {
|
||||
themeClassName = theme;
|
||||
} else if (darkTheme.includes(color.toLowerCase())) {
|
||||
themeClassName = 'blackText';
|
||||
themeClassName = 'black-text';
|
||||
} else {
|
||||
themeClassName =
|
||||
color && color.match(whiteColorRegExp) ? 'light' : 'dark';
|
||||
color && color.match(whiteColorRegExp) ? Themes.LIGHT : Themes.DARK;
|
||||
}
|
||||
|
||||
const bgColor = { backgroundColor: color || '#ffffff' };
|
||||
const containerClass = classNames('container', {
|
||||
'external-border': isExternal,
|
||||
});
|
||||
const actionButtonContainer = classNames('rte-button-container', {
|
||||
'action-container-margin': !isInputHidden,
|
||||
});
|
||||
|
||||
const themeColors = this.getThemeColors();
|
||||
const closeImgFilePath = `../renderer/assets/close-icon-${themeClassName}.svg`;
|
||||
let containerCssClass = `container ${themeClassName} `;
|
||||
const customCssClasses = this.getContainerCssClasses();
|
||||
containerCssClass += customCssClasses.join(' ');
|
||||
return (
|
||||
<div
|
||||
className={containerClass}
|
||||
data-testid='NOTIFICATION_CONTAINER'
|
||||
className={containerCssClass}
|
||||
style={{
|
||||
height: containerHeight,
|
||||
backgroundColor: bgColor.backgroundColor,
|
||||
backgroundColor: themeColors.notificationBackgroundColor,
|
||||
borderColor: themeColors.notificationBorderColor,
|
||||
}}
|
||||
lang={i18n.getLocale()}
|
||||
onMouseEnter={this.eventHandlers.onMouseEnter(id)}
|
||||
onMouseLeave={this.eventHandlers.onMouseLeave(id)}
|
||||
>
|
||||
<div
|
||||
className={`close-button ${themeClassName}`}
|
||||
title={i18n.t('Close')()}
|
||||
>
|
||||
<img
|
||||
src={closeImgFilePath}
|
||||
title={i18n.t('Close')()}
|
||||
alt='close'
|
||||
onClick={this.eventHandlers.onClose(id)}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className='main-container'
|
||||
role='alert'
|
||||
style={bgColor}
|
||||
onContextMenu={this.eventHandlers.onContextMenu}
|
||||
onClick={this.eventHandlers.onClick(id)}
|
||||
onMouseEnter={this.eventHandlers.onMouseEnter(id)}
|
||||
onMouseLeave={this.eventHandlers.onMouseLeave(id)}
|
||||
>
|
||||
<div className='logo-container'>
|
||||
<div className='logo'>
|
||||
<img
|
||||
src='../renderer/assets/notification-symphony-logo.svg'
|
||||
alt='Symphony logo'
|
||||
/>
|
||||
<div className='logo-container'>{this.renderImage(icon)}</div>
|
||||
<div className='notification-container'>
|
||||
<div className='notification-header'>
|
||||
<div className='notification-header-content'>
|
||||
<span className={`title ${themeClassName}`}>{title}</span>
|
||||
{this.renderExtBadge(isExternal)}
|
||||
</div>
|
||||
{this.renderReplyButton(id, themeClassName)}
|
||||
</div>
|
||||
</div>
|
||||
<div className='header'>
|
||||
<div className='title-container'>
|
||||
<span className={`title ${themeClassName}`}>{title}</span>
|
||||
{this.renderExtBadge(isExternal)}
|
||||
</div>
|
||||
<span className={`message ${themeClassName}`}>{body}</span>
|
||||
</div>
|
||||
<div className='actions-container'>
|
||||
<button
|
||||
className={`action-button ${themeClassName}`}
|
||||
title={i18n.t('Close')()}
|
||||
onClick={this.eventHandlers.onClose(id)}
|
||||
>
|
||||
{i18n.t('Close')()}
|
||||
</button>
|
||||
<button
|
||||
className={`action-button ${themeClassName}`}
|
||||
style={{ visibility: hasReply ? 'visible' : 'hidden' }}
|
||||
title={i18n.t('Reply')()}
|
||||
onClick={this.eventHandlers.onOpenReply(id)}
|
||||
>
|
||||
{i18n.t('Reply')()}
|
||||
</button>
|
||||
<span className={`message-preview ${themeClassName}`}>{body}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
...{ display: isInputHidden ? 'none' : 'block' },
|
||||
...bgColor,
|
||||
}}
|
||||
className='rte-container'
|
||||
>
|
||||
{this.renderRTE(themeClassName)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders RTE
|
||||
* @param isInputHidden
|
||||
*/
|
||||
private renderRTE(themeClassName: string): JSX.Element | undefined {
|
||||
const { canSendMessage, isInputHidden, id } = this.state;
|
||||
const actionButtonContainer = classNames('rte-button-container', {
|
||||
'action-container-margin': !isInputHidden,
|
||||
});
|
||||
if (!isInputHidden) {
|
||||
return (
|
||||
<div className='rte-container'>
|
||||
<div className='input-container'>
|
||||
<div className='input-border' />
|
||||
<input
|
||||
style={bgColor}
|
||||
className={themeClassName}
|
||||
autoFocus={true}
|
||||
onKeyUp={this.eventHandlers.onKeyUp(id)}
|
||||
@ -221,8 +238,9 @@ export default class NotificationComp extends React.Component<{}, IState> {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -242,6 +260,45 @@ export default class NotificationComp extends React.Component<{}, IState> {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
/**
|
||||
* Renders image if provided otherwise renders symphony logo
|
||||
* @param imageUrl
|
||||
*/
|
||||
private renderImage(imageUrl: string | undefined): 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');
|
||||
const shouldDisplayBadge = !!imageUrl && !isDefaultUrl;
|
||||
if (imageUrl && !isDefaultUrl) {
|
||||
imgClass = 'profile-picture';
|
||||
url = imageUrl;
|
||||
alt = 'Profile picture';
|
||||
}
|
||||
return (
|
||||
<div className='logo'>
|
||||
<img className={imgClass} src={url} alt={alt} />
|
||||
{this.renderSymphonyBadge(shouldDisplayBadge)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders profile picture symphpony badge
|
||||
* @param hasImageUrl
|
||||
*/
|
||||
private renderSymphonyBadge(hasImageUrl: boolean): JSX.Element | undefined {
|
||||
if (hasImageUrl) {
|
||||
return (
|
||||
<img
|
||||
src='../renderer/assets/symphony-badge.svg'
|
||||
alt=''
|
||||
className='profile-picture-badge'
|
||||
/>
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked when the notification window is clicked
|
||||
@ -250,7 +307,6 @@ export default class NotificationComp extends React.Component<{}, IState> {
|
||||
*/
|
||||
private click(id: number): void {
|
||||
ipcRenderer.send('notification-clicked', id);
|
||||
this.clearFlashInterval();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -258,9 +314,9 @@ export default class NotificationComp extends React.Component<{}, IState> {
|
||||
*
|
||||
* @param id {number}
|
||||
*/
|
||||
private close(id: number): void {
|
||||
private close(event: any, id: number): void {
|
||||
event.stopPropagation();
|
||||
ipcRenderer.send('close-notification', id);
|
||||
this.clearFlashInterval();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -318,15 +374,6 @@ export default class NotificationComp extends React.Component<{}, IState> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears a active notification flash interval
|
||||
*/
|
||||
private clearFlashInterval(): void {
|
||||
if (this.flashTimer) {
|
||||
clearInterval(this.flashTimer);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays an input on the notification
|
||||
*
|
||||
@ -388,30 +435,27 @@ export default class NotificationComp extends React.Component<{}, IState> {
|
||||
* @param data {Object}
|
||||
*/
|
||||
private updateState(_event, data): void {
|
||||
const { color, flash } = data;
|
||||
data.color = color && !color.startsWith('#') ? '#' + color : color;
|
||||
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}`
|
||||
: this.isValidColor(color)
|
||||
? color
|
||||
: '';
|
||||
data.isInputHidden = true;
|
||||
data.containerHeight = CONTAINER_HEIGHT;
|
||||
|
||||
data.color = this.isValidColor(data.color) ? data.color : '';
|
||||
|
||||
// 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.resetNotificationData();
|
||||
this.setState(data as IState);
|
||||
|
||||
if (this.flashTimer) {
|
||||
clearInterval(this.flashTimer);
|
||||
}
|
||||
if (flash) {
|
||||
const origColor = data.color;
|
||||
this.flashTimer = setInterval(() => {
|
||||
const { color: bgColor } = this.state;
|
||||
if (bgColor === 'red') {
|
||||
this.setState({ color: origColor });
|
||||
} else {
|
||||
this.setState({ color: 'red' });
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
this.setState(data as INotificationState);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -432,4 +476,168 @@ export default class NotificationComp extends React.Component<{}, IState> {
|
||||
this.input.current.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @param theming
|
||||
*/
|
||||
private renderReplyButton(
|
||||
id: number,
|
||||
theming: string,
|
||||
): JSX.Element | undefined {
|
||||
const { hasReply } = this.state;
|
||||
if (hasReply) {
|
||||
return (
|
||||
<button
|
||||
className={`action-button ${theming}`}
|
||||
style={{ display: hasReply ? 'block' : 'none' }}
|
||||
title={i18n.t('Reply')()}
|
||||
onClick={this.eventHandlers.onOpenReply(id)}
|
||||
>
|
||||
{i18n.t('Reply')()}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
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';
|
||||
}
|
||||
}
|
||||
|
@ -26,8 +26,9 @@ interface ICorner {
|
||||
}
|
||||
|
||||
type startCorner = 'upper-right' | 'upper-left' | 'lower-right' | 'lower-left';
|
||||
const NEXT_INSERT_POSITION = 96;
|
||||
const NEXT_INSERT_POSITION_WITH_INPUT = 128;
|
||||
const NEXT_INSERT_POSITION = 100;
|
||||
const NEXT_INSERT_POSITION_WITH_INPUT = 142;
|
||||
const NOTIFICATIONS_PADDING_SEPARATION = 12;
|
||||
|
||||
export default class NotificationHandler {
|
||||
public settings: ISettings;
|
||||
@ -138,10 +139,11 @@ export default class NotificationHandler {
|
||||
activeNotifications.forEach((notification) => {
|
||||
if (notification && windowExists(notification)) {
|
||||
const [, height] = notification.getSize();
|
||||
nextNotificationY +=
|
||||
const shift =
|
||||
height > this.settings.height
|
||||
? NEXT_INSERT_POSITION_WITH_INPUT
|
||||
: NEXT_INSERT_POSITION;
|
||||
nextNotificationY += shift + NOTIFICATIONS_PADDING_SEPARATION;
|
||||
}
|
||||
});
|
||||
if (activeNotifications.length < this.settings.maxVisibleNotifications) {
|
||||
@ -297,10 +299,9 @@ export default class NotificationHandler {
|
||||
* Calculates the first and next notification insert position
|
||||
*/
|
||||
private calculateDimensions() {
|
||||
const vertSpace = 8;
|
||||
|
||||
// Calc totalHeight & totalWidth
|
||||
this.settings.totalHeight = this.settings.height + vertSpace;
|
||||
this.settings.totalHeight =
|
||||
this.settings.height + NOTIFICATIONS_PADDING_SEPARATION;
|
||||
this.settings.totalWidth = this.settings.width;
|
||||
|
||||
let firstPosX;
|
||||
|
@ -20,8 +20,9 @@ import NotificationHandler from './notification-handler';
|
||||
|
||||
const CLEAN_UP_INTERVAL = 60 * 1000; // Closes inactive notification
|
||||
const animationQueue = new AnimationQueue();
|
||||
const CONTAINER_HEIGHT_WITH_INPUT = 120; // Notification container height
|
||||
|
||||
const CONTAINER_HEIGHT = 104; // Notification container height
|
||||
const CONTAINER_HEIGHT_WITH_INPUT = 146; // Notification container height including input field
|
||||
const CONTAINER_WIDTH = 363;
|
||||
interface ICustomBrowserWindow extends Electron.BrowserWindow {
|
||||
winName: string;
|
||||
notificationData: INotificationData;
|
||||
@ -34,8 +35,8 @@ type startCorner = 'upper-right' | 'upper-left' | 'lower-right' | 'lower-left';
|
||||
const notificationSettings = {
|
||||
startCorner: 'upper-right' as startCorner,
|
||||
display: '',
|
||||
width: 344,
|
||||
height: 88,
|
||||
width: CONTAINER_WIDTH,
|
||||
height: CONTAINER_HEIGHT,
|
||||
totalHeight: 0,
|
||||
totalWidth: 0,
|
||||
corner: {
|
||||
@ -54,7 +55,7 @@ const notificationSettings = {
|
||||
animationStepMs: 5,
|
||||
logging: true,
|
||||
spacing: 8,
|
||||
differentialHeight: 32,
|
||||
differentialHeight: 42,
|
||||
};
|
||||
|
||||
class Notification extends NotificationHandler {
|
||||
@ -264,8 +265,8 @@ class Notification extends NotificationHandler {
|
||||
isExternal,
|
||||
theme,
|
||||
hasReply,
|
||||
hasMention,
|
||||
} = data;
|
||||
|
||||
notificationWindow.webContents.send('notification-data', {
|
||||
title,
|
||||
company,
|
||||
@ -278,6 +279,7 @@ class Notification extends NotificationHandler {
|
||||
isExternal,
|
||||
theme,
|
||||
hasReply,
|
||||
hasMention,
|
||||
});
|
||||
notificationWindow.showInactive();
|
||||
}
|
||||
@ -425,6 +427,25 @@ class Notification extends NotificationHandler {
|
||||
this.inactiveWindows = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the active notification after certain period
|
||||
*/
|
||||
public cleanUpInactiveNotification() {
|
||||
if (this.inactiveWindows.length > 0) {
|
||||
logger.info('notification: cleaning up inactive notification windows', {
|
||||
inactiveNotification: this.inactiveWindows.length,
|
||||
});
|
||||
this.inactiveWindows.forEach((window) => {
|
||||
if (windowExists(window)) {
|
||||
window.close();
|
||||
}
|
||||
});
|
||||
logger.info(`notification: cleaned up inactive notification windows`, {
|
||||
inactiveNotification: this.inactiveWindows.length,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Brings all the notification to the top
|
||||
* issue: ELECTRON-1382
|
||||
@ -506,25 +527,6 @@ class Notification extends NotificationHandler {
|
||||
this.activeNotifications.push(notificationWindow);
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the active notification after certain period
|
||||
*/
|
||||
private cleanUpInactiveNotification() {
|
||||
if (this.inactiveWindows.length > 0) {
|
||||
logger.info('notification: cleaning up inactive notification windows', {
|
||||
inactiveNotification: this.inactiveWindows.length,
|
||||
});
|
||||
this.inactiveWindows.forEach((window) => {
|
||||
if (windowExists(window)) {
|
||||
window.close();
|
||||
}
|
||||
});
|
||||
logger.info(`notification: cleaned up inactive notification windows`, {
|
||||
inactiveNotification: this.inactiveWindows.length,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the timer for a specific notification window
|
||||
*
|
||||
@ -586,7 +588,11 @@ class Notification extends NotificationHandler {
|
||||
return;
|
||||
}
|
||||
clearTimeout(notificationWindow.displayTimer);
|
||||
notificationWindow.setSize(344, CONTAINER_HEIGHT_WITH_INPUT, true);
|
||||
notificationWindow.setSize(
|
||||
CONTAINER_WIDTH,
|
||||
CONTAINER_HEIGHT_WITH_INPUT,
|
||||
true,
|
||||
);
|
||||
const pos = this.activeNotifications.indexOf(notificationWindow) + 1;
|
||||
this.moveNotificationUp(pos, this.activeNotifications);
|
||||
}
|
||||
@ -596,8 +602,8 @@ class Notification extends NotificationHandler {
|
||||
*/
|
||||
private getNotificationOpts(): Electron.BrowserWindowConstructorOptions {
|
||||
return {
|
||||
width: 344,
|
||||
height: 88,
|
||||
width: CONTAINER_WIDTH,
|
||||
height: CONTAINER_HEIGHT,
|
||||
alwaysOnTop: true,
|
||||
skipTaskbar: true,
|
||||
resizable: isWindowsOS,
|
||||
|
@ -89,6 +89,7 @@ if (ssfWindow.ssf) {
|
||||
closeAllWrapperWindows: ssfWindow.ssf.closeAllWrapperWindows,
|
||||
setZoomLevel: ssfWindow.ssf.setZoomLevel,
|
||||
getZoomLevel: ssfWindow.ssf.getZoomLevel,
|
||||
supportedSettings: ssfWindow.ssf.supportedSettings,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -25,6 +25,9 @@ import { throttle } from '../common/utils';
|
||||
import { getSource } from './desktop-capturer';
|
||||
import SSFNotificationHandler from './notification-ssf-hendler';
|
||||
import { ScreenSnippetBcHandler } from './screen-snippet-bc-handler';
|
||||
|
||||
const SUPPORTED_SETTINGS = ['flashing-notifications'];
|
||||
|
||||
const os = remote.require('os');
|
||||
|
||||
let isAltKey: boolean = false;
|
||||
@ -705,6 +708,14 @@ export class SSFApi {
|
||||
throttledSetZoomLevel(zoomLevel);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get SDA supported settings.
|
||||
* @returns list of supported features
|
||||
*/
|
||||
public supportedSettings(): string[] {
|
||||
return SUPPORTED_SETTINGS || [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,28 +1,36 @@
|
||||
@import 'theme';
|
||||
@inputWidth: 270px;
|
||||
@import 'variables';
|
||||
@import 'notifications-animations';
|
||||
|
||||
.blackText {
|
||||
.black-text {
|
||||
--text-color: #000000;
|
||||
--button-bg-color: #52575f;
|
||||
--button-test-color: #ffffff;
|
||||
--logo-bg: url('../assets/symphony-logo.png');
|
||||
}
|
||||
|
||||
.light {
|
||||
--text-color: #525760;
|
||||
--button-bg-color: linear-gradient(
|
||||
0deg,
|
||||
rgba(255, 255, 255, 0.72),
|
||||
rgba(255, 255, 255, 0.72)
|
||||
),
|
||||
#525760;
|
||||
--text-color: #000000;
|
||||
--notification-bg-color: @light-notification-bg-color;
|
||||
--notification-border-color: transparent;
|
||||
--button-color: #717681;
|
||||
--button-border-color: #717681;
|
||||
--button-hover-color: #717681;
|
||||
--button-hover-border-color: #717681;
|
||||
--button-hover-bg-color: #e3e5e7;
|
||||
--button-test-color: #000000;
|
||||
--logo-bg: url('../assets/symphony-logo.png');
|
||||
}
|
||||
|
||||
.dark {
|
||||
--text-color: #ffffff;
|
||||
--button-bg-color: #52575f;
|
||||
--notification-bg-color: @dark-notification-bg-color;
|
||||
--notification-border-color: #717681;
|
||||
--button-color: #b0b3ba;
|
||||
--button-border-color: #b0b3ba;
|
||||
--button-hover-color: #cdcfd4;
|
||||
--button-hover-border-color: #cdcfd4;
|
||||
--button-hover-bg-color: #3a3d43;
|
||||
--button-test-color: #ffffff;
|
||||
--logo-bg: url('../assets/symphony-logo.png');
|
||||
}
|
||||
|
||||
.big {
|
||||
@ -36,67 +44,119 @@
|
||||
body {
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 344px;
|
||||
height: 88px;
|
||||
display: flex;
|
||||
background-color: #ffffff;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
line-height: 15px;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
background-color: var(--notification-bg-color);
|
||||
border: 2px solid;
|
||||
border-radius: 10px;
|
||||
border-color: var(--notification-border-color);
|
||||
overflow: hidden;
|
||||
line-height: 15px;
|
||||
&:hover > .close-button {
|
||||
display: block;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
&:hover .close {
|
||||
visibility: visible;
|
||||
.main-container {
|
||||
border-radius: 8px;
|
||||
position: relative;
|
||||
height: auto;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
padding-left: 12px;
|
||||
padding-right: 12px;
|
||||
padding-top: 12px;
|
||||
line-height: 15px;
|
||||
overflow: hidden;
|
||||
.logo-container {
|
||||
margin-right: 12px;
|
||||
display: flex;
|
||||
height: 64px;
|
||||
position: relative;
|
||||
.logo {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
.profile-picture {
|
||||
width: 64px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
.default-logo {
|
||||
top: 0;
|
||||
width: 40px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.profile-picture-badge {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
.notification-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
.notification-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
.notification-header-content {
|
||||
display: flex;
|
||||
margin-bottom: 8px;
|
||||
.title {
|
||||
font-family: sans-serif;
|
||||
font-weight: 600;
|
||||
max-width: 190px;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
line-height: 20px;
|
||||
-webkit-box-orient: vertical;
|
||||
cursor: default;
|
||||
color: var(--text-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
.message-preview {
|
||||
font-family: sans-serif;
|
||||
width: 100%;
|
||||
overflow-wrap: break-word;
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
overflow: hidden;
|
||||
-webkit-line-clamp: 3;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
cursor: default;
|
||||
text-overflow: ellipsis;
|
||||
color: var(--text-color);
|
||||
white-space: pre-line;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.external-border {
|
||||
border: 3px solid #f6b202;
|
||||
}
|
||||
|
||||
.main-container {
|
||||
height: 88px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
background-color: #ffffff;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
line-height: 15px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.ext-border {
|
||||
border: 3px solid #f6b202;
|
||||
border-radius: 10px;
|
||||
width: 338px;
|
||||
height: var(--border-height);
|
||||
position: fixed;
|
||||
}
|
||||
|
||||
.header {
|
||||
width: 232px;
|
||||
min-width: 215px;
|
||||
margin-top: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
border: 2px solid #f7ca3b;
|
||||
}
|
||||
|
||||
.header:lang(fr-FR) {
|
||||
min-width: 190px;
|
||||
}
|
||||
|
||||
.title-container {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.user-profile-pic-container {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
@ -123,8 +183,9 @@ body {
|
||||
|
||||
.ext-badge-container {
|
||||
height: 16px;
|
||||
width: 32px;
|
||||
margin: auto 8px;
|
||||
width: 26px;
|
||||
margin-left: 8px;
|
||||
padding-top: 1px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@ -135,37 +196,30 @@ body {
|
||||
margin: 12px;
|
||||
}
|
||||
|
||||
.actions-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
z-index: 5;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.action-container-margin {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.action-button {
|
||||
width: 59px;
|
||||
height: 24px;
|
||||
border-radius: 16px;
|
||||
padding: 2px 10px;
|
||||
background: var(--button-bg-color);
|
||||
color: var(--button-test-color);
|
||||
background: transparent;
|
||||
color: var(--button-color);
|
||||
flex: none;
|
||||
font-weight: 600;
|
||||
border-style: none;
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
line-height: 14px;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
text-transform: uppercase;
|
||||
order: 0;
|
||||
flex-grow: 0;
|
||||
font-style: normal;
|
||||
margin: 4px 8px 4px 0;
|
||||
font-family: @font-family;
|
||||
outline: none;
|
||||
border: 2px solid var(--button-border-color);
|
||||
&:hover {
|
||||
border-color: var(--button-hover-border-color) !important;
|
||||
background-color: var(--button-hover-bg-color) !important;
|
||||
color: var(--button-hover-color) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.action-button:hover {
|
||||
@ -181,66 +235,44 @@ body {
|
||||
}
|
||||
|
||||
.rte-container {
|
||||
width: 100%;
|
||||
height: 38px;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.input-container {
|
||||
width: 100%;
|
||||
height: 38px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.input-border {
|
||||
height: 2px;
|
||||
left: 8px;
|
||||
right: 8px;
|
||||
top: 0;
|
||||
background: #008eff;
|
||||
margin: 0 8px;
|
||||
}
|
||||
|
||||
input {
|
||||
width: @inputWidth;
|
||||
height: 38px;
|
||||
border: none;
|
||||
outline: none;
|
||||
position: relative;
|
||||
margin-left: 8px;
|
||||
font-size: 14px;
|
||||
caret-color: #008eff;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.rte-button-container {
|
||||
margin-right: 8px;
|
||||
display: flex;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
padding: 7px 13px;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-top: 2px solid #008eff;
|
||||
.input-container {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
outline: none;
|
||||
input {
|
||||
width: @input-width;
|
||||
height: 20px;
|
||||
border: none;
|
||||
outline: none;
|
||||
font-size: 14px;
|
||||
caret-color: #008eff;
|
||||
color: var(--text-color);
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
.rte-button-container {
|
||||
display: flex;
|
||||
.rte-thumbsup-button {
|
||||
width: 25px;
|
||||
height: 26px;
|
||||
align-self: center;
|
||||
padding: 3px;
|
||||
background: none;
|
||||
font-size: 14px;
|
||||
border: none;
|
||||
color: var(--text-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.rte-thumbsup-button {
|
||||
width: 25px;
|
||||
height: 26px;
|
||||
align-self: center;
|
||||
padding: 3px;
|
||||
background: none;
|
||||
font-size: 14px;
|
||||
border: none;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.rte-thumbsup-button:focus,
|
||||
.rte-send-button:focus {
|
||||
outline: none;
|
||||
}
|
||||
.rte-send-button:focus:not(:hover):not(:active),
|
||||
.rte-thumbsup-button:focus:not(:hover):not(:active) {
|
||||
outline: #008eff auto 1px;
|
||||
}
|
||||
|
||||
.rte-send-button {
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
@ -251,32 +283,22 @@ input {
|
||||
border: none;
|
||||
color: var(--text-color);
|
||||
}
|
||||
.rte-thumbsup-button:focus,
|
||||
.rte-send-button:focus {
|
||||
outline: none;
|
||||
}
|
||||
.rte-send-button:focus:not(:hover):not(:active),
|
||||
.rte-thumbsup-button:focus:not(:hover):not(:active) {
|
||||
outline: #008eff auto 1px;
|
||||
}
|
||||
|
||||
.rte-send-button:disabled {
|
||||
background: url('../assets/notification-send-button-disabled.svg') no-repeat
|
||||
center;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-family: sans-serif;
|
||||
font-weight: 600;
|
||||
width: auto;
|
||||
max-width: 175px;
|
||||
overflow-wrap: break-word;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
overflow: hidden;
|
||||
-webkit-line-clamp: 1;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
line-height: 24px;
|
||||
-webkit-box-orient: vertical;
|
||||
cursor: default;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.external-border .title {
|
||||
max-width: 142px;
|
||||
max-width: 142px !important;
|
||||
}
|
||||
|
||||
.company {
|
||||
@ -290,42 +312,47 @@ input {
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.message {
|
||||
font-family: sans-serif;
|
||||
width: 205px;
|
||||
overflow-wrap: break-word;
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
overflow: hidden;
|
||||
-webkit-line-clamp: 3;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
cursor: default;
|
||||
text-overflow: ellipsis;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.message:lang(fr-FR) {
|
||||
width: 182px;
|
||||
}
|
||||
|
||||
.logo-container {
|
||||
margin: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.close-button {
|
||||
position: absolute;
|
||||
top: -2;
|
||||
right: 0;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 40px;
|
||||
content: var(--logo-bg);
|
||||
.light-flashing {
|
||||
animation: light-flashing 1s infinite !important;
|
||||
}
|
||||
|
||||
@-webkit-keyframes blink {
|
||||
from,
|
||||
to {
|
||||
background: transparent;
|
||||
}
|
||||
50% {
|
||||
background: #008eff;
|
||||
}
|
||||
.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;
|
||||
}
|
||||
|
78
src/renderer/styles/notifications-animations.less
Normal file
78
src/renderer/styles/notifications-animations.less
Normal file
@ -0,0 +1,78 @@
|
||||
@import 'theme';
|
||||
@import 'variables';
|
||||
|
||||
@keyframes dark-mention-flashing {
|
||||
0% {
|
||||
background-color: @dark-notification-bg-color;
|
||||
border-color: @dark-notification-border-color;
|
||||
}
|
||||
50% {
|
||||
background-color: @dark-mention-flash-bg-color;
|
||||
border: 2px solid @dark-mention-flash-border-color;
|
||||
box-shadow: 0px 2px 4px rgba(5, 6, 6, 0.16),
|
||||
0px 12px 28px rgba(5, 6, 6, 0.64);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes light-mention-flashing {
|
||||
0% {
|
||||
background-color: @light-notification-bg-color;
|
||||
border-color: @light-notification-border-color;
|
||||
}
|
||||
50% {
|
||||
background-color: @light-mention-flash-mention-bg-color;
|
||||
border-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes dark-ext-mention-flashing {
|
||||
0% {
|
||||
background-color: @dark-notification-bg-color;
|
||||
border-color: @dark-external-flash-border-color;
|
||||
}
|
||||
50% {
|
||||
background-color: @dark-mention-flash-bg-color;
|
||||
border: 2px solid @dark-mention-flash-border-color;
|
||||
box-shadow: 0px 2px 4px rgba(5, 6, 6, 0.16),
|
||||
0px 12px 28px rgba(5, 6, 6, 0.64);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes light-ext-mention-flashing {
|
||||
0% {
|
||||
background-color: @light-notification-bg-color;
|
||||
border-color: @light-external-flash-border-color;
|
||||
}
|
||||
50% {
|
||||
background-color: @light-mention-flash-mention-bg-color;
|
||||
border-color: @light-mention-flash-border-color;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes light-flashing {
|
||||
50% {
|
||||
background-color: @light-notification-bg-color;
|
||||
border-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes dark-flashing {
|
||||
50% {
|
||||
background-color: @dark-notification-bg-color;
|
||||
border-color: @dark-notification-border-color;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes light-ext-flashing {
|
||||
50% {
|
||||
background-color: @light-notification-bg-color;
|
||||
border-color: @light-external-flash-border-color;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes dark-ext-flashing {
|
||||
50% {
|
||||
background-color: @dark-notification-bg-color;
|
||||
border-color: @dark-external-flash-border-color;
|
||||
}
|
||||
}
|
17
src/renderer/styles/variables.less
Normal file
17
src/renderer/styles/variables.less
Normal file
@ -0,0 +1,17 @@
|
||||
@input-width: 290px;
|
||||
@dark-notification-bg-color: #27292c;
|
||||
@dark-notification-border-color: #717681;
|
||||
@dark-regular-flash-bg-color: #27588e;
|
||||
@dark-regular-flash-border-color: #2996fd;
|
||||
@dark-mention-flash-bg-color: #99342c;
|
||||
@dark-mention-flash-border-color: #ff5d50;
|
||||
@dark-external-flash-border-color: #f7ca3b;
|
||||
@dark-external-flash-bg-color: #70511f;
|
||||
|
||||
@light-notification-bg-color: #f1f1f3;
|
||||
@light-notification-border-color: transparent;
|
||||
@light-regular-flash-mention-bg-color: #aad4f8;
|
||||
@light-mention-flash-mention-bg-color: #fcc1b9;
|
||||
@light-mention-flash-border-color: #ff5d50;
|
||||
@light-external-flash-border-color: #f7ca3b;
|
||||
@light-external-flash-bg-color: #f6e5a6;
|
Loading…
Reference in New Issue
Block a user