1
0
Fork 0
mirror of https://github.com/FrankerFaceZ/FrankerFaceZ.git synced 2025-06-27 21:05:53 +00:00
* Added: Setting for customizing how tab-completion matches emote names.
* Added: Clips pages now have support for opening emote cards when clicking an emote in the chat replay.
* Fixed: Issue where chat was not rendering as intended on clips pages.
* Fixed: Issue where the FFZ Control Center link was not added to clips pages.
* Fixed: The chat actions module being instantiated in memory twice.
* Fixed: Blank badges appearing in chat, most notably in historic messages, when a chat message has invalid badge data associated with it.
* Fixed: Use a mutation observer for detecting the drops Claim button, rather than a simple timeout, for better consistency.
* Fixed: Issue when using the webpack public path variable that may lead to URL generation with extra `/` characters, breaking some behavior in Firefox when packaged as a local extension.
* API Added: Support for displaying an emote's original name, if an emote has been given a collection-specific name, using an `original_name` field.
This commit is contained in:
SirStendec 2023-09-09 17:43:51 -04:00
parent fc009a84e7
commit 8ac95f3a52
25 changed files with 654 additions and 52 deletions

View file

@ -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",

View file

@ -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;

View file

@ -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'),

View file

@ -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');

158
src/esbridge.js Normal file
View file

@ -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();

View file

@ -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'),

View file

@ -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) )

View file

@ -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 = {};

View file

@ -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') && (<div class="tw-pd-t-05">
{source}
</div>),
original_name && (<div class="tw-pd-t-05">
{original_name}
</div>),
owner && this.context.get('tooltip.emote-sources') && (<div class="tw-pd-t-05">
{owner}
</div>),

View file

@ -22,7 +22,11 @@
</figure>
</div>
<div class="tw-align-left tw-flex-grow-1 tw-ellipsis tw-mg-l-1 tw-mg-y-05 viewer-card__display-name">
<h4 class="tw-inline tw-ellipsis" :title="emote ? emote.name : raw_emote.name">
<h4
class="tw-inline tw-ellipsis"
:class="{'tw-italic': hasOriginalName}"
:title="emote ? emote.name : raw_emote.name"
>
{{ emote ? emote.name : raw_emote.name }}
</h4>
<P
@ -38,6 +42,16 @@
>
{{ emote.source_i18n ? t(emote.source_i18n, emote.source) : emote.source }}
</p>
<p v-if="hasOriginalName" class="tw-c-text-alt-2 tw-font-size-6 tw-ellipsis">
<t-list
phrase="emote.original-name"
default="Name: {name}"
>
<template #name>
{{ emote.originalName }}
</template>
</t-list>
</p>
<p v-if="loaded && emote.owner" class="tw-c-text-alt-2 tw-font-size-6 tw-ellipsis">
<t-list
phrase="emote-card.owner"
@ -227,6 +241,10 @@ export default {
return this.bodyComponent != null
},
hasOriginalName() {
return this.loaded && this.emote.originalName && this.emote.originalName !== this.emote.name;
},
bodyComponent() {
const body = this.emote?.body;

View file

@ -157,7 +157,7 @@ export default {
// Finally, make sure we can find the right UI elements.
const store = this.getFFZ().resolve('site')?.store,
web_munch = this.getFFZ().resolve('site.web_munch'),
sub_form = web_munch.getModule('sub-form');
sub_form = web_munch?.getModule?.('sub-form');
if ( ! store?.dispatch || ! sub_form )
return false;
@ -173,7 +173,7 @@ export default {
const store = this.getFFZ().resolve('site')?.store,
web_munch = this.getFFZ().resolve('site.web_munch'),
sub_form = web_munch.getModule('sub-form');
sub_form = web_munch?.getModule?.('sub-form');
if ( ! store?.dispatch || ! sub_form )
return;

View file

@ -103,7 +103,7 @@ export default class EmoteCard extends Module {
canReportTwitch() {
const site = this.resolve('site'),
core = site.getCore(),
core = site.getCore?.(),
user = site.getUser(),
web_munch = this.resolve('site.web_munch');
@ -395,6 +395,7 @@ export default class EmoteCard extends Module {
width: data.width,
height: data.height,
name: data.name,
originalName: data.original_name,
source: emote_set.source_line || (`${emote_set.source || 'FFZ'} ${emote_set.title || 'Global Emotes'}`),
owner: data.owner
? (data.owner.display_name || data.owner.name)

View file

@ -128,13 +128,14 @@ 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 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'),

View file

@ -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);
}
}
}
// ============================================================================
// 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);
}
}*/

View file

@ -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);
}

View file

@ -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'
};
};
ClipsSite.DIALOG_SELECTOR = '#root > div';

View file

@ -107,7 +107,7 @@ export default class Line extends Module {
}, user_block)
] : user_block);
return (<div class="tw-mg-b-1" style={{marginBottom:'0 !important'}}>
return (<div class="tw-mg-b-1">
<div
data-a-target="tw-animation-target"
class="ffz--clip-chat-line tw-animation tw-animation--animate tw-animation--duration-short tw-animation--fill-mode-both tw-animation--slide-in-bottom tw-animation--timing-ease"
@ -200,7 +200,7 @@ export default class Line extends Module {
if ( msg.message.userBadges )
for(const badge of msg.message.userBadges)
if ( badge )
if ( badge?.setID )
badges[badge.setID] = badge.version;
const out = {
@ -215,6 +215,7 @@ export default class Line extends Module {
roomLogin: room && room.login,
roomID: room && room.id,
badges,
id: msg.id,
ffz_badges: this.chat.badges.getBadges(author.id, author.login, room?.id, room?.login),
messageParts: msg.message.fragments
};

View file

@ -1,4 +1,5 @@
@import 'styles/main.scss';
@import '../../twitch-twilight/styles/mod_card.scss';
.tw-root--theme-dark, html {
body {
@ -38,4 +39,4 @@
padding: 0.5rem;
display: block !important;
}
}
}

View file

@ -1211,7 +1211,7 @@ export default class ChatHook extends Module {
this._ffz_auto_drop = setTimeout(() => {
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;
});
}

View file

@ -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);

View file

@ -56,4 +56,83 @@ export function deserializeBlob(data) {
return new Uint8Array(data.buffer);
throw new TypeError('Invalid type');
}
}
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');
}

View file

@ -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';

View file

@ -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) )

View file

@ -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'