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

-
- );
- }
- return (
-
-
{title.substr(0, 1)}
+
);
}
@@ -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