From 370a579635016cbdad5c8f9e6f5dc00945b281bc Mon Sep 17 00:00:00 2001 From: SirStendec Date: Sat, 5 Mar 2022 15:15:27 -0500 Subject: [PATCH 01/63] 4.32.2 * Added: Setting to limit the size of native Twitch emotes. Enabled by default. --- package.json | 2 +- src/modules/chat/emotes.js | 10 ++++++++++ src/modules/chat/tokenizers.jsx | 4 ++-- src/sites/twitch-twilight/modules/chat/index.js | 16 ++++++++++++++-- 4 files changed, 27 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 781801aa..decb31e0 100755 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "frankerfacez", "author": "Dan Salvato LLC", - "version": "4.32.1", + "version": "4.32.2", "description": "FrankerFaceZ is a Twitch enhancement suite.", "private": true, "license": "Apache-2.0", diff --git a/src/modules/chat/emotes.js b/src/modules/chat/emotes.js index dfc0d9ec..2f8450b8 100644 --- a/src/modules/chat/emotes.js +++ b/src/modules/chat/emotes.js @@ -122,6 +122,16 @@ export default class Emotes extends Module { } }); + this.settings.add('chat.emotes.limit-size', { + default: true, + ui: { + path: 'Chat > Appearance >> Emotes', + title: 'Limit Native Emote Size', + description: 'Sometimes, really obnoxiously large emotes slip through the cracks and wind up on Twitch. This limits the size of Twitch emotes to mitigate the issue.', + component: 'setting-check-box' + } + }); + this.settings.add('chat.fix-bad-emotes', { default: true, ui: { diff --git a/src/modules/chat/tokenizers.jsx b/src/modules/chat/tokenizers.jsx index 8e43cdc2..7e38f800 100644 --- a/src/modules/chat/tokenizers.jsx +++ b/src/modules/chat/tokenizers.jsx @@ -1135,7 +1135,7 @@ const render_emote = (token, createElement, wrapped) => { const mods = token.modifiers || [], ml = mods.length, emote = createElement('img', { - class: `${EMOTE_CLASS} ffz-tooltip${hoverSrc ? ' ffz-hover-emote' : ''}${token.provider === 'ffz' ? ' ffz-emote' : token.provider === 'emoji' ? ' ffz-emoji' : ''}`, + class: `${EMOTE_CLASS} ffz-tooltip${hoverSrc ? ' ffz-hover-emote' : ''}${token.provider === 'twitch' ? ' twitch-emote' : token.provider === 'ffz' ? ' ffz-emote' : token.provider === 'emoji' ? ' ffz-emoji' : ''}`, attrs: { src, srcSet, @@ -1214,7 +1214,7 @@ export const AddonEmotes = { const mods = token.modifiers || [], ml = mods.length, emote = ( - this.css_tweaks.toggle('big-emoji', val > 1)); + this.chat.context.getChanges('chat.emotes.2x', val => { + this.css_tweaks.toggle('big-emoji', val > 1); + this.toggleEmoteJail(); + }); + + this.chat.context.getChanges('chat.emotes.limit-size', () => + this.toggleEmoteJail()); this.chat.context.getChanges('chat.input.show-mod-view', val => this.css_tweaks.toggleHide('mod-view', ! val)); @@ -1330,6 +1335,13 @@ export default class ChatHook extends Module { }); } + toggleEmoteJail() { + const bigger = this.chat.context.get('chat.emotes.2x'), + enabled = this.chat.context.get('chat.emotes.limit-size'); + + this.css_tweaks.toggle('big-emote-jail', enabled && ! bigger); + this.css_tweaks.toggle('bigger-emote-jail', enabled && bigger); + } cleanHighlights() { const types = { From 155938f584d16efe39f5c348ce96389fbf02658a Mon Sep 17 00:00:00 2001 From: SirStendec Date: Wed, 23 Mar 2022 14:10:25 -0400 Subject: [PATCH 02/63] 4.32.3 * Fixed: The FFZ Control Center button being positioned incorrectly on streamer dashboard pages. * Fixed: Profile rules for the current category/title not working on certain pages. * Fixed: Swap Sidebars causing rendering issues when in theater mode, with the chat below the player, when not using FrankerFaceZ's Portrait Mode option. * Fixed: The placeholder text being positioned wrong when using Twitch's WYSIWYG chat input. * Fixed: The entire extension loading when viewing an embedded clip, causing undue load. * Changed: Add a link to YouTube's Privacy Policy to the legal page. --- package.json | 2 +- src/entry.js | 12 ++++---- src/experiments.js | 21 +++++++++----- src/modules/main_menu/legal.md | 2 +- src/sites/twitch-twilight/modules/channel.jsx | 28 +++++++++++++++++-- .../modules/css_tweaks/styles/big-emoji.scss | 6 ++++ .../css_tweaks/styles/big-emote-jail.scss | 4 +++ .../css_tweaks/styles/bigger-emote-jail.scss | 4 +++ .../modules/css_tweaks/styles/chat-fix.scss | 6 ++-- .../modules/css_tweaks/styles/chat-width.scss | 6 ++-- .../css_tweaks/styles/hide-chat-identity.scss | 1 + .../css_tweaks/styles/swap-sidebars.scss | 2 +- .../twitch-twilight/modules/menu_button.jsx | 2 +- .../twitch-twilight/modules/mod-view.jsx | 2 +- .../styles/color_normalizer.scss | 1 + 15 files changed, 74 insertions(+), 25 deletions(-) create mode 100644 src/sites/twitch-twilight/modules/css_tweaks/styles/big-emoji.scss create mode 100644 src/sites/twitch-twilight/modules/css_tweaks/styles/big-emote-jail.scss create mode 100644 src/sites/twitch-twilight/modules/css_tweaks/styles/bigger-emote-jail.scss diff --git a/package.json b/package.json index decb31e0..78eace9a 100755 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "frankerfacez", "author": "Dan Salvato LLC", - "version": "4.32.2", + "version": "4.32.3", "description": "FrankerFaceZ is a Twitch enhancement suite.", "private": true, "license": "Apache-2.0", diff --git a/src/entry.js b/src/entry.js index a2ffbb43..ca7b8c21 100644 --- a/src/entry.js +++ b/src/entry.js @@ -7,14 +7,16 @@ const DEBUG = localStorage.ffzDebugMode == 'true' && document.body.classList.contains('ffz-dev'), HOST = location.hostname, - FLAVOR = + SERVER = DEBUG ? '//localhost:8000' : '//cdn.frankerfacez.com', + script = document.createElement('script'); + + let FLAVOR = HOST.includes('player') ? 'player' : HOST.includes('clips') ? 'clips' : - (location.pathname === '/p/ffz_bridge/' ? 'bridge' : 'avalon'), - SERVER = DEBUG ? '//localhost:8000' : '//cdn.frankerfacez.com', - //CLIPS = /clips\.twitch\.tv/.test(location.hostname) ? 'clips/' : '', + (location.pathname === '/p/ffz_bridge/' ? 'bridge' : 'avalon'); - script = document.createElement('script'); + if (FLAVOR === 'clips' && location.pathname === '/embed') + FLAVOR = 'player'; script.id = 'ffz-script'; script.async = true; diff --git a/src/experiments.js b/src/experiments.js index fcd3b5c3..9086db47 100644 --- a/src/experiments.js +++ b/src/experiments.js @@ -301,7 +301,12 @@ export default class ExperimentManager extends Module { setTwitchOverride(key, value = null) { const overrides = Cookie.getJSON(OVERRIDE_COOKIE) || {}; - overrides[key] = value; + const experiments = overrides.experiments = overrides.experiments || {}; + const disabled = overrides.disabled = overrides.disabled || []; + experiments[key] = value; + const idx = disabled.indexOf(key); + if (idx != -1) + disabled.remove(idx); Cookie.set(OVERRIDE_COOKIE, overrides, COOKIE_OPTIONS); const core = this.resolve('site')?.getCore?.(); @@ -312,12 +317,13 @@ export default class ExperimentManager extends Module { } deleteTwitchOverride(key) { - const overrides = Cookie.getJSON(OVERRIDE_COOKIE); - if ( ! overrides || ! has(overrides, key) ) + const overrides = Cookie.getJSON(OVERRIDE_COOKIE), + experiments = overrides?.experiments; + if ( ! experiments || ! has(experiments, key) ) return; - const old_val = overrides[key]; - delete overrides[key]; + const old_val = experiments[key]; + delete experiments[key]; Cookie.set(OVERRIDE_COOKIE, overrides, COOKIE_OPTIONS); const core = this.resolve('site')?.getCore?.(); @@ -328,8 +334,9 @@ export default class ExperimentManager extends Module { } hasTwitchOverride(key) { // eslint-disable-line class-methods-use-this - const overrides = Cookie.getJSON(OVERRIDE_COOKIE); - return overrides && has(overrides, key); + const overrides = Cookie.getJSON(OVERRIDE_COOKIE), + experiments = overrides?.experiments; + return experiments && has(experiments, key); } getTwitchAssignment(key, channel = null) { diff --git a/src/modules/main_menu/legal.md b/src/modules/main_menu/legal.md index e5d628f0..c0faa9af 100644 --- a/src/modules/main_menu/legal.md +++ b/src/modules/main_menu/legal.md @@ -37,7 +37,7 @@ We use the APIs of the following services for scraping link information: * Twitch ([Terms of Service](https://www.twitch.tv/p/legal/terms-of-service/), [Developer Agreement](https://www.twitch.tv/p/legal/developer-agreement/)) * Twitter ([Terms of Service](https://twitter.com/en/tos), [Developer Terms](https://developer.twitter.com/en/more/developer-terms.html)) * xkcd -* YouTube ([Terms of Service](https://www.youtube.com/t/terms), [Developer Terms of Service](https://developers.google.com/youtube/terms/developer-policies)) +* YouTube ([Terms of Service](https://www.youtube.com/t/terms), [Developer Terms of Service](https://developers.google.com/youtube/terms/developer-policies), [Privacy Policy](https://policies.google.com/privacy)) In addition to scraping via APIs, our link information reads standard metadata tags from HTML responses to support a wide array of other websites. diff --git a/src/sites/twitch-twilight/modules/channel.jsx b/src/sites/twitch-twilight/modules/channel.jsx index 57a5434a..6c3594da 100644 --- a/src/sites/twitch-twilight/modules/channel.jsx +++ b/src/sites/twitch-twilight/modules/channel.jsx @@ -389,6 +389,30 @@ export default class Channel extends Module { el._ffz_links.innerHTML = ''; } + // This is awful, but it works. + let channel = null; + this.fine.searchNode(react, node => { + let state = node?.memoizedState, i = 0; + while(state != null && channel == null && i < 50 ) { + state = state?.next; + channel = state?.memoizedState?.current?.previous?.result?.data?.user; + if (!channel?.lastBroadcast?.game) + channel = null; + i++; + } + return channel != null; + }); + + const game = channel?.lastBroadcast?.game, + title = channel?.lastBroadcast?.title; + + if (game?.id !== el._ffz_game_cache || title !== el._ffz_title_cache) + this.settings.updateContext({ + category: game?.displayName, + categoryID: game?.id, + title + }); + // TODO: See if we can read this data directly from Apollo's cache. // Also, test how it works with videos and clips. /*const raw_game = el.querySelector('a[data-a-target="stream-game-link"]')?.textContent; @@ -405,7 +429,7 @@ export default class Channel extends Module { }).catch(() => { el._ffz_game_cache_updating = false; }); - }*/ + } const other_props = react.child.child?.child?.child?.child?.child?.child?.child?.child?.child?.memoizedProps, title = other_props?.title; @@ -415,7 +439,7 @@ export default class Channel extends Module { this.settings.updateContext({ title }); - } + }*/ if ( ! this.settings.get('channel.hosting.enable') && props.hostLogin ) this.setHost(props.channelID, props.channelLogin, null, null); diff --git a/src/sites/twitch-twilight/modules/css_tweaks/styles/big-emoji.scss b/src/sites/twitch-twilight/modules/css_tweaks/styles/big-emoji.scss new file mode 100644 index 00000000..5dc18a0e --- /dev/null +++ b/src/sites/twitch-twilight/modules/css_tweaks/styles/big-emoji.scss @@ -0,0 +1,6 @@ +.ffz-emoji { + width: 3.6rem !important; + height: 3.6rem !important; + width: calc(var(--ffz-chat-font-size) * 3) !important; + height: calc(var(--ffz-chat-font-size) * 3) !important; +} diff --git a/src/sites/twitch-twilight/modules/css_tweaks/styles/big-emote-jail.scss b/src/sites/twitch-twilight/modules/css_tweaks/styles/big-emote-jail.scss new file mode 100644 index 00000000..d339f88e --- /dev/null +++ b/src/sites/twitch-twilight/modules/css_tweaks/styles/big-emote-jail.scss @@ -0,0 +1,4 @@ +.twitch-emote { + max-height: 32px; + max-width: 64px; +} \ No newline at end of file diff --git a/src/sites/twitch-twilight/modules/css_tweaks/styles/bigger-emote-jail.scss b/src/sites/twitch-twilight/modules/css_tweaks/styles/bigger-emote-jail.scss new file mode 100644 index 00000000..6cfe11ee --- /dev/null +++ b/src/sites/twitch-twilight/modules/css_tweaks/styles/bigger-emote-jail.scss @@ -0,0 +1,4 @@ +.twitch-emote { + max-height: 64px; + max-width: 128px; +} \ No newline at end of file diff --git a/src/sites/twitch-twilight/modules/css_tweaks/styles/chat-fix.scss b/src/sites/twitch-twilight/modules/css_tweaks/styles/chat-fix.scss index fd96aa3a..810fbd2f 100644 --- a/src/sites/twitch-twilight/modules/css_tweaks/styles/chat-fix.scss +++ b/src/sites/twitch-twilight/modules/css_tweaks/styles/chat-fix.scss @@ -1,13 +1,13 @@ body .video-watch-page__right-column, body .clips-watch-page__right-column, body .channel-page__right-column, -body .right-column:not(.right-column--collapsed), +body .right-column:not(.right-column--collapsed):not(.right-column--below), body .channel-videos__right-column, body .channel-clips__sidebar, body .channel-events__sidebar, body .channel-follow-listing__right-column, -body .channel-follow-listing__right-column, -body .channel-root__right-column { +body .channel-follow-listing__right-column/*, +body .channel-root__right-column*/ { width: 34rem; } diff --git a/src/sites/twitch-twilight/modules/css_tweaks/styles/chat-width.scss b/src/sites/twitch-twilight/modules/css_tweaks/styles/chat-width.scss index c9400152..b523c119 100644 --- a/src/sites/twitch-twilight/modules/css_tweaks/styles/chat-width.scss +++ b/src/sites/twitch-twilight/modules/css_tweaks/styles/chat-width.scss @@ -22,13 +22,13 @@ body .channel-page__video-player--theatre-mode { body .video-watch-page__right-column, body .clips-watch-page__right-column, body .channel-page__right-column, -body .right-column:not(.right-column--collapsed), +body .right-column:not(.right-column--collapsed):not(.right-column--below), body .channel-videos__right-column, body .channel-clips__sidebar, body .channel-events__sidebar, body .channel-follow-listing__right-column, -body .channel-follow-listing__right-column, -body .channel-root__right-column { +body .channel-follow-listing__right-column +/*body .channel-root__right-column*/ { width: var(--ffz-chat-width) !important; } diff --git a/src/sites/twitch-twilight/modules/css_tweaks/styles/hide-chat-identity.scss b/src/sites/twitch-twilight/modules/css_tweaks/styles/hide-chat-identity.scss index d48a5e64..7ec8822e 100644 --- a/src/sites/twitch-twilight/modules/css_tweaks/styles/hide-chat-identity.scss +++ b/src/sites/twitch-twilight/modules/css_tweaks/styles/hide-chat-identity.scss @@ -10,6 +10,7 @@ .chat-input__textarea { .chat-wysiwyg-input__editor, + .chat-wysiwyg-input__placeholder, .tw-textarea { padding-left: 1rem !important; } diff --git a/src/sites/twitch-twilight/modules/css_tweaks/styles/swap-sidebars.scss b/src/sites/twitch-twilight/modules/css_tweaks/styles/swap-sidebars.scss index 8d22f6eb..5797d34f 100644 --- a/src/sites/twitch-twilight/modules/css_tweaks/styles/swap-sidebars.scss +++ b/src/sites/twitch-twilight/modules/css_tweaks/styles/swap-sidebars.scss @@ -5,7 +5,7 @@ order: 1; z-index: 2; - #root &:not(.right-column--collapsed) { + #root &:not(.right-column--collapsed):not(.right-column--below) { width: var(--ffz-chat-width); } } diff --git a/src/sites/twitch-twilight/modules/menu_button.jsx b/src/sites/twitch-twilight/modules/menu_button.jsx index ee135ae2..5725a2d6 100644 --- a/src/sites/twitch-twilight/modules/menu_button.jsx +++ b/src/sites/twitch-twilight/modules/menu_button.jsx @@ -56,7 +56,7 @@ export default class MenuButton extends SiteModule { );*/ this.SunlightNav = this.elemental.define( - 'sunlight-nav', '.sunlight-top-nav > div > div > div:nth-last-child(2) > div', + 'sunlight-nav', '.sunlight-top-nav > div > div > div:last-child > div', Twilight.SUNLIGHT_ROUTES, {attributes: true}, 1 ); diff --git a/src/sites/twitch-twilight/modules/mod-view.jsx b/src/sites/twitch-twilight/modules/mod-view.jsx index b47611af..bf38edb4 100644 --- a/src/sites/twitch-twilight/modules/mod-view.jsx +++ b/src/sites/twitch-twilight/modules/mod-view.jsx @@ -216,7 +216,7 @@ export default class ModView extends Module { 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; + channel = state?.memoizedState?.current?.previous?.result?.data?.channel; i++; } diff --git a/src/sites/twitch-twilight/styles/color_normalizer.scss b/src/sites/twitch-twilight/styles/color_normalizer.scss index 79dc8fed..709ca73f 100644 --- a/src/sites/twitch-twilight/styles/color_normalizer.scss +++ b/src/sites/twitch-twilight/styles/color_normalizer.scss @@ -19,6 +19,7 @@ .chat-room, .video-chat, .qa-vod-chat, + .extensions-popover-view-layout, .video-card { background-color: var(--color-background-base) !important; } From 0fcd7d5af60d9fcb56ef0cab00868018d74ac4c8 Mon Sep 17 00:00:00 2001 From: SirStendec Date: Wed, 23 Mar 2022 15:08:22 -0400 Subject: [PATCH 03/63] 4.32.4 * Fixed: Chat Width not applying correctly in some configurations. --- package.json | 2 +- .../twitch-twilight/modules/css_tweaks/styles/chat-width.scss | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 78eace9a..e7c69cb3 100755 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "frankerfacez", "author": "Dan Salvato LLC", - "version": "4.32.3", + "version": "4.32.4", "description": "FrankerFaceZ is a Twitch enhancement suite.", "private": true, "license": "Apache-2.0", diff --git a/src/sites/twitch-twilight/modules/css_tweaks/styles/chat-width.scss b/src/sites/twitch-twilight/modules/css_tweaks/styles/chat-width.scss index b523c119..123efeef 100644 --- a/src/sites/twitch-twilight/modules/css_tweaks/styles/chat-width.scss +++ b/src/sites/twitch-twilight/modules/css_tweaks/styles/chat-width.scss @@ -27,7 +27,8 @@ body .channel-videos__right-column, body .channel-clips__sidebar, body .channel-events__sidebar, body .channel-follow-listing__right-column, -body .channel-follow-listing__right-column +body .channel-follow-listing__right-column, +body .right-column:not(.right-column--collapsed):not(.right-column--below) .channel-root__right-column /*body .channel-root__right-column*/ { width: var(--ffz-chat-width) !important; } From 084a3ee5e06cd8d24b0b635853c7e24379889203 Mon Sep 17 00:00:00 2001 From: SirStendec Date: Thu, 31 Mar 2022 17:15:43 -0400 Subject: [PATCH 04/63] 4.32.5 * Fixed: The new announcement feature not rendering correctly in chat. * Fixed: Metadata failing to render correctly with certain Twitch experiments active. * Fixed: Some promoted streams appearing when users have chosen to hide promoted streams. --- package.json | 2 +- src/sites/twitch-twilight/modules/channel.jsx | 4 ++-- .../twitch-twilight/modules/chat/index.js | 19 ++++++++++++++----- .../modules/directory/index.jsx | 2 +- 4 files changed, 18 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index e7c69cb3..9f16cc9e 100755 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "frankerfacez", "author": "Dan Salvato LLC", - "version": "4.32.4", + "version": "4.32.5", "description": "FrankerFaceZ is a Twitch enhancement suite.", "private": true, "license": "Apache-2.0", diff --git a/src/sites/twitch-twilight/modules/channel.jsx b/src/sites/twitch-twilight/modules/channel.jsx index 6c3594da..3751ae8b 100644 --- a/src/sites/twitch-twilight/modules/channel.jsx +++ b/src/sites/twitch-twilight/modules/channel.jsx @@ -314,12 +314,12 @@ export default class Channel extends Module { } if ( ! el._ffz_cont ) { - const report = el.querySelector('.report-button,button[data-test-selector="video-options-button"],button[data-test-selector="clip-options-button"]'); + const report = el.querySelector('.report-button,button[data-test-selector="video-options-button"],button[data-test-selector="clip-options-button"],button[data-a-target="report-button-more-button"]'); let cont = report && (report.closest('.tw-flex-wrap.tw-justify-content-end') || report.closest('.tw-justify-content-end')); if ( ! cont && report ) { cont = report.parentElement?.parentElement; - if ( cont && cont.parentElement?.childElementCount === 2 ) + if ( cont && cont.parentElement?.childElementCount === 2 && report.dataset.aTarget !== 'report-button-more-button' ) cont = cont.parentElement.firstElementChild; } diff --git a/src/sites/twitch-twilight/modules/chat/index.js b/src/sites/twitch-twilight/modules/chat/index.js index 6869381c..2a3a3b12 100644 --- a/src/sites/twitch-twilight/modules/chat/index.js +++ b/src/sites/twitch-twilight/modules/chat/index.js @@ -2120,6 +2120,12 @@ export default class ChatHook extends Module { return old_action.call(i, e); }*/ + const old_announce = this.onAnnouncementEvent; + this.onAnnouncementEvent = function(e) { + console.log('announcement', e); + return old_announce.call(this, e); + } + const old_sub = this.onSubscriptionEvent; this.onSubscriptionEvent = function(e) { @@ -2456,8 +2462,9 @@ export default class ChatHook extends Module { // For certain message types, the message is contained within // a message sub-object. - if ( message.type === t.chat_types.ChannelPointsReward || message.type === t.chat_types.CommunityIntroduction || (message.message?.user & message.message?.badgeDynamicData) ) + if ( message.type === t.chat_types.ChannelPointsReward || message.type === t.chat_types.CommunityIntroduction || (message.message?.user & message.message?.badgeDynamicData) ) { message = message.message; + } if ( original.channel ) { let chan = message.channel = original.channel.toLowerCase(); @@ -2485,10 +2492,12 @@ export default class ChatHook extends Module { this.props.isCurrentUserModerator = true; } - if ( typeof original.action === 'string' ) - message.message = original.action; - else - message.message = original.message.body; + if (! message.message || typeof message.message === 'string') { + if ( typeof original.action === 'string' ) + message.message = original.action; + else + message.message = original.message.body; + } } this.addMessage(original_msg); diff --git a/src/sites/twitch-twilight/modules/directory/index.jsx b/src/sites/twitch-twilight/modules/directory/index.jsx index 0347fb30..42c389b8 100644 --- a/src/sites/twitch-twilight/modules/directory/index.jsx +++ b/src/sites/twitch-twilight/modules/directory/index.jsx @@ -341,7 +341,7 @@ export default class Directory extends SiteModule { const should_hide = bad_tag || (props.streamType === 'rerun' && this.settings.get('directory.hide-vodcasts')) || (props.context != null && props.context !== CARD_CONTEXTS.SingleGameList && this.settings.provider.get('directory.game.blocked-games', []).includes(game)) || - ((props.sourceType === 'PROMOTION' || props.sourceType === 'SPONSORED') && this.settings.get('directory.hide-promoted')); + ((props.isPromotion || props.sourceType === 'COMMUNITY_BOOST' || props.sourceType === 'PROMOTION' || props.sourceType === 'SPONSORED') && this.settings.get('directory.hide-promoted')); let hide_container = el.closest('.tw-tower > div'); if ( ! hide_container ) From 2af7d5618b865ecdb6eff29443b9c58f029d7c42 Mon Sep 17 00:00:00 2001 From: SirStendec Date: Tue, 19 Apr 2022 15:34:20 -0400 Subject: [PATCH 05/63] 4.33.0 * Added: Formatters for chat action variables. (Closes #1199) * Changed: By default, open the user card to a badge when clicking a badge in chat. (Closes #1195) * Fixed: The settings bridge functioning incorrectly for users without a set storage provider, causing pages that rely on the settings bridge including the Stream Dashboard to never correctly load FFZ. --- package.json | 2 +- src/i18n.js | 43 +++++++++-- .../chat/actions/components/edit-chat.vue | 6 +- src/modules/chat/actions/index.jsx | 74 ++++++++++++++++++- src/modules/chat/badges.jsx | 39 ++++++++-- .../main_menu/components/action-editor.vue | 15 ++++ src/settings/providers.js | 8 +- src/utilities/events.js | 20 ++++- 8 files changed, 186 insertions(+), 21 deletions(-) diff --git a/package.json b/package.json index 9f16cc9e..44bd8259 100755 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "frankerfacez", "author": "Dan Salvato LLC", - "version": "4.32.5", + "version": "4.33.0", "description": "FrankerFaceZ is a Twitch enhancement suite.", "private": true, "license": "Apache-2.0", diff --git a/src/i18n.js b/src/i18n.js index a4a4086c..1041ee86 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -191,7 +191,7 @@ export class TranslationManager extends Module { }, data: () => { const out = [], now = new Date; - for (const [key,fmt] of Object.entries(this._.formats.date)) { + for (const [key, fmt] of Object.entries(this._.formats.date)) { out.push({ value: key, title: `${this.formatDate(now, key)} (${key})` }) @@ -815,6 +815,28 @@ export class TranslationManager extends Module { const DOLLAR_REGEX = /\$/g; const REPLACE = String.prototype.replace; +const FORMAT_REGEX = /^\s*([^(]+?)\s*(?:\(\s*([^)]+?)\s*\))?\s*$/; + +export function parseFormatters(fmt) { + if (!fmt || ! fmt.length) + return; + + const result = []; + + for(const token of fmt.split(/\|/g)) { + const match = FORMAT_REGEX.exec(token); + if (!match) + continue; + + result.push({ + fmt: match[1], + extra: match[2] + }); + } + + return result; +} + export function transformPhrase(phrase, substitutions, locale, token_regex, formatters) { const is_array = Array.isArray(phrase); if ( substitutions == null ) @@ -828,14 +850,23 @@ export function transformPhrase(phrase, substitutions, locale, token_regex, form if ( typeof result === 'string' ) result = REPLACE.call(result, token_regex, (expr, arg, fmt) => { - let val = get(arg, options); + let val = get(arg.trim(), options); if ( val == null ) return ''; - const formatter = formatters[fmt]; - if ( typeof formatter === 'function' ) - val = formatter(val, locale, options); - else if ( typeof val === 'string' ) + const fmts = parseFormatters(fmt); + let formatted = false; + if (fmts) { + for(const format of fmts) { + const formatter = formatters[format.fmt]; + if (typeof formatter === 'function') { + val = formatter(val, locale, options, format.extra); + formatted = true; + } + } + } + + if (! formatted && typeof val === 'string' ) val = REPLACE.call(val, DOLLAR_REGEX, '$$'); return val; diff --git a/src/modules/chat/actions/components/edit-chat.vue b/src/modules/chat/actions/components/edit-chat.vue index b14f7c64..dae26f97 100644 --- a/src/modules/chat/actions/components/edit-chat.vue +++ b/src/modules/chat/actions/components/edit-chat.vue @@ -17,6 +17,10 @@ {{ t('setting.actions.variables', 'Available Variables: {vars}', {vars}) }} +
+ {{ t('setting.actions.formats', 'Available Formatters: {fmts}', {fmts}) }} +
+
1) { + const bit = bits[1].trim(); + if (! bit.length ) + end = -1; + else + try { + end = parseInt(bits[1], 10); + if (isNaN(end) || !isFinite(end)) + return val; + } catch(err) { + this.log.warn('Invalid value for word(end)', bits[1]); + return val; + } + } + + const words = val.split(/\s+/); + + if (start < 0) + start = words.length + start; + if (start < 0) + start = 0; + if (start >= words.length) + start = words.length - 1; + + if (end != null) { + if (end < 0) + end = words.length + end; + if (end < start) + end = start; + if (end > words.length) + end = words.length; + + return words.slice(start, end + 1).join(' '); + } + + return words[start]; + } + } ); } diff --git a/src/modules/chat/badges.jsx b/src/modules/chat/badges.jsx index a008a7f4..c4ab7f19 100644 --- a/src/modules/chat/badges.jsx +++ b/src/modules/chat/badges.jsx @@ -223,12 +223,24 @@ export default class Badges extends Module { }); this.settings.add('chat.badges.clickable', { - default: true, + default: 2, + process(ctx, val) { + if (val === true) + return 2; + else if (val === false) + return 0; + return val; + }, ui: { path: 'Chat > Badges >> Behavior', title: 'Allow clicking badges.', description: 'Certain badges, such as Prime Gaming, act as links when this is enabled.', - component: 'setting-check-box' + component: 'setting-select-box', + data: [ + {value: 0, title: 'Disabled'}, + {value: 1, title: 'Legacy (Open URLs)'}, + {value: 2, title: 'Open Badge Card'} + ] } }); @@ -534,7 +546,8 @@ export default class Badges extends Module { handleClick(event) { - if ( ! this.parent.context.get('chat.badges.clickable') ) + const mode = this.parent.context.get('chat.badges.clickable'); + if ( ! mode ) return; const target = event.target; @@ -544,6 +557,7 @@ export default class Badges extends Module { return; let url = null; + let click_badge = null; for(const d of ds.data) { const p = d.provider; @@ -553,14 +567,14 @@ export default class Badges extends Module { if ( ! bd ) continue; - if ( bd.click_url ) + if ( mode == 1 && bd.click_url ) url = bd.click_url; - else if ( global_badge.click_url ) + else if ( mode == 1 && global_badge.click_url ) url = global_badge.click_url; - else if ( (bd.click_action === 'sub' || global_badge.click_action === 'sub') && ds.room_login ) + else if ( mode == 1 && (bd.click_action === 'sub' || global_badge.click_action === 'sub') && ds.room_login ) url = `https://www.twitch.tv/subs/${ds.room_login}`; else - continue; + click_badge = bd; break; @@ -578,6 +592,17 @@ export default class Badges extends Module { } } + if (click_badge) { + const fine = this.resolve('site.fine'); + if (fine) { + const line = fine.searchParent(target, n => n.openBadgeDetails && n.props?.message); + if (line) { + line.openBadgeDetails(click_badge, event); + return; + } + } + } + if ( url ) { const link = createElement('a', { target: '_blank', diff --git a/src/modules/main_menu/components/action-editor.vue b/src/modules/main_menu/components/action-editor.vue index 0c611fbd..8f80ab24 100644 --- a/src/modules/main_menu/components/action-editor.vue +++ b/src/modules/main_menu/components/action-editor.vue @@ -379,6 +379,7 @@ :value="edit_data.options" :defaults="action_def.defaults" :vars="vars" + :fmts="fmts" @input="onChangeAction($event)" /> @@ -493,6 +494,20 @@ export default { return this.modifiers }, + fmts() { + const out = []; + + out.push('word(start)'); + out.push('word(start,end)'); + out.push('upper'); + out.push('lower'); + out.push('snakecase'); + out.push('slugify'); + out.push('slugify(separator)'); + + return out.join(', '); + }, + vars() { const out = [], ctx = this.context || []; diff --git a/src/settings/providers.js b/src/settings/providers.js index d52c93b8..758e3934 100644 --- a/src/settings/providers.js +++ b/src/settings/providers.js @@ -1027,9 +1027,9 @@ export class CrossOriginStorageBridge extends SettingsProvider { this._last_id = 0; const frame = this.frame = document.createElement('iframe'); - frame.src = this.manager.root.host === 'twitch' ? - '//www.twitch.tv/p/ffz_bridge/' : - '//www.youtube.com/__ffz_bridge/'; + frame.src = this.manager.root.host === 'youtube' ? + '//www.youtube.com/__ffz_bridge/' : + '//www.twitch.tv/p/ffz_bridge/'; frame.id = 'ffz-settings-bridge'; frame.style.width = 0; frame.style.height = 0; @@ -1041,7 +1041,7 @@ export class CrossOriginStorageBridge extends SettingsProvider { // Static Properties - static supported(manager) { return manager.root.host === 'twitch' ? NOT_WWW_TWITCH : NOT_WWW_YT; } + static supported() { return NOT_WWW_TWITCH && NOT_WWW_YT; } static hasContent(manager) { return CrossOriginStorageBridge.supported(manager); } static key = 'cosb'; diff --git a/src/utilities/events.js b/src/utilities/events.js index e7e00ef1..3ac3d5d6 100644 --- a/src/utilities/events.js +++ b/src/utilities/events.js @@ -12,9 +12,27 @@ const SNAKE_CAPS = /([a-z])([A-Z])/g, SNAKE_SPACE = /[ \t\W]/g, SNAKE_TRIM = /^_+|_+$/g; +String.prototype.toSlug = function(separator = '-') { + let result = this; + if (result.normalize) + result = result.normalize('NFD'); + + return result + .replace(/[\u0300-\u036f]/g, '') + .trim() + .toLowerCase() + .replace(/[^a-z0-9 ]/g, '') + .replace(/\s+/g, separator); +} String.prototype.toSnakeCase = function() { - return this + let result = this; + if (result.normalize) + result = result.normalize('NFD'); + + return result + .replace(/[\u0300-\u036f]/g, '') + .trim() .replace(SNAKE_CAPS, '$1_$2') .replace(SNAKE_SPACE, '_') .replace(SNAKE_TRIM, '') From 3ea07abb0e71d685cef4894ebd7631ac5c98b2d8 Mon Sep 17 00:00:00 2001 From: SirStendec Date: Mon, 25 Apr 2022 13:47:10 -0400 Subject: [PATCH 06/63] 4.33.1 * Fixed: Locales failing to load due to missing `day.js` support. * Fixed: Locales failing to load due to capitalization. * Changed: Use a slightly newer API for constructing an audio compressor object for better compatibility. --- package.json | 2 +- src/i18n.js | 24 ++++++++++++++++++++++-- src/sites/shared/player.jsx | 22 +++++++++++++++++++--- src/utilities/translation-core.js | 9 +++++---- 4 files changed, 47 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index 44bd8259..653568d8 100755 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "frankerfacez", "author": "Dan Salvato LLC", - "version": "4.33.0", + "version": "4.33.1", "description": "FrankerFaceZ is a Twitch enhancement suite.", "private": true, "license": "Apache-2.0", diff --git a/src/i18n.js b/src/i18n.js index 1041ee86..27114c4f 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -451,6 +451,10 @@ export class TranslationManager extends Module { } + get dayjsLocale() { + return this._?._dayjs_locale; + } + get locale() { return this._ && this._.locale; } @@ -661,6 +665,9 @@ export class TranslationManager extends Module { async loadLocale(locale, chunk = null) { + // Normalize the locale. + locale = locale.toLowerCase(); + if ( locale === 'en' ) return {}; @@ -710,12 +717,13 @@ export class TranslationManager extends Module { } async setLocale(new_locale) { + // Normalize the locale. + new_locale = new_locale.toLowerCase(); + const old_locale = this._.locale; if ( new_locale === old_locale ) return []; - await this.loadDayjsLocale(new_locale); - this._.locale = new_locale; this._.clear(); this.log.info(`Changed Locale: ${new_locale} -- Old: ${old_locale}`); @@ -726,15 +734,27 @@ export class TranslationManager extends Module { // All the built-in messages are English. We don't need special // logic to load the translations. this.emit(':loaded', []); + this._._dayjs_locale = 'en'; return []; } const data = this.localeData[new_locale]; const phrases = await this.loadLocale(data?.id || new_locale); + let djs; + try { + djs = data?.dayjs_override || new_locale; + await this.loadDayjsLocale(djs); + } catch (err) { + this.log.warn(`Unable to load DayJS locale for ${new_locale}`); + djs = 'en'; + } + if ( this._.locale !== new_locale ) throw new Error('locale has changed since we started loading'); + this._._dayjs_locale = djs; + const added = this._.extend(phrases); if ( added.length ) { this.log.info(`Loaded Locale: ${new_locale} -- Phrases: ${added.length}`); diff --git a/src/sites/shared/player.jsx b/src/sites/shared/player.jsx index 1e9aad38..d084d52d 100644 --- a/src/sites/shared/player.jsx +++ b/src/sites/shared/player.jsx @@ -1541,14 +1541,30 @@ export default class PlayerBase extends Module { src.connect(ctx.destination); - comp = video._ffz_compressor = ctx.createDynamicsCompressor(); + try { + comp = video._ffz_compressor = new DynamicsCompressorNode(ctx); + } catch (err) { + this.log.info('Unable to uew new DynamicsCompressorNode. Falling back to old method.'); + comp = video._ffz_compressor = ctx.createDynamicsCompressor(); + } if ( this.settings.get('player.gain.enable') ) { - const gain = video._ffz_gain = ctx.createGain(); + let gain; let value = video._ffz_gain_value; if ( value == null ) value = this.settings.get('player.gain.default'); - gain.gain.value = value; + + try { + gain = video._ffz_gain = new GainNode(ctx, { + gain: value + }); + + } catch(err) { + this.log.info('Unable to uew new GainNode. Falling back to old method.'); + gain = video._ffz_gain = ctx.createGain(); + gain.gain.value = value; + } + comp.connect(gain); } diff --git a/src/utilities/translation-core.js b/src/utilities/translation-core.js index ece59830..1e663c98 100644 --- a/src/utilities/translation-core.js +++ b/src/utilities/translation-core.js @@ -204,6 +204,7 @@ export default class TranslationCore { this.warn = options.warn; this._locale = options.locale || 'en'; + this._dayjs_locale = options.dayjsLocale || 'en'; this.defaultLocale = options.defaultLocale || this._locale; this.transformation = null; @@ -250,7 +251,7 @@ export default class TranslationCore { without_suffix = f === 'plain'; try { - return d.locale(this._locale).fromNow(without_suffix); + return d.locale(this._dayjs_locale).fromNow(without_suffix); } catch(err) { return d.fromNow(without_suffix); } @@ -286,7 +287,7 @@ export default class TranslationCore { if ( format && ! this.formats.date[format] ) { const d = dayjs(value); try { - return d.locale(this._locale).format(format); + return d.locale(this._dayjs_locale).format(format); } catch(err) { return d.format(format); } @@ -305,7 +306,7 @@ export default class TranslationCore { if ( format && ! this.formats.time[format] ) { const d = dayjs(value); try { - return d.locale(this._locale).format(format); + return d.locale(this._dayjs_locale).format(format); } catch(err) { return d.format(format); } @@ -324,7 +325,7 @@ export default class TranslationCore { if ( format && ! this.formats.datetime[format] ) { const d = dayjs(value); try { - return d.locale(this._locale).format(format); + return d.locale(this._dayjs_locale).format(format); } catch(err) { return d.format(format); } From 3291b95e553547a0a04b52475382fedebadf84c7 Mon Sep 17 00:00:00 2001 From: SirStendec Date: Mon, 25 Apr 2022 14:19:34 -0400 Subject: [PATCH 07/63] 4.33.2 * Changed: Update another method used in the audio compressor to use a newer API when available. --- package.json | 2 +- src/sites/shared/player.jsx | 27 ++++++++++++++++++++++----- 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 653568d8..95d82372 100755 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "frankerfacez", "author": "Dan Salvato LLC", - "version": "4.33.1", + "version": "4.33.2", "description": "FrankerFaceZ is a Twitch enhancement suite.", "private": true, "license": "Apache-2.0", diff --git a/src/sites/shared/player.jsx b/src/sites/shared/player.jsx index d084d52d..4ad2db2b 100644 --- a/src/sites/shared/player.jsx +++ b/src/sites/shared/player.jsx @@ -1419,12 +1419,21 @@ export default class PlayerBase extends Module { return; if ( want_gain && ! gain ) { - gain = video._ffz_gain = ctx.createGain(); let value = video._ffz_gain_value; if ( value == null ) value = this.settings.get('player.gain.default'); - gain.gain.value = value; + try { + gain = video._ffz_gain = new GainNode(ctx, { + gain: value + }); + + } catch(err) { + this.log.info('Unable to use new GainNode. Falling back to old method.'); + gain = video._ffz_gain = ctx.createGain(); + gain.gain.value = value; + } + comp.connect(gain); if ( compressed ) { @@ -1537,14 +1546,22 @@ export default class PlayerBase extends Module { } video._ffz_context = ctx; - const src = video._ffz_source = ctx.createMediaElementSource(video); + let src; + try { + src = video._ffz_source = new MediaElementAudioSourceNode(ctx, { + mediaElement: video + }); + } catch(err) { + this.log.info('Unable to use new MediaElementAudioSourceNode. Falling back to old method.'); + src = video._ffz_source = ctx.createMediaElementSource(video); + } src.connect(ctx.destination); try { comp = video._ffz_compressor = new DynamicsCompressorNode(ctx); } catch (err) { - this.log.info('Unable to uew new DynamicsCompressorNode. Falling back to old method.'); + this.log.info('Unable to use new DynamicsCompressorNode. Falling back to old method.'); comp = video._ffz_compressor = ctx.createDynamicsCompressor(); } @@ -1560,7 +1577,7 @@ export default class PlayerBase extends Module { }); } catch(err) { - this.log.info('Unable to uew new GainNode. Falling back to old method.'); + this.log.info('Unable to use new GainNode. Falling back to old method.'); gain = video._ffz_gain = ctx.createGain(); gain.gain.value = value; } From 4b5827f98beead3439530fdd7272648b7feed29c Mon Sep 17 00:00:00 2001 From: SirStendec Date: Mon, 25 Apr 2022 15:01:40 -0400 Subject: [PATCH 08/63] 4.33.3 * Fixed: Attempt to resume a suspended audio context before giving up. --- package.json | 2 +- src/sites/shared/player.jsx | 24 +++++++++++++++++++++--- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 95d82372..0a8a9889 100755 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "frankerfacez", "author": "Dan Salvato LLC", - "version": "4.33.2", + "version": "4.33.3", "description": "FrankerFaceZ is a Twitch enhancement suite.", "private": true, "license": "Apache-2.0", diff --git a/src/sites/shared/player.jsx b/src/sites/shared/player.jsx index 4ad2db2b..341d510e 100644 --- a/src/sites/shared/player.jsx +++ b/src/sites/shared/player.jsx @@ -1533,15 +1533,33 @@ export default class PlayerBase extends Module { return true; } - createCompressor(inst, video) { + createCompressor(inst, video, _cmp) { if ( ! this.canCompress(inst) ) return; let comp = video._ffz_compressor; if ( ! comp ) { - const ctx = new AudioContext(); + const ctx = _cmp || new AudioContext(); if ( ! IS_FIREFOX && ctx.state === 'suspended' ) { - this.log.info('Aborting due to browser auto-play policy.'); + let timer; + const evt = () => { + clearTimeout(timer); + ctx.removeEventListener('statechange', evt); + if (ctx.state === 'suspended') { + this.log.info('Aborting due to browser auto-play policy.'); + return; + } + + this.createCompressor(inst, video, comp); + } + + this.log.info('Attempting to resume suspended AudioContext.'); + timer = setTimeout(evt, 100); + try { + ctx.addEventListener('statechange', evt); + ctx.resume(); + } catch(err) { } + return; } From 0b43b2f5732f67f86c7d91fec9144942c7104ace Mon Sep 17 00:00:00 2001 From: SirStendec Date: Thu, 5 May 2022 13:41:49 -0400 Subject: [PATCH 09/63] 4.33.4 * Added: Setting to force the use of legacy audio API constructors for browsers with compatibility issues. * Changed: Add support for Twitch's inline chat highlights. --- package.json | 2 +- src/sites/shared/player.jsx | 23 +++++++++++++++++++ .../twitch-twilight/modules/chat/line.js | 1 + 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 0a8a9889..abd140e8 100755 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "frankerfacez", "author": "Dan Salvato LLC", - "version": "4.33.3", + "version": "4.33.4", "description": "FrankerFaceZ is a Twitch enhancement suite.", "private": true, "license": "Apache-2.0", diff --git a/src/sites/shared/player.jsx b/src/sites/shared/player.jsx index 341d510e..b819edf0 100644 --- a/src/sites/shared/player.jsx +++ b/src/sites/shared/player.jsx @@ -118,6 +118,17 @@ export default class PlayerBase extends Module { } }); + this.settings.add('player.compressor.force-legacy', { + default: false, + ui: { + path: 'Player > Compressor >> Advanced', + title: 'Force use of legacy browser API.', + description: 'This setting forces FrankerFaceZ to attempt to use an older browser API to create the compressor. Please reset your player after changing this setting.', + component: 'setting-check-box', + force_seen: true + } + }); + this.settings.add('player.compressor.shortcut', { default: null, requires: ['player.compressor.enable'], @@ -1424,6 +1435,9 @@ export default class PlayerBase extends Module { value = this.settings.get('player.gain.default'); try { + if (this.settings.get('player.compressor.force-legacy')) + throw new Error(); + gain = video._ffz_gain = new GainNode(ctx, { gain: value }); @@ -1566,6 +1580,9 @@ export default class PlayerBase extends Module { video._ffz_context = ctx; let src; try { + if (this.settings.get('player.compressor.force-legacy')) + throw new Error(); + src = video._ffz_source = new MediaElementAudioSourceNode(ctx, { mediaElement: video }); @@ -1577,6 +1594,9 @@ export default class PlayerBase extends Module { src.connect(ctx.destination); try { + if (this.settings.get('player.compressor.force-legacy')) + throw new Error(); + comp = video._ffz_compressor = new DynamicsCompressorNode(ctx); } catch (err) { this.log.info('Unable to use new DynamicsCompressorNode. Falling back to old method.'); @@ -1590,6 +1610,9 @@ export default class PlayerBase extends Module { value = this.settings.get('player.gain.default'); try { + if (this.settings.get('player.compressor.force-legacy')) + throw new Error(); + gain = video._ffz_gain = new GainNode(ctx, { gain: value }); diff --git a/src/sites/twitch-twilight/modules/chat/line.js b/src/sites/twitch-twilight/modules/chat/line.js index 489502c5..b2825f39 100644 --- a/src/sites/twitch-twilight/modules/chat/line.js +++ b/src/sites/twitch-twilight/modules/chat/line.js @@ -742,6 +742,7 @@ other {# messages were deleted by a moderator.} const user_bits = [ t.actions.renderInline(msg, this.props.showModerationIcons, u, r, e), + this.renderInlineHighlight ? this.renderInlineHighlight() : null, e('span', { className: 'chat-line__message--badges' }, t.chat.badges.render(msg, e)), From 82a34cdce92848657969e9bf2de5a11ca01ffaf4 Mon Sep 17 00:00:00 2001 From: SirStendec Date: Fri, 6 May 2022 17:02:49 -0400 Subject: [PATCH 10/63] 4.33.5 * Fixed: Update player element selectors to fix visibility of added player controls. --- package.json | 2 +- src/sites/shared/player.jsx | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index abd140e8..6031e688 100755 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "frankerfacez", "author": "Dan Salvato LLC", - "version": "4.33.4", + "version": "4.33.5", "description": "FrankerFaceZ is a Twitch enhancement suite.", "private": true, "license": "Apache-2.0", diff --git a/src/sites/shared/player.jsx b/src/sites/shared/player.jsx index b819edf0..913bda2c 100644 --- a/src/sites/shared/player.jsx +++ b/src/sites/shared/player.jsx @@ -1171,7 +1171,7 @@ export default class PlayerBase extends Module { addGainSlider(inst, visible_only, tries = 0) { const outer = inst.props.containerRef || this.fine.getChildNode(inst), video = inst.props.mediaPlayerInstance?.mediaSinkManager?.video || inst.props.mediaPlayerInstance?.core?.mediaSinkManager?.video, - container = outer && outer.querySelector('.player-controls__left-control-group'); + container = outer && outer.querySelector('.video-player__default-player .player-controls__left-control-group'); let gain = video != null && video._ffz_compressed && video._ffz_gain; if ( this.areControlsDisabled(inst) ) @@ -1302,7 +1302,7 @@ export default class PlayerBase extends Module { addCompressorButton(inst, visible_only, tries = 0) { const outer = inst.props.containerRef || this.fine.getChildNode(inst), video = inst.props.mediaPlayerInstance?.mediaSinkManager?.video || inst.props.mediaPlayerInstance?.core?.mediaSinkManager?.video, - container = outer && outer.querySelector('.player-controls__left-control-group'), + container = outer && outer.querySelector('.video-player__default-player .player-controls__left-control-group'), has_comp = HAS_COMPRESSOR && video != null && this.settings.get('player.compressor.enable'); if ( ! container ) { @@ -1730,7 +1730,7 @@ export default class PlayerBase extends Module { const outer = inst.props.containerRef || this.fine.getChildNode(inst), video = inst.props.mediaPlayerInstance?.mediaSinkManager?.video || inst.props.mediaPlayerInstance?.core?.mediaSinkManager?.video, is_fs = video && document.fullscreenElement && document.fullscreenElement.contains(video), - container = outer && outer.querySelector('.player-controls__right-control-group'), + container = outer && outer.querySelector('.video-player__default-player .player-controls__right-control-group'), has_pip = document.pictureInPictureEnabled && this.settings.get('player.button.pip'); if ( ! container ) { @@ -1833,7 +1833,7 @@ export default class PlayerBase extends Module { addResetButton(inst, tries = 0) { const outer = inst.props.containerRef || this.fine.getChildNode(inst), - container = outer && outer.querySelector('.player-controls__right-control-group'), + container = outer && outer.querySelector('.video-player__default-player .player-controls__right-control-group'), has_reset = this.settings.get('player.button.reset'); if ( ! container ) { @@ -2055,7 +2055,7 @@ export default class PlayerBase extends Module { return; const outer = inst.props.containerRef || this.fine.getChildNode(inst), - container = outer && outer.querySelector('.player-controls__right-control-group'); + container = outer && outer.querySelector('.video-player__default-player .player-controls__right-control-group'); if ( ! container ) return; From 213c2195ccbee532d6ad50f8fc20b5e2beea3bf0 Mon Sep 17 00:00:00 2001 From: SirStendec Date: Wed, 18 May 2022 17:58:46 -0400 Subject: [PATCH 11/63] 4.33.6 * Fixed: Tab-completion of FFZ emotes. --- package.json | 2 +- src/sites/twitch-twilight/modules/chat/input.jsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 6031e688..ef57be86 100755 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "frankerfacez", "author": "Dan Salvato LLC", - "version": "4.33.5", + "version": "4.33.6", "description": "FrankerFaceZ is a Twitch enhancement suite.", "private": true, "license": "Apache-2.0", diff --git a/src/sites/twitch-twilight/modules/chat/input.jsx b/src/sites/twitch-twilight/modules/chat/input.jsx index 5c57ee0e..4a8114e8 100644 --- a/src/sites/twitch-twilight/modules/chat/input.jsx +++ b/src/sites/twitch-twilight/modules/chat/input.jsx @@ -147,7 +147,7 @@ export default class Input extends Module { this.EmoteSuggestions = this.fine.define( 'tab-emote-suggestions', - n => n && n.getMatchedEmotes, + n => n && n.getMatches && n.autocompleteType === 'emote', Twilight.CHAT_ROUTES ); From bcee12a6b338e9726d521ede9ae9a541bcdf99ba Mon Sep 17 00:00:00 2001 From: SirStendec Date: Wed, 8 Jun 2022 23:07:07 -0400 Subject: [PATCH 12/63] 4.34.0 * Added: Option to hide channels from the directory based on their title. * Fixed: Issue with FrankerFaceZ failing to initialize correctly due to a change in Twitch's JS layout. --- package.json | 2 +- src/modules/chat/index.js | 10 +-- src/sites/twitch-twilight/index.js | 4 +- .../modules/directory/index.jsx | 83 ++++++++++++++++++- src/sites/twitch-twilight/modules/layout.js | 17 +++- src/utilities/compat/webmunch.js | 59 +++++++++++-- src/utilities/constants.js | 1 + src/utilities/object.js | 7 +- 8 files changed, 158 insertions(+), 25 deletions(-) diff --git a/package.json b/package.json index ef57be86..89826189 100755 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "frankerfacez", "author": "Dan Salvato LLC", - "version": "4.33.6", + "version": "4.34.0", "description": "FrankerFaceZ is a Twitch enhancement suite.", "private": true, "license": "Apache-2.0", diff --git a/src/modules/chat/index.js b/src/modules/chat/index.js index a62c0570..702ee3fc 100644 --- a/src/modules/chat/index.js +++ b/src/modules/chat/index.js @@ -8,7 +8,7 @@ import dayjs from 'dayjs'; import Module from 'utilities/module'; import {createElement, ManagedStyle} from 'utilities/dom'; -import {timeout, has, glob_to_regex, escape_regex, split_chars} from 'utilities/object'; +import {timeout, has, addWordSeparators, glob_to_regex, escape_regex, split_chars} from 'utilities/object'; import {Color} from 'utilities/color'; import Badges from './badges'; @@ -24,8 +24,6 @@ import * as RICH_PROVIDERS from './rich_providers'; import Actions from './actions'; import { getFontsList } from 'src/utilities/fonts'; -export const SEPARATORS = '[\\s`~<>!-#%-\\x2A,-/:;\\x3F@\\x5B-\\x5D_\\x7B}\\u00A1\\u00A7\\u00AB\\u00B6\\u00B7\\u00BB\\u00BF\\u037E\\u0387\\u055A-\\u055F\\u0589\\u058A\\u05BE\\u05C0\\u05C3\\u05C6\\u05F3\\u05F4\\u0609\\u060A\\u060C\\u060D\\u061B\\u061E\\u061F\\u066A-\\u066D\\u06D4\\u0700-\\u070D\\u07F7-\\u07F9\\u0830-\\u083E\\u085E\\u0964\\u0965\\u0970\\u0AF0\\u0DF4\\u0E4F\\u0E5A\\u0E5B\\u0F04-\\u0F12\\u0F14\\u0F3A-\\u0F3D\\u0F85\\u0FD0-\\u0FD4\\u0FD9\\u0FDA\\u104A-\\u104F\\u10FB\\u1360-\\u1368\\u1400\\u166D\\u166E\\u169B\\u169C\\u16EB-\\u16ED\\u1735\\u1736\\u17D4-\\u17D6\\u17D8-\\u17DA\\u1800-\\u180A\\u1944\\u1945\\u1A1E\\u1A1F\\u1AA0-\\u1AA6\\u1AA8-\\u1AAD\\u1B5A-\\u1B60\\u1BFC-\\u1BFF\\u1C3B-\\u1C3F\\u1C7E\\u1C7F\\u1CC0-\\u1CC7\\u1CD3\\u2010-\\u2027\\u2030-\\u2043\\u2045-\\u2051\\u2053-\\u205E\\u207D\\u207E\\u208D\\u208E\\u2329\\u232A\\u2768-\\u2775\\u27C5\\u27C6\\u27E6-\\u27EF\\u2983-\\u2998\\u29D8-\\u29DB\\u29FC\\u29FD\\u2CF9-\\u2CFC\\u2CFE\\u2CFF\\u2D70\\u2E00-\\u2E2E\\u2E30-\\u2E3B\\u3001-\\u3003\\u3008-\\u3011\\u3014-\\u301F\\u3030\\u303D\\u30A0\\u30FB\\uA4FE\\uA4FF\\uA60D-\\uA60F\\uA673\\uA67E\\uA6F2-\\uA6F7\\uA874-\\uA877\\uA8CE\\uA8CF\\uA8F8-\\uA8FA\\uA92E\\uA92F\\uA95F\\uA9C1-\\uA9CD\\uA9DE\\uA9DF\\uAA5C-\\uAA5F\\uAADE\\uAADF\\uAAF0\\uAAF1\\uABEB\\uFD3E\\uFD3F\\uFE10-\\uFE19\\uFE30-\\uFE52\\uFE54-\\uFE61\\uFE63\\uFE68\\uFE6A\\uFE6B\\uFF01-\\uFF03\\uFF05-\\uFF0A\\uFF0C-\\uFF0F\\uFF1A\\uFF1B\\uFF1F\\uFF20\\uFF3B-\\uFF3D\\uFF3F\\uFF5B\\uFF5D\\uFF5F-\\uFF65]'; - function sortPriorityColorTerms(list) { list.sort((a,b) => { if ( a[0] < b[0] ) return 1; @@ -37,10 +35,6 @@ function sortPriorityColorTerms(list) { return list; } -function addSeparators(str) { - return `(^|.*?${SEPARATORS})(?:${str})(?=$|${SEPARATORS})` -} - const TERM_FLAGS = ['g', 'gi']; function formatTerms(data) { @@ -49,7 +43,7 @@ function formatTerms(data) { for(let i=0; i < data.length; i++) { const list = data[i]; if ( list[0].length ) - list[1].push(addSeparators(list[0].join('|'))); + list[1].push(addWordSeparators(list[0].join('|'))); out.push(list[1].length ? new RegExp(list[1].join('|'), TERM_FLAGS[i] || 'gi') : null); } diff --git a/src/sites/twitch-twilight/index.js b/src/sites/twitch-twilight/index.js index a92af90b..c63f0bbe 100644 --- a/src/sites/twitch-twilight/index.js +++ b/src/sites/twitch-twilight/index.js @@ -249,7 +249,7 @@ Twilight.KNOWN_MODULES = { } } -const VEND_CHUNK = n => n && n.includes('vendor'); +const VEND_CHUNK = n => ! n || n.includes('vendor'); Twilight.KNOWN_MODULES.core.use_result = true; //Twilight.KNOWN_MODULES.core.chunks = 'core'; @@ -263,7 +263,7 @@ Twilight.KNOWN_MODULES['gql-printer'].chunks = VEND_CHUNK; Twilight.KNOWN_MODULES.mousetrap.chunks = VEND_CHUNK; -const CHAT_CHUNK = n => n && n.includes('chat'); +const CHAT_CHUNK = n => ! n || n.includes('chat'); Twilight.KNOWN_MODULES['chat-types'].use_result = true; Twilight.KNOWN_MODULES['chat-types'].chunks = CHAT_CHUNK; diff --git a/src/sites/twitch-twilight/modules/directory/index.jsx b/src/sites/twitch-twilight/modules/directory/index.jsx index 42c389b8..354916bf 100644 --- a/src/sites/twitch-twilight/modules/directory/index.jsx +++ b/src/sites/twitch-twilight/modules/directory/index.jsx @@ -7,7 +7,7 @@ import {SiteModule} from 'utilities/module'; import {duration_to_string} from 'utilities/time'; import {createElement} from 'utilities/dom'; -import {get} from 'utilities/object'; +import {get, glob_to_regex, escape_regex, addWordSeparators} from 'utilities/object'; import Game from './game'; @@ -18,6 +18,15 @@ export const CARD_CONTEXTS = ((e ={}) => { return e; })(); +function formatTerms(data, flags) { + if ( data[0].length ) + data[1].push(addWordSeparators(data[0].join('|'))); + + if ( ! data[1].length ) + return null; + + return new RegExp(data[1].join('|'), flags); +} //const CREATIVE_ID = 488191; @@ -175,6 +184,58 @@ export default class Directory extends SiteModule { changed: () => this.DirectoryShelf.forceUpdate() }); + this.settings.add('directory.block-titles', { + default: [], + type: 'array_merge', + always_inherit: true, + ui: { + path: 'Directory > Channels >> Block by Title', + component: 'basic-terms' + } + }); + + this.settings.add('__filter:directory.block-titles', { + requires: ['directory.block-titles'], + equals: 'requirements', + process(ctx) { + const val = ctx.get('directory.block-titles'); + if ( ! val || ! val.length ) + return null; + + const out = [ + [ // sensitive + [], [] // word + ], + [ + [], [] + ] + ]; + + 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[item.s ? 0 : 1][item.w ? 0 : 1].push(v); + } + + return [ + formatTerms(out[0], 'g'), + formatTerms(out[1], 'gi') + ]; + }, + + changed: () => this.updateCards() + }); + /*this.settings.add('directory.hide-viewing-history', { default: false, ui: { @@ -339,9 +400,23 @@ export default class Directory extends SiteModule { } } - const should_hide = bad_tag || (props.streamType === 'rerun' && this.settings.get('directory.hide-vodcasts')) || - (props.context != null && props.context !== CARD_CONTEXTS.SingleGameList && this.settings.provider.get('directory.game.blocked-games', []).includes(game)) || - ((props.isPromotion || props.sourceType === 'COMMUNITY_BOOST' || props.sourceType === 'PROMOTION' || props.sourceType === 'SPONSORED') && this.settings.get('directory.hide-promoted')); + let should_hide = false; + if ( bad_tag ) + should_hide = true; + else if ( props.streamType === 'rerun' && this.settings.get('directory.hide-vodcasts') ) + should_hide = true; + else if ( props.context != null && props.context !== CARD_CONTEXTS.SingleGameList && this.settings.provider.get('directory.game.blocked-games', []).includes(game) ) + should_hide = true; + else if ( (props.isPromotion || props.sourceType === 'COMMUNITY_BOOST' || props.sourceType === 'PROMOTION' || props.sourceType === 'SPONSORED') && this.settings.get('directory.hide-promoted') ) + should_hide = true; + else { + const regexes = this.settings.get('__filter:directory.block-titles'); + if ( regexes && + (( regexes[0] && regexes[0].test(props.title) ) || + ( regexes[1] && regexes[1].test(props.title) )) + ) + should_hide = true; + } let hide_container = el.closest('.tw-tower > div'); if ( ! hide_container ) diff --git a/src/sites/twitch-twilight/modules/layout.js b/src/sites/twitch-twilight/modules/layout.js index 7ca3418b..614633ed 100644 --- a/src/sites/twitch-twilight/modules/layout.js +++ b/src/sites/twitch-twilight/modules/layout.js @@ -359,9 +359,24 @@ export default class Layout extends Module { game = stream?.game?.displayName, offline = props?.offline ?? false; + let should_hide = false; + if ( game && blocked_games.includes(game) ) + should_hide = true; + if ( props.isPromoted && this.settings.get('directory.hide-promoted') ) + should_hide = true; + else { + const regexes = this.settings.get('__filter:directory.block-titles'); + const title = stream?.broadcaster?.broadcastSettings?.title; + if ( regexes && title && + (( regexes[0] && regexes[0].test(title) ) || + ( regexes[1] && regexes[1].test(title) )) + ) + should_hide = true; + } + card.classList.toggle('ffz--side-nav-card-rerun', rerun); card.classList.toggle('ffz--side-nav-card-offline', offline); - card.classList.toggle('tw-hide', game ? blocked_games.includes(game) : false); + card.classList.toggle('tw-hide', should_hide); } } diff --git a/src/utilities/compat/webmunch.js b/src/utilities/compat/webmunch.js index 791d2aab..c62d3a3a 100644 --- a/src/utilities/compat/webmunch.js +++ b/src/utilities/compat/webmunch.js @@ -222,7 +222,12 @@ export default class WebMunch extends Module { if ( modules ) this.processModulesV4(modules, false); - this._checked_module = {}; + for(const [key,val] of Object.entries(this._checked_module)) { + if (val == true) + this._checked_module[key] = null; + } + + //this._checked_module = {}; const res = this._original_loader.apply(this._original_store, arguments); // eslint-disable-line prefer-rest-params this.emit(':loaded', chunk_ids, names, modules); return res; @@ -375,7 +380,7 @@ export default class WebMunch extends Module { return null; let ids; - if ( this._original_store && predicate.chunks ) { + if ( this._original_store && predicate.chunks && this._chunk_names && Object.keys(this._chunk_names).length ) { const chunk_pred = typeof predicate.chunks === 'function'; if ( ! chunk_pred && ! Array.isArray(predicate.chunks) ) predicate.chunks = [predicate.chunks]; @@ -429,7 +434,7 @@ export default class WebMunch extends Module { return null; let ids = this._known_ids; - if ( this._original_store && predicate.chunks ) { + if ( this._original_store && predicate.chunks && this._chunk_names && Object.keys(this._chunk_names).length ) { const chunk_pred = typeof predicate.chunks === 'function'; if ( ! chunk_pred && ! Array.isArray(predicate.chunks) ) predicate.chunks = [predicate.chunks]; @@ -498,9 +503,19 @@ export default class WebMunch extends Module { } - _checkModule(id) { - const fn = this._require?.m?.[id]; + _checkModule(id, checked = null) { + if (checked) { + if (checked.has(id)) + return (this._checked_module[id] ?? false); + + checked.add(id); + } + + let fn = this._require?.m?.[id]; if ( fn ) { + if ( fn.original ) + fn = fn.original; + let reqs = fn[Requires], banned = false; @@ -543,12 +558,40 @@ export default class WebMunch extends Module { } else if ( reqs ) { for(const mod_id of reqs) - if ( ! this._require.m[mod_id] ) + if ( ! this._require.m[mod_id] ) { banned = true; + break; + } + } + + if ( ! banned && reqs ) { + if ( ! checked ) { + checked = new Set(); + checked.add(id); + } + + for(const mod_id of reqs) { + let val = this._checked_module[mod_id]; + if (val == null && ! checked.has(mod_id)) + try { + val = this._checkModule(mod_id, checked); + } catch (err) { + this.log.verbose(`Recursion error checking module ${id} (${mod_id})`); + val = true; + } + + if ( val ) { + this.log.verbose(`Unable to load module ${id} due to unable to load dependency ${mod_id}`); + banned = true; + break; + } + } } return this._checked_module[id] = banned; } + + return this._checked_module[id] = true; } @@ -604,7 +647,7 @@ export default class WebMunch extends Module { const loader = require.e && require.e.toString(); let modules; if ( loader && loader.indexOf('Loading chunk') !== -1 ) { - const data = this.v4 ? /assets\/"\+\(({1:.*?})/.exec(loader) : /({0:.*?})/.exec(loader); + const data = this.v4 ? /assets\/"\+\(?({1:.*?})/.exec(loader) : /({0:.*?})/.exec(loader); if ( data ) try { modules = JSON.parse(data[1].replace(/(\d+):/g, '"$1":')) @@ -612,7 +655,7 @@ export default class WebMunch extends Module { } else if ( require.u ) { const builder = require.u.toString(), - match = /assets\/"\+({\d+:.*?})/.exec(builder), + match = /assets\/"\+\(?({\d+:.*?})/.exec(builder), data = match ? match[1].replace(/([\de]+):/g, (_, m) => { if ( /^\d+e\d+$/.test(m) ) { const bits = m.split('e'); diff --git a/src/utilities/constants.js b/src/utilities/constants.js index bd0bcbdf..c1724aab 100644 --- a/src/utilities/constants.js +++ b/src/utilities/constants.js @@ -17,6 +17,7 @@ export const SENTRY_ID = 'https://74b46b3894114f399d51949c6d237489@sentry.franke export const LV_SERVER = 'https://cbenni.com/api'; export const LV_SOCKET_SERVER = 'wss://cbenni.com/socket.io/'; +export const WORD_SEPARATORS = '[\\s`~<>!-#%-\\x2A,-/:;\\x3F@\\x5B-\\x5D_\\x7B}\\u00A1\\u00A7\\u00AB\\u00B6\\u00B7\\u00BB\\u00BF\\u037E\\u0387\\u055A-\\u055F\\u0589\\u058A\\u05BE\\u05C0\\u05C3\\u05C6\\u05F3\\u05F4\\u0609\\u060A\\u060C\\u060D\\u061B\\u061E\\u061F\\u066A-\\u066D\\u06D4\\u0700-\\u070D\\u07F7-\\u07F9\\u0830-\\u083E\\u085E\\u0964\\u0965\\u0970\\u0AF0\\u0DF4\\u0E4F\\u0E5A\\u0E5B\\u0F04-\\u0F12\\u0F14\\u0F3A-\\u0F3D\\u0F85\\u0FD0-\\u0FD4\\u0FD9\\u0FDA\\u104A-\\u104F\\u10FB\\u1360-\\u1368\\u1400\\u166D\\u166E\\u169B\\u169C\\u16EB-\\u16ED\\u1735\\u1736\\u17D4-\\u17D6\\u17D8-\\u17DA\\u1800-\\u180A\\u1944\\u1945\\u1A1E\\u1A1F\\u1AA0-\\u1AA6\\u1AA8-\\u1AAD\\u1B5A-\\u1B60\\u1BFC-\\u1BFF\\u1C3B-\\u1C3F\\u1C7E\\u1C7F\\u1CC0-\\u1CC7\\u1CD3\\u2010-\\u2027\\u2030-\\u2043\\u2045-\\u2051\\u2053-\\u205E\\u207D\\u207E\\u208D\\u208E\\u2329\\u232A\\u2768-\\u2775\\u27C5\\u27C6\\u27E6-\\u27EF\\u2983-\\u2998\\u29D8-\\u29DB\\u29FC\\u29FD\\u2CF9-\\u2CFC\\u2CFE\\u2CFF\\u2D70\\u2E00-\\u2E2E\\u2E30-\\u2E3B\\u3001-\\u3003\\u3008-\\u3011\\u3014-\\u301F\\u3030\\u303D\\u30A0\\u30FB\\uA4FE\\uA4FF\\uA60D-\\uA60F\\uA673\\uA67E\\uA6F2-\\uA6F7\\uA874-\\uA877\\uA8CE\\uA8CF\\uA8F8-\\uA8FA\\uA92E\\uA92F\\uA95F\\uA9C1-\\uA9CD\\uA9DE\\uA9DF\\uAA5C-\\uAA5F\\uAADE\\uAADF\\uAAF0\\uAAF1\\uABEB\\uFD3E\\uFD3F\\uFE10-\\uFE19\\uFE30-\\uFE52\\uFE54-\\uFE61\\uFE63\\uFE68\\uFE6A\\uFE6B\\uFF01-\\uFF03\\uFF05-\\uFF0A\\uFF0C-\\uFF0F\\uFF1A\\uFF1B\\uFF1F\\uFF20\\uFF3B-\\uFF3D\\uFF3F\\uFF5B\\uFF5D\\uFF5F-\\uFF65]'; export const BAD_HOTKEYS = [ 'f', diff --git a/src/utilities/object.js b/src/utilities/object.js index 37916dd5..21f7c518 100644 --- a/src/utilities/object.js +++ b/src/utilities/object.js @@ -1,6 +1,6 @@ 'use strict'; -import {BAD_HOTKEYS, TWITCH_EMOTE_V2} from 'utilities/constants'; +import {BAD_HOTKEYS, TWITCH_EMOTE_V2, WORD_SEPARATORS} from 'utilities/constants'; const HOP = Object.prototype.hasOwnProperty; @@ -539,6 +539,11 @@ export const escape_regex = RegExp.escape || function escape_regex(str) { } +export function addWordSeparators(str) { + return `(^|.*?${WORD_SEPARATORS})(?:${str})(?=$|${WORD_SEPARATORS})` +} + + const CONTROL_CHARS = '/$^+.()=!|'; export function glob_to_regex(input) { From 4d1d3ae0d28a43a264aa84edf395b0899e9956de Mon Sep 17 00:00:00 2001 From: SirStendec Date: Sat, 11 Jun 2022 12:44:54 -0400 Subject: [PATCH 13/63] 4.34.1 * Fixed: Room actions not rendering due to changes in React internals. * Fixed: Bug with sidebar cards not being hidden correctly in some situations. --- package.json | 2 +- .../twitch-twilight/modules/chat/input.jsx | 2 +- src/sites/twitch-twilight/modules/layout.js | 2 +- src/utilities/dom.js | 29 ++++++++++++------- 4 files changed, 21 insertions(+), 14 deletions(-) diff --git a/package.json b/package.json index 89826189..49d55823 100755 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "frankerfacez", "author": "Dan Salvato LLC", - "version": "4.34.0", + "version": "4.34.1", "description": "FrankerFaceZ is a Twitch enhancement suite.", "private": true, "license": "Apache-2.0", diff --git a/src/sites/twitch-twilight/modules/chat/input.jsx b/src/sites/twitch-twilight/modules/chat/input.jsx index 4a8114e8..7cb95b04 100644 --- a/src/sites/twitch-twilight/modules/chat/input.jsx +++ b/src/sites/twitch-twilight/modules/chat/input.jsx @@ -219,7 +219,7 @@ export default class Input extends Module { try { const above = t.chat.context.get('chat.actions.room-above'), state = t.chat.context.get('context.chat_state') || {}, - container = above ? out : findReactFragment(out, n => n.props && n.props.className === 'chat-input__buttons-container'); + container = above ? findReactFragment(out, n => n.props && Array.isArray(n.props.children)) : findReactFragment(out, n => n.props && n.props.className === 'chat-input__buttons-container'); if ( ! container || ! container.props || ! container.props.children ) return out; diff --git a/src/sites/twitch-twilight/modules/layout.js b/src/sites/twitch-twilight/modules/layout.js index 614633ed..79eade88 100644 --- a/src/sites/twitch-twilight/modules/layout.js +++ b/src/sites/twitch-twilight/modules/layout.js @@ -362,7 +362,7 @@ export default class Layout extends Module { let should_hide = false; if ( game && blocked_games.includes(game) ) should_hide = true; - if ( props.isPromoted && this.settings.get('directory.hide-promoted') ) + if ( props?.isPromoted && this.settings.get('directory.hide-promoted') ) should_hide = true; else { const regexes = this.settings.get('__filter:directory.block-titles'); diff --git a/src/utilities/dom.js b/src/utilities/dom.js index b90bb735..86712a74 100644 --- a/src/utilities/dom.js +++ b/src/utilities/dom.js @@ -58,23 +58,30 @@ export function findReactFragment(frag, criteria, depth = 25, current = 0, visit visited.add(frag); - if ( frag && frag.props && Array.isArray(frag.props.children) ) - for(const child of frag.props.children) { - if ( ! child ) - continue; + if ( frag && frag.props && frag.props.children ) { + if ( Array.isArray(frag.props.children) ) { + for(const child of frag.props.children) { + if ( ! child ) + continue; - if ( Array.isArray(child) ) { - for(const f of child) { - const out = findReactFragment(f, criteria, depth, current + 1, visited); + if ( Array.isArray(child) ) { + for(const f of child) { + const out = findReactFragment(f, criteria, depth, current + 1, visited); + if ( out ) + return out; + } + } else { + const out = findReactFragment(child, criteria, depth, current + 1, visited); if ( out ) return out; } - } else { - const out = findReactFragment(child, criteria, depth, current + 1, visited); - if ( out ) - return out; } + } else { + const out = findReactFragment(frag.props.children, criteria, depth, current + 1, visited); + if ( out ) + return out; } + } return null; } From 02efe80a92bf2a8b55d98444d22d8071a8f63003 Mon Sep 17 00:00:00 2001 From: SirStendec Date: Tue, 14 Jun 2022 16:26:34 -0400 Subject: [PATCH 14/63] 4.35.0 * Added: Option to hide the Support Activity Feed from chat. * Fixed: Apply proper colors to the Support Activity Feed when using custom colors. --- package.json | 2 +- src/sites/twitch-twilight/modules/chat/index.js | 12 ++++++++++++ .../twitch-twilight/modules/css_tweaks/index.js | 3 ++- src/sites/twitch-twilight/modules/theme/index.js | 2 +- .../twitch-twilight/styles/color_normalizer.scss | 6 ++++++ 5 files changed, 22 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 49d55823..9858397d 100755 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "frankerfacez", "author": "Dan Salvato LLC", - "version": "4.34.1", + "version": "4.35.0", "description": "FrankerFaceZ is a Twitch enhancement suite.", "private": true, "license": "Apache-2.0", diff --git a/src/sites/twitch-twilight/modules/chat/index.js b/src/sites/twitch-twilight/modules/chat/index.js index 2a3a3b12..1b43f35c 100644 --- a/src/sites/twitch-twilight/modules/chat/index.js +++ b/src/sites/twitch-twilight/modules/chat/index.js @@ -341,6 +341,15 @@ export default class ChatHook extends Module { } }); + this.settings.add('chat.banners.last-events', { + default: true, + ui: { + path: 'Chat > Appearance >> Community', + title: 'Allow the Support Activity Feed to be displayed in chat.', + component: 'setting-check-box' + } + }); + this.settings.add('chat.banners.hype-train', { default: true, ui: { @@ -898,6 +907,9 @@ export default class ChatHook extends Module { this.chat.context.getChanges('chat.emotes.limit-size', () => this.toggleEmoteJail()); + this.chat.context.getChanges('chat.banners.last-events', val => + this.css_tweaks.toggleHide('last-x-events', ! val)); + this.chat.context.getChanges('chat.input.show-mod-view', val => this.css_tweaks.toggleHide('mod-view', ! val)); diff --git a/src/sites/twitch-twilight/modules/css_tweaks/index.js b/src/sites/twitch-twilight/modules/css_tweaks/index.js index 80bc8b3c..da1cbf50 100644 --- a/src/sites/twitch-twilight/modules/css_tweaks/index.js +++ b/src/sites/twitch-twilight/modules/css_tweaks/index.js @@ -46,7 +46,8 @@ const CLASSES = { 'not-live-bar': 'div[data-test-selector="non-live-video-banner-layout"]', 'channel-live-ind': '.channel-header__user .tw-channel-status-text-indicator,.channel-info-content .tw-halo__indicator', 'celebration': 'body .celebration__overlay', - 'mod-view': '.chat-input__buttons-container a[href*="/moderator"]' + 'mod-view': '.chat-input__buttons-container a[href*="/moderator"]', + 'last-x-events': '.last-x-events_container' }; diff --git a/src/sites/twitch-twilight/modules/theme/index.js b/src/sites/twitch-twilight/modules/theme/index.js index 31f1ed47..948c9763 100644 --- a/src/sites/twitch-twilight/modules/theme/index.js +++ b/src/sites/twitch-twilight/modules/theme/index.js @@ -27,7 +27,7 @@ const ACCENT_COLORS = { //light: {'c':{'accent': 9,'background-accent':8,'background-accent-alt':7,'background-accent-alt-2':6,'background-button':7,'background-button-active':7,'background-button-focus':8,'background-button-hover':8,'background-button-primary-active':7,'background-button-primary-default':9,'background-button-primary-hover':8,'background-graph':15,'background-graph-fill':9,'background-input-checkbox-checked':9,'background-input-checked':8,'background-interactable-active':9,'background-interactable-selected':9,'background-interactable-hover':8,'background-progress-countdown-status':8,'background-progress-status':8,'background-range-fill':9,'background-subscriber-stream-tag-active':13,'background-subscriber-stream-tag-default':13,'background-subscriber-stream-tag-hover':14,'background-toggle-checked':9,/*'background-tooltip':1,*/'background-top-nav':7,'border-brand':9,'border-button':7,'border-button-active':8,'border-button-focus':9,'border-button-hover':8,'border-input-checkbox-checked':9,'border-input-checkbox-focus':9,'border-input-focus':9,'border-interactable-selected':9,'border-subscriber-stream-tag':10,'border-tab-active':8,'border-tab-focus':8,'border-tab-hover':8,'border-toggle-focus':8,'border-toggle-hover':8,'border-whisper-incoming':10,'fill-brand':9,'text-button-text':8,'text-button-text-focus':'o1','text-button-text-hover':'o1','text-link':8,'text-link-active':9,'text-link-focus':9,'text-link-hover':9,'text-link-visited':9,'text-overlay-link-active':13,'text-overlay-link-focus':13,'text-overlay-link-hover':13,'text-tab-active':8},'s':{'button-active':[8,'0 0 6px 0',''],'button-focus':[8,'0 0 6px 0',''],'input-focus':[10,'0 0 10px -2px',''],'interactable-focus':[8,'0 0 6px 1px',''],'tab-focus':[8,'0 4px 6px -4px','']}}, //dark: {'c':{'background-accent':8,'background-accent-alt':7,'background-accent-alt-2':6,'background-button':7,'background-button-active':7,'background-button-focus':8,'background-button-hover':8,'background-button-primary-active':7,'background-button-primary-default':9,'background-button-primary-hover':8,'background-graph':2,'background-graph-fill':8,'background-input-checkbox-checked':9,'background-input-checked':8,'background-interactable-selected':9,'background-progress-countdown-status':9,'background-progress-status':9,'background-range-fill':9,'background-subscriber-stream-tag-active':4,'background-subscriber-stream-tag-default':4,'background-subscriber-stream-tag-hover':3,'background-toggle-checked':9,'background-top-nav':6,'border-brand':9,'border-button':7,'border-button-active':8,'border-button-focus':9,'border-button-hover':8,'border-input-checkbox-checked':9,'border-input-checkbox-focus':9,'border-input-focus':9,'border-interactable-selected':10,'border-subscriber-stream-tag':5,'border-tab-active':11,'border-tab-focus':11,'border-tab-hover':11,'border-toggle-focus':7,'border-toggle-hover':7,'border-whisper-incoming':10,'fill-brand':9,'text-button-text':10,'text-button-text-focus':'o1','text-button-text-hover':'o1','text-link':10,'text-link-active':10,'text-link-focus':10,'text-link-hover':10,'text-link-visited':10,'text-overlay-link-active':13,'text-overlay-link-focus':13,'text-overlay-link-hover':13,'text-tab-active':11,'background-chat':1,'background-chat-alt':3,'background-chat-header':2,'background-modal':3,'text-button-text-active':'o2'},'s':{'button-active':[8,'0 0 6px 0',''],'button-focus':[8,'0 0 6px 0',''],'input-focus':[8,'0 0 10px -2px',''],'interactable-focus':[8,'0 0 6px 0',''],'tab-focus':[11,'0 4px 6px -4px',''],'input':[5,'inset 0 0 0 1px','']}}, //light: {'c':{'background-accent':8,'background-accent-alt':7,'background-accent-alt-2':6,'background-button':7,'background-button-active':7,'background-button-focus':8,'background-button-hover':8,'background-button-primary-active':7,'background-button-primary-default':9,'background-button-primary-hover':8,'background-graph':15,'background-graph-fill':9,'background-input-checkbox-checked':9,'background-input-checked':8,'background-interactable-selected':9,'background-progress-countdown-status':8,'background-progress-status':8,'background-range-fill':9,'background-subscriber-stream-tag-active':13,'background-subscriber-stream-tag-default':13,'background-subscriber-stream-tag-hover':14,'background-toggle-checked':9,'background-top-nav':7,'border-brand':9,'border-button':7,'border-button-active':8,'border-button-focus':9,'border-button-hover':8,'border-input-checkbox-checked':9,'border-input-checkbox-focus':9,'border-input-focus':9,'border-interactable-selected':9,'border-subscriber-stream-tag':10,'border-tab-active':8,'border-tab-focus':8,'border-tab-hover':8,'border-toggle-focus':8,'border-toggle-hover':8,'border-whisper-incoming':10,'fill-brand':9,'text-button-text':8,'text-button-text-focus':'o1','text-button-text-hover':'o1','text-link':8,'text-link-active':9,'text-link-focus':9,'text-link-hover':9,'text-link-visited':9,'text-overlay-link-active':13,'text-overlay-link-focus':13,'text-overlay-link-hover':13,'text-tab-active':8},'s':{'button-active':[8,'0 0 6px 0',''],'button-focus':[8,'0 0 6px 0',''],'input-focus':[10,'0 0 10px -2px',''],'interactable-focus':[8,'0 0 6px 1px',''],'tab-focus':[8,'0 4px 6px -4px','']}}, - dark: {'c':{'background-accent':8,'background-accent-alt':7,'background-accent-alt-2':6,'background-button':7,'background-button-active':7,'background-button-focus':8,'background-button-hover':8,'background-button-primary-active':7,'background-button-primary-default':9,'background-button-primary-hover':8,'background-chat':1,'background-chat-alt':3,'background-chat-header':2,'background-graph':2,'background-graph-fill':8,'background-input-checkbox-checked':10,'background-input-checked':8,'background-interactable-selected':9,'background-modal':3,'background-progress-countdown-status':9,'background-progress-status':9,'background-range-fill':10,'background-subscriber-stream-tag-active':4,'background-subscriber-stream-tag-default':4,'background-subscriber-stream-tag-hover':3,'background-toggle-checked':9,'background-top-nav':6,'border-brand':9,'border-button':7,'border-button-active':8,'border-button-focus':9,'border-button-hover':8,'border-input-checkbox-checked':10,'border-input-checkbox-focus':10,'border-input-focus':10,'border-interactable-selected':10,'border-range-handle':10,'border-subscriber-stream-tag':5,'border-tab-active':11,'border-tab-focus':11,'border-tab-hover':11,'border-toggle-checked':10,'border-toggle-focus':10,'border-whisper-incoming':10,'fill-brand':9,'text-button-text-active':'o2','text-link':10,'text-link-active':10,'text-link-focus':10,'text-link-hover':10,'text-link-visited':10,'text-overlay-link-active':13,'text-overlay-link-focus':13,'text-overlay-link-hover':13,'text-tab-active':11,'text-toggle-checked-icon':10,'text-tooltip':1,'text-button-text':10},'s':{'button-active':[8,' 0 0 6px 0',''],'button-focus':[8,' 0 0 6px 0',''],'input':[5,' inset 0 0 0 1px',''],'input-focus':[8,' 0 0 10px -2px',''],'interactable-focus':[8,' 0 0 6px 0',''],'tab-focus':[11,' 0 4px 6px -4px','']}}, + dark: {'c':{'background-brand':11,'background-accent':8,'background-accent-alt':7,'background-accent-alt-2':6,'background-button':7,'background-button-active':7,'background-button-focus':8,'background-button-hover':8,'background-button-primary-active':7,'background-button-primary-default':9,'background-button-primary-hover':8,'background-chat':1,'background-chat-alt':3,'background-chat-header':2,'background-graph':2,'background-graph-fill':8,'background-input-checkbox-checked':10,'background-input-checked':8,'background-interactable-selected':9,'background-modal':3,'background-progress-countdown-status':9,'background-progress-status':9,'background-range-fill':10,'background-subscriber-stream-tag-active':4,'background-subscriber-stream-tag-default':4,'background-subscriber-stream-tag-hover':3,'background-toggle-checked':9,'background-top-nav':6,'border-brand':9,'border-button':7,'border-button-active':8,'border-button-focus':9,'border-button-hover':8,'border-input-checkbox-checked':10,'border-input-checkbox-focus':10,'border-input-focus':10,'border-interactable-selected':10,'border-range-handle':10,'border-subscriber-stream-tag':5,'border-tab-active':11,'border-tab-focus':11,'border-tab-hover':11,'border-toggle-checked':10,'border-toggle-focus':10,'border-whisper-incoming':10,'fill-brand':9,'text-button-text-active':'o2','text-link':10,'text-link-active':10,'text-link-focus':10,'text-link-hover':10,'text-link-visited':10,'text-overlay-link-active':13,'text-overlay-link-focus':13,'text-overlay-link-hover':13,'text-tab-active':11,'text-toggle-checked-icon':10,'text-tooltip':1,'text-button-text':10},'s':{'button-active':[8,' 0 0 6px 0',''],'button-focus':[8,' 0 0 6px 0',''],'input':[5,' inset 0 0 0 1px',''],'input-focus':[8,' 0 0 10px -2px',''],'interactable-focus':[8,' 0 0 6px 0',''],'tab-focus':[11,' 0 4px 6px -4px','']}}, light: {'c':{'background-accent':8,'background-accent-alt':7,'background-accent-alt-2':6,'background-button':7,'background-button-active':7,'background-button-focus':8,'background-button-hover':8,'background-button-primary-active':7,'background-button-primary-default':9,'background-button-primary-hover':8,'background-chat':1,'background-chat-alt':3,'background-chat-header':2,'background-graph':15,'background-graph-fill':9,'background-input-checkbox-checked':9,'background-input-checked':8,'background-interactable-selected':9,'background-modal':3,'background-progress-countdown-status':8,'background-progress-status':8,'background-range-fill':9,'background-subscriber-stream-tag-active':13,'background-subscriber-stream-tag-default':13,'background-subscriber-stream-tag-hover':14,'background-toggle-checked':9,'background-top-nav':7,'border-brand':9,'border-button':7,'border-button-active':8,'border-button-focus':9,'border-button-hover':8,'border-input-checkbox-checked':9,'border-input-checkbox-focus':9,'border-input-focus':9,'border-interactable-selected':9,'border-range-handle':9,'border-subscriber-stream-tag':10,'border-tab-active':8,'border-tab-focus':8,'border-tab-hover':8,'border-toggle-checked':9,'border-toggle-focus':9,'border-whisper-incoming':10,'fill-brand':9,'text-button-text-active':'o2','text-link':8,'text-link-active':9,'text-link-focus':9,'text-link-hover':9,'text-link-visited':9,'text-overlay-link-active':13,'text-overlay-link-focus':13,'text-overlay-link-hover':13,'text-tab-active':8,'text-toggle-checked-icon':9,'text-tooltip':1,'text-button-text':8,'background-tooltip':1,'text-button-text-focus':'o1','text-button-text-hover':'o1'},'s':{'button-active':[8,' 0 0 6px 0',''],'button-focus':[8,' 0 0 6px 0',''],'input':[5,' inset 0 0 0 1px',''],'input-focus':[10,' 0 0 10px -2px',''],'interactable-focus':[8,' 0 0 6px 1px',''],'tab-focus':[8,' 0 4px 6px -4px','']}}, accent_dark: {'c':{'accent-hover':10,'accent':9,'accent-primary-1':1,'accent-primary-2':5,'accent-primary-3':6,'accent-primary-4':7,'accent-primary-5':8},'s':{}}, accent_light: {'c':{'accent-hover':10,'accent':9,'accent-primary-1':1,'accent-primary-2':5,'accent-primary-3':6,'accent-primary-4':7,'accent-primary-5':8},'s':{}} diff --git a/src/sites/twitch-twilight/styles/color_normalizer.scss b/src/sites/twitch-twilight/styles/color_normalizer.scss index 709ca73f..28b3946a 100644 --- a/src/sites/twitch-twilight/styles/color_normalizer.scss +++ b/src/sites/twitch-twilight/styles/color_normalizer.scss @@ -35,6 +35,7 @@ .whispers-list-item:hover .whispers-list-item__archive-button:hover, .clmgr-table__row:hover, + .last-x-events_collapsed_item, .thread-header__title-bar-container.thread-header__title-bar-container--focused, .whispers-list-item:hover, .side-nav-card__link:hover { @@ -50,6 +51,11 @@ } } + .last-x-events_expanded_item_detail_icon, + .last-x-events_collapsed_item_detail { + background-color: var(--color-background-brand); + } + .top-stats-tab, .channel-header__user { color: var(--color-text-base) !important; From c924d643a6f08bdfc17f53d5ac904703ae5769cf Mon Sep 17 00:00:00 2001 From: SirStendec Date: Fri, 22 Jul 2022 19:52:08 -0400 Subject: [PATCH 15/63] 4.35.1 * Fixed: Chat on Videos not being rendered correctly, including name color preferences, emoji, emotes, etc. --- package.json | 2 +- .../modules/video_chat/index.jsx | 67 ++----------------- 2 files changed, 6 insertions(+), 63 deletions(-) diff --git a/package.json b/package.json index 9858397d..1ae8d958 100755 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "frankerfacez", "author": "Dan Salvato LLC", - "version": "4.35.0", + "version": "4.35.1", "description": "FrankerFaceZ is a Twitch enhancement suite.", "private": true, "license": "Apache-2.0", diff --git a/src/sites/twitch-twilight/modules/video_chat/index.jsx b/src/sites/twitch-twilight/modules/video_chat/index.jsx index 2be10899..f951bf92 100644 --- a/src/sites/twitch-twilight/modules/video_chat/index.jsx +++ b/src/sites/twitch-twilight/modules/video_chat/index.jsx @@ -53,7 +53,7 @@ export default class VideoChatHook extends Module { this.VideoChatController = this.fine.define( 'video-chat-controller', - n => n.onMessageScrollAreaMount && n.createReply, + n => n.onMessageScrollAreaMount && n.focusedCommentCallback, ['user-video', 'user-clip', 'video'] ); @@ -65,7 +65,7 @@ export default class VideoChatHook extends Module { this.VideoChatLine = this.fine.define( 'video-chat-line', - n => n.onReplyClickHandler && n.shouldFocusMessage, + n => n.onTimestampClickHandler && n.shouldFocusMessage, ['user-video', 'user-clip', 'video'] ); @@ -127,63 +127,6 @@ export default class VideoChatHook extends Module { if ( ! React ) return; - /*this.MessageMenu = class FFZMessageMenu extends React.Component { - constructor(props) { - super(props); - - this.onClick = () => this.setState({open: ! this.state.open}); - this.onClickOutside = () => this.state.open && this.setState({open: false}); - - this.element = null; - this.saveRef = element => this.element = element; - - this.state = { - open: false - } - } - - componentDidMount() { - if ( this.element ) - this._clicker = new ClickOutside(this.element, this.onClickOutside); - } - - componentWillUnmount() { - this._clicker.destroy(); - this._clicker = null; - } - - render() { - const is_open = this.state.open; - - return (
-
- -
-
-
-
-
- - -
- -
-
-
-
) - } - }*/ - const createElement = React.createElement, FFZRichContent = this.rich_content && this.rich_content.RichContent; @@ -404,7 +347,7 @@ export default class VideoChatHook extends Module { try { this._ffz_no_scan = true; - if ( this.state.showReplyForm || ! t.chat.context.get('chat.video-chat.enabled') ) + if ( this.state?.showReplyForm || ! t.chat.context.get('chat.video-chat.enabled') ) return old_render.call(this); const context = this.props.messageContext, @@ -441,7 +384,7 @@ export default class VideoChatHook extends Module {
{ main_message } { this.props.isExpandedLayout && this.ffzRenderExpanded(msg) } - { context.replies.length > 0 && (
+ { context.replies && context.replies.length > 0 && (
{ context.comment.moreReplies && (
+
); + } + + if ( ! had_action ) + return null; + + return (
+ {actions} +
); + } + + renderInline(msg, mod_icons, current_user, current_room, createElement, instance = null) { const actions = []; @@ -762,6 +901,8 @@ export default class Actions extends Module { if ( ! data.action || ! data.appearance ) continue; + data.ctx = 'inline'; + let ap = data.appearance || {}; const disp = data.display || {}, keys = disp.keys, @@ -781,6 +922,12 @@ export default class Actions extends Module { if ( maybe_call(act.hidden, this, data, msg, current_room, current_user, mod_icons, instance) ) continue; + if ( ap.type === 'dynamic' ) { + const out = act.dynamicAppearance && act.dynamicAppearance.call(this, Object.assign({}, ap), data, msg, current_room, current_user, mod_icons, instance); + if ( out ) + ap = out; + } + if ( act.override_appearance ) { const out = act.override_appearance.call(this, Object.assign({}, ap), data, msg, current_room, current_user, mod_icons, instance); if ( out ) @@ -804,7 +951,7 @@ export default class Actions extends Module { had_action = true; list.push(
-
+
@@ -458,7 +459,7 @@ import {has, maybe_call, deep_copy} from 'utilities/object'; let id = 0; export default { - props: ['action', 'data', 'inline', 'mod_icons', 'context', 'modifiers'], + props: ['vuectx', 'action', 'data', 'inline', 'mod_icons', 'context', 'modifiers', 'hover_modifier'], data() { return { @@ -494,6 +495,10 @@ export default { return this.modifiers }, + has_hover_modifier() { + return this.hover_modifier !== false + }, + fmts() { const out = []; @@ -691,7 +696,7 @@ export default { })); } - if ( disp.hover ) + if ( this.has_hover_modifier && disp.hover ) out.push(this.t('setting.actions.visible.hover', 'when hovering')); if ( ! out.length ) @@ -766,6 +771,29 @@ export default { this.edit_data = null; }, + maybeDynamic(data) { + let ap = data.appearance; + if (ap?.type === 'dynamic') { + const act = this.action_def, + ffz = this.vuectx.getFFZ(), + actions = ffz && ffz.resolve('chat.actions'); + + const out = actions && act?.dynamicAppearance && act.dynamicAppearance + .call(actions, deep_copy(ap), data, null, null, null, null); + if ( out ) + return Object.assign({}, data, {appearance: out}); + } + + return data; + }, + + supportsRenderer(key) { + if (key !== 'dynamic') + return true; + + return this.action_def?.supports_dynamic; + }, + getData() { const def = this.display && this.data.actions[this.display.action]; if ( ! def ) diff --git a/src/modules/main_menu/components/action-preview.vue b/src/modules/main_menu/components/action-preview.vue index 0599cb18..fdc05231 100644 --- a/src/modules/main_menu/components/action-preview.vue +++ b/src/modules/main_menu/components/action-preview.vue @@ -19,9 +19,13 @@ \ No newline at end of file diff --git a/src/modules/chat/actions/components/preview-emote.vue b/src/modules/chat/actions/components/preview-emote.vue new file mode 100644 index 00000000..7ee8b3d9 --- /dev/null +++ b/src/modules/chat/actions/components/preview-emote.vue @@ -0,0 +1,5 @@ + \ No newline at end of file diff --git a/src/modules/chat/actions/index.jsx b/src/modules/chat/actions/index.jsx index acc946bb..12099de8 100644 --- a/src/modules/chat/actions/index.jsx +++ b/src/modules/chat/actions/index.jsx @@ -38,6 +38,18 @@ export default class Actions extends Module { } }); + this.settings.add('chat.actions.hover-size', { + default: 30, + ui: { + path: 'Chat > Actions > Message Hover >> Appearance', + title: 'Action Size', + description: "How tall hover actions should be, in pixels. This may be affected by your browser's zoom and font size settings.", + component: 'setting-text-box', + process: 'to_int', + bounds: [1] + } + }); + this.settings.add('chat.actions.reasons', { default: [ {v: {text: 'One-Man Spam', i18n: 'chat.reasons.spam'}}, diff --git a/src/modules/chat/actions/renderers.jsx b/src/modules/chat/actions/renderers.jsx index 59a4d444..a8ecc3cb 100644 --- a/src/modules/chat/actions/renderers.jsx +++ b/src/modules/chat/actions/renderers.jsx @@ -37,6 +37,21 @@ export const text = { } } +// ============================================================================ +// Emote +// ============================================================================ + +export const emote = { + title: 'Emote', + title_i18n: 'setting.actions.appearance.emote', + + editor: () => import(/* webpackChunkName: 'main-menu' */ './components/edit-emote.vue'), + + component: () => import(/* webpackChunkName: 'main-menu' */ './components/preview-emote.vue'), + render(data, createElement) { + return
; + } +} // ============================================================================ // Icon diff --git a/src/modules/chat/actions/types.jsx b/src/modules/chat/actions/types.jsx index bce2458b..d0b214a5 100644 --- a/src/modules/chat/actions/types.jsx +++ b/src/modules/chat/actions/types.jsx @@ -46,6 +46,10 @@ export const pin = { if ( ! line.props.isPinnable || ! line.onPinMessageClick ) return true; + + // If the message is empty or deleted, we can't pin it. + if ( ! message.message || ! message.message.length || message.deleted ) + return true; }, click(event, data) { diff --git a/src/modules/chat/index.js b/src/modules/chat/index.js index 75ab4b1a..cca33809 100644 --- a/src/modules/chat/index.js +++ b/src/modules/chat/index.js @@ -20,6 +20,7 @@ import Room from './room'; import User from './user'; import * as TOKENIZERS from './tokenizers'; import * as RICH_PROVIDERS from './rich_providers'; +import * as LINK_PROVIDERS from './link_providers'; import Actions from './actions'; import { getFontsList } from 'src/utilities/fonts'; @@ -99,6 +100,9 @@ export default class Chat extends Module { this.rich_providers = {}; this.__rich_providers = []; + this.link_providers = {}; + this.__link_providers = []; + this._hl_reasons = {}; this.addHighlightReason('mention', 'Mentioned'); this.addHighlightReason('user', 'Highlight User'); @@ -1241,6 +1245,8 @@ export default class Chat extends Module { onEnable() { this.socket = this.resolve('socket'); + this.on('site.subpump:pubsub-message', this.onPubSub, this); + if ( this.context.get('chat.filtering.color-mentions') ) this.createColorCache().then(() => this.emit(':update-line-tokens')); @@ -1251,6 +1257,47 @@ export default class Chat extends Module { for(const key in RICH_PROVIDERS) if ( has(RICH_PROVIDERS, key) ) this.addRichProvider(RICH_PROVIDERS[key]); + + for(const key in LINK_PROVIDERS) + if ( has(LINK_PROVIDERS, key) ) + this.addLinkProvider(LINK_PROVIDERS[key]); + } + + + onPubSub(event) { + if ( event.prefix === 'stream-chat-room-v1' && event.message.type === 'chat_rich_embed' ) { + const data = event.message.data, + url = data.request_url, + + providers = this.__link_providers; + + // Don't re-cache. + if ( this._link_info[url] ) + return; + + for(const provider of providers) { + const match = provider.test.call(this, url); + if ( match ) { + const processed = provider.receive ? provider.receive.call(this, match, data) : data; + let result = provider.process.call(this, match, processed); + + if ( !(result instanceof Promise) ) + result = Promise.resolve(result); + + result.then(value => { + // If something is already running, don't override it. + let info = this._link_info[url]; + if ( info ) + return; + + // Save the value. + this._link_info[url] = [true, Date.now() + 120000, value]; + }); + + return; + } + } + } } @@ -1855,6 +1902,11 @@ export default class Chat extends Module { addTokenizer(tokenizer) { const type = tokenizer.type; + if ( has(this.tokenizers, type) ) { + this.log.warn(`Tried adding tokenizer of type '${type}' when one was already present.`); + return; + } + this.tokenizers[type] = tokenizer; if ( tokenizer.priority == null ) tokenizer.priority = 0; @@ -1894,8 +1946,48 @@ export default class Chat extends Module { return tokenizer; } + addLinkProvider(provider) { + const type = provider.type; + if ( has(this.link_providers, type) ) { + this.log.warn(`Tried adding link provider of type '${type}' when one was already present.`); + return; + } + + this.link_providers[type] = provider; + if ( provider.priority == null ) + provider.priority = 0; + + this.__link_providers.push(provider); + this.__link_providers.sort((a,b) => { + if ( a.priority > b.priority ) return -1; + if ( a.priority < b.priority ) return 1; + return a.type < b.type; + }); + } + + removeLinkProvider(provider) { + let type; + if ( typeof provider === 'string' ) type = provider; + else type = provider.type; + + provider = this.link_providers[type]; + if ( ! provider ) + return null; + + const idx = this.__link_providers.indexOf(provider); + if ( idx !== -1 ) + this.__link_providers.splice(idx, 1); + + return provider; + } + addRichProvider(provider) { const type = provider.type; + if ( has(this.rich_providers, type) ) { + this.log.warn(`Tried adding rich provider of type '${type}' when one was already present.`); + return; + } + this.rich_providers[type] = provider; if ( provider.priority == null ) provider.priority = 0; @@ -2108,6 +2200,17 @@ export default class Chat extends Module { cbs[success ? 0 : 1](data); } + // Try using a link provider. + for(const lp of this.__link_providers) { + const match = lp.test.call(this, url); + if ( match ) { + timeout(lp.process.call(this, match), 15000) + .then(data => handle(true, data)) + .catch(err => handle(false, err)); + return; + } + } + let provider = this.settings.get('debug.link-resolver.source'); if ( provider == null ) provider = this.experiments.getAssignment('api_links') ? 'test' : 'socket'; diff --git a/src/modules/chat/link_providers.js b/src/modules/chat/link_providers.js new file mode 100644 index 00000000..0173e134 --- /dev/null +++ b/src/modules/chat/link_providers.js @@ -0,0 +1,459 @@ +'use strict'; + +// ============================================================================ +// Rich Content Providers +// ============================================================================ + +const CLIP_URL = /^(?:https?:\/\/)?clips\.twitch\.tv\/([a-z0-9-_=]+)(?:\/)?(\w+)?(?:\/edit)?/i; +const NEW_CLIP_URL = /^(?:https?:\/\/)?(?:(?:www|m)\.)?twitch\.tv\/\w+\/clip\/([a-z0-9-_=]+)/i; +const VIDEO_URL = /^(?:https?:\/\/)?(?:www\.)?twitch\.tv\/(?:\w+\/v|videos)\/(\w+)/; +const USER_URL = /^(?:https?:\/\/)?(?:www\.)?twitch\.tv\/([^/]+)$/; + +const BAD_USERS = [ + 'directory', '_deck', 'p', 'downloads', 'jobs', 'turbo', 'settings', 'friends', + 'subscriptions', 'inventory', 'wallet' +]; + +import GET_CLIP from './clip_info.gql'; +import GET_VIDEO from './video_info.gql'; + + +// ============================================================================ +// Clips +// ============================================================================ + +export const Clip = { + type: 'clip', + + test(url) { + const match = CLIP_URL.exec(url) || NEW_CLIP_URL.exec(url); + if ( match && match[1] && match[1] !== 'create' ) + return match[1]; + }, + + receive(match, data) { + const cd = data?.twitch_metadata?.clip_metadata; + if ( ! cd ) + return; + + return { + id: cd.id, + slug: cd.slug, + title: data.title, + thumbnailURL: data.thumbnail_url, + curator: { + id: cd.curator_id, + displayName: data.author_name + }, + broadcaster: { + id: cd.broadcaster_id, + displayName: cd.channel_display_name + }, + game: { + displayName: cd.game + } + } + }, + + async process(match, received) { + let clip = received; + + if ( ! clip ) { + const apollo = this.resolve('site.apollo'); + if ( ! apollo ) + return null; + + const result = await apollo.client.query({ + query: GET_CLIP, + variables: { + slug: match + } + }); + + clip = result?.data?.clip; + } + + if ( ! clip || ! clip.broadcaster ) + return null; + + const game = clip.game, + game_display = game && game.displayName; + + let user = { + type: 'style', weight: 'semibold', color: 'alt-2', + content: clip.broadcaster.displayName + }; + + if ( clip.broadcaster.login ) + user = { + type: 'link', url: `https://www.twitch.tv/${clip.broadcaster.login}`, + content: user + }; + + const subtitle = game_display ? { + type: 'i18n', key: 'clip.desc.1.playing', phrase: '{user} playing {game}', content: { + user, + game: {type: 'style', weight: 'semibold', content: game_display} + } + } : {type: 'i18n', key: 'clip.desc.1', phrase: 'Clip of {user}', content: {user}}; + + let curator = clip.curator ? { + type: 'style', color: 'alt-2', + content: clip.curator.displayName + } : {type: 'i18n', key: 'clip.unknown', phrase: 'Unknown'}; + + if ( clip.curator?.login ) + curator = { + type: 'link', url: `https://www.twitch.tv/${clip.curator.login}`, + content: curator + }; + + let extra; + + if ( clip.viewCount > 0 ) + extra = { + type: 'i18n', key: 'clip.desc.2', + phrase: 'Clipped by {curator} — {views, plural, one {# View} other {# Views}}', + content: { + curator, + views: clip.viewCount + } + }; + else + extra = { + type: 'i18n', key: 'clip.desc.no-views', + phrase: 'Clipped by {curator}', + content: { + curator + } + }; + + return { + accent: '#6441a4', + + short: { + type: 'header', + image: {type: 'image', url: clip.thumbnailURL, sfw: true, aspect: 16/9}, + title: clip.title, + subtitle, + extra + } + }; + } +} + + +// ============================================================================ +// Users +// ============================================================================ + +export const User = { + type: 'user', + + test(url) { + const match = USER_URL.exec(url); + if ( match && ! BAD_USERS.includes(match[1]) ) + return match[1]; + }, + + async process(match) { + const twitch_data = this.resolve('site.twitch_data'), + user = twitch_data ? await twitch_data.getUser(null, match) : null; + + if ( ! user || ! user.id ) + return null; + + const game = user.broadcastSettings?.game?.displayName, + stream_id = user.stream?.id; + + const fragments = { + avatar: { + type: 'image', + url: user.profileImageURL, + rounding: -1, + aspect: 1 + }, + desc: user.description, + title: [user.displayName] + }; + + if ( stream_id && game ) + fragments.game = {type: 'style', weight: 'semibold', content: game}; + + if ( user.displayName.trim().toLowerCase() !== user.login ) + fragments.title.push({ + type: 'style', color: 'alt-2', + content: [' (', user.login, ')'] + }); + + if ( user.roles?.isPartner ) + fragments.title.push({ + type: 'style', color: 'link', + content: {type: 'icon', name: 'verified'} + }); + + const full = [ + { + type: 'header', + image: {type: 'ref', name: 'avatar'}, + title: {type: 'ref', name: 'title'}, + }, + { + type: 'box', + 'mg-y': 'small', + wrap: 'pre-wrap', + lines: 5, + content: { + type: 'ref', + name: 'desc' + } + } + ]; + + if ( stream_id && game ) { + const thumb_url = user.stream.previewImageURL + ? user.stream.previewImageURL + .replace('{width}', '320') + .replace('{height}', '180') + : null; + + full.push({ + type: 'link', + url: `https://www.twitch.tv/${user.login}`, + embed: true, + interactive: true, + tooltip: false, + content: [ + { + type: 'conditional', + media: true, + content: { + type: 'gallery', + items: [ + { + type: 'image', + url: thumb_url, + aspect: 16/9 + } + ] + } + }, + { + type: 'box', + 'mg-y': 'small', + lines: 2, + content: user.broadcastSettings.title + }, + { + type: 'ref', + name: 'game' + } + ] + }); + } + + full.push({ + type: 'header', + compact: true, + subtitle: [ + { + type: 'icon', + name: 'twitch' + }, + ' Twitch' + ] + }); + + return { + v: 5, + + accent: user.primaryColorHex ? `#${user.primaryColorHex}` : null, + fragments, + + short: { + type: 'header', + image: {type: 'ref', name: 'avatar'}, + title: {type: 'ref', name: 'title'}, + subtitle: {type: 'ref', name: 'desc'}, + extra: stream_id ? { + type: 'i18n', + key: 'cards.user.streaming', + phrase: 'streaming {game}', + content: { + game: {type: 'ref', name: 'game'} + } + } : null + }, + + full + } + } + +} + + +// ============================================================================ +// Videos +// ============================================================================ + +export const Video = { + type: 'video', + + test(url) { + const match = VIDEO_URL.exec(url); + if ( match ) + return match[1]; + }, + + async process(match) { + const apollo = this.resolve('site.apollo'); + if ( ! apollo ) + return null; + + const result = await apollo.client.query({ + query: GET_VIDEO, + variables: { + id: match + } + }); + + if ( ! result || ! result.data || ! result.data.video || ! result.data.video.owner ) + return null; + + const video = result.data.video, + game = video.game, + game_display = game && game.displayName; + + const fragments = { + title: video.title, + thumbnail: { + type: 'image', + url: video.previewThumbnailURL, + aspect: 16/9 + } + }; + + const user = { + type: 'link', + url: `https://www.twitch.tv/${video.owner.login}`, + content: { + type: 'style', + weight: 'semibold', + color: 'alt-2', + content: video.owner.displayName + } + }; + + fragments.subtitle = video.game?.displayName + ? { + type: 'i18n', + key: 'video.desc.1.playing', + phrase: 'Video of {user} playing {game}', + content: { + user, + game: { + type: 'style', + weight: 'semibold', + content: video.game.displayName + } + } + } + : { + type: 'i18n', + key: 'video.desc.1', + phrase: 'Video of {user}', + content: { + user + } + }; + + let length = video.lengthSeconds; + + return { + v: 5, + + fragments, + + short: { + type: 'header', + image: {type: 'ref', name: 'thumbnail'}, + title: {type: 'ref', name: 'title'}, + subtitle: {type: 'ref', name: 'subtitle'}, + extra: { + type: 'i18n', + key: 'video.desc.2', + phrase: '{length,duration} — {views,number} Views — {date,datetime}', + content: { + length, + views: video.viewCount, + date: video.publishedAt + } + } + }, + + full: [ + { + type: 'header', + image: { + type: 'image', + url: video.owner.profileImageURL, + rounding: -1, + aspect: 1 + }, + title: {type: 'ref', name: 'title'}, + subtitle: {type: 'ref', name: 'subtitle'} + }, + { + type: 'box', + 'mg-y': 'small', + lines: 5, + wrap: 'pre-wrap', + content: video.description + }, + { + type: 'conditional', + media: true, + content: { + type: 'gallery', + items: [ + { + type: 'overlay', + content: {type: 'ref', name: 'thumbnail'}, + 'top-left': { + type: 'format', + format: 'duration', + value: length + }, + 'bottom-left': { + type: 'i18n', + key: 'video.views', + phrase: '{views,number} views', + content: { + views: video.viewCount + } + } + } + ] + } + }, + { + type: 'header', + compact: true, + subtitle: [ + { + type: 'icon', + name: 'twitch' + }, + " Twitch • ", + { + type: 'format', + format: 'datetime', + value: video.publishedAt + } + ] + } + ] + }; + } + +} \ No newline at end of file diff --git a/src/modules/chat/rich_providers.js b/src/modules/chat/rich_providers.js index 7af6a449..5539c7ef 100644 --- a/src/modules/chat/rich_providers.js +++ b/src/modules/chat/rich_providers.js @@ -4,24 +4,6 @@ // Rich Content Providers // ============================================================================ -//const CLIP_URL = /^(?:https?:\/\/)?clips\.twitch\.tv\/(\w+)(?:\/)?(\w+)?(?:\/edit)?/; -//const NEW_CLIP_URL = /^(?:https?:\/\/)?(?:www\.)?twitch\.tv\/\w+\/clip\/(\w+)/; -const CLIP_URL = /^(?:https?:\/\/)?clips\.twitch\.tv\/([a-z0-9-_=]+)(?:\/)?(\w+)?(?:\/edit)?/i; -const NEW_CLIP_URL = /^(?:https?:\/\/)?(?:(?:www|m)\.)?twitch\.tv\/\w+\/clip\/([a-z0-9-_=]+)/i; -const VIDEO_URL = /^(?:https?:\/\/)?(?:www\.)?twitch\.tv\/(?:\w+\/v|videos)\/(\w+)/; -const USER_URL = /^(?:https?:\/\/)?(?:www\.)?twitch\.tv\/([^/]+)$/; - -const BAD_USERS = [ - 'directory', '_deck', 'p', 'downloads', 'jobs', 'turbo', 'settings', 'friends', - 'subscriptions', 'inventory', 'wallet' -]; - -import GET_CLIP from './clip_info.gql'; -import GET_VIDEO from './video_info.gql'; - -import {truncate} from 'utilities/object'; - - // ============================================================================ // General Links // ============================================================================ @@ -32,10 +14,18 @@ export const Links = { priority: -10, test(token) { - if ( ! this.context.get('chat.rich.all-links') && ! token.force_rich ) + if ( token.type !== 'link' ) return false; - return token.type === 'link' + const url = token.url; + + // Link providers always result in embeds. + for(const provider of this.__link_providers) { + if ( provider.test.call(this, url) ) + return true; + } + + return this.context.get('chat.rich.all-links') || token.force_rich; }, process(token, want_mid) { @@ -70,279 +60,3 @@ export const Links = { } } } - - -// ============================================================================ -// Users -// ============================================================================ - -export const Users = { - type: 'user', - can_hide_token: true, - - test(token) { - if ( token.type !== 'link' || (! this.context.get('chat.rich.all-links') && ! token.force_rich) ) - return false; - - return USER_URL.test(token.url); - }, - - process(token) { - const match = USER_URL.exec(token.url), - twitch_data = this.resolve('site.twitch_data'); - - if ( ! twitch_data || ! match || BAD_USERS.includes(match[1]) ) - return; - - return { - url: token.url, - - getData: async () => { - const user = await twitch_data.getUser(null, match[1]); - if ( ! user || ! user.id ) - return null; - - const game = user.broadcastSettings?.game?.displayName, - stream_id = user.stream?.id; - - let subtitle - if ( stream_id && game ) - subtitle = { - type: 'i18n', - key: 'cards.user.streaming', phrase: 'streaming {game}', content: { - game: {type: 'style', weight: 'semibold', content: game} - } - }; - - const extra = truncate(user.description); - const title = [user.displayName]; - - if ( user.displayName.trim().toLowerCase() !== user.login ) - title.push({ - type: 'style', color: 'alt-2', - content: [' (', user.login, ')'] - }); - - if ( user.roles?.isPartner ) - title.push({ - type: 'style', color: 'link', - content: {type: 'icon', name: 'verified'} - }); - - /*const full = [{ - type: 'header', - image: {type: 'image', url: user.profileImageURL, rounding: -1, aspect: 1}, - title, - subtitle, - extra: stream_id ? extra : null - }]; - - if ( stream_id ) { - full.push({type: 'box', 'mg-y': 'small', lines: 1, content: user.broadcastSettings.title}); - full.push({type: 'conditional', content: { - type: 'gallery', items: [{ - type: 'image', aspect: 16/9, sfw: false, url: user.stream.previewImageURL - }] - }}); - } else - full.push({type: 'box', 'mg-y': 'small', wrap: 'pre-wrap', lines: 5, content: truncate(user.description, 1000, undefined, undefined, false)}) - - full.push({ - type: 'fieldset', - fields: [ - { - name: {type: 'i18n', key: 'embed.twitch.views', phrase: 'Views'}, - value: {type: 'format', format: 'number', value: user.profileViewCount}, - inline: true - }, - { - name: {type: 'i18n', key: 'embed.twitch.followers', phrase: 'Followers'}, - value: {type: 'format', format: 'number', value: user.followers?.totalCount}, - inline: true - } - ] - }); - - full.push({ - type: 'header', - subtitle: [{type: 'icon', name: 'twitch'}, ' Twitch'] - });*/ - - return { - url: token.url, - accent: user.primaryColorHex ? `#${user.primaryColorHex}` : null, - short: { - type: 'header', - image: {type: 'image', url: user.profileImageURL, rounding: -1, aspect: 1}, - title, - subtitle, - extra - } - } - } - } - } -} - - -// ============================================================================ -// Clips -// ============================================================================ - -export const Clips = { - type: 'clip', - can_hide_token: true, - - test(token) { - if ( token.type !== 'link' ) - return false; - - return CLIP_URL.test(token.url) || NEW_CLIP_URL.test(token.url); - }, - - process(token) { - let match = CLIP_URL.exec(token.url); - if ( ! match ) - match = NEW_CLIP_URL.exec(token.url); - - const apollo = this.resolve('site.apollo'); - if ( ! apollo || ! match || match[1] === 'create' ) - return; - - return { - url: token.url, - - getData: async () => { - const result = await apollo.client.query({ - query: GET_CLIP, - variables: { - slug: match[1] - } - }); - - if ( ! result || ! result.data || ! result.data.clip || ! result.data.clip.broadcaster ) - return null; - - const clip = result.data.clip, - game = clip.game, - game_display = game && game.displayName; - - const user = { - type: 'link', url: `https://www.twitch.tv/${clip.broadcaster.login}`, - content: { - type: 'style', weight: 'semibold', color: 'alt-2', - content: clip.broadcaster.displayName - } - }; - - const subtitle = game_display ? { - type: 'i18n', key: 'clip.desc.1.playing', phrase: '{user} playing {game}', content: { - user, - game: {type: 'style', weight: 'semibold', content: game_display} - } - } : {type: 'i18n', key: 'clip.desc.1', phrase: 'Clip of {user}', content: {user}}; - - const curator = clip.curator ? { - type: 'link', url: `https://www.twitch.tv/${clip.curator.login}`, - content: { - type: 'style', color: 'alt-2', - content: clip.curator.displayName - } - } : {type: 'i18n', key: 'clip.unknown', phrase: 'Unknown'}; - - const extra = { - type: 'i18n', key: 'clip.desc.2', - phrase: 'Clipped by {curator} — {views, plural, one {# View} other {# Views}}', - content: { - curator, - views: clip.viewCount - } - }; - - return { - url: token.url, - accent: '#6441a4', - - short: { - type: 'header', - image: {type: 'image', url: clip.thumbnailURL, sfw: true, aspect: 16/9}, - title: clip.title, - subtitle, - extra - } - } - } - } - } -} - - -export const Videos = { - type: 'video', - can_hide_token: true, - - test(token) { - return token.type === 'link' && VIDEO_URL.test(token.url) - }, - - process(token) { - const match = VIDEO_URL.exec(token.url), - apollo = this.resolve('site.apollo'); - - if ( ! apollo || ! match ) - return; - - return { - getData: async () => { - const result = await apollo.client.query({ - query: GET_VIDEO, - variables: { - id: match[1] - } - }); - - if ( ! result || ! result.data || ! result.data.video || ! result.data.video.owner ) - return null; - - const video = result.data.video, - game = video.game, - game_display = game && game.displayName; - - const user = { - type: 'link', url: `https://www.twitch.tv/${video.owner.login}`, - content: { - type: 'style', weight: 'semibold', color: 'alt-2', - content: video.owner.displayName - } - }; - - const subtitle = game_display ? { - type: 'i18n', key: 'clip.desc.1.playing', phrase: '{user} playing {game}', content: { - user, - game: {type: 'style', weight: 'semibold', content: game_display} - } - } : {type: 'i18n', key: 'video.desc.1', phrase: 'Video of {user}', content: {user}}; - - const extra = { - type: 'i18n', key: 'video.desc.2', - phrase: '{length,duration} — {views,number} Views — {date,datetime}', content: { - length: video.lengthSeconds, - views: video.viewCount, - date: video.publishedAt - } - }; - - return { - url: token.url, - short: { - type: 'header', - image: {type: 'image', url: video.previewThumbnailURL, sfw: true, aspect: 16/9}, - title: video.title, - subtitle, - extra - } - }; - } - } - } -} \ No newline at end of file diff --git a/src/modules/chat/video_info.gql b/src/modules/chat/video_info.gql index 04fe9f45..55fcd5d5 100644 --- a/src/modules/chat/video_info.gql +++ b/src/modules/chat/video_info.gql @@ -2,7 +2,7 @@ query FFZ_GetVideoInfo($id: ID!) { video(id: $id) { id title - previewThumbnailURL(width: 86, height: 45) + previewThumbnailURL(width: 320, height: 180) lengthSeconds publishedAt viewCount @@ -14,6 +14,7 @@ query FFZ_GetVideoInfo($id: ID!) { id login displayName + profileImageURL(width: 50) } } } \ No newline at end of file diff --git a/src/modules/main_menu/components/chat-tester.vue b/src/modules/main_menu/components/chat-tester.vue new file mode 100644 index 00000000..b9871109 --- /dev/null +++ b/src/modules/main_menu/components/chat-tester.vue @@ -0,0 +1,699 @@ +