1
0
Fork 0
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:
SirStendec 2018-05-31 18:34:15 -04:00
parent 6b2b734ef9
commit 2a790ad7cd
22 changed files with 669 additions and 48 deletions

View file

@ -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>

View file

@ -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' : ''}`

View 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"
>
&times;&times;&times;
</strong>
</template>

View file

@ -0,0 +1,5 @@
<template functional>
<strong class="ffz--highlight">
{{ props.token.text }}
</strong>
</template>

View file

@ -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: {

View file

@ -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"
>
&times;&times;&times;
</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
// ============================================================================

View 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>

View file

@ -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}`">
&nbsp;
</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;
},

View 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}`">
&nbsp;
</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>

View file

@ -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();
}

View file

@ -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();

View file

@ -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 {

View file

@ -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 & {

View file

@ -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 & {

View file

@ -1,3 +1,4 @@
.ffz--highlight,
.ffz--mention-me {
border-radius: .5rem;
padding: .3rem;

View file

@ -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 @@
}
}
}
}

View file

@ -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;

View file

@ -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;

View file

@ -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;
}

View file

@ -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: [

View file

@ -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 = [];

View file

@ -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;