diff --git a/package.json b/package.json index 38bf28b6..62dabfd3 100755 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "frankerfacez", "author": "Dan Salvato LLC", - "version": "4.57.1", + "version": "4.57.2", "description": "FrankerFaceZ is a Twitch enhancement suite.", "private": true, "license": "Apache-2.0", diff --git a/src/addons.js b/src/addons.js index 09fc1600..5fc9dd52 100644 --- a/src/addons.js +++ b/src/addons.js @@ -346,6 +346,9 @@ export default class AddonManager extends Module { other[name] = null; } + // Send off a signal for other modules to unload related data. + this.emit('addon:fully-unload', module.addon_id); + // Clean up the global reference. if ( this.__modules[module.__path] === module ) delete this.__modules[module.__path]; /* = [ @@ -363,7 +366,7 @@ export default class AddonManager extends Module { // Clean up all settings. for(const [key, def] of Array.from(this.settings.definitions.entries())) { - if ( def && def.__source === module.__path ) { + if ( def && def.__source === module.addon_id ) { this.settings.remove(key); } } diff --git a/src/modules/chat/badges.jsx b/src/modules/chat/badges.jsx index 810b7115..dc9ce951 100644 --- a/src/modules/chat/badges.jsx +++ b/src/modules/chat/badges.jsx @@ -395,6 +395,14 @@ export default class Badges extends Module { else store = badge.addon ? addon : ffz; + let name = badge.title; + let extra; + try { + extra = maybe_call(badge.tooltipExtra, this, null, badge); + } catch(err) { extra = null; } + if ( extra && !(extra instanceof Promise) ) + name = name + extra; + const id = badge.base_id ?? key, is_this = id === key; let existing = addon_badges_by_id[id]; @@ -411,14 +419,14 @@ export default class Badges extends Module { existing.versions.push({ version: key, - name: badge.title, + name, color, image: image1x, styleImage: `url("${image1x}")` }); if ( is_this ) { - existing.name = badge.title; + existing.name = name; existing.color = color; existing.image = image; existing.styleImage = `url("${image}")`; @@ -429,7 +437,7 @@ export default class Badges extends Module { id, key, provider: 'ffz', - name: badge.title, + name, color, image, image1x, @@ -542,8 +550,9 @@ export default class Badges extends Module { ); } else if ( p === 'ffz' ) { - const badge = this.badges[d.id], - extra = maybe_call(badge?.tooltipExtra, this, ds, d, target, tip); + const full_badge = this.badges[d.id], + badge = d.badge, + extra = maybe_call(badge?.tooltipExtra ?? full_badge?.tooltipExtra, this, ds, badge, target, tip); if ( extra instanceof Promise ) { promises = true; @@ -582,33 +591,55 @@ export default class Badges extends Module { // Add-On Proxy // ======================================================================== - getAddonProxy(module) { - const path = module.__path; - if ( ! path.startsWith('addon.') ) + getAddonProxy(addon_id, addon, module) { + if ( ! addon_id ) return this; - const addon_id = path.slice(6); + const is_dev = addon?.dev ?? false; - const loadBadgeData = (badge_id, data, ...args) => { + const overrides = {}; + + overrides.loadBadgeData = (badge_id, data, ...args) => { if ( data && data.addon === undefined ) data.addon = addon_id; return this.loadBadgeData(badge_id, data, ...args); }; - const handler = { + if ( is_dev ) { + overrides.setBulk = (source, ...args) => { + if ( ! source.includes(addon_id) ) + module.log.warn('[DEV-CHECK] Call to badges.setBulk did not include addon ID in source:', source); + + return this.setBulk(source, ...args); + }; + + overrides.deleteBulk = (source, ...args) => { + if ( ! source.includes(addon_id) ) + module.log.warn('[DEV-CHECK] Call to badges.deleteBulk did not include addon ID in source:', source); + + return this.deleteBulk(source, ...args); + } + + overrides.extendBulk = (source, ...args) => { + if ( ! source.includes(addon_id) ) + module.log.warn('[DEV-CHECK] Call to badges.extendBulk did not include addon ID in source:', source); + + return this.extendBulk(source, ...args); + } + } + + return new Proxy(this, { get(obj, prop) { - if ( prop === 'loadBadgeData' ) - return loadBadgeData; + const thing = overrides[prop]; + if ( thing ) + return thing; return Reflect.get(...arguments); } - }; - - return new Proxy(this, handler); + }); } - getBadgeData(target) { let container = target.parentElement?.parentElement; if ( ! container?.dataset?.roomId ) @@ -860,6 +891,7 @@ export default class Badges extends Module { bd = { provider: 'ffz', id: badge.id, + badge, image: bu[4] || bu[2] || bu[1], color: badge.color || full_badge.color, title: badge.title || full_badge.title, @@ -1165,17 +1197,22 @@ export default class Badges extends Module { data.click_url = 'https://www.frankerfacez.com/subscribe'; if ( ! data.addon && (data.name === 'subwoofer') ) - data.tooltipExtra = async data => { - const d = await this.getSubwooferMonths(data.user_id); - if ( ! d?.months ) - return; + data.tooltipExtra = data => { + if ( ! data?.user_id ) + return null; - if ( d.lifetime ) - return '\n' + this.i18n.t('badges.subwoofer.lifetime', 'Lifetime Subwoofer'); + return this.getSubwooferMonths(data.user_id) + .then(d => { + if ( ! d?.months ) + return; - return '\n' + this.i18n.t('badges.subwoofer.months', '({count, plural, one {# Month} other {# Months}})', { - count: d.months - }); + if ( d.lifetime ) + return '\n' + this.i18n.t('badges.subwoofer.lifetime', 'Lifetime Subwoofer'); + + return '\n' + this.i18n.t('badges.subwoofer.months', '({count, plural, one {# Month} other {# Months}})', { + count: d.months + }); + }) }; } diff --git a/src/modules/chat/emotes.js b/src/modules/chat/emotes.js index 4c872fa6..abf476f9 100644 --- a/src/modules/chat/emotes.js +++ b/src/modules/chat/emotes.js @@ -599,6 +599,54 @@ export default class Emotes extends Module { this.animLeave = this.animLeave.bind(this); } + + getAddonProxy(addon_id, addon, module) { + if ( ! addon_id ) + return this; + + const overrides = {}; + + if ( addon?.dev ) { + overrides.addDefaultSet = (provider, ...args) => { + if ( ! provider.includes(addon_id) ) + module.log.warn('[DEV-CHECK] Call to emotes.addDefaultSet did not include addon ID in provider:', provider); + + return this.addDefaultSet(provider, ...args); + } + + overrides.removeDefaultSet = (provider, ...args) => { + if ( ! provider.includes(addon_id) ) + module.log.warn('[DEV-CHECK] Call to emotes.removeDefaultSet did not include addon ID in provider:', provider); + + return this.removeDefaultSet(provider, ...args); + } + + overrides.addSubSet = (provider, ...args) => { + if ( ! provider.includes(addon_id) ) + module.log.warn('[DEV-CHECK] Call to emotes.addSubSet did not include addon ID in provider:', provider); + + return this.addSubSet(provider, ...args); + } + + overrides.removeSubSet = (provider, ...args) => { + if ( ! provider.includes(addon_id) ) + module.log.warn('[DEV-CHECK] Call to emotes.removeSubSet did not include addon ID in provider:', provider); + + return this.removeSubSet(provider, ...args); + } + } + + return new Proxy(this, { + get(obj, prop) { + const thing = overrides[prop]; + if ( thing ) + return thing; + return Reflect.get(...arguments); + } + }); + } + + onEnable() { this.style = new ManagedStyle('emotes'); this.effect_style = new ManagedStyle('effects'); diff --git a/src/modules/metadata.jsx b/src/modules/metadata.jsx index f51cfc65..4575d320 100644 --- a/src/modules/metadata.jsx +++ b/src/modules/metadata.jsx @@ -557,6 +557,31 @@ export default class Metadata extends Module { } } + + getAddonProxy(addon_id) { + if ( ! addon_id ) + return this; + + const overrides = {}; + + overrides.define = (key, definition) => { + if ( definition ) + definition.__source = addon_id; + + return this.define(key, definition); + }; + + return new Proxy(this, { + get(obj, prop) { + const thing = overrides[prop]; + if ( thing ) + return thing; + return Reflect.get(...arguments); + } + }); + } + + onEnable() { const md = this.tooltips.types.metadata = target => { let el = target; @@ -592,6 +617,19 @@ export default class Metadata extends Module { opts.modifiers.flip = {behavior: ['bottom','top']}; return opts; } + + this.on('addon:fully-unload', addon_id => { + const removed = new Set; + for(const [key,def] of Object.entries(this.definitions)) { + if ( def?.__source === addon_id ) { + removed.add(key); + this.definitions[key] = undefined; + } + } + + if ( removed.size ) + this.updateMetadata([...removed]); + }); } diff --git a/src/settings/index.js b/src/settings/index.js index d961f420..0f3cd5c0 100644 --- a/src/settings/index.js +++ b/src/settings/index.js @@ -997,22 +997,23 @@ export default class SettingsManager extends Module { // Add-On Proxy // ======================================================================== - getAddonProxy(module) { - const path = module.__path; + getAddonProxy(addon_id) { + if ( ! addon_id ) + return this; const add = (key, definition) => { - return this.add(key, definition, path); + return this.add(key, definition, addon_id); } const addUI = (key, definition) => { - return this.addUI(key, definition, path); + return this.addUI(key, definition, addon_id); } const addClearable = (key, definition) => { - return this.addClearable(key, definition, path); + return this.addClearable(key, definition, addon_id); } - const handler = { + return new Proxy(this, { get(obj, prop) { if ( prop === 'add' ) return add; @@ -1022,9 +1023,7 @@ export default class SettingsManager extends Module { return addClearable; return Reflect.get(...arguments); } - } - - return new Proxy(this, handler); + }); } // ======================================================================== diff --git a/src/sites/clips/line.jsx b/src/sites/clips/line.jsx index 6de51c82..0390a13a 100644 --- a/src/sites/clips/line.jsx +++ b/src/sites/clips/line.jsx @@ -100,7 +100,9 @@ export default class Line extends Module { const override_name = t.overrides.getName(user.id); let user_class = msg.ffz_user_class; - if ( Array.isArray(user_class) ) + if ( user_class instanceof Set ) + user_class = [...user_class].join(' '); + else if ( Array.isArray(user_class) ) user_class = user_class.join(' '); const user_props = { diff --git a/src/sites/shared/player.jsx b/src/sites/shared/player.jsx index 95a92fb5..15dce848 100644 --- a/src/sites/shared/player.jsx +++ b/src/sites/shared/player.jsx @@ -1711,14 +1711,14 @@ export default class PlayerBase extends Module { clearTimeout(timer); ctx.removeEventListener('statechange', evt); if (ctx.state === 'suspended') { - this.log.info('Aborting due to browser auto-play policy.'); + this.log.debug('Aborting due to browser auto-play policy.'); return; } this.createCompressor(inst, video, comp); } - this.log.info('Attempting to resume suspended AudioContext.'); + this.log.debug('Attempting to resume suspended AudioContext.'); timer = setTimeout(evt, 100); try { ctx.addEventListener('statechange', evt); diff --git a/src/sites/twitch-twilight/modules/chat/line.js b/src/sites/twitch-twilight/modules/chat/line.js index 7769fb73..a1834b04 100644 --- a/src/sites/twitch-twilight/modules/chat/line.js +++ b/src/sites/twitch-twilight/modules/chat/line.js @@ -969,7 +969,9 @@ other {# messages were deleted by a moderator.} override_name = t.overrides.getName(user.id); let user_class = msg.ffz_user_class; - if ( Array.isArray(user_class) ) + if ( user_class instanceof Set ) + user_class = [...user_class].join(' '); + else if ( Array.isArray(user_class) ) user_class = user_class.join(' '); const user_props = { diff --git a/src/sites/twitch-twilight/modules/video_chat/index.jsx b/src/sites/twitch-twilight/modules/video_chat/index.jsx index 8a37862e..5e3e9fc7 100644 --- a/src/sites/twitch-twilight/modules/video_chat/index.jsx +++ b/src/sites/twitch-twilight/modules/video_chat/index.jsx @@ -280,7 +280,9 @@ export default class VideoChatHook extends Module { const override_name = t.overrides.getName(user.id); let user_class = msg.ffz_user_class; - if ( Array.isArray(user_class) ) + if ( user_class instanceof Set ) + user_class = [...user_class].join(' '); + else if ( Array.isArray(user_class) ) user_class = user_class.join(' '); const user_props = { diff --git a/src/utilities/addon.js b/src/utilities/addon.js index 37f17a3c..d11ecd31 100644 --- a/src/utilities/addon.js +++ b/src/utilities/addon.js @@ -1,9 +1,18 @@ import Module from 'utilities/module'; +const EXTRACTOR = /^addon\.([^.]+)(?:\.|$)/i; + +function extractAddonId(path) { + const match = EXTRACTOR.exec(path); + if ( match ) + return match[1]; +} + export class Addon extends Module { constructor(...args) { super(...args); + this.addon_id = extractAddonId(this.__path); this.addon_root = this; this.inject('i18n'); diff --git a/src/utilities/module.js b/src/utilities/module.js index dc350d12..9d3763b3 100644 --- a/src/utilities/module.js +++ b/src/utilities/module.js @@ -37,7 +37,10 @@ export class Module extends EventEmitter { this.__modules = parent ? parent.__modules : {}; this.children = {}; - this.addon_root = parent ? parent.addon_root : null; + if ( parent?.addon_id ) { + this.addon_id = parent.addon_id; + this.addon_root = parent.addon_root; + } if ( parent && ! parent.children[this.name] ) parent.children[this.name] = this; @@ -547,8 +550,22 @@ export class Module extends EventEmitter { __processModule(module, name) { - if ( this.addon_root && module.getAddonProxy ) - return module.getAddonProxy(this.addon_root, this); + if ( this.addon_root && module.getAddonProxy ) { + const addon_id = this.addon_id; + if ( ! module.__proxies ) + module.__proxies = {}; + + if ( module.__proxies[addon_id] ) + return module.__proxies[addon_id]; + + const addon = this.resolve('addons')?.getAddon?.(addon_id), + out = module.getAddonProxy(addon_id, addon, this.addon_root, this); + + if ( out !== module ) + module.__proxies[addon_id] = out; + + return out; + } return module; }