diff --git a/package.json b/package.json index 0e8d7590..a7d619e0 100755 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "frankerfacez", "author": "Dan Salvato LLC", - "version": "4.20.48", + "version": "4.20.49", "description": "FrankerFaceZ is a Twitch enhancement suite.", "license": "Apache-2.0", "scripts": { diff --git a/src/experiments.json b/src/experiments.json index 37e859d9..4b20e7e3 100644 --- a/src/experiments.json +++ b/src/experiments.json @@ -3,8 +3,8 @@ "name": "New API Stress Testing", "description": "Send duplicate requests to the new API server for load testing.", "groups": [ - {"value": true, "weight": 100}, - {"value": false, "weight": 0} + {"value": true, "weight": 0}, + {"value": false, "weight": 100} ] }, "api_links": { diff --git a/src/modules/chat/emotes.js b/src/modules/chat/emotes.js index 55a92eaa..70c1f4ae 100644 --- a/src/modules/chat/emotes.js +++ b/src/modules/chat/emotes.js @@ -228,7 +228,7 @@ export default class Emotes extends Module { } getHidden(source) { - return this.settings.provider.get(`hidden-emotes.${source}`, []); + return this.settings.provider.get(`hidden-emotes.${source}`) || []; } setHidden(source, list) { diff --git a/src/modules/chat/tokenizers.jsx b/src/modules/chat/tokenizers.jsx index 9d029eb1..c5774476 100644 --- a/src/modules/chat/tokenizers.jsx +++ b/src/modules/chat/tokenizers.jsx @@ -1169,7 +1169,7 @@ export const AddonEmotes = { }); } else if ( type === EmoteTypes.Prime || type === EmoteTypes.Turbo ) - source = this.i18n.t('emote.prime', 'Twitch Prime'); + source = this.i18n.t('emote.prime', 'Prime Gaming'); else if ( type === EmoteTypes.TwoFactor ) source = this.i18n.t('emote.2fa', 'Twitch 2FA Emote'); diff --git a/src/sites/twitch-twilight/modules/channel.jsx b/src/sites/twitch-twilight/modules/channel.jsx index 2c32066f..aad732ea 100644 --- a/src/sites/twitch-twilight/modules/channel.jsx +++ b/src/sites/twitch-twilight/modules/channel.jsx @@ -181,6 +181,8 @@ export default class Channel extends Module { if ( inst ) this.updatePanelTips(inst); } + + return; } const el = this.fine.getChildNode(inst); @@ -337,6 +339,11 @@ export default class Channel extends Module { isWatchParty: watching }); + if ( ! el._ffz_cont || ! props?.channelID ) { + this.updateSubscription(null); + return; + } + if ( el._ffz_links && props.channelLogin !== el._ffz_link_login ) { const login = el._ffz_link_login = props.channelLogin; if ( login ) { @@ -369,11 +376,6 @@ export default class Channel extends Module { el._ffz_links.innerHTML = ''; } - if ( ! el._ffz_cont || ! props?.channelID ) { - this.updateSubscription(null); - return; - } - // TODO: See if we can read this data directly from Apollo's cache. // Also, test how it works with videos and clips. /*const raw_game = el.querySelector('a[data-a-target="stream-game-link"]')?.textContent; diff --git a/src/sites/twitch-twilight/modules/chat/emote_menu.jsx b/src/sites/twitch-twilight/modules/chat/emote_menu.jsx index 8f052fa7..55d409ac 100644 --- a/src/sites/twitch-twilight/modules/chat/emote_menu.jsx +++ b/src/sites/twitch-twilight/modules/chat/emote_menu.jsx @@ -675,7 +675,7 @@ export default class EmoteMenu extends Module { if ( data.prime ) calendar = { icon: 'crown', - message: t.i18n.t('emote-menu.sub-prime', 'This is your free sub with Twitch Prime.\nIt ends in {seconds,humantime}.', {seconds: ends}) + message: t.i18n.t('emote-menu.sub-prime', 'This is your free sub with Prime Gaming.\nIt ends in {seconds,humantime}.', {seconds: ends}) } else if ( data.gift ) calendar = { @@ -1263,7 +1263,7 @@ export default class EmoteMenu extends Module { return out; const filtering = input && input.length > 0 && input !== ':', - hidden_sets = storage.get('emote-menu.hidden-sets', []); + hidden_sets = storage.get('emote-menu.hidden-sets') || []; for(const emote_set of sets) { if ( ! visibility_control && hidden_sets.includes(emote_set.key) ) diff --git a/src/sites/twitch-twilight/modules/chat/line.js b/src/sites/twitch-twilight/modules/chat/line.js index c8d160ac..bc0ccc87 100644 --- a/src/sites/twitch-twilight/modules/chat/line.js +++ b/src/sites/twitch-twilight/modules/chat/line.js @@ -731,7 +731,7 @@ other {# messages were deleted by a moderator.} className: 'tw-c-text-base tw-strong' }, user.displayName)), plan: plan.prime ? - t.i18n.t('chat.sub.twitch-prime', 'with Twitch Prime') : + t.i18n.t('chat.sub.twitch-prime', 'with Prime Gaming') : t.i18n.t('chat.sub.plan', 'at Tier {tier}', {tier}) }); diff --git a/src/sites/twitch-twilight/modules/chat/settings_menu.jsx b/src/sites/twitch-twilight/modules/chat/settings_menu.jsx index 3ff318b1..90fd5605 100644 --- a/src/sites/twitch-twilight/modules/chat/settings_menu.jsx +++ b/src/sites/twitch-twilight/modules/chat/settings_menu.jsx @@ -6,8 +6,7 @@ import Twilight from 'site'; import Module from 'utilities/module'; - -import { has } from 'utilities/object'; +import {createElement} from 'utilities/dom'; export default class SettingsMenu extends Module { constructor(...args) { @@ -16,6 +15,7 @@ export default class SettingsMenu extends Module { this.inject('settings'); this.inject('i18n'); this.inject('chat'); + this.inject('chat.badges'); this.inject('site.fine'); this.inject('site.web_munch'); @@ -24,12 +24,6 @@ export default class SettingsMenu extends Module { n => n.renderUniversalOptions && n.onBadgesChanged, Twilight.CHAT_ROUTES ); - - /*this.ModSettingsMenu = this.fine.define( - 'chat-mod-settings', - n => n.renderModerationSettingsLink && n.onChatClear, - Twilight.CHAT_ROUTES - );*/ } async onEnable() { @@ -171,35 +165,159 @@ export default class SettingsMenu extends Module { this.SettingsMenu.forceUpdate(); }); - /*this.ModSettingsMenu.ready(cls => { - const old_render = cls.prototype.render; - - cls.prototype.render = function() { - const out = old_render.call(this), - children = out?.props?.children?.[0]?.props?.children; - - if ( Array.isArray(children) ) { - let i = children.length; - while(i--) { - const thing = children[i]; - if ( thing && thing.props && has(thing.props, 'chatPauseSetting') ) { - children.splice(i, 1); - break; - } - } - } - - return out; - } - - this.ModSettingsMenu.forceUpdate(); - })*/ + this.SettingsMenu.on('update', this.updateSettingsMenu, this); this.SettingsMenu.on('unmount', inst => { inst.ffzSettingsClick = null; }); } + updateSettingsMenu(inst) { + const el = this.fine.getChildNode(inst); + if ( ! el || ! document.contains(el) ) + return; + + let cont; + + if ( inst.props?.editAppearance ) { + this.registerBadgePicker(); + const name = el.querySelector('span[data-a-target="edit-display-name"]'); + cont = name && name.closest('.tw-mg-t-1'); + + } else + cont = el.querySelector('.name-display > div:not([data-test-selector="edit-appearance-button"])'); + + if ( ! cont || ! el.contains(cont) ) + return; + + const user = inst.props?.data?.currentUser, + raw_badges = inst.props?.data?.user?.self?.displayBadges; + + if ( ! user || ! user.login || ! Array.isArray(raw_badges) ) + return; + + const is_intl = user.login && user.displayName && user.displayName.trim().toLowerCase() !== user.login, + color = this.parent.colors.process(user.chatColor), + badges = {}; + + for(const child of cont.children) { + if ( ! child?.classList ) + continue; + if ( child.classList.contains('ffz--editor-name') ) + child.remove(); + else + child.classList.add('tw-hide'); + } + + for(const badge of raw_badges) { + if ( badge?.setID && badge.version ) + badges[badge.setID] = badge.version; + } + + cont.appendChild(createElement('span', {className: 'ffz--editor-name'}, [ + createElement('span', { + className: 'ffz--editor-badges', + 'data-room-id': inst.props.channelID, + 'data-room-login': inst.props.channelLogin + }, this.badges.render({ + user, + badges, + roomID: inst.props.channelID, + roomLogin: inst.props.channelLogin + }, createElement, true)), + + + {user.displayName || user.login} + {is_intl && ({user.login})} + + ])); + } + + + registerBadgePicker() { + if ( this.BadgePicker ) + return; + + this.BadgePicker = this.fine.define( + 'badge-picker', + n => n.onGlobalBadgeClicked && n.onGlobalBadgeKeyPress, + Twilight.CHAT_ROUTES + ); + + this.BadgePicker.on('mount', this.updateBadgePicker, this); + this.BadgePicker.on('update', this.updateBadgePicker, this); + + this.BadgePicker.ready((cls, instances) => { + for(const inst of instances) + this.updateBadgePicker(inst); + }); + } + + updateBadgePicker(inst) { + const el = this.fine.getChildNode(inst); + if ( ! el ) { + // This element doesn't populate right away. React is weird. + if ( inst.props?.data?.loading === false && ! inst._ffz_try_again ) + inst._ffz_try_again = requestAnimationFrame(() => + this.updateBadgePicker(inst)); + + return; + } + + const cont = el.querySelector('.tw-flex-column'); + if ( ! cont ) + return; + + const badges = this.badges.getBadges(inst.props.userID, inst.props.userLogin); + let badge; + + for(const b of badges) { + const bd = this.badges.badges[b?.id]; + if ( bd && ! bd.addon ) + badge = bd; + } + + const ffz = cont.querySelector('.ffz--badge-selector'); + if ( ffz ) { + if ( ! badge ) + ffz.remove(); + return; + + } else if ( ! badge ) + return; + + cont.appendChild(
+

+ {this.i18n.tList('chat.ffz-badge.about', '{title}: This badge appears globally for users with FrankerFaceZ.', { + title: {this.i18n.t('chat.ffz-badge.title', 'FrankerFaceZ Badge')} + })}{' '} + {this.i18n.tList('chat.ffz-badge.site', 'Please visit the {website} to change this badge.', { + website: ( + {this.i18n.t('chat.ffz-badge.site-link', 'FrankerFaceZ website')} + ) + })} +

+
+
+
+
+ {badge.title} +
+
+
+
+
); + } + + closeMenu(inst) { const super_parent = this.fine.searchParent(inst, n => n.setChatInputRef && n.setAutocompleteInputRef, 100), parent = super_parent && this.fine.searchTree(super_parent, n => n.props && n.props.isSettingsOpen && n.onClickSettings);