From 49c436f60e9cf28fe01d9945479a6889718d8452 Mon Sep 17 00:00:00 2001 From: sbenmoussati <51402489+sbenmoussati@users.noreply.github.com> Date: Fri, 16 Jul 2021 17:26:13 +0200 Subject: [PATCH 01/11] SDA-3237 Notifications re-design --- gulpfile.js | 95 +- spec/notificationComp.spec.ts | 121 + src/common/api-interface.ts | 1 + src/demo/index.html | 1947 +++++++++-------- src/renderer/assets/close-icon-dark.svg | 3 + src/renderer/assets/close-icon-light.svg | 3 + src/renderer/assets/close-icon.svg | 3 + src/renderer/assets/symphony-badge.svg | 18 + src/renderer/components/notification-comp.tsx | 292 ++- src/renderer/notification-handler.ts | 13 +- src/renderer/notification.ts | 24 +- src/renderer/preload-main.ts | 1 + src/renderer/ssf-api.ts | 11 + src/renderer/styles/notification-comp.less | 385 ++-- .../styles/notifications-animations.less | 82 + src/renderer/styles/variables.less | 17 + 16 files changed, 1769 insertions(+), 1247 deletions(-) create mode 100644 spec/notificationComp.spec.ts create mode 100644 src/renderer/assets/close-icon-dark.svg create mode 100644 src/renderer/assets/close-icon-light.svg create mode 100644 src/renderer/assets/close-icon.svg create mode 100644 src/renderer/assets/symphony-badge.svg create mode 100644 src/renderer/styles/notifications-animations.less create mode 100644 src/renderer/styles/variables.less diff --git a/gulpfile.js b/gulpfile.js index caf3b9dc..81fa05a9 100644 --- a/gulpfile.js +++ b/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')); diff --git a/spec/notificationComp.spec.ts b/spec/notificationComp.spec.ts new file mode 100644 index 00000000..4329d31b --- /dev/null +++ b/spec/notificationComp.spec.ts @@ -0,0 +1,121 @@ +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 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(); + }); +}); diff --git a/src/common/api-interface.ts b/src/common/api-interface.ts index 0167df1a..ededb13d 100644 --- a/src/common/api-interface.ts +++ b/src/common/api-interface.ts @@ -150,6 +150,7 @@ export interface INotificationData { isElectronNotification?: boolean; callback?: () => void; hasReply?: boolean; + hasMention?: boolean; } export enum NotificationActions { diff --git a/src/demo/index.html b/src/demo/index.html index b676c303..2cc218be 100644 --- a/src/demo/index.html +++ b/src/demo/index.html @@ -1,950 +1,1073 @@ - - - - - -

Symphony Electron API Demo

-
-

Remember to set this.origin to '*' in app-bridge.ts when using this API demo

-

Search for // DEMO-APP: and comment that line back in. - Make sure to comment it out again before you commit.

-
-
-

Notifications:

- -

-

- - -

-
-

Reply content will display here

-

- - -

-

- - -

-

- - -

-

- - -

-

- - -

-

- - -

-

- - -

-

Change theme:

- -
-

- - -

-

- - -

-

- - -

- -
-
- -
- - - - - - -
-
-

- Activity Detection: -

- - -

-
-
-

Change language:

- - -
-


-

Badge Count:

- -
- -
-


-

Screen Snippet:

- -

snippet output:

- - + .green-dot { + height: 25px; + width: 25px; + background-color: green; + border-radius: 50%; + display: inline-block; + } + .origin-reminder { + background: lightyellow; + padding: 20px; + margin: 40px; + border: 2px solid black; + display: block; + } + .origin-reminder-tt { + font-weight: normal; + background: yellow; + padding-left: 10px; + padding-right: 10px; + padding-top: 3px; + padding-bottom: 3px; + } + + + + +

Symphony Electron API Demo

+
+

+ Remember to set this.origin to + '*' in app-bridge.ts when using + this API demo +

+

+ Search for // DEMO-APP: and comment + that line back in. + Make sure to comment it out again before you commit. +

+
+
+

Notifications:

+

+ +

-
-

Logs:

- Filename -

Contents

- -

- Filename -

Contents

- +
+

+ + +

+
+

Reply content will display here

+

+ + +

+

+ + +

+

+ + +

+

+ + +

+

+ + +

+

+ + +

+

+ + +

+

+ + +

+

Change theme:

+

+ +
+

-
-

Window activate:

- +

+ + +

+

+ + +

+

+ + +

+ +
+
+ +
+ + + + + + +
+
+

Activity Detection:

- - +

+ + +

+
+
+

Change language:

+

+ + +
+

-
-

Measure cpu usage:

- - +
+

Badge Count:

+

+ +
+ +
+

-
-

Check Media Permission:

- -
- - - - - - - - - - - -
Camera permissionMicrophone permissionScreen permission
-
-

Get Media Sources:

- -
- +
+

Screen Snippet:

+ +

snippet output:

+ + -
-

Get Version Info:

- -
- Version Info: - - - - - - - - - - - - - - - - - -
API VersionContainer IdentifierContainer VersionBuild NumberSearch Api VersionCPU Arch
- - - + document.getElementById('restart-app').addEventListener('click', () => { + if (window.ssf) { + window.ssf.restartApp(); + } else { + postMessage(apiCmds.restartApp); + } + }); + diff --git a/src/renderer/assets/close-icon-dark.svg b/src/renderer/assets/close-icon-dark.svg new file mode 100644 index 00000000..02367378 --- /dev/null +++ b/src/renderer/assets/close-icon-dark.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/renderer/assets/close-icon-light.svg b/src/renderer/assets/close-icon-light.svg new file mode 100644 index 00000000..c6cdeca5 --- /dev/null +++ b/src/renderer/assets/close-icon-light.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/renderer/assets/close-icon.svg b/src/renderer/assets/close-icon.svg new file mode 100644 index 00000000..c6cdeca5 --- /dev/null +++ b/src/renderer/assets/close-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/renderer/assets/symphony-badge.svg b/src/renderer/assets/symphony-badge.svg new file mode 100644 index 00000000..dbbf56bf --- /dev/null +++ b/src/renderer/assets/symphony-badge.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/src/renderer/components/notification-comp.tsx b/src/renderer/components/notification-comp.tsx index 459d570c..12caecab 100644 --- a/src/renderer/components/notification-comp.tsx +++ b/src/renderer/components/notification-comp.tsx @@ -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,9 +17,22 @@ const darkTheme = [ '#58c6ff', '#ebab58', ]; -type Theme = '' | 'light' | 'dark'; -interface IState { +const Colors = { + dark: { + regularFlashingNotificationBgColor: '#27588e', + notificationBackgroundColor: '#27292c', + notificationBorderColor: '#717681', + }, + light: { + regularFlashingNotificationBgColor: '#aad4f8', + notificationBackgroundColor: '#f1f1f3', + notificationBorderColor: 'transparent', + }, +}; + +type Theme = '' | Themes.DARK | Themes.LIGHT; +interface INotificationState { title: string; company: string; body: string; @@ -30,6 +44,7 @@ interface IState { isExternal: boolean; theme: Theme; hasReply: boolean; + hasMention: boolean; isInputHidden: boolean; containerHeight: number; canSendMessage: boolean; @@ -41,10 +56,13 @@ type mouseEventButton = type keyboardEvent = React.KeyboardEvent; // Notification container height -const CONTAINER_HEIGHT = 88; -const CONTAINER_HEIGHT_WITH_INPUT = 120; +const CONTAINER_HEIGHT = 100; +const CONTAINER_HEIGHT_WITH_INPUT = 142; -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), onClick: (data) => (_event: mouseEventButton) => this.click(data), @@ -59,7 +77,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; constructor(props) { @@ -77,6 +94,7 @@ export default class NotificationComp extends React.Component<{}, IState> { theme: '', isInputHidden: true, hasReply: false, + hasMention: false, containerHeight: CONTAINER_HEIGHT, canSendMessage: false, }; @@ -100,7 +118,6 @@ export default class NotificationComp extends React.Component<{}, IState> { */ public componentWillUnmount(): void { ipcRenderer.removeListener('notification-data', this.updateState); - this.clearFlashInterval(); } /** @@ -114,91 +131,79 @@ 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 (
+
+ close +
-
-
- Symphony logo +
{this.renderImage(icon)}
+
+
+
+ {title} + {this.renderExtBadge(isExternal)} +
+ {this.renderReplyButton(id, themeClassName)}
-
-
-
- {title} - {this.renderExtBadge(isExternal)} -
- {body} -
-
- - + {body}
-
+ {this.renderRTE(themeClassName)} +
+ ); + } + + /** + * 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 ( +
-
{ />
-
- ); + ); + } + return; } /** @@ -242,6 +248,45 @@ export default class NotificationComp extends React.Component<{}, IState> {
); } + /** + * Renders image if provided otherwise renders symphony logo + * @param imageUrl + */ + private renderImage(imageUrl: string): JSX.Element | undefined { + let imgClass = 'default-logo'; + let url = '../renderer/assets/notification-symphony-logo.svg'; + let alt = 'Symphony logo'; + const isDefaultUrl = imageUrl.includes('default.png'); + const shouldDisplayBadge = !!imageUrl && !isDefaultUrl; + if (imageUrl && !isDefaultUrl) { + imgClass = 'profile-picture'; + url = imageUrl; + alt = 'Profile picture'; + } + return ( +
+ {alt} + {this.renderSymphonyBadge(shouldDisplayBadge)} +
+ ); + } + + /** + * Renders profile picture symphpony badge + * @param hasImageUrl + */ + private renderSymphonyBadge(hasImageUrl: boolean): JSX.Element | undefined { + if (hasImageUrl) { + return ( + + ); + } + return; + } /** * Invoked when the notification window is clicked @@ -250,7 +295,6 @@ export default class NotificationComp extends React.Component<{}, IState> { */ private click(id: number): void { ipcRenderer.send('notification-clicked', id); - this.clearFlashInterval(); } /** @@ -260,7 +304,6 @@ export default class NotificationComp extends React.Component<{}, IState> { */ private close(id: number): void { ipcRenderer.send('close-notification', id); - this.clearFlashInterval(); } /** @@ -318,15 +361,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 +422,13 @@ 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; + data.color = this.isValidColor(color) ? color : ''; data.isInputHidden = true; data.containerHeight = CONTAINER_HEIGHT; - - data.color = this.isValidColor(data.color) ? data.color : ''; - + data.theme = 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 +449,85 @@ 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 }; + if (flash && theme) { + if (isExternal) { + currentColors.notificationBorderColor = '#F7CA3B'; + } else if (hasMention) { + currentColors.notificationBorderColor = + currentColors.notificationBorderColor; + } else { + // in case of regular message without mention + currentColors.notificationBackgroundColor = color + ? color + : currentColors.regularFlashingNotificationBgColor; + currentColors.notificationBorderColor = color + ? color + : theme === Themes.DARK + ? '#2996fd' + : 'transparent'; + } + } else if (!flash && color) { + currentColors.notificationBackgroundColor = currentColors.notificationBorderColor = color; + } + 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 ( + + ); + } + 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) { + 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; + } } diff --git a/src/renderer/notification-handler.ts b/src/renderer/notification-handler.ts index 913b591d..b0d53019 100644 --- a/src/renderer/notification-handler.ts +++ b/src/renderer/notification-handler.ts @@ -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; diff --git a/src/renderer/notification.ts b/src/renderer/notification.ts index 17be5b49..73f48e08 100644 --- a/src/renderer/notification.ts +++ b/src/renderer/notification.ts @@ -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(); } @@ -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, diff --git a/src/renderer/preload-main.ts b/src/renderer/preload-main.ts index 2cc384cd..bcd97876 100644 --- a/src/renderer/preload-main.ts +++ b/src/renderer/preload-main.ts @@ -89,6 +89,7 @@ if (ssfWindow.ssf) { closeAllWrapperWindows: ssfWindow.ssf.closeAllWrapperWindows, setZoomLevel: ssfWindow.ssf.setZoomLevel, getZoomLevel: ssfWindow.ssf.getZoomLevel, + supportedSettings: ssfWindow.ssf.supportedSettings, }); } diff --git a/src/renderer/ssf-api.ts b/src/renderer/ssf-api.ts index 3e691be1..fe2e9db9 100644 --- a/src/renderer/ssf-api.ts +++ b/src/renderer/ssf-api.ts @@ -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 || []; + } } /** diff --git a/src/renderer/styles/notification-comp.less b/src/renderer/styles/notification-comp.less index 78db995f..91bc82f9 100644 --- a/src/renderer/styles/notification-comp.less +++ b/src/renderer/styles/notification-comp.less @@ -1,28 +1,37 @@ @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-border-color: #717681; + --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 +45,117 @@ 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; + } - &: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); + } + } } } .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 !important; } .header:lang(fr-FR) { min-width: 190px; } -.title-container { - display: flex; -} - .user-profile-pic-container { align-items: center; display: flex; @@ -123,8 +182,8 @@ body { .ext-badge-container { height: 16px; - width: 32px; - margin: auto 8px; + width: 26px; + margin-left: 8px; align-items: center; } @@ -135,37 +194,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,55 +233,54 @@ 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-send-button { + width: 25px; + height: 25px; + align-self: center; + padding: 3px; + background: url('../assets/notification-send-button-enabled.svg') + no-repeat center; + border: none; + color: var(--text-color); + } + } } .rte-thumbsup-button:focus, @@ -241,40 +292,11 @@ input { outline: #008eff auto 1px; } -.rte-send-button { - width: 25px; - height: 25px; - align-self: center; - padding: 3px; - background: url('../assets/notification-send-button-enabled.svg') no-repeat - center; - border: none; - color: var(--text-color); -} - .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; } @@ -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; } diff --git a/src/renderer/styles/notifications-animations.less b/src/renderer/styles/notifications-animations.less new file mode 100644 index 00000000..a4807306 --- /dev/null +++ b/src/renderer/styles/notifications-animations.less @@ -0,0 +1,82 @@ +@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: @light-mention-flash-border-color; + } +} + +@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 { + 0% { + background-color: @light-notification-bg-color; + } + 50% { + background-color: @light-external-flash-mention-bg-color; + } +} + +@keyframes dark-ext-flashing { + 0% { + background-color: @dark-notification-bg-color; + } + 50% { + background-color: @dark-external-flash-bg-color; + } +} diff --git a/src/renderer/styles/variables.less b/src/renderer/styles/variables.less new file mode 100644 index 00000000..d92107d8 --- /dev/null +++ b/src/renderer/styles/variables.less @@ -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-mention-bg-color: #f6e5a6; From 662a67b73859fafb0e1ac254c63bbe45e2427b86 Mon Sep 17 00:00:00 2001 From: sbenmoussati <51402489+sbenmoussati@users.noreply.github.com> Date: Tue, 3 Aug 2021 11:15:12 +0200 Subject: [PATCH 02/11] SDA-3268 Ext room name length fix --- src/renderer/components/notification-comp.tsx | 22 ++++++++++++++-- src/renderer/styles/notification-comp.less | 25 +++++++++---------- 2 files changed, 32 insertions(+), 15 deletions(-) diff --git a/src/renderer/components/notification-comp.tsx b/src/renderer/components/notification-comp.tsx index 12caecab..f20f6a69 100644 --- a/src/renderer/components/notification-comp.tsx +++ b/src/renderer/components/notification-comp.tsx @@ -58,6 +58,8 @@ type keyboardEvent = React.KeyboardEvent; // 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< {}, @@ -466,10 +468,12 @@ export default class NotificationComp extends React.Component< currentColors.notificationBorderColor; } else { // in case of regular message without mention - currentColors.notificationBackgroundColor = color + // 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 = color + currentColors.notificationBorderColor = this.isCustomColor(color) ? color : theme === Themes.DARK ? '#2996fd' @@ -514,6 +518,7 @@ export default class NotificationComp extends React.Component< 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 { @@ -530,4 +535,17 @@ export default class NotificationComp extends React.Component< } 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; + } } diff --git a/src/renderer/styles/notification-comp.less b/src/renderer/styles/notification-comp.less index 91bc82f9..1c596c31 100644 --- a/src/renderer/styles/notification-comp.less +++ b/src/renderer/styles/notification-comp.less @@ -184,6 +184,7 @@ body { height: 16px; width: 26px; margin-left: 8px; + padding-top: 1px; align-items: center; } @@ -269,20 +270,18 @@ body { border: none; color: var(--text-color); } - - .rte-send-button { - width: 25px; - height: 25px; - align-self: center; - padding: 3px; - background: url('../assets/notification-send-button-enabled.svg') - no-repeat center; - border: none; - color: var(--text-color); - } } } - +.rte-send-button { + width: 25px; + height: 25px; + align-self: center; + padding: 3px; + background: url('../assets/notification-send-button-enabled.svg') no-repeat + center; + border: none; + color: var(--text-color); +} .rte-thumbsup-button:focus, .rte-send-button:focus { outline: none; @@ -298,7 +297,7 @@ body { } .external-border .title { - max-width: 142px; + max-width: 142px !important; } .company { From 019f530044dfe91bf9d2981d79f44b8a6e57b052 Mon Sep 17 00:00:00 2001 From: sbenmoussati <51402489+sbenmoussati@users.noreply.github.com> Date: Wed, 4 Aug 2021 17:43:51 +0200 Subject: [PATCH 03/11] SDA-3237 Additional colors use-cases --- src/renderer/components/notification-comp.tsx | 79 ++++++++++++++++++- src/renderer/styles/notification-comp.less | 3 +- .../styles/notifications-animations.less | 12 +-- src/renderer/styles/variables.less | 2 +- 4 files changed, 82 insertions(+), 14 deletions(-) diff --git a/src/renderer/components/notification-comp.tsx b/src/renderer/components/notification-comp.tsx index f20f6a69..c856771f 100644 --- a/src/renderer/components/notification-comp.tsx +++ b/src/renderer/components/notification-comp.tsx @@ -23,11 +23,15 @@ const Colors = { regularFlashingNotificationBgColor: '#27588e', notificationBackgroundColor: '#27292c', notificationBorderColor: '#717681', + mentionBackgroundColor: '#99342c', + mentionBorderColor: '#ff5d50', }, light: { regularFlashingNotificationBgColor: '#aad4f8', notificationBackgroundColor: '#f1f1f3', notificationBorderColor: 'transparent', + mentionBackgroundColor: '#fcc1b9', + mentionBorderColor: 'transparent', }, }; @@ -460,9 +464,23 @@ export default class NotificationComp extends React.Component< 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) { - currentColors.notificationBorderColor = '#F7CA3B'; + 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; @@ -474,13 +492,26 @@ export default class NotificationComp extends React.Component< ? color : currentColors.regularFlashingNotificationBgColor; currentColors.notificationBorderColor = this.isCustomColor(color) - ? color + ? this.getThemedCustomBorderColor(theme, color) : theme === Themes.DARK ? '#2996fd' : 'transparent'; } - } else if (!flash && color) { - currentColors.notificationBackgroundColor = currentColors.notificationBorderColor = color; + } 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; } @@ -548,4 +579,44 @@ export default class NotificationComp extends React.Component< } 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'; + } } diff --git a/src/renderer/styles/notification-comp.less b/src/renderer/styles/notification-comp.less index 1c596c31..cb129828 100644 --- a/src/renderer/styles/notification-comp.less +++ b/src/renderer/styles/notification-comp.less @@ -143,13 +143,14 @@ body { cursor: default; text-overflow: ellipsis; color: var(--text-color); + white-space: pre-line; } } } } .external-border { - border: 2px solid #f7ca3b !important; + border: 2px solid #f7ca3b; } .header:lang(fr-FR) { diff --git a/src/renderer/styles/notifications-animations.less b/src/renderer/styles/notifications-animations.less index a4807306..d38045d1 100644 --- a/src/renderer/styles/notifications-animations.less +++ b/src/renderer/styles/notifications-animations.less @@ -64,19 +64,15 @@ } @keyframes light-ext-flashing { - 0% { - background-color: @light-notification-bg-color; - } 50% { - background-color: @light-external-flash-mention-bg-color; + background-color: @light-notification-bg-color; + border-color: @light-external-flash-border-color; } } @keyframes dark-ext-flashing { - 0% { - background-color: @dark-notification-bg-color; - } 50% { - background-color: @dark-external-flash-bg-color; + background-color: @dark-notification-bg-color; + border-color: @dark-external-flash-border-color; } } diff --git a/src/renderer/styles/variables.less b/src/renderer/styles/variables.less index d92107d8..9d05fb02 100644 --- a/src/renderer/styles/variables.less +++ b/src/renderer/styles/variables.less @@ -14,4 +14,4 @@ @light-mention-flash-mention-bg-color: #fcc1b9; @light-mention-flash-border-color: #ff5d50; @light-external-flash-border-color: #f7ca3b; -@light-external-flash-mention-bg-color: #f6e5a6; +@light-external-flash-bg-color: #f6e5a6; From fbcae64c745d9b2488e7ad65260c7d20d37dace2 Mon Sep 17 00:00:00 2001 From: sbenmoussati <51402489+sbenmoussati@users.noreply.github.com> Date: Thu, 5 Aug 2021 20:02:16 +0200 Subject: [PATCH 04/11] Backward compatibility fix with SDA 9.2.0 and client 1.5 --- src/renderer/components/notification-comp.tsx | 20 ++++++++++++++++--- src/renderer/styles/notification-comp.less | 1 - 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/src/renderer/components/notification-comp.tsx b/src/renderer/components/notification-comp.tsx index c856771f..548a1e64 100644 --- a/src/renderer/components/notification-comp.tsx +++ b/src/renderer/components/notification-comp.tsx @@ -169,7 +169,7 @@ export default class NotificationComp extends React.Component< title={i18n.t('Close')()} onClick={this.eventHandlers.onClose(id)} > - close + close
Date: Fri, 6 Aug 2021 10:37:42 +0200 Subject: [PATCH 05/11] SDA-3267 Close button bugfix --- src/renderer/components/notification-comp.tsx | 14 ++++++++++---- src/renderer/styles/notification-comp.less | 1 + 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/renderer/components/notification-comp.tsx b/src/renderer/components/notification-comp.tsx index 548a1e64..3e6fc744 100644 --- a/src/renderer/components/notification-comp.tsx +++ b/src/renderer/components/notification-comp.tsx @@ -70,7 +70,8 @@ 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) => @@ -167,9 +168,13 @@ export default class NotificationComp extends React.Component<
- close + close
.close-button { display: block; + z-index: 5; } .main-container { From a562bb053cd49ddc529ce10ed2c19a68fb2b7ff7 Mon Sep 17 00:00:00 2001 From: Johan Kwarnmark Date: Fri, 6 Aug 2021 11:43:32 +0200 Subject: [PATCH 06/11] sda-3192 option+command+3 use bff-daily --- src/app/version-handler.ts | 6 +++--- src/app/window-handler.ts | 12 ++++-------- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/src/app/version-handler.ts b/src/app/version-handler.ts index c8cab8ba..be002e5c 100644 --- a/src/app/version-handler.ts +++ b/src/app/version-handler.ts @@ -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 { diff --git a/src/app/window-handler.ts b/src/app/window-handler.ts index 359e7621..f8b75389 100644 --- a/src/app/window-handler.ts +++ b/src/app/window-handler.ts @@ -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}`; From 3c41cf8e50f3a39039060709174acd9b3ad9c872 Mon Sep 17 00:00:00 2001 From: sbenmoussati <51402489+sbenmoussati@users.noreply.github.com> Date: Mon, 9 Aug 2021 16:25:41 +0200 Subject: [PATCH 07/11] SDA-3281 Client 1.5 not sending user picture in Settings > Alerts > See sample --- spec/notificationComp.spec.ts | 10 ++++++++++ src/renderer/components/notification-comp.tsx | 6 +++--- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/spec/notificationComp.spec.ts b/spec/notificationComp.spec.ts index 4329d31b..7f13127e 100644 --- a/spec/notificationComp.spec.ts +++ b/spec/notificationComp.spec.ts @@ -34,6 +34,16 @@ describe('Toast notification component', () => { 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, { diff --git a/src/renderer/components/notification-comp.tsx b/src/renderer/components/notification-comp.tsx index 3e6fc744..d7f077e5 100644 --- a/src/renderer/components/notification-comp.tsx +++ b/src/renderer/components/notification-comp.tsx @@ -41,7 +41,7 @@ interface INotificationState { company: string; body: string; image: string; - icon: string; + icon: string | undefined; id: number; color: string; flash: boolean; @@ -263,11 +263,11 @@ export default class NotificationComp extends React.Component< * Renders image if provided otherwise renders symphony logo * @param imageUrl */ - private renderImage(imageUrl: string): JSX.Element | undefined { + 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.includes('default.png'); + const isDefaultUrl = imageUrl && imageUrl.includes('default.png'); const shouldDisplayBadge = !!imageUrl && !isDefaultUrl; if (imageUrl && !isDefaultUrl) { imgClass = 'profile-picture'; From 1f7eb92d73cd3c5a5715181e60f4339947307b43 Mon Sep 17 00:00:00 2001 From: sbenmoussati <51402489+sbenmoussati@users.noreply.github.com> Date: Mon, 9 Aug 2021 09:39:15 +0200 Subject: [PATCH 08/11] SDA-3237 Notification border color fix in light mode --- src/renderer/styles/notifications-animations.less | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/renderer/styles/notifications-animations.less b/src/renderer/styles/notifications-animations.less index d38045d1..a91336ac 100644 --- a/src/renderer/styles/notifications-animations.less +++ b/src/renderer/styles/notifications-animations.less @@ -21,7 +21,7 @@ } 50% { background-color: @light-mention-flash-mention-bg-color; - border-color: @light-mention-flash-border-color; + border-color: transparent; } } From 9bd96a4cdaa65b890be27d3da9fb07c53bb1290a Mon Sep 17 00:00:00 2001 From: sbenmoussati <51402489+sbenmoussati@users.noreply.github.com> Date: Fri, 13 Aug 2021 14:22:52 +0200 Subject: [PATCH 09/11] SDA-3292 Close button hovering not taken into account to prevent notification close --- spec/notificationComp.spec.ts | 10 ++++++++++ src/renderer/components/notification-comp.tsx | 5 +++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/spec/notificationComp.spec.ts b/spec/notificationComp.spec.ts index 7f13127e..240c3039 100644 --- a/spec/notificationComp.spec.ts +++ b/spec/notificationComp.spec.ts @@ -128,4 +128,14 @@ describe('Toast notification component', () => { 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); + }); }); diff --git a/src/renderer/components/notification-comp.tsx b/src/renderer/components/notification-comp.tsx index d7f077e5..2f846be7 100644 --- a/src/renderer/components/notification-comp.tsx +++ b/src/renderer/components/notification-comp.tsx @@ -157,6 +157,7 @@ export default class NotificationComp extends React.Component< containerCssClass += customCssClasses.join(' '); return (
{this.renderImage(icon)}
From a0e5bdeea0e6a64c551f88119694764255665071 Mon Sep 17 00:00:00 2001 From: Johan Kwarnmark Date: Thu, 26 Aug 2021 10:26:29 +0200 Subject: [PATCH 10/11] Correct documentation --- installer/win/install_instructions_win.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/installer/win/install_instructions_win.md b/installer/win/install_instructions_win.md index 6dcfe19f..3a6b6129 100644 --- a/installer/win/install_instructions_win.md +++ b/installer/win/install_instructions_win.md @@ -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) From dbd4e59cbc72696f682faaf8f4658912c1c36783 Mon Sep 17 00:00:00 2001 From: Johan Kwarnmark Date: Tue, 31 Aug 2021 13:35:10 +0200 Subject: [PATCH 11/11] Clean up after new locale is set --- src/app/window-utils.ts | 4 ++++ src/renderer/notification.ts | 38 ++++++++++++++++++------------------ 2 files changed, 23 insertions(+), 19 deletions(-) diff --git a/src/app/window-utils.ts b/src/app/window-utils.ts index ec7ab5b3..080f3e1c 100644 --- a/src/app/window-utils.ts +++ b/src/app/window-utils.ts @@ -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 => { 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 }); diff --git a/src/renderer/notification.ts b/src/renderer/notification.ts index 73f48e08..915a403b 100644 --- a/src/renderer/notification.ts +++ b/src/renderer/notification.ts @@ -427,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 @@ -508,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 *