diff --git a/package.json b/package.json
index f68f80eb..fffea042 100755
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "frankerfacez",
"author": "Dan Salvato LLC",
- "version": "4.51.1",
+ "version": "4.52.0",
"description": "FrankerFaceZ is a Twitch enhancement suite.",
"private": true,
"license": "Apache-2.0",
diff --git a/src/bridge.js b/src/bridge.js
index 7b36cac5..fb9441a8 100644
--- a/src/bridge.js
+++ b/src/bridge.js
@@ -217,7 +217,7 @@ const VER = FFZBridge.version_info = Object.freeze({
build: __version_build__,
hash: __webpack_hash__,
toString: () =>
- `${VER.major}.${VER.minor}.${VER.revision}${VER.extra || ''}${DEBUG ? '-dev' : ''}${VER.build ? `+${VER.build}` : ''}`
+ `${VER.major}.${VER.minor}.${VER.revision}${VER.build ? `.${VER.build}` : ''}${VER.extra || ''}${DEBUG ? '-dev' : ''}`
});
window.FFZBridge = FFZBridge;
diff --git a/src/clips.js b/src/clips.js
index 9ea2a23e..883d1883 100644
--- a/src/clips.js
+++ b/src/clips.js
@@ -17,8 +17,11 @@ import StagingSelector from './staging';
import LoadTracker from './load_tracker';
import Site from './sites/clips';
+import Vue from 'utilities/vue';
+
import Tooltips from 'src/modules/tooltips';
import Chat from 'src/modules/chat';
+import EmoteCard from 'src/modules/emote_card';
class FrankerFaceZ extends Module {
constructor() {
@@ -59,27 +62,46 @@ class FrankerFaceZ extends Module {
this.inject('site', Site);
this.inject('addons', AddonManager);
+ this.register('vue', Vue);
+
// ========================================================================
// Startup
// ========================================================================
this.inject('tooltips', Tooltips);
- this.register('chat', Chat);
- this.enable().then(() => {
- const duration = performance.now() - start_time;
- this.core_log.info(`Initialization complete in ${duration.toFixed(5)}ms.`);
- this.log.init = false;
- }).catch(err => {
- this.core_log.error(`An error occurred during initialization.`, err);
- this.log.init = false;
- });
+ this.register('chat', Chat);
+ this.register('emote_card', EmoteCard);
+
+ this.enable()
+ .then(() => this.enableInitialModules())
+ .then(() => {
+ const duration = performance.now() - start_time;
+ this.core_log.info(`Initialization complete in ${duration.toFixed(5)}ms.`);
+ this.log.init = false;
+
+ }).catch(err => {
+ this.core_log.error(`An error occurred during initialization.`, err);
+ this.log.init = false;
+ });
}
static get() {
return FrankerFaceZ.instance;
}
+ async enableInitialModules() {
+ const promises = [];
+ /* eslint guard-for-in: off */
+ for(const key in this.__modules) {
+ const module = this.__modules[key];
+ if ( module instanceof Module && module.should_enable )
+ promises.push(module.enable());
+ }
+
+ await Promise.all(promises);
+ }
+
// ========================================================================
// Generate Log
// ========================================================================
@@ -134,12 +156,13 @@ const VER = FrankerFaceZ.version_info = Object.freeze({
build: __version_build__,
hash: __webpack_hash__,
toString: () =>
- `${VER.major}.${VER.minor}.${VER.revision}${VER.extra || ''}${DEBUG ? '-dev' : ''}${VER.build ? `+${VER.build}` : ''}`
+ `${VER.major}.${VER.minor}.${VER.revision}${VER.build ? `.${VER.build}` : ''}${VER.extra || ''}${DEBUG ? '-dev' : ''}`
});
// We don't support addons in the player right now, so
FrankerFaceZ.utilities = {
addon: require('utilities/addon'),
+ blobs: require('utilities/blobs'),
color: require('utilities/color'),
constants: require('utilities/constants'),
dom: require('utilities/dom'),
diff --git a/src/entry_ext.js b/src/entry_ext.js
index 4f32cc4e..93e47954 100644
--- a/src/entry_ext.js
+++ b/src/entry_ext.js
@@ -5,7 +5,9 @@
if ( /^(?:localhost\.rig|blog|im|chatdepot|tmi|api|brand|dev|gql|passport)\./.test(location.hostname) )
return;
- const HOST = location.hostname,
+ const browser = globalThis.browser ?? globalThis.chrome,
+
+ HOST = location.hostname,
SERVER = browser.runtime.getURL("web"),
script = document.createElement('script');
diff --git a/src/esbridge.js b/src/esbridge.js
new file mode 100644
index 00000000..ec26b1b3
--- /dev/null
+++ b/src/esbridge.js
@@ -0,0 +1,158 @@
+'use strict';
+
+import Logger from 'utilities/logging';
+
+
+class FFZESBridge {
+
+ constructor() {
+
+ FFZESBridge.instance = this;
+
+ this.host = 'null';
+ this.flavor = 'esbridge';
+
+ // ========================================================================
+ // Logging
+ // ========================================================================
+
+ this.log = new Logger(null, null, null);
+ this.log.label = 'FFZESBridge';
+
+ this.core_log = this.log.get('core');
+ this.log.hi(this);
+
+ // ========================================================================
+ // Startup
+ // ========================================================================
+
+ this.onWindowMessage = this.onWindowMessage.bind(this);
+ this.onRuntimeDisconnect = this.onRuntimeDisconnect.bind(this);
+ this.onRuntimeMessage = this.onRuntimeMessage.bind(this);
+
+ window.addEventListener('message', this.onWindowMessage);
+
+ document.addEventListener('readystatechange', event => {
+ if ( document.documentElement )
+ document.documentElement.dataset.ffzEsbridge = true;
+ });
+ }
+
+ static get() {
+ return FFZESBridge.instance;
+ }
+
+ // ========================================================================
+ // Window Communication
+ // ========================================================================
+
+ windowSend(msg, transfer) {
+ if ( typeof msg === 'string' )
+ msg = {ffz_esb_type: msg};
+
+ try {
+ window.postMessage(
+ msg,
+ location.origin,
+ transfer ? (Array.isArray(transfer) ? transfer : [transfer]) : undefined
+ );
+ } catch(err) {
+ this.log.error('Error sending message to window.', err, msg, transfer);
+ }
+ }
+
+ onWindowMessage(event) {
+ if ( event.origin !== location.origin )
+ return;
+
+ const msg = event.data,
+ id = msg?.id,
+ type = msg?.ffz_esb_type;
+
+ if ( ! type )
+ return;
+
+ this.log.info('Received Message from Page', type, id, msg);
+
+ if ( type === 'init' ) {
+ this.received_init = true;
+ if ( this.active )
+ this.runtimeHeartbeat();
+ }
+
+ this.runtimeSend(msg);
+ }
+
+ // ========================================================================
+ // Runtime Communication
+ // ========================================================================
+
+ runtimeOpen() {
+ if ( this.active )
+ return Promise.resolve();
+
+ this.log.info('Connecting to worker.');
+
+ this.port = (window.browser ?? window.chrome).runtime.connect({name: 'esbridge'});
+
+ this.port.onMessage.addListener(this.onRuntimeMessage);
+ this.port.onDisconnect.addListener(this.onRuntimeDisconnect);
+
+ if ( this.received_init )
+ this.runtimeHeartbeat();
+ }
+
+ onRuntimeMessage(msg) {
+ this.windowSend(msg);
+ }
+
+ onRuntimeDisconnect(...args) {
+ this.log.info('Disconnected from worker.', args);
+ this.active = false;
+ this.port = null;
+ if ( this._heartbeat ) {
+ clearInterval(this._heartbeat);
+ this._heartbeat = null;
+ }
+ }
+
+ runtimeHeartbeat() {
+ if ( this._heartbeat )
+ return;
+
+ this._heartbeat = setInterval(() => {
+ if ( this.active )
+ this.runtimeSend('heartbeat');
+ }, 30000);
+ }
+
+ runtimeSend(msg) {
+ if ( typeof msg === 'string' )
+ msg = {ffz_esb_type: msg};
+
+ if ( ! this.active )
+ // We need to create our port.
+ this.runtimeOpen();
+
+ // Send the message, knowing we have an open port.
+ this.port.postMessage(msg);
+ }
+
+};
+
+FFZESBridge.Logger = Logger;
+
+const VER = FFZESBridge.version_info = Object.freeze({
+ major: __version_major__,
+ minor: __version_minor__,
+ revision: __version_patch__,
+ extra: __version_prerelease__?.length && __version_prerelease__[0],
+ commit: __git_commit__,
+ build: __version_build__,
+ hash: __webpack_hash__,
+ toString: () =>
+ `${VER.major}.${VER.minor}.${VER.revision}${VER.extra || ''}${VER.build ? `+${VER.build}` : ''}`
+});
+
+window.FFZESBridge = FFZESBridge;
+window.ffz_esbridge = new FFZESBridge();
diff --git a/src/main.js b/src/main.js
index e53a7957..6af53cca 100644
--- a/src/main.js
+++ b/src/main.js
@@ -138,7 +138,13 @@ ${typeof x[1] === 'string' ? x[1] : JSON.stringify(x[1], null, 4)}`).join('\n\n'
async discoverModules() {
// TODO: Actually do async modules.
- const ctx = await require.context('src/modules', true, /(?:^(?:\.\/)?[^/]+|index)\.jsx?$/ /*, 'lazy-once' */);
+ const ctx = await require.context(
+ 'src/modules',
+ true,
+ /(?:^(?:\.\/)?[^/]+|index)\.jsx?$/
+ /*, 'lazy-once' */
+ );
+
const modules = this.populate(ctx, this.core_log);
this.core_log.info(`Loaded descriptions of ${Object.keys(modules).length} modules.`);
@@ -169,12 +175,13 @@ const VER = FrankerFaceZ.version_info = Object.freeze({
build: __version_build__,
hash: __webpack_hash__,
toString: () =>
- `${VER.major}.${VER.minor}.${VER.revision}${VER.extra || ''}${DEBUG ? '-dev' : ''}${VER.build ? `+${VER.build}` : ''}`
+ `${VER.major}.${VER.minor}.${VER.revision}${VER.build ? `.${VER.build}` : ''}${VER.extra || ''}${DEBUG ? '-dev' : ''}`
});
FrankerFaceZ.utilities = {
addon: require('utilities/addon'),
+ blobs: require('utilities/blobs'),
color: require('utilities/color'),
constants: require('utilities/constants'),
dialog: require('utilities/dialog'),
diff --git a/src/modules/chat/actions/index.jsx b/src/modules/chat/actions/actions.jsx
similarity index 100%
rename from src/modules/chat/actions/index.jsx
rename to src/modules/chat/actions/actions.jsx
diff --git a/src/modules/chat/badges.jsx b/src/modules/chat/badges.jsx
index cbf8c27f..ba910f34 100644
--- a/src/modules/chat/badges.jsx
+++ b/src/modules/chat/badges.jsx
@@ -665,7 +665,7 @@ export default class Badges extends Module {
bdata = tb && tb[badge_id],
cat = bdata && bdata.__cat || 'm-twitch';
- if ( is_hidden || (is_hidden == null && hidden_badges[cat]) )
+ if ( ! badge_id || is_hidden || (is_hidden == null && hidden_badges[cat]) )
continue;
if ( has(BADGE_POSITIONS, badge_id) )
diff --git a/src/modules/chat/index.js b/src/modules/chat/index.js
index 9f05befa..3ce2ef57 100644
--- a/src/modules/chat/index.js
+++ b/src/modules/chat/index.js
@@ -22,7 +22,7 @@ import * as TOKENIZERS from './tokenizers';
import * as RICH_PROVIDERS from './rich_providers';
import * as LINK_PROVIDERS from './link_providers';
-import Actions from './actions';
+import Actions from './actions/actions';
import { getFontsList } from 'src/utilities/fonts';
function sortPriorityColorTerms(list) {
@@ -77,7 +77,7 @@ export default class Chat extends Module {
this.inject(Emotes);
this.inject(Emoji);
this.inject(Actions);
- this.inject(Overrides);
+ this.inject('overrides', Overrides);
this._link_info = {};
diff --git a/src/modules/chat/tokenizers.jsx b/src/modules/chat/tokenizers.jsx
index ed0e412a..b54a209c 100644
--- a/src/modules/chat/tokenizers.jsx
+++ b/src/modules/chat/tokenizers.jsx
@@ -1486,6 +1486,7 @@ export const AddonEmotes = {
modifiers = ds.modifierInfo;
let name, preview, source, artist, owner, mods, fav_source, emote_id,
+ original_name,
plain_name = false;
const hide_source = ds.noSource === 'true';
@@ -1577,6 +1578,12 @@ export const AddonEmotes = {
if ( emote.artist )
artist = emote.artist.display_name || emote.artist.name;
+ if ( emote.original_name && emote.original_name !== emote.name )
+ original_name = this.i18n.t(
+ 'emote.original-name', 'Name: {name}',
+ {name: emote.original_name}
+ );
+
if ( emote.owner )
owner = this.i18n.t(
'emote.owner', 'By: {owner}',
@@ -1751,12 +1758,18 @@ export const AddonEmotes = {
onLoad={tip.update}
/>) : preview),
- plain_name || (hide_source && ! owner) ? name : this.i18n.t('tooltip.emote', 'Emote: {name}', {name}),
+ plain_name || (hide_source && ! owner)
+ ? name
+ : this.i18n.t('tooltip.emote', 'Emote: {name}', {name}),
! hide_source && source && this.context.get('tooltip.emote-sources') && (
{source}
),
+ original_name && (
+ {original_name}
+
),
+
owner && this.context.get('tooltip.emote-sources') && (
{owner}
),
diff --git a/src/modules/emote_card/components/card.vue b/src/modules/emote_card/components/card.vue
index f5bfcb9c..c2dd686a 100644
--- a/src/modules/emote_card/components/card.vue
+++ b/src/modules/emote_card/components/card.vue
@@ -22,7 +22,11 @@
-
+
{{ emote ? emote.name : raw_emote.name }}
{{ emote.source_i18n ? t(emote.source_i18n, emote.source) : emote.source }}
+
+
+
+ {{ emote.originalName }}
+
+
+
- `${VER.major}.${VER.minor}.${VER.revision}${VER.extra || ''}${DEBUG ? '-dev' : ''}${VER.build ? `+${VER.build}` : ''}`
+ `${VER.major}.${VER.minor}.${VER.revision}${VER.build ? `.${VER.build}` : ''}${VER.extra || ''}${DEBUG ? '-dev' : ''}`
});
// We don't support addons in the player right now, so a few
// of these are unavailable.
FrankerFaceZ.utilities = {
addon: require('utilities/addon'),
+ blobs: require('utilities/blobs'),
//color: require('utilities/color'),
constants: require('utilities/constants'),
dom: require('utilities/dom'),
diff --git a/src/settings/providers.js b/src/settings/providers.js
index 758e3934..3a644aa8 100644
--- a/src/settings/providers.js
+++ b/src/settings/providers.js
@@ -1,6 +1,8 @@
'use strict';
-import { isValidBlob, deserializeBlob, serializeBlob } from 'src/utilities/blobs';
+import { isValidBlob, deserializeBlob, serializeBlob } from 'utilities/blobs';
+import { EXTENSION } from 'utilities/constants';
+
// ============================================================================
// Settings Providers
// ============================================================================
@@ -1254,4 +1256,269 @@ export class CrossOriginStorageBridge extends SettingsProvider {
this._rpc.delete(id);
cbs[success ? 0 : 1](msg);
}
-}
\ No newline at end of file
+}
+
+
+// ============================================================================
+// ExtensionStorage
+// ============================================================================
+
+/*export class ExtensionStorageBridge extends SettingsProvider {
+ constructor(manager, start = true) {
+ super(manager);
+
+ this._start_time = performance.now();
+
+ this._rpc = new Map;
+
+ this._cached = new Map;
+ this.resolved_ready = false;
+ this.ready = false;
+ this._ready_wait = null;
+
+ this._last_id = 0;
+
+ if ( start ) {
+ window.addEventListener('message', this.onMessage.bind(this));
+ this.send('init');
+ }
+ }
+
+ // Static Properties
+
+ static supported() { return EXTENSION && document.documentElement.dataset.ffzEsbridge; }
+ static hasContent(manager) {
+ if ( ! ExtensionStorageBridge.supported(manager) )
+ return Promise.resolve(false);
+
+ return new Promise((s,f) => {
+ const onMessage = event => {
+ if ( event.origin !== location.origin )
+ return;
+
+ const msg = event.data,
+ type = msg?.ffz_esb_type;
+ if ( type === 'content-state' ) {
+ window.removeEventListener('message', onMessage);
+ s(msg.has_content);
+ }
+
+ else if ( type === 'error' && msg.id === -1 ) {
+ window.removeEventListener('message', onMessage);
+ s(false);
+ }
+ };
+
+ window.addEventListener('message', onMessage);
+ window.postMessage({ffz_esb_type: 'check-content', id: -1});
+ });
+ }
+
+ static key = 'ext';
+ static priority = 200;
+ static title = 'Extension Storage';
+ static description = 'This provider stores your settings within the FrankerFaceZ extension context, rather than the webpage context. Extension storage is independent from individual websites and should not be cleared automatically by your browser.';
+ static supportsBlobs = true;
+
+ // Initialization
+
+ _resolveReady(success, data) {
+ if ( this.manager )
+ this.manager.log.info(`ESB ready in ${(performance.now() - this._start_time).toFixed(5)}ms`);
+
+ this.resolved_ready = true;
+ this.ready = success;
+ const waiters = this._ready_wait;
+ this._ready_wait = null;
+ if ( waiters )
+ for(const pair of waiters)
+ pair[success ? 0 : 1](data);
+ }
+
+ awaitReady() {
+ if ( this.resolved_ready ) {
+ if ( this.ready )
+ return Promise.resolve();
+ return Promise.reject();
+ }
+
+ return new Promise((s,f) => {
+ const waiters = this._ready_wait = this._ready_wait || [];
+ waiters.push([s,f]);
+ })
+ }
+
+
+ // Provider Methods
+
+ get(key, default_value) {
+ return this._cached.has(key) ? this._cached.get(key) : default_value;
+ }
+
+ set(key, value) {
+ if ( value === undefined ) {
+ if ( this.has(key) )
+ this.delete(key);
+ return;
+ }
+
+ this._cached.set(key, value);
+ this.rpc({ffz_esb_type: 'set', key, value})
+ .catch(err => this.manager.log.error('Error setting value', err));
+ this.emit('set', key, value, false);
+ }
+
+ delete(key) {
+ this._cached.delete(key);
+ this.rpc({ffz_esb_type: 'delete', key})
+ .catch(err => this.manager.log.error('Error deleting value', err));
+ this.emit('set', key, undefined, true);
+ }
+
+ clear() {
+ const old_cache = this._cached;
+ this._cached = new Map;
+ for(const key of old_cache.keys())
+ this.emit('changed', key, undefined, true);
+
+ this.rpc('clear').catch(err => this.manager.log.error('Error clearing storage', err));
+ }
+
+ has(key) { return this._cached.has(key); }
+ keys() { return this._cached.keys(); }
+ entries() { return this._cached.entries(); }
+ get size() { return this._cached.size; }
+
+ async flush() {
+ await this.rpc('flush');
+ }
+
+
+ // Provider Methods: Blobs
+
+ async getBlob(key) {
+ const msg = await this.rpc({ffz_esb_type: 'get-blob', key});
+ return msg.reply && deserializeBlobForExt(msg.reply);
+ }
+
+ async setBlob(key, value) {
+ await this.rpc({
+ ffz_esb_type: 'set-blob',
+ key,
+ value: await serializeBlobForExt(value)
+ });
+ }
+
+ async deleteBlob(key) {
+ await this.rpc({
+ ffz_esb_type: 'delete-blob',
+ key
+ });
+ }
+
+ async hasBlob(key) {
+ const msg = await this.rpc({ffz_esb_type: 'has-blob', key});
+ return msg.reply;
+ }
+
+ async clearBlobs() {
+ await this.rpc('clear-blobs');
+ }
+
+ async blobKeys() {
+ const msg = await this.rpc('blob-keys');
+ return msg.reply;
+ }
+
+
+ // CORS Communication
+
+ send(msg, transfer) {
+ if ( typeof msg === 'string' )
+ msg = {ffz_esb_type: msg};
+
+ try {
+ window.postMessage(
+ msg,
+ location.origin,
+ transfer ? (Array.isArray(transfer) ? transfer : [transfer]) : undefined
+ );
+ } catch(err) {
+ this.manager.log.error('Error sending message to bridge.', err, msg, transfer);
+ }
+ }
+
+ rpc(msg, transfer) {
+ const id = ++this._last_id;
+
+ return new Promise((s,f) => {
+ this._rpc.set(id, [s,f]);
+
+ if ( typeof msg === 'string' )
+ msg = {ffz_esb_type: msg};
+
+ msg.id = id;
+
+ this.send(msg, transfer);
+ });
+ }
+
+ onMessage(event) {
+ if ( event.origin !== location.origin )
+ return;
+
+ const msg = event.data;
+ if ( ! msg || ! msg.ffz_esb_type )
+ return;
+
+ if ( msg.ffz_esb_type === 'ready' )
+ this.rpc('init-load').then(msg => {
+ for(const [key, value] of Object.entries(msg.reply.values))
+ this._cached.set(key, value);
+
+ this._resolveReady(true);
+ }).catch(err => {
+ this._resolveReady(false, err);
+ });
+
+ else if ( msg.ffz_esb_type === 'change' )
+ this.onChange(msg);
+
+ else if ( msg.ffz_esb_type === 'change-blob' )
+ this.emit('changed-blob', msg.key, msg.deleted);
+
+ else if ( msg.ffz_esb_type === 'clear-blobs' )
+ this.emit('clear-blobs');
+
+ else if ( msg.ffz_esb_type === 'reply' || msg.ffz_esb_type === 'reply-error' )
+ this.onReply(msg);
+
+ else
+ this.manager.log.warn('Unknown Message', msg.ffz_esb_type, msg);
+ }
+
+ onChange(msg) {
+ const key = msg.key,
+ value = msg.value,
+ deleted = msg.deleted;
+
+ if ( deleted ) {
+ this._cached.delete(key);
+ this.emit('changed', key, undefined, true);
+ } else {
+ this._cached.set(key, value);
+ this.emit('changed', key, value, false);
+ }
+ }
+
+ onReply(msg) {
+ const id = msg.id,
+ success = msg.ffz_esb_type === 'reply',
+ cbs = this._rpc.get(id);
+ if ( ! cbs )
+ return this.manager.log.warn('Received reply for unknown ID', id);
+
+ this._rpc.delete(id);
+ cbs[success ? 0 : 1](msg);
+ }
+}*/
\ No newline at end of file
diff --git a/src/sites/clips/css_tweaks/player-fade-paused.scss b/src/sites/clips/css_tweaks/player-fade-paused.scss
new file mode 100644
index 00000000..b637519d
--- /dev/null
+++ b/src/sites/clips/css_tweaks/player-fade-paused.scss
@@ -0,0 +1,4 @@
+.video-player__container[data-paused="true"] video,
+.video-player__container[data-buffering="true"] video {
+ filter: grayscale(0.25) brightness(0.5);
+}
\ No newline at end of file
diff --git a/src/sites/clips/index.jsx b/src/sites/clips/index.jsx
index 07da71ec..4f9eb2d7 100644
--- a/src/sites/clips/index.jsx
+++ b/src/sites/clips/index.jsx
@@ -57,7 +57,7 @@ export default class ClipsSite extends BaseSite {
this.ClipsMenu = this.fine.define(
'clips-menu',
- n => n.props?.changeTheme && has(n.state, 'dropdownOpen')
+ n => n.props?.signup && has(n.state, 'dropdownOpen')
)
document.head.appendChild(createElement('link', {
@@ -216,4 +216,6 @@ export default class ClipsSite extends BaseSite {
ClipsSite.CLIP_ROUTES = {
'clip-page': '/:slug'
-};
\ No newline at end of file
+};
+
+ClipsSite.DIALOG_SELECTOR = '#root > div';
diff --git a/src/sites/clips/line.jsx b/src/sites/clips/line.jsx
index 115aa22d..94775417 100644
--- a/src/sites/clips/line.jsx
+++ b/src/sites/clips/line.jsx
@@ -107,7 +107,7 @@ export default class Line extends Module {
}, user_block)
] : user_block);
- return (
+ return (
{
this._ffz_auto_drop = null;
t.autoClickDrop(this);
- }, 250);
+ }, 0);
} catch(err) {
t.log.capture(err);
@@ -1512,19 +1512,40 @@ export default class ChatHook extends Module {
autoClickDrop(inst) {
+ if ( inst._ffz_clicking )
+ return;
+
+ // Check to ensure the active callout is a drop to claim.
const callout = inst.props?.callouts?.[0] || inst.props?.pinnedCallout,
ctype = callout?.event?.type;
if ( ctype !== 'drop' || ! this.chat.context.get('chat.drops.auto-rewards') )
return;
- const node = this.fine.getHostNode(inst),
- btn = node.querySelector('button[data-a-target="chat-private-callout__primary-button"]');
+ inst._ffz_clicking = true;
- if ( ! btn )
- return;
+ // Wait for the button to be added to the DOM.
+ const waiter = this.resolve('site').awaitElement(
+ 'button[data-a-target="chat-private-callout__primary-button"]',
+ this.fine.getHostNode(inst),
+ 10000
+ );
- btn.click();
+ waiter.then(btn => {
+ inst._ffz_clicking = false;
+
+ // Check AGAIN because time has passed.
+ const callout = inst.props?.callouts?.[0] || inst.props?.pinnedCallout,
+ ctype = callout?.event?.type;
+
+ if ( ctype !== 'drop' || ! this.chat.context.get('chat.drops.auto-rewards') )
+ return;
+
+ btn.click();
+
+ }).catch(() => {
+ inst._ffz_clicking = false;
+ });
}
diff --git a/src/sites/twitch-twilight/modules/chat/input.jsx b/src/sites/twitch-twilight/modules/chat/input.jsx
index eba96084..6b1cade8 100644
--- a/src/sites/twitch-twilight/modules/chat/input.jsx
+++ b/src/sites/twitch-twilight/modules/chat/input.jsx
@@ -173,9 +173,7 @@ export default class Input extends Module {
{value: 2, title: '2: Non-Prefix (Old FFZ style)'},
{value: 3, title: '3: Exact (Case-Insensitive)'}
]
- },
-
- changed: () => this.uncacheTabCompletion()
+ }
});
@@ -365,6 +363,7 @@ export default class Input extends Module {
this.chat.context.on('changed:chat.emotes.animated', this.uncacheTabCompletion, this);
this.chat.context.on('changed:chat.emotes.enabled', this.uncacheTabCompletion, this);
+ this.chat.context.on('changed:chat.tab-complete.matching', this.uncacheTabCompletion, this);
this.on('chat.emotes:change-hidden', this.uncacheTabCompletion, this);
this.on('chat.emotes:change-set-hidden', this.uncacheTabCompletion, this);
this.on('chat.emotes:change-favorite', this.uncacheTabCompletion, this);
diff --git a/src/utilities/blobs.js b/src/utilities/blobs.js
index d1d713ba..796b87fe 100644
--- a/src/utilities/blobs.js
+++ b/src/utilities/blobs.js
@@ -56,4 +56,83 @@ export function deserializeBlob(data) {
return new Uint8Array(data.buffer);
throw new TypeError('Invalid type');
-}
\ No newline at end of file
+}
+
+export function serializeBlobUrl(blob) {
+ return new Promise((s,f) => {
+ const reader = new FileReader();
+ reader.onabort = f;
+ reader.onerror = f;
+ reader.onload = e => {
+ s(e.target.result);
+ }
+ reader.readAsDataURL(blob);
+ });
+}
+
+export function deserializeBlobUrl(url) {
+ return fetch(blob).then(res => res.blob())
+}
+
+export function deserializeABUrl(url) {
+ return fetch(blob).then(res => res.arrayBuffer())
+}
+
+export async function serializeBlobForExt(blob) {
+ if ( ! blob )
+ return null;
+
+ if ( blob instanceof Blob )
+ return {
+ type: 'blob',
+ mime: blob.type,
+ url: await serializeBlobUrl(blob)
+ }
+
+ if ( blob instanceof File )
+ return {
+ type: 'file',
+ mime: blob.type,
+ name: blob.name,
+ modified: blob.lastModified,
+ url: await serializeBlobUrl(blob)
+ }
+
+ if ( blob instanceof ArrayBuffer )
+ return {
+ type: 'ab',
+ url: await serializeBlobUrl(new Blob([blob]))
+ }
+
+ if ( blob instanceof Uint8Array )
+ return {
+ type: 'u8',
+ url: await serializeBlobUrl(new Blob([blob]))
+ }
+
+ throw new TypeError('Invalid type');
+
+}
+
+export async function deserializeBlobForExt(data) {
+ if ( ! data || ! data.type )
+ return null;
+
+ if ( data.type === 'blob' )
+ return await deserializeBlobUrl(data.url);
+
+ if ( data.type === 'file' )
+ return new File(
+ [await deserializeBlobUrl(data.url)],
+ data.name,
+ {type: data.mime, lastModified: data.modified}
+ );
+
+ if ( data.type === 'ab' )
+ return await deserializeABUrl(data.url);
+
+ if ( data.type === 'u8' )
+ return new Uint8Array(await deserializeABUrl(data.url));
+
+ throw new TypeError('Invalid type');
+}
diff --git a/src/utilities/constants.js b/src/utilities/constants.js
index d2b9ce62..70c38610 100644
--- a/src/utilities/constants.js
+++ b/src/utilities/constants.js
@@ -5,17 +5,23 @@ import {make_enum} from 'utilities/object';
export const DEBUG = localStorage.ffzDebugMode === 'true' && document.body.classList.contains('ffz-dev');
export const EXTENSION = !!__extension__;
-export const SERVER = DEBUG ? '//localhost:8000' : 'https://cdn.frankerfacez.com';
+export const SERVER = DEBUG ? 'https://localhost:8000' : 'https://cdn.frankerfacez.com';
-export const SERVER_OR_EXT = EXTENSION
- ? __webpack_public_path__
- : `${SERVER}/script`;
+let path = `${SERVER}/script`;
+
+if ( EXTENSION ) {
+ path = __webpack_public_path__;
+ if ( path.endsWith('/') )
+ path = path.slice(0, path.length - 1);
+}
+
+export const SERVER_OR_EXT = path;
export const CLIENT_ID = 'a3bc9znoz6vi8ozsoca0inlcr4fcvkl';
-export const API_SERVER = '//api.frankerfacez.com';
-export const STAGING_API = '//api-staging.frankerfacez.com';
-export const STAGING_CDN = '//cdn-staging.frankerfacez.com';
-export const NEW_API = '//api2.frankerfacez.com';
+export const API_SERVER = 'https://api.frankerfacez.com';
+export const STAGING_API = 'https://api-staging.frankerfacez.com';
+export const STAGING_CDN = 'https://cdn-staging.frankerfacez.com';
+export const NEW_API = 'https://api2.frankerfacez.com';
//export const SENTRY_ID = 'https://1c3b56f127254d3ba1bd1d6ad8805eee@sentry.io/1186960';
//export const SENTRY_ID = 'https://07ded545d3224ca59825daee02dc7745@catbag.frankerfacez.com:444/2';
diff --git a/src/utilities/logging.js b/src/utilities/logging.js
index e95a6dc2..0559976c 100644
--- a/src/utilities/logging.js
+++ b/src/utilities/logging.js
@@ -1,7 +1,5 @@
'use strict';
-import { has } from 'utilities/object';
-
const RAVEN_LEVELS = {
1: 'debug',
2: 'info',
@@ -16,7 +14,7 @@ function readLSLevel() {
return null;
const upper = level.toUpperCase();
- if ( has(Logger, upper) )
+ if ( Logger.hasOwnProperty(upper) )
return Logger[upper];
if ( /^\d+$/.test(level) )
diff --git a/webpack.config.js b/webpack.config.js
index 8b5890f9..6cb312a6 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -46,6 +46,7 @@ const commit_hash = DEV_SERVER
const ENTRY_POINTS = {
bridge: './src/bridge.js',
+ esbridge: './src/esbridge.js',
player: './src/player.js',
avalon: './src/main.js',
clips: './src/clips.js'