1
0
Fork 0
mirror of https://github.com/FrankerFaceZ/FrankerFaceZ.git synced 2025-06-27 12:55:55 +00:00
* Added: When searching in the FFZ Control Center, you can now use the tag `@modified` to filter by settings that have been changed in the current profile.
* Added: Add-ons now have Changelog buttons that navigate to a changelog showing only entries for that add-on.
* Changed: The Changelog pages now has nicer formatting for each commit, including add-on icons and clickable links to add-on sub-pages when viewing the Add-on Changelog.
* API Added: To prevent FrankerFaceZ from loading into a page, include `disable_frankerfacez` in the URL query parameters.
* Experiment Changed: Fix incorrect roll-out percentage for API-Based Link Lookups. This should be fully enabled.
* Experiment Changed: Lower the percentage of users in the MQTT-Based PubSub experiment.
This commit is contained in:
SirStendec 2023-11-06 20:47:19 -05:00
parent 5046088bf7
commit ba72969c51
17 changed files with 416 additions and 72 deletions

View file

@ -1,7 +1,7 @@
{
"name": "frankerfacez",
"author": "Dan Salvato LLC",
"version": "4.59.1",
"version": "4.60.0",
"description": "FrankerFaceZ is a Twitch enhancement suite.",
"private": true,
"license": "Apache-2.0",

View file

@ -7,7 +7,7 @@
import Module from 'utilities/module';
import { EXTENSION, SERVER_OR_EXT } from 'utilities/constants';
import { createElement } from 'utilities/dom';
import { timeout, has } from 'utilities/object';
import { timeout, has, deep_copy } from 'utilities/object';
import { getBuster } from 'utilities/time';
const fetchJSON = (url, options) => fetch(url, options).then(r => r.ok ? r.json() : null).catch(() => null);
@ -51,6 +51,7 @@ export default class AddonManager extends Module {
getExtraTerms: () => Object.values(this.addons).map(addon => addon.search_terms),
getFFZ: () => this,
isReady: () => this.enabled,
getAddons: () => Object.values(this.addons),
hasAddon: id => this.hasAddon(id),
@ -216,6 +217,16 @@ export default class AddonManager extends Module {
this.addons[id] = [addon.id];
}
if ( ! old )
this.settings.addUI(`addon-changelog.${addon.id}`, {
path: `Add-Ons > Changelog > ${addon.name}`,
component: 'changelog',
force_seen: true,
addons: true,
addon: deep_copy(addon),
getFFZ: () => this
});
this.emit(':added');
}

View file

@ -5,6 +5,9 @@
if ( /^(?:localhost\.rig|blog|im|chatdepot|tmi|api|brand|dev|gql|passport)\./.test(location.hostname) )
return;
if ( /disable_frankerfacez/.test(location.search) )
return;
const DEBUG = localStorage.ffzDebugMode == 'true' && document.body.classList.contains('ffz-dev'),
HOST = location.hostname,
SERVER = DEBUG ? '//localhost:8000' : '//cdn.frankerfacez.com',

View file

@ -5,6 +5,9 @@
if ( /^(?:localhost\.rig|blog|im|chatdepot|tmi|api|brand|dev|gql|passport)\./.test(location.hostname) )
return;
if ( /disable_frankerfacez/.test(location.search) )
return;
const browser = globalThis.browser ?? globalThis.chrome,
HOST = location.hostname,

View file

@ -11,17 +11,17 @@
"name": "API-Based Link Lookups",
"description": "Use the new API to look up links instead of the socket cluster.",
"groups": [
{"value": true, "weight": 30},
{"value": "cf", "weight": 10},
{"value": false, "weight": 60}
{"value": true, "weight": 0},
{"value": "cf", "weight": 100},
{"value": false, "weight": 0}
]
},
"cf_pubsub": {
"name": "MQTT-Based PubSub",
"description": "An experimental new pubsub system that should be more reliable than the existing socket cluster.",
"groups": [
{"value": true, "weight": 10},
{"value": false, "weight": 90}
{"value": true, "weight": 5},
{"value": false, "weight": 95}
]
}
}

