1
0
Fork 0
mirror of https://github.com/FrankerFaceZ/FrankerFaceZ.git synced 2025-07-25 12:08:30 +00:00
Nice.

* Changed: Warn users that they have `Show Mod Icons` disabled within [Chat > Actions](~chat.actions).
* Changed: Blocked Badges, Highlight Badges, and Blocked Types within [Chat > Filtering](~chat.filtering) no longer have a default item. This will hopefully minimize user confusion.
* Changed: Blocked Badges also has a new description telling users that it isn't for hiding badges, with a link to the correct place to change badge visibility.
* Changed: Remove the New Link Tokenization experiment, making it enabled for all users.
* Changed: When navigating within the FFZ Control Center in a pop-out window, update the URL so that it can be shared to link to a specific settings page.
* Changed: Disable the websocket connection for users in the API Links experiment to reduce load on the socket cluster.
* Fixed: Bug with the FFZ Control Center failing to load if experiments haven't been populated correctly.
* Fixed: Badge Visibility not being populated when opening the FFZ Control Center on a page without chat.
* API Added: `<markdown />` now supports a link syntax for navigating to a new section of the FFZ Control Center.
* API Fixed: Better tokenization for settings paths. Brackets can now be used safely in embedded JSON.
* API Fixed: `deep_equals` and `shallow_object_equals` returning false when objects were otherwise equal but had keys in a different order.
This commit is contained in:
SirStendec 2021-02-22 20:11:35 -05:00
parent f5135ad291
commit 77d6cf56d2
27 changed files with 365 additions and 96 deletions

View file

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

View file

