2018-04-07 19:09:32 -04:00
'use strict' ;
// ============================================================================
// RichContent Component
// ============================================================================
import Module from 'utilities/module' ;
2019-08-13 15:58:01 -04:00
import { findReactFragment } from 'utilities/dom' ;
2021-12-16 15:00:14 -05:00
import { FFZEvent } from 'utilities/events' ;
import { getTwitchEmoteSrcSet , has , getTwitchEmoteURL } from 'utilities/object' ;
2021-06-17 14:27:04 -04:00
import { TWITCH _POINTS _SETS , TWITCH _GLOBAL _SETS , TWITCH _PRIME _SETS , KNOWN _CODES , REPLACEMENTS , REPLACEMENT _BASE , KEYS } from 'utilities/constants' ;
2020-07-01 19:07:17 -04:00
2018-04-07 19:09:32 -04:00
import Twilight from 'site' ;
2021-12-16 15:00:14 -05:00
2022-01-15 21:30:45 -05:00
// Prefer using these statically-allocated collators to String.localeCompare
const locale = Intl . Collator ( ) ;
const localeCaseInsensitive = Intl . Collator ( undefined , { sensitivity : 'accent' } ) ;
2022-01-17 15:57:44 -05:00
// Describes how an emote matches against a given input
// Higher values represent a more exact match
const NO _MATCH = 0 ;
2023-08-31 15:48:38 +02:00
const MATCH _ANY = 1 ;
2023-06-12 23:30:03 +03:00
const NON _PREFIX _MATCH = 2 ;
const CASE _INSENSITIVE _PREFIX _MATCH = 3 ;
const EXACT _PREFIX _MATCH = 4 ;
2022-01-17 15:57:44 -05:00
2021-12-16 15:00:14 -05:00
function getNodeText ( node ) {
if ( ! node )
return '' ;
if ( node . type === 'emote' )
return node . emoteName ;
if ( node . type === 'text' )
return node . text ;
if ( Array . isArray ( node . children ) )
return node . children . map ( getNodeText ) . join ( '' ) ;
return '' ;
}
function getNodeOffset ( nodes , path ) {
let offset = 0 , pidx = 0 , n = nodes ;
while ( pidx < path . length ) {
const p = path [ pidx ] ;
for ( let i = 0 ; i < p ; i ++ )
offset += getNodeText ( n [ i ] ) . length ;
n = Array . isArray ( n [ p ] ) ? n [ p ] : n [ p ] ? . children ;
pidx ++ ;
}
return offset ;
}
2019-05-07 15:04:12 -04:00
export default class Input extends Module {
2018-04-07 19:09:32 -04:00
constructor ( ... args ) {
super ( ... args ) ;
this . inject ( 'chat' ) ;
2019-05-07 15:04:12 -04:00
this . inject ( 'chat.actions' ) ;
2018-04-07 19:09:32 -04:00
this . inject ( 'chat.emotes' ) ;
2018-04-12 20:30:00 -04:00
this . inject ( 'chat.emoji' ) ;
2018-04-07 19:09:32 -04:00
this . inject ( 'i18n' ) ;
this . inject ( 'settings' ) ;
this . inject ( 'site.fine' ) ;
this . inject ( 'site.web_munch' ) ;
// Settings
2023-06-26 13:11:27 -04:00
this . settings . add ( 'chat.hype.display-input' , {
default : true ,
ui : {
path : 'Chat > Hype Chat >> Input' ,
title : 'Allow the Hype Chat button to appear in the chat input element.' ,
component : 'setting-check-box'
}
} ) ;
2023-03-10 17:06:12 -05:00
this . settings . add ( 'chat.inline-preview.enabled' , {
default : true ,
ui : {
path : 'Chat > Input >> Appearance' ,
title : 'Display in-line previews of FrankerFaceZ emotes when entering a chat message.' ,
2023-03-27 18:50:32 -04:00
description : '**Note:** This feature is temperamental. It may not display all emotes, and emote effects and overlay emotes are not displayed correctly. Once this setting has been enabled, it cannot be reasonably disabled and will remain active until you refresh the page.' ,
2023-03-10 17:06:12 -05:00
component : 'setting-check-box'
}
} ) ;
2019-07-31 17:13:56 -04:00
this . settings . add ( 'chat.mru.enabled' , {
default : true ,
ui : {
path : 'Chat > Input >> Recent Messages' ,
title : 'Allow pressing up and down to recall previously sent chat messages.' ,
component : 'setting-check-box'
}
} ) ;
2018-04-07 19:09:32 -04:00
this . settings . add ( 'chat.tab-complete.ffz-emotes' , {
default : true ,
ui : {
path : 'Chat > Input >> Tab Completion' ,
title : 'Allow tab-completion of FrankerFaceZ emotes.' ,
component : 'setting-check-box'
}
} ) ;
2018-04-12 20:30:00 -04:00
this . settings . add ( 'chat.tab-complete.emoji' , {
default : true ,
ui : {
path : 'Chat > Input >> Tab Completion' ,
title : 'Allow tab-completion of emoji.' ,
component : 'setting-check-box'
}
} ) ;
2019-06-28 04:54:58 +02:00
this . settings . add ( 'chat.tab-complete.emotes-without-colon' , {
default : false ,
ui : {
path : 'Chat > Input >> Tab Completion' ,
title : 'Allow tab-completion of emotes without typing a colon. (:)' ,
description : 'This will prevent the tab-completion of usernames without the @ prefix.' ,
component : 'setting-check-box'
}
} ) ;
this . settings . add ( 'chat.tab-complete.limit-results' , {
default : true ,
ui : {
path : 'Chat > Input >> Tab Completion' ,
title : 'Limit tab-completion results to 25.' ,
component : 'setting-check-box'
}
} ) ;
2019-07-31 22:25:21 +02:00
this . settings . add ( 'chat.tab-complete.prioritize-favorites' , {
default : false ,
ui : {
path : 'Chat > Input >> Tab Completion' ,
title : 'Prioritize favorite emotes at the top.' ,
component : 'setting-check-box'
}
} ) ;
2022-01-20 21:48:43 -05:00
this . settings . add ( 'chat.tab-complete.prioritize-prefix-matches' , {
default : false ,
ui : {
path : 'Chat > Input >> Tab Completion' ,
title : 'Prioritize emotes that start with user input.' ,
component : 'setting-check-box'
}
} ) ;
2023-08-31 15:48:38 +02:00
this . settings . add ( 'chat.tab-complete.matching' , {
default : 1 ,
ui : {
path : 'Chat > Input >> Tab Completion' ,
title : 'Emote Matching Type' ,
description : '1: `ppa` would match `Kappa`\n\n' +
'2: `sip` would match `cohhSip` but not `Gossip`\n\n' +
2023-09-09 18:10:27 -05:00
'3: `hol` would match `HolidayTree` but not `WholeWheat`' ,
2023-08-31 15:48:38 +02:00
component : 'setting-select-box' ,
data : [
{ value : 1 , title : '1: Anything (Twitch style)' } ,
{ value : 2 , title : '2: Non-Prefix (Old FFZ style)' } ,
2023-09-09 18:10:27 -05:00
{ value : 3 , title : '3: Prefix-Only (Case-Insensitive)' }
2023-08-31 15:48:38 +02:00
]
2023-09-09 17:43:51 -04:00
}
2023-08-31 15:48:38 +02:00
} ) ;
2018-04-07 19:09:32 -04:00
// Components
this . ChatInput = this . fine . define (
'chat-input' ,
2019-10-22 18:32:56 -04:00
n => n && n . setLocalChatInputRef && n . setLocalAutocompleteInputRef ,
2018-04-07 19:09:32 -04:00
Twilight . CHAT _ROUTES
) ;
this . EmoteSuggestions = this . fine . define (
'tab-emote-suggestions' ,
2022-05-18 17:58:46 -04:00
n => n && n . getMatches && n . autocompleteType === 'emote' ,
2018-04-07 19:09:32 -04:00
Twilight . CHAT _ROUTES
) ;
2019-06-28 04:54:58 +02:00
this . MentionSuggestions = this . fine . define (
'tab-mention-suggestions' ,
n => n && n . getMentions && n . renderMention ,
Twilight . CHAT _ROUTES
) ;
2019-07-31 22:25:21 +02:00
2020-08-12 16:10:06 -04:00
this . CommandSuggestions = this . fine . define (
'tab-cmd-suggestions' ,
n => n && n . getMatches && n . doesCommandMatchTerm ,
Twilight . CHAT _ROUTES
) ;
2019-07-31 22:25:21 +02:00
// Implement Twitch's unfinished emote usage object for prioritizing sorting
this . EmoteUsageCount = {
TriHard : 196568036 ,
Kappa : 192158118 ,
'4Head' : 155758710 ,
PogChamp : 151485090 ,
cmonBruh : 146352878 ,
BibleThump : 56472964 ,
WutFace : 45069031 ,
Kreygasm : 41387580 ,
DansGame : 38097659 ,
SMOrc : 34734484 ,
KappaPride : 34262839 ,
VoHiYo : 27886434 ,
SwiftRage : 24561900 ,
ResidentSleeper : 24438298 ,
EleGiggle : 19891526 ,
FailFish : 19118343 ,
NotLikeThis : 18802905 ,
Keepo : 18351415 ,
BabyRage : 18220906 ,
MingLee : 18026207 ,
HeyGuys : 14851569 ,
ANELE : 14648986 ,
PJSalt : 14438861
} ;
2018-04-07 19:09:32 -04:00
}
2018-05-18 17:48:10 -04:00
async onEnable ( ) {
2023-06-26 13:11:27 -04:00
this . chat . context . on ( 'changed:chat.hype.display-input' , ( ) => this . ChatInput . forceUpdate ( ) ) ;
2019-05-07 15:04:12 -04:00
this . chat . context . on ( 'changed:chat.actions.room' , ( ) => this . ChatInput . forceUpdate ( ) ) ;
2019-08-13 16:22:04 -04:00
this . chat . context . on ( 'changed:chat.actions.room-above' , ( ) => this . ChatInput . forceUpdate ( ) ) ;
2019-06-28 04:54:58 +02:00
this . chat . context . on ( 'changed:chat.tab-complete.emotes-without-colon' , enabled => {
for ( const inst of this . EmoteSuggestions . instances )
inst . canBeTriggeredByTab = enabled ;
for ( const inst of this . MentionSuggestions . instances )
inst . canBeTriggeredByTab = ! enabled ;
} ) ;
2019-05-07 15:04:12 -04:00
2023-03-10 17:06:12 -05:00
this . use _previews = this . chat . context . get ( 'chat.inline-preview.enabled' ) ;
this . chat . context . on ( 'changed:chat.inline-preview.enabled' , val => {
if ( this . use _previews )
return ;
this . use _previews = val ;
if ( val )
for ( const inst of this . ChatInput . instances ) {
this . installPreviewObserver ( inst ) ;
inst . ffzInjectEmotes ( ) ;
inst . forceUpdate ( ) ;
this . emit ( 'site:dom-update' , 'chat-input' , inst ) ;
}
} ) ;
2018-05-18 17:48:10 -04:00
const React = await this . web _munch . findModule ( 'react' ) ,
2018-04-07 19:09:32 -04:00
createElement = React && React . createElement ;
if ( ! createElement )
return this . log . warn ( 'Unable to get React.' ) ;
2019-05-07 15:04:12 -04:00
const t = this ;
2018-04-07 19:09:32 -04:00
this . ChatInput . ready ( ( cls , instances ) => {
2019-05-07 15:04:12 -04:00
const old _render = cls . prototype . render ;
cls . prototype . render = function ( ) {
const out = old _render . call ( this ) ;
2023-06-26 13:11:27 -04:00
2019-05-07 15:04:12 -04:00
try {
2023-06-26 13:11:27 -04:00
const hide _hype = ! t . chat . context . get ( 'chat.hype.display-input' ) ;
if ( hide _hype ) {
const frag = findReactFragment ( out , n => n . key === 'paidPinnedMessage' ) ;
if ( frag )
frag . type = ( ) => null ;
}
2019-08-13 16:22:04 -04:00
const above = t . chat . context . get ( 'chat.actions.room-above' ) ,
2019-08-27 16:18:12 -04:00
state = t . chat . context . get ( 'context.chat_state' ) || { } ,
2022-06-11 12:44:54 -04:00
container = above ? findReactFragment ( out , n => n . props && Array . isArray ( n . props . children ) ) : findReactFragment ( out , n => n . props && n . props . className === 'chat-input__buttons-container' ) ;
2019-08-13 16:22:04 -04:00
if ( ! container || ! container . props || ! container . props . children )
2019-05-07 15:04:12 -04:00
return out ;
const props = this . props ;
if ( ! props || ! props . channelID )
return out ;
const u = props . sessionUser ? {
id : props . sessionUser . id ,
login : props . sessionUser . login ,
displayName : props . sessionUser . displayName ,
mod : props . isCurrentUserModerator ,
staff : props . isStaff
} : null ,
r = {
id : props . channelID ,
login : props . channelLogin ,
2019-08-12 22:52:57 -04:00
displayName : props . channelDisplayName ,
emoteOnly : props . emoteOnlyMode ,
slowMode : props . slowMode ,
slowDuration : props . slowModeDuration ,
2019-08-27 16:18:12 -04:00
subsMode : props . subsOnlyMode ,
r9kMode : state . r9k ,
followersOnly : state . followersOnly ,
followersDuration : state . followersOnlyRequirement
2019-05-07 15:04:12 -04:00
}
2019-08-13 16:22:04 -04:00
const actions = t . actions . renderRoom ( t . chat . context . get ( 'context.chat.showModIcons' ) , u , r , above , createElement ) ;
if ( above )
container . props . children . unshift ( actions || null ) ;
else
container . props . children . splice ( 1 , 0 , actions || null ) ;
2019-05-07 15:04:12 -04:00
} catch ( err ) {
t . log . error ( err ) ;
t . log . capture ( err ) ;
}
return out ;
}
for ( const inst of instances ) {
inst . forceUpdate ( ) ;
2019-10-13 00:41:09 -04:00
this . emit ( 'site:dom-update' , 'chat-input' , inst ) ;
2018-04-07 19:09:32 -04:00
this . updateEmoteCompletion ( inst ) ;
2019-07-31 22:26:27 +02:00
this . overrideChatInput ( inst ) ;
2023-03-10 17:06:12 -05:00
inst . ffzInjectEmotes ( ) ;
this . installPreviewObserver ( inst ) ;
2019-05-07 15:04:12 -04:00
}
2018-04-07 19:09:32 -04:00
} ) ;
this . EmoteSuggestions . ready ( ( cls , instances ) => {
for ( const inst of instances )
this . overrideEmoteMatcher ( inst ) ;
} ) ;
2019-06-28 04:54:58 +02:00
this . MentionSuggestions . ready ( ( cls , instances ) => {
for ( const inst of instances )
this . overrideMentionMatcher ( inst ) ;
} ) ;
2020-08-12 16:10:06 -04:00
this . CommandSuggestions . ready ( ( cls , instances ) => {
for ( const inst of instances )
this . overrideCommandMatcher ( inst ) ;
} ) ;
2018-04-07 19:09:32 -04:00
this . ChatInput . on ( 'update' , this . updateEmoteCompletion , this ) ;
2019-07-31 22:26:27 +02:00
this . ChatInput . on ( 'mount' , this . overrideChatInput , this ) ;
2023-03-10 17:06:12 -05:00
this . ChatInput . on ( 'mount' , this . installPreviewObserver , this ) ;
this . ChatInput . on ( 'unmount' , this . removePreviewObserver , this ) ;
2018-04-07 19:09:32 -04:00
this . EmoteSuggestions . on ( 'mount' , this . overrideEmoteMatcher , this ) ;
2019-06-28 04:54:58 +02:00
this . MentionSuggestions . on ( 'mount' , this . overrideMentionMatcher , this ) ;
2020-08-12 16:10:06 -04:00
this . CommandSuggestions . on ( 'mount' , this . overrideCommandMatcher , this ) ;
2019-08-24 00:01:46 +02:00
2021-03-20 19:49:20 -04:00
this . chat . context . on ( 'changed:chat.emotes.animated' , this . uncacheTabCompletion , this ) ;
2021-04-27 16:23:19 -04:00
this . chat . context . on ( 'changed:chat.emotes.enabled' , this . uncacheTabCompletion , this ) ;
2023-09-09 17:43:51 -04:00
this . chat . context . on ( 'changed:chat.tab-complete.matching' , this . uncacheTabCompletion , this ) ;
2020-07-01 19:07:17 -04:00
this . on ( 'chat.emotes:change-hidden' , this . uncacheTabCompletion , this ) ;
this . on ( 'chat.emotes:change-set-hidden' , this . uncacheTabCompletion , this ) ;
this . on ( 'chat.emotes:change-favorite' , this . uncacheTabCompletion , this ) ;
this . on ( 'chat.emotes:update-default-sets' , this . uncacheTabCompletion , this ) ;
this . on ( 'chat.emotes:update-user-sets' , this . uncacheTabCompletion , this ) ;
this . on ( 'chat.emotes:update-room-sets' , this . uncacheTabCompletion , this ) ;
2019-08-23 18:13:35 -04:00
this . on ( 'site.css_tweaks:update-chat-css' , this . resizeInput , this ) ;
}
2020-07-01 19:07:17 -04:00
uncacheTabCompletion ( ) {
for ( const inst of this . EmoteSuggestions . instances ) {
inst . ffz _ffz _cache = null ;
inst . ffz _twitch _cache = null ;
}
2023-03-10 17:06:12 -05:00
if ( this . use _previews )
2023-03-30 14:54:33 -04:00
for ( const inst of this . ChatInput . instances ) {
2023-03-10 17:06:12 -05:00
inst . ffzInjectEmotes ( ) ;
2023-03-30 14:54:33 -04:00
inst . forceUpdate ( ) ;
this . emit ( 'site:dom-update' , 'chat-input' , inst ) ;
}
2020-07-01 19:07:17 -04:00
}
2020-03-31 18:14:27 -04:00
updateInput ( ) {
for ( const inst of this . ChatInput . instances ) {
if ( inst ) {
inst . forceUpdate ( ) ;
this . emit ( 'site:dom-update' , 'chat-input' , inst ) ;
}
}
}
2019-08-23 18:13:35 -04:00
resizeInput ( ) {
if ( this . _resize _waiter )
cancelAnimationFrame ( this . _resize _waiter ) ;
this . _resize _waiter = requestAnimationFrame ( ( ) => this . _resizeInput ( ) )
}
_resizeInput ( ) {
this . _resize _waiter = null ;
for ( const chat _input of this . ChatInput . instances )
chat _input . resizeInput ( ) ;
2018-04-07 19:09:32 -04:00
}
2023-03-10 17:06:12 -05:00
installPreviewObserver ( inst ) {
if ( inst . _ffz _preview _observer || ! window . MutationObserver )
return ;
if ( ! this . use _previews )
return ;
const el = this . fine . getHostNode ( inst ) ,
target = el && el . querySelector ( '.chat-input__textarea' ) ;
if ( ! target )
return ;
inst . _ffz _preview _observer = new MutationObserver ( mutations => {
for ( const mut of mutations ) {
//if ( mut.target instanceof Element )
// this.checkForPreviews(inst, mut.target);
for ( const node of mut . addedNodes ) {
if ( node instanceof Element )
this . checkForPreviews ( inst , node ) ;
}
}
} ) ;
inst . _ffz _preview _observer . observe ( target , {
childList : true ,
subtree : true ,
//attributeFilter: ['src']
} ) ;
}
checkForPreviews ( inst , node ) {
2023-05-19 15:02:25 -04:00
// We can't find the tooltip element directly (without digging into React tree at least)
// So instead just find the relevant images in the document. This shouldn't happen TOO
// frequently, with any luck, so the performance impact should be small.
if ( node . querySelector ? . ( 'span[data-a-target="chat-input-emote-preview"]' ) ) {
for ( const target of document . querySelectorAll ( '.tw-tooltip-layer img.chat-line__message--emote' ) ) {
if ( target && target . src . startsWith ( 'https://static-cdn.jtvnw.net/emoticons/v2/__FFZ__' ) )
this . updatePreview ( inst , target ) ;
}
}
// This no longer works because they removed aria-describedby
/ * f o r ( c o n s t e l o f n o d e . q u e r y S e l e c t o r A l l ? . ( ' s p a n [ d a t a - a - t a r g e t = " c h a t - i n p u t - e m o t e - p r e v i e w " ] [ a r i a - d e s c r i b e d b y ] ' ) ? ? [ ] ) {
2023-03-10 17:06:12 -05:00
const cont = document . getElementById ( el . getAttribute ( 'aria-describedby' ) ) ,
target = cont && cont . querySelector ( 'img.chat-line__message--emote' ) ;
if ( target && target . src . startsWith ( 'https://static-cdn.jtvnw.net/emoticons/v2/__FFZ__' ) )
this . updatePreview ( inst , target ) ;
2023-05-19 15:02:25 -04:00
} * /
2023-03-10 17:06:12 -05:00
for ( const target of node . querySelectorAll ? . ( 'img.chat-line__message--emote' ) ) {
if ( target && ( target . dataset . ffzId || target . src . startsWith ( 'https://static-cdn.jtvnw.net/emoticons/v2/__FFZ__' ) ) )
this . updatePreview ( inst , target ) ;
}
}
updatePreview ( inst , target ) {
let set _id = target . dataset . ffzSet ,
emote _id = target . dataset . ffzId ;
if ( ! emote _id ) {
const idx = target . src . indexOf ( '__FFZ__' , 49 ) ,
raw _id = target . src . slice ( 49 , idx ) ;
const raw _idx = raw _id . indexOf ( '::' ) ;
if ( raw _idx === - 1 )
return ;
set _id = raw _id . slice ( 0 , raw _idx ) ;
emote _id = raw _id . slice ( raw _idx + 2 ) ;
target . dataset . ffzSet = set _id ;
target . dataset . ffzId = emote _id ;
}
const emote _set = this . emotes . emote _sets [ set _id ] ,
emote = emote _set ? . emotes ? . [ emote _id ] ;
if ( ! emote )
return ;
const anim = this . chat . context . get ( 'chat.emotes.animated' ) > 0 ;
target . src = ( anim ? emote . animSrc : null ) ? ? emote . src ;
target . srcset = ( anim ? emote . animSrcSet : null ) ? ? emote . srcSet ;
const w = ` ${ emote . width } px ` ;
const h = ` ${ emote . height } px ` ;
target . style . width = w ;
target . style . height = h ;
// Find the parent.
const cont = target . closest ( '.chat-image__container' ) ;
if ( cont ) {
cont . style . width = w ;
cont . style . height = h ;
const outer = cont . closest ( '.chat-line__message--emote-button' ) ;
if ( outer ) {
outer . style . width = w ;
outer . style . height = h ;
2023-03-30 14:54:33 -04:00
if ( ! outer . _ffz _click _handler ) {
outer . _ffz _click _handler = this . previewClick . bind ( this , emote . id , emote _set . id , emote . name ) ;
outer . addEventListener ( 'click' , outer . _ffz _click _handler ) ;
}
2023-03-10 17:06:12 -05:00
}
}
}
2023-03-30 14:54:33 -04:00
previewClick ( id , set , name , evt ) {
const fe = new FFZEvent ( {
provider : 'ffz' ,
id ,
set ,
name ,
source : evt
} ) ;
this . emit ( 'chat.emotes:click' , fe ) ;
if ( ! fe . defaultPrevented )
return ;
evt . preventDefault ( ) ;
evt . stopImmediatePropagation ( ) ;
}
2023-03-10 17:06:12 -05:00
removePreviewObserver ( inst ) {
if ( inst . _ffz _preview _observer ) {
inst . _ffz _preview _observer . disconnect ( ) ;
inst . _ffz _preview _observer = null ;
}
}
2018-04-07 19:09:32 -04:00
updateEmoteCompletion ( inst , child ) {
if ( ! child )
2019-03-26 17:37:00 -04:00
child = this . fine . searchTree ( inst , 'tab-emote-suggestions' , 50 ) ;
2018-04-07 19:09:32 -04:00
if ( ! child )
return ;
child . _ffz _user = inst . props . sessionUser ;
child . _ffz _channel _id = inst . props . channelID ;
child . _ffz _channel _login = inst . props . channelLogin ;
}
2019-07-31 22:26:27 +02:00
overrideChatInput ( inst ) {
if ( inst . _ffz _override )
return ;
2019-07-31 17:13:56 -04:00
2019-07-31 22:26:27 +02:00
const t = this ;
const originalOnKeyDown = inst . onKeyDown ,
2021-11-19 17:12:17 -05:00
originalOnMessageSend = inst . onMessageSend ,
old _resize = inst . resizeInput ;
2020-04-22 14:30:34 -04:00
2023-03-10 17:06:12 -05:00
const old _componentDidUpdate = inst . componentDidUpdate ;
inst . ffzInjectEmotes = function ( ) {
const idx = this . props . emotes . findIndex ( item => item ? . id === 'FrankerFaceZWasHere' ) ,
data = t . createFakeEmoteSet ( inst ) ;
if ( idx === - 1 && data )
this . props . emotes . push ( data ) ;
else if ( idx !== - 1 && data )
this . props . emotes . splice ( idx , 1 , data ) ;
else if ( idx !== - 1 && ! data )
this . props . emotes . splice ( idx , 1 ) ;
2023-03-30 14:54:33 -04:00
else
return ;
2023-03-31 23:15:49 -04:00
// TODO: Somehow update other React state to deal with our
// injected changes. Making a shallow copy of the array
// runs too frequently.
2023-03-10 17:06:12 -05:00
}
inst . componentDidUpdate = function ( props , ... args ) {
try {
if ( props . emotes !== this . props . emotes && Array . isArray ( this . props . emotes ) )
inst . ffzInjectEmotes ( ) ;
} catch ( err ) {
t . log . error ( 'Error updating emote autocompletion data.' , err ) ;
}
if ( old _componentDidUpdate )
old _componentDidUpdate . call ( this , props , ... args ) ;
}
2021-11-19 17:12:17 -05:00
inst . resizeInput = function ( msg , ... args ) {
try {
if ( msg ) {
if ( inst . chatInputRef instanceof Element ) {
const style = getComputedStyle ( inst . chatInputRef ) ,
height = style && parseFloat ( style . lineHeight || 18 ) || 18 ,
2022-01-12 14:35:38 -05:00
t = height * 1 + 20.5 ,
2021-11-19 17:12:17 -05:00
i = Math . ceil ( ( inst . chatInputRef . scrollHeight - t ) / height ) ,
a = Math . min ( 1 + i , 4 ) ;
inst . setState ( {
numInputRows : a
} ) ;
}
} else
2020-04-22 14:30:34 -04:00
inst . setState ( {
2021-11-19 17:12:17 -05:00
numInputRows : 1
2020-04-22 14:30:34 -04:00
} ) ;
2021-11-19 17:12:17 -05:00
} catch ( err ) {
t . log . error ( 'Error in resizeInput' , err ) ;
return old _resize . call ( this , msg , ... args ) ;
}
2020-04-22 14:30:34 -04:00
}
2019-07-31 22:26:27 +02:00
2019-07-31 17:13:56 -04:00
inst . messageHistory = [ ] ;
inst . tempInput = '' ;
inst . messageHistoryPos = - 1 ;
2021-12-16 15:00:14 -05:00
inst . ffzGetValue = function ( ) {
if ( inst . chatInputRef && typeof inst . chatInputRef . value === 'string' )
return inst . chatInputRef . value ;
if ( inst . state . value && typeof inst . state . value === 'string' )
return inst . state . value ;
return '' ;
}
inst . ffzGetSelection = function ( ) {
if ( typeof inst . chatInputRef ? . selectionEnd === 'number' ) {
return [ inst . chatInputRef . selectionStart , inst . chatInputRef . selectionEnd ]
}
if ( inst . chatInputRef ? . state ? . slateEditor ) {
const editor = inst . chatInputRef . state . slateEditor ,
sel = editor . selection ,
nodes = editor . children ;
if ( ! sel ? . anchor ? . path || ! sel ? . focus ? . path )
return [ 0 , 0 ] ;
const first = getNodeOffset ( nodes , sel . anchor . path ) + sel . anchor . offset ,
second = getNodeOffset ( nodes , sel . focus . path ) + sel . focus . offset ;
if ( first < second )
return [ first , second ] ;
else
return [ second , first ] ;
}
return [ 0 , 0 ] ;
}
inst . ffzSetSelection = function ( start , end ) {
if ( inst . chatInputRef ? . setSelectionRange )
inst . chatInputRef . setSelectionRange ( start , end ) ;
}
2019-07-31 22:26:27 +02:00
inst . onKeyDown = function ( event ) {
2019-07-31 17:13:56 -04:00
try {
2020-08-12 16:10:06 -04:00
const code = event . charCode || event . keyCode ;
2020-07-01 19:07:17 -04:00
if ( inst . onEmotePickerToggle && t . chat . context . get ( 'chat.emote-menu.shortcut' ) && event . key === 'e' && event . ctrlKey && ! event . altKey && ! event . shiftKey ) {
inst . onEmotePickerToggle ( ) ;
event . preventDefault ( ) ;
return ;
}
2021-12-16 15:00:14 -05:00
const val = inst . ffzGetValue ( ) ;
if ( inst . autocompleteInputRef && inst . chatInputRef && t . chat . context . get ( 'chat.mru.enabled' ) && ! event . shiftKey && ! event . ctrlKey && ! event . altKey ) {
const sel = inst . ffzGetSelection ( ) ;
2019-07-31 17:13:56 -04:00
// Arrow Up
2021-12-16 15:00:14 -05:00
if ( code === 38 && sel [ 0 ] === 0 && sel [ 1 ] === 0 ) {
2019-07-31 17:13:56 -04:00
if ( ! inst . messageHistory . length )
return ;
2021-12-16 15:00:14 -05:00
if ( val && inst . messageHistoryPos === - 1 )
inst . tempInput = val ;
2019-07-31 17:13:56 -04:00
if ( inst . messageHistoryPos < inst . messageHistory . length - 1 ) {
inst . messageHistoryPos ++ ;
inst . autocompleteInputRef . setValue ( inst . messageHistory [ inst . messageHistoryPos ] ) ;
2021-12-16 15:00:14 -05:00
inst . ffzSetSelection ( 0 ) ;
2019-07-31 17:13:56 -04:00
}
2019-07-31 22:26:27 +02:00
return ;
2019-07-31 17:13:56 -04:00
// Arrow Down
2021-12-16 15:00:14 -05:00
} else if ( code === 40 && sel [ 0 ] >= val . length && sel [ 1 ] === sel [ 0 ] ) {
2019-07-31 17:13:56 -04:00
if ( ! inst . messageHistory . length )
return ;
if ( inst . messageHistoryPos > 0 ) {
inst . messageHistoryPos -- ;
inst . autocompleteInputRef . setValue ( inst . messageHistory [ inst . messageHistoryPos ] ) ;
2021-12-16 15:00:14 -05:00
inst . ffzSetSelection ( inst . messageHistory [ inst . messageHistoryPos ] . length ) ;
2019-07-31 17:13:56 -04:00
} else if ( inst . messageHistoryPos === 0 ) {
inst . autocompleteInputRef . setValue ( inst . tempInput ) ;
2021-12-16 15:00:14 -05:00
inst . ffzSetSelection ( inst . tempInput . length ) ;
2019-07-31 17:13:56 -04:00
inst . messageHistoryPos = - 1 ;
}
return ;
2019-07-31 22:26:27 +02:00
}
}
2019-07-31 17:13:56 -04:00
2020-08-12 16:10:06 -04:00
// Let users close stuff with Escape.
if ( code === KEYS . Escape && ! event . shiftKey && ! event . ctrlKey && ! event . altKey ) {
if ( inst . props . isShowingEmotePicker )
inst . props . closeEmotePicker ( ) ;
else if ( inst . props . tray && ( ! inst . state . value || ! inst . state . value . length ) )
inst . closeTray ( ) ;
}
2019-07-31 17:13:56 -04:00
} catch ( err ) {
t . log . capture ( err ) ;
t . log . error ( err ) ;
2019-07-31 22:26:27 +02:00
}
2019-07-31 17:13:56 -04:00
originalOnKeyDown . call ( this , event ) ;
2019-07-31 22:26:27 +02:00
}
inst . onMessageSend = function ( event ) {
2019-07-31 17:13:56 -04:00
try {
if ( t . chat . context . get ( 'chat.mru.enabled' ) ) {
2021-12-16 15:00:14 -05:00
const val = inst . ffzGetValue ( ) ;
if ( val && val . length ) {
if ( ! inst . messageHistory . length || inst . messageHistory [ 0 ] !== val ) {
inst . messageHistory . unshift ( val ) ;
inst . messageHistory = inst . messageHistory . slice ( 0 , 20 ) ;
}
2019-07-31 17:13:56 -04:00
}
inst . messageHistoryPos = - 1 ;
inst . tempInput = '' ;
}
} catch ( err ) {
t . log . capture ( err ) ;
t . log . error ( err ) ;
2019-07-31 22:26:27 +02:00
}
originalOnMessageSend . call ( this , event ) ;
}
}
2018-04-07 19:09:32 -04:00
2019-06-28 04:54:58 +02:00
overrideMentionMatcher ( inst ) {
2020-08-12 16:10:06 -04:00
inst . canBeTriggeredByTab = ! this . chat . context . get ( 'chat.tab-complete.emotes-without-colon' ) ;
}
overrideCommandMatcher ( inst ) {
2019-06-28 04:54:58 +02:00
if ( inst . _ffz _override )
return ;
2020-08-12 16:10:06 -04:00
inst . _ffz _override = true ;
inst . oldCommands = inst . getCommands ;
const t = this ;
inst . getCommands = function ( input ) { try {
const commands = inst . props . getCommands ( inst . props . permissionLevel , {
isEditor : inst . props . isCurrentUserEditor
} ) ;
const event = new FFZEvent ( {
input ,
permissionLevel : inst . props . permissionLevel ,
isEditor : inst . props . isCurrentUserEditor ,
commands
} ) ;
t . emit ( 'chat:get-tab-commands' , event ) ;
if ( ! commands || ! commands . length )
return null ;
// Trim off the starting /
const i = input . slice ( 1 ) ;
const sorted = commands . filter ( cmd => inst . doesCommandMatchTerm ( cmd , i ) ) . sort ( inst . sortCommands ) ;
const out = [ ] ;
for ( const cmd of sorted ) {
const arg = cmd . commandArgs ? . [ 0 ] ;
let selection ;
if ( arg ? . isRequired )
selection = ` [ ${ arg . name } ] ` ;
out . push ( {
current : input ,
replacement : inst . determineReplacement ( cmd ) ,
element : inst . renderCommandSuggestion ( cmd , i ) ,
group : cmd . ffz _group ?
( Array . isArray ( cmd . ffz _group ) ? t . i18n . t ( ... cmd . ffz _group ) : cmd . ffz _group )
: inst . determineGroup ( cmd ) ,
selection
} ) ;
}
return out ;
} catch ( err ) {
console . error ( err ) ;
return inst . oldCommands ( input ) ;
} }
2019-06-28 04:54:58 +02:00
}
2023-03-10 17:06:12 -05:00
createFakeEmoteSet ( inst ) {
if ( ! this . use _previews )
return null ;
if ( ! inst . _ffz _channel _login ) {
const parent = this . fine . searchParent ( inst , 'chat-input' , 50 ) ;
if ( parent )
this . updateEmoteCompletion ( parent , inst ) ;
}
const user = inst . _ffz _user ,
channel _id = inst . _ffz _channel _id ,
channel _login = inst . _ffz _channel _login ;
if ( ! channel _login )
return null ;
const sets = this . emotes . getSets ( user ? . id , user ? . login , channel _id , channel _login ) ;
if ( ! sets || ! sets . length )
return null ;
const out = [ ] ,
added _emotes = new Set ;
for ( const set of sets ) {
if ( ! set || ! set . emotes )
continue ;
const source = set . source || 'ffz' ;
for ( const emote of Object . values ( set . emotes ) ) {
if ( ! emote || ! emote . id || ! emote . name || added _emotes . has ( emote . name ) )
continue ;
added _emotes . add ( emote . name ) ;
out . push ( {
id : ` __FFZ__ ${ set . id } :: ${ emote . id } __FFZ__ ` ,
modifiers : null ,
setID : 'FrankerFaceZWasHere' ,
token : emote . name
} ) ;
}
}
return {
_ _typename : 'EmoteSet' ,
emotes : out ,
id : 'FrankerFaceZWasHere' ,
owner : null
}
}
2018-04-07 19:09:32 -04:00
overrideEmoteMatcher ( inst ) {
if ( inst . _ffz _override )
return ;
2019-06-28 04:54:58 +02:00
const t = this ;
inst . canBeTriggeredByTab = this . chat . context . get ( 'chat.tab-complete.emotes-without-colon' ) ;
inst . getMatches = function ( input , pressedTab ) {
return pressedTab
? input . length < 2 ? null : inst . getMatchedEmotes ( input )
: input . startsWith ( ':' ) ? input . length < 3 ? null : inst . getMatchedEmotes ( input ) : null ;
}
2018-04-07 19:09:32 -04:00
inst . doesEmoteMatchTerm = function ( emote , term ) {
2020-07-10 20:08:29 -04:00
const emote _name = emote . name || emote . token ;
if ( ! emote _name )
2022-01-17 15:57:44 -05:00
return NO _MATCH ;
if ( emote _name . startsWith ( term ) )
return EXACT _PREFIX _MATCH ;
2018-04-07 19:09:32 -04:00
2020-07-10 20:08:29 -04:00
let emote _lower = emote . tokenLower ;
if ( ! emote _lower )
emote _lower = emote _name . toLowerCase ( ) ;
const term _lower = term . toLowerCase ( ) ;
2018-04-07 19:09:32 -04:00
if ( emote _lower . startsWith ( term _lower ) )
2022-01-17 15:57:44 -05:00
return CASE _INSENSITIVE _PREFIX _MATCH ;
2018-04-07 19:09:32 -04:00
const idx = emote _name . indexOf ( term . charAt ( 0 ) . toUpperCase ( ) ) ;
2022-01-17 15:57:44 -05:00
if ( idx !== - 1 && emote _lower . slice ( idx + 1 ) . startsWith ( term _lower . slice ( 1 ) ) )
return NON _PREFIX _MATCH ;
2020-07-10 20:08:29 -04:00
2023-06-12 23:30:03 +03:00
if ( emote _lower . includes ( term _lower ) )
2023-08-31 15:48:38 +02:00
return MATCH _ANY ;
2023-06-12 23:30:03 +03:00
2022-01-17 15:57:44 -05:00
return NO _MATCH ;
2018-04-07 19:09:32 -04:00
}
inst . getMatchedEmotes = function ( input ) {
2021-04-27 16:23:19 -04:00
const setting = t . chat . context . get ( 'chat.emotes.enabled' ) ;
2019-06-28 04:54:58 +02:00
const limitResults = t . chat . context . get ( 'chat.tab-complete.limit-results' ) ;
2021-04-27 16:23:19 -04:00
let results = setting ? t . getTwitchEmoteSuggestions ( input , this ) : [ ] ;
2018-04-12 20:30:00 -04:00
2021-04-27 16:23:19 -04:00
if ( setting > 1 && t . chat . context . get ( 'chat.tab-complete.ffz-emotes' ) ) {
2019-02-12 17:39:54 -05:00
const ffz _emotes = t . getEmoteSuggestions ( input , this ) ;
if ( Array . isArray ( ffz _emotes ) && ffz _emotes . length )
2019-06-30 15:05:05 -04:00
results = results . concat ( ffz _emotes ) ;
2019-02-12 17:39:54 -05:00
}
2018-04-12 20:30:00 -04:00
2019-08-13 10:42:37 +08:00
if ( t . chat . context . get ( 'chat.tab-complete.emoji' ) ) {
const emoji = t . getEmojiSuggestions ( input , this ) ;
if ( Array . isArray ( emoji ) && emoji . length )
results = Array . isArray ( results ) ? results . concat ( emoji ) : emoji ;
}
2019-02-12 17:39:54 -05:00
2022-01-17 16:30:14 -05:00
results = t . sortEmotes ( results ) ;
2019-06-28 04:54:58 +02:00
return limitResults && results . length > 25 ? results . slice ( 0 , 25 ) : results ;
2018-04-12 20:30:00 -04:00
}
const React = this . web _munch . getModule ( 'react' ) ,
createElement = React && React . createElement ;
inst . renderFFZEmojiSuggestion = function ( data ) {
2019-02-12 17:39:54 -05:00
return ( < React.Fragment >
2019-08-05 23:12:50 +02:00
< div class = "tw-relative tw-flex-shrink-0 tw-pd-05" title = { data . token } favorite = { data . favorite } >
2018-04-12 20:30:00 -04:00
< img
class = "emote-autocomplete-provider__image ffz-emoji"
src = { data . src }
srcSet = { data . srcset }
/ >
2019-07-31 22:25:21 +02:00
{ data . favorite && < figure class = "ffz--favorite ffz-i-star" / > }
2019-02-12 17:39:54 -05:00
< / div >
2019-07-31 22:25:21 +02:00
< div class = "tw-ellipsis" title = { data . token } >
2018-04-12 20:30:00 -04:00
{ data . token }
< / div >
2019-02-12 17:39:54 -05:00
< / React.Fragment > ) ;
2018-04-07 19:09:32 -04:00
}
2019-07-31 22:25:21 +02:00
inst . renderEmoteSuggestion = function ( emote ) {
return ( < React.Fragment >
2019-08-05 23:12:50 +02:00
< div class = "tw-relative tw-flex-shrink-0 tw-pd-05" title = { emote . token } favorite = { emote . favorite } >
2019-07-31 22:25:21 +02:00
< img
class = "emote-autocomplete-provider__image"
srcSet = { emote . srcSet }
/ >
{ emote . favorite && < figure class = "ffz--favorite ffz-i-star" / > }
< / div >
< div class = "tw-ellipsis" title = { emote . token } >
{ emote . token }
< / div >
< / React.Fragment > ) ;
}
}
// eslint-disable-next-line class-methods-use-this
2022-01-17 16:30:14 -05:00
sortEmotes ( emotes ) {
2022-01-12 12:12:35 -05:00
const preferFavorites = this . chat . context . get ( 'chat.tab-complete.prioritize-favorites' ) ;
2022-01-20 21:03:27 -05:00
const canBeTriggeredByTab = this . chat . context . get ( 'chat.tab-complete.emotes-without-colon' ) ;
2022-01-20 21:48:43 -05:00
const prioritizePrefixMatches = this . chat . context . get ( 'chat.tab-complete.prioritize-prefix-matches' ) ;
2019-07-31 22:25:21 +02:00
2022-01-12 12:12:35 -05:00
return emotes . sort ( ( a , b ) => {
2022-01-20 21:03:27 -05:00
const aStr = a . matched || a . replacement ;
const bStr = b . matched || b . replacement ;
2022-01-12 12:12:35 -05:00
// Prefer favorites over non-favorites, if enabled
if ( preferFavorites && ( a . favorite ^ b . favorite ) )
return 0 - a . favorite + b . favorite ;
2022-01-20 21:48:43 -05:00
if ( prioritizePrefixMatches ) {
// Prefer emoji over emotes if tab-complete is enabled, disprefer them otherwise
const aIsEmoji = ! ! a . matched ;
const bIsEmoji = ! ! b . matched ;
if ( aIsEmoji ^ bIsEmoji ) {
if ( canBeTriggeredByTab ) return 0 - aIsEmoji + bIsEmoji ;
else return 0 - bIsEmoji + aIsEmoji ;
}
2019-07-31 22:25:21 +02:00
2022-01-20 21:48:43 -05:00
// Prefer case-sensitive prefix matches
const aStartsWithInput = ( a . match _type === EXACT _PREFIX _MATCH ) ;
const bStartsWithInput = ( b . match _type === EXACT _PREFIX _MATCH ) ;
if ( aStartsWithInput && bStartsWithInput )
return locale . compare ( aStr , bStr ) ;
else if ( aStartsWithInput ) return - 1 ;
else if ( bStartsWithInput ) return 1 ;
// Else prefer case-insensitive prefix matches
const aStartsWithInputCI = ( a . match _type === CASE _INSENSITIVE _PREFIX _MATCH ) ;
const bStartsWithInputCI = ( b . match _type === CASE _INSENSITIVE _PREFIX _MATCH ) ;
if ( aStartsWithInputCI && bStartsWithInputCI )
return localeCaseInsensitive . compare ( aStr , bStr ) ;
else if ( aStartsWithInputCI ) return - 1 ;
else if ( bStartsWithInputCI ) return 1 ;
// Else alphabetize
2022-01-12 12:12:35 -05:00
return locale . compare ( aStr , bStr ) ;
2019-07-31 22:25:21 +02:00
}
2022-01-20 21:48:43 -05:00
// Keep unsorted order for non-favorite items if prefix matching is not enabled.
return 0 ;
2019-07-31 22:25:21 +02:00
} ) ;
2018-04-07 19:09:32 -04:00
}
2019-06-27 23:19:05 -04:00
2020-07-01 19:07:17 -04:00
buildTwitchCache ( emotes ) {
if ( ! Array . isArray ( emotes ) )
return { emotes : [ ] , length : 0 } ;
const out = [ ] ,
2022-12-18 17:30:34 -05:00
seen = new Set ,
2021-06-18 14:27:14 -04:00
anim = this . chat . context . get ( 'chat.emotes.animated' ) > 0 ,
2020-07-01 19:07:17 -04:00
hidden _sets = this . settings . provider . get ( 'emote-menu.hidden-sets' ) ,
has _hidden = Array . isArray ( hidden _sets ) && hidden _sets . length > 0 ,
hidden _emotes = this . emotes . getHidden ( 'twitch' ) ,
favorites = this . emotes . getFavorites ( 'twitch' ) ;
for ( const set of emotes ) {
2021-06-10 18:53:16 -04:00
const int _id = parseInt ( set . id , 10 ) ,
owner = set . owner ,
is _points = TWITCH _POINTS _SETS . includes ( int _id ) || owner ? . login === 'channel_points' ,
channel = is _points ? null : owner ;
2023-03-10 17:06:12 -05:00
// Skip this set.
if ( set . id === 'FrankerFaceZWasHere' )
continue ;
2021-06-10 18:53:16 -04:00
let key = ` twitch-set- ${ set . id } ` ;
let extra = null ;
if ( channel ? . login ) {
key = ` twitch- ${ channel . id } ` ;
extra = channel . displayName || channel . login ;
} else if ( is _points )
key = 'twitch-points' ;
else if ( TWITCH _GLOBAL _SETS . includes ( int _id ) )
key = 'twitch-global' ;
else if ( TWITCH _PRIME _SETS . includes ( int _id ) )
key = 'twitch-prime' ;
else
key = 'twitch-misc' ;
2020-07-01 19:07:17 -04:00
2021-06-10 18:53:16 -04:00
if ( has _hidden && hidden _sets . includes ( key ) )
continue ;
2020-07-01 19:07:17 -04:00
for ( const emote of set . emotes ) {
if ( ! emote || ! emote . id || hidden _emotes . includes ( emote . id ) )
continue ;
const id = emote . id ,
2020-07-10 20:08:29 -04:00
token = KNOWN _CODES [ emote . token ] || emote . token ;
2022-12-18 17:30:34 -05:00
if ( ! token || seen . has ( token ) )
2020-07-10 20:08:29 -04:00
continue ;
2020-07-01 19:07:17 -04:00
2022-12-18 17:30:34 -05:00
seen . add ( token ) ;
2020-07-10 20:08:29 -04:00
const replacement = REPLACEMENTS [ id ] ;
2021-06-18 14:27:14 -04:00
let srcSet ;
2020-07-01 19:07:17 -04:00
if ( replacement && this . chat . context . get ( 'chat.fix-bad-emotes' ) ) {
2021-06-17 14:27:04 -04:00
srcSet = ` ${ REPLACEMENT _BASE } ${ replacement } 1x ` ;
2021-06-18 14:27:14 -04:00
} else
srcSet = getTwitchEmoteSrcSet ( id , anim ) ;
2020-07-01 19:07:17 -04:00
out . push ( {
id ,
2021-06-10 18:53:16 -04:00
source : key ,
extra ,
2020-07-01 19:07:17 -04:00
setID : set . id ,
2020-07-10 20:08:29 -04:00
token ,
tokenLower : token . toLowerCase ( ) ,
2020-07-01 19:07:17 -04:00
srcSet ,
favorite : favorites . includes ( id )
} ) ;
}
}
return {
emotes : out ,
length : emotes . length
}
}
2019-06-28 04:54:58 +02:00
getTwitchEmoteSuggestions ( input , inst ) {
2020-07-01 19:07:17 -04:00
if ( inst . ffz _twitch _cache ? . length !== inst . props . emotes ? . length )
inst . ffz _twitch _cache = this . buildTwitchCache ( inst . props . emotes ) ;
2023-08-31 15:48:38 +02:00
const emoteMatchingType = this . chat . context . get ( 'chat.tab-complete.matching' ) ;
2020-07-01 19:07:17 -04:00
const emotes = inst . ffz _twitch _cache . emotes ;
if ( ! emotes . length )
2019-06-28 04:54:58 +02:00
return [ ] ;
2020-07-01 19:07:17 -04:00
const results _usage = [ ] ,
results _starting = [ ] ,
results _other = [ ] ,
2019-07-31 22:25:21 +02:00
2020-07-01 19:07:17 -04:00
search = input . startsWith ( ':' ) ? input . slice ( 1 ) : input ;
2019-06-30 20:55:53 +02:00
2020-07-01 19:07:17 -04:00
for ( const emote of emotes ) {
2022-01-17 15:57:44 -05:00
const match _type = inst . doesEmoteMatchTerm ( emote , search ) ;
2023-08-31 15:48:38 +02:00
if ( match _type < emoteMatchingType )
continue ;
const element = {
current : input ,
emote ,
replacement : emote . token ,
element : inst . renderEmoteSuggestion ( emote ) ,
favorite : emote . favorite ,
count : this . EmoteUsageCount [ emote . token ] || 0 ,
match _type
} ;
if ( match _type < emoteMatchingType )
continue ;
if ( element . count > 0 )
results _usage . push ( element ) ;
else if ( match _type > NON _PREFIX _MATCH )
results _starting . push ( element ) ;
else
results _other . push ( element ) ;
2019-06-28 04:54:58 +02:00
}
2019-06-30 20:55:53 +02:00
2020-07-01 19:07:17 -04:00
results _usage . sort ( ( a , b ) => b . count - a . count ) ;
2022-01-15 21:30:45 -05:00
results _starting . sort ( ( a , b ) => locale . compare ( a . replacement , b . replacement ) ) ;
results _other . sort ( ( a , b ) => locale . compare ( a . replacement , b . replacement ) ) ;
2019-06-30 20:55:53 +02:00
2020-07-01 19:07:17 -04:00
return results _usage . concat ( results _starting ) . concat ( results _other ) ;
2019-06-28 04:54:58 +02:00
}
2018-04-07 19:09:32 -04:00
2018-04-12 20:30:00 -04:00
getEmojiSuggestions ( input , inst ) {
2019-06-28 04:54:58 +02:00
if ( ! input . startsWith ( ':' ) ) {
return [ ] ;
}
2018-04-13 00:16:58 -04:00
let search = input . slice ( 1 ) . toLowerCase ( ) ;
const style = this . chat . context . get ( 'chat.emoji.style' ) ,
2018-07-26 19:40:53 -04:00
tone = this . settings . provider . get ( 'emoji-tone' , null ) ,
2019-07-31 22:25:21 +02:00
favorites = this . emotes . getFavorites ( 'emoji' ) ,
2018-04-15 17:19:22 -04:00
results = [ ] ,
has _colon = search . endsWith ( ':' ) ;
2018-04-12 20:30:00 -04:00
2018-04-15 17:19:22 -04:00
if ( has _colon )
search = search . slice ( 0 , - 1 ) ;
2018-04-13 00:16:58 -04:00
2020-08-15 15:45:50 -04:00
const included = new Set ;
2018-04-12 20:30:00 -04:00
for ( const name in this . emoji . names )
2018-04-15 17:19:22 -04:00
if ( has _colon ? name === search : name . startsWith ( search ) ) {
2018-07-26 19:40:53 -04:00
const emoji = this . emoji . emoji [ this . emoji . names [ name ] ] ,
toned = emoji . variants && emoji . variants [ tone ] ,
source = toned || emoji ;
2020-08-15 15:45:50 -04:00
if ( emoji && ( style === 0 || source . has [ style ] ) && ! included . has ( source . raw ) ) {
included . add ( source . raw ) ;
2021-06-08 19:13:22 -04:00
const srcSet = this . emoji . getFullImageSet ( source . image , style ) ;
2021-06-10 18:53:16 -04:00
const matched = ` : ${ name } : ` ;
2021-06-08 19:13:22 -04:00
2019-07-31 22:25:21 +02:00
const favorite = favorites . includes ( emoji . code ) ;
2018-04-12 20:30:00 -04:00
results . push ( {
current : input ,
2021-06-08 19:13:22 -04:00
emoji : source ,
2021-06-10 18:53:16 -04:00
matched ,
2021-06-08 19:13:22 -04:00
srcSet ,
2018-07-26 19:40:53 -04:00
replacement : source . raw ,
2018-04-12 20:30:00 -04:00
element : inst . renderFFZEmojiSuggestion ( {
2021-06-10 18:53:16 -04:00
token : matched ,
2018-04-12 20:30:00 -04:00
id : ` emoji- ${ emoji . code } ` ,
2018-07-26 19:40:53 -04:00
src : this . emoji . getFullImage ( source . image , style ) ,
2021-06-08 19:13:22 -04:00
srcSet ,
2019-07-31 22:25:21 +02:00
favorite
} ) ,
favorite
2018-04-12 20:30:00 -04:00
} ) ;
2019-07-31 22:25:21 +02:00
}
2018-04-12 20:30:00 -04:00
}
return results ;
}
2020-07-01 19:07:17 -04:00
buildFFZCache ( user _id , user _login , channel _id , channel _login ) {
const sets = this . emotes . getSets ( user _id , user _login , channel _id , channel _login ) ;
if ( ! sets || ! sets . length )
return { emotes : [ ] , length : 0 , user _id , user _login , channel _id , channel _login } ;
const out = [ ] ,
2021-03-20 19:49:20 -04:00
anim = this . chat . context . get ( 'chat.emotes.animated' ) > 0 ,
2020-07-01 19:07:17 -04:00
hidden _sets = this . settings . provider . get ( 'emote-menu.hidden-sets' ) ,
2020-07-10 20:08:29 -04:00
has _hidden = Array . isArray ( hidden _sets ) && hidden _sets . length > 0 ,
added _emotes = new Set ;
2020-07-01 19:07:17 -04:00
for ( const set of sets ) {
if ( ! set || ! set . emotes )
continue ;
const source = set . source || 'ffz' ,
2021-06-10 18:53:16 -04:00
source _line = set . source _line || ( ` ${ set . source || 'FFZ' } ${ set . title || 'Global' } ` ) ,
2020-07-01 19:07:17 -04:00
key = ` ${ set . merge _source || source } - ${ set . merge _id || set . id } ` ;
if ( has _hidden && hidden _sets . includes ( key ) )
continue ;
const hidden _emotes = this . emotes . getHidden ( source ) ,
favorites = this . emotes . getFavorites ( source ) ;
for ( const emote of Object . values ( set . emotes ) ) {
2020-07-10 20:08:29 -04:00
if ( ! emote || ! emote . id || emote . hidden || hidden _emotes . includes ( emote . id ) || added _emotes . has ( emote . name ) )
2020-07-01 19:07:17 -04:00
continue ;
2020-07-10 20:08:29 -04:00
if ( ! emote . name )
continue ;
added _emotes . add ( emote . name ) ;
2020-07-01 19:07:17 -04:00
out . push ( {
id : ` ${ source } - ${ emote . id } ` ,
2021-06-10 18:53:16 -04:00
source ,
extra : source _line ,
2020-07-01 19:07:17 -04:00
token : emote . name ,
2020-07-10 20:08:29 -04:00
tokenLower : emote . name . toLowerCase ( ) ,
2021-03-20 19:49:20 -04:00
srcSet : anim && emote . animSrcSet || emote . srcSet ,
2020-07-01 19:07:17 -04:00
favorite : favorites . includes ( emote . id )
} ) ;
}
}
return {
emotes : out ,
length : sets . length
}
}
2018-04-07 19:09:32 -04:00
getEmoteSuggestions ( input , inst ) {
2022-11-30 19:48:44 -05:00
if ( ! inst . _ffz _channel _login ) {
2019-03-26 17:37:00 -04:00
const parent = this . fine . searchParent ( inst , 'chat-input' , 50 ) ;
2018-04-07 19:09:32 -04:00
if ( parent )
this . updateEmoteCompletion ( parent , inst ) ;
}
2023-08-31 15:48:38 +02:00
const emoteMatchingType = this . chat . context . get ( 'chat.tab-complete.matching' ) ;
2022-11-30 19:48:44 -05:00
const user = inst . _ffz _user ,
channel _id = inst . _ffz _channel _id ,
channel _login = inst . _ffz _channel _login ;
if ( ! channel _login )
return [ ] ;
2020-07-01 19:07:17 -04:00
let cache = inst . ffz _ffz _cache ;
if ( ! cache || cache . user _id !== user ? . id || cache . user _login !== user ? . login || cache . channel _id !== channel _id || cache . channel _login !== channel _login )
cache = inst . ffz _ffz _cache = this . buildFFZCache ( user ? . id , user ? . login , channel _id , channel _login ) ;
const emotes = cache . emotes ;
if ( ! emotes . length )
return [ ] ;
2019-06-28 04:54:58 +02:00
const search = input . startsWith ( ':' ) ? input . slice ( 1 ) : input ,
2020-07-10 20:08:29 -04:00
results = [ ] ;
2018-04-07 19:09:32 -04:00
2020-07-01 19:07:17 -04:00
for ( const emote of emotes ) {
2022-01-17 15:57:44 -05:00
const match _type = inst . doesEmoteMatchTerm ( emote , search )
2023-08-31 15:48:38 +02:00
if ( match _type < emoteMatchingType )
continue ;
results . push ( {
current : input ,
emote ,
replacement : emote . token ,
element : inst . renderEmoteSuggestion ( emote ) ,
favorite : emote . favorite ,
count : 0 , // TODO: Count stuff?
match _type
} ) ;
2020-07-01 19:07:17 -04:00
}
return results ;
/ * f o r ( c o n s t s e t o f s e t s ) {
if ( ! set || ! set . emotes )
continue ;
const
2019-08-12 22:52:57 -04:00
if ( set && set . emotes )
for ( const emote of Object . values ( set . emotes ) )
2020-06-23 17:17:00 -04:00
if ( inst . doesEmoteMatchTerm ( emote , search ) && ! added _emotes . has ( emote . name ) && ! this . emotes . isHidden ( set . source || 'ffz' , emote . id ) ) {
2019-08-12 22:52:57 -04:00
const favorite = this . emotes . isFavorite ( set . source || 'ffz' , emote . id ) ;
results . push ( {
current : input ,
replacement : emote . name ,
element : inst . renderEmoteSuggestion ( {
token : emote . name ,
id : ` ${ set . source } - ${ emote . id } ` ,
srcSet : emote . srcSet ,
favorite
} ) ,
2019-08-13 10:42:37 +08:00
favorite
2019-08-12 22:52:57 -04:00
} ) ;
2019-09-07 19:57:30 +02:00
added _emotes . add ( emote . name ) ;
2019-08-12 22:52:57 -04:00
}
}
2018-04-07 19:09:32 -04:00
2020-07-01 19:07:17 -04:00
return results ; * /
2018-04-07 19:09:32 -04:00
}
2019-06-27 23:19:05 -04:00
2023-03-06 17:08:47 -05:00
getInput ( ) {
for ( const inst of this . ChatInput . instances ) {
if ( ! inst . autocompleteInputRef || ! inst . state )
continue ;
if ( inst . state . value )
return inst . state . value ;
}
return null ;
}
2019-06-27 23:19:05 -04:00
pasteMessage ( room , message ) {
for ( const inst of this . ChatInput . instances ) {
if ( inst ? . props ? . channelLogin !== room )
continue ;
if ( ! inst . autocompleteInputRef || ! inst . state )
return ;
if ( inst . state . value )
message = ` ${ inst . state . value } ${ message } ` ;
inst . autocompleteInputRef . setValue ( message ) ;
inst . autocompleteInputRef . componentRef ? . focus ? . ( ) ;
}
}
2018-04-07 19:09:32 -04:00
}