mirror of
https://github.com/FrankerFaceZ/FrankerFaceZ.git
synced 2025-07-28 21:48:31 +00:00
4.0.0-rc3. Display a warning for complex or invalid filtering terms, using the safe-regex NPM module. Add a separate Regex (Word)
filtering mode. Fix channel hosting control. Fix hide extensions. Add a fix for bad local echo emote indices for chat. Position the color picker above rather than below for filter terms. Apply a dark theme to the color picker. Rewrite the filter terms editor to use the term editor component for adding a new term.
This commit is contained in:
parent
2a790ad7cd
commit
038270d232
23 changed files with 669 additions and 423 deletions
|
@ -1,3 +1,14 @@
|
||||||
|
<div class="list-header">4.0.0-rc3<span>@440f1fb9360578cbde7b</span> <time datetime="2018-05-31">(2018-06-27)</time></div>
|
||||||
|
<ul class="chat-menu-content menu-side-padding">
|
||||||
|
<li>Added: Display a warning if filter terms are invalid or potentially complex.</li>
|
||||||
|
<li>Changed: Separate <code>Regex</code> and <code>Regex (Word)</code> term modes to allow matching regular expressions without your expression being wrapped in separators.</li>
|
||||||
|
<li>Fixed: Disable channel hosting now works again.</li>
|
||||||
|
<li>Fixed: Hide extensions now works again, though it isn't as necessary now that you can hide individual player extensions natively.</li>
|
||||||
|
<li>Fixed: Twitch's updated chat code not calculating emote positions in locally echoed messages correctly.</li>
|
||||||
|
<li>Fixed: Position the color picker above the control rather than below when creating highlight terms to avoid the control going out of the window.</li>
|
||||||
|
<li>Fixed: Apply the dark theme to the color picker.</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
<div class="list-header">4.0.0-rc2<span>@377f701926189263186b</span> <time datetime="2018-05-31">(2018-05-31)</time></div>
|
<div class="list-header">4.0.0-rc2<span>@377f701926189263186b</span> <time datetime="2018-05-31">(2018-05-31)</time></div>
|
||||||
<ul class="chat-menu-content menu-side-padding">
|
<ul class="chat-menu-content menu-side-padding">
|
||||||
<li>Added: Basic support for custom highlight terms and blocked terms in chat. This system will later be replaced with a more powerful chat filtering system.</li>
|
<li>Added: Basic support for custom highlight terms and blocked terms in chat. This system will later be replaced with a more powerful chat filtering system.</li>
|
||||||
|
|
4
package-lock.json
generated
4
package-lock.json
generated
|
@ -7680,8 +7680,7 @@
|
||||||
"ret": {
|
"ret": {
|
||||||
"version": "0.1.15",
|
"version": "0.1.15",
|
||||||
"resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz",
|
"resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz",
|
||||||
"integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==",
|
"integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"right-align": {
|
"right-align": {
|
||||||
"version": "0.1.3",
|
"version": "0.1.3",
|
||||||
|
@ -7754,7 +7753,6 @@
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz",
|
||||||
"integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=",
|
"integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=",
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"ret": "0.1.15"
|
"ret": "0.1.15"
|
||||||
}
|
}
|
||||||
|
|
|
@ -58,6 +58,7 @@
|
||||||
"path-to-regexp": "^2.2.1",
|
"path-to-regexp": "^2.2.1",
|
||||||
"popper.js": "^1.14.3",
|
"popper.js": "^1.14.3",
|
||||||
"raven-js": "^3.24.2",
|
"raven-js": "^3.24.2",
|
||||||
|
"safe-regex": "^1.1.0",
|
||||||
"sortablejs": "^1.7.0",
|
"sortablejs": "^1.7.0",
|
||||||
"vue": "^2.5.16",
|
"vue": "^2.5.16",
|
||||||
"vue-clickaway": "^2.2.2",
|
"vue-clickaway": "^2.2.2",
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
|
|
||||||
import Module from 'utilities/module';
|
import Module from 'utilities/module';
|
||||||
import {createElement, ManagedStyle} from 'utilities/dom';
|
import {createElement, ManagedStyle} from 'utilities/dom';
|
||||||
import {timeout, has, glob_to_regex, escape_regex} from 'utilities/object';
|
import {timeout, has, glob_to_regex, escape_regex, split_chars} from 'utilities/object';
|
||||||
|
|
||||||
import Badges from './badges';
|
import Badges from './badges';
|
||||||
import Emotes from './emotes';
|
import Emotes from './emotes';
|
||||||
|
@ -111,6 +111,7 @@ export default class Chat extends Module {
|
||||||
this.settings.add('chat.filtering.highlight-basic-terms', {
|
this.settings.add('chat.filtering.highlight-basic-terms', {
|
||||||
default: [],
|
default: [],
|
||||||
type: 'array_merge',
|
type: 'array_merge',
|
||||||
|
always_inherit: true,
|
||||||
ui: {
|
ui: {
|
||||||
path: 'Chat > Filtering >> Highlight Terms',
|
path: 'Chat > Filtering >> Highlight Terms',
|
||||||
component: 'basic-terms',
|
component: 'basic-terms',
|
||||||
|
@ -129,30 +130,44 @@ export default class Chat extends Module {
|
||||||
const colors = new Map;
|
const colors = new Map;
|
||||||
|
|
||||||
for(const item of val) {
|
for(const item of val) {
|
||||||
let list;
|
|
||||||
const c = item.c || null,
|
const c = item.c || null,
|
||||||
t = item.t;
|
t = item.t;
|
||||||
|
|
||||||
let v = item.v;
|
let v = item.v, word = true;
|
||||||
|
|
||||||
if ( t === 'glob' )
|
if ( t === 'glob' )
|
||||||
v = glob_to_regex(v);
|
v = glob_to_regex(v);
|
||||||
|
|
||||||
else if ( t !== 'raw' )
|
else if ( t === 'raw' )
|
||||||
|
word = false;
|
||||||
|
|
||||||
|
else if ( t !== 'regex' )
|
||||||
v = escape_regex(v);
|
v = escape_regex(v);
|
||||||
|
|
||||||
if ( ! v || ! v.length )
|
if ( ! v || ! v.length )
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
try {
|
||||||
|
new RegExp(v);
|
||||||
|
} catch(err) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if ( colors.has(c) )
|
if ( colors.has(c) )
|
||||||
colors.get(c).push(v);
|
colors.get(c)[word ? 0 : 1].push(v);
|
||||||
else
|
else {
|
||||||
colors.set(c, [v]);
|
const vals = [[],[]];
|
||||||
|
colors.set(c, vals);
|
||||||
|
vals[word ? 0 : 1].push(v);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for(const [key, list] of colors) {
|
||||||
|
if ( list[0].length )
|
||||||
|
list[1].push(`\\b(?:${list[0].join('|')})\\b`);
|
||||||
|
|
||||||
for(const [key, list] of colors)
|
colors.set(key, new RegExp(list[1].join('|'), 'gi'));
|
||||||
colors.set(key, new RegExp(`\\b(${list.join('|')})\\b`, 'gi'));
|
}
|
||||||
|
|
||||||
return colors;
|
return colors;
|
||||||
}
|
}
|
||||||
|
@ -162,6 +177,7 @@ export default class Chat extends Module {
|
||||||
this.settings.add('chat.filtering.highlight-basic-blocked', {
|
this.settings.add('chat.filtering.highlight-basic-blocked', {
|
||||||
default: [],
|
default: [],
|
||||||
type: 'array_merge',
|
type: 'array_merge',
|
||||||
|
always_inherit: true,
|
||||||
ui: {
|
ui: {
|
||||||
path: 'Chat > Filtering >> Blocked Terms',
|
path: 'Chat > Filtering >> Blocked Terms',
|
||||||
component: 'basic-terms'
|
component: 'basic-terms'
|
||||||
|
@ -176,28 +192,34 @@ export default class Chat extends Module {
|
||||||
if ( ! val || ! val.length )
|
if ( ! val || ! val.length )
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
const out = [];
|
const out = [[], []];
|
||||||
|
|
||||||
for(const item of val) {
|
for(const item of val) {
|
||||||
const t = item.t;
|
const t = item.t;
|
||||||
let v = item.v;
|
let v = item.v, word = true;
|
||||||
|
|
||||||
if ( t === 'glob' )
|
if ( t === 'glob' )
|
||||||
v = glob_to_regex(v);
|
v = glob_to_regex(v);
|
||||||
|
|
||||||
else if ( t !== 'raw' )
|
else if ( t === 'raw' )
|
||||||
|
word = false;
|
||||||
|
|
||||||
|
else if ( t !== 'regex' )
|
||||||
v = escape_regex(v);
|
v = escape_regex(v);
|
||||||
|
|
||||||
if ( ! v || ! v.length )
|
if ( ! v || ! v.length )
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
out.push(v);
|
out[word ? 0 : 1].push(v);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( ! out.length )
|
if ( out[0].length )
|
||||||
|
out[1].push(`\\b(?:${out[0].join('|')})\\b`);
|
||||||
|
|
||||||
|
if ( ! out[1].length )
|
||||||
return;
|
return;
|
||||||
|
|
||||||
return new RegExp(`\\b(${out.join('|')})\\b`, 'gi');
|
return new RegExp(out[1].join('|'), 'gi');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -621,7 +643,7 @@ export default class Chat extends Module {
|
||||||
ret = ret.slice(4);
|
ret = ret.slice(4);
|
||||||
}
|
}
|
||||||
|
|
||||||
idx += ret.length;
|
idx += split_chars(ret).length;
|
||||||
out.push(ret);
|
out.push(ret);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -305,10 +305,10 @@ export const CustomHighlights = {
|
||||||
|
|
||||||
out.push({
|
out.push({
|
||||||
type: 'highlight',
|
type: 'highlight',
|
||||||
text: match[1]
|
text: match[0]
|
||||||
});
|
});
|
||||||
|
|
||||||
idx = nix + match[1].length;
|
idx = nix + match[0].length;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( idx < text.length )
|
if ( idx < text.length )
|
||||||
|
@ -376,10 +376,10 @@ export const BlockedTerms = {
|
||||||
|
|
||||||
out.push({
|
out.push({
|
||||||
type: 'blocked',
|
type: 'blocked',
|
||||||
text: match[1]
|
text: match[0]
|
||||||
});
|
});
|
||||||
|
|
||||||
idx = nix + match[1].length;
|
idx = nix + match[0].length;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( idx < text.length )
|
if ( idx < text.length )
|
||||||
|
|
|
@ -1,38 +1,15 @@
|
||||||
<template lang="html">
|
<template lang="html">
|
||||||
<section class="ffz--widget ffz--basic-terms">
|
<section class="ffz--widget ffz--basic-terms">
|
||||||
<div class="tw-align-items-center tw-flex tw-flex-nowrap tw-flex-row tw-full-width tw-pd-b-1">
|
<term-editor
|
||||||
<div class="tw-flex-grow-1">
|
:term="default_term"
|
||||||
<input
|
:colored="item.colored"
|
||||||
v-model="new_term"
|
:adding="true"
|
||||||
:placeholder="t('setting.terms.add-placeholder', 'Add a new term')"
|
@save="new_term"
|
||||||
type="text"
|
/>
|
||||||
class="tw-input"
|
<div v-if="! val.length || val.length === 1 && hasInheritance" class="tw-mg-t-05 tw-c-text-alt-2 tw-font-size-4 tw-align-center tw-c-text-alt-2 tw-pd-05">
|
||||||
autocapitalize="off"
|
|
||||||
autocorrect="off"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<div v-if="item.colored" class="tw-flex-shrink-0 tw-mg-l-05">
|
|
||||||
<color-picker v-model="new_color" :nullable="true" :show-input="false" />
|
|
||||||
</div>
|
|
||||||
<div class="tw-flex-shrink-0 tw-mg-x-05">
|
|
||||||
<select v-model="new_type" class="tw-select ffz-min-width-unset">
|
|
||||||
<option value="text">{{ t('setting.terms.type.text', 'Text') }}</option>
|
|
||||||
<option value="raw">{{ t('setting.terms.type.regex', 'Regex') }}</option>
|
|
||||||
<option value="glob">{{ t('setting.terms.type.glob', 'Glob') }}</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="tw-flex-shrink-0">
|
|
||||||
<button class="tw-button" @click="add">
|
|
||||||
<span class="tw-button__text">
|
|
||||||
{{ t('setting.terms.add-term', 'Add') }}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-if="! val.length || val.length === 1 && hasInheritance" class="tw-c-text-alt-2 tw-font-size-4 tw-align-center tw-c-text-alt-2 tw-pd-05">
|
|
||||||
{{ t('setting.terms.no-terms', 'no terms are defined in this profile') }}
|
{{ t('setting.terms.no-terms', 'no terms are defined in this profile') }}
|
||||||
</div>
|
</div>
|
||||||
<ul v-else class="ffz--term-list">
|
<ul v-else class="ffz--term-list tw-mg-t-05">
|
||||||
<term-editor
|
<term-editor
|
||||||
v-for="term in val"
|
v-for="term in val"
|
||||||
v-if="term.t !== 'inherit'"
|
v-if="term.t !== 'inherit'"
|
||||||
|
@ -59,9 +36,11 @@ export default {
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
new_term: '',
|
default_term: {
|
||||||
new_type: 'text',
|
v: '',
|
||||||
new_color: ''
|
t: 'text',
|
||||||
|
c: ''
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -84,17 +63,12 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
add() {
|
new_term(term) {
|
||||||
const vals = Array.from(this.val);
|
if ( ! term.v )
|
||||||
vals.push({
|
return;
|
||||||
v: {
|
|
||||||
t: this.new_type,
|
|
||||||
v: this.new_term,
|
|
||||||
c: typeof this.new_color === 'string' ? this.new_color : null
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.new_term = '';
|
const vals = Array.from(this.val);
|
||||||
|
vals.push({v: term});
|
||||||
this.set(deep_copy(vals));
|
this.set(deep_copy(vals));
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -38,7 +38,12 @@
|
||||||
</figure>
|
</figure>
|
||||||
<figure v-else class="ffz-i-eyedropper" />
|
<figure v-else class="ffz-i-eyedropper" />
|
||||||
</button>
|
</button>
|
||||||
<div v-on-clickaway="closePicker" v-if="open" class="tw-absolute tw-z-default tw-right-0">
|
<div
|
||||||
|
v-on-clickaway="closePicker"
|
||||||
|
v-if="open"
|
||||||
|
:class="{'ffz-bottom-100': openUp}"
|
||||||
|
class="tw-absolute tw-z-default tw-balloon--up tw-balloon--right"
|
||||||
|
>
|
||||||
<chrome-picker :value="colors" @input="onPick" />
|
<chrome-picker :value="colors" @input="onPick" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -74,6 +79,10 @@ export default {
|
||||||
default: true
|
default: true
|
||||||
},
|
},
|
||||||
showInput: {
|
showInput: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
openUp: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false
|
default: false
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,18 @@
|
||||||
<template lang="html">
|
<template lang="html">
|
||||||
<li class="ffz--term">
|
<div class="ffz--term">
|
||||||
<div class="tw-align-items-center tw-flex tw-flex-nowrap tw-flex-row tw-full-width">
|
<div class="tw-align-items-center tw-flex tw-flex-nowrap tw-flex-row tw-full-width">
|
||||||
|
<div v-if="! is_valid" class="tw-tooltip-wrapper tw-mg-r-05">
|
||||||
|
<figure class="tw-c-text-error ffz-i-attention" />
|
||||||
|
<div class="tw-tooltip tw-tooltip--down tw-tooltip--align-left">
|
||||||
|
{{ t('setting.terms.warn-invalid', 'This highlight term is invalid.') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="! is_safe" class="tw-tooltip-wrapper tw-mg-r-05">
|
||||||
|
<figure class="tw-c-text-hint ffz-i-attention" />
|
||||||
|
<div class="tw-tooltip tw-tooltip--down tw-tooltip--align-left">
|
||||||
|
{{ t('setting.terms.warn-complex', 'This highlight term is potentially too complex. It may cause client lag.') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="tw-flex-grow-1">
|
<div class="tw-flex-grow-1">
|
||||||
<h4 v-if="! editing" class="ffz-monospace">
|
<h4 v-if="! editing" class="ffz-monospace">
|
||||||
<pre>{{ term.v }}</pre>
|
<pre>{{ term.v }}</pre>
|
||||||
|
@ -8,7 +20,7 @@
|
||||||
<input
|
<input
|
||||||
v-else
|
v-else
|
||||||
v-model="edit_data.v"
|
v-model="edit_data.v"
|
||||||
:placeholder="edit_data.v"
|
:placeholder="adding ? t('setting.terms.add-placeholder', 'Add a new term') : edit_data.v"
|
||||||
type="text"
|
type="text"
|
||||||
class="tw-input"
|
class="tw-input"
|
||||||
autocapitalize="off"
|
autocapitalize="off"
|
||||||
|
@ -16,7 +28,7 @@
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="colored" class="tw-flex-shrink-0 tw-mg-l-05">
|
<div v-if="colored" class="tw-flex-shrink-0 tw-mg-l-05">
|
||||||
<color-picker v-if="editing" v-model="edit_data.c" :nullable="true" :show-input="false" />
|
<color-picker v-if="editing" v-model="edit_data.c" :nullable="true" :show-input="false" :open-up="true" />
|
||||||
<div v-else-if="term.c" class="ffz-color-preview">
|
<div v-else-if="term.c" class="ffz-color-preview">
|
||||||
<figure :style="`background-color: ${term.c}`">
|
<figure :style="`background-color: ${term.c}`">
|
||||||
|
|
||||||
|
@ -27,11 +39,19 @@
|
||||||
<span v-if="! editing">{{ term_type }}</span>
|
<span v-if="! editing">{{ term_type }}</span>
|
||||||
<select v-else v-model="edit_data.t" class="tw-select ffz-min-width-unset">
|
<select v-else v-model="edit_data.t" class="tw-select ffz-min-width-unset">
|
||||||
<option value="text">{{ t('setting.terms.type.text', 'Text') }}</option>
|
<option value="text">{{ t('setting.terms.type.text', 'Text') }}</option>
|
||||||
<option value="raw">{{ t('setting.terms.type.regex', 'Regex') }}</option>
|
|
||||||
<option value="glob">{{ t('setting.terms.type.glob', 'Glob') }}</option>
|
<option value="glob">{{ t('setting.terms.type.glob', 'Glob') }}</option>
|
||||||
|
<option value="regex">{{ t('setting.terms.type.regex-word', 'Regex (Word)') }}</option>
|
||||||
|
<option value="raw">{{ t('setting.terms.type.regex', 'Regex') }}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="editing" class="tw-flex-shrink-0">
|
<div v-if="adding" class="tw-flex-shrink-0">
|
||||||
|
<button class="tw-button" @click="save">
|
||||||
|
<span class="tw-button__text">
|
||||||
|
{{ t('setting.terms.add-term', 'Add') }}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="editing" class="tw-flex-shrink-0">
|
||||||
<button class="tw-button tw-button--text tw-tooltip-wrapper" @click="save">
|
<button class="tw-button tw-button--text tw-tooltip-wrapper" @click="save">
|
||||||
<span class="tw-button__text ffz-i-floppy" />
|
<span class="tw-button__text ffz-i-floppy" />
|
||||||
<div class="tw-tooltip tw-tooltip--down tw-tooltip--align-right">
|
<div class="tw-tooltip tw-tooltip--down tw-tooltip--align-right">
|
||||||
|
@ -74,17 +94,36 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
import {deep_copy} from 'utilities/object';
|
import safety from 'safe-regex';
|
||||||
|
|
||||||
|
import {deep_copy, glob_to_regex, escape_regex} from 'utilities/object';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: ['term', 'colored'],
|
props: {
|
||||||
|
term: Object,
|
||||||
|
colored: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
adding: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
|
if ( this.adding )
|
||||||
|
return {
|
||||||
|
deleting: false,
|
||||||
|
editing: true,
|
||||||
|
edit_data: deep_copy(this.term)
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
deleting: false,
|
deleting: false,
|
||||||
editing: false,
|
editing: false,
|
||||||
|
@ -93,6 +132,41 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
|
is_valid() {
|
||||||
|
const data = this.editing ? this.edit_data : this.term,
|
||||||
|
t = data.t;
|
||||||
|
|
||||||
|
let v = data.v;
|
||||||
|
|
||||||
|
if ( t === 'text' )
|
||||||
|
v = escape_regex(v);
|
||||||
|
|
||||||
|
else if ( t === 'glob' )
|
||||||
|
v = glob_to_regex(v);
|
||||||
|
|
||||||
|
try {
|
||||||
|
new RegExp(v);
|
||||||
|
return true;
|
||||||
|
} catch(err) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
is_safe() {
|
||||||
|
const data = this.editing ? this.edit_data : this.term,
|
||||||
|
t = data.t;
|
||||||
|
|
||||||
|
let v = data.v;
|
||||||
|
|
||||||
|
if ( t === 'text' )
|
||||||
|
v = escape_regex(v);
|
||||||
|
|
||||||
|
else if ( t === 'glob' )
|
||||||
|
v = glob_to_regex(v);
|
||||||
|
|
||||||
|
return safety(v);
|
||||||
|
},
|
||||||
|
|
||||||
term_type() {
|
term_type() {
|
||||||
const t = this.term && this.term.t;
|
const t = this.term && this.term.t;
|
||||||
if ( t === 'text' )
|
if ( t === 'text' )
|
||||||
|
@ -104,6 +178,9 @@ export default {
|
||||||
else if ( t === 'glob' )
|
else if ( t === 'glob' )
|
||||||
return this.t('setting.terms.type.glob', 'Glob');
|
return this.t('setting.terms.type.glob', 'Glob');
|
||||||
|
|
||||||
|
else if ( t === 'regex' )
|
||||||
|
return this.t('setting.terms.type.regex-word', 'Regex (Word)');
|
||||||
|
|
||||||
return this.t('setting.unknown', 'Unknown Value');
|
return this.t('setting.unknown', 'Unknown Value');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -115,8 +192,13 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
cancel() {
|
cancel() {
|
||||||
this.editing = false;
|
if ( this.adding ) {
|
||||||
this.edit_data = null;
|
this.edit_data = deep_copy(this.term);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
this.editing = false;
|
||||||
|
this.edit_data = null;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
save() {
|
save() {
|
||||||
|
|
|
@ -243,7 +243,7 @@ export default class SettingsContext extends EventEmitter {
|
||||||
value = def_default;
|
value = def_default;
|
||||||
|
|
||||||
if ( type.default )
|
if ( type.default )
|
||||||
value = type.default(value);
|
value = type.default(value, definition, this.manager.log);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( definition.requires )
|
if ( definition.requires )
|
||||||
|
@ -269,9 +269,9 @@ export default class SettingsContext extends EventEmitter {
|
||||||
|
|
||||||
_getRaw(key, type) {
|
_getRaw(key, type) {
|
||||||
if ( ! type )
|
if ( ! type )
|
||||||
throw new Error(`non-existent `)
|
throw new Error(`non-existent type for ${key}`)
|
||||||
|
|
||||||
return type.get(key, this.profiles(), this.manager.log);
|
return type.get(key, this.profiles(), this.manager.definitions.get(key), this.manager.log);
|
||||||
}
|
}
|
||||||
/* for(const profile of this.__profiles)
|
/* for(const profile of this.__profiles)
|
||||||
if ( profile.has(key) )
|
if ( profile.has(key) )
|
||||||
|
|
|
@ -52,7 +52,7 @@ export const array_merge = {
|
||||||
return values;
|
return values;
|
||||||
},
|
},
|
||||||
|
|
||||||
get(key, profiles, log) {
|
get(key, profiles, definition, log) {
|
||||||
const values = [],
|
const values = [],
|
||||||
trailing = [],
|
trailing = [],
|
||||||
sources = [];
|
sources = [];
|
||||||
|
@ -80,7 +80,7 @@ export const array_merge = {
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we didn't run into an inherit, don't inherit.
|
// If we didn't run into an inherit, don't inherit.
|
||||||
if ( ! is_trailing )
|
if ( ! is_trailing && ! definition.always_inherit )
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
import Module from 'utilities/module';
|
import Module from 'utilities/module';
|
||||||
|
import { has } from 'utilities/object';
|
||||||
|
|
||||||
|
|
||||||
export default class Channel extends Module {
|
export default class Channel extends Module {
|
||||||
|
@ -41,7 +42,7 @@ export default class Channel extends Module {
|
||||||
|
|
||||||
this.ChannelPage = this.fine.define(
|
this.ChannelPage = this.fine.define(
|
||||||
'channel-page',
|
'channel-page',
|
||||||
n => n.handleHostingChange,
|
n => n.hostModeFromGraphQL,
|
||||||
['user']
|
['user']
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -70,10 +71,9 @@ export default class Channel extends Module {
|
||||||
// We can't do this immediately because the player state
|
// We can't do this immediately because the player state
|
||||||
// occasionally screws up if we do.
|
// occasionally screws up if we do.
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const current_channel = inst.props.data && inst.props.data.variables && inst.props.data.variables.currentChannelLogin;
|
if ( inst.state.hostMode ) {
|
||||||
if ( current_channel && current_channel !== inst.state.videoPlayerSource ) {
|
inst.ffzExpectedHost = inst.state.hostMode;
|
||||||
inst.ffzExpectedHost = inst.state.videoPlayerSource;
|
inst.ffzOldSetState({hostMode: null});
|
||||||
inst.ffzOldHostHandler(null);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -107,24 +107,32 @@ export default class Channel extends Module {
|
||||||
|
|
||||||
const t = this;
|
const t = this;
|
||||||
|
|
||||||
inst._ffz_hosting_wrapped = true;
|
inst.ffzOldSetState = inst.setState;
|
||||||
|
inst.setState = function(state, ...args) {
|
||||||
|
try {
|
||||||
|
if ( has(state, 'hostMode') ) {
|
||||||
|
inst.ffzExpectedHost = state.hostMode;
|
||||||
|
if ( state.hostMode && ! t.settings.get('channel.hosting.enable') ) {
|
||||||
|
state.hostMode = null;
|
||||||
|
state.videoPlayerSource = inst.props.match.params.channelName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
inst.ffzOldHostHandler = inst.handleHostingChange;
|
} catch(err) {
|
||||||
inst.handleHostingChange = function(channel) {
|
t.log.capture(err, {extra: {props: inst.props, state}});
|
||||||
inst.ffzExpectedHost = channel;
|
}
|
||||||
|
|
||||||
if ( t.settings.get('channel.hosting.enable') )
|
return inst.ffzOldSetState(state, ...args);
|
||||||
return inst.ffzOldHostHandler(channel);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store the current state and disable the current host if needed.
|
inst._ffz_hosting_wrapped = true;
|
||||||
inst.ffzExpectedHost = inst.state.isHosting ? inst.state.videoPlayerSource : null;
|
|
||||||
if ( ! this.settings.get('channel.hosting.enable') )
|
|
||||||
inst.ffzOldHostHandler(null);
|
|
||||||
|
|
||||||
// Finally, we force an update so that any child components
|
const hosted = inst.ffzExpectedHost = inst.state.hostMode;
|
||||||
// receive our updated handler.
|
if ( hosted && ! this.settings.get('channel.hosting.enable') )
|
||||||
inst.forceUpdate();
|
inst.ffzOldSetState({
|
||||||
|
hostMode: null,
|
||||||
|
videoPlayerSource: inst.props.match.params.channelName
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -132,7 +140,14 @@ export default class Channel extends Module {
|
||||||
if ( val === undefined )
|
if ( val === undefined )
|
||||||
val = this.settings.get('channel.hosting.enable');
|
val = this.settings.get('channel.hosting.enable');
|
||||||
|
|
||||||
for(const inst of this.ChannelPage.instances)
|
for(const inst of this.ChannelPage.instances) {
|
||||||
inst.ffzOldHostHandler(val ? inst.ffzExpectedHost : null);
|
const host = val ? inst.ffzExpectedHost : null,
|
||||||
|
target = host && host.hostedChannel && host.hostedChannel.login || inst.props.match.params.channelName;
|
||||||
|
|
||||||
|
inst.ffzOldSetState({
|
||||||
|
hostMode: host,
|
||||||
|
videoPlayerSource: target
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -566,6 +566,25 @@ export default class ChatHook extends Module {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
cls.prototype.ffzGetEmotes = function() {
|
||||||
|
const emote_sets = this.client && this.client.session && this.client.session.emoteSets;
|
||||||
|
if ( this._ffz_cached_sets === emote_sets )
|
||||||
|
return this._ffz_cached_emotes;
|
||||||
|
|
||||||
|
this._ffz_cached_sets = emote_sets;
|
||||||
|
const emotes = this._ffz_cached_emotes = {};
|
||||||
|
|
||||||
|
if ( emote_sets )
|
||||||
|
for(const set of emote_sets)
|
||||||
|
if ( set && set.emotes )
|
||||||
|
for(const emote of set.emotes)
|
||||||
|
if ( emote )
|
||||||
|
emotes[emote.token] = emote.id;
|
||||||
|
|
||||||
|
return emotes;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
cls.prototype.connectHandlers = function(...args) {
|
cls.prototype.connectHandlers = function(...args) {
|
||||||
if ( ! this._ffz_init ) {
|
if ( ! this._ffz_init ) {
|
||||||
const i = this;
|
const i = this;
|
||||||
|
@ -581,6 +600,42 @@ export default class ChatHook extends Module {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const old_chat = this.onChatMessageEvent;
|
||||||
|
this.onChatMessageEvent = function(e) {
|
||||||
|
if ( e && e.sentByCurrentUser ) {
|
||||||
|
try {
|
||||||
|
e.message.user.emotes = findEmotes(
|
||||||
|
e.message.body,
|
||||||
|
i.ffzGetEmotes()
|
||||||
|
);
|
||||||
|
|
||||||
|
} catch(err) {
|
||||||
|
t.log.capture(err, {extra: e});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return old_chat.call(i, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const old_action = this.onChatActionEvent;
|
||||||
|
this.onChatActionEvent = function(e) {
|
||||||
|
if ( e && e.sentByCurrentUser ) {
|
||||||
|
try {
|
||||||
|
e.message.user.emotes = findEmotes(
|
||||||
|
e.message.body.slice(8, -1),
|
||||||
|
i.ffzGetEmotes()
|
||||||
|
);
|
||||||
|
|
||||||
|
} catch(err) {
|
||||||
|
t.log.capture(err, {extra: e});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return old_action.call(i, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
const old_resub = this.onResubscriptionEvent;
|
const old_resub = this.onResubscriptionEvent;
|
||||||
this.onResubscriptionEvent = function(e) {
|
this.onResubscriptionEvent = function(e) {
|
||||||
try {
|
try {
|
||||||
|
@ -626,11 +681,11 @@ export default class ChatHook extends Module {
|
||||||
|
|
||||||
const old_post = this.postMessage;
|
const old_post = this.postMessage;
|
||||||
this.postMessage = function(e) {
|
this.postMessage = function(e) {
|
||||||
const original = this._wrapped;
|
const original = i._wrapped;
|
||||||
if ( original && ! e._ffz_checked )
|
if ( original && ! e._ffz_checked )
|
||||||
return this.postMessageToCurrentChannel(original, e);
|
return i.postMessageToCurrentChannel(original, e);
|
||||||
|
|
||||||
return old_post.call(this, e);
|
return old_post.call(i, e);
|
||||||
}
|
}
|
||||||
|
|
||||||
this._ffz_init = true;
|
this._ffz_init = true;
|
||||||
|
@ -662,11 +717,6 @@ export default class ChatHook extends Module {
|
||||||
message.message = original.action;
|
message.message = original.action;
|
||||||
else
|
else
|
||||||
message.message = original.message.body;
|
message.message = original.message.body;
|
||||||
|
|
||||||
// Twitch doesn't generate a proper emote tag for echoed back
|
|
||||||
// actions, so we have to regenerate it. Fun. :D
|
|
||||||
if ( user && user.username === this.userLogin )
|
|
||||||
message.emotes = findEmotes(message.message, this.selfEmotes);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.postMessage(message);
|
this.postMessage(message);
|
||||||
|
@ -903,12 +953,14 @@ export function findEmotes(msg, emotes) {
|
||||||
const out = {};
|
const out = {};
|
||||||
let idx = 0;
|
let idx = 0;
|
||||||
|
|
||||||
|
console.log('findEmotes', msg, emotes);
|
||||||
|
|
||||||
for(const part of msg.split(' ')) {
|
for(const part of msg.split(' ')) {
|
||||||
const len = split_chars(part).length;
|
const len = split_chars(part).length;
|
||||||
|
|
||||||
if ( has(emotes, part) ) {
|
if ( has(emotes, part) ) {
|
||||||
const em = emotes[part],
|
const em = emotes[part],
|
||||||
matches = out[em.id] = out[em.id] || [];
|
matches = out[em] = out[em] || [];
|
||||||
|
|
||||||
matches.push({
|
matches.push({
|
||||||
startIndex: idx,
|
startIndex: idx,
|
||||||
|
|
|
@ -244,15 +244,17 @@ export default class ChatLine extends Module {
|
||||||
e('span', {
|
e('span', {
|
||||||
className: 'chat-line__message--badges'
|
className: 'chat-line__message--badges'
|
||||||
}, t.chat.badges.render(msg, e)),
|
}, t.chat.badges.render(msg, e)),
|
||||||
e('a', {
|
e('button', {
|
||||||
className: 'chat-author__display-name notranslate',
|
className: 'chat-line__username notranslate',
|
||||||
style: { color },
|
style: { color },
|
||||||
onClick: this.usernameClickHandler, // this.ffz_user_click_handler
|
onClick: this.usernameClickHandler, // this.ffz_user_click_handler
|
||||||
}, [
|
}, [
|
||||||
user.userDisplayName,
|
e('span', {
|
||||||
|
className: 'chat-author__display-name'
|
||||||
|
}, user.displayName),
|
||||||
user.isIntl && e('span', {
|
user.isIntl && e('span', {
|
||||||
className: 'chat-author__intl-login'
|
className: 'chat-author__intl-login'
|
||||||
}, ` (${user.userLogin})`)
|
}, ` (${user.login})`)
|
||||||
]),
|
]),
|
||||||
e('span', null, is_action ? ' ' : ': '),
|
e('span', null, is_action ? ' ' : ': '),
|
||||||
show ?
|
show ?
|
||||||
|
|
|
@ -19,8 +19,8 @@ const CLASSES = {
|
||||||
|
|
||||||
'prime-offers': '.top-nav__prime',
|
'prime-offers': '.top-nav__prime',
|
||||||
|
|
||||||
'player-ext': '.player .extension-overlay',
|
'player-ext': '.player .extension-taskbar,.player .extension-container',
|
||||||
'player-ext-hover': '.player:not([data-controls="true"]) .extension-overlay',
|
'player-ext-hover': '.player:not([data-controls="true"]) .extension-container',
|
||||||
|
|
||||||
'player-event-bar': '.channel-page .live-event-banner-ui__header',
|
'player-event-bar': '.channel-page .live-event-banner-ui__header',
|
||||||
'player-rerun-bar': '.channel-page div.tw-c-text-overlay:not([data-a-target="hosting-ui-header"])',
|
'player-rerun-bar': '.channel-page div.tw-c-text-overlay:not([data-a-target="hosting-ui-header"])',
|
||||||
|
@ -28,11 +28,8 @@ const CLASSES = {
|
||||||
'pinned-cheer': '.pinned-cheer,.pinned-cheer-v2',
|
'pinned-cheer': '.pinned-cheer,.pinned-cheer-v2',
|
||||||
'whispers': '.whispers',
|
'whispers': '.whispers',
|
||||||
|
|
||||||
'dir-live-ind': '.live-channel-card:not([data-a-target*="host"]) .stream-type-indicator.stream-type-indicator--live',
|
'dir-live-ind': '.live-channel-card:not([data-a-target*="host"]) .stream-type-indicator.stream-type-indicator--live,.stream-thumbnail__card .stream-type-indicator.stream-type-indicator--live',
|
||||||
'boxart-hover': '.tw-card .tw-full-width:hover a[data-a-target="live-channel-card-game-link"]',
|
'profile-hover': '.preview-card .tw-relative:hover .ffz-channel-avatar',
|
||||||
'boxart-hide': '.tw-card a[data-a-target="live-channel-card-game-link"]',
|
|
||||||
'profile-hover-following': '.tw-card .tw-full-width:hover .ffz-channel-avatar',
|
|
||||||
'profile-hover-game': '.tw-thumbnail-card .tw-card-img:hover .ffz-channel-avatar',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -18,79 +18,25 @@ export default class BrowsePopular extends SiteModule {
|
||||||
this.inject('settings');
|
this.inject('settings');
|
||||||
|
|
||||||
this.apollo.registerModifier('BrowsePage_Popular', BROWSE_POPULAR);
|
this.apollo.registerModifier('BrowsePage_Popular', BROWSE_POPULAR);
|
||||||
|
|
||||||
this.ChannelCard = this.fine.define(
|
|
||||||
'browse-all-channel-card',
|
|
||||||
n => n.props && n.props.channelName && n.props.linkTo && n.props.linkTo.state && n.props.linkTo.state.medium === 'twitch_browse_directory',
|
|
||||||
['dir-all']
|
|
||||||
);
|
|
||||||
|
|
||||||
this.apollo.registerModifier('BrowsePage_Popular', res => this.modifyStreams(res), false);
|
this.apollo.registerModifier('BrowsePage_Popular', res => this.modifyStreams(res), false);
|
||||||
}
|
}
|
||||||
|
|
||||||
onEnable() {
|
onEnable() {
|
||||||
this.ChannelCard.ready((cls, instances) => {
|
// Popular Directory Channel Cards
|
||||||
// Popular Directory Channel Cards
|
this.apollo.ensureQuery(
|
||||||
this.apollo.ensureQuery(
|
'BrowsePage_Popular',
|
||||||
'BrowsePage_Popular',
|
'data.streams.edges.node.0.createdAt'
|
||||||
'data.streams.edges.node.0.createdAt'
|
);
|
||||||
);
|
|
||||||
|
|
||||||
for(const inst of instances) this.updateChannelCard(inst);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.ChannelCard.on('update', this.updateChannelCard, this);
|
|
||||||
this.ChannelCard.on('mount', this.updateChannelCard, this);
|
|
||||||
this.ChannelCard.on('unmount', this.parent.clearUptime, this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
modifyStreams(res) { // eslint-disable-line class-methods-use-this
|
modifyStreams(res) { // eslint-disable-line class-methods-use-this
|
||||||
const blockedGames = this.settings.provider.get('directory.game.blocked-games') || [];
|
this.log.info('Streams', res);
|
||||||
|
|
||||||
const newStreams = [];
|
|
||||||
|
|
||||||
const edges = get('data.streams.edges', res);
|
const edges = get('data.streams.edges', res);
|
||||||
if (!edges) return res;
|
if ( ! edges || ! edges.length )
|
||||||
|
return res;
|
||||||
|
|
||||||
for (let i = 0; i < edges.length; i++) {
|
res.data.streams.edges = this.parent.processNodes(edges);
|
||||||
const edge = edges[i];
|
|
||||||
const node = edge.node;
|
|
||||||
|
|
||||||
const s = node.viewersCount = new Number(node.viewersCount || 0);
|
|
||||||
s.profileImageURL = node.broadcaster.profileImageURL;
|
|
||||||
s.createdAt = node.createdAt;
|
|
||||||
s.login = node.broadcaster.login;
|
|
||||||
s.displayName = node.broadcaster.displayName;
|
|
||||||
|
|
||||||
if (!node.game || node.game && !blockedGames.includes(node.game.name)) newStreams.push(edge);
|
|
||||||
}
|
|
||||||
res.data.streams.edges = newStreams;
|
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
updateChannelCard(inst) {
|
|
||||||
const container = this.fine.getChildNode(inst);
|
|
||||||
if (!container) return;
|
|
||||||
|
|
||||||
if (container.classList.contains('ffz-modified-channel-card')) return;
|
|
||||||
container.classList.add('ffz-modified-channel-card');
|
|
||||||
|
|
||||||
this.parent.updateUptime(inst, 'props.viewerCount.createdAt', '.tw-card-img');
|
|
||||||
this.parent.addCardAvatar(inst, 'props.viewerCount', '.tw-card');
|
|
||||||
|
|
||||||
const hiddenThumbnails = this.settings.provider.get('directory.game.hidden-thumbnails') || [];
|
|
||||||
const hiddenPreview = 'https://static-cdn.jtvnw.net/ttv-static/404_preview-320x180.jpg';
|
|
||||||
|
|
||||||
if (inst.props.type === 'watch_party')
|
|
||||||
container.classList.toggle('tw-hide', this.settings.get('directory.hide-vodcasts'));
|
|
||||||
|
|
||||||
const img = container.querySelector && container.querySelector('.tw-card-img img');
|
|
||||||
if (img == null) return;
|
|
||||||
|
|
||||||
if (hiddenThumbnails.includes(inst.props.gameTitle)) {
|
|
||||||
img.src = hiddenPreview;
|
|
||||||
} else {
|
|
||||||
img.src = inst.props.imageSrc;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -68,102 +68,66 @@ export default class Following extends SiteModule {
|
||||||
this.apollo.registerModifier('FollowingHosts_CurrentUser', FOLLOWED_HOSTS);
|
this.apollo.registerModifier('FollowingHosts_CurrentUser', FOLLOWED_HOSTS);
|
||||||
this.apollo.registerModifier('FollowedChannels', FOLLOWED_CHANNELS);
|
this.apollo.registerModifier('FollowedChannels', FOLLOWED_CHANNELS);
|
||||||
|
|
||||||
this.ChannelCard = this.fine.define(
|
|
||||||
'following-channel-card',
|
|
||||||
n => n.renderGameBoxArt && n.renderContentType,
|
|
||||||
['dir-following']
|
|
||||||
);
|
|
||||||
|
|
||||||
this.apollo.registerModifier('FollowedIndex_CurrentUser', res => {
|
this.apollo.registerModifier('FollowedIndex_CurrentUser', res => {
|
||||||
this.modifyLiveUsers(res);
|
this.modifyLiveUsers(res);
|
||||||
this.modifyLiveHosts(res);
|
this.modifyLiveHosts(res);
|
||||||
}, false);
|
}, false);
|
||||||
|
|
||||||
this.on('settings:changed:directory.uptime', () => this.ChannelCard.forceUpdate());
|
|
||||||
this.on('settings:changed:directory.show-channel-avatars', () => this.ChannelCard.forceUpdate());
|
|
||||||
this.on('settings:changed:directory.show-boxart', () => this.ChannelCard.forceUpdate());
|
|
||||||
this.on('settings:changed:directory.hide-vodcasts', () => this.ChannelCard.forceUpdate());
|
|
||||||
|
|
||||||
this.apollo.registerModifier('FollowedChannels', res => this.modifyLiveUsers(res), false);
|
this.apollo.registerModifier('FollowedChannels', res => this.modifyLiveUsers(res), false);
|
||||||
this.apollo.registerModifier('FollowingLive_CurrentUser', res => this.modifyLiveUsers(res), false);
|
this.apollo.registerModifier('FollowingLive_CurrentUser', res => this.modifyLiveUsers(res), false);
|
||||||
this.apollo.registerModifier('FollowingHosts_CurrentUser', res => this.modifyLiveHosts(res), false);
|
this.apollo.registerModifier('FollowingHosts_CurrentUser', res => this.modifyLiveHosts(res), false);
|
||||||
}
|
}
|
||||||
|
|
||||||
modifyLiveUsers(res) {
|
modifyLiveUsers(res) {
|
||||||
const hiddenThumbnails = this.settings.provider.get('directory.game.hidden-thumbnails') || [];
|
const edges = get('data.currentUser.followedLiveUsers.nodes', res);
|
||||||
const blockedGames = this.settings.provider.get('directory.game.blocked-games') || [];
|
if ( ! edges || ! edges.length )
|
||||||
|
|
||||||
const newStreams = [];
|
|
||||||
|
|
||||||
const followedLiveUsers = get('data.currentUser.followedLiveUsers', res);
|
|
||||||
if (!followedLiveUsers)
|
|
||||||
return res;
|
return res;
|
||||||
|
|
||||||
const oldMode = !!followedLiveUsers.nodes;
|
res.data.currentUser.followedLiveUsers.nodes = this.parent.processNodes(edges);
|
||||||
const edgesOrNodes = followedLiveUsers.nodes || followedLiveUsers.edges;
|
|
||||||
|
|
||||||
for (let i = 0; i < edgesOrNodes.length; i++) {
|
|
||||||
const edge = edgesOrNodes[i],
|
|
||||||
node = edge.node || edge;
|
|
||||||
|
|
||||||
if ( ! node || ! node.stream )
|
|
||||||
continue;
|
|
||||||
|
|
||||||
const s = node.stream.viewersCount = new Number(node.stream.viewersCount || 0);
|
|
||||||
s.profileImageURL = node.profileImageURL;
|
|
||||||
s.createdAt = node.stream.createdAt;
|
|
||||||
|
|
||||||
if (node.stream.game && hiddenThumbnails.includes(node.stream.game.name)) node.stream.previewImageURL = 'https://static-cdn.jtvnw.net/ttv-static/404_preview-320x180.jpg';
|
|
||||||
if (!node.stream.game || node.stream.game && !blockedGames.includes(node.stream.game.name)) newStreams.push(edge);
|
|
||||||
}
|
|
||||||
res.data.currentUser.followedLiveUsers[oldMode ? 'nodes' : 'edges'] = newStreams;
|
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
modifyLiveHosts(res) {
|
modifyLiveHosts(res) {
|
||||||
const hiddenThumbnails = this.settings.provider.get('directory.game.hidden-thumbnails') || [];
|
const blocked_games = this.settings.provider.get('directory.game.blocked-games', []),
|
||||||
const blockedGames = this.settings.provider.get('directory.game.blocked-games') || [];
|
do_grouping = this.settings.get('directory.following.group-hosts'),
|
||||||
|
edges = get('data.currentUser.followedHosts.nodes', res);
|
||||||
|
|
||||||
this.hosts = {};
|
if ( ! edges || ! edges.length )
|
||||||
|
|
||||||
const followedHosts = get('data.currentUser.followedHosts', res);
|
|
||||||
if (!followedHosts)
|
|
||||||
return res;
|
return res;
|
||||||
|
|
||||||
const newHostNodes = [];
|
const hosts = {},
|
||||||
|
out = [];
|
||||||
|
|
||||||
const oldMode = !!followedHosts.nodes;
|
for(const edge of edges) {
|
||||||
const edgesOrNodes = followedHosts.nodes || followedHosts.edges;
|
const node = edge.node || edge,
|
||||||
|
hosted = node.hosting,
|
||||||
|
stream = hosted && hosted.stream;
|
||||||
|
|
||||||
for (let i = 0; i < edgesOrNodes.length; i++) {
|
if ( ! stream || stream.game && blocked_games.includes(stream.game.game) )
|
||||||
const edge = edgesOrNodes[i],
|
|
||||||
node = edge.node || edge;
|
|
||||||
|
|
||||||
if ( ! node || ! node.hosting || ! node.hosting.stream )
|
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
const s = node.hosting.stream.viewersCount = new Number(node.hosting.stream.viewersCount || 0);
|
const store = {}; // stream.viewersCount = new Number(stream.viewersCount || 0);
|
||||||
s.profileImageURL = node.hosting.profileImageURL;
|
|
||||||
s.createdAt = node.hosting.stream.createdAt;
|
|
||||||
|
|
||||||
if (!this.hosts[node.hosting.displayName]) {
|
store.createdAt = stream.createdAt;
|
||||||
this.hosts[node.hosting.displayName] = {
|
store.title = stream.title;
|
||||||
channel: node.hosting.login,
|
store.game = stream.game;
|
||||||
nodes: [node],
|
|
||||||
channels: [node.displayName]
|
if ( do_grouping ) {
|
||||||
};
|
const host_nodes = hosts[hosted.login];
|
||||||
if (node.hosting.stream.game && hiddenThumbnails.includes(node.hosting.stream.game.name)) node.hosting.stream.previewImageURL = 'https://static-cdn.jtvnw.net/ttv-static/404_preview-320x180.jpg';
|
if ( host_nodes ) {
|
||||||
if (!node.hosting.stream.game || node.hosting.stream.game && !blockedGames.includes(node.hosting.stream.game.name)) newHostNodes.push(edge);
|
host_nodes.push(node);
|
||||||
} else {
|
store.host_nodes = node._ffz_host_nodes = host_nodes;
|
||||||
this.hosts[node.hosting.displayName].nodes.push(node);
|
|
||||||
this.hosts[node.hosting.displayName].channels.push(node.displayName);
|
} else {
|
||||||
}
|
store.host_nodes = node._ffz_host_nodes = hosts[hosted.login] = [node];
|
||||||
|
out.push(edge);
|
||||||
|
}
|
||||||
|
|
||||||
|
} else
|
||||||
|
out.push(edge);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.settings.get('directory.following.group-hosts')) {
|
res.data.currentUser.followedHosts.nodes = out;
|
||||||
res.data.currentUser.followedHosts[oldMode ? 'nodes' : 'edges'] = newHostNodes;
|
|
||||||
}
|
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -200,18 +164,7 @@ export default class Following extends SiteModule {
|
||||||
}
|
}
|
||||||
|
|
||||||
onEnable() {
|
onEnable() {
|
||||||
this.ChannelCard.ready((cls, instances) => {
|
this.ensureQueries();
|
||||||
this.ensureQueries();
|
|
||||||
|
|
||||||
for(const inst of instances) this.updateChannelCard(inst);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.ChannelCard.on('update', inst => {
|
|
||||||
this.ensureQueries();
|
|
||||||
this.updateChannelCard(inst)
|
|
||||||
}, this);
|
|
||||||
this.ChannelCard.on('mount', this.updateChannelCard, this);
|
|
||||||
this.ChannelCard.on('unmount', this.parent.clearUptime, this);
|
|
||||||
|
|
||||||
document.body.addEventListener('click', this.destroyHostMenu.bind(this));
|
document.body.addEventListener('click', this.destroyHostMenu.bind(this));
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,7 +30,8 @@ export default class Game extends SiteModule {
|
||||||
|
|
||||||
onEnable() {
|
onEnable() {
|
||||||
this.GameHeader.ready((cls, instances) => {
|
this.GameHeader.ready((cls, instances) => {
|
||||||
for(const inst of instances) this.updateButtons(inst);
|
for(const inst of instances)
|
||||||
|
this.updateButtons(inst);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.GameHeader.on('update', this.updateButtons, this);
|
this.GameHeader.on('update', this.updateButtons, this);
|
||||||
|
@ -85,7 +86,7 @@ export default class Game extends SiteModule {
|
||||||
this.i18n.t('directory.show-thumbnails', 'Show Thumbnails') :
|
this.i18n.t('directory.show-thumbnails', 'Show Thumbnails') :
|
||||||
this.i18n.t('directory.hide-thumbnails', 'Hide Thumbnails');
|
this.i18n.t('directory.hide-thumbnails', 'Hide Thumbnails');
|
||||||
|
|
||||||
this.parent.ChannelCard.forceUpdate();
|
this.parent.DirectoryCard.forceUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
hidden_btn = (<button
|
hidden_btn = (<button
|
||||||
|
|
|
@ -13,6 +13,18 @@ import Following from './following';
|
||||||
import Game from './game';
|
import Game from './game';
|
||||||
import BrowsePopular from './browse_popular';
|
import BrowsePopular from './browse_popular';
|
||||||
|
|
||||||
|
|
||||||
|
export const CARD_CONTEXTS = ((e ={}) => {
|
||||||
|
e[e.SingleGameList = 1] = 'SingleGameList';
|
||||||
|
e[e.SingleChannelList = 2] = 'SingleChannelList';
|
||||||
|
e[e.MixedGameAndChannelList = 3] = 'MixedGameAndChannelList';
|
||||||
|
return e;
|
||||||
|
})();
|
||||||
|
|
||||||
|
|
||||||
|
const DIR_ROUTES = ['dir', 'dir-community', 'dir-community-index', 'dir-creative', 'dir-following', 'dir-game-index', 'dir-game-clips', 'dir-game-videos', 'dir-all', 'dir-category', 'user-videos', 'user-clips'];
|
||||||
|
|
||||||
|
|
||||||
export default class Directory extends SiteModule {
|
export default class Directory extends SiteModule {
|
||||||
constructor(...args) {
|
constructor(...args) {
|
||||||
super(...args);
|
super(...args);
|
||||||
|
@ -23,6 +35,7 @@ export default class Directory extends SiteModule {
|
||||||
this.inject('site.router');
|
this.inject('site.router');
|
||||||
this.inject('site.apollo');
|
this.inject('site.apollo');
|
||||||
this.inject('site.css_tweaks');
|
this.inject('site.css_tweaks');
|
||||||
|
this.inject('site.web_munch');
|
||||||
|
|
||||||
this.inject('i18n');
|
this.inject('i18n');
|
||||||
this.inject('settings');
|
this.inject('settings');
|
||||||
|
@ -33,12 +46,17 @@ export default class Directory extends SiteModule {
|
||||||
|
|
||||||
this.apollo.registerModifier('GamePage_Game', res => this.modifyStreams(res), false);
|
this.apollo.registerModifier('GamePage_Game', res => this.modifyStreams(res), false);
|
||||||
|
|
||||||
this.ChannelCard = this.fine.define(
|
this.DirectoryCard = this.fine.define(
|
||||||
'channel-card',
|
'directory-card',
|
||||||
n => n.props && n.props.streamNode,
|
n => n.renderTitles && n.renderIconicImage,
|
||||||
['dir-community', 'dir-game-index']
|
DIR_ROUTES
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.CardWrapper = this.fine.define(
|
||||||
|
'directory-card-wrapper',
|
||||||
|
n => n.renderFallback && n.renderStreamFlag,
|
||||||
|
DIR_ROUTES
|
||||||
|
);
|
||||||
|
|
||||||
this.settings.add('directory.uptime', {
|
this.settings.add('directory.uptime', {
|
||||||
default: 1,
|
default: 1,
|
||||||
|
@ -56,12 +74,12 @@ export default class Directory extends SiteModule {
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
changed: () => this.ChannelCard.forceUpdate()
|
changed: () => this.DirectoryCard.forceUpdate()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
this.settings.add('directory.show-channel-avatars', {
|
this.settings.add('directory.show-channel-avatars', {
|
||||||
default: 0,
|
default: 1,
|
||||||
|
|
||||||
ui: {
|
ui: {
|
||||||
path: 'Directory > Channels >> Appearance',
|
path: 'Directory > Channels >> Appearance',
|
||||||
|
@ -78,33 +96,8 @@ export default class Directory extends SiteModule {
|
||||||
},
|
},
|
||||||
|
|
||||||
changed: value => {
|
changed: value => {
|
||||||
this.css_tweaks.toggleHide('profile-hover-following', value === 2);
|
this.css_tweaks.toggleHide('profile-hover', value === 2);
|
||||||
this.css_tweaks.toggleHide('profile-hover-game', value === 2);
|
this.DirectoryCard.forceUpdate();
|
||||||
this.ChannelCard.forceUpdate();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
this.settings.add('directory.show-boxart', {
|
|
||||||
default: 2,
|
|
||||||
|
|
||||||
ui: {
|
|
||||||
path: 'Directory > Channels >> Appearance',
|
|
||||||
title: 'Show Boxart',
|
|
||||||
description: 'Display boxart over stream and video thumbnails.',
|
|
||||||
component: 'setting-select-box',
|
|
||||||
|
|
||||||
data: [
|
|
||||||
{value: 0, title: 'Disabled'},
|
|
||||||
{value: 1, title: 'Hidden on Hover'},
|
|
||||||
{value: 2, title: 'Always'}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
|
|
||||||
changed: value => {
|
|
||||||
this.css_tweaks.toggleHide('boxart-hide', value === 0);
|
|
||||||
this.css_tweaks.toggleHide('boxart-hover', value === 1);
|
|
||||||
this.ChannelCard.forceUpdate();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -130,89 +123,149 @@ export default class Directory extends SiteModule {
|
||||||
component: 'setting-check-box'
|
component: 'setting-check-box'
|
||||||
},
|
},
|
||||||
|
|
||||||
changed: () => this.ChannelCard.forceUpdate()
|
changed: () => this.CardWrapper.forceUpdate()
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
onEnable() {
|
async onEnable() {
|
||||||
const avatars = this.settings.get('directory.show-channel-avatars'),
|
this.css_tweaks.toggleHide('profile-hover', this.settings.get('directory.show-channel-avatars') === 2);
|
||||||
boxart = this.settings.get('directory.show-boxart');
|
|
||||||
|
|
||||||
this.css_tweaks.toggleHide('profile-hover-game', avatars === 2);
|
|
||||||
this.css_tweaks.toggleHide('profile-hover-following', avatars === 2);
|
|
||||||
|
|
||||||
this.css_tweaks.toggleHide('dir-live-ind', this.settings.get('directory.hide-live'));
|
this.css_tweaks.toggleHide('dir-live-ind', this.settings.get('directory.hide-live'));
|
||||||
this.css_tweaks.toggleHide('boxart-hide', boxart === 0);
|
|
||||||
this.css_tweaks.toggleHide('boxart-hover', boxart === 1);
|
|
||||||
|
|
||||||
this.ChannelCard.ready((cls, instances) => {
|
const t = this,
|
||||||
|
React = await this.web_munch.findModule('react');
|
||||||
|
|
||||||
|
const createElement = React && React.createElement;
|
||||||
|
|
||||||
|
this.CardWrapper.ready(cls => {
|
||||||
|
const old_render = cls.prototype.render;
|
||||||
|
|
||||||
|
cls.prototype.render = function() {
|
||||||
|
if ( get('props.streamNode.type', this) === 'rerun' && t.settings.get('directory.hide-vodcasts') )
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return old_render.call(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.CardWrapper.forceUpdate();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.DirectoryCard.ready(cls => {
|
||||||
|
const old_render_iconic = cls.prototype.renderIconicImage,
|
||||||
|
old_render_titles = cls.prototype.renderTitles;
|
||||||
|
|
||||||
|
cls.prototype.renderIconicImage = function() {
|
||||||
|
if ( this.props.context !== CARD_CONTEXTS.SingleChannelList &&
|
||||||
|
t.settings.get('directory.show-channel-avatars') !== 1 )
|
||||||
|
return;
|
||||||
|
|
||||||
|
return old_render_iconic.call(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
cls.prototype.renderTitles = function() {
|
||||||
|
const nodes = get('props.currentViewerCount.host_nodes', this);
|
||||||
|
if ( this.props.hostedByChannelLogin == null || ! nodes || ! nodes.length )
|
||||||
|
return old_render_titles.call(this);
|
||||||
|
|
||||||
|
const channel = nodes[0].hosting,
|
||||||
|
stream = channel.stream;
|
||||||
|
|
||||||
|
return (<div>
|
||||||
|
<a class="tw-link tw-link--inherit" data-test-selector="preview-card-titles__primary-link">
|
||||||
|
<h3 class="tw-ellipsis tw-font-size-5 tw-strong" title={stream.title}>{stream.title}</h3>
|
||||||
|
</a>
|
||||||
|
<div class="preview-card-titles__subtitle-wrapper">
|
||||||
|
<div data-test-selector="preview-card-titles__subtitle">
|
||||||
|
<p class="tw-c-text-alt tw-ellipsis">
|
||||||
|
<a class="tw-link tw-link--inherit">{channel.displayName}</a> playing <a class="tw-link tw-link--inherit">{stream.game.name}</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div data-test-selector="preview-card-titles__subtitle">
|
||||||
|
<p class="tw-c-text-alt tw-ellipsis">
|
||||||
|
Hosted by {nodes.length > 1 ? `${nodes.length} channels` : nodes[0].displayName}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.DirectoryCard.forceUpdate();
|
||||||
|
|
||||||
// Game Directory Channel Cards
|
// Game Directory Channel Cards
|
||||||
|
// TODO: Better query handling.
|
||||||
this.apollo.ensureQuery(
|
this.apollo.ensureQuery(
|
||||||
'GamePage_Game',
|
'GamePage_Game',
|
||||||
'data.directory.streams.edges.0.node.createdAt'
|
'data.directory.streams.edges.0.node.createdAt'
|
||||||
);
|
);
|
||||||
|
|
||||||
for(const inst of instances) this.updateChannelCard(inst);
|
//for(const inst of instances)
|
||||||
|
// this.updateCard(inst);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.ChannelCard.on('update', this.updateChannelCard, this);
|
this.DirectoryCard.on('update', this.updateCard, this);
|
||||||
this.ChannelCard.on('mount', this.updateChannelCard, this);
|
this.DirectoryCard.on('mount', this.updateCard, this);
|
||||||
this.ChannelCard.on('unmount', this.clearUptime, this);
|
this.DirectoryCard.on('unmount', this.clearCard, this);
|
||||||
|
|
||||||
|
// TODO: Queries
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
updateChannelCard(inst) {
|
updateCard(inst) {
|
||||||
const container = this.fine.getChildNode(inst);
|
const container = this.fine.getChildNode(inst);
|
||||||
if (!container) return;
|
if ( ! container )
|
||||||
|
return;
|
||||||
|
|
||||||
this.updateUptime(inst, 'props.streamNode.viewersCount.createdAt', '.tw-card-img');
|
const props = inst.props,
|
||||||
this.addCardAvatar(inst, 'props.streamNode.viewersCount', '.tw-card');
|
game = props.gameTitle || props.playerMetadataGame,
|
||||||
|
is_video = props.durationInSeconds != null,
|
||||||
|
is_host = props.hostedByChannelLogin != null;
|
||||||
|
|
||||||
const type = get('props.directoryType', inst);
|
container.classList.toggle('ffz-hide-thumbnail', this.settings.provider.get('directory.game.hidden-thumbnails', []).includes(game));
|
||||||
const hiddenThumbnails = this.settings.provider.get('directory.game.hidden-thumbnails') || [];
|
|
||||||
const hiddenPreview = 'https://static-cdn.jtvnw.net/ttv-static/404_preview-320x180.jpg';
|
|
||||||
|
|
||||||
if (get('props.streamNode.type', inst) === 'rerun' || get('props.type', inst) === 'rerun')
|
//this.log.info('Card Update', inst.props.channelDisplayName, is_video ? 'Video' : 'Live', is_host ? 'Host' : 'Not-Host', inst);
|
||||||
container.classList.toggle('tw-hide', this.settings.get('directory.hide-vodcasts'));
|
|
||||||
|
|
||||||
const img = container.querySelector && container.querySelector('.tw-card-img img');
|
this.updateUptime(inst, 'props.currentViewerCount.createdAt');
|
||||||
if (img == null) return;
|
this.updateAvatar(inst);
|
||||||
|
}
|
||||||
|
|
||||||
if (type === 'GAMES' && hiddenThumbnails.includes(get('props.directoryName', inst)) ||
|
|
||||||
type === 'COMMUNITIES' && hiddenThumbnails.includes(get('props.streamNode.game.name', inst))) {
|
clearCard(inst) {
|
||||||
img.src = hiddenPreview;
|
this.clearUptime(inst);
|
||||||
} else {
|
}
|
||||||
img.src = get('props.streamNode.previewImageURL', inst) || get('props.imageSrc', inst);
|
|
||||||
|
|
||||||
|
processNodes(edges, is_game_query = false, blocked_games) {
|
||||||
|
const out = [];
|
||||||
|
|
||||||
|
if ( blocked_games === undefined )
|
||||||
|
blocked_games = this.settings.provider.get('directory.game.blocked-games', []);
|
||||||
|
|
||||||
|
for(const edge of edges) {
|
||||||
|
const node = edge.node || edge,
|
||||||
|
store = {}; // node.viewersCount = new Number(node.viewersCount || 0);
|
||||||
|
|
||||||
|
store.createdAt = node.createdAt;
|
||||||
|
store.title = node.title;
|
||||||
|
store.game = node.game;
|
||||||
|
|
||||||
|
if ( is_game_query || (! node.game || node.game && ! blocked_games.includes(node.game.game)) )
|
||||||
|
out.push(edge);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
modifyStreams(res) { // eslint-disable-line class-methods-use-this
|
modifyStreams(res) { // eslint-disable-line class-methods-use-this
|
||||||
const blockedGames = this.settings.provider.get('directory.game.blocked-games') || [];
|
this.log.info('Modify Streams', res);
|
||||||
const gamePage = get('data.directory.__typename', res) === 'Game';
|
|
||||||
|
|
||||||
const newStreams = [];
|
const is_game_query = get('data.directory.__typename', res) === 'Game',
|
||||||
|
edges = get('data.directory.streams.edges', res);
|
||||||
|
|
||||||
const edges = get('data.directory.streams.edges', res);
|
if ( ! edges || ! edges.length )
|
||||||
if (!edges) return res;
|
return res;
|
||||||
|
|
||||||
for (let i = 0; i < edges.length; i++) {
|
res.data.directory.streams.edges = this.processNodes(edges, is_game_query);
|
||||||
const edge = edges[i],
|
|
||||||
node = edge.node || edge;
|
|
||||||
|
|
||||||
const s = node.viewersCount = new Number(node.viewersCount || 0);
|
|
||||||
s.createdAt = node.createdAt;
|
|
||||||
|
|
||||||
if ( node.broadcaster ) {
|
|
||||||
s.profileImageURL = node.broadcaster.profileImageURL;
|
|
||||||
s.login = node.broadcaster.login;
|
|
||||||
s.displayName = node.broadcaster.displayName;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (gamePage || (!node.game || node.game && !blockedGames.includes(node.game.name))) newStreams.push(edge);
|
|
||||||
}
|
|
||||||
res.data.directory.streams.edges = newStreams;
|
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -233,9 +286,9 @@ export default class Directory extends SiteModule {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
updateUptime(inst, created_path, selector) {
|
updateUptime(inst, created_path) {
|
||||||
const container = this.fine.getChildNode(inst),
|
const container = this.fine.getChildNode(inst),
|
||||||
card = container && container.querySelector && container.querySelector(selector),
|
card = container && container.querySelector && container.querySelector('.preview-card-overlay'),
|
||||||
setting = this.settings.get('directory.uptime'),
|
setting = this.settings.get('directory.uptime'),
|
||||||
created_at = get(created_path, inst),
|
created_at = get(created_path, inst),
|
||||||
up_since = created_at && new Date(created_at),
|
up_since = created_at && new Date(created_at),
|
||||||
|
@ -246,100 +299,93 @@ export default class Directory extends SiteModule {
|
||||||
|
|
||||||
const up_text = duration_to_string(uptime, false, false, false, setting === 1);
|
const up_text = duration_to_string(uptime, false, false, false, setting === 1);
|
||||||
|
|
||||||
if ( ! inst.ffz_uptime_el || card.querySelector('.ffz-uptime-element') === undefined ) {
|
if ( ! inst.ffz_uptime_el ) {
|
||||||
card.appendChild(inst.ffz_uptime_el = (<div class="video-preview-card__preview-overlay-stat tw-c-background-overlay tw-c-text-overlay tw-font-size-6 tw-top-0 tw-right-0 tw-z-default tw-inline-flex tw-absolute tw-mg-05 ffz-uptime-element">
|
inst.ffz_uptime_el = card.querySelector('.ffz-uptime-element');
|
||||||
<div class="tw-tooltip-wrapper tw-inline-flex">
|
if ( ! inst.ffz_uptime_el )
|
||||||
<div class="tw-stat">
|
card.appendChild(inst.ffz_uptime_el = (<div class="ffz-uptime-element tw-absolute tw-right-0 tw-top-0 tw-mg-1">
|
||||||
<span class="tw-c-text-live tw-stat__icon">
|
<div class="tw-tooltip-wrapper">
|
||||||
<figure class="ffz-i-clock" />
|
<div class="preview-card-stat tw-align-items-center tw-border-radi-us-small tw-c-background-overlay tw-c-text-overlay tw-flex tw-font-size-6 tw-justify-content-center">
|
||||||
</span>
|
<div class="tw-flex tw-c-text-live">
|
||||||
{inst.ffz_uptime_span = <span class="tw-stat__value" />}
|
<figure class="ffz-i-clock" />
|
||||||
|
</div>
|
||||||
|
{inst.ffz_uptime_span = <p />}
|
||||||
|
</div>
|
||||||
|
<div class="tw-tooltip tw-tooltip--down tw-tooltip--align-center">
|
||||||
|
{this.i18n.t('metadata.uptime.tooltip', 'Stream Uptime')}
|
||||||
|
{inst.ffz_uptime_tt = <div class="tw-pd-t-05" />}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{inst.ffz_uptime_tt = <div class="tw-tooltip tw-tooltip--down tw-tooltip--align-center" />}
|
</div>));
|
||||||
</div>
|
|
||||||
</div>));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( ! inst.ffz_update_timer )
|
if ( ! inst.ffz_update_timer )
|
||||||
inst.ffz_update_timer = setInterval(this.updateUptime.bind(this, inst, created_path, selector), 1000);
|
inst.ffz_update_timer = setInterval(this.updateUptime.bind(this, inst, created_path), 1000);
|
||||||
|
|
||||||
inst.ffz_uptime_span.textContent = up_text;
|
inst.ffz_uptime_span.textContent = up_text;
|
||||||
|
|
||||||
if ( inst.ffz_last_created_at !== created_at ) {
|
if ( inst.ffz_last_created_at !== created_at ) {
|
||||||
inst.ffz_uptime_tt.innerHTML = `${this.i18n.t(
|
inst.ffz_uptime_tt.textContent = this.i18n.t(
|
||||||
'metadata.uptime.tooltip',
|
|
||||||
'Stream Uptime'
|
|
||||||
)}<div class="pd-t-05">${this.i18n.t(
|
|
||||||
'metadata.uptime.since',
|
'metadata.uptime.since',
|
||||||
'(since %{since})',
|
'(since %{since})',
|
||||||
{since: up_since.toLocaleString()}
|
{since: up_since.toLocaleString()}
|
||||||
)}</div>`;
|
);
|
||||||
|
|
||||||
inst.ffz_last_created_at = created_at;
|
inst.ffz_last_created_at = created_at;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
addCardAvatar(inst, created_path, selector, data) {
|
updateAvatar(inst) {
|
||||||
const container = this.fine.getChildNode(inst),
|
const container = this.fine.getChildNode(inst),
|
||||||
card = container && container.querySelector && container.querySelector(selector),
|
card = container && container.querySelector && container.querySelector('.preview-card-overlay'),
|
||||||
setting = this.settings.get('directory.show-channel-avatars');
|
setting = this.settings.get('directory.show-channel-avatars');
|
||||||
|
|
||||||
if ( ! data )
|
|
||||||
data = get(created_path, inst);
|
|
||||||
|
|
||||||
if ( ! card )
|
if ( ! card )
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// Get the old element.
|
const props = inst.props,
|
||||||
const channel_avatar = card.querySelector('.ffz-channel-avatar');
|
is_video = props.durationInSeconds != null,
|
||||||
|
src = props.channelImageProps && props.channelImageProps.src;
|
||||||
|
|
||||||
if ( ! data || ! data.profileImageURL || setting === 0 ) {
|
const avatar = card.querySelector('.ffz-channel-avatar');
|
||||||
if ( channel_avatar !== null )
|
|
||||||
channel_avatar.remove();
|
if ( ! src || setting < 2 || props.context === CARD_CONTEXTS.SingleChannelList ) {
|
||||||
|
if ( avatar )
|
||||||
|
avatar.remove();
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( setting !== inst.ffz_av_setting || data.login !== inst.ffz_av_login || data.profileImageURL !== inst.ffz_av_image ) {
|
if ( setting === inst.ffz_av_setting && props.channelLogin === inst.ffz_av_login && src === inst.ffz_av_src )
|
||||||
if ( channel_avatar )
|
return;
|
||||||
channel_avatar.remove();
|
|
||||||
|
|
||||||
inst.ffz_av_setting = setting;
|
if ( avatar )
|
||||||
inst.ffz_av_login = data.login;
|
avatar.remove();
|
||||||
inst.ffz_av_image = data.profileImageURL;
|
|
||||||
|
|
||||||
if ( setting === 1 ) {
|
inst.ffz_av_setting = setting;
|
||||||
const body = card.querySelector('.tw-card-body .tw-flex'),
|
inst.ffz_av_login = props.channelLogin;
|
||||||
avatar = (<a
|
inst.ffz_av_src = src;
|
||||||
class="ffz-channel-avatar tw-mg-r-05 tw-mg-t-05"
|
|
||||||
href={`/${data.login}`}
|
|
||||||
title={data.displayName}
|
|
||||||
onClick={e => this.hijackUserClick(e, data.login)} // eslint-disable-line react/jsx-no-bind
|
|
||||||
>
|
|
||||||
<img src={data.profileImageURL} />
|
|
||||||
</a>);
|
|
||||||
|
|
||||||
body.insertBefore(avatar, body.firstElementChild);
|
card.appendChild(<a
|
||||||
|
class="ffz-channel-avatar"
|
||||||
|
href={props.channelLinkTo && props.channelLinkTo.pathname}
|
||||||
|
onClick={e => this.routeClick(e, props.channelLinkTo)} // eslint-disable-line react/jsx-no-bind
|
||||||
|
>
|
||||||
|
<div class={`tw-absolute tw-right-0 tw-border-l tw-c-background ${is_video ? 'tw-top-0 tw-border-b' : 'tw-bottom-0 tw-border-t'}`}>
|
||||||
|
<figure class="tw-aspect tw-aspect--align-top">
|
||||||
|
<img src={src} title={props.channelDisplayName} />
|
||||||
|
</figure>
|
||||||
|
</div>
|
||||||
|
</a>);
|
||||||
|
}
|
||||||
|
|
||||||
} else if ( setting === 2 || setting === 3 ) {
|
|
||||||
const avatar_el = (<a
|
|
||||||
class="ffz-channel-avatar"
|
|
||||||
href={`/${data.login}`}
|
|
||||||
onClick={e => this.hijackUserClick(e, data.login)} // eslint-disable-line react/jsx-no-bind
|
|
||||||
>
|
|
||||||
<div class="live-channel-card__boxart tw-bottom-0 tw-absolute">
|
|
||||||
<figure class="tw-aspect tw-aspect--align-top">
|
|
||||||
<img src={data.profileImageURL} title={data.displayName} />
|
|
||||||
</figure>
|
|
||||||
</div>
|
|
||||||
</a>);
|
|
||||||
|
|
||||||
const cont = card.querySelector('figure.tw-aspect > div');
|
routeClick(event, route) {
|
||||||
if ( cont )
|
event.preventDefault();
|
||||||
cont.appendChild(avatar_el);
|
event.stopPropagation();
|
||||||
}
|
|
||||||
}
|
if ( route && route.pathname )
|
||||||
|
this.router.history.push(route.pathname);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -347,7 +393,8 @@ export default class Directory extends SiteModule {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
|
||||||
if (optionalFn) optionalFn();
|
if ( optionalFn )
|
||||||
|
optionalFn(event, user);
|
||||||
|
|
||||||
this.router.navigate('user', { userName: user });
|
this.router.navigate('user', { userName: user });
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,4 +38,12 @@
|
||||||
|
|
||||||
.ffz-stat-arrow {
|
.ffz-stat-arrow {
|
||||||
border-left: none !important;
|
border-left: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.ffz-auto-host-options {
|
||||||
|
.ffz-channel-avatar {
|
||||||
|
max-width: 3.2rem;
|
||||||
|
max-height: 3.2rem;
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -106,6 +106,11 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.chat-line__username:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
.ffz--emote-picker {
|
.ffz--emote-picker {
|
||||||
section:not(.filtered) heading {
|
section:not(.filtered) heading {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
.ffz-channel-avatar {
|
.ffz-channel-avatar {
|
||||||
flex-shrink: 0 !important;
|
.tw-aspect {
|
||||||
width: 4rem;
|
|
||||||
|
|
||||||
img {
|
|
||||||
width: 4rem;
|
width: 4rem;
|
||||||
height: 4rem
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ffz-channel-avatar,
|
||||||
|
.ffz-uptime-element {
|
||||||
|
pointer-events: all;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// TODO: Color variables
|
// TODO: Color variables
|
||||||
|
|
||||||
|
@ -27,6 +28,25 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.ffz-hide-thumbnail {
|
||||||
|
.preview-card-thumbnail__image {
|
||||||
|
&:before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0; left: 0;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
background: url("https://static-cdn.jtvnw.net/ttv-static/404_preview-320x180.jpg") no-repeat;
|
||||||
|
background-size: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
.ffz-host-menu {
|
.ffz-host-menu {
|
||||||
.scrollable-area {
|
.scrollable-area {
|
||||||
max-height: 25vh;
|
max-height: 25vh;
|
||||||
|
|
|
@ -7,6 +7,8 @@
|
||||||
@import "./widgets/profile-selector.scss";
|
@import "./widgets/profile-selector.scss";
|
||||||
@import "./widgets/badge-visibility.scss";
|
@import "./widgets/badge-visibility.scss";
|
||||||
|
|
||||||
|
@import "./widgets/color-picker.scss";
|
||||||
|
|
||||||
|
|
||||||
.tw-display-inline { display: inline !important }
|
.tw-display-inline { display: inline !important }
|
||||||
.tw-width-auto { width: auto !important }
|
.tw-width-auto { width: auto !important }
|
||||||
|
@ -16,6 +18,10 @@
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ffz-bottom-100 {
|
||||||
|
bottom: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
.ffz--widget {
|
.ffz--widget {
|
||||||
input, select {
|
input, select {
|
||||||
|
@ -55,6 +61,7 @@
|
||||||
figure {
|
figure {
|
||||||
width: 3rem;
|
width: 3rem;
|
||||||
margin: .4rem;
|
margin: .4rem;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
height: calc(100% - .8rem);
|
height: calc(100% - .8rem);
|
||||||
}
|
}
|
||||||
|
|
96
styles/widgets/color-picker.scss
Normal file
96
styles/widgets/color-picker.scss
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
$color: #dad8de;
|
||||||
|
$bg-color: #17141f;
|
||||||
|
$border-color: #2c2541;
|
||||||
|
|
||||||
|
$input-color: #faf9fa;
|
||||||
|
$input-bg: #0e0c13;
|
||||||
|
$input-border: #392e5c;
|
||||||
|
$input-active-border: #7d5bbe;
|
||||||
|
|
||||||
|
|
||||||
|
.tw-theme--dark {
|
||||||
|
.vc-sketch {
|
||||||
|
background: $bg-color;
|
||||||
|
box-shadow: 0 0 0 1px $border-color,
|
||||||
|
0 8px 16px rgba($border-color, .15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-sketch-active-color {
|
||||||
|
box-shadow: inset 0 0 0 1px $border-color,
|
||||||
|
inset 0 0 4px rgba($border-color, .25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-sketch-field {
|
||||||
|
.vc-input__input {
|
||||||
|
background: $input-bg;
|
||||||
|
color: $input-color;
|
||||||
|
box-shadow: inset 0 0 0 1px $input-border;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
box-shadow: inset 0 0 0 1px $input-active-border,
|
||||||
|
0 0 6px -2px $input-active-border;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-input__label {
|
||||||
|
color: $color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-sketch-presets {
|
||||||
|
border-top-color: $border-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-sketch-presets-color {
|
||||||
|
box-shadow: inset 0 0 0 1px $border-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
$color: var(--ffz-color-1);
|
||||||
|
$bg-color: var(--ffz-color-21);
|
||||||
|
$border-color: var(--ffz-color-20);
|
||||||
|
|
||||||
|
$input-color: var(--ffz-color-23);
|
||||||
|
$input-bg: var(--ffz-color-27);
|
||||||
|
$input-border: var(--ffz-color-26);
|
||||||
|
$input-active-border: var(--ffz-color-5);
|
||||||
|
|
||||||
|
|
||||||
|
.tw-theme--dark.tw-theme--ffz {
|
||||||
|
.vc-sketch {
|
||||||
|
background: $bg-color;
|
||||||
|
box-shadow: 0 0 0 1px $border-color,
|
||||||
|
0 8px 16px var(--ffz-color-37);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-sketch-active-color {
|
||||||
|
box-shadow: inset 0 0 0 1px $border-color,
|
||||||
|
inset 0 0 4px var(--ffz-color-37);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-sketch-field {
|
||||||
|
.vc-input__input {
|
||||||
|
background: $input-bg;
|
||||||
|
color: $input-color;
|
||||||
|
box-shadow: inset 0 0 0 1px $input-border;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
box-shadow: inset 0 0 0 1px $input-active-border,
|
||||||
|
0 0 6px -2px $input-active-border;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-input__label {
|
||||||
|
color: $color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-sketch-presets {
|
||||||
|
border-top-color: $border-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-sketch-presets-color {
|
||||||
|
box-shadow: inset 0 0 0 1px $border-color;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue