diff --git a/src/modules/chat/emotes.js b/src/modules/chat/emotes.js index c80ec8b1..27a42d07 100644 --- a/src/modules/chat/emotes.js +++ b/src/modules/chat/emotes.js @@ -6,8 +6,8 @@ import Module from 'utilities/module'; import {ManagedStyle} from 'utilities/dom'; -import {has, timeout} from 'utilities/object'; -import {API_SERVER} from 'utilities/constants'; +import {has, timeout, SourcedSet} from 'utilities/object'; +import {CLIENT_ID, API_SERVER} from 'utilities/constants'; const MODIFIERS = { @@ -61,13 +61,16 @@ export default class Emotes extends Module { this.__twitch_emote_to_set = new Map; this.__twitch_set_to_channel = new Map; - this.default_sets = new Set; - this.global_sets = new Set; + this.default_sets = new SourcedSet; + this.global_sets = new SourcedSet; this.emote_sets = {}; } onEnable() { + // Just in case there's a weird load order going on. + this.on('site:enabled', this.refresh_twitch_inventory); + this.style = new ManagedStyle('emotes'); if ( Object.keys(this.emote_sets).length ) { @@ -96,9 +99,9 @@ export default class Emotes extends Module { const room = this.parent.getRoom(room_id, room_login, true), user = this.parent.getUser(user_id, user_login, true); - return (user ? user.emote_sets : []).concat( - room ? room.emote_sets : [], - Array.from(this.default_sets) + return (user ? user.emote_sets._cache : []).concat( + room ? room.emote_sets._cache : [], + this.default_sets._cache ); } @@ -136,12 +139,11 @@ export default class Emotes extends Module { const sets = data.sets || {}; - for(const set_id of data.default_sets) - this.default_sets.add(set_id); + this.default_sets.extend('ffz-global', ...data.default_sets); for(const set_id in sets) if ( has(sets, set_id) ) { - this.global_sets.add(set_id); + this.global_sets.push('ffz-global', set_id); this.load_set_data(set_id, sets[set_id]); } @@ -160,8 +162,7 @@ export default class Emotes extends Module { const user = this.parent.getUser(undefined, login), sets = user.emote_sets; - if ( sets.indexOf(set_id) === -1 ) - sets.push(set_id); + sets.push('ffz-global', set_id); } this.log.info(`Added "${emote_set ? emote_set.title : set_id}" emote set to ${users.length} users.`); @@ -279,8 +280,27 @@ export default class Emotes extends Module { // Twitch Data Lookup // ======================================================================== - refresh_twitch_inventory() { - this.log.debug('Unimplemented: refresh_twitch_inventory'); + async refresh_twitch_inventory() { + const user = this.resolve('site').getUser(); + if ( ! user ) + return; + + let data; + try { + data = await fetch('https://api.twitch.tv/v5/inventory/emoticons', { + headers: { + 'Client-ID': CLIENT_ID, + 'Authorization': `OAuth ${user.authToken}` + } + }).then(r => r.json()); + + } catch(err) { + this.log.error('Error loading Twitch inventory.', err); + return; + } + + this.twitch_inventory_sets = data.emoticon_sets ? Object.keys(data.emoticon_sets) : []; + this.log.info('Twitch Inventory Sets:', this.twitch_inventory_sets); } diff --git a/src/modules/chat/index.js b/src/modules/chat/index.js index 4668b101..76e73914 100644 --- a/src/modules/chat/index.js +++ b/src/modules/chat/index.js @@ -9,7 +9,7 @@ const WEBKIT = IS_WEBKIT ? '-webkit-' : ''; import Module from 'utilities/module'; import {createElement, ManagedStyle} from 'utilities/dom'; -import {timeout, has} from 'utilities/object'; +import {timeout, has, SourcedSet} from 'utilities/object'; import Badges from './badges'; import Emotes from './emotes'; @@ -283,7 +283,7 @@ export default class Chat extends Module { return null; else - user = {id, login, badges: [], emote_sets: []}; + user = {id, login, badges: [], emote_sets: new SourcedSet}; if ( id && id !== user.id ) { // If the ID isn't what we expected, something is very wrong here. diff --git a/src/modules/chat/room.js b/src/modules/chat/room.js index ace10d80..fd7b269d 100644 --- a/src/modules/chat/room.js +++ b/src/modules/chat/room.js @@ -8,7 +8,7 @@ import {API_SERVER, IS_WEBKIT} from 'utilities/constants'; import {EventEmitter} from 'utilities/events'; import {createElement as e, ManagedStyle} from 'utilities/dom'; -import {has} from 'utilities/object'; +import {has, SourcedSet} from 'utilities/object'; const WEBKIT = IS_WEBKIT ? '-webkit-' : ''; @@ -20,42 +20,71 @@ export default class Room extends EventEmitter { this._destroy_timer = null; this.manager = manager; - this.id = id; + this._id = id; this.login = login; - if ( login ) - this.manager.rooms[login] = this; - if ( id ) this.manager.room_ids[id] = this; this.refs = new Set; this.style = new ManagedStyle(`room--${login}`); - this.emote_sets = []; + this.emote_sets = new SourcedSet; this.users = []; this.user_ids = []; - if ( this.login ) { - this.manager.socket.subscribe(`room.${login}`); - this.load_data(); - } + this.manager.emit(':room-add', this); + this.load_data(); } + destroy() { clearTimeout(this._destroy_timer); this._destroy_timer = null; + this.destroyed = true; - this.manager.socket.unsubscribe(`room.${this.login}`); + this.manager.emit(':room-remove', this); this.style.destroy(); + if ( this._login ) { + if ( this.manager.rooms[this._login] === this ) + this.manager.rooms[this._login] = null; + + this.manager.socket.unsubscribe(`room.${this.login}`); + } + if ( this.manager.room_ids[this.id] === this ) this.manager.room_ids[this.id] = null; + } - if ( this.manager.rooms[this.login] === this ) - this.manager.rooms[this.login] = null; + + get id() { + return this._id; + } + + get login() { + return this._login; + } + + set login(val) { + if ( this._login === val ) + return; + + if ( this._login ) { + if ( this.manager.rooms[this._login] === this ) + this.manager.rooms[this._login] = null; + + this.manager.socket.unsubscribe(`room.${this.login}`); + } + + this._login = val; + if ( ! val ) + return; + + this.manager.socket.subscribe(`room.${val}`); + this.manager.emit(':room-update-login', this, val); } @@ -69,7 +98,7 @@ export default class Room extends EventEmitter { let response, data; try { - response = await fetch(`${API_SERVER}/v1/room/${this.login}`); + response = await fetch(`${API_SERVER}/v1/room/${this.id ? `id/${this.id}` : this.login}`); } catch(err) { tries++; if ( tries < 10 ) @@ -89,9 +118,26 @@ export default class Room extends EventEmitter { return false; } - const d = this.data = data.room; - if ( d.set && this.emote_sets.indexOf(d) === -1 ) - this.emote_sets.push(d.set); + const d = data.room, + id = '' + d.twitch_id; + + if ( ! this._id ) { + this._id = id; + this.manager.room_ids[id] = this; + + } else if ( this._id !== id ) { + this.manager.log.warn(`Received data for ${this.id}:${this.login} with the wrong ID: ${id}`); + return false; + } + + this.login = d.id; + this.data = d; + + if ( d.set ) + this.emote_sets.set('main', d.set); + else + this.emote_sets.delete('main'); + if ( data.sets ) for(const set_id in data.sets) diff --git a/src/utilities/constants.js b/src/utilities/constants.js index 66969762..bc99c32d 100644 --- a/src/utilities/constants.js +++ b/src/utilities/constants.js @@ -3,6 +3,7 @@ export const DEBUG = localStorage.ffzDebugMode === 'true' && document.body.classList.contains('ffz-dev'); export const SERVER = DEBUG ? '//localhost:8000' : 'https://cdn.frankerfacez.com'; +export const CLIENT_ID = 'a3bc9znoz6vi8ozsoca0inlcr4fcvkl'; export const API_SERVER = '//api.frankerfacez.com'; export const WS_CLUSTERS = { diff --git a/src/utilities/object.js b/src/utilities/object.js index de114c0c..833bc6f7 100644 --- a/src/utilities/object.js +++ b/src/utilities/object.js @@ -164,4 +164,76 @@ export function maybe_call(fn, ctx, ...args) { } return fn; +} + + +export class SourcedSet { + constructor() { + this._cache = []; + } + + _rebuild() { + if ( ! this._sources ) + return; + + this._cache = []; + for(const items of this._sources.values()) + for(const i of items) + if ( ! this._cache.includes(i) ) + this._cache.push(i); + } + + get(key) { return this._sources && this._sources.get(key) } + has(key) { return this._sources ? this._sources.has(key) : false } + + delete(key) { + if ( this._sources && this._sources.has(key) ) { + this._sources.delete(key); + this._rebuild(); + } + } + + extend(key, ...items) { + if ( ! this._sources ) + this._sources = new Map; + + const had = this.has(key); + this._sources.set(key, [false, items]); + if ( had ) + this._rebuild(); + else + for(const i of items) + if ( ! this._cache.includes(i) ) + this._cache.push(i); + } + + set(key, val) { + if ( ! this._sources ) + this._sources = new Map; + + const had = this.has(key); + this._sources.set(key, [val]); + + if ( had ) + this._rebuild(); + + else if ( ! this._cache.includes(val) ) + this._cache.push(val); + } + + push(key, val) { + if ( ! this._sources ) + return this.set(key, val); + + const old_val = this._sources.get(key); + if ( old_val === undefined ) + return this.set(key, val); + + else if ( old_val.includes(val) ) + return; + + old_val.push(val); + if ( ! this._cache.includes(val) ) + this._cache.push(val); + } } \ No newline at end of file