import { Button , ButtonGroup , Classes , InputGroup , Position , Tooltip } from '@blueprintjs/core' ; import _ from 'lodash' ; import * as React from 'react' ; import { Flipper } from 'react-flip-toolkit' ; import { connect } from 'react-redux' ; import { RouteComponentProps , withRouter } from 'react-router' ; import { compose } from 'recompose' ; import FlexContent from 'components/FlexContent' ; import FlexLayout from 'components/FlexLayout' ; import Follow from 'components/Follow' ; import NonIdealState from 'components/NonIdealState' ; import Spinner from 'components/Spinner' ; import Twitch , { Follower , Followers } from 'libs/Twitch' ; import { FollowsSortOrder , setFollowsSortOrder , toggleHideOfflineFollows } from 'store/ducks/settings' ; import { ApplicationState } from 'store/reducers' ; import { getFollowsSortOrder , getHideOfflineFollows } from 'store/selectors/settings' ; import styled , { size , theme } from 'styled' ; /**
 * Toolbar component.
 */ const Toolbar = styled ( FlexLayout ) `
  border-bottom: 1px solid ${ theme ( 'follows.border' ) };
  display: flex;
  align-items: center;
  padding-right: 10px;
` ; /**
 * SortGroup component.
 */ const SortGroup = styled ( ButtonGroup ) `
  margin-right: 10px;
` ; /**
 * OfflineButton component.
 */ const OfflineButton = styled ( Button ) `
  &.${ Classes . BUTTON } {
    padding: 5px 15px;
  }
` ; /**
 * Wrapper component.
 */ const Wrapper = styled . div `
  height: calc(100% - 51px);
  overflow-x: hidden;
  overflow-y: scroll;
` ; /**
 * Grid component.
 */ const Grid = styled . div `
  display: grid;
  grid-auto-rows: ${ size ( 'follows.height' ) };
  grid-column-gap: ${ size ( 'follows.margin' ) };
  grid-row-gap: ${ size ( 'follows.margin' ) };
  grid-template-columns: repeat(auto-fit, minmax(${ size ( 'follows.width' ) }, 1fr));
  padding: ${ size ( 'follows.margin' ) };
  padding-right: 10px;
` ; /**
 * Search component.
 */ const Search = styled ( InputGroup ) `
  margin: 10px;
` ; /**
 * Sort type.
 */ enum SortType { Viewers , Uptime } /**
 * React State.
 */ const initialState = { error : undefined as Optional < Error > , filter : '' , filteredFollows : undefined as Optional < FilterableFollower [ ] > , followers : undefined as Optional < Followers > , follows : undefined as Optional < FilterableFollower [ ] > , prevPropsFollowsSortOrder : FollowsSortOrder . ViewersDesc , prevPropsHideOfflineFollows : false , } ; type State = Readonly < typeof initialState > ; /**
 * Follows Component.
 */ class Follows extends React . Component < Props , State > { param nextProps param prevState return public static getDerivedStateFromProps ( { followsSortOrder , hideOfflineFollows } , { filter , followers , prevPropsFollowsSortOrder , prevPropsHideOfflineFollows } ) { if ( ! _ . isNil ( followers ) && ( followsSortOrder !== prevPropsFollowsSortOrder || hideOfflineFollows !== prevPropsHideOfflineFollows ) ) { const follows = Follows . getSortedFollows ( followers , followsSortOrder , hideOfflineFollows ) ; return { filteredFollows : Follows . getFilteredFollows ( follows , filter ) , follows , prevPropsFollowsSortOrder : followsSortOrder , prevPropsHideOfflineFollows : hideOfflineFollows , } ; } return null ; } param followers param sortOrder- param hideOffline return private static getSortedFollows ( { offline , online , own } , sortOrder , hideOffline ) { const isSortingByViewers = this . isSortingByViewers ( sortOrder ) ; const isSortingDesc = this . isSortingDesc ( sortOrder ) ; const type = isSortingByViewers ? 'viewers' : 'created_at' ; const direction = isSortingByViewers ? ( isSortingDesc ? 'desc' : 'asc' ) : isSortingDesc ? 'asc' : 'desc' ; const streams = _ . orderBy ( online , [ type ] , [ direction ] ) ; let followers = ! _ . isNil ( own ) ? [ own , ... streams ] : [ ... streams ] ; followers = hideOffline ? followers : [ ... followers , ... offline ] ; return _ . map ( followers , ( follower ) => { let name ; let title ; if ( Twitch . isStream ( follower ) ) { name = follower . channel . name ; title = follower . channel . status || '' ; } else { name = follower . name ; title = follower . status || '' ; } return { ... follower , content : `${ name } ${ title . toLowerCase ( ) }` } ; } ) ; } param followers param filter- return private static getFilteredFollows ( follows , filter ) { let filteredFollows ; if ( ! _ . isNil ( follows ) && filter . length > 0 ) { const sanitizedFilter = filter . toLowerCase ( ) ; filteredFollows = _ . filter ( follows , ( follow ) => follow . content . includes ( sanitizedFilter ) ) ; } return filteredFollows ; } param sortOrder return private static isSortingByViewers ( sortOrder ) { return sortOrder === FollowsSortOrder . ViewersAsc || sortOrder === FollowsSortOrder . ViewersDesc ; } param sortOrder return private static isSortingDesc ( sortOrder ) { return sortOrder === FollowsSortOrder . ViewersDesc || sortOrder === FollowsSortOrder . UptimeDesc ; } public state : State = initialState ; private search : HTMLInputElement | null = null ; /**
     * Lifecycle: componentDidMount.
     */ public async componentDidMount ( ) { try { const { followsSortOrder , hideOfflineFollows } = this . props ; const followers = await Twitch . fetchFollows ( ) ; this . setState ( ( ) => ( { follows : Follows . getSortedFollows ( followers , followsSortOrder , hideOfflineFollows ) , followers } ) , ( ) => { this . focusFilterInput ( ) ; } ) ; } catch ( error ) { this . setState ( ( ) => ( { error } ) ) ; } } return public render ( ) { const { error , filter , filteredFollows , follows } = this . state ; const { followsSortOrder , hideOfflineFollows } = this . props ; if ( ! _ . isNil ( error ) ) { throw error ; } if ( _ . isNil ( follows ) ) { return < Spinner large / > ; } if ( follows . length === 0 ) { return ( < NonIdealState title = "Looks like you're alone!" details = { < > Maybe try to find streamers you like on < a href = "https://twitch.tv" > Twitch < /a>. < />}/ > ) ; } const followsToRender = filter . length > 0 && ! _ . isNil ( filteredFollows ) ? filteredFollows : follows ; return ( < > < Toolbar > < FlexContent > < Search placeholder = "Filter…" type = "search" leftIcon = "search" value = { filter } onChange = { this . onChangeFilter } inputRef = { this . setSearchElementRef } / > < /FlexContent> { this . renderContols ( ) } < /Toolbar> < Wrapper > { followsToRender . length > 0 ? ( < Flipper flipKey = { `${ filter }-${ followsSortOrder }-${ hideOfflineFollows }` } spring = "veryGentle" > < Grid > { _ . map ( followsToRender , ( follow ) => ( < Follow key = { follow . _id } follow = { follow } goToChannel = { this . goToChannel } / > ) ) } < /Grid> < /Flipper>) : (<NonIdealState details="Maybe try other options." title="No results!"/ > ) } < /Wrapper> < />); } private renderContols ( ) { const { followsSortOrder , hideOfflineFollows } = this . props ; const byViewers = Follows . isSortingByViewers ( followsSortOrder ) ; const byDesc = Follows . isSortingDesc ( followsSortOrder ) ; const byViewersIcon = byViewers ? ( byDesc ? 'sort-desc' : 'sort-asc' ) : 'sort-desc' ; const byUptimeIcon = ! byViewers ? ( byDesc ? 'sort-desc' : 'sort-asc' ) : 'sort-desc' ; const hideOfflineIcon = hideOfflineFollows ? 'eye-off' : 'eye-open' ; return ( < FlexLayout > < SortGroup > < Tooltip content = "Sort by viewers" position = { Position . BOTTOM } > < Button icon = "people" rightIcon = { byViewersIcon } active = { byViewers } onClick = { this . onClickSortByViewers } / > < /Tooltip> < Tooltip content = "Sort by uptime" position = { Position . BOTTOM } > < Button icon = "time" rightIcon = { byUptimeIcon } active = { ! byViewers } onClick = { this . onClickSortByUptime } / > < /Tooltip> < /SortGroup> < Tooltip content = "Toggle offline channels" position = { Position . BOTTOM } > < OfflineButton onClick = { this . onClickToggleOfflineChannels } active = { ! hideOfflineFollows } icon = { hideOfflineIcon } / > < /Tooltip> < /FlexLayout>); } private onClickToggleOfflineChannels = ( ) => { this . props . toggleHideOfflineFollows ( ) ; this . focusFilterInput ( ) ; } ; private onClickSortByViewers = ( ) => { this . sortBy ( SortType . Viewers ) ; } ; private onClickSortByUptime = ( ) => { this . sortBy ( SortType . Uptime ) ; } ; private onChangeFilter = ( event ) => { const filter = event . currentTarget . value ; this . setState ( ( { follows } ) => ( { filter , filteredFollows : Follows . getFilteredFollows ( follows , filter ) } ) ) ; } ; private focusFilterInput ( ) { if ( ! _ . isNil ( this . search ) ) { this . search . focus ( ) ; } } private goToChannel = ( channel ) => { this . props . history . push ( `/${ channel }` ) ; } ; private sortBy ( type ) { const { followsSortOrder } = this . props ; let sortOrder ; if ( Follows . isSortingByViewers ( followsSortOrder ) ) { if ( type === SortType . Viewers ) { sortOrder = followsSortOrder === FollowsSortOrder . ViewersDesc ? FollowsSortOrder . ViewersAsc : FollowsSortOrder . ViewersDesc ; } else { sortOrder = FollowsSortOrder . UptimeDesc ; } } else { if ( type === SortType . Viewers ) { sortOrder = FollowsSortOrder . ViewersDesc ; } else { sortOrder = followsSortOrder === FollowsSortOrder . UptimeDesc ? FollowsSortOrder . UptimeAsc : FollowsSortOrder . UptimeDesc ; } } this . props . setFollowsSortOrder ( sortOrder ) ; this . focusFilterInput ( ) ; } private setSearchElementRef = ( ref ) => { this . search = ref ; } ; } /**
 * Component enhancer.
 */ const enhance = compose < Props , { } > ( connect < StateProps , DispatchProps , { } , ApplicationState > ( ( state ) => ( { followsSortOrder : getFollowsSortOrder ( state ) , hideOfflineFollows : getHideOfflineFollows ( state ) , } ) , { setFollowsSortOrder , toggleHideOfflineFollows } ) , withRouter ) ; export default enhance ( Follows ) ; /**
 * React Props.
 */ interface StateProps { followsSortOrder : ReturnType < typeof getFollowsSortOrder > ; hideOfflineFollows : ReturnType < typeof getHideOfflineFollows > ; } /**
 * React Props.
 */ interface DispatchProps { setFollowsSortOrder : typeof setFollowsSortOrder ; toggleHideOfflineFollows : typeof toggleHideOfflineFollows ; } /**
 * React Props.
 */ type Props = StateProps & DispatchProps & RouteComponentProps < { } > ; /**
 * Follower optimized for filtering by computing ahead of time filterable content.
 */ type FilterableFollower = Follower & { content : string ; } ;