1
0
Fork 0
mirror of https://github.com/FrankerFaceZ/FrankerFaceZ.git synced 2025-06-27 21:05:53 +00:00
* Added: Display rich tool-tips for channel panels.
* Fixed: Hide Unfollow button in theater mode with the appropriate setting. (Closes #860)
* Fixed: Automatically open Theater Mode not working when the channel is window is not visible. (Closes #861)
* Fixed: Game titles not appearing in clip embeds.
* Fixed: Featured Follow metadata failing when trying to open the menu.
* Debug Added: Setting to choose the link resolver.
* Debug Added: Test UI for working on link resolvers.
This commit is contained in:
SirStendec 2020-07-29 02:22:45 -04:00
parent 05e8428a4a
commit eec65551fb
14 changed files with 519 additions and 86 deletions

View file

@ -1,7 +1,7 @@
{ {
"name": "frankerfacez", "name": "frankerfacez",
"author": "Dan Salvato LLC", "author": "Dan Salvato LLC",
"version": "4.20.20", "version": "4.20.21",
"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

@ -6,13 +6,12 @@ import {ALLOWED_ATTRIBUTES, ALLOWED_TAGS} from 'utilities/constants';
const ERROR_IMAGE = 'https://static-cdn.jtvnw.net/emoticons/v1/58765/2.0'; const ERROR_IMAGE = 'https://static-cdn.jtvnw.net/emoticons/v1/58765/2.0';
export default { export default {
props: ['data', 'url'], props: ['data', 'url', 'events'],
data() { data() {
return { return {
loaded: false, loaded: false,
error: false, error: null,
html: null,
title: this.t('card.loading', 'Loading...'), title: this.t('card.loading', 'Loading...'),
title_tokens: null, title_tokens: null,
desc_1: null, desc_1: null,
@ -26,58 +25,96 @@ export default {
} }
}, },
async mounted() { watch: {
let data; data() {
try { this.reset();
data = this.data.getData(); this.load();
if ( data instanceof Promise ) { }
const to_wait = has(this.data, 'timeout') ? this.data.timeout : 1000; },
if ( to_wait )
data = await timeout(data, to_wait);
else
data = await data;
}
if ( ! data ) created() {
data = { if ( this.events ) {
error: true, this._events = this.events;
title: this.t('card.error', 'An error occured.'), this._events.on('chat:update-link-resolver', this.checkRefresh, this);
desc_1: this.t('card.empty', 'No data was returned.')
}
} catch(err) {
data = {
error: true,
title: this.t('card.error', 'An error occured.'),
desc_1: String(err)
}
} }
this.loaded = true; this.load();
this.error = data.error; },
this.html = data.html;
this.title = data.title; beforeDestroy() {
this.title_tokens = data.title_tokens; if ( this._events ) {
this.desc_1 = data.desc_1; this._events.off('chat:update-link-resolver', this.checkRefresh, this);
this.desc_1_tokens = data.desc_1_tokens; this._events = null;
this.desc_2 = data.desc_2; }
this.desc_2_tokens = data.desc_2_tokens;
this.image = data.image;
this.image_square = data.image_square;
this.image_title = data.image_title;
}, },
methods: { methods: {
checkRefresh(url) {
if ( ! url || (url && url === this.url) ) {
this.reset();
this.load();
}
},
reset() {
this.loaded = false;
this.error = null;
this.title = this.t('card.loading', 'Loading...');
this.title_tokens = null;
this.desc_1 = null;
this.desc_1_tokens = null;
this.desc_2 = null;
this.desc_2_tokens = null;
this.image = null;
this.image_title = null;
this.image_square = null;
this.accent = null;
},
async load() {
let data;
try {
data = this.data.getData();
if ( data instanceof Promise ) {
const to_wait = has(this.data, 'timeout') ? this.data.timeout : 1000;
if ( to_wait )
data = await timeout(data, to_wait);
else
data = await data;
}
if ( ! data )
data = {
error: true,
title: this.t('card.error', 'An error occured.'),
desc_1: this.t('card.empty', 'No data was returned.')
}
} catch(err) {
data = {
error: true,
title: this.t('card.error', 'An error occured.'),
desc_1: String(err)
}
}
this.loaded = true;
this.error = data.error;
this.title = data.title;
this.title_tokens = data.title_tokens;
this.desc_1 = data.desc_1;
this.desc_1_tokens = data.desc_1_tokens;
this.desc_2 = data.desc_2;
this.desc_2_tokens = data.desc_2_tokens;
this.image = data.image;
this.image_square = data.image_square;
this.image_title = data.image_title;
this.accent = data.accent;
},
renderCard(h) { renderCard(h) {
if ( this.data.renderBody ) if ( this.data.renderBody )
return [this.data.renderBody(h)]; return [this.data.renderBody(h)];
if ( this.html )
return [h('div', {
domProps: {
innerHTML: this.html
}
})];
return [ return [
this.renderImage(h), this.renderImage(h),
this.renderDescription(h) this.renderDescription(h)

View file

@ -71,6 +71,32 @@ export default class Chat extends Module {
// Settings // Settings
// ======================================================================== // ========================================================================
this.settings.add('debug.link-resolver.source', {
default: null,
ui: {
path: 'Debugging > Data Sources >> Links',
title: 'Link Resolver',
component: 'setting-select-box',
force_seen: true,
data: [
{value: null, title: 'Automatic'},
{value: 'dev', title: 'localhost'},
{value: 'test', title: 'API Test'},
{value: 'prod', title: 'API Production' },
{value: 'socket', title: 'Socket Cluster (Deprecated)'}
]
},
changed: () => this.clearLinkCache()
});
this.settings.addUI('debug.link-resolver.test', {
path: 'Debugging > Data Sources >> Links',
component: 'link-tester',
getChat: () => this,
force_seen: true
});
this.settings.add('chat.font-size', { this.settings.add('chat.font-size', {
default: 12, default: 12,
ui: { ui: {
@ -1506,6 +1532,33 @@ export default class Chat extends Module {
// Twitch Crap // Twitch Crap
// ==== // ====
clearLinkCache(url) {
if ( url ) {
const info = this._link_info[url];
if ( ! info[0] ) {
for(const pair of info[2])
pair[1]();
}
this._link_info[url] = null;
this.emit(':update-link-resolver', url);
return;
}
const old = this._link_info;
this._link_info = {};
for(const info of Object.values(old)) {
if ( ! info[0] ) {
for(const pair of info[2])
pair[1]();
}
}
this.emit(':update-link-resolver');
}
get_link_info(url, no_promises) { get_link_info(url, no_promises) {
let info = this._link_info[url]; let info = this._link_info[url];
const expires = info && info[1]; const expires = info && info[1];
@ -1536,15 +1589,23 @@ export default class Chat extends Module {
cbs[success ? 0 : 1](data); cbs[success ? 0 : 1](data);
} }
if ( this.experiments.getAssignment('api_links') ) let provider = this.settings.get('debug.link-resolver.source');
timeout(fetch(`https://api-test.frankerfacez.com/v2/link?url=${encodeURIComponent(url)}`).then(r => r.json()), 15000) if ( provider == null )
.then(data => handle(true, data)) provider = this.experiments.getAssignment('api_links') ? 'test' : 'socket';
.catch(err => handle(false, err));
else if ( provider === 'socket' ) {
timeout(this.socket.call('get_link', url), 15000) timeout(this.socket.call('get_link', url), 15000)
.then(data => handle(true, data)) .then(data => handle(true, data))
.catch(err => handle(false, err)); .catch(err => handle(false, err));
} else {
const host = provider === 'dev' ? 'https://localhost:8002/' :
provider === 'test' ? 'https://api-test.frankerfacez.com/v2/link' :
'https://api.frankerfacez.com/v2/link';
timeout(fetch(`${host}?url=${encodeURIComponent(url)}`).then(r => r.json()), 15000)
.then(data => handle(true, data))
.catch(err => handle(false, err));
}
}); });
} }
} }

View file

@ -63,10 +63,14 @@ export const Links = {
url: token.url, url: token.url,
accent: data.accent, accent: data.accent,
image: this.context.get('tooltip.link-images') ? (data.image_safe || this.context.get('tooltip.link-nsfw-images') ) ? data.preview || data.image : null : null, image: this.context.get('tooltip.link-images') ? (data.image_safe || this.context.get('tooltip.link-nsfw-images') ) ? data.preview || data.image : null : null,
image_title: data.image_title,
image_square: data.image_square, image_square: data.image_square,
title: data.title, title: data.title,
title_tokens: data.title_tokens,
desc_1: data.desc_1, desc_1: data.desc_1,
desc_2: data.desc_2 desc_1_tokens: data.desc_1_tokens,
desc_2: data.desc_2,
desc_2_tokens: data.desc_2_tokens
} }
} }
} }
@ -227,7 +231,7 @@ export const Clips = {
} else if ( game ) { } else if ( game ) {
desc_1_tokens = this.i18n.tList('clip.desc.1.playing', '{user} playing {game}', { desc_1_tokens = this.i18n.tList('clip.desc.1.playing', '{user} playing {game}', {
user: {class: 'tw-semibold', content: user}, user: {class: 'tw-semibold', content: user},
game: {class: 'tw-semibold', game_display} game: {class: 'tw-semibold', content: game_display}
}); });
desc_1 = this.i18n.t('clip.desc.1.playing', '{user} playing {game}', { desc_1 = this.i18n.t('clip.desc.1.playing', '{user} playing {game}', {
user, user,

View file

@ -47,7 +47,9 @@ export const Links = {
if ( target.dataset.isMail === 'true' ) if ( target.dataset.isMail === 'true' )
return [this.i18n.t('tooltip.email-link', 'E-Mail {address}', {address: target.textContent})]; return [this.i18n.t('tooltip.email-link', 'E-Mail {address}', {address: target.textContent})];
return this.get_link_info(target.dataset.url).then(data => { const url = target.dataset.url || target.href;
return this.get_link_info(url).then(data => {
if ( ! data || (data.v || 1) > TOOLTIP_VERSION ) if ( ! data || (data.v || 1) > TOOLTIP_VERSION )
return ''; return '';

View file

@ -0,0 +1,230 @@
<template>
<div class="ffz--link-tester">
<div class="ffz--widget ffz--select-box">
<div class="tw-flex tw-align-items-start">
<label for="selector" class="tw-mg-y-05">
{{ t('debug.link-provider.url', 'Test URL') }}
</label>
<div class="tw-flex tw-flex-column tw-mg-05">
<select
id="selector"
ref="selector"
class="tw-border-top-left-radius-medium tw-border-top-right-radius-medium tw-font-size-6 tw-select tw-pd-l-1 tw-pd-r-3 tw-pd-y-05"
@change="onSelectChange"
>
<option
v-for="i in stock_urls"
:key="i"
:selected="i === raw_url"
>
{{ i }}
</option>
<option :selected="isCustomURL">
{{ t('setting.combo-box.custom', 'Custom') }}
</option>
</select>
<input
ref="text"
v-model="raw_url"
:disabled="! isCustomURL"
class="ffz-mg-t-1p tw-border-bottom-left-radius-medium tw-border-bottom-right-radius-medium tw-font-size-6 tw-pd-x-1 tw-pd-y-05 tw-input"
>
</div>
</div>
</div>
<div class="tw-flex tw-mg-b-1">
<div class="tw-flex-grow-1" />
<button
class="tw-mg-l-1 tw-button tw-button--text"
@click="refresh"
>
<span class="tw-button__text ffz-i-arrows-cw">
{{ t('debug.link-provider.refresh', 'Refresh') }}
</span>
</button>
</div>
<div class="tw-flex tw-mg-b-1 tw-full-width">
<label>
{{ t('debug.link-provider.embed', 'Rich Embed') }}
</label>
<div class="tw-full-width tw-overflow-hidden">
<chat-rich
v-if="rich_data"
:data="rich_data"
:url="url"
:events="events"
/>
</div>
</div>
<div class="tw-flex tw-mg-b-1 tw-full-width">
<label>
{{ t('debug.link-provider.link', 'Chat Link') }}
</label>
<div class="tw-full-width tw-overflow-hidden">
<a
v-if="url"
:href="url"
:data-url="url"
class="ffz-tooltip"
data-tooltip-type="link"
data-force-tooltip="true"
data-is-mail="false"
rel="noopener noreferrer"
target="_blank"
>
{{ url }}
</a>
</div>
</div>
<div class="tw-flex tw-mg-b-1 tw-full-width">
<label>
{{ t('debug.link-provider.raw', 'Raw Data') }}
</label>
<div class="tw-full-width tw-overflow-hidden ffz--example-report">
<div v-if="url" class="tw-c-background-alt-2 tw-font-size-5 tw-pd-y-05 tw-pd-x-1 tw-border-radius-large">
<div v-if="raw_loading" class="tw-align-center">
<h1 class="tw-mg-5 ffz-i-zreknarf loading" />
</div>
<code v-else>{{ raw_data }}</code>
</div>
</div>
</div>
</div>
</template>
<script>
import { deep_copy } from 'utilities/object'
import { debounce } from '../../../utilities/object';
const STOCK_URLS = [
'https://www.twitch.tv/sirstendec',
'https://discord.gg/UrAkGhT',
'https://www.youtube.com/watch?v=CAL4WMpBNs0',
'https://xkcd.com/221/',
'https://github.com/FrankerFaceZ/FrankerFaceZ',
'https://twitter.com/frankerfacez',
'https://twitter.com/FrankerFaceZ/status/1240717057630625792'
]
export default {
components: {
'chat-rich': async () => {
const stuff = await import(/* webpackChunkName: "chat" */ 'src/modules/chat/components');
return stuff.default('./chat-rich.vue').default;
}
},
props: ['item', 'context'],
data() {
return {
stock_urls: deep_copy(STOCK_URLS),
raw_url: STOCK_URLS[Math.floor(Math.random() * STOCK_URLS.length)],
rich_data: null,
isCustomURL: false,
raw_loading: false,
raw_data: null,
events: {
on: (...args) => this.item.getChat().on(...args),
off: (...args) => this.item.getChat().off(...args)
}
}
},
computed: {
url() {
try {
return new URL(this.raw_url).toString();
} catch(err) {
return null;
}
}
},
watch: {
url() {
this.rebuildData();
},
rich_data() {
this.refreshRaw();
}
},
created() {
this.rebuildData = debounce(this.rebuildData, 250);
this.refreshRaw = debounce(this.refreshRaw, 250);
},
mounted() {
this.chat = this.item.getChat();
this.chat.on('chat:update-link-resolver', this.checkRefreshRaw, this);
this.rebuildData();
},
beforeDestroy() {
this.chat.off('chat:update-link-resolver', this.checkRefreshRaw, this);
this.chat = null;
},
methods: {
checkRefreshRaw(url) {
if ( ! url || (url && url === this.url) )
this.refreshRaw();
},
async refreshRaw() {
this.raw_data = null;
if ( ! this.rich_data ) {
this.raw_loading = false;
return;
}
this.raw_loading = true;
try {
this.raw_data = JSON.stringify(await this.chat.get_link_info(this.url), null, '\t');
} catch(err) {
this.raw_data = `Error\n\n${err.toString()}`;
}
this.raw_loading = false;
},
rebuildData() {
if ( ! this.url )
return this.rich_data = null;
const token = {
type: 'link',
force_rich: true,
is_mail: false,
url: this.url,
text: this.url
};
this.rich_data = this.chat.rich_providers.link.process.call(this.chat, token);
},
refresh() {
this.chat.clearLinkCache(this.url);
},
onSelectChange() {
const idx = this.$refs.selector.selectedIndex,
raw_value = this.stock_urls[idx];
if ( raw_value ) {
this.raw_url = raw_value;
this.isCustomURL = false;
} else
this.isCustomURL = true;
},
onTextChange() {
this.raw_url = this.$refs.text
}
}
}
</script>

View file

@ -78,22 +78,22 @@ export default class TooltipProvider extends Module {
} }
_createInstance(container) { _createInstance(container, klass = 'ffz-tooltip', default_type) {
return new Tooltip(container, 'ffz-tooltip', { return new Tooltip(container, klass, {
html: true, html: true,
i18n: this.i18n, i18n: this.i18n,
live: true, live: true,
delayHide: this.checkDelayHide.bind(this), delayHide: this.checkDelayHide.bind(this, default_type),
delayShow: this.checkDelayShow.bind(this), delayShow: this.checkDelayShow.bind(this, default_type),
content: this.process.bind(this), content: this.process.bind(this, default_type),
interactive: this.checkInteractive.bind(this), interactive: this.checkInteractive.bind(this, default_type),
hover_events: this.checkHoverEvents.bind(this), hover_events: this.checkHoverEvents.bind(this, default_type),
onShow: this.delegateOnShow.bind(this), onShow: this.delegateOnShow.bind(this, default_type),
onHide: this.delegateOnHide.bind(this), onHide: this.delegateOnHide.bind(this, default_type),
popperConfig: this.delegatePopperConfig.bind(this), popperConfig: this.delegatePopperConfig.bind(this, default_type),
popper: { popper: {
placement: 'top', placement: 'top',
modifiers: { modifiers: {
@ -132,8 +132,8 @@ export default class TooltipProvider extends Module {
this.tips.cleanup(); this.tips.cleanup();
} }
delegatePopperConfig(target, tip, pop_opts) { delegatePopperConfig(default_type, target, tip, pop_opts) {
const type = target.dataset.tooltipType, const type = target.dataset.tooltipType || default_type,
handler = this.types[type]; handler = this.types[type];
if ( handler && handler.popperConfig ) if ( handler && handler.popperConfig )
@ -142,24 +142,24 @@ export default class TooltipProvider extends Module {
return pop_opts; return pop_opts;
} }
delegateOnShow(target, tip) { delegateOnShow(default_type, target, tip) {
const type = target.dataset.tooltipType, const type = target.dataset.tooltipType || default_type,
handler = this.types[type]; handler = this.types[type];
if ( handler && handler.onShow ) if ( handler && handler.onShow )
handler.onShow(target, tip); handler.onShow(target, tip);
} }
delegateOnHide(target, tip) { delegateOnHide(default_type, target, tip) {
const type = target.dataset.tooltipType, const type = target.dataset.tooltipType || default_type,
handler = this.types[type]; handler = this.types[type];
if ( handler && handler.onHide ) if ( handler && handler.onHide )
handler.onHide(target, tip); handler.onHide(target, tip);
} }
checkDelayShow(target, tip) { checkDelayShow(default_type, target, tip) {
const type = target.dataset.tooltipType, const type = target.dataset.tooltipType || default_type,
handler = this.types[type]; handler = this.types[type];
if ( has(handler, 'delayShow') ) if ( has(handler, 'delayShow') )
@ -168,8 +168,8 @@ export default class TooltipProvider extends Module {
return 0; return 0;
} }
checkDelayHide(target, tip) { checkDelayHide(default_type, target, tip) {
const type = target.dataset.tooltipType, const type = target.dataset.tooltipType || default_type,
handler = this.types[type]; handler = this.types[type];
if ( has(handler, 'delayHide') ) if ( has(handler, 'delayHide') )
@ -178,8 +178,8 @@ export default class TooltipProvider extends Module {
return 0; return 0;
} }
checkInteractive(target, tip) { checkInteractive(default_type, target, tip) {
const type = target.dataset.tooltipType, const type = target.dataset.tooltipType || default_type,
handler = this.types[type]; handler = this.types[type];
if ( has(handler, 'interactive') ) if ( has(handler, 'interactive') )
@ -188,8 +188,8 @@ export default class TooltipProvider extends Module {
return false; return false;
} }
checkHoverEvents(target, tip) { checkHoverEvents(default_type, target, tip) {
const type = target.dataset.tooltipType, const type = target.dataset.tooltipType || default_type,
handler = this.types[type]; handler = this.types[type];
if ( has(handler, 'hover_events') ) if ( has(handler, 'hover_events') )
@ -198,8 +198,8 @@ export default class TooltipProvider extends Module {
return false; return false;
} }
process(target, tip) { process(default_type, target, tip) {
const type = target.dataset.tooltipType || 'text', const type = target.dataset.tooltipType || default_type || 'text',
handler = this.types[type]; handler = this.types[type];
if ( ! handler ) if ( ! handler )

View file

@ -10,7 +10,7 @@ import {debounce} from 'utilities/object';
import { createElement, setChildren } from 'utilities/dom'; import { createElement, setChildren } from 'utilities/dom';
const USER_PAGES = ['user', 'user-home', 'video', 'user-video', 'user-clip', 'user-videos', 'user-clips', 'user-collections', 'user-events', 'user-followers', 'user-following']; const USER_PAGES = ['user', 'user-home', 'user-about', 'video', 'user-video', 'user-clip', 'user-videos', 'user-clips', 'user-collections', 'user-events', 'user-followers', 'user-following'];
export default class Channel extends Module { export default class Channel extends Module {
@ -31,6 +31,17 @@ export default class Channel extends Module {
this.inject('metadata'); this.inject('metadata');
this.inject('socket'); this.inject('socket');
this.settings.add('channel.panel-tips', {
default: true,
ui: {
path: 'Channel > Behavior >> Panels',
title: 'Display rich tool-tips for links in channel panels.',
component: 'setting-check-box'
},
changed: () => this.updatePanelTips()
});
this.settings.add('channel.auto-click-chat', { this.settings.add('channel.auto-click-chat', {
default: false, default: false,
ui: { ui: {
@ -61,6 +72,13 @@ export default class Channel extends Module {
}); });
this.ChannelPanels = this.fine.define(
'channel-panels',
n => n.layoutMasonry && n.updatePanelOrder && n.onExtensionPoppedOut,
USER_PAGES
);
this.ChannelRoot = this.elemental.define( this.ChannelRoot = this.elemental.define(
'channel-root', '.channel-root', 'channel-root', '.channel-root',
USER_PAGES, USER_PAGES,
@ -91,6 +109,14 @@ export default class Channel extends Module {
this.on('i18n:update', this.updateLinks, this); this.on('i18n:update', this.updateLinks, this);
this.ChannelPanels.on('mount', this.updatePanelTips, this);
this.ChannelPanels.on('update', this.updatePanelTips, this);
this.ChannelPanels.on('unmount', this.removePanelTips, this);
this.ChannelPanels.ready((cls, instances) => {
for(const inst of instances)
this.updatePanelTips(inst);
});
this.ChannelRoot.on('mount', this.updateRoot, this); this.ChannelRoot.on('mount', this.updateRoot, this);
this.ChannelRoot.on('mutate', this.updateRoot, this); this.ChannelRoot.on('mutate', this.updateRoot, this);
this.ChannelRoot.on('unmount', this.removeRoot, this); this.ChannelRoot.on('unmount', this.removeRoot, this);
@ -107,6 +133,35 @@ export default class Channel extends Module {
this.checkNavigation(); this.checkNavigation();
} }
updatePanelTips(inst) {
if ( ! inst ) {
for(const inst of this.ChannelPanels.instances) {
if ( inst )
this.updatePanelTips(inst);
}
}
const el = this.fine.getChildNode(inst);
if ( ! el || ! this.settings.get('channel.panel-tips') )
return this.removePanelTips(inst);
if ( inst._ffz_tips && inst._ffz_tip_el !== el )
this.removePanelTips(inst);
if ( ! inst._ffz_tips ) {
inst._ffz_tips = this.resolve('tooltips')._createInstance(el, 'tw-link', 'link');
inst._ffz_tip_el = el;
}
}
removePanelTips(inst) { // eslint-disable-line class-methods-use-this
if ( inst._ffz_tips ) {
inst._ffz_tips.destroy();
inst._ffz_tips = null;
inst._ffz_tip_el = null;
}
}
checkNavigation() { checkNavigation() {
if ( ! this.settings.get('channel.auto-click-chat') || this.router.current_name !== 'user-home' ) if ( ! this.settings.get('channel.auto-click-chat') || this.router.current_name !== 'user-home' )
return; return;

View file

@ -38,7 +38,7 @@ export default class RichContent extends Module {
} }
} }
async componentDidMount() { async load() {
try { try {
let data = this.props.getData(); let data = this.props.getData();
if ( data instanceof Promise ) { if ( data instanceof Promise ) {
@ -75,6 +75,28 @@ export default class RichContent extends Module {
} }
} }
checkReload(url) {
if ( ! url || (url && this.props.url === url) )
this.reload();
}
reload() {
this.setState({
loaded: false,
error: false
}, () => this.load());
}
componentDidMount() {
t.on('chat:update-link-resolver', this.checkReload, this);
this.load();
}
componentWillUnmount() {
t.off('chat:update-link-resolver', this.checkReload, this);
}
renderCardImage() { renderCardImage() {
return (<div class={`chat-card__preview-img tw-align-items-center tw-c-background-alt-2 tw-flex tw-flex-shrink-0 tw-justify-content-center${this.state.image_square ? ' square' : ''}`}> return (<div class={`chat-card__preview-img tw-align-items-center tw-c-background-alt-2 tw-flex tw-flex-shrink-0 tw-justify-content-center${this.state.image_square ? ' square' : ''}`}>
{this.state.error ? {this.state.error ?

View file

@ -11,7 +11,7 @@ import {has} from 'utilities/object';
const STYLE_VALIDATOR = document.createElement('span'); const STYLE_VALIDATOR = document.createElement('span');
const CLASSES = { const CLASSES = {
'unfollow': '.follow-btn__follow-btn--following', 'unfollow': '.follow-btn__follow-btn--following,.follow-btn--following',
'top-discover': '.navigation-link[data-a-target="discover-link"]', 'top-discover': '.navigation-link[data-a-target="discover-link"]',
'side-nav': '.side-nav', 'side-nav': '.side-nav',
'side-rec-channels': '.side-nav .recommended-channels,.side-nav .side-nav-section + .side-nav-section:not(.online-friends)', 'side-rec-channels': '.side-nav .recommended-channels,.side-nav .side-nav-section + .side-nav-section:not(.online-friends)',

View file

@ -1,4 +1,4 @@
query($logins: [String!]) { query FFZ_FollowsList($logins: [String!]) {
users(logins: $logins) { users(logins: $logins) {
id id
login login

View file

@ -1511,7 +1511,7 @@ export default class Player extends Module {
tryTheatreMode(inst) { tryTheatreMode(inst) {
if ( ! inst._ffz_theater_timer ) if ( ! inst._ffz_theater_timer )
inst._ffz_theater_timer = requestAnimationFrame(() => { inst._ffz_theater_timer = setTimeout(() => {
inst._ffz_theater_timer = null; inst._ffz_theater_timer = null;
if ( ! this.settings.get('player.theatre.auto-enter') || ! inst._ffz_mounted ) if ( ! this.settings.get('player.theatre.auto-enter') || ! inst._ffz_mounted )
@ -1525,7 +1525,7 @@ export default class Player extends Module {
if ( inst.props.onTheatreModeEnabled ) if ( inst.props.onTheatreModeEnabled )
inst.props.onTheatreModeEnabled(); inst.props.onTheatreModeEnabled();
}); }, 250);
} }

View file

@ -34,6 +34,18 @@
} }
} }
.channel-panels {
.default-panel {
& > .tw-link {
display: block;
& > * {
pointer-events: none;
}
}
}
}
.tw-root--theme-ffz, .tw-root--theme-ffz.tw-root--theme-dark, .tw-root--theme-dark, body { .tw-root--theme-ffz, .tw-root--theme-ffz.tw-root--theme-dark, .tw-root--theme-dark, body {
.ffz-stat > .tw-button--text, .ffz-stat > .tw-button--text,
.ffz-stat.tw-button--text { .ffz-stat.tw-button--text {

View file

@ -59,6 +59,16 @@ textarea.tw-input {
} }
} }
.ffz--link-tester {
code {
tab-size: 4;
}
label {
min-width: 15rem;
}
}
.ffz-min-width-unset { .ffz-min-width-unset {
min-width: unset !important; min-width: unset !important;