1
0
Fork 0
mirror of https://github.com/FrankerFaceZ/FrankerFaceZ.git synced 2025-06-27 21:05:53 +00:00
* Added: Automatically reprocess chat messages when loading a channel for the first time. (Closes #1333)
* Fixed: Random emotes being insert into chat when using the emote menu in some situations. (Closes #1337)
* Fixed: When tokenizing messages, ignore fake emotes injected into Twitch's chat handler for the purpose of auto-completion and WYSIWYG support.
* Changed: Switch to a better method for how to get `require` from webpack.
* Changed: Update the logic used to calculate the container size when overlaying emotes.
* API Added: `load_tracker` module for waiting for multiple events to finish. This is used to reprocess chat lines once every data source has finished loading to avoid multiple unnecessary updates.
* API Added: Add-ons can now set a `load_events` array in their manifest to have the add-on loader register them with `load_tracker`, ensuring events don't fire before the add-on is able to execute.
This commit is contained in:
SirStendec 2023-03-10 17:06:12 -05:00
parent e26f836267
commit daa193aa03
17 changed files with 481 additions and 68 deletions

View file

@ -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",

View file

@ -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) {

View file

@ -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);

77
src/load_tracker.jsx Normal file
View file

@ -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);
}
}
}

View file

@ -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);

View file

@ -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||''}

View file

@ -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);

View file

@ -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;
}

View file

@ -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 = (<img
const emote = (<img
class={`${EMOTE_CLASS} ffz--pointer-events ffz-tooltip${hoverSrc ? ' ffz-hover-emote' : ''}${token.provider === 'twitch' ? ' twitch-emote' : token.provider === 'ffz' ? ' ffz-emote' : token.provider === 'emoji' ? ' ffz-emoji' : ''}`}
src={src}
srcSet={srcSet}
@ -1303,14 +1329,14 @@ export const AddonEmotes = {
data-provider={token.provider}
data-id={token.id}
data-set={token.set}
style={style}
style={outerStyle}
data-modifiers={ml ? mods.map(x => 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 <span key={t.text}>
{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 (<span class="tw-mg-05">
{emote_id?.icon ? <img class="ffz__tooltip__mod-icon" src={emote_id.icon} /> : null}
{emote_id?.icon ? ` - ${emote_id?.label}` : emote_id?.label}
</span>);
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 && (<div class="tw-mg-t-05 tw-border-t tw-pd-t-05">{ds.sellout}</div>),
mods && (<div class="tw-pd-t-1">{mods}</div>),
mods && (<div class="tw-pd-t-1 tw-pd-b-05">{mods}</div>),
favorite && (<figure class="ffz--favorite ffz-i-star" />)
];
@ -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.

View file

@ -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);

View file

@ -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 = [];

View file

@ -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}`);

View file

@ -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;

View file

@ -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;

View file

@ -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)
]);
})
}

View file

@ -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%);

View file

@ -208,6 +208,12 @@ body {
}
.ffz__tooltip__mod-icon {
max-width: 8rem;
max-height: 4rem;
}
.ffz__tooltip--badges {
display: flex;
flex-wrap: wrap;