mirror of
https://github.com/FrankerFaceZ/FrankerFaceZ.git
synced 2025-09-16 18:06:55 +00:00
4.78.0
* Fixed: FFZ failing to load correctly due to an issue with a pubsub module. * Fixed: The Subtember banner not being hidden. * Fixed: Some themed elements not having their colors correctly changed. * API Changed: Introduce way for the user-script to register a settings provider for cross-origin storage of settings. * API Changed: Partial work for registering a settings provider for extension storage.
This commit is contained in:
parent
6e49b689e9
commit
b62e2f7530
10 changed files with 196 additions and 127 deletions
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "frankerfacez",
|
||||
"author": "Dan Salvato LLC",
|
||||
"version": "4.77.12",
|
||||
"version": "4.78.0",
|
||||
"description": "FrankerFaceZ is a Twitch enhancement suite.",
|
||||
"private": true,
|
||||
"license": "Apache-2.0",
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
'use strict';
|
||||
(() => {
|
||||
const browser = globalThis.browser ?? globalThis.chrome;
|
||||
const is_firefox = (typeof browser === 'object' && browser.runtime.getURL('').startsWith('moz'));
|
||||
|
||||
if (
|
||||
// Don't run on certain sub-domains.
|
||||
|
@ -21,6 +22,7 @@
|
|||
}
|
||||
|
||||
document.body.dataset.ffzSource = 'extension';
|
||||
document.body.dataset.ffzExtension = browser.runtime.id;
|
||||
|
||||
// Make sure to wake the service worker up early.
|
||||
browser.runtime.sendMessage({
|
||||
|
@ -29,75 +31,34 @@
|
|||
|
||||
// Set up a bridge for connections, since Firefox
|
||||
// doesn't support externally_connectable.
|
||||
const connections = new Map;
|
||||
|
||||
function handleConnect(id) {
|
||||
if ( connections.has(id) )
|
||||
return;
|
||||
|
||||
const port = browser.runtime.connect();
|
||||
connections.set(id, port);
|
||||
|
||||
if (is_firefox) {
|
||||
const port = browser.runtime.connect({ name: 'ffz-cs-bridge' });
|
||||
port.onMessage.addListener(msg => {
|
||||
window.postMessage({
|
||||
type: 'ffz-con-message',
|
||||
id,
|
||||
payload: msg
|
||||
})
|
||||
ffz_from_worker: true,
|
||||
...msg
|
||||
}, window.location.origin);
|
||||
});
|
||||
|
||||
port.onDisconnect.addListener(() => {
|
||||
connections.delete(id);
|
||||
window.postMessage({
|
||||
type: 'ffz-con-disconnect',
|
||||
id
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function handleDisconnect(id) {
|
||||
const port = connections.get(id);
|
||||
if ( port ) {
|
||||
connections.delete(id);
|
||||
port.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
function handleMessage(id, payload) {
|
||||
const port = connection.get(id);
|
||||
if ( port ) {
|
||||
port.postMessage(payload);
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('message', evt => {
|
||||
if (evt.source !== window || ! evt.data )
|
||||
if (evt.source !== window || ! evt.data?.ffz_to_worker )
|
||||
return;
|
||||
|
||||
const { type, id, payload } = evt.data;
|
||||
|
||||
if ( type === 'ffz-con-connect' )
|
||||
handleConnect(id);
|
||||
|
||||
else if ( type === 'ffz-con-message' )
|
||||
handleMessage(id, payload);
|
||||
|
||||
else if ( type === 'ffz-con-disconnect' )
|
||||
handleDisconnect(id);
|
||||
port.postMessage(evt.data);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
// Let the extension send messages to the page directly.
|
||||
browser.runtime.onMessage.addListener((msg, sender) => {
|
||||
if (msg?.type === 'ffz_to_page')
|
||||
window.postMessage(msg.data, '*');
|
||||
browser.runtime.onMessage.addListener(msg => {
|
||||
if (msg?.ffz_from_worker)
|
||||
window.postMessage(msg, window.location.origin);
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
// Now, inject our script into the page context.
|
||||
const HOST = location.hostname,
|
||||
SERVER = browser.runtime.getURL("web"),
|
||||
SERVER = browser.runtime.getURL('web'),
|
||||
script = document.createElement('script');
|
||||
|
||||
let FLAVOR =
|
||||
|
|
|
@ -38,6 +38,7 @@ import * as Utility_Time from 'utilities/time';
|
|||
import * as Utility_Tooltip from 'utilities/tooltip';
|
||||
import * as Utility_I18n from 'utilities/translation-core';
|
||||
import * as Utility_Filtering from 'utilities/filtering';
|
||||
import { installPort } from './utilities/extension_port';
|
||||
|
||||
class FrankerFaceZ extends Module {
|
||||
|
||||
|
@ -128,6 +129,9 @@ class FrankerFaceZ extends Module {
|
|||
// Core Systems
|
||||
// ========================================================================
|
||||
|
||||
if (Utility_Constants.EXTENSION)
|
||||
installPort(this);
|
||||
|
||||
this.inject('settings', SettingsManager);
|
||||
this.inject('experiments', ExperimentManager);
|
||||
this.inject('i18n', TranslationManager);
|
||||
|
|
|
@ -19,8 +19,9 @@ import * as CLEARABLES from './clearables';
|
|||
|
||||
import type { SettingsProfileMetadata, ContextData, ExportedFullDump, SettingsClearable, SettingDefinition, SettingProcessor, SettingUiDefinition, SettingValidator, SettingType, ExportedBlobMetadata, SettingsKeys, AllSettingsKeys, ConcreteLocalStorageData } from './types';
|
||||
import type { FilterType } from 'utilities/filtering';
|
||||
import { AdvancedSettingsProvider, IndexedDBProvider, LocalStorageProvider, Providers, type SettingsProvider } from './providers';
|
||||
import { AdvancedSettingsProvider, IndexedDBProvider, LocalStorageProvider, Providers, SettingsProvider } from './providers';
|
||||
import type { AddonInfo, SettingsTypeMap } from 'utilities/types';
|
||||
import { FFZEvent } from '../utilities/events';
|
||||
|
||||
export {parse as parse_path} from 'utilities/path-parser';
|
||||
|
||||
|
@ -42,6 +43,21 @@ export const NO_SYNC_KEYS = ['session'];
|
|||
// Registration
|
||||
// ============================================================================
|
||||
|
||||
type FFZProviderConstructorEvent = {
|
||||
settings: SettingsManager;
|
||||
Provider: typeof SettingsProvider;
|
||||
AdvancedProvider: typeof AdvancedSettingsProvider;
|
||||
registerProvider: (key: string, provider: typeof SettingsProvider) => void;
|
||||
};
|
||||
|
||||
type FFZProviderConstructor = (evt: FFZProviderConstructorEvent) => void;
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
ffz_providers?: FFZProviderConstructor[];
|
||||
}
|
||||
}
|
||||
|
||||
declare module 'utilities/types' {
|
||||
interface ModuleEventMap {
|
||||
settings: SettingsEvents;
|
||||
|
@ -147,10 +163,33 @@ export default class SettingsManager extends Module<'settings', SettingsEvents>
|
|||
|
||||
this.providers = {};
|
||||
for(const [key, provider] of Object.entries(Providers)) {
|
||||
if ( provider.supported() )
|
||||
if ( provider.supported(this) )
|
||||
this.providers[key] = provider;
|
||||
}
|
||||
|
||||
// Load any dynamic providers that have been registered.
|
||||
// Now that we're here, no further providers can be registered, so seal them.
|
||||
window.ffz_providers = window.ffz_providers || [];
|
||||
Object.seal(window.ffz_providers);
|
||||
if ( window.ffz_providers.length > 0 ) {
|
||||
const evt = {
|
||||
settings: this,
|
||||
Provider: SettingsProvider,
|
||||
AdvancedProvider: AdvancedSettingsProvider,
|
||||
registerProvider: (key: string, provider: typeof SettingsProvider) => {
|
||||
if ( ! this.providers[key] && provider.supported(this) )
|
||||
this.providers[key] = provider;
|
||||
}
|
||||
};
|
||||
|
||||
for(const p of window.ffz_providers)
|
||||
try {
|
||||
p(evt);
|
||||
} catch(err) {
|
||||
this.log.error('Error while registering external settings provider:', err);
|
||||
}
|
||||
}
|
||||
|
||||
// This cannot be modified at a future time, as providers NEED
|
||||
// to be ready very early in FFZ intitialization. Seal it.
|
||||
Object.seal(this.providers);
|
||||
|
@ -689,13 +728,35 @@ export default class SettingsManager extends Module<'settings', SettingsEvents>
|
|||
* @returns {SettingsProvider} The provider to store everything.
|
||||
*/
|
||||
async _createProvider() {
|
||||
// If we should be using Cross-Origin Storage Bridge, do so.
|
||||
//if ( this.providers.cosb && this.providers.cosb.supported() )
|
||||
// return new this.providers.cosb(this);
|
||||
// There are a couple situations where we might want to ignore a
|
||||
// pre-set provider and sniff anyways. These are situations where
|
||||
// a user can't open the control center. So, the player embeds,
|
||||
// clips pages, and embedded chat that's embedded within other sites.
|
||||
let ignore_choice = false;
|
||||
if ( (this.root as any).host === 'twitch' ) {
|
||||
// Player or clips
|
||||
if ( location.hostname.startsWith('player.') || location.hostname.startsWith('clips.') )
|
||||
ignore_choice = true;
|
||||
|
||||
let wanted = localStorage.ffzProviderv2;
|
||||
// Embedded stuff
|
||||
else if ( location.pathname.startsWith('/embed/') && (
|
||||
// ancestorOrigins > 0
|
||||
( location.ancestorOrigins && location.ancestorOrigins.length )
|
||||
||
|
||||
// parent.location mismatch
|
||||
( window.parent && window.parent.location !== location )
|
||||
) )
|
||||
ignore_choice = true;
|
||||
}
|
||||
|
||||
let wanted;
|
||||
if (ignore_choice)
|
||||
wanted = await this.sniffProvider();
|
||||
else {
|
||||
wanted = localStorage.ffzProviderv2;
|
||||
if ( wanted == null )
|
||||
wanted = localStorage.ffzProviderv2 = await this.sniffProvider();
|
||||
}
|
||||
|
||||
if ( this.providers[wanted] ) {
|
||||
const provider = new (this.providers[wanted] as any)(this) as SettingsProvider;
|
||||
|
@ -726,7 +787,7 @@ export default class SettingsManager extends Module<'settings', SettingsEvents>
|
|||
);
|
||||
|
||||
for(const [key, provider] of providers) {
|
||||
if ( provider.supported() && await provider.hasContent() ) // eslint-disable-line no-await-in-loop
|
||||
if ( provider.supported(this) && await provider.hasContent(this) ) // eslint-disable-line no-await-in-loop
|
||||
return key;
|
||||
}
|
||||
|
||||
|
|
|
@ -51,7 +51,7 @@ export abstract class SettingsProvider extends EventEmitter<ProviderEvents> {
|
|||
static title: string;
|
||||
static description: string;
|
||||
|
||||
static hasContent: () => OptionalPromise<boolean>;
|
||||
static hasContent: (manager: SettingsManager) => OptionalPromise<boolean>;
|
||||
|
||||
|
||||
manager: SettingsManager;
|
||||
|
@ -70,7 +70,7 @@ export abstract class SettingsProvider extends EventEmitter<ProviderEvents> {
|
|||
this.disabled = false;
|
||||
}
|
||||
|
||||
static supported() {
|
||||
static supported(manager: SettingsManager) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -103,14 +103,14 @@ export abstract class SettingsProvider extends EventEmitter<ProviderEvents> {
|
|||
): ProviderTypeMap[K];
|
||||
abstract get<K extends keyof ProviderTypeMap>(
|
||||
key: K
|
||||
): ProviderTypeMap[K] | null;
|
||||
): ProviderTypeMap[K] | undefined;
|
||||
abstract get<T>(
|
||||
key: Exclude<string, keyof ProviderTypeMap>,
|
||||
default_value: T
|
||||
): T;
|
||||
abstract get<T>(
|
||||
key: Exclude<string, keyof ProviderTypeMap>
|
||||
): T | null;
|
||||
): T | undefined;
|
||||
|
||||
abstract set<K extends keyof ProviderTypeMap>(key: K, value: ProviderTypeMap[K]): void;
|
||||
abstract set<K extends string>(key: Exclude<K, keyof ProviderTypeMap>, value: unknown): void;
|
||||
|
@ -230,7 +230,12 @@ export abstract class RemoteSettingsProvider extends AdvancedSettingsProvider {
|
|||
|
||||
// Provider Methods
|
||||
|
||||
get<T>(key: string, default_value?: T): T {
|
||||
get<T>(key: string): T | undefined;
|
||||
get<T>(key: string, default_value: T): T;
|
||||
get<T>(
|
||||
key: string,
|
||||
default_value?: T
|
||||
): T | undefined {
|
||||
return this._cached.has(key)
|
||||
? this._cached.get(key)
|
||||
: default_value;
|
||||
|
@ -412,9 +417,8 @@ export class LocalStorageProvider extends SettingsProvider {
|
|||
return true;
|
||||
}
|
||||
|
||||
static hasContent(prefix?: string) {
|
||||
if ( ! prefix )
|
||||
prefix = 'FFZ:setting:';
|
||||
static hasContent() {
|
||||
const prefix = 'FFZ:setting:';
|
||||
|
||||
for(const key in localStorage)
|
||||
if ( key.startsWith(prefix) && has(localStorage, key) )
|
||||
|
@ -432,9 +436,9 @@ export class LocalStorageProvider extends SettingsProvider {
|
|||
private _boundHandleMessage?: ((event: MessageEvent) => void) | null;
|
||||
private _boundHandleStorage?: ((event: StorageEvent) => void) | null;
|
||||
|
||||
constructor(manager: SettingsManager, prefix?: string) {
|
||||
constructor(manager: SettingsManager) {
|
||||
super(manager);
|
||||
this.prefix = prefix = prefix == null ? 'FFZ:setting:' : prefix;
|
||||
const prefix = this.prefix = 'FFZ:setting:';
|
||||
|
||||
const cache = this._cached = new Map,
|
||||
len = prefix.length;
|
||||
|
@ -564,10 +568,12 @@ export class LocalStorageProvider extends SettingsProvider {
|
|||
}
|
||||
}
|
||||
|
||||
get<T>(key: string): T | undefined;
|
||||
get<T>(key: string, default_value: T): T;
|
||||
get<T>(
|
||||
key: string,
|
||||
default_value?: T
|
||||
): T {
|
||||
): T | undefined {
|
||||
return this._cached.has(key)
|
||||
? this._cached.get(key)
|
||||
: default_value;
|
||||
|
@ -887,7 +893,12 @@ export class IndexedDBProvider extends AdvancedSettingsProvider {
|
|||
|
||||
// Synchronous Methods
|
||||
|
||||
get<T>(key: string, default_value?: T): T {
|
||||
get<T>(key: string): T | undefined;
|
||||
get<T>(key: string, default_value: T): T;
|
||||
get<T>(
|
||||
key: string,
|
||||
default_value?: T
|
||||
): T | undefined {
|
||||
return this._cached.has(key)
|
||||
? this._cached.get(key)
|
||||
: default_value;
|
||||
|
@ -1513,7 +1524,7 @@ export class ExtensionProvider extends RemoteSettingsProvider {
|
|||
|
||||
static supported() { return EXTENSION }
|
||||
|
||||
static hasContent() {
|
||||
static hasContent(manager: SettingsManager) {
|
||||
if ( ! ExtensionProvider.supported() )
|
||||
return false;
|
||||
|
||||
|
@ -1523,20 +1534,12 @@ export class ExtensionProvider extends RemoteSettingsProvider {
|
|||
let responded = false,
|
||||
timeout: ReturnType<typeof setTimeout> | null = null ;
|
||||
|
||||
const listener = (evt: MessageEvent<any>) => {
|
||||
if (evt.source !== window)
|
||||
return;
|
||||
|
||||
if (evt.data && evt.data.type === 'ffz_from_ext') {
|
||||
const msg = evt.data.data,
|
||||
type = msg?.ffz_type;
|
||||
|
||||
if (type === 'has-keys') {
|
||||
const listener = (msg: any) => {
|
||||
if ( msg.type === 'has-keys' ) {
|
||||
responded = true;
|
||||
resolve(msg.value);
|
||||
cleanup();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const cleanup = () => {
|
||||
|
@ -1550,17 +1553,11 @@ export class ExtensionProvider extends RemoteSettingsProvider {
|
|||
timeout = null;
|
||||
}
|
||||
|
||||
window.removeEventListener('message', listener);
|
||||
manager.off('ext:message', listener);
|
||||
}
|
||||
|
||||
window.addEventListener('message', listener);
|
||||
|
||||
window.postMessage({
|
||||
type: 'ffz_to_ext',
|
||||
data: {
|
||||
ffz_type: 'check-has-keys'
|
||||
}
|
||||
}, '*');
|
||||
manager.on('ext:message', listener);
|
||||
manager.emit('ext:message', { type: 'check-has-keys' });
|
||||
|
||||
timeout = setTimeout(cleanup, 1000);
|
||||
});
|
||||
|
@ -1578,8 +1575,8 @@ export class ExtensionProvider extends RemoteSettingsProvider {
|
|||
constructor(manager: SettingsManager) {
|
||||
super(manager);
|
||||
|
||||
this.onExtMessage = this.onExtMessage.bind(this);
|
||||
window.addEventListener('message', this.onExtMessage);
|
||||
manager.on('ext:message', this.handleMessage, this);
|
||||
this.send('ready');
|
||||
}
|
||||
|
||||
// Stuff
|
||||
|
@ -1589,36 +1586,16 @@ export class ExtensionProvider extends RemoteSettingsProvider {
|
|||
}
|
||||
|
||||
disableEvents() {
|
||||
|
||||
this.manager.off('ext:message', this.handleMessage, this);
|
||||
}
|
||||
|
||||
// Communication
|
||||
|
||||
onExtMessage(evt: MessageEvent<any>) {
|
||||
if (evt.source !== window)
|
||||
return;
|
||||
|
||||
if (evt.data?.type === 'ffz_from_ext' && evt.data.data?.ffz_type)
|
||||
this.handleMessage(evt.data.data);
|
||||
}
|
||||
|
||||
send(msg: string | CorsMessage, transfer?: OptionalArray<Transferable>) {
|
||||
if ( typeof msg === 'string' )
|
||||
msg = {ffz_type: msg} as any;
|
||||
|
||||
try {
|
||||
window.postMessage(
|
||||
{
|
||||
type: 'ffz_to_ext',
|
||||
data: msg
|
||||
},
|
||||
'*',
|
||||
transfer ? (Array.isArray(transfer) ? transfer : [transfer]) : undefined
|
||||
);
|
||||
|
||||
} catch(err) {
|
||||
this.manager.log.error('Error sending message to extension.', err, msg, transfer);
|
||||
}
|
||||
this.manager.emit('ext:message', msg);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -23,6 +23,7 @@ declare module 'utilities/types' {
|
|||
interface SettingsTypeMap {
|
||||
'chat.hype.show-pinned': boolean;
|
||||
'layout.turbo-cta': boolean;
|
||||
'layout.subtember': boolean;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -95,6 +96,10 @@ export default class Loadable extends Module {
|
|||
this.toggle('TopNav__TurboButton_Available', val);
|
||||
});
|
||||
|
||||
this.settings.getChanges('layout.subtember', val => {
|
||||
this.toggle('TokenizedCommerceBanner', val);
|
||||
});
|
||||
|
||||
this.ErrorBoundaryComponent.ready((cls, instances) => {
|
||||
this.log.debug('Found Error Boundary component wrapper.');
|
||||
|
||||
|
|
|
@ -575,7 +575,7 @@ export default class ThemeEngine extends Module {
|
|||
const selector = dark ? '' : `:not(.settings-menu-button-component--forced-dark-theme)`;
|
||||
|
||||
if ( bits.length )
|
||||
this.css_tweaks.set('colors', `body,body .tw-root--theme-light,body .tw-root--theme-dark${selector} {${bits.join('\n')}}.channel-info-content .tw-accent-region,.channel-info-content div[class^="ScAccentRegion"]{${accent_bits.join('\n')}}`);
|
||||
this.css_tweaks.set('colors', `body,body .tw-root--theme-light.tw-root--theme-light,body .tw-root--theme-dark.tw-root--theme-dark${selector} {${bits.join('\n')}}.channel-info-content .tw-accent-region,.channel-info-content div[class^="ScAccentRegion"]{${accent_bits.join('\n')}}`);
|
||||
else
|
||||
this.css_tweaks.delete('colors');
|
||||
}
|
||||
|
|
|
@ -150,7 +150,12 @@ export default class Subpump extends Module<'site.subpump', SubpumpEvents> {
|
|||
|
||||
if ( instance ) {
|
||||
this.instance = instance;
|
||||
try {
|
||||
this.hookClient(instance);
|
||||
} catch(err) {
|
||||
this.instance = null;
|
||||
this.log.error('Error hooking PubSub client.', err);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|
|
54
src/utilities/extension_port.ts
Normal file
54
src/utilities/extension_port.ts
Normal file
|
@ -0,0 +1,54 @@
|
|||
import Module from "./module";
|
||||
|
||||
type PortType = {
|
||||
postMessage: (msg: any) => void;
|
||||
}
|
||||
|
||||
export function installPort(module: Module) {
|
||||
let port: PortType | null = null;
|
||||
|
||||
function initialize() {
|
||||
try {
|
||||
// Try connecting with externally_connectable first.
|
||||
const cp = port = chrome.runtime.connect(document.body.dataset.ffzExtension!, {
|
||||
name: 'ffz-ext-port'
|
||||
});
|
||||
|
||||
cp.onMessage.addListener(msg => {
|
||||
module.emit('ext:message', msg);
|
||||
});
|
||||
|
||||
cp.onDisconnect.addListener(p => {
|
||||
module.log.warn('Extension port disconnected.', (p as any)?.error ?? chrome.runtime.lastError);
|
||||
port = null;
|
||||
});
|
||||
|
||||
return;
|
||||
} catch(err) {
|
||||
module.log.info('Unable to connect using externally_connectable, falling back to bridge.');
|
||||
}
|
||||
|
||||
window.addEventListener('message', evt => {
|
||||
if ( evt.source !== window || ! evt.data || evt.data.ffz_from_worker )
|
||||
return;
|
||||
|
||||
module.emit('ext:message', evt.data);
|
||||
});
|
||||
|
||||
port = {
|
||||
postMessage(msg) {
|
||||
window.postMessage({
|
||||
ffz_to_worker: true,
|
||||
...msg
|
||||
}, window.location.origin);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
module.on('ext:post-message', msg => {
|
||||
if ( ! port )
|
||||
initialize();
|
||||
|
||||
port!.postMessage(msg);
|
||||
});
|
||||
}
|
|
@ -60,7 +60,9 @@ export type ModuleEvents = {
|
|||
':loaded': [module: GenericModule],
|
||||
':unloaded': [module: GenericModule],
|
||||
':enabled': [module: GenericModule],
|
||||
':disabled': [module: GenericModule]
|
||||
':disabled': [module: GenericModule],
|
||||
'ext:message': [msg: any],
|
||||
'ext:post-message': [msg: any]
|
||||
};
|
||||
|
||||
export type GenericModule = Module<any, any, any, any>;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue