diff --git a/package.json b/package.json index 778499a1..5da0ac0b 100755 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "frankerfacez", "author": "Dan Salvato LLC", - "version": "4.20.63", + "version": "4.20.64", "description": "FrankerFaceZ is a Twitch enhancement suite.", "license": "Apache-2.0", "scripts": { diff --git a/src/experiments.json b/src/experiments.json index a2d5be28..3593ec5f 100644 --- a/src/experiments.json +++ b/src/experiments.json @@ -3,8 +3,8 @@ "name": "New Link Tokenization", "description": "Update to Twitch's latest link regex. Experiment while this is checked for bugs.", "groups": [ - {"value": true, "weight": 50}, - {"value": false, "weight": 50} + {"value": true, "weight": 100}, + {"value": false, "weight": 0} ] }, "api_load": { @@ -19,8 +19,8 @@ "name": "API-Based Link Lookups", "description": "Use the new API to look up links instead of the socket cluster.", "groups": [ - {"value": true, "weight": 50}, - {"value": false, "weight": 50} + {"value": true, "weight": 30}, + {"value": false, "weight": 70} ] } } \ No newline at end of file diff --git a/src/modules/chat/emotes.js b/src/modules/chat/emotes.js index 70c1f4ae..590947a8 100644 --- a/src/modules/chat/emotes.js +++ b/src/modules/chat/emotes.js @@ -84,6 +84,16 @@ export default class Emotes extends Module { this._set_refs = {}; this._set_timers = {}; + this.settings.add('chat.emotes.2x', { + default: false, + ui: { + path: 'Chat > Appearance >> Emotes', + title: 'Larger Emotes', + description: 'This setting will make emotes appear twice as large in chat. It\'s good for use with larger fonts or just if you really like emotes.', + component: 'setting-check-box' + } + }); + this.settings.add('chat.fix-bad-emotes', { default: true, ui: { @@ -721,6 +731,14 @@ export default class Emotes extends Module { if ( emote.urls[4] ) emote.srcSet += `, ${emote.urls[4]} 4x`; + if ( emote.urls[2] ) { + emote.can_big = true; + emote.src2 = emote.urls[2]; + emote.srcSet2 = `${emote.urls[2]} 1x`; + if ( emote.urls[4] ) + emote.srcSet2 += `, ${emote.urls[4]} 2x`; + } + emote.token = { type: 'emote', id: emote.id, @@ -728,8 +746,12 @@ export default class Emotes extends Module { provider: 'ffz', src: emote.urls[1], srcSet: emote.srcSet, + can_big: !! emote.urls[2], + src2: emote.src2, + srcSet2: emote.srcSet2, text: emote.hidden ? '???' : emote.name, - length: emote.name.length + length: emote.name.length, + height: emote.height }; if ( has(MODIFIERS, emote.id) ) diff --git a/src/modules/chat/rich_providers.js b/src/modules/chat/rich_providers.js index b0138fca..678bab96 100644 --- a/src/modules/chat/rich_providers.js +++ b/src/modules/chat/rich_providers.js @@ -6,8 +6,8 @@ //const CLIP_URL = /^(?:https?:\/\/)?clips\.twitch\.tv\/(\w+)(?:\/)?(\w+)?(?:\/edit)?/; //const NEW_CLIP_URL = /^(?:https?:\/\/)?(?:www\.)?twitch\.tv\/\w+\/clip\/(\w+)/; -const CLIP_URL = /^(?:https?:\/\/)?clips\.twitch\.tv\/([a-z0-9-]+)(?:\/)?(\w+)?(?:\/edit)?/i; -const NEW_CLIP_URL = /^(?:https?:\/\/)?(?:www\.)?twitch\.tv\/\w+\/clip\/([a-z0-9-]+)/i; +const CLIP_URL = /^(?:https?:\/\/)?clips\.twitch\.tv\/([a-z0-9-_=]+)(?:\/)?(\w+)?(?:\/edit)?/i; +const NEW_CLIP_URL = /^(?:https?:\/\/)?(?:(?:www|m)\.)?twitch\.tv\/\w+\/clip\/([a-z0-9-_=]+)/i; const VIDEO_URL = /^(?:https?:\/\/)?(?:www\.)?twitch\.tv\/(?:\w+\/v|videos)\/(\w+)/; const USER_URL = /^(?:https?:\/\/)?(?:www\.)?twitch\.tv\/([^/]+)$/; diff --git a/src/modules/chat/tokenizers.jsx b/src/modules/chat/tokenizers.jsx index 4d1e8ab0..1c33084c 100644 --- a/src/modules/chat/tokenizers.jsx +++ b/src/modules/chat/tokenizers.jsx @@ -12,6 +12,7 @@ import {CATEGORIES} from './emoji'; const EMOTE_CLASS = 'chat-image chat-line__message--emote', + WHITESPACE = /^\s*$/, LINK_REGEX = /([^\w@#%\-+=:~])?((?:(https?:\/\/)?(?:[\w@#%\-+=:~]+\.)+[a-z]{2,6}(?:\/[\w./@#%&()\-+=:?~]*)?))([^\w./@#%&()\-+=:?~]|\s|$)/g, NEW_LINK_REGEX = /(?:(https?:\/\/)?((?:[\w#%\-+=:~]+\.)+[a-z]{2,10}(?:\/[\w./#%&@()\-+=:?~]*)?))/g, //MENTION_REGEX = /([^\w@#%\-+=:~])?(@([^\u0000-\u007F]+|\w+)+)([^\w./@#%&()\-+=:?~]|\s|$)/g; // eslint-disable-line no-control-regex @@ -1057,9 +1058,10 @@ const render_emote = (token, createElement, wrapped) => { emote = createElement('img', { class: `${EMOTE_CLASS} ffz-tooltip${token.provider === 'ffz' ? ' ffz-emote' : token.provider === 'emoji' ? ' ffz-emoji' : ''}`, attrs: { - src: token.src, - srcSet: token.srcSet, + src: token.big && token.src2 || token.src, + srcSet: token.big && token.srcSet2 || token.srcSet, alt: token.text, + height: (token.big && ! token.can_big && token.height) ? `${token.height * 2}px` : undefined, 'data-tooltip-type': 'emote', 'data-provider': token.provider, 'data-id': token.id, @@ -1111,8 +1113,9 @@ export const AddonEmotes = { const mods = token.modifiers || [], ml = mods.length, emote = ({token.text} 1 || (text.length === 1 && text[0] !== '') ) - out.push({type: 'text', text: text.join(' ')}); + if ( text.length > 1 || (text.length === 1 && text[0] !== '') ) { + const t = {type: 'text', text: text.join(' ')}; + out.push(t); + } } return out; @@ -1445,6 +1463,8 @@ export const TwitchEmotes = { return tokens; const data = msg.ffz_emotes, + big = this.context.get('chat.emotes.2x'), + use_replacements = this.context.get('chat.fix-bad-emotes'), emotes = []; for(const emote_id in data) @@ -1516,15 +1536,21 @@ export const TwitchEmotes = { }); let src, srcSet; + let src2, srcSet2; const replacement = REPLACEMENTS[e_id]; - if ( replacement && this.context.get('chat.fix-bad-emotes') ) { + if ( replacement && use_replacements ) { src = `${REPLACEMENT_BASE}${replacement}`; srcSet = ''; } else { src = `${TWITCH_EMOTE_BASE}${e_id}/1.0`; srcSet = `${TWITCH_EMOTE_BASE}${e_id}/1.0 1x, ${TWITCH_EMOTE_BASE}${e_id}/2.0 2x`; + + if ( big ) { + src2 = `${TWITCH_EMOTE_BASE}${e_id}/2.0`; + srcSet2 = `${TWITCH_EMOTE_BASE}${e_id}/2.0 1x, ${TWITCH_EMOTE_BASE}${e_id}/3.0 2x`; + } } out.push({ @@ -1533,6 +1559,9 @@ export const TwitchEmotes = { provider: 'twitch', src, srcSet, + src2, + srcSet2, + big, text: text.slice(e_start - t_start, e_end - t_start).join(''), modifiers: [] }); diff --git a/src/sites/twitch-twilight/modules/channel.jsx b/src/sites/twitch-twilight/modules/channel.jsx index e65806e4..8483ca3e 100644 --- a/src/sites/twitch-twilight/modules/channel.jsx +++ b/src/sites/twitch-twilight/modules/channel.jsx @@ -33,7 +33,7 @@ export default class Channel extends Module { this.settings.add('channel.panel-tips', { - default: true, + default: false, ui: { path: 'Channel > Behavior >> Panels', title: 'Display rich tool-tips for links in channel panels.', diff --git a/src/sites/twitch-twilight/modules/chat/index.js b/src/sites/twitch-twilight/modules/chat/index.js index 5754683d..f5d4c816 100644 --- a/src/sites/twitch-twilight/modules/chat/index.js +++ b/src/sites/twitch-twilight/modules/chat/index.js @@ -354,6 +354,15 @@ export default class ChatHook extends Module { } }); + this.settings.add('chat.banners.prediction', { + default: true, + ui: { + path: 'Chat > Appearance >> Community', + title: 'Allow Predictions to be displayed in chat.', + component: 'setting-check-box' + } + }); + this.settings.add('chat.community-chest.show', { default: true, ui: { @@ -773,6 +782,7 @@ export default class ChatHook extends Module { this.chat.context.on('changed:chat.banners.hype-train', this.cleanHighlights, this); this.chat.context.on('changed:chat.subs.gift-banner', this.cleanHighlights, this); this.chat.context.on('changed:chat.banners.polls', this.cleanHighlights, this); + this.chat.context.on('changed:chat.banners.prediction', this.cleanHighlights, this); this.chat.context.on('changed:chat.subs.gift-banner', () => this.GiftBanner.forceUpdate(), this); this.chat.context.on('changed:chat.width', this.updateChatCSS, this); @@ -1258,6 +1268,7 @@ export default class ChatHook extends Module { 'community_sub_gift': this.chat.context.get('chat.subs.gift-banner'), 'megacheer': this.chat.context.get('chat.bits.show'), 'hype_train': this.chat.context.get('chat.banners.hype-train'), + 'prediction': this.chat.context.get('chat.banners.prediction'), 'poll': this.chat.context.get('chat.banners.polls') }; diff --git a/src/sites/twitch-twilight/modules/chat/line.js b/src/sites/twitch-twilight/modules/chat/line.js index fc7c7949..cfab5c98 100644 --- a/src/sites/twitch-twilight/modules/chat/line.js +++ b/src/sites/twitch-twilight/modules/chat/line.js @@ -59,6 +59,7 @@ export default class ChatLine extends Module { this.on('chat:update-lines', this.updateLines, this); this.on('i18n:update', this.updateLines, this); + this.chat.context.on('changed:chat.emotes.2x', this.updateLines, this); this.chat.context.on('changed:chat.emoji.style', this.updateLines, this); this.chat.context.on('changed:chat.bits.stack', this.updateLines, this); this.chat.context.on('changed:chat.badges.style', this.updateLines, this); diff --git a/src/sites/twitch-twilight/modules/css_tweaks/index.js b/src/sites/twitch-twilight/modules/css_tweaks/index.js index 5e1de877..0d29144c 100644 --- a/src/sites/twitch-twilight/modules/css_tweaks/index.js +++ b/src/sites/twitch-twilight/modules/css_tweaks/index.js @@ -53,8 +53,6 @@ export default class CSSTweaks extends Module { this.should_enable = true; this.inject('settings'); - this.inject('site.chat'); - this.inject('site.theme'); this.style = new ManagedStyle; this.chunks = {}; diff --git a/src/sites/twitch-twilight/modules/dashboard.js b/src/sites/twitch-twilight/modules/dashboard.js index 5fc76814..52231041 100644 --- a/src/sites/twitch-twilight/modules/dashboard.js +++ b/src/sites/twitch-twilight/modules/dashboard.js @@ -5,7 +5,7 @@ // ============================================================================ import Module from 'utilities/module'; -import { get } from 'utilities/object'; +import { get, has } from 'utilities/object'; import Twilight from 'site'; @@ -19,83 +19,69 @@ export default class Dashboard extends Module { this.inject('site.fine'); this.inject('site.channel'); - this.HostBar = this.fine.define( - 'sunlight-host-bar', - n => n.props && n.props.channel && n.props.hostedChannel !== undefined, + this.SunlightBroadcast = this.fine.define( + 'sunlight-bcast', + n => n.getGame && n.getTitle && n.props?.data, Twilight.SUNLIGHT_ROUTES - ) + ); - this.Dashboard = this.fine.define( - 'sunlight-dash', - n => n.getIsChannelEditor && n.getIsChannelModerator && n.getIsAdsEnabled && n.getIsSquadStreamsEnabled, + this.SunlightManager = this.fine.define( + 'sunlight-manager', + n => n.props?.channelID && n.handleChange && has(n, 'hasVisitedStreamManager'), Twilight.SUNLIGHT_ROUTES ); } onEnable() { - this.Dashboard.on('mount', this.onDashUpdate, this); - this.Dashboard.on('update', this.onDashUpdate, this); - this.Dashboard.on('unmount', this.onDashUnmount, this); - - this.HostBar.on('mount', this.onHostBarUpdate, this); - this.HostBar.on('update', this.onHostBarUpdate, this); - this.HostBar.on('unmount', this.onHostBarUnmount, this); - - this.Dashboard.ready((cls, instances) => { + this.SunlightManager.on('mount', this.updateSunlight, this); + this.SunlightManager.on('update', this.updateSunlight, this); + this.SunlightManager.on('unmount', this.removeSunlight, this); + this.SunlightManager.ready((cls, instances) => { for(const inst of instances) - this.onDashUpdate(inst); + this.updateSunlight(inst); }); - this.HostBar.ready((cls, instances) => { + this.SunlightBroadcast.on('mount', this.updateBroadcast, this); + this.SunlightBroadcast.on('update', this.updateBroadcast, this); + this.SunlightBroadcast.on('unmount', this.removeBroadcast, this); + this.SunlightBroadcast.ready((cls, instances) => { for(const inst of instances) - this.onHostBarUpdate(inst); + this.updateBroadcast(inst); }); } - onDashUpdate(inst) { + updateSunlight(inst) { this.settings.updateContext({ channel: get('props.channelLogin', inst), - channelID: get('props.channelID', inst) + channelID: get('props.channelID', inst), + hosting: !! inst.props?.hostedChannel?.id }); } - onDashUnmount() { + removeSunlight() { this.settings.updateContext({ channel: null, - channelID: null - }); - } - - onHostBarUpdate(inst) { - const channel = inst.props?.channel, - source = channel?.stream || channel?.broadcastSettings; - - const game = source?.game, - title = source?.title || null, - color = channel?.primaryColorHex || null; - - this.channel.updateChannelColor(color); - - this.settings.updateContext({ - /*channel: channel?.login, - channelID: channel?.id,*/ - category: game?.name, - categoryID: game?.id, - title, - channelColor: color, - hosting: !! inst.props?.hostedChannel - }); - } - - onHostBarUnmount() { - this.settings.updateContext({ - /*channel: null, - channelID: null,*/ - channelColor: null, - category: null, - categoryID: null, - title: null, + channelID: null, hosting: false }); } + + updateBroadcast(inst) { + const data = inst.props?.data?.user?.broadcastSettings, + game = data?.game; + + this.settings.updateContext({ + category: game?.name, + categoryID: game?.id, + title: data?.title + }); + } + + removeBroadcast() { + this.settings.updateContext({ + category: null, + categoryID: null, + title: null + }); + } } \ No newline at end of file diff --git a/src/sites/twitch-twilight/switchboard.js b/src/sites/twitch-twilight/switchboard.js index 7e953760..e8ce9e75 100644 --- a/src/sites/twitch-twilight/switchboard.js +++ b/src/sites/twitch-twilight/switchboard.js @@ -66,10 +66,18 @@ export default class Switchboard extends Module { this.log.info(`Found Route and Switch with ${da_switch.props.children.length} routes.`); const location = router.props.location.pathname; + if ( ! this.loadRoute(da_switch, location, false) ) + this.loadRoute(da_switch, location, true); + } + + loadRoute(da_switch, location, with_params) { for(const route of da_switch.props.children) { if ( ! route.props || ! route.props.component ) continue; + if ( with_params !== null && with_params !== route.props.path.includes(':') ) + continue; + try { const reg = pathToRegexp(route.props.path); if ( ! reg.exec || reg.exec(location) ) @@ -124,7 +132,9 @@ export default class Switchboard extends Module { } } - break; + return true; } + + return false; } } \ No newline at end of file diff --git a/src/utilities/constants.js b/src/utilities/constants.js index 1e5db5a9..938403de 100644 --- a/src/utilities/constants.js +++ b/src/utilities/constants.js @@ -106,7 +106,7 @@ export const WS_CLUSTERS = { export const IS_OSX = navigator.platform ? navigator.platform.indexOf('Mac') !== -1 : /OS X/.test(navigator.userAgent); export const IS_WIN = navigator.platform ? navigator.platform.indexOf('Win') !== -1 : /Windows/.test(navigator.userAgent); export const IS_WEBKIT = navigator.userAgent.indexOf('AppleWebKit/') !== -1 && navigator.userAgent.indexOf('Edge/') === -1; -export const IS_FIREFOX = navigator.userAgent.indexOf('Firefox/') !== -1; +export const IS_FIREFOX = (navigator.userAgent.indexOf('Firefox/') !== -1) || (window.InstallTrigger !== undefined); export const WEBKIT_CSS = IS_WEBKIT ? '-webkit-' : ''; diff --git a/src/utilities/module.js b/src/utilities/module.js index 7e0bacaa..077e9215 100644 --- a/src/utilities/module.js +++ b/src/utilities/module.js @@ -116,6 +116,17 @@ export class Module extends EventEmitter { const path = this.__path || this.name, state = this.__load_state; + if ( chain.includes(this) ) + return Promise.reject(new CyclicDependencyError(`cyclic load requirements when loading ${initial}`, [...chain, this])); + else if ( this.load_requires ) + for(const name of this.load_requires) { + const module = this.resolve(name); + if ( module && chain.includes(module) ) + return Promise.reject(new CyclicDependencyError(`cyclic load requirements when loading ${initial}`, [...chain, this, module])); + } + + chain.push(this); + if ( state === State.LOADING ) return this.__load_promise; @@ -125,11 +136,6 @@ export class Module extends EventEmitter { else if ( state === State.UNLOADING ) return Promise.reject(new ModuleError(`attempted to load module ${path} while module is being unloaded`)); - else if ( chain.includes(this) ) - return Promise.reject(new CyclicDependencyError(`cyclic load requirements when loading ${initial}`, chain)); - - chain.push(this); - this.__time('load-start'); this.__load_state = State.LOADING; return this.__load_promise = (async () => { @@ -170,6 +176,17 @@ export class Module extends EventEmitter { const path = this.__path || this.name, state = this.__load_state; + if ( chain.includes(this) ) + return Promise.reject(new CyclicDependencyError(`cyclic load requirements when unloading ${initial}`, [...chain, this])); + else if ( this.load_dependents ) + for(const dep of this.load_dependents) { + const module = this.resolve(dep); + if ( module && chain.includes(module) ) + return Promise.reject(new CyclicDependencyError(`cyclic load requirements when unloading ${initial}`, [...chain, this, module])); + } + + chain.push(this); + if ( state === State.UNLOADING ) return this.__load_promise; @@ -182,10 +199,6 @@ export class Module extends EventEmitter { else if ( state === State.LOADING ) return Promise.reject(new ModuleError(`attempted to unload module ${path} while module is being loaded`)); - else if ( chain.includes(this) ) - return Promise.reject(new CyclicDependencyError(`cyclic load requirements when unloading ${initial}`, chain)); - - chain.push(this); this.__time('unload-start'); this.__load_state = State.UNLOADING; return this.__load_promise = (async () => { @@ -197,7 +210,8 @@ export class Module extends EventEmitter { for(const name of this.load_dependents) { const module = this.resolve(name); if ( ! module ) - throw new ModuleError(`cannot find depending module ${name} when unloading ${path}`); + //throw new ModuleError(`cannot find depending module ${name} when unloading ${path}`); + continue; promises.push(module.__unload([], initial, Array.from(chain))); } @@ -227,6 +241,17 @@ export class Module extends EventEmitter { const path = this.__path || this.name, state = this.__state; + if ( chain.includes(this) ) + return Promise.reject(new CyclicDependencyError(`cyclic requirements when enabling ${initial}`, [...chain, this])); + else if ( this.requires ) + for(const name of this.requires) { + const module = this.resolve(name); + if ( module && chain.includes(module) ) + return Promise.reject(new CyclicDependencyError(`cyclic requirements when enabling ${initial}`, [...chain, this, module])); + } + + chain.push(this); + if ( state === State.ENABLING ) return this.__state_promise; @@ -236,10 +261,6 @@ export class Module extends EventEmitter { else if ( state === State.DISABLING ) return Promise.reject(new ModuleError(`attempted to enable module ${path} while module is being disabled`)); - else if ( chain.includes(this) ) - return Promise.reject(new CyclicDependencyError(`cyclic requirements when enabling ${initial}`, chain)); - - chain.push(this); this.__time('enable-start'); this.__state = State.ENABLING; return this.__state_promise = (async () => { @@ -290,6 +311,17 @@ export class Module extends EventEmitter { const path = this.__path || this.name, state = this.__state; + if ( chain.includes(this) ) + return Promise.reject(new CyclicDependencyError(`cyclic requirements when disabling ${initial}`, [...chain, this])); + else if ( this.dependents ) + for(const dep of this.dependents) { + const module = this.resolve(dep); + if ( module && chain.includes(module) ) + return Promise.reject(new CyclicDependencyError(`cyclic requirements when disabling ${initial}`, [...chain, this, dep])); + } + + chain.push(this); + if ( state === State.DISABLING ) return this.__state_promise; @@ -302,10 +334,6 @@ export class Module extends EventEmitter { else if ( state === State.ENABLING ) return Promise.reject(new ModuleError(`attempted to disable module ${path} but module is being enabled`)); - else if ( chain.includes(this) ) - return Promise.reject(new CyclicDependencyError(`cyclic requirements when disabling ${initial}`, chain)); - - chain.push(this); this.__time('disable-start'); this.__state = State.DISABLING; return this.__state_promise = (async () => { @@ -319,7 +347,9 @@ export class Module extends EventEmitter { for(const name of this.dependents) { const module = this.resolve(name); if ( ! module ) - throw new ModuleError(`cannot find depending module ${name} when disabling ${path}`); + // Assume a non-existent module isn't enabled. + //throw new ModuleError(`cannot find depending module ${name} when disabling ${path}`); + continue; promises.push(module.__disable([], initial, Array.from(chain))); } @@ -665,7 +695,7 @@ export class ModuleError extends Error { } export class CyclicDependencyError extends ModuleError { constructor(message, modules) { - super(message); + super(`${message} (${modules.map(x => x.path).join(' => ')})`); this.modules = modules; } } \ No newline at end of file