diff --git a/package.json b/package.json
index 438ccbf3..e2bf3c11 100755
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "frankerfacez",
"author": "Dan Salvato LLC",
- "version": "4.42.1",
+ "version": "4.43.0",
"description": "FrankerFaceZ is a Twitch enhancement suite.",
"private": true,
"license": "Apache-2.0",
diff --git a/src/addons.js b/src/addons.js
index a7d97bdf..8ad6e432 100644
--- a/src/addons.js
+++ b/src/addons.js
@@ -24,6 +24,7 @@ export default class AddonManager extends Module {
this.inject('settings');
this.inject('i18n');
+ this.inject('load_tracker');
this.load_requires = ['settings'];
@@ -33,6 +34,8 @@ export default class AddonManager extends Module {
this.reload_required = false;
this.addons = {};
this.enabled_addons = [];
+
+ this.load_tracker.schedule('chat-data', 'addon-initial');
}
onLoad() {
@@ -92,6 +95,7 @@ export default class AddonManager extends Module {
this.log.capture(err);
});
+ this.load_tracker.notify('chat-data', 'addon-initial');
this.emit(':ready');
});
}
@@ -431,11 +435,19 @@ export default class AddonManager extends Module {
if ( ! addon )
throw new Error(`Unknown add-on id: ${id}`);
+ if ( Array.isArray(addon.load_events) )
+ for(const event of addon.load_events)
+ this.load_tracker.schedule(event, `addon.${id}`);
+
await this.loadAddon(id);
const module = this.resolve(`addon.${id}`);
if ( module && ! module.enabled )
await module.enable();
+
+ if ( Array.isArray(addon.load_events) )
+ for(const event of addon.load_events)
+ this.load_tracker.notify(event, `addon.${id}`, false);
}
async loadAddon(id) {
diff --git a/src/clips.js b/src/clips.js
index cdcfc726..7a8d519e 100644
--- a/src/clips.js
+++ b/src/clips.js
@@ -14,6 +14,7 @@ import AddonManager from './addons';
import ExperimentManager from './experiments';
import {TranslationManager} from './i18n';
import StagingSelector from './staging';
+import LoadTracker from './load_tracker';
import Site from './sites/clips';
import Tooltips from 'src/modules/tooltips';
@@ -54,6 +55,7 @@ class FrankerFaceZ extends Module {
this.inject('experiments', ExperimentManager);
this.inject('i18n', TranslationManager);
this.inject('staging', StagingSelector);
+ this.inject('load_tracker', LoadTracker);
this.inject('site', Site);
this.inject('addons', AddonManager);
diff --git a/src/load_tracker.jsx b/src/load_tracker.jsx
new file mode 100644
index 00000000..9bcdd419
--- /dev/null
+++ b/src/load_tracker.jsx
@@ -0,0 +1,77 @@
+'use strict';
+
+// ============================================================================
+// Loading Tracker
+// ============================================================================
+
+import Module from 'utilities/module';
+
+export default class LoadTracker extends Module {
+
+ constructor(...args) {
+ super(...args);
+
+ this.should_enable = true;
+
+ this.inject('settings');
+
+ this.settings.add('chat.update-when-loaded', {
+ default: true,
+ ui: {
+ path: 'Chat > Behavior >> General',
+ title: 'Update existing chat messages when loading new data.',
+ component: 'setting-check-box',
+ description: 'This may cause elements in chat to move, so you may wish to disable this when performing moderation.'
+ }
+ });
+
+ this.pending_loads = new Map;
+
+ this.on(':schedule', this.schedule, this);
+
+ }
+
+ schedule(type, key) {
+ let data = this.pending_loads.get(type);
+ if ( ! data || ! data.pending || ! data.timers ) {
+ data = {
+ pending: new Set,
+ timers: {},
+ success: false
+ };
+ this.pending_loads.set(type, data);
+ }
+
+ if ( data.pending.has(key) )
+ return;
+
+ data.pending.add(key);
+ data.timers[key] = setTimeout(() => this.notify(type, key, false), 15000);
+ }
+
+ notify(type, key, success = true) {
+ const data = this.pending_loads.get(type);
+ if ( ! data || ! data.pending || ! data.timers )
+ return;
+
+ if ( data.timers[key] ) {
+ clearTimeout(data.timers[key]);
+ data.timers[key] = null;
+ }
+
+ if ( ! data.pending.has(key) )
+ return;
+
+ data.pending.delete(key);
+ if ( success )
+ data.success = true;
+
+ if ( ! data.pending.size ) {
+ this.log.debug('complete', type, Object.keys(data.timers));
+ if ( data.success )
+ this.emit(`:complete:${type}`);
+ this.pending_loads.delete(type);
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/src/main.js b/src/main.js
index 5e2df55e..17249b27 100644
--- a/src/main.js
+++ b/src/main.js
@@ -18,6 +18,7 @@ import SocketClient from './socket';
import Site from 'site';
import Vue from 'utilities/vue';
import StagingSelector from './staging';
+import LoadTracker from './load_tracker';
//import Timing from 'utilities/timing';
class FrankerFaceZ extends Module {
@@ -58,6 +59,7 @@ class FrankerFaceZ extends Module {
this.inject('experiments', ExperimentManager);
this.inject('i18n', TranslationManager);
this.inject('staging', StagingSelector);
+ this.inject('load_tracker', LoadTracker);
this.inject('socket', SocketClient);
//this.inject('pubsub', PubSubClient);
this.inject('site', Site);
diff --git a/src/modules/chat/emotes.js b/src/modules/chat/emotes.js
index 0975fe1e..689fcdda 100644
--- a/src/modules/chat/emotes.js
+++ b/src/modules/chat/emotes.js
@@ -309,6 +309,7 @@ export default class Emotes extends Module {
this.inject('settings');
this.inject('experiments');
this.inject('staging');
+ this.inject('load_tracker');
this.twitch_inventory_sets = new Set; //(EXTRA_INVENTORY);
this.__twitch_emote_to_set = {};
@@ -1333,6 +1334,8 @@ export default class Emotes extends Module {
// ========================================================================
async loadGlobalSets(tries = 0) {
+ this.load_tracker.schedule('chat-data', 'ffz-global');
+
let response, data;
if ( this.experiments.getAssignment('api_load') && tries < 1 )
@@ -1348,16 +1351,20 @@ export default class Emotes extends Module {
return setTimeout(() => this.loadGlobalSets(tries), 500 * tries);
this.log.error('Error loading global emote sets.', err);
+ this.load_tracker.notify('chat-data', 'ffz-global', false);
return false;
}
- if ( ! response.ok )
+ if ( ! response.ok ) {
+ this.load_tracker.notify('chat-data', 'ffz-global', false);
return false;
+ }
try {
data = await response.json();
} catch(err) {
this.log.error('Error parsing global emote data.', err);
+ this.load_tracker.notify('chat-data', 'ffz-global', false);
return false;
}
@@ -1379,11 +1386,14 @@ export default class Emotes extends Module {
else if ( data.users )
this.loadSetUsers(data.users);
+ this.load_tracker.notify('chat-data', 'ffz-global');
return true;
}
async loadSet(set_id, suppress_log = false, tries = 0) {
+ const load_key = `ffz-${set_id}`;
+ this.load_tracker.schedule('chat-data', load_key);
let response, data;
if ( this.experiments.getAssignment('api_load') )
@@ -1399,16 +1409,20 @@ export default class Emotes extends Module {
return setTimeout(() => this.loadGlobalSets(tries), 500 * tries);
this.log.error(`Error loading data for set "${set_id}".`, err);
+ this.load_tracker.notify('chat-data', load_key, false);
return false;
}
- if ( ! response.ok )
+ if ( ! response.ok ) {
+ this.load_tracker.notify('chat-data', load_key, false);
return false;
+ }
try {
data = await response.json();
} catch(err) {
this.log.error(`Error parsing data for set "${set_id}".`, err);
+ this.load_tracker.notify('chat-data', load_key, false);
return false;
}
@@ -1421,6 +1435,7 @@ export default class Emotes extends Module {
else if ( data.users )
this.loadSetUsers(data.users);
+ this.load_tracker.notify('chat-data', load_key, true);
return true;
}
@@ -1503,6 +1518,8 @@ export default class Emotes extends Module {
animSrcSet: emote.animSrcSet,
animSrc2: emote.animSrc2,
animSrcSet2: emote.animSrcSet2,
+ masked: !! emote.mask,
+ hidden: (emote.modifier_flags & 1) === 1,
text: emote.hidden ? '???' : emote.name,
length: emote.name.length,
height: emote.height,
@@ -1705,7 +1722,7 @@ export default class Emotes extends Module {
// ========================================================================
generateEmoteCSS(emote) { // eslint-disable-line class-methods-use-this
- if ( ! emote.margins && ( ! emote.modifier || ( ! emote.modifier_offset && ! emote.extra_width && ! emote.shrink_to_fit ) ) && ! emote.css )
+ if ( ! emote.mask && ! emote.margins && ( ! emote.modifier || ( ! emote.modifier_offset && ! emote.extra_width && ! emote.shrink_to_fit ) ) && ! emote.css )
return '';
let output = '';
@@ -1728,6 +1745,13 @@ export default class Emotes extends Module {
}`;
}
+ if ( emote.modifier && emote.mask?.[1] ) {
+ output = (output || '') + `.modified-emote[data-modifiers~="${emote.id}"] > img {
+ -webkit-mask-image: url("${emote.mask[1]}");
+ -webkit-mask-position: center center;
+}`
+ }
+
return `${output}.ffz-emote[data-id="${emote.id}"] {
${(emote.margins && ! emote.modifier) ? `margin: ${emote.margins} !important;` : ''}
${emote.css||''}
diff --git a/src/modules/chat/index.js b/src/modules/chat/index.js
index 36d4f915..e202094d 100644
--- a/src/modules/chat/index.js
+++ b/src/modules/chat/index.js
@@ -71,6 +71,7 @@ export default class Chat extends Module {
this.inject('tooltips');
this.inject('experiments');
this.inject('staging');
+ this.inject('load_tracker');
this.inject(Badges);
this.inject(Emotes);
diff --git a/src/modules/chat/room.js b/src/modules/chat/room.js
index e5898146..3583a29f 100644
--- a/src/modules/chat/room.js
+++ b/src/modules/chat/room.js
@@ -253,6 +253,9 @@ export default class Room {
if ( this.destroyed )
return;
+ const load_key = `ffz-room-${this.id ? `id:${this.id}` : this.login}`;
+ this.manager.load_tracker.schedule('chat-data', load_key);
+
if ( this.manager.experiments.getAssignment('api_load') )
try {
fetch(`${NEW_API}/v1/room/${this.id ? `id/${this.id}` : this.login}`).catch(() => {});
@@ -267,16 +270,20 @@ export default class Room {
return setTimeout(() => this.load_data(tries), 500 * tries);
this.manager.log.error(`Error loading room data for ${this.id}:${this.login}`, err);
+ this.manager.load_tracker.notify('chat-data', load_key, false);
return false;
}
- if ( ! response.ok )
+ if ( ! response.ok ) {
+ this.manager.load_tracker.notify('chat-data', load_key, false);
return false;
+ }
try {
data = await response.json();
} catch(err) {
this.manager.log.error(`Error parsing room data for ${this.id}:${this.login}`, err);
+ this.manager.load_tracker.notify('chat-data', load_key, false);
return false;
}
@@ -296,6 +303,7 @@ export default class Room {
} else if ( this._id !== id ) {
this.manager.log.warn(`Received data for ${this.id}:${this.login} with the wrong ID: ${id}`);
+ this.manager.load_tracker.notify('chat-data', load_key, false);
return false;
}
@@ -331,6 +339,7 @@ export default class Room {
this.buildModBadgeCSS();
this.buildVIPBadgeCSS();
+ this.manager.load_tracker.notify('chat-data', load_key);
return true;
}
diff --git a/src/modules/chat/tokenizers.jsx b/src/modules/chat/tokenizers.jsx
index 42c97528..86083dc2 100644
--- a/src/modules/chat/tokenizers.jsx
+++ b/src/modules/chat/tokenizers.jsx
@@ -1222,53 +1222,79 @@ export const AddonEmotes = {
hoverSrcSet = big ? token.animSrcSet2 : token.animSrcSet;
}
- let style = undefined;
- const effects = token.modifier_flags,
+ let style = undefined, outerStyle = undefined;
+ const mods = token.modifiers || [], ml = mods.length,
+ effects = token.modifier_flags,
is_big = (token.big && ! token.can_big && token.height);
- if ( effects ) {
- this.emotes.ensureEffect(effects);
- let make_bigger = big;
+ if ( effects || ml ) {
+ // We need to calculate the size of the emote and the biggest
+ // modifier so that everything can be nicely centered.
if ( token.provider === 'emoji' ) {
- const size = 1.5 * (this.context.get('chat.font-size') ?? 13);
+ const factor = token.big_emoji ? 2 : 1,
+ size = factor * 1.5 * (this.context.get('chat.font-size') ?? 13);
+
style = {
+ width: size,
+ height: size,
+ };
+ outerStyle = {
width: size,
height: size
};
- make_bigger = token.big_emoji;
-
- } else
+ } else {
+ const factor = big ? 2 : 1;
style = {
- width: token.width,
- height: token.height
+ width: token.width * factor,
+ height: token.height * factor
+ };
+ outerStyle = {
+ width: style.width,
+ height: style.height
};
-
- if ( make_bigger ) {
- style.width *= 2;
- style.height *= 2;
}
- if ( (effects & SHRINK_X) === SHRINK_X )
- style.width *= 0.5;
- if ( (effects & STRETCH_X) === STRETCH_X )
- style.width *= 2;
- if ( (effects & SHRINK_Y) === SHRINK_Y )
- style.height *= 0.5;
- if ( (effects & STRETCH_Y) === STRETCH_Y )
- style.height *= 2;
+ for(const mod of mods) {
+ if ( ! mod.hidden && mod.set !== 'info' ) {
+ const factor = mod.big ? 2 : 1,
+ width = mod.width * factor,
+ height = mod.height * factor;
- if ( (effects & ROTATE_90) === ROTATE_90 ) {
- const w = style.width;
- style.width = style.height;
- style.height = w;
+ if ( width > outerStyle.width )
+ outerStyle.width = width;
+ if ( height > outerStyle.height )
+ outerStyle.height = height;
+ }
}
- style.width = Math.min(style.width, big ? 256 : 128);
- style.height = Math.min(style.height, big ? 80 : 40);
+ if ( effects ) {
+ this.emotes.ensureEffect(effects);
+
+ if ( (effects & SHRINK_X) === SHRINK_X )
+ style.width *= 0.5;
+ if ( (effects & STRETCH_X) === STRETCH_X )
+ style.width *= 2;
+ if ( (effects & SHRINK_Y) === SHRINK_Y )
+ style.height *= 0.5;
+ if ( (effects & STRETCH_Y) === STRETCH_Y )
+ style.height *= 2;
+
+ style.width = Math.min(style.width, big ? 256 : 128);
+ style.height = Math.min(style.height, big ? 80 : 40);
+
+ if ( style.width > outerStyle.width )
+ outerStyle.width = style.width;
+ if ( style.height > outerStyle.height )
+ outerStyle.height = style.height;
+ }
+
+ if ( style.width !== outerStyle.width )
+ style.marginLeft = (outerStyle.width - style.width) / 2;
+ if ( style.height !== outerStyle.height )
+ style.marginTop = (outerStyle.height - style.height) / 2;
}
- const mods = token.modifiers || [], ml = mods.length,
- emote = (
x.id).join(' ') : null}
data-effects={effects ? effects : undefined}
onClick={this.emotes.handleClick}
>
{emote}
{mods.map(t => {
- if ( (t.source_modifier_flags & 1) === 1)
+ if ( (t.source_modifier_flags & 1) === 1 || t.set === 'info')
return null;
return
{this.tokenizers.emote.render.call(this, t, createElement, true)}
@@ -1331,6 +1357,12 @@ export const AddonEmotes = {
if ( modifiers && modifiers !== 'null' ) {
mods = JSON.parse(modifiers).map(([set_id, emote_id]) => {
+ if ( set_id === 'info' )
+ return (
+ {emote_id?.icon ?
: null}
+ {emote_id?.icon ? ` - ${emote_id?.label}` : emote_id?.label}
+ );
+
const emote_set = this.emotes.emote_sets[set_id],
emote = emote_set && emote_set.emotes[emote_id];
@@ -1542,7 +1574,7 @@ export const AddonEmotes = {
ds.sellout && ({ds.sellout}
),
- mods && ({mods}
),
+ mods && ({mods}
),
favorite && ()
];
@@ -1796,6 +1828,15 @@ export const TwitchEmotes = {
while( eix < e_length ) {
const [e_id, e_start, e_end] = emotes[eix];
+ // Do not honor fake emotes that were created for the sake
+ // of WYSIWYG / autocompletion.
+ if ( typeof e_id === 'string' ) {
+ if ( e_id.startsWith('__FFZ__') || e_id.startsWith('__BTTV__') ) {
+ eix++;
+ continue;
+ }
+ }
+
// Does this emote go outside the bounds of this token?
if ( e_start > t_end || e_end > t_end ) {
// Output the remainder of this token.
diff --git a/src/player.js b/src/player.js
index f200553f..1f043f7e 100644
--- a/src/player.js
+++ b/src/player.js
@@ -14,6 +14,7 @@ import AddonManager from './addons';
import ExperimentManager from './experiments';
import {TranslationManager} from './i18n';
import StagingSelector from './staging';
+import LoadTracker from './load_tracker';
import Site from './sites/player';
class FrankerFaceZ extends Module {
@@ -51,6 +52,7 @@ class FrankerFaceZ extends Module {
this.inject('experiments', ExperimentManager);
this.inject('i18n', TranslationManager);
this.inject('staging', StagingSelector);
+ this.inject('load_tracker', LoadTracker);
this.inject('site', Site);
this.inject('addons', AddonManager);
diff --git a/src/sites/twitch-twilight/index.js b/src/sites/twitch-twilight/index.js
index e591291f..fa03b79f 100644
--- a/src/sites/twitch-twilight/index.js
+++ b/src/sites/twitch-twilight/index.js
@@ -14,7 +14,7 @@ import Apollo from 'utilities/compat/apollo';
import TwitchData from 'utilities/twitch-data';
import Subpump from 'utilities/compat/subpump';
-import Switchboard from './switchboard';
+//import Switchboard from './switchboard';
import {createElement} from 'utilities/dom';
import {has} from 'utilities/object';
@@ -36,7 +36,7 @@ export default class Twilight extends BaseSite {
this.inject('router', FineRouter);
this.inject(Apollo, false);
this.inject(TwitchData);
- this.inject(Switchboard);
+ //this.inject(Switchboard);
this.inject(Subpump);
this._dom_updates = [];
diff --git a/src/sites/twitch-twilight/modules/chat/emote_menu.jsx b/src/sites/twitch-twilight/modules/chat/emote_menu.jsx
index 1682fd7c..9dad6bf6 100644
--- a/src/sites/twitch-twilight/modules/chat/emote_menu.jsx
+++ b/src/sites/twitch-twilight/modules/chat/emote_menu.jsx
@@ -690,7 +690,8 @@ export default class EmoteMenu extends Module {
// Check for magic.
let prefix = '';
- if ( event.currentTarget.dataset.effects != '0' && t.emotes.target_emote )
+ const effects = event.currentTarget.dataset.effects;
+ if ( effects?.length > 0 && effects != '0' && t.emotes.target_emote )
prefix = `${t.emotes.target_emote.name} `;
this.props.onClickToken(`${prefix}${event.currentTarget.dataset.name}`);
diff --git a/src/sites/twitch-twilight/modules/chat/input.jsx b/src/sites/twitch-twilight/modules/chat/input.jsx
index 737ac8bd..840a8d43 100644
--- a/src/sites/twitch-twilight/modules/chat/input.jsx
+++ b/src/sites/twitch-twilight/modules/chat/input.jsx
@@ -72,6 +72,16 @@ export default class Input extends Module {
// Settings
+ this.settings.add('chat.inline-preview.enabled', {
+ default: true,
+ ui: {
+ path: 'Chat > Input >> Appearance',
+ title: 'Display in-line previews of FrankerFaceZ emotes when entering a chat message.',
+ description: '**Note:** This feature is tempermental. It may not display all emotes, and emote effects and overlay emotes are not displayed correctly. Once this setting has been enabled, it cannot be reasonably disabled and will remain active until you refresh the page.',
+ component: 'setting-check-box'
+ }
+ });
+
this.settings.add('chat.mru.enabled', {
default: true,
ui: {
@@ -203,6 +213,22 @@ export default class Input extends Module {
inst.canBeTriggeredByTab = !enabled;
});
+ this.use_previews = this.chat.context.get('chat.inline-preview.enabled');
+
+ this.chat.context.on('changed:chat.inline-preview.enabled', val => {
+ if ( this.use_previews )
+ return;
+
+ this.use_previews = val;
+ if ( val )
+ for(const inst of this.ChatInput.instances) {
+ this.installPreviewObserver(inst);
+ inst.ffzInjectEmotes();
+ inst.forceUpdate();
+ this.emit('site:dom-update', 'chat-input', inst);
+ }
+ });
+
const React = await this.web_munch.findModule('react'),
createElement = React && React.createElement;
@@ -266,6 +292,8 @@ export default class Input extends Module {
this.emit('site:dom-update', 'chat-input', inst);
this.updateEmoteCompletion(inst);
this.overrideChatInput(inst);
+ inst.ffzInjectEmotes();
+ this.installPreviewObserver(inst);
}
});
@@ -286,6 +314,10 @@ export default class Input extends Module {
this.ChatInput.on('update', this.updateEmoteCompletion, this);
this.ChatInput.on('mount', this.overrideChatInput, this);
+
+ this.ChatInput.on('mount', this.installPreviewObserver, this);
+ this.ChatInput.on('unmount', this.removePreviewObserver, this);
+
this.EmoteSuggestions.on('mount', this.overrideEmoteMatcher, this);
this.MentionSuggestions.on('mount', this.overrideMentionMatcher, this);
this.CommandSuggestions.on('mount', this.overrideCommandMatcher, this);
@@ -307,6 +339,10 @@ export default class Input extends Module {
inst.ffz_ffz_cache = null;
inst.ffz_twitch_cache = null;
}
+
+ if ( this.use_previews )
+ for(const inst of this.ChatInput.instances)
+ inst.ffzInjectEmotes();
}
updateInput() {
@@ -332,6 +368,110 @@ export default class Input extends Module {
}
+ installPreviewObserver(inst) {
+ if ( inst._ffz_preview_observer || ! window.MutationObserver )
+ return;
+
+ if ( ! this.use_previews )
+ return;
+
+ const el = this.fine.getHostNode(inst),
+ target = el && el.querySelector('.chat-input__textarea');
+ if ( ! target )
+ return;
+
+ inst._ffz_preview_observer = new MutationObserver(mutations => {
+ for(const mut of mutations) {
+ //if ( mut.target instanceof Element )
+ // this.checkForPreviews(inst, mut.target);
+
+ for(const node of mut.addedNodes) {
+ if ( node instanceof Element )
+ this.checkForPreviews(inst, node);
+ }
+ }
+ });
+
+ inst._ffz_preview_observer.observe(target, {
+ childList: true,
+ subtree: true,
+ //attributeFilter: ['src']
+ });
+ }
+
+ checkForPreviews(inst, node) {
+ for(const el of node.querySelectorAll?.('span[data-a-target="chat-input-emote-preview"][aria-describedby]') ?? []) {
+ const cont = document.getElementById(el.getAttribute('aria-describedby')),
+ target = cont && cont.querySelector('img.chat-line__message--emote');
+
+ if ( target && target.src.startsWith('https://static-cdn.jtvnw.net/emoticons/v2/__FFZ__') )
+ this.updatePreview(inst, target);
+ }
+
+ for(const target of node.querySelectorAll?.('img.chat-line__message--emote')) {
+ if ( target && (target.dataset.ffzId || target.src.startsWith('https://static-cdn.jtvnw.net/emoticons/v2/__FFZ__')) )
+ this.updatePreview(inst, target);
+ }
+ }
+
+ updatePreview(inst, target) {
+ let set_id = target.dataset.ffzSet,
+ emote_id = target.dataset.ffzId;
+
+ if ( ! emote_id ) {
+ const idx = target.src.indexOf('__FFZ__', 49),
+ raw_id = target.src.slice(49, idx);
+
+ const raw_idx = raw_id.indexOf('::');
+ if ( raw_idx === -1 )
+ return;
+
+ set_id = raw_id.slice(0, raw_idx);
+ emote_id = raw_id.slice(raw_idx + 2);
+
+ target.dataset.ffzSet = set_id;
+ target.dataset.ffzId = emote_id;
+ }
+
+ const emote_set = this.emotes.emote_sets[set_id],
+ emote = emote_set?.emotes?.[emote_id];
+
+ if ( ! emote )
+ return;
+
+ const anim = this.chat.context.get('chat.emotes.animated') > 0;
+
+ target.src = (anim ? emote.animSrc : null) ?? emote.src;
+ target.srcset = (anim ? emote.animSrcSet : null) ?? emote.srcSet;
+
+ const w = `${emote.width}px`;
+ const h = `${emote.height}px`;
+
+ target.style.width = w;
+ target.style.height = h;
+
+ // Find the parent.
+ const cont = target.closest('.chat-image__container');
+ if ( cont ) {
+ cont.style.width = w;
+ cont.style.height = h;
+
+ const outer = cont.closest('.chat-line__message--emote-button');
+ if ( outer ) {
+ outer.style.width = w;
+ outer.style.height = h;
+ }
+ }
+ }
+
+ removePreviewObserver(inst) {
+ if ( inst._ffz_preview_observer ) {
+ inst._ffz_preview_observer.disconnect();
+ inst._ffz_preview_observer = null;
+ }
+ }
+
+
updateEmoteCompletion(inst, child) {
if ( ! child )
child = this.fine.searchTree(inst, 'tab-emote-suggestions', 50);
@@ -353,6 +493,33 @@ export default class Input extends Module {
originalOnMessageSend = inst.onMessageSend,
old_resize = inst.resizeInput;
+ const old_componentDidUpdate = inst.componentDidUpdate;
+
+ inst.ffzInjectEmotes = function() {
+ const idx = this.props.emotes.findIndex(item => item?.id === 'FrankerFaceZWasHere'),
+ data = t.createFakeEmoteSet(inst);
+
+ if ( idx === -1 && data )
+ this.props.emotes.push(data);
+ else if ( idx !== -1 && data )
+ this.props.emotes.splice(idx, 1, data);
+ else if ( idx !== -1 && ! data )
+ this.props.emotes.splice(idx, 1);
+ }
+
+ inst.componentDidUpdate = function(props, ...args) {
+ try {
+ if ( props.emotes !== this.props.emotes && Array.isArray(this.props.emotes) )
+ inst.ffzInjectEmotes();
+
+ } catch(err) {
+ t.log.error('Error updating emote autocompletion data.', err);
+ }
+
+ if ( old_componentDidUpdate )
+ old_componentDidUpdate.call(this, props, ...args);
+ }
+
inst.resizeInput = function(msg, ...args) {
try {
if ( msg ) {
@@ -574,6 +741,60 @@ export default class Input extends Module {
}
+ createFakeEmoteSet(inst) {
+ if ( ! this.use_previews )
+ return null;
+
+ if ( ! inst._ffz_channel_login ) {
+ const parent = this.fine.searchParent(inst, 'chat-input', 50);
+ if ( parent )
+ this.updateEmoteCompletion(parent, inst);
+ }
+
+ const user = inst._ffz_user,
+ channel_id = inst._ffz_channel_id,
+ channel_login = inst._ffz_channel_login;
+
+ if ( ! channel_login )
+ return null;
+
+ const sets = this.emotes.getSets(user?.id, user?.login, channel_id, channel_login);
+ if ( ! sets || ! sets.length )
+ return null;
+
+ const out = [],
+ added_emotes = new Set;
+
+ for(const set of sets) {
+ if ( ! set || ! set.emotes )
+ continue;
+
+ const source = set.source || 'ffz';
+
+ for(const emote of Object.values(set.emotes)) {
+ if ( ! emote || ! emote.id || ! emote.name || added_emotes.has(emote.name) )
+ continue;
+
+ added_emotes.add(emote.name);
+
+ out.push({
+ id: `__FFZ__${set.id}::${emote.id}__FFZ__`,
+ modifiers: null,
+ setID: 'FrankerFaceZWasHere',
+ token: emote.name
+ });
+ }
+ }
+
+ return {
+ __typename: 'EmoteSet',
+ emotes: out,
+ id: 'FrankerFaceZWasHere',
+ owner: null
+ }
+ }
+
+
overrideEmoteMatcher(inst) {
if ( inst._ffz_override )
return;
@@ -735,6 +956,10 @@ export default class Input extends Module {
is_points = TWITCH_POINTS_SETS.includes(int_id) || owner?.login === 'channel_points',
channel = is_points ? null : owner;
+ // Skip this set.
+ if ( set.id === 'FrankerFaceZWasHere' )
+ continue;
+
let key = `twitch-set-${set.id}`;
let extra = null;
diff --git a/src/sites/twitch-twilight/modules/chat/line.js b/src/sites/twitch-twilight/modules/chat/line.js
index bd22a477..5dee3ecd 100644
--- a/src/sites/twitch-twilight/modules/chat/line.js
+++ b/src/sites/twitch-twilight/modules/chat/line.js
@@ -352,6 +352,20 @@ export default class ChatLine extends Module {
this.on('i18n:update', this.rerenderLines, this);
this.on('chat.emotes:update-effects', this.checkEffects, this);
+ this.can_reprocess = true;
+
+ this.on('chat:room-add', () => this.can_reprocess = true);
+
+ this.on('load_tracker:complete:chat-data', () => {
+ const val = this.chat.context.get('chat.update-when-loaded');
+ if ( ! val || ! this.can_reprocess )
+ return;
+
+ this.can_reprocess = false;
+ this.log.info('Reprocessing chat lines due to data loads.');
+ this.updateLines();
+ });
+
this.on('experiments:changed:line_renderer', () => {
const value = this.experiments.get('line_renderer'),
cls = this.ChatLine._class;
@@ -552,6 +566,10 @@ export default class ChatLine extends Module {
}
cls.prototype.ffzOpenReply = function() {
+ if ( this.onMessageClick ) {
+ return this.onMessageClick();
+ }
+
if ( this.props.reply ) {
this.setOPCardTray(this.props.reply);
return;
diff --git a/src/utilities/compat/webmunch.js b/src/utilities/compat/webmunch.js
index c62d3a3a..e95119ce 100644
--- a/src/utilities/compat/webmunch.js
+++ b/src/utilities/compat/webmunch.js
@@ -6,7 +6,7 @@
// ============================================================================
import Module from 'utilities/module';
-import {has} from 'utilities/object';
+import {has, generateUUID} from 'utilities/object';
import { DEBUG } from 'utilities/constants';
@@ -193,7 +193,7 @@ export default class WebMunch extends Module {
for(const [mod_id, original_module] of Object.entries(modules)) {
this._known_ids.add(mod_id);
- modules[mod_id] = function(module, exports, require, ...args) {
+ /*modules[mod_id] = function(module, exports, require, ...args) {
if ( ! t._require && typeof require === 'function' ) {
t.log.debug(`require() grabbed from invocation of module ${mod_id}`);
try {
@@ -206,7 +206,7 @@ export default class WebMunch extends Module {
return original_module.call(this, module, exports, require, ...args);
}
- modules[mod_id].original = original_module;
+ modules[mod_id].original = original_module;*/
}
}
@@ -604,7 +604,7 @@ export default class WebMunch extends Module {
return Promise.resolve(this._require);
return new Promise((resolve, reject) => {
- const fn = this._original_loader;
+ let fn = this._original_loader;
if ( ! fn ) {
if ( limit > 500 )
reject(new Error('unable to find webpackJsonp'));
@@ -612,28 +612,20 @@ export default class WebMunch extends Module {
return setTimeout(() => this.getRequire(limit++).then(resolve), 250);
}
- if ( this.v4 ) {
- // There's currently no good way to grab require from
- // webpack 4 due to its lazy loading, so we just wait
- // and hope that a module is imported.
- if ( this._resolve_require )
- this._resolve_require.push(resolve);
- else
- this._resolve_require = [resolve];
+ if ( this.v4 )
+ fn = fn.bind(this._original_store);
- } else {
- // Inject a fake module and use that to grab require.
- const id = `${this._id}$${this._rid++}`;
- fn(
- [],
- {
- [id]: (module, exports, __webpack_require__) => {
- resolve(this._require = __webpack_require__);
- }
- },
- [id]
- )
- }
+ // Inject a fake module and use that to grab require.
+ const id = `ffz-loader$${generateUUID()}`;
+ fn([
+ [id],
+ {
+ [id]: (module, exports, __webpack_require__) => {
+ resolve(this._require = __webpack_require__);
+ }
+ },
+ req => req(id)
+ ]);
})
}
diff --git a/styles/chat.scss b/styles/chat.scss
index 46310032..c31aafef 100644
--- a/styles/chat.scss
+++ b/styles/chat.scss
@@ -502,13 +502,14 @@
span {
position: absolute;
- top: -20px; bottom: -20px; left: -20px; right: -20px;
+ top: 0; bottom: 0; left: 0; right: 0; // -20px; bottom: -20px; left: -20px; right: -20px;
margin: auto;
pointer-events: none;
img {
position: absolute;
pointer-events: none !important;
+ margin: 0;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
diff --git a/styles/tooltips.scss b/styles/tooltips.scss
index 850bb889..520cd225 100644
--- a/styles/tooltips.scss
+++ b/styles/tooltips.scss
@@ -208,6 +208,12 @@ body {
}
+.ffz__tooltip__mod-icon {
+ max-width: 8rem;
+ max-height: 4rem;
+}
+
+
.ffz__tooltip--badges {
display: flex;
flex-wrap: wrap;