diff --git a/package.json b/package.json
index 78a3bd4e..3872b0c8 100755
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "frankerfacez",
"author": "Dan Salvato LLC",
- "version": "4.20.78",
+ "version": "4.20.79",
"description": "FrankerFaceZ is a Twitch enhancement suite.",
"private": true,
"license": "Apache-2.0",
diff --git a/src/modules/chat/emotes.js b/src/modules/chat/emotes.js
index e4e32d18..9ad40159 100644
--- a/src/modules/chat/emotes.js
+++ b/src/modules/chat/emotes.js
@@ -12,6 +12,9 @@ import {NEW_API, API_SERVER, IS_OSX, EmoteTypes, TWITCH_GLOBAL_SETS, TWITCH_POIN
import GET_EMOTE from './emote_info.gql';
import GET_EMOTE_SET from './emote_set_info.gql';
+const HoverRAF = Symbol('FFZ:Hover:RAF');
+const HoverState = Symbol('FFZ:Hover:State');
+
const MOD_KEY = IS_OSX ? 'metaKey' : 'ctrlKey';
const MODIFIERS = {
@@ -133,6 +136,8 @@ export default class Emotes extends Module {
// Because this may be used elsewhere.
this.handleClick = this.handleClick.bind(this);
+ this.animHover = this.animHover.bind(this);
+ this.animLeave = this.animLeave.bind(this);
}
onEnable() {
@@ -249,6 +254,68 @@ export default class Emotes extends Module {
}
+ // ========================================================================
+ // Animation Hover
+ // ========================================================================
+
+ animHover(event) { // eslint-disable-line class-methods-use-this
+ const target = event.currentTarget;
+ if ( target[HoverState] )
+ return;
+
+ if ( target[HoverRAF] )
+ cancelAnimationFrame(target[HoverRAF]);
+
+ target[HoverRAF] = requestAnimationFrame(() => {
+ target[HoverRAF] = null;
+ if ( target[HoverState] )
+ return;
+
+ if ( ! target.matches(':hover') )
+ return;
+
+ target[HoverState] = true;
+ const emotes = target.querySelectorAll('.ffz-hover-emote');
+ for(const em of emotes) {
+ const ds = em.dataset;
+ if ( ds.normalSrc && ds.hoverSrc ) {
+ em.src = ds.hoverSrc;
+ em.srcset = ds.hoverSrcSet;
+ }
+ }
+ });
+ }
+
+
+ animLeave(event) { // eslint-disable-line class-methods-use-this
+ const target = event.currentTarget;
+ if ( ! target[HoverState] )
+ return;
+
+ if ( target[HoverRAF] )
+ cancelAnimationFrame(target[HoverRAF]);
+
+ target[HoverRAF] = requestAnimationFrame(() => {
+ target[HoverRAF] = null;
+ if ( ! target[HoverState] )
+ return;
+
+ if ( target.matches(':hover') )
+ return;
+
+ target[HoverState] = false;
+ const emotes = target.querySelectorAll('.ffz-hover-emote');
+ for(const em of emotes) {
+ const ds = em.dataset;
+ if ( ds.normalSrc ) {
+ em.src = ds.normalSrc;
+ em.srcset = ds.normalSrcSet;
+ }
+ }
+ });
+ }
+
+
// ========================================================================
// Favorite Checking
// ========================================================================
@@ -724,6 +791,7 @@ export default class Emotes extends Module {
}
emote.set_id = set_id;
+ emote.src = emote.urls[1];
emote.srcSet = `${emote.urls[1]} 1x`;
if ( emote.urls[2] )
emote.srcSet += `, ${emote.urls[2]} 2x`;
@@ -738,16 +806,35 @@ export default class Emotes extends Module {
emote.srcSet2 += `, ${emote.urls[4]} 2x`;
}
+ if ( emote.animated?.[1] ) {
+ emote.animSrc = emote.animated[1];
+ emote.animSrcSet = `${emote.animated[1]} 1x`;
+ if ( emote.animated[2] ) {
+ emote.animSrcSet += `, ${emote.animated[2]} 2x`;
+ emote.animSrc2 = emote.animated[2];
+ emote.animSrcSet2 = `${emote.animated[2]} 1x`;
+
+ if ( emote.animated[4] ) {
+ emote.animSrcSet += `, ${emote.animated[4]} 4x`;
+ emote.animSrcSet2 += `, ${emote.animated[4]} 2x`;
+ }
+ }
+ }
+
emote.token = {
type: 'emote',
id: emote.id,
set: set_id,
provider: 'ffz',
- src: emote.urls[1],
+ src: emote.src,
srcSet: emote.srcSet,
can_big: !! emote.urls[2],
src2: emote.src2,
srcSet2: emote.srcSet2,
+ animSrc: emote.animSrc,
+ animSrcSet: emote.animSrcSet,
+ animSrc2: emote.animSrc2,
+ animSrcSet2: emote.animSrcSet2,
text: emote.hidden ? '???' : emote.name,
length: emote.name.length,
height: emote.height
diff --git a/src/modules/chat/index.js b/src/modules/chat/index.js
index fad5f6c0..69794123 100644
--- a/src/modules/chat/index.js
+++ b/src/modules/chat/index.js
@@ -918,8 +918,49 @@ export default class Chat extends Module {
}
});
+ this.settings.add('chat.emotes.animated', {
+ default: null,
+ process(ctx, val) {
+ if ( val == null )
+ val = ctx.get('ffzap.betterttv.gif_emoticons_mode') === 2 ? 1 : 0;
+ return val;
+ },
+ ui: {
+ path: 'Chat > Appearance >> Emotes',
+ title: 'Animated Emotes',
+ 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.',
+ 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'
+ }
+ });
+
this.settings.add('chat.bits.animated', {
- default: true,
+ requires: ['chat.emotes.animated'],
+ default: null,
+ process(ctx, val) {
+ if ( val == null )
+ val = ctx.get('chat.emotes.animated') ? true : false
+ },
ui: {
path: 'Chat > Bits and Cheering >> Appearance',
diff --git a/src/modules/chat/tokenizers.jsx b/src/modules/chat/tokenizers.jsx
index 0f88a165..027968ef 100644
--- a/src/modules/chat/tokenizers.jsx
+++ b/src/modules/chat/tokenizers.jsx
@@ -1091,12 +1091,30 @@ export const CheerEmotes = {
// ============================================================================
const render_emote = (token, createElement, wrapped) => {
+ const hover = token.anim === 2;
+ let src, srcSet, hoverSrc, hoverSrcSet, normalSrc, normalSrcSet;
+
+ if ( token.anim === 1 && token.animSrc ) {
+ src = token.big ? token.animSrc2 : token.animSrc;
+ srcSet = token.big ? token.animSrcSet2 : token.animSrcSet;
+ } else {
+ src = token.big ? token.src2 : token.src;
+ srcSet = token.big ? token.srcSet2 : token.srcSet;
+ }
+
+ if ( hover && token.animSrc ) {
+ normalSrc = src;
+ normalSrcSet = srcSet;
+ hoverSrc = token.big ? token.animSrc2 : token.animSrc;
+ hoverSrcSet = token.big ? token.animSrcSet2 : token.animSrcSet;
+ }
+
const mods = token.modifiers || [], ml = mods.length,
emote = createElement('img', {
- class: `${EMOTE_CLASS} ffz-tooltip${token.provider === 'ffz' ? ' ffz-emote' : token.provider === 'emoji' ? ' ffz-emoji' : ''}`,
+ class: `${EMOTE_CLASS} ffz-tooltip${hoverSrc ? ' ffz-hover-emote' : ''}${token.provider === 'ffz' ? ' ffz-emote' : token.provider === 'emoji' ? ' ffz-emoji' : ''}`,
attrs: {
- src: token.big && token.src2 || token.src,
- srcSet: token.big && token.srcSet2 || token.srcSet,
+ src,
+ srcSet,
alt: token.text,
height: (token.big && ! token.can_big && token.height) ? `${token.height * 2}px` : undefined,
'data-tooltip-type': 'emote',
@@ -1105,6 +1123,10 @@ const render_emote = (token, createElement, wrapped) => {
'data-set': token.set,
'data-code': token.code,
'data-variant': token.variant,
+ 'data-normal-src': normalSrc,
+ 'data-normal-src-set': normalSrcSet,
+ 'data-hover-src': hoverSrc,
+ 'data-hover-src-set': hoverSrcSet,
'data-modifiers': ml ? mods.map(x => x.id).join(' ') : null,
'data-modifier-info': ml ? JSON.stringify(mods.map(x => [x.set, x.id])) : null
}
@@ -1147,11 +1169,29 @@ export const AddonEmotes = {
},
render(token, createElement, wrapped) {
+ const hover = token.anim === 2;
+ let src, srcSet, hoverSrc, hoverSrcSet, normalSrc, normalSrcSet;
+
+ if ( token.anim === 1 && token.animSrc ) {
+ src = token.big ? token.animSrc2 : token.animSrc;
+ srcSet = token.big ? token.animSrcSet2 : token.animSrcSet;
+ } else {
+ src = token.big ? token.src2 : token.src;
+ srcSet = token.big ? token.srcSet2 : token.srcSet;
+ }
+
+ if ( hover && token.animSrc ) {
+ normalSrc = src;
+ normalSrcSet = srcSet;
+ hoverSrc = token.big ? token.animSrc2 : token.animSrc;
+ hoverSrcSet = token.big ? token.animSrcSet2 : token.animSrcSet;
+ }
+
const mods = token.modifiers || [], ml = mods.length,
emote = ( x.id).join(' ') : null}
data-modifier-info={ml ? JSON.stringify(mods.map(x => [x.set, x.id])) : null}
onClick={this.emotes.handleClick}
@@ -1265,10 +1309,19 @@ export const AddonEmotes = {
'emote.owner', 'By: {owner}',
{owner: emote.owner.display_name});
- if ( emote.urls[4] )
- preview = emote.urls[4];
- else if ( emote.urls[2] )
- preview = emote.urls[2];
+ const anim = this.context.get('tooltip.emote-images.animated');
+ if ( anim && emote.animated?.[1] ) {
+ if ( emote.animated[4] )
+ preview = emote.animated[4];
+ else if ( emote.animated[2] )
+ preview = emote.animated[2];
+
+ } else {
+ if ( emote.urls[4] )
+ preview = emote.urls[4];
+ else if ( emote.urls[2] )
+ preview = emote.urls[2];
+ }
}
} else if ( provider === 'emoji' ) {
@@ -1341,6 +1394,7 @@ export const AddonEmotes = {
return tokens;
const big = this.context.get('chat.emotes.2x'),
+ anim = this.context.get('chat.emotes.animated'),
out = [];
let last_token, emote;
@@ -1391,7 +1445,8 @@ export const AddonEmotes = {
const t = Object.assign({
modifiers: [],
- big
+ big,
+ anim
}, emote.token);
out.push(t);
last_token = t;
diff --git a/src/sites/clips/line.jsx b/src/sites/clips/line.jsx
index d9788f1d..c09a27ad 100644
--- a/src/sites/clips/line.jsx
+++ b/src/sites/clips/line.jsx
@@ -34,6 +34,7 @@ export default class Line extends Module {
onEnable() {
this.chat.context.on('changed:chat.emotes.2x', this.updateLines, this);
+ this.chat.context.on('changed:chat.emotes.animated', this.updateLines, this);
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);
@@ -64,6 +65,7 @@ export default class Line extends Module {
const msg = t.standardizeMessage(this.props.node, this.props.video),
+ anim_hover = t.chat.context.get('chat.emotes.animated') === 2,
is_action = msg.is_action,
user = msg.user,
color = t.parent.colors.process(user.color),
@@ -72,31 +74,35 @@ export default class Line extends Module {
const tokens = msg.ffz_tokens = msg.ffz_tokens || t.chat.tokenizeMessage(msg, u);
- return (