diff --git a/.eslintrc.js b/.eslintrc.js index a3347c5c..d0218663 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -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' + } }; \ No newline at end of file diff --git a/package.json b/package.json index 73457907..6f2aae3e 100755 --- a/package.json +++ b/package.json @@ -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": { diff --git a/src/modules/chat/actions/index.jsx b/src/modules/chat/actions/index.jsx index 701bb6b4..b12cea47 100644 --- a/src/modules/chat/actions/index.jsx +++ b/src/modules/chat/actions/index.jsx @@ -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', diff --git a/src/modules/chat/badges.jsx b/src/modules/chat/badges.jsx index 85850e1e..b869dfac 100644 --- a/src/modules/chat/badges.jsx +++ b/src/modules/chat/badges.jsx @@ -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; } \ No newline at end of file diff --git a/src/modules/chat/room.js b/src/modules/chat/room.js index 698162e0..8ec27313 100644 --- a/src/modules/chat/room.js +++ b/src/modules/chat/room.js @@ -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; diff --git a/src/modules/main_menu/components/blocked-type-editor.vue b/src/modules/main_menu/components/blocked-type-editor.vue new file mode 100644 index 00000000..22e09e3b --- /dev/null +++ b/src/modules/main_menu/components/blocked-type-editor.vue @@ -0,0 +1,119 @@ + + + \ No newline at end of file diff --git a/src/modules/main_menu/components/blocked-types.vue b/src/modules/main_menu/components/blocked-types.vue new file mode 100644 index 00000000..41dec236 --- /dev/null +++ b/src/modules/main_menu/components/blocked-types.vue @@ -0,0 +1,105 @@ + + + \ No newline at end of file diff --git a/src/modules/main_menu/components/menu-container.vue b/src/modules/main_menu/components/menu-container.vue index 55a4212e..2999928a 100644 --- a/src/modules/main_menu/components/menu-container.vue +++ b/src/modules/main_menu/components/menu-container.vue @@ -1,11 +1,11 @@