SDA-3237 Notifications re-design

This commit is contained in:
sbenmoussati
2021-07-16 17:26:13 +02:00
parent dcf104d748
commit 49c436f60e
16 changed files with 1769 additions and 1247 deletions

View File

@@ -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'));

View File

@@ -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();
});
});

View File

@@ -150,6 +150,7 @@ export interface INotificationData {
isElectronNotification?: boolean;
callback?: () => void;
hasReply?: boolean;
hasMention?: boolean;
}
export enum NotificationActions {

File diff suppressed because it is too large Load Diff

View 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

View 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

View 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

View 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

View File

@@ -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<HTMLInputElement>;
// 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<HTMLInputElement>;
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 (
<div
className={containerClass}
className={containerCssClass}
style={{
height: containerHeight,
backgroundColor: bgColor.backgroundColor,
backgroundColor: themeColors.notificationBackgroundColor,
borderColor: themeColors.notificationBorderColor,
}}
lang={i18n.getLocale()}
>
<div
className={`close-button ${themeClassName}`}
title={i18n.t('Close')()}
onClick={this.eventHandlers.onClose(id)}
>
<img src={closeImgFilePath} alt='close' />
</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 +226,9 @@ export default class NotificationComp extends React.Component<{}, IState> {
/>
</div>
</div>
</div>
);
);
}
return;
}
/**
@@ -242,6 +248,45 @@ export default class NotificationComp extends React.Component<{}, IState> {
</div>
);
}
/**
* 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 (
<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 +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 (
<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) {
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;
}
}

View File

@@ -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;

View File

@@ -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,

View File

@@ -89,6 +89,7 @@ if (ssfWindow.ssf) {
closeAllWrapperWindows: ssfWindow.ssf.closeAllWrapperWindows,
setZoomLevel: ssfWindow.ssf.setZoomLevel,
getZoomLevel: ssfWindow.ssf.getZoomLevel,
supportedSettings: ssfWindow.ssf.supportedSettings,
});
}

View File

@@ -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 || [];
}
}
/**

View File

@@ -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;
}

View File

@@ -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;
}
}

View 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-mention-bg-color: #f6e5a6;