diff --git a/package.json b/package.json index cf0068b1..dcb6e203 100755 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "frankerfacez", "author": "Dan Salvato LLC", - "version": "4.50.0", + "version": "4.51.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 98635679..fcaec38e 100644 --- a/src/modules/chat/index.js +++ b/src/modules/chat/index.js @@ -900,6 +900,16 @@ export default class Chat extends Module { } }); + this.settings.add('chat.filtering.all-mentions', { + default: false, + ui: { + component: 'setting-check-box', + path: 'Chat > Filtering > General >> Appearance', + title: 'Display mentions for all users without requiring an at sign (@).', + description: '**Note**: This setting can increase memory usage and impact chat performance.' + } + }); + this.settings.add('chat.filtering.color-mentions', { default: false, ui: { @@ -910,6 +920,13 @@ export default class Chat extends Module { } }); + this.settings.add('chat.filtering.need-colors', { + requires: ['chat.filtering.all-mentions' ,'chat.filtering.color-mentions'], + process(ctx) { + return ctx.get('chat.filtering.all-mentions') || ctx.get('chat.filtering.color-mentions') + } + }); + this.settings.add('chat.filtering.bold-mentions', { default: true, ui: { @@ -1218,7 +1235,7 @@ export default class Chat extends Module { room.buildBitsCSS(); }); - this.context.on('changed:chat.filtering.color-mentions', async val => { + this.context.on('changed:chat.filtering.need-colors', async val => { if ( val ) await this.createColorCache(); else @@ -1226,6 +1243,9 @@ export default class Chat extends Module { this.emit(':update-line-tokens'); }); + + this.context.on('changed:chat.filtering.all-mentions', () => this.emit(':update-line-tokens')); + this.context.on('changed:chat.filtering.color-mentions', () => this.emit(':update-line-tokens')); } @@ -1249,7 +1269,7 @@ export default class Chat extends Module { this.on('site.subpump:pubsub-message', this.onPubSub, this); - if ( this.context.get('chat.filtering.color-mentions') ) + if ( this.context.get('chat.filtering.need-colors') ) this.createColorCache().then(() => this.emit(':update-line-tokens')); for(const key in TOKENIZERS) diff --git a/src/modules/chat/tokenizers.jsx b/src/modules/chat/tokenizers.jsx index 52d1cabb..ed0e412a 100644 --- a/src/modules/chat/tokenizers.jsx +++ b/src/modules/chat/tokenizers.jsx @@ -313,6 +313,81 @@ export const Replies = { // Mentions // ============================================================================ +function mention_processAll(tokens, msg, user, color_mentions) { + const can_highlight_user = user && user.login && user.login == msg.user.login && ! this.context.get('chat.filtering.process-own'), + priority = this.context.get('chat.filtering.mention-priority'); + + let login, display, mentionable = false; + if ( user && user.login && ! can_highlight_user ) { + login = user.login.toLowerCase(); + display = user.displayName && user.displayName.toLowerCase(); + if ( display === login ) + display = null; + + mentionable = true; + } + + const out = []; + for(const token of tokens) { + if ( token.type !== 'text' ) { + out.push(token); + continue; + } + + let text = []; + + for(const segment of token.text.split(/ +/)) { + const match = /^(@?)(\S+?)(?:\b|$)/.exec(segment); + if ( match ) { + let recipient = match[2], + has_at = match[1] === '@', + mentioned = false; + + const rlower = recipient ? recipient.toLowerCase() : '', + color = this.color_cache ? this.color_cache.get(rlower) : null; + + if ( rlower === login || rlower === display ) + mentioned = true; + + if ( ! has_at && ! color && ! mentioned ) { + text.push(segment); + + } else { + // If we have pending text, join it together. + if ( text.length ) { + out.push({ + type: 'text', + text: `${text.join(' ')} ` + }); + text = []; + } + + out.push({ + type: 'mention', + text: match[0], + me: mentioned, + color: color_mentions ? color : null, + recipient: rlower + }); + + if ( mentioned ) + this.applyHighlight(msg, priority, null, 'mention', true); + + // Push the remaining text from the token. + text.push(segment.substr(match[0].length)); + } + + } else + text.push(segment); + } + + if ( text.length > 1 || (text.length === 1 && text[0] !== '') ) + out.push({type: 'text', text: text.join(' ')}) + } + + return out; +} + export const Mentions = { type: 'mention', priority: 0, @@ -346,6 +421,12 @@ export const Mentions = { if ( ! tokens || ! tokens.length ) return; + const all_mentions = this.context.get('chat.filtering.all-mentions'), + color_mentions = this.context.get('chat.filtering.color-mentions'); + + if ( all_mentions ) + return mention_processAll.call(this, tokens, msg, user, color_mentions); + const can_highlight_user = user && user.login && user.login == msg.user.login && ! this.context.get('chat.filtering.process-own'), priority = this.context.get('chat.filtering.mention-priority'); @@ -396,7 +477,7 @@ export const Mentions = { } const rlower = recipient ? recipient.toLowerCase() : '', - color = this.color_cache ? this.color_cache.get(rlower) : null; + color = (color_mentions && this.color_cache) ? this.color_cache.get(rlower) : null; out.push({ type: 'mention', diff --git a/src/modules/main_menu/components/chat-tester.vue b/src/modules/main_menu/components/chat-tester.vue index 56d3bdde..4147e3c1 100644 --- a/src/modules/main_menu/components/chat-tester.vue +++ b/src/modules/main_menu/components/chat-tester.vue @@ -650,10 +650,12 @@ export default { replayItem(item) { if ( item.pubsub ) { - const channel = this.chat.ChatService.first?.props?.channelID; + const channel = this.chat.ChatService.first?.props?.channelID, + user = this.chat.resolve('site').getUser(); if ( this.replay_fix ) { item.topic = item.topic.replace(//gi, channel); + item.topic = item.topic.replace(//gi, user.id); // TODO: Crawl, replacing ids. // TODO: Update timestamps for pinned chat? } diff --git a/src/modules/main_menu/sample-chat-messages.json b/src/modules/main_menu/sample-chat-messages.json index ce132e51..015e627e 100644 --- a/src/modules/main_menu/sample-chat-messages.json +++ b/src/modules/main_menu/sample-chat-messages.json @@ -38,5 +38,10 @@ "name": "Hype Chat (PubSub)", "topic": "pinned-chat-updates-v1.", "data": {"type":"pin-message","data":{"id":"deea93ac-66c7-4500-aa38-d9cfb82f14bc","pinned_by":{"id":"128900149","display_name":"JailBreakRules"},"message":{"id":"deea93ac-66c7-4500-aa38-d9cfb82f14bc","sender":{"id":"128900149","display_name":"JailBreakRules","badges":[{"id":"subscriber","version":"12"},{"id":"glhf-pledge","version":"1"}],"chat_color":"#F31995"},"content":{"text":"xQc gets 70 cents off this lmfao","fragments":[{"text":"xQc gets 70 cents off this lmfao"}]},"type":"PAID","starts_at":1687737336,"updated_at":1687737336,"ends_at":1687737366,"sent_at":1687737336,"metadata":{"amount":"100","canonical-amount":"100","currency":"USD","exponent":"2","isSystemMessage":"false","level":"ONE"}}}} + }, + { + "name": "Drop Claim Reward (PubSub)", + "topic": "user-drop-events.", + "data": {"type":"drop-claim","data":{"drop_instance_id":"9f21b210-63b0-4725-be46-b8e49207f533","drop_id":"4da46d69-269e-4709-baf0-1dc62dcf39b2","channel_id":"118336478"}} } ] \ No newline at end of file diff --git a/src/modules/metadata.jsx b/src/modules/metadata.jsx index 4f4a9676..62753d3e 100644 --- a/src/modules/metadata.jsx +++ b/src/modules/metadata.jsx @@ -351,6 +351,7 @@ export default class Metadata extends Module { videoWidth, displayHeight, displayWidth, + buffered: maybe_call(player.getBufferDuration, player) || -1, rate: maybe_call(player.getPlaybackRate, player), fps: Math.floor(maybe_call(player.getVideoFrameRate, player) || 0), hlsLatencyBroadcaster: maybe_call(player.getLiveLatency, player) || 0, @@ -499,12 +500,22 @@ export default class Metadata extends Module { stats ); - const desync = data.avOffset !== 0 + const desync = /*data.avOffset !== 0 ? (
{this.i18n.t( 'metadata.player-stats.av-offset', 'A/V Offset: {avOffset, number} seconds', stats )}
) + :*/ null; + + const buffer = stats.buffered > 0 + ? (
{this.i18n.t( + 'metadata.player-stats.buffered', + 'Buffered: {buffered} seconds', + { + buffered: stats.buffered.toFixed(2) + } + )}
) : null; if ( data.old ) @@ -525,6 +536,7 @@ export default class Metadata extends Module { {video_info} , desync, + buffer, tampered ]; @@ -538,6 +550,7 @@ export default class Metadata extends Module { {video_info} , desync, + buffer, tampered ]; } diff --git a/src/sites/twitch-twilight/modules/chat/index.js b/src/sites/twitch-twilight/modules/chat/index.js index 1c3cc614..467f2681 100644 --- a/src/sites/twitch-twilight/modules/chat/index.js +++ b/src/sites/twitch-twilight/modules/chat/index.js @@ -190,6 +190,12 @@ export default class ChatHook extends Module { this.inject(Input); this.inject(ViewerCards); + this.ChatLeaderboard = this.fine.define( + 'chat-leaderboard', + n => n.props?.topItems && has(n.props, 'forceMiniView') && has(n.props, 'leaderboardType'), + Twilight.CHAT_ROUTES + ); + this.ChatService = this.fine.define( 'chat-service', n => n.join && n.client && n.props.setChatConnectionAPI, @@ -399,7 +405,7 @@ export default class ChatHook extends Module { this.settings.add('chat.banners.drops', { default: true, ui: { - path: 'Chat > Appearance >> Community', + path: 'Chat > Drops >> Appearance', title: 'Allow messages about Drops to be displayed in chat.', component: 'setting-check-box' } @@ -490,6 +496,15 @@ export default class ChatHook extends Module { } }); + this.settings.add('chat.drops.auto-rewards', { + default: false, + ui: { + path: 'Chat > Drops >> Behavior', + title: 'Automatically claim drops.', + component: 'setting-check-box', + } + }); + this.settings.add('chat.pin-resubs', { default: false, ui: { @@ -1022,8 +1037,8 @@ export default class ChatHook extends Module { this.chat.context.getChanges('chat.bits.show', val => this.css_tweaks.toggle('hide-bits', !val)); - this.chat.context.getChanges('chat.bits.show-pinned', val => - this.css_tweaks.toggleHide('pinned-cheer', !val)); + this.chat.context.on('changed:chat.bits.show-pinned', () => + this.ChatLeaderboard.forceUpdate()); this.chat.context.getChanges('chat.filtering.deleted-style', val => { this.css_tweaks.toggle('chat-deleted-strike', val === 1 || val === 2); @@ -1108,6 +1123,18 @@ export default class ChatHook extends Module { this.PointsInfo.on('unmount', () => this.updatePointsInfo(null)); this.PointsInfo.ready(() => this.updatePointsInfo(this.PointsInfo.first)); + this.ChatLeaderboard.ready(cls => { + const old_render = cls.prototype.render; + cls.prototype.render = function() { + if ( ! t.chat.context.get('chat.bits.show-pinned') ) + return null; + + return old_render.call(this); + } + + this.ChatLeaderboard.forceUpdate(); + }); + this.GiftBanner.ready(cls => { const old_render = cls.prototype.render; cls.prototype.render = function() { @@ -1180,6 +1207,12 @@ export default class ChatHook extends Module { if ( (ctype === 'mega-recipient-rewards' || ctype === 'mega-benefactor-rewards') && ! t.chat.context.get('chat.bits.show-rewards') ) return null; + if ( ctype === 'drop' && ! this._ffz_auto_drop && t.chat.context.get('chat.drops.auto-rewards') ) + this._ffz_auto_drop = setTimeout(() => { + this._ffz_auto_drop = null; + t.autoClickDrop(this); + }, 250); + } catch(err) { t.log.capture(err); t.log.error(err); @@ -1478,6 +1511,23 @@ export default class ChatHook extends Module { } + autoClickDrop(inst) { + const callout = inst.props?.callouts?.[0] || inst.props?.pinnedCallout, + ctype = callout?.event?.type; + + if ( ctype !== 'drop' || ! this.chat.context.get('chat.drops.auto-rewards') ) + return; + + const node = this.fine.getHostNode(inst), + btn = node.querySelector('button[data-a-target="chat-private-callout__primary-button"]'); + + if ( ! btn ) + return; + + btn.click(); + } + + wrapRaidController(inst) { if ( inst._ffz_wrapped ) return this.noAutoRaids(inst); diff --git a/src/sites/twitch-twilight/modules/css_tweaks/index.js b/src/sites/twitch-twilight/modules/css_tweaks/index.js index a959df33..d79ddf23 100644 --- a/src/sites/twitch-twilight/modules/css_tweaks/index.js +++ b/src/sites/twitch-twilight/modules/css_tweaks/index.js @@ -31,6 +31,8 @@ const CLASSES = { 'prime-offers': '.top-nav__prime', 'discover-luna': '.top-nav__external-link[data-a-target="try-presto-link"]', + 'subtember': '.subtember-gradient', + 'player-gain-volume': '.video-player__container[data-compressed="true"] .volume-slider__slider-container:not(.ffz--player-gain)', 'player-ext': '.video-player .extension-taskbar,.video-player .extension-container,.video-player .extensions-dock__layout,.video-player .extensions-notifications,.video-player .extensions-video-overlay-size-container,.video-player .extensions-dock__layout', @@ -318,6 +320,15 @@ export default class CSSTweaks extends Module { } }); + this.settings.add('layout.subtember', { + default: true, + ui: { + path: 'Appearance > Layout >> Channel', + title: 'Allow the Subtember upsell banner to appear.', + component: 'setting-check-box' + } + }); + this.settings.add('layout.discover', { default: true, ui: { @@ -499,6 +510,8 @@ export default class CSSTweaks extends Module { this.toggleHide('whispers', !this.settings.get('whispers.show')); this.toggleHide('celebration', ! this.settings.get('channel.show-celebrations')); + this.settings.getChanges('layout.subtember', val => this.toggleHide('subtember', !val)); + this.updateFont(); this.updateTopNav(); diff --git a/src/sites/twitch-twilight/modules/mod-view.jsx b/src/sites/twitch-twilight/modules/mod-view.jsx index bf38edb4..7fe5640e 100644 --- a/src/sites/twitch-twilight/modules/mod-view.jsx +++ b/src/sites/twitch-twilight/modules/mod-view.jsx @@ -228,6 +228,11 @@ export default class ModView extends Module { //if ( channel?.id && channel.id != this._cached_id ) // this.checkRoot(); + if ( this._cached_bar_id != channel?.id ) { + this._cached_bar_id = channel?.id; + this._cached_bar_channel = channel; + } + if ( title != el._cached_title || game?.id != el._cached_game ) { el._cached_title = title; el._cached_game = game?.id; @@ -277,7 +282,7 @@ export default class ModView extends Module { updateMetadata(el, keys) { const cont = el._ffz_cont, - channel = this._cached_channel; + channel = this._cached_bar_channel; //root = this.fine.getReactInstance(el); /*let channel = null, state = root?.return?.memoizedState, i = 0; diff --git a/src/sites/twitch-twilight/styles/color_normalizer.scss b/src/sites/twitch-twilight/styles/color_normalizer.scss index 62f7bf05..6c9e6eb4 100644 --- a/src/sites/twitch-twilight/styles/color_normalizer.scss +++ b/src/sites/twitch-twilight/styles/color_normalizer.scss @@ -130,3 +130,10 @@ html, .tw-root--theme-dark, .tw-root--theme-light { border-color: var(--color-background-button-primary-default) !important; } } + +.subtember-gradient { + --color-text-base: #0e0c10; + --color-fill-button-icon: #0e0c10; + + color: var(--color-text-base); +} diff --git a/src/socket.js b/src/socket.js index 2fe7454d..8509912f 100644 --- a/src/socket.js +++ b/src/socket.js @@ -36,6 +36,31 @@ export default class SocketClient extends Module { getSocket: () => this, }); + this.settings.add('auth.mode', { + default: 'chat', + + ui: { + path: 'Data Management > Authentication >> General', + title: 'Authentication Provider', + description: 'Which method should the FrankerFaceZ client use to authenticate against the FFZ servers when necessary?', + component: 'setting-select-box', + force_seen: true, + + data: [ + { + value: 'chat', + title: 'Twitch Chat' + }, + { + value: false, + title: 'Disabled (No Authentication)' + } + ] + }, + + changed: () => this._cached_token = null + }); + this.settings.add('socket.use-cluster', { default: 'Production', @@ -135,6 +160,15 @@ export default class SocketClient extends Module { // ======================================================================== getAPIToken() { + const mode = this.settings.get('auth.mode'); + + if ( mode === 'chat' ) + return this._getAPIToken_Chat(); + + return Promise.reject(new Error('The user has disabled authentication.')); + } + + _getAPIToken_Chat() { if ( this._cached_token ) { if ( this._cached_token.expires > (Date.now() + 15000) ) return Promise.resolve(this._cached_token); diff --git a/src/utilities/compat/fine.js b/src/utilities/compat/fine.js index d9aa1d2f..a2248b35 100644 --- a/src/utilities/compat/fine.js +++ b/src/utilities/compat/fine.js @@ -619,6 +619,8 @@ export class FineWrapper extends EventEmitter { this._wrapped.add('UNSAFE_componentWillMount'); this._wrapped.add('componentWillUnmount'); + cls._ffz_wrapper = this; + const t = this, _instances = this.instances, proto = cls.prototype, diff --git a/webpack.config.js b/webpack.config.js index 9d8db722..c483eaed 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -90,7 +90,7 @@ const config = { output: { chunkFormat: 'array-push', clean: true, - publicPath: true + publicPath: FOR_EXTENSION ? 'auto' : FILE_PATH, path: path.resolve(__dirname, 'dist'),