mirror of
https://github.com/FrankerFaceZ/FrankerFaceZ.git
synced 2025-08-02 16:08:31 +00:00
4.0.0-rc2. Add basic custom highlight terms and blocked terms. Change the socket cluster setting because users are users. Open settings in a new window if clicking the chat menu link with ctrl or shift. Hide the Get Bits button in the site navigation. Add an additional socket server.
This commit is contained in:
parent
6b2b734ef9
commit
2a790ad7cd
22 changed files with 669 additions and 48 deletions
|
@ -1,3 +1,12 @@
|
|||
<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">
|
||||
<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>Changed: Reset the Socket Cluster debugging setting to default for all users and make it clear that it isn't something that should be changed.</li>
|
||||
<li>Changed: Allow opening settings in a new window by holding Ctrl or Shift when clicking the link.</li>
|
||||
<li>Changed: Hide the <code>Get Bits</code> button in the site navigation bar when hiding bits is enabled.</li>
|
||||
<li>Changed: Add the YooHoo server to the production socket server pool.</li>
|
||||
</ul>
|
||||
|
||||
<div class="list-header">4.0.0-rc1.12<span>@b04d3c600e5260fcd7cd</span> <time datetime="2018-05-25">(2018-05-25)</time></div>
|
||||
<ul class="chat-menu-content menu-side-padding">
|
||||
<li>Changed: Disable including user IDs in error reports by default.</li>
|
||||
|
|
|
@ -100,7 +100,7 @@ class FrankerFaceZ extends Module {
|
|||
FrankerFaceZ.Logger = Logger;
|
||||
|
||||
const VER = FrankerFaceZ.version_info = {
|
||||
major: 4, minor: 0, revision: 0, extra: '-rc1.12',
|
||||
major: 4, minor: 0, revision: 0, extra: '-rc2',
|
||||
build: __webpack_hash__,
|
||||
toString: () =>
|
||||
`${VER.major}.${VER.minor}.${VER.revision}${VER.extra || ''}${DEBUG ? '-dev' : ''}`
|
||||
|
|
9
src/modules/chat/components/chat-blocked.vue
Normal file
9
src/modules/chat/components/chat-blocked.vue
Normal file
|
@ -0,0 +1,9 @@
|
|||
<template functional>
|
||||
<strong
|
||||
:data-text="props.token.text"
|
||||
data-tooltip-type="blocked"
|
||||
class="ffz-tooltip ffz--blocked ffz-i-cancel"
|
||||
>
|
||||
×××
|
||||
</strong>
|
||||
</template>
|
5
src/modules/chat/components/chat-highlight.vue
Normal file
5
src/modules/chat/components/chat-highlight.vue
Normal file
|
@ -0,0 +1,5 @@
|
|||
<template functional>
|
||||
<strong class="ffz--highlight">
|
||||
{{ props.token.text }}
|
||||
</strong>
|
||||
</template>
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
import Module from 'utilities/module';
|
||||
import {createElement, ManagedStyle} from 'utilities/dom';
|
||||
import {timeout, has} from 'utilities/object';
|
||||
import {timeout, has, glob_to_regex, escape_regex} from 'utilities/object';
|
||||
|
||||
import Badges from './badges';
|
||||
import Emotes from './emotes';
|
||||
|
@ -108,6 +108,100 @@ export default class Chat extends Module {
|
|||
});
|
||||
|
||||
|
||||
this.settings.add('chat.filtering.highlight-basic-terms', {
|
||||
default: [],
|
||||
type: 'array_merge',
|
||||
ui: {
|
||||
path: 'Chat > Filtering >> Highlight Terms',
|
||||
component: 'basic-terms',
|
||||
colored: true
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
this.settings.add('chat.filtering.highlight-basic-terms--color-regex', {
|
||||
requires: ['chat.filtering.highlight-basic-terms'],
|
||||
process(ctx) {
|
||||
const val = ctx.get('chat.filtering.highlight-basic-terms');
|
||||
if ( ! val || ! val.length )
|
||||
return null;
|
||||
|
||||
const colors = new Map;
|
||||
|
||||
for(const item of val) {
|
||||
let list;
|
||||
const c = item.c || null,
|
||||
t = item.t;
|
||||
|
||||
let v = item.v;
|
||||
|
||||
if ( t === 'glob' )
|
||||
v = glob_to_regex(v);
|
||||
|
||||
else if ( t !== 'raw' )
|
||||
v = escape_regex(v);
|
||||
|
||||
if ( ! v || ! v.length )
|
||||
continue;
|
||||
|
||||
if ( colors.has(c) )
|
||||
colors.get(c).push(v);
|
||||
else
|
||||
colors.set(c, [v]);
|
||||
}
|
||||
|
||||
|
||||
for(const [key, list] of colors)
|
||||
colors.set(key, new RegExp(`\\b(${list.join('|')})\\b`, 'gi'));
|
||||
|
||||
return colors;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
this.settings.add('chat.filtering.highlight-basic-blocked', {
|
||||
default: [],
|
||||
type: 'array_merge',
|
||||
ui: {
|
||||
path: 'Chat > Filtering >> Blocked Terms',
|
||||
component: 'basic-terms'
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
this.settings.add('chat.filtering.highlight-basic-blocked--regex', {
|
||||
requires: ['chat.filtering.highlight-basic-blocked'],
|
||||
process(ctx) {
|
||||
const val = ctx.get('chat.filtering.highlight-basic-blocked');
|
||||
if ( ! val || ! val.length )
|
||||
return null;
|
||||
|
||||
const out = [];
|
||||
|
||||
for(const item of val) {
|
||||
const t = item.t;
|
||||
let v = item.v;
|
||||
|
||||
if ( t === 'glob' )
|
||||
v = glob_to_regex(v);
|
||||
|
||||
else if ( t !== 'raw' )
|
||||
v = escape_regex(v);
|
||||
|
||||
if ( ! v || ! v.length )
|
||||
continue;
|
||||
|
||||
out.push(v);
|
||||
}
|
||||
|
||||
if ( ! out.length )
|
||||
return;
|
||||
|
||||
return new RegExp(`\\b(${out.join('|')})\\b`, 'gi');
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
this.settings.add('chat.filtering.highlight-mentions', {
|
||||
default: false,
|
||||
ui: {
|
||||
|
|
|
@ -260,6 +260,138 @@ export const Mentions = {
|
|||
}
|
||||
|
||||
|
||||
// ============================================================================
|
||||
// Custom Highlight Terms
|
||||
// ============================================================================
|
||||
|
||||
export const CustomHighlights = {
|
||||
type: 'highlight',
|
||||
priority: 100,
|
||||
|
||||
component: () => import(/* webpackChunkName: 'vue-chat' */ './components/chat-highlight.vue'),
|
||||
|
||||
render(token, createElement) {
|
||||
return (<strong class="ffz--highlight">{token.text}</strong>);
|
||||
},
|
||||
|
||||
process(tokens, msg) {
|
||||
if ( ! tokens || ! tokens.length )
|
||||
return tokens;
|
||||
|
||||
const colors = this.context.get('chat.filtering.highlight-basic-terms--color-regex');
|
||||
if ( ! colors || ! colors.size )
|
||||
return tokens;
|
||||
|
||||
for(const [color, regex] of colors) {
|
||||
const out = [];
|
||||
for(const token of tokens) {
|
||||
if ( token.type !== 'text' ) {
|
||||
out.push(token);
|
||||
continue;
|
||||
}
|
||||
|
||||
regex.lastIndex = 0;
|
||||
const text = token.text;
|
||||
let idx = 0, match;
|
||||
|
||||
while((match = regex.exec(text))) {
|
||||
const nix = match.index;
|
||||
|
||||
if ( idx !== nix )
|
||||
out.push({type: 'text', text: text.slice(idx, nix)});
|
||||
|
||||
msg.mentioned = true;
|
||||
msg.mention_color = color;
|
||||
|
||||
out.push({
|
||||
type: 'highlight',
|
||||
text: match[1]
|
||||
});
|
||||
|
||||
idx = nix + match[1].length;
|
||||
}
|
||||
|
||||
if ( idx < text.length )
|
||||
out.push({type: 'text', text: text.slice(idx)});
|
||||
}
|
||||
|
||||
tokens = out;
|
||||
}
|
||||
|
||||
return tokens;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export const BlockedTerms = {
|
||||
type: 'blocked',
|
||||
priority: 99,
|
||||
|
||||
component: () => import(/* webpackChunkName: 'vue-chat' */ './components/chat-blocked.vue'),
|
||||
|
||||
render(token, createElement) {
|
||||
return (<strong
|
||||
data-text={token.text}
|
||||
data-tooltip-type="blocked"
|
||||
class="ffz-tooltip ffz--blocked"
|
||||
>
|
||||
×××
|
||||
</strong>);
|
||||
},
|
||||
|
||||
tooltip(target) {
|
||||
const ds = target.dataset;
|
||||
return [
|
||||
(<div class="tw-border-b tw-mg-b-05">{ // eslint-disable-line react/jsx-key
|
||||
this.i18n.t('chat.filtering.blocked-term', 'Blocked Term')
|
||||
}</div>),
|
||||
ds.text
|
||||
]
|
||||
},
|
||||
|
||||
process(tokens) {
|
||||
if ( ! tokens || ! tokens.length )
|
||||
return tokens;
|
||||
|
||||
const regex = this.context.get('chat.filtering.highlight-basic-blocked--regex');
|
||||
if ( ! regex )
|
||||
return tokens;
|
||||
|
||||
const out = [];
|
||||
for(const token of tokens) {
|
||||
if ( token.type !== 'text' ) {
|
||||
out.push(token);
|
||||
continue;
|
||||
}
|
||||
|
||||
regex.lastIndex = 0;
|
||||
const text = token.text;
|
||||
let idx = 0, match;
|
||||
|
||||
while((match = regex.exec(text))) {
|
||||
const nix = match.index;
|
||||
|
||||
if ( idx !== nix )
|
||||
out.push({type: 'text', text: text.slice(idx, nix)});
|
||||
|
||||
out.push({
|
||||
type: 'blocked',
|
||||
text: match[1]
|
||||
});
|
||||
|
||||
idx = nix + match[1].length;
|
||||
}
|
||||
|
||||
if ( idx < text.length )
|
||||
out.push({type: 'text', text: text.slice(idx)});
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ============================================================================
|
||||
// Cheers
|
||||
// ============================================================================
|
||||
|
|
121
src/modules/main_menu/components/basic-terms.vue
Normal file
121
src/modules/main_menu/components/basic-terms.vue
Normal file
|
@ -0,0 +1,121 @@
|
|||
<template lang="html">
|
||||
<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">
|
||||
<div class="tw-flex-grow-1">
|
||||
<input
|
||||
v-model="new_term"
|
||||
:placeholder="t('setting.terms.add-placeholder', 'Add a new term')"
|
||||
type="text"
|
||||
class="tw-input"
|
||||
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') }}
|
||||
</div>
|
||||
<ul v-else class="ffz--term-list">
|
||||
<term-editor
|
||||
v-for="term in val"
|
||||
v-if="term.t !== 'inherit'"
|
||||
:key="term.id"
|
||||
:term="term.v"
|
||||
:colored="item.colored"
|
||||
@remove="remove(term)"
|
||||
@save="save(term, $event)"
|
||||
/>
|
||||
</ul>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import SettingMixin from '../setting-mixin';
|
||||
import {deep_copy} from 'utilities/object';
|
||||
|
||||
let last_id = 0;
|
||||
|
||||
export default {
|
||||
mixins: [SettingMixin],
|
||||
props: ['item', 'context'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
new_term: '',
|
||||
new_type: 'text',
|
||||
new_color: ''
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
hasInheritance() {
|
||||
for(const val of this.val)
|
||||
if ( val.t === 'inherit' )
|
||||
return true;
|
||||
},
|
||||
|
||||
val() {
|
||||
if ( ! this.has_value )
|
||||
return [];
|
||||
|
||||
return this.value.map(x => {
|
||||
x.id = x.id || `${Date.now()}-${Math.random()}-${last_id++}`;
|
||||
return x;
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
add() {
|
||||
const vals = Array.from(this.val);
|
||||
vals.push({
|
||||
v: {
|
||||
t: this.new_type,
|
||||
v: this.new_term,
|
||||
c: typeof this.new_color === 'string' ? this.new_color : null
|
||||
}
|
||||
});
|
||||
|
||||
this.new_term = '';
|
||||
this.set(deep_copy(vals));
|
||||
},
|
||||
|
||||
remove(val) {
|
||||
const vals = Array.from(this.val),
|
||||
idx = vals.indexOf(val);
|
||||
|
||||
if ( idx !== -1 ) {
|
||||
vals.splice(idx, 1);
|
||||
if ( vals.length )
|
||||
this.set(deep_copy(vals));
|
||||
else
|
||||
this.clear();
|
||||
}
|
||||
},
|
||||
|
||||
save(val, new_val) {
|
||||
val.v = new_val;
|
||||
this.set(deep_copy(this.val));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
|
@ -1,6 +1,8 @@
|
|||
<template lang="html">
|
||||
<div class="ffz--color-widget tw-relative tw-full-width tw-mg-y-05">
|
||||
<div class="ffz--color-widget">
|
||||
<div v-if="showInput" class="tw-relative tw-full-width tw-mg-y-05">
|
||||
<input
|
||||
v-if="showInput"
|
||||
ref="input"
|
||||
v-bind="$attrs"
|
||||
v-model="color"
|
||||
|
@ -24,18 +26,35 @@
|
|||
<chrome-picker :value="colors" @input="onPick" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="tw-relative">
|
||||
<button
|
||||
class="tw-button tw-button--hollow ffz-color-preview"
|
||||
@click="togglePicker"
|
||||
@contextmenu.prevent="maybeResetColor"
|
||||
>
|
||||
<figure v-if="! valid" class="ffz-i-attention tw-c-text-alert" />
|
||||
<figure v-else-if="color" :style="`background-color: ${color}`">
|
||||
|
||||
</figure>
|
||||
<figure v-else class="ffz-i-eyedropper" />
|
||||
</button>
|
||||
<div v-on-clickaway="closePicker" v-if="open" class="tw-absolute tw-z-default tw-right-0">
|
||||
<chrome-picker :value="colors" @input="onPick" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import {Color} from 'utilities/color';
|
||||
|
||||
import {Chrome} from 'vue-color';
|
||||
import {Sketch} from 'vue-color';
|
||||
import {mixin as clickaway} from 'vue-clickaway';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
'chrome-picker': Chrome
|
||||
'chrome-picker': Sketch
|
||||
},
|
||||
|
||||
mixins: [clickaway],
|
||||
|
@ -46,9 +65,17 @@ export default {
|
|||
type: String,
|
||||
default: '#000'
|
||||
},
|
||||
nullable: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
validate: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
showInput: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -84,6 +111,17 @@ export default {
|
|||
},
|
||||
|
||||
methods: {
|
||||
maybeResetColor() {
|
||||
if ( this.open )
|
||||
return this.open = false;
|
||||
|
||||
if ( this.nullable ) {
|
||||
this.color = '';
|
||||
this._validate();
|
||||
this.emit();
|
||||
}
|
||||
},
|
||||
|
||||
openPicker() {
|
||||
this.open = true;
|
||||
},
|
||||
|
|
129
src/modules/main_menu/components/term-editor.vue
Normal file
129
src/modules/main_menu/components/term-editor.vue
Normal file
|
@ -0,0 +1,129 @@
|
|||
<template lang="html">
|
||||
<li class="ffz--term">
|
||||
<div class="tw-align-items-center tw-flex tw-flex-nowrap tw-flex-row tw-full-width">
|
||||
<div class="tw-flex-grow-1">
|
||||
<h4 v-if="! editing" class="ffz-monospace">
|
||||
<pre>{{ term.v }}</pre>
|
||||
</h4>
|
||||
<input
|
||||
v-else
|
||||
v-model="edit_data.v"
|
||||
:placeholder="edit_data.v"
|
||||
type="text"
|
||||
class="tw-input"
|
||||
autocapitalize="off"
|
||||
autocorrect="off"
|
||||
>
|
||||
</div>
|
||||
<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" />
|
||||
<div v-else-if="term.c" class="ffz-color-preview">
|
||||
<figure :style="`background-color: ${term.c}`">
|
||||
|
||||
</figure>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tw-flex-shrink-0 tw-mg-x-05">
|
||||
<span v-if="! editing">{{ term_type }}</span>
|
||||
<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="raw">{{ t('setting.terms.type.regex', 'Regex') }}</option>
|
||||
<option value="glob">{{ t('setting.terms.type.glob', 'Glob') }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div v-if="editing" class="tw-flex-shrink-0">
|
||||
<button class="tw-button tw-button--text tw-tooltip-wrapper" @click="save">
|
||||
<span class="tw-button__text ffz-i-floppy" />
|
||||
<div class="tw-tooltip tw-tooltip--down tw-tooltip--align-right">
|
||||
{{ t('setting.save', 'Save') }}
|
||||
</div>
|
||||
</button>
|
||||
<button class="tw-button tw-button--text tw-tooltip-wrapper" @click="cancel">
|
||||
<span class="tw-button__text ffz-i-cancel" />
|
||||
<div class="tw-tooltip tw-tooltip--down tw-tooltip--align-right">
|
||||
{{ t('setting.cancel', 'Cancel') }}
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div v-else-if="deleting" class="tw-flex-shrink-0">
|
||||
<button class="tw-button tw-button--text tw-tooltip-wrapper" @click="$emit('remove', term)">
|
||||
<span class="tw-button__text ffz-i-trash" />
|
||||
<div class="tw-tooltip tw-tooltip--down tw-tooltip--align-right">
|
||||
{{ t('setting.delete', 'Delete') }}
|
||||
</div>
|
||||
</button>
|
||||
<button class="tw-button tw-button--text tw-tooltip-wrapper" @click="deleting = false">
|
||||
<span class="tw-button__text ffz-i-cancel" />
|
||||
<div class="tw-tooltip tw-tooltip--down tw-tooltip--align-right">
|
||||
{{ t('setting.cancel', 'Cancel') }}
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div v-else class="tw-flex-shrink-0">
|
||||
<button class="tw-button tw-button--text tw-tooltip-wrapper" @click="edit">
|
||||
<span class="tw-button__text ffz-i-cog" />
|
||||
<div class="tw-tooltip tw-tooltip--down tw-tooltip--align-right">
|
||||
{{ t('setting.edit', 'Edit') }}
|
||||
</div>
|
||||
</button>
|
||||
<button class="tw-button tw-button--text tw-tooltip-wrapper" @click="deleting = true">
|
||||
<span class="tw-button__text ffz-i-trash" />
|
||||
<div class="tw-tooltip tw-tooltip--down tw-tooltip--align-right">
|
||||
{{ t('setting.delete', 'Delete') }}
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import {deep_copy} from 'utilities/object';
|
||||
|
||||
export default {
|
||||
props: ['term', 'colored'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
deleting: false,
|
||||
editing: false,
|
||||
edit_data: null
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
term_type() {
|
||||
const t = this.term && this.term.t;
|
||||
if ( t === 'text' )
|
||||
return this.t('setting.terms.type.text', 'Text');
|
||||
|
||||
else if ( t === 'raw' )
|
||||
return this.t('setting.terms.type.raw', 'Regex');
|
||||
|
||||
else if ( t === 'glob' )
|
||||
return this.t('setting.terms.type.glob', 'Glob');
|
||||
|
||||
return this.t('setting.unknown', 'Unknown Value');
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
edit() {
|
||||
this.editing = true;
|
||||
this.edit_data = deep_copy(this.term);
|
||||
},
|
||||
|
||||
cancel() {
|
||||
this.editing = false;
|
||||
this.edit_data = null;
|
||||
},
|
||||
|
||||
save() {
|
||||
this.$emit('save', this.edit_data);
|
||||
this.cancel();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
|
@ -285,7 +285,7 @@ export default class ChatHook extends Module {
|
|||
|
||||
ic._base = is_dark ? '#dad8de' : '#19171c';
|
||||
ic.mode = mode;
|
||||
ic.contrast = is_dark ? 13 : 16;
|
||||
ic.contrast = contrast;
|
||||
|
||||
this.updateChatLines();
|
||||
}
|
||||
|
|
|
@ -55,6 +55,8 @@ export default class ChatLine extends Module {
|
|||
this.chat.context.on('changed:chat.rich.hide-tokens', this.updateLines, this);
|
||||
this.chat.context.on('changed:chat.actions.inline', this.updateLines, this);
|
||||
this.chat.context.on('changed:chat.filtering.show-deleted', this.updateLines, this);
|
||||
this.chat.context.on('changed:chat.filtering.highlight-basic-terms--color-regex', this.updateLines, this);
|
||||
this.chat.context.on('changed:chat.filtering.highlight-basic-blocked--regex', this.updateLines, this);
|
||||
|
||||
const t = this,
|
||||
React = await this.web_munch.findModule('react');
|
||||
|
@ -195,7 +197,6 @@ export default class ChatLine extends Module {
|
|||
|
||||
const user = msg.user,
|
||||
color = t.parent.colors.process(user.color),
|
||||
bg_css = null, //Math.random() > .7 ? t.parent.inverse_colors.process(user.color) : null,
|
||||
show_deleted = t.chat.context.get('chat.filtering.show-deleted');
|
||||
|
||||
let show, show_class;
|
||||
|
@ -228,7 +229,8 @@ export default class ChatLine extends Module {
|
|||
}
|
||||
|
||||
const tokens = msg.ffz_tokens = msg.ffz_tokens || t.chat.tokenizeMessage(msg, u, r),
|
||||
rich_content = FFZRichContent && t.chat.pluckRichContent(tokens, msg);
|
||||
rich_content = FFZRichContent && t.chat.pluckRichContent(tokens, msg),
|
||||
bg_css = msg.mentioned && msg.mention_color ? t.parent.inverse_colors.process(msg.mention_color) : null;
|
||||
|
||||
if ( ! this.ffz_user_click_handler )
|
||||
this.ffz_user_click_handler = event => event.ctrlKey ? this.usernameClickHandler(event) : t.viewer_cards.openCard(r, user, event);
|
||||
|
@ -342,7 +344,7 @@ export default class ChatLine extends Module {
|
|||
return null;
|
||||
|
||||
return e('div', {
|
||||
className: `${cls}${msg.mentioned ? ' ffz-mentioned' : ''}`,
|
||||
className: `${cls}${msg.mentioned ? ' ffz-mentioned' : ''}${bg_css ? ' ffz-custom-color' : ''}`,
|
||||
style: {backgroundColor: bg_css},
|
||||
'data-room-id': this.props.channelID,
|
||||
'data-room': room,
|
||||
|
@ -361,14 +363,18 @@ export default class ChatLine extends Module {
|
|||
updateLines() {
|
||||
for(const inst of this.ChatLine.instances) {
|
||||
const msg = inst.props.message;
|
||||
if ( msg )
|
||||
if ( msg ) {
|
||||
msg.ffz_tokens = null;
|
||||
msg.mentioned = msg.mention_color = null;
|
||||
}
|
||||
}
|
||||
|
||||
for(const inst of this.ChatRoomLine.instances) {
|
||||
const msg = inst.props.message;
|
||||
if ( msg )
|
||||
if ( msg ) {
|
||||
msg.ffz_tokens = null;
|
||||
msg.mentioned = msg.mention_color = null;
|
||||
}
|
||||
}
|
||||
|
||||
this.ChatLine.forceUpdate();
|
||||
|
|
|
@ -69,15 +69,17 @@ export default class SettingsMenu extends Module {
|
|||
});
|
||||
}
|
||||
|
||||
click(inst) {
|
||||
// Pop-out chat check
|
||||
const twMinimalRoot = document.querySelector('.twilight-minimal-root');
|
||||
if (twMinimalRoot) {
|
||||
click(inst, event) {
|
||||
// If we're on a page with minimal root, we want to open settings
|
||||
// in a popout as we're almost certainly within Popout Chat.
|
||||
const minimal_root = document.querySelector('.twilight-minimal-root');
|
||||
if ( minimal_root || (event && (event.ctrlKey || event.shiftKey)) ) {
|
||||
const win = window.open(
|
||||
'https://twitch.tv/popout/frankerfacez/chat?ffz-settings',
|
||||
'_blank',
|
||||
'resizable=yes,scrollbars=yes,width=850,height=600'
|
||||
);
|
||||
|
||||
if ( win )
|
||||
win.focus();
|
||||
else {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
.chat-line__message:not(.chat-line--inline),
|
||||
.user-notice-line {
|
||||
&.ffz-mentioned:nth-child(2n+0) {
|
||||
&.ffz-mentioned:not(.ffz-custom-color):nth-child(2n+0) {
|
||||
background-color: rgba(255,127,127,.4) !important;
|
||||
|
||||
.tw-theme--dark & {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
.chat-line__message:not(.chat-line--inline),
|
||||
.user-notice-line {
|
||||
&.ffz-mentioned {
|
||||
&.ffz-mentioned:not(.ffz-custom-color) {
|
||||
background-color: rgba(255,127,127,.2) !important;
|
||||
|
||||
.tw-theme--dark & {
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
.ffz--highlight,
|
||||
.ffz--mention-me {
|
||||
border-radius: .5rem;
|
||||
padding: .3rem;
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
.chat-list__lines .chat-line__bits-charity,
|
||||
.user-notice-line,
|
||||
.chat-line__message:not(.chat-line--inline) {
|
||||
&:not(.ffz-custom-color) {
|
||||
background-color: transparent !important;
|
||||
|
||||
&:nth-child(2n+0) {
|
||||
|
@ -18,4 +19,5 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
.get-bits-button,
|
||||
.chat-input button[data-a-target="bits-button"],
|
||||
.channel-header__right > .tw-mg-l-1 > div > div > button:not([data-a-target]) {
|
||||
display: none;
|
||||
|
|
|
@ -22,12 +22,13 @@ export default class SocketClient extends Module {
|
|||
|
||||
this.inject('settings');
|
||||
|
||||
this.settings.add('socket.cluster', {
|
||||
this.settings.add('socket.use-cluster', {
|
||||
default: 'Production',
|
||||
|
||||
ui: {
|
||||
path: 'Debugging @{"expanded": false, "sort": 9999} > Socket >> General',
|
||||
title: 'Server Cluster',
|
||||
description: 'Which server cluster to connect to. Do not change this unless you are actually doing development work on the socket server backend. Doing so will break all features relying on the socket server, including emote information lookups, link tooltips, and live data updates.',
|
||||
|
||||
component: 'setting-select-box',
|
||||
|
||||
|
@ -59,7 +60,7 @@ export default class SocketClient extends Module {
|
|||
this._host_pool = -1;
|
||||
|
||||
|
||||
this.settings.on(':changed:socket.cluster', () => {
|
||||
this.settings.on(':changed:socket.use-cluster', () => {
|
||||
this._host = null;
|
||||
if ( this.disconnected)
|
||||
this.connect();
|
||||
|
@ -113,7 +114,7 @@ export default class SocketClient extends Module {
|
|||
// ========================================================================
|
||||
|
||||
selectHost() {
|
||||
const cluster_id = this.settings.get('socket.cluster'),
|
||||
const cluster_id = this.settings.get('socket.use-cluster'),
|
||||
cluster = WS_CLUSTERS[cluster_id],
|
||||
l = cluster && cluster.length;
|
||||
|
||||
|
|
|
@ -657,7 +657,7 @@ export class ColorAdjuster {
|
|||
rgb = rgb.brighten(-1);
|
||||
}
|
||||
|
||||
const out = rgb.toHex();
|
||||
const out = rgb.toCSS();
|
||||
this._cache.set(color, out);
|
||||
return out;
|
||||
}
|
||||
|
|
|
@ -65,7 +65,8 @@ export const WS_CLUSTERS = {
|
|||
['wss://catbag.frankerfacez.com/', 0.25],
|
||||
['wss://andknuckles.frankerfacez.com/', 1],
|
||||
['wss://tuturu.frankerfacez.com/', 1],
|
||||
['wss://lilz.frankerfacez.com/', 1]
|
||||
['wss://lilz.frankerfacez.com/', 1],
|
||||
['wss://yoohoo.frankerfacez.com/', 1]
|
||||
],
|
||||
|
||||
Development: [
|
||||
|
|
|
@ -251,6 +251,65 @@ export const escape_regex = RegExp.escape || function escape_regex(str) {
|
|||
}
|
||||
|
||||
|
||||
const CONTROL_CHARS = '/$^+.()=!|';
|
||||
|
||||
export function glob_to_regex(input) {
|
||||
if ( typeof input !== 'string' )
|
||||
throw new TypeError('input must be a string');
|
||||
|
||||
let output = '',
|
||||
groups = 0;
|
||||
|
||||
for(let i=0, l=input.length; i<l; i++) {
|
||||
const char = input[i];
|
||||
|
||||
if ( CONTROL_CHARS.includes(char) )
|
||||
output += `\\${char}`;
|
||||
|
||||
else if ( char === '?' )
|
||||
output += '.';
|
||||
|
||||
else if ( char === '[' || char === ']' )
|
||||
output += char;
|
||||
|
||||
else if ( char === '{' ) {
|
||||
output += '(?:';
|
||||
groups++;
|
||||
|
||||
} else if ( char === '}' ) {
|
||||
if ( groups > 0 ) {
|
||||
output += ')';
|
||||
groups--;
|
||||
}
|
||||
|
||||
} else if ( char === ',' && groups > 0 )
|
||||
output += '|';
|
||||
|
||||
else if ( char === '*' ) {
|
||||
let count = 1;
|
||||
while(input[i+1] === '*') {
|
||||
count++;
|
||||
i++;
|
||||
}
|
||||
|
||||
if ( count > 1 )
|
||||
output += '.*?';
|
||||
else
|
||||
output += '[^ ]*?';
|
||||
|
||||
} else
|
||||
output += char;
|
||||
}
|
||||
|
||||
while(groups > 0) {
|
||||
output += ')';
|
||||
groups--;
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
|
||||
export class SourcedSet {
|
||||
constructor() {
|
||||
this._cache = [];
|
||||
|
|
|
@ -11,6 +11,12 @@
|
|||
.tw-display-inline { display: inline !important }
|
||||
.tw-width-auto { width: auto !important }
|
||||
|
||||
|
||||
.ffz-monospace {
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
|
||||
.ffz--widget {
|
||||
input, select {
|
||||
min-width: 20rem;
|
||||
|
@ -22,6 +28,11 @@
|
|||
}
|
||||
|
||||
|
||||
.ffz-min-width-unset {
|
||||
min-width: unset !important;
|
||||
}
|
||||
|
||||
|
||||
.ffz--color-widget input,
|
||||
.ffz--inline label {
|
||||
min-width: unset;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue