From 1c2bf202fc8b72c3d28ba95e16b922493c3ff3f1 Mon Sep 17 00:00:00 2001 From: SirStendec Date: Wed, 22 Jul 2020 21:31:41 -0400 Subject: [PATCH] 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. --- package-lock.json | 8 +- package.json | 2 +- src/addons.js | 23 +- src/experiments.json | 8 + src/i18n.js | 2 +- src/modules/chat/tokenizers.jsx | 7 +- src/modules/main_menu/index.js | 205 +++++----- src/settings/index.js | 21 +- src/settings/providers.js | 371 ++++++++++++------ src/sites/base.js | 2 +- src/sites/twitch-twilight/index.js | 2 + src/sites/twitch-twilight/modules/channel.js | 10 +- .../twitch-twilight/modules/chat/index.js | 34 ++ .../twitch-twilight/modules/chat/line.js | 14 +- .../modules/css_tweaks/styles/emote-menu.scss | 2 + src/sites/twitch-twilight/styles/chat.scss | 5 + src/utilities/logging.js | 2 +- styles/icons.scss | 4 +- 18 files changed, 478 insertions(+), 244 deletions(-) diff --git a/package-lock.json b/package-lock.json index fd379507..07dba84b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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": { diff --git a/package.json b/package.json index 3fbbf3d2..ef45b622 100755 --- a/package.json +++ b/package.json @@ -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": { diff --git a/src/addons.js b/src/addons.js index 30475594..39e53007 100644 --- a/src/addons.js +++ b/src/addons.js @@ -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() { diff --git a/src/experiments.json b/src/experiments.json index 8dea498d..f18b7761 100644 --- a/src/experiments.json +++ b/src/experiments.json @@ -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} + ] } } \ No newline at end of file diff --git a/src/i18n.js b/src/i18n.js index 3c2f7a47..8312f520 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -337,7 +337,7 @@ export class TranslationManager extends Module { get locale() { - return this._.locale; + return this._ && this._.locale; } set locale(new_locale) { diff --git a/src/modules/chat/tokenizers.jsx b/src/modules/chat/tokenizers.jsx index 40792a69..93a3974f 100644 --- a/src/modules/chat/tokenizers.jsx +++ b/src/modules/chat/tokenizers.jsx @@ -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; diff --git a/src/modules/main_menu/index.js b/src/modules/main_menu/index.js index ee948d8b..5d625cf3 100644 --- a/src/modules/main_menu/index.js +++ b/src/modules/main_menu/index.js @@ -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; } diff --git a/src/settings/index.js b/src/settings/index.js index 0f22b4b4..efd5271a 100644 --- a/src/settings/index.js +++ b/src/settings/index.js @@ -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(); diff --git a/src/settings/providers.js b/src/settings/providers.js index 8d52c014..610351c7 100644 --- a/src/settings/providers.js +++ b/src/settings/providers.js @@ -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(); + }); + } + + + +} \ No newline at end of file diff --git a/src/sites/base.js b/src/sites/base.js index 44fe8501..65c1824d 100644 --- a/src/sites/base.js +++ b/src/sites/base.js @@ -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}`); } diff --git a/src/sites/twitch-twilight/index.js b/src/sites/twitch-twilight/index.js index b53391df..1993336a 100644 --- a/src/sites/twitch-twilight/index.js +++ b/src/sites/twitch-twilight/index.js @@ -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; diff --git a/src/sites/twitch-twilight/modules/channel.js b/src/sites/twitch-twilight/modules/channel.js index 71d63ed1..43c55fed 100644 --- a/src/sites/twitch-twilight/modules/channel.js +++ b/src/sites/twitch-twilight/modules/channel.js @@ -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); diff --git a/src/sites/twitch-twilight/modules/chat/index.js b/src/sites/twitch-twilight/modules/chat/index.js index cf700d2d..fa68d8f3 100644 --- a/src/sites/twitch-twilight/modules/chat/index.js +++ b/src/sites/twitch-twilight/modules/chat/index.js @@ -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(); + }); } diff --git a/src/sites/twitch-twilight/modules/chat/line.js b/src/sites/twitch-twilight/modules/chat/line.js index 958affbf..9010d581 100644 --- a/src/sites/twitch-twilight/modules/chat/line.js +++ b/src/sites/twitch-twilight/modules/chat/line.js @@ -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; } } diff --git a/src/sites/twitch-twilight/modules/css_tweaks/styles/emote-menu.scss b/src/sites/twitch-twilight/modules/css_tweaks/styles/emote-menu.scss index e2ba8fb8..01bb6c16 100644 --- a/src/sites/twitch-twilight/modules/css_tweaks/styles/emote-menu.scss +++ b/src/sites/twitch-twilight/modules/css_tweaks/styles/emote-menu.scss @@ -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; diff --git a/src/sites/twitch-twilight/styles/chat.scss b/src/sites/twitch-twilight/styles/chat.scss index 3a814996..11291684 100644 --- a/src/sites/twitch-twilight/styles/chat.scss +++ b/src/sites/twitch-twilight/styles/chat.scss @@ -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; diff --git a/src/utilities/logging.js b/src/utilities/logging.js index fdedb15a..00275317 100644 --- a/src/utilities/logging.js +++ b/src/utilities/logging.js @@ -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); diff --git a/styles/icons.scss b/styles/icons.scss index 90816d62..d62c25fb 100644 --- a/styles/icons.scss +++ b/styles/icons.scss @@ -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; } }