View file

@ -60,6 +60,7 @@
:item="item"
:context="context"
@navigate="navigate"
@change-item="changeItem"
/>
</div>
@ -68,7 +69,7 @@
visible: visible_addons.length,
total: listed_addons.length
}) }}
<template v-if="filter && filter.length">
<template v-if="filter">
{{ t('addon.displaying.filtered', 'The visible add-ons are being filtered by your search. Clear it to view all available add-ons.') }}
</template>
</div>
@ -222,10 +223,14 @@ export default {
if ( this.filter_enabled && ! enabled )
return false;
if ( ! this.filter || ! this.filter.length )
return true;
if ( this.filter ) {
if ( this.filter.query ) {
if ( ! addon.search_terms || ! addon.search_terms.includes(this.filter.query) )
return false;
}
}
return addon.search_terms.includes(this.filter)
return true;
},
onAdded() {
@ -242,6 +247,10 @@ export default {
navigate(...args) {
this.$emit('navigate', ...args);
},
changeItem(...args) {
this.$emit('change-item', ...args);
}
}
}

View file

@ -145,6 +145,18 @@
</span>
</button>
</template>
<button
v-if="! external"
class="tw-button ffz-button--hollow tw-mg-r-1"
@click="openChangelog"
>
<span class="tw-button__icon tw-button__icon--left">
<figure class="ffz-i-doc-text" />
</span>
<span class="tw-button__text">
{{ t('addon.changelog', 'Changelog') }}
</span>
</button>
<a
v-if="addon.website"
:href="addon.website"
@ -285,6 +297,16 @@ export default {
this.$emit('navigate', ...list);
},
openChangelog() {
const list = [`add_ons.changelog.${this.id}`];
if ( this.addon.short_name )
list.push(`add_ons.changelog.${this.addon.short_name.toSnakeCase()}`);
if ( this.addon.name )
list.push(`add_ons.changelog.${this.addon.name.toSnakeCase()}`);
this.$emit('navigate', ...list);
},
reloadAddon() {
this.reloading = true;
this.item.reloadAddon(this.id)

View file

@ -1,12 +1,26 @@
<template lang="html">
<div class="ffz--changelog tw-border-t tw-pd-t-1">
<div class="tw-align-center">
<h2 v-if="addons">
<h3 v-if="addon" class="tw-mg-b-1 tw-flex tw-align-items-center tw-justify-content-center">
<figure
v-if="addon.icon"
class="ffz-avatar ffz-avatar--size-30 tw-mg-r-05"
>
<img
:src="addon.icon"
class="tw-block tw-image tw-image-avatar"
>
</figure>
{{ t('setting.add_ons.specific-changelog', '{name} Changelog', {
name: addon.name
}) }}
</h3>
<h3 v-else-if="addons" class="tw-mg-b-1">
{{ t('setting.add_ons.changelog.title', 'Add-Ons Changelog') }}
</h2>
<h2 v-else>
</h3>
<h3 v-else class="tw-mg-b-1">
{{ t('home.changelog', 'Changelog') }}
</h2>
</h3>
</div>
<div v-if=" ! addons" class="tw-mg-b-1 tw-flex tw-align-items-center">
@ -38,10 +52,30 @@
<div v-if="! addons && commit.active" class="ffz-pill tw-mg-r-05">
{{ t('home.changelog.current', 'Current Version') }}
</div>
<div v-if="commit.title" class="tw-font-size-4">
<figure
v-if="commit.icon"
class="ffz-avatar ffz-avatar--size-20 tw-mg-r-05"
>
<img
:src="commit.icon"
class="tw-block tw-image tw-image-avatar"
>
</figure>
<a
v-if="commit.title && commit.title_nav"
class="tw-font-size-5 ffz-link ffz-link--inherit"
href="#"
@click.prevent="titleNav(commit.title_nav)"
>
{{ commit.title }}
</a>
<div v-else-if="commit.title" class="tw-font-size-5">
{{ commit.title }}
</div>
<div v-if="commit.author">
<div v-if="commit.version" class="tw-font-size-4 tw-mg-l-05">
<span class="tw-c-text-alt-2">v</span>{{ commit.version }}
</div>
<div v-if="commit.author" class="tw-mg-l-05">
<t-list
phrase="home.changelog.by-line"
default="By: {user}"
@ -86,6 +120,10 @@
>({{ formatDate(commit.date) }})</time>
</div>
<markdown :source="commit.message" />
<div v-for="entry in commit.segments" class="ffz--changelog-segment">
<strong>{{ entry.key }}</strong>
<markdown :source="entry.value" />
</div>
</li>
</ul>
@ -111,7 +149,21 @@
import {get} from 'utilities/object';
const TITLE_MATCH = /^v?(\d+\.\d+\.\d+(?:-[^\n]+)?)\n+/;
const TITLE_MATCH = /^(.+?)?\s*v?(\d+\.\d+\.\d+(?:\-[a-z0-9-]+)?)$/i,
SETTING_REGEX = /\]\(~([^)]+)\)/g,
CHANGE_REGEX = /^\*\s*([^:]+?):\s*(.+)$/i,
ISSUE_REGEX = /(^|\s)#(\d+)\b/g;
function linkify(text, repo) {
text = text.replace(SETTING_REGEX, (_, link) => {
return `](~${link})`
});
return text.replace(ISSUE_REGEX, (_, space, number) => {
return `${space}[#${number}](https://github.com/FrankerFaceZ/${repo}/issues/${number})`;
});
}
export default {
@ -120,6 +172,7 @@ export default {
data() {
return {
error: false,
addon: this.item.addon,
addons: this.item.addons,
nonversioned: false,
loading: false,
@ -130,35 +183,130 @@ export default {
computed: {
display() {
window.thing = this;
const out = [],
addons = this.addons ? this.item.getFFZ().resolve('addons') : null,
old_commit = this.t('home.changelog.nonversioned', 'Non-Versioned Commit');
for(const commit of this.commits) {
let message = commit.commit.message,
const input = commit.commit.message;
let title = old_commit,
title_nav = null,
icon = null,
version = null,
author = null,
title = old_commit;
sections = {},
description = [];
if ( this.addons ) {
title = null;
author = commit.author;
if ( /\bskiplog\b/i.test(input) && ! this.nonversion )
continue;
} else {
const match = TITLE_MATCH.exec(message);
if ( match ) {
title = match[1];
message = message.slice(match[0].length);
} else if ( ! this.nonversioned )
continue;
}
const lines = input.split(/\r?\n/),
first = lines.shift(),
match = first ? TITLE_MATCH.exec(first) : null;
const date = new Date(commit.commit.author.date),
active = commit.sha === window.FrankerFaceZ.version_info.commit;
active = commit.sha === window.FrankerFaceZ.version_info.commit,
has_content = lines.length && match;
if ( ! this.nonversion && ! has_content )
continue;
let last_bit = null;
if ( match ) {
title = match[1];
version = match[2];
}
if ( has_content )
for(const line of lines) {
const trimmed = line.trim();
if ( ! trimmed.length ) {
if ( ! last_bit && description.length )
description.push(line);
continue;
}
const m = CHANGE_REGEX.exec(trimmed);
if ( ! m ) {
if ( ! last_bit )
description.push(line);
else
last_bit.push(trimmed);
} else {
const section = sections[m[1]] = sections[m[1]] || [];
last_bit = [m[2]];
section.push(last_bit);
}
}
else {
lines.unshift(first);
description = lines;
}
let message = description.join('\n').trim();
const segments = [];
for(const [key, val] of Object.entries(sections)) {
if ( ! val?.length )
continue;
const bit = val.map(x => `* ${x.join(' ')}`).join('\n').trim();
segments.push({
key,
value: linkify(bit, this.addons ? 'add-ons' : 'frankerfacez')
});
}
if ( this.addons ) {
author = commit.author;
if ( title ) {
const ltitle = title.toLowerCase();
if ( addons?.addons )
for(const addon of Object.values(addons.addons)) {
if ((addon.short_name && addon.short_name.toLowerCase() === ltitle) ||
(addon.name && addon.name.toLowerCase() === ltitle) ||
(addon.id && addon.id.toLowerCase() === ltitle)
) {
icon = addon.icon;
title_nav = [`add_ons.changelog.${addon.id}`];
if ( addon.short_name )
title_nav.push(`add_ons.changelog.${addon.short_name.toSnakeCase()}`);
if ( addon.name )
title_nav.push(`add_ons.changelog.${addon.name.toSnakeCase()}`);
break;
}
}
// Default Icon
if ( ! icon )
icon = 'https://cdn.frankerfacez.com/badge/2/4/solid';
}
}
if ( this.addon ) {
icon = null;
title = null;
}
out.push({
icon,
title,
title_nav,
version,
author,
message,
segments,
active,
hash: commit.sha && commit.sha.slice(0,7),
link: commit.html_url,
@ -177,6 +325,11 @@ export default {
},
methods: {
titleNav(nav) {
if ( Array.isArray(nav) )
this.$emit('navigate', ...nav);
},
formatDate(value) {
if ( ! value )
return '';
@ -197,8 +350,15 @@ export default {
this.loading = true;
const url = new URL(`https://api.github.com/repos/frankerfacez/${this.addons ? 'add-ons' : 'frankerfacez'}/commits`);
if ( until )
url.searchParams.append('until', until);
if ( this.addon )
url.searchParams.append('path', `src/${this.addon.id}`);
try {
const resp = await fetch(`https://api.github.com/repos/frankerfacez/${this.addons ? 'add-ons' : 'frankerfacez'}/commits${until ? `?until=${until}` : ''}`),
const resp = await fetch(url),
data = resp.ok ? await resp.json() : null;
if ( ! data || ! Array.isArray(data) ) {

View file

@ -252,12 +252,15 @@ export default {
},
visible_ffz() {
const items = this.sorted_ffz,
f = this.filter && this.filter.toLowerCase();
if ( ! f )
return items;
let out = this.sorted_ffz;
return items.filter(x => matches(x, f));
if ( this.filter?.query )
out = out.filter(x => matches(x, this.filter.query));
if ( this.filter?.flags && this.filter.flags.has('modified') )
out = out.filter(x => this.item.hasOverride(x.key));
return out;
},
sorted_twitch() {
@ -265,12 +268,15 @@ export default {
},
visible_twitch() {
const items = this.sorted_twitch,
f = this.filter && this.filter.toLowerCase();
if ( ! f )
return items;
let out = this.sorted_twitch;
return items.filter(x => matches(x, f));
if ( this.filter?.query )
out = out.filter(x => matches(x, this.filter.query));
if ( this.filter?.flags && this.filter.flags.has('modified') )
out = out.filter(x => this.item.hasTwitchOverride(x.key));
return out;
}
},

View file

@ -79,6 +79,7 @@
:current-item="currentItem"
:modal="nav"
:filter="filter"
:context="context"
@change-item="changeItem"
@mark-seen="markSeen"
@mark-expanded="markExpanded"
@ -128,6 +129,10 @@
import displace from 'displacejs';
import { getDialogNextZ } from 'src/utilities/dialog';
const VALID_FLAGS = [
'modified'
];
export default {
data() {
const out = this.$vnode.data;
@ -139,7 +144,32 @@ export default {
computed: {
filter() {
return this.query.toLowerCase()
let query = this.query.toLowerCase();
let flags = new Set;
query = query.replace(/(?<=^|\s)@(\S+)(?:\s+|$)/g, (match, flag, index) => {
if ( VALID_FLAGS.includes(flag) ) {
flags.add(flag);
return '';
}
return match;
});
query = query.trim();
if ( ! query.length )
query = null;
if ( ! flags.size )
flags = null;
if ( ! query && ! flags )
return null;
return {
flags,
query
}
}
},

View file

@ -57,11 +57,32 @@ export default {
this.$emit('navigate', ...args);
},
shouldShow(item) {
if ( ! this.filter || ! this.filter.length || ! item.search_terms )
shouldShow(item, is_walking = false) {
if ( ! this.filter || item.no_filter )
return true;
return item.search_terms.includes(this.filter);
if ( this.filter.flags ) {
if ( this.filter.flags.has('modified') ) {
// We need to tree walk for this one.
if ( ! is_walking ) {
for(const key of ['tabs', 'contents', 'items'])
if ( item[key] )
for(const thing of item[key])
if ( this.shouldShow(thing) )
return true;
}
if ( ! item.setting || ! this.context.currentProfile.has(item.setting) )
return false;
}
}
if ( this.filter.query ) {
if ( ! item.search_terms || ! item.search_terms.includes(this.filter.query) )
return false;
}
return true;
}
}
}

View file

@ -149,15 +149,36 @@ export default {
},
methods: {
shouldShow(item) {
if ( ! this.filter || ! this.filter.length || ! item.search_terms )
shouldShow(item, is_walking = false) {
if ( ! this.filter || item.no_filter )
return true;
return item.no_filter || item.search_terms.includes(this.filter);
if ( this.filter.flags ) {
if ( this.filter.flags.has('modified') ) {
// We need to tree walk for this one.
if ( ! is_walking ) {
for(const key of ['tabs', 'contents', 'items'])
if ( item[key] )
for(const thing of item[key])
if ( this.shouldShow(thing) )
return true;
}
if ( ! item.setting || ! this.context.currentProfile.has(item.setting) )
return false;
}
}
if ( this.filter.query ) {
if ( ! item.search_terms || ! item.search_terms.includes(this.filter.query) )
return false;
}
return true;
},
countMatches(item, seen) {
if ( ! this.filter || ! this.filter.length || ! item )
if ( ! this.filter || ! item )
return 0;
if ( seen && seen.has(item) )
@ -175,7 +196,7 @@ export default {
for(const thing of item[key])
count += this.countMatches(thing, seen);
if ( item.setting && item.search_terms && item.search_terms.includes(this.filter) )
if ( item.setting && this.shouldShow(item, true) )
count++;
return count;

View file

@ -13,7 +13,7 @@
<li
v-for="item in displayed"
:key="item.full_key"
:class="[currentItem === item ? 'active' : '']"
:class="[(currentItem === item || item.hide_children && containsCurrent(item)) ? 'active' : '']"
:data-key="item.full_key"
role="presentation"
>
@ -26,7 +26,7 @@
>
<span
:class="[
item.items ? '' : 'ffz--invisible',
(item.items && ! item.hide_children) ? '' : 'ffz--invisible',
item.expanded ? 'ffz-i-down-dir' : 'ffz-i-right-dir'
]"
role="presentation"
@ -46,10 +46,11 @@
</span>
</div>
<menu-tree
v-if="item.items && item.expanded"
v-if="item.items && item.expanded && ! item.hide_children"
:root="item"
:current-item="currentItem"
:modal="item.items"
:context="context"
:filter="filter"
@change-item="i => $emit('change-item', i)"
@mark-seen="i => $emit('mark-seen', i)"
@ -97,7 +98,7 @@ function recursiveExpand(node, vue) {
export default {
props: ['root', 'modal', 'currentItem', 'filter'],
props: ['root', 'modal', 'currentItem', 'filter', 'context'],
computed: {
tabIndex() {
@ -110,18 +111,36 @@ export default {
},
methods: {
shouldShow(item) {
if ( ! this.filter || ! this.filter.length || this.containsCurrent(item) )
shouldShow(item, is_walking = false) {
if ( ! this.filter || this.containsCurrent(item) )
return true;
if ( item.search_terms && item.search_terms.includes(this.filter) )
return true;
if ( this.filter.flags ) {
if ( this.filter.flags.has('modified') ) {
// We need to tree walk for this one.
if ( ! is_walking ) {
for(const key of ['tabs', 'contents', 'items'])
if ( item[key] )
for(const thing of item[key])
if ( this.shouldShow(thing) )
return true;
}
return false;
if ( ! item.setting || ! this.context.currentProfile.has(item.setting) )
return false;
}
}
if ( this.filter.query ) {
if ( ! item.search_terms || ! item.search_terms.includes(this.filter.query) )
return false;
}
return true;
},
countMatches(item, seen) {
if ( ! this.filter || ! this.filter.length || ! item )
if ( ! this.filter || ! item )
return 0;
if ( seen && seen.has(item) )
@ -139,7 +158,7 @@ export default {
for(const thing of item[key])
count += this.countMatches(thing, seen);
if ( item.setting && item.search_terms && item.search_terms.includes(this.filter) )
if ( item.setting && this.shouldShow(item, true) )
count++;
return count;

View file

@ -196,14 +196,16 @@ export default class MainMenu extends Module {
this.settings.addUI('changelog', {
path: 'Home > Changelog @{"profile_warning": false}',
component: 'changelog'
component: 'changelog',
getFFZ: () => this
});
this.settings.addUI('addon-changelog', {
path: 'Add-Ons > Changelog @{"sort": -1000, "profile_warning": false}',
path: 'Add-Ons > Changelog @{"sort": -1000, "profile_warning": false, "hide_children": true}',
component: 'changelog',
force_seen: true,
addons: true
addons: true,
getFFZ: () => this
});
this.settings.addUI('legal', {

View file

@ -103,6 +103,16 @@
background: linear-gradient(90deg, var(--color-background-body) 60%, transparent) !important;
}
.directory-header-new__banner-cover {
background: linear-gradient(0deg,
var(--color-background-body),
color-mix(in srgb, var(--color-background-body) 25%, transparent)
), linear-gradient(90deg,
var(--color-background-body),
color-mix(in srgb, var(--color-background-body) 25%, transparent)
);
}
.emote-picker__emote-link {
&:hover {
background-color: var(--color-background-button-text-hover) !important;

View file

@ -119,7 +119,7 @@ export default {
methods: {
countMatches(item, seen) {
if ( ! this.filter || ! this.filter.length || ! item )
if ( ! this.filter || ! item )
return 0;
if ( seen && seen.has(item) )
@ -137,7 +137,7 @@ export default {
for(const thing of item[key])
count += this.countMatches(thing, seen);
if ( item.setting && item.search_terms && item.search_terms.includes(this.filter) )
if ( item.setting && this.shouldShow(item, true) )
count++;
return count;
@ -209,11 +209,32 @@ export default {
}
},
shouldShow(item) {
if ( ! this.filter || ! this.filter.length || ! item.search_terms )
shouldShow(item, is_walking = false) {
if ( ! this.filter || item.no_filter )
return true;
return item.search_terms.includes(this.filter);
if ( this.filter.flags ) {
if ( this.filter.flags.has('modified') ) {
// We need to tree walk for this one.
if ( ! is_walking ) {
for(const key of ['tabs', 'contents', 'items'])
if ( item[key] )
for(const thing of item[key])
if ( this.shouldShow(thing) )
return true;
}
if ( ! item.setting || ! this.context.currentProfile.has(item.setting) )
return false;
}
}
if ( this.filter.query ) {
if ( ! item.search_terms || ! item.search_terms.includes(this.filter.query) )
return false;
}
return true;
}
}
}

View file

@ -397,6 +397,12 @@ textarea.ffz-input {
}
}
.ffz--changelog-segment {
ul {
list-style: disc;
}
}
.ffz--clear-settings code,
.ffz--experiments code {
user-select: none;