diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md new file mode 100644 index 00000000..0744bcba --- /dev/null +++ b/RELEASE_NOTES.md @@ -0,0 +1,64 @@ +# Release notes + + +------------------------------------------------------------------- +## SDA 9.2.x + +### 9.2.0 + +1. [SDA-2402 CLIENT Annotate screen capture - PHASE 1](https://perzoinc.atlassian.net/browse/SDA-2402) +2. [SDA-2404 CLIENT Toast Action: Phase 1](https://perzoinc.atlassian.net/browse/SDA-2404) +3. [SDA-2385 CORE Windows Wix installer](https://perzoinc.atlassian.net/browse/SDA-2385) + +* 9.3.5 (Electron-version) +* 83 (Chromium-version) + +------------------------------------------------------------------- +## SDA 9.1.x + +### SDA 9.1.4 + +1. [SDA-2694 Point72 blank screen windows-only](https://perzoinc.atlassian.net/browse/SDA-2694) +2. [SDA-2731 Blank screen after upgrade (fresh installation) windows-only](https://perzoinc.atlassian.net/browse/SDA-2731) + +* 9.3.5 (Electron-version) +* 83 (Chromium-version) + +### SDA 9.1.3 + +1. [SDA-2559 CORE Remove Cache on New Install](https://perzoinc.atlassian.net/browse/SDA-2559) +2. [SDA-2359 CORE Instinet Prod - SDA blank screen](https://perzoinc.atlassian.net/browse/SDA-2359) +3. [SDA-2524 CORE Unpreventable top-level navigation](https://perzoinc.atlassian.net/browse/SDA-2524) +4. [SDA-2525 CORE Context isolation bypass via prevented window.open](https://perzoinc.atlassian.net/browse/SDA-2525) + +* 9.3.2 (Electron-version) +* 83 (Chromium-version) + +### SDA 9.1.2 + +1. [SDA-2417 Add E2EE encryption flag requested by RTC part2](https://perzoinc.atlassian.net/browse/SDA-2417) +2. [SDA-2418 CORE Barclays - SDA 6.x - 9.x takes noticeably longer to start than 3.x](https://perzoinc.atlassian.net/browse/SDA-2418) +3. [SDA-2406 CORE GS Prod: Mac SDA 6.1 polling for python3, pip3 and other packages](https://perzoinc.atlassian.net/browse/SDA-2406) +4. [SDA-2465 CORE GS - SDA/RTC - Exception when started Screen Share](https://perzoinc.atlassian.net/browse/SDA-2465) + +* 9.2.1 (Electron-version) +* 83 (Chromium-version) + +------------------------------------------------------------------- +## SDA 9.0.x + +### SDA 9.0.3 + +1. [SDA-2287 Context Isolation: Missing Hamburger menu when setting contextIsolation = false](https://perzoinc.atlassian.net/browse/SDA-2287) +2. [SDA-2285 Memory Refresh: Update electron API to support memory refresh in 9.x version](https://perzoinc.atlassian.net/browse/SDA-2285) +3. [RTC-8098 RTC/SDA: "Meeting failed" to many meetings from SDA 9.0.x](https://perzoinc.atlassian.net/browse/RTC-8098) +4. [SDA-2229 [WFC] Unable to login to Symphony on SDA but able to login using Chrome on a macOS](https://perzoinc.atlassian.net/browse/SDA-2229) + +* 9.2.1 (Electron-version) +* 83 (Chromium-version) + + +### SDA 9.0.0 + +* 9.0.2 (Electron-version) +* 83 (Chromium-version) diff --git a/docs/features/assets/custom_notifications.gif b/docs/features/assets/custom_notifications.gif new file mode 100644 index 00000000..cf19c2a2 Binary files /dev/null and b/docs/features/assets/custom_notifications.gif differ diff --git a/docs/features/assets/notifications_mac.png b/docs/features/assets/notifications_mac.png index 6fb4a4ac..7386621c 100644 Binary files a/docs/features/assets/notifications_mac.png and b/docs/features/assets/notifications_mac.png differ diff --git a/docs/features/assets/notifications_mac_2.png b/docs/features/assets/notifications_mac_2.png new file mode 100644 index 00000000..4eb0779d Binary files /dev/null and b/docs/features/assets/notifications_mac_2.png differ diff --git a/docs/features/notifications.md b/docs/features/notifications.md index 508258b9..fdcdb756 100644 --- a/docs/features/notifications.md +++ b/docs/features/notifications.md @@ -17,23 +17,26 @@ We support the following set of notifications along with badge / taskbar count o - Wall Posts - Signals -## macOS -macOS notifications are native chrome notifications which appear like any other desktop app notifications on a Mac. -Emojis are supported in the toast. -![notifications_mac.png](assets/notifications_mac.png) +## OS Native notification +We support OS native notifications for users who'd like to keep it simple. This is the same as what you see with toasts in any other apps (on Windows and macOS). Emojis and Quick Replies are supported in the notification. -## Windows -Windows notifications are custom built to support the following use cases: +- Inline reply + +![notifications_mac.png](assets/notifications_mac.png) +![notifications_mac_2.png](assets/notifications_mac_2.png) + +## Custom SDA Notification (Windows, macOS, Linux) +Notifications are custom built to support the following use cases: - Custom Color - Position in the screen - Custom screen (in case of multiple displays connected) -- Flash a notification +- Flashing a notification to call for attention +- Inline reply +- Quick reactions (👍) +- Emojis ![Notification_screen.png](assets/Notification_screen.png) -![Top Right Notification](assets/top_right_notification.png) -![Bottom Right Notification](assets/bottom_right_notification.png) - -On Windows 7 & 10, emojis are supported in the toast. +![New Custom Notification](assets/custom_notifications.gif) # Example N/A diff --git a/scripts/build-mac.sh b/scripts/build-mac.sh index e4202ebf..cf8f37fa 100755 --- a/scripts/build-mac.sh +++ b/scripts/build-mac.sh @@ -153,6 +153,9 @@ fi echo "Generating PDF for installation instructions" markdown-pdf installer/mac/install_instructions_mac.md +echo "Generate release notes" +markdown-pdf RELEASE_NOTES.md + # Create targets directory mkdir -p targets @@ -160,9 +163,11 @@ mkdir -p targets if [ "${EXPIRY_PERIOD}" != "0" ]; then cp $SIGNED_PACKAGE "targets/Symphony-macOS-${PKG_VERSION}-${PARENT_BUILD_VERSION}-TTL-${EXPIRY_PERIOD}.pkg" cp installer/mac/install_instructions_mac.pdf "targets/Install-Instructions-macOS-${PKG_VERSION}-${PARENT_BUILD_VERSION}-TTL-${EXPIRY_PERIOD}.pdf" + cp RELEASE_NOTES.pdf "targets/Release-Notes-macOS-${PKG_VERSION}-${PARENT_BUILD_VERSION}-TTL-${EXPIRY_PERIOD}.pdf" else cp $SIGNED_PACKAGE "targets/Symphony-macOS-${PKG_VERSION}-${PARENT_BUILD_VERSION}.pkg" cp installer/mac/install_instructions_mac.pdf "targets/Install-Instructions-macOS-${PKG_VERSION}-${PARENT_BUILD_VERSION}.pdf" + cp RELEASE_NOTES.pdf "targets/Release-Notes-macOS-${PKG_VERSION}-${PARENT_BUILD_VERSION}.pdf" fi echo "All done, job successfull :)" diff --git a/scripts/build-win64.bat b/scripts/build-win64.bat index 97acc3e2..957a0fbb 100644 --- a/scripts/build-win64.bat +++ b/scripts/build-win64.bat @@ -106,6 +106,7 @@ IF "%EXPIRY_PERIOD%"=="0" ( set installerDir="%CD%\installer\win" set distDir="%CD%\dist" +set rootDir="%CD%" if NOT EXIST "%PFX_DIR%\%PFX_FILE%" ( echo "can not find .pfx file" "%pfxDir%\%pfxFile%" @@ -171,4 +172,9 @@ echo "Generating installation instructions" call %appdata%\npm\markdown-pdf install_instructions_win.md copy install_instructions_win.pdf "%targetsDir%\Install-Instructions-%archiveName%.pdf" +echo Generate release notes +cd %rootDir% +call %appdata%\npm\markdown-pdf RELEASE_NOTES.md +copy RELEASE_NOTES.pdf "%targetsDir%\Release-Notes-%archiveName%.pdf" + echo "All done, job successfull :)" diff --git a/src/common/api-interface.ts b/src/common/api-interface.ts index c2340d6c..145fe032 100644 --- a/src/common/api-interface.ts +++ b/src/common/api-interface.ts @@ -140,6 +140,7 @@ export interface INotificationData { theme: Theme; isElectronNotification?: boolean; callback?: () => void; + hasReply?: boolean; } export enum NotificationActions { diff --git a/src/demo/index.html b/src/demo/index.html index 0ff95062..b676c303 100644 --- a/src/demo/index.html +++ b/src/demo/index.html @@ -339,7 +339,9 @@ ssfNotificationHandler.addEventListener('error', onerror); } else if (window.manaSSF) { const callback = (event, data) => { - document.getElementById('reply').innerText = data.notificationData; + if (event === 'notification-reply') { + document.getElementById('reply').innerText = data.notificationData; + } }; window.manaSSF.showNotification(notf, callback); } else { diff --git a/src/locale/en-US.json b/src/locale/en-US.json index 96a46f8a..e4f231b7 100644 --- a/src/locale/en-US.json +++ b/src/locale/en-US.json @@ -131,6 +131,7 @@ "Redo": "Redo", "Refresh app when idle": "Refresh app when idle", "Relaunch": "Relaunch", + "Reply": "Reply", "Restart": "Restart", "Relaunch Application": "Relaunch Application", "Reload": "Reload", @@ -162,6 +163,7 @@ "Clear": "Clear" }, "Select All": "Select All", + "Send": "Send", "Services": "Services", "Show All": "Show All", "Show crash dump in Explorer": "Show crash dump in Explorer", diff --git a/src/locale/en.json b/src/locale/en.json index 96a46f8a..e4f231b7 100644 --- a/src/locale/en.json +++ b/src/locale/en.json @@ -131,6 +131,7 @@ "Redo": "Redo", "Refresh app when idle": "Refresh app when idle", "Relaunch": "Relaunch", + "Reply": "Reply", "Restart": "Restart", "Relaunch Application": "Relaunch Application", "Reload": "Reload", @@ -162,6 +163,7 @@ "Clear": "Clear" }, "Select All": "Select All", + "Send": "Send", "Services": "Services", "Show All": "Show All", "Show crash dump in Explorer": "Show crash dump in Explorer", diff --git a/src/locale/fr-FR.json b/src/locale/fr-FR.json index c41c5814..9316fd34 100644 --- a/src/locale/fr-FR.json +++ b/src/locale/fr-FR.json @@ -131,6 +131,7 @@ "Redo": "Répéter la dernière opération", "Refresh app when idle": "Rafraîchir Symphony pendant les périodes d'inactivité", "Relaunch": "Redémarrer", + "Reply": "Répondre", "Restart": "Redémarrer", "Relaunch Application": "Redémarrer l'application", "Reload": "Recharger", @@ -162,6 +163,7 @@ "Clear": "Effacer" }, "Select All": "Tout sélectionner", + "Send": "Envoyer", "Services": "Services", "Show All": "Tout afficher", "Show crash dump in Explorer": "Afficher rapport de crash dans Explorateur", diff --git a/src/locale/fr.json b/src/locale/fr.json index c41c5814..9316fd34 100644 --- a/src/locale/fr.json +++ b/src/locale/fr.json @@ -131,6 +131,7 @@ "Redo": "Répéter la dernière opération", "Refresh app when idle": "Rafraîchir Symphony pendant les périodes d'inactivité", "Relaunch": "Redémarrer", + "Reply": "Répondre", "Restart": "Redémarrer", "Relaunch Application": "Redémarrer l'application", "Reload": "Recharger", @@ -162,6 +163,7 @@ "Clear": "Effacer" }, "Select All": "Tout sélectionner", + "Send": "Envoyer", "Services": "Services", "Show All": "Tout afficher", "Show crash dump in Explorer": "Afficher rapport de crash dans Explorateur", diff --git a/src/locale/ja-JP.json b/src/locale/ja-JP.json index f937d76b..8409fde9 100644 --- a/src/locale/ja-JP.json +++ b/src/locale/ja-JP.json @@ -131,6 +131,7 @@ "Redo": "やり直し", "Refresh app when idle": "アイドル時にアプリを再表示", "Relaunch": "「リスタート」", + "Reply": "応答", "Restart": "再起動する", "Relaunch Application": "アプリケーションの再起動", "Reload": "再読み込み", @@ -162,6 +163,7 @@ "Clear": "消去" }, "Select All": "すべてを選択", + "Send": "送る", "Services": "サービス", "Show All": "すべてを表示", "Show crash dump in Explorer": "Explorerにクラッシュダンプを表示", diff --git a/src/locale/ja.json b/src/locale/ja.json index f937d76b..8409fde9 100644 --- a/src/locale/ja.json +++ b/src/locale/ja.json @@ -131,6 +131,7 @@ "Redo": "やり直し", "Refresh app when idle": "アイドル時にアプリを再表示", "Relaunch": "「リスタート」", + "Reply": "応答", "Restart": "再起動する", "Relaunch Application": "アプリケーションの再起動", "Reload": "再読み込み", @@ -162,6 +163,7 @@ "Clear": "消去" }, "Select All": "すべてを選択", + "Send": "送る", "Services": "サービス", "Show All": "すべてを表示", "Show crash dump in Explorer": "Explorerにクラッシュダンプを表示", diff --git a/src/renderer/assets/notification-ext-badge.svg b/src/renderer/assets/notification-ext-badge.svg new file mode 100644 index 00000000..99f03061 --- /dev/null +++ b/src/renderer/assets/notification-ext-badge.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/renderer/assets/notification-send-button-disabled.svg b/src/renderer/assets/notification-send-button-disabled.svg new file mode 100644 index 00000000..56712d21 --- /dev/null +++ b/src/renderer/assets/notification-send-button-disabled.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/renderer/assets/notification-send-button-enabled.svg b/src/renderer/assets/notification-send-button-enabled.svg new file mode 100644 index 00000000..4e2ea3ed --- /dev/null +++ b/src/renderer/assets/notification-send-button-enabled.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/renderer/assets/notification-symphony-logo.svg b/src/renderer/assets/notification-symphony-logo.svg new file mode 100644 index 00000000..816c1fa5 --- /dev/null +++ b/src/renderer/assets/notification-symphony-logo.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/src/renderer/assets/notification-thumbsup.svg b/src/renderer/assets/notification-thumbsup.svg new file mode 100644 index 00000000..80b458be --- /dev/null +++ b/src/renderer/assets/notification-thumbsup.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/src/renderer/components/notification-comp.tsx b/src/renderer/components/notification-comp.tsx index 0ca5d967..3fe09fb4 100644 --- a/src/renderer/components/notification-comp.tsx +++ b/src/renderer/components/notification-comp.tsx @@ -1,10 +1,11 @@ +import classNames from 'classnames'; import { ipcRenderer } from 'electron'; import * as React from 'react'; import { i18n } from '../../common/i18n-preload'; const whiteColorRegExp = new RegExp(/^(?:white|#fff(?:fff)?|rgba?\(\s*255\s*,\s*255\s*,\s*255\s*(?:,\s*1\s*)?\))$/i); -const darkTheme = ['#e23030', '#b5616a', '#ab8ead', '#ebc875', '#a3be77', '#58c6ff', '#ebab58']; +const darkTheme = [ '#e23030', '#b5616a', '#ab8ead', '#ebc875', '#a3be77', '#58c6ff', '#ebab58' ]; type Theme = '' | 'light' | 'dark'; interface IState { @@ -18,9 +19,18 @@ interface IState { flash: boolean; isExternal: boolean; theme: Theme; + hasReply: boolean; + isInputHidden: boolean; + containerHeight: number; + canSendMessage: boolean; } -type mouseEventButton = React.MouseEvent; +type mouseEventButton = React.MouseEvent | React.MouseEvent; +type keyboardEvent = React.KeyboardEvent; + +// Notification container height +const CONTAINER_HEIGHT = 64; +const CONTAINER_HEIGHT_WITH_INPUT = 104; export default class NotificationComp extends React.Component<{}, IState> { @@ -30,8 +40,15 @@ export default class NotificationComp extends React.Component<{}, IState> { onContextMenu: (event) => this.contextMenu(event), onMouseEnter: (winKey) => (_event: mouseEventButton) => this.onMouseEnter(winKey), onMouseLeave: (winKey) => (_event: mouseEventButton) => this.onMouseLeave(winKey), + onOpenReply: (winKey) => (event: mouseEventButton) => this.onOpenReply(event, winKey), + onThumbsUp: () => (_event: mouseEventButton) => this.onThumbsUp(), + onReply: (winKey) => (_event: mouseEventButton) => this.onReply(winKey), + onKeyUp: (winKey) => (event: keyboardEvent) => this.onKeyUp(event, winKey), }; private flashTimer: NodeJS.Timer | undefined; + private customInput: React.RefObject; + private inputCaret: React.RefObject; + private input: React.RefObject; constructor(props) { super(props); @@ -46,8 +63,19 @@ export default class NotificationComp extends React.Component<{}, IState> { flash: false, isExternal: false, theme: '', + isInputHidden: true, + hasReply: false, + containerHeight: CONTAINER_HEIGHT, + canSendMessage: false, }; this.updateState = this.updateState.bind(this); + this.setInputCaretPosition = this.setInputCaretPosition.bind(this); + this.resetNotificationData = this.resetNotificationData.bind(this); + this.getInputValue = this.getInputValue.bind(this); + + this.customInput = React.createRef(); + this.inputCaret = React.createRef(); + this.input = React.createRef(); } public componentDidMount(): void { @@ -63,7 +91,7 @@ export default class NotificationComp extends React.Component<{}, IState> { * Renders the custom title bar */ public render(): JSX.Element { - const { title, body, image, icon, id, color, isExternal, theme } = this.state; + const { title, body, id, color, isExternal, theme, isInputHidden, containerHeight, hasReply, canSendMessage } = this.state; let themeClassName; if (theme) { themeClassName = theme; @@ -74,50 +102,89 @@ export default class NotificationComp extends React.Component<{}, IState> { } const bgColor = { backgroundColor: color || '#ffffff' }; + const containerClass = classNames('container', { 'external-border': isExternal }); return ( -
- {isExternal ?
: null} -
-
- - - - - - - - - - - - - - - - +
+
+
+
+ Symphony logo +
+
+
+
+ {title} + {this.renderExtBadge(isExternal)} +
+ {body} +
+
+ +
-
-
- {title} - {this.renderExtBadge(isExternal)} +
+
+
+
+ +
+
+ this.animateCaret(true)} + onBlur={() => this.animateCaret(false)} + ref={this.input}/> +
+
+
- {body} -
- {this.renderProfile(icon, image, title)} -
- - - -
); @@ -133,35 +200,7 @@ export default class NotificationComp extends React.Component<{}, IState> { } return (
- - - - - - -
- ); - } - - /** - * Renders user profile image if present else use - * the first char of the notification title - * - * @param icon - * @param image - * @param title - */ - private renderProfile(icon: string, image: string, title: string): JSX.Element { - if (icon || image) { - return ( -
- user profile picture -
- ); - } - return ( -
- {title.substr(0, 1)} + ext-badge
); } @@ -210,7 +249,35 @@ export default class NotificationComp extends React.Component<{}, IState> { * @param id {number} */ private onMouseLeave(id: number): void { - ipcRenderer.send('notification-mouseleave', id); + const { isInputHidden } = this.state; + ipcRenderer.send('notification-mouseleave', id, isInputHidden); + } + + /** + * Insets a thumbs up emoji + * @private + */ + private onThumbsUp(): void { + if (this.input.current) { + const input = this.input.current.value; + this.input.current.value = input + '👍'; + this.setInputCaretPosition(); + this.input.current.focus(); + } + } + + /** + * Handles reply action + * @param id + * @private + */ + private onReply(id: number): void { + let replyText = this.getInputValue(); + if (replyText) { + // need to replace 👍 with :thumbsup: to make sure client displays the correct emoji + replyText = replyText.replace(/👍/g, replyText.length <= 2 ? ':thumbsup: ' : ':thumbsup:'); + ipcRenderer.send('notification-on-reply', id, replyText); + } } /** @@ -222,6 +289,83 @@ export default class NotificationComp extends React.Component<{}, IState> { } } + /** + * Displays an input on the notification + * + * @private + */ + private onOpenReply(event, id) { + event.stopPropagation(); + ipcRenderer.send('show-reply', id); + this.setState({ + isInputHidden: false, + hasReply: false, + containerHeight: CONTAINER_HEIGHT_WITH_INPUT, + }, () => { + this.input.current?.focus(); + }); + } + + /** + * Trim and returns the input value + * @private + */ + private getInputValue(): string | undefined { + return this.input.current?.value.trim(); + } + + /** + * Handles key up event and enter keyCode + * + * @param event + * @param id + * @private + */ + private onKeyUp(event, id) { + this.setInputCaretPosition(); + if (event.key === 'Enter' || event.keyCode === 13) { + this.onReply(id); + } + } + + /** + * Moves the custom input caret based on input text + * @private + */ + private setInputCaretPosition() { + if (this.customInput.current) { + if (this.input.current) { + const inputText = this.input.current.value || ''; + const selectionStart = this.input.current.selectionStart || 0; + this.customInput.current.innerText = inputText.substring(0, selectionStart).replace(/\n$/, '\n\u0001'); + this.setState({ + canSendMessage: inputText.trim().length > 0, + }); + } + + const rects = this.customInput.current.getClientRects(); + const lastRect = rects && rects[ rects.length - 1 ]; + + const x = lastRect && lastRect.width || 0; + if (this.inputCaret.current) { + this.inputCaret.current.style.left = x + 'px'; + } + } + } + + /** + * Adds blinking animation to input caret + * @param hasFocus + * @private + */ + private animateCaret(hasFocus: boolean) { + if (hasFocus) { + this.inputCaret.current?.classList.add('input-caret-focus'); + } else { + this.inputCaret.current?.classList.remove('input-caret-focus'); + } + } + /** * Sets the component state * @@ -231,7 +375,12 @@ export default class NotificationComp extends React.Component<{}, IState> { private updateState(_event, data): void { const { color, flash } = data; data.color = (color && !color.startsWith('#')) ? '#' + color : color; + data.isInputHidden = true; + data.containerHeight = CONTAINER_HEIGHT; + + this.resetNotificationData(); this.setState(data as IState); + if (this.flashTimer) { clearInterval(this.flashTimer); } @@ -247,4 +396,15 @@ export default class NotificationComp extends React.Component<{}, IState> { }, 1000); } } + + /** + * Reset data for new notification + * @private + */ + private resetNotificationData(): void { + if (this.input.current) { + this.input.current.value = ''; + } + this.setInputCaretPosition(); + } } diff --git a/src/renderer/notification.ts b/src/renderer/notification.ts index 9f82e411..be839788 100644 --- a/src/renderer/notification.ts +++ b/src/renderer/notification.ts @@ -51,7 +51,8 @@ class Notification extends NotificationHandler { onCleanUpInactiveNotification: () => this.cleanUpInactiveNotification(), onCreateNotificationWindow: (data: INotificationData) => this.createNotificationWindow(data), onMouseOver: (_event, windowId) => this.onMouseOver(windowId), - onMouseLeave: (_event, windowId) => this.onMouseLeave(windowId), + onMouseLeave: (_event, windowId, isInputHidden) => this.onMouseLeave(windowId, isInputHidden), + onShowReply: (_event, windowId) => this.onShowReply(windowId), }; private activeNotifications: Electron.BrowserWindow[] = []; private inactiveWindows: Electron.BrowserWindow[] = []; @@ -73,6 +74,10 @@ class Notification extends NotificationHandler { }); ipcMain.on('notification-mouseenter', this.funcHandlers.onMouseOver); ipcMain.on('notification-mouseleave', this.funcHandlers.onMouseLeave); + ipcMain.on('notification-on-reply', (_event, windowId, replyText) => { + this.onNotificationReply(windowId, replyText); + }); + ipcMain.on('show-reply', this.funcHandlers.onShowReply); // Update latest notification settings from config app.on('ready', () => this.updateNotificationSettings()); this.cleanUpTimer = setInterval(this.funcHandlers.onCleanUpInactiveNotification, CLEAN_UP_INTERVAL); @@ -182,6 +187,8 @@ class Notification extends NotificationHandler { if (notificationWindow.displayTimer) { clearTimeout(notificationWindow.displayTimer); } + // Reset notification window size to default + notificationWindow.setSize(notificationSettings.width, notificationSettings.height, true); // Move notification to top notificationWindow.moveTop(); @@ -203,6 +210,7 @@ class Notification extends NotificationHandler { flash, isExternal, theme, + hasReply, } = data; notificationWindow.webContents.send('notification-data', { @@ -216,6 +224,7 @@ class Notification extends NotificationHandler { flash, isExternal, theme, + hasReply, }); notificationWindow.showInactive(); } @@ -288,6 +297,24 @@ class Notification extends NotificationHandler { } } + /** + * Handles notification reply action which updates client + * @param clientId {number} + * @param replyText {string} + */ + public onNotificationReply(clientId: number, replyText: string): void { + const browserWindow = this.getNotificationWindow(clientId); + if (browserWindow && windowExists(browserWindow) && browserWindow.notificationData) { + const data = browserWindow.notificationData; + const callback = this.notificationCallbacks[ clientId ]; + if (typeof callback === 'function') { + callback(NotificationActions.notificationReply, data, replyText); + } + this.notificationClosed(clientId); + this.hideNotification(clientId); + } + } + /** * Returns the notification based on the client id * @@ -434,8 +461,9 @@ class Notification extends NotificationHandler { * Start a new timer to close the notification * * @param windowId + * @param isInputHidden {boolean} - whether the inline reply is hidden */ - private onMouseLeave(windowId: number): void { + private onMouseLeave(windowId: number, isInputHidden: boolean): void { const notificationWindow = this.getNotificationWindow(windowId); if (!notificationWindow || !windowExists(notificationWindow)) { return; @@ -445,6 +473,10 @@ class Notification extends NotificationHandler { return; } + if (!isInputHidden) { + return; + } + const displayTime = (notificationWindow.notificationData && notificationWindow.notificationData.displayTime) ? notificationWindow.notificationData.displayTime : notificationSettings.displayTime; @@ -455,6 +487,22 @@ class Notification extends NotificationHandler { } } + /** + * Increase the notification height to + * make space for reply input element + * + * @param windowId + * @private + */ + private onShowReply(windowId: number): void { + const notificationWindow = this.getNotificationWindow(windowId); + if (!notificationWindow || !windowExists(notificationWindow)) { + return; + } + clearTimeout(notificationWindow.displayTimer); + notificationWindow.setSize(344, 104, true); + } + /** * notification window opts */ diff --git a/src/renderer/styles/notification-comp.less b/src/renderer/styles/notification-comp.less index a0b2f7cb..becb9c6f 100644 --- a/src/renderer/styles/notification-comp.less +++ b/src/renderer/styles/notification-comp.less @@ -1,18 +1,33 @@ @import "theme"; +@inputWidth: 270px; .blackText { --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; + --button-test-color: #000000; --logo-bg: url('../assets/symphony-logo.png'); } .dark { --text-color: #FFFFFF; + --button-bg-color: #52575f; + --button-test-color: #FFFFFF; --logo-bg: url('../assets/symphony-logo.png'); } +.big { + --border-height: 98px; +} + +.small { + --border-height: 58px; +} + body { margin: 0; overflow: hidden; @@ -30,19 +45,34 @@ body { position: relative; line-height: 15px; box-sizing: border-box; - border-radius: 8px; + flex-direction: column; + border-radius: 10px; &:hover .close { visibility: visible; } } +.external-border { + border: 3px solid #f6b202; +} + +.main-container { + height: 64px; + display: flex; + justify-content: center; + background-color: #ffffff; + overflow: hidden; + position: relative; + line-height: 15px; +} + .ext-border { - border: 2px solid #f6b202; - border-radius: 8px; - width: 340px; - height: 60px; - position: absolute; + border: 3px solid #f6b202; + border-radius: 10px; + width: 338px; + height: var(--border-height); + position: fixed; } .header { @@ -96,11 +126,138 @@ body { margin: 12px; } -.close { +.actions-container { + display: flex; + flex-direction: column; + justify-content: center; + z-index: 5; +} + +.action-button { + border-radius: 16px; + padding: 2px 10px; + background: var(--button-bg-color); + color: var(--button-test-color); + flex: none; + font-weight: 600; + border-style: none; + font-size: 12px; + line-height: 16px; + 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; +} + +.action-button:hover { + background: #A5A8AC; +} + +.rte-container { + width: 100%; + display: flex; +} + +.input-container { + width: 100%; + outline: none; +} + +.input-border { + height: 2px; + left: 8px; + right: 8px; + top: 0; + background: #008EFF; + margin: 0 8px; +} + +.input-caret-container { + position: absolute; + left: 8px; + top: 60px; + opacity: 0; + border: 0; + padding: 0; + margin-bottom: 0; + background: transparent; + height: 38px; + width: @inputWidth; + outline: none; + color: transparent; + text-shadow: 0 0 0 black; + z-index: -3; + white-space: pre; + margin-left: 8px; + display: inline-block; + max-width: 330px; +} + +.custom-input { + font-size: 14px; + display: inline-block; + max-width: @inputWidth; +} + +.input-caret { + position: absolute; + width: 2px; + height: 20px; + border-radius: 1px; + margin: 8px 8px; + background: transparent; +} + +.input-caret-focus { + -webkit-animation: 1s blink step-end infinite; +} + +input { + width: @inputWidth; + height: 38px; + border: none; + outline: none; + margin-left: 8px; + font-size: 14px; + caret-color: transparent; + color: var(--text-color); +} + +.rte-button-container { + display: flex; position: absolute; right: 0; - visibility: hidden; - padding: 0 5.5px 5.5px 5.5px; + bottom: 0; + padding: 7px 13px; +} + +.rte-thumbsup-button { + width: 25px; + height: 25px; + align-self: center; + padding: 3px; + background: url('../assets/notification-thumbsup.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-send-button:disabled { + background: url('../assets/notification-send-button-disabled.svg') no-repeat center; } .title { @@ -157,3 +314,12 @@ body { width: 40px; content: var(--logo-bg); } + +@-webkit-keyframes blink { + from, to { + background: transparent; + } + 50% { + background: #008EFF; + } +} \ No newline at end of file