2018-04-12 20:30:00 -04:00
'use strict' ;
// ============================================================================
// Emoji Handling
// ============================================================================
import Module from 'utilities/module' ;
import { SERVER } from 'utilities/constants' ;
2018-04-28 17:56:03 -04:00
import { has } from 'utilities/object' ;
2019-06-03 19:47:41 -04:00
import { getBuster } from 'utilities/time' ;
2018-04-12 20:30:00 -04:00
import splitter from 'emoji-regex/es2015/index' ;
2020-08-15 15:45:50 -04:00
/ * e x p o r t c o n s t S I Z E S = {
2018-04-12 20:30:00 -04:00
apple : [ 64 , 160 ] ,
emojione : [ 64 ] ,
facebook : [ 64 , 96 ] ,
google : [ 64 , 136 ] ,
messenger : [ 64 , 128 ] ,
twitter : [ 64 , 72 ]
2020-08-15 15:45:50 -04:00
} * /
export const HIDDEN _CATEGORIES = [
'component'
] ;
export const CATEGORIES = {
'smileys-emotion' : 'Smileys & Emotions' ,
'people-body' : 'People' ,
'component' : 'Components' ,
'animals-nature' : 'Animals & Nature' ,
'food-drink' : 'Food & Drink' ,
'travel-places' : 'Travel & Places' ,
'activities' : 'Activities' ,
'objects' : 'Objects' ,
'symbols' : 'Symbols' ,
'flags' : 'Flags'
} ;
export const CATEGORY _SORT = Object . keys ( CATEGORIES ) ;
export const SKIN _TONES = {
1 : '1f3fb' ,
2 : '1f3fc' ,
3 : '1f3fd' ,
4 : '1f3fe' ,
5 : '1f3ff'
} ;
2022-02-11 15:17:32 -05:00
2022-02-11 16:35:06 -05:00
let enable _replace _joiner = true ;
let joiner ;
try {
joiner = new RegExp ( '(?<!\\u{E0002})\\u{E0002}' , 'gu' ) ;
} catch ( err ) {
enable _replace _joiner = false ;
joiner = null ;
}
export const JOINER _REPLACEMENT = joiner ; // /(?<!\u{E0002})\u{E0002}/gu;
2022-02-11 15:17:32 -05:00
export const ZWD _REPLACEMENT = /\u{200D}/gu ;
2021-12-12 00:15:29 +01:00
export const EMOJI _JOINER = '\u{E0002}' ;
2020-08-15 15:45:50 -04:00
export const IMAGE _PATHS = {
google : 'noto' ,
twitter : 'twemoji' ,
open : 'openmoji' ,
blob : 'blob'
} ;
2018-04-12 20:30:00 -04:00
export function codepoint _to _emoji ( cp ) {
let code = typeof cp === 'number' ? cp : parseInt ( cp , 16 ) ;
if ( code < 0x10000 )
return String . fromCharCode ( code ) ;
code -= 0x10000 ;
return String . fromCharCode (
0xD800 + ( code >> 10 ) ,
0xDC00 + ( code & 0x3FF )
) ;
}
export default class Emoji extends Module {
constructor ( ... args ) {
super ( ... args ) ;
this . inject ( '..emotes' ) ;
this . inject ( 'settings' ) ;
2022-02-11 16:35:06 -05:00
if ( enable _replace _joiner )
this . settings . add ( 'chat.emoji.replace-joiner' , {
default : 2 ,
ui : {
path : 'Chat > Behavior >> Emoji' ,
title : 'Emoji Joiner Workaround' ,
description : 'This feature is intended to allow the use of combined emoji in supported clients. This is required due to a bug in TMI that strips ZWJ characters from chat messages. [Visit the original issue](https://github.com/FrankerFaceZ/FrankerFaceZ/issues/1147) for more details.' ,
component : 'setting-select-box' ,
data : [
{ value : 0 , title : 'Disabled' } ,
{ value : 1 , title : 'Display Only' } ,
{ value : 2 , title : 'Display and Send' }
]
}
} ) ;
else {
this . log . warn ( 'This browser does not support regexp lookbehind. The "Emoji Joiner Workaround" feature will be disabled.' ) ;
this . settings . add ( 'chat.emoji.replace-joiner' , {
process ( ) { return 0 }
} ) ;
}
2022-02-11 15:17:32 -05:00
2018-04-12 20:30:00 -04:00
this . settings . add ( 'chat.emoji.style' , {
default : 'twitter' ,
2020-10-14 14:55:10 -04:00
process ( ctx , val ) {
if ( val != 0 && ! IMAGE _PATHS [ val ] )
return 'twitter' ;
return val ;
} ,
2018-04-12 20:30:00 -04:00
ui : {
path : 'Chat > Appearance >> Emoji' ,
title : 'Emoji Style' ,
component : 'setting-select-box' ,
data : [
2020-08-15 15:45:50 -04:00
{ value : 'twitter' , title : 'Twitter (Twemoji)' } ,
{ value : 'google' , title : 'Google (Noto)' } ,
{ value : 'blob' , title : 'Blob' } ,
{ value : 'open' , title : 'OpenMoji' } ,
{ value : 0 , title : 'Native' }
2018-04-12 20:30:00 -04:00
]
}
} ) ;
// For some reason, splitter is a function.
this . splitter = splitter ( ) ;
2021-06-10 18:53:16 -04:00
this . categories = CATEGORIES ;
2018-04-12 20:30:00 -04:00
this . emoji = { } ;
this . names = { } ;
this . chars = new Map ;
}
onEnable ( ) {
2022-02-11 15:17:32 -05:00
this . on ( 'chat:pre-send-message' , event => {
if ( event . context . get ( 'chat.emoji.replace-joiner' ) < 2 )
return ;
event . message = event . message . replace ( ZWD _REPLACEMENT , EMOJI _JOINER ) ;
} ) ;
2018-04-12 20:30:00 -04:00
this . loadEmojiData ( ) ;
}
async loadEmojiData ( tries = 0 ) {
let data ;
try {
2020-08-15 15:45:50 -04:00
data = await fetch ( ` ${ SERVER } /script/emoji/v3.2.json?_ ${ getBuster ( 60 ) } ` ) . then ( r =>
2018-04-12 20:30:00 -04:00
r . ok ? r . json ( ) : null
) ;
} catch ( err ) {
tries ++ ;
if ( tries < 10 )
return setTimeout ( ( ) => this . loadEmojiData ( tries ) , 500 * tries ) ;
this . log . error ( 'Error loading emoji data.' , err ) ;
return false ;
}
if ( ! data )
return false ;
const cats = data . c ,
out = { } ,
names = { } ,
chars = new Map ;
for ( const raw of data . e ) {
const emoji = Object . assign ( hydrate _emoji ( raw . slice ( 4 ) ) , {
category : cats [ raw [ 0 ] ] ,
sort : raw [ 1 ] ,
names : raw [ 2 ] ,
name : raw [ 3 ]
} ) ;
if ( ! Array . isArray ( emoji . names ) )
emoji . names = [ emoji . names ] ;
if ( ! emoji . name )
emoji . name = emoji . names [ 0 ] . replace ( /_/g , ' ' ) ;
out [ emoji . code ] = emoji ;
chars . set ( emoji . raw , [ emoji . code , null ] ) ;
for ( const name of emoji . names )
names [ name ] = emoji . code ;
// Variations
if ( raw [ 7 ] ) {
const vars = emoji . variants = { } ;
for ( const r of raw [ 7 ] ) {
2020-08-15 15:45:50 -04:00
if ( Array . isArray ( r [ 3 ] ) || ! r [ 3 ] ) {
// The tone picker doesn't support multiple tones
// for a single emoji. Just make this variation a
// new emoji.
const em = Object . assign ( hydrate _emoji ( r ) , {
category : cats [ raw [ 0 ] ] ,
sort : raw [ 1 ] ,
names : r [ 5 ] ,
hidden : true
} ) ;
if ( ! Array . isArray ( em . names ) )
em . names = [ em . names ] ;
em . name = em . names [ 0 ] . replace ( /_/g , ' ' ) ;
out [ em . code ] = em ;
chars . set ( em . raw , [ em . code , null ] ) ;
for ( const name of em . names )
names [ name ] = em . code ;
continue ;
}
// We just have a normal tone. We need to look
// up the modifier and use it.
const tone = SKIN _TONES [ r [ 3 ] ] ;
if ( ! tone ) {
console . warn ( 'Unknown tone:' , r [ 3 ] , r , emoji ) ;
continue ;
}
2018-04-12 20:30:00 -04:00
const vari = Object . assign ( hydrate _emoji ( r ) , {
2020-08-15 15:45:50 -04:00
key : tone
2018-04-12 20:30:00 -04:00
} ) ;
2020-08-15 15:45:50 -04:00
vars [ tone ] = vari ;
2018-04-12 20:30:00 -04:00
chars . set ( vari . raw , [ emoji . code , vari . key ] ) ;
}
}
}
this . emoji = out ;
this . names = names ;
this . chars = chars ;
this . log . info ( ` Loaded data about ${ Object . keys ( out ) . length } emoji. ` ) ;
this . emit ( ':populated' ) ;
return true ;
}
getFullImage ( image , style ) {
if ( ! style )
style = this . parent . context . get ( 'chat.emoji.style' ) ;
2020-08-15 15:45:50 -04:00
if ( ! has ( IMAGE _PATHS , style ) )
style = 'twitter' ;
return ` ${ SERVER } /static/emoji/images/ ${ IMAGE _PATHS [ style ] } / ${ image } ` ;
/ * i f ( ! h a s ( S I Z E S , s t y l e ) )
2018-04-12 20:30:00 -04:00
style = 'twitter' ;
2020-08-15 15:45:50 -04:00
return ` ${ SERVER } /static/emoji/img- ${ style } - ${ SIZES [ style ] [ 0 ] } / ${ image } ` ; * /
2018-04-12 20:30:00 -04:00
}
getFullImageSet ( image , style ) {
if ( ! style )
style = this . parent . context . get ( 'chat.emoji.style' ) ;
2020-08-15 15:45:50 -04:00
if ( ! has ( IMAGE _PATHS , style ) )
style = 'twitter' ;
return ` ${ SERVER } /static/emoji/images/ ${ IMAGE _PATHS [ style ] } / ${ image } 72w ` ;
/ * i f ( ! h a s ( S I Z E S , s t y l e ) )
2018-04-12 20:30:00 -04:00
style = 'twitter' ;
return SIZES [ style ] . map ( w =>
` ${ SERVER } /static/emoji/img- ${ style } - ${ w } / ${ image } ${ w } w `
2020-08-15 15:45:50 -04:00
) . join ( ', ' ) ; * /
2018-04-12 20:30:00 -04:00
}
}
function hydrate _emoji ( data ) {
2020-08-15 15:45:50 -04:00
let code = data [ 0 ] ;
if ( data [ 4 ] === 0 )
code = ` ${ code } -fe0f ` ;
2018-04-12 20:30:00 -04:00
return {
2020-08-15 15:45:50 -04:00
code ,
2018-04-12 20:30:00 -04:00
image : ` ${ data [ 0 ] } .png ` ,
2020-08-15 15:45:50 -04:00
raw : code . split ( '-' ) . map ( codepoint _to _emoji ) . join ( '' ) ,
2018-04-12 20:30:00 -04:00
sheet _x : data [ 1 ] [ 0 ] ,
sheet _y : data [ 1 ] [ 1 ] ,
has : {
2020-08-15 15:45:50 -04:00
google : ! ! ( 0b1000 & data [ 2 ] ) ,
blob : ! ! ( 0b0100 & data [ 2 ] ) || ! ! ( 0b1000 & data [ 2 ] ) , // Blob falls back to Noto
twitter : ! ! ( 0b0010 & data [ 2 ] ) ,
open : ! ! ( 0b0001 & data [ 2 ] )
2018-04-12 20:30:00 -04:00
}
} ;
}