mirror of
https://github.com/FrankerFaceZ/FrankerFaceZ.git
synced 2025-06-27 21:05:53 +00:00
4.75.0
* Added: Setting to use native styling for subscription notices in chat. (Closes #1551) * Added: Setting to hide the stories UI in the side navigation. (Closes #1531) * Added: Setting to hide specific users from the directory and sidebar. * Changed: Hide the top navigation's search field when using minimal navigation for a cleaner look. (Closes #1556) * Changed: More internal changes for the support to Manifest v3. * Fixed: Uploading logs from the FFZ Control Center not working. * API Changed: Removed support for `no_sanitize` from `setChildren`. I don't think anything was currently using this, but going forward it is unsupported. We want to avoid using `innerHTML` as much as possible to simplify the approval process.
This commit is contained in:
parent
533bf52c9e
commit
1ee737f2ca
17 changed files with 669 additions and 280 deletions
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "frankerfacez",
|
"name": "frankerfacez",
|
||||||
"author": "Dan Salvato LLC",
|
"author": "Dan Salvato LLC",
|
||||||
"version": "4.74.1",
|
"version": "4.75.0",
|
||||||
"description": "FrankerFaceZ is a Twitch enhancement suite.",
|
"description": "FrankerFaceZ is a Twitch enhancement suite.",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
|
@ -11,7 +11,9 @@
|
||||||
"clean": "rimraf dist",
|
"clean": "rimraf dist",
|
||||||
"dev": "cross-env NODE_ENV=development webpack serve",
|
"dev": "cross-env NODE_ENV=development webpack serve",
|
||||||
"dev:prod": "cross-env NODE_ENV=production webpack serve",
|
"dev:prod": "cross-env NODE_ENV=production webpack serve",
|
||||||
|
"dev:ext": "cross-env NODE_ENV=development FFZ_EXTENSION=true webpack serve",
|
||||||
"build": "pnpm build:prod",
|
"build": "pnpm build:prod",
|
||||||
|
"build:ext": "cross-env NODE_ENV=production FFZ_EXTENSION=true webpack build",
|
||||||
"build:stats": "cross-env NODE_ENV=production webpack build --json > stats.json",
|
"build:stats": "cross-env NODE_ENV=production webpack build --json > stats.json",
|
||||||
"build:prod": "cross-env NODE_ENV=production webpack build",
|
"build:prod": "cross-env NODE_ENV=production webpack build",
|
||||||
"build:dev": "cross-env NODE_ENV=development webpack build",
|
"build:dev": "cross-env NODE_ENV=development webpack build",
|
||||||
|
@ -25,6 +27,7 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@ffz/fontello-cli": "^1.0.4",
|
"@ffz/fontello-cli": "^1.0.4",
|
||||||
|
"@types/chrome": "^0.0.277",
|
||||||
"@types/crypto-js": "^4.2.1",
|
"@types/crypto-js": "^4.2.1",
|
||||||
"@types/js-cookie": "^3.0.6",
|
"@types/js-cookie": "^3.0.6",
|
||||||
"@types/safe-regex": "^1.1.6",
|
"@types/safe-regex": "^1.1.6",
|
||||||
|
|
24
pnpm-lock.yaml
generated
24
pnpm-lock.yaml
generated
|
@ -97,6 +97,9 @@ devDependencies:
|
||||||
'@ffz/fontello-cli':
|
'@ffz/fontello-cli':
|
||||||
specifier: ^1.0.4
|
specifier: ^1.0.4
|
||||||
version: 1.0.4
|
version: 1.0.4
|
||||||
|
'@types/chrome':
|
||||||
|
specifier: ^0.0.277
|
||||||
|
version: 0.0.277
|
||||||
'@types/crypto-js':
|
'@types/crypto-js':
|
||||||
specifier: ^4.2.1
|
specifier: ^4.2.1
|
||||||
version: 4.2.1
|
version: 4.2.1
|
||||||
|
@ -588,6 +591,13 @@ packages:
|
||||||
'@types/node': 20.5.7
|
'@types/node': 20.5.7
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@types/chrome@0.0.277:
|
||||||
|
resolution: {integrity: sha512-qoTgBcDWblSsX+jvFnpUlLUE3LAuOhZfBh9MyMWMQHDsQiYVgBvdZWu9COrdB9+aNnInEyXcFgfc2HE16sdSYQ==}
|
||||||
|
dependencies:
|
||||||
|
'@types/filesystem': 0.0.36
|
||||||
|
'@types/har-format': 1.2.16
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@types/connect-history-api-fallback@1.5.0:
|
/@types/connect-history-api-fallback@1.5.0:
|
||||||
resolution: {integrity: sha512-4x5FkPpLipqwthjPsF7ZRbOv3uoLUFkTA9G9v583qi4pACvq0uTELrB8OLUzPWUI4IJIyvM85vzkV1nyiI2Lig==}
|
resolution: {integrity: sha512-4x5FkPpLipqwthjPsF7ZRbOv3uoLUFkTA9G9v583qi4pACvq0uTELrB8OLUzPWUI4IJIyvM85vzkV1nyiI2Lig==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -641,6 +651,20 @@ packages:
|
||||||
'@types/serve-static': 1.15.2
|
'@types/serve-static': 1.15.2
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@types/filesystem@0.0.36:
|
||||||
|
resolution: {integrity: sha512-vPDXOZuannb9FZdxgHnqSwAG/jvdGM8Wq+6N4D/d80z+D4HWH+bItqsZaVRQykAn6WEVeEkLm2oQigyHtgb0RA==}
|
||||||
|
dependencies:
|
||||||
|
'@types/filewriter': 0.0.33
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/@types/filewriter@0.0.33:
|
||||||
|
resolution: {integrity: sha512-xFU8ZXTw4gd358lb2jw25nxY9QAgqn2+bKKjKOYfNCzN4DKCFetK7sPtrlpg66Ywe3vWY9FNxprZawAh9wfJ3g==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/@types/har-format@1.2.16:
|
||||||
|
resolution: {integrity: sha512-fluxdy7ryD3MV6h8pTfTYpy/xQzCFC7m89nOH9y94cNqJ1mDIDPut7MnRHI3F6qRmh/cT2fUjG1MLdCNb4hE9A==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@types/http-errors@2.0.1:
|
/@types/http-errors@2.0.1:
|
||||||
resolution: {integrity: sha512-/K3ds8TRAfBvi5vfjuz8y6+GiAYBZ0x4tXv1Av6CWBWn0IlADc+ZX9pMq7oU0fNQPnBwIZl3rmeLp6SBApbxSQ==}
|
resolution: {integrity: sha512-/K3ds8TRAfBvi5vfjuz8y6+GiAYBZ0x4tXv1Av6CWBWn0IlADc+ZX9pMq7oU0fNQPnBwIZl3rmeLp6SBApbxSQ==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
|
@ -1,16 +1,52 @@
|
||||||
/* eslint strict: off */
|
/* eslint strict: off */
|
||||||
'use strict';
|
'use strict';
|
||||||
(() => {
|
(() => {
|
||||||
|
const browser = globalThis.browser ?? globalThis.chrome;
|
||||||
|
|
||||||
|
if (
|
||||||
// Don't run on certain sub-domains.
|
// Don't run on certain sub-domains.
|
||||||
if ( /^(?:localhost\.rig|blog|im|chatdepot|tmi|api|brand|dev|gql|passport)\./.test(location.hostname) )
|
/^(?:localhost\.rig|blog|im|chatdepot|tmi|api|brand|dev|gql|passport)\./.test(location.hostname)
|
||||||
|
||
|
||||||
|
// Don't run on pages that have disabled FFZ.
|
||||||
|
/disable_frankerfacez/.test(location.search)
|
||||||
|
) {
|
||||||
|
// Tell the service worker we aren't injecting.
|
||||||
|
browser.runtime.sendMessage({
|
||||||
|
type: 'ffz_not_supported'
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure to wake the service worker up early.
|
||||||
|
browser.runtime.sendMessage({
|
||||||
|
type: 'ffz_injecting'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set up the extension message bridge.
|
||||||
|
window.addEventListener('message', evt => {
|
||||||
|
if (evt.source !== window)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if ( /disable_frankerfacez/.test(location.search) )
|
if (evt.data && evt.data.type === 'ffz_to_ext')
|
||||||
return;
|
browser.runtime.sendMessage(evt.data.data, resp => {
|
||||||
|
if (resp)
|
||||||
|
window.postMessage({
|
||||||
|
type: 'ffz_from_ext',
|
||||||
|
data: resp
|
||||||
|
}, '*');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
const browser = globalThis.browser ?? globalThis.chrome,
|
browser.runtime.onMessage.addListener((msg, sender) => {
|
||||||
|
window.postMessage({
|
||||||
|
type: 'ffz_from_ext',
|
||||||
|
data: msg
|
||||||
|
}, '*');
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
HOST = location.hostname,
|
// 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');
|
script = document.createElement('script');
|
||||||
|
|
||||||
|
|
|
@ -93,7 +93,7 @@ export default {
|
||||||
return;
|
return;
|
||||||
|
|
||||||
this.uploading = true;
|
this.uploading = true;
|
||||||
const response = await fetch('https://putco.de', {
|
const response = await fetch('https://logs.frankerfacez.com', {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
body: this.text
|
body: this.text
|
||||||
});
|
});
|
||||||
|
|
|
@ -255,11 +255,11 @@ export default class MainMenu extends Module {
|
||||||
// If we're on a page with minimal root, we want to open settings
|
// If we're on a page with minimal root, we want to open settings
|
||||||
// in a popout as we're almost certainly within Popout Chat.
|
// in a popout as we're almost certainly within Popout Chat.
|
||||||
const layout = this.resolve('site.layout'),
|
const layout = this.resolve('site.layout'),
|
||||||
item = evt.item,
|
item = evt?.item,
|
||||||
event = evt.event;
|
event = evt?.event;
|
||||||
|
|
||||||
if ( (layout && layout.is_minimal) || (event && (event.ctrlKey || event.shiftKey)) ) {
|
if ( (layout && layout.is_minimal) || (event && (event.ctrlKey || event.shiftKey)) ) {
|
||||||
if ( ! this.openPopout(item) )
|
if ( ! this.openPopout(item) && evt )
|
||||||
evt.errored = true;
|
evt.errored = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -245,7 +245,10 @@ export default class SettingsManager extends Module<'settings', SettingsEvents>
|
||||||
window.addEventListener('message', event => {
|
window.addEventListener('message', event => {
|
||||||
const type = event.data?.ffz_type;
|
const type = event.data?.ffz_type;
|
||||||
|
|
||||||
if ( type === 'request-context' && event.source ) {
|
if ( type === 'open-settings' )
|
||||||
|
this.emit('main_menu:open');
|
||||||
|
|
||||||
|
else if ( type === 'request-context' && event.source ) {
|
||||||
this._context_proxies.add(event.source);
|
this._context_proxies.add(event.source);
|
||||||
this._updateContextProxies(event.source);
|
this._updateContextProxies(event.source);
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@ import {EventEmitter} from 'utilities/events';
|
||||||
import {TicketLock, has, once} from 'utilities/object';
|
import {TicketLock, has, once} from 'utilities/object';
|
||||||
import type SettingsManager from '.';
|
import type SettingsManager from '.';
|
||||||
import type { OptionalArray, OptionalPromise, ProviderTypeMap } from '../utilities/types';
|
import type { OptionalArray, OptionalPromise, ProviderTypeMap } from '../utilities/types';
|
||||||
|
import { EXTENSION } from '../utilities/constants';
|
||||||
|
|
||||||
const DB_VERSION = 1,
|
const DB_VERSION = 1,
|
||||||
NOT_WWW_TWITCH = window.location.host !== 'www.twitch.tv',
|
NOT_WWW_TWITCH = window.location.host !== 'www.twitch.tv',
|
||||||
|
@ -144,6 +145,255 @@ export abstract class AdvancedSettingsProvider extends SettingsProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export abstract class RemoteSettingsProvider extends AdvancedSettingsProvider {
|
||||||
|
|
||||||
|
// State and Storage
|
||||||
|
private _start_time: number;
|
||||||
|
private _cached: Map<string, any>;
|
||||||
|
|
||||||
|
private _blobs: boolean | null;
|
||||||
|
private _rpc: Map<number, [(input: any) => void, () => void]>;
|
||||||
|
private _last_id: number;
|
||||||
|
|
||||||
|
private resolved_ready: boolean;
|
||||||
|
private _ready_wait_resolve?: (() => void) | null;
|
||||||
|
private _ready_wait_fail?: ((err: any) => void) | null;
|
||||||
|
private _ready_wait?: Promise<void> | null;
|
||||||
|
|
||||||
|
constructor(manager: SettingsManager) {
|
||||||
|
super(manager);
|
||||||
|
|
||||||
|
this._start_time = performance.now();
|
||||||
|
|
||||||
|
this._rpc = new Map;
|
||||||
|
|
||||||
|
this._cached = new Map;
|
||||||
|
this.resolved_ready = false;
|
||||||
|
this.ready = false;
|
||||||
|
this._ready_wait = null;
|
||||||
|
|
||||||
|
this._blobs = null;
|
||||||
|
this._last_id = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
get supportsBlobs() {
|
||||||
|
return this._blobs ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stuff
|
||||||
|
|
||||||
|
broadcastTransfer() {
|
||||||
|
// TODO: Figure out what this would mean for CORS.
|
||||||
|
}
|
||||||
|
|
||||||
|
disableEvents() {
|
||||||
|
// TODO: Figure out what this would mean for CORS.
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Initialization
|
||||||
|
|
||||||
|
protected resolveReady(success: boolean, data?: any) {
|
||||||
|
if ( this.manager )
|
||||||
|
this.manager.log.info(`${this.constructor.name} ready in ${(performance.now() - this._start_time).toFixed(5)}ms`);
|
||||||
|
|
||||||
|
this.resolved_ready = true;
|
||||||
|
this.ready = success;
|
||||||
|
|
||||||
|
if ( success && this._ready_wait_resolve )
|
||||||
|
this._ready_wait_resolve();
|
||||||
|
else if ( ! success && this._ready_wait_fail )
|
||||||
|
this._ready_wait_fail(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
awaitReady() {
|
||||||
|
if ( this.resolved_ready ) {
|
||||||
|
if ( this.ready )
|
||||||
|
return Promise.resolve();
|
||||||
|
return Promise.reject();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( this._ready_wait )
|
||||||
|
return this._ready_wait;
|
||||||
|
|
||||||
|
return this._ready_wait = new Promise<void>((resolve, fail) => {
|
||||||
|
this._ready_wait_resolve = resolve;
|
||||||
|
this._ready_wait_fail = fail;
|
||||||
|
|
||||||
|
}).finally(() => {
|
||||||
|
this._ready_wait = null;
|
||||||
|
this._ready_wait_resolve = null;
|
||||||
|
this._ready_wait_fail = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Provider Methods
|
||||||
|
|
||||||
|
get<T>(key: string, default_value?: T): T {
|
||||||
|
return this._cached.has(key)
|
||||||
|
? this._cached.get(key)
|
||||||
|
: default_value;
|
||||||
|
}
|
||||||
|
|
||||||
|
set(key: string, value: any) {
|
||||||
|
if ( value === undefined ) {
|
||||||
|
if ( this.has(key) )
|
||||||
|
this.delete(key);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._cached.set(key, value);
|
||||||
|
this.rpc({ffz_type: 'set', key, value})
|
||||||
|
.catch(err => this.manager.log.error('Error setting value', err));
|
||||||
|
this.emit('set', key, value, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(key: string) {
|
||||||
|
this._cached.delete(key);
|
||||||
|
this.rpc({ffz_type: 'delete', key})
|
||||||
|
.catch(err => this.manager.log.error('Error deleting value', err));
|
||||||
|
this.emit('set', key, undefined, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
const old_cache = this._cached;
|
||||||
|
this._cached = new Map;
|
||||||
|
for(const key of old_cache.keys())
|
||||||
|
this.emit('changed', key, undefined, true);
|
||||||
|
|
||||||
|
this.rpc('clear')
|
||||||
|
.catch(err => this.manager.log.error('Error clearing storage', err));
|
||||||
|
}
|
||||||
|
|
||||||
|
has(key: string) { return this._cached.has(key); }
|
||||||
|
keys() { return this._cached.keys(); }
|
||||||
|
entries() { return this._cached.entries(); }
|
||||||
|
get size() { return this._cached.size; }
|
||||||
|
|
||||||
|
async flush() {
|
||||||
|
await this.rpc('flush');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Provider Methods: Blobs
|
||||||
|
|
||||||
|
async getBlob(key: string) {
|
||||||
|
const msg = await this.rpc({ffz_type: 'get-blob', key});
|
||||||
|
return msg ? deserializeBlob(msg) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async setBlob(key: string, value: BlobLike) {
|
||||||
|
await this.rpc({
|
||||||
|
ffz_type: 'set-blob',
|
||||||
|
key,
|
||||||
|
value: await serializeBlob(value)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteBlob(key: string) {
|
||||||
|
await this.rpc({
|
||||||
|
ffz_type: 'delete-blob',
|
||||||
|
key
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async hasBlob(key: string) {
|
||||||
|
return this.rpc({ffz_type: 'has-blob', key});
|
||||||
|
}
|
||||||
|
|
||||||
|
async clearBlobs() {
|
||||||
|
await this.rpc('clear-blobs');
|
||||||
|
}
|
||||||
|
|
||||||
|
async blobKeys() {
|
||||||
|
return this.rpc('blob-keys');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Communication
|
||||||
|
|
||||||
|
abstract send(msg: string | CorsMessage, transfer?: OptionalArray<Transferable>): void;
|
||||||
|
|
||||||
|
rpc<K extends keyof CorsRpcTypes>(
|
||||||
|
msg: K | RPCInputMessage<K>,
|
||||||
|
transfer?: OptionalArray<Transferable>
|
||||||
|
) {
|
||||||
|
const id = ++this._last_id;
|
||||||
|
|
||||||
|
return new Promise<CorsOutput<K>>((resolve,fail) => {
|
||||||
|
this._rpc.set(id, [resolve, fail]);
|
||||||
|
let out: CorsMessage;
|
||||||
|
if ( typeof msg === 'string' )
|
||||||
|
out = {ffz_type: msg} as CorsMessage;
|
||||||
|
else
|
||||||
|
out = msg as unknown as CorsMessage;
|
||||||
|
|
||||||
|
out.id = id;
|
||||||
|
this.send(out, transfer);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMessage(msg: CorsMessage) {
|
||||||
|
if ( msg.ffz_type === 'ready' )
|
||||||
|
this.rpc('init-load').then(msg => {
|
||||||
|
this._blobs = msg.blobs;
|
||||||
|
for(const [key, value] of Object.entries(msg.values))
|
||||||
|
this._cached.set(key, value);
|
||||||
|
|
||||||
|
this.resolveReady(true);
|
||||||
|
|
||||||
|
}).catch(err => {
|
||||||
|
this.resolveReady(false, err);
|
||||||
|
});
|
||||||
|
|
||||||
|
else if ( msg.ffz_type === 'change' )
|
||||||
|
this.onChange(msg);
|
||||||
|
|
||||||
|
else if ( msg.ffz_type === 'change-blob' )
|
||||||
|
this.emit('changed-blob', msg.key, msg.deleted);
|
||||||
|
|
||||||
|
else if ( msg.ffz_type === 'clear-blobs' )
|
||||||
|
this.emit('clear-blobs');
|
||||||
|
|
||||||
|
else if ( msg.ffz_type === 'reply' || msg.ffz_type === 'reply-error' )
|
||||||
|
this.onReply(msg);
|
||||||
|
|
||||||
|
else
|
||||||
|
this.manager.log.warn('Unknown Message', msg.ffz_type, msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
onChange(msg: RPCInputMessage<'change'>) {
|
||||||
|
const key = msg.key,
|
||||||
|
value = msg.value,
|
||||||
|
deleted = msg.deleted;
|
||||||
|
|
||||||
|
if ( deleted ) {
|
||||||
|
this._cached.delete(key);
|
||||||
|
this.emit('changed', key, undefined, true);
|
||||||
|
} else {
|
||||||
|
this._cached.set(key, value);
|
||||||
|
this.emit('changed', key, value, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onReply(msg: CorsReplyMessage | CorsReplyErrorMessage) {
|
||||||
|
const id = msg.id,
|
||||||
|
success = msg.ffz_type === 'reply',
|
||||||
|
cbs = this._rpc.get(id);
|
||||||
|
if ( ! cbs )
|
||||||
|
return this.manager.log.warn('Received reply for unknown ID', id);
|
||||||
|
|
||||||
|
this._rpc.delete(id);
|
||||||
|
if ( success )
|
||||||
|
cbs[0](msg.reply);
|
||||||
|
else
|
||||||
|
cbs[1]();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
@ -1177,7 +1427,7 @@ export class IndexedDBProvider extends AdvancedSettingsProvider {
|
||||||
// CrossOriginStorageBridge
|
// CrossOriginStorageBridge
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
export class CrossOriginStorageBridge extends AdvancedSettingsProvider {
|
export class CrossOriginStorageBridge extends RemoteSettingsProvider {
|
||||||
|
|
||||||
// Static Stuff
|
// Static Stuff
|
||||||
|
|
||||||
|
@ -1194,36 +1444,11 @@ export class CrossOriginStorageBridge extends AdvancedSettingsProvider {
|
||||||
static shouldUpdate = false;
|
static shouldUpdate = false;
|
||||||
|
|
||||||
// State and Storage
|
// State and Storage
|
||||||
private _start_time: number;
|
|
||||||
private _cached: Map<string, any>;
|
|
||||||
|
|
||||||
private _blobs: boolean | null;
|
|
||||||
private _rpc: Map<number, [(input: any) => void, () => void]>;
|
|
||||||
private _last_id: number;
|
|
||||||
|
|
||||||
private frame: HTMLIFrameElement | null;
|
private frame: HTMLIFrameElement | null;
|
||||||
private _boundHandleMessage?: ((event: MessageEvent) => void) | null;
|
|
||||||
|
|
||||||
private resolved_ready: boolean;
|
|
||||||
private _ready_wait_resolve?: (() => void) | null;
|
|
||||||
private _ready_wait_fail?: ((err: any) => void) | null;
|
|
||||||
private _ready_wait?: Promise<void> | null;
|
|
||||||
|
|
||||||
constructor(manager: SettingsManager) {
|
constructor(manager: SettingsManager) {
|
||||||
super(manager);
|
super(manager);
|
||||||
|
|
||||||
this._start_time = performance.now();
|
|
||||||
|
|
||||||
this._rpc = new Map;
|
|
||||||
|
|
||||||
this._cached = new Map;
|
|
||||||
this.resolved_ready = false;
|
|
||||||
this.ready = false;
|
|
||||||
this._ready_wait = null;
|
|
||||||
|
|
||||||
this._blobs = null;
|
|
||||||
this._last_id = 0;
|
|
||||||
|
|
||||||
const frame = this.frame = document.createElement('iframe');
|
const frame = this.frame = document.createElement('iframe');
|
||||||
frame.src = (this.manager.root as any).host === 'youtube' ?
|
frame.src = (this.manager.root as any).host === 'youtube' ?
|
||||||
'//www.youtube.com/__ffz_bridge/' :
|
'//www.youtube.com/__ffz_bridge/' :
|
||||||
|
@ -1232,13 +1457,10 @@ export class CrossOriginStorageBridge extends AdvancedSettingsProvider {
|
||||||
frame.style.width = '0';
|
frame.style.width = '0';
|
||||||
frame.style.height = '0';
|
frame.style.height = '0';
|
||||||
|
|
||||||
this._boundHandleMessage = this.onMessage.bind(this);
|
this.onMessage = this.onMessage.bind(this);
|
||||||
window.addEventListener('message', this._boundHandleMessage);
|
|
||||||
document.body.appendChild(frame);
|
|
||||||
}
|
|
||||||
|
|
||||||
get supportsBlobs() {
|
window.addEventListener('message', this.onMessage);
|
||||||
return this._blobs ?? false;
|
document.body.appendChild(frame);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stuff
|
// Stuff
|
||||||
|
@ -1252,128 +1474,16 @@ export class CrossOriginStorageBridge extends AdvancedSettingsProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Initialization
|
|
||||||
|
|
||||||
private _resolveReady(success: boolean, data?: any) {
|
|
||||||
if ( this.manager )
|
|
||||||
this.manager.log.info(`COSB ready in ${(performance.now() - this._start_time).toFixed(5)}ms`);
|
|
||||||
|
|
||||||
this.resolved_ready = true;
|
|
||||||
this.ready = success;
|
|
||||||
|
|
||||||
if ( success && this._ready_wait_resolve )
|
|
||||||
this._ready_wait_resolve();
|
|
||||||
else if ( ! success && this._ready_wait_fail )
|
|
||||||
this._ready_wait_fail(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
awaitReady() {
|
|
||||||
if ( this.resolved_ready ) {
|
|
||||||
if ( this.ready )
|
|
||||||
return Promise.resolve();
|
|
||||||
return Promise.reject();
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( this._ready_wait )
|
|
||||||
return this._ready_wait;
|
|
||||||
|
|
||||||
return this._ready_wait = new Promise<void>((resolve, fail) => {
|
|
||||||
this._ready_wait_resolve = resolve;
|
|
||||||
this._ready_wait_fail = fail;
|
|
||||||
|
|
||||||
}).finally(() => {
|
|
||||||
this._ready_wait = null;
|
|
||||||
this._ready_wait_resolve = null;
|
|
||||||
this._ready_wait_fail = null;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Provider Methods
|
|
||||||
|
|
||||||
get<T>(key: string, default_value?: T): T {
|
|
||||||
return this._cached.has(key)
|
|
||||||
? this._cached.get(key)
|
|
||||||
: default_value;
|
|
||||||
}
|
|
||||||
|
|
||||||
set(key: string, value: any) {
|
|
||||||
if ( value === undefined ) {
|
|
||||||
if ( this.has(key) )
|
|
||||||
this.delete(key);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this._cached.set(key, value);
|
|
||||||
this.rpc({ffz_type: 'set', key, value})
|
|
||||||
.catch(err => this.manager.log.error('Error setting value', err));
|
|
||||||
this.emit('set', key, value, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
delete(key: string) {
|
|
||||||
this._cached.delete(key);
|
|
||||||
this.rpc({ffz_type: 'delete', key})
|
|
||||||
.catch(err => this.manager.log.error('Error deleting value', err));
|
|
||||||
this.emit('set', key, undefined, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
clear() {
|
|
||||||
const old_cache = this._cached;
|
|
||||||
this._cached = new Map;
|
|
||||||
for(const key of old_cache.keys())
|
|
||||||
this.emit('changed', key, undefined, true);
|
|
||||||
|
|
||||||
this.rpc('clear')
|
|
||||||
.catch(err => this.manager.log.error('Error clearing storage', err));
|
|
||||||
}
|
|
||||||
|
|
||||||
has(key: string) { return this._cached.has(key); }
|
|
||||||
keys() { return this._cached.keys(); }
|
|
||||||
entries() { return this._cached.entries(); }
|
|
||||||
get size() { return this._cached.size; }
|
|
||||||
|
|
||||||
async flush() {
|
|
||||||
await this.rpc('flush');
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Provider Methods: Blobs
|
|
||||||
|
|
||||||
async getBlob(key: string) {
|
|
||||||
const msg = await this.rpc({ffz_type: 'get-blob', key});
|
|
||||||
return msg ? deserializeBlob(msg) : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
async setBlob(key: string, value: BlobLike) {
|
|
||||||
await this.rpc({
|
|
||||||
ffz_type: 'set-blob',
|
|
||||||
key,
|
|
||||||
value: await serializeBlob(value)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async deleteBlob(key: string) {
|
|
||||||
await this.rpc({
|
|
||||||
ffz_type: 'delete-blob',
|
|
||||||
key
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async hasBlob(key: string) {
|
|
||||||
return this.rpc({ffz_type: 'has-blob', key});
|
|
||||||
}
|
|
||||||
|
|
||||||
async clearBlobs() {
|
|
||||||
await this.rpc('clear-blobs');
|
|
||||||
}
|
|
||||||
|
|
||||||
async blobKeys() {
|
|
||||||
return this.rpc('blob-keys');
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// CORS Communication
|
// CORS Communication
|
||||||
|
|
||||||
|
onMessage(event: MessageEvent) {
|
||||||
|
const msg = event.data;
|
||||||
|
if ( ! msg || ! msg.ffz_type )
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.handleMessage(msg);
|
||||||
|
}
|
||||||
|
|
||||||
send(msg: string | CorsMessage, transfer?: OptionalArray<Transferable>) {
|
send(msg: string | CorsMessage, transfer?: OptionalArray<Transferable>) {
|
||||||
if ( typeof msg === 'string' )
|
if ( typeof msg === 'string' )
|
||||||
msg = {ffz_type: msg} as any;
|
msg = {ffz_type: msg} as any;
|
||||||
|
@ -1390,90 +1500,138 @@ export class CrossOriginStorageBridge extends AdvancedSettingsProvider {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
rpc<K extends keyof CorsRpcTypes>(
|
|
||||||
msg: K | RPCInputMessage<K>,
|
|
||||||
transfer?: OptionalArray<Transferable>
|
|
||||||
) {
|
|
||||||
const id = ++this._last_id;
|
|
||||||
|
|
||||||
return new Promise<CorsOutput<K>>((resolve,fail) => {
|
|
||||||
this._rpc.set(id, [resolve, fail]);
|
|
||||||
let out: CorsMessage;
|
|
||||||
if ( typeof msg === 'string' )
|
|
||||||
out = {ffz_type: msg} as CorsMessage;
|
|
||||||
else
|
|
||||||
out = msg as unknown as CorsMessage;
|
|
||||||
|
|
||||||
out.id = id;
|
|
||||||
this.send(out, transfer);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMessage(event: MessageEvent) {
|
|
||||||
const msg = event.data;
|
// ============================================================================
|
||||||
if ( ! msg || ! msg.ffz_type )
|
// ExtensionProvider
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export class ExtensionProvider extends RemoteSettingsProvider {
|
||||||
|
|
||||||
|
// Static Stuff
|
||||||
|
|
||||||
|
static supported() { return EXTENSION }
|
||||||
|
|
||||||
|
static hasContent() {
|
||||||
|
if ( ! ExtensionProvider.supported() )
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// We need a promise since we need to message the extension and
|
||||||
|
// request to know if it has keys or not.
|
||||||
|
return new Promise<boolean>((resolve) => {
|
||||||
|
let responded = false,
|
||||||
|
timeout: ReturnType<typeof setTimeout> | null = null ;
|
||||||
|
|
||||||
|
const listener = (evt: MessageEvent<any>) => {
|
||||||
|
if (evt.source !== window)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if ( msg.ffz_type === 'ready' )
|
if (evt.data && evt.data.type === 'ffz_from_ext') {
|
||||||
this.rpc('init-load').then(msg => {
|
const msg = evt.data.data,
|
||||||
this._blobs = msg.blobs;
|
type = msg?.ffz_type;
|
||||||
for(const [key, value] of Object.entries(msg.values))
|
|
||||||
this._cached.set(key, value);
|
|
||||||
|
|
||||||
this._resolveReady(true);
|
if (type === 'has-keys') {
|
||||||
|
responded = true;
|
||||||
|
resolve(msg.value);
|
||||||
|
cleanup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
}).catch(err => {
|
const cleanup = () => {
|
||||||
this._resolveReady(false, err);
|
if (!responded) {
|
||||||
|
responded = true;
|
||||||
|
resolve(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (timeout) {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
timeout = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.removeEventListener('message', listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('message', listener);
|
||||||
|
|
||||||
|
window.postMessage({
|
||||||
|
type: 'ffz_to_ext',
|
||||||
|
data: {
|
||||||
|
ffz_type: 'check-has-keys'
|
||||||
|
}
|
||||||
|
}, '*');
|
||||||
|
|
||||||
|
timeout = setTimeout(cleanup, 1000);
|
||||||
});
|
});
|
||||||
|
|
||||||
else if ( msg.ffz_type === 'change' )
|
|
||||||
this.onChange(msg);
|
|
||||||
|
|
||||||
else if ( msg.ffz_type === 'change-blob' )
|
|
||||||
this.emit('changed-blob', msg.key, msg.deleted);
|
|
||||||
|
|
||||||
else if ( msg.ffz_type === 'clear-blobs' )
|
|
||||||
this.emit('clear-blobs');
|
|
||||||
|
|
||||||
else if ( msg.ffz_type === 'reply' || msg.ffz_type === 'reply-error' )
|
|
||||||
this.onReply(msg);
|
|
||||||
|
|
||||||
else
|
|
||||||
this.manager.log.warn('Unknown Message', msg.ffz_type, msg);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onChange(msg: RPCInputMessage<'change'>) {
|
static priority = 101;
|
||||||
const key = msg.key,
|
static title = 'Browser Extension Storage';
|
||||||
value = msg.value,
|
static description = 'This provider uses a browser extension service worker to store settings in a location that should not suffer from issues due to storage partitioning or cache clearing.';
|
||||||
deleted = msg.deleted;
|
|
||||||
|
|
||||||
if ( deleted ) {
|
static allowTransfer = true;
|
||||||
this._cached.delete(key);
|
static shouldUpdate = true;
|
||||||
this.emit('changed', key, undefined, true);
|
|
||||||
} else {
|
// State and Storage
|
||||||
this._cached.set(key, value);
|
|
||||||
this.emit('changed', key, value, false);
|
constructor(manager: SettingsManager) {
|
||||||
|
super(manager);
|
||||||
|
|
||||||
|
this.onExtMessage = this.onExtMessage.bind(this);
|
||||||
|
window.addEventListener('message', this.onExtMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stuff
|
||||||
|
|
||||||
|
broadcastTransfer() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
disableEvents() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onReply(msg: CorsReplyMessage | CorsReplyErrorMessage) {
|
}
|
||||||
const id = msg.id,
|
|
||||||
success = msg.ffz_type === 'reply',
|
|
||||||
cbs = this._rpc.get(id);
|
|
||||||
if ( ! cbs )
|
|
||||||
return this.manager.log.warn('Received reply for unknown ID', id);
|
|
||||||
|
|
||||||
this._rpc.delete(id);
|
|
||||||
if ( success )
|
|
||||||
cbs[0](msg.reply);
|
|
||||||
else
|
|
||||||
cbs[1]();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
type CorsRpcTypes = {
|
type CorsRpcTypes = {
|
||||||
|
|
||||||
|
'ready': {
|
||||||
|
input: void;
|
||||||
|
output: void;
|
||||||
|
};
|
||||||
|
|
||||||
'load': {
|
'load': {
|
||||||
input: void;
|
input: void;
|
||||||
output: Record<string, any>;
|
output: Record<string, any>;
|
||||||
|
@ -1531,6 +1689,14 @@ type CorsRpcTypes = {
|
||||||
output: void;
|
output: void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
'change-blob': {
|
||||||
|
input: {
|
||||||
|
key: string;
|
||||||
|
deleted: boolean;
|
||||||
|
};
|
||||||
|
output: void;
|
||||||
|
}
|
||||||
|
|
||||||
'delete-blob': {
|
'delete-blob': {
|
||||||
input: {
|
input: {
|
||||||
key: string;
|
key: string;
|
||||||
|
@ -1595,6 +1761,7 @@ export const Providers: Record<string, typeof SettingsProvider> = {
|
||||||
|
|
||||||
local: LocalStorageProvider,
|
local: LocalStorageProvider,
|
||||||
idb: IndexedDBProvider,
|
idb: IndexedDBProvider,
|
||||||
cosb: CrossOriginStorageBridge
|
cosb: CrossOriginStorageBridge,
|
||||||
|
//ext: ExtensionProvider
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -292,6 +292,15 @@ export default class ChatHook extends Module {
|
||||||
|
|
||||||
// Settings
|
// Settings
|
||||||
|
|
||||||
|
this.settings.add('chat.subs.native', {
|
||||||
|
default: false,
|
||||||
|
ui: {
|
||||||
|
path: 'Chat > Appearance >> Subscriptions',
|
||||||
|
title: 'Display subscription notices using Twitch\'s native UI.',
|
||||||
|
component: 'setting-check-box'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
this.settings.add('chat.filtering.show-reasons', {
|
this.settings.add('chat.filtering.show-reasons', {
|
||||||
default: false,
|
default: false,
|
||||||
ui: {
|
ui: {
|
||||||
|
@ -1671,7 +1680,7 @@ export default class ChatHook extends Module {
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if ( event.prefix === 'pinned-chat-updates-v1' ) {
|
if ( event.prefix === 'pinned-chat-updates-v1' ) {
|
||||||
this.log.info('Pinned Chat', event);
|
this.log.debug('Pinned Chat', event);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2686,7 +2695,7 @@ export default class ChatHook extends Module {
|
||||||
if ( t.chat.context.get('chat.filtering.blocked-types').has('Subscription') )
|
if ( t.chat.context.get('chat.filtering.blocked-types').has('Subscription') )
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if ( t.disable_handling )
|
if ( t.disable_handling || t.chat.context.get('chat.subs.native') )
|
||||||
return old_sub.call(i, e);
|
return old_sub.call(i, e);
|
||||||
|
|
||||||
if ( t.chat.context.get('chat.subs.show') < 3 )
|
if ( t.chat.context.get('chat.subs.show') < 3 )
|
||||||
|
@ -2772,7 +2781,7 @@ export default class ChatHook extends Module {
|
||||||
if ( t.chat.context.get('chat.filtering.blocked-types').has('Resubscription') )
|
if ( t.chat.context.get('chat.filtering.blocked-types').has('Resubscription') )
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if ( t.disable_handling )
|
if ( t.disable_handling || t.chat.context.get('chat.subs.native') )
|
||||||
return old_resub.call(i, e);
|
return old_resub.call(i, e);
|
||||||
|
|
||||||
if ( t.chat.context.get('chat.subs.show') < 2 && ! e.body )
|
if ( t.chat.context.get('chat.subs.show') < 2 && ! e.body )
|
||||||
|
@ -2811,7 +2820,7 @@ export default class ChatHook extends Module {
|
||||||
if ( t.chat.context.get('chat.filtering.blocked-types').has('SubGift') )
|
if ( t.chat.context.get('chat.filtering.blocked-types').has('SubGift') )
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if ( t.disable_handling )
|
if ( t.disable_handling || t.chat.context.get('chat.subs.native') )
|
||||||
return old_subgift.call(i, e);
|
return old_subgift.call(i, e);
|
||||||
|
|
||||||
const key = `${e.channel}:${e.user.userID}`,
|
const key = `${e.channel}:${e.user.userID}`,
|
||||||
|
@ -2887,7 +2896,7 @@ export default class ChatHook extends Module {
|
||||||
if ( t.chat.context.get('chat.filtering.blocked-types').has('AnonSubGift') )
|
if ( t.chat.context.get('chat.filtering.blocked-types').has('AnonSubGift') )
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if ( t.disable_handling )
|
if ( t.disable_handling || t.chat.context.get('chat.subs.native') )
|
||||||
return old_anonsubgift.call(i, e);
|
return old_anonsubgift.call(i, e);
|
||||||
|
|
||||||
const key = `${e.channel}:ANON`,
|
const key = `${e.channel}:ANON`,
|
||||||
|
@ -2944,7 +2953,7 @@ export default class ChatHook extends Module {
|
||||||
if ( t.chat.context.get('chat.filtering.blocked-types').has('SubMysteryGift') )
|
if ( t.chat.context.get('chat.filtering.blocked-types').has('SubMysteryGift') )
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if ( t.disable_handling )
|
if ( t.disable_handling || t.chat.context.get('chat.subs.native') )
|
||||||
return old_submystery.call(i, e);
|
return old_submystery.call(i, e);
|
||||||
|
|
||||||
let mystery = null;
|
let mystery = null;
|
||||||
|
@ -2983,7 +2992,7 @@ export default class ChatHook extends Module {
|
||||||
if ( t.chat.context.get('chat.filtering.blocked-types').has('AnonSubMysteryGift') )
|
if ( t.chat.context.get('chat.filtering.blocked-types').has('AnonSubMysteryGift') )
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if ( t.disable_handling )
|
if ( t.disable_handling || t.chat.context.get('chat.subs.native') )
|
||||||
return old_anonsubmystery.call(i, e);
|
return old_anonsubmystery.call(i, e);
|
||||||
|
|
||||||
let mystery = null;
|
let mystery = null;
|
||||||
|
|
|
@ -54,6 +54,8 @@ const CLASSES = {
|
||||||
|
|
||||||
'pinned-hype-chat': '.paid-pinned-chat-message-list',
|
'pinned-hype-chat': '.paid-pinned-chat-message-list',
|
||||||
|
|
||||||
|
'side-stories': '.side-nav__title + div[class*=storiesLeftNavSection]',
|
||||||
|
|
||||||
'ci-mod-view': '.chat-input__buttons-container a[href*="/moderator"]',
|
'ci-mod-view': '.chat-input__buttons-container a[href*="/moderator"]',
|
||||||
'ci-highlight-settings': '.chat-input__buttons-container button[data-highlight-selector="chat-highlights-shortcut"]',
|
'ci-highlight-settings': '.chat-input__buttons-container button[data-highlight-selector="chat-highlights-shortcut"]',
|
||||||
'ci-shield-mode': '.chat-input__buttons-container > div:last-child button[class|="ScCoreButton"]:not([data-highlight-selector]):not([data-a-target])'
|
'ci-shield-mode': '.chat-input__buttons-container > div:last-child button[class|="ScCoreButton"]:not([data-highlight-selector]):not([data-a-target])'
|
||||||
|
@ -140,6 +142,15 @@ export default class CSSTweaks extends Module {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.settings.add('layout.side-nav.hide-stories', {
|
||||||
|
default: false,
|
||||||
|
ui: {
|
||||||
|
path: 'Appearance > Layout >> Side Navigation',
|
||||||
|
title: 'Hide Stories',
|
||||||
|
component: 'setting-check-box'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
this.settings.add('layout.side-nav.show', {
|
this.settings.add('layout.side-nav.show', {
|
||||||
default: 1,
|
default: 1,
|
||||||
requires: ['layout.use-portrait'],
|
requires: ['layout.use-portrait'],
|
||||||
|
@ -522,6 +533,7 @@ export default class CSSTweaks extends Module {
|
||||||
this.toggleHide('celebration', ! this.settings.get('channel.show-celebrations'));
|
this.toggleHide('celebration', ! this.settings.get('channel.show-celebrations'));
|
||||||
|
|
||||||
this.settings.getChanges('layout.subtember', val => this.toggleHide('subtember', !val));
|
this.settings.getChanges('layout.subtember', val => this.toggleHide('subtember', !val));
|
||||||
|
this.settings.getChanges('layout.side-nav.hide-stories', val => this.toggleHide('side-stories', val));
|
||||||
|
|
||||||
this.updateFont();
|
this.updateFont();
|
||||||
this.updateTopNav();
|
this.updateTopNav();
|
||||||
|
|
|
@ -10,16 +10,21 @@
|
||||||
|
|
||||||
transition: top ease-in-out 75ms, bottom ease-in-out 75ms;
|
transition: top ease-in-out 75ms, bottom ease-in-out 75ms;
|
||||||
|
|
||||||
|
.top-nav__search-container,
|
||||||
.tw-svg__asset--logotwitch {
|
.tw-svg__asset--logotwitch {
|
||||||
visibility: hidden;
|
transition: opacity ease-in-out 75ms;
|
||||||
|
opacity: 0;
|
||||||
|
//visibility: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:focus-within,
|
&:focus-within,
|
||||||
&:hover {
|
&:hover {
|
||||||
top: 0 !important;
|
top: 0 !important;
|
||||||
|
|
||||||
|
.top-nav__search-container,
|
||||||
.tw-svg__asset--logotwitch {
|
.tw-svg__asset--logotwitch {
|
||||||
visibility: visible;
|
opacity: 1;
|
||||||
|
//visibility: visible;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -211,6 +211,57 @@ export default class Directory extends Module {
|
||||||
changed: () => this.DirectoryShelf.forceUpdate()
|
changed: () => this.DirectoryShelf.forceUpdate()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
this.settings.add('directory.block-users', {
|
||||||
|
default: [],
|
||||||
|
type: 'array_merge',
|
||||||
|
always_inherit: true,
|
||||||
|
ui: {
|
||||||
|
path: 'Directory > Channels >> Block by Username',
|
||||||
|
component: 'basic-terms',
|
||||||
|
words: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.settings.add('__filter:directory.block-users', {
|
||||||
|
requires: ['directory.block-users'],
|
||||||
|
equals: 'requirements',
|
||||||
|
process(ctx) {
|
||||||
|
const val = ctx.get('directory.block-users');
|
||||||
|
if ( ! val || ! val.length )
|
||||||
|
return null;
|
||||||
|
|
||||||
|
const out = [[], []];
|
||||||
|
|
||||||
|
for(const item of val) {
|
||||||
|
const t = item.t;
|
||||||
|
let v = item.v;
|
||||||
|
|
||||||
|
if ( t === 'glob' )
|
||||||
|
v = glob_to_regex(v);
|
||||||
|
|
||||||
|
else if ( t !== 'raw' )
|
||||||
|
v = escape_regex(v);
|
||||||
|
|
||||||
|
if ( ! v || ! v.length )
|
||||||
|
continue;
|
||||||
|
|
||||||
|
out[item.s ? 0 : 1].push(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
out[0].length
|
||||||
|
? new RegExp(`^(?:${out[0].join('|')})$`)
|
||||||
|
: null,
|
||||||
|
out[1].length
|
||||||
|
? new RegExp(`^(?:${out[1].join('|')})$`, 'i')
|
||||||
|
: null
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
|
changed: () => this.updateCards()
|
||||||
|
});
|
||||||
|
|
||||||
this.settings.add('directory.block-titles', {
|
this.settings.add('directory.block-titles', {
|
||||||
default: [],
|
default: [],
|
||||||
type: 'array_merge',
|
type: 'array_merge',
|
||||||
|
@ -702,6 +753,19 @@ export default class Directory extends Module {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ( ! should_hide ) {
|
||||||
|
const regexes = this.settings.get('__filter:directory.block-users');
|
||||||
|
if ( regexes ) {
|
||||||
|
if ( regexes[0] )
|
||||||
|
regexes[0].lastIndex = -1;
|
||||||
|
if ( regexes[1] )
|
||||||
|
regexes[1].lastIndex = -1;
|
||||||
|
|
||||||
|
if (( regexes[0] && regexes[0].test(props.channelLogin) ) || ( regexes[1] && regexes[1].test(props.channelLogin) ))
|
||||||
|
should_hide = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if ( ! should_hide ) {
|
if ( ! should_hide ) {
|
||||||
const regexes = this.settings.get('__filter:directory.block-titles');
|
const regexes = this.settings.get('__filter:directory.block-titles');
|
||||||
if ( regexes ) {
|
if ( regexes ) {
|
||||||
|
|
|
@ -383,6 +383,20 @@ export default class Layout extends Module {
|
||||||
if ( props?.isPromoted && this.settings.get('directory.hide-promoted') )
|
if ( props?.isPromoted && this.settings.get('directory.hide-promoted') )
|
||||||
should_hide = true;
|
should_hide = true;
|
||||||
else {
|
else {
|
||||||
|
if ( ! should_hide ) {
|
||||||
|
const regexes = this.settings.get('__filter:directory.block-users');
|
||||||
|
const login = props.userLogin;
|
||||||
|
if ( regexes && login ) {
|
||||||
|
if ( regexes[0] )
|
||||||
|
regexes[0].lastIndex = -1;
|
||||||
|
if ( regexes[1] )
|
||||||
|
regexes[1].lastIndex = -1;
|
||||||
|
if ( (regexes[0] && regexes[0].test(login)) || (regexes[1] && regexes[1].test(login)) )
|
||||||
|
should_hide = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! should_hide) {
|
||||||
const regexes = this.settings.get('__filter:directory.block-titles');
|
const regexes = this.settings.get('__filter:directory.block-titles');
|
||||||
const title = stream?.broadcaster?.broadcastSettings?.title;
|
const title = stream?.broadcaster?.broadcastSettings?.title;
|
||||||
if ( regexes && title ) {
|
if ( regexes && title ) {
|
||||||
|
@ -394,6 +408,7 @@ export default class Layout extends Module {
|
||||||
should_hide = true;
|
should_hide = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
card.classList.toggle('ffz--side-nav-card-rerun', rerun);
|
card.classList.toggle('ffz--side-nav-card-rerun', rerun);
|
||||||
card.classList.toggle('ffz--side-nav-card-offline', offline);
|
card.classList.toggle('ffz--side-nav-card-offline', offline);
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
|
|
||||||
import {has} from 'utilities/object';
|
import {has} from 'utilities/object';
|
||||||
import type { DomFragment, OptionalArray } from './types';
|
import type { DomFragment } from './types';
|
||||||
import { DEBUG } from './constants';
|
import { DEBUG } from './constants';
|
||||||
|
|
||||||
const ATTRS = [
|
const ATTRS = [
|
||||||
|
@ -242,6 +242,9 @@ export function setChildren(
|
||||||
no_sanitize: boolean = false,
|
no_sanitize: boolean = false,
|
||||||
no_empty: boolean = false
|
no_empty: boolean = false
|
||||||
) {
|
) {
|
||||||
|
if (no_sanitize)
|
||||||
|
window.FrankerFaceZ.get().log.warn('call to setChildren with no_sanitize set to true -- this is no longer supported');
|
||||||
|
|
||||||
if (children instanceof Node ) {
|
if (children instanceof Node ) {
|
||||||
if (! no_empty )
|
if (! no_empty )
|
||||||
element.innerHTML = '';
|
element.innerHTML = '';
|
||||||
|
@ -260,15 +263,20 @@ export function setChildren(
|
||||||
else if (child) {
|
else if (child) {
|
||||||
const val = typeof child === 'string' ? child : String(child);
|
const val = typeof child === 'string' ? child : String(child);
|
||||||
|
|
||||||
element.appendChild(no_sanitize ?
|
// We no longer support no_sanitize
|
||||||
range.createContextualFragment(val) : document.createTextNode(val));
|
//element.appendChild(no_sanitize ?
|
||||||
|
// range.createContextualFragment(val) : document.createTextNode(val));
|
||||||
|
|
||||||
|
element.appendChild(document.createTextNode(val));
|
||||||
}
|
}
|
||||||
|
|
||||||
} else if (children) {
|
} else if (children) {
|
||||||
const val = typeof children === 'string' ? children : String(children);
|
const val = typeof children === 'string' ? children : String(children);
|
||||||
|
|
||||||
element.appendChild(no_sanitize ?
|
// We no longer support no_sanitize
|
||||||
range.createContextualFragment(val) : document.createTextNode(val));
|
//element.appendChild(no_sanitize ?
|
||||||
|
// range.createContextualFragment(val) : document.createTextNode(val));
|
||||||
|
element.appendChild(document.createTextNode(val));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -248,7 +248,14 @@ export interface ExperimentTypeMap {
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface ModuleEventMap {
|
export interface ModuleEventMap {
|
||||||
|
'main_menu': MainMenuEvents;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type MainMenuEvents = {
|
||||||
|
':open': [event?: {
|
||||||
|
item?: string;
|
||||||
|
event?: MouseEvent;
|
||||||
|
}]
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface ModuleMap {
|
export interface ModuleMap {
|
||||||
|
|
31
src/worker.ts
Normal file
31
src/worker.ts
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
const browser = ((globalThis as any).browser ?? globalThis.chrome) as typeof globalThis.chrome;
|
||||||
|
|
||||||
|
browser.runtime.onInstalled.addListener(() => {
|
||||||
|
browser.action.disable();
|
||||||
|
});
|
||||||
|
|
||||||
|
browser.action.onClicked.addListener(tab => {
|
||||||
|
if ( ! tab?.id )
|
||||||
|
return;
|
||||||
|
|
||||||
|
browser.tabs.sendMessage(tab.id, {
|
||||||
|
type: 'ffz_to_page',
|
||||||
|
data: {
|
||||||
|
ffz_type: 'open-settings'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||||
|
const type = message?.type;
|
||||||
|
if ( ! type || ! sender?.tab?.id )
|
||||||
|
return;
|
||||||
|
|
||||||
|
if ( type === 'ffz_not_supported' )
|
||||||
|
browser.action.disable(sender.tab.id);
|
||||||
|
|
||||||
|
else if ( type === 'ffz_injecting' )
|
||||||
|
browser.action.enable(sender.tab.id);
|
||||||
|
|
||||||
|
console.log('got message', message, sender);
|
||||||
|
});
|
|
@ -54,6 +54,20 @@ const ENTRY_POINTS = {
|
||||||
clips: './src/clips.js'
|
clips: './src/clips.js'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if ( FOR_EXTENSION )
|
||||||
|
ENTRY_POINTS.worker = './src/worker.ts';
|
||||||
|
|
||||||
|
const COPY_PATTERNS = [
|
||||||
|
{
|
||||||
|
from: FOR_EXTENSION
|
||||||
|
? './src/entry_ext.js'
|
||||||
|
: './src/entry.js',
|
||||||
|
to: (DEV_SERVER || DEV_BUILD)
|
||||||
|
? 'script.js'
|
||||||
|
: 'script.min.js'
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
const TARGET = 'es2020';
|
const TARGET = 'es2020';
|
||||||
|
|
||||||
/** @type {import('webpack').Configuration} */
|
/** @type {import('webpack').Configuration} */
|
||||||
|
@ -125,16 +139,7 @@ const config = {
|
||||||
|
|
||||||
plugins: [
|
plugins: [
|
||||||
new CopyPlugin({
|
new CopyPlugin({
|
||||||
patterns: [
|
patterns: COPY_PATTERNS
|
||||||
{
|
|
||||||
from: FOR_EXTENSION
|
|
||||||
? './src/entry_ext.js'
|
|
||||||
: './src/entry.js',
|
|
||||||
to: (DEV_SERVER || DEV_BUILD)
|
|
||||||
? 'script.js'
|
|
||||||
: 'script.min.js'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}),
|
}),
|
||||||
new VueLoaderPlugin(),
|
new VueLoaderPlugin(),
|
||||||
new EsbuildPlugin({
|
new EsbuildPlugin({
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue