1
0
Fork 0
mirror of https://github.com/FrankerFaceZ/FrankerFaceZ.git synced 2025-08-11 00:20:54 +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

@ -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';
export default {
props: ['data', 'url'],
props: ['data', 'url', 'events'],
data() {
return {
loaded: false,
error: false,
html: null,
error: null,
title: this.t('card.loading', 'Loading...'),
title_tokens: null,
desc_1: null,
@ -26,58 +25,96 @@ export default {
}
},
async mounted() {
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;
}
watch: {
data() {
this.reset();
this.load();
}
},
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)
}
created() {
if ( this.events ) {
this._events = this.events;
this._events.on('chat:update-link-resolver', this.checkRefresh, this);
}
this.loaded = true;
this.error = data.error;
this.html = data.html;
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.load();
},
beforeDestroy() {
if ( this._events ) {
this._events.off('chat:update-link-resolver', this.checkRefresh, this);
this._events = null;
}
},
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) {
if ( this.data.renderBody )
return [this.data.renderBody(h)];
if ( this.html )
return [h('div', {
domProps: {
innerHTML: this.html
}
})];
return [
this.renderImage(h),
this.renderDescription(h)

View file

@ -71,6 +71,32 @@ export default class Chat extends Module {
// 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', {
default: 12,
ui: {
@ -1506,6 +1532,33 @@ export default class Chat extends Module {
// 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) {
let info = this._link_info[url];
const expires = info && info[1];
@ -1536,15 +1589,23 @@ export default class Chat extends Module {
cbs[success ? 0 : 1](data);
}
if ( this.experiments.getAssignment('api_links') )
timeout(fetch(`https://api-test.frankerfacez.com/v2/link?url=${encodeURIComponent(url)}`).then(r => r.json()), 15000)
.then(data => handle(true, data))
.catch(err => handle(false, err));
let provider = this.settings.get('debug.link-resolver.source');
if ( provider == null )
provider = this.experiments.getAssignment('api_links') ? 'test' : 'socket';
else
if ( provider === 'socket' ) {
timeout(this.socket.call('get_link', url), 15000)
.then(data => handle(true, data))
.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,
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_title: data.image_title,
image_square: data.image_square,
title: data.title,
title_tokens: data.title_tokens,
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 ) {
desc_1_tokens = this.i18n.tList('clip.desc.1.playing', '{user} playing {game}', {
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}', {
user,

View file

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