1
0
Fork 0
mirror of https://github.com/FrankerFaceZ/FrankerFaceZ.git synced 2025-06-27 12:55:55 +00:00
* 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:
SirStendec 2024-10-09 17:09:09 -04:00
parent 533bf52c9e
commit 1ee737f2ca
17 changed files with 669 additions and 280 deletions

View file

@ -1,7 +1,7 @@
{
"name": "frankerfacez",
"author": "Dan Salvato LLC",
"version": "4.74.1",
"version": "4.75.0",
"description": "FrankerFaceZ is a Twitch enhancement suite.",
"private": true,
"license": "Apache-2.0",
@ -11,7 +11,9 @@
"clean": "rimraf dist",
"dev": "cross-env NODE_ENV=development 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:ext": "cross-env NODE_ENV=production FFZ_EXTENSION=true webpack build",
"build:stats": "cross-env NODE_ENV=production webpack build --json > stats.json",
"build:prod": "cross-env NODE_ENV=production webpack build",
"build:dev": "cross-env NODE_ENV=development webpack build",
@ -25,6 +27,7 @@
},
"devDependencies": {
"@ffz/fontello-cli": "^1.0.4",
"@types/chrome": "^0.0.277",
"@types/crypto-js": "^4.2.1",
"@types/js-cookie": "^3.0.6",
"@types/safe-regex": "^1.1.6",

24
pnpm-lock.yaml generated
View file

@ -97,6 +97,9 @@ devDependencies:
'@ffz/fontello-cli':
specifier: ^1.0.4
version: 1.0.4
'@types/chrome':
specifier: ^0.0.277
version: 0.0.277
'@types/crypto-js':
specifier: ^4.2.1
version: 4.2.1
@ -588,6 +591,13 @@ packages:
'@types/node': 20.5.7
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:
resolution: {integrity: sha512-4x5FkPpLipqwthjPsF7ZRbOv3uoLUFkTA9G9v583qi4pACvq0uTELrB8OLUzPWUI4IJIyvM85vzkV1nyiI2Lig==}
dependencies:
@ -641,6 +651,20 @@ packages:
'@types/serve-static': 1.15.2
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:
resolution: {integrity: sha512-/K3ds8TRAfBvi5vfjuz8y6+GiAYBZ0x4tXv1Av6CWBWn0IlADc+ZX9pMq7oU0fNQPnBwIZl3rmeLp6SBApbxSQ==}
dev: true

View file

@ -1,16 +1,52 @@
/* eslint strict: off */
'use strict';
(() => {
// Don't run on certain sub-domains.
if ( /^(?:localhost\.rig|blog|im|chatdepot|tmi|api|brand|dev|gql|passport)\./.test(location.hostname) )
const browser = globalThis.browser ?? globalThis.chrome;
if (
// Don't run on certain sub-domains.
/^(?: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;
}
if ( /disable_frankerfacez/.test(location.search) )
return;
// Make sure to wake the service worker up early.
browser.runtime.sendMessage({
type: 'ffz_injecting'
});
const browser = globalThis.browser ?? globalThis.chrome,
// Set up the extension message bridge.
window.addEventListener('message', evt => {
if (evt.source !== window)
return;
HOST = location.hostname,
if (evt.data && evt.data.type === 'ffz_to_ext')
browser.runtime.sendMessage(evt.data.data, resp => {
if (resp)
window.postMessage({
type: 'ffz_from_ext',
data: resp
}, '*');
});
});
browser.runtime.onMessage.addListener((msg, sender) => {
window.postMessage({
type: 'ffz_from_ext',
data: msg
}, '*');
return true;
});
// Now, inject our script into the page context.
const HOST = location.hostname,
SERVER = browser.runtime.getURL("web"),
script = document.createElement('script');

View file

@ -34,7 +34,7 @@ class FFZESBridge {
document.addEventListener('readystatechange', event => {
if ( document.documentElement )
document.documentElement.dataset.ffzEsbridge = true;
document.documentElement.dataset.ffzEsbridge = true;
});
}

View file

@ -93,7 +93,7 @@ export default {
return;
this.uploading = true;
const response = await fetch('https://putco.de', {
const response = await fetch('https://logs.frankerfacez.com', {
method: 'PUT',
body: this.text
});
@ -120,4 +120,4 @@ export default {
}
}
</script>
</script>

View file

@ -255,11 +255,11 @@ export default class MainMenu extends Module {
// 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.
const layout = this.resolve('site.layout'),
item = evt.item,
event = evt.event;
item = evt?.item,
event = evt?.event;
if ( (layout && layout.is_minimal) || (event && (event.ctrlKey || event.shiftKey)) ) {
if ( ! this.openPopout(item) )
if ( ! this.openPopout(item) && evt )
evt.errored = true;
return;
}

View file

@ -245,7 +245,10 @@ export default class SettingsManager extends Module<'settings', SettingsEvents>
window.addEventListener('message', event => {
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._updateContextProxies(event.source);
}

View file

@ -11,6 +11,7 @@ import {EventEmitter} from 'utilities/events';
import {TicketLock, has, once} from 'utilities/object';
import type SettingsManager from '.';
import type { OptionalArray, OptionalPromise, ProviderTypeMap } from '../utilities/types';
import { EXTENSION } from '../utilities/constants';
const DB_VERSION = 1,
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
// ============================================================================
export class CrossOriginStorageBridge extends AdvancedSettingsProvider {
export class CrossOriginStorageBridge extends RemoteSettingsProvider {
// Static Stuff
@ -1194,36 +1444,11 @@ export class CrossOriginStorageBridge extends AdvancedSettingsProvider {
static shouldUpdate = false;
// 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 _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) {
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');
frame.src = (this.manager.root as any).host === 'youtube' ?
'//www.youtube.com/__ffz_bridge/' :
@ -1232,13 +1457,10 @@ export class CrossOriginStorageBridge extends AdvancedSettingsProvider {
frame.style.width = '0';
frame.style.height = '0';
this._boundHandleMessage = this.onMessage.bind(this);
window.addEventListener('message', this._boundHandleMessage);
document.body.appendChild(frame);
}
this.onMessage = this.onMessage.bind(this);
get supportsBlobs() {
return this._blobs ?? false;
window.addEventListener('message', this.onMessage);
document.body.appendChild(frame);
}
// 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
onMessage(event: MessageEvent) {
const msg = event.data;
if ( ! msg || ! msg.ffz_type )
return;
this.handleMessage(msg);
}
send(msg: string | CorsMessage, transfer?: OptionalArray<Transferable>) {
if ( typeof msg === 'string' )
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 )
return;
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]();
}
}
// ============================================================================
// 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;
if (evt.data && evt.data.type === 'ffz_from_ext') {
const msg = evt.data.data,
type = msg?.ffz_type;
if (type === 'has-keys') {
responded = true;
resolve(msg.value);
cleanup();
}
}
};
const cleanup = () => {
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);
});
}
static priority = 101;
static title = 'Browser Extension Storage';
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.';
static allowTransfer = true;
static shouldUpdate = true;
// State and Storage
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);
}
}
}
type CorsRpcTypes = {
'ready': {
input: void;
output: void;
};
'load': {
input: void;
output: Record<string, any>;
@ -1531,6 +1689,14 @@ type CorsRpcTypes = {
output: void;
};
'change-blob': {
input: {
key: string;
deleted: boolean;
};
output: void;
}
'delete-blob': {
input: {
key: string;
@ -1595,6 +1761,7 @@ export const Providers: Record<string, typeof SettingsProvider> = {
local: LocalStorageProvider,
idb: IndexedDBProvider,
cosb: CrossOriginStorageBridge
cosb: CrossOriginStorageBridge,
//ext: ExtensionProvider
};

View file

@ -292,6 +292,15 @@ export default class ChatHook extends Module {
// 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', {
default: false,
ui: {
@ -1671,7 +1680,7 @@ export default class ChatHook extends Module {
return;
if ( event.prefix === 'pinned-chat-updates-v1' ) {
this.log.info('Pinned Chat', event);
this.log.debug('Pinned Chat', event);
return;
}
@ -2686,7 +2695,7 @@ export default class ChatHook extends Module {
if ( t.chat.context.get('chat.filtering.blocked-types').has('Subscription') )
return;
if ( t.disable_handling )
if ( t.disable_handling || t.chat.context.get('chat.subs.native') )
return old_sub.call(i, e);
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') )
return;
if ( t.disable_handling )
if ( t.disable_handling || t.chat.context.get('chat.subs.native') )
return old_resub.call(i, e);
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') )
return;
if ( t.disable_handling )
if ( t.disable_handling || t.chat.context.get('chat.subs.native') )
return old_subgift.call(i, e);
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') )
return;
if ( t.disable_handling )
if ( t.disable_handling || t.chat.context.get('chat.subs.native') )
return old_anonsubgift.call(i, e);
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') )
return;
if ( t.disable_handling )
if ( t.disable_handling || t.chat.context.get('chat.subs.native') )
return old_submystery.call(i, e);
let mystery = null;
@ -2983,7 +2992,7 @@ export default class ChatHook extends Module {
if ( t.chat.context.get('chat.filtering.blocked-types').has('AnonSubMysteryGift') )
return;
if ( t.disable_handling )
if ( t.disable_handling || t.chat.context.get('chat.subs.native') )
return old_anonsubmystery.call(i, e);
let mystery = null;

View file

@ -54,6 +54,8 @@ const CLASSES = {
'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-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])'
@ -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', {
default: 1,
requires: ['layout.use-portrait'],
@ -522,6 +533,7 @@ export default class CSSTweaks extends Module {
this.toggleHide('celebration', ! this.settings.get('channel.show-celebrations'));
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.updateTopNav();

View file

@ -10,17 +10,22 @@
transition: top ease-in-out 75ms, bottom ease-in-out 75ms;
.top-nav__search-container,
.tw-svg__asset--logotwitch {
visibility: hidden;
transition: opacity ease-in-out 75ms;
opacity: 0;
//visibility: hidden;
}
&:focus-within,
&:hover {
top: 0 !important;
.top-nav__search-container,
.tw-svg__asset--logotwitch {
visibility: visible;
opacity: 1;
//visibility: visible;
}
}
}
}
}

View file

@ -211,6 +211,57 @@ export default class Directory extends Module {
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', {
default: [],
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 ) {
const regexes = this.settings.get('__filter:directory.block-titles');
if ( regexes ) {

View file

@ -383,15 +383,30 @@ export default class Layout extends Module {
if ( props?.isPromoted && this.settings.get('directory.hide-promoted') )
should_hide = true;
else {
const regexes = this.settings.get('__filter:directory.block-titles');
const title = stream?.broadcaster?.broadcastSettings?.title;
if ( regexes && title ) {
if ( regexes[0] )
regexes[0].lastIndex = -1;
if ( regexes[1] )
regexes[1].lastIndex = -1;
if ( (regexes[0] && regexes[0].test(title)) || (regexes[1] && regexes[1].test(title)) )
should_hide = true;
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 title = stream?.broadcaster?.broadcastSettings?.title;
if ( regexes && title ) {
if ( regexes[0] )
regexes[0].lastIndex = -1;
if ( regexes[1] )
regexes[1].lastIndex = -1;
if ( (regexes[0] && regexes[0].test(title)) || (regexes[1] && regexes[1].test(title)) )
should_hide = true;
}
}
}

View file

@ -1,6 +1,6 @@
import {has} from 'utilities/object';
import type { DomFragment, OptionalArray } from './types';
import type { DomFragment } from './types';
import { DEBUG } from './constants';
const ATTRS = [
@ -242,6 +242,9 @@ export function setChildren(
no_sanitize: 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 (! no_empty )
element.innerHTML = '';
@ -260,15 +263,20 @@ export function setChildren(
else if (child) {
const val = typeof child === 'string' ? child : String(child);
element.appendChild(no_sanitize ?
range.createContextualFragment(val) : document.createTextNode(val));
// We no longer support no_sanitize
//element.appendChild(no_sanitize ?
// range.createContextualFragment(val) : document.createTextNode(val));
element.appendChild(document.createTextNode(val));
}
} else if (children) {
const val = typeof children === 'string' ? children : String(children);
element.appendChild(no_sanitize ?
range.createContextualFragment(val) : document.createTextNode(val));
// We no longer support no_sanitize
//element.appendChild(no_sanitize ?
// range.createContextualFragment(val) : document.createTextNode(val));
element.appendChild(document.createTextNode(val));
}
}

View file

@ -248,7 +248,14 @@ export interface ExperimentTypeMap {
};
export interface ModuleEventMap {
'main_menu': MainMenuEvents;
};
export type MainMenuEvents = {
':open': [event?: {
item?: string;
event?: MouseEvent;
}]
};
export interface ModuleMap {

31
src/worker.ts Normal file
View 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);
});

View file

@ -54,6 +54,20 @@ const ENTRY_POINTS = {
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';
/** @type {import('webpack').Configuration} */
@ -125,16 +139,7 @@ const config = {
plugins: [
new CopyPlugin({
patterns: [
{
from: FOR_EXTENSION
? './src/entry_ext.js'
: './src/entry.js',
to: (DEV_SERVER || DEV_BUILD)
? 'script.js'
: 'script.min.js'
}
]
patterns: COPY_PATTERNS
}),
new VueLoaderPlugin(),
new EsbuildPlugin({