mirror of
https://github.com/FrankerFaceZ/FrankerFaceZ.git
synced 2025-07-25 20:18:31 +00:00
4.20.15
* Changed: Implemented FFZ rendering of Channel Points redemption notices with no associated messages. (Experiment, 50% roll-out) * Fixed: Channel not properly detecting the current channel's branding color. * Fixed: Unable to delete the profile 0 (Default Profile) * Fixed: Twitch prevented viewer cards from appearing when moved out of the chat area. * Fixed: `addons` should not block loading while its data loads. * Fixed: Issue accessing `i18n` before `settings` has fully loaded. * Fixed: `main_menu` tries to use `i18n` before `i18n` is ready. * Fixed: Main menu throws error if profiles are changed while main menu is open. * Fixed: `site` should not block loading waiting for `settings` * Maintenance: Updated dependencies. * API Added: Initial support for using IndexedDB to store settings rather than localStorage * API Added: Messages now have a `highlights` object if they've matched filters, describing which filters they matched.
This commit is contained in:
parent
65a00df2a9
commit
1c2bf202fc
18 changed files with 478 additions and 244 deletions
8
package-lock.json
generated
8
package-lock.json
generated
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "frankerfacez",
|
||||
"version": "4.20.5",
|
||||
"version": "4.20.14",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
@ -5168,9 +5168,9 @@
|
|||
}
|
||||
},
|
||||
"lodash": {
|
||||
"version": "4.17.15",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
|
||||
"integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==",
|
||||
"version": "4.17.19",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz",
|
||||
"integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==",
|
||||
"dev": true
|
||||
},
|
||||
"lodash.tail": {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "frankerfacez",
|
||||
"author": "Dan Salvato LLC",
|
||||
"version": "4.20.14",
|
||||
"version": "4.20.15",
|
||||
"description": "FrankerFaceZ is a Twitch enhancement suite.",
|
||||
"license": "Apache-2.0",
|
||||
"scripts": {
|
||||
|
|
|
@ -25,12 +25,18 @@ export default class AddonManager extends Module {
|
|||
this.inject('settings');
|
||||
this.inject('i18n');
|
||||
|
||||
this.load_requires = ['settings'];
|
||||
|
||||
this.has_dev = false;
|
||||
this.reload_required = false;
|
||||
this.addons = {};
|
||||
this.enabled_addons = [];
|
||||
}
|
||||
|
||||
onLoad() {
|
||||
this._loader = this.loadAddonData();
|
||||
}
|
||||
|
||||
async onEnable() {
|
||||
this.settings.addUI('add-ons', {
|
||||
path: 'Add-Ons @{"description": "Add-Ons are additional modules, often written by other people, that can be loaded automatically by FrankerFaceZ to add new capabilities and behaviors to the extension and Twitch.", "profile_warning": false}',
|
||||
|
@ -69,16 +75,17 @@ export default class AddonManager extends Module {
|
|||
|
||||
this.settings.provider.on('changed', this.onProviderChange, this);
|
||||
|
||||
await this.loadAddonData();
|
||||
this.enabled_addons = this.settings.provider.get('addons.enabled', []);
|
||||
this._loader.then(() => {
|
||||
this.enabled_addons = this.settings.provider.get('addons.enabled', []);
|
||||
|
||||
// We do not await enabling add-ons because that would delay the
|
||||
// main script's execution.
|
||||
for(const id of this.enabled_addons)
|
||||
if ( this.hasAddon(id) )
|
||||
this._enableAddon(id);
|
||||
// We do not await enabling add-ons because that would delay the
|
||||
// main script's execution.
|
||||
for(const id of this.enabled_addons)
|
||||
if ( this.hasAddon(id) )
|
||||
this._enableAddon(id);
|
||||
|
||||
this.emit(':ready');
|
||||
this.emit(':ready');
|
||||
});
|
||||
}
|
||||
|
||||
generateLog() {
|
||||
|
|
|
@ -6,5 +6,13 @@
|
|||
{"value": true, "weight": 0},
|
||||
{"value": false, "weight": 100}
|
||||
]
|
||||
},
|
||||
"all_points": {
|
||||
"name": "Override Channel Points Rendering",
|
||||
"description": "Override rendering for all channel points messages, even when no message is present.",
|
||||
"groups": [
|
||||
{"value": true, "weight": 50},
|
||||
{"value": false, "weight": 50}
|
||||
]
|
||||
}
|
||||
}
|
|
@ -337,7 +337,7 @@ export class TranslationManager extends Module {
|
|||
|
||||
|
||||
get locale() {
|
||||
return this._.locale;
|
||||
return this._ && this._.locale;
|
||||
}
|
||||
|
||||
set locale(new_locale) {
|
||||
|
|
|
@ -277,8 +277,10 @@ export const Mentions = {
|
|||
recipient: recipient ? recipient.toLowerCase() : ''
|
||||
});
|
||||
|
||||
if ( mentioned )
|
||||
if ( mentioned ) {
|
||||
(msg.highlights = (msg.highlights || new Set())).add('mention');
|
||||
msg.mentioned = true;
|
||||
}
|
||||
|
||||
// Push the remaining text from the token.
|
||||
text.push(segment.substr(match[0].length));
|
||||
|
@ -315,6 +317,7 @@ export const UserHighlights = {
|
|||
const u = msg.user;
|
||||
for(const [color, regex] of colors) {
|
||||
if ( regex.test(u.login) || regex.test(u.displayName) ) {
|
||||
(msg.highlights = (msg.highlights || new Set())).add('user');
|
||||
msg.mentioned = true;
|
||||
if ( color ) {
|
||||
msg.mention_color = color;
|
||||
|
@ -371,6 +374,7 @@ export const BadgeHighlights = {
|
|||
for(const badge of Object.keys(badges)) {
|
||||
if ( colors.has(badge) ) {
|
||||
const color = colors.get(badge);
|
||||
(msg.highlights = (msg.highlights || new Set())).add('badge');
|
||||
msg.mentioned = true;
|
||||
if ( color ) {
|
||||
msg.mention_color = color;
|
||||
|
@ -454,6 +458,7 @@ export const CustomHighlights = {
|
|||
if ( idx !== nix )
|
||||
out.push({type: 'text', text: text.slice(idx, nix)});
|
||||
|
||||
(msg.highlights = (msg.highlights || new Set())).add('term');
|
||||
msg.mentioned = true;
|
||||
msg.mention_color = color || msg.mention_color;
|
||||
|
||||
|
|
|
@ -252,7 +252,10 @@ export default class MainMenu extends Module {
|
|||
if ( this._update_timer )
|
||||
return;
|
||||
|
||||
this._update_timer = setTimeout(() => this.updateLiveMenu(), 250);
|
||||
this._update_timer = setTimeout(() => {
|
||||
// Make sure i18n is loaded before we try this.
|
||||
this.i18n.enable().then(() => this.updateLiveMenu());
|
||||
}, 250);
|
||||
}
|
||||
|
||||
|
||||
|
@ -597,118 +600,144 @@ export default class MainMenu extends Module {
|
|||
Vue = this.vue.Vue,
|
||||
settings = this.settings,
|
||||
context = settings.main_context,
|
||||
[profiles, profile_keys] = this.getProfiles(),
|
||||
[profiles, profile_keys] = this.getProfiles();
|
||||
|
||||
_c = {
|
||||
profiles,
|
||||
profile_keys,
|
||||
currentProfile: profile_keys[0],
|
||||
let currentProfile = profile_keys[0];
|
||||
if ( ! currentProfile ) {
|
||||
for(let i=profiles.length - 1; i >= 0; i--) {
|
||||
if ( profiles[i].live ) {
|
||||
currentProfile = profiles[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
has_update: this.has_update,
|
||||
if ( ! currentProfile )
|
||||
currentProfile = profiles[0];
|
||||
}
|
||||
|
||||
createProfile: data => {
|
||||
const profile = settings.createProfile(data);
|
||||
return t.getProfileProxy(profile, context);
|
||||
},
|
||||
const _c = {
|
||||
profiles,
|
||||
profile_keys,
|
||||
currentProfile: profile_keys[0] || profiles[0],
|
||||
|
||||
deleteProfile: profile => settings.deleteProfile(profile),
|
||||
has_update: this.has_update,
|
||||
|
||||
getFFZ: () => t.resolve('core'),
|
||||
createProfile: data => {
|
||||
const profile = settings.createProfile(data);
|
||||
return t.getProfileProxy(profile, context);
|
||||
},
|
||||
|
||||
context: {
|
||||
_users: 0,
|
||||
deleteProfile: profile => settings.deleteProfile(profile),
|
||||
|
||||
profiles: context.__profiles.map(profile => profile.id),
|
||||
get: key => context.get(key),
|
||||
uses: key => context.uses(key),
|
||||
getFFZ: () => t.resolve('core'),
|
||||
|
||||
on: (...args) => context.on(...args),
|
||||
off: (...args) => context.off(...args),
|
||||
context: {
|
||||
_users: 0,
|
||||
|
||||
order: id => context.order.indexOf(id),
|
||||
context: deep_copy(context._context),
|
||||
profiles: context.__profiles.map(profile => profile.id),
|
||||
get: key => context.get(key),
|
||||
uses: key => context.uses(key),
|
||||
|
||||
_update_profiles(changed) {
|
||||
const new_list = [],
|
||||
profiles = context.manager.__profiles;
|
||||
on: (...args) => context.on(...args),
|
||||
off: (...args) => context.off(...args),
|
||||
|
||||
for(let i=0; i < profiles.length; i++) {
|
||||
const profile = profile_keys[profiles[i].id];
|
||||
order: id => context.order.indexOf(id),
|
||||
context: deep_copy(context._context),
|
||||
|
||||
_update_profiles(changed) {
|
||||
const new_list = [],
|
||||
profiles = context.manager.__profiles;
|
||||
|
||||
for(let i=0; i < profiles.length; i++) {
|
||||
const profile = profile_keys[profiles[i].id];
|
||||
if ( profile ) {
|
||||
profile.order = i;
|
||||
|
||||
new_list.push(profile);
|
||||
}
|
||||
}
|
||||
|
||||
Vue.set(_c, 'profiles', new_list);
|
||||
Vue.set(_c, 'profiles', new_list);
|
||||
|
||||
if ( changed && changed.id === _c.currentProfile.id )
|
||||
_c.currentProfile = profile_keys[changed.id];
|
||||
},
|
||||
if ( changed && changed.id === _c.currentProfile.id )
|
||||
_c.currentProfile = profile_keys[changed.id];
|
||||
},
|
||||
|
||||
_profile_created(profile) {
|
||||
Vue.set(profile_keys, profile.id, t.getProfileProxy(profile, context));
|
||||
this._update_profiles()
|
||||
},
|
||||
_profile_created(profile) {
|
||||
Vue.set(profile_keys, profile.id, t.getProfileProxy(profile, context));
|
||||
this._update_profiles()
|
||||
},
|
||||
|
||||
_profile_changed(profile) {
|
||||
Vue.set(profile_keys, profile.id, t.getProfileProxy(profile, context));
|
||||
this._update_profiles(profile);
|
||||
},
|
||||
_profile_changed(profile) {
|
||||
Vue.set(profile_keys, profile.id, t.getProfileProxy(profile, context));
|
||||
this._update_profiles(profile);
|
||||
},
|
||||
|
||||
_profile_toggled(profile, val) {
|
||||
Vue.set(profile_keys[profile.id], 'toggled', val);
|
||||
this._update_profiles(profile);
|
||||
},
|
||||
_profile_toggled(profile, val) {
|
||||
Vue.set(profile_keys[profile.id], 'toggled', val);
|
||||
this._update_profiles(profile);
|
||||
},
|
||||
|
||||
_profile_deleted(profile) {
|
||||
Vue.delete(profile_keys, profile.id);
|
||||
this._update_profiles();
|
||||
_profile_deleted(profile) {
|
||||
Vue.delete(profile_keys, profile.id);
|
||||
this._update_profiles();
|
||||
|
||||
if ( _c.currentProfile.id === profile.id )
|
||||
_c.currentProfile = profile_keys[0]
|
||||
},
|
||||
if ( _c.currentProfile.id === profile.id ) {
|
||||
_c.currentProfile = profile_keys[0];
|
||||
if ( ! _c.currentProfile ) {
|
||||
for(let i=_c.profiles.length - 1; i >= 0; i--) {
|
||||
if ( _c.profiles[i].live ) {
|
||||
_c.currentProfile = _c.profiles[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
_context_changed() {
|
||||
this.context = deep_copy(context._context);
|
||||
const profiles = context.manager.__profiles,
|
||||
ids = this.profiles = context.__profiles.map(profile => profile.id);
|
||||
|
||||
for(let i=0; i < profiles.length; i++) {
|
||||
const id = profiles[i].id,
|
||||
profile = profile_keys[id];
|
||||
|
||||
profile.live = ids.includes(id);
|
||||
}
|
||||
},
|
||||
|
||||
_add_user() {
|
||||
this._users++;
|
||||
if ( this._users === 1 ) {
|
||||
settings.on(':profile-toggled', this._profile_toggled, this);
|
||||
settings.on(':profile-created', this._profile_created, this);
|
||||
settings.on(':profile-changed', this._profile_changed, this);
|
||||
settings.on(':profile-deleted', this._profile_deleted, this);
|
||||
settings.on(':profiles-reordered', this._update_profiles, this);
|
||||
context.on('context_changed', this._context_changed, this);
|
||||
context.on('profiles_changed', this._context_changed, this);
|
||||
this.profiles = context.__profiles.map(profile => profile.id);
|
||||
}
|
||||
},
|
||||
|
||||
_remove_user() {
|
||||
this._users--;
|
||||
if ( this._users === 0 ) {
|
||||
settings.off(':profile-toggled', this._profile_toggled, this);
|
||||
settings.off(':profile-created', this._profile_created, this);
|
||||
settings.off(':profile-changed', this._profile_changed, this);
|
||||
settings.off(':profile-deleted', this._profile_deleted, this);
|
||||
settings.off(':profiles-reordered', this._update_profiles, this);
|
||||
context.off('context_changed', this._context_changed, this);
|
||||
context.off('profiles_changed', this._context_changed, this);
|
||||
if ( ! _c.currentProfile )
|
||||
_c.currentProfile = _c.profiles[0];
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
_context_changed() {
|
||||
this.context = deep_copy(context._context);
|
||||
const profiles = context.manager.__profiles,
|
||||
ids = this.profiles = context.__profiles.map(profile => profile.id);
|
||||
|
||||
for(let i=0; i < profiles.length; i++) {
|
||||
const id = profiles[i].id,
|
||||
profile = profile_keys[id];
|
||||
|
||||
profile.live = ids.includes(id);
|
||||
}
|
||||
},
|
||||
|
||||
_add_user() {
|
||||
this._users++;
|
||||
if ( this._users === 1 ) {
|
||||
settings.on(':profile-toggled', this._profile_toggled, this);
|
||||
settings.on(':profile-created', this._profile_created, this);
|
||||
settings.on(':profile-changed', this._profile_changed, this);
|
||||
settings.on(':profile-deleted', this._profile_deleted, this);
|
||||
settings.on(':profiles-reordered', this._update_profiles, this);
|
||||
context.on('context_changed', this._context_changed, this);
|
||||
context.on('profiles_changed', this._context_changed, this);
|
||||
this.profiles = context.__profiles.map(profile => profile.id);
|
||||
}
|
||||
},
|
||||
|
||||
_remove_user() {
|
||||
this._users--;
|
||||
if ( this._users === 0 ) {
|
||||
settings.off(':profile-toggled', this._profile_toggled, this);
|
||||
settings.off(':profile-created', this._profile_created, this);
|
||||
settings.off(':profile-changed', this._profile_changed, this);
|
||||
settings.off(':profile-deleted', this._profile_deleted, this);
|
||||
settings.off(':profiles-reordered', this._update_profiles, this);
|
||||
context.off('context_changed', this._context_changed, this);
|
||||
context.off('profiles_changed', this._context_changed, this);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
return _c;
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
import Module from 'utilities/module';
|
||||
import {deep_equals, has, debounce} from 'utilities/object';
|
||||
|
||||
import {CloudStorageProvider, LocalStorageProvider} from './providers';
|
||||
import {IndexedDBProvider, LocalStorageProvider} from './providers';
|
||||
import SettingsProfile from './profile';
|
||||
import SettingsContext from './context';
|
||||
import MigrationManager from './migration';
|
||||
|
@ -218,11 +218,15 @@ export default class SettingsManager extends Module {
|
|||
/**
|
||||
* Evaluate the environment that FFZ is running in and then decide which
|
||||
* provider should be used to retrieve and store settings.
|
||||
*
|
||||
* @returns {SettingsProvider} The provider to store everything.
|
||||
*/
|
||||
_createProvider() {
|
||||
// If the loader has reported support for cloud settings...
|
||||
if ( document.body.classList.contains('ffz-cloud-storage') )
|
||||
return new CloudStorageProvider(this);
|
||||
// Prefer IndexedDB if it's available because it's more persistent
|
||||
// and can store more data. Plus, we don't have to faff around with
|
||||
// JSON conversion all the time.
|
||||
if ( IndexedDBProvider.supported() && localStorage.ffzIDB )
|
||||
return this._idb = new IndexedDBProvider(this);
|
||||
|
||||
// Fallback
|
||||
return new LocalStorageProvider(this);
|
||||
|
@ -427,15 +431,18 @@ export default class SettingsManager extends Module {
|
|||
* @param {number|SettingsProfile} id - The profile to delete
|
||||
*/
|
||||
deleteProfile(id) {
|
||||
if ( typeof id === 'object' && id.id )
|
||||
if ( typeof id === 'object' && id.id != null )
|
||||
id = id.id;
|
||||
|
||||
const profile = this.__profile_ids[id];
|
||||
if ( ! profile )
|
||||
return;
|
||||
|
||||
if ( profile.id === 0 )
|
||||
throw new Error('cannot delete default profile');
|
||||
if ( this.__profiles.length === 1 )
|
||||
throw new Error('cannot delete only profile');
|
||||
|
||||
/*if ( profile.id === 0 )
|
||||
throw new Error('cannot delete default profile');*/
|
||||
|
||||
profile.off('toggled', this._onProfileToggled, this);
|
||||
profile.clear();
|
||||
|
|
|
@ -7,6 +7,8 @@
|
|||
import {EventEmitter} from 'utilities/events';
|
||||
import {has} from 'utilities/object';
|
||||
|
||||
const DB_VERSION = 1;
|
||||
|
||||
|
||||
// ============================================================================
|
||||
// SettingsProvider
|
||||
|
@ -48,6 +50,9 @@ export class SettingsProvider extends EventEmitter {
|
|||
keys() { throw new Error('Not Implemented') } // eslint-disable-line class-methods-use-this
|
||||
entries() { throw new Error('Not Implemented') } // eslint-disable-line class-methods-use-this
|
||||
get size() { throw new Error('Not Implemented') } // eslint-disable-line class-methods-use-this
|
||||
|
||||
get supportsBlobs() { return false; } // eslint-disable-line class-methods-use-this
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
@ -224,147 +229,133 @@ export class IndexedDBProvider extends SettingsProvider {
|
|||
constructor(manager) {
|
||||
super(manager);
|
||||
|
||||
this._cached = new Map;
|
||||
this.ready = false;
|
||||
this._ready_wait = null;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.disable();
|
||||
this._cached.clear();
|
||||
}
|
||||
|
||||
disable() {
|
||||
this.disabled = true;
|
||||
}
|
||||
|
||||
awaitReady() {
|
||||
if ( this.ready )
|
||||
return Promise.resolve();
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const waiters = this._ready_wait = this._ready_wait || [];
|
||||
waiters.push([resolve, reject]);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class CloudStorageProvider extends SettingsProvider {
|
||||
constructor(manager) {
|
||||
super(manager);
|
||||
this._start_time = performance.now();
|
||||
|
||||
this._cached = new Map;
|
||||
this.ready = false;
|
||||
this._ready_wait = null;
|
||||
|
||||
this._boundHandleStorage = this.handleStorage.bind(this);
|
||||
window.addEventListener('message', this._boundHandleStorage);
|
||||
this._send('get_all');
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.disable();
|
||||
this._cached.clear();
|
||||
}
|
||||
|
||||
disable() {
|
||||
this.disabled = true;
|
||||
|
||||
if ( this._boundHandleStorage ) {
|
||||
window.removeEventListener('message', this._boundHandleStorage)
|
||||
this._boundHandleStorage = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
awaitReady() {
|
||||
if ( this.ready )
|
||||
return Promise.resolve();
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const waiters = this._ready_wait = this._ready_wait || [];
|
||||
waiters.push([resolve, reject]);
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
// ========================================================================
|
||||
// Communication
|
||||
// ========================================================================
|
||||
|
||||
handleStorage(event) {
|
||||
if ( event.source !== window || ! event.data || ! event.data.ffz )
|
||||
return;
|
||||
|
||||
const cmd = event.data.cmd,
|
||||
data = event.data.data;
|
||||
|
||||
if ( cmd === 'all_values' ) {
|
||||
const old_keys = new Set(this._cached.keys());
|
||||
|
||||
for(const key in data)
|
||||
if ( has(data, key) ) {
|
||||
const val = data[key];
|
||||
old_keys.delete(key);
|
||||
this._cached.set(key, val);
|
||||
if ( this.ready )
|
||||
this.emit('changed', key, val);
|
||||
}
|
||||
|
||||
for(const key of old_keys) {
|
||||
this._cached.delete(key);
|
||||
if ( this.ready )
|
||||
this.emit('changed', key, undefined, true);
|
||||
}
|
||||
|
||||
this.ready = true;
|
||||
if ( this._ready_wait ) {
|
||||
for(const resolve of this._ready_wait)
|
||||
resolve();
|
||||
this._ready_wait = null;
|
||||
}
|
||||
|
||||
} else if ( cmd === 'changed' ) {
|
||||
this._cached.set(data.key, data.value);
|
||||
this.emit('changed', data.key, data.value);
|
||||
|
||||
} else if ( cmd === 'deleted' ) {
|
||||
this._cached.delete(data);
|
||||
this.emit('changed', data, undefined, true);
|
||||
if ( window.BroadcastChannel ) {
|
||||
const bc = this._broadcaster = new BroadcastChannel('ffz-settings');
|
||||
bc.addEventListener('message',
|
||||
this._boundHandleMessage = this.handleMessage.bind(this));
|
||||
|
||||
} else {
|
||||
this.manager.log.info('unknown storage event', event);
|
||||
window.addEventListener('storage',
|
||||
this._boundHandleStorage = this.handleStorage.bind(this));
|
||||
}
|
||||
|
||||
this.loadSettings()
|
||||
.then(() => this._resolveReady(true))
|
||||
.catch(err => this._resolveReady(false, err));
|
||||
}
|
||||
|
||||
_resolveReady(success, data) {
|
||||
this.manager.log.info(`IDB ready in ${(performance.now() - this._start_time).toFixed(5)}ms`);
|
||||
this.ready = success;
|
||||
const waiters = this._ready_wait;
|
||||
this._ready_wait = null;
|
||||
if ( waiters )
|
||||
for(const pair of waiters)
|
||||
pair[success ? 0 : 1](data);
|
||||
}
|
||||
|
||||
static supported() {
|
||||
return window.indexedDB != null;
|
||||
}
|
||||
|
||||
get supportsBlobs() { return true; } // eslint-disable-line class-methods-use-this
|
||||
|
||||
destroy() {
|
||||
this.disable();
|
||||
this._cached.clear();
|
||||
}
|
||||
|
||||
disable() {
|
||||
this.disabled = true;
|
||||
|
||||
if ( this.db ) {
|
||||
this.db.close();
|
||||
this.db = null;
|
||||
}
|
||||
|
||||
if ( this._broadcaster ) {
|
||||
this._broadcaster.removeEventListener('message', this._boundHandleMessage);
|
||||
this._broadcaster.close();
|
||||
this._boundHandleMessage = this._broadcaster = null;
|
||||
}
|
||||
}
|
||||
|
||||
_send(cmd, data) { // eslint-disable-line class-methods-use-this
|
||||
window.postMessage({
|
||||
ffz: true,
|
||||
cmd,
|
||||
data
|
||||
}, location.origin);
|
||||
broadcast(msg) {
|
||||
if ( this._broadcaster )
|
||||
this._broadcaster.postMessage(msg);
|
||||
}
|
||||
|
||||
|
||||
// ========================================================================
|
||||
// Data Access
|
||||
// ========================================================================
|
||||
handleMessage(event) {
|
||||
if ( this.disabled || ! event.isTrusted || ! event.data )
|
||||
return;
|
||||
|
||||
this.manager.log.debug('storage broadcast event', event.data);
|
||||
const {type, key} = event.data;
|
||||
|
||||
if ( type === 'set' ) {
|
||||
const val = JSON.parse(localStorage.getItem(this.prefix + key));
|
||||
this._cached.set(key, val);
|
||||
this.emit('changed', key, val, false);
|
||||
|
||||
} else if ( type === 'delete' ) {
|
||||
this._cached.delete(key);
|
||||
this.emit('changed', key, undefined, true);
|
||||
|
||||
} else if ( type === 'clear' ) {
|
||||
const old_keys = Array.from(this._cached.keys());
|
||||
this._cached.clear();
|
||||
for(const key of old_keys)
|
||||
this.emit('changed', key, undefined, true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
awaitReady() {
|
||||
if ( this.ready )
|
||||
return Promise.resolve();
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const waiters = this._ready_wait = this._ready_wait || [];
|
||||
waiters.push([resolve, reject]);
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
// Synchronous Methods
|
||||
|
||||
get(key, default_value) {
|
||||
return this._cached.has(key) ?
|
||||
this._cached.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._send('set', {key, value});
|
||||
this._set(key, value)
|
||||
.catch(err => this.manager.log.error(`Error saving setting "${key}" to database`, err))
|
||||
.then(() => this.broadcast({type: 'set', key}));
|
||||
|
||||
this.emit('set', key, value, false);
|
||||
}
|
||||
|
||||
delete(key) {
|
||||
this._cached.delete(key);
|
||||
this._send('delete', key);
|
||||
this._delete(key)
|
||||
.catch(err => this.manager.log.error(`Error deleting setting "${key}" from database`, err))
|
||||
.then(() => this.broadcast({type: 'delete', key}));
|
||||
|
||||
this.emit('set', key, undefined, true);
|
||||
}
|
||||
|
||||
has(key) {
|
||||
|
@ -376,8 +367,15 @@ export class CloudStorageProvider extends SettingsProvider {
|
|||
}
|
||||
|
||||
clear() {
|
||||
this._cached.clear();
|
||||
this._send('clear');
|
||||
const old_cache = this._cached;
|
||||
this._cached = new Map;
|
||||
|
||||
for(const key of old_cache.keys())
|
||||
this.emit('changed', key, undefined, true);
|
||||
|
||||
this._clear()
|
||||
.catch(err => this.manager.log.error(`Error clearing database`, err))
|
||||
.then(() => this.broadcast({type: 'clear'}));
|
||||
}
|
||||
|
||||
entries() {
|
||||
|
@ -387,4 +385,135 @@ export class CloudStorageProvider extends SettingsProvider {
|
|||
get size() {
|
||||
return this._cached.size;
|
||||
}
|
||||
|
||||
|
||||
// IDB Interaction
|
||||
|
||||
getDB() {
|
||||
if ( this.db )
|
||||
return Promise.resolve(this.db);
|
||||
|
||||
if ( this._listeners )
|
||||
return new Promise((s,f) => this._listeners.push([s,f]));
|
||||
|
||||
return new Promise((s,f) => {
|
||||
const listeners = this._listeners = [[s,f]],
|
||||
done = (success, data) => {
|
||||
if ( this._listeners === listeners ) {
|
||||
this._listeners = null;
|
||||
for(const pair of listeners)
|
||||
pair[success ? 0 : 1](data);
|
||||
}
|
||||
}
|
||||
|
||||
const request = window.indexedDB.open('FFZ', DB_VERSION);
|
||||
request.onerror = e => {
|
||||
this.manager.log.error('Error opening database.', e);
|
||||
done(false, e);
|
||||
}
|
||||
|
||||
request.onupgradeneeded = e => {
|
||||
this.manager.log.info(`Upgrading database from version ${e.oldVersion} to ${DB_VERSION}`);
|
||||
|
||||
const db = request.result;
|
||||
|
||||
db.createObjectStore('settings', {keyPath: 'k'});
|
||||
db.createObjectStore('blobs');
|
||||
}
|
||||
|
||||
request.onsuccess = () => {
|
||||
this.manager.log.info(`Database opened. (After: ${(performance.now() - this._start_time).toFixed(5)}ms)`);
|
||||
this.db = request.result;
|
||||
done(true, this.db);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
async loadSettings() {
|
||||
const db = await this.getDB(),
|
||||
trx = db.transaction(['settings'], 'readonly'),
|
||||
store = trx.objectStore('settings');
|
||||
|
||||
return new Promise((s,f) => {
|
||||
const request = store.getAll();
|
||||
|
||||
request.onsuccess = () => {
|
||||
for(const entry of request.result)
|
||||
this._cached.set(entry.k, entry.v);
|
||||
|
||||
s();
|
||||
}
|
||||
|
||||
request.onerror = err => {
|
||||
this.manager.log.error('Error reading settings from database.', err);
|
||||
f();
|
||||
}
|
||||
});
|
||||
|
||||
/*cursor = store.openCursor();
|
||||
|
||||
return new Promise((s,f) => {
|
||||
cursor.onsuccess = e => {
|
||||
const entry = e.target.result;
|
||||
if ( entry ) {
|
||||
this._cached.set(entry.key, entry.value);
|
||||
entry.continue();
|
||||
} else {
|
||||
// We're done~!
|
||||
s();
|
||||
}
|
||||
};
|
||||
|
||||
cursor.onerror = e => {
|
||||
this.manager.log.error('Error reading settings from database.', e);
|
||||
f(e);
|
||||
}
|
||||
});*/
|
||||
}
|
||||
|
||||
|
||||
async _set(key, value) {
|
||||
const db = await this.getDB(),
|
||||
trx = db.transaction(['settings'], 'readwrite'),
|
||||
store = trx.objectStore('settings');
|
||||
|
||||
return new Promise((s,f) => {
|
||||
store.onerror = f;
|
||||
store.onsuccess = s;
|
||||
|
||||
store.put({k: key, v: value});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
async _delete(key) {
|
||||
const db = await this.getDB(),
|
||||
trx = db.transaction(['settings'], 'readwrite'),
|
||||
store = trx.objectStore('settings');
|
||||
|
||||
return new Promise((s,f) => {
|
||||
store.onerror = f;
|
||||
store.onsuccess = s;
|
||||
|
||||
store.delete(key);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
async _clear() {
|
||||
const db = await this.getDB(),
|
||||
trx = db.transaction(['settings'], 'readwrite'),
|
||||
store = trx.objectStore('settings');
|
||||
|
||||
return new Promise((s,f) => {
|
||||
store.onerror = f;
|
||||
store.onsuccess = s;
|
||||
|
||||
store.clear();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
|
@ -10,7 +10,7 @@ export default class BaseSite extends Module {
|
|||
super(...args);
|
||||
this._id = `_ffz$${last_site++}`;
|
||||
|
||||
this.inject('settings');
|
||||
//this.inject('settings');
|
||||
|
||||
this.log.info(`Using: ${this.constructor.name}`);
|
||||
}
|
||||
|
|
|
@ -54,6 +54,8 @@ export default class Twilight extends BaseSite {
|
|||
}
|
||||
|
||||
onEnable() {
|
||||
this.settings = this.resolve('settings');
|
||||
|
||||
const thing = this.fine.searchNode(null, n => n?.pendingProps?.store?.getState),
|
||||
store = this.store = thing?.pendingProps?.store;
|
||||
|
||||
|
|
|
@ -270,8 +270,14 @@ export default class Channel extends Module {
|
|||
|
||||
|
||||
updateRoot(el) {
|
||||
const root = this.fine.getReactInstance(el),
|
||||
channel = root?.return?.memoizedState?.next?.memoizedState?.current?.previousData?.result?.data?.user;
|
||||
const root = this.fine.getReactInstance(el);
|
||||
|
||||
let channel = null, state = root?.return?.memoizedState, i = 0;
|
||||
while(state != null && channel == null && i < 50 ) {
|
||||
state = state?.next;
|
||||
channel = state?.memoizedState?.current?.previousData?.result?.data?.user;
|
||||
i++;
|
||||
}
|
||||
|
||||
if ( channel && channel.id ) {
|
||||
this.updateChannelColor(channel.primaryColorHex);
|
||||
|
|
|
@ -143,12 +143,14 @@ export default class ChatHook extends Module {
|
|||
|
||||
this.inject('settings');
|
||||
this.inject('i18n');
|
||||
this.inject('experiments');
|
||||
|
||||
this.inject('site');
|
||||
this.inject('site.router');
|
||||
this.inject('site.fine');
|
||||
this.inject('site.web_munch');
|
||||
this.inject('site.css_tweaks');
|
||||
this.inject('site.subpump');
|
||||
|
||||
this.inject('chat');
|
||||
|
||||
|
@ -1076,6 +1078,38 @@ export default class ChatHook extends Module {
|
|||
for(const inst of instances)
|
||||
this.containerMounted(inst);
|
||||
});
|
||||
|
||||
this.subpump.on(':pubsub-message', event => {
|
||||
if ( event.prefix !== 'community-points-channel-v1' || ! this.experiments.getAssignment('all_points') )
|
||||
return;
|
||||
|
||||
const service = this.ChatService.first,
|
||||
message = event.message,
|
||||
data = message?.data?.redemption;
|
||||
if ( ! message || ! service || message.type !== 'reward-redeemed' || service.props.channelID != data?.channel_id )
|
||||
return;
|
||||
|
||||
const reward = data.reward?.id && get(data.reward.id, service.props.rewardMap);
|
||||
if ( ! reward )
|
||||
return;
|
||||
|
||||
const msg = {
|
||||
id: data.id,
|
||||
type: this.chat_types.Message,
|
||||
ffz_type: 'points',
|
||||
ffz_reward: reward,
|
||||
messageParts: [],
|
||||
user: {
|
||||
id: data.user.id,
|
||||
login: data.user.login,
|
||||
displayName: data.user.display_name
|
||||
},
|
||||
timestamp: new Date(message.data.timestamp || data.redeemed_at).getTime()
|
||||
};
|
||||
|
||||
service.postMessageToCurrentChannel({}, msg);
|
||||
event.preventDefault();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -385,7 +385,7 @@ other {# messages were deleted by a moderator.}
|
|||
onClick: this.ffz_user_click_handler
|
||||
}, e('span', {
|
||||
className: 'tw-c-text-base tw-strong'
|
||||
}, user.userDisplayName)),
|
||||
}, user.displayName)),
|
||||
count: msg.sub_count,
|
||||
tier: SUB_TIERS[msg.sub_plan] || 1,
|
||||
channel: msg.roomLogin
|
||||
|
@ -477,7 +477,7 @@ other {# messages were deleted by a moderator.}
|
|||
onClick: this.ffz_user_click_handler
|
||||
}, e('span', {
|
||||
className: 'tw-c-text-base tw-strong'
|
||||
}, user.userDisplayName)),
|
||||
}, user.displayName)),
|
||||
plan: plan.plan === 'custom' ? '' :
|
||||
t.i18n.t('chat.sub.gift-plan', 'Tier {tier}', {tier}),
|
||||
recipient: e('span', {
|
||||
|
@ -539,7 +539,7 @@ other {# messages were deleted by a moderator.}
|
|||
onClick: this.ffz_user_click_handler
|
||||
}, e('span', {
|
||||
className: 'tw-c-text-base tw-strong'
|
||||
}, user.userDisplayName)),
|
||||
}, user.displayName)),
|
||||
plan: plan.prime ?
|
||||
t.i18n.t('chat.sub.twitch-prime', 'with Twitch Prime') :
|
||||
t.i18n.t('chat.sub.plan', 'at Tier {tier}', {tier})
|
||||
|
@ -598,7 +598,7 @@ other {# messages were deleted by a moderator.}
|
|||
onClick: this.ffz_user_click_handler
|
||||
}, e('span', {
|
||||
className: 'tw-c-text-base tw-strong'
|
||||
}, user.userDisplayName))
|
||||
}, user.displayName))
|
||||
})
|
||||
]);
|
||||
|
||||
|
@ -640,7 +640,7 @@ other {# messages were deleted by a moderator.}
|
|||
onClick: this.ffz_user_click_handler
|
||||
}, e('span', {
|
||||
className: 'tw-c-text-base tw-strong'
|
||||
}, user.userDisplayName))
|
||||
}, user.displayName))
|
||||
})
|
||||
]),
|
||||
out && e('div', {
|
||||
|
@ -793,7 +793,7 @@ other {# messages were deleted by a moderator.}
|
|||
const msg = inst.props.message;
|
||||
if ( msg ) {
|
||||
msg.ffz_tokens = null;
|
||||
msg.mentioned = msg.mention_color = null;
|
||||
msg.highlights = msg.mentioned = msg.mention_color = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -801,7 +801,7 @@ other {# messages were deleted by a moderator.}
|
|||
const msg = inst.props.message;
|
||||
if ( msg ) {
|
||||
msg.ffz_tokens = null;
|
||||
msg.mentioned = msg.mention_color = null;
|
||||
msg.highlights = msg.mentioned = msg.mention_color = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -12,6 +12,8 @@ button[data-a-target="emote-picker-button"] {
|
|||
font-weight: normal;
|
||||
speak: none;
|
||||
|
||||
margin-top: -.3rem;
|
||||
|
||||
font-variant: normal;
|
||||
text-transform: none;
|
||||
|
||||
|
|
|
@ -1,3 +1,8 @@
|
|||
// Fix: Unable to move emote/user cards out of chat
|
||||
.channel-root__right-column > div.tw-overflow-hidden {
|
||||
overflow: unset !important;
|
||||
}
|
||||
|
||||
.chat-line__message--emote {
|
||||
vertical-align: middle;
|
||||
margin: -.5rem 0;
|
||||
|
|
|
@ -97,7 +97,7 @@ export class Logger {
|
|||
message.unshift('%cFFZ:%c', 'color:#755000; font-weight:bold', '');
|
||||
|
||||
if ( level === Logger.DEBUG )
|
||||
console.info(...message);
|
||||
console.debug(...message);
|
||||
|
||||
else if ( level === Logger.INFO )
|
||||
console.info(...message);
|
||||
|
|
|
@ -70,13 +70,13 @@
|
|||
|
||||
.ffz-i-zreknarf:before {
|
||||
vertical-align: middle;
|
||||
margin-bottom: -0.1em;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.ffz-i-discord:before,
|
||||
.ffz-i-twitter:before {
|
||||
vertical-align: middle;
|
||||
margin-bottom: -0.3em;
|
||||
margin-bottom: -0.1em;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue