mirror of
https://github.com/FrankerFaceZ/FrankerFaceZ.git
synced 2025-08-03 00:18:31 +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">
|
||||
<li>Fixed: Issue sorting settings on Edge and Safari.</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"
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-2.1.0.tgz",
|
||||
|
|
|
@ -51,6 +51,7 @@
|
|||
"dependencies": {
|
||||
"crypto-js": "^3.1.9-1",
|
||||
"displacejs": "^1.2.4",
|
||||
"emoji-regex": "^6.5.1",
|
||||
"graphql": "^0.13.2",
|
||||
"graphql-tag": "^2.8.0",
|
||||
"js-cookie": "^2.2.0",
|
||||
|
|
|
@ -100,7 +100,7 @@ class FrankerFaceZ extends Module {
|
|||
FrankerFaceZ.Logger = Logger;
|
||||
|
||||
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__,
|
||||
toString: () =>
|
||||
`${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';
|
||||
id = emote.id;
|
||||
|
||||
} else if ( provider === 'emoji' ) {
|
||||
source = 'emoji';
|
||||
id = ds.code;
|
||||
|
||||
} else
|
||||
return;
|
||||
|
||||
|
@ -467,6 +471,8 @@ export default class Emotes extends Module {
|
|||
|
||||
if ( data.users )
|
||||
this.loadSetUsers(data.users);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ import {timeout, has} from 'utilities/object';
|
|||
|
||||
import Badges from './badges';
|
||||
import Emotes from './emotes';
|
||||
//import Emoji from './emoji';
|
||||
import Emoji from './emoji';
|
||||
|
||||
import Room from './room';
|
||||
import User from './user';
|
||||
|
@ -32,7 +32,7 @@ export default class Chat extends Module {
|
|||
|
||||
this.inject(Badges);
|
||||
this.inject(Emotes);
|
||||
//this.inject(Emoji);
|
||||
this.inject(Emoji);
|
||||
|
||||
this._link_info = {};
|
||||
|
||||
|
|
|
@ -437,7 +437,7 @@ export const AddonEmotes = {
|
|||
render(token, createElement) {
|
||||
const mods = token.modifiers || [], ml = mods.length,
|
||||
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}
|
||||
srcSet={token.srcSet}
|
||||
alt={token.text}
|
||||
|
@ -445,6 +445,8 @@ export const AddonEmotes = {
|
|||
data-provider={token.provider}
|
||||
data-id={token.id}
|
||||
data-set={token.set}
|
||||
data-code={token.code}
|
||||
data-variant={token.variant}
|
||||
data-modifiers={ml ? mods.map(x => x.id).join(' ') : null}
|
||||
data-modifier-info={ml ? JSON.stringify(mods.map(x => [x.set, x.id])) : null}
|
||||
onClick={this.emotes.handleClick}
|
||||
|
@ -461,7 +463,7 @@ export const AddonEmotes = {
|
|||
onClick={this.emotes.handleClick}
|
||||
>
|
||||
{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>);
|
||||
},
|
||||
|
||||
|
@ -470,7 +472,9 @@ export const AddonEmotes = {
|
|||
provider = ds.provider,
|
||||
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' ) {
|
||||
mods = JSON.parse(modifiers).map(([set_id, emote_id]) => {
|
||||
|
@ -479,7 +483,7 @@ export const AddonEmotes = {
|
|||
|
||||
if ( emote )
|
||||
return (<span>
|
||||
{this.tokenizers.emote.render(emote.token, createElement)}
|
||||
{this.tokenizers.emote.render.call(this, emote.token, createElement)}
|
||||
{` - ${emote.hidden ? '???' : emote.name}`}
|
||||
</span>);
|
||||
})
|
||||
|
@ -532,21 +536,42 @@ export const AddonEmotes = {
|
|||
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
|
||||
return;
|
||||
|
||||
const name = ds.name || target.alt,
|
||||
favorite = fav_source && this.emotes.isFavorite(fav_source, emote_id),
|
||||
hide_source = ds.noSource === 'true';
|
||||
if ( ! name )
|
||||
name = ds.name || target.alt;
|
||||
|
||||
const favorite = fav_source && this.emotes.isFavorite(fav_source, emote_id);
|
||||
|
||||
return [
|
||||
preview && this.context.get('tooltip.emote-images') && (<img
|
||||
preview && this.context.get('tooltip.emote-images') && (typeof preview === 'string' ? (<img
|
||||
class="preview-image"
|
||||
src={preview}
|
||||
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">
|
||||
{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
|
||||
// ============================================================================
|
||||
|
|
|
@ -77,6 +77,7 @@ export default class EmoteMenu extends Module {
|
|||
this.inject('chat');
|
||||
this.inject('chat.badges');
|
||||
this.inject('chat.emotes');
|
||||
this.inject('chat.emoji');
|
||||
|
||||
this.inject('site');
|
||||
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-user-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.EmoteMenu.forceUpdate());
|
||||
|
@ -264,6 +266,14 @@ export default class EmoteMenu extends Module {
|
|||
this.EmoteMenu.forceUpdate();
|
||||
}
|
||||
|
||||
updateFavorite() {
|
||||
this.maybeUpdate();
|
||||
}
|
||||
|
||||
updateEmoji() {
|
||||
this.maybeUpdate();
|
||||
}
|
||||
|
||||
|
||||
defineClasses() {
|
||||
const t = this,
|
||||
|
@ -300,6 +310,8 @@ export default class EmoteMenu extends Module {
|
|||
data-provider={data.provider}
|
||||
data-id={data.id}
|
||||
data-set={data.set_id}
|
||||
data-code={data.code}
|
||||
data-variant={data.variant}
|
||||
data-no-source={this.props.source}
|
||||
data-name={data.name}
|
||||
aria-label={data.name}
|
||||
|
@ -309,7 +321,7 @@ export default class EmoteMenu extends Module {
|
|||
>
|
||||
<figure class="emote-picker__emote-figure">
|
||||
<img
|
||||
class="emote-picker__emote-image"
|
||||
class={`emote-picker__emote-image${data.emoji ? ' ffz-emoji' : ''}`}
|
||||
src={data.src}
|
||||
srcSet={data.srcSet}
|
||||
alt={data.name}
|
||||
|
@ -331,7 +343,7 @@ export default class EmoteMenu extends Module {
|
|||
super(props);
|
||||
|
||||
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.onMouseEnter = this.onMouseEnter.bind(this);
|
||||
|
@ -344,11 +356,14 @@ export default class EmoteMenu extends Module {
|
|||
|
||||
const collapsed = storage.get('emote-menu.collapsed') || [],
|
||||
key = this.props.data.key,
|
||||
idx = collapsed.indexOf(key),
|
||||
val = ! this.state.collapsed;
|
||||
idx = collapsed.indexOf(key);
|
||||
|
||||
let val = ! this.state.collapsed;
|
||||
this.setState({collapsed: val});
|
||||
|
||||
if ( this.props.data.collapsed )
|
||||
val = ! val;
|
||||
|
||||
if ( val && idx === -1 )
|
||||
collapsed.push(key);
|
||||
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_all_sets = this.filterSets(input, state.all_sets);
|
||||
state.filtered_fav_sets = this.filterSets(input, state.fav_sets);
|
||||
state.filtered_emoji_sets = this.filterSets(input, state.emoji_sets);
|
||||
|
||||
return state;
|
||||
}
|
||||
|
@ -633,7 +649,8 @@ export default class EmoteMenu extends Module {
|
|||
if ( ! filter || ! filter.length )
|
||||
return true;
|
||||
|
||||
const emote_lower = emote.name.toLowerCase(),
|
||||
const emote_name = emote.search || emote.name,
|
||||
emote_lower = emote_name.toLowerCase(),
|
||||
term_lower = filter.toLowerCase();
|
||||
|
||||
if ( ! filter.startsWith(':') )
|
||||
|
@ -642,12 +659,74 @@ export default class EmoteMenu extends Module {
|
|||
if ( emote_lower.startsWith(term_lower.slice(1)) )
|
||||
return true;
|
||||
|
||||
const idx = emote.name.indexOf(filter.charAt(1).toUpperCase());
|
||||
const idx = emote_name.indexOf(filter.charAt(1).toUpperCase());
|
||||
if ( idx !== -1 )
|
||||
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) {
|
||||
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.
|
||||
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');
|
||||
|
||||
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';
|
||||
|
||||
switch(tab) {
|
||||
|
@ -1096,6 +1175,9 @@ export default class EmoteMenu extends Module {
|
|||
case 'channel':
|
||||
sets = this.state.filtered_channel_sets;
|
||||
break;
|
||||
case 'emoji':
|
||||
sets = this.state.filtered_emoji_sets;
|
||||
break;
|
||||
case 'all':
|
||||
default:
|
||||
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')}
|
||||
</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" />
|
||||
{!loading && (<div
|
||||
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() {
|
||||
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.badges.style', 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.emotes');
|
||||
this.inject('chat.emoji');
|
||||
this.inject('i18n');
|
||||
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
|
||||
|
||||
|
@ -103,12 +113,59 @@ export default class TabCompletion extends Module {
|
|||
}
|
||||
|
||||
inst.getMatchedEmotes = function(input) {
|
||||
const results = old_get_matched.call(this, input);
|
||||
if ( ! t.chat.context.get('chat.tab-complete.ffz-emotes') )
|
||||
let results = old_get_matched.call(this, input);
|
||||
|
||||
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.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 {
|
||||
section:not(.filtered) heading {
|
||||
|
@ -90,6 +106,10 @@
|
|||
pointer-events: none;
|
||||
}
|
||||
|
||||
.emote-picker__emote-image {
|
||||
max-height: 3.2rem
|
||||
}
|
||||
|
||||
section:last-of-type {
|
||||
& > div:last-child,
|
||||
& > heading:last-child {
|
||||
|
|
|
@ -13,6 +13,12 @@ const BAD_ERRORS = [
|
|||
'timeout',
|
||||
'unable to load',
|
||||
'error internal',
|
||||
'context deadline exceeded',
|
||||
'500',
|
||||
'501',
|
||||
'502',
|
||||
'503',
|
||||
'504',
|
||||
'Internal Server Error',
|
||||
'http://',
|
||||
'https://'
|
||||
|
@ -41,7 +47,6 @@ export default class Apollo extends Module {
|
|||
|
||||
this.inject('..web_munch');
|
||||
this.inject('..fine');
|
||||
//this.inject('core');
|
||||
}
|
||||
|
||||
onEnable() {
|
||||
|
|
|
@ -73,7 +73,7 @@ export class Tooltip {
|
|||
} else if ( this.live ) {
|
||||
this._onMouseOver = e => {
|
||||
const target = e.target;
|
||||
if ( target.classList.contains(this.cls) )
|
||||
if ( target && target.classList.contains(this.cls) )
|
||||
this._enter(target);
|
||||
};
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue