1
0
Fork 0
mirror of https://github.com/FrankerFaceZ/FrankerFaceZ.git synced 2025-06-27 21:05:53 +00:00
* Added: Setting to filter out chat messages by type. Can be used to remove hosting and raid notices, among other things.
* Added: Support for clicking badges. It's a native feature that's been missing for ages.
* Changed: Make headers slightly bigger in settings so that they stand out more.
* Fixed: Certain settings inheriting default values when they should not.
* Fixed: Bug in the FFZ Emote Menu when garbage subscription data is returned from Twitch.
* Fixed: Do not suppress error messages in the console when an error happens in an event handler.
This commit is contained in:
SirStendec 2020-08-17 13:33:30 -04:00
parent fd2977f899
commit 129f350a80
12 changed files with 626 additions and 200 deletions

View file

@ -1,158 +1,159 @@
/* globals module */
module.exports = {
"env": {
"browser": true,
"es6": true
},
"extends": [
"eslint:recommended",
"plugin:vue/recommended"
],
"plugins": [
"vue",
"react"
],
"parserOptions": {
"parser": "babel-eslint",
"ecmaVersion": 8,
"sourceType": "module",
"ecmaFeatures": {
"jsx": true
}
},
"settings": {
"react": {
"pragma": "createElement"
}
},
"globals": {
"import": false,
"require": false,
"__webpack_hash__": false,
"__git_commit__": false,
"__version_major__": false,
"__version_minor__": false,
"__version_patch__": false,
"__version_prerelease__": false,
"FrankerFaceZ": false
},
"rules": {
"require-atomic-updates": "off",
"accessor-pairs": ["error"],
"block-scoped-var": ["error"],
"class-methods-use-this": ["error"],
"for-direction": ["error"],
"guard-for-in": ["warn"],
"no-alert": ["error"],
"no-await-in-loop": ["error"],
"no-caller": ["error"],
"no-catch-shadow": ["error"],
"no-invalid-this": ["error"],
"no-iterator": ["error"],
"no-labels": ["error"],
"no-lone-blocks": ["error"],
"no-octal-escape": ["error"],
"no-proto": ["warn"],
"no-return-await": ["error"],
"no-self-compare": ["error"],
"no-sequences": ["error"],
"no-shadow-restricted-names": ["error"],
"no-template-curly-in-string": ["warn"],
"no-throw-literal": ["error"],
"no-undef-init": ["error"],
"no-unmodified-loop-condition": ["error"],
"no-use-before-define": ["error", {
"functions": false,
"classes": false
}],
"no-useless-call": ["warn"],
"no-useless-concat": ["warn"],
"no-useless-return": ["warn"],
"no-void": ["error"],
"no-warning-comments": ["warn"],
"no-with": ["error"],
"radix": ["error"],
"require-await": ["warn"],
"valid-jsdoc": ["warn"],
"yoda": ["warn"],
'env': {
'browser': true,
'es6': true
},
'extends': [
'eslint:recommended',
'plugin:vue/recommended'
],
'plugins': [
'vue',
'react'
],
'parserOptions': {
'parser': 'babel-eslint',
'ecmaVersion': 8,
'sourceType': 'module',
'ecmaFeatures': {
'jsx': true
}
},
'settings': {
'react': {
'pragma': 'createElement'
}
},
'globals': {
'import': false,
'require': false,
'__webpack_hash__': false,
'__git_commit__': false,
'__version_major__': false,
'__version_minor__': false,
'__version_patch__': false,
'__version_prerelease__': false,
'FrankerFaceZ': false
},
'rules': {
'require-atomic-updates': 'off',
'accessor-pairs': ['error'],
'block-scoped-var': ['error'],
'class-methods-use-this': ['error'],
'for-direction': ['error'],
'guard-for-in': ['warn'],
'no-alert': ['error'],
'no-await-in-loop': ['error'],
'no-caller': ['error'],
'no-catch-shadow': ['error'],
'no-invalid-this': ['error'],
'no-iterator': ['error'],
'no-labels': ['error'],
'no-lone-blocks': ['error'],
'no-octal-escape': ['error'],
'no-proto': ['warn'],
'no-return-await': ['error'],
'no-self-compare': ['error'],
'no-sequences': ['error'],
'no-shadow-restricted-names': ['error'],
'no-template-curly-in-string': ['warn'],
'no-throw-literal': ['error'],
'no-undef-init': ['error'],
'no-unmodified-loop-condition': ['error'],
'no-use-before-define': ['error', {
'functions': false,
'classes': false
}],
'no-useless-call': ['warn'],
'no-useless-concat': ['warn'],
'no-useless-return': ['warn'],
'no-void': ['error'],
'no-warning-comments': ['warn'],
'no-with': ['error'],
'radix': ['error'],
'require-await': ['warn'],
'valid-jsdoc': ['warn'],
'yoda': ['warn'],
"arrow-body-style": ["warn", "as-needed"],
"arrow-parens": ["warn", "as-needed"],
"arrow-spacing": ["warn"],
"generator-star-spacing": ["warn"],
"no-duplicate-imports": ["error"],
"no-useless-computed-key": ["error"],
"no-useless-constructor": ["error"],
"no-useless-rename": ["error"],
"no-var": ["error"],
"no-cond-assign": ["warn"],
"object-shorthand": ["warn"],
"prefer-arrow-callback": ["warn", {"allowUnboundThis": true}],
"prefer-const": ["warn", {"ignoreReadBeforeAssign": true}],
"prefer-rest-params": ["warn"],
"prefer-spread": ["error"],
"prefer-template": ["warn"],
"rest-spread-spacing": ["error", "never"],
"yield-star-spacing": ["warn"],
'arrow-body-style': ['warn', 'as-needed'],
'arrow-parens': ['warn', 'as-needed'],
'arrow-spacing': ['warn'],
'generator-star-spacing': ['warn'],
'no-duplicate-imports': ['error'],
'no-useless-computed-key': ['error'],
'no-useless-constructor': ['error'],
'no-useless-rename': ['error'],
'no-var': ['error'],
'no-cond-assign': ['warn'],
'object-shorthand': ['warn'],
'prefer-arrow-callback': ['warn', {'allowUnboundThis': true}],
'prefer-const': ['warn', {'ignoreReadBeforeAssign': true}],
'prefer-rest-params': ['warn'],
'prefer-spread': ['error'],
'prefer-template': ['warn'],
'rest-spread-spacing': ['error', 'never'],
'yield-star-spacing': ['warn'],
"indent": [
"warn",
"tab",
{
"SwitchCase": 1
}
],
"linebreak-style": [
"error",
"unix"
],
"quotes": [
"error",
"single",
{
"avoidEscape": true,
"allowTemplateLiterals": true
}
],
'indent': [
'warn',
'tab',
{
'SwitchCase': 1
}
],
'linebreak-style': [
'error',
'unix'
],
'quotes': [
'error',
'single',
{
'avoidEscape': true,
'allowTemplateLiterals': true
}
],
"vue/html-indent": [
"warn",
"tab"
],
"vue/valid-template-root": "off",
"vue/max-attributes-per-line": "off",
"vue/require-prop-types": "off",
"vue/require-default-prop": "off",
"vue/html-closing-bracket-newline": [
"error",
{
"singleline": "never",
"multiline": "always"
}
],
'vue/html-indent': [
'warn',
'tab'
],
'vue/valid-template-root': 'off',
'vue/max-attributes-per-line': 'off',
'vue/require-prop-types': 'off',
'vue/require-default-prop': 'off',
'vue/html-closing-bracket-newline': [
'error',
{
'singleline': 'never',
'multiline': 'always'
}
],
"jsx-quotes": ["error", "prefer-double"],
"react/jsx-boolean-value": "error",
"react/jsx-closing-bracket-location": ["error", "line-aligned"],
//"react/jsx-closing-tag-location": "error" -- stupid rule that doesn't allow line-aligned
"react/jsx-equals-spacing": "error",
"react/jsx-filename-extension": "error",
"react/jsx-first-prop-new-line": ["error", "multiline-multiprop"],
"react/jsx-indent": ["warn", "tab"],
"react/jsx-indent-props": ["warn", "tab"],
//"react/jsx-key": "warn",
"react/jsx-no-bind": "error",
"react/jsx-no-comment-textnodes": "error",
"react/jsx-no-duplicate-props": "error",
"react/jsx-no-target-blank": "error",
"react/jsx-sort-props": ["error", {
"callbacksLast": true,
"reservedFirst": true,
"noSortAlphabetically": true
}],
"react/jsx-tag-spacing": ["error", {
"beforeClosing": "never"
}],
"react/jsx-uses-react": "error",
"react/jsx-wrap-multilines": "error"
}
'jsx-quotes': ['error', 'prefer-double'],
'react/jsx-boolean-value': 'error',
'react/jsx-closing-bracket-location': ['error', 'line-aligned'],
//'react/jsx-closing-tag-location': 'error' -- stupid rule that doesn't allow line-aligned
'react/jsx-equals-spacing': 'error',
'react/jsx-filename-extension': 'error',
'react/jsx-first-prop-new-line': ['error', 'multiline-multiprop'],
'react/jsx-indent': ['warn', 'tab'],
'react/jsx-indent-props': ['warn', 'tab'],
//'react/jsx-key': 'warn',
'react/jsx-no-bind': 'error',
'react/jsx-no-comment-textnodes': 'error',
'react/jsx-no-duplicate-props': 'error',
'react/jsx-no-target-blank': 'error',
'react/jsx-sort-props': ['error', {
'callbacksLast': true,
'reservedFirst': true,
'noSortAlphabetically': true
}],
'react/jsx-tag-spacing': ['error', {
'beforeClosing': 'never'
}],
'react/jsx-uses-react': 'error',
'react/jsx-wrap-multilines': 'error'
}
};

View file

@ -1,7 +1,7 @@
{
"name": "frankerfacez",
"author": "Dan Salvato LLC",
"version": "4.20.32",
"version": "4.20.33",
"description": "FrankerFaceZ is a Twitch enhancement suite.",
"license": "Apache-2.0",
"scripts": {

View file

@ -73,6 +73,8 @@ export default class Actions extends Module {
],
type: 'array_merge',
inherit_default: true,
ui: {
path: 'Chat > Actions > In-Line @{"description": "Here, you can define custom actions that will appear along messages in chat. If you aren\'t seeing an action you\'ve defined here in chat, please make sure that you have enabled Mod Icons in the chat settings menu."}',
component: 'chat-actions',
@ -103,6 +105,8 @@ export default class Actions extends Module {
default: [],
type: 'array_merge',
inherit_default: true,
ui: {
path: 'Chat > Actions > User Context @{"description": "Here, you can define custom actions that will appear in a context menu when you right-click a username in chat."}',
component: 'chat-actions',
@ -131,6 +135,8 @@ export default class Actions extends Module {
default: [],
type: 'array_merge',
inherit_default: true,
ui: {
path: 'Chat > Actions > Room @{"description": "Here, you can define custom actions that will appear above the chat input box."}',
component: 'chat-actions',

View file

@ -91,7 +91,7 @@ export function generateOverrideCSS(data, style) {
}
export function generateBadgeCSS(badge, version, data, style, is_dark, badge_version = 2, color_fixer, scale = 1) {
export function generateBadgeCSS(badge, version, data, style, is_dark, badge_version = 2, color_fixer, scale = 1, clickable = false) {
let color = data.color || 'transparent',
base_image = data.image || `${BASE_IMAGE}${badge_version}/${badge}${data.svg ? '.svg' : `/${version}/`}`,
trans = false,
@ -148,7 +148,7 @@ export function generateBadgeCSS(badge, version, data, style, is_dark, badge_ver
color = color_fixer.process(color) || color;
// TODO: Fix the click_url name once we actually support badge clicking.
return `${data.__click_url ? 'cursor:pointer;' : ''}${invert ? 'filter:invert(100%);' : ''}${CSS_TEMPLATES[style]({
return `${clickable && (data.click_url || data.click_action) ? 'cursor:pointer;' : ''}${invert ? 'filter:invert(100%);' : ''}${CSS_TEMPLATES[style]({
scale: 1,
color,
image,
@ -203,6 +203,16 @@ export default class Badges extends Module {
}
});
this.settings.add('chat.badges.clickable', {
default: true,
ui: {
path: 'Chat > Badges >> Behavior',
title: 'Allow clicking badges.',
description: 'Certain badges, such as Prime Gaming, act as links when this is enabled.',
component: 'setting-check-box'
}
});
this.settings.add('chat.badges.fix-colors', {
default: true,
ui: {
@ -250,6 +260,8 @@ export default class Badges extends Module {
]
}
});
this.handleClick = this.handleClick.bind(this);
}
getSettingsBadges(include_addons) {
@ -339,6 +351,7 @@ export default class Badges extends Module {
this.parent.context.on('changed:chat.badges.version', this.rebuildAllCSS, this);
this.parent.context.on('changed:chat.badges.media-queries', this.rebuildAllCSS, this);
this.parent.context.on('changed:chat.badges.fix-colors', this.rebuildColoredBadges, this);
this.parent.context.on('changed:chat.badges.clickable', this.rebuildAllCSS, this);
this.rebuildAllCSS();
this.loadGlobalBadges();
@ -434,6 +447,67 @@ export default class Badges extends Module {
}
handleClick(event) {
if ( ! this.parent.context.get('chat.badges.clickable') )
return;
const target = event.target;
let container = target.parentElement.parentElement;
if ( ! container.dataset.roomId )
container = target.closest('[data-room-id]');
const room_id = container.dataset.roomId,
room_login = container.dataset.room,
data = JSON.parse(target.dataset.badgeData);
if ( data == null )
return;
let url = null;
for(const d of data) {
const p = d.provider;
if ( p === 'twitch' ) {
const bd = this.getTwitchBadge(d.badge, d.version, room_id, room_login),
global_badge = this.getTwitchBadge(d.badge, d.version, null, null, true) || {};
if ( ! bd )
continue;
if ( bd.click_url )
url = bd.click_url;
else if ( global_badge.click_url )
url = global_badge.click_url;
else if ( (bd.click_action === 'sub' || global_badge.click_action === 'sub') && room_login )
url = `https://www.twitch.tv/subs/${room_login}`;
else
continue;
break;
} else if ( p === 'ffz' ) {
const badge = this.badges[target.dataset.badge];
if ( badge?.click_url ) {
url = badge.click_url;
break;
}
}
}
this.log.info('badge-click', event.target);
if ( url ) {
const link = createElement('a', {
target: '_blank',
rel: 'noopener noreferrer',
href: url
});
link.click();
}
event.preventDefault();
}
render(msg, createElement) { // eslint-disable-line class-methods-use-this
const hidden_badges = this.parent.context.get('chat.badges.hidden') || {},
badge_style = this.parent.context.get('chat.badges.style'),
@ -603,6 +677,8 @@ export default class Badges extends Module {
props['data-tooltip-type'] = 'badge';
props['data-badge-data'] = JSON.stringify(data.badges);
props.onClick = this.handleClick;
if ( data.replaced )
props['data-replaced'] = data.replaced;
@ -756,6 +832,7 @@ export default class Badges extends Module {
buildBadgeCSS() {
const style = this.parent.context.get('chat.badges.style'),
is_dark = this.parent.context.get('theme.is-dark'),
can_click = this.parent.context.get('chat.badges.clickable'),
use_media = IS_FIREFOX && this.parent.context.get('chat.badges.media-queries');
const out = [];
@ -767,11 +844,11 @@ export default class Badges extends Module {
out.push(`.ffz-badge[data-replaced="${key}"]{${generateOverrideCSS(data, style, is_dark)}}`);
if ( use_media ) {
out.push(`@media (max-resolution: 99dpi) {${selector}{${generateBadgeCSS(key, 0, data, style, is_dark, 0, this.color_fixer, 1)}}}`);
out.push(`@media (min-resolution: 100dpi) and (max-resolution:199dpi) {${selector}{${generateBadgeCSS(key, 0, data, style, is_dark, 0, this.color_fixer, 2)}}}`);
out.push(`@media (min-resolution: 200dpi) {${selector}{${generateBadgeCSS(key, 0, data, style, is_dark, 0, this.color_fixer, 4)}}}`);
out.push(`@media (max-resolution: 99dpi) {${selector}{${generateBadgeCSS(key, 0, data, style, is_dark, 0, this.color_fixer, 1, can_click)}}}`);
out.push(`@media (min-resolution: 100dpi) and (max-resolution:199dpi) {${selector}{${generateBadgeCSS(key, 0, data, style, is_dark, 0, this.color_fixer, 2, can_click)}}}`);
out.push(`@media (min-resolution: 200dpi) {${selector}{${generateBadgeCSS(key, 0, data, style, is_dark, 0, this.color_fixer, 4, can_click)}}}`);
} else
out.push(`${selector}{${generateBadgeCSS(key, 0, data, style, is_dark, 0, this.color_fixer)}}`);
out.push(`${selector}{${generateBadgeCSS(key, 0, data, style, is_dark, 0, this.color_fixer, undefined, can_click)}}`);
}
this.style.set('ext-badges', out.join('\n'));
@ -823,6 +900,8 @@ export default class Badges extends Module {
__cat: getBadgeCategory(sid)
};
fixBadgeData(data);
this.twitch_badge_count++;
bs[data.version] = data;
}
@ -832,32 +911,42 @@ export default class Badges extends Module {
}
this.buildTwitchBadgeCSS();
this.buildTwitchCSSBadgeCSS();
}
buildTwitchCSSBadgeCSS() {
const style = this.parent.context.get('chat.badges.style'),
is_dark = this.parent.context.get('theme.is-dark'),
can_click = this.parent.context.get('chat.badges.clickable'),
use_media = IS_FIREFOX && this.parent.context.get('chat.badges.media-queries'),
badge_version = this.parent.context.get('chat.badges.version'),
versioned = CSS_BADGES[badge_version] || {};
versioned = CSS_BADGES[badge_version] || {},
twitch_data = this.twitch_badges || {};
const out = [];
for(const key in versioned)
if ( has(versioned, key) ) {
const data = versioned[key];
const data = versioned[key],
twitch = twitch_data[key];
for(const version in data)
if ( has(data, version) ) {
const d = data[version],
td = twitch?.[version],
selector = `.ffz-badge[data-badge="${key}"][data-version="${version}"]`;
if ( td && td.click_url )
d.click_url = td.click_url;
if ( td && td.click_action )
d.click_action = td.click_action;
if ( use_media ) {
out.push(`@media (max-resolution: 99dpi) {${selector}{${generateBadgeCSS(key, version, d, style, is_dark, badge_version, this.color_fixer, 1)}}}`);
out.push(`@media (min-resolution: 100dpi) and (max-resolution:199dpi) {${selector}{${generateBadgeCSS(key, version, d, style, is_dark, badge_version, this.color_fixer, 2)}}}`);
out.push(`@media (min-resolution: 200dpi) {${selector}{${generateBadgeCSS(key, version, d, style, is_dark, badge_version, this.color_fixer, 4)}}}`);
out.push(`@media (max-resolution: 99dpi) {${selector}{${generateBadgeCSS(key, version, d, style, is_dark, badge_version, this.color_fixer, 1, can_click)}}}`);
out.push(`@media (min-resolution: 100dpi) and (max-resolution:199dpi) {${selector}{${generateBadgeCSS(key, version, d, style, is_dark, badge_version, this.color_fixer, 2, can_click)}}}`);
out.push(`@media (min-resolution: 200dpi) {${selector}{${generateBadgeCSS(key, version, d, style, is_dark, badge_version, this.color_fixer, 4, can_click)}}}`);
} else
out.push(`${selector}{${generateBadgeCSS(key, version, d, style, is_dark, badge_version, this.color_fixer)}}`);
out.push(`${selector}{${generateBadgeCSS(key, version, d, style, is_dark, badge_version, this.color_fixer, undefined, can_click)}}`);
}
}
@ -871,6 +960,7 @@ export default class Badges extends Module {
const badge_version = this.parent.context.get('chat.badges.version'),
use_media = IS_FIREFOX && this.parent.context.get('chat.badges.media-queries'),
can_click = this.parent.context.get('chat.badges.clickable'),
versioned = CSS_BADGES[badge_version] || {};
const out = [];
@ -886,6 +976,7 @@ export default class Badges extends Module {
selector = `.ffz-badge[data-badge="${key}"][data-version="${version}"]`;
out.push(`${selector} {
${can_click && (data.click_action || data.click_url) ? 'cursor:pointer;' : ''}
background-color: transparent;
filter: none;
${WEBKIT}mask-image: none;
@ -917,7 +1008,7 @@ export default class Badges extends Module {
}
function getBadgeCategory(key) {
export function getBadgeCategory(key) {
if ( key.startsWith('overwatch-league') )
return 'm-owl';
else if ( key.startsWith('twitchcon') )
@ -926,4 +1017,34 @@ function getBadgeCategory(key) {
return 'm-game';
return 'm-twitch';
}
export function fixBadgeData(badge) {
if ( ! badge )
return badge;
// Click Behavior
if ( badge.clickAction === 'VISIT_URL' && badge.clickURL )
badge.click_url = badge.clickURL;
if ( badge.clickAction === 'TURBO' )
badge.click_url = 'https://www.twitch.tv/products/turbo?ref=chat_badge';
if ( badge.clickAction === 'SUBSCRIBE' && badge.channelName )
badge.click_url = `https://www.twitch.tv/subs/${badge.channelName}`;
else if ( badge.clickAction )
badge.click_action = 'sub';
// Subscriber Tier
if ( badge.setID === 'subscriber' ) {
const id = parseInt(badge.version, 10);
if ( ! isNaN(id) && isFinite(id) ) {
badge.tier = (id - (id % 1000)) / 1000;
if ( badge.tier < 0 )
badge.tier = 0;
} else
badge.tier = 0;
}
return badge;
}

View file

@ -10,6 +10,7 @@ import {NEW_API, API_SERVER, WEBKIT_CSS as WEBKIT, IS_FIREFOX} from 'utilities/c
import {ManagedStyle} from 'utilities/dom';
import {has, SourcedSet, set_equals} from 'utilities/object';
import { getBadgeCategory, fixBadgeData } from './badges';
export default class Room {
@ -399,17 +400,11 @@ export default class Room {
const b = {};
for(const data of badges) {
const sid = data.setID,
bs = b[sid] = b[sid] || {};
bs = b[sid] = b[sid] || {
__cat: getBadgeCategory(sid)
};
if ( sid === 'subscriber' ) {
const id = parseInt(data.version, 10);
if ( ! isNaN(id) && isFinite(id) ) {
data.tier = (id - (id % 1000)) / 1000;
if ( data.tier < 0 )
data.tier = 0;
} else
data.tier = 0;
}
fixBadgeData(data);
bs[data.version] = data;
this.badge_count++;
@ -456,6 +451,7 @@ export default class Room {
return this.style.delete('badges');
const use_media = IS_FIREFOX && this.manager.context.get('chat.badges.media-queries'),
can_click = this.manager.context.get('chat.badges.clickable'),
out = [],
id = this.id;
@ -468,6 +464,7 @@ export default class Room {
selector = `[data-room-id="${id}"] .ffz-badge[data-badge="${key}"][data-version="${version}"]`;
out.push(`${selector} {
${can_click && (data.click_action || data.click_url) ? 'cursor:pointer;' : ''}
background-color: transparent;
filter: none;
${WEBKIT}mask-image: none;

View file

@ -0,0 +1,119 @@
<template lang="html">
<div class="ffz--term ffz--blocked-type">
<div class="tw-align-items-center tw-flex tw-flex-nowrap tw-flex-row tw-full-width">
<div class="tw-flex-grow-1 tw-mg-r-05">
<h4 v-if="! editing">
{{ display.v }}
</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"
>
<option
v-for="type in types"
:key="type"
:value="type"
>
{{ type }}
</option>
</select>
</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="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="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: {
types: 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;
}
},
methods: {
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

@ -0,0 +1,105 @@
<template lang="html">
<section class="ffz--widget ffz--blocked-types">
<blocked-type-editor
:term="default_type"
:types="data"
: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">
<blocked-type-editor
v-for="term in terms"
:key="term.id"
:term="term.v"
:types="data"
@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_type: {
v: 'Hosted'
}
}
},
computed: {
hasInheritance() {
for(const val of this.val)
if ( val.t === 'inherit' )
return true;
return false;
},
terms() {
const out = [];
if ( Array.isArray(this.val) )
for(const term of this.val)
if ( term && term.t !== 'inherit' )
out.push(term);
return out;
},
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

@ -1,11 +1,11 @@
<template lang="html">
<div v-if="item.contents" :class="classes">
<header v-if="! item.no_header">
<header v-if="! item.no_header" class="tw-font-size-5">
{{ t(item.i18n_key, item.title) }}
</header>
<section
v-if="item.description"
class="tw-pd-b-1"
class="tw-pd-b-1 tw-c-text-alt"
>
<markdown :source="t(item.desc_i18n_key, item.description)" />
</section>

View file

@ -60,7 +60,9 @@ export const array_merge = {
let trailing = [];
let had_value = false;
const profs = [...profiles, DEFAULT];
let profs = profiles;
if ( definition.inherit_default )
profs = [...profiles, DEFAULT];
for(const profile of profs) {
let value;

View file

@ -2287,7 +2287,7 @@ export default class EmoteMenu extends Module {
if ( nodes && nodes.length )
for(const node of nodes) {
const product = node.product,
const product = node && node.product,
set_id = product && product.emoteSetID;
if ( ! set_id )

View file

@ -68,6 +68,26 @@ const AUTOMOD_TYPES = make_enum(
'MessageModDenied'
);
const UNBLOCKABLE_TYPES = [
'Message',
'Notice',
'Moderation',
'ModerationAction',
'TargetedModerationAction',
'AutoMod',
'SubscriberOnlyMode',
'FollowerOnlyMode',
'SlowMode',
'EmoteOnlyMode',
'R9KMode',
'Connected',
'Disconnected',
'Reconnect',
'RoomMods',
'RoomState',
'BadgesUpdated'
]
const CHAT_TYPES = make_enum(
'Message',
'ExtensionMessage',
@ -250,6 +270,28 @@ export default class ChatHook extends Module {
// Settings
this.settings.add('chat.filtering.blocked-types', {
default: [],
type: 'array_merge',
always_inherit: true,
process(ctx, val) {
const out = new Set;
for(const v of val)
if ( v?.v || ! UNBLOCKABLE_TYPES.includes(v.v) )
out.add(v.v);
return out;
},
ui: {
path: 'Chat > Filtering >> Blocked Message Types @{"description":"This filter allows you to remove all messages of a certain type from Twitch chat. It can be used to filter system messages, such as Hosts or Raids. Some types, such as moderation actions, cannot be blocked to prevent chat functionality from breaking."}',
component: 'blocked-types',
data: () => Object
.keys(this.chat_types)
.filter(key => ! UNBLOCKABLE_TYPES.includes(key) && ! /^\d+$/.test(key))
}
});
this.settings.add('chat.replies.style', {
default: 1,
ui: {
@ -1113,21 +1155,24 @@ export default class ChatHook extends Module {
if ( ! reward )
return;
const msg = {
id: data.id,
type: this.chat_types.Message,
ffz_type: 'points',
ffz_reward: reward,
messageParts: [],
user: {
id: data.user.id,
login: data.user.login,
displayName: data.user.display_name
},
timestamp: new Date(message.data.timestamp || data.redeemed_at).getTime()
};
if ( ! this.chat.context.get('chat.filtering.blocked-types').has('ChannelPointsReward') ) {
const msg = {
id: data.id,
type: this.chat_types.Message,
ffz_type: 'points',
ffz_reward: reward,
messageParts: [],
user: {
id: data.user.id,
login: data.user.login,
displayName: data.user.display_name
},
timestamp: new Date(message.data.timestamp || data.redeemed_at).getTime()
};
service.postMessageToCurrentChannel({}, msg);
}
service.postMessageToCurrentChannel({}, msg);
event.preventDefault();
});
}
@ -1328,7 +1373,11 @@ export default class ChatHook extends Module {
if ( msg ) {
try {
const types = t.chat_types || {},
mod_types = t.mod_types || {};
mod_types = t.mod_types || {},
blocked_types = t.chat.context.get('chat.filtering.blocked-types');
if ( blocked_types.has(types[msg.type]) )
return;
if ( msg.type === types.RewardGift && ! t.chat.context.get('chat.bits.show-rewards') )
return;
@ -1776,7 +1825,7 @@ export default class ChatHook extends Module {
}
}
const old_chat = this.onChatMessageEvent;
/*const old_chat = this.onChatMessageEvent;
this.onChatMessageEvent = function(e) {
/*if ( e && e.sentByCurrentUser ) {
try {
@ -1788,7 +1837,7 @@ export default class ChatHook extends Module {
} catch(err) {
t.log.capture(err, {extra: e});
}
}*/
}* /
return old_chat.call(i, e);
}
@ -1806,15 +1855,18 @@ export default class ChatHook extends Module {
} catch(err) {
t.log.capture(err, {extra: e});
}
}*/
}* /
return old_action.call(i, e);
}
}*/
const old_sub = this.onSubscriptionEvent;
this.onSubscriptionEvent = function(e) {
try {
if ( t.chat.context.get('chat.filtering.blocked-types').has('Subscription') )
return;
if ( t.chat.context.get('chat.subs.show') < 3 )
return;
@ -1849,6 +1901,9 @@ export default class ChatHook extends Module {
const old_resub = this.onResubscriptionEvent;
this.onResubscriptionEvent = function(e) {
try {
if ( t.chat.context.get('chat.filtering.blocked-types').has('Resubscription') )
return;
if ( t.chat.context.get('chat.subs.show') < 2 && ! e.body )
return;
@ -1875,6 +1930,9 @@ export default class ChatHook extends Module {
const old_subgift = this.onSubscriptionGiftEvent;
this.onSubscriptionGiftEvent = function(e) {
try {
if ( t.chat.context.get('chat.filtering.blocked-types').has('SubGift') )
return;
const key = `${e.channel}:${e.user.userID}`,
mystery = mysteries[key];
@ -1922,6 +1980,9 @@ export default class ChatHook extends Module {
const old_anonsubgift = this.onAnonSubscriptionGiftEvent;
this.onAnonSubscriptionGiftEvent = function(e) {
try {
if ( t.chat.context.get('chat.filtering.blocked-types').has('AnonSubGift') )
return;
const key = `${e.channel}:ANON`,
mystery = mysteries[key];
@ -1970,6 +2031,9 @@ export default class ChatHook extends Module {
const old_submystery = this.onSubscriptionMysteryGiftEvent;
this.onSubscriptionMysteryGiftEvent = function(e) {
try {
if ( t.chat.context.get('chat.filtering.blocked-types').has('SubMysteryGift') )
return;
let mystery = null;
if ( e.massGiftCount > t.chat.context.get('chat.subs.merge-gifts') ) {
const key = `${e.channel}:${e.user.userID}`;
@ -2000,6 +2064,9 @@ export default class ChatHook extends Module {
const old_anonsubmystery = this.onAnonSubscriptionMysteryGiftEvent;
this.onAnonSubscriptionMysteryGiftEvent = function(e) {
try {
if ( t.chat.context.get('chat.filtering.blocked-types').has('AnonSubMysteryGift') )
return;
let mystery = null;
if ( e.massGiftCount > t.chat.context.get('chat.subs.merge-gifts') ) {
const key = `${e.channel}:ANON`;
@ -2031,6 +2098,9 @@ export default class ChatHook extends Module {
const old_ritual = this.onRitualEvent;
this.onRitualEvent = function(e) {
try {
if ( t.chat.context.get('chat.filtering.blocked-types').has('Ritual') )
return;
const out = i.convertMessage(e);
out.ffz_type = 'ritual';
out.ritual = e.type;
@ -2046,6 +2116,9 @@ export default class ChatHook extends Module {
const old_points = this.onChannelPointsRewardEvent;
this.onChannelPointsRewardEvent = function(e) {
try {
if ( t.chat.context.get('chat.filtering.blocked-types').has('ChannelPointsReward') )
return;
const reward = e.rewardID && get(e.rewardID, i.props.rewardMap);
if ( reward ) {
const out = i.convertMessage(e);

View file

@ -209,8 +209,10 @@ export class EventEmitter {
try {
ret = fn.apply(ctx, args);
} catch(err) {
if ( this.log )
if ( this.log ) {
this.log.capture(err, {tags: {event}, extra:{args}});
this.log.error(err);
}
}
if ( ret === Detach )