import { Classes , Colors , Icon , InputGroup , Popover , Position } from '@blueprintjs/core' ; import _ from 'lodash' ; import * as React from 'react' ; import { connect } from 'react-redux' ; import { Grid , GridCellRenderer } from 'react-virtualized/dist/es/Grid' ; import { compose } from 'recompose' ; import EmotePickerEmote from 'components/EmotePickerEmote' ; import EmotePickerProvider from 'components/EmotePickerProvider' ; import FlexContent from 'components/FlexContent' ; import FlexLayout from 'components/FlexLayout' ; import NonIdealState from 'components/NonIdealState' ; import { Emote , EmoteProviderPrefix } from 'libs/EmotesProvider' ; import Resources from 'libs/Resources' ; import { ApplicationState } from 'store/reducers' ; import { getEmotesSets } from 'store/selectors/app' ; import styled , { ifProp , size , theme , ThemeProps , withTheme } from 'styled' ; /**
 * EmotePickerButton component.
 */ const EmotePickerButton = styled . button < EmotePickerButtonProps > `
  background-color: initial;
  border: none;
  cursor: ${ ifProp ( 'disabled' , 'not-allowed' , 'pointer' ) };
  filter: ${ ifProp ( 'disabled' , 'grayscale(100%)' , ifProp ( 'isOpen' , 'none' , 'grayscale(100%)' ) ) };
  opacity: ${ ifProp ( 'disabled' , 0.5 , 1 ) };
  position: absolute;
  right: 12px;
  top: 20px;

  &:hover {
    filter: ${ ifProp ( 'disabled' , 'grayscale(100%)' , 'none' ) };
  }
` ; /**
 * Header component.
 */ const Header = styled ( FlexLayout ) `
  padding: ${ size ( 'emotePicker.padding' ) };
` ; /**
 * Emotes component.
 */ const Emotes = styled ( FlexContent ) `
  border-bottom: 1px solid ${ theme ( 'emotePicker.border' ) };
  border-top: 1px solid ${ theme ( 'emotePicker.border' ) };
  background-color: ${ theme ( 'emotePicker.background' ) };

  & > div::-webkit-scrollbar {
    display: none;
  }
` ; /**
 * Preview component.
 */ const Preview = styled ( FlexLayout ) `
  align-items: center;
  height: ${ ( props ) => props . theme . emotePicker . cellSize * 2 + props . theme . emotePicker . padding * 2 }px;
  padding: ${ size ( 'emotePicker.padding' ) };

  img {
    max-height: ${ size ( 'emotePicker.maxSize' , 'double' ) };
    max-width: 100%;
  }
` ; /**
 * PreviewImage component.
 */ const PreviewImage = styled ( FlexLayout ) `
  align-items: center;
  height: ${ size ( 'emotePicker.cellSize' , 'double' ) };
  justify-content: center;
  margin-right: 10px;
  width: ${ size ( 'emotePicker.cellSize' , 'double' ) };
` ; /**
 * Notice component.
 */ const Notice = styled . div `
  color: ${ Colors . GRAY4 };
  font-size: 0.7rem;
  margin: 7px 0;
` ; /**
 * Emote provider icon mapping.
 */ const EmoteProviderIconMap = { [ EmoteProviderPrefix . Twitch ] : [ '115847' , '28087' , '64138' , '68856' , '123171' , '30259' , '4339' , '114836' , '425618' ] , [ EmoteProviderPrefix . Bttv ] : '56e9f494fff3cc5c35e5287e' , [ EmoteProviderPrefix . Ffz ] : '28136' , } ; /**
 * React State.
 */ const initialState = { buttonIcon : undefined as Optional < string > , filter : '' , filteredSet : [ ] as Emote [ ] , hovered : undefined as Optional < Emote > , prefix : EmoteProviderPrefix . Twitch , visible : false , } ; type State = Readonly < typeof initialState > ; /**
 * EmotePicker Component.
 */ class EmotePicker extends React . Component < Props , State > { public state : State = initialState ; private search : HTMLInputElement | null = null ; private columnCount : number ; private columnWidth : number ; private rowHeight : number ; param props constructor ( props ) { super ( props ) ; this . columnCount = _ . floor ( props . theme . emotePicker . width / ( props . theme . emotePicker . cellSize + props . theme . emotePicker . cellGutter ) ) ; this . columnWidth = props . theme . emotePicker . cellSize + props . theme . emotePicker . cellGutter ; this . rowHeight = props . theme . emotePicker . cellSize + props . theme . emotePicker . cellGutter ; } param prevProps param prevState public componentDidUpdate ( _prevProps , prevState ) { if ( _ . isNil ( prevState . buttonIcon ) ) { this . updateButtonIcon ( ) ; } } return public render ( ) { const { buttonIcon , visible } = this . state ; if ( _ . isNil ( buttonIcon ) ) { return null ; } return ( < Popover isOpen = { visible } content = { this . renderPicker ( ) } onInteraction = { this . toggleVisibility } position = { Position . LEFT_BOTTOM } popoverClassName = "emotePickerPopover" minimal > < EmotePickerButton isOpen = { visible } title = "Emote Picker" disabled = { this . props . disabled } onMouseEnter = { this . onMouseEnterButton } > < img src = { buttonIcon } alt = "Emote Picker" / > < /EmotePickerButton> < /Popover>); } return private renderPicker = ( ) => { const { filter , prefix } = this . state ; const { sets } = this . props ; return ( < FlexLayout vertical > < Header > { _ . map ( _ . keys ( sets ) , ( providerPrefix ) => { const icon = this . getProviderIcon ( providerPrefix ) ; if ( _ . isNil ( icon ) ) { return null ; } return ( < EmotePickerProvider selected = { providerPrefix === prefix } onClick = { this . onClickProvider } prefix = { providerPrefix } key = { providerPrefix } icon = { icon } / > ) ; } ) } < InputGroup inputRef = { this . setSearchElementRef } onChange = { this . onChangeFilter } className = { Classes . FILL } placeholder = "Filter…" leftIcon = "search" value = { filter } type = "search" / > < /Header> < Emotes > < Grid rowCount = { _ . ceil ( _ . size ( this . getEmotesSet ( ) ) / this . columnCount ) } height = { this . props . theme . emotePicker . height } width = { this . props . theme . emotePicker . width } noContentRenderer = { this . noEmoteRenderer } cellRenderer = { this . emoteRenderer } columnCount = { this . columnCount } columnWidth = { this . columnWidth } rowHeight = { this . rowHeight } filter = { filter } prefix = { prefix } / > < /Emotes> { this . renderPreview ( ) } < /FlexLayout>); } ; private renderPreview ( ) { const { hovered } = this . state ; if ( _ . isNil ( hovered ) ) { return ( < Preview > < PreviewImage > < Icon iconSize = { 40 } icon = "hand-up" / > < /PreviewImage> < div > Pick your emote ! < Notice > Shift to pick more than one . < /Notice> < /div> < /Preview>); } const provider = this . getProvider ( ) ; if ( _ . isNil ( provider ) ) { return < Preview />; } return ( < Preview > < PreviewImage > < img src = { provider . getEmoteUrlWithSize ( hovered . id , '2x' ) } alt = { hovered . code } / > < /PreviewImage> { hovered . code } < /Preview>); } private noEmoteRenderer = ( ) => { const details = this . isFiltering ( ) ? 'Try another search.' : 'Try in a few seconds.' ; return < NonIdealState small title = "No emote!" details = { details } / > ; } ; private emoteRenderer : GridCellRenderer = ( { columnIndex , key , rowIndex , style } ) => { const emote = _ . get ( this . getEmotesSet ( ) , this . columnCount * rowIndex + columnIndex ) ; const provider = this . getProvider ( ) ; if ( _ . isNil ( emote ) || _ . isNil ( provider ) ) { return null ; } return ( < EmotePickerEmote key = { key } style = { style } emote = { emote } urls = { provider . getEmoteTagUrls ( emote . id ) } onMouseEnter = { this . onMouseEnterEmote } onMouseLeave = { this . onMouseLeaveEmote } onClick = { this . onPickEmote } / > ) ; } ; private updateButtonIcon ( ) { const buttonIcon = this . getProviderIcon ( EmoteProviderPrefix . Twitch , true ) ; if ( ! _ . isNil ( buttonIcon ) ) { if ( this . state . buttonIcon !== buttonIcon ) { this . setState ( ( ) => ( { buttonIcon } ) ) ; } else { this . updateButtonIcon ( ) ; } } } private getProvider ( ) { return Resources . manager ( ) . getEmotesProvider ( this . state . prefix ) ; } private getProviderIcon ( prefix , random = false ) { const provider = Resources . manager ( ) . getEmotesProvider ( prefix ) ; if ( _ . isNil ( provider ) ) { return null ; } const ids = EmoteProviderIconMap [ prefix ] ; let id ; if ( _ . isString ( ids ) ) { id = ids ; } else { id = random ? ( _ . sample ( EmoteProviderIconMap [ prefix ] ) as string ) : EmoteProviderIconMap [ prefix ] [ 0 ] ; } return provider . getEmoteTagUrls ( id ) [ '1x' ] ; } private getEmotesSet ( ) { const { filteredSet , prefix } = this . state ; return this . isFiltering ( ) ? filteredSet : this . props . sets [ prefix ] ; } private isFiltering ( ) { return this . state . filter . length > 0 ; } private getFilteredEmotesSet ( set , filter ) { const sanitizedFilter = filter . toLowerCase ( ) ; return _ . filter ( set , ( emote ) => emote . code . toLowerCase ( ) . includes ( sanitizedFilter ) ) ; } private onMouseEnterButton = ( ) => { if ( ! this . props . disabled && ! this . state . visible ) { this . updateButtonIcon ( ) ; } } ; private onPickEmote = ( emote , withShiftKey ) => { if ( ! withShiftKey ) { this . setState ( ( ) => ( { visible : false } ) ) ; } this . props . onPick ( emote , withShiftKey ) ; } ; private onMouseEnterEmote = ( emote ) => { this . setState ( ( ) => ( { hovered : emote } ) ) ; } ; private onMouseLeaveEmote = ( emote ) => { this . setState ( ( { hovered } ) => ( { hovered : ! _ . isNil ( hovered ) && hovered . code === emote . code ? undefined : hovered , } ) ) ; } ; private onClickProvider = ( prefix ) => { const { filter } = this . state ; this . setState ( ( { filteredSet } ) => ( { filteredSet : this . isFiltering ( ) ? this . getFilteredEmotesSet ( this . props . sets [ prefix ] , filter ) : filteredSet , hovered : undefined , prefix , } ) ) ; } ; private onChangeFilter = ( event ) => { const filter = event . currentTarget . value ; this . setState ( ( { prefix } ) => ( { filter , filteredSet : this . getFilteredEmotesSet ( this . props . sets [ prefix ] , filter ) , hovered : undefined , } ) ) ; } ; private toggleVisibility = ( nextOpenState ) => { if ( nextOpenState && this . props . disabled ) { return ; } this . setState ( ( ) => ( { visible : nextOpenState , filter : '' , filteredSet : [ ] } ) , ( ) => { if ( nextOpenState && ! _ . isNil ( this . search ) ) { this . search . focus ( ) ; } else if ( ! nextOpenState ) { this . props . onCancel ( ) ; } } ) ; } ; private setSearchElementRef = ( ref ) => { this . search = ref ; } ; } /**
 * Component enhancer.
 */ const enhance = compose < Props , OwnProps > ( connect < StateProps , { } , OwnProps , ApplicationState > ( ( state ) => ( { sets : getEmotesSets ( state ) , } ) ) , withTheme ) ; export default enhance ( EmotePicker ) ; /**
 * React Props.
 */ interface StateProps { sets : ReturnType < typeof getEmotesSets > ; } /**
 * React Props.
 */ interface OwnProps { disabled : boolean ; onCancel : ( ) => void ; onPick : ( emote : Emote , withShiftKey : boolean ) => void ; } /**
 * React Props.
 */ type Props = StateProps & OwnProps & ThemeProps ; /**
 * React Props.
 */ interface EmotePickerButtonProps { disabled : boolean ; isOpen : boolean ; }