diff --git a/package.json b/package.json index 2fea4816..9a5103b5 100755 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "frankerfacez", "author": "Dan Salvato LLC", - "version": "4.62.2", + "version": "4.63.0", "description": "FrankerFaceZ is a Twitch enhancement suite.", "private": true, "license": "Apache-2.0", diff --git a/src/experiments.json b/src/experiments.json index 5ceea43e..836acc1d 100644 --- a/src/experiments.json +++ b/src/experiments.json @@ -20,8 +20,8 @@ "name": "MQTT-Based PubSub", "description": "An experimental new pubsub system that should be more reliable than the existing socket cluster.", "groups": [ - {"value": true, "weight": 5}, - {"value": false, "weight": 95} + {"value": true, "weight": 0}, + {"value": false, "weight": 100} ] } -} \ No newline at end of file +} diff --git a/src/experiments.ts b/src/experiments.ts index e1f36069..98587996 100644 --- a/src/experiments.ts +++ b/src/experiments.ts @@ -4,7 +4,7 @@ // Experiments // ============================================================================ -import {DEBUG, SERVER} from 'utilities/constants'; +import {DEBUG, SERVER, SERVER_OR_EXT} from 'utilities/constants'; import Module, { GenericModule } from 'utilities/module'; import {has, deep_copy, fetchJSON} from 'utilities/object'; import { getBuster } from 'utilities/time'; @@ -465,11 +465,43 @@ export default class ExperimentManager extends Module<'experiments', ExperimentE } + private _checkExternalAccess() { + let stack; + try { + stack = new Error().stack; + } catch(err) { + /* :thinking: */ + try { + stack = err.stack; + } catch(err_again) { /* aww */ } + } + + if ( ! stack ) + return; + + stack = stack.split(/\s*\n+\s*/g).filter(x => x.startsWith('at ')); + + let external = false; + + for(const line of stack) { + if ( ! line.includes(SERVER_OR_EXT) ) { + external = true; + break; + } + } + + if ( external ) + this.log.warn('Detected access by external script.'); + } + + setTwitchOverride(key: string, value: string) { const overrides = this._getOverrideCookie(), experiments = overrides.experiments, disabled = overrides.disabled; + this._checkExternalAccess(); + experiments[key] = value; const idx = disabled.indexOf(key); @@ -489,6 +521,8 @@ export default class ExperimentManager extends Module<'experiments', ExperimentE const overrides = this._getOverrideCookie(), experiments = overrides.experiments; + this._checkExternalAccess(); + if ( ! has(experiments, key) ) return; diff --git a/src/sites/clips/index.jsx b/src/sites/clips/index.jsx index 4f9eb2d7..4c949906 100644 --- a/src/sites/clips/index.jsx +++ b/src/sites/clips/index.jsx @@ -125,6 +125,11 @@ export default class ClipsSite extends BaseSite { route_data: this.router.match }); + // We need the default to be defined to get the correct value. + this.settings.add('clips.layout.big', { + default: false, + }); + this.settings.getChanges('channel.hide-unfollow', val => this.css_tweaks.toggleHide('unfollow-button', val)); diff --git a/src/sites/shared/player.jsx b/src/sites/shared/player.jsx index 15dce848..1e6da86e 100644 --- a/src/sites/shared/player.jsx +++ b/src/sites/shared/player.jsx @@ -526,7 +526,7 @@ export default class PlayerBase extends Module { ui: { path: 'Player > General >> Extensions', title: 'Show Overlay Extensions', - description: 'Note: This feature does not prevent extensions from loading. Hidden extensions are merely invisible. Hiding extensions with this feature will not improve your security.', + description: '**Note**: This feature does not prevent extensions from loading. Hidden extensions are merely invisible. Hiding extensions with this feature will not improve your security. To prevent extensions from loading entirely, we recommend using the [Disable Twitch Extensions browser extension](https://twitch-tools.rootonline.de/disable_twitch_extensions.php) by CommanderRoot.', component: 'setting-select-box', data: [ {value: 2, title: 'Never'}, @@ -2327,4 +2327,4 @@ export default class PlayerBase extends Module { return null; } -} \ No newline at end of file +} diff --git a/src/sites/twitch-twilight/modules/chat/input.jsx b/src/sites/twitch-twilight/modules/chat/input.jsx index 48643dfb..5bf9a93c 100644 --- a/src/sites/twitch-twilight/modules/chat/input.jsx +++ b/src/sites/twitch-twilight/modules/chat/input.jsx @@ -725,7 +725,7 @@ export default class Input extends Module { if ( inst.props.isShowingEmotePicker ) inst.props.closeEmotePicker(); else if ( inst.props.tray && (! inst.state.value || ! inst.state.value.length) ) - inst.closeTray(); + inst.props.clearTray(); } } catch(err) { diff --git a/src/sites/twitch-twilight/modules/chat/scroller.js b/src/sites/twitch-twilight/modules/chat/scroller.js index c9dbfaa7..5b842fe2 100644 --- a/src/sites/twitch-twilight/modules/chat/scroller.js +++ b/src/sites/twitch-twilight/modules/chat/scroller.js @@ -582,8 +582,10 @@ export default class Scroller extends Module { // Pause Stuff cls.prototype.ffzShouldBePaused = function(since) { + // We may not have moved the mouse. If that's the case, + // since should be zero. if ( since == null ) - since = Date.now() - this.ffz_last_move; + since = Date.now() - (this.ffz_last_move ?? Date.now()); if ( this.state.ffz_scrolled_up ) return true; diff --git a/src/sites/twitch-twilight/modules/directory/index.jsx b/src/sites/twitch-twilight/modules/directory/index.jsx index ec44bb07..10858863 100644 --- a/src/sites/twitch-twilight/modules/directory/index.jsx +++ b/src/sites/twitch-twilight/modules/directory/index.jsx @@ -24,7 +24,7 @@ export const CONTENT_FLAGS = [ 'MatureGame', 'ProfanityVulgarity', 'SexualThemes', - 'ViolentGrpahic' + 'ViolentGraphic' ]; function formatTerms(data, flags) { @@ -61,7 +61,8 @@ export default class Directory extends Module { this.inject(Game); this.DirectoryCard = this.elemental.define( - 'directory-card', 'article[data-a-target^="followed-vod-"],article[data-a-target^="card-"],div[data-a-target^="video-tower-card-"] article,div[data-a-target^="clips-card-"] article,.shelf-card__impression-wrapper article,.tw-tower div article', + 'directory-card', + 'article[data-a-target^="video-carousel-card-"],article[data-a-target^="followed-vod-"],article[data-a-target^="card-"],div[data-a-target^="video-tower-card-"] article,div[data-a-target^="clips-card-"] article,.shelf-card__impression-wrapper article,.tw-tower div article', DIR_ROUTES, null, 0, 0 ); @@ -138,6 +139,17 @@ export default class Directory extends Module { changed: () => this.updateCards() }); + this.settings.add('directory.show-flags', { + default: false, + + ui: { + path: 'Directory > Channels >> Appearance', + title: 'Display Content Flags on channel cards.', + component: 'setting-check-box' + }, + + changed: () => this.updateCards() + }); /*this.settings.add('directory.show-channel-avatars', { default: true, @@ -343,9 +355,13 @@ export default class Directory extends Module { always_inherit: true, process(ctx, val) { const out = new Set; - for(const v of val) - if ( v?.v ) - out.add(v.v); + for(const v of val) { + let item = v?.v; + if ( item === 'ViolentGrpahic') + item = 'ViolentGraphic'; + if ( item ) + out.add(item); + } return out; }, @@ -588,16 +604,34 @@ export default class Directory extends Module { tags = props.tagListProps?.freeformTags; const need_flags = this.settings.get('directory.wait-flags'), + show_flags = this.settings.get('directory.show-flags'), blur_flags = this.settings.get('directory.blur-flags', []), block_flags = this.settings.get('directory.block-flags', []), - has_flags = blur_flags.size > 0 || block_flags.size > 0; + filter_flags = blur_flags.size > 0 || block_flags.size > 0, + has_flags = show_flags || filter_flags; if ( el._ffz_flags === undefined && has_flags ) { el._ffz_flags = null; - this.twitch_data.getStreamFlags(null, props.channelLogin).then(data => { - el._ffz_flags = data ?? []; - this.updateCard(el); - }); + + // Are we getting a clip, a video, or a stream? + if ( props.slug ) { + // Clip + console.log('need flags for clip', props.slug); + el._ffz_flags = []; + + } else if ( props.vodID ) { + // Video + console.log('need flags for vod', props.vodID); + el._ffz_flags = []; + + } else { + // Stream? + console.log('need flags for stream', props.channelLogin); + this.twitch_data.getStreamFlags(null, props.channelLogin).then(data => { + el._ffz_flags = data ?? []; + this.updateCard(el); + }); + } } let bad_tag = false, @@ -623,7 +657,7 @@ export default class Directory extends Module { } let should_blur = blur_tag; - if ( need_flags && has_flags && el._ffz_flags == null ) + if ( need_flags && filter_flags && el._ffz_flags == null ) should_blur = true; if ( ! should_blur ) should_blur = this.settings.provider.get('directory.game.hidden-thumbnails', []).includes(game); @@ -682,8 +716,41 @@ export default class Directory extends Module { hide_container.classList.toggle('tw-hide', should_hide); this.updateUptime(el, props); + this.updateFlags(el); } + updateFlags(el) { + if ( ! document.contains(el) ) + return this.clearFlags(el); + + const setting = this.settings.get('directory.show-flags'); + + if ( ! setting || ! el._ffz_flags?.length ) + return this.clearFlags(el); + + const container = this._getTopRightContainer(el); + if ( ! container ) + return this.clearFlags(el); + + if ( ! el.ffz_flags_el ) + container.appendChild(el.ffz_flags_el = (
+
+
+
+ {el.ffz_flags_tt =
} +
)); + + el.ffz_flags_tt.textContent = this.i18n.t('metadata.flags.tooltip', 'Intended for certain audiences. May contain:') + + '\n\n' + + el._ffz_flags.map(x => x.localizedName).join('\n'); + } + + clearFlags(el) { + if ( el.ffz_flags_el ) { + el.ffz_flags_el.remove(); + el.ffz_flags_tt = null; + } + } updateCards() { this.DirectoryCard.each(el => this.updateCard(el)); @@ -695,6 +762,32 @@ export default class Directory extends Module { clearCard(el) { this.clearUptime(el); + this.clearFlags(el); + + const cont = this._getTopRightContainer(el, false); + if ( cont ) + cont.remove(); + + el._ffz_top_right = null; + } + + _getTopRightContainer(el, should_create = true) { + let cont = el._ffz_top_right ?? el.querySelector('.ffz-top-right'); + if ( cont || ! should_create ) + return cont; + + const container = el.querySelector('a[data-a-target="preview-card-image-link"] > div'); + if ( ! container ) + return null; + + cont = (
); + el._ffz_top_right = cont; + + container.appendChild(cont); + return cont; } @@ -702,9 +795,12 @@ export default class Directory extends Module { if ( ! document.contains(el) ) return this.clearUptime(el); - const container = el.querySelector('a[data-a-target="preview-card-image-link"] > div'), + const container = this._getTopRightContainer(el), setting = this.settings.get('directory.uptime'); + //const container = el.querySelector('a[data-a-target="preview-card-image-link"] > div'), + // setting = this.settings.get('directory.uptime'); + if ( ! container || setting === 0 || props.viewCount || props.animatedImageProps ) return this.clearUptime(el); @@ -733,8 +829,8 @@ export default class Directory extends Module { if ( ! el.ffz_uptime_el ) { el.ffz_uptime_el = container.querySelector('.ffz-uptime-element'); if ( ! el.ffz_uptime_el ) - container.appendChild(el.ffz_uptime_el = (
-
+ container.appendChild(el.ffz_uptime_el = ( +
@@ -746,7 +842,7 @@ export default class Directory extends Module { {el.ffz_uptime_tt =
}
-
)); + )); } if ( ! el.ffz_uptime_span ) diff --git a/src/sites/twitch-twilight/styles/directory.scss b/src/sites/twitch-twilight/styles/directory.scss index f651aaab..6691f2cb 100644 --- a/src/sites/twitch-twilight/styles/directory.scss +++ b/src/sites/twitch-twilight/styles/directory.scss @@ -5,6 +5,7 @@ } .ffz-channel-avatar, +.ffz-flags-element, .ffz-uptime-element { pointer-events: all; } @@ -36,6 +37,9 @@ } } +.ffz-uptime-element { order: 0; } +.ffz-flags-element { order: 1; } + .ffz-host-menu { .scrollable-area { @@ -54,4 +58,4 @@ height: 3rem; } } -} \ No newline at end of file +} diff --git a/src/utilities/color.ts b/src/utilities/color.ts index 55d29f61..adf89ec4 100644 --- a/src/utilities/color.ts +++ b/src/utilities/color.ts @@ -820,6 +820,9 @@ export class ColorAdjuster { } process(color: BaseColor | string, throw_errors = false) { + if ( ! color ) + return null; + if ( this._mode === -1 ) return ''; @@ -829,9 +832,6 @@ export class ColorAdjuster { if ( this._mode === 0 ) return color; - if ( ! color ) - return null; - if ( this._cache.has(color) ) return this._cache.get(color); diff --git a/src/utilities/dom.ts b/src/utilities/dom.ts index d60faff7..d1abb9b1 100644 --- a/src/utilities/dom.ts +++ b/src/utilities/dom.ts @@ -177,18 +177,23 @@ export function createElement(tag: string, props?: any, ...children: DomFragment if ( lk === 'style' ) { if ( typeof prop === 'string' ) el.style.cssText = prop; - else + else if ( prop && typeof prop === 'object' ) for(const [key, val] of Object.entries(prop)) { if ( has(el.style, key) || has(Object.getPrototypeOf(el.style), key) ) (el.style as any)[key] = val; else el.style.setProperty(key, prop[key]); } + else + console.warn('unsupported style value', prop); } else if ( lk === 'dataset' ) { - for(const k in prop) - if ( has(prop, k) ) - el.dataset[camelCase(k)] = prop[k]; + if ( prop && typeof prop === 'object' ) { + for(const k in prop) + if ( has(prop, k) ) + el.dataset[camelCase(k)] = prop[k]; + } else + console.warn('unsupported dataset value', prop); } else if ( key === 'dangerouslySetInnerHTML' ) { // React compatibility is cool. SeemsGood diff --git a/src/utilities/object.ts b/src/utilities/object.ts index 13abb2c6..a7b4fe6f 100644 --- a/src/utilities/object.ts +++ b/src/utilities/object.ts @@ -779,6 +779,12 @@ export function deep_copy(object: T, seen?: Set): T { if ( typeof object === 'function' ) return function(this: ThisParameterType, ...args: any[]) { return object.apply(this, args); } as T // eslint-disable-line no-invalid-this + if ( object instanceof RegExp ) + return new RegExp(object.source, object.flags) as T; + + if ( object instanceof Date ) + return new Date(object) as T; + if ( typeof object !== 'object' ) return object as T; @@ -793,6 +799,30 @@ export function deep_copy(object: T, seen?: Set): T { if ( Array.isArray(object) ) return object.map(x => deep_copy(x, new Set(seen))) as T; + if ( object instanceof Set ) { + const out = new Set(); + for(const item of object) { + if ( typeof item === 'object' ) + out.add(deep_copy(item)); + else + out.add(item); + } + + return out as T; + } + + if ( object instanceof Map ) { + const out = new Map(); + for(const [key, val] of object.entries()) { + let k = typeof key === 'object' ? deep_copy(key) : key, + v = typeof val === 'object' ? deep_copy(val) : val; + + out.set(k, v); + } + + return out as T; + } + const out: any = {}; for(const [key, val] of Object.entries(object)) { if ( typeof val === 'object' ) diff --git a/src/utilities/twitch-data.ts b/src/utilities/twitch-data.ts index 29cdc0a2..8f6b0f0a 100644 --- a/src/utilities/twitch-data.ts +++ b/src/utilities/twitch-data.ts @@ -914,11 +914,11 @@ export default class TwitchData extends Module { f('id and login cannot both be null'); if ( ! this._loading_flags ) - this._loadFlags(); + this._loadStreamFlags(); }) } - async _loadFlags() { + async _loadStreamFlags() { if ( this._loading_flags ) return; @@ -1015,7 +1015,7 @@ export default class TwitchData extends Module { this._loading_flags = false; if ( this._waiting_flag_ids.size || this._waiting_flag_logins.size ) - this._loadFlags(); + this._loadStreamFlags(); } diff --git a/styles/native/tooltip.scss b/styles/native/tooltip.scss index c3f3188f..dcdb22c4 100644 --- a/styles/native/tooltip.scss +++ b/styles/native/tooltip.scss @@ -46,6 +46,10 @@ } &--wrap { white-space: normal; } + &--pre { white-space: pre; } + &--prewrap { white-space: pre-wrap; } + &--20 { width: 20rem; } + &--30 { width: 30rem; } &--left { left: auto; @@ -142,4 +146,4 @@ top: -3px; } } -} \ No newline at end of file +}