@ -58,18 +58,17 @@ export default class ExperimentManager extends Module {
getExtraTerms: () => { getExtraTerms: () => {
const values = []; const values = [];
for(const [key,val] of Object.entries(this.experiments)) { for(const exps of [this.experiments, this.getTwitchExperiments()]) {
values.push(key); if ( ! exps )
if ( val.name ) continue;
values.push(val.name);
if ( val.description )
values.push(val.description);
}
for(const [key, val] of Object.entries(this.getTwitchExperiments())) { for(const [key, val] of Object.entries(exps)) {
values.push(key); values.push(key);
if ( val.name ) if ( val.name )
values.push(val.name); values.push(val.name);
if ( val.description )
values.push(val.description);
}
} }
return values; return values;

View file

@ -1,12 +1,4 @@
{ {
"new_links": {
"name": "New Link Tokenization",
"description": "Update to Twitch's latest link regex. Experiment while this is checked for bugs.",
"groups": [
{"value": true, "weight": 100},
{"value": false, "weight": 0}
]
},
"api_load": { "api_load": {
"name": "New API Stress Testing", "name": "New API Stress Testing",
"description": "Send duplicate requests to the new API server for load testing.", "description": "Send duplicate requests to the new API server for load testing.",

View file

@ -242,7 +242,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: () => this.getSettingsBadges(true) getBadges: cb => this.getSettingsBadges(true, cb)
} }
}); });
@ -276,7 +276,7 @@ export default class Badges extends Module {
this.handleClick = this.handleClick.bind(this); this.handleClick = this.handleClick.bind(this);
} }
getSettingsBadges(include_addons) { getSettingsBadges(include_addons, callback) {
const twitch = [], const twitch = [],
owl = [], owl = [],
tcon = [], tcon = [],
@ -284,6 +284,16 @@ export default class Badges extends Module {
ffz = [], ffz = [],
addon = []; addon = [];
const twitch_keys = Object.keys(this.twitch_badges);
if ( ! twitch_keys.length && callback ) {
const td = this.resolve('site.twitch_data');
if ( td )
td.getBadges().then(data => {
this.updateTwitchBadges(data);
callback();
});
}
for(const key in this.twitch_badges) for(const key in this.twitch_badges)
if ( has(this.twitch_badges, key) ) { if ( has(this.twitch_badges, key) ) {
const badge = this.twitch_badges[key], const badge = this.twitch_badges[key],
@ -334,6 +344,9 @@ export default class Badges extends Module {
const badge = this.badges[key], const badge = this.badges[key],
image = badge.urls ? (badge.urls[2] || badge.urls[1]) : badge.image; image = badge.urls ? (badge.urls[2] || badge.urls[1]) : badge.image;
if ( badge.no_visibility )
continue;
(badge.addon ? addon : ffz).push({ (badge.addon ? addon : ffz).push({
id: key, id: key,
provider: 'ffz', provider: 'ffz',
@ -1070,6 +1083,9 @@ export function fixBadgeData(badge) {
return badge; return badge;
// Click Behavior // Click Behavior
if ( ! badge.clickAction && badge.onClickAction )
badge.clickAction = badge.onClickAction;
if ( badge.clickAction === 'VISIT_URL' && badge.clickURL ) if ( badge.clickAction === 'VISIT_URL' && badge.clickURL )
badge.click_url = badge.clickURL; badge.click_url = badge.clickURL;

View file

@ -500,7 +500,7 @@ export default class Chat extends Module {
type: 'array_merge', type: 'array_merge',
always_inherit: true, always_inherit: true,
ui: { ui: {
path: 'Chat > Filtering >> Blocked Badges', path: 'Chat > Filtering >> Blocked Badges @{"description": "**Note:** This section is for filtering messages out of chat from users with specific badges. If you wish to hide a badge, go to [Chat > Badges >> Visibility](~chat.badges.tabs.visibility)."}',
component: 'badge-highlighting', component: 'badge-highlighting',
removable: true, removable: true,
data: () => this.badges.getSettingsBadges() data: () => this.badges.getSettingsBadges()

View file

@ -12,8 +12,8 @@ import {CATEGORIES} from './emoji';
const EMOTE_CLASS = 'chat-image chat-line__message--emote', const EMOTE_CLASS = 'chat-image chat-line__message--emote',
WHITESPACE = /^\s*$/, //WHITESPACE = /^\s*$/,
LINK_REGEX = /([^\w@#%\-+=:~])?((?:(https?:\/\/)?(?:[\w@#%\-+=:~]+\.)+[a-z]{2,6}(?:\/[\w./@#%&()\-+=:?~]*)?))([^\w./@#%&()\-+=:?~]|\s|$)/g, //LINK_REGEX = /([^\w@#%\-+=:~])?((?:(https?:\/\/)?(?:[\w@#%\-+=:~]+\.)+[a-z]{2,6}(?:\/[\w./@#%&()\-+=:?~]*)?))([^\w./@#%&()\-+=:?~]|\s|$)/g,
NEW_LINK_REGEX = /(?:(https?:\/\/)?((?:[\w#%\-+=:~]+\.)+[a-z]{2,10}(?:\/[\w./#%&@()\-+=:?~]*)?))/g, NEW_LINK_REGEX = /(?:(https?:\/\/)?((?:[\w#%\-+=:~]+\.)+[a-z]{2,10}(?:\/[\w./#%&@()\-+=:?~]*)?))/g,
//MENTION_REGEX = /([^\w@#%\-+=:~])?(@([^\u0000-\u007F]+|\w+)+)([^\w./@#%&()\-+=:?~]|\s|$)/g; // eslint-disable-line no-control-regex //MENTION_REGEX = /([^\w@#%\-+=:~])?(@([^\u0000-\u007F]+|\w+)+)([^\w./@#%&()\-+=:?~]|\s|$)/g; // eslint-disable-line no-control-regex
MENTION_REGEX = /^(['"*([{<\\/]*)(@)((?:[^\u0000-\u007F]|[\w-])+)(?:\b|$)/; // eslint-disable-line no-control-regex MENTION_REGEX = /^(['"*([{<\\/]*)(@)((?:[^\u0000-\u007F]|[\w-])+)(?:\b|$)/; // eslint-disable-line no-control-regex
@ -148,7 +148,7 @@ export const Links = {
if ( ! tokens || ! tokens.length ) if ( ! tokens || ! tokens.length )
return tokens; return tokens;
const use_new = this.experiments.getAssignment('new_links'); //const use_new = this.experiments.getAssignment('new_links');
const out = []; const out = [];
for(const token of tokens) { for(const token of tokens) {
@ -157,28 +157,28 @@ export const Links = {
continue; continue;
} }
LINK_REGEX.lastIndex = 0; //LINK_REGEX.lastIndex = 0;
NEW_LINK_REGEX.lastIndex = 0; NEW_LINK_REGEX.lastIndex = 0;
const text = token.text; const text = token.text;
let idx = 0, match; let idx = 0, match;
if ( use_new ) { //if ( use_new ) {
while((match = NEW_LINK_REGEX.exec(text))) { while((match = NEW_LINK_REGEX.exec(text))) {
const nix = match.index; const nix = match.index;
if ( idx !== nix ) if ( idx !== nix )
out.push({type: 'text', text: text.slice(idx, nix)}); out.push({type: 'text', text: text.slice(idx, nix)});
out.push({ out.push({
type: 'link', type: 'link',
url: `${match[1] ? '' : 'https://'}${match[0]}`, url: `${match[1] ? '' : 'https://'}${match[0]}`,
is_mail: false, is_mail: false,
text: match[0] text: match[0]
}); });
idx = nix + match[0].length; idx = nix + match[0].length;
} }
} else { /*} else {
while((match = LINK_REGEX.exec(text))) { while((match = LINK_REGEX.exec(text))) {
const nix = match.index + (match[1] ? match[1].length : 0); const nix = match.index + (match[1] ? match[1].length : 0);
if ( idx !== nix ) if ( idx !== nix )
@ -195,7 +195,7 @@ export const Links = {
idx = nix + match[2].length; idx = nix + match[2].length;
} }
} }*/
if ( idx < text.length ) if ( idx < text.length )
out.push({type: 'text', text: text.slice(idx)}); out.push({type: 'text', text: text.slice(idx)});

View file

@ -28,6 +28,7 @@
:key="addon.id" :key="addon.id"
:addon="addon" :addon="addon"
:item="item" :item="item"
:context="context"
@navigate="navigate" @navigate="navigate"
/> />
</div> </div>

View file

@ -124,7 +124,7 @@
<script> <script>
export default { export default {
props: ['id', 'addon', 'item'], props: ['id', 'addon', 'item', 'context'],
data() { data() {
return { return {

View file

@ -40,7 +40,7 @@ export default {
data() { data() {
return { return {
default_term: { default_term: {
v: 'broadcaster', v: '',
c: '', c: '',
remove: false remove: false
} }

View file

@ -7,6 +7,10 @@
:src="current.image" :src="current.image"
class="ffz--badge-term-image" class="ffz--badge-term-image"
> >
<div
v-else
class="ffz--badge-term-image"
/>
</div> </div>
<div class="tw-flex-grow-1 tw-mg-r-05"> <div class="tw-flex-grow-1 tw-mg-r-05">
<h4 v-if="! editing && ! current" class="ffz-monospace"> <h4 v-if="! editing && ! current" class="ffz-monospace">
@ -20,6 +24,9 @@
v-model="edit_data.v" v-model="edit_data.v"
class="tw-block tw-full-width tw-border-radius-medium tw-font-size-6 tw-full-width ffz-select tw-pd-x-1 tw-pd-y-05 tw-mg-y-05" class="tw-block tw-full-width tw-border-radius-medium tw-font-size-6 tw-full-width ffz-select tw-pd-x-1 tw-pd-y-05 tw-mg-y-05"
> >
<option v-if="adding" value="">
{{ t('setting.terms.please-select', 'Please select an option.') }}
</option>
<optgroup <optgroup
v-for="section in badges" v-for="section in badges"
:key="section.title" :key="section.title"
@ -69,14 +76,24 @@
</div> </div>
</div> </div>
<div v-if="adding" class="tw-flex-shrink-0"> <div v-if="adding" class="tw-flex-shrink-0">
<button class="tw-button" @click="save"> <button
class="tw-button"
:class="! valid && 'tw-button--disabled'"
:disabled="! valid"
@click="save"
>
<span class="tw-button__text"> <span class="tw-button__text">
{{ t('setting.terms.add-term', 'Add') }} {{ t('setting.terms.add-term', 'Add') }}
</span> </span>
</button> </button>
</div> </div>
<div v-else-if="editing" class="tw-flex-shrink-0"> <div v-else-if="editing" class="tw-flex-shrink-0">
<button class="tw-button tw-button--text tw-tooltip__container" @click="save"> <button
class="tw-button tw-button--text tw-tooltip__container"
:class="! valid && 'tw-button--disabled'"
:disabled="! valid"
@click="save"
>
<span class="tw-button__text ffz-i-floppy" /> <span class="tw-button__text ffz-i-floppy" />
<div class="tw-tooltip tw-tooltip--down tw-tooltip--align-right"> <div class="tw-tooltip tw-tooltip--down tw-tooltip--align-right">
{{ t('setting.save', 'Save') }} {{ t('setting.save', 'Save') }}
@ -163,6 +180,10 @@ export default {
}, },
computed: { computed: {
valid() {
return this.display.v && this.display.v !== '';
},
display() { display() {
return this.editing ? this.edit_data : this.term; return this.editing ? this.edit_data : this.term;
}, },
@ -207,7 +228,8 @@ export default {
}, },
save() { save() {
this.$emit('save', this.edit_data); if ( this.valid )
this.$emit('save', this.edit_data);
this.cancel(); this.cancel();
} }
} }

View file

@ -25,7 +25,7 @@
</div> </div>
<section <section
v-for="sec in data" v-for="sec in badges"
:key="sec.title" :key="sec.title"
class="ffz--menu-container tw-border-t" class="ffz--menu-container tw-border-t"
> >
@ -58,7 +58,7 @@
</header> </header>
<ul class="tw-flex tw-flex-wrap tw-align-content-start"> <ul class="tw-flex tw-flex-wrap tw-align-content-start">
<li <li
v-for="i in sort(sec.badges)" v-for="i in sec.badges"
:key="i.id" :key="i.id"
:class="{default: badgeDefault(i.id)}" :class="{default: badgeDefault(i.id)}"
class="ffz--badge-info tw-pd-y-1 tw-pd-r-1 tw-flex ffz-checkbox" class="ffz--badge-info tw-pd-y-1 tw-pd-r-1 tw-flex ffz-checkbox"
@ -115,11 +115,41 @@
import SettingMixin from '../setting-mixin'; import SettingMixin from '../setting-mixin';
import {has} from 'utilities/object'; import {has} from 'utilities/object';
function sortBadges(items) {
return items.sort((a, b) => {
const an = a.name.toLowerCase(),
bn = b.name.toLowerCase();
if ( an < bn ) return -1;
if ( an > bn ) return 1;
return 0;
});
}
export default { export default {
mixins: [SettingMixin], mixins: [SettingMixin],
props: ['item', 'context'], props: ['item', 'context'],
data() {
return {
badges: []
}
},
created() {
this.updateBadges();
},
methods: { methods: {
updateBadges() {
const badges = this.item.getBadges(() => this.updateBadges());
for(const section of badges) {
section.badges = sortBadges(section.badges);
}
this.badges = badges;
},
badgeChecked(id) { badgeChecked(id) {
return ! this.value[id]; return ! this.value[id];
}, },
@ -143,17 +173,6 @@ export default {
this.clear(); this.clear();
else else
this.set(val); this.set(val);
},
sort(items) {
return items.sort((a, b) => {
const an = a.name.toLowerCase(),
bn = b.name.toLowerCase();
if ( an < bn ) return -1;
if ( an > bn ) return 1;
return 0;
});
} }
} }
} }

View file

@ -10,6 +10,9 @@
v-model="edit_data.v" v-model="edit_data.v"
class="tw-block tw-full-width tw-border-radius-medium tw-font-size-6 tw-full-width ffz-select tw-pd-x-1 tw-pd-y-05 tw-mg-y-05" class="tw-block tw-full-width tw-border-radius-medium tw-font-size-6 tw-full-width ffz-select tw-pd-x-1 tw-pd-y-05 tw-mg-y-05"
> >
<option v-if="adding" value="">
{{ t('setting.terms.please-select', 'Please select an option.') }}
</option>
<option <option
v-for="type in types" v-for="type in types"
:key="type" :key="type"
@ -20,7 +23,12 @@
</select> </select>
</div> </div>
<div v-if="adding" class="tw-flex-shrink-0"> <div v-if="adding" class="tw-flex-shrink-0">
<button class="tw-button" @click="save"> <button
class="tw-button"
:class="! valid && 'tw-button--disabled'"
:disabled="! valid"
@click="save"
>
<span class="tw-button__text"> <span class="tw-button__text">
{{ t('setting.terms.add-term', 'Add') }} {{ t('setting.terms.add-term', 'Add') }}
</span> </span>
@ -94,6 +102,10 @@ export default {
}, },
computed: { computed: {
valid() {
return this.display.v && this.display.v !== '';
},
display() { display() {
return this.editing ? this.edit_data : this.term; return this.editing ? this.edit_data : this.term;
} }

View file

@ -36,7 +36,7 @@ export default {
data() { data() {
return { return {
default_type: { default_type: {
v: 'Hosted' v: ''
} }
} }
}, },

View file

@ -8,6 +8,14 @@
{{ t('setting.warn-inheritence', 'These values are being overridden by another profile and may not take effect.') }} {{ t('setting.warn-inheritence', 'These values are being overridden by another profile and may not take effect.') }}
</div> </div>
<div
v-if="(item.warn_icons || (has_icons && item.warn_icons !== false)) && context.mod_icons === false"
class="tw-c-background-accent tw-c-text-overlay tw-pd-1 tw-mg-b-1"
>
<span class="ffz-i-info" />
{{ t('setting.actions.warn-hidden', 'You currently have Mod Icons turned off in your Twitch chat settings, so some actions might be hidden as a result. Use the settings menu in chat to toggle them.') }}
</div>
<div class="tw-pd-b-1 tw-border-b tw-mg-b-1"> <div class="tw-pd-b-1 tw-border-b tw-mg-b-1">
<div class="tw-flex tw-flex-wrap tw-align-items-center ffz--inline"> <div class="tw-flex tw-flex-wrap tw-align-items-center ffz--inline">
{{ t('setting.actions.preview', 'Preview:') }} {{ t('setting.actions.preview', 'Preview:') }}

View file

@ -193,14 +193,24 @@ export default {
this.markSeen(item); this.markSeen(item);
if ( item.redirect )
return this.navigate(Array.isArray(item.redirect) ? item.redirect : item.redirect.split(/\./g));
this.currentItem = item; this.currentItem = item;
this.restoredItem = true; this.restoredItem = true;
let url;
if ( this.exclusive ) {
url = new URL(location.href);
url.searchParams.set('ffz-settings', item.full_key);
url = url.toString();
}
try { try {
window.history.replaceState({ window.history.replaceState({
...window.history.state, ...window.history.state,
ffzcc: item.full_key ffzcc: item.full_key
}, document.title) }, document.title, url);
} catch(err) { } catch(err) {
/* no-op */ /* no-op */
} }
@ -251,13 +261,34 @@ export default {
break; break;
} }
while(item && item.page) const tabs = [];
while(item && item.page) {
if ( item.tab && item.parent?.tabs )
tabs.push([item.parent, item.parent.tabs.indexOf(item)]);
item = item.parent; item = item.parent;
}
if ( ! item ) if ( ! item )
return; return;
this.changeItem(item); this.changeItem(item);
// Asynchronously walk down the tab tree, so that
// we can switch every tab correctly.
if ( tabs.length ) {
const bits = () => {
const latest = tabs.pop();
if ( latest?.[0]?._component )
latest[0]._component.select(latest[1]);
if ( tabs.length )
this.$nextTick(bits);
}
this.$nextTick(bits);
}
} }
} }
} }

View file

@ -1,6 +1,6 @@
<template lang="html"> <template lang="html">
<div class="ffz--provider tw-pd-t-05"> <div class="ffz--provider tw-pd-t-05">
<div v-if="not_www" class="tw-c-background-accent tw-c-text-overlay tw-pd-1 tw-mg-b-1"> <div v-if="not_www" class="ffz--notice tw-c-background-accent tw-c-text-overlay tw-pd-1 tw-mg-b-1">
<h3 class="ffz-i-attention"> <h3 class="ffz-i-attention">
{{ t('setting.provider.warn-domain.title', 'You\'re far from home!') }} {{ t('setting.provider.warn-domain.title', 'You\'re far from home!') }}
</h3> </h3>
@ -9,12 +9,12 @@
</div> </div>
</div> </div>
<div class="tw-c-background-accent tw-c-text-overlay tw-pd-1 tw-mg-b-1"> <div class="ffz--notice tw-c-background-accent tw-c-text-overlay tw-pd-1 tw-mg-b-1">
<h3 class="ffz-i-attention"> <h3 class="ffz-i-attention">
{{ t('setting.provider.warn.title', 'Be careful!') }} {{ t('setting.provider.warn.title', 'Be careful!') }}
</h3> </h3>
<div> <div>
<markdown :source="t('setting.provider.warn.desc', 'Please close any other Twitch tabs before using this tool. It is **recommended to create a backup** before changing your provider, in case anything happens.')" /> <markdown :source="t('setting.provider.warn.desc', 'Please close any other Twitch tabs before using this tool. It is **recommended to [create a backup](~data_management.backup_and_restore)** before changing your provider, in case anything happens.')" />
</div> </div>
</div> </div>

View file

@ -216,6 +216,15 @@ export default class MainMenu extends Module {
} }
mdNavigate(thing) {
const path = thing?.dataset?.settingsLink;
if ( ! path )
return;
this.requestPage(path);
}
updateContext(context) { updateContext(context) {
if ( ! context ) if ( ! context )
context = this._context; context = this._context;
@ -732,6 +741,7 @@ export default class MainMenu extends Module {
can_proxy: context._context.can_proxy, can_proxy: context._context.can_proxy,
proxied: context._context.proxied, proxied: context._context.proxied,
has_update: this.has_update, has_update: this.has_update,
mod_icons: context.get('context.chat.showModIcons'),
setProxied: val => { setProxied: val => {
this.use_context = val; this.use_context = val;
@ -828,8 +838,9 @@ export default class MainMenu extends Module {
const profiles = context.manager.__profiles, const profiles = context.manager.__profiles,
ids = this.profiles = context.__profiles.map(profile => profile.id); ids = this.profiles = context.__profiles.map(profile => profile.id);
this.proxied = this.context.proxied; _c.proxied = this.context.proxied;
this.can_proxy = this.context.can_proxy; _c.can_proxy = this.context.can_proxy;
_c.mod_icons = context.get('context.chat.showModIcons');
for(let i=0; i < profiles.length; i++) { for(let i=0; i < profiles.length; i++) {
const id = profiles[i].id, const id = profiles[i].id,

View file

@ -6,6 +6,7 @@
import Module from 'utilities/module'; import Module from 'utilities/module';
import {deep_equals, has, debounce, deep_copy} from 'utilities/object'; import {deep_equals, has, debounce, deep_copy} from 'utilities/object';
import {parse as new_parse} from 'utilities/path-parser';
import SettingsProfile from './profile'; import SettingsProfile from './profile';
import SettingsContext from './context'; import SettingsContext from './context';
@ -913,9 +914,14 @@ export default class SettingsManager extends Module {
} }
const PATH_SPLITTER = /(?:^|\s*([~>]+))\s*([^~>@]+)\s*(?:@([^~>]+))?/g;
export function parse_path(path) { export function parse_path(path) {
return new_parse(path);
}
/*const PATH_SPLITTER = /(?:^|\s*([~>]+))\s*([^~>@]+)\s*(?:@([^~>]+))?/g;
export function old_parse_path(path) {
const tokens = []; const tokens = [];
let match; let match;
@ -928,14 +934,20 @@ export function parse_path(path) {
opts = { key, title, page, tab }; opts = { key, title, page, tab };
if ( options ) if ( options ) {
Object.assign(opts, JSON.parse(options)); try {
Object.assign(opts, JSON.parse(options));
} catch(err) {
console.warn('Matched segment:', options);
throw err;
}
}
tokens.push(opts); tokens.push(opts);
} }
return tokens; return tokens;
} }*/
export function format_path_tokens(tokens) { export function format_path_tokens(tokens) {

View file

@ -29,7 +29,7 @@ export default class BitsButton extends Module {
ui: { ui: {
path: 'Appearance > Layout >> Top Navigation', path: 'Appearance > Layout >> Top Navigation',
title: 'Show the Get Bits button.', title: 'Show the Get Bits button.',
description: 'By default, this inherits its value from Chat > Bits and Cheering > Display Bits', description: 'By default, this inherits its value from [Chat > Bits and Cheering > Display Bits](~chat.bits_and_cheering)',
component: 'setting-check-box' component: 'setting-check-box'
}, },

View file

@ -298,7 +298,7 @@ export default class ChatHook extends Module {
ui: { ui: {
path: 'Chat > Appearance >> Replies', path: 'Chat > Appearance >> Replies',
title: 'Style', title: 'Style',
description: `Twitch's default style adds a floating button to the right and displays a notice above messages that are replies. FrankerFaceZ uses an In-Line Chat Action (that can be removed in Chat > Actions > In-Line) and uses an in-line mention to denote replies.`, description: `Twitch's default style adds a floating button to the right and displays a notice above messages that are replies. FrankerFaceZ uses an In-Line Chat Action (that can be removed in [Chat > Actions > In-Line](~chat.actions.in_line)) and uses an in-line mention to denote replies.`,
component: 'setting-select-box', component: 'setting-select-box',
data: [ data: [
{value: 0, title: 'Disabled'}, {value: 0, title: 'Disabled'},
@ -469,7 +469,7 @@ export default class ChatHook extends Module {
ui: { ui: {
path: 'Chat > Appearance >> Community', path: 'Chat > Appearance >> Community',
title: 'Display Leaderboard', title: 'Display Leaderboard',
description: 'The leaderboard shows the top cheerers and sub gifters in a channel.\n\nBy default due to a previous implementation, this inherits its value from Chat > Bits and Cheering > Display Bits.', description: 'The leaderboard shows the top cheerers and sub gifters in a channel.\n\nBy default due to a previous implementation, this inherits its value from [Chat > Bits and Cheering > Display Bits](~chat.bits_and_cheering).',
component: 'setting-check-box' component: 'setting-check-box'
} }
}); });

View file

@ -25,6 +25,7 @@ export default class SocketClient extends Module {
super(...args); super(...args);
this.inject('settings'); this.inject('settings');
this.inject('experiments');
this.settings.addUI('socket.info', { this.settings.addUI('socket.info', {
path: 'Debugging > Socket >> Info @{"sort": -1000}', path: 'Debugging > Socket >> Info @{"sort": -1000}',
@ -99,7 +100,14 @@ export default class SocketClient extends Module {
} }
onEnable() { this.connect() } onEnable() {
// For now, stop connecting to the sockets for people using the
// API links experiment.
if ( this.experiments.getAssignment('api_links') )
return;
this.connect();
}
onDisable() { this.disconnect() } onDisable() { this.disconnect() }

View file

@ -7,6 +7,99 @@
import MD from 'markdown-it'; import MD from 'markdown-it';
import MILA from 'markdown-it-link-attributes'; import MILA from 'markdown-it-link-attributes';
import {parse as parse_path} from 'utilities/path-parser';
let _md;
function getMD() {
if ( ! _md ) {
const md = _md = new MD({
html: false,
linkify: true
});
md.use(SettingsLinks);
md.use(MILA, {
attrs: {
class: 'ffz-tooltip',
target: '_blank',
rel: 'noopener',
'data-tooltip-type': 'link'
}
});
}
return _md;
}
function SettingsLinks(md) {
const default_render = md.renderer.rules.link_open || this.defaultRender;
md.renderer.rules.link_open = function(tokens, idx, options, env, self) {
const token = tokens[idx];
if ( token && token.type === 'link_open' && Array.isArray(token.attrs) ) {
let href;
for(const attr of token.attrs) {
if ( attr[0] === 'href' ) {
href = attr[1];
break;
}
}
if ( href.startsWith('~') ) {
let path;
if ( href === '~' ) {
// We don't have a path, make one from the bits.
let i = idx + 1;
let bits = [];
while(i < tokens.length) {
const tok = tokens[i],
type = tok?.type;
if ( type === 'text' )
bits.push(tok);
else if ( type === 'link_close' )
break;
i++;
}
bits = bits.map(x => x.content).join('');
const toks = parse_path(bits);
path = toks.map(x => x.key).join('.');
} else
path = href.slice(1);
if ( path && path.length ) {
for(const attr of token.attrs) {
if ( attr[0] === 'class' ) {
attr[1] = attr[1].replace(/ffz-tooltip/g, '');
break;
}
}
token.attrs.push([
'data-settings-link',
path
]);
token.attrs.push([
'onclick',
'FrankerFaceZ.get().resolve("main_menu").mdNavigate(this);return false'
]);
}
}
}
return default_render(tokens, idx, options, env, self);
}
}
SettingsLinks.defaultRender = function(tokens, idx, options, env, self) {
return self.renderToken(tokens, idx, options);
}
export default { export default {
props: { props: {
@ -15,21 +108,7 @@ export default {
computed: { computed: {
md() { md() {
const md = new MD({ return getMD();
html: false,
linkify: true
});
md.use(MILA, {
attrs: {
class: 'ffz-tooltip',
target: '_blank',
rel: 'noopener',
'data-tooltip-type': 'link'
}
});
return md;
}, },
output() { output() {

View file

@ -82,10 +82,20 @@ export default {
} }
}, },
created() {
if ( ! this.item._component )
this.item._component = this;
},
mounted() { mounted() {
this.markSeen() this.markSeen()
}, },
destroyed() {
if ( this.item._component === this )
this.item._component = null;
},
methods: { methods: {
focus() { focus() {
this.$el.querySelector('header').focus(); this.$el.querySelector('header').focus();

View file

@ -0,0 +1,13 @@
query FFZ_GlobalBadges {
badges {
id
clickURL
onClickAction
title
setID
version
image1x: imageURL(size: NORMAL)
image2x: imageURL(size: DOUBLE)
image4x: imageURL(size: QUADRUPLE)
}
}

View file

@ -197,7 +197,7 @@ export function deep_equals(object, other, ignore_undefined = false, seen, other
const source_keys = Object.keys(object), const source_keys = Object.keys(object),
dest_keys = Object.keys(other); dest_keys = Object.keys(other);
if ( ! ignore_undefined && ! array_equals(source_keys, dest_keys) ) if ( ! ignore_undefined && ! set_equals(new Set(source_keys), new Set(dest_keys)) )
return false; return false;
for(const key of source_keys) for(const key of source_keys)
@ -216,10 +216,14 @@ export function deep_equals(object, other, ignore_undefined = false, seen, other
export function shallow_object_equals(a, b) { export function shallow_object_equals(a, b) {
if ( typeof a !== 'object' || typeof b !== 'object' || ! array_equals(Object.keys(a), Object.keys(b)) ) if ( typeof a !== 'object' || typeof b !== 'object' )
return false; return false;
for(const key in a) const keys = Object.keys(a);
if ( ! set_equals(new Set(keys), new Set(Object.keys(b))) )
return false;
for(const key of keys)
if ( a[key] !== b[key] ) if ( a[key] !== b[key] )
return false; return false;

View file

@ -158,6 +158,19 @@ export default class TwitchData extends Module {
return this._search; return this._search;
} }
// ========================================================================
// Badges
// ========================================================================
async getBadges() {
const data = await this.queryApollo(
await import(/* webpackChunkName: 'queries' */ './data/global-badges.gql')
);
return get('data.badges', data);
}
// ======================================================================== // ========================================================================
// Categories // Categories
// ======================================================================== // ========================================================================

View file

@ -18,10 +18,29 @@
.tw-display-inline { display: inline !important } .tw-display-inline { display: inline !important }
.tw-width-auto { width: auto !important } .tw-width-auto { width: auto !important }
.ffz--notice {
a {
color: var(--color-text-overlay-link);
&:hover, &:hover:focus {
color: var(--color-text-overlay-link-hover);
}
&:focus {
color: var(--color-text-overlay-link-focus);
}
}
}
.ffz-unmatched-item { opacity: 0.25 } .ffz-unmatched-item { opacity: 0.25 }
.ffz-monospace { font-family: monospace } .ffz-monospace { font-family: monospace }
.ffz-bottom-100 { bottom: 100% } .ffz-bottom-100 { bottom: 100% }
.ffz--badge-term-image {
height: 3.6rem;
width: 3.6rem;
}
.ffz--autocomplete { .ffz--autocomplete {
.scrollable-area { .scrollable-area {
max-height: 20rem; max-height: 20rem;