feat: SDA-2619 (implement reply feature for custom notification) (#1153)

* feat: SDA-2619 - implement reply feature for custom notification

* feat: SDA-2619 - Move SVG to file
This commit is contained in:
Kiran Niranjan 2020-12-16 17:08:18 +05:30 committed by GitHub
parent 9eca131a3c
commit e25f281ca2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 528 additions and 95 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 801 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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éponse",
"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",

View File

@ -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éponse",
"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",

View File

@ -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にクラッシュダンプを表示",

View File

@ -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にクラッシュダンプを表示",

View File

@ -0,0 +1,4 @@
<svg width="26" height="17" viewBox="0 0 26 17" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect y="1" width="26" height="16" rx="3" fill="#F8C43F"/>
<path d="M8.82129 13H4.49756V5.29785H8.64941V6.38281H5.77588V8.56348H8.42383V9.64307H5.77588V11.9204H8.82129V13ZM16.0776 13H14.5469L12.9678 10.1748C12.9212 10.0889 12.8693 9.96712 12.812 9.80957H12.7905C12.7583 9.88835 12.7046 10.0101 12.6294 10.1748L11.002 13H9.46045L11.9849 9.12744L9.66455 5.29785H11.2275L12.6187 7.89746C12.7082 8.06934 12.7887 8.24121 12.8604 8.41309H12.8765C12.9803 8.1875 13.0698 8.00846 13.145 7.87598L14.5898 5.29785H16.0293L13.6553 9.1167L16.0776 13ZM22.2759 6.38281H20.063V13H18.7847V6.38281H16.5771V5.29785H22.2759V6.38281Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 731 B

View File

@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M15.952 1.30603C16.0685 0.943628 15.9692 0.546516 15.6957 0.281687C15.4223 0.016859 15.0222 -0.0697491 14.6637 0.0582825L0.663674 5.05825C0.286419 5.19299 0.0259746 5.53985 0.00182759 5.93972C-0.0223194 6.33958 0.194491 6.71527 0.552792 6.89442L6.28634 9.76122L9.63623 15.5039C9.83512 15.8448 10.2159 16.0369 10.6083 15.9941C11.0007 15.9514 11.3313 15.6818 11.452 15.306L15.952 1.30603ZM6.80256 7.78326L3.54321 6.15358L13.4278 2.62338L10.2341 12.5595L8.25222 9.16203L9.70712 7.70713C10.0976 7.31661 10.0976 6.68344 9.70712 6.29292C9.31659 5.90239 8.68343 5.90239 8.2929 6.29292L6.80256 7.78326Z" fill="#525760"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M15.952 1.30603C16.0685 0.943628 15.9692 0.546516 15.6957 0.281687C15.4223 0.016859 15.0222 -0.0697491 14.6637 0.0582825L0.663674 5.05825C0.286419 5.19299 0.0259746 5.53985 0.00182759 5.93972C-0.0223194 6.33958 0.194491 6.71527 0.552792 6.89442L6.28634 9.76122L9.63623 15.5039C9.83512 15.8448 10.2159 16.0369 10.6083 15.9941C11.0007 15.9514 11.3313 15.6818 11.452 15.306L15.952 1.30603ZM6.80256 7.78326L3.54321 6.15358L13.4278 2.62338L10.2341 12.5595L8.25222 9.16203L9.70712 7.70713C10.0976 7.31661 10.0976 6.68344 9.70712 6.29292C9.31659 5.90239 8.68343 5.90239 8.2929 6.29292L6.80256 7.78326Z" fill="white" fill-opacity="0.72"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

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 fill-rule="evenodd" clip-rule="evenodd" d="M15.952 1.30603C16.0685 0.943628 15.9692 0.546516 15.6957 0.281687C15.4223 0.016859 15.0222 -0.0697491 14.6637 0.0582825L0.663674 5.05825C0.286419 5.19299 0.0259746 5.53985 0.00182759 5.93972C-0.0223194 6.33958 0.194491 6.71527 0.552792 6.89442L6.28634 9.76122L9.63623 15.5039C9.83512 15.8448 10.2159 16.0369 10.6083 15.9941C11.0007 15.9514 11.3313 15.6818 11.452 15.306L15.952 1.30603ZM6.80256 7.78326L3.54321 6.15358L13.4278 2.62338L10.2341 12.5595L8.25222 9.16203L9.70712 7.70713C10.0976 7.31661 10.0976 6.68344 9.70712 6.29292C9.31659 5.90239 8.68343 5.90239 8.2929 6.29292L6.80256 7.78326Z" fill="#008EFF"/>
</svg>

After

Width:  |  Height:  |  Size: 769 B

View File

@ -0,0 +1,18 @@
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 20C0 12.1746 0 8.26188 1.80534 5.41094C2.72586 3.95728 3.95728 2.72586 5.41094 1.80534C8.26188 0 12.1746 0 20 0C27.8254 0 31.7381 0 34.5891 1.80534C36.0427 2.72586 37.2741 3.95728 38.1947 5.41094C40 8.26188 40 12.1746 40 20C40 27.8254 40 31.7381 38.1947 34.5891C37.2741 36.0427 36.0427 37.2741 34.5891 38.1947C31.7381 40 27.8254 40 20 40C12.1746 40 8.26188 40 5.41094 38.1947C3.95728 37.2741 2.72586 36.0427 1.80534 34.5891C0 31.7381 0 27.8254 0 20Z" fill="#000028"/>
<path d="M0 20C0 12.1746 0 8.26188 1.80534 5.41094C2.72586 3.95728 3.95728 2.72586 5.41094 1.80534C8.26188 0 12.1746 0 20 0C27.8254 0 31.7381 0 34.5891 1.80534C36.0427 2.72586 37.2741 3.95728 38.1947 5.41094C40 8.26188 40 12.1746 40 20C40 27.8254 40 31.7381 38.1947 34.5891C37.2741 36.0427 36.0427 37.2741 34.5891 38.1947C31.7381 40 27.8254 40 20 40C12.1746 40 8.26188 40 5.41094 38.1947C3.95728 37.2741 2.72586 36.0427 1.80534 34.5891C0 31.7381 0 27.8254 0 20Z" fill="url(#paint0_linear)"/>
<g opacity="0.8">
<path d="M29.028 15.7647V11.1598C29.028 10.2164 28.5297 9.33088 27.7219 8.84574C26.5177 8.12189 23.9094 6.90906 19.9685 6.90906C16.0276 6.90906 13.4193 8.12189 12.2151 8.84959C11.4073 9.33088 10.9091 10.2164 10.9091 11.1598V18.0748L25.2532 22.3101V25.3903C25.2532 25.8062 24.9965 26.0988 24.5586 26.3183L19.9685 28.6631L15.352 26.3067C14.9405 26.0988 14.6838 25.8062 14.6838 25.3903V23.0802L10.9091 21.9251V25.3903C10.9091 27.2924 11.9773 28.9095 13.676 29.7527L19.9685 33.0909L26.2346 29.7681C27.9597 28.9095 29.028 27.2924 29.028 25.3903V19.6149L14.6838 15.3796V11.8644C15.7106 11.36 17.4545 10.7593 19.9685 10.7593C22.4825 10.7593 24.2265 11.36 25.2532 11.8644V14.6096L29.028 15.7647Z" fill="#0098FF"/>
<path d="M29.028 15.7647V11.1598C29.028 10.2164 28.5297 9.33088 27.7219 8.84574C26.5177 8.12189 23.9094 6.90906 19.9685 6.90906C16.0276 6.90906 13.4193 8.12189 12.2151 8.84959C11.4073 9.33088 10.9091 10.2164 10.9091 11.1598V18.0748L25.2532 22.3101V25.3903C25.2532 25.8062 24.9965 26.0988 24.5586 26.3183L19.9685 28.6631L15.352 26.3067C14.9405 26.0988 14.6838 25.8062 14.6838 25.3903V23.0802L10.9091 21.9251V25.3903C10.9091 27.2924 11.9773 28.9095 13.676 29.7527L19.9685 33.0909L26.2346 29.7681C27.9597 28.9095 29.028 27.2924 29.028 25.3903V19.6149L14.6838 15.3796V11.8644C15.7106 11.36 17.4545 10.7593 19.9685 10.7593C22.4825 10.7593 24.2265 11.36 25.2532 11.8644V14.6096L29.028 15.7647Z" fill="url(#paint1_radial)"/>
</g>
<defs>
<linearGradient id="paint0_linear" x1="20" y1="0" x2="20" y2="40" 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(20 6.90906) rotate(90) scale(17.7273 22.8164)">
<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.1 KiB

View File

@ -0,0 +1,12 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.9499 6.77495C7.9499 6.77495 6.8499 6.99995 7.7499 5.12495C8.3999 3.77495 8.3249 2.19995 7.7499 1.37495C6.7999 0.0499523 4.9749 0.474952 5.1749 1.24995C5.8249 3.87495 4.3499 4.67495 3.5999 6.32495C2.8249 7.99995 2.8999 10.4 3.2499 12.525C3.4749 13.85 4.0499 15.5 6.1249 15.5H8.9999L7.9499 6.77495Z" fill="#FFDD67"/>
<path d="M6.45004 15.125C4.37504 15.125 3.92504 13.475 3.70004 12.15C3.35004 10.025 3.30004 8.32495 3.97504 6.59995C4.72504 4.72495 5.50004 4.67495 5.50004 0.974951C5.50004 0.799951 5.60004 0.674951 5.70004 0.574951C5.35004 0.699951 5.15004 0.899951 5.15004 1.19995C5.15004 3.97495 4.37504 4.64995 3.62504 6.32495C2.82504 7.99995 2.90004 10.4 3.25004 12.525C3.47504 13.85 4.05004 15.5 6.12504 15.5H9.00004V15.125H6.45004Z" fill="#EBA352"/>
<path d="M11.5 8.9499H7.94995C6.69995 8.9499 6.69995 6.7749 7.94995 6.7749H11.5C12.75 6.7749 12.75 8.9499 11.5 8.9499Z" fill="#FFDD67"/>
<path d="M11.775 8.6001H8.22495C7.37495 8.6001 7.12495 7.6001 7.39995 6.9751C6.72495 7.5001 6.92495 8.9751 7.92495 8.9751H11.5C11.9 8.9751 12.175 8.7501 12.325 8.4251C12.175 8.5251 12 8.6001 11.775 8.6001Z" fill="#EBA352"/>
<path d="M11.875 11.1501H7.625C6.125 11.1501 6.125 8.9751 7.625 8.9751H11.9C13.375 8.9751 13.375 11.1501 11.875 11.1501Z" fill="#FFDD67"/>
<path d="M12.225 10.7749H7.95004C6.95004 10.7749 6.62504 9.7749 6.97504 9.1499C6.17504 9.6749 6.40004 11.1499 7.62504 11.1499H11.9C12.375 11.1499 12.7 10.9249 12.875 10.5999C12.7 10.6999 12.475 10.7749 12.225 10.7749Z" fill="#EBA352"/>
<path d="M11.475 13.3249H7.875C6.625 13.3249 6.625 11.1499 7.875 11.1499H11.475C12.75 11.1499 12.75 13.3249 11.475 13.3249Z" fill="#FFDD67"/>
<path d="M11.775 12.95H8.15003C7.30003 12.95 7.02503 11.95 7.32503 11.3C6.65003 11.825 6.82503 13.3 7.87503 13.3H11.475C11.875 13.3 12.15 13.075 12.3 12.75C12.175 12.9 11.975 12.95 11.775 12.95Z" fill="#EBA352"/>
<path d="M11.1 15.5H8.77495C7.42495 15.5 7.42495 13.325 8.77495 13.325H11.1C12.45 13.325 12.45 15.5 11.1 15.5Z" fill="#FFDD67"/>
<path d="M11.4 15.15H9.07504C8.17504 15.15 7.87504 14.15 8.20004 13.5C7.47504 14.025 7.67504 15.5 8.77504 15.5H11.1C11.55 15.5 11.825 15.275 11.975 14.95C11.825 15.075 11.625 15.15 11.4 15.15Z" fill="#EBA352"/>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -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<HTMLDivElement>;
type mouseEventButton = React.MouseEvent<HTMLDivElement> | React.MouseEvent<HTMLButtonElement>;
type keyboardEvent = React.KeyboardEvent<HTMLInputElement>;
// 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<HTMLSpanElement>;
private inputCaret: React.RefObject<HTMLDivElement>;
private input: React.RefObject<HTMLInputElement>;
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 (
<div className='container'
role='alert'
style={bgColor}
onContextMenu={this.eventHandlers.onContextMenu}
onClick={this.eventHandlers.onClick(id)}
onMouseEnter={this.eventHandlers.onMouseEnter(id)}
onMouseLeave={this.eventHandlers.onMouseLeave(id)}
>
{isExternal ? <div className='ext-border' /> : null}
<div className='logo-container'>
<div className='logo'>
<svg width='40' height='40' viewBox='0 0 40 40' fill='none' xmlns='http://www.w3.org/2000/svg'>
<path d='M0 20C0 12.1746 0 8.26188 1.80534 5.41094C2.72586 3.95728 3.95728 2.72586 5.41094 1.80534C8.26188 0 12.1746 0 20 0C27.8254 0 31.7381 0 34.5891 1.80534C36.0427 2.72586 37.2741 3.95728 38.1947 5.41094C40 8.26188 40 12.1746 40 20C40 27.8254 40 31.7381 38.1947 34.5891C37.2741 36.0427 36.0427 37.2741 34.5891 38.1947C31.7381 40 27.8254 40 20 40C12.1746 40 8.26188 40 5.41094 38.1947C3.95728 37.2741 2.72586 36.0427 1.80534 34.5891C0 31.7381 0 27.8254 0 20Z' fill='#000028' />
<path d='M0 20C0 12.1746 0 8.26188 1.80534 5.41094C2.72586 3.95728 3.95728 2.72586 5.41094 1.80534C8.26188 0 12.1746 0 20 0C27.8254 0 31.7381 0 34.5891 1.80534C36.0427 2.72586 37.2741 3.95728 38.1947 5.41094C40 8.26188 40 12.1746 40 20C40 27.8254 40 31.7381 38.1947 34.5891C37.2741 36.0427 36.0427 37.2741 34.5891 38.1947C31.7381 40 27.8254 40 20 40C12.1746 40 8.26188 40 5.41094 38.1947C3.95728 37.2741 2.72586 36.0427 1.80534 34.5891C0 31.7381 0 27.8254 0 20Z' fill='url(#paint0_linear)' />
<path d='M28 17.1029V13.4094C28 12.6528 27.56 11.9425 26.8467 11.5534C25.7833 10.9728 23.48 10 20 10C16.52 10 14.2167 10.9728 13.1533 11.5565C12.44 11.9425 12 12.6528 12 13.4094V18.9559L24.6667 22.3529V24.8235C24.6667 25.1571 24.44 25.3918 24.0533 25.5678L20 27.4485L15.9233 25.5585C15.56 25.3918 15.3333 25.1571 15.3333 24.8235V22.9706L12 22.0441V24.8235C12 26.3491 12.9433 27.6462 14.4433 28.3225L20 31L25.5333 28.3349C27.0567 27.6462 28 26.3491 28 24.8235V20.1912L15.3333 16.7941V13.9746C16.24 13.57 17.78 13.0882 20 13.0882C22.22 13.0882 23.76 13.57 24.6667 13.9746V16.1765L28 17.1029Z' fill='#0098FF' />
<path d='M28 17.1029V13.4094C28 12.6528 27.56 11.9425 26.8467 11.5534C25.7833 10.9728 23.48 10 20 10C16.52 10 14.2167 10.9728 13.1533 11.5565C12.44 11.9425 12 12.6528 12 13.4094V18.9559L24.6667 22.3529V24.8235C24.6667 25.1571 24.44 25.3918 24.0533 25.5678L20 27.4485L15.9233 25.5585C15.56 25.3918 15.3333 25.1571 15.3333 24.8235V22.9706L12 22.0441V24.8235C12 26.3491 12.9433 27.6462 14.4433 28.3225L20 31L25.5333 28.3349C27.0567 27.6462 28 26.3491 28 24.8235V20.1912L15.3333 16.7941V13.9746C16.24 13.57 17.78 13.0882 20 13.0882C22.22 13.0882 23.76 13.57 24.6667 13.9746V16.1765L28 17.1029Z' fill='url(#paint1_radial)' />
<defs>
<linearGradient id='paint0_linear' x1='20' y1='0' x2='20' y2='40' gradientUnits='userSpaceOnUse'>
<stop stopColor='white' stopOpacity='0.2' />
<stop offset='1' stopColor='white' stopOpacity='0' />
</linearGradient>
<radialGradient id='paint1_radial' cx='0' cy='0' r='1' gradientUnits='userSpaceOnUse' gradientTransform='translate(20.0278 10) rotate(90) scale(14.2187 20.1481)'>
<stop stopColor='white' stopOpacity='0.4' />
<stop offset='1' stopColor='white' stopOpacity='0' />
</radialGradient>
</defs>
</svg>
<div className={containerClass} style={{ height: containerHeight }}>
<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>
</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>
</div>
</div>
<div className='header'>
<div className='title-container'>
<span className={`title ${themeClassName}`}>{title}</span>
{this.renderExtBadge(isExternal)}
<div style={{
...{ display: isInputHidden ? 'none' : 'flex' },
...bgColor,
}} className='rte-container'>
<div className='input-container'>
<div className='input-border'/>
<div className='input-caret-container'>
<span ref={this.customInput} className='custom-input'/>
</div>
<div ref={this.inputCaret} className='input-caret'/>
<input
style={bgColor}
className={themeClassName}
autoFocus={true}
onInput={this.setInputCaretPosition}
onKeyDown={this.setInputCaretPosition}
onKeyUp={this.eventHandlers.onKeyUp(id)}
onChange={this.setInputCaretPosition}
onClick={this.setInputCaretPosition}
onPaste={this.setInputCaretPosition}
onCut={this.setInputCaretPosition}
onCopy={this.setInputCaretPosition}
onMouseDown={this.setInputCaretPosition}
onMouseUp={this.setInputCaretPosition}
onFocus={() => this.animateCaret(true)}
onBlur={() => this.animateCaret(false)}
ref={this.input}/>
</div>
<div className='rte-button-container'>
<button
className={`rte-thumbsup-button ${themeClassName}`}
onClick={this.eventHandlers.onThumbsUp()}
/>
<button
className={`rte-send-button ${themeClassName}`}
onClick={this.eventHandlers.onReply(id)}
disabled={!canSendMessage}
title={i18n.t('Send')()}
/>
</div>
<span className={`message ${themeClassName}`}>{body}</span>
</div>
{this.renderProfile(icon, image, title)}
<div className='close' title={i18n.t('Close')()} onClick={this.eventHandlers.onClose(id)}>
<svg width='8' height='8' viewBox='0 0 8 8' fill='none' xmlns='http://www.w3.org/2000/svg'>
<path d='M1.35355 0.646447C1.15829 0.451184 0.841709 0.451184 0.646447 0.646447C0.451184 0.841709 0.451184 1.15829 0.646447 1.35355L3.29289 4L0.646447 6.64645C0.451185 6.84171 0.451185 7.15829 0.646447 7.35356C0.841709 7.54882 1.15829 7.54882 1.35355 7.35356L4 4.70711L6.64645 7.35355C6.84171 7.54882 7.15829 7.54882 7.35355 7.35355C7.54882 7.15829 7.54882 6.84171 7.35355 6.64645L4.70711 4L7.35355 1.35356C7.54882 1.15829 7.54882 0.84171 7.35355 0.646448C7.15829 0.451186 6.84171 0.451186 6.64645 0.646448L4 3.29289L1.35355 0.646447Z' fill='#525760' />
<path d='M1.35355 0.646447C1.15829 0.451184 0.841709 0.451184 0.646447 0.646447C0.451184 0.841709 0.451184 1.15829 0.646447 1.35355L3.29289 4L0.646447 6.64645C0.451185 6.84171 0.451185 7.15829 0.646447 7.35356C0.841709 7.54882 1.15829 7.54882 1.35355 7.35356L4 4.70711L6.64645 7.35355C6.84171 7.54882 7.15829 7.54882 7.35355 7.35355C7.54882 7.15829 7.54882 6.84171 7.35355 6.64645L4.70711 4L7.35355 1.35356C7.54882 1.15829 7.54882 0.84171 7.35355 0.646448C7.15829 0.451186 6.84171 0.451186 6.64645 0.646448L4 3.29289L1.35355 0.646447Z' fill='white' fill-opacity='0.96' />
</svg>
</div>
</div>
);
@ -133,35 +200,7 @@ export default class NotificationComp extends React.Component<{}, IState> {
}
return (
<div className='ext-badge-container'>
<svg width='32' height='16' viewBox='0 0 32 16' fill='none' xmlns='http://www.w3.org/2000/svg'>
<rect width='32' height='16' rx='8' fill='#F6B202' />
<rect width='32' height='16' rx='8' fill='white' fill-opacity='0.24' />
<path d='M11.4414 13H6.72461V4.59766H11.2539V5.78125H8.11914V8.16016H11.0078V9.33789H8.11914V11.8223H11.4414V13ZM19.3574 13H17.6875L15.9648 9.91797C15.9141 9.82422 15.8574 9.69141 15.7949 9.51953H15.7715C15.7363 9.60547 15.6777 9.73828 15.5957 9.91797L13.8203 13H12.1387L14.8926 8.77539L12.3613 4.59766H14.0664L15.584 7.43359C15.6816 7.62109 15.7695 7.80859 15.8477 7.99609H15.8652C15.9785 7.75 16.0762 7.55469 16.1582 7.41016L17.7344 4.59766H19.3047L16.7148 8.76367L19.3574 13ZM26.1191 5.78125H23.7051V13H22.3105V5.78125H19.9023V4.59766H26.1191V5.78125Z' fill='#525760' />
<path d='M11.4414 13H6.72461V4.59766H11.2539V5.78125H8.11914V8.16016H11.0078V9.33789H8.11914V11.8223H11.4414V13ZM19.3574 13H17.6875L15.9648 9.91797C15.9141 9.82422 15.8574 9.69141 15.7949 9.51953H15.7715C15.7363 9.60547 15.6777 9.73828 15.5957 9.91797L13.8203 13H12.1387L14.8926 8.77539L12.3613 4.59766H14.0664L15.584 7.43359C15.6816 7.62109 15.7695 7.80859 15.8477 7.99609H15.8652C15.9785 7.75 16.0762 7.55469 16.1582 7.41016L17.7344 4.59766H19.3047L16.7148 8.76367L19.3574 13ZM26.1191 5.78125H23.7051V13H22.3105V5.78125H19.9023V4.59766H26.1191V5.78125Z' fill='black' fill-opacity='0.24' />
</svg>
</div>
);
}
/**
* 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 (
<div className='user-profile-pic-container'>
<img src={image || icon || '../renderer/assets/symphony-default-profile-pic.png'} className='user-profile-pic' alt='user profile picture' />
</div>
);
}
return (
<div className='user-name-text-container'>
<span className='user-name-text'>{title.substr(0, 1)}</span>
<img src='../renderer/assets/notification-ext-badge.svg' alt='ext-badge'/>
</div>
);
}
@ -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();
}
}

View File

@ -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
*/

View File

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