1
0
Fork 0
mirror of https://github.com/FrankerFaceZ/FrankerFaceZ.git synced 2025-08-10 16:10:55 +00:00
FrankerFaceZ/src/modules/chat/emoji.js
SirStendec 6a7ac8c194 4.32.1
* Fixed: FFZ no longer working correctly for Safari users owing to poor regex support. Safari users will be unable to use the Emoji Joiner Workaround introduced in 4.32.0.
2022-02-11 16:35:06 -05:00

309 lines
No EOL
7.1 KiB
JavaScript

'use strict';
// ============================================================================
// Emoji Handling
// ============================================================================
import Module from 'utilities/module';
import {SERVER} from 'utilities/constants';
import {has} from 'utilities/object';
import { getBuster } from 'utilities/time';
import splitter from 'emoji-regex/es2015/index';
/*export const SIZES = {
apple: [64, 160],
emojione: [64],
facebook: [64, 96],
google: [64, 136],
messenger: [64, 128],
twitter: [64, 72]
}*/
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'
};
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;
export const ZWD_REPLACEMENT = /\u{200D}/gu;
export const EMOJI_JOINER = '\u{E0002}';
export const IMAGE_PATHS = {
google: 'noto',
twitter: 'twemoji',
open: 'openmoji',
blob: 'blob'
};
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');
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 }
});
}
this.settings.add('chat.emoji.style', {
default: 'twitter',
process(ctx, val) {
if ( val != 0 && ! IMAGE_PATHS[val] )
return 'twitter';
return val;
},
ui: {
path: 'Chat > Appearance >> Emoji',
title: 'Emoji Style',
component: 'setting-select-box',
data: [
{value: 'twitter', title: 'Twitter (Twemoji)'},
{value: 'google', title: 'Google (Noto)'},
{value: 'blob', title: 'Blob'},
{value: 'open', title: 'OpenMoji'},
{value: 0, title: 'Native'}
]
}
});
// For some reason, splitter is a function.
this.splitter = splitter();
this.categories = CATEGORIES;
this.emoji = {};
this.names = {};
this.chars = new Map;
}
onEnable() {
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);
});
this.loadEmojiData();
}
async loadEmojiData(tries = 0) {
let data;
try {
data = await fetch(`${SERVER}/script/emoji/v3.2.json?_${getBuster(60)}`).then(r =>
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]) {
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;
}
const vari = Object.assign(hydrate_emoji(r), {
key: tone
});
vars[tone] = vari;
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');
if ( ! has(IMAGE_PATHS, style) )
style = 'twitter';
return `${SERVER}/static/emoji/images/${IMAGE_PATHS[style]}/${image}`;
/*if ( ! has(SIZES, style) )
style = 'twitter';
return `${SERVER}/static/emoji/img-${style}-${SIZES[style][0]}/${image}`;*/
}
getFullImageSet(image, style) {
if ( ! style )
style = this.parent.context.get('chat.emoji.style');
if ( ! has(IMAGE_PATHS, style) )
style = 'twitter';
return `${SERVER}/static/emoji/images/${IMAGE_PATHS[style]}/${image} 72w`;
/*if ( ! has(SIZES, style) )
style = 'twitter';
return SIZES[style].map(w =>
`${SERVER}/static/emoji/img-${style}-${w}/${image} ${w}w`
).join(', ');*/
}
}
function hydrate_emoji(data) {
let code = data[0];
if ( data[4] === 0 )
code = `${code}-fe0f`;
return {
code,
image: `${data[0]}.png`,
raw: code.split('-').map(codepoint_to_emoji).join(''),
sheet_x: data[1][0],
sheet_y: data[1][1],
has: {
google: !!(0b1000 & data[2]),
blob: !!(0b0100 & data[2]) || !!(0b1000 & data[2]), // Blob falls back to Noto
twitter: !!(0b0010 & data[2]),
open: !!(0b0001 & data[2])
}
};
}