import { Button , Classes , Colors , Intent , Menu , MenuDivider , MenuItem , Popover } from '@blueprintjs/core' ; import _ from 'lodash' ; import * as React from 'react' ; import MessageContent from 'components/MessageContent' ; import Preview from 'components/Preview' ; import ReplyReference from 'components/ReplyReference' ; import ActionMenuItems from 'containers/ActionMenuItems' ; import { ActionHandler } from 'libs/Action' ; import { SerializedChatter , WithNameColorProps } from 'libs/Chatter' ; import { SerializedMessage } from 'libs/Message' ; import styled , { ifProp , prop , size , theme } from 'styled' ; /**
 * Wrapper component.
 */ const Wrapper = styled . div < WrapperProps > `
  background-color: ${ ifProp ( 'highlighted' , theme ( 'log.permanent.background' ) , ifProp ( 'mentioned' , theme ( 'log.mention.self.background' ) , ifProp ( 'alternate' , theme ( 'log.alternate' ) , 'inherit' ) ) ) };
  border-left: 3px solid
    ${ ifProp ( 'highlighted' , theme ( 'log.permanent.border' ) , ifProp ( 'mentioned' , theme ( 'log.mention.self.color' ) , ifProp ( 'read' , ifProp ( 'twitchHighlighted' , theme ( 'twitch.purple' ) , 'transparent' ) , Colors . BLUE4 ) ) ) };
  opacity: ${ ifProp ( 'purged' , 0.5 , 1.0 ) };
  padding: 4px ${ size ( 'log.hPadding' ) } 1px 7px;
  white-space: pre-wrap;

  ${ ( props ) => props . twitchHighlighted && props . increaseTwitchHighlight && `
      background-color: ${ theme ( 'twitch.lightPurple' ) ( props ) };
      ` };
` ; /**
 * HistoricalWrapper component.
 */ const HistoricalWrapper = styled . div `
  border-left: 3px solid transparent;
  color: ${ theme ( 'message.time.color' ) };
  font-style: italic;
  padding: 4px ${ size ( 'log.hPadding' ) } 1px 7px;
  white-space: pre-wrap;

  .mention,
  .replyReference {
    color: ${ Colors . LIGHT_GRAY5 };

    .${ Classes . DARK } & {
      color: ${ Colors . LIGHT_GRAY3 };
    }
  }
` ; /**
 * MenuButton component.
 */ const MenuButton = styled ( Button ) `
  &.${ Classes . BUTTON }.${ Classes . MINIMAL }, .${ Classes . DARK } &.${ Classes . BUTTON }.${ Classes . MINIMAL } {
    height: 16px;
    min-height: 16px;
    min-width: 24px;
    padding: 0;
    position: relative;
    vertical-align: top;
    width: 24px;

    & svg {
      color: ${ Colors . GRAY2 };
      position: absolute;
      left: 0;
      top: 3px;
    }

    &:active,
    &:hover,
    &.${ Classes . ACTIVE } {
      background: initial;

      & svg {
        color: ${ Colors . LIGHT_GRAY1 };
      }
    }
  }
` ; /**
 * ContextMenu component.
 */ const ContextMenu = styled ( Menu ) `
  & .${ Classes . MENU_ITEM }.${ Classes . DISABLED } {
    cursor: auto !important;
  }
` ; /**
 * Time component.
 */ const Time = styled . span `
  color: ${ theme ( 'message.time.color' ) };
  display: inline-block;
  font-size: 0.77rem;
  min-width: 42px;
` ; /**
 * Badges component.
 */ const Badges = styled . span `
  .badge {
    border-radius: 2px;
    display: inline-block;
    margin-top: -1px;
    min-width: 18px;
    margin-right: 4px;
    vertical-align: middle;

    &:last-of-type {
      margin-right: 6px;
    }
  }
` ; /**
 * Name component.
 */ const Name = styled . span < WithNameColorProps > `
  color: ${ prop ( 'color' ) };
  cursor: pointer;
  font-weight: bold;
  padding-right: 2px;
` ; /**
 * HistoricalName component.
 */ const HistoricalName = styled . span `
  color: ${ theme ( 'message.time.color' ) };
  font-weight: bold;
  padding-right: 2px;
` ; /**
 * Username component.
 */ const Username = styled . span `
  font-size: 0.8rem;
  font-weight: normal;
` ; /**
 * React State.
 */ const initialState = { isContextMenuOpened : false } ; type State = Readonly < typeof initialState > ; /**
 * Message Component.
 */ export default class Message extends React . Component < Props , State > { public state : State = initialState ; param nextProps param nextState return public shouldComponentUpdate ( nextProps , nextState ) { const { isContextMenuOpened } = this . state ; const { isContextMenuOpened : nextIsContextMenuOpened } = nextState ; const { markNewAsUnread , message , showUnbanContextMenuItem , style } = this . props ; const { markNewAsUnread : prevMarkNewAsUnread , message : nextMessage , showUnbanContextMenuItem : prevShowUnbanContextMenuItem , style : nextStyle , } = nextProps ; return ( isContextMenuOpened !== nextIsContextMenuOpened || message . id !== nextMessage . id || message . purged !== nextMessage . purged || message . read !== nextMessage . read || showUnbanContextMenuItem !== prevShowUnbanContextMenuItem || markNewAsUnread !== prevMarkNewAsUnread || ! _ . isEqual ( style , nextStyle ) ) ; } return public render ( ) { const { focusEmote , increaseTwitchHighlight , markNewAsUnread , message , style , useAlternate } = this . props ; if ( message . historical ) { return this . renderHistoricalMessage ( ) ; } const usernameColor = message . user . color as string ; return ( < Wrapper increaseTwitchHighlight = { increaseTwitchHighlight } twitchHighlighted = { message . twitchHighlighted } read = { ! markNewAsUnread || message . read } onDoubleClick = { this . onDoubleClick } highlighted = { message . highlighted } mentioned = { message . mentioned } alternate = { useAlternate } purged = { message . purged } onClick = { this . onClick } style = { style } > { this . renderContextMenu ( ) } < Time > { message . time } < /Time> { this . renderBadges ( ) } < Name color = { usernameColor } onClick = { this . onClickUsername } onMouseDown = { this . onMouseDownUsername } > { message . user . displayName } { message . user . showUserName && < Username > ( { message . user . userName } ) < /Username>} < /Name>{' '} < ReplyReference message = { message } / > < MessageContent message = { message } focusEmote = { focusEmote } withEmoteDetails / > { '\n' } { this . renderPreviews ( ) } < /Wrapper>); } private renderHistoricalMessage ( ) { const { message , style } = this . props ; return ( < HistoricalWrapper style = { style } onDoubleClick = { this . onDoubleClick } onClick = { this . onClick } > < Time > { message . time } < /Time> < HistoricalName > { message . user . displayName } { message . user . showUserName && < Username > ( { message . user . userName } ) < /Username>} < /HistoricalName>{' '} < ReplyReference message = { message } / > < MessageContent message = { message } /> { '\n' } < /HistoricalWrapper>); } private renderPreviews ( ) { const { message } = this . props ; if ( _ . size ( message . previews ) <= 0 ) { return null ; } return _ . map ( message . previews , ( preview ) => { return < Preview key = { preview . id } preview = { preview } / > ; } ) ; } private renderContextMenu ( ) { const { actionHandler , canModerate , message , showContextMenu , showUnbanContextMenuItem } = this . props ; if ( ! showContextMenu ) { return null ; } const isHighlighted = message . highlighted || message . mentioned || message . twitchHighlighted || _ . includes ( message . message , 'class="highlight' ) ; const menu = ( < ContextMenu > < MenuDivider title = { `${ message . user . displayName }${ message . user . showUserName ? ` (${ message . user . userName })` : '' }` } /> < MenuItem text = "" disabled / > { ! message . user . isSelf && ( < > < MenuItem icon = "envelope" text = "Whisper" onClick = { this . onClickWhisper } / > < MenuItem icon = "inheritance" text = "Reply" onClick = { this . onClickReply } / > < MenuDivider /> < />)} < MenuItem icon = "clipboard" text = "Copy message" onClick = { this . copyMessage } / > < MenuItem icon = "clipboard" text = "Copy username" onClick = { this . onCopyUsername } / > { isHighlighted && ( < MenuItem text = { `Stop highlighting ${ message . user . displayName }` } onClick = { this . onClickStopHighlights } icon = "delete" / > ) } < ActionMenuItems startDivider actionHandler = { actionHandler } chatter = { message . user } / > { canModerate ( message . user ) && ( < > < MenuDivider /> < MenuItem icon = "remove" text = "Delete message" onClick = { this . onClickDelete } / > < MenuItem icon = "trash" text = "Purge" onClick = { this . onClickPurge } / > < MenuItem icon = "time" text = "Timeout" > < MenuItem text = "10m" onClick = { this . onClickTimeout10M } / > < MenuItem text = "1h" onClick = { this . onClickTimeout1H } / > < MenuItem text = "6h" onClick = { this . onClickTimeout6H } / > < MenuItem text = "24h" onClick = { this . onClickTimeout24H } / > < /MenuItem> < MenuItem icon = "disable" text = "Ban" intent = { Intent . DANGER } onClick = { this . onClickBan } / > { showUnbanContextMenuItem && ( < MenuItem icon = "unlock" text = "Unban" intent = { Intent . DANGER } onClick = { this . onClickUnban } / > ) } < />)} < /ContextMenu>); return ( < Popover content = { menu } onInteraction = { this . onToggleContextMenu } isOpen = { this . state . isContextMenuOpened } > < MenuButton icon = "menu" minimal / > < /Popover>); } private renderBadges ( ) { const { badges } = this . props . message ; if ( _ . isNil ( badges ) ) { return null ; } return < Badges dangerouslySetInnerHTML = { { __html : badges } } />; } private onToggleContextMenu = ( nextOpenState ) => { const { isContextMenuOpened } = this . state ; this . setState ( ( ) => ( { isContextMenuOpened : nextOpenState } ) ) ; if ( isContextMenuOpened !== nextOpenState ) { this . props . onToggleContextMenu ( nextOpenState ) ; } } ; private onClick = ( event ) => { const { message , onClick , reply } = this . props ; onClick ( message . id ) ; if ( event . altKey && ! message . historical && ! message . user . isSelf ) { reply ( message ) ; } } ; private onDoubleClick = ( ) => { const { copyMessageOnDoubleClick , copyMessageToClipboard , message } = this . props ; if ( copyMessageOnDoubleClick ) { copyMessageToClipboard ( message ) ; } } ; private onClickUsername = ( ) => { const { focusChatter , message } = this . props ; focusChatter ( message . user ) ; } ; private onMouseDownUsername = ( event ) => { const { message , openTwitchViewerCard } = this . props ; if ( event . button === 1 ) { openTwitchViewerCard ( message . user ) ; } } ; private copyMessage = ( ) => { const { copyMessageToClipboard , message } = this . props ; copyMessageToClipboard ( message ) ; } ; private onCopyUsername = ( ) => { const { message } = this . props ; this . props . copyToClipboard ( message . user . displayName ) ; } ; private onClickStopHighlights = ( ) => { const { addHighlightsIgnoredUser , message } = this . props ; addHighlightsIgnoredUser ( message . user . userName ) ; } ; private onClickDelete = ( ) => { const { deleteMessage , message } = this . props ; deleteMessage ( message . id ) ; } ; private onClickPurge = ( ) => { this . timeout ( 1 ) ; } ; private onClickTimeout10M = ( ) => { this . timeout ( 600 ) ; } ; private onClickTimeout1H = ( ) => { this . timeout ( 3600 ) ; } ; private onClickTimeout6H = ( ) => { this . timeout ( 21600 ) ; } ; private onClickTimeout24H = ( ) => { this . timeout ( 86400 ) ; } ; private timeout ( duration ) { const { message , timeout } = this . props ; timeout ( message . user . userName , duration ) ; } private onClickBan = ( ) => { const { ban , message } = this . props ; ban ( message . user . userName ) ; } ; private onClickUnban = ( ) => { const { message , unban } = this . props ; unban ( message . user . userName ) ; } ; private onClickWhisper = ( ) => { const { whisper , message } = this . props ; whisper ( message . user . userName ) ; } ; private onClickReply = ( ) => { const { message , reply } = this . props ; reply ( message ) ; } ; } /**
 * React Props.
 */ interface Props { actionHandler : ActionHandler ; addHighlightsIgnoredUser : ( username : string ) => void ; ban : ( username : string ) => void ; canModerate : ( chatter : SerializedChatter ) => boolean ; copyMessageOnDoubleClick : boolean ; copyMessageToClipboard : ( message : SerializedMessage ) => void ; copyToClipboard : ( message : string ) => void ; deleteMessage : ( id : string ) => void ; focusChatter : ( chatter : SerializedChatter ) => void ; focusEmote : ( id : string , name : string , provider : string ) => void ; increaseTwitchHighlight : boolean ; markNewAsUnread : boolean ; message : SerializedMessage ; onClick : ( id : string ) => void ; onToggleContextMenu : ( open : boolean ) => void ; openTwitchViewerCard : ( user : Optional < SerializedChatter > ) => void ; reply : ( message : SerializedMessage ) => void ; showContextMenu : boolean ; showUnbanContextMenuItem : boolean ; style : React . CSSProperties ; timeout : ( username : string , duration : number ) => void ; unban : ( username : string ) => void ; useAlternate : boolean ; whisper : ( username : string ) => void ; } /**
 * React Props.
 */ interface WrapperProps { alternate : boolean ; highlighted : boolean ; increaseTwitchHighlight : boolean ; mentioned : boolean ; purged : boolean ; read : boolean ; twitchHighlighted : boolean ; }