mirror of
https://github.com/FrankerFaceZ/FrankerFaceZ.git
synced 2025-06-27 21:05:53 +00:00
4.20.69
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:
parent
f5135ad291
commit
77d6cf56d2
27 changed files with 365 additions and 96 deletions
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "frankerfacez",
|
||||
"author": "Dan Salvato LLC",
|
||||
"version": "4.20.68",
|
||||
"version": "4.20.69",
|
||||
"description": "FrankerFaceZ is a Twitch enhancement suite.",
|
||||
"license": "Apache-2.0",
|
||||
"scripts": {
|
||||
|
|
|
@ -58,18 +58,17 @@ export default class ExperimentManager extends Module {
|
|||
getExtraTerms: () => {
|
||||
const values = [];
|
||||
|
||||
for(const [key,val] of Object.entries(this.experiments)) {
|
||||
values.push(key);
|
||||
if ( val.name )
|
||||
values.push(val.name);
|
||||
if ( val.description )
|
||||
values.push(val.description);
|
||||
}
|
||||
for(const exps of [this.experiments, this.getTwitchExperiments()]) {
|
||||
if ( ! exps )
|
||||
continue;
|
||||
|
||||
for(const [key, val] of Object.entries(this.getTwitchExperiments())) {
|
||||
values.push(key);
|
||||
if ( val.name )
|
||||
values.push(val.name);
|
||||
for(const [key, val] of Object.entries(exps)) {
|
||||
values.push(key);
|
||||
if ( val.name )
|
||||
values.push(val.name);
|
||||
if ( val.description )
|
||||
values.push(val.description);
|
||||
}
|
||||
}
|
||||
|
||||
return values;
|
||||
|
|
|
@ -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": {
|
||||
"name": "New API Stress Testing",
|
||||
"description": "Send duplicate requests to the new API server for load testing.",
|
||||
|
|
|
@ -242,7 +242,7 @@ export default class Badges extends Module {
|
|||
path: 'Chat > Badges >> tabs ~> Visibility',
|
||||
title: '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);
|
||||
}
|
||||
|
||||
getSettingsBadges(include_addons) {
|
||||
getSettingsBadges(include_addons, callback) {
|
||||
const twitch = [],
|
||||
owl = [],
|
||||
tcon = [],
|
||||
|
@ -284,6 +284,16 @@ export default class Badges extends Module {
|
|||
ffz = [],
|
||||
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)
|
||||
if ( has(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],
|
||||
image = badge.urls ? (badge.urls[2] || badge.urls[1]) : badge.image;
|
||||
|
||||
if ( badge.no_visibility )
|
||||
continue;
|
||||
|
||||
(badge.addon ? addon : ffz).push({
|
||||
id: key,
|
||||
provider: 'ffz',
|
||||
|
@ -1070,6 +1083,9 @@ export function fixBadgeData(badge) {
|
|||
return badge;
|
||||
|
||||
// Click Behavior
|
||||
if ( ! badge.clickAction && badge.onClickAction )
|
||||
badge.clickAction = badge.onClickAction;
|
||||
|
||||
if ( badge.clickAction === 'VISIT_URL' && badge.clickURL )
|
||||
badge.click_url = badge.clickURL;
|
||||
|
||||
|
|
|
@ -500,7 +500,7 @@ export default class Chat extends Module {
|
|||
type: 'array_merge',
|
||||
always_inherit: true,
|
||||
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',
|
||||
removable: true,
|
||||
data: () => this.badges.getSettingsBadges()
|
||||
|
|
|
@ -12,8 +12,8 @@ import {CATEGORIES} from './emoji';
|
|||
|
||||
|
||||
const EMOTE_CLASS = 'chat-image chat-line__message--emote',
|
||||
WHITESPACE = /^\s*$/,
|
||||
LINK_REGEX = /([^\w@#%\-+=:~])?((?:(https?:\/\/)?(?:[\w@#%\-+=:~]+\.)+[a-z]{2,6}(?:\/[\w./@#%&()\-+=:?~]*)?))([^\w./@#%&()\-+=:?~]|\s|$)/g,
|
||||
//WHITESPACE = /^\s*$/,
|
||||
//LINK_REGEX = /([^\w@#%\-+=:~])?((?:(https?:\/\/)?(?:[\w@#%\-+=:~]+\.)+[a-z]{2,6}(?:\/[\w./@#%&()\-+=:?~]*)?))([^\w./@#%&()\-+=:?~]|\s|$)/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 = /^(['"*([{<\\/]*)(@)((?:[^\u0000-\u007F]|[\w-])+)(?:\b|$)/; // eslint-disable-line no-control-regex
|
||||
|
@ -148,7 +148,7 @@ export const Links = {
|
|||
if ( ! tokens || ! tokens.length )
|
||||
return tokens;
|
||||
|
||||
const use_new = this.experiments.getAssignment('new_links');
|
||||
//const use_new = this.experiments.getAssignment('new_links');
|
||||
|
||||
const out = [];
|
||||
for(const token of tokens) {
|
||||
|
@ -157,28 +157,28 @@ export const Links = {
|
|||
continue;
|
||||
}
|
||||
|
||||
LINK_REGEX.lastIndex = 0;
|
||||
//LINK_REGEX.lastIndex = 0;
|
||||
NEW_LINK_REGEX.lastIndex = 0;
|
||||
const text = token.text;
|
||||
let idx = 0, match;
|
||||
|
||||
if ( use_new ) {
|
||||
while((match = NEW_LINK_REGEX.exec(text))) {
|
||||
const nix = match.index;
|
||||
if ( idx !== nix )
|
||||
out.push({type: 'text', text: text.slice(idx, nix)});
|
||||
//if ( use_new ) {
|
||||
while((match = NEW_LINK_REGEX.exec(text))) {
|
||||
const nix = match.index;
|
||||
if ( idx !== nix )
|
||||
out.push({type: 'text', text: text.slice(idx, nix)});
|
||||
|
||||
out.push({
|
||||
type: 'link',
|
||||
url: `${match[1] ? '' : 'https://'}${match[0]}`,
|
||||
is_mail: false,
|
||||
text: match[0]
|
||||
});
|
||||
out.push({
|
||||
type: 'link',
|
||||
url: `${match[1] ? '' : 'https://'}${match[0]}`,
|
||||
is_mail: false,
|
||||
text: match[0]
|
||||
});
|
||||
|
||||
idx = nix + match[0].length;
|
||||
}
|
||||
idx = nix + match[0].length;
|
||||
}
|
||||
|
||||
} else {
|
||||
/*} else {
|
||||
while((match = LINK_REGEX.exec(text))) {
|
||||
const nix = match.index + (match[1] ? match[1].length : 0);
|
||||
if ( idx !== nix )
|
||||
|
@ -195,7 +195,7 @@ export const Links = {
|
|||
|
||||
idx = nix + match[2].length;
|
||||
}
|
||||
}
|
||||
}*/
|
||||
|
||||
if ( idx < text.length )
|
||||
out.push({type: 'text', text: text.slice(idx)});
|
||||
|
|
|
@ -28,6 +28,7 @@
|
|||
:key="addon.id"
|
||||
:addon="addon"
|
||||
:item="item"
|
||||
:context="context"
|
||||
@navigate="navigate"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -124,7 +124,7 @@
|
|||
<script>
|
||||
|
||||
export default {
|
||||
props: ['id', 'addon', 'item'],
|
||||
props: ['id', 'addon', 'item', 'context'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
|
|
|
@ -40,7 +40,7 @@ export default {
|
|||
data() {
|
||||
return {
|
||||
default_term: {
|
||||
v: 'broadcaster',
|
||||
v: '',
|
||||
c: '',
|
||||
remove: false
|
||||
}
|
||||
|
|
|
@ -7,6 +7,10 @@
|
|||
:src="current.image"
|
||||
class="ffz--badge-term-image"
|
||||
>
|
||||
<div
|
||||
v-else
|
||||
class="ffz--badge-term-image"
|
||||
/>
|
||||
</div>
|
||||
<div class="tw-flex-grow-1 tw-mg-r-05">
|
||||
<h4 v-if="! editing && ! current" class="ffz-monospace">
|
||||
|
@ -20,6 +24,9 @@
|
|||
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"
|
||||
>
|
||||
<option v-if="adding" value="">
|
||||
{{ t('setting.terms.please-select', 'Please select an option.') }}
|
||||
</option>
|
||||
<optgroup
|
||||
v-for="section in badges"
|
||||
:key="section.title"
|
||||
|
@ -69,14 +76,24 @@
|
|||
</div>
|
||||
</div>
|
||||
<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">
|
||||
{{ 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__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" />
|
||||
<div class="tw-tooltip tw-tooltip--down tw-tooltip--align-right">
|
||||
{{ t('setting.save', 'Save') }}
|
||||
|
@ -163,6 +180,10 @@ export default {
|
|||
},
|
||||
|
||||
computed: {
|
||||
valid() {
|
||||
return this.display.v && this.display.v !== '';
|
||||
},
|
||||
|
||||
display() {
|
||||
return this.editing ? this.edit_data : this.term;
|
||||
},
|
||||
|
@ -207,7 +228,8 @@ export default {
|
|||
},
|
||||
|
||||
save() {
|
||||
this.$emit('save', this.edit_data);
|
||||
if ( this.valid )
|
||||
this.$emit('save', this.edit_data);
|
||||
this.cancel();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
</div>
|
||||
|
||||
<section
|
||||
v-for="sec in data"
|
||||
v-for="sec in badges"
|
||||
:key="sec.title"
|
||||
class="ffz--menu-container tw-border-t"
|
||||
>
|
||||
|
@ -58,7 +58,7 @@
|
|||
</header>
|
||||
<ul class="tw-flex tw-flex-wrap tw-align-content-start">
|
||||
<li
|
||||
v-for="i in sort(sec.badges)"
|
||||
v-for="i in sec.badges"
|
||||
:key="i.id"
|
||||
:class="{default: badgeDefault(i.id)}"
|
||||
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 {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 {
|
||||
mixins: [SettingMixin],
|
||||
props: ['item', 'context'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
badges: []
|
||||
}
|
||||
},
|
||||
|
||||
created() {
|
||||
this.updateBadges();
|
||||
},
|
||||
|
||||
methods: {
|
||||
updateBadges() {
|
||||
const badges = this.item.getBadges(() => this.updateBadges());
|
||||
for(const section of badges) {
|
||||
section.badges = sortBadges(section.badges);
|
||||
}
|
||||
|
||||
this.badges = badges;
|
||||
},
|
||||
|
||||
badgeChecked(id) {
|
||||
return ! this.value[id];
|
||||
},
|
||||
|
@ -143,17 +173,6 @@ export default {
|
|||
this.clear();
|
||||
else
|
||||
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;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,6 +10,9 @@
|
|||
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"
|
||||
>
|
||||
<option v-if="adding" value="">
|
||||
{{ t('setting.terms.please-select', 'Please select an option.') }}
|
||||
</option>
|
||||
<option
|
||||
v-for="type in types"
|
||||
:key="type"
|
||||
|
@ -20,7 +23,12 @@
|
|||
</select>
|
||||
</div>
|
||||
<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">
|
||||
{{ t('setting.terms.add-term', 'Add') }}
|
||||
</span>
|
||||
|
@ -94,6 +102,10 @@ export default {
|
|||
},
|
||||
|
||||
computed: {
|
||||
valid() {
|
||||
return this.display.v && this.display.v !== '';
|
||||
},
|
||||
|
||||
display() {
|
||||
return this.editing ? this.edit_data : this.term;
|
||||
}
|
||||
|
|
|
@ -36,7 +36,7 @@ export default {
|
|||
data() {
|
||||
return {
|
||||
default_type: {
|
||||
v: 'Hosted'
|
||||
v: ''
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -8,6 +8,14 @@
|
|||
{{ t('setting.warn-inheritence', 'These values are being overridden by another profile and may not take effect.') }}
|
||||
</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-flex tw-flex-wrap tw-align-items-center ffz--inline">
|
||||
{{ t('setting.actions.preview', 'Preview:') }}
|
||||
|
|
|
@ -193,14 +193,24 @@ export default {
|
|||
|
||||
this.markSeen(item);
|
||||
|
||||
if ( item.redirect )
|
||||
return this.navigate(Array.isArray(item.redirect) ? item.redirect : item.redirect.split(/\./g));
|
||||
|
||||
this.currentItem = item;
|
||||
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 {
|
||||
window.history.replaceState({
|
||||
...window.history.state,
|
||||
ffzcc: item.full_key
|
||||
}, document.title)
|
||||
}, document.title, url);
|
||||
} catch(err) {
|
||||
/* no-op */
|
||||
}
|
||||
|
@ -251,13 +261,34 @@ export default {
|
|||
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;
|
||||
}
|
||||
|
||||
if ( ! item )
|
||||
return;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<template lang="html">
|
||||
<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">
|
||||
{{ t('setting.provider.warn-domain.title', 'You\'re far from home!') }}
|
||||
</h3>
|
||||
|
@ -9,12 +9,12 @@
|
|||
</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">
|
||||
{{ t('setting.provider.warn.title', 'Be careful!') }}
|
||||
</h3>
|
||||
<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>
|
||||
|
||||
|
|
|
@ -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) {
|
||||
if ( ! context )
|
||||
context = this._context;
|
||||
|
@ -732,6 +741,7 @@ export default class MainMenu extends Module {
|
|||
can_proxy: context._context.can_proxy,
|
||||
proxied: context._context.proxied,
|
||||
has_update: this.has_update,
|
||||
mod_icons: context.get('context.chat.showModIcons'),
|
||||
|
||||
setProxied: val => {
|
||||
this.use_context = val;
|
||||
|
@ -828,8 +838,9 @@ export default class MainMenu extends Module {
|
|||
const profiles = context.manager.__profiles,
|
||||
ids = this.profiles = context.__profiles.map(profile => profile.id);
|
||||
|
||||
this.proxied = this.context.proxied;
|
||||
this.can_proxy = this.context.can_proxy;
|
||||
_c.proxied = this.context.proxied;
|
||||
_c.can_proxy = this.context.can_proxy;
|
||||
_c.mod_icons = context.get('context.chat.showModIcons');
|
||||
|
||||
for(let i=0; i < profiles.length; i++) {
|
||||
const id = profiles[i].id,
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
import Module from 'utilities/module';
|
||||
import {deep_equals, has, debounce, deep_copy} from 'utilities/object';
|
||||
import {parse as new_parse} from 'utilities/path-parser';
|
||||
|
||||
import SettingsProfile from './profile';
|
||||
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) {
|
||||
return new_parse(path);
|
||||
}
|
||||
|
||||
|
||||
/*const PATH_SPLITTER = /(?:^|\s*([~>]+))\s*([^~>@]+)\s*(?:@([^~>]+))?/g;
|
||||
|
||||
export function old_parse_path(path) {
|
||||
const tokens = [];
|
||||
let match;
|
||||
|
||||
|
@ -928,14 +934,20 @@ export function parse_path(path) {
|
|||
|
||||
opts = { key, title, page, tab };
|
||||
|
||||
if ( options )
|
||||
Object.assign(opts, JSON.parse(options));
|
||||
if ( options ) {
|
||||
try {
|
||||
Object.assign(opts, JSON.parse(options));
|
||||
} catch(err) {
|
||||
console.warn('Matched segment:', options);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
tokens.push(opts);
|
||||
}
|
||||
|
||||
return tokens;
|
||||
}
|
||||
}*/
|
||||
|
||||
|
||||
export function format_path_tokens(tokens) {
|
||||
|
|
|
@ -29,7 +29,7 @@ export default class BitsButton extends Module {
|
|||
ui: {
|
||||
path: 'Appearance > Layout >> Top Navigation',
|
||||
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'
|
||||
},
|
||||
|
||||
|
|
|
@ -298,7 +298,7 @@ export default class ChatHook extends Module {
|
|||
ui: {
|
||||
path: 'Chat > Appearance >> Replies',
|
||||
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',
|
||||
data: [
|
||||
{value: 0, title: 'Disabled'},
|
||||
|
@ -469,7 +469,7 @@ export default class ChatHook extends Module {
|
|||
ui: {
|
||||
path: 'Chat > Appearance >> Community',
|
||||
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'
|
||||
}
|
||||
});
|
||||
|
|
|
@ -25,6 +25,7 @@ export default class SocketClient extends Module {
|
|||
super(...args);
|
||||
|
||||
this.inject('settings');
|
||||
this.inject('experiments');
|
||||
|
||||
this.settings.addUI('socket.info', {
|
||||
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() }
|
||||
|
||||
|
||||
|
|
|
@ -7,6 +7,99 @@
|
|||
|
||||
import MD from 'markdown-it';
|
||||
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 {
|
||||
props: {
|
||||
|
@ -15,21 +108,7 @@ export default {
|
|||
|
||||
computed: {
|
||||
md() {
|
||||
const md = new MD({
|
||||
html: false,
|
||||
linkify: true
|
||||
});
|
||||
|
||||
md.use(MILA, {
|
||||
attrs: {
|
||||
class: 'ffz-tooltip',
|
||||
target: '_blank',
|
||||
rel: 'noopener',
|
||||
'data-tooltip-type': 'link'
|
||||
}
|
||||
});
|
||||
|
||||
return md;
|
||||
return getMD();
|
||||
},
|
||||
|
||||
output() {
|
||||
|
|
|
@ -82,10 +82,20 @@ export default {
|
|||
}
|
||||
},
|
||||
|
||||
created() {
|
||||
if ( ! this.item._component )
|
||||
this.item._component = this;
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.markSeen()
|
||||
},
|
||||
|
||||
destroyed() {
|
||||
if ( this.item._component === this )
|
||||
this.item._component = null;
|
||||
},
|
||||
|
||||
methods: {
|
||||
focus() {
|
||||
this.$el.querySelector('header').focus();
|
||||
|
|
13
src/utilities/data/global-badges.gql
Normal file
13
src/utilities/data/global-badges.gql
Normal 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)
|
||||
}
|
||||
}
|
|
@ -197,7 +197,7 @@ export function deep_equals(object, other, ignore_undefined = false, seen, other
|
|||
const source_keys = Object.keys(object),
|
||||
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;
|
||||
|
||||
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) {
|
||||
if ( typeof a !== 'object' || typeof b !== 'object' || ! array_equals(Object.keys(a), Object.keys(b)) )
|
||||
if ( typeof a !== 'object' || typeof b !== 'object' )
|
||||
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] )
|
||||
return false;
|
||||
|
||||
|
|
|
@ -158,6 +158,19 @@ export default class TwitchData extends Module {
|
|||
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
|
||||
// ========================================================================
|
||||
|
|
|
@ -18,10 +18,29 @@
|
|||
.tw-display-inline { display: inline !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-monospace { font-family: monospace }
|
||||
.ffz-bottom-100 { bottom: 100% }
|
||||
|
||||
.ffz--badge-term-image {
|
||||
height: 3.6rem;
|
||||
width: 3.6rem;
|
||||
}
|
||||
|
||||
.ffz--autocomplete {
|
||||
.scrollable-area {
|
||||
max-height: 20rem;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue