From 136a2491c8fba808aa532a953070a30b69f54a4c Mon Sep 17 00:00:00 2001 From: SirStendec Date: Thu, 16 Nov 2023 18:41:50 -0500 Subject: [PATCH] More type progress. --- package.json | 7 +- pnpm-lock.yaml | 41 +- src/clips.js | 4 +- src/{experiments.js => experiments.ts} | 353 ++++-- src/load_tracker.ts | 14 + src/main.ts | 4 +- src/modules/chat/badges.jsx | 2 +- .../main_menu/components/chat-tester.vue | 4 +- .../main_menu/components/experiments.vue | 4 +- .../main_menu/components/home-page.vue | 21 +- .../main_menu/components/rich-feed.vue | 88 ++ src/modules/metadata.tsx | 25 +- src/modules/tooltips.ts | 10 + src/settings/{context.js => context.ts} | 174 ++- src/settings/index.ts | 207 ++-- src/settings/processors.ts | 8 +- src/settings/providers.ts | 27 +- src/settings/types.ts | 76 +- src/settings/validators.ts | 8 +- src/sites/twitch-twilight/modules/channel.jsx | 47 +- .../modules/{loadable.jsx => loadable.tsx} | 101 +- .../{sub_button.jsx => sub_button.tsx} | 81 +- src/utilities/color.js | 682 ----------- src/utilities/color.ts | 895 ++++++++++++++ .../compat/{elemental.js => elemental.ts} | 186 ++- .../compat/{fine-router.js => fine-router.ts} | 81 +- src/utilities/compat/fine.js | 798 ------------- src/utilities/compat/fine.ts | 1032 +++++++++++++++++ src/utilities/compat/react-types.ts | 75 ++ src/utilities/compat/subpump.ts | 58 +- src/utilities/events.ts | 20 +- src/utilities/{graphql.js => graphql.ts} | 43 +- src/utilities/module.ts | 12 +- src/utilities/object.ts | 41 +- src/utilities/path-parser.ts | 6 +- src/utilities/translation-core.ts | 195 +--- .../{twitch-data.js => twitch-data.ts} | 70 +- src/utilities/types.ts | 131 ++- src/utilities/{vue.js => vue.ts} | 114 +- styles/chat.scss | 4 +- tsconfig.json | 1 + types/import-types.d.ts | 12 + 42 files changed, 3451 insertions(+), 2311 deletions(-) rename src/{experiments.js => experiments.ts} (51%) create mode 100644 src/modules/main_menu/components/rich-feed.vue rename src/settings/{context.js => context.ts} (71%) rename src/sites/twitch-twilight/modules/{loadable.jsx => loadable.tsx} (53%) rename src/sites/twitch-twilight/modules/{sub_button.jsx => sub_button.tsx} (60%) delete mode 100644 src/utilities/color.js create mode 100644 src/utilities/color.ts rename src/utilities/compat/{elemental.js => elemental.ts} (51%) rename src/utilities/compat/{fine-router.js => fine-router.ts} (73%) delete mode 100644 src/utilities/compat/fine.js create mode 100644 src/utilities/compat/fine.ts create mode 100644 src/utilities/compat/react-types.ts rename src/utilities/{graphql.js => graphql.ts} (60%) rename src/utilities/{twitch-data.js => twitch-data.ts} (93%) rename src/utilities/{vue.js => vue.ts} (59%) diff --git a/package.json b/package.json index 94494133..c1680802 100755 --- a/package.json +++ b/package.json @@ -25,7 +25,10 @@ }, "devDependencies": { "@ffz/fontello-cli": "^1.0.4", + "@types/crypto-js": "^4.2.1", + "@types/js-cookie": "^3.0.6", "@types/safe-regex": "^1.1.6", + "@types/vue-clickaway": "^2.2.4", "@types/webpack-env": "^1.18.4", "browserslist": "^4.21.10", "copy-webpack-plugin": "^11.0.0", @@ -66,7 +69,7 @@ "dependencies": { "@ffz/icu-msgparser": "^2.0.0", "@popperjs/core": "^2.11.8", - "crypto-js": "^3.3.0", + "crypto-js": "^4.2.0", "dayjs": "^1.10.7", "denoflare-mqtt": "^0.0.2", "displacejs": "^1.4.1", @@ -74,7 +77,7 @@ "file-saver": "^2.0.5", "graphql": "^16.0.1", "graphql-tag": "^2.12.6", - "js-cookie": "^2.2.1", + "js-cookie": "^3.0.5", "jszip": "^3.7.1", "markdown-it": "^12.2.0", "markdown-it-link-attributes": "^3.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a543a568..ef82d740 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -18,8 +18,8 @@ dependencies: specifier: ^2.11.8 version: 2.11.8 crypto-js: - specifier: ^3.3.0 - version: 3.3.0 + specifier: ^4.2.0 + version: 4.2.0 dayjs: specifier: ^1.10.7 version: 1.10.7 @@ -42,8 +42,8 @@ dependencies: specifier: ^2.12.6 version: 2.12.6(graphql@16.0.1) js-cookie: - specifier: ^2.2.1 - version: 2.2.1 + specifier: ^3.0.5 + version: 3.0.5 jszip: specifier: ^3.7.1 version: 3.7.1 @@ -97,9 +97,18 @@ devDependencies: '@ffz/fontello-cli': specifier: ^1.0.4 version: 1.0.4 + '@types/crypto-js': + specifier: ^4.2.1 + version: 4.2.1 + '@types/js-cookie': + specifier: ^3.0.6 + version: 3.0.6 '@types/safe-regex': specifier: ^1.1.6 version: 1.1.6 + '@types/vue-clickaway': + specifier: ^2.2.4 + version: 2.2.4 '@types/webpack-env': specifier: ^1.18.4 version: 1.18.4 @@ -592,6 +601,10 @@ packages: '@types/node': 20.5.7 dev: true + /@types/crypto-js@4.2.1: + resolution: {integrity: sha512-FSPGd9+OcSok3RsM0UZ/9fcvMOXJ1ENE/ZbLfOPlBWj7BgXtEAM8VYfTtT760GiLbQIMoVozwVuisjvsVwqYWw==} + dev: true + /@types/eslint-scope@3.7.4: resolution: {integrity: sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA==} dependencies: @@ -638,6 +651,10 @@ packages: '@types/node': 20.5.7 dev: true + /@types/js-cookie@3.0.6: + resolution: {integrity: sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==} + dev: true + /@types/json-schema@7.0.9: resolution: {integrity: sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==} dev: true @@ -701,6 +718,12 @@ packages: '@types/node': 20.5.7 dev: true + /@types/vue-clickaway@2.2.4: + resolution: {integrity: sha512-Jy0dGNUrm/Fya1hY8bHM5lXJvZvlyU/rvgLEFVcjQkwNp2Z2IGNnRKS6ZH9orMDkUI7Qj0oyWp0b89VTErAS9Q==} + dependencies: + vue: 2.6.14 + dev: true + /@types/webpack-env@1.18.4: resolution: {integrity: sha512-I6e+9+HtWADAWeeJWDFQtdk4EVSAbj6Rtz4q8fJ7mSr1M0jzlFcs8/HZ+Xb5SHzVm1dxH7aUiI+A8kA8Gcrm0A==} dev: true @@ -2173,8 +2196,8 @@ packages: which: 2.0.2 dev: true - /crypto-js@3.3.0: - resolution: {integrity: sha512-DIT51nX0dCfKltpRiXV+/TVZq+Qq2NgF4644+K7Ttnla7zEzqc+kjJyiB96BHNyUTBxyjzRcZYpUdZa+QAqi6Q==} + /crypto-js@4.2.0: + resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==} dev: false /css-loader@6.8.1(webpack@5.88.2): @@ -3701,8 +3724,9 @@ packages: supports-color: 8.1.1 dev: true - /js-cookie@2.2.1: - resolution: {integrity: sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ==} + /js-cookie@3.0.5: + resolution: {integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==} + engines: {node: '>=14'} dev: false /js-tokens@3.0.2: @@ -5652,7 +5676,6 @@ packages: /vue@2.6.14: resolution: {integrity: sha512-x2284lgYvjOMj3Za7kqzRcUSxBboHqtgRE2zlos1qWaOye5yUmHn42LB1250NJBLRwEcdrB0JRwyPTEPhfQjiQ==} - dev: false /vuedraggable@2.24.3: resolution: {integrity: sha512-6/HDXi92GzB+Hcs9fC6PAAozK1RLt1ewPTLjK0anTYguXLAeySDmcnqE8IC0xa7shvSzRjQXq3/+dsZ7ETGF3g==} diff --git a/src/clips.js b/src/clips.js index a6e9443c..ce6d98b6 100644 --- a/src/clips.js +++ b/src/clips.js @@ -18,7 +18,7 @@ import StagingSelector from './staging'; import LoadTracker from './load_tracker'; import Site from './sites/clips'; -import Vue from 'utilities/vue'; +import VueModule from 'utilities/vue'; import Tooltips from 'src/modules/tooltips'; import Chat from 'src/modules/chat'; @@ -64,7 +64,7 @@ class FrankerFaceZ extends Module { this.inject('site', Site); this.inject('addons', AddonManager); - this.register('vue', Vue); + this.register('vue', VueModule); // ======================================================================== // Startup diff --git a/src/experiments.js b/src/experiments.ts similarity index 51% rename from src/experiments.js rename to src/experiments.ts index 26686f6a..3c3f80fc 100644 --- a/src/experiments.js +++ b/src/experiments.ts @@ -5,13 +5,33 @@ // ============================================================================ import {DEBUG, SERVER} from 'utilities/constants'; -import Module from 'utilities/module'; -import {has, deep_copy} from 'utilities/object'; +import Module, { GenericModule } from 'utilities/module'; +import {has, deep_copy, fetchJSON} from 'utilities/object'; import { getBuster } from 'utilities/time'; import Cookie from 'js-cookie'; import SHA1 from 'crypto-js/sha1'; +import type SettingsManager from './settings'; + +declare module 'utilities/types' { + interface ModuleMap { + experiments: ExperimentManager; + } + interface ProviderTypeMap { + 'experiment-overrides': Record + } +} + +declare global { + interface Window { + __twilightSettings?: { + experiments?: Record; + } + } +} + + const OVERRIDE_COOKIE = 'experiment_overrides', COOKIE_OPTIONS = { expires: 7, @@ -21,9 +41,68 @@ const OVERRIDE_COOKIE = 'experiment_overrides', // We want to import this so that the file is included in the output. // We don't load using this because we might want a newer file from the -// server. -import EXPERIMENTS from './experiments.json'; // eslint-disable-line no-unused-vars +// server. Because of our webpack settings, this is imported as a URL +// and not an object. +const EXPERIMENTS: string = require('./experiments.json'); +// ============================================================================ +// Data Types +// ============================================================================ + +export enum TwitchExperimentType { + Unknown = 0, + Device = 1, + User = 2, + Channel = 3 +}; + +export type ExperimentGroup = { + value: unknown; + weight: number; +}; + +export type FFZExperimentData = { + name: string; + seed?: number; + description: string; + groups: ExperimentGroup[]; +} + +export type TwitchExperimentData = { + name: string; + t: TwitchExperimentType; + v: number; + groups: ExperimentGroup[]; +}; + +export type ExperimentData = FFZExperimentData | TwitchExperimentData; + + +export type OverrideCookie = { + experiments: Record; + disabled: string[]; +}; + + +type ExperimentEvents = { + ':changed': [key: string, new_value: any, old_value: any]; + ':twitch-changed': [key: string, new_value: string | null, old_value: string | null]; + [key: `:changed:${string}`]: [new_value: any, old_value: any]; + [key: `:twitch-changed:${string}`]: [new_value: string | null, old_value: string | null]; +} + + +// ============================================================================ +// Helper Methods +// ============================================================================ + +export function isTwitchExperiment(exp: ExperimentData): exp is TwitchExperimentData { + return 't' in exp; +} + +export function isFFZExperiment(exp: ExperimentData): exp is FFZExperimentData { + return 'description' in exp; +} function sortExperimentLog(a,b) { if ( a.rarity < b.rarity ) @@ -44,9 +123,24 @@ function sortExperimentLog(a,b) { // Experiment Manager // ============================================================================ -export default class ExperimentManager extends Module { - constructor(...args) { - super(...args); +export default class ExperimentManager extends Module<'experiments', ExperimentEvents> { + + // Dependencies + settings: SettingsManager = null as any; + + // State + unique_id?: string; + experiments: Record; + + private cache: Map; + + + // Helpers + Cookie: typeof Cookie; + + + constructor(name?: string, parent?: GenericModule) { + super(name, parent); this.get = this.getAssignment; @@ -81,23 +175,23 @@ export default class ExperimentManager extends Module { unique_id: () => this.unique_id, - ffz_data: () => deep_copy(this.experiments), + ffz_data: () => deep_copy(this.experiments) ?? {}, twitch_data: () => deep_copy(this.getTwitchExperiments()), - usingTwitchExperiment: key => this.usingTwitchExperiment(key), - getTwitchAssignment: key => this.getTwitchAssignment(key), - getTwitchType: type => this.getTwitchType(type), - hasTwitchOverride: key => this.hasTwitchOverride(key), - setTwitchOverride: (key, val) => this.setTwitchOverride(key, val), - deleteTwitchOverride: key => this.deleteTwitchOverride(key), + usingTwitchExperiment: (key: string) => this.usingTwitchExperiment(key), + getTwitchAssignment: (key: string) => this.getTwitchAssignment(key), + getTwitchType: (type: TwitchExperimentType) => this.getTwitchType(type), + hasTwitchOverride: (key: string) => this.hasTwitchOverride(key), + setTwitchOverride: (key: string, val: string) => this.setTwitchOverride(key, val), + deleteTwitchOverride: (key: string) => this.deleteTwitchOverride(key), - getAssignment: key => this.getAssignment(key), - hasOverride: key => this.hasOverride(key), - setOverride: (key, val) => this.setOverride(key, val), - deleteOverride: key => this.deleteOverride(key), + getAssignment: (key: string) => this.getAssignment(key), + hasOverride: (key: string) => this.hasOverride(key), + setOverride: (key: string, val: any) => this.setOverride(key, val), + deleteOverride: (key: string) => this.deleteOverride(key), - on: (...args) => this.on(...args), - off: (...args) => this.off(...args) + on: (...args: Parameters) => this.on(...args), + off: (...args: Parameters) => this.off(...args) }); this.unique_id = Cookie.get('unique_id'); @@ -112,7 +206,7 @@ export default class ExperimentManager extends Module { if ( DEBUG ) return false; - const ts = this.settings.provider.get('exp-lock', 0); + const ts = this.settings.provider.get('exp-lock', 0); if ( isNaN(ts) || ! isFinite(ts) ) return true; @@ -129,14 +223,17 @@ export default class ExperimentManager extends Module { async loadExperiments() { - let data; + let data: Record | null; try { - data = await fetch(DEBUG ? EXPERIMENTS : `${SERVER}/script/experiments.json?_=${getBuster()}`).then(r => - r.ok ? r.json() : null); + data = await fetchJSON(DEBUG + ? EXPERIMENTS + : `${SERVER}/script/experiments.json?_=${getBuster()}` + ); } catch(err) { this.log.warn('Unable to load experiment data.', err); + return; } if ( ! data ) @@ -153,8 +250,8 @@ export default class ExperimentManager extends Module { const new_val = this.getAssignment(key); if ( old_val !== new_val ) { changed++; - this.emit(':changed', key, new_val); - this.emit(`:changed:${key}`, new_val); + this.emit(':changed', key, new_val, old_val); + this.emit(`:changed:${key}`, new_val, old_val); } } @@ -162,21 +259,25 @@ export default class ExperimentManager extends Module { //this.emit(':loaded'); } - + /** @internal */ onEnable() { this.on('pubsub:command:reload_experiments', this.loadExperiments, this); this.on('pubsub:command:update_experiment', this.updateExperiment, this); } - updateExperiment(key, data) { - this.log.info(`Received updated data for experiment "${key}" via WebSocket.`, data); + updateExperiment(key: string, data: FFZExperimentData | ExperimentGroup[]) { + this.log.info(`Received updated data for experiment "${key}" via PubSub.`, data); + + if ( Array.isArray(data) ) { + if ( ! this.experiments[key] ) + return; - if ( data.groups ) - this.experiments[key] = data; - else this.experiments[key].groups = data; + } else if ( data?.groups ) + this.experiments[key] = data; + this._rebuildKey(key); } @@ -261,7 +362,7 @@ export default class ExperimentManager extends Module { // Twitch Experiments - getTwitchType(type) { + getTwitchType(type: number) { const core = this.resolve('site')?.getCore?.(); if ( core?.experiments?.getExperimentType ) return core.experiments.getExperimentType(type); @@ -275,10 +376,9 @@ export default class ExperimentManager extends Module { return type; } - getTwitchTypeByKey(key) { - const core = this.resolve('site')?.getCore?.(), - exps = core && core.experiments, - exp = exps?.experiments?.[key]; + getTwitchTypeByKey(key: string) { + const exps = this.getTwitchExperiments(), + exp = exps?.[key]; if ( exp?.t ) return this.getTwitchType(exp.t); @@ -286,30 +386,67 @@ export default class ExperimentManager extends Module { return null; } - getTwitchExperiments() { + getTwitchExperiments(): Record { if ( window.__twilightSettings ) - return window.__twilightSettings.experiments; + return window.__twilightSettings.experiments ?? {}; const core = this.resolve('site')?.getCore?.(); - return core && core.experiments.experiments; + return core && core.experiments.experiments || {}; } - usingTwitchExperiment(key) { + usingTwitchExperiment(key: string) { const core = this.resolve('site')?.getCore?.(); return core && has(core.experiments.assignments, key) } - setTwitchOverride(key, value = null) { - const overrides = Cookie.getJSON(OVERRIDE_COOKIE) || {}; - const experiments = overrides.experiments = overrides.experiments || {}; - const disabled = overrides.disabled = overrides.disabled || []; + private _getOverrideCookie() { + const raw = Cookie.get(OVERRIDE_COOKIE); + let out: OverrideCookie; + + try { + out = raw ? JSON.parse(raw) : {}; + } catch(err) { + out = {} as OverrideCookie; + } + + if ( ! out.experiments ) + out.experiments = {}; + + if ( ! out.disabled ) + out.disabled = []; + + return out; + } + + private _saveOverrideCookie(value?: OverrideCookie) { + if ( value ) { + if ((! value.experiments || ! Object.keys(value.experiments).length) && + (! value.disabled || ! value.disabled.length) + ) + value = undefined; + } + + if ( value ) + Cookie.set(OVERRIDE_COOKIE, JSON.stringify(value), COOKIE_OPTIONS); + else + Cookie.remove(OVERRIDE_COOKIE, COOKIE_OPTIONS); + } + + + setTwitchOverride(key: string, value: string) { + const overrides = this._getOverrideCookie(), + experiments = overrides.experiments, + disabled = overrides.disabled; + experiments[key] = value; + const idx = disabled.indexOf(key); if (idx != -1) - disabled.remove(idx); - Cookie.set(OVERRIDE_COOKIE, overrides, COOKIE_OPTIONS); + disabled.splice(idx, 1); + + this._saveOverrideCookie(overrides); const core = this.resolve('site')?.getCore?.(); if ( core ) @@ -318,15 +455,17 @@ export default class ExperimentManager extends Module { this._rebuildTwitchKey(key, true, value); } - deleteTwitchOverride(key) { - const overrides = Cookie.getJSON(OVERRIDE_COOKIE), - experiments = overrides?.experiments; - if ( ! experiments || ! has(experiments, key) ) + deleteTwitchOverride(key: string) { + const overrides = this._getOverrideCookie(), + experiments = overrides.experiments; + + if ( ! has(experiments, key) ) return; const old_val = experiments[key]; delete experiments[key]; - Cookie.set(OVERRIDE_COOKIE, overrides, COOKIE_OPTIONS); + + this._saveOverrideCookie(overrides); const core = this.resolve('site')?.getCore?.(); if ( core ) @@ -335,13 +474,14 @@ export default class ExperimentManager extends Module { this._rebuildTwitchKey(key, false, old_val); } - hasTwitchOverride(key) { // eslint-disable-line class-methods-use-this - const overrides = Cookie.getJSON(OVERRIDE_COOKIE), - experiments = overrides?.experiments; - return experiments && has(experiments, key); + hasTwitchOverride(key: string) { // eslint-disable-line class-methods-use-this + const overrides = this._getOverrideCookie(), + experiments = overrides.experiments; + + return has(experiments, key); } - getTwitchAssignment(key, channel = null) { + getTwitchAssignment(key: string, channel: string | null = null) { const core = this.resolve('site')?.getCore?.(), exps = core && core.experiments; @@ -355,22 +495,47 @@ export default class ExperimentManager extends Module { this.log.warn('Error attempting to initialize Twitch experiments tracker.', err); } - if ( channel || this.getTwitchType(exps.experiments[key]?.t) === 'channel_id' ) - return exps.getAssignmentById(key, {channel: channel ?? this.settings.get('context.channel')}); - if ( exps.overrides && exps.overrides[key] ) return exps.overrides[key]; - else if ( exps.assignments && exps.assignments[key] ) + const exp_data = exps.experiments[key], + type = this.getTwitchType(exp_data?.t ?? 0); + + // channel_id experiments always use getAssignmentById + if ( type === 'channel_id' ) { + return exps.getAssignmentById(key, { + bucketing: { + type: 1, + value: channel ?? this.settings.get('context.channelID') + } + }); + } + + // Otherwise, just use the default assignment? + if ( exps.assignments?.[key] ) return exps.assignments[key]; + // If there is no default assignment, we should try to figure out + // what assignment they *would* get. + + if ( type === 'device_id' ) + return exps.selectTreatment(key, exp_data, this.unique_id); + + else if ( type === 'user_id' ) + // Technically, some experiments are expecting to get the user's + // login rather than user ID. But we don't care that much if an + // inactive legacy experiment is shown wrong. Meh. + return exps.selectTreatment(key, exp_data, this.resolve('site')?.getUser?.()?.id); + + // We don't know what kind of experiment this is. + // Give up! return null; } - getTwitchKeyFromName(name) { + getTwitchKeyFromName(name: string) { const experiments = this.getTwitchExperiments(); if ( ! experiments ) - return undefined; + return; name = name.toLowerCase(); for(const key in experiments) @@ -381,30 +546,37 @@ export default class ExperimentManager extends Module { } } - getTwitchAssignmentByName(name, channel = null) { - return this.getTwitchAssignment(this.getTwitchKeyFromName(name), channel); + getTwitchAssignmentByName(name: string, channel: string | null = null) { + const key = this.getTwitchKeyFromName(name); + if ( ! key ) + return null; + return this.getTwitchAssignment(key, channel); } - _rebuildTwitchKey(key, is_set, new_val) { + _rebuildTwitchKey( + key: string, + is_set: boolean, + new_val: string | null + ) { const core = this.resolve('site')?.getCore?.(), exps = core.experiments, old_val = has(exps.assignments, key) ? - exps.assignments[key] : - undefined; + exps.assignments[key] as string : + null; if ( old_val !== new_val ) { const value = is_set ? new_val : old_val; - this.emit(':twitch-changed', key, value); - this.emit(`:twitch-changed:${key}`, value); + this.emit(':twitch-changed', key, value, old_val); + this.emit(`:twitch-changed:${key}`, value, old_val); } } // FFZ Experiments - setOverride(key, value = null) { - const overrides = this.settings.provider.get('experiment-overrides') || {}; + setOverride(key: string, value: unknown = null) { + const overrides = this.settings.provider.get('experiment-overrides', {}); overrides[key] = value; this.settings.provider.set('experiment-overrides', overrides); @@ -412,25 +584,30 @@ export default class ExperimentManager extends Module { this._rebuildKey(key); } - deleteOverride(key) { + deleteOverride(key: string) { const overrides = this.settings.provider.get('experiment-overrides'); if ( ! overrides || ! has(overrides, key) ) return; delete overrides[key]; - this.settings.provider.set('experiment-overrides', overrides); + if ( Object.keys(overrides).length ) + this.settings.provider.set('experiment-overrides', overrides); + else + this.settings.provider.delete('experiment-overrides'); this._rebuildKey(key); } - hasOverride(key) { + hasOverride(key: string) { const overrides = this.settings.provider.get('experiment-overrides'); - return overrides && has(overrides, key); + return overrides ? has(overrides, key): false; } - getAssignment(key) { + get: (key: string) => T | null; + + getAssignment(key: string): T | null { if ( this.cache.has(key) ) - return this.cache.get(key); + return this.cache.get(key) as T; const experiment = this.experiments[key]; if ( ! experiment ) { @@ -440,14 +617,14 @@ export default class ExperimentManager extends Module { const overrides = this.settings.provider.get('experiment-overrides'), out = overrides && has(overrides, key) ? - overrides[key] : - ExperimentManager.selectGroup(key, experiment, this.unique_id); + overrides[key] as T : + ExperimentManager.selectGroup(key, experiment, this.unique_id ?? ''); this.cache.set(key, out); return out; } - _rebuildKey(key) { + _rebuildKey(key: string) { if ( ! this.cache.has(key) ) return; @@ -456,13 +633,17 @@ export default class ExperimentManager extends Module { const new_val = this.getAssignment(key); if ( new_val !== old_val ) { - this.emit(':changed', key, new_val); - this.emit(`:changed:${key}`, new_val); + this.emit(':changed', key, new_val, old_val); + this.emit(`:changed:${key}`, new_val, old_val); } } - static selectGroup(key, experiment, unique_id) { + static selectGroup( + key: string, + experiment: FFZExperimentData, + unique_id: string + ): T | null { const seed = key + unique_id + (experiment.seed || ''), total = experiment.groups.reduce((a,b) => a + b.weight, 0); @@ -471,9 +652,9 @@ export default class ExperimentManager extends Module { for(const group of experiment.groups) { value -= group.weight / total; if ( value <= 0 ) - return group.value; + return group.value as T; } return null; } -} \ No newline at end of file +} diff --git a/src/load_tracker.ts b/src/load_tracker.ts index ffe27445..1edd73ed 100644 --- a/src/load_tracker.ts +++ b/src/load_tracker.ts @@ -7,6 +7,20 @@ import Module, { GenericModule } from 'utilities/module'; import type SettingsManager from './settings'; + +declare module 'utilities/types' { + interface ModuleEventMap { + load_tracker: LoadEvents; + } + interface ModuleMap { + load_tracker: LoadTracker; + } + interface SettingsTypeMap { + 'chat.update-when-loaded': boolean; + } +} + + type PendingLoadData = { pending: Set; timers: Record | null>; diff --git a/src/main.ts b/src/main.ts index efa5df72..029437a3 100644 --- a/src/main.ts +++ b/src/main.ts @@ -16,7 +16,7 @@ import TranslationManager from './i18n'; import SocketClient from './socket'; import PubSubClient from './pubsub'; import Site from 'site'; -import Vue from 'utilities/vue'; +import VueModule from 'utilities/vue'; import StagingSelector from './staging'; import LoadTracker from './load_tracker'; @@ -138,7 +138,7 @@ class FrankerFaceZ extends Module { this.inject('site', Site); this.inject('addons', AddonManager); - this.register('vue', Vue); + this.register('vue', VueModule); // ======================================================================== diff --git a/src/modules/chat/badges.jsx b/src/modules/chat/badges.jsx index 71274db1..551cf1a4 100644 --- a/src/modules/chat/badges.jsx +++ b/src/modules/chat/badges.jsx @@ -1554,4 +1554,4 @@ export function fixBadgeData(badge) { } return badge; -} \ No newline at end of file +} diff --git a/src/modules/main_menu/components/chat-tester.vue b/src/modules/main_menu/components/chat-tester.vue index 521f949b..0d1e321f 100644 --- a/src/modules/main_menu/components/chat-tester.vue +++ b/src/modules/main_menu/components/chat-tester.vue @@ -633,7 +633,7 @@ export default { // TODO: Update timestamps for pinned chat? } - this.chat.resolve('site.subpump').inject(item.topic, item.data); + this.chat.resolve('site.subpump').simulateMessage(item.topic, item.data); } if ( item.chat ) { @@ -731,4 +731,4 @@ export default { } - \ No newline at end of file + diff --git a/src/modules/main_menu/components/experiments.vue b/src/modules/main_menu/components/experiments.vue index bb455f8c..24c34a03 100644 --- a/src/modules/main_menu/components/experiments.vue +++ b/src/modules/main_menu/components/experiments.vue @@ -173,7 +173,7 @@ @change="onTwitchChange($event)" >