mirror of
https://github.com/FrankerFaceZ/FrankerFaceZ.git
synced 2025-06-27 21:05:53 +00:00
Add experiments system. Add experiments UI. Update disabled buttons to use tw-button--disabled. Update chat line rendering. Add preset emote sizes to the emote menu to reduce reflows when loading. Fix directory issues caused by fixes to route sorting. Update the theme. Add a current route name value to fine router. Add recursive object protection to deep_copy.
This commit is contained in:
parent
1841ab156c
commit
e3a7e3b64d
35 changed files with 1075 additions and 451 deletions
|
@ -1,3 +1,14 @@
|
|||
<div class="list-header">4.0.0-beta2.4<span>@b3fb24504616675ad2b9</span> <time datetime="2018-04-10">(2018-04-10)</time></div>
|
||||
<ul class="chat-menu-content menu-side-padding">
|
||||
<li>Added: Debugging > Experiments for viewing active experiment information.</li>
|
||||
<li>Added: Experiments system in case I ever need to A/B something, like the new backend under development.</li>
|
||||
<li>Changed: Cleaned up a lot of instances of buttons that weren't being marked as disabled properly.</li>
|
||||
<li>Changed: Update dark theme for latest Twitch changes.</li>
|
||||
<li>Fixed: Update the render method for chat lines to bring subscription notices and rituals back in line with what Twitch generates.</li>
|
||||
<li>Fixed: Following directory not being properly modified because of changes made to router.</li>
|
||||
<li>Fixed? Made changes to how we modify data requesting profile images to hopefully stop breaking the game directory for people till we can make a more proper fix.</li>
|
||||
</ul>
|
||||
|
||||
<div class="list-header">4.0.0-beta2.3<span>@a07fb33207e6659acc9f</span> <time datetime="2018-04-09">(2018-04-09)</time></div>
|
||||
<ul class="chat-menu-content menu-side-padding">
|
||||
<li>Added: Favorite emotes by Ctrl-Clicking them! ⌘-Click for Mac users.</li>
|
||||
|
|
10
package-lock.json
generated
10
package-lock.json
generated
|
@ -2357,6 +2357,11 @@
|
|||
"randomfill": "1.0.4"
|
||||
}
|
||||
},
|
||||
"crypto-js": {
|
||||
"version": "3.1.9-1",
|
||||
"resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-3.1.9-1.tgz",
|
||||
"integrity": "sha1-/aGedh/Ad+Af+/3G6f38WeiAbNg="
|
||||
},
|
||||
"css-color-names": {
|
||||
"version": "0.0.4",
|
||||
"resolved": "https://registry.npmjs.org/css-color-names/-/css-color-names-0.0.4.tgz",
|
||||
|
@ -5103,6 +5108,11 @@
|
|||
"integrity": "sha512-H7ErYLM34CvDMto3GbD6xD0JLUGYXR3QTcH6B/tr4Hi/QpSThnCsIp+Sy5FRTw3B0d6py4HcNkW7nO/wdtGWEw==",
|
||||
"dev": true
|
||||
},
|
||||
"js-cookie": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-2.2.0.tgz",
|
||||
"integrity": "sha1-Gywnmm7s44ChIWi5JIUmWzWx7/s="
|
||||
},
|
||||
"js-tokens": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz",
|
||||
|
|
|
@ -47,9 +47,11 @@
|
|||
"url": "https://github.com/FrankerFaceZ/FrankerFaceZ.git"
|
||||
},
|
||||
"dependencies": {
|
||||
"crypto-js": "^3.1.9-1",
|
||||
"displacejs": "^1.2.4",
|
||||
"graphql": "^0.13.2",
|
||||
"graphql-tag": "^2.8.0",
|
||||
"js-cookie": "^2.2.0",
|
||||
"path-to-regexp": "^2.2.0",
|
||||
"popper.js": "^1.14.2",
|
||||
"sortablejs": "^1.7.0",
|
||||
|
|
272
src/experiments.js
Normal file
272
src/experiments.js
Normal file
|
@ -0,0 +1,272 @@
|
|||
'use strict';
|
||||
|
||||
// ============================================================================
|
||||
// Experiments
|
||||
// ============================================================================
|
||||
|
||||
import {SERVER} from 'utilities/constants';
|
||||
import Module from 'utilities/module';
|
||||
import {has, deep_copy} from 'utilities/object';
|
||||
|
||||
import Cookie from 'js-cookie';
|
||||
import SHA1 from 'crypto-js/sha1';
|
||||
|
||||
const OVERRIDE_COOKIE = 'experiment_overrides',
|
||||
COOKIE_OPTIONS = {
|
||||
expires: 7,
|
||||
domain: '.twitch.tv'
|
||||
};
|
||||
|
||||
|
||||
// ============================================================================
|
||||
// Experiment Manager
|
||||
// ============================================================================
|
||||
|
||||
export default class ExperimentManager extends Module {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
|
||||
this.inject('settings');
|
||||
|
||||
this.settings.addUI('experiments', {
|
||||
path: 'Debugging > Experiments',
|
||||
component: 'experiments',
|
||||
ffz_data: () => deep_copy(this.experiments),
|
||||
twitch_data: () => deep_copy(this.getTwitchExperiments()),
|
||||
|
||||
usingTwitchExperiment: key => this.usingTwitchExperiment(key),
|
||||
getTwitchAssignment: key => this.getTwitchAssignment(key),
|
||||
hasTwitchOverride: key => this.hasTwitchOverride(key),
|
||||
setTwitchOverride: (key, val) => this.setTwitchOverride(key, val),
|
||||
deleteTwitchOverride: key => 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),
|
||||
|
||||
on: (...args) => this.on(...args),
|
||||
off: (...args) => this.off(...args)
|
||||
});
|
||||
|
||||
this.unique_id = Cookie.get('unique_id');
|
||||
|
||||
this.Cookie = Cookie;
|
||||
|
||||
this.experiments = {};
|
||||
this.cache = new Map;
|
||||
}
|
||||
|
||||
async onLoad() {
|
||||
await this.loadExperiments();
|
||||
}
|
||||
|
||||
|
||||
async loadExperiments() {
|
||||
let data;
|
||||
|
||||
try {
|
||||
data = await fetch(`${SERVER}/static/experiments.json?_=${Date.now()}`).then(r =>
|
||||
r.ok ? r.json() : null);
|
||||
|
||||
} catch(err) {
|
||||
this.log.warn('Unable to load experiment data.', err);
|
||||
}
|
||||
|
||||
if ( ! data )
|
||||
return;
|
||||
|
||||
this.experiments = data;
|
||||
|
||||
const old_cache = this.cache;
|
||||
this.cache = new Map;
|
||||
|
||||
let changed = 0;
|
||||
|
||||
for(const [key, old_val] of old_cache.entries()) {
|
||||
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.log.info(`Loaded information on ${Object.keys(data).length} experiments.${changed > 0 ? ` ${changed} values updated.` : ''}`);
|
||||
}
|
||||
|
||||
|
||||
onEnable() {
|
||||
this.on('socket:command:reload_experiments', this.loadExperiments, this);
|
||||
this.on('socket:command:update_experiment', this.updateExperiment, this);
|
||||
}
|
||||
|
||||
|
||||
updateExperiment(key, data) {
|
||||
this.log.info(`Received updated data for experiment "${key}" via WebSocket.`, data);
|
||||
|
||||
if ( data.groups )
|
||||
this.experiments[key] = data;
|
||||
else
|
||||
this.experiments[key].groups = data;
|
||||
|
||||
this._rebuildKey(key);
|
||||
}
|
||||
|
||||
|
||||
// Twitch Experiments
|
||||
|
||||
getTwitchExperiments() {
|
||||
if ( window.__twilightSettings )
|
||||
return window.__twilightSettings.experiments;
|
||||
|
||||
const core = this.resolve('site').getCore();
|
||||
return core && core.experiments.experiments;
|
||||
}
|
||||
|
||||
|
||||
usingTwitchExperiment(key) {
|
||||
const core = this.resolve('site').getCore();
|
||||
return core && has(core.experiments.assignments, key)
|
||||
}
|
||||
|
||||
|
||||
setTwitchOverride(key, value = null) {
|
||||
const overrides = Cookie.getJSON(OVERRIDE_COOKIE) || {};
|
||||
overrides[key] = value;
|
||||
Cookie.set(OVERRIDE_COOKIE, overrides, COOKIE_OPTIONS);
|
||||
|
||||
const core = this.resolve('site').getCore();
|
||||
if ( core )
|
||||
core.experiments.overrides[key] = value;
|
||||
|
||||
this._rebuildTwitchKey(key, true, value);
|
||||
}
|
||||
|
||||
deleteTwitchOverride(key) {
|
||||
const overrides = Cookie.getJSON(OVERRIDE_COOKIE);
|
||||
if ( ! overrides || ! has(overrides, key) )
|
||||
return;
|
||||
|
||||
const old_val = overrides[key];
|
||||
delete overrides[key];
|
||||
Cookie.set(OVERRIDE_COOKIE, overrides, COOKIE_OPTIONS);
|
||||
|
||||
const core = this.resolve('site').getCore();
|
||||
if ( core )
|
||||
delete core.experiments.overrides[key];
|
||||
|
||||
this._rebuildTwitchKey(key, false, old_val);
|
||||
}
|
||||
|
||||
hasTwitchOverride(key) { // eslint-disable-line class-methods-use-this
|
||||
const overrides = Cookie.getJSON(OVERRIDE_COOKIE);
|
||||
return overrides && has(overrides, key);
|
||||
}
|
||||
|
||||
getTwitchAssignment(key) {
|
||||
const core = this.resolve('site').getCore(),
|
||||
exps = core && core.experiments;
|
||||
|
||||
if ( exps && exps.overrides[key] )
|
||||
return exps.overrides[key];
|
||||
|
||||
else if ( exps && exps.assignments[key] )
|
||||
return exps.assignments[key];
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
_rebuildTwitchKey(key, is_set, new_val) {
|
||||
const core = this.resolve('site').getCore(),
|
||||
exps = core.experiments;
|
||||
|
||||
if ( ! has(exps.assignments, key) )
|
||||
return;
|
||||
|
||||
const old_val = exps.assignments[key];
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// FFZ Experiments
|
||||
|
||||
setOverride(key, value = null) {
|
||||
const overrides = this.settings.provider.get('experiment-overrides') || {};
|
||||
overrides[key] = value;
|
||||
|
||||
this.settings.provider.set('experiment-overrides', overrides);
|
||||
|
||||
this._rebuildKey(key);
|
||||
}
|
||||
|
||||
deleteOverride(key) {
|
||||
const overrides = this.settings.provider.get('experiment-overrides');
|
||||
if ( ! overrides || ! has(overrides, key) )
|
||||
return;
|
||||
|
||||
delete overrides[key];
|
||||
this.settings.provider.set('experiment-overrides', overrides);
|
||||
|
||||
this._rebuildKey(key);
|
||||
}
|
||||
|
||||
hasOverride(key) {
|
||||
const overrides = this.settings.provider.get('experiment-overrides');
|
||||
return overrides && has(overrides, key);
|
||||
}
|
||||
|
||||
getAssignment(key) {
|
||||
if ( this.cache.has(key) )
|
||||
return this.cache.get(key);
|
||||
|
||||
const experiment = this.experiments[key];
|
||||
if ( ! experiment ) {
|
||||
this.log.warn(`Tried to get assignment for experiment "${key}" which is not known.`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const overrides = this.settings.provider.get('experiment-overrides'),
|
||||
out = overrides && has(overrides, key) ?
|
||||
overrides[key] :
|
||||
ExperimentManager.selectGroup(key, experiment, this.unique_id);
|
||||
|
||||
this.cache.set(key, out);
|
||||
return out;
|
||||
}
|
||||
|
||||
_rebuildKey(key) {
|
||||
if ( ! this.cache.has(key) )
|
||||
return;
|
||||
|
||||
const old_val = this.cache.get(key);
|
||||
this.cache.delete(key);
|
||||
const new_val = this.getAssignment(key);
|
||||
|
||||
if ( new_val !== old_val ) {
|
||||
this.emit(':changed', key, new_val);
|
||||
this.emit(`:changed:${key}`, new_val);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static selectGroup(key, experiment, unique_id) {
|
||||
const seed = key + unique_id + (experiment.seed || ''),
|
||||
total = experiment.groups.reduce((a,b) => a + b.weight, 0);
|
||||
|
||||
let value = (SHA1(seed).words[0] >>> 0) / Math.pow(2, 32);
|
||||
|
||||
for(const group of experiment.groups) {
|
||||
value -= group.weight / total;
|
||||
if ( value <= 0 )
|
||||
return group.value;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
|
@ -6,7 +6,7 @@ import Module from 'utilities/module';
|
|||
import {DEBUG} from 'utilities/constants';
|
||||
|
||||
import SettingsManager from './settings/index';
|
||||
//import ExperimentManager from './experiments';
|
||||
import ExperimentManager from './experiments';
|
||||
import {TranslationManager} from './i18n';
|
||||
import SocketClient from './socket';
|
||||
import Site from 'site';
|
||||
|
@ -35,7 +35,7 @@ class FrankerFaceZ extends Module {
|
|||
// ========================================================================
|
||||
|
||||
this.inject('settings', SettingsManager);
|
||||
//this.inject('experiments', ExperimentManager);
|
||||
this.inject('experiments', ExperimentManager);
|
||||
this.inject('i18n', TranslationManager);
|
||||
this.inject('socket', SocketClient);
|
||||
this.inject('site', Site);
|
||||
|
@ -97,7 +97,7 @@ class FrankerFaceZ extends Module {
|
|||
FrankerFaceZ.Logger = Logger;
|
||||
|
||||
const VER = FrankerFaceZ.version_info = {
|
||||
major: 4, minor: 0, revision: 0, extra: '-beta2.3',
|
||||
major: 4, minor: 0, revision: 0, extra: '-beta2.4',
|
||||
build: __webpack_hash__,
|
||||
toString: () =>
|
||||
`${VER.major}.${VER.minor}.${VER.revision}${VER.extra || ''}${DEBUG ? '-dev' : ''}`
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
<div class="tw-mg-b-2 tw-align-right">
|
||||
<button
|
||||
:disabled="! has_value"
|
||||
:class="{'tw-button--disabled': ! has_value}"
|
||||
class="tw-mg-l-05 tw-button tw-button--hollow tw-tooltip-wrapper"
|
||||
@click="clear"
|
||||
>
|
||||
|
|
251
src/modules/main_menu/components/experiments.vue
Normal file
251
src/modules/main_menu/components/experiments.vue
Normal file
|
@ -0,0 +1,251 @@
|
|||
<template lang="html">
|
||||
<div class="ffz--experiments tw-pd-t-05">
|
||||
<div class="tw-pd-b-1 tw-mg-b-2 tw-border-b">
|
||||
{{ t('settings.experiments.about', 'This feature allows you to override experiment values. Please note that, for most experiments, you may have to refresh the page for your changes to take effect.') }}
|
||||
</div>
|
||||
|
||||
<h3 class="tw-mg-b-1">
|
||||
{{ t('settings.experiments.ffz', 'FrankerFaceZ Experiments') }}
|
||||
</h3>
|
||||
|
||||
<div class="ffz--experiment-list">
|
||||
<section
|
||||
v-for="({key, exp}) of sorted(ffz_data)"
|
||||
:key="key"
|
||||
:data-key="key"
|
||||
>
|
||||
<div class="tw-elevation-1 tw-c-background tw-border tw-pd-y-05 tw-pd-x-1 tw-mg-y-05 tw-flex tw-flex-nowrap">
|
||||
|
||||
<div class="tw-flex-grow-1">
|
||||
<h4>{{ exp.name }}</h4>
|
||||
<div v-if="exp.description" class="description">
|
||||
{{ exp.description }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tw-flex tw-flex-shrink-0 tw-align-items-start">
|
||||
<select
|
||||
:data-key="key"
|
||||
class="tw-mg-05 tw-select tw-display-line tw-width-auto"
|
||||
@change="onChange($event)"
|
||||
>
|
||||
<option
|
||||
v-for="(i, idx) in exp.groups"
|
||||
:key="idx"
|
||||
:selected="i.value === exp.value"
|
||||
>
|
||||
{{ t('settings.exepriments.entry', '%{value} (weight: %{weight})', i) }}
|
||||
</option>
|
||||
</select>
|
||||
|
||||
<button
|
||||
:disabled="exp.default"
|
||||
:class="{'tw-button--disabled': exp.default}"
|
||||
class="tw-mg-t-05 tw-button tw-button--text tw-tooltip-wrapper"
|
||||
@click="reset(key)"
|
||||
>
|
||||
<span class="tw-button__text ffz-i-cancel" />
|
||||
<span class="tw-tooltip tw-tooltip--down tw-tooltip--align-right">
|
||||
{{ t('setting.reset', 'Reset to Default') }}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<div v-if="! Object.keys(ffz_data).length">
|
||||
{{ t('settings.experiments.none', 'There are no current experiments.') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 class="tw-mg-t-5 tw-mg-b-1">
|
||||
{{ t('settings.experiments.twitch', 'Twitch Experiments') }}
|
||||
</h3>
|
||||
|
||||
<div class="ffz--experiment-list">
|
||||
<section
|
||||
v-for="({key, exp}) of sorted(twitch_data)"
|
||||
:key="key"
|
||||
:data-key="key"
|
||||
>
|
||||
<div
|
||||
:class="{live: exp.in_use}"
|
||||
class="ffz--experiment-row tw-elevation-1 tw-c-background tw-border tw-pd-y-05 tw-pd-x-1 tw-mg-y-05 tw-flex"
|
||||
>
|
||||
<div class="tw-flex tw-flex-shrink-0 tw-align-items-center tw-border-r tw-mg-r-1 tw-pd-r-1">
|
||||
<div v-if="exp.in_use" class="ffz--profile__icon ffz-i-ok tw-tooltip-wrapper">
|
||||
<div class="tw-tooltip tw-tooltip--down tw-tooltip--align-left">
|
||||
{{ t('setting.experiments.active', 'This experiment is active.') }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="ffz--profile__icon ffz-i-cancel tw-tooltip-wrapper">
|
||||
<div class="tw-tooltip tw-tooltip--down tw-tooltip--align-left">
|
||||
{{ t('setting.experiments.inactive', 'This experiment is not active.') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tw-flex-grow-1">
|
||||
<h4>{{ exp.name }}</h4>
|
||||
<div class="description">
|
||||
{{ exp.remainder }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tw-flex tw-flex-shrink-0 tw-align-items-start">
|
||||
<select
|
||||
:data-key="key"
|
||||
class="tw-mg-05 tw-select tw-display-line tw-width-auto"
|
||||
@change="onTwitchChange($event)"
|
||||
>
|
||||
<option
|
||||
v-for="(i, idx) in exp.groups"
|
||||
:key="idx"
|
||||
:selected="i.value === exp.value"
|
||||
>
|
||||
{{ i.value }} (weight: {{ i.weight }})
|
||||
</option>
|
||||
</select>
|
||||
|
||||
<button
|
||||
:disabled="exp.default"
|
||||
:class="{'tw-button--disabled': exp.default}"
|
||||
class="tw-mg-t-05 tw-button tw-button--text tw-tooltip-wrapper"
|
||||
@click="resetTwitch(key)"
|
||||
>
|
||||
<span class="tw-button__text ffz-i-cancel" />
|
||||
<span class="tw-tooltip tw-tooltip--down tw-tooltip--align-right">
|
||||
{{ t('setting.reset', 'Reset to Default') }}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<div v-if="! Object.keys(twitch_data).length">
|
||||
{{ t('settings.experiments.none', 'There are no current experiments.') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import {has} from 'utilities/object';
|
||||
|
||||
export default {
|
||||
props: ['item'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
ffz_data: this.item.ffz_data(),
|
||||
twitch_data: this.item.twitch_data()
|
||||
}
|
||||
},
|
||||
|
||||
created() {
|
||||
for(const key in this.ffz_data)
|
||||
if ( has(this.ffz_data, key) ) {
|
||||
const exp = this.ffz_data[key];
|
||||
this.$set(exp, 'value', this.item.getAssignment(key));
|
||||
this.$set(exp, 'default', ! this.item.hasOverride(key));
|
||||
}
|
||||
|
||||
for(const key in this.twitch_data)
|
||||
if ( has(this.twitch_data, key) ) {
|
||||
const exp = this.twitch_data[key];
|
||||
this.$set(exp, 'value', this.item.getTwitchAssignment(key));
|
||||
this.$set(exp, 'default', ! this.item.hasTwitchOverride(key));
|
||||
|
||||
exp.in_use = this.item.usingTwitchExperiment(key);
|
||||
exp.remainder = `v: ${exp.v}, t: ${exp.t}`;
|
||||
}
|
||||
|
||||
this.item.on(':changed', this.valueChanged, this);
|
||||
this.item.on(':twitch-changed', this.twitchValueChanged, this);
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
this.item.off(':changed', this.valueChanged, this);
|
||||
this.item.off(':twitch-changed', this.twitchValueChanged, this);
|
||||
},
|
||||
|
||||
methods: {
|
||||
sorted(data) {
|
||||
const out = Object.entries(data).map(x => ({key: x[0], exp: x[1]}));
|
||||
|
||||
out.sort((a,b) => {
|
||||
const a_use = a.exp.in_use,
|
||||
b_use = b.exp.in_use;
|
||||
|
||||
if ( a_use && ! b_use ) return -1;
|
||||
if ( ! a_use && b_use ) return 1;
|
||||
|
||||
const a_n = a.exp.name.toLowerCase(),
|
||||
b_n = b.exp.name.toLowerCase();
|
||||
|
||||
if ( a_n < b_n ) return -1;
|
||||
if ( a_n > b_n ) return 1;
|
||||
return 0;
|
||||
});
|
||||
|
||||
return out;
|
||||
},
|
||||
|
||||
reset(key) {
|
||||
this.item.deleteOverride(key);
|
||||
const exp = this.ffz_data[key];
|
||||
if ( exp )
|
||||
exp.default = ! this.item.hasOverride(key);
|
||||
},
|
||||
|
||||
resetTwitch(key) {
|
||||
this.item.deleteTwitchOverride(key);
|
||||
const exp = this.twitch_data[key];
|
||||
if ( exp )
|
||||
exp.default = ! this.item.hasTwitchOverride(key);
|
||||
},
|
||||
|
||||
onChange(event) {
|
||||
const el = event.target,
|
||||
idx = el.selectedIndex,
|
||||
key = el.dataset.key;
|
||||
|
||||
const exp = this.ffz_data[key],
|
||||
groups = exp && exp.groups,
|
||||
entry = groups && groups[idx];
|
||||
|
||||
if ( entry )
|
||||
this.item.setOverride(key, entry.value);
|
||||
},
|
||||
|
||||
onTwitchChange(event) {
|
||||
const el = event.target,
|
||||
idx = el.selectedIndex,
|
||||
key = el.dataset.key;
|
||||
|
||||
const exp = this.twitch_data[key],
|
||||
groups = exp && exp.groups,
|
||||
entry = groups && groups[idx];
|
||||
|
||||
if ( entry )
|
||||
this.item.setTwitchOverride(key, entry.value);
|
||||
},
|
||||
|
||||
valueChanged(key, value) {
|
||||
const exp = this.ffz_data[key];
|
||||
if ( exp ) {
|
||||
exp.value = value;
|
||||
exp.default = ! this.item.hasOverride(key);
|
||||
}
|
||||
},
|
||||
|
||||
twitchValueChanged(key, value) {
|
||||
const exp = this.twitch_data[key];
|
||||
if ( exp ) {
|
||||
exp.value = value;
|
||||
exp.default = ! this.item.hasTwitchOverride(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
|
@ -12,6 +12,7 @@
|
|||
</button>
|
||||
<button
|
||||
:disabled="item.profile && context.profiles.length < 2"
|
||||
:class="{'tw-button--disabled': item.profile && context.profiles.length < 2}"
|
||||
class="tw-mg-l-1 tw-button tw-button--text"
|
||||
@click="del"
|
||||
>
|
||||
|
|
|
@ -96,12 +96,28 @@ export default class Twilight extends BaseSite {
|
|||
const session = this.getSession();
|
||||
return session && session.user;
|
||||
}
|
||||
|
||||
getCore() {
|
||||
if ( this._core )
|
||||
return this._core;
|
||||
|
||||
let core = this.web_munch.getModule('core-1');
|
||||
if ( core )
|
||||
return this._core = core.o;
|
||||
|
||||
core = this.web_munch.getModule('core-2');
|
||||
if ( core )
|
||||
return this._core = core.p;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Twilight.KNOWN_MODULES = {
|
||||
simplebar: n => n.globalObserver && n.initDOMLoadedElements,
|
||||
react: n => n.Component && n.createElement,
|
||||
'core-1': n => n.o && n.o.experiments,
|
||||
'core-2': n => n.p && n.p.experiments,
|
||||
cookie: n => n && n.set && n.get && n.getJSON && n.withConverter,
|
||||
'extension-service': n => n.extensionService,
|
||||
'chat-types': n => n.a && n.a.PostWithMention,
|
||||
'gql-printer': n => n !== window && n.print
|
||||
|
|
|
@ -313,6 +313,8 @@ export default class EmoteMenu extends Module {
|
|||
src={data.src}
|
||||
srcSet={data.srcSet}
|
||||
alt={data.name}
|
||||
height={data.height ? `${data.height}px` : null}
|
||||
width={data.width ? `${data.width}px` : null}
|
||||
/>
|
||||
</figure>
|
||||
{favorite && (<figure class="ffz--favorite ffz-i-star" />)}
|
||||
|
@ -1007,7 +1009,9 @@ export default class EmoteMenu extends Module {
|
|||
src: emote.urls[1],
|
||||
srcSet: emote.srcSet,
|
||||
name: emote.name,
|
||||
favorite: is_fav
|
||||
favorite: is_fav,
|
||||
height: emote.height,
|
||||
width: emote.width
|
||||
};
|
||||
|
||||
emotes.push(em);
|
||||
|
|
|
@ -168,9 +168,9 @@ export default class ChatLine extends Module {
|
|||
months = msg.sub_months || 1,
|
||||
tier = SUB_TIERS[plan.plan] || 1;
|
||||
|
||||
cls = 'chat-line__subscribe';
|
||||
cls = 'user-notice-line tw-pd-y-05 tw-pd-r-2 ffz--subscribe-line';
|
||||
out = [
|
||||
e('span', null, [
|
||||
e('div', {className: 'tw-c-text-alt-2'}, [
|
||||
t.i18n.t('chat.sub.main', '%{user} just subscribed with %{plan}!', {
|
||||
user: user.userDisplayName,
|
||||
plan: plan.prime ?
|
||||
|
@ -187,7 +187,7 @@ export default class ChatLine extends Module {
|
|||
})}` : null
|
||||
]),
|
||||
out && e('div', {
|
||||
className: 'chat-line__subscribe--message',
|
||||
className: 'chat-line--inline chat-line__message',
|
||||
'data-room-id': this.props.channelID,
|
||||
'data-room': room,
|
||||
'data-user-id': user.userID,
|
||||
|
@ -198,16 +198,18 @@ export default class ChatLine extends Module {
|
|||
} else if ( msg.ffz_type === 'ritual' && t.chat.context.get('chat.rituals.show') ) {
|
||||
let system_msg;
|
||||
if ( msg.ritual === 'new_chatter' )
|
||||
system_msg = t.i18n.t('chat.ritual', '%{user} is new here. Say hello!', {
|
||||
user: user.userDisplayName
|
||||
});
|
||||
system_msg = e('div', {className: 'tw-c-text-alt-2'}, [
|
||||
t.i18n.t('chat.ritual', '%{user} is new here. Say hello!', {
|
||||
user: user.userDisplayName
|
||||
})
|
||||
]);
|
||||
|
||||
if ( system_msg ) {
|
||||
cls = 'chat-line__ritual';
|
||||
cls = 'user-notice-line tw-pd-y-05 tw-pd-r-2 ffz--ritual-line';
|
||||
out = [
|
||||
system_msg,
|
||||
out && e('div', {
|
||||
className: 'chat-line__ritual--message',
|
||||
className: 'chat-line--inline chat-line__message',
|
||||
'data-room-id': this.props.channelID,
|
||||
'data-room': room,
|
||||
'data-user-id': user.userID,
|
||||
|
|
|
@ -2,11 +2,11 @@
|
|||
.thread-message__timestamp,
|
||||
.thread-message__warning,
|
||||
|
||||
.chat-line__message,
|
||||
.chat-line__message:not(.chat-line--inline),
|
||||
.chat-line__moderation,
|
||||
.chat-line__status,
|
||||
.chat-line__raid,
|
||||
.chat-line__subscribe {
|
||||
.user-notice-line {
|
||||
padding-top: calc(.5rem - 1px) !important;
|
||||
|
||||
border-top: 1px solid #aaa;
|
||||
|
|
|
@ -2,11 +2,11 @@
|
|||
.thread-message__timestamp,
|
||||
.thread-message__warning,
|
||||
|
||||
.chat-line__message,
|
||||
.chat-line__message:not(.chat-line--inline),
|
||||
.chat-line__moderation,
|
||||
.chat-line__status,
|
||||
.chat-line__raid,
|
||||
.chat-line__subscribe {
|
||||
.user-notice-line {
|
||||
padding-top: calc(.5rem - 1px) !important;
|
||||
|
||||
border-top: 1px solid rgba(255,255,255,0.5);
|
||||
|
|
|
@ -2,11 +2,11 @@
|
|||
.thread-message__timestamp,
|
||||
.thread-message__warning,
|
||||
|
||||
.chat-line__message,
|
||||
.chat-line__message:not(.chat-line--inline),
|
||||
.chat-line__moderation,
|
||||
.chat-line__status,
|
||||
.chat-line__raid,
|
||||
.chat-line__subscribe {
|
||||
.user-notice-line {
|
||||
padding-top: calc(.5rem - 1px) !important;
|
||||
border-top: 1px solid var(--ffz-border-color);
|
||||
|
||||
|
|
|
@ -2,11 +2,11 @@
|
|||
.thread-message__timestamp,
|
||||
.thread-message__warning,
|
||||
|
||||
.chat-line__message,
|
||||
.chat-line__message:not(.chat-line--inline),
|
||||
.chat-line__moderation,
|
||||
.chat-line__status,
|
||||
.chat-line__raid,
|
||||
.chat-line__subscribe {
|
||||
.user-notice-line {
|
||||
padding-bottom: calc(.5rem - 1px) !important;
|
||||
border-bottom: 1px solid var(--ffz-border-color);
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
.chat-line__message,
|
||||
.chat-line__subscribe {
|
||||
.chat-line__message:not(.chat-line--inline),
|
||||
.user-notice-line {
|
||||
&.ffz-mentioned:nth-child(2n+0) {
|
||||
background-color: rgba(255,127,127,.4) !important;
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
.chat-line__message,
|
||||
.chat-line__subscribe {
|
||||
.chat-line__message:not(.chat-line--inline),
|
||||
.user-notice-line {
|
||||
&.ffz-mentioned {
|
||||
background-color: rgba(255,127,127,.2) !important;
|
||||
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
.chat-line__message,
|
||||
.chat-line__message:not(.chat-line--inline),
|
||||
.chat-line__moderation,
|
||||
.chat-line__status,
|
||||
.chat-line__raid,
|
||||
.chat-line__subscribe {
|
||||
.chat-line__raid {
|
||||
padding: .5rem 1rem !important;
|
||||
}
|
||||
|
||||
.user-notice-line {
|
||||
padding: .5rem 1rem !important;
|
||||
padding-left: .6rem !important;
|
||||
}
|
|
@ -5,11 +5,9 @@
|
|||
.chat-line__moderation,
|
||||
.chat-line__status,
|
||||
.chat-list__lines .chat-line__raid,
|
||||
.chat-list__lines .chat-line__subscribe,
|
||||
.chat-list__lines .chat-line__bits-charity,
|
||||
.chat-list__lines .chat-line__ritual,
|
||||
.chat-line__subscribe,
|
||||
.chat-line__message {
|
||||
.user-notice-line,
|
||||
.chat-line__message:not(.chat-line--inline) {
|
||||
background-color: transparent !important;
|
||||
|
||||
&:nth-child(2n+0) {
|
||||
|
|
|
@ -4,7 +4,7 @@ query {
|
|||
node {
|
||||
createdAt
|
||||
broadcaster {
|
||||
profileImageURL(width: 70)
|
||||
profileImageURL(width: 50)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ query {
|
|||
currentUser {
|
||||
followedLiveUsers {
|
||||
nodes {
|
||||
profileImageURL(width: 70)
|
||||
profileImageURL(width: 50)
|
||||
stream {
|
||||
type
|
||||
createdAt
|
||||
|
|
|
@ -2,9 +2,9 @@ query {
|
|||
currentUser {
|
||||
followedHosts {
|
||||
nodes {
|
||||
profileImageURL(width: 70)
|
||||
profileImageURL(width: 50)
|
||||
hosting {
|
||||
profileImageURL(width: 70)
|
||||
profileImageURL(width: 50)
|
||||
stream {
|
||||
createdAt
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ query {
|
|||
currentUser {
|
||||
followedLiveUsers {
|
||||
nodes {
|
||||
profileImageURL(width: 70)
|
||||
profileImageURL(width: 50)
|
||||
stream {
|
||||
createdAt
|
||||
}
|
||||
|
@ -10,9 +10,9 @@ query {
|
|||
}
|
||||
followedHosts {
|
||||
nodes {
|
||||
profileImageURL(width: 70)
|
||||
profileImageURL(width: 50)
|
||||
hosting {
|
||||
profileImageURL(width: 70)
|
||||
profileImageURL(width: 50)
|
||||
stream {
|
||||
createdAt
|
||||
type
|
||||
|
|
|
@ -3,7 +3,7 @@ query {
|
|||
followedLiveUsers {
|
||||
edges {
|
||||
node {
|
||||
profileImageURL(width: 70)
|
||||
profileImageURL(width: 50)
|
||||
stream {
|
||||
createdAt
|
||||
}
|
||||
|
|
|
@ -89,11 +89,6 @@ export default class Following extends SiteModule {
|
|||
this.apollo.registerModifier('FollowingHosts_CurrentUser', res => this.modifyLiveHosts(res), false);
|
||||
}
|
||||
|
||||
isRouteAcceptable() {
|
||||
return this.router.current.name === 'dir-following'
|
||||
|| this.router.current.name === 'dir-category' && this.router.match[1] === 'following';
|
||||
}
|
||||
|
||||
modifyLiveUsers(res) {
|
||||
const hiddenThumbnails = this.settings.provider.get('directory.game.hidden-thumbnails') || [];
|
||||
const blockedGames = this.settings.provider.get('directory.game.blocked-games') || [];
|
||||
|
@ -167,34 +162,35 @@ export default class Following extends SiteModule {
|
|||
}
|
||||
|
||||
ensureQueries () {
|
||||
if (this.router && this.router.match) {
|
||||
this.apollo.ensureQuery(
|
||||
'FollowedChannels',
|
||||
'data.currentUser.followedLiveUsers.nodes.0.profileImageURL'
|
||||
);
|
||||
|
||||
if ( this.router.current_name !== 'dir-following' )
|
||||
return;
|
||||
|
||||
const bit = this.router.match[1];
|
||||
|
||||
if ( ! bit )
|
||||
this.apollo.ensureQuery(
|
||||
'FollowedChannels',
|
||||
'data.currentUser.followedLiveUsers.nodes.0.profileImageURL'
|
||||
'FollowedIndex_CurrentUser',
|
||||
n =>
|
||||
get('data.currentUser.followedLiveUsers.nodes.0.stream.createdAt', n) !== undefined ||
|
||||
get('data.currentUser.followedHosts.nodes.0.hosting.stream.createdAt', n) !== undefined
|
||||
);
|
||||
|
||||
if (!this.router.match[1] || this.router.match[1] === 'following') {
|
||||
this.apollo.ensureQuery(
|
||||
'FollowedIndex_CurrentUser',
|
||||
n =>
|
||||
get('data.currentUser.followedLiveUsers.nodes.0.profileImageURL', n) !== undefined
|
||||
||
|
||||
get('data.currentUser.followedLiveUsers.edges.0.node.profileImageURL', n) !== undefined
|
||||
||
|
||||
get('data.currentUser.followedHosts.nodes.0.hosting.profileImageURL', n) !== undefined
|
||||
);
|
||||
} else if (this.router.match[1] === 'live') {
|
||||
this.apollo.ensureQuery(
|
||||
'FollowingLive_CurrentUser',
|
||||
'data.currentUser.followedLiveUsers.nodes.0.profileImageURL' || 'data.currentUser.followedLiveUsers.edges.0.node.profileImageURL'
|
||||
);
|
||||
} else if (this.router.match[1] === 'hosts') {
|
||||
this.apollo.ensureQuery(
|
||||
'FollowingHosts_CurrentUser',
|
||||
'data.currentUser.followedHosts.nodes.0.hosting.profileImageURL'
|
||||
);
|
||||
}
|
||||
}
|
||||
else if ( bit === 'live' )
|
||||
this.apollo.ensureQuery(
|
||||
'FollowingLive_CurrentUser',
|
||||
'data.currentUser.followedLiveUsers.nodes.0.stream.createdAt'
|
||||
);
|
||||
|
||||
else if ( bit === 'hosts' )
|
||||
this.apollo.ensureQuery(
|
||||
'FollowingHosts_CurrentUser',
|
||||
'data.currentUser.followedHosts.nodes.0.hosting.stream.createdAt'
|
||||
);
|
||||
}
|
||||
|
||||
onEnable() {
|
||||
|
|
|
@ -6,9 +6,6 @@ query {
|
|||
node {
|
||||
createdAt
|
||||
type
|
||||
broadcaster {
|
||||
profileImageURL(width: 70)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -19,9 +16,6 @@ query {
|
|||
node {
|
||||
createdAt
|
||||
type
|
||||
broadcaster {
|
||||
profileImageURL(width: 70)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,14 @@
|
|||
<template lang="html">
|
||||
<div class="ffz-featured-follow tw-c-background">
|
||||
<header class="tw-full-width tw-align-items-center tw-flex tw-flex-nowrap">
|
||||
<h4>{{ t('metadata.featured-follow.title', 'Featured Follow') }}</h4>
|
||||
<h4>{{ t('metadata.featured-follow.title', 'Featured Channels') }}</h4>
|
||||
|
||||
<div class="tw-flex-grow-1 tw-pd-x-2"/>
|
||||
<button :class="{ 'ffz--featured-follow-update': hasUpdate, 'tw-button--disabled': !hasUpdate }" class="tw-button tw-button--hollow" @click="refresh">
|
||||
<button
|
||||
v-if="hasUpdate"
|
||||
class="ffz--featured-follow-update tw-button tw-button--hollow"
|
||||
@click="refresh"
|
||||
>
|
||||
<span class="tw-button__icon tw-button__icon--left">
|
||||
<figure class="ffz-i-arrows-cw"/>
|
||||
</span>
|
||||
|
@ -32,6 +36,7 @@
|
|||
<button
|
||||
v-if="user.following"
|
||||
:disabled="user.loading"
|
||||
:class="{'tw-button--disabled': user.loading}"
|
||||
:data-title="user.loading ? null : t('featured-follow.button.unfollow', 'Unfollow %{user}', {user: user.displayName})"
|
||||
data-tooltip-type="html"
|
||||
class="tw-button tw-button--status tw-button--success ffz-tooltip ffz--featured-button-unfollow"
|
||||
|
@ -44,6 +49,7 @@
|
|||
<button
|
||||
v-if="user.following"
|
||||
:disabled="user.loading"
|
||||
:class="{'tw-button--disabled': user.loading}"
|
||||
:data-title="notifyTip(user.disableNotifications)"
|
||||
data-tooltip-type="html"
|
||||
class="tw-button-icon tw-mg-l-05 ffz-tooltip ffz--featured-button-notification"
|
||||
|
@ -56,6 +62,7 @@
|
|||
<button
|
||||
v-else
|
||||
:disabled="user.loading"
|
||||
:class="{'tw-button--disabled': user.loading}"
|
||||
class="tw-button"
|
||||
@click="followUser(user.id)"
|
||||
>
|
||||
|
|
|
@ -3,7 +3,7 @@ query($logins: [String!]) {
|
|||
id
|
||||
login
|
||||
displayName
|
||||
profileImageURL(width: 70)
|
||||
profileImageURL(width: 50)
|
||||
self {
|
||||
follower {
|
||||
disableNotifications
|
||||
|
|
|
@ -25,7 +25,7 @@ export default class FollowingText extends SiteModule {
|
|||
currentUser {
|
||||
followedLiveUsers {
|
||||
nodes {
|
||||
profileImageURL(width: 70)
|
||||
profileImageURL(width: 50)
|
||||
stream {
|
||||
type
|
||||
createdAt
|
||||
|
|
|
@ -11,6 +11,11 @@
|
|||
will-change: opacity;
|
||||
}
|
||||
|
||||
.user-notice-line.tw-mg-y-05 {
|
||||
margin-top: 0 !important;
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
.autocomplete-balloon {
|
||||
.autocomplete-balloon__item {
|
||||
> .tw-flex {
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -136,13 +136,14 @@ export default class Apollo extends Module {
|
|||
query = query_map && query_map.get(id),
|
||||
modifiers = this.modifiers[operation];
|
||||
|
||||
if ( modifiers )
|
||||
if ( modifiers ) {
|
||||
for(const mod of modifiers) {
|
||||
if ( typeof mod === 'function' )
|
||||
mod(request);
|
||||
else if ( mod[1] )
|
||||
this.applyModifier(request, mod[1]);
|
||||
}
|
||||
}
|
||||
|
||||
this.emit(`:request.${operation}`, request.query, request.variables);
|
||||
|
||||
|
@ -358,18 +359,23 @@ function merge(a, b) {
|
|||
const s = a.selectionSet.selections,
|
||||
selects = {};
|
||||
for(const sel of b.selectionSet.selections) {
|
||||
if (sel.name && sel.name.value) {
|
||||
selects[`${sel.name.value}:${sel.alias?sel.alias.value:null}`] = sel;
|
||||
} else {
|
||||
if (sel.kind === 'InlineFragment') {
|
||||
selects[`${sel.typeCondition.name.value}:${sel.alias?sel.alias.value:null}`] = sel;
|
||||
}
|
||||
}
|
||||
const name = sel.kind === 'InlineFragment' ?
|
||||
(sel.typeCondition.name ?
|
||||
sel.typeCondition.name.value : null) :
|
||||
(sel.name ? sel.name.value : null),
|
||||
alias = sel.alias ? sel.alias.value : null,
|
||||
key = `${name}:${alias}`;
|
||||
|
||||
if ( name )
|
||||
selects[key] = sel;
|
||||
}
|
||||
|
||||
for(let i=0, l = s.length; i < l; i++) {
|
||||
const sel = s[i],
|
||||
name = sel.kind === 'InlineFragment' ? (sel.typeCondition.name ? sel.typeCondition.name.value : null) : (sel.name ? sel.name.value : null),
|
||||
name = sel.kind === 'InlineFragment' ?
|
||||
(sel.typeCondition.name ?
|
||||
sel.typeCondition.name.value : null) :
|
||||
(sel.name ? sel.name.value : null),
|
||||
alias = sel.alias ? sel.alias.value : null,
|
||||
key = `${name}:${alias}`,
|
||||
other = selects[key];
|
||||
|
|
|
@ -17,6 +17,7 @@ export default class FineRouter extends Module {
|
|||
this.__routes = [];
|
||||
this.routes = {};
|
||||
this.current = null;
|
||||
this.current_name = null;
|
||||
this.match = null;
|
||||
this.location = null;
|
||||
}
|
||||
|
@ -58,6 +59,7 @@ export default class FineRouter extends Module {
|
|||
if ( match ) {
|
||||
this.log.debug('Matching Route', route, match);
|
||||
this.current = route;
|
||||
this.current_name = route.name;
|
||||
this.match = match;
|
||||
this.emitSafe(':route', route, match);
|
||||
this.emitSafe(`:route:${route.name}`, ...match);
|
||||
|
@ -65,7 +67,7 @@ export default class FineRouter extends Module {
|
|||
}
|
||||
}
|
||||
|
||||
this.current = this.match = null;
|
||||
this.current = this.current_name = this.match = null;
|
||||
this.emitSafe(':route', null, null);
|
||||
}
|
||||
|
||||
|
|
|
@ -185,19 +185,27 @@ export function get(path, object) {
|
|||
}
|
||||
|
||||
|
||||
export function deep_copy(object) {
|
||||
export function deep_copy(object, seen) {
|
||||
if ( typeof object !== 'object' )
|
||||
return object;
|
||||
|
||||
if ( ! seen )
|
||||
seen = new Set;
|
||||
|
||||
if ( seen.has(object) )
|
||||
throw new Error('recursive structure detected');
|
||||
|
||||
seen.add(object);
|
||||
|
||||
if ( Array.isArray(object) )
|
||||
return object.map(deep_copy);
|
||||
return object.map(x => deep_copy(x, seen));
|
||||
|
||||
const out = {};
|
||||
for(const key in object)
|
||||
if ( HOP.call(object, key) ) {
|
||||
const val = object[key];
|
||||
if ( typeof val === 'object' )
|
||||
out[key] = deep_copy(val);
|
||||
out[key] = deep_copy(val, seen);
|
||||
else
|
||||
out[key] = val;
|
||||
}
|
||||
|
|
|
@ -62,4 +62,15 @@
|
|||
position: absolute;
|
||||
top: 0.5rem; right: 0.5rem;
|
||||
font-size: 1.6rem;
|
||||
}
|
||||
|
||||
|
||||
.ffz--experiment-row {
|
||||
&:not(.live):not(:hover):not(:focus) {
|
||||
opacity: 0.5;
|
||||
|
||||
.tw-theme--dark & {
|
||||
opacity: .25;
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue