diff --git a/package.json b/package.json index 75e351af..a6072cd7 100755 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "frankerfacez", "author": "Dan Salvato LLC", - "version": "4.24.1", + "version": "4.25.0", "description": "FrankerFaceZ is a Twitch enhancement suite.", "private": true, "license": "Apache-2.0", diff --git a/src/modules/chat/badges.jsx b/src/modules/chat/badges.jsx index 586093ea..b648234a 100644 --- a/src/modules/chat/badges.jsx +++ b/src/modules/chat/badges.jsx @@ -49,10 +49,10 @@ const CSS_BADGES = { } export const BADGE_POSITIONS = { + staff: -2, + admin: -1, + global_mod: -1, broadcaster: 0, - staff: 0, - admin: 0, - global_mod: 0, mod: 1, moderator: 1, twitchbot: 1, @@ -396,6 +396,7 @@ export default class Badges extends Module { onEnable() { this.parent.context.on('changed:chat.badges.custom-mod', this.rebuildAllCSS, this); + this.parent.context.on('changed:chat.badges.custom-vip', this.rebuildAllCSS, this); this.parent.context.on('changed:chat.badges.style', this.rebuildAllCSS, this); this.parent.context.on('changed:theme.is-dark', this.rebuildAllCSS, this); this.parent.context.on('changed:theme.tooltips-dark', this.rebuildAllCSS, this); @@ -417,9 +418,32 @@ export default class Badges extends Module { const room_id = container?.dataset?.roomId, room_login = container?.dataset?.room, - data = JSON.parse(target.dataset.badgeData), out = []; + let data; + if ( target.dataset.badgeData ) + data = JSON.parse(target.dataset.badgeData); + else { + const badge_idx = target.dataset.badgeIdx, + fine = this.resolve('site.fine'); + + if ( fine ) { + let message; + message = container[fine.accessor]?.return?.stateNode?.props?.message; + if ( ! message ) + message = fine.searchParent(container, n => n.props?.message)?.props?.message; + if ( ! message ) + message = fine.searchParent(container, n => n.props?.node)?.props?.node?._ffz_message; + if ( ! message ) + message = fine.searchParent(container, n => n.props?.messageContext)?.props?.messageContext?.comment?._ffz_message; + + if ( message?._ffz_message) + message = message._ffz_message; + if ( message ) + data = message.ffz_badge_cache?.[badge_idx]?.[1]?.badges; + } + } + if ( data == null ) return out; @@ -565,11 +589,9 @@ export default class Badges extends Module { } - render(msg, createElement, skip_hide = false, skip_click = false) { // eslint-disable-line class-methods-use-this - if ( ! msg.badges && ! msg.ffz_badges ) - return null; - - // TODO: A lot of this can be cached + cacheBadges(msg, skip_hide = false) { + if ( msg.ffz_badge_cache ) + return msg.ffz_badge_cache; const hidden_badges = skip_hide ? {} : (this.parent.context.get('chat.badges.hidden') || {}), badge_style = this.parent.context.get('chat.badges.style'), @@ -584,8 +606,7 @@ export default class Badges extends Module { tb = this.twitch_badges, - out = [], - slotted = {}, + slotted = new Map, twitch_badges = msg.badges || {}, dynamic_data = msg.badgeDynamicData || {}, @@ -648,7 +669,7 @@ export default class Badges extends Module { data }); - slotted[slot] = { + slotted.set(slot, { id: badge_id, props: { 'data-provider': 'twitch', @@ -657,7 +678,7 @@ export default class Badges extends Module { style: {} }, badges - }; + }); } if ( Array.isArray(badges) ) { @@ -677,7 +698,7 @@ export default class Badges extends Module { continue; const slot = has(badge, 'slot') ? badge.slot : full_badge.slot, - old_badge = slotted[slot], + old_badge = slotted.get(slot), urls = badge.urls || (badge.image ? {1: badge.image} : null), color = badge.color || full_badge.color || 'transparent', no_invert = badge.no_invert, @@ -725,17 +746,18 @@ export default class Badges extends Module { style }; - slotted[slot] = { + slotted.set(slot, { id: badge.id, props, badges: [bd], content: badge.content || full_badge.content - } + }) } if (no_invert) { - slotted[slot].full_size = true; - slotted[slot].no_invert = true; + const old = slotted.get(slot); + old.full_size = true; + old.no_invert = true; style.background = 'unset'; style.backgroundSize = 'unset'; @@ -762,28 +784,43 @@ export default class Badges extends Module { } } - for(const slot in slotted) - if ( has(slotted, slot) ) { - const data = slotted[slot], - props = data.props; + return msg.ffz_badge_cache = Array.from(slotted).sort((a,b) => a[0] - b[0]); + } - let content = maybe_call(data.content, this, data, msg, createElement); - if ( content && ! Array.isArray(content) ) - content = [content]; - props.className = `ffz-tooltip ffz-badge${content ? ' tw-pd-x-05' : ''}${data.full_size ? ' ffz-full-size' : ''}${data.no_invert ? ' ffz-no-invert' : ''}`; - props.key = `${props['data-provider']}-${props['data-badge']}`; - props['data-tooltip-type'] = 'badge'; - props['data-badge-data'] = JSON.stringify(data.badges); + render(msg, createElement, skip_hide = false, skip_click = false) { + if ( ! msg.badges && ! msg.ffz_badges ) + return null; - if ( ! skip_click ) - props.onClick = this.handleClick; + if ( ! msg.ffz_badge_cache ) + this.cacheBadges(msg, skip_hide); - if ( data.replaced ) - props['data-replaced'] = data.replaced; + if ( ! msg.ffz_badge_cache.length ) + return null; - out.push(createElement('span', props, content || undefined)); - } + const out = []; + for(let i=0, l = msg.ffz_badge_cache.length; i < l; i++) { + const data = msg.ffz_badge_cache[i][1], + props = data.props; + + let content = maybe_call(data.content, this, data, msg, createElement); + if ( content && ! Array.isArray(content) ) + content = [content]; + + props.className = `ffz-tooltip ffz-badge${content ? ' tw-pd-x-05' : ''}${data.full_size ? ' ffz-full-size' : ''}${data.no_invert ? ' ffz-no-invert' : ''}`; + props.key = `${props['data-provider']}-${props['data-badge']}`; + props['data-tooltip-type'] = 'badge'; + props['data-badge-idx'] = i; + //props['data-badge-data'] = JSON.stringify(data.badges); + + if ( ! skip_click ) + props.onClick = this.handleClick; + + if ( data.replaced ) + props['data-replaced'] = data.replaced; + + out.push(createElement('span', props, content || undefined)); + } return out; } diff --git a/src/modules/chat/index.js b/src/modules/chat/index.js index c7a40b36..5592faa6 100644 --- a/src/modules/chat/index.js +++ b/src/modules/chat/index.js @@ -1185,7 +1185,7 @@ export default class Chat extends Module { else this.color_cache = null; - this.emit(':update-lines'); + this.emit(':update-line-tokens'); }); } @@ -1209,7 +1209,7 @@ export default class Chat extends Module { this.socket = this.resolve('socket'); if ( this.context.get('chat.filtering.color-mentions') ) - this.createColorCache().then(() => this.emit(':update-lines')); + this.createColorCache().then(() => this.emit(':update-line-tokens')); for(const key in TOKENIZERS) if ( has(TOKENIZERS, key) ) diff --git a/src/sites/clips/line.jsx b/src/sites/clips/line.jsx index 24eed62e..a02c820b 100644 --- a/src/sites/clips/line.jsx +++ b/src/sites/clips/line.jsx @@ -8,6 +8,7 @@ import Module from 'utilities/module'; import {createElement} from 'react'; import { split_chars } from 'utilities/object'; +import { RERENDER_SETTINGS, UPDATE_BADGE_SETTINGS, UPDATE_TOKEN_SETTINGS } from 'utilities/constants'; export default class Line extends Module { constructor(...args) { @@ -33,26 +34,25 @@ export default class Line extends Module { } onEnable() { - this.chat.context.on('changed:chat.me-style', this.updateLines, this); - this.chat.context.on('changed:chat.emotes.enabled', this.updateLines, this); - this.chat.context.on('changed:chat.emotes.2x', this.updateLines, this); - this.chat.context.on('changed:chat.emotes.animated', this.updateLines, this); - this.chat.context.on('changed:chat.emoji.style', this.updateLines, this); - this.chat.context.on('changed:chat.bits.stack', this.updateLines, this); - this.chat.context.on('changed:chat.badges.style', this.updateLines, this); - this.chat.context.on('changed:chat.badges.hidden', this.updateLines, this); - this.chat.context.on('changed:chat.badges.custom-mod', this.updateLines, this); - this.chat.context.on('changed:chat.rich.enabled', this.updateLines, this); - this.chat.context.on('changed:chat.rich.hide-tokens', this.updateLines, this); - this.chat.context.on('changed:chat.rich.all-links', this.updateLines, this); - this.chat.context.on('changed:chat.rich.minimum-level', this.updateLines, this); - this.chat.context.on('changed:chat.name-format', this.updateLines, this); - this.chat.context.on('changed:tooltip.link-images', this.maybeUpdateLines, this); - this.chat.context.on('changed:tooltip.link-nsfw-images', this.maybeUpdateLines, this); - + this.on('chat.overrides:changed', id => this.updateLinesByUser(id, null, false, false), this); this.on('chat:update-lines-by-user', this.updateLinesByUser, this); this.on('chat:update-lines', this.updateLines, this); - this.on('i18n:update', this.updateLines, this); + this.on('chat:rerender-lines', this.rerenderLines, this); + this.on('chat:update-line-tokens', this.updateLineTokens, this); + this.on('chat:update-line-badges', this.updateLineBadges, this); + this.on('i18n:update', this.rerenderLines, this); + + for(const setting of RERENDER_SETTINGS) + this.chat.context.on(`changed:${setting}`, this.rerenderLines, this); + + for(const setting of UPDATE_TOKEN_SETTINGS) + this.chat.context.on(`changed:${setting}`, this.updateLineTokens, this); + + for(const setting of UPDATE_BADGE_SETTINGS) + this.chat.context.on(`changed:${setting}`, this.updateLineBadges, this); + + this.chat.context.on('changed:tooltip.link-images', this.maybeUpdateLines, this); + this.chat.context.on('changed:tooltip.link-nsfw-images', this.maybeUpdateLines, this); this.site = this.resolve('site'); @@ -138,11 +138,27 @@ export default class Line extends Module { this.updateLines(); } - updateLines() { + return this._updateLines(); + } + + rerenderLines() { + this.ChatLine.forceUpdate(); + } + + updateLineTokens() { + return this._updateLines(true, false); + } + + updateLineBadges() { + return this._updateLines(false, true); + } + + _updateLines(clear_tokens = true, clear_badges = true) { // eslint-disable-line no-unused-vars for(const inst of this.ChatLine.instances) { const msg = inst.props.node; - if ( msg ) + // TODO: Selective state clear. + if ( msg?._ffz_message ) msg._ffz_message = null; } diff --git a/src/sites/twitch-twilight/modules/chat/index.js b/src/sites/twitch-twilight/modules/chat/index.js index eb4c4d54..f1d91f0d 100644 --- a/src/sites/twitch-twilight/modules/chat/index.js +++ b/src/sites/twitch-twilight/modules/chat/index.js @@ -673,7 +673,8 @@ export default class ChatHook extends Module { ic.mode = mode; ic.contrast = contrast; - this.updateChatLines(); + this.chat_line.rerenderLines(); + //this.updateChatLines(); this.updateMentionCSS(); this.emit(':update-colors'); } @@ -844,11 +845,6 @@ export default class ChatHook extends Module { this.chat.context.on('changed:chat.filtering.highlight-mentions', this.updateMentionCSS, this); this.chat.context.on('changed:chat.filtering.highlight-tokens', this.updateMentionCSS, this); this.chat.context.on('changed:chat.filtering.mention-color', this.updateMentionCSS, this); - this.chat.context.on('changed:chat.fix-bad-emotes', this.updateChatLines, this); - this.chat.context.on('changed:chat.points.allow-highlight', this.updateChatLines, this); - this.chat.context.on('changed:chat.filtering.display-deleted', this.updateChatLines, this); - this.chat.context.on('changed:chat.filtering.display-mod-action', this.updateChatLines, this); - this.chat.context.on('changed:chat.replies.style', this.updateChatLines, this); this.chat.context.on('changed:chat.filtering.clickable-mentions', val => this.css_tweaks.toggle('clickable-mentions', val)); this.chat.context.on('changed:chat.filtering.bold-mentions', val => this.css_tweaks.toggle('chat-mention-no-bold', ! val)); this.chat.context.on('changed:chat.pin-resubs', val => { @@ -2382,9 +2378,9 @@ export default class ChatHook extends Module { } - updateChatLines() { + /*updateChatLines() { this.chat_line.updateLines(); - } + }*/ // ======================================================================== @@ -2541,7 +2537,8 @@ export default class ChatHook extends Module { this._ffz_old_bits = new_bits; room.updateBitsConfig(formatBitsConfig(config)); - this.updateChatLines(); + this.chat_line.updateLineTokens(); + //this.updateChatLines(); } @@ -2688,7 +2685,8 @@ export default class ChatHook extends Module { return; room.updateBadges(badges); - this.updateChatLines(); + this.chat_line.updateLineBadges(); + //this.updateChatLines(); } updateRoomRules(cont, rules) { // eslint-disable-line class-methods-use-this diff --git a/src/sites/twitch-twilight/modules/chat/line.js b/src/sites/twitch-twilight/modules/chat/line.js index 47e38c87..55b5ecf4 100644 --- a/src/sites/twitch-twilight/modules/chat/line.js +++ b/src/sites/twitch-twilight/modules/chat/line.js @@ -9,7 +9,7 @@ import Module from 'utilities/module'; import RichContent from './rich_content'; import { has } from 'utilities/object'; -import { KEYS } from 'utilities/constants'; +import { KEYS, RERENDER_SETTINGS, UPDATE_BADGE_SETTINGS, UPDATE_TOKEN_SETTINGS } from 'utilities/constants'; import { print_duration } from 'utilities/time'; import { FFZEvent } from 'utilities/events'; import { getRewardTitle, getRewardCost, isHighlightedReward } from './points'; @@ -55,42 +55,25 @@ export default class ChatLine extends Module { } async onEnable() { - this.on('chat.overrides:changed', id => this.updateLinesByUser(id), this); + this.on('chat.overrides:changed', id => this.updateLinesByUser(id, null, false, false), this); this.on('chat:update-lines-by-user', this.updateLinesByUser, this); this.on('chat:update-lines', this.updateLines, this); - this.on('i18n:update', this.updateLines, this); + this.on('chat:rerender-lines', this.rerenderLines, this); + this.on('chat:update-line-tokens', this.updateLineTokens, this); + this.on('chat:update-line-badges', this.updateLineBadges, this); + this.on('i18n:update', this.rerenderLines, this); + + for(const setting of RERENDER_SETTINGS) + this.chat.context.on(`changed:${setting}`, this.rerenderLines, this); + + for(const setting of UPDATE_TOKEN_SETTINGS) + this.chat.context.on(`changed:${setting}`, this.updateLineTokens, this); + + for(const setting of UPDATE_BADGE_SETTINGS) + this.chat.context.on(`changed:${setting}`, this.updateLineBadges, this); - this.chat.context.on('changed:chat.name-format', this.updateLines, this); - this.chat.context.on('changed:chat.me-style', this.updateLines, this); - this.chat.context.on('changed:chat.emotes.enabled', this.updateLines, this); - this.chat.context.on('changed:chat.emotes.2x', this.updateLines, this); - this.chat.context.on('changed:chat.emotes.animated', this.updateLines, this); - this.chat.context.on('changed:chat.emoji.style', this.updateLines, this); - this.chat.context.on('changed:chat.bits.stack', this.updateLines, this); - this.chat.context.on('changed:chat.badges.style', this.updateLines, this); - this.chat.context.on('changed:chat.badges.hidden', this.updateLines, this); - this.chat.context.on('changed:chat.badges.custom-mod', this.updateLines, this); - this.chat.context.on('changed:chat.rituals.show', this.updateLines, this); - this.chat.context.on('changed:chat.subs.show', this.updateLines, this); - this.chat.context.on('changed:chat.subs.compact', this.updateLines, this); - this.chat.context.on('changed:chat.rich.enabled', this.updateLines, this); - this.chat.context.on('changed:chat.rich.hide-tokens', this.updateLines, this); - this.chat.context.on('changed:chat.rich.all-links', this.updateLines, this); - this.chat.context.on('changed:chat.rich.minimum-level', this.updateLines, this); this.chat.context.on('changed:tooltip.link-images', this.maybeUpdateLines, this); this.chat.context.on('changed:tooltip.link-nsfw-images', this.maybeUpdateLines, this); - this.chat.context.on('changed:chat.actions.inline', this.updateLines, this); - this.chat.context.on('changed:chat.filtering.show-deleted', this.updateLines, this); - this.chat.context.on('changed:chat.filtering.process-own', this.updateLines, this); - this.chat.context.on('changed:chat.timestamp-format', this.updateLines, this); - this.chat.context.on('changed:chat.filtering.mention-priority', this.updateLines, this); - this.chat.context.on('changed:chat.filtering.debug', this.updateLines, this); - this.chat.context.on('changed:__filter:highlight-terms', this.updateLines, this); - this.chat.context.on('changed:__filter:highlight-users', this.updateLines, this); - this.chat.context.on('changed:__filter:highlight-badges', this.updateLines, this); - this.chat.context.on('changed:__filter:block-terms', this.updateLines, this); - this.chat.context.on('changed:__filter:block-users', this.updateLines, this); - this.chat.context.on('changed:__filter:block-badges', this.updateLines, this); this.on('chat:get-tab-commands', e => { if ( this.experiments.getTwitchAssignmentByName('chat_replies') === 'control' ) @@ -882,7 +865,7 @@ other {# messages were deleted by a moderator.} 'data-test-selector': 'chat-message-highlight' }), e('div', { - className: 'chat-line__message-container' + className: 'chat-line__message-container tw-relative' }, [ this.props.repliesAppearancePreference && this.props.repliesAppearancePreference === 'expanded' ? this.renderReplyLine() : null, out @@ -1013,14 +996,19 @@ other {# messages were deleted by a moderator.} } - updateLinesByUser(id, login) { + updateLinesByUser(id, login, clear_tokens = true, clear_badges = true) { for(const inst of this.ChatLine.instances) { const msg = inst.props.message, user = msg?.user; if ( user && ((id && id == user.id) || (login && login == user.login)) ) { - msg.ffz_tokens = null; - msg.ffz_badges = null; - msg.highlights = msg.mentioned = msg.mention_color = msg.color_priority = null; + if ( clear_badges ) + msg.ffz_badges = msg.ffz_badge_cache = null; + + if ( clear_tokens ) { + msg.ffz_tokens = null; + msg.highlights = msg.mentioned = msg.mention_color = msg.color_priority = null; + } + inst.forceUpdate(); } } @@ -1042,21 +1030,45 @@ other {# messages were deleted by a moderator.} } updateLines() { + return this._updateLines(); + } + + rerenderLines() { + return this._updateLines(false, false); + } + + updateLineTokens() { + return this._updateLines(true, false); + } + + updateLineBadges() { + return this._updateLines(false, true); + } + + _updateLines(clear_tokens = true, clear_badges = true) { for(const inst of this.ChatLine.instances) { const msg = inst.props.message; if ( msg ) { - msg.ffz_tokens = null; - msg.ffz_badges = null; - msg.highlights = msg.mentioned = msg.mention_color = msg.mention_priority = msg.clear_priority = null; + if ( clear_badges ) + msg.ffz_badge_cache = msg.ffz_badges = null; + + if ( clear_tokens ) { + msg.ffz_tokens = null; + msg.highlights = msg.mentioned = msg.mention_color = msg.mention_priority = msg.clear_priority = null; + } } } for(const inst of this.ExtensionLine.instances) { const msg = inst.props.message; if ( msg ) { - msg.ffz_tokens = null; - msg.ffz_badges = null; - msg.highlights = msg.mentioned = msg.mention_color = msg.mention_priority = msg.clear_priority = null; + if ( clear_badges ) + msg.ffz_badge_cache = msg.ffz_badges = null; + + if ( clear_tokens ) { + msg.ffz_tokens = null; + msg.highlights = msg.mentioned = msg.mention_color = msg.mention_priority = msg.clear_priority = null; + } } } diff --git a/src/sites/twitch-twilight/modules/chat/scroller.js b/src/sites/twitch-twilight/modules/chat/scroller.js index 6fe6fa0f..6b9c6fb4 100644 --- a/src/sites/twitch-twilight/modules/chat/scroller.js +++ b/src/sites/twitch-twilight/modules/chat/scroller.js @@ -362,7 +362,7 @@ export default class Scroller extends Module { } const smoothAnimation = () => { - if ( this.state.isPaused || ! this.state.isAutoScrolling ) + if ( this.state.isPaused ) return this.ffz_is_smooth_scrolling = false; // See how much time has passed to get a step based off the delta diff --git a/src/sites/twitch-twilight/modules/video_chat/index.jsx b/src/sites/twitch-twilight/modules/video_chat/index.jsx index 93c72c5c..1639a052 100644 --- a/src/sites/twitch-twilight/modules/video_chat/index.jsx +++ b/src/sites/twitch-twilight/modules/video_chat/index.jsx @@ -10,6 +10,7 @@ import {print_duration} from 'utilities/time'; import {formatBitsConfig} from '../chat'; import Module from 'utilities/module'; +import { RERENDER_SETTINGS, UPDATE_BADGE_SETTINGS, UPDATE_TOKEN_SETTINGS } from 'src/utilities/constants'; const SUB_REGEX = /^([^\s]+) subscribed ([^.]+)\. They've subscribed for (\d+) months(?:[^!]+streak)?!/; const SUB_TIERS = { @@ -91,12 +92,24 @@ export default class VideoChatHook extends Module { async onEnable() { - this.chat.context.on('changed:chat.video-chat.enabled', this.updateLines, this); - this.chat.context.on('changed:chat.video-chat.timestamps', this.updateLines, this); - this.on('chat.overrides:changed', id => this.updateLinesByUser(id), this); - this.on('chat:update-lines', this.updateLines, this); + this.chat.context.on('changed:chat.video-chat.enabled', this.rerenderLines, this); + this.chat.context.on('changed:chat.video-chat.timestamps', this.rerenderLines, this); + this.on('chat.overrides:changed', id => this.updateLinesByUser(id, null, false, false), this); this.on('chat:update-lines-by-user', this.updateLinesByUser, this); - this.on('i18n:update', this.updateLines, this); + this.on('chat:update-lines', this.updateLines, this); + this.on('chat:rerender-lines', this.rerenderLines, this); + this.on('chat:update-line-tokens', this.updateLineTokens, this); + this.on('chat:update-line-badges', this.updateLineBadges, this); + this.on('i18n:update', this.rerenderLines, this); + + for(const setting of RERENDER_SETTINGS) + this.chat.context.on(`changed:${setting}`, this.rerenderLines, this); + + for(const setting of UPDATE_TOKEN_SETTINGS) + this.chat.context.on(`changed:${setting}`, this.updateLineTokens, this); + + for(const setting of UPDATE_BADGE_SETTINGS) + this.chat.context.on(`changed:${setting}`, this.updateLineBadges, this); this.VideoChatController.on('mount', this.chatMounted, this); this.VideoChatController.on('unmount', this.chatUnmounted, this); @@ -311,9 +324,7 @@ export default class VideoChatHook extends Module { rel="noopener noreferrer" target="_blank" style={{color}} - >{ - t.chat.formatUser(user, createElement) - } + >{t.chat.formatUser(user, createElement)}
{is_action ? ' ' : ': '}