diff --git a/package.json b/package.json index 543e9ade..e5993f9c 100755 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "frankerfacez", "author": "Dan Salvato LLC", - "version": "4.20.1", + "version": "4.20.2", "description": "FrankerFaceZ is a Twitch enhancement suite.", "license": "Apache-2.0", "scripts": { diff --git a/src/experiments.js b/src/experiments.js index 282807b4..6f120f47 100644 --- a/src/experiments.js +++ b/src/experiments.js @@ -60,6 +60,9 @@ export default class ExperimentManager extends Module { return values; }, + is_locked: () => this.getControlsLocked(), + unlock: () => this.unlockControls(), + unique_id: () => this.unique_id, ffz_data: () => deep_copy(this.experiments), @@ -88,6 +91,21 @@ export default class ExperimentManager extends Module { this.cache = new Map; } + getControlsLocked() { + if ( DEBUG ) + return false; + + const ts = this.settings.provider.get('exp-lock', 0); + if ( isNaN(ts) || ! isFinite(ts) ) + return true; + + return Date.now() - ts >= 86400000; + } + + unlockControls() { + this.settings.provider.set('exp-lock', Date.now()); + } + async onLoad() { await this.loadExperiments(); } diff --git a/src/experiments.json b/src/experiments.json index 257f9b09..8dea498d 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": 25}, - {"value": false, "weight": 75} + {"value": true, "weight": 0}, + {"value": false, "weight": 100} ] } } \ No newline at end of file diff --git a/src/modules/main_menu/components/experiments.vue b/src/modules/main_menu/components/experiments.vue index 7a278460..bdcc3ddc 100644 --- a/src/modules/main_menu/components/experiments.vue +++ b/src/modules/main_menu/components/experiments.vue @@ -4,155 +4,211 @@ {{ t('setting.experiments.about', 'This feature allows you to override experiment values. Please note that, for most experiments, you may have to refresh the page for your changes to take effect.') }} -
-
- {{ t('setting.experiments.unique-id', 'Unique ID: {id}', {id: unique_id}) }} +
+
+

+ {{ t('setting.dev-warning', "It's dangerous to go at all.") }} +

+
- -
-

- {{ t('setting.experiments.ffz', 'FrankerFaceZ Experiments') }} -

- -
-
-
-
-

{{ exp.name }}

-
- {{ exp.description }} -
-
- -
- - - -
-
-
-
- {{ t('setting.experiments.none', 'There are no current experiments.') }} -
-
- {{ t('setting.experiments.none-filter', 'There are no matching experiments.') }} -
-
- -

- {{ t('setting.experiments.twitch', 'Twitch Experiments') }} -

- -
-
-
+ -
-
-
- {{ t('setting.experiments.active', 'This experiment is active.') }} -
-
-
-
- {{ t('setting.experiments.inactive', 'This experiment is not active.') }} -
-
-
+
+
-
-

{{ exp.name }}

-
- {{ exp.remainder }} -
-
- -
- - - -
+
+
+
+ {{ t('setting.experiments.unique-id', 'Unique ID: {id}', {id: unique_id}) }}
-
-
- {{ t('setting.experiments.none', 'There are no current experiments.') }} +
-
- {{ t('setting.experiments.none-filter', 'There are no matching experiments.') }} +
+
+
+ + + +
-
+ +

+ + {{ t('setting.experiments.ffz', 'FrankerFaceZ Experiments') }} + + + {{ t('setting.experiments.visible', '(Showing {visible,number} of {total,number})', { + visible: visible_ffz.length, + total: sorted_ffz.length + }) }} + +

+ +
+
+
+
+

{{ exp.name }}

+
+ {{ exp.description }} +
+
+ +
+ + + +
+
+
+
+ {{ t('setting.experiments.none', 'There are no current experiments.') }} +
+
+ {{ t('setting.experiments.none-filter', 'There are no matching experiments.') }} +
+
+ +

+ + {{ t('setting.experiments.twitch', 'Twitch Experiments') }} + + + {{ t('setting.experiments.visible', '(Showing {visible,number} of {total,number})', { + visible: visible_twitch.length, + total: sorted_twitch.length + }) }} + +

+ +
+
+
+
+
+
+ {{ t('setting.experiments.active', 'This experiment is active.') }} +
+
+
+
+ {{ t('setting.experiments.inactive', 'This experiment is not active.') }} +
+
+
+ +
+

{{ exp.name }}

+
+ {{ exp.remainder }} +
+
+ +
+ + + +
+
+
+
+ {{ t('setting.experiments.none', 'There are no current experiments.') }} +
+
+ {{ t('setting.experiments.none-filter', 'There are no matching experiments.') }} +
+
+
@@ -173,7 +229,9 @@ export default { data() { return { + experiments_locked: this.item.is_locked(), sort_by: 1, + unused: false, unique_id: this.item.unique_id(), ffz_data: this.item.ffz_data(), twitch_data: this.item.twitch_data() @@ -242,6 +300,14 @@ export default { }, methods: { + enterCode() { + if ( this.$refs.code.value !== 'sv_cheats 1' ) + return; + + this.experiments_locked = false; + this.item.unlock(); + }, + calculateRarity(exp) { let rarity; for(const group of exp.groups) @@ -254,7 +320,15 @@ export default { }, sorted(data) { - const out = Object.entries(data).map(x => ({key: x[0], exp: x[1]})); + const out = []; + for(const [k,v] of Object.entries(data)) { + if ( ! this.unused && v.in_use === false ) + continue; + + out.push({key: k, exp: v}); + } + + //const out = Object.entries(data).map(x => ({key: x[0], exp: x[1]})); out.sort((a,b) => { const a_use = a.exp.in_use, diff --git a/src/sites/twitch-twilight/modules/channel.js b/src/sites/twitch-twilight/modules/channel.js index 7f9a6011..cd00e39f 100644 --- a/src/sites/twitch-twilight/modules/channel.js +++ b/src/sites/twitch-twilight/modules/channel.js @@ -27,6 +27,12 @@ export default class Channel extends Module { this.inject('metadata'); this.inject('socket'); + /*this.SideNav = this.elemental.define( + 'side-nav', '.side-bar-contents .side-nav-section:first-child', + null, + {childNodes: true, subtree: true}, 1 + );*/ + this.ChannelRoot = this.elemental.define( 'channel-root', '.channel-root', USER_PAGES, @@ -43,6 +49,9 @@ export default class Channel extends Module { onEnable() { this.updateChannelColor(); + //this.SideNav.on('mount', this.updateHidden, this); + //this.SideNav.on('mutate', this.updateHidden, this); + this.ChannelRoot.on('mount', this.updateRoot, this); this.ChannelRoot.on('mutate', this.updateRoot, this); this.ChannelRoot.on('unmount', this.removeRoot, this); @@ -54,6 +63,19 @@ export default class Channel extends Module { this.InfoBar.each(el => this.updateBar(el)); } + /*updateHidden(el) { // eslint-disable-line class-methods-use-this + if ( ! el._ffz_raf ) + el._ffz_raf = requestAnimationFrame(() => { + el._ffz_raf = null; + const nodes = el.querySelectorAll('.side-nav-card__avatar--offline'); + for(const node of nodes) { + const par = node.closest('.tw-transition'); + if ( par && el.contains(par) ) + par.classList.add('tw-hide'); + } + }); + }*/ + updateSubscription(login) { if ( this._subbed_login === login ) return; diff --git a/src/sites/twitch-twilight/modules/chat/emote_menu.jsx b/src/sites/twitch-twilight/modules/chat/emote_menu.jsx index 6f7fe564..caba4cce 100644 --- a/src/sites/twitch-twilight/modules/chat/emote_menu.jsx +++ b/src/sites/twitch-twilight/modules/chat/emote_menu.jsx @@ -126,6 +126,16 @@ export default class EmoteMenu extends Module { this.SUB_STATUS = SUB_STATUS; + this.settings.add('chat.emote-menu.shortcut', { + default: false, + ui: { + path: 'Chat > Emote Menu >> General', + title: 'Use Ctrl+E to open the Emote Menu.', + description: 'When enabled and you press Ctrl+E with the chat input focused, the emote menu will open.', + component: 'setting-check-box' + } + }); + this.settings.add('chat.emote-menu.enabled', { default: true, ui: { @@ -568,6 +578,7 @@ export default class EmoteMenu extends Module { } storage.set('emote-menu.hidden-sets', hidden); + t.emit('chat.emotes:change-set-hidden', key); return; } diff --git a/src/sites/twitch-twilight/modules/chat/input.jsx b/src/sites/twitch-twilight/modules/chat/input.jsx index 3fe87aa5..cb1dbcb5 100644 --- a/src/sites/twitch-twilight/modules/chat/input.jsx +++ b/src/sites/twitch-twilight/modules/chat/input.jsx @@ -6,6 +6,8 @@ import Module from 'utilities/module'; import { findReactFragment } from 'utilities/dom'; +import { TWITCH_POINTS_SETS, TWITCH_GLOBAL_SETS, TWITCH_PRIME_SETS, KNOWN_CODES, REPLACEMENTS, REPLACEMENT_BASE, TWITCH_EMOTE_BASE } from 'utilities/constants'; + import Twilight from 'site'; export default class Input extends Module { @@ -223,9 +225,23 @@ export default class Input extends Module { this.EmoteSuggestions.on('mount', this.overrideEmoteMatcher, this); this.MentionSuggestions.on('mount', this.overrideMentionMatcher, this); + this.on('chat.emotes:change-hidden', this.uncacheTabCompletion, this); + this.on('chat.emotes:change-set-hidden', this.uncacheTabCompletion, this); + this.on('chat.emotes:change-favorite', this.uncacheTabCompletion, this); + this.on('chat.emotes:update-default-sets', this.uncacheTabCompletion, this); + this.on('chat.emotes:update-user-sets', this.uncacheTabCompletion, this); + this.on('chat.emotes:update-room-sets', this.uncacheTabCompletion, this); + this.on('site.css_tweaks:update-chat-css', this.resizeInput, this); } + uncacheTabCompletion() { + for(const inst of this.EmoteSuggestions.instances) { + inst.ffz_ffz_cache = null; + inst.ffz_twitch_cache = null; + } + } + updateInput() { for(const inst of this.ChatInput.instances) { if ( inst ) { @@ -295,6 +311,12 @@ export default class Input extends Module { inst.onKeyDown = function(event) { try { + if ( inst.onEmotePickerToggle && t.chat.context.get('chat.emote-menu.shortcut') && event.key === 'e' && event.ctrlKey && ! event.altKey && ! event.shiftKey ) { + inst.onEmotePickerToggle(); + event.preventDefault(); + return; + } + if ( inst.autocompleteInputRef && t.chat.context.get('chat.mru.enabled') && ! event.shiftKey && ! event.ctrlKey && ! event.altKey ) { const code = event.charCode || event.keyCode; @@ -471,54 +493,114 @@ export default class Input extends Module { } - // eslint-disable-next-line class-methods-use-this - getTwitchEmoteSuggestions(input, inst) { - const hydratedEmotes = inst.hydrateEmotes(inst.props.emotes); - if (!Array.isArray(hydratedEmotes)) { - return []; - } + buildTwitchCache(emotes) { + if ( ! Array.isArray(emotes) ) + return {emotes: [], length: 0}; - const usageResults = [], - startingResults = [], - otherResults = [], - favorites = this.emotes.getFavorites('twitch'), - hidden = this.emotes.getHidden('twitch'), - search = input.startsWith(':') ? input.slice(1) : input; + const out = [], + hidden_sets = this.settings.provider.get('emote-menu.hidden-sets'), + has_hidden = Array.isArray(hidden_sets) && hidden_sets.length > 0, + hidden_emotes = this.emotes.getHidden('twitch'), + favorites = this.emotes.getFavorites('twitch'); - for (const set of hydratedEmotes) { - if (set && Array.isArray(set.emotes)) { - for (const emote of set.emotes) { - if (inst.doesEmoteMatchTerm(emote, search) && ! hidden.includes(emote.id)) { - const favorite = favorites.includes(emote.id); - const element = { - current: input, - replacement: emote.token, - element: inst.renderEmoteSuggestion({ - ...emote, - favorite - }), - favorite - }; + for(const set of emotes) { + if ( has_hidden ) { + const int_id = parseInt(set.id, 10), + owner = set.owner, + is_points = TWITCH_POINTS_SETS.includes(int_id) || owner?.login === 'channel_points', + channel = is_points ? null : owner; - if (this.EmoteUsageCount[emote.token]) { - usageResults.push(element); - } - else if (emote.token.toLowerCase().startsWith(search)) { - startingResults.push(element); - } - else { - otherResults.push(element); - } - } + let key = `twitch-set-${set.id}`; + + if ( channel?.login ) + key = `twitch-${channel.id}`; + else if ( is_points ) + key = 'twitch-points'; + else if ( TWITCH_GLOBAL_SETS.includes(int_id) ) + key = 'twitch-global'; + else if ( TWITCH_PRIME_SETS.includes(int_id) ) + key = 'twitch-prime'; + else + key = 'twitch-misc'; + + if ( hidden_sets.includes(key) ) + continue; + } + + for(const emote of set.emotes) { + if ( ! emote || ! emote.id || hidden_emotes.includes(emote.id) ) + continue; + + const id = emote.id, + replacement = REPLACEMENTS[id]; + + let src, srcSet; + + if ( replacement && this.chat.context.get('chat.fix-bad-emotes') ) { + src = `${REPLACEMENT_BASE}${replacement}`; + srcSet = `${src} 1x`; + } else { + const base = `${TWITCH_EMOTE_BASE}${id}`; + src = `${base}/1.0`; + srcSet = `${src} 1x, ${base}/2.0 2x` } + + out.push({ + id, + setID: set.id, + token: KNOWN_CODES[emote.token] || emote.token, + srcSet, + favorite: favorites.includes(id) + }); } } - usageResults.sort((a, b) => this.EmoteUsageCount[b.replacement] - this.EmoteUsageCount[a.replacement]); - startingResults.sort((a, b) => a.replacement.localeCompare(b.replacement)); - otherResults.sort((a, b) => a.replacement.localeCompare(b.replacement)); + return { + emotes: out, + length: emotes.length + } + } - return usageResults.concat(startingResults).concat(otherResults); + + getTwitchEmoteSuggestions(input, inst) { + if ( inst.ffz_twitch_cache?.length !== inst.props.emotes?.length ) + inst.ffz_twitch_cache = this.buildTwitchCache(inst.props.emotes); + + const emotes = inst.ffz_twitch_cache.emotes; + + if ( ! emotes.length ) + return []; + + const results_usage = [], + results_starting = [], + results_other = [], + + search = input.startsWith(':') ? input.slice(1) : input; + + for(const emote of emotes) { + if ( inst.doesEmoteMatchTerm(emote, search) ) { + const element = { + current: input, + replacement: emote.token, + element: inst.renderEmoteSuggestion(emote), + favorite: emote.favorite, + count: this.EmoteUsageCount[emote.token] || 0 + }; + + if ( element.count > 0 ) + results_usage.push(element); + else if ( emote.token.toLowerCase().startsWith(search) ) + results_starting.push(element); + else + results_other.push(element); + } + } + + results_usage.sort((a,b) => b.count - a.count); + results_starting.sort((a,b) => a.replacement.localeCompare(b.replacement)); + results_other.sort((a,b) => a.replacement.localeCompare(b.replacement)); + + return results_usage.concat(results_starting).concat(results_other); } @@ -564,6 +646,48 @@ export default class Input extends Module { } + buildFFZCache(user_id, user_login, channel_id, channel_login) { + const sets = this.emotes.getSets(user_id, user_login, channel_id, channel_login); + if ( ! sets || ! sets.length ) + return {emotes: [], length: 0, user_id, user_login, channel_id, channel_login}; + + const out = [], + hidden_sets = this.settings.provider.get('emote-menu.hidden-sets'), + has_hidden = Array.isArray(hidden_sets) && hidden_sets.length > 0; + + for(const set of sets) { + if ( ! set || ! set.emotes ) + continue; + + const source = set.source || 'ffz', + key = `${set.merge_source || source}-${set.merge_id || set.id}`; + + if ( has_hidden && hidden_sets.includes(key) ) + continue; + + const hidden_emotes = this.emotes.getHidden(source), + favorites = this.emotes.getFavorites(source); + + for(const emote of Object.values(set.emotes)) { + if ( ! emote || ! emote.id || hidden_emotes.includes(emote.id) ) + continue; + + out.push({ + id: `${source}-${emote.id}`, + token: emote.name, + srcSet: emote.srcSet, + favorite: favorites.includes(emote.id) + }); + } + } + + return { + emotes: out, + length: sets.length + } + } + + getEmoteSuggestions(input, inst) { const user = inst._ffz_user, channel_id = inst._ffz_channel_id, @@ -578,17 +702,38 @@ export default class Input extends Module { return []; } + let cache = inst.ffz_ffz_cache; + if ( ! cache || cache.user_id !== user?.id || cache.user_login !== user?.login || cache.channel_id !== channel_id || cache.channel_login !== channel_login ) + cache = inst.ffz_ffz_cache = this.buildFFZCache(user?.id, user?.login, channel_id, channel_login); + + const emotes = cache.emotes; + if ( ! emotes.length ) + return []; + const search = input.startsWith(':') ? input.slice(1) : input, results = [], - sets = this.emotes.getSets( - user && user.id, - user && user.login, - channel_id, - channel_login - ), added_emotes = new Set(); - for(const set of sets) { + for(const emote of emotes) { + if ( inst.doesEmoteMatchTerm(emote, search) && ! added_emotes.has(emote.name) ) { + results.push({ + current: input, + replacement: emote.token, + element: inst.renderEmoteSuggestion(emote), + favorite: emote.favorite, + count: 0 // TODO: Count stuff? + }); + } + } + + return results; + + /*for(const set of sets) { + if ( ! set || ! set.emotes ) + continue; + + const + if ( set && set.emotes ) for(const emote of Object.values(set.emotes)) if ( inst.doesEmoteMatchTerm(emote, search) && !added_emotes.has(emote.name) && ! this.emotes.isHidden(set.source || 'ffz', emote.id) ) { @@ -608,7 +753,7 @@ export default class Input extends Module { } } - return results; + return results;*/ } pasteMessage(room, message) { diff --git a/src/sites/twitch-twilight/modules/css_tweaks/styles/square-avatars.scss b/src/sites/twitch-twilight/modules/css_tweaks/styles/square-avatars.scss index dad1efee..5a6451d4 100644 --- a/src/sites/twitch-twilight/modules/css_tweaks/styles/square-avatars.scss +++ b/src/sites/twitch-twilight/modules/css_tweaks/styles/square-avatars.scss @@ -4,6 +4,7 @@ --border-radius-rounded: 0 !important; } +.user-avatar-card__halo, .player-streaminfo__picture img[src] { border-radius: 0 !important; } diff --git a/src/sites/twitch-twilight/modules/autohost_list.gql b/src/sites/twitch-twilight/modules/host_button/autohost_list.gql similarity index 100% rename from src/sites/twitch-twilight/modules/autohost_list.gql rename to src/sites/twitch-twilight/modules/host_button/autohost_list.gql diff --git a/src/sites/twitch-twilight/modules/autohost_list_mutate.gql b/src/sites/twitch-twilight/modules/host_button/autohost_list_mutate.gql similarity index 100% rename from src/sites/twitch-twilight/modules/autohost_list_mutate.gql rename to src/sites/twitch-twilight/modules/host_button/autohost_list_mutate.gql diff --git a/src/sites/twitch-twilight/modules/autohost_settings.gql b/src/sites/twitch-twilight/modules/host_button/autohost_settings.gql similarity index 100% rename from src/sites/twitch-twilight/modules/autohost_settings.gql rename to src/sites/twitch-twilight/modules/host_button/autohost_settings.gql diff --git a/src/sites/twitch-twilight/modules/autohost_settings_mutate.gql b/src/sites/twitch-twilight/modules/host_button/autohost_settings_mutate.gql similarity index 100% rename from src/sites/twitch-twilight/modules/autohost_settings_mutate.gql rename to src/sites/twitch-twilight/modules/host_button/autohost_settings_mutate.gql diff --git a/src/sites/twitch-twilight/modules/host-options.vue b/src/sites/twitch-twilight/modules/host_button/host-options.vue similarity index 86% rename from src/sites/twitch-twilight/modules/host-options.vue rename to src/sites/twitch-twilight/modules/host_button/host-options.vue index d5d40865..66349301 100644 --- a/src/sites/twitch-twilight/modules/host-options.vue +++ b/src/sites/twitch-twilight/modules/host_button/host-options.vue @@ -84,8 +84,7 @@
- {{ t('metadata.host.setting.auto-hosting.description', 'Toggle all forms of auto hosting: teammates, host list, and similar channels.') }}
- {{ t('metadata.host.setting.auto-hosting.link', 'Learn More') }} + {{ t('metadata.host.setting.auto-hosting.description', 'Automatically host channels from your host list when you\'re offline.') }}
@@ -105,16 +104,14 @@
- {{ t('metadata.host.setting.team-hosting.description', - "Automatically host random channels from your team when you're not live. " + - 'Team channels will be hosted before any channels in your host list.') }} + {{ t('metadata.host.setting.team-hosting.description', "Include team channels in your host list.") }}
- {{ t('metadata.host.setting.vodcast-hosting.description', 'Include Vodcasts in auto host.') }} - {{ t('metadata.host.setting.vodcast-hosting.link', 'Learn about Vodcasts') }} + {{ t('metadata.host.setting.vodcast-hosting.description', 'Include channels streaming pre-recorded video, like Premieres or Reruns') }}
{{ t('metadata.host.setting.strategy.description', - 'If enabled, auto-hosts will be picked at random. ' + - "Otherwise they're picked in order.") }} + 'When enabled, host channels are chosen randomly from the list.') }}
diff --git a/src/sites/twitch-twilight/modules/host_button.js b/src/sites/twitch-twilight/modules/host_button/index.js similarity index 95% rename from src/sites/twitch-twilight/modules/host_button.js rename to src/sites/twitch-twilight/modules/host_button/index.js index 982972cc..39da2bc2 100644 --- a/src/sites/twitch-twilight/modules/host_button.js +++ b/src/sites/twitch-twilight/modules/host_button/index.js @@ -235,10 +235,9 @@ export default class HostButton extends Module { else if ( setting === 'teamHost' ) setting = 'willAutohostTeam'; else if ( setting === 'strategy' ) - state = state ? 'random' : 'ordered'; + state = state ? 'RANDOM' : 'ORDERED'; else if ( setting === 'deprioritizeVodcast' ) { setting = 'willPrioritizeAutohost'; - state = ! state; } this.updateAutoHostSetting(setting, state); @@ -255,7 +254,7 @@ export default class HostButton extends Module { return; const result = await this.twitch_data.queryApollo( - await import(/* webpackChunkName: 'queries' */ './autohost_list.gql'), + await import(/* webpackChunkName: 'host-options' */ './autohost_list.gql'), { id: user.id }, @@ -273,7 +272,7 @@ export default class HostButton extends Module { return; const result = await this.twitch_data.queryApollo( - await import(/* webpackChunkName: 'queries' */ './autohost_settings.gql'), + await import(/* webpackChunkName: 'host-options' */ './autohost_settings.gql'), { id: user.id }, @@ -341,7 +340,7 @@ export default class HostButton extends Module { const autoHosts = this.getAutoHostIDs(newHosts); const result = await this.twitch_data.mutate({ - mutation: await import(/* webpackChunkName: 'queries' */ './autohost_list_mutate.gql'), + mutation: await import(/* webpackChunkName: 'host-options' */ './autohost_list_mutate.gql'), variables: { userID: user.id, channelIDs: autoHosts @@ -361,7 +360,7 @@ export default class HostButton extends Module { return; const result = await this.twitch_data.mutate({ - mutation: await import(/* webpackChunkName: 'queries' */ './autohost_settings_mutate.gql'), + mutation: await import(/* webpackChunkName: 'host-options' */ './autohost_settings_mutate.gql'), variables: { userID: user.id, [setting]: newValue diff --git a/styles/widgets.scss b/styles/widgets.scss index 5396dbab..3f8981e3 100644 --- a/styles/widgets.scss +++ b/styles/widgets.scss @@ -279,6 +279,19 @@ textarea.tw-input { } } +.ffz--experiments code { + user-select: none; + padding: 2px 5px; + border-radius: 2px; + background-color: rgba(0,0,0,0.2); + font-family: monospace; + white-space: pre; + + .tw-root--theme-dark & { + background-color:rgba(255,255,255,0.1); + } +} + .ffz--changelog, .ffz--widget { code {