import classNames from 'classnames' ; 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 ) ; const darkTheme = [ '#e23030' , '#b5616a' , '#ab8ead' , '#ebc875' , '#a3be77' , '#58c6ff' , '#ebab58' , ] ; const Colors = { dark : { regularFlashingNotificationBgColor : '#27588e' , notificationBackgroundColor : '#27292c' , notificationBorderColor : '#717681' , mentionBackgroundColor : '#99342c' , mentionBorderColor : '#ff5d50' , } , light : { regularFlashingNotificationBgColor : '#aad4f8' , notificationBackgroundColor : '#f1f1f3' , notificationBorderColor : 'transparent' , mentionBackgroundColor : '#fcc1b9' , mentionBorderColor : 'transparent' , } , } ; type Theme = '' | Themes . DARK | Themes . LIGHT ; interface INotificationState { title : string ; company : string ; body : string ; image : string ; icon : string | undefined ; id : number ; color : string ; flash : boolean ; isExternal : boolean ; theme : Theme ; hasReply : boolean ; hasMention : boolean ; isInputHidden : boolean ; containerHeight : number ; canSendMessage : boolean ; } type mouseEventButton = React . MouseEvent < HTMLDivElement > | React . MouseEvent < HTMLButtonElement > ; type keyboardEvent = React . KeyboardEvent < HTMLInputElement > ; const CONTAINER_HEIGHT = 100 ; const CONTAINER_HEIGHT_WITH_INPUT = 142 ; const LIGHT_THEME = '#EAEBEC' ; const DARK_THEME = '#25272B' ; export default class NotificationComp extends React . Component < { } , INotificationState > { private readonly eventHandlers = { onClose : ( winKey ) => ( _event ) => this . close ( _event , winKey ) , onClick : ( data ) => ( _event ) => this . click ( data ) , onContextMenu : ( event ) => this . contextMenu ( event ) , onMouseEnter : ( winKey ) => ( _event ) => this . onMouseEnter ( winKey ) , onMouseLeave : ( winKey ) => ( _event ) => this . onMouseLeave ( winKey ) , onOpenReply : ( winKey ) => ( event ) => this . onOpenReply ( event , winKey ) , onThumbsUp : ( ) => ( _event ) => this . onThumbsUp ( ) , onReply : ( winKey ) => ( _event ) => this . onReply ( winKey ) , onKeyUp : ( winKey ) => ( event ) => this . onKeyUp ( event , winKey ) , } ; private input : React . RefObject < HTMLInputElement > ; constructor ( props ) { super ( props ) ; this . state = { title : '' , company : 'Symphony' , body : '' , image : '' , icon : '' , id : 0 , color : '' , flash : false , isExternal : false , theme : '' , isInputHidden : true , hasReply : false , hasMention : false , containerHeight : CONTAINER_HEIGHT , canSendMessage : false , } ; this . updateState = this . updateState . bind ( this ) ; this . onInputChange = this . onInputChange . bind ( this ) ; this . resetNotificationData = this . resetNotificationData . bind ( this ) ; this . getInputValue = this . getInputValue . bind ( this ) ; this . input = React . createRef ( ) ; } /**
     * Callback to handle event when a component is mounted
     */ public componentDidMount ( ) { ipcRenderer . on ( 'notification-data' , this . updateState ) ; } /**
     * Callback to handle event when a component is unmounted
     */ public componentWillUnmount ( ) { ipcRenderer . removeListener ( 'notification-data' , this . updateState ) ; } /**
     * Renders the component
     */ public render ( ) { const { title , body , id , color , isExternal , theme , containerHeight , icon , } = this . state ; let themeClassName ; if ( theme ) { themeClassName = theme ; } else if ( darkTheme . includes ( color . toLowerCase ( ) ) ) { themeClassName = 'black-text' ; } else { themeClassName = color && color . match ( whiteColorRegExp ) ? Themes . LIGHT : Themes . DARK ; } 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 data - testid = 'NOTIFICATION_CONTAINER' className = { containerCssClass } style = { { height : containerHeight , backgroundColor : themeColors . notificationBackgroundColor , borderColor : themeColors . notificationBorderColor , } } lang = { i18n . getLocale ( ) } onMouseEnter = { this . eventHandlers . onMouseEnter ( id ) } onMouseLeave = { this . eventHandlers . onMouseLeave ( id ) } > < div className = { `close-button ${ themeClassName }` } title = { i18n . t ( 'Close' ) ( ) } > < img src = { closeImgFilePath } title = { i18n . t ( 'Close' ) ( ) } alt = 'close' onClick = { this . eventHandlers . onClose ( id ) } / > < /div> < div className = 'main-container' role = 'alert' onContextMenu = { this . eventHandlers . onContextMenu } onClick = { this . eventHandlers . onClick ( id ) } > < 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> < span className = { `message-preview ${ themeClassName }` } > { body } < /span> < /div> < /div> { this . renderRTE ( themeClassName ) } < /div>); } private renderRTE ( themeClassName ) { 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' > < input className = { themeClassName } autoFocus = { true } onKeyUp = { this . eventHandlers . onKeyUp ( id ) } onChange = { this . onInputChange } ref = { this . input } / > < /div> < div className = { actionButtonContainer } > < button className = { `rte-thumbsup-button ${ themeClassName }` } onClick = { this . eventHandlers . onThumbsUp ( ) } > 👍 < /button> < button className = { `rte-send-button ${ themeClassName }` } onClick = { this . eventHandlers . onReply ( id ) } disabled = { ! canSendMessage } title = { i18n . t ( 'Send' ) ( ) } / > < /div> < /div>); } return ; } private renderExtBadge ( isExternal ) { if ( ! isExternal ) { return ; } return ( < div className = 'ext-badge-container' > < img src = '../renderer/assets/notification-ext-badge.svg' alt = 'ext-badge' / > < /div>); } private renderImage ( imageUrl ) { let imgClass = 'default-logo' ; let url = '../renderer/assets/notification-symphony-logo.svg' ; let alt = 'Symphony logo' ; const isDefaultUrl = imageUrl && 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>); } private renderSymphonyBadge ( hasImageUrl ) { if ( hasImageUrl ) { return ( < img src = '../renderer/assets/symphony-badge.svg' alt = '' className = 'profile-picture-badge' / > ) ; } return ; } private click ( id ) { ipcRenderer . send ( 'notification-clicked' , id ) ; } private close ( event , id ) { event . stopPropagation ( ) ; ipcRenderer . send ( 'close-notification' , id ) ; } private contextMenu ( event ) { event . preventDefault ( ) ; } private onMouseEnter ( id ) { ipcRenderer . send ( 'notification-mouseenter' , id ) ; } private onMouseLeave ( id ) { const { isInputHidden } = this . state ; ipcRenderer . send ( 'notification-mouseleave' , id , isInputHidden ) ; } private onThumbsUp ( ) { if ( this . input . current ) { const input = this . input . current . value ; this . input . current . value = input + '👍' ; this . onInputChange ( ) ; this . input . current . focus ( ) ; } } private onReply ( id ) { let replyText = this . getInputValue ( ) ; if ( replyText ) { replyText = replyText . replace ( /👍/g , ':thumbsup: ' ) ; ipcRenderer . send ( 'notification-on-reply' , id , replyText ) ; } } 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 ( ) ; } ) ; } private getInputValue ( ) { return this . input . current ?. value . trim ( ) ; } private onKeyUp ( event , id ) { if ( event . key === 'Enter' || event . keyCode === 13 ) { this . onReply ( id ) ; } } private onInputChange ( ) { if ( this . input . current ) { const inputText = this . input . current . value || '' ; this . setState ( { canSendMessage : inputText . trim ( ) . length > 0 , } ) ; } } private updateState ( _event , data ) { const { color } = data ; const isOldColor = /^([A-Fa-f0-9]{6})/ . test ( color ) ; data . color = isOldColor ? `#${ color }` : this . isValidColor ( color ) ? color : '' ; data . isInputHidden = true ; data . containerHeight = CONTAINER_HEIGHT ; data . theme = isOldColor && darkTheme . includes ( data . color ) ? Themes . DARK : data . theme ? data . theme : Themes . LIGHT ; this . resetNotificationData ( ) ; this . setState ( data as INotificationState ) ; } private isValidColor ( color ) { return /^#([A-Fa-f0-9]{6})/ . test ( color ) ; } private resetNotificationData ( ) { if ( this . input . current ) { this . input . current . value = '' ; } } private getThemeColors ( ) { const { theme , flash , isExternal , hasMention , color } = this . state ; const currentColors = theme === Themes . DARK ? { ... Colors . dark } : { ... Colors . light } ; const externalFlashingBackgroundColor = theme === Themes . DARK ? '#70511f' : '#f6e5a6' ; if ( flash && theme ) { if ( isExternal ) { if ( ! hasMention ) { currentColors . notificationBorderColor = '#F7CA3B' ; currentColors . notificationBackgroundColor = externalFlashingBackgroundColor ; if ( this . isCustomColor ( color ) ) { currentColors . notificationBorderColor = this . getThemedCustomBorderColor ( theme , color ) ; currentColors . notificationBackgroundColor = color ; } } else { currentColors . notificationBorderColor = '#F7CA3B' ; } } else if ( hasMention ) { currentColors . notificationBorderColor = currentColors . notificationBorderColor ; } else { currentColors . notificationBackgroundColor = this . isCustomColor ( color ) ? color : currentColors . regularFlashingNotificationBgColor ; currentColors . notificationBorderColor = this . isCustomColor ( color ) ? this . getThemedCustomBorderColor ( theme , color ) : theme === Themes . DARK ? '#2996fd' : 'transparent' ; } } else if ( ! flash ) { if ( hasMention ) { currentColors . notificationBackgroundColor = currentColors . mentionBackgroundColor ; currentColors . notificationBorderColor = currentColors . mentionBorderColor ; } else if ( this . isCustomColor ( color ) ) { currentColors . notificationBackgroundColor = color ; currentColors . notificationBorderColor = this . getThemedCustomBorderColor ( theme , color ) ; } else if ( isExternal ) { currentColors . notificationBorderColor = '#F7CA3B' ; } } return currentColors ; } private renderReplyButton ( id , theming ) { 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 ; } private getContainerCssClasses ( ) { const customClasses = [ ] ; const { flash , theme , hasMention , isExternal } = this . state ; if ( flash && theme ) { if ( isExternal ) { customClasses . push ( 'external-border' ) ; if ( hasMention ) { customClasses . push ( `${ theme }-ext-mention-flashing` ) ; } else { customClasses . push ( `${ theme }-ext-flashing` ) ; } } else if ( hasMention ) { customClasses . push ( `${ theme }-mention-flashing` ) ; } else { customClasses . push ( `${ theme }-flashing` ) ; } } else if ( isExternal ) { customClasses . push ( 'external-border' ) ; } return customClasses ; } private isCustomColor ( color ) { if ( color && color !== LIGHT_THEME && color !== DARK_THEME ) { return true ; } return false ; } private increaseBrightness ( hex , percent ) { hex = hex . replace ( /^\s*#|\s*$/g , '' ) ; const r = parseInt ( hex . substr ( 0 , 2 ) , 16 ) ; const g = parseInt ( hex . substr ( 2 , 2 ) , 16 ) ; const b = parseInt ( hex . substr ( 4 , 2 ) , 16 ) ; return ( '#' + ( 0 | ( ( 1 << 8 ) + r + ( ( 256 - r ) * percent ) / 100 ) ) . toString ( 16 ) . substr ( 1 ) + ( 0 | ( ( 1 << 8 ) + g + ( ( 256 - g ) * percent ) / 100 ) ) . toString ( 16 ) . substr ( 1 ) + ( 0 | ( ( 1 << 8 ) + b + ( ( 256 - b ) * percent ) / 100 ) ) . toString ( 16 ) . substr ( 1 ) ) ; } private getThemedCustomBorderColor ( theme , customColor ) { return theme === Themes . DARK ? this . increaseBrightness ( customColor , 50 ) : 'transparent' ; } }