mirror of
https://github.com/FrankerFaceZ/FrankerFaceZ.git
synced 2025-08-03 00:18:31 +00:00
* Added: Link Cards. As an option, you can open a preview card when clicking on links in chat. These preview cards function similarly to the existing tool-tip or rich embed options but can provide for enhanced interaction (e.g. an embedded video player), with potential for more in the future. * Changed: When using a custom theme with a dark background, use lighter border colors. * Changed: Draw the FFZ Control Center and other dialogs with rounded corners. * Fixed: Issue when clicking certain global Twitch emotes preventing the emote card from appearing correctly. * Fixed: Issue with URL/safety data not being loaded correctly from the link service when the overall result was an error. * Fixed: Issue with link tool-tips still appearing, but with no content, when link tool-tips are disabled. * Fixed: Issue where (re)subscription notices in chat for multiple-month-at-once subscriptions would not be displayed correctly. * Fixed: Tool-tips not displaying correctly in chat pop-outs in some circumstances. * Fixed: Incorrect border styles when the chat is in portrait mode. * Experiment Added: Set up an MQTT-based PubSub system. Let's see how well this scales.
603 lines
No EOL
14 KiB
Vue
603 lines
No EOL
14 KiB
Vue
<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-full-width tw-border-top-left-radius-medium tw-border-top-right-radius-medium tw-font-size-6 ffz-select tw-pd-l-1 tw-pd-r-3 tw-pd-y-05"
|
|
@change="onSelectChange"
|
|
>
|
|
<option :selected="is_custom" value="custom">
|
|
{{ t('setting.combo-box.custom', 'Custom') }}
|
|
</option>
|
|
<optgroup
|
|
v-for="prov in examples"
|
|
:key="prov.key"
|
|
:label="prov.key"
|
|
>
|
|
<option
|
|
v-for="i in prov.items"
|
|
:key="i.url"
|
|
:selected="i.url === raw_url"
|
|
:value="i.url"
|
|
>
|
|
{{ i.title ? `${i.title} (${i.url})` : i.url }}
|
|
</option>
|
|
</optgroup>
|
|
</select>
|
|
<input
|
|
ref="text"
|
|
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 ffz-input"
|
|
@blur="updateText"
|
|
@input="onTextChange"
|
|
>
|
|
</div>
|
|
|
|
<button
|
|
class="tw-mg-05 tw-button tw-button--text ffz-il-tooltip__container"
|
|
:class="examples_loading && 'tw-button--disabled'"
|
|
:disabled="examples_loading"
|
|
@click="updateExamples"
|
|
>
|
|
<span class="tw-button__text ffz-i-arrows-cw" />
|
|
<div class="ffz-il-tooltip ffz-il-tooltip--down ffz-il-tooltip--align-right">
|
|
{{ t('debug.link-provider.refresh', 'Refresh') }}
|
|
</div>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="tw-flex tw-mg-b-1 tw-align-items-center">
|
|
<div class="tw-flex-grow-1" />
|
|
|
|
<div class="tw-pd-x-1 ffz-checkbox">
|
|
<input
|
|
id="force_media"
|
|
ref="force_media"
|
|
:checked="force_media"
|
|
type="checkbox"
|
|
class="ffz-checkbox__input"
|
|
@change="onCheck"
|
|
>
|
|
|
|
<label for="force_media" class="ffz-checkbox__label">
|
|
<span class="tw-mg-l-1">
|
|
{{ t('debug.link-provider.allow.media', 'Allow Media') }}
|
|
</span>
|
|
</label>
|
|
</div>
|
|
|
|
<div class="tw-pd-x-1 ffz-checkbox">
|
|
<input
|
|
id="force_unsafe"
|
|
ref="force_unsafe"
|
|
:checked="force_unsafe"
|
|
type="checkbox"
|
|
class="ffz-checkbox__input"
|
|
@change="onCheck"
|
|
>
|
|
|
|
<label for="force_unsafe" class="ffz-checkbox__label">
|
|
<span class="tw-mg-l-1">
|
|
{{ t('debug.link-provider.allow.unsafe', 'Allow NSFW') }}
|
|
</span>
|
|
</label>
|
|
</div>
|
|
|
|
<button
|
|
class="tw-mg-l-1 tw-button tw-button--text"
|
|
:class="es_waiting && 'tw-button--disabled'"
|
|
:disabled="es_waiting"
|
|
@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">
|
|
<div class="tw-flex tw-flex-column">
|
|
<label>
|
|
{{ t('debug.link-provider.link', 'Chat Link') }}
|
|
</label>
|
|
|
|
<div class="tw-pd-t-05 ffz-checkbox">
|
|
<input
|
|
id="force_tooltip"
|
|
ref="force_tooltip"
|
|
:checked="force_tooltip"
|
|
type="checkbox"
|
|
class="ffz-checkbox__input"
|
|
@change="onTooltip"
|
|
>
|
|
|
|
<label for="force_tooltip" class="ffz-checkbox__label">
|
|
<span class="tw-mg-l-1">
|
|
{{ t('debug.link-provider.force-tooltip', 'Force Tooltip') }}
|
|
</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
<div class="tw-full-width tw-overflow-hidden">
|
|
<a
|
|
v-if="url"
|
|
ref="link"
|
|
:href="url"
|
|
:data-url="url"
|
|
class="ffz-tooltip"
|
|
data-tooltip-type="link"
|
|
data-force-tooltip="true"
|
|
:data-force-open="force_tooltip ? 'true' : 'false'"
|
|
:data-force-media="force_media ? 'true' : 'false'"
|
|
:data-force-unsafe="force_unsafe ? 'true' : 'false'"
|
|
data-is-mail="false"
|
|
rel="noopener noreferrer"
|
|
target="_blank"
|
|
>
|
|
{{ decodeURI(url) }}
|
|
</a>
|
|
</div>
|
|
</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"
|
|
:force-mid="false"
|
|
:force-full="false"
|
|
: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.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-full="false"
|
|
: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') }}
|
|
</label>
|
|
<div class="tw-full-width tw-overflow-hidden">
|
|
<chat-rich
|
|
v-if="rich_data"
|
|
:data="rich_data"
|
|
:url="url"
|
|
:force-full="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.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') }}
|
|
</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-if="typeof raw_data === 'string'">{{ raw_data }}</code>
|
|
<code v-else v-html="highlightJson(raw_data, true)"></code>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
|
|
import { debounce, timeout, pick_random } from 'utilities/object'
|
|
import { highlightJson } from 'utilities/dom';
|
|
|
|
const STOCK_URLS = [
|
|
'https://www.twitch.tv/sirstendec',
|
|
'https://www.twitch.tv/videos/42968068',
|
|
'https://www.twitch.tv/sirstendec/clip/HedonisticMagnificentSoymilkChocolateRain',
|
|
'https://clips.twitch.tv/HedonisticMagnificentSoymilkChocolateRain',
|
|
'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',
|
|
'http://testsafebrowsing.appspot.com/apiv4/ANY_PLATFORM/MALWARE/URL/',
|
|
'https://en.wikipedia.org/wiki/Emoji',
|
|
'https://en.wikipedia.org/wiki/Naginata',
|
|
'https://www.smbc-comics.com/comic/punishment'
|
|
]
|
|
|
|
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() {
|
|
const state = window.history.state;
|
|
let url = state?.ffz_lt_url;
|
|
if ( ! url )
|
|
url = pick_random(STOCK_URLS);
|
|
|
|
const stuff = this.formatExamples(STOCK_URLS.map(x => ({url: x})), url);
|
|
|
|
return {
|
|
raw_url: url,
|
|
examples: stuff[0],
|
|
examples_loading: false,
|
|
is_custom: stuff[1],
|
|
rich_data: null,
|
|
raw_loading: false,
|
|
es_waiting: false,
|
|
raw_data: null,
|
|
length: 0,
|
|
|
|
force_media: state?.ffz_lt_media ?? true,
|
|
force_unsafe: state?.ffz_lt_unsafe ?? false,
|
|
force_tooltip: state?.ffz_lt_tip ?? false,
|
|
|
|
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: {
|
|
raw_url() {
|
|
if ( ! this.is_custom )
|
|
this.$refs.text.value = this.raw_url;
|
|
},
|
|
|
|
url() {
|
|
this.rebuildData();
|
|
this.saveState();
|
|
|
|
if ( this.force_tooltip ) {
|
|
const link = this.$refs.link;
|
|
if ( ! link || ! this.chat )
|
|
return;
|
|
|
|
const tips = this.chat.resolve('tooltips')?.tips;
|
|
if ( ! tips )
|
|
return;
|
|
|
|
tips._exit(link);
|
|
setTimeout(() => tips._enter(link), 250);
|
|
}
|
|
},
|
|
|
|
rich_data() {
|
|
this.refreshRaw();
|
|
},
|
|
|
|
force_tooltip() {
|
|
const link = this.$refs.link;
|
|
if ( ! link || ! this.chat )
|
|
return;
|
|
|
|
const tips = this.chat.resolve('tooltips')?.tips;
|
|
if ( ! tips )
|
|
return;
|
|
|
|
if ( this.force_tooltip )
|
|
tips._enter(link);
|
|
else
|
|
tips._exit(link);
|
|
|
|
}
|
|
},
|
|
|
|
created() {
|
|
this.chat = this.item.getChat();
|
|
this.settings = this.chat.resolve('settings');
|
|
|
|
this.refreshES = debounce(this.refresh, 1000, true);
|
|
this.rebuildData = debounce(this.rebuildData, 250);
|
|
this.refreshRaw = debounce(this.refreshRaw, 250);
|
|
this.onTextChange = debounce(this.onTextChange, 500);
|
|
this.updateExamples = debounce(this.updateExamples, 500);
|
|
this.updateEventSource = debounce(this.updateEventSource, 500);
|
|
|
|
this.chat.on('chat:update-link-resolver', this.checkRefreshRaw, this);
|
|
this.settings.on(':changed:debug.link-resolver.source', this.changeProvider, this);
|
|
this.updateExamples();
|
|
this.updateEventSource();
|
|
},
|
|
|
|
mounted() {
|
|
this.rebuildData();
|
|
|
|
this.$refs.text.value = this.raw_url;
|
|
|
|
|
|
if ( this.force_tooltip ) {
|
|
const link = this.$refs.link;
|
|
if ( ! link || ! this.chat )
|
|
return;
|
|
|
|
const tips = this.chat.resolve('tooltips')?.tips;
|
|
if ( ! tips )
|
|
return;
|
|
|
|
tips._enter(link);
|
|
}
|
|
},
|
|
|
|
beforeDestroy() {
|
|
if (this.es) {
|
|
this.es.close();
|
|
this.es = null;
|
|
}
|
|
|
|
this.chat.off('chat:update-link-resolver', this.checkRefreshRaw, this);
|
|
this.settings.off(':changed:debug.link-resolver.source', this.changeProvider, this);
|
|
this.chat = null;
|
|
this.settings = null;
|
|
},
|
|
|
|
methods: {
|
|
changeProvider() {
|
|
this.updateEventSource();
|
|
this.updateExamples();
|
|
},
|
|
|
|
updateEventSource() {
|
|
const provider = this.settings.get('debug.link-resolver.source');
|
|
if ( provider !== 'dev' ) {
|
|
if ( this.es ) {
|
|
this.es.close();
|
|
this.es = null;
|
|
}
|
|
|
|
this.es_waiting = false;
|
|
return;
|
|
}
|
|
|
|
if ( this.es )
|
|
return;
|
|
|
|
this.es = new EventSource('https://localhost:8002/sse');
|
|
this.es.addEventListener('error', () => {
|
|
this.es_waiting = true;
|
|
});
|
|
this.es.addEventListener('message', () => {
|
|
if ( this.es_waiting ) {
|
|
this.es_waiting = false;
|
|
this.refreshES();
|
|
}
|
|
})
|
|
},
|
|
|
|
saveState() {
|
|
try {
|
|
window.history.replaceState({
|
|
...window.history.state,
|
|
ffz_lt_url: this.raw_url,
|
|
ffz_lt_media: this.force_media,
|
|
ffz_lt_unsafe: this.force_unsafe,
|
|
ffz_lt_tip: this.force_tooltip
|
|
}, document.title);
|
|
|
|
} catch(err) {
|
|
/* no-op */
|
|
}
|
|
},
|
|
|
|
formatExamples(examples, url) {
|
|
const out = [],
|
|
resolvers = {};
|
|
|
|
if ( url === undefined )
|
|
url = this.raw_url;
|
|
|
|
let is_custom = true;
|
|
|
|
for(const example of examples) {
|
|
const resolver = example.resolver || 'Unknown';
|
|
let prov = resolvers[resolver];
|
|
if ( ! prov ) {
|
|
prov = resolvers[resolver] = [];
|
|
out.push({
|
|
key: resolver,
|
|
items: prov
|
|
});
|
|
}
|
|
|
|
if ( url === example.url )
|
|
is_custom = false;
|
|
|
|
prov.push(example);
|
|
}
|
|
|
|
out.sort((a,b) => a.key.localeCompare(b.key));
|
|
|
|
return [out, is_custom];
|
|
},
|
|
|
|
async updateExamples() {
|
|
if ( this.examples_loading )
|
|
return;
|
|
|
|
this.examples_loading = true;
|
|
const provider = this.settings.get('debug.link-resolver.source');
|
|
let examples;
|
|
if ( provider === 'dev' ) {
|
|
try {
|
|
examples = (await timeout(fetch('https://localhost:8002/examples'), 15000).then(resp => resp.ok ? resp.json() : null)).examples;
|
|
} catch(err) {
|
|
console.error(err);
|
|
}
|
|
}
|
|
|
|
if ( ! examples ) {
|
|
try {
|
|
examples = (await timeout(fetch('https://api-test.frankerfacez.com/v2/link/examples'), 15000).then(resp => resp.ok ? resp.json() : null)).examples;
|
|
} catch(err) {
|
|
console.error(err);
|
|
}
|
|
}
|
|
|
|
if ( ! examples )
|
|
examples = [];
|
|
|
|
const urls = examples.map(x => x.url);
|
|
for(const url of STOCK_URLS)
|
|
if ( ! urls.includes(url) )
|
|
examples.push({
|
|
url,
|
|
resolver: ' Stock'
|
|
});
|
|
|
|
const out = this.formatExamples(examples);
|
|
|
|
this.examples = out[0];
|
|
this.is_custom = out[1];
|
|
this.examples_loading = false;
|
|
},
|
|
|
|
checkRefreshRaw(url) {
|
|
if ( ! url || (url && url === this.url) )
|
|
this.refreshRaw();
|
|
},
|
|
|
|
async refreshRaw() {
|
|
this.raw_data = undefined;
|
|
this.length = 0;
|
|
if ( ! this.rich_data ) {
|
|
this.raw_loading = false;
|
|
return;
|
|
}
|
|
|
|
this.raw_loading = true;
|
|
try {
|
|
const data = await this.chat.get_link_info(this.url);
|
|
this.raw_data = data; //JSON.stringify(data, null, '\t');
|
|
this.length = JSON.stringify(data).length;
|
|
} 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 raw_value = this.$refs.selector.value;
|
|
|
|
if ( raw_value && raw_value !== 'custom' ) {
|
|
this.raw_url = raw_value;
|
|
this.is_custom = false;
|
|
} else
|
|
this.is_custom = true;
|
|
},
|
|
|
|
updateText() {
|
|
const value = this.$refs.text.value;
|
|
let custom = true;
|
|
for(const provider of this.examples) {
|
|
for(const url of provider.items) {
|
|
if ( url === value ) {
|
|
custom = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if ( ! custom )
|
|
break;
|
|
}
|
|
|
|
this.is_custom = custom;
|
|
if ( this.is_custom )
|
|
this.raw_url = this.$refs.text.value;
|
|
},
|
|
|
|
onTextChange() {
|
|
this.updateText();
|
|
},
|
|
|
|
onCheck() {
|
|
this.force_media = this.$refs.force_media.checked;
|
|
this.force_unsafe = this.$refs.force_unsafe.checked;
|
|
|
|
this.saveState();
|
|
},
|
|
|
|
onTooltip() {
|
|
this.force_tooltip = this.$refs.force_tooltip.checked;
|
|
|
|
this.saveState();
|
|
},
|
|
|
|
highlightJson(object, pretty) {
|
|
return highlightJson(object, pretty);
|
|
},
|
|
}
|
|
|
|
}
|
|
|
|
</script> |