mirror of
https://github.com/FrankerFaceZ/FrankerFaceZ.git
synced 2025-08-12 09:00:54 +00:00
Add Emoji Rendering. Add basic emoji tab-completion. The emote menu needs a re-think for performance. Strip out more Apollo bugs. Fix tooltips being silly.
This commit is contained in:
parent
1b2ff27530
commit
9c95335743
14 changed files with 504 additions and 28 deletions
|
@ -1,4 +1,16 @@
|
||||||
<div class="list-header">4.0.0-beta2.13<span>@64fec6b80d1f6a60c263</span> <time datetime="2018-04-12">(2018-04-12)</time></div>
|
<div class="list-header">4.0.0-beta2.15<span>@61e6d676fdac89cf0592</span> <time datetime="2018-04-12">(2018-04-12)</time></div>
|
||||||
|
<ul class="chat-menu-content menu-side-padding">
|
||||||
|
<li>Added: Emoji Rendering.</li>
|
||||||
|
<li>Added: Basic emoji tab-completion.</li>
|
||||||
|
<li> </li>
|
||||||
|
<li>Emoji aren't in the emote menu yet due to performance concerns. They'll get there. We just have to refactor the menu a bit.</li>
|
||||||
|
<li>Tab-completion also isn't great for emoji. We need to change how input handling works overall, but it isn't the priority yet.</li>
|
||||||
|
<li> </li>
|
||||||
|
<li>Fixed: Sometimes a tooltip hover method is called with no target.</li>
|
||||||
|
<li>Fixed: More junk logging from Apollo is stripped out.</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="list-header">4.0.0-beta2.14<span>@d66f702097d2c0295697</span> <time datetime="2018-04-12">(2018-04-12)</time></div>
|
||||||
<ul class="chat-menu-content menu-side-padding">
|
<ul class="chat-menu-content menu-side-padding">
|
||||||
<li>Fixed: Issue sorting settings on Edge and Safari.</li>
|
<li>Fixed: Issue sorting settings on Edge and Safari.</li>
|
||||||
<li>Fixed: Issue processing metadata on game pages when broadcasters aren't defined.</li>
|
<li>Fixed: Issue processing metadata on game pages when broadcasters aren't defined.</li>
|
||||||
|
|
5
package-lock.json
generated
5
package-lock.json
generated
|
@ -2782,6 +2782,11 @@
|
||||||
"minimalistic-crypto-utils": "1.0.1"
|
"minimalistic-crypto-utils": "1.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"emoji-regex": {
|
||||||
|
"version": "6.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-6.5.1.tgz",
|
||||||
|
"integrity": "sha512-PAHp6TxrCy7MGMFidro8uikr+zlJJKJ/Q6mm2ExZ7HwkyR9lSVFfE3kt36qcwa24BQL7y0G9axycGjK1A/0uNQ=="
|
||||||
|
},
|
||||||
"emojis-list": {
|
"emojis-list": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-2.1.0.tgz",
|
||||||
|
|
|
@ -51,6 +51,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"crypto-js": "^3.1.9-1",
|
"crypto-js": "^3.1.9-1",
|
||||||
"displacejs": "^1.2.4",
|
"displacejs": "^1.2.4",
|
||||||
|
"emoji-regex": "^6.5.1",
|
||||||
"graphql": "^0.13.2",
|
"graphql": "^0.13.2",
|
||||||
"graphql-tag": "^2.8.0",
|
"graphql-tag": "^2.8.0",
|
||||||
"js-cookie": "^2.2.0",
|
"js-cookie": "^2.2.0",
|
||||||
|
|
|
@ -100,7 +100,7 @@ class FrankerFaceZ extends Module {
|
||||||
FrankerFaceZ.Logger = Logger;
|
FrankerFaceZ.Logger = Logger;
|
||||||
|
|
||||||
const VER = FrankerFaceZ.version_info = {
|
const VER = FrankerFaceZ.version_info = {
|
||||||
major: 4, minor: 0, revision: 0, extra: '-beta2.14',
|
major: 4, minor: 0, revision: 0, extra: '-beta2.15',
|
||||||
build: __webpack_hash__,
|
build: __webpack_hash__,
|
||||||
toString: () =>
|
toString: () =>
|
||||||
`${VER.major}.${VER.minor}.${VER.revision}${VER.extra || ''}${DEBUG ? '-dev' : ''}`
|
`${VER.major}.${VER.minor}.${VER.revision}${VER.extra || ''}${DEBUG ? '-dev' : ''}`
|
||||||
|
|
180
src/modules/chat/emoji.js
Normal file
180
src/modules/chat/emoji.js
Normal file
|
@ -0,0 +1,180 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Emoji Handling
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
import Module from 'utilities/module';
|
||||||
|
import {SERVER} from 'utilities/constants';
|
||||||
|
|
||||||
|
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 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');
|
||||||
|
|
||||||
|
this.settings.add('chat.emoji.style', {
|
||||||
|
default: 'twitter',
|
||||||
|
ui: {
|
||||||
|
path: 'Chat > Appearance >> Emoji',
|
||||||
|
title: 'Emoji Style',
|
||||||
|
component: 'setting-select-box',
|
||||||
|
data: [
|
||||||
|
{value: 0, title: 'Native'},
|
||||||
|
{value: 'twitter', title: 'Twitter'},
|
||||||
|
{value: 'google', title: 'Google'},
|
||||||
|
//{value: 'apple', title: 'Apple'},
|
||||||
|
{value: 'emojione', title: 'EmojiOne'},
|
||||||
|
//{value: 'facebook', title: 'Facebook'},
|
||||||
|
//{value: 'messenger', title: 'Messenger'}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// For some reason, splitter is a function.
|
||||||
|
this.splitter = splitter();
|
||||||
|
|
||||||
|
this.emoji = {};
|
||||||
|
this.names = {};
|
||||||
|
this.chars = new Map;
|
||||||
|
}
|
||||||
|
|
||||||
|
onEnable() {
|
||||||
|
this.loadEmojiData();
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadEmojiData(tries = 0) {
|
||||||
|
let data;
|
||||||
|
try {
|
||||||
|
data = await fetch(`${SERVER}/script/emoji/v2-.json?_${Date.now()}`).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]) {
|
||||||
|
const vari = Object.assign(hydrate_emoji(r), {
|
||||||
|
key: r[3].toLowerCase()
|
||||||
|
});
|
||||||
|
|
||||||
|
vars[vari.key] = 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 ( style === 0 )
|
||||||
|
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 ( style === 0 )
|
||||||
|
style = 'twitter';
|
||||||
|
|
||||||
|
return SIZES[style].map(w =>
|
||||||
|
`${SERVER}/static/emoji/img-${style}-${w}/${image} ${w}w`
|
||||||
|
).join(', ');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function hydrate_emoji(data) {
|
||||||
|
return {
|
||||||
|
code: data[0],
|
||||||
|
image: `${data[0]}.png`,
|
||||||
|
raw: data[0].split('-').map(codepoint_to_emoji).join(''),
|
||||||
|
sheet_x: data[1][0],
|
||||||
|
sheet_y: data[1][1],
|
||||||
|
|
||||||
|
has: {
|
||||||
|
apple: !!(0b100000 & data[2]),
|
||||||
|
google: !!(0b010000 & data[2]),
|
||||||
|
twitter: !!(0b001000 & data[2]),
|
||||||
|
emojione: !!(0b000100 & data[2]),
|
||||||
|
facebook: !!(0b000010 & data[2]),
|
||||||
|
messenger: !!(0b000001 & data[2])
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
|
@ -247,6 +247,10 @@ export default class Emotes extends Module {
|
||||||
source = emote_set.source || 'ffz';
|
source = emote_set.source || 'ffz';
|
||||||
id = emote.id;
|
id = emote.id;
|
||||||
|
|
||||||
|
} else if ( provider === 'emoji' ) {
|
||||||
|
source = 'emoji';
|
||||||
|
id = ds.code;
|
||||||
|
|
||||||
} else
|
} else
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
@ -467,6 +471,8 @@ export default class Emotes extends Module {
|
||||||
|
|
||||||
if ( data.users )
|
if ( data.users )
|
||||||
this.loadSetUsers(data.users);
|
this.loadSetUsers(data.users);
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,7 @@ import {timeout, has} from 'utilities/object';
|
||||||
|
|
||||||
import Badges from './badges';
|
import Badges from './badges';
|
||||||
import Emotes from './emotes';
|
import Emotes from './emotes';
|
||||||
//import Emoji from './emoji';
|
import Emoji from './emoji';
|
||||||
|
|
||||||
import Room from './room';
|
import Room from './room';
|
||||||
import User from './user';
|
import User from './user';
|
||||||
|
@ -32,7 +32,7 @@ export default class Chat extends Module {
|
||||||
|
|
||||||
this.inject(Badges);
|
this.inject(Badges);
|
||||||
this.inject(Emotes);
|
this.inject(Emotes);
|
||||||
//this.inject(Emoji);
|
this.inject(Emoji);
|
||||||
|
|
||||||
this._link_info = {};
|
this._link_info = {};
|
||||||
|
|
||||||
|
|
|
@ -437,7 +437,7 @@ export const AddonEmotes = {
|
||||||
render(token, createElement) {
|
render(token, createElement) {
|
||||||
const mods = token.modifiers || [], ml = mods.length,
|
const mods = token.modifiers || [], ml = mods.length,
|
||||||
emote = (<img
|
emote = (<img
|
||||||
class={`${EMOTE_CLASS} ffz-tooltip${token.provider === 'ffz' ? ' ffz-emote' : ''}`}
|
class={`${EMOTE_CLASS} ffz-tooltip${token.provider === 'ffz' ? ' ffz-emote' : token.provider === 'emoji' ? ' ffz-emoji' : ''}`}
|
||||||
src={token.src}
|
src={token.src}
|
||||||
srcSet={token.srcSet}
|
srcSet={token.srcSet}
|
||||||
alt={token.text}
|
alt={token.text}
|
||||||
|
@ -445,6 +445,8 @@ export const AddonEmotes = {
|
||||||
data-provider={token.provider}
|
data-provider={token.provider}
|
||||||
data-id={token.id}
|
data-id={token.id}
|
||||||
data-set={token.set}
|
data-set={token.set}
|
||||||
|
data-code={token.code}
|
||||||
|
data-variant={token.variant}
|
||||||
data-modifiers={ml ? mods.map(x => x.id).join(' ') : null}
|
data-modifiers={ml ? mods.map(x => x.id).join(' ') : null}
|
||||||
data-modifier-info={ml ? JSON.stringify(mods.map(x => [x.set, x.id])) : null}
|
data-modifier-info={ml ? JSON.stringify(mods.map(x => [x.set, x.id])) : null}
|
||||||
onClick={this.emotes.handleClick}
|
onClick={this.emotes.handleClick}
|
||||||
|
@ -461,7 +463,7 @@ export const AddonEmotes = {
|
||||||
onClick={this.emotes.handleClick}
|
onClick={this.emotes.handleClick}
|
||||||
>
|
>
|
||||||
{emote}
|
{emote}
|
||||||
{mods.map(t => <span key={t.text}>{this.tokenizers.emote.render(t, createElement)}</span>)}
|
{mods.map(t => <span key={t.text}>{this.tokenizers.emote.render.call(this, t, createElement)}</span>)}
|
||||||
</span>);
|
</span>);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -470,7 +472,9 @@ export const AddonEmotes = {
|
||||||
provider = ds.provider,
|
provider = ds.provider,
|
||||||
modifiers = ds.modifierInfo;
|
modifiers = ds.modifierInfo;
|
||||||
|
|
||||||
let preview, source, owner, mods, fav_source, emote_id;
|
let name, preview, source, owner, mods, fav_source, emote_id,
|
||||||
|
plain_name = false,
|
||||||
|
hide_source = ds.noSource === 'true';
|
||||||
|
|
||||||
if ( modifiers && modifiers !== 'null' ) {
|
if ( modifiers && modifiers !== 'null' ) {
|
||||||
mods = JSON.parse(modifiers).map(([set_id, emote_id]) => {
|
mods = JSON.parse(modifiers).map(([set_id, emote_id]) => {
|
||||||
|
@ -479,7 +483,7 @@ export const AddonEmotes = {
|
||||||
|
|
||||||
if ( emote )
|
if ( emote )
|
||||||
return (<span>
|
return (<span>
|
||||||
{this.tokenizers.emote.render(emote.token, createElement)}
|
{this.tokenizers.emote.render.call(this, emote.token, createElement)}
|
||||||
{` - ${emote.hidden ? '???' : emote.name}`}
|
{` - ${emote.hidden ? '???' : emote.name}`}
|
||||||
</span>);
|
</span>);
|
||||||
})
|
})
|
||||||
|
@ -532,21 +536,42 @@ export const AddonEmotes = {
|
||||||
preview = emote.urls[2];
|
preview = emote.urls[2];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
} else if ( provider === 'emoji' ) {
|
||||||
|
const emoji = this.emoji.emoji[ds.code],
|
||||||
|
style = this.context.get('chat.emoji.style'),
|
||||||
|
variant = ds.variant ? emoji.variants[ds.variant] : emoji,
|
||||||
|
vcode = ds.variant ? this.emoji.emoji[ds.variant] : null;
|
||||||
|
|
||||||
|
fav_source = 'emoji';
|
||||||
|
emote_id = ds.code;
|
||||||
|
|
||||||
|
preview = (<img
|
||||||
|
class="preview-image ffz-emoji"
|
||||||
|
src={this.emoji.getFullImage(variant.image, style)}
|
||||||
|
srcSet={this.emoji.getFullImageSet(variant.image, style)}
|
||||||
|
onLoad={tip.update}
|
||||||
|
/>);
|
||||||
|
|
||||||
|
plain_name = true;
|
||||||
|
name = `:${emoji.names[0]}:${vcode ? `:${vcode.names[0]}:` : ''}`;
|
||||||
|
source = this.i18n.t('tooltip.emoji', 'Emoji - %{category}', emoji);
|
||||||
|
|
||||||
} else
|
} else
|
||||||
return;
|
return;
|
||||||
|
|
||||||
const name = ds.name || target.alt,
|
if ( ! name )
|
||||||
favorite = fav_source && this.emotes.isFavorite(fav_source, emote_id),
|
name = ds.name || target.alt;
|
||||||
hide_source = ds.noSource === 'true';
|
|
||||||
|
const favorite = fav_source && this.emotes.isFavorite(fav_source, emote_id);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
preview && this.context.get('tooltip.emote-images') && (<img
|
preview && this.context.get('tooltip.emote-images') && (typeof preview === 'string' ? (<img
|
||||||
class="preview-image"
|
class="preview-image"
|
||||||
src={preview}
|
src={preview}
|
||||||
onLoad={tip.update}
|
onLoad={tip.update}
|
||||||
/>),
|
/>) : preview),
|
||||||
|
|
||||||
(hide_source && ! owner) ? name : this.i18n.t('tooltip.emote', 'Emote: %{code}', {code: ds.name || target.alt}),
|
plain_name || (hide_source && ! owner) ? name : this.i18n.t('tooltip.emote', 'Emote: %{name}', {name}),
|
||||||
|
|
||||||
! hide_source && source && this.context.get('tooltip.emote-sources') && (<div class="tw-pd-t-05">
|
! hide_source && source && this.context.get('tooltip.emote-sources') && (<div class="tw-pd-t-05">
|
||||||
{source}
|
{source}
|
||||||
|
@ -636,6 +661,80 @@ export const AddonEmotes = {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Emoji
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export const Emoji = {
|
||||||
|
type: 'emoji',
|
||||||
|
priority: 15,
|
||||||
|
|
||||||
|
process(tokens) {
|
||||||
|
if ( ! tokens || ! tokens.length )
|
||||||
|
return tokens;
|
||||||
|
|
||||||
|
const splitter = this.emoji.splitter,
|
||||||
|
style = this.context.get('chat.emoji.style'),
|
||||||
|
out = [];
|
||||||
|
|
||||||
|
if ( style === 0 )
|
||||||
|
return tokens;
|
||||||
|
|
||||||
|
for(const token of tokens) {
|
||||||
|
if ( ! token )
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if ( token.type !== 'text' ) {
|
||||||
|
out.push(token);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = token.text;
|
||||||
|
|
||||||
|
splitter.lastIndex = 0;
|
||||||
|
let idx = 0, match;
|
||||||
|
|
||||||
|
while((match = splitter.exec(text))) {
|
||||||
|
const start = match.index,
|
||||||
|
key = this.emoji.chars.get(match[0]);
|
||||||
|
|
||||||
|
if ( ! key )
|
||||||
|
continue;
|
||||||
|
|
||||||
|
const emoji = this.emoji.emoji[key[0]],
|
||||||
|
variant = key[1] ? emoji.variants[key[1]] : emoji,
|
||||||
|
length = split_chars(match[0]).length;
|
||||||
|
|
||||||
|
if ( idx !== start )
|
||||||
|
out.push({type: 'text', text: text.slice(idx, start)});
|
||||||
|
|
||||||
|
out.push({
|
||||||
|
type: 'emote',
|
||||||
|
|
||||||
|
provider: 'emoji',
|
||||||
|
code: key[0],
|
||||||
|
variant: key[1],
|
||||||
|
|
||||||
|
src: this.emoji.getFullImage(variant.image, style),
|
||||||
|
srcSet: this.emoji.getFullImageSet(variant.image, style),
|
||||||
|
|
||||||
|
text: match[0],
|
||||||
|
length,
|
||||||
|
modifiers: []
|
||||||
|
});
|
||||||
|
|
||||||
|
idx = start + match[0].length;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( idx < text.length )
|
||||||
|
out.push({type: 'text', text: text.slice(idx)});
|
||||||
|
}
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Twitch Emotes
|
// Twitch Emotes
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
|
@ -77,6 +77,7 @@ export default class EmoteMenu extends Module {
|
||||||
this.inject('chat');
|
this.inject('chat');
|
||||||
this.inject('chat.badges');
|
this.inject('chat.badges');
|
||||||
this.inject('chat.emotes');
|
this.inject('chat.emotes');
|
||||||
|
this.inject('chat.emoji');
|
||||||
|
|
||||||
this.inject('site');
|
this.inject('site');
|
||||||
this.inject('site.fine');
|
this.inject('site.fine');
|
||||||
|
@ -201,7 +202,8 @@ export default class EmoteMenu extends Module {
|
||||||
this.on('chat.emotes:update-default-sets', this.maybeUpdate, this);
|
this.on('chat.emotes:update-default-sets', this.maybeUpdate, this);
|
||||||
this.on('chat.emotes:update-user-sets', this.maybeUpdate, this);
|
this.on('chat.emotes:update-user-sets', this.maybeUpdate, this);
|
||||||
this.on('chat.emotes:update-room-sets', this.maybeUpdate, this);
|
this.on('chat.emotes:update-room-sets', this.maybeUpdate, this);
|
||||||
this.on('chat.emotes:change-favorite', this.maybeUpdate, this);
|
this.on('chat.emotes:change-favorite', this.updateFavorite, this);
|
||||||
|
this.on('chat.emoji:populated', this.updateEmoji, this);
|
||||||
|
|
||||||
this.chat.context.on('changed:chat.emote-menu.enabled', () =>
|
this.chat.context.on('changed:chat.emote-menu.enabled', () =>
|
||||||
this.EmoteMenu.forceUpdate());
|
this.EmoteMenu.forceUpdate());
|
||||||
|
@ -264,6 +266,14 @@ export default class EmoteMenu extends Module {
|
||||||
this.EmoteMenu.forceUpdate();
|
this.EmoteMenu.forceUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateFavorite() {
|
||||||
|
this.maybeUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateEmoji() {
|
||||||
|
this.maybeUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
defineClasses() {
|
defineClasses() {
|
||||||
const t = this,
|
const t = this,
|
||||||
|
@ -300,6 +310,8 @@ export default class EmoteMenu extends Module {
|
||||||
data-provider={data.provider}
|
data-provider={data.provider}
|
||||||
data-id={data.id}
|
data-id={data.id}
|
||||||
data-set={data.set_id}
|
data-set={data.set_id}
|
||||||
|
data-code={data.code}
|
||||||
|
data-variant={data.variant}
|
||||||
data-no-source={this.props.source}
|
data-no-source={this.props.source}
|
||||||
data-name={data.name}
|
data-name={data.name}
|
||||||
aria-label={data.name}
|
aria-label={data.name}
|
||||||
|
@ -309,7 +321,7 @@ export default class EmoteMenu extends Module {
|
||||||
>
|
>
|
||||||
<figure class="emote-picker__emote-figure">
|
<figure class="emote-picker__emote-figure">
|
||||||
<img
|
<img
|
||||||
class="emote-picker__emote-image"
|
class={`emote-picker__emote-image${data.emoji ? ' ffz-emoji' : ''}`}
|
||||||
src={data.src}
|
src={data.src}
|
||||||
srcSet={data.srcSet}
|
srcSet={data.srcSet}
|
||||||
alt={data.name}
|
alt={data.name}
|
||||||
|
@ -331,7 +343,7 @@ export default class EmoteMenu extends Module {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
const collapsed = storage.get('emote-menu.collapsed') || [];
|
const collapsed = storage.get('emote-menu.collapsed') || [];
|
||||||
this.state = {collapsed: props.data && collapsed.includes(props.data.key)}
|
this.state = {collapsed: props.data && (collapsed.includes(props.data.key) !== props.data.collapsed)}
|
||||||
|
|
||||||
this.clickHeading = this.clickHeading.bind(this);
|
this.clickHeading = this.clickHeading.bind(this);
|
||||||
this.onMouseEnter = this.onMouseEnter.bind(this);
|
this.onMouseEnter = this.onMouseEnter.bind(this);
|
||||||
|
@ -344,11 +356,14 @@ export default class EmoteMenu extends Module {
|
||||||
|
|
||||||
const collapsed = storage.get('emote-menu.collapsed') || [],
|
const collapsed = storage.get('emote-menu.collapsed') || [],
|
||||||
key = this.props.data.key,
|
key = this.props.data.key,
|
||||||
idx = collapsed.indexOf(key),
|
idx = collapsed.indexOf(key);
|
||||||
val = ! this.state.collapsed;
|
|
||||||
|
|
||||||
|
let val = ! this.state.collapsed;
|
||||||
this.setState({collapsed: val});
|
this.setState({collapsed: val});
|
||||||
|
|
||||||
|
if ( this.props.data.collapsed )
|
||||||
|
val = ! val;
|
||||||
|
|
||||||
if ( val && idx === -1 )
|
if ( val && idx === -1 )
|
||||||
collapsed.push(key);
|
collapsed.push(key);
|
||||||
else if ( ! val && idx !== -1 )
|
else if ( ! val && idx !== -1 )
|
||||||
|
@ -607,6 +622,7 @@ export default class EmoteMenu extends Module {
|
||||||
state.filtered_channel_sets = this.filterSets(input, state.channel_sets);
|
state.filtered_channel_sets = this.filterSets(input, state.channel_sets);
|
||||||
state.filtered_all_sets = this.filterSets(input, state.all_sets);
|
state.filtered_all_sets = this.filterSets(input, state.all_sets);
|
||||||
state.filtered_fav_sets = this.filterSets(input, state.fav_sets);
|
state.filtered_fav_sets = this.filterSets(input, state.fav_sets);
|
||||||
|
state.filtered_emoji_sets = this.filterSets(input, state.emoji_sets);
|
||||||
|
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
@ -633,7 +649,8 @@ export default class EmoteMenu extends Module {
|
||||||
if ( ! filter || ! filter.length )
|
if ( ! filter || ! filter.length )
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
const emote_lower = emote.name.toLowerCase(),
|
const emote_name = emote.search || emote.name,
|
||||||
|
emote_lower = emote_name.toLowerCase(),
|
||||||
term_lower = filter.toLowerCase();
|
term_lower = filter.toLowerCase();
|
||||||
|
|
||||||
if ( ! filter.startsWith(':') )
|
if ( ! filter.startsWith(':') )
|
||||||
|
@ -642,12 +659,74 @@ export default class EmoteMenu extends Module {
|
||||||
if ( emote_lower.startsWith(term_lower.slice(1)) )
|
if ( emote_lower.startsWith(term_lower.slice(1)) )
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
const idx = emote.name.indexOf(filter.charAt(1).toUpperCase());
|
const idx = emote_name.indexOf(filter.charAt(1).toUpperCase());
|
||||||
if ( idx !== -1 )
|
if ( idx !== -1 )
|
||||||
return emote_lower.slice(idx+1).startsWith(term_lower.slice(2));
|
return emote_lower.slice(idx+1).startsWith(term_lower.slice(2));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
buildEmoji(old_state) { // eslint-disable-line class-methods-use-this
|
||||||
|
return old_state;
|
||||||
|
|
||||||
|
/*const state = Object.assign({}, old_state),
|
||||||
|
|
||||||
|
sets = state.emoji_sets = [],
|
||||||
|
emoji_favorites = t.emotes.getFavorites('emoji'),
|
||||||
|
style = t.chat.context.get('chat.emoji.style') || 'twitter',
|
||||||
|
favorites = state.favorites = state.favorites || [],
|
||||||
|
categories = {};
|
||||||
|
|
||||||
|
for(const emoji of Object.values(t.emoji.emoji)) {
|
||||||
|
if ( ! emoji.has[style] )
|
||||||
|
continue;
|
||||||
|
|
||||||
|
let cat = categories[emoji.category];
|
||||||
|
if ( ! cat ) {
|
||||||
|
cat = categories[emoji.category] = [];
|
||||||
|
|
||||||
|
sets.push({
|
||||||
|
key: `emoji-${emoji.category}`,
|
||||||
|
collapsed: true,
|
||||||
|
image: t.emoji.getFullImage(emoji.image),
|
||||||
|
title: emoji.category,
|
||||||
|
source: t.i18n.t('emote-menu.emoji', 'Emoji'),
|
||||||
|
emotes: cat
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const is_fav = emoji_favorites.includes(emoji.code),
|
||||||
|
em = {
|
||||||
|
provider: 'emoji',
|
||||||
|
emoji: true,
|
||||||
|
code: emoji.code,
|
||||||
|
name: emoji.raw,
|
||||||
|
|
||||||
|
search: emoji.names[0],
|
||||||
|
|
||||||
|
height: 18,
|
||||||
|
width: 18,
|
||||||
|
|
||||||
|
x: emoji.sheet_x,
|
||||||
|
y: emoji.sheet_y,
|
||||||
|
|
||||||
|
favorite: is_fav,
|
||||||
|
|
||||||
|
src: t.emoji.getFullImage(emoji.image),
|
||||||
|
srcSet: t.emoji.getFullImageSet(emoji.image)
|
||||||
|
};
|
||||||
|
|
||||||
|
cat.push(em);
|
||||||
|
|
||||||
|
if ( is_fav )
|
||||||
|
favorites.push(em);
|
||||||
|
}
|
||||||
|
|
||||||
|
state.has_emoji_tab = sets.length > 0;
|
||||||
|
|
||||||
|
return state;*/
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
buildState(props, old_state) {
|
buildState(props, old_state) {
|
||||||
const state = Object.assign({}, old_state),
|
const state = Object.assign({}, old_state),
|
||||||
|
|
||||||
|
@ -961,7 +1040,7 @@ export default class EmoteMenu extends Module {
|
||||||
// We use this sorter because we don't want things grouped by sets.
|
// We use this sorter because we don't want things grouped by sets.
|
||||||
favorites.sort(sorter);
|
favorites.sort(sorter);
|
||||||
|
|
||||||
return state;
|
return this.buildEmoji(state);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -1086,7 +1165,7 @@ export default class EmoteMenu extends Module {
|
||||||
padding = t.chat.context.get('chat.emote-menu.reduced-padding');
|
padding = t.chat.context.get('chat.emote-menu.reduced-padding');
|
||||||
|
|
||||||
let tab = this.state.tab || t.chat.context.get('chat.emote-menu.default-tab'), sets;
|
let tab = this.state.tab || t.chat.context.get('chat.emote-menu.default-tab'), sets;
|
||||||
if ( tab === 'channel' && ! this.state.has_channel_tab )
|
if ( (tab === 'channel' && ! this.state.has_channel_tab) || (tab === 'emoji' && ! this.state.has_emoji_tab) )
|
||||||
tab = 'all';
|
tab = 'all';
|
||||||
|
|
||||||
switch(tab) {
|
switch(tab) {
|
||||||
|
@ -1096,6 +1175,9 @@ export default class EmoteMenu extends Module {
|
||||||
case 'channel':
|
case 'channel':
|
||||||
sets = this.state.filtered_channel_sets;
|
sets = this.state.filtered_channel_sets;
|
||||||
break;
|
break;
|
||||||
|
case 'emoji':
|
||||||
|
sets = this.state.filtered_emoji_sets;
|
||||||
|
break;
|
||||||
case 'all':
|
case 'all':
|
||||||
default:
|
default:
|
||||||
sets = this.state.filtered_all_sets;
|
sets = this.state.filtered_all_sets;
|
||||||
|
@ -1168,6 +1250,14 @@ export default class EmoteMenu extends Module {
|
||||||
>
|
>
|
||||||
{t.i18n.t('emote-menu.my-emotes', 'My Emotes')}
|
{t.i18n.t('emote-menu.my-emotes', 'My Emotes')}
|
||||||
</div>
|
</div>
|
||||||
|
{this.state.has_emoji_tab && <div
|
||||||
|
class={`emote-picker__tab tw-pd-x-1${tab === 'emoji' ? ' emote-picker__tab--active' : ''}`}
|
||||||
|
id="emote-picker__emoji"
|
||||||
|
data-tab="emoji"
|
||||||
|
onClick={this.clickTab}
|
||||||
|
>
|
||||||
|
{t.i18n.t('emote-menu.emoji', 'Emoji')}
|
||||||
|
</div>}
|
||||||
<div class="tw-flex-grow-1" />
|
<div class="tw-flex-grow-1" />
|
||||||
{!loading && (<div
|
{!loading && (<div
|
||||||
class="ffz-tooltip emote-picker__tab tw-pd-x-1 tw-mg-r-0"
|
class="ffz-tooltip emote-picker__tab tw-pd-x-1 tw-mg-r-0"
|
||||||
|
|
|
@ -41,6 +41,7 @@ export default class ChatLine extends Module {
|
||||||
}
|
}
|
||||||
|
|
||||||
onEnable() {
|
onEnable() {
|
||||||
|
this.chat.context.on('changed:chat.emoji.style', this.updateLines, this);
|
||||||
this.chat.context.on('changed:chat.bits.stack', this.updateLines, this);
|
this.chat.context.on('changed:chat.bits.stack', this.updateLines, this);
|
||||||
this.chat.context.on('changed:chat.badges.style', this.updateLines, this);
|
this.chat.context.on('changed:chat.badges.style', this.updateLines, this);
|
||||||
this.chat.context.on('changed:chat.badges.hidden', this.updateLines, this);
|
this.chat.context.on('changed:chat.badges.hidden', this.updateLines, this);
|
||||||
|
|
|
@ -13,6 +13,7 @@ export default class TabCompletion extends Module {
|
||||||
|
|
||||||
this.inject('chat');
|
this.inject('chat');
|
||||||
this.inject('chat.emotes');
|
this.inject('chat.emotes');
|
||||||
|
this.inject('chat.emoji');
|
||||||
this.inject('i18n');
|
this.inject('i18n');
|
||||||
this.inject('settings');
|
this.inject('settings');
|
||||||
|
|
||||||
|
@ -31,6 +32,15 @@ export default class TabCompletion extends Module {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
// Components
|
// Components
|
||||||
|
|
||||||
|
@ -103,12 +113,59 @@ export default class TabCompletion extends Module {
|
||||||
}
|
}
|
||||||
|
|
||||||
inst.getMatchedEmotes = function(input) {
|
inst.getMatchedEmotes = function(input) {
|
||||||
const results = old_get_matched.call(this, input);
|
let results = old_get_matched.call(this, input);
|
||||||
if ( ! t.chat.context.get('chat.tab-complete.ffz-emotes') )
|
|
||||||
|
if ( t.chat.context.get('chat.tab-complete.ffz-emotes') )
|
||||||
|
results = results.concat(t.getEmoteSuggestions(input, this));
|
||||||
|
|
||||||
|
if ( ! t.chat.context.get('chat.tab-complete.emoji') )
|
||||||
return results;
|
return results;
|
||||||
|
|
||||||
return results.concat(t.getEmoteSuggestions(input, this));
|
return results.concat(t.getEmojiSuggestions(input, this));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const React = this.web_munch.getModule('react'),
|
||||||
|
createElement = React && React.createElement;
|
||||||
|
|
||||||
|
inst.renderFFZEmojiSuggestion = function(data) {
|
||||||
|
return [
|
||||||
|
<div class="tw-pd-r-05">
|
||||||
|
<img
|
||||||
|
class="emote-autocomplete-provider__image ffz-emoji"
|
||||||
|
src={data.src}
|
||||||
|
srcSet={data.srcset}
|
||||||
|
/>
|
||||||
|
</div>,
|
||||||
|
<div>
|
||||||
|
{data.token}
|
||||||
|
</div>
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
getEmojiSuggestions(input, inst) {
|
||||||
|
const search = input.slice(1).toLowerCase(),
|
||||||
|
style = this.chat.context.get('chat.emoji.style'),
|
||||||
|
results = [];
|
||||||
|
|
||||||
|
for(const name in this.emoji.names)
|
||||||
|
if ( name.startsWith(search) ) {
|
||||||
|
const emoji = this.emoji.emoji[this.emoji.names[name]];
|
||||||
|
if ( emoji && (style === 0 || emoji.has[style]) )
|
||||||
|
results.push({
|
||||||
|
current: input,
|
||||||
|
replacement: emoji.raw,
|
||||||
|
element: inst.renderFFZEmojiSuggestion({
|
||||||
|
token: `:${name}:`,
|
||||||
|
id: `emoji-${emoji.code}`,
|
||||||
|
src: this.emoji.getFullImage(emoji.image, style),
|
||||||
|
srcSet: this.emoji.getFullImageSet(emoji.image, style)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -40,6 +40,22 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ffz-emoji {
|
||||||
|
width: calc(var(--ffz-chat-font-size) * 1.5);
|
||||||
|
height: calc(var(--ffz-chat-font-size) * 1.5);
|
||||||
|
|
||||||
|
&.preview-image {
|
||||||
|
width: 7.2rem;
|
||||||
|
height: 7.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.emote-autocomplete-provider__image {
|
||||||
|
width: 1.8rem;
|
||||||
|
height: 1.8rem;
|
||||||
|
margin: .5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
.ffz--emote-picker {
|
.ffz--emote-picker {
|
||||||
section:not(.filtered) heading {
|
section:not(.filtered) heading {
|
||||||
|
@ -90,6 +106,10 @@
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.emote-picker__emote-image {
|
||||||
|
max-height: 3.2rem
|
||||||
|
}
|
||||||
|
|
||||||
section:last-of-type {
|
section:last-of-type {
|
||||||
& > div:last-child,
|
& > div:last-child,
|
||||||
& > heading:last-child {
|
& > heading:last-child {
|
||||||
|
|
|
@ -13,6 +13,12 @@ const BAD_ERRORS = [
|
||||||
'timeout',
|
'timeout',
|
||||||
'unable to load',
|
'unable to load',
|
||||||
'error internal',
|
'error internal',
|
||||||
|
'context deadline exceeded',
|
||||||
|
'500',
|
||||||
|
'501',
|
||||||
|
'502',
|
||||||
|
'503',
|
||||||
|
'504',
|
||||||
'Internal Server Error',
|
'Internal Server Error',
|
||||||
'http://',
|
'http://',
|
||||||
'https://'
|
'https://'
|
||||||
|
@ -41,7 +47,6 @@ export default class Apollo extends Module {
|
||||||
|
|
||||||
this.inject('..web_munch');
|
this.inject('..web_munch');
|
||||||
this.inject('..fine');
|
this.inject('..fine');
|
||||||
//this.inject('core');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onEnable() {
|
onEnable() {
|
||||||
|
|
|
@ -73,7 +73,7 @@ export class Tooltip {
|
||||||
} else if ( this.live ) {
|
} else if ( this.live ) {
|
||||||
this._onMouseOver = e => {
|
this._onMouseOver = e => {
|
||||||
const target = e.target;
|
const target = e.target;
|
||||||
if ( target.classList.contains(this.cls) )
|
if ( target && target.classList.contains(this.cls) )
|
||||||
this._enter(target);
|
this._enter(target);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue