diff --git a/package.json b/package.json index 19eaccc0..241c4c68 100755 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "frankerfacez", "author": "Dan Salvato LLC", - "version": "4.54.0", + "version": "4.55.0", "description": "FrankerFaceZ is a Twitch enhancement suite.", "private": true, "license": "Apache-2.0", @@ -73,6 +73,7 @@ "sortablejs": "^1.14.0", "sourcemapped-stacktrace": "^1.1.11", "text-diff": "^1.0.1", + "u8-mqtt": "^0.5.3", "vue": "^2.6.14", "vue-clickaway": "^2.2.2", "vue-color": "^2.8.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bfd1540e..5d8234a7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -69,6 +69,9 @@ dependencies: text-diff: specifier: ^1.0.1 version: 1.0.1 + u8-mqtt: + specifier: ^0.5.3 + version: 0.5.3 vue: specifier: ^2.6.14 version: 2.6.14 @@ -2985,7 +2988,7 @@ packages: dev: true /fs.realpath@1.0.0: - resolution: {integrity: sha1-FQStJSMVjKpA20onh8sBQRmU6k8=} + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} dev: true /fsevents@2.3.2: @@ -3359,7 +3362,7 @@ packages: dev: true /inflight@1.0.6: - resolution: {integrity: sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=} + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} dependencies: once: 1.4.0 wrappy: 1.0.2 @@ -4180,7 +4183,7 @@ packages: dev: true /once@1.4.0: - resolution: {integrity: sha1-WDsap3WWHUsROsF9nFC6753Xa9E=} + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} dependencies: wrappy: 1.0.2 dev: true @@ -5320,6 +5323,10 @@ packages: is-typed-array: 1.1.12 dev: true + /u8-mqtt@0.5.3: + resolution: {integrity: sha512-C9eaN2/kxtmMhLVrKT8Yk6a3pRj12K+nNpylDqUn/rKYwAaMEUnvXNWqd4QMd/EaKKcMxpeA9cyCU8DlUOvKsw==} + dev: false + /uc.micro@1.0.6: resolution: {integrity: sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==} dev: false @@ -5371,7 +5378,7 @@ packages: dev: true /util-deprecate@1.0.2: - resolution: {integrity: sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=} + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} /utils-merge@1.0.1: resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} @@ -5841,7 +5848,7 @@ packages: dev: true /wrappy@1.0.2: - resolution: {integrity: sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=} + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} dev: true /ws@8.13.0: diff --git a/src/experiments.json b/src/experiments.json index 1edc1968..f1ae351d 100644 --- a/src/experiments.json +++ b/src/experiments.json @@ -14,5 +14,13 @@ {"value": true, "weight": 30}, {"value": false, "weight": 70} ] + }, + "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": 50}, + {"value": false, "weight": 50} + ] } } \ No newline at end of file diff --git a/src/main.js b/src/main.js index 6af53cca..f0c54798 100644 --- a/src/main.js +++ b/src/main.js @@ -14,7 +14,7 @@ import AddonManager from './addons'; import ExperimentManager from './experiments'; import {TranslationManager} from './i18n'; import SocketClient from './socket'; -//import PubSubClient from './pubsub'; +import PubSubClient from './pubsub'; import Site from 'site'; import Vue from 'utilities/vue'; import StagingSelector from './staging'; @@ -61,7 +61,7 @@ class FrankerFaceZ extends Module { this.inject('staging', StagingSelector); this.inject('load_tracker', LoadTracker); this.inject('socket', SocketClient); - //this.inject('pubsub', PubSubClient); + this.inject('pubsub', PubSubClient); this.inject('site', Site); this.inject('addons', AddonManager); diff --git a/src/modules/chat/components/chat-rich.vue b/src/modules/chat/components/chat-rich.vue index 908fb40c..dab22448 100644 --- a/src/modules/chat/components/chat-rich.vue +++ b/src/modules/chat/components/chat-rich.vue @@ -1,5 +1,6 @@ \ No newline at end of file diff --git a/src/modules/link_card/index.jsx b/src/modules/link_card/index.jsx new file mode 100644 index 00000000..ed5f9fa7 --- /dev/null +++ b/src/modules/link_card/index.jsx @@ -0,0 +1,197 @@ +'use strict'; + +// ============================================================================ +// Link Cards +// ============================================================================ + +import { createElement } from 'utilities/dom'; +import { deep_copy } from 'utilities/object'; + +import Module from 'utilities/module'; + + +export default class LinkCard extends Module { + + constructor(...args) { + super(...args); + + this.should_enable = true; + + this.inject('i18n'); + this.inject('chat'); + this.inject('site'); + this.inject('settings'); + + this.vue = this.resolve('vue'); + + this.settings.add('link-cards.enable', { + default: false, + ui: { + path: 'Chat > Link Cards >> General', + title: 'Enable Link Cards.', + description: 'When this is enabled and you click a link in chat or whispers, a popup will open with information about the link. This provides the same data as rich link tooltips, but in a form that allows more interaction.', + component: 'setting-check-box' + } + }); + + this.settings.add('link-cards.use-destination', { + default: false, + ui: { + path: 'Chat > Link Cards >> General', + title: 'Bypass Known Shorteners', + description: 'When clicking "Open Link" from a Link Card with this enabled, you will bypass known shorteners and tracking services and go directly to the destination URL.', + component: 'setting-check-box' + } + }); + + this.last_z = 9000; + this.open_cards = {}; + this.last_card = null; + } + + onEnable() { + this.on('chat:click-link', this.handleClick, this); + } + + handleClick(evt) { + evt.preventDefault(); + + this.openCard(evt.url, evt.source); + } + + async loadVue() { + if ( this._vue_loaded ) + return; + + await this.vue.enable(); + const card_component = await import(/* webpackChunkName: 'emote-cards' */ './components/card.vue'); + this.vue.component('link-card', card_component.default); + + this.vue.component('lc-url', { + functional: true, + props: ['url', 'show-protocol'], + render(createElement, context) { + + let url = context.props.url; + if ( !(url instanceof URL) ) + url = new URL(url); + + const out = []; + + if ( context.props.showProtocol ) + out.push(createElement('span', { + class: 'tw-c-text-alt-2' + }, `${url.protocol}//`)); + + out.push(createElement('span', url.host)); + + let suffix = url.toString().slice(url.origin.length); + + if ( suffix.length && suffix !== '/' ) + out.push(createElement('span', { + class: 'tw-c-text-alt-2' + }, suffix)); + + return createElement('span', out); + } + }); + + this._vue_loaded = true; + } + + async openCard(link, event) { + const card_key = `${link}`, + old_card = this.open_cards[card_key]; + + if ( old_card ) { + old_card.$el.style.zIndex = ++this.last_z; + old_card.focus(); + return; + } + + let pos_x = event ? event.clientX : window.innerWidth / 2, + pos_y = event ? event.clientY + 15 : window.innerHeight / 2; + + /*if ( this.last_card ) { + const card = this.last_card; + + if ( ! event ) { + pos_x = card.$el.offsetLeft; + pos_y = card.$el.offsetTop; + } + + card.close(); + }*/ + + // Start loading data. Don't await it yet, so we can + // wait for Vue at the same time. + const data = this.chat.get_link_info(link); + + // Now load vue. + await this.loadVue(); + + // Display the card. + this.last_card = this.open_cards[card_key] = this.buildCard( + pos_x, + pos_y, + link, + data + ); + } + + buildCard(pos_x, pos_y, link, data) { + let child; + + const component = new this.vue.Vue({ + el: createElement('div'), + render: h => h('link-card', { + props: { + url: link, + data: data, + + use_dest: this.settings.get('link-cards.use-destination'), + + getFFZ: () => this, + getZ: () => ++this.last_z + }, + + on: { + emit: (event, ...data) => this.emit(event, ...data), + + close: () => { + const el = component.$el; + el.remove(); + component.$destroy(); + + if ( this.last_card === child ) + this.last_card = null; + + const card_key = link; + if ( this.open_cards[card_key] === child ) + this.open_cards[card_key] = null; + + this.emit('tooltips:cleanup'); + }, + + pin: () => { + if ( this.last_card === child ) + this.last_card = null; + } + } + }) + }); + + child = component.$children[0]; + + const el = component.$el; + el.style.left = `${pos_x}px`; + el.style.top = `${pos_y}px`; + + const container = document.querySelector(this.site.constructor.DIALOG_SELECTOR ?? '#root>div>.tw-full-height,.twilight-minimal-root>.tw-full-height'); + container.appendChild(el); + + requestAnimationFrame(() => child.constrain()); + + return child; + } +} \ No newline at end of file diff --git a/src/modules/main_menu/components/link-tester.vue b/src/modules/main_menu/components/link-tester.vue index 4630ac63..16d0001a 100644 --- a/src/modules/main_menu/components/link-tester.vue +++ b/src/modules/main_menu/components/link-tester.vue @@ -152,6 +152,8 @@ 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" @@ -168,6 +170,7 @@ :data="rich_data" :url="url" :force-mid="true" + :force-full="false" :force-media="force_media" :force-unsafe="force_unsafe" :events="events" @@ -207,7 +210,8 @@
{{ raw_data }}
+ {{ raw_data }}
+
@@ -215,7 +219,9 @@