mirror of
https://github.com/FrankerFaceZ/FrankerFaceZ.git
synced 2025-06-27 21:05:53 +00:00
4.43.0
* 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:
parent
e26f836267
commit
daa193aa03
17 changed files with 481 additions and 68 deletions
|
@ -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",
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
77
src/load_tracker.jsx
Normal 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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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||''}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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 = [];
|
||||
|
|
|
@ -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}`);
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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)
|
||||
]);
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -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%);
|
||||
|
|
|
@ -208,6 +208,12 @@ body {
|
|||
}
|
||||
|
||||
|
||||
.ffz__tooltip__mod-icon {
|
||||
max-width: 8rem;
|
||||
max-height: 4rem;
|
||||
}
|
||||
|
||||
|
||||
.ffz__tooltip--badges {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue