import _ from 'lodash' ; import { Component } from 'react' ; import { connect } from 'react-redux' ; import { Redirect } from 'react-router' ; import { nanoid } from 'nanoid' ; import tmi , { Client as TwitchClient , EmoteSets , Payment , Raid , Ritual , RoomState as RawRoomState , UserState , } from 'twitch-js' ; import Emoticons from 'constants/emoticons' ; import Event from 'constants/event' ; import LogType from 'constants/logType' ; import Notices from 'constants/notices' ; import Page from 'constants/page' ; import ReadyState from 'constants/readyState' ; import RitualType from 'constants/ritualType' ; import Status from 'constants/status' ; import Bttv from 'libs/Bttv' ; import Chatter from 'libs/Chatter' ; import EmotesProvider , { Emote , EmoteProviderPrefix } from 'libs/EmotesProvider' ; import Message , { SerializedMessage } from 'libs/Message' ; import Notice from 'libs/Notice' ; import Notification , { NotificationEvent } from 'libs/Notification' ; import RejectedMessage from 'libs/RejectedMessage' ; import Resources from 'libs/Resources' ; import Robotty from 'libs/Robotty' ; import RoomState from 'libs/RoomState' ; import Sound , { SoundId } from 'libs/Sound' ; import Twitch from 'libs/Twitch' ; import Ffz from 'libs/Ffz' ; import PubSub , { PubSubEvent } from 'libs/PubSub' ; import { resetAppState , setLastWhisperSender , updateEmotes , updateRoomState , updateStatus } from 'store/ducks/app' ; import { addChatter , addPotentialChatter , clearChatters , markChatterAsBanned , markChatterAsUnbanned , } from 'store/ducks/chatters' ; import { addLog , clearLogs , markRejectedMessageAsHandled , purgeLog , purgeLogs , unshiftLog } from 'store/ducks/logs' ; import { setModerator } from 'store/ducks/user' ; import { ApplicationState } from 'store/reducers' ; import { getChannel } from 'store/selectors/app' ; import { getChatters , getChattersMap } from 'store/selectors/chatters' ; import { getAutoHostThreshold , getDelayBetweenThrottledSounds , getHideVIPBadges , getHideWhispers , getHighlightAllMentions , getHighlights , getHighlightsIgnoredUsers , getHighlightsPermanentUsers , getHostThreshold , getPlayMessageSoundOnlyInOwnChannel , getSoundSettings , getTheme , } from 'store/selectors/settings' ; import { getChatLoginDetails , getIsMod } from 'store/selectors/user' ; /**
 * React State.
 */ const initialState = { error : undefined as Optional < Error > } ; type State = Readonly < typeof initialState > ; /**
 * Chat Component.
 */ export class ChatClient extends Component < Props , State > { public state : State = initialState ; public client : TwitchClient ; public nextWhisperRecipient : string | null = null ; private didFetchExternalResources = false ; private isFetchingHistoricalMessage = false ; param props constructor ( props ) { super ( props ) ; if ( _ . isNil ( props . loginDetails ) ) { this . state = { error : new Error ( 'No login details provided.' ) , } ; } this . client = tmi . client ( { channels : [ ] , connection : { reconnect : true , secure : true } , identity : props . loginDetails || undefined , options : { clientId : process . env . REACT_APP_TWITCH_CLIENT_ID , debug : false } , } ) ; } /**
     * Lifecycle: componentDidMount.
     */ public async componentDidMount ( ) { if ( ! _ . isNil ( this . state . error ) ) { return ; } const { channel , loginDetails } = this . props ; if ( _ . isNil ( channel ) ) { this . setState ( ( ) => ( { error : new Error ( 'No channel provided.' ) } ) ) ; return ; } this . updateHighlights ( ) ; this . updateSoundVolumes ( ) ; this . updateDelayBetweenThrottledSounds ( ) ; this . client . on ( Event . Notices , this . onNotices ) ; this . client . on ( Event . UserNotices , this . onUserNotices ) ; this . client . on ( Event . Connecting , this . onConnecting ) ; this . client . on ( Event . Connected , this . onConnected ) ; this . client . on ( Event . Logon , this . onLogon ) ; this . client . on ( Event . Disconnected , this . onDisconnected ) ; this . client . on ( Event . Reconnect , this . onReconnect ) ; this . client . on ( Event . Roomstate , this . onRoomStateUpdate ) ; this . client . on ( Event . Clearchat , this . onClearChat ) ; this . client . on ( Event . FollowersOnly , this . onFollowersOnly ) ; this . client . on ( Event . EmoteOnly , this . onEmoteOnly ) ; this . client . on ( Event . Hosted , this . onHosted ) ; this . client . on ( Event . Hosting , this . onHosting ) ; this . client . on ( Event . Message , this . onMessage ) ; this . client . on ( Event . R9k , this . onR9k ) ; this . client . on ( Event . Slowmode , this . onSlowMode ) ; this . client . on ( Event . Subscribers , this . onSubscribers ) ; this . client . on ( Event . Unhost , this . onUnhost ) ; this . client . on ( Event . Mods , this . onMods ) ; this . client . on ( Event . Mod , this . onMod ) ; this . client . on ( Event . Unmod , this . onUnmod ) ; this . client . on ( Event . Ban , this . onIrcBan ) ; this . client . on ( Event . Timeout , this . onIrcTimeout ) ; this . client . on ( Event . Notice , this . onNotice ) ; this . client . on ( Event . Subscription , this . onSubscription ) ; this . client . on ( Event . ReSub , this . onResub ) ; this . client . on ( Event . SubGift , this . onSubGift ) ; this . client . on ( Event . Ritual , this . onRitual ) ; this . client . on ( Event . Raid , this . onRaid ) ; this . client . on ( Event . Cheer , this . onCheer ) ; this . client . on ( Event . EmoteSets , this . onEmoteSets ) ; this . client . on ( Event . MessageDeleted , this . onMessageDeleted ) ; PubSub . addHandler ( PubSubEvent . Ban , this . onPubSubBan ) ; PubSub . addHandler ( PubSubEvent . Unban , this . onPubSubUnban ) ; PubSub . addHandler ( PubSubEvent . Timeout , this . onPubSubTimeout ) ; PubSub . addHandler ( PubSubEvent . Untimeout , this . onPubSubUntimeout ) ; PubSub . addHandler ( PubSubEvent . AutomodRejected , this . onAutomodRejected ) ; PubSub . addHandler ( PubSubEvent . AutomodMessageRejected , this . onAutomodMessageRejected ) ; PubSub . addHandler ( PubSubEvent . AutomodMessageApproved , this . onAutomodMessageApproved ) ; PubSub . addHandler ( PubSubEvent . AutomodMessageDenied , this . onAutomodMessageDenied ) ; PubSub . addHandler ( PubSubEvent . ApprovedAutomodMessage , this . onApprovedAutomodMessage ) ; PubSub . addHandler ( PubSubEvent . DeniedAutomodMessage , this . onDeniedAutomodMessage ) ; PubSub . addHandler ( PubSubEvent . ExpiredAutomodMessage , this . onExpiredAutomodMessage ) ; try { await this . client . connect ( ) ; await this . client . join ( channel ) ; if ( ! _ . isNil ( loginDetails ) && loginDetails . username === channel ) { this . props . setModerator ( true ) ; } this . props . addPotentialChatter ( Chatter . createPotentialChatter ( channel ) . serialize ( ) ) ; } catch ( error ) { let theError ; if ( _ . isString ( error ) ) { theError = new Error ( error ) ; } else if ( _ . isError ( error ) ) { theError = error ; } else { theError = new Error ( 'Something went wrong.' ) ; } this . setState ( ( ) => ( { error : theError } ) ) ; } } param nextProps param nextState return public shouldComponentUpdate ( nextProps , nextState ) { return ( this . state . error !== nextState . error || this . props . highlights !== nextProps . highlights || this . props . highlightsIgnoredUsers . length !== nextProps . highlightsIgnoredUsers . length || this . props . highlightsPermanentUsers . length !== nextProps . highlightsPermanentUsers . length || this . props . highlightAllMentions !== nextProps . highlightAllMentions || this . props . delayBetweenThrottledSounds !== nextProps . delayBetweenThrottledSounds || ! _ . isEqual ( this . props . soundSettings , nextProps . soundSettings ) ) ; } param prevProps public componentDidUpdate ( prevProps ) { if ( prevProps . highlights !== this . props . highlights || prevProps . highlightsIgnoredUsers . length !== this . props . highlightsIgnoredUsers . length || prevProps . highlightsPermanentUsers . length !== this . props . highlightsPermanentUsers . length || prevProps . highlightAllMentions !== this . props . highlightAllMentions ) { this . updateHighlights ( ) ; } if ( ! _ . isEqual ( prevProps . soundSettings , this . props . soundSettings ) ) { this . updateSoundVolumes ( ) ; } if ( prevProps . delayBetweenThrottledSounds !== this . props . delayBetweenThrottledSounds ) { this . updateDelayBetweenThrottledSounds ( ) ; } } /**
     * Lifecycle: componentWillUnmount.
     */ public async componentWillUnmount ( ) { this . didFetchExternalResources = false ; this . props . setModerator ( false ) ; this . props . resetAppState ( ) ; this . props . clearLogs ( ) ; this . props . clearChatters ( ) ; try { if ( this . client . readyState ( ) === ReadyState . Open ) { await this . client . disconnect ( ) ; } this . client . removeAllListeners ( ) ; PubSub . disconnect ( ) ; } catch { } } return public render ( ) { const { error } = this . state ; if ( ! _ . isNil ( error ) ) { if ( error . message . includes ( 'No response from Twitch' ) ) { if ( this . props . banned ) { return null ; } else { return < Redirect to = { Page . Home } />; } } throw error ; } return null ; } param channel param id param message private onNotices = ( _channel , id , message ) => { if ( _ . includes ( Notices . Extra , id ) ) { const notice = new Notice ( message , Event . Notice ) ; this . props . addLog ( notice . serialize ( ) ) ; } } ; param channel param id param message param tags private onUserNotices = ( _channel , id , _message , tags ) => { if ( id === Notices . ExtraUser . Charity ) { const notice = Notice . fromCharity ( tags ) ; this . props . addLog ( notice . serialize ( ) ) ; } else if ( id === Notices . ExtraUser . SubMysteryGift ) { const notification = Notification . fromSubMysteryGift ( tags ) ; this . props . addLog ( notification . serialize ( ) ) ; } else if ( id === Notices . ExtraUser . AnonSubGift ) { const notification = Notification . fromAnonSubGift ( tags ) ; this . props . addLog ( notification . serialize ( ) ) ; } else if ( id === Notices . ExtraUser . AnonSubMysteryGift ) { const notification = Notification . fromAnonSubMysteryGift ( tags ) ; this . props . addLog ( notification . serialize ( ) ) ; } else if ( id === Notices . ExtraUser . GiftPaidUpgrade ) { const notification = Notification . fromGiftPaidUpgrade ( tags ) ; this . props . addLog ( notification . serialize ( ) ) ; } else if ( id === Notices . ExtraUser . AnonGiftPaidUpgrade ) { const notification = Notification . fromAnonGiftPaidUpgrade ( tags ) ; this . props . addLog ( notification . serialize ( ) ) ; } else if ( id === Notices . ExtraUser . PrimePaidUpgrade ) { const notification = Notification . fromPrimePaidUpgrade ( tags ) ; this . props . addLog ( notification . serialize ( ) ) ; } else if ( id === Notices . ExtraUser . RewardGift ) { const notification = Notification . fromRewardGift ( tags ) ; this . props . addLog ( notification . serialize ( ) ) ; } else if ( id === Notices . ExtraUser . BitsBadgeTier ) { const notification = Notification . fromBitsBadgeTier ( tags ) ; this . props . addLog ( notification . serialize ( ) ) ; } else if ( id === Notices . ExtraUser . ExtendedSub ) { const notification = Notification . fromExtendedSub ( tags ) ; this . props . addLog ( notification . serialize ( ) ) ; } } ; /**
     * Triggered when connecting.
     */ private onConnecting = ( ) => { this . props . updateStatus ( Status . Connecting ) ; } ; /**
     * Triggered when connected.
     */ private onConnected = ( ) => { this . props . updateStatus ( Status . Connected ) ; } ; /**
     * Triggered during logon.
     */ private onLogon = ( ) => { this . props . updateStatus ( Status . Logon ) ; } ; /**
     * Triggered when disconnected.
     */ private onDisconnected = ( ) => { this . props . updateStatus ( Status . Disconnected ) ; } ; /**
     * Triggered when reconnecting.
     */ private onReconnect = ( ) => { this . props . updateStatus ( Status . Reconnecting ) ; } ; param channel param rawState private onRoomStateUpdate = async ( _channel , rawState ) => { const state = new RoomState ( rawState ) ; this . props . updateRoomState ( state . serialize ( ) ) ; this . fetchExternalResources ( state . roomId ) ; if ( ! PubSub . isConnected ( ) ) { PubSub . connect ( state . roomId ) ; } } ; /**
     * Triggered when the chat is cleared.
     */ private onClearChat = ( ) => { const notice = new Notice ( 'Chat was cleared by a moderator.' , Event . Clearchat ) ; this . props . addLog ( notice . serialize ( ) ) ; } ; param channel param enabled private onFollowersOnly = ( _channel , enabled ) => { const notice = new Notice ( enabled ? 'This room is in followers-only mode.' : 'This room is no longer in followers-only mode.' , Event . FollowersOnly ) ; this . props . addLog ( notice . serialize ( ) ) ; } ; param channel param enabled private onEmoteOnly = ( _channel , enabled ) => { const notice = new Notice ( enabled ? 'This room is now in emote-only mode.' : 'This room is no longer in emote-only mode.' , Event . EmoteOnly ) ; this . props . addLog ( notice . serialize ( ) ) ; } ; param channel param username param viewers param autohost private onHosted = ( channel , username , viewers , autohost ) => { if ( Twitch . sanitizeChannel ( channel ) !== this . props . channel || ( ! autohost && viewers < this . props . hostThreshold ) || ( autohost && viewers < this . props . autoHostThreshold ) ) { return ; } const notification = new Notification ( `${ username } is now ${ autohost ? 'auto-' : '' }hosting you for up to ${ viewers } viewers.` , NotificationEvent . Host ) ; const serializedNotification = notification . serialize ( ) ; this . props . addLog ( serializedNotification ) ; this . props . addPotentialChatter ( Chatter . createPotentialChatter ( username ) . serialize ( ) , serializedNotification . id ) ; } ; param channel param target param viewers private onHosting = ( _channel , target , viewers ) => { const notice = new Notice ( `Now hosting <a href="/${ target }">${ target }</a> for up to ${ viewers } viewers.` , Event . Hosting , true ) ; this . props . addLog ( notice . serialize ( ) ) ; } ; param channel param userstate param message param self param msgId private onMessage = async ( _channel , userstate , message , self , msgId ) => { if ( self && ! _ . isNil ( this . props . replyReference ) ) { this . setSelfReplyReference ( userstate ) ; message = `@${ this . props . replyReference . user . displayName } ${ message }` ; this . props . clearReply ( ) ; } const parsedMessage = this . parseRawMessage ( message , { ... userstate , 'msg-id' : msgId } , self ) ; if ( ! _ . isNil ( parsedMessage ) ) { if ( ! _ . isNil ( this . props . loginDetails ) && parsedMessage . user . userName === this . props . loginDetails . username && parsedMessage . user . isMod ) { this . props . setModerator ( true ) ; } const serializedMessage = parsedMessage . serialize ( ) ; if ( this . isFetchingHistoricalMessage && ! serializedMessage . historical ) { return ; } if ( ! serializedMessage . historical && ! Resources . manager ( ) . isBot ( serializedMessage . user . userName ) && _ . size ( serializedMessage . previews ) > 0 ) { const providers = Resources . manager ( ) . getPreviewProviders ( ) ; for ( const previewId in serializedMessage . previews ) { if ( serializedMessage . previews . hasOwnProperty ( previewId ) ) { const preview = serializedMessage . previews [ previewId ] ; if ( ! preview . resolved ) { const provider = providers [ preview . provider ] ; try { serializedMessage . previews [ previewId ] = await provider . resolve ( preview ) ; } catch { } } } } } if ( serializedMessage . type === LogType . Whisper && this . props . hideWhispers ) { return ; } this . props . addLog ( serializedMessage ) ; if ( ! serializedMessage . historical && ( serializedMessage . type === LogType . Chat || serializedMessage . type === LogType . Action ) ) { this . props . addChatter ( serializedMessage . user , serializedMessage . id ) ; const shouldPlayMentionSound = this . props . soundSettings [ SoundId . Mention ] . enabled && serializedMessage . mentioned ; if ( shouldPlayMentionSound ) { Sound . manager ( ) . play ( SoundId . Mention ) ; } if ( this . props . soundSettings [ SoundId . Message ] . enabled && ! shouldPlayMentionSound && ! self && ! Resources . manager ( ) . isBot ( userstate . username ) ) { if ( ! this . props . playMessageSoundOnlyInOwnChannel || ( this . props . playMessageSoundOnlyInOwnChannel && ! _ . isNil ( this . props . loginDetails ) && this . props . loginDetails . username === this . props . channel ) ) { Sound . manager ( ) . play ( SoundId . Message , true ) ; } } } if ( serializedMessage . type === LogType . Whisper && ! self ) { this . props . setLastWhisperSender ( serializedMessage . user . userName ) ; if ( this . props . soundSettings [ SoundId . Whisper ] . enabled ) { Sound . manager ( ) . play ( SoundId . Whisper ) ; } } } } ; param channel param enabled private onR9k = ( _channel , enabled ) => { const notice = new Notice ( enabled ? 'This room is now in unique-chat mode.' : 'This room is no longer in unique-chat mode.' , Event . R9k ) ; this . props . addLog ( notice . serialize ( ) ) ; } ; param channel param enabled param length private onSlowMode = ( _channel , enabled , length ) => { const notice = new Notice ( enabled ? `This room is now in slow mode. You may send messages every ${ length } seconds.` : 'This room is no longer in slow mode.' , Event . Slowmode ) ; this . props . addLog ( notice . serialize ( ) ) ; } ; param channel param enabled private onSubscribers = ( _channel , enabled ) => { const notice = new Notice ( enabled ? 'This room is now in subscriber-only mode.' : 'This room is no longer in subscriber-only mode.' , Event . Subscribers ) ; this . props . addLog ( notice . serialize ( ) ) ; } ; /**
     * Triggered when stoppping to host.
     */ private onUnhost = ( ) => { const notice = new Notice ( 'No longer hosting.' , Event . Unhost ) ; this . props . addLog ( notice . serialize ( ) ) ; } ; param channel param mods private onMods = ( _channel , mods ) => { _ . forEach ( mods , ( mod ) => { if ( ! _ . isNil ( this . props . loginDetails ) && mod === this . props . loginDetails . username ) { this . props . setModerator ( true ) ; } } ) ; if ( mods . length > 0 ) { const notice = new Notice ( `The moderators of this channel are: ${ mods . join ( ', ' ) }.` , Event . Mods ) ; this . props . addLog ( notice . serialize ( ) ) ; } } ; param channel param username private onMod = ( _channel , username ) => { if ( ! _ . isNil ( this . props . loginDetails ) && username === this . props . loginDetails . username ) { this . props . setModerator ( true ) ; } } ; param channel param username private onUnmod = ( channel , username ) => { const { loginDetails } = this . props ; if ( ! _ . isNil ( loginDetails ) && loginDetails . username !== channel && username === loginDetails . username ) { this . props . setModerator ( false ) ; } } ; param channel param id param username param message private onMessageDeleted = ( _channel , id , username , message ) => { this . props . purgeLog ( id ) ; if ( this . props . isMod ) { const notice = new Notice ( `A message from ${ username } has been deleted. Message: ${ message }` , Event . MessageDeleted ) ; this . props . addLog ( notice . serialize ( ) ) ; } } ; param channel param username param reason private onIrcBan = ( channel , username , reason ) => { if ( ! PubSub . isConnected ( ) || ! this . props . isMod ) { this . onBan ( channel , username , reason ) ; } } ; param author param username param reason private onPubSubBan = ( author , username , reason ) => { const { channel } = this . props ; if ( channel ) { this . onBan ( channel , username , reason ? reason : null , author ) ; } } ; param channel param username param reason param author private onBan = ( _channel , username , reason , author ? ) => { if ( this . props . isMod ) { let noticeMsg = `${ username } was banned` ; noticeMsg += ! _ . isEmpty ( author ) ? ` by ${ author }.` : '.' ; noticeMsg += ! _ . isEmpty ( reason ) ? ` Reason: ${ reason }.` : '' ; const notice = new Notice ( noticeMsg , Event . Ban ) ; this . props . addLog ( notice . serialize ( ) ) ; } const userId = _ . get ( this . props . chattersMap , username ) ; if ( ! _ . isNil ( userId ) ) { this . props . markChatterAsBanned ( userId ) ; const user = _ . get ( this . props . chatters , userId ) ; if ( user . logs . length > 0 ) { this . props . purgeLogs ( user . logs ) ; } } } ; param author param username private onPubSubUnban = ( author , username ) => { if ( this . props . isMod ) { const notice = new Notice ( `${ author } removed the ban on ${ username }.` , Event . Unban ) ; this . props . addLog ( notice . serialize ( ) ) ; } this . props . markChatterAsUnbanned ( username ) ; } ; param channel param username param reason param duration private onIrcTimeout = ( channel , username , reason , duration ) => { if ( ! PubSub . isConnected ( ) || ! this . props . isMod ) { this . onTimeout ( channel , username , reason , duration ) ; } } ; param author param username param duration param reason private onPubSubTimeout = ( author , username , duration , reason ) => { const { channel } = this . props ; if ( channel ) { this . onTimeout ( channel , username , reason ? reason : null , duration , author ) ; } } ; param channel param username param reason param duration param author private onTimeout = ( _channel , username , reason , duration , author ? ) => { if ( this . props . isMod ) { let noticeMsg = `${ username } was timed out` ; noticeMsg += ! _ . isEmpty ( author ) ? ` by ${ author }` : '' ; noticeMsg += ` for ${ duration } seconds.` ; noticeMsg += ! _ . isNil ( reason ) ? ` Reason: ${ reason }.` : '' ; const notice = new Notice ( noticeMsg , Event . Timeout ) ; this . props . addLog ( notice . serialize ( ) ) ; } const userId = _ . get ( this . props . chattersMap , username ) ; if ( ! _ . isNil ( userId ) ) { if ( duration > 1 ) { this . props . markChatterAsBanned ( userId ) ; } const user = _ . get ( this . props . chatters , userId ) ; if ( user . logs . length > 0 ) { this . props . purgeLogs ( user . logs ) ; } } } ; param author param username private onPubSubUntimeout = ( author , username ) => { if ( this . props . isMod ) { const notice = new Notice ( `${ author } removed the time out on ${ username }.` , Event . Untimeout ) ; this . props . addLog ( notice . serialize ( ) ) ; } this . props . markChatterAsUnbanned ( username ) ; } ; param username param messageId param message param reason private onAutomodRejected = ( username , messageId , message , reason ) => { const rejectedMessage = new RejectedMessage ( username , messageId , message , reason ) ; this . props . addLog ( rejectedMessage . serialize ( ) ) ; } ; param username private onAutomodMessageRejected = ( username ) => { const { loginDetails } = this . props ; if ( loginDetails && loginDetails . username . toLowerCase ( ) === username . toLowerCase ( ) ) { const notice = new Notice ( 'Your message is being checked by moderators before being sent.' , Event . AutomodMessageRejected ) ; this . props . addLog ( notice . serialize ( ) ) ; } } ; param username private onAutomodMessageApproved = ( username ) => { const { loginDetails } = this . props ; if ( loginDetails && loginDetails . username . toLowerCase ( ) === username . toLowerCase ( ) ) { const notice = new Notice ( 'Your message has been approved by moderators.' , Event . AutomodMessageApproved ) ; this . props . addLog ( notice . serialize ( ) ) ; } } ; param username private onAutomodMessageDenied = ( username ) => { const { loginDetails } = this . props ; if ( loginDetails && loginDetails . username . toLowerCase ( ) === username . toLowerCase ( ) ) { const notice = new Notice ( 'Your message has been removed by moderators.' , Event . AutomodMessageDenied ) ; this . props . addLog ( notice . serialize ( ) ) ; } } ; param messageId param username param moderator private onApprovedAutomodMessage = ( messageId , username , moderator ) => { if ( this . props . isMod ) { const notice = new Notice ( `The message from ${ username } was allowed by ${ moderator }.` , Event . ApprovedAutomodMessage ) ; this . props . addLog ( notice . serialize ( ) ) ; } this . props . markRejectedMessageAsHandled ( RejectedMessage . getRejectedMessageInternalId ( messageId ) ) ; } ; param messageId param username param moderator private onDeniedAutomodMessage = ( messageId , username , moderator ) => { if ( this . props . isMod ) { const notice = new Notice ( `The message from ${ username } was denied by ${ moderator }.` , Event . DeniedAutomodMessage ) ; this . props . addLog ( notice . serialize ( ) ) ; } this . props . markRejectedMessageAsHandled ( RejectedMessage . getRejectedMessageInternalId ( messageId ) ) ; } ; param messageId private onExpiredAutomodMessage = ( messageId ) => { this . props . markRejectedMessageAsHandled ( RejectedMessage . getRejectedMessageInternalId ( messageId ) ) ; } ; param channel param username param method param message private onSubscription = ( _channel , username , method , message , userstate ) => { const notification = new Notification ( `${ username } just subscribed${ this . getPaymentString ( method ) }!` , NotificationEvent . Subscription , this . parseSubMessage ( message , userstate ) ) ; const serializedNotification = notification . serialize ( ) ; this . props . addLog ( serializedNotification ) ; this . props . addPotentialChatter ( Chatter . createPotentialChatter ( username ) . serialize ( ) , serializedNotification . id ) ; } ; param message param userstate return private parseSubMessage ( message , userstate ) { let notificationMessage ; if ( ! _ . isNil ( message ) ) { const { hideVIPBadges , loginDetails , theme } = this . props ; userstate . username = userstate [ 'display-name' ] ; const parsedMessage = new Message ( message , userstate , false , loginDetails ! . username , { hideVIPBadge : hideVIPBadges , theme , } ) ; if ( ! _ . isNil ( parsedMessage ) ) { const serializedMessage = parsedMessage . serialize ( ) ; notificationMessage = serializedMessage . message ; } } return notificationMessage ; } param channel param username param months param message param userstate param method param monthsStreak private onResub = ( _channel , username , months , message , userstate , method , monthsStreak ) => { const streakMessage = ! _ . isNil ( monthsStreak ) && monthsStreak > 0 ? ` (${ monthsStreak } months streak)` : '' ; const notification = new Notification ( `${ username } just re-subscribed for ${ months } months${ streakMessage }${ this . getPaymentString ( method ) }!` , NotificationEvent . ReSub , this . parseSubMessage ( message , userstate ) ) ; const serializedNotification = notification . serialize ( ) ; this . props . addLog ( serializedNotification ) ; this . props . addPotentialChatter ( Chatter . createPotentialChatter ( username ) . serialize ( ) , serializedNotification . id ) ; } ; param channel param username param recipient private onSubGift = ( _channel , username , recipient , method ) => { let tierInfo = '' ; if ( method . plan === '2000' ) { tierInfo = ' tier 2' ; } else if ( method . plan === '3000' ) { tierInfo = ' tier 3' ; } const notification = new Notification ( `${ username } just gifted a${ tierInfo } sub to ${ recipient }!` , NotificationEvent . SubGift ) ; const serializedNotification = notification . serialize ( ) ; this . props . addLog ( serializedNotification ) ; this . props . addPotentialChatter ( Chatter . createPotentialChatter ( username ) . serialize ( ) , serializedNotification . id ) ; this . props . addPotentialChatter ( Chatter . createPotentialChatter ( recipient ) . serialize ( ) , serializedNotification . id ) ; } ; param ritual private onRitual = ( { username , type } ) => { if ( type === RitualType . NewChatter ) { const notification = new Notification ( `${ username } is new here! Say hi to ${ username }!` , NotificationEvent . RitualNewChatter ) ; const serializedNotification = notification . serialize ( ) ; this . props . addLog ( serializedNotification ) ; this . props . addPotentialChatter ( Chatter . createPotentialChatter ( username ) . serialize ( ) , serializedNotification . id ) ; } } ; param raid private onRaid = ( { raider , viewers } ) => { const notification = new Notification ( `${ raider } is raiding with a party of ${ viewers }!` , NotificationEvent . Raid ) ; const serializedNotification = notification . serialize ( ) ; this . props . addLog ( serializedNotification ) ; this . props . addPotentialChatter ( Chatter . createPotentialChatter ( raider ) . serialize ( ) , serializedNotification . id ) ; } ; param channel param userstate param message private onCheer = ( _channel , userstate , message ) => { const parsedMessage = this . parseRawMessage ( message , { ... userstate , 'message-type' : LogType . Cheer } , false ) ; if ( ! _ . isNil ( parsedMessage ) ) { const serializedMessage = parsedMessage . serialize ( ) ; this . props . addLog ( serializedMessage ) ; this . props . addChatter ( serializedMessage . user , serializedMessage . id ) ; } } ; param channel param id param message private onNotice = ( _channel , id , message ) => { if ( _ . includes ( Notices . IgnoredNotices , id ) ) { return ; } const notice = new Notice ( message , Event . Notice ) ; if ( ! PubSub . isConnected ( ) || ! _ . includes ( Notices . PubSubNotices , id ) ) { this . props . addLog ( notice . serialize ( ) ) ; } if ( _ . includes ( Notices . UnbanNotices , id ) ) { const username = _ . first ( message . split ( ' ' ) ) ; if ( ! _ . isNil ( username ) ) { this . props . markChatterAsUnbanned ( username ) ; } } else if ( id === Notices . BanNotice ) { this . props . markUserAsBanned ( ) ; const { channel } = this . props ; const notice = new Notice ( `You are unable to read or participate in ${ channel }'s channel until a moderator unbans you. You may be able to submit an appeal through <a href="https://www.twitch.tv/popout/${ channel }/chat?popout=" target="_blank">the official Twitch chat</a>.` , null , true ) ; this . props . addLog ( notice . serialize ( ) ) ; } } ; param setsList param sets private onEmoteSets = async ( _setsList , sets ) => { let emoticonsSetId = 0 ; let twitchEmotes = [ ] ; try { const { emoticon_sets } = await Twitch . fetchUserEmotes ( ) ; twitchEmotes = this . flattenEmoteSets ( emoticon_sets ) ; } catch ( error ) { } _ . forEach ( Emoticons , ( _emoticons , idStr ) => { const id = parseInt ( idStr , 10 ) ; if ( _ . has ( sets , id ) ) { emoticonsSetId = id ; } } ) ; Resources . manager ( ) . setEmoticonsSetId ( emoticonsSetId ) ; const emotes = EmotesProvider . sanitizeTwitchEmotes ( _ . unionBy ( this . flattenEmoteSets ( sets ) , twitchEmotes , 'code' ) ) ; const provider = new EmotesProvider ( EmoteProviderPrefix . Twitch , emotes , 'https://static-cdn.jtvnw.net/emoticons/v2/{{id}}/default/dark/{{image}}' , '.0' ) ; this . addEmotesProvider ( provider ) ; } ; param sets return private flattenEmoteSets ( sets ) { return _ . flatten ( _ . map ( sets , ( set ) => _ . filter ( set , ( emote ) => ! Resources . manager ( ) . isEmoticon ( emote . id ) ) ) ) ; } param payment return private getPaymentString ( payment ) { if ( payment . prime ) { return ' with Prime Gaming' ; } else if ( payment . plan === '2000' ) { return ' with a tier 2 sub' ; } else if ( payment . plan === '3000' ) { return ' with a tier 3 sub' ; } return '' ; } param provider private addEmotesProvider ( provider ) { const providers = Resources . manager ( ) . getEmotesProviders ( ) ; if ( providers . size > 1 ) { const allEmoteCodes = [ ] ; providers . forEach ( ( aProvider ) => { if ( aProvider . prefix !== provider . prefix ) { allEmoteCodes . push ( ... _ . map ( aProvider . emotes , 'code' ) ) ; } } ) ; provider . emotes = _ . filter ( provider . emotes , ( emote ) => { return ! _ . includes ( allEmoteCodes , emote . code ) ; } ) ; } Resources . manager ( ) . addEmotesProvider ( provider ) ; this . props . updateEmotes ( provider . prefix , provider . emotes ) ; } param channelId private async fetchExternalResources ( channelId ) { if ( this . didFetchExternalResources ) { return ; } try { if ( ! _ . isNil ( this . props . channel ) ) { try { this . isFetchingHistoricalMessage = true ; const recentMessages = await Robotty . fetchRecentMessages ( this . props . channel ) ; _ . forEach ( recentMessages , ( message ) => { this . client . _onMessage ( { data : message } ) ; } ) ; } catch ( error ) { } finally { this . isFetchingHistoricalMessage = false ; } } const channelInfos = await Twitch . fetchChannelInformations ( channelId ) ; if ( ! _ . isNil ( channelInfos . title ) && ! _ . isEmpty ( channelInfos . title ) ) { const notice = new Notice ( `Current title: ${ channelInfos . title }` , null , true ) ; this . props . unshiftLog ( notice . serialize ( ) ) ; } Resources . manager ( ) . setBadges ( await Twitch . fetchBadges ( channelId ) ) ; Resources . manager ( ) . setCheermotes ( ( await Twitch . fetchCheermotes ( channelId ) ) . data ) ; if ( ! _ . isNil ( this . props . channel ) ) { try { const emotesAndBots = await Bttv . fetchEmotesAndBots ( channelId ) ; this . addEmotesProvider ( emotesAndBots . emotes ) ; if ( ! _ . isNil ( emotesAndBots . bots ) ) { Resources . manager ( ) . addBots ( emotesAndBots . bots ) ; } } catch ( error ) { } try { const emotes = await Ffz . fetchEmotes ( this . props . channel ) ; this . addEmotesProvider ( emotes ) ; } catch ( error ) { } this . didFetchExternalResources = true ; } } catch { } } /**
     * Updates sound volumes.
     */ private updateSoundVolumes ( ) { Sound . manager ( ) . udpateVolumes ( this . props . soundSettings ) ; } /**
     * Updates the delay between throttled sounds.
     */ private updateDelayBetweenThrottledSounds ( ) { Sound . manager ( ) . updateDelayBetweenThrottledSounds ( this . props . delayBetweenThrottledSounds ) ; } /**
     * Updates highlights, highlights ignored users and highlights permanent users used when parsing messages.
     */ private updateHighlights ( ) { const { highlightAllMentions , highlights , highlightsIgnoredUsers , highlightsPermanentUsers } = this . props ; Resources . manager ( ) . setHighlights ( highlights , highlightsIgnoredUsers , highlightsPermanentUsers ) ; Resources . manager ( ) . setHighlightAllMentions ( highlightAllMentions ) ; } param message param userstate param self return private parseRawMessage ( message , userstate , self ) { let parsedMessage ; if ( userstate [ 'message-type' ] === LogType . Whisper && self && _ . isNil ( this . nextWhisperRecipient ) ) { return null ; } switch ( userstate [ 'message-type' ] ) { case LogType . Action : case LogType . Cheer : case LogType . Whisper : case LogType . Chat : { if ( userstate [ 'message-type' ] !== LogType . Whisper && self ) { userstate . id = nanoid ( ) ; userstate [ 'user-id' ] = 'self' ; userstate [ 'tmi-sent-ts' ] = Date . now ( ) . toString ( ) ; } else if ( userstate [ 'message-type' ] === LogType . Whisper ) { userstate . id = self ? nanoid ( ) : `${ userstate [ 'thread-id' ] }-${ userstate [ 'message-id' ] }` ; userstate [ 'tmi-sent-ts' ] = Date . now ( ) . toString ( ) ; if ( self ) { userstate [ 'user-id' ] = 'self' ; if ( ! _ . isNil ( this . nextWhisperRecipient ) ) { userstate [ 'display-name' ] = this . nextWhisperRecipient ; userstate . username = this . nextWhisperRecipient ; this . nextWhisperRecipient = null ; } } } const { channel , hideVIPBadges , loginDetails , theme } = this . props ; const hideVIPBadge = hideVIPBadges && ! _ . isNil ( loginDetails ) && loginDetails . username !== channel ; parsedMessage = new Message ( message , userstate , self , this . props . loginDetails ! . username , { hideVIPBadge , theme , } ) ; if ( _ . isNil ( parsedMessage . user . color ) ) { const user = _ . get ( this . props . chatters , parsedMessage . user . id ) ; parsedMessage . updateColor ( _ . isNil ( user ) || _ . isNil ( user . color ) ? parsedMessage . user . generateColor ( ) : user . color ) ; } break ; } default : { parsedMessage = null ; break ; } } return parsedMessage ; } param userstate private setSelfReplyReference ( userstate ) { const { replyReference } = this . props ; if ( _ . isNil ( replyReference ) ) { return ; } userstate [ 'reply-parent-display-name' ] = replyReference . user . displayName ; userstate [ 'reply-parent-msg-body' ] = replyReference . text ; userstate [ 'reply-parent-msg-id' ] = replyReference . id ; userstate [ 'reply-parent-user-id' ] = replyReference . user . id ; userstate [ 'reply-parent-user-login' ] = replyReference . user . userName ; } } export default connect < StateProps , DispatchProps , OwnProps , ApplicationState > ( ( state ) => ( { autoHostThreshold : getAutoHostThreshold ( state ) , channel : getChannel ( state ) , chatters : getChatters ( state ) , chattersMap : getChattersMap ( state ) , delayBetweenThrottledSounds : getDelayBetweenThrottledSounds ( state ) , hideVIPBadges : getHideVIPBadges ( state ) , hideWhispers : getHideWhispers ( state ) , highlightAllMentions : getHighlightAllMentions ( state ) , highlights : getHighlights ( state ) , highlightsIgnoredUsers : getHighlightsIgnoredUsers ( state ) , highlightsPermanentUsers : getHighlightsPermanentUsers ( state ) , hostThreshold : getHostThreshold ( state ) , isMod : getIsMod ( state ) , loginDetails : getChatLoginDetails ( state ) , playMessageSoundOnlyInOwnChannel : getPlayMessageSoundOnlyInOwnChannel ( state ) , soundSettings : getSoundSettings ( state ) , theme : getTheme ( state ) , } ) , { addChatter , addLog , addPotentialChatter , clearChatters , clearLogs , markChatterAsBanned , markChatterAsUnbanned , markRejectedMessageAsHandled , purgeLog , purgeLogs , resetAppState , setLastWhisperSender , setModerator , unshiftLog , updateEmotes , updateRoomState , updateStatus , } , null , { forwardRef : true } ) ( ChatClient ) ; /**
 * React Props.
 */ interface StateProps { autoHostThreshold : ReturnType < typeof getAutoHostThreshold > ; channel : ReturnType < typeof getChannel > ; chatters : ReturnType < typeof getChatters > ; chattersMap : ReturnType < typeof getChattersMap > ; delayBetweenThrottledSounds : ReturnType < typeof getDelayBetweenThrottledSounds > ; hideVIPBadges : ReturnType < typeof getHideVIPBadges > ; hideWhispers : ReturnType < typeof getHideWhispers > ; highlightAllMentions : ReturnType < typeof getHighlightAllMentions > ; highlights : ReturnType < typeof getHighlights > ; highlightsIgnoredUsers : ReturnType < typeof getHighlightsIgnoredUsers > ; highlightsPermanentUsers : ReturnType < typeof getHighlightsPermanentUsers > ; hostThreshold : ReturnType < typeof getHostThreshold > ; isMod : ReturnType < typeof getIsMod > ; loginDetails : ReturnType < typeof getChatLoginDetails > ; playMessageSoundOnlyInOwnChannel : ReturnType < typeof getPlayMessageSoundOnlyInOwnChannel > ; soundSettings : ReturnType < typeof getSoundSettings > ; theme : ReturnType < typeof getTheme > ; } /**
 * React Props.
 */ interface DispatchProps { addLog : typeof addLog ; addChatter : typeof addChatter ; addPotentialChatter : typeof addPotentialChatter ; clearChatters : typeof clearChatters ; clearLogs : typeof clearLogs ; markChatterAsBanned : typeof markChatterAsBanned ; markChatterAsUnbanned : typeof markChatterAsUnbanned ; markRejectedMessageAsHandled : typeof markRejectedMessageAsHandled ; purgeLog : typeof purgeLog ; purgeLogs : typeof purgeLogs ; resetAppState : typeof resetAppState ; setLastWhisperSender : typeof setLastWhisperSender ; setModerator : typeof setModerator ; unshiftLog : typeof unshiftLog ; updateRoomState : typeof updateRoomState ; updateEmotes : typeof updateEmotes ; updateStatus : typeof updateStatus ; } /**
 * React Props.
 */ interface OwnProps { banned : boolean ; clearReply : ( ) => void ; markUserAsBanned : ( ) => void ; replyReference ? : SerializedMessage ; } /**
 * React Props.
 */ type Props = StateProps & DispatchProps & OwnProps ;