'use strict'; // ============================================================================ // Mod View Module // ============================================================================ import Module from 'utilities/module'; import {debounce} from 'utilities/object'; export default class ModView extends Module { constructor(...args) { super(...args); this.inject('i18n'); this.inject('settings'); this.inject('site.channel'); this.inject('site.css_tweaks'); this.inject('site.fine'); this.inject('site.elemental'); this.inject('site.router'); this.inject('site.twitch_data'); this.inject('metadata'); this.inject('socket'); this.inject('pubsub'); this.should_enable = true; this._cached_color = null; this._cached_channel = null; this._cached_id = null; this.Root = this.elemental.define( 'mod-view-root', '.moderation-view-page', ['mod-view'], {attributes: true}, 1 ); this.ModInfoBar = this.elemental.define( 'mod-info-bar', '.modview-player-widget__stream-info .scrollable-area', ['mod-view'], {childNodes: true, subtree: true}, 1, 30000, false ); this.checkRoot = debounce(this.checkRoot, 250); this.checkBar = debounce(this.checkBar, 250); } onEnable() { this.Root.on('mount', this.updateRoot, this); this.Root.on('mutate', this.updateRoot, this); this.Root.on('unmount', this.removeRoot, this); this.Root.each(el => this.updateRoot(el)); this.ModInfoBar.on('mount', this.updateBar, this); this.ModInfoBar.on('mutate', this.updateBar, this); this.ModInfoBar.on('unmount', this.removeBar, this); this.ModInfoBar.each(el => this.updateBar(el)); this.router.on(':route', this.checkNavigation, this); this.checkNavigation(); } updateSubscription(id, login) { if ( this._subbed_login === login && this._subbed_id === id ) return; if ( this._subbed_id ) { this.pubsub.unsubscribe(this, `twitch/${this._subbed_id}`); this._subbed_id = null; } if ( this._subbed_login ) { this.socket.unsubscribe(this, `channel.${this._subbed_login}`); this._subbed_login = null; } if ( id ) { this.pubsub.subscribe(this, `twitch/${id}`); this._subbed_id = id; } if ( login ) { this.socket.subscribe(this, `channel.${login}`); this._subbed_login = login; } } checkNavigation() { if ( this.router.current_name === 'mod-view' ) { this.channel.updateChannelColor(this._cached_color); this.checkRoot(); } } checkBar() { this.ModInfoBar.clean(); } checkRoot() { this.Root.each(el => this.updateRoot(el)); } updateRoot(el) { const root = this.fine.getReactInstance(el); let channel = null; let node = this.fine.searchNode(root, n => { let i = 0; let state = n.memoizedState; while(state != null && channel == null && i < 50) { channel = state?.memoizedState?.current?.result?.data?.user ?? state?.memoizedState?.current?.previousData?.user; if (!channel?.id || !channel?.login) channel = null; state = state?.next; i++; } if (channel) return true; }); if ( channel?.id ) { if ( this._cached_id != channel.id ) { this._cached_id = channel.id; this._cached_channel = channel; this._cached_color = null; this.updateSubscription(channel.id, channel.login); this.getChannelColor(el, channel.id).then(color => { if ( this._cached_id != channel.id ) return; this._cached_color = color; this.channel.updateChannelColor(color); this.settings.updateContext({ channelColor: color }); }).catch(() => { if ( this._cached_id != channel.id ) return; this._cached_color = null; this.channel.updateChannelColor(); this.settings.updateContext({ channelColor: null }); }); this.settings.updateContext({ channel: channel.login, channelID: channel.id }); } } else this.removeRoot(); } getChannelColor(el, channel_id, no_promise) { const cache = el._ffz_color_cache = el._ffz_color_cache || {}; if ( channel_id === cache.channel_id ) { if ( Date.now() - cache.saved < 60000 ) { if ( no_promise ) return cache.data; return Promise.resolve(cache.data); } } return new Promise(async (s, f) => { if ( cache.updating ) { cache.updating.push([s, f]); return ; } cache.channel_id = channel_id; cache.updating = [[s,f]]; let data, err; try { data = await this.twitch_data.getChannelColor(channel_id); } catch(error) { data = null; err = error; } const waiters = cache.updating; cache.updating = null; if ( cache.channel_id !== channel_id ) { err = new Error('Outdated'); cache.channel_id = null; cache.data = null; cache.saved = 0; for(const pair of waiters) pair[1](err); return; } cache.data = data; cache.saved = Date.now(); for(const pair of waiters) err ? pair[1](err) : pair[0](data); }); } removeRoot() { this._cached_id = null; this._cached_channel = null; this._cached_color = null; this.updateSubscription(); this.channel.updateChannelColor(); this.settings.updateContext({ channel: null, channelID: null, channelColor: null }); } updateBar(el) { const container = el.closest('.modview-player-widget__stream-info'), root = container && this.fine.getReactInstance(container); let channel = null, state = root?.return?.memoizedState, i = 0; while(state != null && channel == null && i < 50 ) { channel = state?.memoizedState?.current?.result?.data?.channel ?? state?.memoizedState?.current?.previousData?.channel; state = state?.next; i++; } const bcast = channel?.lastBroadcast, title = bcast?.title, game = bcast?.game; // This doesn't work because hosting in mod view. //if ( channel?.id && channel.id != this._cached_id ) // this.checkRoot(); if ( this._cached_bar_id != channel?.id ) { this._cached_bar_id = channel?.id; this._cached_bar_channel = channel; } if ( title != el._cached_title || game?.id != el._cached_game ) { el._cached_title = title; el._cached_game = game?.id; this.settings.updateContext({ category: game?.name, categoryID: game?.id, title }); } if ( container ) { if ( ! container._ffz_cont ) { const e = container._ffz_cont = container.querySelector('.modview-player-widget__viewcount'); if ( e ) e.classList.add('ffz--mod-tray'); } this.updateMetadata(container); } } removeBar(el) { this.settings.updateContext({ category: null, categoryID: null, title: null }); const container = el.closest('.modview-player-widget__stream-info'); if ( ! container ) return; if ( container._ffz_cont ) container._ffz_cont.classList.remove('ffz--mod-tray'); container._ffz_cont = null; if ( container._ffz_meta_timers ) { for(const val of Object.values(container._ffz_meta_timers)) clearTimeout(val); container._ffz_meta_timers = null; } container._ffz_update = null; } updateMetadata(el, keys) { const cont = el._ffz_cont, channel = this._cached_bar_channel; //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?.channel; i++; }*/ if ( ! cont || ! document.contains(cont) ) { this.checkBar(); return; } if ( ! channel?.id ) return; if ( ! keys ) keys = this.metadata.keys; else if ( ! Array.isArray(keys) ) keys = [keys]; const timers = el._ffz_meta_timers = el._ffz_meta_timers || {}, refresh_fn = key => this.updateMetadata(el, key), data = { channel: { id: channel.id, login: channel.login, display_name: channel.displayName }, el, getViewerCount: () => 0, getUserSelfImmediate: () => null, getUserSelf: () => null, getBroadcastID: () => null }; for(const key of keys) if ( this.metadata.definitions[key].modview ) this.metadata.renderLegacy(key, data, cont, timers, refresh_fn); } }