diff --git a/package.json b/package.json index 8339e64d..5a7e0ce7 100755 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "frankerfacez", "author": "Dan Salvato LLC", - "version": "4.40.0", + "version": "4.41.0", "description": "FrankerFaceZ is a Twitch enhancement suite.", "private": true, "license": "Apache-2.0", diff --git a/src/experiments.json b/src/experiments.json index 513291a6..1fe1346c 100644 --- a/src/experiments.json +++ b/src/experiments.json @@ -3,8 +3,8 @@ "name": "Modular Chat Line Rendering", "description": "Enable a newer, modular chat line renderer.", "groups": [ - {"value": true, "weight": 0}, - {"value": false, "weight": 100} + {"value": true, "weight": 20}, + {"value": false, "weight": 80} ] }, "api_load": { diff --git a/src/main.js b/src/main.js index 073f8036..5e2df55e 100644 --- a/src/main.js +++ b/src/main.js @@ -17,6 +17,7 @@ import SocketClient from './socket'; //import PubSubClient from './pubsub'; import Site from 'site'; import Vue from 'utilities/vue'; +import StagingSelector from './staging'; //import Timing from 'utilities/timing'; class FrankerFaceZ extends Module { @@ -56,6 +57,7 @@ class FrankerFaceZ extends Module { this.inject('settings', SettingsManager); this.inject('experiments', ExperimentManager); this.inject('i18n', TranslationManager); + this.inject('staging', StagingSelector); this.inject('socket', SocketClient); //this.inject('pubsub', PubSubClient); this.inject('site', Site); diff --git a/src/modules/chat/badges.jsx b/src/modules/chat/badges.jsx index c485af81..34138a54 100644 --- a/src/modules/chat/badges.jsx +++ b/src/modules/chat/badges.jsx @@ -4,7 +4,7 @@ // Badge Handling // ============================================================================ -import {NEW_API, SERVER, API_SERVER, IS_WEBKIT, IS_FIREFOX, WEBKIT_CSS as WEBKIT} from 'utilities/constants'; +import {NEW_API, SERVER, IS_WEBKIT, IS_FIREFOX, WEBKIT_CSS as WEBKIT} from 'utilities/constants'; import {createElement, ManagedStyle} from 'utilities/dom'; import {has, maybe_call, SourcedSet} from 'utilities/object'; @@ -184,6 +184,7 @@ export default class Badges extends Module { this.inject('settings'); this.inject('tooltips'); this.inject('experiments'); + this.inject('staging'); this.style = new ManagedStyle('badges'); @@ -958,7 +959,7 @@ export default class Badges extends Module { } catch(err) { /* do nothing */ } try { - response = await fetch(`${API_SERVER}/v1/badges/ids`); + response = await fetch(`${this.staging.api}/v1/badges/ids`); } catch(err) { tries++; if ( tries < 10 ) diff --git a/src/modules/chat/emotes.js b/src/modules/chat/emotes.js index 6e4a3bc2..838c91f2 100644 --- a/src/modules/chat/emotes.js +++ b/src/modules/chat/emotes.js @@ -6,8 +6,8 @@ import Module from 'utilities/module'; import {ManagedStyle} from 'utilities/dom'; -import {get, has, timeout, SourcedSet} from 'utilities/object'; -import {NEW_API, API_SERVER, IS_OSX, EmoteTypes, TWITCH_GLOBAL_SETS, TWITCH_POINTS_SETS, TWITCH_PRIME_SETS} from 'utilities/constants'; +import {get, has, timeout, SourcedSet, make_enum_flags} from 'utilities/object'; +import {NEW_API, IS_OSX, EmoteTypes, TWITCH_GLOBAL_SETS, TWITCH_POINTS_SETS, TWITCH_PRIME_SETS} from 'utilities/constants'; import GET_EMOTE from './emote_info.gql'; import GET_EMOTE_SET from './emote_set_info.gql'; @@ -17,6 +17,148 @@ const HoverState = Symbol('FFZ:Hover:State'); const MOD_KEY = IS_OSX ? 'metaKey' : 'ctrlKey'; +export const MODIFIER_FLAGS = make_enum_flags( + 'Hidden', + 'FlipX', + 'FlipY', + 'GrowX', + 'GrowY', + 'ShrinkX', + 'ShrinkY', + 'Rotate45', + 'Rotate90', + 'Greyscale', + 'Sepia', + 'Rainbow', + 'HyperRed', + 'Shake', + 'Photocopy' +); + +export const MODIFIER_KEYS = Object.values(MODIFIER_FLAGS).filter(x => typeof x === 'number'); + +const MODIFIER_FLAG_CSS = { + FlipX: { + title: 'Flip Horizontal', + transform: 'scaleX(-1)' + }, + FlipY: { + title: 'Flip Vertical', + transform: 'scaleY(-1)' + }, + ShrinkX: { + title: 'Squish Horizontal', + transform: 'scaleX(0.5)' + }, + GrowX: { + title: 'Stretch Horizontal', + transform: 'scaleX(2)' + }, + ShrinkY: { + title: 'Squish Vertical', + transform: 'scaleY(0.5)' + }, + GrowY: { + title: 'Stretch Vertical', + transform: 'scaleY(2)' + }, + Rotate45: { + title: 'Rotate 45 Degrees', + transform: 'rotate(45deg)' + }, + Rotate90: { + title: 'Rotate 90 Degrees', + transform: 'rotate(90deg)' + }, + Greyscale: { + title: 'Grayscale', + filter: 'grayscale(1)' + }, + Sepia: { + title: 'Sepia', + filter: 'sepia(1)' + }, + Rainbow: { + title: 'Rainbow Animation', + animation: 'ffz-effect-rainbow 2s linear infinite', + animationFilter: 'ffz-effect-rainbow-filter 2s linear infinite', + raw: `@keyframes ffz-effect-rainbow { +0% { filter: hue-rotate(0deg) } +100% { filter: hue-rotate(360deg) } +} +@keyframes ffz-effect-rainbow-filter { +0% { filter: var(--ffz-effect-filters) hue-rotate(0deg) } +100% { filter: var(--ffz-effect-filters) hue-rotate(360deg) } +}` + }, + HyperRed: { + title: 'Hyper Red', + filter: 'brightness(0.2) sepia(1) brightness(2.2) contrast(3) saturate(8)' + }, + Shake: { + title: 'Hyper Shake Animation', + animation: 'ffz-effect-shake 0.1s linear infinite', + animationTransform: 'ffz-effect-shake-transform 0.1s linear infinite', + raw: `@keyframes ffz-effect-shake-transform { +0% { transform: var(--ffz-effect-transforms) translate(1px, 1px); } +10% { transform: var(--ffz-effect-transforms) translate(-1px, -2px); } +20% { transform: var(--ffz-effect-transforms) translate(-3px, 0px); } +30% { transform: var(--ffz-effect-transforms) translate(3px, 2px); } +40% { transform: var(--ffz-effect-transforms) translate(1px, -1px); } +50% { transform: var(--ffz-effect-transforms) translate(-1px, 2px); } +60% { transform: var(--ffz-effect-transforms) translate(-3px, 1px); } +70% { transform: var(--ffz-effect-transforms) translate(3px, 1px); } +80% { transform: var(--ffz-effect-transforms) translate(-1px, -1px); } +90% { transform: var(--ffz-effect-transforms) translate(1px, 2px); } +100% { transform: var(--ffz-effect-transforms) translate(1px, -2px); } +} +@keyframes ffz-effect-shake { +0% { transform: translate(1px, 1px); } +10% { transform: translate(-1px, -2px); } +20% { transform: translate(-3px, 0px); } +30% { transform: translate(3px, 2px); } +40% { transform: translate(1px, -1px); } +50% { transform: translate(-1px, 2px); } +60% { transform: translate(-3px, 1px); } +70% { transform: translate(3px, 1px); } +80% { transform: translate(-1px, -1px); } +90% { transform: translate(1px, 2px); } +100% { transform: translate(1px, -2px); } +}` + }, + Photocopy: { + title: 'Photocopy', + filter: 'grayscale(1) brightness(0.65) contrast(5)' + } +}; + + +function generateBaseFilterCss() { + console.log('flags', MODIFIER_FLAGS); + console.log('css', MODIFIER_FLAG_CSS); + + const out = [ + `.modified-emote[data-effects] > img { + --ffz-effect-filters: none; + --ffz-effect-transforms: initial; + --ffz-effect-animations: initial; +}`/*, + `.modified-emote[data-effects] > img { + filter: var(--ffz-effect-filters); + transform: var(--ffz-effect-transforms); + animation: var(--ffz-effect-animations); +}`*/ + ]; + + for(const [key, val] of Object.entries(MODIFIER_FLAG_CSS)) { + if ( val.raw ) + out.push(val.raw); + } + + return out.join('\n'); +} + + const MODIFIERS = { 59847: { modifier_offset: '0 15px 15px 0', @@ -62,14 +204,25 @@ export default class Emotes extends Module { super(...args); this.EmoteTypes = EmoteTypes; + this.ModifierFlags = MODIFIER_FLAGS; this.inject('settings'); this.inject('experiments'); + this.inject('staging'); this.twitch_inventory_sets = new Set; //(EXTRA_INVENTORY); this.__twitch_emote_to_set = {}; this.__twitch_set_to_channel = {}; + // Bulk data structure for collections applied to a lot of users. + // This lets us avoid allocating lots of individual user + // objects when we don't need to do so. + this.bulk = new Map; + + this.effects_enabled = {}; + this.pending_effects = new Set(); + this.applyEffects = this.applyEffects.bind(this); + this.default_sets = new SourcedSet; this.global_sets = new SourcedSet; @@ -170,6 +323,42 @@ export default class Emotes extends Module { } }); + this.settings.add('chat.effects.enable', { + default: true, + ui: { + path: 'Chat > Emote Effects >> General', + title: 'Enable the use of emote effects.', + description: 'Emote Effects are special effects that can be applied to some emotes using special modifiers.', + component: 'setting-check-box' + } + }); + + for(const [key, val] of Object.entries(MODIFIER_FLAG_CSS)) { + const setting = { + default: val.animation + ? null + : true, + ui: { + path: 'Chat > Emote Effects >> Specific Effect @{"description": "**Note:** Animated effects are, by default, only enabled when [Animated Emotes](~chat.appearance.emotes) are enabled."}', + title: `Enable the effect "${val.title ?? key}".`, + component: 'setting-check-box', + force_seen: true + } + }; + + if ( val.animation ) { + setting.default = null; + setting.requires = ['chat.emotes.animated']; + setting.process = function(ctx, val) { + if ( val == null ) + return ctx.get('chat.emotes.animated') === 1; + return val; + }; + } + + this.settings.add(`chat.effects.${key}`, setting); + } + // Because this may be used elsewhere. this.handleClick = this.handleClick.bind(this); this.animHover = this.animHover.bind(this); @@ -178,6 +367,16 @@ export default class Emotes extends Module { onEnable() { this.style = new ManagedStyle('emotes'); + this.effect_style = new ManagedStyle('effects'); + + // Generate the base filter CSS. + this.base_effect_css = generateBaseFilterCss(); + + this.parent.context.on('changed:chat.effects.enable', this.updateEffects, this); + for(const key of Object.keys(MODIFIER_FLAG_CSS)) + this.parent.context.on(`changed:chat.effects.${key}`, this.updateEffects, this); + + this.updateEffects(); // Fix numeric Twitch favorite IDs. const favs = this.getFavorites('twitch'); @@ -210,6 +409,103 @@ export default class Emotes extends Module { } + // ======================================================================== + // Load Modifier Effects + // ======================================================================== + + ensureEffect(flags) { + if ( ! this.effect_style.has(`${flags}`) ) { + this.pending_effects.add(flags); + if ( ! this._effect_timer ) + this._effect_timer = requestAnimationFrame(this.applyEffects); + } + } + + applyEffects() { + this._effect_timer = null; + const effects = this.pending_effects; + this.pending_effects = new Set; + + for(const flags of effects) { + const result = this.generateFilterCss(flags); + this.effect_style.set(`${flags}`, result ?? ''); + } + } + + generateFilterCss(flags) { + if ( ! this.parent.context.get('chat.effects.enable') ) + return null; + + let filter, transform, animation, animations = []; + + for(const key of MODIFIER_KEYS) { + if ( (flags & key) !== key || ! this.effects_enabled[key] ) + continue; + + const input = MODIFIER_FLAG_CSS[MODIFIER_FLAGS[key]]; + if ( ! input ) + continue; + + if ( input.animation ) + animations.push(input); + + if ( input.filter ) + filter = filter + ? `${filter} ${input.filter}` + : input.filter; + + if ( input.transform ) + transform = transform + ? `${transform} ${input.transform}` + : input.transform; + } + + if ( animations.length ) + for(const input of animations) { + if ( filter && input.animationFilter ) + animation = animation + ? `${animation}, ${input.animationFilter}` + : input.animationFilter; + else if ( transform && input.animationTransform ) + animation = animation + ? `${animation}, ${input.animationTransform}` + : input.animationTransform; + else + animation = animation + ? `${animation}, ${input.animation}` + : input.animation; + } + + if ( ! filter && ! transform && ! animation ) + return null; + + return `.modified-emote[data-effects="${flags}"] > img {${filter ? ` + --ffz-effect-filters: ${filter}; + filter: var(--ffz-effect-filters);` : ''}${transform ? ` + --ffz-effect-transforms: ${transform}; + transform: var(--ffz-effect-transforms);` : ''}${animation ? ` + --ffz-effect-animations: ${animation}; + animation: var(--ffz-effect-animations);` : ''} +}`; + } + + updateEffects() { + // TODO: Smarter logic so it does less work. + const enabled = this.parent.context.get('chat.effects.enable'); + + this.effects_enabled = {}; + for(const key of Object.keys(MODIFIER_FLAG_CSS)) + this.effects_enabled[MODIFIER_FLAGS[key]] = this.effects_enabled[key] = this.parent.context.get(`chat.effects.${key}`); + + this.effect_style.clear(); + if ( ! enabled ) + return; + + this.effect_style.set('base', this.base_effect_css); + this.emit(':update-effects'); + } + + // ======================================================================== // Featured Sets // ======================================================================== @@ -513,7 +809,7 @@ export default class Emotes extends Module { return; const line = fine.searchParent(target, n => n.props && n.props.message), - opener = fine.searchParent(target, n => n.onShowEmoteCard, 250); + opener = fine.searchParent(target, n => n.onShowEmoteCard, 500); if ( ! line || ! opener ) return; @@ -545,11 +841,21 @@ export default class Emotes extends Module { room_user = room && room.getUser(user_id, user_login, true), user = this.parent.getUser(user_id, user_login, true); - return (user?.emote_sets ? user.emote_sets._cache : []).concat( + const out = (user?.emote_sets ? user.emote_sets._cache : []).concat( room_user?.emote_sets ? room_user.emote_sets._cache : [], room?.emote_sets ? room.emote_sets._cache : [], this.default_sets._cache ); + + if ( this.bulk.size ) { + const str_user = String(user_id); + for(const [set_id, users] of this.bulk) { + if ( users?._cache.has(str_user) ) + out.push(set_id); + } + } + + return out; } getSets(user_id, user_login, room_id, room_login) { @@ -621,6 +927,20 @@ export default class Emotes extends Module { if ( user ) this._withSources(out, seen, user.emote_sets); + if ( this.bulk.size ) { + const str_user = String(user_id); + for(const [set_id, users] of this.bulk) { + if ( ! seen.has(set_id) && users?._cache.has(str_user) ) { + for(const [provider, data] of users._sources) { + if ( data && data.includes(str_user) ) { + out.push([set_id, provider]); + break; + } + } + } + } + } + return out; } @@ -631,10 +951,21 @@ export default class Emotes extends Module { getGlobalSetIDs(user_id, user_login) { const user = this.parent.getUser(user_id, user_login, true); - if ( ! user?.emote_sets ) - return this.default_sets._cache; - return user.emote_sets._cache.concat(this.default_sets._cache); + const out = (user?.emote_sets ? user.emote_sets._cache : []).concat( + this.default_sets._cache + ); + + if ( this.bulk.size ) { + const str_user = String(user_id); + for(const [set_id, users] of this.bulk) { + if ( users?._cache.has(str_user) ) + out.push(set_id); + } + } + + return out; + } getGlobalSets(user_id, user_login) { @@ -653,6 +984,52 @@ export default class Emotes extends Module { return emotes; } + // ======================================================================== + // Bulk Management + // ======================================================================== + + setBulk(source, set_id, entries) { + let set = this.bulk.get(set_id); + if ( ! set ) + this.bulk.set(set_id, set = new SourcedSet(true)); + + const size = set._cache.size; + set.set(source, entries); + const new_size = set._cache.size; + + if ( ! size && new_size ) + this.refSet(set_id); + } + + deleteBulk(source, set_id) { + const set = this.bulk.get(set_id); + if ( ! set ) + return; + + const size = set._cache.size; + set.delete(source); + const new_size = set._cache.size; + + if ( size && ! new_size ) + this.unrefSet(set_id); + } + + extendBulk(source, set_id, entries) { + let set = this.bulk.get(set_id); + if ( ! set ) + this.bulk.set(set_id, set = new SourcedSet(true)); + + if ( ! Array.isArray(entries) ) + entries = [entries]; + + const size = set._cache.size; + set.extend(source, ...entries); + const new_size = set._cache.size; + + if ( ! size && new_size ) + this.refSet(set_id); + } + // ======================================================================== // Emote Set Ref Counting // ======================================================================== @@ -724,7 +1101,7 @@ export default class Emotes extends Module { } catch(err) { /* do nothing */ } try { - response = await fetch(`${API_SERVER}/v1/set/global`) + response = await fetch(`${this.staging.api}/v1/set/global${this.staging.active ? '/ids' : ''}`) } catch(err) { tries++; if ( tries < 10 ) @@ -753,7 +1130,9 @@ export default class Emotes extends Module { if ( has(sets, set_id) ) this.loadSetData(set_id, sets[set_id]); - if ( data.users ) + if ( data.user_ids ) + this.loadSetUserIds(data.user_ids); + else if ( data.users ) this.loadSetUsers(data.users); return true; @@ -769,7 +1148,7 @@ export default class Emotes extends Module { } catch(err) { /* do nothing */ } try { - response = await fetch(`${API_SERVER}/v1/set/${set_id}`) + response = await fetch(`${this.staging.api}/v1/set/${set_id}${this.staging.active ? '/ids' : ''}`) } catch(err) { tries++; if ( tries < 10 ) @@ -793,13 +1172,28 @@ export default class Emotes extends Module { if ( set ) this.loadSetData(set.id, set, suppress_log); - if ( data.users ) + if ( data.user_ids ) + this.loadSetUserIds(data.user_ids); + else if ( data.users ) this.loadSetUsers(data.users); return true; } + loadSetUserIds(data, suppress_log = false) { + for(const set_id in data) + if ( has(data, set_id) ) { + const emote_set = this.emote_sets[set_id], + users = data[set_id]; + + this.setBulk('ffz-global', set_id, users.map(x => String(x))); + if ( ! suppress_log ) + this.log.info(`Added "${emote_set ? emote_set.title : set_id}" emote set to ${users.length} users.`); + } + } + + loadSetUsers(data, suppress_log = false) { for(const set_id in data) if ( has(data, set_id) ) { @@ -867,7 +1261,8 @@ export default class Emotes extends Module { animSrcSet2: emote.animSrcSet2, text: emote.hidden ? '???' : emote.name, length: emote.name.length, - height: emote.height + height: emote.height, + source_modifier_flags: emote.modifier_flags ?? 0 }; if ( has(MODIFIERS, emote.id) ) diff --git a/src/modules/chat/index.js b/src/modules/chat/index.js index 0fa307fb..36d4f915 100644 --- a/src/modules/chat/index.js +++ b/src/modules/chat/index.js @@ -70,6 +70,7 @@ export default class Chat extends Module { this.inject('i18n'); this.inject('tooltips'); this.inject('experiments'); + this.inject('staging'); this.inject(Badges); this.inject(Emotes); diff --git a/src/modules/chat/room.js b/src/modules/chat/room.js index 183a64fa..e5898146 100644 --- a/src/modules/chat/room.js +++ b/src/modules/chat/room.js @@ -6,7 +6,7 @@ import User from './user'; -import {NEW_API, API_SERVER, WEBKIT_CSS as WEBKIT, IS_FIREFOX} from 'utilities/constants'; +import {NEW_API, WEBKIT_CSS as WEBKIT, IS_FIREFOX} from 'utilities/constants'; import {ManagedStyle} from 'utilities/dom'; import {has, SourcedSet, set_equals} from 'utilities/object'; @@ -260,7 +260,7 @@ export default class Room { let response, data; try { - response = await fetch(`${API_SERVER}/v1/room/${this.id ? `id/${this.id}` : this.login}`); + response = await fetch(`${this.manager.staging.api}/v1/room/${this.id ? `id/${this.id}` : this.login}`); } catch(err) { tries++; if ( tries < 10 ) diff --git a/src/modules/chat/tokenizers.jsx b/src/modules/chat/tokenizers.jsx index 06c6f58e..c54c50f0 100644 --- a/src/modules/chat/tokenizers.jsx +++ b/src/modules/chat/tokenizers.jsx @@ -1242,6 +1242,10 @@ export const AddonEmotes = { return (
{emote}
); } + const effects = token.modifier_flags; + if ( effects ) + this.emotes.ensureEffect(effects); + return (
x.id).join(' ') : null} + data-effects={effects ? effects : undefined} onClick={this.emotes.handleClick} > {emote} - {mods.map(t => {this.tokenizers.emote.render.call(this, t, createElement, true)})} + {mods.map(t => { + if ( (t.source_modifier_flags & 1) === 1) + return null; + return + {this.tokenizers.emote.render.call(this, t, createElement, true)} + + })}
); }, @@ -1443,8 +1454,10 @@ export const AddonEmotes = { if ( token.type !== 'text' ) { if ( token.type === 'emote' ) { - if ( ! token.modifiers ) + if ( ! token.modifiers ) { token.modifiers = []; + token.modifier_flags = 0; + } } out.push(token); @@ -1461,6 +1474,9 @@ export const AddonEmotes = { // Is this emote a modifier? if ( emote.modifier && last_token && last_token.modifiers && (!text.length || (text.length === 1 && text[0] === '')) ) { if ( last_token.modifiers.indexOf(emote.token) === -1 ) { + if ( emote.modifier_flags ) + last_token.modifier_flags |= emote.modifier_flags; + last_token.modifiers.push( Object.assign({ big, @@ -1486,6 +1502,7 @@ export const AddonEmotes = { const t = Object.assign({ modifiers: [], + modifier_flags: 0, big, anim }, emote.token); @@ -1582,7 +1599,8 @@ export const Emoji = { text: match[0], length, - modifiers: [] + modifiers: [], + modifier_flags: 0 }); idx = start + match[0].length; @@ -1734,7 +1752,8 @@ export const TwitchEmotes = { can_big, height: 28, // Not always accurate but close enough. text: text.slice(e_start - t_start, e_end - t_start).join(''), - modifiers: [] + modifiers: [], + modifier_flags: 0 }); idx = e_end; diff --git a/src/sites/twitch-twilight/modules/chat/emote_menu.jsx b/src/sites/twitch-twilight/modules/chat/emote_menu.jsx index a5a22cd4..fc3fa409 100644 --- a/src/sites/twitch-twilight/modules/chat/emote_menu.jsx +++ b/src/sites/twitch-twilight/modules/chat/emote_menu.jsx @@ -373,6 +373,7 @@ export default class EmoteMenu extends Module { this.on('chat.emotes:update-default-sets', this.maybeUpdate, this); this.on('chat.emotes:update-user-sets', this.maybeUpdate, this); this.on('chat.emotes:update-room-sets', this.maybeUpdate, this); + this.on('chat.emotes:loaded', this.maybeUpdate, this); this.on('chat.emotes:change-favorite', this.maybeUpdate, this); this.on('chat.emotes:change-hidden', this.maybeUpdate, this); this.on('chat.emoji:populated', this.maybeUpdate, this); diff --git a/src/sites/twitch-twilight/modules/chat/index.js b/src/sites/twitch-twilight/modules/chat/index.js index d5c9afb4..a6d968b7 100644 --- a/src/sites/twitch-twilight/modules/chat/index.js +++ b/src/sites/twitch-twilight/modules/chat/index.js @@ -104,9 +104,6 @@ const CHAT_TYPES = make_enum( 'Connected', 'Disconnected', 'Reconnect', - 'Hosting', - 'Unhost', - 'Hosted', 'Subscription', 'Resubscription', 'GiftPaidUpgrade', @@ -121,7 +118,6 @@ const CHAT_TYPES = make_enum( 'RoomState', 'Raid', 'Unraid', - 'Ritual', 'Notice', 'Info', 'BadgesUpdated', @@ -139,10 +135,16 @@ const CHAT_TYPES = make_enum( 'InlinePrivateCallout', 'ChannelPointsReward', 'CommunityChallengeContribution', - 'CelebrationPurchase', 'LiveMessageSeparator', 'RestrictedLowTrustUserMessage', - 'CommunityIntroduction' + 'CommunityIntroduction', + 'Shoutout', + 'AnnouncementMessage', + 'MidnightSquid', + 'CharityDonation', + 'MessageIdUpdate', + 'PinnedChat', + 'ViewerMilestone' ); diff --git a/src/sites/twitch-twilight/modules/chat/line.js b/src/sites/twitch-twilight/modules/chat/line.js index 47c3430f..bd22a477 100644 --- a/src/sites/twitch-twilight/modules/chat/line.js +++ b/src/sites/twitch-twilight/modules/chat/line.js @@ -36,6 +36,7 @@ export default class ChatLine extends Module { this.inject('chat.actions'); this.inject('chat.overrides'); + this.inject('chat.emotes'); this.line_types = {}; @@ -349,6 +350,7 @@ export default class ChatLine extends Module { 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); + this.on('chat.emotes:update-effects', this.checkEffects, this); this.on('experiments:changed:line_renderer', () => { const value = this.experiments.get('line_renderer'), @@ -1766,6 +1768,31 @@ other {# messages were deleted by a moderator.} } + checkEffects() { + for(const inst of this.ChatLine.instances) { + const msg = inst.props.message, + tokens = msg?.ffz_tokens; + + if ( tokens ) + for(const token of tokens) { + if ( token.type === 'emote' && token.modifier_flags ) + this.emotes.ensureEffect(token.modifier_flags); + } + } + + for(const inst of this.WhisperLine.instances) { + const msg = inst.props.message?._ffz_message, + tokens = msg?.ffz_tokens; + + if ( tokens ) + for(const token of tokens) { + if ( token.type === 'emote' && token.modifier_flags ) + this.emotes.ensureEffect(token.modifier_flags); + } + } + } + + updateLinesByUser(id, login, clear_tokens = true, clear_badges = true) { for(const inst of this.ChatLine.instances) { const msg = inst.props.message, diff --git a/src/sites/twitch-twilight/modules/video_chat/index.jsx b/src/sites/twitch-twilight/modules/video_chat/index.jsx index 4991b277..f59b60bc 100644 --- a/src/sites/twitch-twilight/modules/video_chat/index.jsx +++ b/src/sites/twitch-twilight/modules/video_chat/index.jsx @@ -47,6 +47,7 @@ export default class VideoChatHook extends Module { this.inject('site.web_munch'); this.inject('chat'); + this.inject('chat.emotes'); this.inject('chat.overrides'); this.injectAs('site_chat', 'site.chat'); this.inject('site.chat.chat_line.rich_content'); @@ -104,6 +105,7 @@ export default class VideoChatHook extends Module { 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); + this.on('chat.emotes:update-effects', this.checkEffects, this); for(const setting of RERENDER_SETTINGS) this.chat.context.on(`changed:${setting}`, this.rerenderLines, this); @@ -466,6 +468,21 @@ export default class VideoChatHook extends Module { } + checkEffects() { + for(const inst of this.VideoChatLine.instances) { + const context = inst.props.messageContext, + msg = context?.comment?._ffz_message, + tokens = msg?.ffz_tokens; + + if ( tokens ) + for(const token of tokens) { + if ( token.type === 'emote' && token.modifier_flags ) + this.emotes.ensureEffect(token.modifier_flags); + } + } + } + + // ======================================================================== // Message Standardization // ======================================================================== diff --git a/src/sites/twitch-twilight/styles/chat.scss b/src/sites/twitch-twilight/styles/chat.scss index 613bda88..db1cc4af 100644 --- a/src/sites/twitch-twilight/styles/chat.scss +++ b/src/sites/twitch-twilight/styles/chat.scss @@ -8,6 +8,14 @@ margin: -.5rem 0; } +.modified-emote { + margin: -.5rem 0; + + & > .chat-line__message--emote { + margin: 0; + } +} + .chat-author__display-name, .chat-author__intl-login { cursor: pointer; diff --git a/src/staging.jsx b/src/staging.jsx new file mode 100644 index 00000000..1fb016b6 --- /dev/null +++ b/src/staging.jsx @@ -0,0 +1,46 @@ +'use strict'; + +// ============================================================================ +// Staging Selector +// ============================================================================ + +import Module from 'utilities/module'; +import { API_SERVER, SERVER, STAGING_API, STAGING_CDN } from './utilities/constants'; + +export default class StagingSelector extends Module { + constructor(...args) { + super(...args); + + this.inject('settings'); + + this.settings.add('data.use-staging', { + default: false, + ui: { + path: 'Debugging > Data Sources >> Staging @{"sort": -1}', + force_seen: true, + title: 'Use staging as data source.', + component: 'setting-check-box' + } + }); + + this.updateStaging(false); + } + + onEnable() { + this.settings.getChanges('data.use-staging', this.updateStaging, this); + } + + updateStaging(val) { + this.active = val; + + this.api = val + ? STAGING_API + : API_SERVER; + + this.cdn = val + ? STAGING_CDN + : SERVER; + + this.emit(':updated', this.api, this.cdn); + } +} diff --git a/src/utilities/constants.js b/src/utilities/constants.js index b284b5d9..3add9180 100644 --- a/src/utilities/constants.js +++ b/src/utilities/constants.js @@ -8,15 +8,14 @@ export const SERVER = DEBUG ? '//localhost:8000' : 'https://cdn.frankerfacez.com export const CLIENT_ID = 'a3bc9znoz6vi8ozsoca0inlcr4fcvkl'; export const API_SERVER = '//api.frankerfacez.com'; +export const STAGING_API = '//api-staging.frankerfacez.com'; +export const STAGING_CDN = '//cdn-staging.frankerfacez.com'; export const NEW_API = '//api2.frankerfacez.com'; //export const SENTRY_ID = 'https://1c3b56f127254d3ba1bd1d6ad8805eee@sentry.io/1186960'; //export const SENTRY_ID = 'https://07ded545d3224ca59825daee02dc7745@catbag.frankerfacez.com:444/2'; export const SENTRY_ID = 'https://74b46b3894114f399d51949c6d237489@sentry.frankerfacez.com/2'; -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 = [ diff --git a/src/utilities/dom.js b/src/utilities/dom.js index 92221706..0bdfd055 100644 --- a/src/utilities/dom.js +++ b/src/utilities/dom.js @@ -271,6 +271,11 @@ export class ManagedStyle { this._style = null; } + clear() { + this._blocks = {}; + this._style.innerHTML = ''; + } + get(key) { const block = this._blocks[key]; if ( block ) @@ -278,6 +283,10 @@ export class ManagedStyle { return undefined; } + has(key) { + return !! this._blocks[key]; + } + set(key, value, force) { const block = this._blocks[key]; if ( block ) { diff --git a/src/utilities/object.js b/src/utilities/object.js index 21f7c518..d6748cba 100644 --- a/src/utilities/object.js +++ b/src/utilities/object.js @@ -66,6 +66,22 @@ export function make_enum(...array) { return out; } +export function make_enum_flags(...array) { + const out = {}; + + out.None = 0; + out[0] = 'None'; + + for(let i = 0; i < array.length; i++) { + const word = array[i], + value = Math.pow(2, i); + out[word] = value; + out[value] = word; + } + + return out; +} + export function timeout(promise, delay) { return new Promise((resolve, reject) => {