1
0
Fork 0
mirror of https://github.com/FrankerFaceZ/FrankerFaceZ.git synced 2025-07-05 10:38:30 +00:00

4.0.0-rc17

* Added: Highlight messages based on usernames and badges.
* Added: Block messages based on usernames and badges.

* Fixed: Display the number of months a subscriber has subscribed in badge tool-tips.
* Fixed: Rendering of chat messages sent from Twitch Extensions.
This commit is contained in:
SirStendec 2019-04-28 17:28:16 -04:00
parent b9cca1053d
commit 1649294bde
10 changed files with 809 additions and 82 deletions

View file

@ -149,7 +149,7 @@ ${typeof x[1] === 'string' ? x[1] : JSON.stringify(x[1], null, 4)}`
FrankerFaceZ.Logger = Logger; FrankerFaceZ.Logger = Logger;
const VER = FrankerFaceZ.version_info = { const VER = FrankerFaceZ.version_info = {
major: 4, minor: 0, revision: 0, extra: '-rc16.3', major: 4, minor: 0, revision: 0, extra: '-rc17',
commit: __git_commit__, commit: __git_commit__,
build: __webpack_hash__, build: __webpack_hash__,
toString: () => toString: () =>

View file

@ -155,67 +155,7 @@ export default class Badges extends Module {
path: 'Chat > Badges >> tabs ~> Visibility', path: 'Chat > Badges >> tabs ~> Visibility',
title: 'Visibility', title: 'Visibility',
component: 'badge-visibility', component: 'badge-visibility',
data: () => { data: () => this.getSettingsBadges(true)
const twitch = [],
game = [],
ffz = [],
addon = [];
for(const key in this.twitch_badges)
if ( has(this.twitch_badges, key) ) {
const badge = this.twitch_badges[key],
vs = [];
let v = badge && (badge[1] || badge[0]);
for(const key in badge)
if ( has(badge, key) ) {
const version = badge[key];
if ( ! v )
v = version;
if ( version && version.image1x )
vs.push({
version: key,
name: version.title,
image: version.image1x,
styleImage: `url("${version.image1x}")`
});
}
if ( v )
(badge.__game ? game : twitch).push({
id: key,
provider: 'twitch',
name: v.title,
color: 'transparent',
image: v.image2x,
versions: vs,
styleImage: `url("${v.image2x}")`
});
}
for(const key in this.badges)
if ( has(this.badges, key) ) {
const badge = this.badges[key],
image = badge.urls ? (badge.urls[2] || badge.urls[1]) : badge.image;
(/^addon/.test(key) ? addon : ffz).push({
id: key,
provider: 'ffz',
name: badge.title,
color: badge.color || 'transparent',
image,
styleImage: `url("${image}")`
});
}
return [
{title: 'Twitch', badges: twitch},
{title: 'Twitch: Game', key: 'game', badges: game},
{title: 'FrankerFaceZ', badges: ffz},
{title: 'Add-on', badges: addon}
];
}
} }
}); });
@ -247,6 +187,69 @@ export default class Badges extends Module {
}); });
} }
getSettingsBadges(include_addons) {
const twitch = [],
game = [],
ffz = [],
addon = [];
for(const key in this.twitch_badges)
if ( has(this.twitch_badges, key) ) {
const badge = this.twitch_badges[key],
vs = [];
let v = badge && (badge[1] || badge[0]);
for(const key in badge)
if ( has(badge, key) ) {
const version = badge[key];
if ( ! v )
v = version;
if ( version && version.image1x )
vs.push({
version: key,
name: version.title,
image: version.image1x,
styleImage: `url("${version.image1x}")`
});
}
if ( v )
(badge.__game ? game : twitch).push({
id: key,
provider: 'twitch',
name: v.title,
color: 'transparent',
image: v.image2x,
versions: vs,
styleImage: `url("${v.image2x}")`
});
}
if ( include_addons )
for(const key in this.badges)
if ( has(this.badges, key) ) {
const badge = this.badges[key],
image = badge.urls ? (badge.urls[2] || badge.urls[1]) : badge.image;
(/^addon/.test(key) ? addon : ffz).push({
id: key,
provider: 'ffz',
name: badge.title,
color: badge.color || 'transparent',
image,
styleImage: `url("${image}")`
});
}
return [
{title: 'Twitch', badges: twitch},
{title: 'Twitch: Game', key: 'game', badges: game},
{title: 'FrankerFaceZ', badges: ffz},
{title: 'Add-on', badges: addon}
];
}
onEnable() { onEnable() {
this.parent.context.on('changed:chat.badges.custom-mod', this.rebuildAllCSS, this); this.parent.context.on('changed:chat.badges.custom-mod', this.rebuildAllCSS, this);
@ -278,9 +281,19 @@ export default class Badges extends Module {
if ( ! bd ) if ( ! bd )
continue; continue;
let title = bd.title || global_badge.title;
if ( d.data ) {
if ( d.badge === 'subscriber' ) {
title = this.i18n.t('badges.subscriber.months', '%{title} (%{count} Month%{count|en_plural})', {
title,
count: d.data
});
}
}
out.push(<div class="ffz-badge-tip"> out.push(<div class="ffz-badge-tip">
{show_previews && <img class="preview-image ffz-badge" src={bd.image4x} />} {show_previews && <img class="preview-image ffz-badge" src={bd.image4x} />}
{bd.title || global_badge.title} {title}
</div>); </div>);
/*out.push(e('div', {className: 'ffz-badge-tip'}, [ /*out.push(e('div', {className: 'ffz-badge-tip'}, [
@ -332,6 +345,7 @@ export default class Badges extends Module {
out = [], out = [],
slotted = {}, slotted = {},
twitch_badges = msg.badges || {}, twitch_badges = msg.badges || {},
dynamic_data = msg.badgeDynamicData || {},
user = msg.user || {}, user = msg.user || {},
user_id = user.id, user_id = user.id,
@ -357,7 +371,8 @@ export default class Badges extends Module {
else else
slot = last_slot++; slot = last_slot++;
const urls = badge_id === 'moderator' && custom_mod && room && room.data && room.data.mod_urls, const data = dynamic_data[badge_id],
urls = badge_id === 'moderator' && custom_mod && room && room.data && room.data.mod_urls,
badges = []; badges = [];
if ( urls ) { if ( urls ) {
@ -366,14 +381,16 @@ export default class Badges extends Module {
provider: 'ffz', provider: 'ffz',
image: urls[4] || urls[2] || urls[1], image: urls[4] || urls[2] || urls[1],
color: '#34ae0a', color: '#34ae0a',
title: bd ? bd.title : 'Moderator' title: bd ? bd.title : 'Moderator',
data
}); });
} else } else
badges.push({ badges.push({
provider: 'twitch', provider: 'twitch',
badge: badge_id, badge: badge_id,
version version,
data
}); });
slotted[slot] = { slotted[slot] = {

View file

@ -186,6 +186,7 @@ export default class Chat extends Module {
ui: { ui: {
path: 'Chat > Behavior >> Deleted Messages', path: 'Chat > Behavior >> Deleted Messages',
title: 'Deleted Message Style', title: 'Deleted Message Style',
description: 'This style will be applied to deleted messages showed in Detailed rendering mode to differentiate them from normal chat messages.',
component: 'setting-select-box', component: 'setting-select-box',
data: [ data: [
{value: 0, title: 'Faded'}, {value: 0, title: 'Faded'},
@ -200,8 +201,8 @@ export default class Chat extends Module {
default: false, default: false,
ui: { ui: {
path: 'Chat > Behavior >> Deleted Messages', path: 'Chat > Behavior >> Deleted Messages',
title: 'Deleted Message Rendering', title: 'Rendering Mode',
description: 'This, when set, overrides the mode selected in Twitch Chat settings. We do this to allow non-moderators access to the setting.', description: 'This, when set, overrides the mode selected in Twitch chat settings. We do this to allow non-moderators access to the setting.',
component: 'setting-select-box', component: 'setting-select-box',
data: [ data: [
{value: false, title: 'Do Not Override'}, {value: false, title: 'Do Not Override'},
@ -216,7 +217,7 @@ export default class Chat extends Module {
default: 1, default: 1,
ui: { ui: {
path: 'Chat > Behavior >> Deleted Messages', path: 'Chat > Behavior >> Deleted Messages',
title: 'Display Deletion Reason', title: 'Display Reason',
component: 'setting-select-box', component: 'setting-select-box',
data: [ data: [
{value: 0, title: 'Never'}, {value: 0, title: 'Never'},
@ -285,6 +286,177 @@ export default class Chat extends Module {
} }
}); });
this.settings.add('chat.filtering.highlight-basic-users', {
default: [],
type: 'array_merge',
always_inherit: true,
ui: {
path: 'Chat > Filtering >> Highlight Users',
component: 'basic-terms',
colored: true,
words: false
}
});
this.settings.add('chat.filtering.highlight-basic-users--color-regex', {
requires: ['chat.filtering.highlight-basic-users'],
process(ctx) {
const val = ctx.get('chat.filtering.highlight-basic-users');
if ( ! val || ! val.length )
return null;
const colors = new Map;
for(const item of val) {
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;
try {
new RegExp(v);
} catch(err) {
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(`^${list.join('|')}$`, 'gi'));
}
return colors;
}
});
this.settings.add('chat.filtering.highlight-basic-users-blocked', {
default: [],
type: 'array_merge',
always_inherit: true,
ui: {
path: 'Chat > Filtering >> Blocked Users',
component: 'basic-terms',
removable: true,
words: false
}
});
this.settings.add('chat.filtering.highlight-basic-users-blocked--regex', {
requires: ['chat.filtering.highlight-basic-blocked'],
process(ctx) {
const val = ctx.get('chat.filtering.highlight-basic-users-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[item.remove ? 1 : 0].push(v);
}
return out.map(data => {
if ( ! data.length )
return null;
return new RegExp(`^${data.join('|')}$`, 'gi');
});
}
});
this.settings.add('chat.filtering.highlight-basic-badges', {
default: [],
type: 'array_merge',
always_inherit: true,
ui: {
path: 'Chat > Filtering >> Highlight Badges',
component: 'badge-highlighting',
colored: true,
data: () => this.badges.getSettingsBadges()
}
});
this.settings.add('chat.filtering.highlight-basic-badges--colors', {
requires: ['chat.filtering.highlight-basic-badges'],
process(ctx) {
const val = ctx.get('chat.filtering.highlight-basic-badges');
if ( ! val || ! val.length )
return null;
const colors = new Map;
for(const item of val) {
const c = item.c || null,
v = item.v;
colors.set(v, c);
}
return colors;
}
});
this.settings.add('chat.filtering.highlight-basic-badges-blocked', {
default: [],
type: 'array_merge',
always_inherit: true,
ui: {
path: 'Chat > Filtering >> Blocked Badges',
component: 'badge-highlighting',
removable: true,
data: () => this.badges.getSettingsBadges()
}
});
this.settings.add('chat.filtering.highlight-basic-badges-blocked--list', {
requires: ['chat.filtering.highlight-basic-badges-blocked'],
process(ctx) {
const val = ctx.get('chat.filtering.highlight-basic-badges-blocked');
if ( ! val || ! val.length )
return null;
const out = [[], []];
for(const item of val)
if ( item.v )
out[item.remove ? 1 : 0].push(item.v);
if ( ! out[0].length && ! out[1].length )
return null;
return out;
}
});
this.settings.add('chat.filtering.highlight-basic-terms', { this.settings.add('chat.filtering.highlight-basic-terms', {
default: [], default: [],
@ -297,7 +469,6 @@ export default class Chat extends Module {
} }
}); });
this.settings.add('chat.filtering.highlight-basic-terms--color-regex', { this.settings.add('chat.filtering.highlight-basic-terms--color-regex', {
requires: ['chat.filtering.highlight-basic-terms'], requires: ['chat.filtering.highlight-basic-terms'],
process(ctx) { process(ctx) {
@ -636,10 +807,10 @@ export default class Chat extends Module {
if ( id && typeof id === 'number' ) if ( id && typeof id === 'number' )
id = `${id}`; id = `${id}`;
if ( this.user_ids[id] ) if ( id && this.user_ids[id] )
user = this.user_ids[id]; user = this.user_ids[id];
else if ( this.users[login] && ! no_login ) else if ( login && this.users[login] && ! no_login )
user = this.users[login]; user = this.users[login];
if ( user && user.destroyed ) if ( user && user.destroyed )
@ -696,10 +867,10 @@ export default class Chat extends Module {
if ( id && typeof id === 'number' ) if ( id && typeof id === 'number' )
id = `${id}`; id = `${id}`;
if ( this.room_ids[id] ) if ( id && this.room_ids[id] )
room = this.room_ids[id]; room = this.room_ids[id];
else if ( this.rooms[login] && ! no_login ) else if ( login && this.rooms[login] && ! no_login )
room = this.rooms[login]; room = this.rooms[login];
if ( room && room.destroyed ) if ( room && room.destroyed )
@ -863,7 +1034,9 @@ export default class Chat extends Module {
if ( ! user ) if ( ! user )
user = msg.user = {}; user = msg.user = {};
user.color = user.color || user.chatColor || null; const ext = msg.extension || {};
user.color = user.color || user.chatColor || ext.chatColor || null;
user.type = user.type || user.userType || null; user.type = user.type || user.userType || null;
user.id = user.id || user.userID || null; user.id = user.id || user.userID || null;
user.login = user.login || user.userLogin || null; user.login = user.login || user.userLogin || null;
@ -888,7 +1061,13 @@ export default class Chat extends Module {
// Standardize Badges // Standardize Badges
if ( ! msg.badges && user.displayBadges ) { if ( ! msg.badges && user.displayBadges ) {
const b = msg.badges = {}; const b = msg.badges = {};
for(const item of msg.user.displayBadges) for(const item of user.displayBadges)
b[item.setID] = item.version;
}
if ( ! msg.badges && ext.displayBadges ) {
const b = msg.badges = {};
for(const item of ext.displayBadges)
b[item.setID] = item.version; b[item.setID] = item.version;
} }

View file

@ -185,10 +185,10 @@ export default class Room {
if ( id && typeof id === 'number' ) if ( id && typeof id === 'number' )
id = `${id}`; id = `${id}`;
if ( this.user_ids[id] ) if ( id && this.user_ids[id] )
user = this.user_ids[id]; user = this.user_ids[id];
else if ( this.users[login] && ! no_login ) else if ( login && this.users[login] && ! no_login )
user = this.users[login]; user = this.users[login];
if ( user && user.destroyed ) if ( user && user.destroyed )

View file

@ -268,6 +268,119 @@ export const Mentions = {
// Custom Highlight Terms // Custom Highlight Terms
// ============================================================================ // ============================================================================
export const UserHighlights = {
type: 'user_highlight',
priority: 90,
process(tokens, msg, user) {
if ( user && user.login && user.login == msg.user.login && ! this.context.get('chat.filtering.process-own') )
return tokens;
const colors = this.context.get('chat.filtering.highlight-basic-users--color-regex');
if ( ! colors || ! colors.size )
return tokens;
const u = msg.user;
for(const [color, regex] of colors) {
if ( regex.test(u.login) || regex.test(u.displayName) ) {
msg.mentioned = true;
if ( color ) {
msg.mention_color = color;
return tokens;
}
}
}
return tokens;
}
}
export const BlockedUsers = {
type: 'user_block',
priority: 100,
process(tokens, msg, user) {
if ( user && user.login && user.login == msg.user.login && ! this.context.get('chat.filtering.process-own') )
return tokens;
const u = msg.user,
regexes = this.context.get('chat.filtering.highlight-basic-users-blocked--regex');
if ( ! regexes )
return tokens;
if ( regexes[1] && (regexes[1].test(u.login) || regexes[1].test(u.displayName)) ) {
msg.deleted = true;
msg.ffz_removed = true;
}
if ( ! msg.deleted && regexes[0] && (regexes[0].test(u.login) || regexes[0].test(u.displayName)) )
msg.deleted = true;
return tokens;
}
}
export const BadgeHighlights = {
type: 'badge_highlight',
priority: 80,
process(tokens, msg, user) {
if ( user && user.login && user.login == msg.user.login && ! this.context.get('chat.filtering.process-own') )
return tokens;
const badges = msg.badges;
if ( ! badges )
return tokens;
const colors = this.context.get('chat.filtering.highlight-basic-badges--colors');
if ( ! colors || ! colors.size )
return tokens;
for(const badge of Object.keys(badges)) {
if ( colors.has(badge) ) {
const color = colors.get(badge);
msg.mentioned = true;
if ( color ) {
msg.mention_color = color;
return tokens;
}
}
}
return tokens;
}
}
export const BlockedBadges = {
type: 'badge_block',
priority: 100,
process(tokens, msg, user) {
if ( user && user.login && user.login == msg.user.login && ! this.context.get('chat.filtering.process-own') )
return tokens;
const badges = msg.badges;
if ( ! badges )
return tokens;
const list = this.context.get('chat.filtering.highlight-basic-badges-blocked--list');
if ( ! list || (! list[0].length && ! list[1].length) )
return tokens;
for(const badge of Object.keys(badges)) {
if ( list[1].includes(badge) ) {
msg.deleted = true;
msg.ffz_removed = true;
return tokens;
}
if ( ! msg.deleted && list[0].includes(badge) )
msg.deleted = true;
}
return tokens;
}
}
export const CustomHighlights = { export const CustomHighlights = {
type: 'highlight', type: 'highlight',
priority: 100, priority: 100,
@ -310,7 +423,7 @@ export const CustomHighlights = {
out.push({type: 'text', text: text.slice(idx, nix)}); out.push({type: 'text', text: text.slice(idx, nix)});
msg.mentioned = true; msg.mentioned = true;
msg.mention_color = color; msg.mention_color = color || msg.mention_color;
out.push({ out.push({
type: 'highlight', type: 'highlight',

View file

@ -0,0 +1,99 @@
<template lang="html">
<section class="ffz--widget ffz--badge-highlighting">
<badge-term-editor
:term="default_term"
:badges="data"
:colored="item.colored"
:removable="item.removable"
:adding="true"
@save="new_term"
/>
<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">
{{ t('setting.terms.no-terms', 'no terms are defined in this profile') }}
</div>
<ul v-else class="ffz--term-list tw-mg-t-05">
<badge-term-editor
v-for="term in val"
v-if="term.t !== 'inherit'"
:key="term.id"
:term="term.v"
:badges="data"
:colored="item.colored"
:removable="item.removable"
@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 {
default_term: {
v: 'broadcaster',
c: '',
remove: false
}
}
},
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: {
new_term(term) {
if ( ! term.v )
return;
const vals = Array.from(this.val);
vals.push({v: 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

@ -0,0 +1,213 @@
<template lang="html">
<div class="ffz--term ffz--badge-term">
<div class="tw-align-items-center tw-flex tw-flex-nowrap tw-flex-row tw-full-width">
<div class="tw-mg-r-1">
<img
v-if="current"
:src="current.image"
class="ffz--badge-term-image"
>
</div>
<div class="tw-flex-grow-1 tw-mg-r-05">
<h4 v-if="! editing && ! current" class="ffz-monospace">
<pre>{{ t('setting.terms.invalid-badge', 'unknown/unloaded badge') }}</pre>
</h4>
<h4 v-if="! editing && current">
{{ current.name }}
</h4>
<select
v-if="editing"
v-model="edit_data.v"
class="tw-block tw-full-width tw-border-radius-medium tw-font-size-6 tw-full-width tw-select tw-pd-x-1 tw-pd-y-05 tw-mg-y-05">
<optgroup
v-for="section in badges"
:key="section.title"
:label="section.title"
>
<option
v-for="badge in section.badges"
:key="badge.id"
:value="badge.id"
>
{{ badge.name }}
</option>
</optgroup>
</select>
</div>
<div v-if="colored" class="tw-flex-shrink-0 tw-mg-r-05">
<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">
<figure :style="`background-color: ${term.c}`">
&nbsp;
</figure>
</div>
</div>
<div v-if="removable" class="tw-flex-shrink-0 tw-mg-r-05 tw-tooltip-wrapper">
<button
v-if="editing"
:class="{active: edit_data.remove}"
class="tw-button ffz-directory-toggle-block"
@click="toggleRemove"
>
<span
:class="edit_data.remove ? 'ffz-i-eye-off' : 'ffz-i-eye'"
class="tw-button__text"
/>
</button>
<span
v-else-if="term.remove"
class="ffz-i-eye-off tw-pd-x-1"
/>
<div class="tw-tooltip tw-tooltip--down tw-tooltip--align-right">
<span v-if="display.remove">
{{ t('setting.terms.remove.on', 'Remove matching messages from chat.') }}
</span>
<span v-else>
{{ t('setting.terms.remove.off', 'Do not remove matching messages from chat.') }}
</span>
</div>
</div>
<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">
<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>
</div>
</template>
<script>
import {deep_copy} from 'utilities/object';
let id = 0;
export default {
props: {
badges: Array,
term: Object,
colored: {
type: Boolean,
default: false
},
removable: {
type: Boolean,
default: false
},
adding: {
type: Boolean,
default: false
}
},
data() {
if ( this.adding )
return {
editor_id: id++,
deleting: false,
editing: true,
edit_data: deep_copy(this.term)
};
return {
editor_id: id++,
deleting: false,
editing: false,
edit_data: null
}
},
computed: {
display() {
return this.editing ? this.edit_data : this.term;
},
current() {
if ( ! this.badges || ! this.display || ! this.display.v )
return null;
const v = this.display.v;
for(const section of this.badges) {
if ( ! section || ! section.badges )
continue;
for(const badge of section.badges)
if ( badge.id === v )
return badge;
}
}
},
methods: {
edit() {
this.editing = true;
this.edit_data = Object.assign({remove: false}, deep_copy(this.term));
},
toggleRemove() {
if ( this.editing )
this.edit_data.remove = ! this.edit_data.remove;
},
cancel() {
if ( this.adding )
this.edit_data = deep_copy(this.term);
else {
this.editing = false;
this.edit_data = null
}
},
save() {
this.$emit('save', this.edit_data);
this.cancel();
}
}
}
</script>

View file

@ -3,6 +3,7 @@
<term-editor <term-editor
:term="default_term" :term="default_term"
:colored="item.colored" :colored="item.colored"
:words="item.words"
:removable="item.removable" :removable="item.removable"
:adding="true" :adding="true"
@save="new_term" @save="new_term"
@ -17,6 +18,7 @@
:key="term.id" :key="term.id"
:term="term.v" :term="term.v"
:colored="item.colored" :colored="item.colored"
:words="item.words"
:removable="item.removable" :removable="item.removable"
@remove="remove(term)" @remove="remove(term)"
@save="save(term, $event)" @save="save(term, $event)"

View file

@ -44,7 +44,7 @@
> >
<option value="text">{{ t('setting.terms.type.text', 'Text') }}</option> <option value="text">{{ t('setting.terms.type.text', 'Text') }}</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 v-if="words" value="regex">{{ t('setting.terms.type.regex-word', 'Regex (Word)') }}</option>
<option value="raw">{{ t('setting.terms.type.regex', 'Regex') }}</option> <option value="raw">{{ t('setting.terms.type.regex', 'Regex') }}</option>
</select> </select>
</div> </div>
@ -137,6 +137,10 @@ let id = 0;
export default { export default {
props: { props: {
term: Object, term: Object,
words: {
type: Boolean,
default: true
},
colored: { colored: {
type: Boolean, type: Boolean,
default: false default: false

View file

@ -36,13 +36,19 @@ export default class ChatLine extends Module {
this.ChatLine = this.fine.define( this.ChatLine = this.fine.define(
'chat-line', 'chat-line',
n => n.renderMessageBody && n.props && !has(n.props, 'hasModPermissions'), n => n.renderMessageBody && n.props && ! n.onExtensionNameClick && !has(n.props, 'hasModPermissions'),
Twilight.CHAT_ROUTES
);
this.ExtensionLine = this.fine.define(
'extension-line',
n => n.renderMessageBody && n.onExtensionNameClick,
Twilight.CHAT_ROUTES Twilight.CHAT_ROUTES
); );
this.ChatRoomLine = this.fine.define( this.ChatRoomLine = this.fine.define(
'chat-room-line', 'chat-room-line',
n => n.renderMessageBody && n.props && has(n.props, 'hasModPermissions'), n => n.renderMessageBody && n.props && ! n.onExtensionNameClick && has(n.props, 'hasModPermissions'),
Twilight.CHAT_ROUTES Twilight.CHAT_ROUTES
); );
@ -78,7 +84,11 @@ export default class ChatLine extends Module {
this.chat.context.on('changed:chat.filtering.process-own', this.updateLines, this); this.chat.context.on('changed:chat.filtering.process-own', this.updateLines, this);
this.chat.context.on('changed:chat.timestamp-format', this.updateLines, this); this.chat.context.on('changed:chat.timestamp-format', 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-terms--color-regex', this.updateLines, this);
this.chat.context.on('changed:chat.filtering.highlight-basic-users--color-regex', this.updateLines, this);
this.chat.context.on('changed:chat.filtering.highlight-basic-badges--colors', this.updateLines, this);
this.chat.context.on('changed:chat.filtering.highlight-basic-blocked--regex', this.updateLines, this); this.chat.context.on('changed:chat.filtering.highlight-basic-blocked--regex', this.updateLines, this);
this.chat.context.on('changed:chat.filtering.highlight-basic-users-blocked--regex', this.updateLines, this);
this.chat.context.on('changed:chat.filtering.highlight-basic-badges-blocked--list', this.updateLines, this);
const t = this, const t = this,
React = await this.web_munch.findModule('react'); React = await this.web_munch.findModule('react');
@ -692,6 +702,8 @@ export default class ChatLine extends Module {
}, out); }, out);
} catch(err) { } catch(err) {
t.log.info(err);
t.log.capture(err, { t.log.capture(err, {
extra: { extra: {
props: this.props props: this.props
@ -704,6 +716,85 @@ export default class ChatLine extends Module {
// Do this after a short delay to hopefully reduce the chance of React // Do this after a short delay to hopefully reduce the chance of React
// freaking out on us. // freaking out on us.
setTimeout(() => this.ChatLine.forceUpdate()); setTimeout(() => this.ChatLine.forceUpdate());
});
this.ExtensionLine.ready(cls => {
const old_render = cls.prototype.render;
cls.prototype.render = function() { try {
if ( ! this.props.installedExtensions )
return null;
const msg = t.chat.standardizeMessage(this.props.message),
ext = msg && msg.extension;
if( ! ext )
return null;
if ( ! this.props.installedExtensions.some(val => {
const e = val.extension;
return e && e.clientID === ext.clientID && e.version === ext.version;
}) )
return null;
const color = t.parent.colors.process(ext.chatColor);
let room = msg.roomLogin ? msg.roomLogin : msg.channel ? msg.channel.slice(1) : undefined;
if ( ! room && this.props.channelID ) {
const r = t.chat.getRoom(this.props.channelID, null, true);
if ( r && r.login )
room = msg.roomLogin = r.login;
}
const u = t.site.getUser(),
r = {id: this.props.channelID, login: room},
tokens = msg.ffz_tokens = msg.ffz_tokens || t.chat.tokenizeMessage(msg, u, r),
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 ( ! tokens.length )
return null;
return e('div', {
className: `chat-line__message${msg.mentioned ? ' ffz-mentioned' : ''}${bg_css ? ' ffz-custom-color' : ''}`,
style: {backgroundColor: bg_css},
'data-room-id': r.id,
'data-room': r.login,
'data-extension': ext.clientID
}, [
this.props.showTimestamps && e('span', {
className: 'chat-line__timestamp'
}, t.chat.formatTime(msg.timestamp)),
e('span', {
className: 'chat-line__message--badges'
}, t.chat.badges.render(msg, e)),
e('button', {
className: 'chat-line__username notranslate',
style: { color },
onClick: this.onExtensionNameClick
}, e('span', {
className: 'chat-author__display-name'
}, ext.displayName)),
e('span', null, ': '),
e('span', {
className: 'message'
}, t.chat.renderTokens(tokens, e)),
rich_content && e(FFZRichContent, rich_content)
]);
} catch(err) {
t.log.info(err);
t.log.capture(err, {
extra: {
props: this.props
}
});
return old_render.call(this);
} }
// Do this after a short delay to hopefully reduce the chance of React
// freaking out on us.
setTimeout(() => this.ExtensionLine.forceUpdate());
}) })
} }
@ -722,6 +813,14 @@ export default class ChatLine extends Module {
} }
} }
for(const inst of this.ExtensionLine.instances) {
const msg = inst.props.message;
if ( msg ) {
msg.ffz_tokens = null;
msg.mentioned = msg.mention_color = null;
}
}
for(const inst of this.ChatRoomLine.instances) { for(const inst of this.ChatRoomLine.instances) {
const msg = inst.props.message; const msg = inst.props.message;
if ( msg ) { if ( msg ) {
@ -737,6 +836,7 @@ export default class ChatLine extends Module {
} }
this.ChatLine.forceUpdate(); this.ChatLine.forceUpdate();
this.ExtensionLine.forceUpdate();
this.ChatRoomLine.forceUpdate(); this.ChatRoomLine.forceUpdate();
this.WhisperLine.forceUpdate(); this.WhisperLine.forceUpdate();