2017-11-13 01:23:39 -05:00
'use strict' ;
// ============================================================================
// Chat
// ============================================================================
2018-12-03 18:08:32 -05:00
2018-10-01 21:06:42 +02:00
import dayjs from 'dayjs' ;
2017-11-13 01:23:39 -05:00
import Module from 'utilities/module' ;
import { createElement , ManagedStyle } from 'utilities/dom' ;
2022-06-08 23:07:07 -04:00
import { timeout , has , addWordSeparators , glob _to _regex , escape _regex , split _chars } from 'utilities/object' ;
2021-05-03 15:33:03 -04:00
import { Color } from 'utilities/color' ;
2017-11-13 01:23:39 -05:00
import Badges from './badges' ;
import Emotes from './emotes' ;
2018-04-12 20:30:00 -04:00
import Emoji from './emoji' ;
2020-01-11 17:13:56 -05:00
import Overrides from './overrides' ;
2017-11-13 01:23:39 -05:00
import Room from './room' ;
2017-11-22 20:21:01 -05:00
import User from './user' ;
2017-11-13 01:23:39 -05:00
import * as TOKENIZERS from './tokenizers' ;
2018-04-03 19:28:06 -04:00
import * as RICH _PROVIDERS from './rich_providers' ;
2022-12-18 17:30:34 -05:00
import * as LINK _PROVIDERS from './link_providers' ;
2017-11-13 01:23:39 -05:00
2018-04-28 17:56:03 -04:00
import Actions from './actions' ;
2021-09-06 16:48:48 -04:00
import { getFontsList } from 'src/utilities/fonts' ;
2018-04-28 17:56:03 -04:00
2021-04-30 17:38:49 -04:00
function sortPriorityColorTerms ( list ) {
list . sort ( ( a , b ) => {
if ( a [ 0 ] < b [ 0 ] ) return 1 ;
if ( a [ 0 ] > b [ 0 ] ) return - 1 ;
if ( ! a [ 1 ] && b [ 1 ] ) return 1 ;
if ( a [ 1 ] && ! b [ 1 ] ) return - 1 ;
return 0 ;
} ) ;
return list ;
}
2021-03-03 17:10:14 -05:00
const TERM _FLAGS = [ 'g' , 'gi' ] ;
2022-08-02 16:59:50 -04:00
const UNBLOCKABLE _TOKENS = [
'filter_test'
] ;
2021-03-03 17:10:14 -05:00
function formatTerms ( data ) {
const out = [ ] ;
for ( let i = 0 ; i < data . length ; i ++ ) {
const list = data [ i ] ;
if ( list [ 0 ] . length )
2022-06-08 23:07:07 -04:00
list [ 1 ] . push ( addWordSeparators ( list [ 0 ] . join ( '|' ) ) ) ;
2021-03-03 17:10:14 -05:00
out . push ( list [ 1 ] . length ? new RegExp ( list [ 1 ] . join ( '|' ) , TERM _FLAGS [ i ] || 'gi' ) : null ) ;
}
return out ;
}
2020-08-04 18:26:11 -04:00
const ERROR _IMAGE = 'https://static-cdn.jtvnw.net/emoticons/v1/58765/2.0' ;
2020-05-27 17:58:59 -04:00
const EMOTE _CHARS = /[ .,!]/ ;
2021-03-21 12:50:45 -04:00
const GIF _TERMS = [ 'gif emotes' , 'gif emoticons' , 'gifs' ] ;
2017-11-13 01:23:39 -05:00
export default class Chat extends Module {
constructor ( ... args ) {
super ( ... args ) ;
this . should _enable = true ;
this . inject ( 'settings' ) ;
this . inject ( 'i18n' ) ;
this . inject ( 'tooltips' ) ;
2018-04-12 02:29:43 -04:00
this . inject ( 'experiments' ) ;
2023-03-03 15:24:20 -05:00
this . inject ( 'staging' ) ;
2023-03-10 17:06:12 -05:00
this . inject ( 'load_tracker' ) ;
2017-11-13 01:23:39 -05:00
this . inject ( Badges ) ;
this . inject ( Emotes ) ;
2018-04-12 20:30:00 -04:00
this . inject ( Emoji ) ;
2018-04-28 17:56:03 -04:00
this . inject ( Actions ) ;
2020-01-11 17:13:56 -05:00
this . inject ( Overrides ) ;
2017-11-13 01:23:39 -05:00
this . _link _info = { } ;
2019-01-18 19:07:57 -05:00
// Bind for JSX stuff
this . clickToReveal = this . clickToReveal . bind ( this ) ;
2019-05-31 16:05:50 -04:00
this . handleMentionClick = this . handleMentionClick . bind ( this ) ;
2020-08-12 16:10:06 -04:00
this . handleReplyClick = this . handleReplyClick . bind ( this ) ;
2019-01-18 19:07:57 -05:00
2017-11-13 01:23:39 -05:00
this . style = new ManagedStyle ;
this . context = this . settings . context ( { } ) ;
this . rooms = { } ;
this . users = { } ;
this . room _ids = { } ;
this . user _ids = { } ;
this . tokenizers = { } ;
this . _ _tokenizers = [ ] ;
2018-04-03 19:28:06 -04:00
this . rich _providers = { } ;
this . _ _rich _providers = [ ] ;
2022-12-18 17:30:34 -05:00
this . link _providers = { } ;
this . _ _link _providers = [ ] ;
2021-04-14 16:53:15 -04:00
this . _hl _reasons = { } ;
this . addHighlightReason ( 'mention' , 'Mentioned' ) ;
this . addHighlightReason ( 'user' , 'Highlight User' ) ;
this . addHighlightReason ( 'badge' , 'Highlight Badge' ) ;
this . addHighlightReason ( 'term' , 'Highlight Term' ) ;
2017-11-13 01:23:39 -05:00
// ========================================================================
// Settings
// ========================================================================
2021-04-14 16:53:15 -04:00
/ * t h i s . s e t t i n g s . a d d ( ' d e b u g . h i g h l i g h t - r e a s o n ' , {
default : [ ] ,
type : 'basic_array_merge' ,
ui : {
path : 'Chat > Debugging >> General' ,
title : 'Test' ,
component : 'setting-select-box' ,
multiple : true ,
data : ( ) => this . getHighlightReasons ( )
}
} ) ; * /
2020-07-29 02:22:45 -04:00
this . settings . add ( 'debug.link-resolver.source' , {
default : null ,
ui : {
path : 'Debugging > Data Sources >> Links' ,
title : 'Link Resolver' ,
component : 'setting-select-box' ,
force _seen : true ,
data : [
{ value : null , title : 'Automatic' } ,
{ value : 'dev' , title : 'localhost' } ,
{ value : 'test' , title : 'API Test' } ,
{ value : 'prod' , title : 'API Production' } ,
{ value : 'socket' , title : 'Socket Cluster (Deprecated)' }
]
} ,
changed : ( ) => this . clearLinkCache ( )
} ) ;
this . settings . addUI ( 'debug.link-resolver.test' , {
path : 'Debugging > Data Sources >> Links' ,
component : 'link-tester' ,
getChat : ( ) => this ,
force _seen : true
} ) ;
2021-03-02 19:50:25 -05:00
this . settings . add ( 'chat.timestamp-size' , {
default : null ,
ui : {
path : 'Chat > Appearance >> General' ,
title : 'Timestamp Font Size' ,
description : 'How large should timestamps be, in pixels. Defaults to Font Size if not set.' ,
component : 'setting-text-box' ,
2021-04-24 14:37:01 -04:00
process : 'to_int' ,
bounds : [ 1 ]
2021-03-02 19:50:25 -05:00
}
} ) ;
2018-07-20 19:10:39 -04:00
this . settings . add ( 'chat.font-size' , {
2020-10-05 13:16:37 -04:00
default : 13 ,
2018-07-20 19:10:39 -04:00
ui : {
path : 'Chat > Appearance >> General' ,
title : 'Font Size' ,
description : "How large should text in chat be, in pixels. This may be affected by your browser's zoom and font size settings." ,
component : 'setting-text-box' ,
2021-04-24 14:37:01 -04:00
process : 'to_int' ,
bounds : [ 1 ]
2018-07-20 19:10:39 -04:00
}
} ) ;
this . settings . add ( 'chat.font-family' , {
default : '' ,
ui : {
path : 'Chat > Appearance >> General' ,
title : 'Font Family' ,
description : 'Set the font used for displaying chat messages.' ,
2021-09-06 16:48:48 -04:00
component : 'setting-combo-box' ,
data : ( ) => getFontsList ( )
2018-07-20 19:10:39 -04:00
}
} ) ;
2021-06-08 19:13:22 -04:00
this . settings . add ( 'chat.name-format' , {
default : 0 ,
ui : {
path : 'Chat > Appearance >> Usernames' ,
title : 'Display Style' ,
description : 'Change how usernames are displayed in chat when users have an international display name set.' ,
component : 'setting-select-box' ,
data : [
{ value : 0 , title : 'International Name (Username) <Default>' } ,
{ value : 1 , title : 'International Name' } ,
{ value : 2 , title : 'Username' }
]
}
} ) ;
2018-07-20 19:10:39 -04:00
this . settings . add ( 'chat.lines.emote-alignment' , {
default : 0 ,
ui : {
path : 'Chat > Appearance >> Chat Lines' ,
title : 'Emote Alignment' ,
description : 'Change how emotes are positioned in chat, potentially making messages taller in order to avoid having emotes overlap.' ,
component : 'setting-select-box' ,
data : [
{ value : 0 , title : 'Standard' } ,
{ value : 1 , title : 'Padded' } ,
{ value : 2 , title : 'Baseline (BTTV-Like)' }
]
}
} ) ;
2018-04-03 19:28:06 -04:00
this . settings . add ( 'chat.rich.enabled' , {
default : true ,
ui : {
path : 'Chat > Appearance >> Rich Content' ,
title : 'Display rich content in chat.' ,
description : 'This displays rich content blocks for things like linked clips and videos.' ,
component : 'setting-check-box'
}
} ) ;
2021-11-15 17:12:01 -05:00
this . settings . add ( 'chat.rich.want-mid' , {
default : false ,
ui : {
path : 'Chat > Appearance >> Rich Content' ,
title : 'Display larger rich content in chat.' ,
description : 'This enables the use of bigger rich content embeds in chat. This is **not** recommended for most users and/or chats.\n\n**Note:** Enabling this may cause chat to scroll at inopportune times due to content loading. Moderators should not use this feature.' ,
component : 'setting-check-box'
}
} ) ;
2018-04-03 19:28:06 -04:00
this . settings . add ( 'chat.rich.hide-tokens' , {
2021-04-19 15:08:12 -04:00
default : false ,
2018-04-03 19:28:06 -04:00
ui : {
path : 'Chat > Appearance >> Rich Content' ,
title : 'Hide matching links for rich content.' ,
component : 'setting-check-box'
}
} ) ;
2018-12-13 15:21:57 -05:00
this . settings . add ( 'chat.rich.all-links' , {
default : false ,
ui : {
path : 'Chat > Appearance >> Rich Content' ,
title : 'Display rich content embeds for all links.' ,
2018-12-13 16:18:39 -05:00
description : '*Streamers: Please be aware that this is a potential vector for NSFW imagery via thumbnails, so be mindful when capturing chat with this enabled.*' ,
2019-09-12 13:11:08 -04:00
component : 'setting-check-box' ,
extra : {
component : 'chat-rich-example' ,
getChat : ( ) => this
}
2018-12-13 15:21:57 -05:00
}
} ) ;
this . settings . add ( 'chat.rich.minimum-level' , {
default : 0 ,
ui : {
path : 'Chat > Appearance >> Rich Content' ,
title : 'Required User Level' ,
description : 'Only display rich content embeds on messages posted by users with this level or higher.' ,
component : 'setting-select-box' ,
data : [
{ value : 4 , title : 'Broadcaster' } ,
{ value : 3 , title : 'Moderator' } ,
{ value : 2 , title : 'VIP' } ,
{ value : 1 , title : 'Subscriber' } ,
{ value : 0 , title : 'Everyone' }
]
}
} ) ;
2017-11-17 14:59:46 -05:00
this . settings . add ( 'chat.scrollback-length' , {
default : 150 ,
ui : {
path : 'Chat > Behavior >> General' ,
title : 'Scrollback Length' ,
2017-11-17 17:31:21 -05:00
description : 'Keep up to this many lines in chat. Setting this too high will create lag.' ,
2017-11-17 14:59:46 -05:00
component : 'setting-text-box' ,
2021-04-24 14:37:01 -04:00
process : 'to_int' ,
bounds : [ 1 ]
2017-11-17 14:59:46 -05:00
}
} ) ;
2021-05-03 15:33:03 -04:00
this . settings . add ( 'chat.filtering.debug' , {
default : false ,
ui : {
path : 'Chat > Filtering > General >> Behavior' ,
title : 'Display a list of highlight reasons on every chat message for debugging.' ,
component : 'setting-check-box' ,
force _seen : true
}
} ) ;
2021-04-19 15:08:12 -04:00
this . settings . addUI ( 'chat.filtering.pad-bottom' , {
path : 'Chat > Filtering > Highlight' ,
sort : 1000 ,
component : 'setting-spacer' ,
2021-04-30 17:38:49 -04:00
top : '30rem' ,
force _seen : true
2021-04-19 15:08:12 -04:00
} ) ;
2019-01-18 19:07:57 -05:00
this . settings . add ( 'chat.filtering.click-to-reveal' , {
default : false ,
ui : {
2021-03-03 17:10:14 -05:00
path : 'Chat > Filtering > General @{"sort":-1} >> Behavior' ,
2019-01-18 19:07:57 -05:00
title : 'Click to reveal deleted terms.' ,
component : 'setting-check-box'
}
} ) ;
2018-08-02 14:29:18 -04:00
this . settings . add ( 'chat.filtering.deleted-style' , {
default : 1 ,
ui : {
path : 'Chat > Behavior >> Deleted Messages' ,
2020-04-22 14:30:34 -04:00
title : 'Detailed Message Style' ,
2019-04-28 17:28:16 -04:00
description : 'This style will be applied to deleted messages showed in Detailed rendering mode to differentiate them from normal chat messages.' ,
2018-08-02 14:29:18 -04:00
component : 'setting-select-box' ,
data : [
{ value : 0 , title : 'Faded' } ,
2019-04-18 03:16:19 -04:00
{ value : 1 , title : 'Faded, Line Through' } ,
{ value : 2 , title : 'Line Through' } ,
{ value : 3 , title : 'No Change' }
2018-08-02 14:29:18 -04:00
]
}
} ) ;
2019-04-18 21:07:11 -04:00
this . settings . add ( 'chat.filtering.display-deleted' , {
default : false ,
ui : {
path : 'Chat > Behavior >> Deleted Messages' ,
2020-04-22 14:30:34 -04:00
sort : - 1 ,
2019-04-28 17:28:16 -04:00
title : 'Rendering Mode' ,
2020-04-22 14:30:34 -04:00
description : 'This, when set, overrides the `Deleted Messages` mode selected in Twitch chat settings, which is normally only accessible for moderators. Brief hides messages entirely and shows a notice in chat that a number of messages were hidden. Detailed shows the contents of the message. Legacy shows `<message deleted>` with click to reveal.' ,
2019-04-18 21:07:11 -04:00
component : 'setting-select-box' ,
data : [
{ value : false , title : 'Do Not Override' } ,
{ value : 'BRIEF' , title : 'Brief' } ,
{ value : 'DETAILED' , title : 'Detailed' } ,
{ value : 'LEGACY' , title : 'Legacy' }
]
}
} ) ;
this . settings . add ( 'chat.filtering.display-mod-action' , {
default : 1 ,
ui : {
path : 'Chat > Behavior >> Deleted Messages' ,
2019-04-28 17:28:16 -04:00
title : 'Display Reason' ,
2019-04-18 21:07:11 -04:00
component : 'setting-select-box' ,
data : [
{ value : 0 , title : 'Never' } ,
{ value : 1 , title : 'In Detailed Mode' } ,
{ value : 2 , title : 'Always' }
]
}
} ) ;
2018-08-02 14:29:18 -04:00
2019-10-11 17:41:07 -04:00
this . settings . add ( 'chat.automod.delete-messages' , {
default : true ,
ui : {
2021-03-03 17:10:14 -05:00
path : 'Chat > Filtering > General >> AutoMod Filters @{"description": "Extra configuration for Twitch\'s native `Chat Filters`."}' ,
2019-10-11 17:41:07 -04:00
title : 'Mark messages as deleted if they contain filtered phrases.' ,
component : 'setting-check-box'
}
} ) ;
this . settings . add ( 'chat.automod.remove-messages' , {
default : true ,
ui : {
2021-03-03 17:10:14 -05:00
path : 'Chat > Filtering > General >> AutoMod Filters' ,
2019-10-11 17:41:07 -04:00
title : 'Remove messages entirely if they contain filtered phrases.' ,
component : 'setting-check-box'
}
} ) ;
this . settings . add ( 'chat.automod.run-as-mod' , {
default : false ,
ui : {
2021-03-03 17:10:14 -05:00
path : 'Chat > Filtering > General >> AutoMod Filters' ,
2019-10-11 17:41:07 -04:00
title : 'Use Chat Filters as a moderator.' ,
description : 'By default, Twitch\'s Chat Filters feature does not function for moderators. This overrides that behavior.' ,
component : 'setting-check-box'
}
} ) ;
2018-08-02 14:29:18 -04:00
this . settings . add ( 'chat.filtering.process-own' , {
default : false ,
ui : {
2021-03-03 17:10:14 -05:00
path : 'Chat > Filtering > General >> Behavior' ,
2018-08-02 14:29:18 -04:00
title : 'Filter your own messages.' ,
2018-05-22 17:23:20 -04:00
component : 'setting-check-box'
}
} ) ;
2018-07-16 15:07:22 -04:00
this . settings . add ( 'chat.filtering.ignore-clear' , {
default : false ,
ui : {
path : 'Chat > Behavior >> Deleted Messages' ,
title : 'Do not Clear Chat when commanded to.' ,
component : 'setting-check-box'
}
} ) ;
this . settings . add ( 'chat.filtering.remove-deleted' , {
default : 1 ,
ui : {
path : 'Chat > Behavior >> Deleted Messages' ,
title : 'Remove deleted messages from chat.' ,
description : 'Deleted messages will be removed from chat entirely. This setting is not recommended for moderators.' ,
component : 'setting-select-box' ,
data : [
{ value : 0 , title : 'Do Not Remove' } ,
{ value : 1 , title : 'Remove Unseen (Default)' } ,
2018-07-16 15:28:53 -04:00
{ value : 2 , title : 'Remove Unseen as Moderator' } ,
{ value : 3 , title : 'Remove All' }
2018-07-16 15:07:22 -04:00
]
}
} ) ;
this . settings . add ( 'chat.delay' , {
default : - 1 ,
ui : {
path : 'Chat > Behavior >> General' ,
title : 'Artificial Chat Delay' ,
description : 'Delay the appearance of chat messages to allow for moderation before you see them.' ,
component : 'setting-select-box' ,
data : [
{ value : - 1 , title : 'Default Delay (Room Specific; Non-Mod Only)' } ,
{ value : 0 , title : 'No Delay' } ,
{ value : 300 , title : 'Minor (Bot Moderation; 0.3s)' } ,
{ value : 1200 , title : 'Normal (Human Moderation; 1.2s)' } ,
{ value : 5000 , title : 'Large (Spoiler Removal / Slow Mods; 5s)' } ,
{ value : 10000 , title : 'Extra Large (10s)' } ,
{ value : 15000 , title : 'Extremely Large (15s)' } ,
{ value : 20000 , title : 'Mods Asleep; Delay Chat (20s)' } ,
{ value : 30000 , title : 'Half a Minute (30s)' } ,
{ value : 60000 , title : 'Why??? (1m)' } ,
{ value : 788400000000 , title : 'The CBenni Option (Literally 25 Years)' }
]
}
} ) ;
2022-08-02 16:59:50 -04:00
this . settings . add ( 'chat.filtering.hidden-tokens' , {
default : [ ] ,
type : 'array_merge' ,
always _inherit : true ,
process ( ctx , val ) {
const out = new Set ;
for ( const v of val )
if ( v ? . v || ! UNBLOCKABLE _TOKENS . includes ( v . v ) )
out . add ( v . v ) ;
return out ;
} ,
ui : {
path : 'Chat > Appearance >> Hidden Token Types @{"description":"This filter allows you to prevent specific content token types from appearing chat messages, such as hiding all cheers or emotes."}' ,
component : 'blocked-types' ,
data : ( ) => Object
. keys ( this . tokenizers )
. filter ( key => ! UNBLOCKABLE _TOKENS . includes ( key ) && this . tokenizers [ key ] ? . render )
. sort ( )
}
} ) ;
2019-04-28 17:28:16 -04:00
this . settings . add ( 'chat.filtering.highlight-basic-users' , {
default : [ ] ,
type : 'array_merge' ,
always _inherit : true ,
ui : {
2021-05-03 15:33:03 -04:00
path : 'Chat > Filtering > Highlight @{"description": "These settings allow you to highlight messages in chat based on their contents. Setting priorities on rules allows you to determine which highlight color should be applied if a message matches multiple rules. Rules with a higher priority take priority over rules with lower priorities.\\n\\nYou can also create a rule that removes highlights from messages, preventing lower priority rules from highlighting them, by setting a color with an alpha value of zero. Example: `#00000000`"} >> Users' ,
2019-04-28 17:28:16 -04:00
component : 'basic-terms' ,
colored : true ,
2021-04-30 17:38:49 -04:00
words : false ,
priority : true
2019-04-28 17:28:16 -04:00
}
} ) ;
2021-04-30 17:38:49 -04:00
this . settings . add ( '__filter:highlight-users' , {
2019-04-28 17:28:16 -04:00
requires : [ 'chat.filtering.highlight-basic-users' ] ,
2019-04-30 15:18:29 -04:00
equals : 'requirements' ,
2019-04-28 17:28:16 -04:00
process ( ctx ) {
const val = ctx . get ( 'chat.filtering.highlight-basic-users' ) ;
if ( ! val || ! val . length )
return null ;
2021-04-30 17:38:49 -04:00
const temp = new Map ;
2019-04-28 17:28:16 -04:00
for ( const item of val ) {
2021-05-03 15:33:03 -04:00
const p = item . p || 0 ,
2019-04-28 17:28:16 -04:00
t = item . t ;
2021-05-03 15:33:03 -04:00
let c = item . c || null ;
2019-04-28 17:28:16 -04:00
let v = item . v ;
if ( t === 'glob' )
v = glob _to _regex ( v ) ;
else if ( t !== 'raw' )
v = escape _regex ( v ) ;
if ( ! v || ! v . length )
continue ;
try {
new RegExp ( v ) ;
} catch ( err ) {
continue ;
}
2021-04-30 17:38:49 -04:00
let colors = temp . get ( p ) ;
if ( ! colors ) {
colors = new Map ;
temp . set ( p , colors ) ;
}
2021-05-03 15:33:03 -04:00
if ( c ) {
const test = Color . RGBA . fromCSS ( c ) ;
if ( ! test || ! test . a )
c = false ;
}
2019-04-28 17:28:16 -04:00
if ( colors . has ( c ) )
colors . get ( c ) . push ( v ) ;
else {
colors . set ( c , [ v ] ) ;
}
}
2021-04-30 17:38:49 -04:00
const out = [ ] ;
for ( const [ priority , list ] of temp ) {
for ( const [ color , entries ] of list ) {
out . push ( [
priority ,
color ,
new RegExp ( ` ^(?: ${ entries . join ( '|' ) } ) $ ` , 'gi' )
] ) ;
//list.set(k, new RegExp(`^(?:${entries.join('|')})$`, 'gi'));
}
2019-04-28 17:28:16 -04:00
}
2021-04-30 17:38:49 -04:00
return sortPriorityColorTerms ( out ) ;
2019-04-28 17:28:16 -04:00
}
} ) ;
this . settings . add ( 'chat.filtering.highlight-basic-users-blocked' , {
default : [ ] ,
type : 'array_merge' ,
always _inherit : true ,
ui : {
2021-03-03 17:10:14 -05:00
path : 'Chat > Filtering > Block >> Users' ,
2019-04-28 17:28:16 -04:00
component : 'basic-terms' ,
removable : true ,
words : false
}
} ) ;
2021-04-30 17:38:49 -04:00
this . settings . add ( '__filter:block-users' , {
2019-04-30 15:18:29 -04:00
requires : [ 'chat.filtering.highlight-basic-users-blocked' ] ,
equals : 'requirements' ,
2019-04-28 17:28:16 -04:00
process ( ctx ) {
const val = ctx . get ( 'chat.filtering.highlight-basic-users-blocked' ) ;
if ( ! val || ! val . length )
return null ;
const out = [ [ ] , [ ] ] ;
for ( const item of val ) {
const t = item . t ;
let v = item . v ;
if ( t === 'glob' )
v = glob _to _regex ( v ) ;
else if ( t !== 'raw' )
v = escape _regex ( v ) ;
if ( ! v || ! v . length )
continue ;
out [ item . remove ? 1 : 0 ] . push ( v ) ;
}
return out . map ( data => {
if ( ! data . length )
return null ;
2019-07-29 15:48:00 -04:00
return new RegExp ( ` ^(?: ${ data . join ( '|' ) } ) $ ` , 'gi' ) ;
2019-04-28 17:28:16 -04:00
} ) ;
}
} ) ;
this . settings . add ( 'chat.filtering.highlight-basic-badges' , {
default : [ ] ,
type : 'array_merge' ,
always _inherit : true ,
ui : {
2021-03-03 17:10:14 -05:00
path : 'Chat > Filtering > Highlight >> Badges' ,
2019-04-28 17:28:16 -04:00
component : 'badge-highlighting' ,
colored : true ,
2021-04-30 17:38:49 -04:00
priority : true ,
2021-03-22 18:19:09 -04:00
data : ( ) => this . badges . getSettingsBadges ( true )
2019-04-28 17:28:16 -04:00
}
} ) ;
2021-04-30 17:38:49 -04:00
this . settings . add ( '__filter:highlight-badges' , {
2019-04-28 17:28:16 -04:00
requires : [ 'chat.filtering.highlight-basic-badges' ] ,
2019-04-30 15:18:29 -04:00
equals : 'requirements' ,
2019-04-28 17:28:16 -04:00
process ( ctx ) {
const val = ctx . get ( 'chat.filtering.highlight-basic-badges' ) ;
if ( ! val || ! val . length )
return null ;
2021-04-30 17:38:49 -04:00
const badges = new Map ;
2019-04-28 17:28:16 -04:00
for ( const item of val ) {
2021-05-03 15:33:03 -04:00
let c = item . c || null ;
const p = item . p || 0 ,
2019-04-28 17:28:16 -04:00
v = item . v ;
2021-05-03 15:33:03 -04:00
if ( c ) {
const test = Color . RGBA . fromCSS ( c ) ;
if ( ! test || ! test . a )
c = false ;
}
2021-04-30 17:38:49 -04:00
const existing = badges . get ( v ) ;
if ( ! existing || existing [ 0 ] < p || ( c && ! existing [ 1 ] && existing [ 0 ] <= p ) )
badges . set ( v , [ p , c ] ) ;
2019-04-28 17:28:16 -04:00
}
2021-04-30 17:38:49 -04:00
return badges ;
2019-04-28 17:28:16 -04:00
}
} ) ;
this . settings . add ( 'chat.filtering.highlight-basic-badges-blocked' , {
default : [ ] ,
type : 'array_merge' ,
always _inherit : true ,
ui : {
2021-03-03 17:10:14 -05:00
path : 'Chat > Filtering > Block >> Badges @{"description": "**Note:** This section is for filtering messages out of chat from users with specific badges. If you wish to hide a badge, go to [Chat > Badges >> Visibility](~chat.badges.tabs.visibility)."}' ,
2019-04-28 17:28:16 -04:00
component : 'badge-highlighting' ,
removable : true ,
2021-03-22 18:19:09 -04:00
data : ( ) => this . badges . getSettingsBadges ( true )
2019-04-28 17:28:16 -04:00
}
} ) ;
2021-04-30 17:38:49 -04:00
this . settings . add ( '__filter:block-badges' , {
2019-04-28 17:28:16 -04:00
requires : [ 'chat.filtering.highlight-basic-badges-blocked' ] ,
2019-04-30 15:18:29 -04:00
equals : 'requirements' ,
2019-04-28 17:28:16 -04:00
process ( ctx ) {
const val = ctx . get ( 'chat.filtering.highlight-basic-badges-blocked' ) ;
if ( ! val || ! val . length )
return null ;
const out = [ [ ] , [ ] ] ;
for ( const item of val )
if ( item . v )
out [ item . remove ? 1 : 0 ] . push ( item . v ) ;
if ( ! out [ 0 ] . length && ! out [ 1 ] . length )
return null ;
return out ;
}
} ) ;
2018-05-22 17:23:20 -04:00
2018-05-31 18:34:15 -04:00
this . settings . add ( 'chat.filtering.highlight-basic-terms' , {
default : [ ] ,
type : 'array_merge' ,
2018-06-27 14:13:59 -04:00
always _inherit : true ,
2018-05-31 18:34:15 -04:00
ui : {
2021-03-03 17:10:14 -05:00
path : 'Chat > Filtering > Highlight >> Terms @{"description": "Please see [Chat > Filtering > Syntax Help](~) for details on how to use terms."}' ,
2018-05-31 18:34:15 -04:00
component : 'basic-terms' ,
2021-03-03 17:10:14 -05:00
colored : true ,
2021-04-30 17:38:49 -04:00
priority : true ,
2021-03-03 17:10:14 -05:00
highlight : true
2018-05-31 18:34:15 -04:00
}
} ) ;
2021-04-30 17:38:49 -04:00
this . settings . add ( '__filter:highlight-terms' , {
2021-03-03 17:10:14 -05:00
requires : [ 'chat.filtering.highlight-tokens' , 'chat.filtering.highlight-basic-terms' ] ,
2019-04-30 15:18:29 -04:00
equals : 'requirements' ,
2018-05-31 18:34:15 -04:00
process ( ctx ) {
2021-03-03 17:10:14 -05:00
const can _highlight = ctx . get ( 'chat.filtering.highlight-tokens' ) ;
2018-05-31 18:34:15 -04:00
const val = ctx . get ( 'chat.filtering.highlight-basic-terms' ) ;
if ( ! val || ! val . length )
return null ;
2021-04-30 17:38:49 -04:00
const temp = new Map ;
//const colors = new Map;
2021-03-03 17:10:14 -05:00
let has _highlight = false ,
has _non = false ;
2018-05-31 18:34:15 -04:00
for ( const item of val ) {
2021-05-03 15:33:03 -04:00
const p = item . p || 0 ,
2021-03-03 17:10:14 -05:00
highlight = can _highlight && ( has ( item , 'h' ) ? item . h : true ) ,
sensitive = item . s ,
t = item . t ,
word = has ( item , 'w' ) ? item . w : t !== 'raw' ;
2018-05-31 18:34:15 -04:00
2021-05-03 15:33:03 -04:00
let c = item . c || null ;
2021-03-03 17:10:14 -05:00
let v = item . v ;
2018-05-31 18:34:15 -04:00
if ( t === 'glob' )
v = glob _to _regex ( v ) ;
2021-03-03 17:10:14 -05:00
else if ( t !== 'regex' && t !== 'raw' )
2018-05-31 18:34:15 -04:00
v = escape _regex ( v ) ;
if ( ! v || ! v . length )
continue ;
2018-06-27 14:13:59 -04:00
try {
new RegExp ( v ) ;
} catch ( err ) {
continue ;
}
2021-03-03 17:10:14 -05:00
if ( highlight )
has _highlight = true ;
else
has _non = true ;
2021-04-30 17:38:49 -04:00
let colors = temp . get ( p ) ;
if ( ! colors ) {
colors = new Map ;
temp . set ( p , colors ) ;
}
2021-05-03 15:33:03 -04:00
if ( c ) {
const test = Color . RGBA . fromCSS ( c ) ;
if ( ! test || ! test . a )
c = false ;
}
2021-03-03 17:10:14 -05:00
let data = colors . get ( c ) ;
if ( ! data )
colors . set ( c , data = [
[ // highlight
[ // sensitive
[ ] , [ ] // word
] ,
[
[ ] , [ ]
]
] ,
[
[
[ ] , [ ]
] ,
[
[ ] , [ ]
]
]
] ) ;
data [ highlight ? 0 : 1 ] [ sensitive ? 0 : 1 ] [ word ? 0 : 1 ] . push ( v ) ;
2018-05-31 18:34:15 -04:00
}
2021-03-03 17:10:14 -05:00
if ( ! has _highlight && ! has _non )
return null ;
const out = {
2021-04-30 17:38:49 -04:00
hl : has _highlight ? [ ] : null ,
non : has _non ? [ ] : null
2021-03-03 17:10:14 -05:00
} ;
2021-04-30 17:38:49 -04:00
for ( const [ priority , colors ] of temp ) {
for ( const [ color , list ] of colors ) {
const highlights = formatTerms ( list [ 0 ] ) ,
non _highlights = formatTerms ( list [ 1 ] ) ;
if ( highlights [ 0 ] || highlights [ 1 ] )
out . hl . push ( [
priority ,
color ,
highlights
] ) ;
if ( non _highlights [ 0 ] || non _highlights [ 1 ] )
out . non . push ( [
priority ,
color ,
non _highlights
] ) ;
}
}
2021-03-03 17:10:14 -05:00
2021-04-30 17:38:49 -04:00
if ( has _highlight )
sortPriorityColorTerms ( out . hl ) ;
2018-05-31 18:34:15 -04:00
2021-04-30 17:38:49 -04:00
if ( has _non )
sortPriorityColorTerms ( out . non ) ;
2018-05-31 18:34:15 -04:00
2021-03-03 17:10:14 -05:00
return out ;
2018-05-31 18:34:15 -04:00
}
} ) ;
this . settings . add ( 'chat.filtering.highlight-basic-blocked' , {
default : [ ] ,
type : 'array_merge' ,
2018-06-27 14:13:59 -04:00
always _inherit : true ,
2018-05-31 18:34:15 -04:00
ui : {
2021-03-03 17:10:14 -05:00
path : 'Chat > Filtering > Block >> Terms @{"description": "Please see [Chat > Filtering > Syntax Help](~) for details on how to use terms."}' ,
2018-07-14 14:13:28 -04:00
component : 'basic-terms' ,
removable : true
2018-05-31 18:34:15 -04:00
}
} ) ;
2021-04-30 17:38:49 -04:00
this . settings . add ( '__filter:block-terms' , {
2018-05-31 18:34:15 -04:00
requires : [ 'chat.filtering.highlight-basic-blocked' ] ,
2019-04-30 15:18:29 -04:00
equals : 'requirements' ,
2018-05-31 18:34:15 -04:00
process ( ctx ) {
const val = ctx . get ( 'chat.filtering.highlight-basic-blocked' ) ;
if ( ! val || ! val . length )
return null ;
2021-03-22 18:19:09 -04:00
const data = [
[ // no-remove
[ // sensitive
[ ] , [ ] // word
] ,
[ // intensitive
[ ] , [ ]
]
] ,
[ // remove
[ // sensitive
[ ] , [ ] // word
] ,
[ // intensiitve
[ ] , [ ]
]
]
2018-07-14 14:13:28 -04:00
] ;
2018-05-31 18:34:15 -04:00
2021-03-22 18:19:09 -04:00
let had _remove = false ,
had _non = false ;
2018-05-31 18:34:15 -04:00
for ( const item of val ) {
2021-03-03 17:10:14 -05:00
const t = item . t ,
2021-03-22 18:19:09 -04:00
sensitive = item . s ,
2021-03-03 17:10:14 -05:00
word = has ( item , 'w' ) ? item . w : t !== 'raw' ;
let v = item . v ;
2018-05-31 18:34:15 -04:00
if ( t === 'glob' )
v = glob _to _regex ( v ) ;
2021-03-03 17:10:14 -05:00
else if ( t !== 'regex' && t !== 'raw' )
2018-05-31 18:34:15 -04:00
v = escape _regex ( v ) ;
if ( ! v || ! v . length )
continue ;
2021-03-22 18:19:09 -04:00
if ( item . remove )
had _remove = true ;
else
had _non = true ;
data [ item . remove ? 1 : 0 ] [ sensitive ? 0 : 1 ] [ word ? 0 : 1 ] . push ( v ) ;
2018-05-31 18:34:15 -04:00
}
2021-03-22 18:19:09 -04:00
if ( ! had _remove && ! had _non )
return null ;
2018-05-31 18:34:15 -04:00
2021-03-22 18:19:09 -04:00
return {
remove : had _remove ? formatTerms ( data [ 1 ] ) : null ,
non : had _non ? formatTerms ( data [ 0 ] ) : null
} ;
2018-05-31 18:34:15 -04:00
}
} ) ;
2019-05-31 16:05:50 -04:00
this . settings . add ( 'chat.filtering.clickable-mentions' , {
default : false ,
ui : {
component : 'setting-check-box' ,
2019-05-31 23:21:49 -04:00
path : 'Chat > Viewer Cards >> Behavior' ,
2019-05-31 16:05:50 -04:00
title : 'Enable opening viewer cards by clicking mentions in chat.'
}
} ) ;
2023-09-06 16:10:47 -04:00
this . settings . add ( 'chat.filtering.all-mentions' , {
default : false ,
ui : {
component : 'setting-check-box' ,
path : 'Chat > Filtering > General >> Appearance' ,
title : 'Display mentions for all users without requiring an at sign (@).' ,
description : '**Note**: This setting can increase memory usage and impact chat performance.'
}
} ) ;
2020-07-24 17:55:11 -04:00
this . settings . add ( 'chat.filtering.color-mentions' , {
default : false ,
ui : {
component : 'setting-check-box' ,
2021-03-03 17:10:14 -05:00
path : 'Chat > Filtering > General >> Appearance' ,
2020-07-24 17:55:11 -04:00
title : 'Display mentions in chat with username colors.' ,
description : '**Note:** Not compatible with color overrides as mentions do not include user IDs.'
}
} ) ;
2023-09-06 16:10:47 -04:00
this . settings . add ( 'chat.filtering.need-colors' , {
requires : [ 'chat.filtering.all-mentions' , 'chat.filtering.color-mentions' ] ,
process ( ctx ) {
return ctx . get ( 'chat.filtering.all-mentions' ) || ctx . get ( 'chat.filtering.color-mentions' )
}
} ) ;
2020-06-23 17:17:00 -04:00
this . settings . add ( 'chat.filtering.bold-mentions' , {
default : true ,
ui : {
component : 'setting-check-box' ,
2021-03-03 17:10:14 -05:00
path : 'Chat > Filtering > General >> Appearance' ,
2020-06-23 17:17:00 -04:00
title : 'Display mentions in chat with a bold font.'
}
} ) ;
2021-04-30 17:38:49 -04:00
this . settings . add ( 'chat.filtering.mention-priority' , {
default : 0 ,
ui : {
path : 'Chat > Filtering > General >> Appearance' ,
title : 'Mention Priority' ,
component : 'setting-text-box' ,
type : 'number' ,
process : 'to_int' ,
2021-05-07 18:22:55 -04:00
description : 'Mentions of your name have this priority for the purpose of highlighting. See [Chat > Filtering > Highlight](~chat.filtering.highlight) for more details.'
2021-04-30 17:38:49 -04:00
}
} ) ;
2019-07-31 17:13:56 -04:00
this . settings . add ( 'chat.filtering.mention-color' , {
default : '' ,
ui : {
2021-03-03 17:10:14 -05:00
path : 'Chat > Filtering > General >> Appearance' ,
2019-07-31 17:13:56 -04:00
title : 'Custom Highlight Color' ,
component : 'setting-color-box' ,
description : 'If this is set, highlighted messages with no default color set will use this color rather than red.'
}
} ) ;
2017-11-17 14:59:46 -05:00
this . settings . add ( 'chat.filtering.highlight-mentions' , {
default : false ,
ui : {
2021-03-03 17:10:14 -05:00
path : 'Chat > Filtering > General >> Appearance' ,
2017-11-17 14:59:46 -05:00
title : 'Highlight messages that mention you.' ,
component : 'setting-check-box'
}
} ) ;
this . settings . add ( 'chat.filtering.highlight-tokens' , {
default : false ,
ui : {
2021-03-03 17:10:14 -05:00
path : 'Chat > Filtering > General >> Appearance' ,
2017-11-17 14:59:46 -05:00
title : 'Highlight matched words in chat.' ,
component : 'setting-check-box'
}
} ) ;
2017-11-13 01:23:39 -05:00
this . settings . add ( 'tooltip.images' , {
default : true ,
ui : {
path : 'Chat > Tooltips >> General @{"sort": -1}' ,
title : 'Display images in tooltips.' ,
component : 'setting-check-box'
}
} ) ;
this . settings . add ( 'tooltip.badge-images' , {
default : true ,
requires : [ 'tooltip.images' ] ,
process ( ctx , val ) {
return ctx . get ( 'tooltip.images' ) ? val : false
} ,
ui : {
path : 'Chat > Tooltips >> Badges' ,
title : 'Display large images of badges.' ,
component : 'setting-check-box'
}
} ) ;
this . settings . add ( 'tooltip.emote-sources' , {
default : true ,
ui : {
path : 'Chat > Tooltips >> Emotes' ,
title : 'Display known sources.' ,
component : 'setting-check-box'
}
} ) ;
this . settings . add ( 'tooltip.emote-images' , {
default : true ,
requires : [ 'tooltip.images' ] ,
process ( ctx , val ) {
return ctx . get ( 'tooltip.images' ) ? val : false
} ,
ui : {
path : 'Chat > Tooltips >> Emotes' ,
title : 'Display large images of emotes.' ,
component : 'setting-check-box'
}
} ) ;
this . settings . add ( 'tooltip.rich-links' , {
default : true ,
ui : {
sort : - 1 ,
path : 'Chat > Tooltips >> Links' ,
title : 'Display rich tooltips for links.' ,
2019-09-12 13:11:08 -04:00
component : 'setting-check-box' ,
extra : {
component : 'chat-tooltip-example'
}
2017-11-13 01:23:39 -05:00
}
} ) ;
2017-11-14 04:11:43 -05:00
this . settings . add ( 'tooltip.link-interaction' , {
default : true ,
ui : {
path : 'Chat > Tooltips >> Links' ,
title : 'Allow interaction with supported link tooltips.' ,
component : 'setting-check-box'
}
} ) ;
2017-11-13 01:23:39 -05:00
this . settings . add ( 'tooltip.link-images' , {
default : true ,
requires : [ 'tooltip.images' ] ,
process ( ctx , val ) {
return ctx . get ( 'tooltip.images' ) ? val : false
} ,
ui : {
path : 'Chat > Tooltips >> Links' ,
title : 'Display images for links.' ,
component : 'setting-check-box'
}
} ) ;
this . settings . add ( 'tooltip.link-nsfw-images' , {
default : false ,
ui : {
path : 'Chat > Tooltips >> Links' ,
title : 'Display potentially NSFW images.' ,
description : 'When enabled, FrankerFaceZ will include images that are tagged as unsafe or that are not rated.' ,
component : 'setting-check-box'
}
} ) ;
this . settings . add ( 'chat.adjustment-mode' , {
2021-06-18 14:27:14 -04:00
default : null ,
process ( ctx , val ) {
if ( val == null )
return ( ctx . get ( 'ls.useHighContrastColors' ) ? ? true ) ? 1 : 0 ;
return val ;
} ,
requires : [ 'ls.useHighContrastColors' ] ,
2017-11-13 01:23:39 -05:00
ui : {
path : 'Chat > Appearance >> Colors' ,
title : 'Adjustment' ,
description : 'Alter user colors to ensure that they remain readable.' ,
2021-06-18 14:27:14 -04:00
default ( ctx ) {
return ( ctx . get ( 'ls.useHighContrastColors' ) ? ? true ) ? 1 : 0 ;
} ,
2017-11-13 01:23:39 -05:00
component : 'setting-select-box' ,
data : [
{ value : - 1 , title : 'No Color' } ,
{ value : 0 , title : 'Unchanged' } ,
{ value : 1 , title : 'HSL Luma' } ,
{ value : 2 , title : 'Luv Luma' } ,
2017-11-16 15:54:58 -05:00
{ value : 3 , title : 'HSL Loop (BTTV-Like)' } ,
{ value : 4 , title : 'RGB Loop (Deprecated)' }
2017-11-13 01:23:39 -05:00
]
}
} ) ;
this . settings . add ( 'chat.adjustment-contrast' , {
default : 4.5 ,
ui : {
path : 'Chat > Appearance >> Colors' ,
title : 'Minimum Contrast' ,
description : 'Set the minimum contrast ratio used by Luma adjustments when determining readability.' ,
component : 'setting-text-box' ,
2021-04-24 14:37:01 -04:00
process : 'to_float'
2017-11-13 01:23:39 -05:00
}
} ) ;
2021-04-28 16:27:58 -04:00
this . settings . add ( 'chat.me-style' , {
default : 2 ,
ui : {
path : 'Chat > Appearance >> Chat Lines' ,
title : 'Action Style' ,
description : 'When someone uses `/me`, the message will be rendered in this style.' ,
component : 'setting-select-box' ,
data : [
{ value : 0 , title : 'No Style' } ,
{ value : 1 , title : 'Colorized (Old Style)' } ,
{ value : 2 , title : 'Italic (New Style)' } ,
{ value : 3 , title : 'Colorized Italic' }
]
}
} ) ;
2017-11-13 01:23:39 -05:00
this . settings . add ( 'chat.bits.stack' , {
default : 0 ,
ui : {
path : 'Chat > Bits and Cheering >> Appearance' ,
title : 'Cheer Stacking' ,
description : 'Collect all the cheers in a message into a single cheer at the start of the message.' ,
component : 'setting-select-box' ,
data : [
{ value : 0 , title : 'Disabled' } ,
{ value : 1 , title : 'Grouped by Type' } ,
{ value : 2 , title : 'All in One' }
]
}
} ) ;
2021-03-20 18:47:12 -04:00
this . settings . add ( 'chat.emotes.animated' , {
2021-06-18 14:27:14 -04:00
default : null ,
requires : [ 'ls.emoteAnimationsEnabled' ] ,
process ( ctx , val ) {
if ( val == null )
return ( ctx . get ( 'ls.emoteAnimationsEnabled' ) ? ? true ) ? 1 : 0 ;
return val ;
} ,
2021-03-20 18:47:12 -04:00
ui : {
path : 'Chat > Appearance >> Emotes' ,
2021-04-27 16:23:19 -04:00
sort : - 50 ,
2021-03-20 18:47:12 -04:00
title : 'Animated Emotes' ,
2021-03-21 12:50:45 -04:00
2021-06-18 14:27:14 -04:00
default ( ctx ) {
return ( ctx . get ( 'ls.emoteAnimationsEnabled' ) ? ? true ) ? 1 : 0 ;
} ,
2021-03-21 12:50:45 -04:00
getExtraTerms : ( ) => GIF _TERMS ,
2021-03-20 18:47:12 -04:00
description : 'This controls whether or not animated emotes are allowed to play in chat. When this is `Disabled`, emotes will appear as static images. Setting this to `Enable on Hover` may cause performance issues.' ,
component : 'setting-select-box' ,
data : [
{ value : 0 , title : 'Disabled' } ,
{ value : 1 , title : 'Enabled' } ,
{ value : 2 , title : 'Enable on Hover' }
]
}
} ) ;
this . settings . add ( 'tooltip.emote-images.animated' , {
requires : [ 'chat.emotes.animated' ] ,
default : null ,
process ( ctx , val ) {
if ( val == null )
val = ctx . get ( 'chat.emotes.animated' ) ? true : false ;
return val ;
} ,
ui : {
path : 'Chat > Tooltips >> Emotes' ,
title : 'Display animated images of emotes.' ,
2021-03-21 12:50:45 -04:00
getExtraTerms : ( ) => GIF _TERMS ,
2021-03-20 18:47:12 -04:00
description : 'If this is not overridden, animated images are only shown in emote tool-tips if [Chat > Appearance >> Emotes > Animated Emotes](~chat.appearance.emotes) is not disabled.' ,
component : 'setting-check-box'
}
} ) ;
2017-11-13 01:23:39 -05:00
this . settings . add ( 'chat.bits.animated' , {
2021-03-22 18:19:09 -04:00
default : true ,
2017-11-13 01:23:39 -05:00
ui : {
path : 'Chat > Bits and Cheering >> Appearance' ,
title : 'Display animated cheers.' ,
component : 'setting-check-box'
}
} ) ;
2018-10-01 15:36:38 -04:00
const ts = new Date ( 0 ) . toLocaleTimeString ( ) . toUpperCase ( ) ,
default _24 = ts . lastIndexOf ( 'PM' ) === - 1 && ts . lastIndexOf ( 'AM' ) === - 1 ;
2018-10-01 21:06:42 +02:00
this . settings . add ( 'chat.timestamp-format' , {
2018-10-01 15:36:38 -04:00
default : default _24 ? 'H:mm' : 'h:mm' ,
2018-10-01 21:06:42 +02:00
ui : {
path : 'Chat > Appearance >> Chat Lines' ,
title : 'Timestamp Format' ,
2018-10-01 15:36:38 -04:00
component : 'setting-combo-box' ,
description : 'Timestamps are formatted using the [Day.js](https://github.com/iamkun/dayjs#readme) library. More details about formatting strings [can be found here](https://github.com/iamkun/dayjs/blob/HEAD/docs/en/API-reference.md#list-of-all-available-formats)' ,
2018-10-01 21:06:42 +02:00
data : [
2018-10-01 15:36:38 -04:00
{ value : 'h:mm' , title : '12 Hour' } ,
{ value : 'h:mm:ss' , title : '12 Hour with Seconds' } ,
{ value : 'H:mm' , title : '24 Hour' } ,
{ value : 'H:mm:ss' , title : '24 Hour with Seconds' } ,
{ value : 'hh:mm' , title : 'Padded' } ,
{ value : 'hh:mm:ss' , title : 'Padded with Seconds' } ,
{ value : 'HH:mm' , title : 'Padded 24 Hour' } ,
{ value : 'HH:mm:ss' , title : 'Padded 24 Hour with Seconds' } ,
2018-10-01 21:06:42 +02:00
]
}
} ) ;
2017-11-13 01:23:39 -05:00
this . context . on ( 'changed:theme.is-dark' , ( ) => {
2018-03-15 03:31:30 -04:00
for ( const room of this . iterateRooms ( ) )
room . buildBitsCSS ( ) ;
2017-11-13 01:23:39 -05:00
} ) ;
this . context . on ( 'changed:chat.bits.animated' , ( ) => {
2018-03-15 03:31:30 -04:00
for ( const room of this . iterateRooms ( ) )
room . buildBitsCSS ( ) ;
2017-11-13 01:23:39 -05:00
} ) ;
2020-07-24 17:55:11 -04:00
2023-09-06 16:10:47 -04:00
this . context . on ( 'changed:chat.filtering.need-colors' , async val => {
2020-07-24 17:55:11 -04:00
if ( val )
await this . createColorCache ( ) ;
else
this . color _cache = null ;
4.25.0
* Fixed: Smooth Scrolling no longer causing chat to scroll. (Closes #1068)
* Fixed: Issue with users using certain external stylesheets causing chat messages to become impossible to read on mouse hover. (Closes #1066)
* Fixed: Issues with badge sorting causing some badges to be overridden when they shouldn't be.
* Changed: Improve caching of badge data, such that re-rendering chat lines requires less computation.
* Changed: Refactor how chat lines listen for settings changes to reduce code duplication.
* Changed: Refactor how chat lines are invalidated to minimize work when changing settings.
* API Added: `chat:rerender-lines` event that, when emitted, causes all chat lines to be re-rendered.
* API Added: `chat:update-line-tokens` event that, when emitted, causes all chat lines to have their tokens invalidated and recalculated.
* API Added: `chat:update-line-badges` event that, when emitted, causes all chat lines to have their cached badges invalidated and recalculated.
* API Changed: `chat:update-lines-by-user` now has extra properties for separately invalidating tokens or badges. The full signature is `chat:update-lines-by-user(id, login, invalidate_tokens = true, invalidate_badges = true)`
2021-06-23 16:08:57 -04:00
this . emit ( ':update-line-tokens' ) ;
2020-07-24 17:55:11 -04:00
} ) ;
2023-09-06 16:10:47 -04:00
this . context . on ( 'changed:chat.filtering.all-mentions' , ( ) => this . emit ( ':update-line-tokens' ) ) ;
this . context . on ( 'changed:chat.filtering.color-mentions' , ( ) => this . emit ( ':update-line-tokens' ) ) ;
2020-07-24 17:55:11 -04:00
}
async createColorCache ( ) {
const LRUCache = await require ( /* webpackChunkName: 'utils' */ 'mnemonist/lru-cache' ) ;
this . color _cache = new LRUCache ( 150 ) ;
2017-11-13 01:23:39 -05:00
}
2018-12-03 18:08:32 -05:00
generateLog ( ) {
const out = [ 'chat settings' , '-------------------------------------------------------------------------------' ] ;
for ( const [ key , value ] of this . context . _ _cache . entries ( ) )
out . push ( ` ${ key } : ${ JSON . stringify ( value ) } ` ) ;
return out . join ( '\n' ) ;
}
2017-11-13 01:23:39 -05:00
onEnable ( ) {
2021-03-02 16:55:25 -05:00
this . socket = this . resolve ( 'socket' ) ;
2022-12-18 17:30:34 -05:00
this . on ( 'site.subpump:pubsub-message' , this . onPubSub , this ) ;
2023-09-06 16:10:47 -04:00
if ( this . context . get ( 'chat.filtering.need-colors' ) )
4.25.0
* Fixed: Smooth Scrolling no longer causing chat to scroll. (Closes #1068)
* Fixed: Issue with users using certain external stylesheets causing chat messages to become impossible to read on mouse hover. (Closes #1066)
* Fixed: Issues with badge sorting causing some badges to be overridden when they shouldn't be.
* Changed: Improve caching of badge data, such that re-rendering chat lines requires less computation.
* Changed: Refactor how chat lines listen for settings changes to reduce code duplication.
* Changed: Refactor how chat lines are invalidated to minimize work when changing settings.
* API Added: `chat:rerender-lines` event that, when emitted, causes all chat lines to be re-rendered.
* API Added: `chat:update-line-tokens` event that, when emitted, causes all chat lines to have their tokens invalidated and recalculated.
* API Added: `chat:update-line-badges` event that, when emitted, causes all chat lines to have their cached badges invalidated and recalculated.
* API Changed: `chat:update-lines-by-user` now has extra properties for separately invalidating tokens or badges. The full signature is `chat:update-lines-by-user(id, login, invalidate_tokens = true, invalidate_badges = true)`
2021-06-23 16:08:57 -04:00
this . createColorCache ( ) . then ( ( ) => this . emit ( ':update-line-tokens' ) ) ;
2020-07-24 17:55:11 -04:00
2017-11-13 01:23:39 -05:00
for ( const key in TOKENIZERS )
if ( has ( TOKENIZERS , key ) )
this . addTokenizer ( TOKENIZERS [ key ] ) ;
2018-04-03 19:28:06 -04:00
for ( const key in RICH _PROVIDERS )
if ( has ( RICH _PROVIDERS , key ) )
this . addRichProvider ( RICH _PROVIDERS [ key ] ) ;
2022-12-18 17:30:34 -05:00
for ( const key in LINK _PROVIDERS )
if ( has ( LINK _PROVIDERS , key ) )
this . addLinkProvider ( LINK _PROVIDERS [ key ] ) ;
2023-04-24 15:09:21 -04:00
this . on ( 'chat:reload-data' , flags => {
for ( const room of this . iterateRooms ( ) )
room . load _data ( ) ;
} ) ;
this . on ( 'chat:get-tab-commands' , event => {
event . commands . push ( {
2023-05-19 15:02:25 -04:00
name : 'ffz:reload' ,
2023-04-24 15:09:21 -04:00
description : this . i18n . t ( 'chat.command.reload' , 'Reload FFZ and add-on chat data (emotes, badges, etc.)' ) ,
permissionLevel : 0 ,
ffz _group : 'FrankerFaceZ'
} ) ;
} ) ;
this . triggered _reload = false ;
this . on ( 'chat:ffz-command:reload' , event => {
if ( this . triggered _reload )
return ;
const sc = this . resolve ( 'site.chat' ) ;
if ( sc ? . addNotice )
sc . addNotice ( '*' , this . i18n . t ( 'chat.command.reload.starting' , 'FFZ is reloading data...' ) ) ;
this . triggered _reload = true ;
this . emit ( 'chat:reload-data' ) ;
} ) ;
2023-05-19 15:02:25 -04:00
this . on ( 'load_tracker:complete:chat-data' , ( list ) => {
2023-04-24 15:09:21 -04:00
if ( this . triggered _reload ) {
const sc = this . resolve ( 'site.chat' ) ;
if ( sc ? . addNotice )
2023-05-19 15:02:25 -04:00
sc . addNotice ( '*' , this . i18n . t ( 'chat.command.reload.done' , 'FFZ has finished reloading data. (Sources: {list})' , { list : list . join ( ', ' ) } ) ) ;
2023-04-24 15:09:21 -04:00
}
this . triggered _reload = false ;
} ) ;
2022-12-18 17:30:34 -05:00
}
onPubSub ( event ) {
if ( event . prefix === 'stream-chat-room-v1' && event . message . type === 'chat_rich_embed' ) {
const data = event . message . data ,
url = data . request _url ,
providers = this . _ _link _providers ;
// Don't re-cache.
if ( this . _link _info [ url ] )
return ;
for ( const provider of providers ) {
const match = provider . test . call ( this , url ) ;
if ( match ) {
const processed = provider . receive ? provider . receive . call ( this , match , data ) : data ;
let result = provider . process . call ( this , match , processed ) ;
if ( ! ( result instanceof Promise ) )
result = Promise . resolve ( result ) ;
result . then ( value => {
// If something is already running, don't override it.
let info = this . _link _info [ url ] ;
if ( info )
return ;
// Save the value.
this . _link _info [ url ] = [ true , Date . now ( ) + 120000 , value ] ;
} ) ;
return ;
}
}
}
2017-11-13 01:23:39 -05:00
}
2018-04-07 17:59:16 -04:00
getUser ( id , login , no _create , no _login , error = false ) {
2017-11-13 01:23:39 -05:00
let user ;
2017-11-22 20:21:01 -05:00
if ( id && typeof id === 'number' )
id = ` ${ id } ` ;
2017-11-13 01:23:39 -05:00
2019-04-28 17:28:16 -04:00
if ( id && this . user _ids [ id ] )
2017-11-13 01:23:39 -05:00
user = this . user _ids [ id ] ;
2019-04-28 17:28:16 -04:00
else if ( login && this . users [ login ] && ! no _login )
2017-11-13 01:23:39 -05:00
user = this . users [ login ] ;
2018-04-11 17:05:31 -04:00
if ( user && user . destroyed )
user = null ;
if ( ! user ) {
if ( no _create )
return null ;
else
user = new User ( this , null , id , login ) ;
}
2017-11-13 01:23:39 -05:00
if ( id && id !== user . id ) {
// If the ID isn't what we expected, something is very wrong here.
// Blame name changes.
2018-04-07 17:59:16 -04:00
if ( user . id ) {
this . log . warn ( ` Data mismatch for user # ${ id } -- Stored ID: ${ user . id } -- Login: ${ login } -- Stored Login: ${ user . login } ` ) ;
if ( error )
throw new Error ( 'id mismatch' ) ;
// Remove the old reference if we're going with this.
if ( this . user _ids [ user . id ] === user )
this . user _ids [ user . id ] = null ;
}
2017-11-13 01:23:39 -05:00
// Otherwise, we're just here to set the ID.
2017-11-22 20:21:01 -05:00
user . _id = id ;
2017-11-13 01:23:39 -05:00
this . user _ids [ id ] = user ;
}
if ( login ) {
const other = this . users [ login ] ;
if ( other ) {
2018-09-24 14:33:06 -04:00
if ( other !== user && ! no _login ) {
2017-11-13 01:23:39 -05:00
// If the other has an ID, something weird happened. Screw it
// and just take over.
if ( other . id )
this . users [ login ] = user ;
else {
2018-09-24 14:33:06 -04:00
user . merge ( other ) ;
other . destroy ( true ) ;
2017-11-13 01:23:39 -05:00
}
}
} else
this . users [ login ] = user ;
}
return user ;
}
2018-04-07 17:59:16 -04:00
getRoom ( id , login , no _create , no _login , error = false ) {
2017-11-13 01:23:39 -05:00
let room ;
2017-11-22 20:21:01 -05:00
if ( id && typeof id === 'number' )
id = ` ${ id } ` ;
2017-11-13 01:23:39 -05:00
2019-04-28 17:28:16 -04:00
if ( id && this . room _ids [ id ] )
2017-11-13 01:23:39 -05:00
room = this . room _ids [ id ] ;
2019-04-28 17:28:16 -04:00
else if ( login && this . rooms [ login ] && ! no _login )
2017-11-13 01:23:39 -05:00
room = this . rooms [ login ] ;
2018-04-11 17:05:31 -04:00
if ( room && room . destroyed )
room = null ;
2017-11-13 01:23:39 -05:00
2018-04-11 17:05:31 -04:00
if ( ! room ) {
if ( no _create )
return null ;
else
room = new Room ( this , id , login ) ;
}
2017-11-13 01:23:39 -05:00
if ( id && id !== room . id ) {
// If the ID isn't what we expected, something is very wrong here.
// Blame name changes. Or React not being atomic.
2018-04-07 17:59:16 -04:00
if ( room . id ) {
this . log . warn ( ` Data mismatch for room # ${ id } -- Stored ID: ${ room . id } -- Login: ${ login } -- Stored Login: ${ room . login } ` ) ;
if ( error )
throw new Error ( 'id mismatch' ) ;
// Remove the old reference if we're going with this.
if ( this . room _ids [ room . id ] === room )
this . room _ids [ room . id ] = null ;
}
2017-11-13 01:23:39 -05:00
// Otherwise, we're just here to set the ID.
2017-11-22 20:21:01 -05:00
room . _id = id ;
2017-11-13 01:23:39 -05:00
this . room _ids [ id ] = room ;
}
if ( login ) {
const other = this . rooms [ login ] ;
if ( other ) {
2018-09-24 14:33:06 -04:00
if ( other !== room && ! no _login ) {
2017-11-13 01:23:39 -05:00
// If the other has an ID, something weird happened. Screw it
// and just take over.
if ( other . id )
this . rooms [ login ] = room ;
else {
2018-09-24 14:33:06 -04:00
room . merge ( other ) ;
other . destroy ( true ) ;
2017-11-13 01:23:39 -05:00
}
}
} else
this . rooms [ login ] = room ;
}
return room ;
}
2018-04-02 03:30:22 -04:00
* iterateRooms ( ) {
2017-11-28 02:03:59 -05:00
const visited = new Set ;
for ( const id in this . room _ids )
if ( has ( this . room _ids , id ) ) {
const room = this . room _ids [ id ] ;
2019-05-16 14:46:26 -04:00
if ( room && ! room . destroyed ) {
2017-11-28 02:03:59 -05:00
visited . add ( room ) ;
yield room ;
}
}
for ( const login in this . rooms )
if ( has ( this . rooms , login ) ) {
const room = this . rooms [ login ] ;
2019-05-16 14:46:26 -04:00
if ( room && ! room . destroyed && ! visited . has ( room ) )
2017-11-28 02:03:59 -05:00
yield room ;
}
}
2020-08-12 16:10:06 -04:00
handleReplyClick ( event ) {
const target = event . target ,
fine = this . resolve ( 'site.fine' ) ;
if ( ! target || ! fine )
return ;
const chat = fine . searchParent ( target , n => n . props && n . props . reply && n . setOPCardTray ) ;
if ( chat )
chat . setOPCardTray ( chat . props . reply ) ;
}
2019-05-31 16:05:50 -04:00
handleMentionClick ( event ) {
if ( ! this . context . get ( 'chat.filtering.clickable-mentions' ) )
return ;
const target = event . target ,
ds = target && target . dataset ;
if ( ! ds || ! ds . login )
return ;
const fine = this . resolve ( 'site.fine' ) ;
if ( ! fine )
return ;
2020-08-12 16:10:06 -04:00
const chat = fine . searchParent ( target , n => n . props && n . props . onUsernameClick ) ;
2019-05-31 16:05:50 -04:00
if ( ! chat )
return ;
chat . props . onUsernameClick (
ds . login ,
undefined , undefined ,
event . currentTarget . getBoundingClientRect ( ) . bottom
) ;
}
2019-01-18 19:07:57 -05:00
clickToReveal ( event ) {
const target = event . target ;
if ( target ) {
if ( target . _ffz _visible )
target . textContent = '× × × ' ;
else if ( ! this . context . get ( 'chat.filtering.click-to-reveal' ) )
return ;
else if ( target . dataset )
target . textContent = target . dataset . text ;
target . _ffz _visible = ! target . _ffz _visible ;
}
}
2018-07-13 14:32:12 -04:00
standardizeWhisper ( msg ) { // eslint-disable-line class-methods-use-this
if ( ! msg )
return msg ;
if ( msg . _ffz _message )
return msg . _ffz _message ;
const emotes = { } ,
is _action = msg . content . startsWith ( '/me ' ) ,
offset = is _action ? 4 : 0 ,
out = msg . _ffz _message = {
2021-08-16 17:23:12 -04:00
user : { ... msg . from } , // Apollo seals this~
2018-07-13 14:32:12 -04:00
message : msg . content . slice ( offset ) ,
is _action ,
2018-07-16 13:57:56 -04:00
ffz _emotes : emotes ,
2018-07-13 14:32:12 -04:00
timestamp : msg . sentAt && msg . sentAt . getTime ( ) ,
deleted : false
} ;
out . user . color = out . user . chatColor ;
if ( Array . isArray ( msg . emotes ) && msg . emotes . length )
for ( const emote of msg . emotes ) {
const id = emote . emoteID ,
em = emotes [ id ] = emotes [ id ] || [ ] ;
em . push ( {
startIndex : emote . from - offset ,
endIndex : emote . to - offset
} ) ;
}
return out ;
}
2018-12-13 15:21:57 -05:00
getUserLevel ( msg ) { // eslint-disable-line class-methods-use-this
if ( ! msg || ! msg . user )
return 0 ;
2018-12-13 16:18:39 -05:00
if ( msg . user . login === msg . roomLogin || ( msg . badges && msg . badges . broadcaster ) )
2018-12-13 15:21:57 -05:00
return 4 ;
2018-12-13 16:18:39 -05:00
if ( ! msg . badges )
return 0 ;
2018-12-13 15:21:57 -05:00
if ( msg . badges . moderator )
return 3 ;
if ( msg . badges . vip )
return 2 ;
if ( msg . badges . subscriber )
return 1 ;
return 0 ;
}
2020-08-12 16:10:06 -04:00
tokenizeReply ( reply ) {
if ( ! reply )
return null ;
return [
{
type : 'reply' ,
text : reply . parentDisplayName ,
color : this . color _cache ? this . color _cache . get ( reply . parentUserLogin ) : null ,
recipient : reply . parentUserLogin
} ,
{
type : 'text' ,
text : ' '
}
] ;
}
2021-05-03 15:33:03 -04:00
applyHighlight ( msg , priority , color , reason , use _null _color = false ) { // eslint-disable-line class-methods-use-this
if ( ! msg )
return msg ;
const is _null = msg . mention _priority == null ,
matched = is _null || priority >= msg . mention _priority ,
higher = is _null || priority > msg . mention _priority ;
if ( msg . filters )
msg . filters . push ( ` ${ reason } ( ${ priority } ) ${ matched && color === false ? ':remove' : color ? ` : ${ color } ` : '' } ` ) ;
if ( matched ) {
msg . mention _priority = priority ;
if ( color === false ) {
if ( higher ) {
msg . mentioned = false ;
msg . clear _priority = priority ;
msg . mention _color = msg . highlights = null ;
}
return ;
}
msg . mentioned = true ;
if ( ! msg . highlights )
msg . highlights = new Set ;
}
if ( msg . mentioned && ( msg . clear _priority == null || priority >= msg . clear _priority ) ) {
msg . highlights . add ( reason ) ;
if ( ( color || use _null _color ) && ( higher || ! msg . mention _color ) )
msg . mention _color = color ;
}
}
2018-05-18 02:10:00 -04:00
standardizeMessage ( msg ) { // eslint-disable-line class-methods-use-this
if ( ! msg )
return msg ;
// Standardize User
if ( msg . sender && ! msg . user )
msg . user = msg . sender ;
2018-07-13 14:32:12 -04:00
if ( msg . from && ! msg . user )
msg . user = msg . from ;
2018-05-18 02:10:00 -04:00
let user = msg . user ;
if ( ! user )
user = msg . user = { } ;
2019-04-28 17:28:16 -04:00
const ext = msg . extension || { } ;
user . color = user . color || user . chatColor || ext . chatColor || null ;
2018-05-18 02:10:00 -04:00
user . type = user . type || user . userType || null ;
user . id = user . id || user . userID || null ;
user . login = user . login || user . userLogin || null ;
2019-10-22 17:29:59 -05:00
user . displayName = user . displayName || user . userDisplayName || user . login || ext . displayName ;
2018-05-18 02:10:00 -04:00
user . isIntl = user . login && user . displayName && user . displayName . trim ( ) . toLowerCase ( ) !== user . login ;
2020-07-24 17:55:11 -04:00
if ( this . color _cache && user . color )
this . color _cache . set ( user . login , user . color ) ;
2018-05-18 02:10:00 -04:00
// Standardize Message Content
if ( ! msg . message && msg . messageParts )
this . detokenizeMessage ( msg ) ;
if ( msg . content && ! msg . message ) {
if ( msg . content . fragments )
this . detokenizeContent ( msg ) ;
else
msg . message = msg . content . text ;
}
2018-07-13 17:02:40 -04:00
// Standardize Emotes
if ( ! msg . ffz _emotes )
this . standardizeEmotes ( msg ) ;
2018-05-18 02:10:00 -04:00
// Standardize Badges
if ( ! msg . badges && user . displayBadges ) {
const b = msg . badges = { } ;
2019-04-28 17:28:16 -04:00
for ( const item of user . displayBadges )
b [ item . setID ] = item . version ;
}
if ( ! msg . badges && ext . displayBadges ) {
const b = msg . badges = { } ;
for ( const item of ext . displayBadges )
2018-05-18 02:10:00 -04:00
b [ item . setID ] = item . version ;
}
2023-01-19 17:00:09 -05:00
// Validate User Type
if ( user . type == null && msg . badges && msg . badges . moderator )
user . type = 'mod' ;
2018-05-18 02:10:00 -04:00
// Standardize Timestamp
if ( ! msg . timestamp && msg . sentAt )
msg . timestamp = new Date ( msg . sentAt ) . getTime ( ) ;
// Standardize Deletion
if ( msg . deletedAt !== undefined )
msg . deleted = ! ! msg . deletedAt ;
2021-03-22 18:19:09 -04:00
// Addon Badges
msg . ffz _badges = this . badges . getBadges ( user . id , user . login , msg . roomID , msg . roomLogin ) ;
2018-05-18 02:10:00 -04:00
return msg ;
}
2018-07-13 17:02:40 -04:00
standardizeEmotes ( msg ) { // eslint-disable-line class-methods-use-this
if ( msg . emotes && msg . message ) {
const emotes = { } ,
chars = split _chars ( msg . message ) ;
2020-08-12 18:34:40 -04:00
let offset = 0 ;
if ( msg . message && msg . messageBody && msg . message !== msg . messageBody )
offset = chars . length - split _chars ( msg . messageBody ) . length ;
2018-07-13 17:02:40 -04:00
for ( const key in msg . emotes )
if ( has ( msg . emotes , key ) ) {
const raw _emote = msg . emotes [ key ] ;
if ( Array . isArray ( raw _emote ) )
return msg . ffz _emotes = msg . emotes ;
2020-05-27 15:44:37 -04:00
const em = emotes [ raw _emote . id ] = emotes [ raw _emote . id ] || [ ] ;
2020-08-12 18:34:40 -04:00
let idx = raw _emote . startIndex + 1 + offset ;
2020-05-27 15:44:37 -04:00
while ( idx < chars . length ) {
if ( EMOTE _CHARS . test ( chars [ idx ] ) )
break ;
idx ++ ;
}
2018-07-13 17:02:40 -04:00
em . push ( {
2020-08-12 18:34:40 -04:00
startIndex : raw _emote . startIndex + offset ,
2020-05-27 15:44:37 -04:00
endIndex : idx - 1
2018-07-13 17:02:40 -04:00
} ) ;
}
msg . ffz _emotes = emotes ;
return ;
}
if ( msg . messageParts )
this . detokenizeMessage ( msg , true ) ;
else if ( msg . content && msg . content . fragments )
this . detokenizeContent ( msg , true ) ;
}
detokenizeContent ( msg , emotes _only = false ) { // eslint-disable-line class-methods-use-this
2018-05-18 02:10:00 -04:00
const out = [ ] ,
parts = msg . content . fragments ,
l = parts . length ,
emotes = { } ;
let idx = 0 , ret , first = true ;
for ( let i = 0 ; i < l ; i ++ ) {
const part = parts [ i ] ,
content = part . content ,
ct = content && content . _ _typename ;
ret = part . text ;
if ( ct === 'Emote' ) {
const id = content . emoteID ,
em = emotes [ id ] = emotes [ id ] || [ ] ;
em . push ( { startIndex : idx , endIndex : idx + ret . length - 1 } ) ;
}
if ( ret && ret . length ) {
if ( first && ret . startsWith ( '/me ' ) ) {
msg . is _action = true ;
ret = ret . slice ( 4 ) ;
}
2018-06-27 14:13:59 -04:00
idx += split _chars ( ret ) . length ;
2018-05-18 02:10:00 -04:00
out . push ( ret ) ;
}
2019-06-20 15:15:54 -04:00
first = false ;
2018-05-18 02:10:00 -04:00
}
2018-07-13 17:02:40 -04:00
if ( ! emotes _only )
msg . message = out . join ( '' ) ;
msg . ffz _emotes = emotes ;
2018-05-18 02:10:00 -04:00
return msg ;
}
2018-07-13 17:02:40 -04:00
detokenizeMessage ( msg , emotes _only = false ) { // eslint-disable-line class-methods-use-this
2018-05-18 02:10:00 -04:00
const out = [ ] ,
parts = msg . messageParts ,
l = parts . length ,
emotes = { } ;
2019-05-03 19:30:46 -04:00
let idx = 0 , ret , last _type = null , bits = 0 ;
2018-05-18 02:10:00 -04:00
for ( let i = 0 ; i < l ; i ++ ) {
const part = parts [ i ] ,
2021-04-30 17:38:49 -04:00
content = part . ffz _content ? ? part . content ;
2018-05-18 02:10:00 -04:00
if ( ! content )
continue ;
if ( typeof content === 'string' )
ret = content ;
else if ( content . recipient )
ret = ` @ ${ content . recipient } ` ;
else if ( content . url )
ret = content . url ;
2019-05-03 19:30:46 -04:00
else if ( content . cheerAmount ) {
bits += content . cheerAmount ;
2018-05-18 02:10:00 -04:00
ret = ` ${ content . alt } ${ content . cheerAmount } ` ;
2019-05-03 19:30:46 -04:00
} else if ( content . images ) {
2020-09-15 19:17:29 -04:00
const url = ( content . images . themed ? content . images . dark : content . images . sources ) ;
let id = content . emoteID ;
if ( ! id ) {
const match = url && (
/\/emoticons\/v1\/(\d+)\/[\d.]+$/ . exec ( url [ '1x' ] ) ||
/\/emoticons\/v2\/(\d+)\// . exec ( url [ '1x' ] )
) ;
2018-05-18 02:10:00 -04:00
id = match && match [ 1 ] ;
2020-09-15 19:17:29 -04:00
}
2018-05-18 02:10:00 -04:00
ret = content . alt ;
if ( id ) {
const em = emotes [ id ] = emotes [ id ] || [ ] ,
offset = last _type > 0 ? 1 : 0 ;
em . push ( { startIndex : idx + offset , endIndex : idx + ret . length - 1 } ) ;
}
if ( last _type > 0 )
ret = ` ${ ret } ` ;
} else
continue ;
if ( ret ) {
2018-07-13 17:02:40 -04:00
idx += split _chars ( ret ) . length ;
2018-05-18 02:10:00 -04:00
last _type = part . type ;
out . push ( ret )
}
}
2018-07-13 17:02:40 -04:00
if ( ! emotes _only )
msg . message = out . join ( '' ) ;
2019-05-03 19:30:46 -04:00
msg . bits = bits ;
2018-07-13 17:02:40 -04:00
msg . ffz _emotes = emotes ;
2018-05-18 02:10:00 -04:00
return msg ;
}
2021-06-08 19:13:22 -04:00
/ * *
* Format a user block . This uses our use "chat.name-format" style .
*
* @ param { Object } user The user object we ' re rendering .
* @ param { Function } e createElement method , either from React or utilities / dom .
* @ returns { Array } Array of rendered elements .
* /
formatUser ( user , e ) {
const setting = this . context . get ( 'chat.name-format' ) ;
const name = setting === 2 && user . isIntl ? user . login : ( user . displayName || user . login ) ;
const out = [ e ( 'span' , {
className : 'chat-author__display-name'
} , name ) ] ;
if ( setting === 0 && user . isIntl )
out . push ( e ( 'span' , {
className : 'chat-author__intl-login'
} , ` ( ${ user . login } ) ` ) ) ;
2023-03-27 18:50:32 -04:00
return out ;
2021-06-08 19:13:22 -04:00
}
2018-10-01 21:06:42 +02:00
formatTime ( time ) {
2017-11-13 01:23:39 -05:00
if ( ! ( time instanceof Date ) )
time = new Date ( time ) ;
2019-05-03 19:30:46 -04:00
const fmt = this . context . get ( 'chat.timestamp-format' ) ,
d = dayjs ( time ) ;
try {
return d . locale ( this . i18n . locale ) . format ( fmt ) ;
} catch ( err ) {
// If the locale isn't loaded, this can fail.
return d . format ( fmt ) ;
}
2017-11-13 01:23:39 -05:00
}
2021-04-14 16:53:15 -04:00
addHighlightReason ( key , data ) {
if ( typeof key === 'object' && key . key ) {
data = key ;
key = data . key ;
} else if ( typeof data === 'string' )
data = { title : data } ;
data . value = data . key = key ;
if ( ! data . i18n _key )
data . i18n _key = ` hl-reason. ${ key } ` ;
if ( this . _hl _reasons [ key ] )
throw new Error ( ` Highlight Reason already exists with key ${ key } ` ) ;
this . _hl _reasons [ key ] = data ;
}
getHighlightReasons ( ) {
return Object . values ( this . _hl _reasons ) ;
}
2017-11-13 01:23:39 -05:00
addTokenizer ( tokenizer ) {
const type = tokenizer . type ;
2022-12-18 17:30:34 -05:00
if ( has ( this . tokenizers , type ) ) {
this . log . warn ( ` Tried adding tokenizer of type ' ${ type } ' when one was already present. ` ) ;
return ;
}
2017-11-13 01:23:39 -05:00
this . tokenizers [ type ] = tokenizer ;
if ( tokenizer . priority == null )
tokenizer . priority = 0 ;
2017-11-14 04:11:43 -05:00
if ( tokenizer . tooltip ) {
const tt = tokenizer . tooltip ;
const tk = this . tooltips . types [ type ] = tt . bind ( this ) ;
2020-08-04 18:26:11 -04:00
for ( const i of [ 'interactive' , 'delayShow' , 'delayHide' , 'onShow' , 'onHide' ] )
2017-11-14 04:11:43 -05:00
tk [ i ] = typeof tt [ i ] === 'function' ? tt [ i ] . bind ( this ) : tt [ i ] ;
}
2017-11-13 01:23:39 -05:00
this . _ _tokenizers . push ( tokenizer ) ;
this . _ _tokenizers . sort ( ( a , b ) => {
if ( a . priority > b . priority ) return - 1 ;
if ( a . priority < b . priority ) return 1 ;
2018-04-03 19:28:06 -04:00
return a . type < b . type ;
} ) ;
}
2021-12-01 16:48:10 -05:00
removeTokenizer ( tokenizer ) {
let type ;
if ( typeof tokenizer === 'string' ) type = tokenizer ;
else type = tokenizer . type ;
tokenizer = this . tokenizers [ type ] ;
if ( ! tokenizer )
return null ;
if ( tokenizer . tooltip )
delete this . tooltips . types [ type ] ;
const idx = this . _ _tokenizers . indexOf ( tokenizer ) ;
if ( idx !== - 1 )
this . _ _tokenizers . splice ( idx , 1 ) ;
return tokenizer ;
}
2018-04-03 19:28:06 -04:00
2022-12-18 17:30:34 -05:00
addLinkProvider ( provider ) {
const type = provider . type ;
if ( has ( this . link _providers , type ) ) {
this . log . warn ( ` Tried adding link provider of type ' ${ type } ' when one was already present. ` ) ;
return ;
}
this . link _providers [ type ] = provider ;
if ( provider . priority == null )
provider . priority = 0 ;
this . _ _link _providers . push ( provider ) ;
this . _ _link _providers . sort ( ( a , b ) => {
if ( a . priority > b . priority ) return - 1 ;
if ( a . priority < b . priority ) return 1 ;
return a . type < b . type ;
} ) ;
}
removeLinkProvider ( provider ) {
let type ;
if ( typeof provider === 'string' ) type = provider ;
else type = provider . type ;
provider = this . link _providers [ type ] ;
if ( ! provider )
return null ;
const idx = this . _ _link _providers . indexOf ( provider ) ;
if ( idx !== - 1 )
this . _ _link _providers . splice ( idx , 1 ) ;
return provider ;
}
2018-04-03 19:28:06 -04:00
addRichProvider ( provider ) {
const type = provider . type ;
2022-12-18 17:30:34 -05:00
if ( has ( this . rich _providers , type ) ) {
this . log . warn ( ` Tried adding rich provider of type ' ${ type } ' when one was already present. ` ) ;
return ;
}
2018-04-03 19:28:06 -04:00
this . rich _providers [ type ] = provider ;
if ( provider . priority == null )
provider . priority = 0 ;
this . _ _rich _providers . push ( provider ) ;
this . _ _rich _providers . sort ( ( a , b ) => {
if ( a . priority > b . priority ) return - 1 ;
if ( a . priority < b . priority ) return 1 ;
return a . type < b . type ;
2017-11-13 01:23:39 -05:00
} ) ;
}
2021-12-01 16:48:10 -05:00
removeRichProvider ( provider ) {
let type ;
if ( typeof provider === 'string' ) type = provider ;
else type = provider . type ;
provider = this . rich _providers [ type ] ;
if ( ! provider )
return null ;
const idx = this . _ _rich _providers . indexOf ( provider ) ;
if ( idx !== - 1 )
this . _ _rich _providers . splice ( idx , 1 ) ;
return provider ;
}
2017-11-13 01:23:39 -05:00
2021-12-01 16:48:10 -05:00
tokenizeString ( message , msg , user , haltable = false ) {
2017-11-13 01:23:39 -05:00
let tokens = [ { type : 'text' , text : message } ] ;
2021-12-01 16:48:10 -05:00
for ( const tokenizer of this . _ _tokenizers ) {
if ( ! tokenizer . process )
continue ;
const new _tokens = tokenizer . process . call ( this , tokens , msg , user , haltable ) ;
if ( new _tokens )
tokens = new _tokens ;
if ( haltable && msg . ffz _halt _tokens ) {
msg . ffz _halt _tokens = undefined ;
break ;
}
}
2017-11-13 01:23:39 -05:00
return tokens ;
}
2018-12-13 15:21:57 -05:00
pluckRichContent ( tokens , msg ) { // eslint-disable-line class-methods-use-this
if ( ! this . context . get ( 'chat.rich.enabled' ) || this . context . get ( 'chat.rich.minimum-level' ) > this . getUserLevel ( msg ) )
2018-04-03 19:28:06 -04:00
return ;
2021-04-30 17:38:49 -04:00
if ( ! Array . isArray ( tokens ) )
return ;
2018-04-03 19:28:06 -04:00
const providers = this . _ _rich _providers ;
2021-11-15 17:12:01 -05:00
const want _mid = this . context . get ( 'chat.rich.want-mid' ) ;
2018-04-03 19:28:06 -04:00
for ( const token of tokens ) {
2023-04-20 00:55:52 -04:00
if ( token . allow _rich ? ? true )
2023-04-19 17:19:10 -04:00
for ( const provider of providers )
if ( provider . test . call ( this , token , msg ) ) {
token . hidden = provider . can _hide _token && ( this . context . get ( 'chat.rich.hide-tokens' ) || provider . hide _token ) ;
return provider . process . call ( this , token , want _mid ) ;
}
2018-04-03 19:28:06 -04:00
}
}
2021-03-22 18:19:09 -04:00
tokenizeMessage ( msg , user , haltable = false ) {
2018-02-22 18:23:44 -05:00
if ( msg . content && ! msg . message )
msg . message = msg . content . text ;
if ( msg . sender && ! msg . user )
msg . user = msg . sender ;
2018-03-01 04:13:52 -05:00
if ( ! msg . message )
return [ ] ;
2017-11-13 01:23:39 -05:00
let tokens = [ { type : 'text' , text : msg . message } ] ;
2021-03-22 18:19:09 -04:00
for ( const tokenizer of this . _ _tokenizers ) {
2021-12-01 16:48:10 -05:00
if ( ! tokenizer . process )
continue ;
const new _tokens = tokenizer . process . call ( this , tokens , msg , user , haltable ) ;
if ( new _tokens )
tokens = new _tokens ;
2021-03-22 18:19:09 -04:00
if ( haltable && msg . ffz _halt _tokens ) {
msg . ffz _halt _tokens = undefined ;
break ;
}
}
2017-11-13 01:23:39 -05:00
2021-04-30 17:38:49 -04:00
return tokens || [ ] ;
2017-11-13 01:23:39 -05:00
}
2020-08-12 16:10:06 -04:00
renderTokens ( tokens , e , reply ) {
2017-11-13 01:23:39 -05:00
if ( ! e )
e = createElement ;
const out = [ ] ,
tokenizers = this . tokenizers ,
l = tokens . length ;
2022-08-02 16:59:50 -04:00
const hidden = this . context . get ( 'chat.filtering.hidden-tokens' ) ;
2017-11-13 01:23:39 -05:00
for ( let i = 0 ; i < l ; i ++ ) {
const token = tokens [ i ] ,
type = token . type ,
tk = tokenizers [ type ] ;
2022-08-02 16:59:50 -04:00
if ( token . hidden || hidden . has ( type ) )
2017-11-13 01:23:39 -05:00
continue ;
let res ;
2020-08-12 16:10:06 -04:00
// If we have a reply, skip the initial mention.
if ( reply && i === 0 && type === 'mention' && token . recipient && token . recipient === reply . parentUserLogin )
continue ;
2017-11-13 01:23:39 -05:00
if ( type === 'text' )
2017-11-14 04:11:43 -05:00
res = e ( 'span' , {
2019-05-16 14:46:26 -04:00
className : 'text-fragment' ,
2017-11-14 04:11:43 -05:00
'data-a-target' : 'chat-message-text'
} , token . text ) ;
2017-11-13 01:23:39 -05:00
else if ( tk )
2020-08-12 16:10:06 -04:00
res = tk . render . call ( this , token , e , reply ) ;
2017-11-13 01:23:39 -05:00
else
res = e ( 'em' , {
className : 'ffz-unknown-token ffz-tooltip' ,
'data-tooltip-type' : 'json' ,
'data-data' : JSON . stringify ( token , null , 2 )
} , ` [unknown token: ${ type } ] ` )
if ( res )
out . push ( res ) ;
}
return out ;
}
// ====
// Twitch Crap
// ====
2020-07-29 02:22:45 -04:00
clearLinkCache ( url ) {
if ( url ) {
const info = this . _link _info [ url ] ;
if ( ! info [ 0 ] ) {
for ( const pair of info [ 2 ] )
pair [ 1 ] ( ) ;
}
this . _link _info [ url ] = null ;
this . emit ( ':update-link-resolver' , url ) ;
return ;
}
const old = this . _link _info ;
this . _link _info = { } ;
for ( const info of Object . values ( old ) ) {
if ( ! info [ 0 ] ) {
for ( const pair of info [ 2 ] )
pair [ 1 ] ( ) ;
}
}
this . emit ( ':update-link-resolver' ) ;
}
2020-08-07 01:32:18 -04:00
get _link _info ( url , no _promises , refresh = false ) {
2017-11-16 15:54:58 -05:00
let info = this . _link _info [ url ] ;
const expires = info && info [ 1 ] ;
2017-11-13 01:23:39 -05:00
2020-08-07 01:32:18 -04:00
if ( ( info && info [ 0 ] && refresh ) || ( expires && Date . now ( ) > expires ) )
2017-11-13 01:23:39 -05:00
info = this . _link _info [ url ] = null ;
if ( info && info [ 0 ] )
return no _promises ? info [ 2 ] : Promise . resolve ( info [ 2 ] ) ;
if ( no _promises )
return null ;
else if ( info )
return new Promise ( ( resolve , reject ) => info [ 2 ] . push ( [ resolve , reject ] ) )
return new Promise ( ( resolve , reject ) => {
info = this . _link _info [ url ] = [ false , null , [ [ resolve , reject ] ] ] ;
const handle = ( success , data ) => {
2020-08-04 18:26:11 -04:00
data = this . fixLinkInfo ( data ) ;
2017-11-13 01:23:39 -05:00
const callbacks = ! info [ 0 ] && info [ 2 ] ;
info [ 0 ] = true ;
info [ 1 ] = Date . now ( ) + 120000 ;
info [ 2 ] = success ? data : null ;
if ( callbacks )
for ( const cbs of callbacks )
cbs [ success ? 0 : 1 ] ( data ) ;
}
2022-12-18 17:30:34 -05:00
// Try using a link provider.
for ( const lp of this . _ _link _providers ) {
const match = lp . test . call ( this , url ) ;
if ( match ) {
timeout ( lp . process . call ( this , match ) , 15000 )
. then ( data => handle ( true , data ) )
. catch ( err => handle ( false , err ) ) ;
return ;
}
}
2020-07-29 02:22:45 -04:00
let provider = this . settings . get ( 'debug.link-resolver.source' ) ;
if ( provider == null )
provider = this . experiments . getAssignment ( 'api_links' ) ? 'test' : 'socket' ;
2021-03-02 16:55:25 -05:00
if ( provider === 'socket' && ! this . socket )
provider = 'test' ;
2020-07-29 02:22:45 -04:00
if ( provider === 'socket' ) {
timeout ( this . socket . call ( 'get_link' , url ) , 15000 )
2020-07-26 21:26:42 -04:00
. then ( data => handle ( true , data ) )
. catch ( err => handle ( false , err ) ) ;
2020-07-29 02:22:45 -04:00
} else {
const host = provider === 'dev' ? 'https://localhost:8002/' :
provider === 'test' ? 'https://api-test.frankerfacez.com/v2/link' :
'https://api.frankerfacez.com/v2/link' ;
2017-11-13 01:23:39 -05:00
2020-07-29 02:22:45 -04:00
timeout ( fetch ( ` ${ host } ?url= ${ encodeURIComponent ( url ) } ` ) . then ( r => r . json ( ) ) , 15000 )
2020-07-26 21:26:42 -04:00
. then ( data => handle ( true , data ) )
. catch ( err => handle ( false , err ) ) ;
2020-07-29 02:22:45 -04:00
}
2017-11-13 01:23:39 -05:00
} ) ;
}
2020-08-04 18:26:11 -04:00
fixLinkInfo ( data ) {
2023-01-19 17:00:09 -05:00
if ( ! data )
return data ;
2020-08-04 18:26:11 -04:00
if ( data . error && data . message )
data . error = data . message ;
if ( data . error )
data = {
v : 5 ,
2020-10-14 14:55:10 -04:00
title : this . i18n . t ( 'card.error' , 'An error occurred.' ) ,
2020-08-04 18:26:11 -04:00
description : data . error ,
short : {
type : 'header' ,
image : { type : 'image' , url : ERROR _IMAGE } ,
2020-10-14 14:55:10 -04:00
title : { type : 'i18n' , key : 'card.error' , phrase : 'An error occurred.' } ,
2020-08-04 18:26:11 -04:00
subtitle : data . error
}
}
if ( data . v < 5 && ! data . short && ! data . full && ( data . title || data . desc _1 || data . desc _2 ) ) {
const image = data . preview || data . image ;
data = {
v : 5 ,
short : {
type : 'header' ,
image : image ? {
type : 'image' ,
url : image ,
sfw : data . image _safe ? ? false ,
} : null ,
title : data . title ,
subtitle : data . desc _1 ,
extra : data . desc _2
}
}
}
return data ;
}
2017-11-13 01:23:39 -05:00
}