1
0
Fork 0
mirror of https://github.com/FrankerFaceZ/FrankerFaceZ.git synced 2025-06-27 21:05:53 +00:00
* Added: Option for displaying larger embeds in chat for supported sources. This won't do anything until the link service is updated with support.
* Added: Support for v6 rich content for embeds, tool-tips, and the rich content debugger.
* Changed: Limit the width of rich content embeds in portrait mode.
* Fixed: Clicking badges not working correctly.
* Fixed: Rich embeds being rendered when an unsupported version is returned from the embed server.
* Fixed: The month being off by one in the default filename when saving a settings backup.
* Fixed: The Chat Identity entry not appearing in the chat settings menu when appropriate.
* API Added: `Mutex()` class for limiting something to a certain number of accessors at once.
This commit is contained in:
SirStendec 2021-11-15 17:12:01 -05:00
parent 97c96be276
commit e704677e84
14 changed files with 228 additions and 104 deletions

View file

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

View file

@ -412,50 +412,17 @@ export default class Badges extends Module {
tip.add_class = 'ffz__tooltip--badges';
const show_previews = this.parent.context.get('tooltip.badge-images');
let container = target.parentElement.parentElement;
if ( ! container.dataset.roomId )
container = target.closest('[data-room-id]');
const ds = this.getBadgeData(target);
const room_id = container?.dataset?.roomId,
room_login = container?.dataset?.room,
out = [];
const out = [];
let data;
if ( target.dataset.badgeData )
data = JSON.parse(target.dataset.badgeData);
else {
const badge_idx = target.dataset.badgeIdx;
let message;
if ( container.message )
message = container.message;
else {
const fine = this.resolve('site.fine');
if ( fine ) {
message = container[fine.accessor]?.return?.stateNode?.props?.message;
if ( ! message )
message = fine.searchParent(container, n => n.props?.message)?.props?.message;
if ( ! message )
message = fine.searchParent(container, n => n.props?.node)?.props?.node?._ffz_message;
if ( ! message )
message = fine.searchParent(container, n => n.props?.messageContext)?.props?.messageContext?.comment?._ffz_message;
}
}
if ( message?._ffz_message)
message = message._ffz_message;
if ( message )
data = message.ffz_badge_cache?.[badge_idx]?.[1]?.badges;
}
if ( data == null )
if ( ds.data == null )
return out;
for(const d of data) {
for(const d of ds.data) {
const p = d.provider;
if ( p === 'twitch' ) {
const bd = this.getTwitchBadge(d.badge, d.version, room_id, room_login),
const bd = this.getTwitchBadge(d.badge, d.version, ds.room_id, ds.room_login),
global_badge = this.getTwitchBadge(d.badge, d.version, null, null, true) || {};
if ( ! bd )
continue;
@ -489,14 +456,6 @@ export default class Badges extends Module {
{title}
</div>);
/*out.push(e('div', {className: 'ffz-badge-tip'}, [
show_previews && e('img', {
className: 'preview-image ffz-badge',
src: bd.image4x
}),
bd.title
]));*/
} else if ( p === 'ffz' ) {
out.push(<div class="ffz-badge-tip">
{show_previews && d.image && <div
@ -508,17 +467,6 @@ export default class Badges extends Module {
/>}
{d.title}
</div>);
/*out.push(e('div', {className: 'ffz-badge-tip'}, [
show_previews && e('div', {
className: 'preview-image ffz-badge',
style: {
backgroundColor: d.color,
backgroundImage: `url("${d.image}")`
}
}),
d.title
]));*/
}
}
@ -527,31 +475,74 @@ export default class Badges extends Module {
}
getBadgeData(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,
user_id = container?.dataset?.userId,
user_login = container?.dataset?.user;
let data;
if (target.dataset.badgeData )
data = JSON.parse(target.dataset.badgeData);
else {
const badge_idx = target.dataset.badgeIdx;
let message;
if ( container.message )
message = container.message;
else {
const fine = this.resolve('site.fine');
if ( fine ) {
message = container[fine.accessor]?.return?.stateNode?.props?.message;
if ( ! message )
message = fine.searchParent(container, n => n.props?.message)?.props?.message;
if ( ! message )
message = fine.searchParent(container, n => n.props?.node)?.props?.node?._ffz_message;
if ( ! message )
message = fine.searchParent(container, n => n.props?.messageContext)?.props?.messageContext?.comment?._ffz_message;
if ( ! message )
message = fine.searchParent(container, n => n._ffzIdentityMsg, 50)?._ffzIdentityMsg;
}
}
if ( message?._ffz_message)
message = message._ffz_message;
if ( message )
data = message.ffz_badge_cache?.[badge_idx]?.[1]?.badges;
}
return {
room_id: room_id,
room_login: room_login,
user_id: user_id,
user_login: user_login,
data
};
}
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 ds = this.getBadgeData(target);
const ds = container?.dataset,
room_id = ds?.roomId,
room_login = ds?.room,
user_id = ds?.userId,
user_login = ds?.user,
data = JSON.parse(target.dataset.badgeData);
if ( data == null )
if ( ds.data == null )
return;
let url = null;
for(const d of data) {
for(const d of ds.data) {
const p = d.provider;
if ( p === 'twitch' ) {
const bd = this.getTwitchBadge(d.badge, d.version, room_id, room_login),
const bd = this.getTwitchBadge(d.badge, d.version, ds.room_id, ds.room_login),
global_badge = this.getTwitchBadge(d.badge, d.version, null, null, true) || {};
if ( ! bd )
continue;
@ -560,8 +551,8 @@ export default class Badges extends Module {
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 if ( (bd.click_action === 'sub' || global_badge.click_action === 'sub') && ds.room_login )
url = `https://www.twitch.tv/subs/${ds.room_login}`;
else
continue;
@ -570,7 +561,7 @@ export default class Badges extends Module {
} else if ( p === 'ffz' ) {
const badge = this.badges[target.dataset.badge];
if ( badge?.click_handler ) {
url = badge.click_handler(user_id, user_login, room_id, room_login, data, event);
url = badge.click_handler(ds.user_id, ds.user_login, ds.room_id, ds.room_login, ds.data, event);
break;
}

View file

@ -8,15 +8,18 @@ let tokenizer;
export default {
props: ['data', 'url', 'events', 'forceFull', 'forceUnsafe', 'forceMedia'],
props: ['data', 'url', 'events', 'forceFull', 'forceUnsafe', 'forceMedia', 'forceMid'],
data() {
return {
has_tokenizer: false,
loaded: false,
version: null,
fragments: {},
error: null,
accent: null,
short: null,
mid: null,
full: null,
unsafe: false,
urls: null,
@ -103,9 +106,12 @@ export default {
this.loaded = false;
this.error = null;
this.version = null;
this.accent = null;
this.short = null;
this.mid = null;
this.full = null;
this.fragments = {};
this.unsafe = false;
this.urls = null;
this.allow_media = false;
@ -164,10 +170,13 @@ export default {
}
this.loaded = true;
this.version = data.v;
this.error = data.error;
this.accent = data.accent;
this.short = data.short;
this.mid = data.mid;
this.full = data.full;
this.fragments = data.fragments ?? {};
this.unsafe = data.unsafe;
this.urls = data.urls;
this.allow_media = data.allow_media;
@ -214,14 +223,22 @@ export default {
},
renderBody(h) {
if ( this.has_tokenizer && this.loaded && (this.forceFull ? this.full : this.short) ) {
let body = this.forceFull ? this.full :
this.forceMid ? this.mid : this.short;
if ( this.has_tokenizer && this.version && this.version > tokenizer.VERSION )
body = null;
if ( this.has_tokenizer && this.loaded && body ) {
return h('div', {
class: 'ffz--card-rich tw-full-width tw-overflow-hidden tw-flex tw-flex-column'
}, tokenizer.renderTokens(this.forceFull ? this.full : this.short, h, {
}, tokenizer.renderTokens(body, h, {
vue: true,
tList: (...args) => this.tList(...args),
i18n: this.getI18n(),
fragments: this.fragments,
allow_media: this.forceMedia ?? this.allow_media,
allow_unsafe: this.forceUnsafe ?? this.allow_unsafe
}));
@ -234,6 +251,9 @@ export default {
if ( this.loaded && this.forceFull && ! this.full ) {
description = 'null';
} else if ( this.loaded && this.forceMid && ! this.mid ) {
description = 'null -- will use short instead';
} else if ( this.error ) {
title = this.t('card.error', 'An error occurred.');
description = this.error;

View file

@ -224,6 +224,16 @@ export default class Chat extends Module {
}
});
this.settings.add('chat.rich.want-mid', {
default: false,
ui: {
path: 'Chat > Appearance >> Rich Content',
title: 'Display larger rich content in chat.',
description: 'This enables the use of bigger rich content embeds in chat. This is **not** recommended for most users and/or chats.\n\n**Note:** Enabling this may cause chat to scroll at inopportune times due to content loading. Moderators should not use this feature.',
component: 'setting-check-box'
}
});
this.settings.add('chat.rich.hide-tokens', {
default: false,
ui: {
@ -1879,11 +1889,13 @@ export default class Chat extends Module {
const providers = this.__rich_providers;
const want_mid = this.context.get('chat.rich.want-mid');
for(const token of tokens) {
for(const provider of providers)
if ( provider.test.call(this, token, msg) ) {
token.hidden = provider.can_hide_token && (this.context.get('chat.rich.hide-tokens') || provider.hide_token);
return provider.process.call(this, token);
return provider.process.call(this, token, want_mid);
}
}
}

View file

@ -38,11 +38,12 @@ export const Links = {
return token.type === 'link'
},
process(token) {
process(token, want_mid) {
return {
card_tooltip: true,
url: token.url,
timeout: 0,
want_mid,
getData: async (refresh = false) => {
let data;

View file

@ -56,8 +56,6 @@ function datasetBool(value) {
return value == null ? null : value === 'true';
}
const TOOLTIP_VERSION = 5;
export const Links = {
type: 'link',
priority: 50,
@ -91,12 +89,15 @@ export const Links = {
import(/* webpackChunkName: 'rich_tokens' */ 'utilities/rich_tokens'),
this.get_link_info(url)
]).then(([rich_tokens, data]) => {
if ( ! data || (data.v || 1) > TOOLTIP_VERSION )
if ( ! data || (data.v || 1) > rich_tokens.VERSION )
return '';
const ctx = {
tList: (...args) => this.i18n.tList(...args),
i18n: this.i18n,
fragments: data.fragments,
allow_media: show_images,
allow_unsafe: show_unsafe,
onload: () => requestAnimationFrame(() => tip.update())
@ -111,12 +112,14 @@ export const Links = {
if ( data.full ) {
content = rich_tokens.renderTokens(data.full, createElement, ctx);
} else {
if ( data.short ) {
content = rich_tokens.renderTokens(data.short, createElement, ctx);
} else
content = this.i18n.t('card.empty', 'No data was returned.');
}
} else if ( data.mid ) {
content = rich_tokens.renderTokens(data.mid, createElement, ctx);
} else if ( data.short ) {
content = rich_tokens.renderTokens(data.short, createElement, ctx);
} else
content = this.i18n.t('card.empty', 'No data was returned.');
if ( ! data.urls )
return content;
@ -1038,7 +1041,7 @@ export const CheerEmotes = {
if ( length > 12 ) {
out.push(<br />);
out.push(this.i18n.t('tooltip.bits.more', '(and {count} more)', length-12));
out.push(this.i18n.t('tooltip.bits.more', '(and {count, number} more)', length-12));
}
}

View file

@ -156,6 +156,22 @@
/>
</div>
</div>
<div class="tw-flex tw-mg-b-1 tw-full-width">
<label>
{{ t('debug.link-provider.mid-embed', 'Mid Embed') }}
</label>
<div class="tw-full-width tw-overflow-hidden">
<chat-rich
v-if="rich_data"
:data="rich_data"
:url="url"
:force-mid="true"
:force-media="force_media"
:force-unsafe="force_unsafe"
:events="events"
/>
</div>
</div>
<div class="tw-flex tw-mg-b-1 tw-full-width">
<label>
{{ t('debug.link-provider.full-embed', 'Full Embed') }}
@ -172,6 +188,14 @@
/>
</div>
</div>
<div class="tw-flex tw-mg-b-1 tw-full-width">
<label>
{{ t('debug.link-provider.raw-length', 'Raw Length') }}
</label>
<div>
{{ tNumber(length) }}
</div>
</div>
<div class="tw-flex tw-mg-b-1 tw-full-width">
<label>
{{ t('debug.link-provider.raw', 'Raw Data') }}
@ -234,6 +258,7 @@ export default {
rich_data: null,
raw_loading: false,
raw_data: null,
length: 0,
force_media: state?.ffz_lt_media ?? true,
force_unsafe: state?.ffz_lt_unsafe ?? false,
@ -436,6 +461,7 @@ export default {
async refreshRaw() {
this.raw_data = null;
this.length = 0;
if ( ! this.rich_data ) {
this.raw_loading = false;
return;
@ -443,7 +469,9 @@ export default {
this.raw_loading = true;
try {
this.raw_data = JSON.stringify(await this.chat.get_link_info(this.url), null, '\t');
const data = await this.chat.get_link_info(this.url);
this.raw_data = JSON.stringify(data, null, '\t');
this.length = JSON.stringify(data).length;
} catch(err) {
this.raw_data = `Error\n\n${err.toString()}`;
}

View file

@ -389,7 +389,7 @@ export default class SettingsManager extends Module {
async generateBackupFile() {
const now = new Date(),
timestamp = `${now.getFullYear()}-${now.getMonth()}-${now.getDate()}`;
timestamp = `${now.getFullYear()}-${now.getMonth()+1}-${now.getDate()}`;
if ( await this._needsZipBackup() ) {
const blob = await this._getZipBackup();

View file

@ -176,7 +176,14 @@ export default class RichContent extends Module {
}
renderBody() {
const doc = this.props.force_full ? this.state.full : this.state.short;
let doc = this.props.force_full ? this.state.full :
this.props.force_mid ? this.state.mid :
((this.props.want_mid ? this.state.mid : null) ?? this.state.short);
if ( t.has_tokenizer && this.state.v && this.state.v > t.tokenizer.VERSION)
doc = null;
//const doc = (this.props.force_full ? this.state.full : null) ?? (this.props.force_mid ? this.state.mid : null) ?? this.state.short;
if ( t.has_tokenizer && this.state.loaded && doc ) {
return (<div class="ffz-card-rich tw-full-width tw-overflow-hidden tw-flex tw-flex-column">
{t.tokenizer.renderTokens(doc, createElement, {
@ -184,6 +191,8 @@ export default class RichContent extends Module {
tList: (...args) => t.i18n.tList(...args),
i18n: t.i18n,
fragments: this.state.fragments,
allow_media: t.chat.context.get('tooltip.link-images'),
allow_unsafe: t.chat.context.get('tooltip.link-nsfw-images')
})}

View file

@ -145,6 +145,16 @@ export default class SettingsMenu extends Module {
this.props.onCloseSettings();
}
const msg = {
user,
badges,
ffz_badges: t.badges.getBadges(user.id, user.login, this.props.channelID, this.props.channelLogin),
roomID: this.props.channelID,
roomLogin: this.props.channelLogin
};
this._ffzIdentityMsg = msg;
return (<div class="ffz-identity">
<div class="tw-mg-y-05 tw-pd-x-05">
<p class="tw-c-text-alt-2 tw-font-size-6 tw-strong tw-upcase">
@ -162,15 +172,9 @@ export default class SettingsMenu extends Module {
<span
class="ffz--editor-badges"
data-room-id={this.props.channelID}
data-room-login={this.props.channelLogin}
data-room={this.props.channelLogin}
>
{t.badges.render({
user,
badges,
ffz_badges: t.badges.getBadges(user.id, user.login, this.props.channelID, this.props.channelLogin),
roomID: this.props.channelID,
roomLogin: this.props.channelLogin
}, createElement, true, true)}
{t.badges.render(msg, createElement, true, true)}
</span>
<span class="tw-strong notranslate" style={{color}}>
@ -192,7 +196,8 @@ export default class SettingsMenu extends Module {
const out = old_render.call(this);
try {
const children = out?.props?.children?.props?.children?.[1]?.props?.children?.props?.children;
const children = out?.props?.children?.props?.children?.props?.children?.[1]?.props?.children?.props?.children;
//const children = out?.props?.children?.props?.children?.[1]?.props?.children?.props?.children;
if ( Array.isArray(children) ) {
const extra = this.ffzRenderIdentity();
if ( extra )

View file

@ -4,6 +4,12 @@
--ffz-theater-height: calc(calc(100vw * 0.5625) + var(--ffz-portrait-extra-height));
--ffz-chat-height: calc(100vh - var(--ffz-player-height));
.chat-shell .ffz--chat-card {
--width: max(30rem, min(50%, calc(1.5 * var(--ffz-chat-width))));
width: var(--width);
margin-left: min(2rem, calc(100% - calc(4rem + var(--width))));
}
& > div:first-child > div[class^="Layout-sc"] {
.ffz--portrait-invert & {
position: absolute;

View file

@ -61,6 +61,7 @@ export const UPDATE_TOKEN_SETTINGS = [
'chat.emoji.style',
'chat.bits.stack',
'chat.rich.enabled',
'chat.rich.want-mid',
'chat.rich.hide-tokens',
'chat.rich.all-links',
'chat.rich.minimum-level',

View file

@ -94,6 +94,39 @@ export function timeout(promise, delay) {
}
export class Mutex {
constructor(limit = 1) {
this.limit = limit;
this._active = 0;
this._waiting = [];
this._done = this._done.bind(this);
}
get available() { return this._active < this.limit }
_done() {
this._active--;
while(this._active < this.limit && this._waiting.length > 0) {
this._active++;
const waiter = this._waiting.shift();
waiter(this._done);
}
}
wait() {
if ( this._active < this.limit) {
this._active++;
return Promise.resolve(this._done);
}
return new Promise(s => this._waiting.push(s));
}
}
/**
* Return a wrapper for a function that will only execute the function
* a period of time after it has stopped being called.

View file

@ -8,6 +8,8 @@ import {has} from 'utilities/object';
import Markdown from 'markdown-it';
import MILA from 'markdown-it-link-attributes';
export const VERSION = 6;
export const TOKEN_TYPES = {};
const validate = (input, valid) => valid.includes(input) ? input : null;
@ -243,6 +245,17 @@ export function renderTokens(tokens, createElement, ctx, markdown) {
export default renderTokens;
// ============================================================================
// Token Type: Reference
// ============================================================================
TOKEN_TYPES.ref = function(token, createElement, ctx) {
const frag = ctx.fragments?.[token.name];
if (frag )
return renderTokens(frag, createElement, ctx);
}
// ============================================================================
// Token Type: Box
// ============================================================================
@ -667,9 +680,11 @@ function header_normal(token, createElement, ctx) {
content.appendChild(image);
else
content.insertBefore(image, content.firstChild);
} else {
console.warn('Add React support!');
console.log(content);
} else if ( Array.isArray(content?.props?.children) ) {
if ( right )
content.props.children.push(image);
else
content.props.children.unshift(image);
}
} else {