From 92fcc853a60c9e95c48fd8108dd7e7349272e0bb Mon Sep 17 00:00:00 2001 From: SirStendec Date: Wed, 13 Sep 2023 16:08:10 -0400 Subject: [PATCH] 4.53.0 * Added: Setting to hide streams in the directory based upon tags. * Added: Setting to not automatically join raids to specific channels. * Added: Setting to attempt to display Golden Kappa Trains when Hype Trains are otherwise hidden. * Added: Settings profile filter rule for when the window is in fullscreen. * Fixed: Do not activate theater mode settings when in fullscreen. * API Added: The `site.player` module now has a `getUptime` method for getting the uptime of the current stream, if one is available. * API Removed: The tag-related methods of `site.twitch_data`, aside from `getMatchingTags` which now has a different signature. The methods were non-functional due to Twitch removing the relevant endpoints. * Developer: Added a debugging tool for viewing GraphQL queries in Apollo's cache. * Maintenance: Tweak the webpack build to hopefully get Mozilla to stop complaining that their build environment is weird while accusing me of having the weird build environment. --- package.json | 2 +- .../main_menu/components/chat-tester.vue | 33 +-- .../main_menu/components/graphql-inspect.vue | 160 +++++++++++ .../main_menu/components/tag-list-editor.vue | 134 +++++++++ src/modules/main_menu/index.js | 9 +- src/settings/filters.js | 19 +- src/sites/player/player.jsx | 4 + src/sites/shared/player.jsx | 21 +- src/sites/twitch-twilight/index.js | 7 + .../twitch-twilight/modules/chat/index.js | 93 ++++++- .../modules/css_tweaks/index.js | 4 +- .../modules/directory/game.jsx | 2 +- .../modules/directory/index.jsx | 66 +---- src/sites/twitch-twilight/modules/layout.js | 17 +- .../twitch-twilight/modules/loadable.jsx | 103 +++++++ src/sites/twitch-twilight/modules/player.jsx | 15 +- src/std-components/autocomplete.vue | 3 +- src/utilities/compat/apollo.js | 18 +- src/utilities/data/search-tags.gql | 11 - src/utilities/data/tags-fetch.gql | 11 - src/utilities/data/tags-top.gql | 11 - src/utilities/dom.js | 43 +++ src/utilities/twitch-data.js | 261 +----------------- src/utilities/vue.js | 2 +- styles/widgets.scss | 4 + webpack.config.js | 15 + 26 files changed, 665 insertions(+), 403 deletions(-) create mode 100644 src/modules/main_menu/components/graphql-inspect.vue create mode 100644 src/modules/main_menu/components/tag-list-editor.vue create mode 100644 src/sites/twitch-twilight/modules/loadable.jsx delete mode 100644 src/utilities/data/search-tags.gql delete mode 100644 src/utilities/data/tags-fetch.gql delete mode 100644 src/utilities/data/tags-top.gql diff --git a/package.json b/package.json index fffea042..829c6e67 100755 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "frankerfacez", "author": "Dan Salvato LLC", - "version": "4.52.0", + "version": "4.53.0", "description": "FrankerFaceZ is a Twitch enhancement suite.", "private": true, "license": "Apache-2.0", diff --git a/src/modules/main_menu/components/chat-tester.vue b/src/modules/main_menu/components/chat-tester.vue index 4147e3c1..521f949b 100644 --- a/src/modules/main_menu/components/chat-tester.vue +++ b/src/modules/main_menu/components/chat-tester.vue @@ -196,8 +196,8 @@ \ No newline at end of file diff --git a/src/modules/main_menu/components/tag-list-editor.vue b/src/modules/main_menu/components/tag-list-editor.vue new file mode 100644 index 00000000..1e3849d0 --- /dev/null +++ b/src/modules/main_menu/components/tag-list-editor.vue @@ -0,0 +1,134 @@ + + + \ No newline at end of file diff --git a/src/modules/main_menu/index.js b/src/modules/main_menu/index.js index 0861d1ae..111b1dba 100644 --- a/src/modules/main_menu/index.js +++ b/src/modules/main_menu/index.js @@ -151,6 +151,13 @@ export default class MainMenu extends Module { } }); + this.settings.addUI('debug.graphql-test', { + path: 'Debugging > GraphQL >> Inspector', + component: 'graphql-inspect', + getFFZ: () => this.resolve('core'), + force_seen: true + }); + this.settings.addUI('faq', { path: 'Home > FAQ @{"profile_warning": false}', component: 'md-page', @@ -1210,7 +1217,7 @@ export default class MainMenu extends Module { if ( this.dialog.exclusive || this.site?.router?.current_name === 'squad' || this.site?.router?.current_name === 'command-center' ) return; - if ( this.settings.get('context.ui.theatreModeEnabled') ) + if ( this.settings.get('layout.is-theater-mode') ) return; this.dialog.toggleSize(e); diff --git a/src/settings/filters.js b/src/settings/filters.js index c5ad3443..33b3a8e7 100644 --- a/src/settings/filters.js +++ b/src/settings/filters.js @@ -172,7 +172,11 @@ export const Time = { export const TheaterMode = { createTest(config) { - return ctx => ctx.ui && ctx.ui.theatreModeEnabled === config; + return ctx => { + if ( ctx.fullscreen ) + return config === false; + return ctx.ui && ctx.ui.theatreModeEnabled === config; + } }, title: 'Theater Mode', @@ -183,6 +187,19 @@ export const TheaterMode = { editor: () => import(/* webpackChunkName: 'main-menu' */ './components/basic-toggle.vue') }; +export const Fullscreen = { + createTest(config) { + return ctx => ctx.fullscreen === config; + }, + + title: 'Fullscreen', + i18n: 'settings.filter.fullscreen', + + default: true, + + editor: () => import(/* webpackChunkName: 'main-menu' */ './components/basic-toggle.vue') +}; + export const Moderator = { createTest(config) { return ctx => ctx.moderator === config; diff --git a/src/sites/player/player.jsx b/src/sites/player/player.jsx index b101ec7f..fe7578da 100644 --- a/src/sites/player/player.jsx +++ b/src/sites/player/player.jsx @@ -29,6 +29,10 @@ export default class Player extends PlayerBase { return this.settings.get('player.embed-metadata'); } + getData() { + return this.parent.data; + } + async getBroadcastID(inst, channel_id) { this.twitch_data = await this.parent.awaitTwitchData(); return super.getBroadcastID(inst, channel_id); diff --git a/src/sites/shared/player.jsx b/src/sites/shared/player.jsx index d49ffb2b..e634ece2 100644 --- a/src/sites/shared/player.jsx +++ b/src/sites/shared/player.jsx @@ -2214,7 +2214,7 @@ export default class PlayerBase extends Module { else if ( ! Array.isArray(keys) ) keys = [keys]; - const source = this.parent.data, + const source = this.getData(), user = source?.props?.data?.user; const timers = inst._ffz_meta_timers = inst._ffz_meta_timers || {}, @@ -2240,6 +2240,25 @@ export default class PlayerBase extends Module { } + getUptime(inst) { + // TODO: Support multiple instances. + const source = this.getData(), + user = source?.props?.data?.user; + + let created = user?.stream?.createdAt; + + if ( ! created ) + return null; + + if ( !(created instanceof Date) ) + created = new Date(created); + + const now = Date.now(); + + return (now - created.getTime()) / 1000; + } + + getBroadcastID(inst, channel_id) { if ( ! this.twitch_data ) return Promise.resolve(null); diff --git a/src/sites/twitch-twilight/index.js b/src/sites/twitch-twilight/index.js index 72027834..fd1256eb 100644 --- a/src/sites/twitch-twilight/index.js +++ b/src/sites/twitch-twilight/index.js @@ -106,6 +106,13 @@ export default class Twilight extends BaseSite { window.addEventListener('resize', update_size); update_size(); + const update_fullscreen = () => this.settings.updateContext({ + fullscreen: !! document.fullscreenElement + }); + + document.addEventListener('fullscreenchange', update_fullscreen); + update_fullscreen(); + // Share Context store.subscribe(() => this.updateContext()); this.updateContext(); diff --git a/src/sites/twitch-twilight/modules/chat/index.js b/src/sites/twitch-twilight/modules/chat/index.js index 6ec4fa1c..dc800779 100644 --- a/src/sites/twitch-twilight/modules/chat/index.js +++ b/src/sites/twitch-twilight/modules/chat/index.js @@ -5,7 +5,7 @@ // ============================================================================ import {Color, ColorAdjuster} from 'utilities/color'; -import {get, has, make_enum, shallow_object_equals, set_equals, deep_equals} from 'utilities/object'; +import {get, has, make_enum, shallow_object_equals, set_equals, deep_equals, glob_to_regex, escape_regex} from 'utilities/object'; import {WEBKIT_CSS as WEBKIT} from 'utilities/constants'; import {FFZEvent} from 'utilities/events'; import {useFont} from 'utilities/fonts'; @@ -356,6 +356,50 @@ export default class ChatHook extends Module { } }); + this.settings.add('channel.raids.blocked-channels', { + default: [], + type: 'array_merge', + always_inherit: true, + ui: { + path: 'Channel > Behavior >> Raids: Blocked Channels @{"description": "You will not automatically join raids to channels listed here."}', + component: 'basic-terms', + words: false + } + }); + + this.settings.add('__filter:channel.raids.blocked-channels', { + requires: ['channel.raids.blocked-channels'], + equals: 'requirements', + process(ctx) { + const val = ctx.get('channel.raids.blocked-channels'); + if ( ! val || ! val.length ) + return null; + + const out = []; + + for(const item of val) { + const t = item.t; + let v = item.v; + + if ( t === 'glob' ) + v = glob_to_regex(v); + + else if ( t !== 'raw' ) + v = escape_regex(v); + + if ( ! v || ! v.length ) + continue; + + out.push(v); + } + + if ( out.length ) + return new RegExp(`^(?:${out.join('|')})$`, 'gi'); + + return null; + } + }) + this.settings.add('chat.hide-community-highlights', { default: false, ui: { @@ -402,6 +446,16 @@ export default class ChatHook extends Module { } }); + this.settings.add('chat.banners.kappa-train', { + default: false, + ui: { + path: 'Chat > Appearance >> Community', + title: 'Attempt to always display the Golden Kappa Train, even if other Hype Trains are hidden.', + description: '**Note**: This setting is currently theoretical and may not work, or may cause non-Kappa hype trains to appear. Due to the infrequent nature of hype trains, and especially the golden kappa hype train, it is very hard to test.', + component: 'setting-check-box' + } + }); + this.settings.add('chat.banners.drops', { default: true, ui: { @@ -1570,17 +1624,32 @@ export default class ChatHook extends Module { } noAutoRaids(inst) { - if ( this.settings.get('channel.raids.no-autojoin') ) - setTimeout(() => { - if ( inst.props && inst.props.raid && ! inst.isRaidCreator && inst.hasJoinedCurrentRaid ) { - const id = inst.props.raid.id; - if ( this.joined_raids.has(id) ) - return; + if ( inst._ffz_no_raid ) + return; - this.log.info('Automatically leaving raid:', id); - inst.handleLeaveRaid(); + inst._ffz_no_raid = setTimeout(() => { + inst._ffz_no_raid = null; + + if ( inst.props && inst.props.raid && ! inst.isRaidCreator && inst.hasJoinedCurrentRaid ) { + const id = inst.props.raid.id; + if ( this.joined_raids.has(id) ) + return; + + let leave = this.settings.get('channel.raids.no-autojoin'); + + if ( ! leave ) { + const blocked = this.settings.get('__filter:channel.raids.blocked-channels'); + if ( blocked && ((inst.props.raid.targetLogin && blocked.test(inst.props.raid.targetLogin)) || (inst.props.raid.targetDisplayName && blocked.test(inst.props.raid.targetDisplayName))) ) + leave = true; + + if ( ! leave ) + return; } - }); + + this.log.info('Automatically leaving raid:', id); + inst.handleLeaveRaid(); + } + }); } toggleEmoteJail() { @@ -1611,6 +1680,10 @@ export default class ChatHook extends Module { const type = entry.event.type; if ( type && has(types, type) && ! types[type] ) { + // Attempt to allow Golden Kappa hype trains? + if ( type === 'hype_train' && entry.event.typeDetails === '0' && this.chat.context.get('chat.banners.kappa-train') ) + continue; + this.log.info('Removing community highlight: ', type, '#', entry.id); this.community_dispatch({ type: 'remove-highlight', diff --git a/src/sites/twitch-twilight/modules/css_tweaks/index.js b/src/sites/twitch-twilight/modules/css_tweaks/index.js index d79ddf23..f8f0bd6e 100644 --- a/src/sites/twitch-twilight/modules/css_tweaks/index.js +++ b/src/sites/twitch-twilight/modules/css_tweaks/index.js @@ -304,10 +304,10 @@ export default class CSSTweaks extends Module { }); this.settings.add('layout.theatre-navigation', { - requires: ['context.ui.theatreModeEnabled'], + requires: ['layout.is-theater-mode'], default: false, process(ctx, val) { - return ctx.get('context.ui.theatreModeEnabled') ? val : false + return ctx.get('layout.is-theater-mode') ? val : false }, ui: { path: 'Appearance > Layout >> Top Navigation', diff --git a/src/sites/twitch-twilight/modules/directory/game.jsx b/src/sites/twitch-twilight/modules/directory/game.jsx index 560f19c9..181dff0b 100644 --- a/src/sites/twitch-twilight/modules/directory/game.jsx +++ b/src/sites/twitch-twilight/modules/directory/game.jsx @@ -51,7 +51,7 @@ export default class Game extends SiteModule { }); this.settings.provider.on('changed', key => { - if ( key === 'directory.game.blocked-games' || key === 'directory.game.hidden-thumbnails' || key === 'directory.game.blocked-tags' ) { + if ( key === 'directory.game.blocked-games' || key === 'directory.game.hidden-thumbnails' ) { this.parent.updateCards(); for(const inst of this.GameHeader.instances) diff --git a/src/sites/twitch-twilight/modules/directory/index.jsx b/src/sites/twitch-twilight/modules/directory/index.jsx index 9ed24f67..92854b11 100644 --- a/src/sites/twitch-twilight/modules/directory/index.jsx +++ b/src/sites/twitch-twilight/modules/directory/index.jsx @@ -235,6 +235,17 @@ export default class Directory extends SiteModule { changed: () => this.updateCards() }); + this.settings.add('directory.blocked-tags', { + default: [], + type: 'basic_array_merge', + always_inherit: true, + ui: { + path: 'Directory > Channels >> Block by Tag', + component: 'tag-list-editor' + }, + changed: () => this.updateCards() + }); + /*this.settings.add('directory.hide-viewing-history', { default: false, ui: { @@ -342,7 +353,7 @@ export default class Directory extends SiteModule { let bad_tag = false; if ( Array.isArray(tags) ) { - const bad_tags = this.settings.provider.get('directory.game.blocked-tags', []); + const bad_tags = this.settings.get('directory.blocked-tags', []); if ( bad_tags.length ) { for(const tag of tags) { if ( tag?.id && bad_tags.includes(tag.id) ) { @@ -380,7 +391,7 @@ export default class Directory extends SiteModule { return; const game = props.gameTitle || props.trackingProps?.categoryName || props.trackingProps?.category || props.contextualCardActionProps?.props?.categoryName, - tags = props.tagListProps?.tags; + tags = props.tagListProps?.freeformTags; let bad_tag = false; @@ -388,10 +399,10 @@ export default class Directory extends SiteModule { el.dataset.ffzType = props.streamType; if ( Array.isArray(tags) ) { - const bad_tags = this.settings.provider.get('directory.game.blocked-tags', []); + const bad_tags = this.settings.get('directory.blocked-tags', []); if ( bad_tags.length ) { for(const tag of tags) { - if ( tag?.id && bad_tags.includes(tag.id) ) { + if ( tag?.name && bad_tags.includes(tag.name.toLowerCase()) ) { bad_tag = true; break; } @@ -535,53 +546,6 @@ export default class Directory extends SiteModule { } - /*updateAvatar(inst) { - const container = this.fine.getChildNode(inst), - card = container && container.querySelector && container.querySelector('.preview-card-overlay'), - setting = this.settings.get('directory.show-channel-avatars'); - - if ( ! card ) - return; - - const props = inst.props, - is_video = props.durationInSeconds != null, - src = props.channelImageProps && props.channelImageProps.src; - - const avatar = card.querySelector('.ffz-channel-avatar'); - - if ( ! src || setting < 2 || props.context === CARD_CONTEXTS.SingleChannelList ) { - if ( avatar ) - avatar.remove(); - - return; - } - - if ( setting === inst.ffz_av_setting && props.channelLogin === inst.ffz_av_login && src === inst.ffz_av_src ) - return; - - if ( avatar ) - avatar.remove(); - - inst.ffz_av_setting = setting; - inst.ffz_av_login = props.channelLogin; - inst.ffz_av_src = src; - - const link = props.channelLinkTo && props.channelLinkTo.pathname; - - card.appendChild( this.routeClick(e, link)} // eslint-disable-line react/jsx-no-bind - > -
-
- -
-
-
); - }*/ - - routeClick(event, url) { event.preventDefault(); event.stopPropagation(); diff --git a/src/sites/twitch-twilight/modules/layout.js b/src/sites/twitch-twilight/modules/layout.js index 6615f71d..1fca497e 100644 --- a/src/sites/twitch-twilight/modules/layout.js +++ b/src/sites/twitch-twilight/modules/layout.js @@ -162,6 +162,15 @@ export default class Layout extends Module { changed: val => this.css_tweaks.toggle('portrait-metadata-top', val) }); + this.settings.add('layout.is-theater-mode', { + requires: ['context.ui.theatreModeEnabled', 'context.fullscreen'], + process(ctx) { + if ( ctx.get('context.fullscreen') ) + return false; + return ctx.get('context.ui.theatreModeEnabled'); + } + }); + this.settings.add('layout.show-portrait-chat', { requires: ['layout.use-portrait', 'layout.portrait-extra-height', 'layout.portrait-extra-width'], process() { @@ -172,10 +181,10 @@ export default class Layout extends Module { }); this.settings.add('layout.portrait-extra-height', { - requires: ['context.new_channel', 'context.squad_bar', /*'context.hosting',*/ 'context.ui.theatreModeEnabled', 'player.theatre.no-whispers', 'whispers.show', 'layout.minimal-navigation'], + requires: ['context.new_channel', 'context.squad_bar', /*'context.hosting',*/ 'layout.is-theater-mode', 'player.theatre.no-whispers', 'whispers.show', 'layout.minimal-navigation'], process(ctx) { let height = 0; - if ( ctx.get('context.ui.theatreModeEnabled') ) { + if ( ctx.get('layout.is-theater-mode') ) { if ( ctx.get('layout.minimal-navigation') ) height += 1; @@ -203,9 +212,9 @@ export default class Layout extends Module { }) this.settings.add('layout.portrait-extra-width', { - require: ['layout.side-nav.show', 'context.ui.theatreModeEnabled', 'context.ui.sideNavExpanded'], + require: ['layout.side-nav.show', 'layout.is-theater-mode', 'context.ui.sideNavExpanded'], process(ctx) { - if ( ! ctx.get('layout.side-nav.show') || ctx.get('context.ui.theatreModeEnabled') ) + if ( ! ctx.get('layout.side-nav.show') || ctx.get('layout.is-theater-mode') ) return 0; return ctx.get('context.ui.sideNavExpanded') ? 24 : 5 diff --git a/src/sites/twitch-twilight/modules/loadable.jsx b/src/sites/twitch-twilight/modules/loadable.jsx new file mode 100644 index 00000000..5f2bc949 --- /dev/null +++ b/src/sites/twitch-twilight/modules/loadable.jsx @@ -0,0 +1,103 @@ +'use strict'; + +// ============================================================================ +// Loadable Stuff +// ============================================================================ + +import Module from 'utilities/module'; + + +export default class Loadable extends Module { + constructor(...args) { + super(...args); + + this.should_enable = true; + + this.inject('settings'); + this.inject('site.fine'); + this.inject('site.web_munch'); + + this.LoadableComponent = this.fine.define( + 'loadable-component', + n => n.props?.component && n.props.loader + ); + + this.overrides = new Map(); + + } + + onEnable() { + this.settings.getChanges('chat.hype.show-pinned', val => { + this.toggle('PaidPinnedChatMessageList', val); + }); + + this.LoadableComponent.ready((cls, instances) => { + this.log.debug('Found Loadable component wrapper.'); + + const t = this, + old_render = cls.prototype.render; + + cls.prototype.render = function() { + try { + const type = this.props.component; + if ( t.overrides.has(type) ) { + let cmp = this.state.Component; + if ( typeof cmp === 'function' && ! cmp.ffzWrapped ) { + const React = t.web_munch.getModule('react'), + createElement = React && React.createElement; + + if ( createElement ) { + if ( ! cmp.ffzWrapper ) { + const th = this; + function FFZWrapper(props, state) { + if ( t.shouldRender(th.props.component, props, state) ) + return createElement(cmp, props); + return null; + } + + FFZWrapper.ffzWrapped = true; + FFZWrapper.displayName = `FFZWrapper(${this.props.component})`; + cmp.ffzWrapper = FFZWrapper; + } + + this.state.Component = cmp.ffzWrapper; + } + } + } + } catch(err) { + /* no-op */ + console.error(err); + } + + return old_render.call(this); + } + + }); + } + + toggle(cmp, state = null) { + const existing = this.overrides.get(cmp) ?? true; + + if ( state == null ) + state = ! existing; + else + state = !! state; + + if ( state !== existing ) { + this.overrides.set(cmp, state); + this.update(cmp); + } + } + + update(cmp) { + for(const inst of this.LoadableComponent.instances) { + if ( inst?.props?.component === cmp ) + inst.forceUpdate(); + } + } + + shouldRender(cmp, props) { + return this.overrides.get(cmp) ?? true; + } + +} \ No newline at end of file diff --git a/src/sites/twitch-twilight/modules/player.jsx b/src/sites/twitch-twilight/modules/player.jsx index 1fdf17ec..d48541c4 100644 --- a/src/sites/twitch-twilight/modules/player.jsx +++ b/src/sites/twitch-twilight/modules/player.jsx @@ -41,6 +41,12 @@ export default class Player extends PlayerBase { n => n.state && n.state.playerStyles );*/ + this.DataSource = this.fine.define( + 'data-source', + n => n.consentMetadata && n.onPlaying && n.props && n.props.data, + PLAYER_ROUTES + ); + this.Player = this.fine.define( 'highwind-player', n => n.setPlayerActive && n.props?.playerEvents && n.props?.mediaPlayerInstance, @@ -108,9 +114,9 @@ export default class Player extends PlayerBase { this.settings.add('player.theatre.metadata', { default: false, - requires: ['context.route.name'], + requires: ['context.route.name', 'layout.is-theater-mode'], process(ctx, val) { - if ( ctx.get('context.route.name') === 'video' ) + if ( ! ctx.get('layout.is-theater-mode') || ctx.get('context.route.name') === 'video' ) return false; return val }, @@ -215,6 +221,11 @@ export default class Player extends PlayerBase { } + getData() { + return this.DataSource.first; + } + + tryTheatreMode(inst) { if ( ! inst._ffz_theater_timer ) inst._ffz_theater_timer = setTimeout(() => { diff --git a/src/std-components/autocomplete.vue b/src/std-components/autocomplete.vue index ef1c2d4b..5d196e6f 100644 --- a/src/std-components/autocomplete.vue +++ b/src/std-components/autocomplete.vue @@ -54,7 +54,8 @@ >
- {{ item.displayName || item.label || item.name }} + {{ item }} + {{ item.displayName || item.label || item.name }}
diff --git a/src/utilities/compat/apollo.js b/src/utilities/compat/apollo.js index e4351d53..e214ee50 100644 --- a/src/utilities/compat/apollo.js +++ b/src/utilities/compat/apollo.js @@ -384,18 +384,16 @@ export default class Apollo extends Module { // ======================================================================== getQuery(operation) { - const qm = this.client.queryManager, - name_map = qm && qm.queryIdsByName, - query_map = qm && qm.queries, - query_id = name_map && name_map[operation], - query = query_map && query_id && query_map.get(Array.isArray(query_id) ? query_id[0] : query_id); + const query_map = this.client.queryManager?.queries; - if ( ! query_map && ! this.warn_qm ) { - this.log.error('Unable to find the Apollo query map. We cannot access data properly.'); - this.warn_qm = true; + if ( ! query_map ) + return; + + for(const val of query_map.values()) { + const obs = val?.observableQuery; + if ( obs?.queryName === operation ) + return obs; } - - return query && query.observableQuery; } diff --git a/src/utilities/data/search-tags.gql b/src/utilities/data/search-tags.gql deleted file mode 100644 index 36cab7c1..00000000 --- a/src/utilities/data/search-tags.gql +++ /dev/null @@ -1,11 +0,0 @@ -query FFZ_SearchLiveTags($query: String!, $categoryID: ID, $limit: Int) { - searchLiveTags(userQuery: $query, categoryID: $categoryID, limit: $limit) { - id - isAutomated - isLanguageTag - localizedDescription - localizedName - scope - tagName - } -} \ No newline at end of file diff --git a/src/utilities/data/tags-fetch.gql b/src/utilities/data/tags-fetch.gql deleted file mode 100644 index 85db1c11..00000000 --- a/src/utilities/data/tags-fetch.gql +++ /dev/null @@ -1,11 +0,0 @@ -query FFZ_FetchTags($ids: [ID!]) { - contentTags(ids: $ids) { - id - isAutomated - isLanguageTag - localizedDescription - localizedName - scope - tagName - } -} \ No newline at end of file diff --git a/src/utilities/data/tags-top.gql b/src/utilities/data/tags-top.gql deleted file mode 100644 index 0ac9c3df..00000000 --- a/src/utilities/data/tags-top.gql +++ /dev/null @@ -1,11 +0,0 @@ -query FFZ_TopTags($limit: Int) { - topTags(limit: $limit) { - id - isAutomated - isLanguageTag - localizedDescription - localizedName - scope - tagName - } -} \ No newline at end of file diff --git a/src/utilities/dom.js b/src/utilities/dom.js index 0bdfd055..3f63cf0c 100644 --- a/src/utilities/dom.js +++ b/src/utilities/dom.js @@ -329,4 +329,47 @@ export class ClickOutside { if ( this.el && ! this.el.contains(e.target) ) this.cb(e); } +} + + +// TODO: Rewrite this method to not use raw HTML. + +export function highlightJson(object, pretty = false, depth = 1) { + let indent = '', indent_inner = ''; + if ( pretty ) { + indent = ' '.repeat(depth - 1); + indent_inner = ' '.repeat(depth); + } + + if ( depth > 10 ) + return `<nested>`; + + if (object == null) + return `null`; + + if ( typeof object === 'number' || typeof object === 'boolean' ) + return `${object}`; + + if ( typeof object === 'string' ) + return `"${sanitize(object)}"`; + + if ( Array.isArray(object) ) + return `[` + + object.map(x => (pretty ? `\n${indent_inner}` : '') + highlightJson(x, pretty, depth + 1)).join(`, `) + + (pretty ? `\n${indent}` : '') + + `]`; + + const out = []; + + for(const [key, val] of Object.entries(object)) { + if ( out.length > 0 ) + out.push(`, `); + + if ( pretty ) + out.push(`\n${indent_inner}`); + out.push(`"${sanitize(key)}": `); + out.push(highlightJson(val, pretty, depth + 1)); + } + + return `{${out.join('')}${pretty ? `\n${indent}` : ''}}`; } \ No newline at end of file diff --git a/src/utilities/twitch-data.js b/src/utilities/twitch-data.js index e9b29c12..d3d10ae6 100644 --- a/src/utilities/twitch-data.js +++ b/src/utilities/twitch-data.js @@ -794,247 +794,6 @@ export default class TwitchData extends Module { // Tags // ======================================================================== - memorizeTag(node, dispatch = true) { - // We want properly formed tags. - if ( ! node || ! node.id || ! node.tagName || ! node.localizedName ) - return; - - let tag = this.tag_cache.get(node.id); - if ( ! tag ) { - const match = node.isLanguageTag && LANGUAGE_MATCHER.exec(node.tagName), - lang = match && match[1] || null; - - tag = { - id: node.id, - value: node.id, - is_auto: node.isAutomated, - is_language: node.isLanguageTag, - language: lang, - name: node.tagName, - scope: node.scope - }; - - this.tag_cache.set(node.id, tag); - } - - if ( node.localizedName ) - tag.label = node.localizedName; - if ( node.localizedDescription ) - tag.description = node.localizedDescription; - - if ( dispatch && tag.description && this._waiting_tags.has(tag.id) ) { - const promises = this._waiting_tags.get(tag.id); - this._waiting_tags.delete(tag.id); - for(const pair of promises) - pair[0](tag); - } - - return tag; - } - - async _loadTags() { - if ( this._loading_tags ) - return; - - this._loading_tags = true; - - // Get the first 50 tags. - const ids = [...this._waiting_tags.keys()].slice(0, 50); - - let nodes - - try { - const data = await this.queryApollo( - await import(/* webpackChunkName: 'queries' */ './data/tags-fetch.gql'), - { - ids - } - ); - - nodes = get('data.contentTags', data); - - } catch(err) { - for(const id of ids) { - const promises = this._waiting_tags.get(id); - this._waiting_tags.delete(id); - - for(const pair of promises) - pair[1](err); - } - - return; - } - - const id_set = new Set(ids); - - if ( Array.isArray(nodes) ) - for(const node of nodes) { - const tag = this.memorizeTag(node, false), - promises = this._waiting_tags.get(tag.id); - - this._waiting_tags.delete(tag.id); - id_set.delete(tag.id); - - if ( promises ) - for(const pair of promises) - pair[0](tag); - } - - for(const id of id_set) { - const promises = this._waiting_tags.get(id); - this._waiting_tags.delete(id); - - for(const pair of promises) - pair[0](null); - } - - this._loading_tags = false; - - if ( this._waiting_tags.size ) - this._loadTags(); - } - - /** - * Queries Apollo for tag information - * @function getTag - * @memberof TwitchData - * - * @param {int|string} id - the tag id - * @param {bool} [want_description=false] - whether the description is also required - * @returns {Promise} tag information - * - * @example - * - * this.twitch_data.getTag(50).then(function(returnObj){console.log(returnObj);}); - */ - getTag(id, want_description = false) { - // Make sure we weren't accidentally handed a tag object. - if ( id && id.id ) - id = id.id; - - if ( this.tag_cache.has(id) ) { - const out = this.tag_cache.get(id); - if ( out && (out.description || ! want_description) ) - return Promise.resolve(out); - } - - return new Promise((s, f) => { - if ( this._waiting_tags.has(id) ) - this._waiting_tags.get(id).push([s, f]); - else { - this._waiting_tags.set(id, [[s, f]]); - if ( ! this._loading_tags ) - this._loadTags(); - } - }); - } - - /** - * Queries the tag cache for tag information, queries Apollo on cache miss - * @function getTagImmediate - * @memberof TwitchData - * - * @param {int|string} id - the tag id - * @param {getTagImmediateCallback} callback - callback function for use when requested tag information is not cached - * @param {bool} [want_description=false] - whether the tag description is required - * @returns {Object|null} tag information object, or on null, expect callback - * - * @example - * - * console.log(this.twitch_data.getTagImmediate(50)); - */ - getTagImmediate(id, callback, want_description = false) { - // Make sure we weren't accidentally handed a tag object. - if ( id && id.id ) - id = id.id; - - let out = null; - if ( this.tag_cache.has(id) ) - out = this.tag_cache.get(id); - - if ( (want_description && (! out || ! out.description)) || (! out && callback) ) { - const promise = this.getTag(id, want_description); - if ( callback ) - promise.then(tag => callback(id, tag)).catch(err => callback(id, null, err)); - } - - return out; - } - - /** - * Callback function used when getTagImmediate experiences a cache miss - * @callback getTagImmediateCallback - * @param {int} tag_id - The tag ID number - * @param {Object} tag_object - the object containing tag data - * @param {Object} [error_object] - returned error information on tag data fetch failure - */ - - /** - * Get top [n] tags - * @function getTopTags - * @memberof TwitchData - * @async - * - * @param {int|string} limit=50 - the number of tags to return (can be an integer string) - * @returns {string[]} an array containing the top tags up to the limit requested - * - * @example - * - * console.log(this.twitch_data.getTopTags(20)); - */ - async getTopTags(limit = 50) { - const data = await this.queryApollo( - await import(/* webpackChunkName: 'queries' */ './data/tags-top.gql'), - {limit} - ); - - const nodes = get('data.topTags', data); - if ( ! Array.isArray(nodes) ) - return []; - - const out = [], seen = new Set; - for(const node of nodes) { - if ( ! node || seen.has(node.id) ) - continue; - - seen.add(node.id); - out.push(this.memorizeTag(node)); - } - - return out; - } - - /** - * Queries tag languages - * @function getLanguagesFromTags - * @memberof TwitchData - * - * @param {int[]} tags - an array of tag IDs - * @returns {string[]} tag information - * - * @example - * - * console.log(this.twitch_data.getLanguagesFromTags([50, 53, 58, 84])); - */ - getLanguagesFromTags(tags, callback) { // TODO: actually use the callback - const out = [], - fn = callback ? debounce(() => { - this.getLanguagesFromTags(tags, callback); - }, 16) : null - - if ( Array.isArray(tags) ) - for(const tag_id of tags) { - const tag = this.getTagImmediate(tag_id, fn); - if ( tag && tag.is_language ) { - const match = LANGUAGE_MATCHER.exec(tag.name); - if ( match ) - out.push(match[1]); - } - } - - return out; - } - /** * Search tags * @function getMatchingTags @@ -1042,34 +801,28 @@ export default class TwitchData extends Module { * @async * * @param {string} query - the search string - * @param {string} [locale] - UNUSED. the locale to return tags from - * @param {string} [category=null] - the category to return tags from * @returns {string[]} an array containing tags that match the query string * * @example * * console.log(await this.twitch_data.getMatchingTags("Rainbo")); */ - async getMatchingTags(query, locale, category = null) { - /*if ( ! locale ) - locale = this.locale;*/ - + async getMatchingTags(query) { const data = await this.queryApollo({ - query: await import(/* webpackChunkName: 'queries' */ './data/search-tags.gql'), + query: await import(/* webpackChunkName: 'queries' */ './data/tag-search.gql'), variables: { query, - categoryID: category || null, - limit: 100 + first: 100 } }); - const nodes = data?.data?.searchLiveTags; - if ( ! Array.isArray(nodes) || ! nodes.length ) + const edges = data?.data?.searchFreeformTags?.edges; + if ( ! Array.isArray(edges) || ! edges.length ) return []; const out = []; - for(const node of nodes) { - const tag = this.memorizeTag(node); + for(const edge of edges) { + const tag = edge?.node?.tagName; if ( tag ) out.push(tag); } diff --git a/src/utilities/vue.js b/src/utilities/vue.js index 8f78fed2..c168139d 100644 --- a/src/utilities/vue.js +++ b/src/utilities/vue.js @@ -176,7 +176,7 @@ export class Vue extends Module { }) ); } - }) + }); vue.mixin({ methods: { diff --git a/styles/widgets.scss b/styles/widgets.scss index 91490cea..0686bb8e 100644 --- a/styles/widgets.scss +++ b/styles/widgets.scss @@ -492,6 +492,10 @@ textarea.ffz-input { } .ffz--example-report { + &.ffz--tall div { + max-height: 60rem; + } + div { max-height: 30rem; overflow-y: auto; diff --git a/webpack.config.js b/webpack.config.js index 6cb312a6..e308a09e 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -181,6 +181,21 @@ const config = { : '[name].[contenthash:8].json' } }, + { + // This stupid rule goes out to Mozilla, who consistantly + // manage to have this one file not included in the bundle + // the same way as every other build on every other machine + // out of like twelve I've tested. So fine. We'll do it + // your way. Whatever. I don't care. + test: /entities.json$/, + include: /node_modules/, + type: 'asset/resource', + generator: { + filename: (FOR_EXTENSION || DEV_BUILD) + ? '[name].json' + : '[name].[contenthash:8].json' + } + }, { test: /\.(?:otf|eot|ttf|woff|woff2)$/, use: [{